/** * Verify tab component — unified verification with [鼠标 | 滚轮] sub-tabs. */ import { api } from './api.js' import { TAB10_COLORS, coolwarmColor, createChart, disposeChart } from './charts.js' const MODE_LABELS = { target: '定点', fast: '快速', precise: '精确' } const template = `
{{ verify.error }}
距离 {{ verify.stats.dist }} px
角度 {{ verify.stats.angle }}°
平均时长 {{ verify.stats.avgDur }} ms
均值 Δt {{ verify.stats.meanDt }} ms
轨迹可视化(颜色=速度)
时间间隔 Δt
{{ scrollVerify.error }}
距离 {{ scrollVerify.stats.distance }} px
模式 {{ scrollVerify.stats.mode }}
平均时长 {{ scrollVerify.stats.avgDur }} ms
事件数 {{ scrollVerify.stats.avgEvents }}
均值 |δY| {{ scrollVerify.stats.meanAbsDy }}
累计滚动位置
deltaY 事件
` export const VerifyView = { template, setup() { const { ref, reactive, nextTick } = Vue const subTab = ref('mouse') // ═══════════════════════════════════════════════════════════════ // 鼠标验证 // ═══════════════════════════════════════════════════════════════ const verify = reactive({ startStr: '100,200', endStr: '700,400', nPaths: 5, loading: false, error: '', paths: [], stats: { dist: 0, angle: 0, avgDur: 0, meanDt: 0 }, }) let trajChartInst = null, dtChartInst = null async function startVerify() { verify.error = ''; verify.loading = true; verify.paths = [] const start = verify.startStr.split(',').map(Number) const end = verify.endStr.split(',').map(Number) if (start.length !== 2 || end.length !== 2 || start.some(isNaN) || end.some(isNaN)) { verify.error = '坐标格式错误,请输入 x,y'; verify.loading = false; return } try { const r = await api.post('/verify', { start, end, n_paths: Math.max(1, Math.min(12, verify.nPaths)) }) verify.paths = r.data.paths // Stats const dx = end[0] - start[0], dy = end[1] - start[1] verify.stats.dist = Math.round(Math.sqrt(dx*dx + dy*dy)) verify.stats.angle = Math.round(Math.atan2(dy, dx) * 180 / Math.PI) let totalDur = 0, totalDt = 0, dtCount = 0 for (const path of r.data.paths) { if (path.length > 2) totalDur += path[path.length - 3][2] // last move point for (let i = 1; i < path.length - 2; i++) { totalDt += path[i][2] - path[i-1][2]; dtCount++ } } const n = r.data.paths.length || 1 verify.stats.avgDur = Math.round(totalDur / n) verify.stats.meanDt = dtCount > 0 ? Math.round(totalDt / dtCount) : 0 await nextTick() drawMouseCharts(r.data.paths, start, end) } catch (e) { verify.error = e.response?.data?.detail || e.message } finally { verify.loading = false } } function drawMouseCharts(paths, start, end) { const trajDom = document.getElementById('trajChart') const dtDom = document.getElementById('dtChart') trajChartInst = disposeChart(trajChartInst) dtChartInst = disposeChart(dtChartInst) trajChartInst = createChart(trajDom) dtChartInst = createChart(dtDom) // ── Trajectory chart with speed coloring ── const allSpeeds = [] for (const path of paths) { for (let i = 1; i < path.length - 2; i++) { const dx = path[i][0] - path[i-1][0], dy = path[i][1] - path[i-1][1] const dt = (path[i][2] - path[i-1][2]) || 1 allSpeeds.push(Math.sqrt(dx*dx + dy*dy) / dt) } } const maxSpd = Math.max(...allSpeeds, 0.01) const segSeries = [] for (const path of paths) { for (let i = 1; i < path.length - 2; i++) { const dx = path[i][0] - path[i-1][0], dy = path[i][1] - path[i-1][1] const dt = (path[i][2] - path[i-1][2]) || 1 const spd = Math.sqrt(dx*dx + dy*dy) / dt const col = coolwarmColor(1 - spd / maxSpd) segSeries.push({ type: 'line', data: [[path[i-1][0], path[i-1][1]], [path[i][0], path[i][1]]], lineStyle: { color: col, width: 2, opacity: 0.8 }, symbol: 'none', silent: true, animation: false, }) } } // Start/end markers segSeries.push({ type: 'scatter', data: [[start[0], start[1]]], symbolSize: 14, itemStyle: { color: '#22c55e' }, label: { show: true, formatter: 'S', color: '#fff', fontSize: 10 }, }) segSeries.push({ type: 'scatter', data: [[end[0], end[1]]], symbolSize: 14, itemStyle: { color: '#fbbf24' }, label: { show: true, formatter: 'E', color: '#fff', fontSize: 10 }, }) trajChartInst.setOption({ backgroundColor: '#0a0e1a', grid: { left: 48, right: 20, top: 16, bottom: 32 }, xAxis: { type: 'value', axisLine: { lineStyle: { color: '#334155' } }, axisLabel: { color: '#475569', fontSize: 10 }, splitLine: { lineStyle: { color: '#1e293b' } } }, yAxis: { type: 'value', inverse: true, axisLine: { lineStyle: { color: '#334155' } }, axisLabel: { color: '#475569', fontSize: 10 }, splitLine: { lineStyle: { color: '#1e293b' } } }, series: segSeries, }, true) // ── Δt chart ── const dtSeries = paths.map((path, idx) => ({ type: 'line', name: `路径 ${idx+1}`, data: path.slice(1, -2).map((p, i) => path[i+1][2] - path[i][2]), lineStyle: { color: TAB10_COLORS[idx % TAB10_COLORS.length], width: 1.5 }, itemStyle: { color: TAB10_COLORS[idx % TAB10_COLORS.length] }, symbol: 'none', smooth: false, })) // Mean line const allDt = []; paths.forEach(p => { for (let i=1; i 0 ? allDt.reduce((a,b)=>a+b,0)/allDt.length : 0 dtChartInst.setOption({ backgroundColor: '#0a0e1a', tooltip: { trigger: 'axis', backgroundColor: '#1e293b', borderColor: '#334155', textStyle: { color: '#f8fafc', fontSize: 12 } }, legend: { show: paths.length > 1, top: 6, right: 8, textStyle: { color: '#64748b', fontSize: 11 }, itemWidth: 18, itemHeight: 3, icon: 'rect' }, grid: { left: 48, right: 20, top: paths.length > 1 ? 36 : 16, bottom: 32 }, xAxis: { type: 'category', name: '点序号', axisLine: { lineStyle: { color: '#334155' } }, axisLabel: { color: '#475569', fontSize: 10 }, splitLine: { show: false } }, yAxis: { type: 'value', name: 'ms', axisLine: { lineStyle: { color: '#334155' } }, axisLabel: { color: '#475569', fontSize: 10 }, splitLine: { lineStyle: { color: '#1e293b' } } }, dataZoom: [{ type: 'inside', xAxisIndex: 0, start: 0, end: 100 }], series: [...dtSeries, { type: 'line', name: '均值', data: [], markLine: { silent: true, data: [{ yAxis: meanDt, label: { formatter: `均值 ${meanDt.toFixed(1)}ms`, color: '#64748b', fontSize: 10 } }], lineStyle: { color: '#64748b', type: 'dashed' }, }, itemStyle: { color: 'transparent' }, }], }, true) } // ═══════════════════════════════════════════════════════════════ // 滚轮验证 // ═══════════════════════════════════════════════════════════════ const scrollVerify = reactive({ startScrollY: 0, targetScrollY: 2000, mode: 'target', nPaths: 5, loading: false, error: '', paths: [], stats: { distance: 0, mode: '', avgDur: 0, avgEvents: 0, meanAbsDy: 0 }, }) let scrollPosChartInst = null, scrollDyChartInst = null async function startScrollVerify() { scrollVerify.error = ''; scrollVerify.loading = true; scrollVerify.paths = [] try { const r = await api.post('/scroll/verify', { start_scrollY: scrollVerify.startScrollY, target_scrollY: scrollVerify.targetScrollY, mode: scrollVerify.mode, n_paths: Math.max(1, Math.min(12, scrollVerify.nPaths)), }) scrollVerify.paths = r.data.paths const distance = Math.abs(scrollVerify.targetScrollY - scrollVerify.startScrollY) let totalDur = 0, totalEvents = 0, totalAbsDy = 0, dyCount = 0 for (const path of r.data.paths) { if (path.length > 0) { totalDur += path[path.length-1].t; totalEvents += path.length } for (const ev of path) { totalAbsDy += Math.abs(ev.deltaY); dyCount++ } } const n = r.data.paths.length || 1 scrollVerify.stats = { distance, mode: MODE_LABELS[scrollVerify.mode] || scrollVerify.mode, avgDur: Math.round(totalDur / n), avgEvents: Math.round(totalEvents / n), meanAbsDy: dyCount > 0 ? Math.round(totalAbsDy / dyCount) : 0, } await nextTick() drawScrollCharts(r.data.paths) } catch (e) { scrollVerify.error = e.response?.data?.detail || e.message } finally { scrollVerify.loading = false } } function drawScrollCharts(paths) { const posDom = document.getElementById('scrollPosChart') const dyDom = document.getElementById('scrollDyChart') scrollPosChartInst = disposeChart(scrollPosChartInst) scrollDyChartInst = disposeChart(scrollDyChartInst) scrollPosChartInst = createChart(posDom) scrollDyChartInst = createChart(dyDom) // Cumulative position chart with speed coloring const allSpeeds = [] for (const path of paths) { for (let i = 1; i < path.length; i++) { const dy = Math.abs(path[i].deltaY), dt = (path[i].t - path[i-1].t) || 1 allSpeeds.push(dy / dt) } } const sMax = Math.max(...allSpeeds, 0.01) const segSeries = [] for (const path of paths) { let cumY = 0; const pts = [{ t: 0, y: 0 }] for (const ev of path) { cumY += ev.deltaY; pts.push({ t: ev.t, y: cumY }) } for (let i = 1; i < pts.length; i++) { const dy = Math.abs(pts[i].y - pts[i-1].y), dt = (pts[i].t - pts[i-1].t) || 1 const col = coolwarmColor(1 - (dy/dt) / sMax) segSeries.push({ type: 'line', data: [[pts[i-1].t, pts[i-1].y], [pts[i].t, pts[i].y]], lineStyle: { color: col, width: 2, opacity: 0.85 }, symbol: 'none', silent: true, animation: false, }) } } scrollPosChartInst.setOption({ backgroundColor: '#0a0e1a', tooltip: { trigger: 'axis', backgroundColor: '#1e293b', borderColor: '#334155', textStyle: { color: '#f8fafc', fontSize: 12 } }, grid: { left: 56, right: 20, top: 16, bottom: 32 }, xAxis: { type: 'value', name: 'ms', axisLine: { lineStyle: { color: '#334155' } }, axisLabel: { color: '#475569', fontSize: 10 }, splitLine: { lineStyle: { color: '#1e293b' } } }, yAxis: { type: 'value', name: 'scrollY', axisLine: { lineStyle: { color: '#334155' } }, axisLabel: { color: '#475569', fontSize: 10 }, splitLine: { lineStyle: { color: '#1e293b' } } }, series: segSeries, }, true) // DeltaY chart const dySeries = paths.map((path, idx) => ({ type: 'line', name: `路径 ${idx+1}`, data: path.map(ev => ev.deltaY), lineStyle: { color: TAB10_COLORS[idx % TAB10_COLORS.length], width: 1.8 }, itemStyle: { color: TAB10_COLORS[idx % TAB10_COLORS.length] }, symbol: 'none', })) scrollDyChartInst.setOption({ backgroundColor: '#0a0e1a', tooltip: { trigger: 'axis', backgroundColor: '#1e293b', borderColor: '#334155', textStyle: { color: '#f8fafc', fontSize: 12 } }, legend: { show: paths.length > 1, top: 6, right: 8, textStyle: { color: '#64748b', fontSize: 11 }, itemWidth: 18, itemHeight: 3, icon: 'rect' }, grid: { left: 52, right: 20, top: paths.length > 1 ? 36 : 16, bottom: 32 }, xAxis: { type: 'category', name: '事件序号', axisLine: { lineStyle: { color: '#334155' } }, axisLabel: { color: '#475569', fontSize: 10 }, splitLine: { show: false } }, yAxis: { type: 'value', name: 'deltaY', axisLine: { lineStyle: { color: '#334155' } }, axisLabel: { color: '#475569', fontSize: 10 }, splitLine: { lineStyle: { color: '#1e293b' } } }, dataZoom: [{ type: 'inside', xAxisIndex: 0, start: 0, end: 100 }], series: dySeries, }, true) } return { subTab, verify, scrollVerify, startVerify, startScrollVerify, } } }