feat: 优化接口速度
This commit is contained in:
parent
fd20a4411b
commit
d1c5b818d9
@ -12,6 +12,30 @@ import { ScreenRecord, TimeDistributionPoint, Segment, Marker } from '../types';
|
|||||||
import { formatMemory, formatDate } from '../utils';
|
import { formatMemory, formatDate } from '../utils';
|
||||||
import { useStarToggle } from '../hooks/useStarToggle';
|
import { useStarToggle } from '../hooks/useStarToggle';
|
||||||
|
|
||||||
|
const inFlightJsonRequests = new Map<string, Promise<unknown>>();
|
||||||
|
|
||||||
|
const fetchJsonOnce = async <T,>(url: string): Promise<T> => {
|
||||||
|
const existingRequest = inFlightJsonRequests.get(url);
|
||||||
|
if (existingRequest) {
|
||||||
|
return existingRequest as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = fetch(url, { cache: 'no-store' })
|
||||||
|
.then(async (response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Request failed with status ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<T>;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
inFlightJsonRequests.delete(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
inFlightJsonRequests.set(url, request);
|
||||||
|
return request as Promise<T>;
|
||||||
|
};
|
||||||
|
|
||||||
interface ScreenshotsTabProps {
|
interface ScreenshotsTabProps {
|
||||||
hostname: string;
|
hostname: string;
|
||||||
selectedDate: string | null;
|
selectedDate: string | null;
|
||||||
@ -27,6 +51,9 @@ export default function ScreenshotsTab({
|
|||||||
jumpRequest,
|
jumpRequest,
|
||||||
onLastUpdateChange
|
onLastUpdateChange
|
||||||
}: ScreenshotsTabProps) {
|
}: ScreenshotsTabProps) {
|
||||||
|
const timeDistributionRequestIdRef = useRef(0);
|
||||||
|
const hourlyRecordsRequestIdRef = useRef(0);
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const [timeDistribution, setTimeDistribution] = useState<TimeDistributionPoint[]>([]);
|
const [timeDistribution, setTimeDistribution] = useState<TimeDistributionPoint[]>([]);
|
||||||
const [records, setRecords] = useState<ScreenRecord[]>([]);
|
const [records, setRecords] = useState<ScreenRecord[]>([]);
|
||||||
@ -58,30 +85,50 @@ export default function ScreenshotsTab({
|
|||||||
|
|
||||||
// 获取时间分布数据
|
// 获取时间分布数据
|
||||||
const fetchTimeDistribution = async () => {
|
const fetchTimeDistribution = async () => {
|
||||||
|
const requestId = ++timeDistributionRequestIdRef.current;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoadingDistribution(true);
|
setLoadingDistribution(true);
|
||||||
const response = await fetch(`/hosts/${hostname}/time-distribution`);
|
|
||||||
if (!response.ok) throw new Error('获取时间分布数据失败');
|
const data = await fetchJsonOnce<{ distribution: TimeDistributionPoint[] }>(
|
||||||
const data = await response.json();
|
`/hosts/${hostname}/time-distribution`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (requestId !== timeDistributionRequestIdRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setTimeDistribution(data.distribution);
|
setTimeDistribution(data.distribution);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (requestId === timeDistributionRequestIdRef.current) {
|
||||||
console.error('获取时间分布数据失败:', error);
|
console.error('获取时间分布数据失败:', error);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
if (requestId === timeDistributionRequestIdRef.current) {
|
||||||
setLoadingDistribution(false);
|
setLoadingDistribution(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取小时记录
|
// 获取小时记录
|
||||||
const fetchHourlyRecords = async (startTime: number, endTime: number, options?: { targetRecordId?: string, keepSelection?: boolean }) => {
|
const fetchHourlyRecords = async (startTime: number, endTime: number, options?: { targetRecordId?: string, keepSelection?: boolean }) => {
|
||||||
|
const requestId = ++hourlyRecordsRequestIdRef.current;
|
||||||
|
const requestUrl = `/hosts/${hostname}/screenshots?startTime=${startTime}&endTime=${endTime}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoadingRecords(true);
|
setLoadingRecords(true);
|
||||||
setShowDetailTimeline(true);
|
setShowDetailTimeline(true);
|
||||||
const response = await fetch(
|
|
||||||
`/hosts/${hostname}/screenshots?startTime=${startTime}&endTime=${endTime}`
|
const data = await fetchJsonOnce<{
|
||||||
);
|
lastUpdate: string | null;
|
||||||
if (!response.ok) throw new Error('获取记录数据失败');
|
records: ScreenRecord[];
|
||||||
const data = await response.json();
|
}>(requestUrl);
|
||||||
const newRecords = data.records.reverse();
|
|
||||||
|
if (requestId !== hourlyRecordsRequestIdRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRecords = data.records;
|
||||||
setRecords(newRecords);
|
setRecords(newRecords);
|
||||||
onLastUpdateChange(data.lastUpdate);
|
onLastUpdateChange(data.lastUpdate);
|
||||||
setTimeRange({ min: startTime * 1000, max: endTime * 1000 });
|
setTimeRange({ min: startTime * 1000, max: endTime * 1000 });
|
||||||
@ -104,10 +151,14 @@ export default function ScreenshotsTab({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (requestId === hourlyRecordsRequestIdRef.current) {
|
||||||
console.error('获取记录数据失败:', error);
|
console.error('获取记录数据失败:', error);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
if (requestId === hourlyRecordsRequestIdRef.current) {
|
||||||
setLoadingRecords(false);
|
setLoadingRecords(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 生成视频
|
// 生成视频
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
|||||||
import { win32 } from 'path'
|
import { win32 } from 'path'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import { withCors } from '@/lib/middleware'
|
import { withCors } from '@/lib/middleware'
|
||||||
|
import { Prisma } from '@prisma/client'
|
||||||
|
|
||||||
const MAX_GAP_MS = 5 * 60 * 1000
|
const MAX_GAP_MS = 5 * 60 * 1000
|
||||||
const DEFAULT_SAMPLE_MS = 30 * 1000
|
const DEFAULT_SAMPLE_MS = 30 * 1000
|
||||||
@ -36,6 +37,12 @@ interface HourBucket {
|
|||||||
appDurations: Map<string, number>
|
appDurations: Map<string, number>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ScreenTimeRecordRow {
|
||||||
|
timestamp: Date
|
||||||
|
title: string | null
|
||||||
|
path: string | null
|
||||||
|
}
|
||||||
|
|
||||||
const cleanTitle = (title: string) => title.replace(/\s+/g, ' ').trim()
|
const cleanTitle = (title: string) => title.replace(/\s+/g, ' ').trim()
|
||||||
|
|
||||||
const trimLabelSeparators = (label: string) => label
|
const trimLabelSeparators = (label: string) => label
|
||||||
@ -304,27 +311,36 @@ async function handleScreenTime(req: NextRequest) {
|
|||||||
const aggregateEndMs = unit === 'week' ? weekEndMs : selectedDayEndMs
|
const aggregateEndMs = unit === 'week' ? weekEndMs : selectedDayEndMs
|
||||||
const aggregateEndDate = addDays(aggregateStartDate, unit === 'week' ? 7 : 1)
|
const aggregateEndDate = addDays(aggregateStartDate, unit === 'week' ? 7 : 1)
|
||||||
|
|
||||||
const records = await prisma.record.findMany({
|
const rangeStart = new Date(weekStartMs - MAX_GAP_MS)
|
||||||
where: {
|
const rangeEnd = new Date(weekEndMs + MAX_GAP_MS)
|
||||||
hostname,
|
|
||||||
timestamp: {
|
const records = await prisma.$queryRaw<ScreenTimeRecordRow[]>(Prisma.sql`
|
||||||
gte: new Date(weekStartMs - MAX_GAP_MS),
|
WITH scoped_records AS (
|
||||||
lte: new Date(weekEndMs + MAX_GAP_MS)
|
SELECT "id", "timestamp"
|
||||||
}
|
FROM "records"
|
||||||
},
|
WHERE "hostname" = ${hostname}
|
||||||
select: {
|
AND "timestamp" >= ${rangeStart}
|
||||||
timestamp: true,
|
AND "timestamp" <= ${rangeEnd}
|
||||||
windows: {
|
),
|
||||||
select: {
|
ranked_windows AS (
|
||||||
title: true,
|
SELECT
|
||||||
path: true
|
w."recordId",
|
||||||
}
|
w."title",
|
||||||
}
|
w."path",
|
||||||
},
|
ROW_NUMBER() OVER (PARTITION BY w."recordId" ORDER BY w."id") AS row_number
|
||||||
orderBy: {
|
FROM "windows" w
|
||||||
timestamp: 'asc'
|
INNER JOIN scoped_records r ON r."id" = w."recordId"
|
||||||
}
|
)
|
||||||
})
|
SELECT
|
||||||
|
r."timestamp",
|
||||||
|
w."title",
|
||||||
|
w."path"
|
||||||
|
FROM scoped_records r
|
||||||
|
LEFT JOIN ranked_windows w
|
||||||
|
ON w."recordId" = r."id"
|
||||||
|
AND w.row_number = 1
|
||||||
|
ORDER BY r."timestamp" ASC
|
||||||
|
`)
|
||||||
|
|
||||||
const validDiffs = records
|
const validDiffs = records
|
||||||
.slice(0, -1)
|
.slice(0, -1)
|
||||||
@ -346,8 +362,7 @@ async function handleScreenTime(req: NextRequest) {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
records.forEach((record, index) => {
|
records.forEach((record, index) => {
|
||||||
const activeWindow = record.windows[0]
|
if (record.title === null && record.path === null) return
|
||||||
if (!activeWindow) return
|
|
||||||
|
|
||||||
const currentMs = record.timestamp.getTime()
|
const currentMs = record.timestamp.getTime()
|
||||||
const nextMs = records[index + 1]?.timestamp.getTime()
|
const nextMs = records[index + 1]?.timestamp.getTime()
|
||||||
@ -359,10 +374,10 @@ async function handleScreenTime(req: NextRequest) {
|
|||||||
|
|
||||||
const intervalStartMs = currentMs
|
const intervalStartMs = currentMs
|
||||||
const intervalEndMs = currentMs + intervalMs
|
const intervalEndMs = currentMs + intervalMs
|
||||||
const title = cleanTitle(activeWindow.title)
|
const title = cleanTitle(record.title ?? '')
|
||||||
|| stripExtension(win32.basename(activeWindow.path || ''))
|
|| stripExtension(win32.basename(record.path ?? ''))
|
||||||
|| '未知窗口'
|
|| '未知窗口'
|
||||||
const process = normalizeProcess(activeWindow.path, title)
|
const process = normalizeProcess(record.path ?? '', title)
|
||||||
|
|
||||||
const aggregateContributionMs = Math.max(0, Math.min(intervalEndMs, aggregateEndMs) - Math.max(intervalStartMs, aggregateStartMs))
|
const aggregateContributionMs = Math.max(0, Math.min(intervalEndMs, aggregateEndMs) - Math.max(intervalStartMs, aggregateStartMs))
|
||||||
addDurationToUsageMap(
|
addDurationToUsageMap(
|
||||||
|
|||||||
@ -218,26 +218,40 @@ async function handleGetScreenshots(req: NextRequest) {
|
|||||||
if (endTime) whereClause.timestamp.lte = endTime
|
if (endTime) whereClause.timestamp.lte = endTime
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get records
|
const [records, host] = await Promise.all([
|
||||||
const records = await prisma.record.findMany({
|
prisma.record.findMany({
|
||||||
where: whereClause,
|
where: whereClause,
|
||||||
include: {
|
select: {
|
||||||
windows: true,
|
id: true,
|
||||||
screenshots: true
|
timestamp: true,
|
||||||
|
isStarred: true,
|
||||||
|
windows: {
|
||||||
|
select: {
|
||||||
|
title: true,
|
||||||
|
path: true,
|
||||||
|
memory: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
screenshots: {
|
||||||
|
select: {
|
||||||
|
fileId: true,
|
||||||
|
filename: true,
|
||||||
|
monitorName: true
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
timestamp: 'desc'
|
timestamp: 'asc'
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
prisma.host.findUnique({
|
||||||
// Get host info
|
|
||||||
const host = await prisma.host.findUnique({
|
|
||||||
where: { hostname },
|
where: { hostname },
|
||||||
select: {
|
select: {
|
||||||
hostname: true,
|
hostname: true,
|
||||||
lastUpdate: true
|
lastUpdate: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
])
|
||||||
|
|
||||||
if (!host) {
|
if (!host) {
|
||||||
return NextResponse.json({ error: '未找到主机记录' }, { status: 404 })
|
return NextResponse.json({ error: '未找到主机记录' }, { status: 404 })
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "windows_recordId_idx" ON "windows"("recordId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "screenshots_recordId_idx" ON "screenshots"("recordId");
|
||||||
@ -56,6 +56,8 @@ model Window {
|
|||||||
// Relations
|
// Relations
|
||||||
record Record @relation(fields: [recordId], references: [id], onDelete: Cascade)
|
record Record @relation(fields: [recordId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([recordId])
|
||||||
|
|
||||||
@@map("windows")
|
@@map("windows")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,6 +74,7 @@ model Screenshot {
|
|||||||
// Relations
|
// Relations
|
||||||
record Record @relation(fields: [recordId], references: [id], onDelete: Cascade)
|
record Record @relation(fields: [recordId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([recordId])
|
||||||
@@index([objectName])
|
@@index([objectName])
|
||||||
@@map("screenshots")
|
@@map("screenshots")
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user