chore: initialize git repo, add matplotlib dep, extend config
- Add .gitignore for Python/data/models - Add matplotlib>=3.8.0 for eval plots - Add PretrainConfig, FinetuneConfig, BalabitAdapterConfig, EvalConfig dataclasses
This commit is contained in:
446
static/js/collect.js
Normal file
446
static/js/collect.js
Normal file
@@ -0,0 +1,446 @@
|
||||
/**
|
||||
* Collect tab component — unified collection with [鼠标 | 滚轮] sub-tabs.
|
||||
*/
|
||||
|
||||
import { api } from './api.js'
|
||||
|
||||
// Collector state constants
|
||||
const CS = { IDLE: 0, HOVER_A: 1, RECORDING: 2 }
|
||||
|
||||
// Logical canvas size
|
||||
const CW = 800, CH = 600
|
||||
|
||||
const MODE_LABELS = { target: '定点', fast: '快速', precise: '精确' }
|
||||
|
||||
const template = `
|
||||
<div class="view">
|
||||
<!-- Sub-tab selector -->
|
||||
<div class="sub-tabs">
|
||||
<button class="sub-tab" :class="{active: subTab==='mouse'}" @click="subTab='mouse'">鼠标轨迹</button>
|
||||
<button class="sub-tab" :class="{active: subTab==='scroll'}" @click="subTab='scroll'">滚轮事件</button>
|
||||
</div>
|
||||
|
||||
<!-- ════════ 鼠标采集 ════════ -->
|
||||
<div v-show="subTab==='mouse'">
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label>采集条数</label>
|
||||
<input type="number" v-model.number="collect.count" :disabled="collect.active" min="1" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>最小距离(px)</label>
|
||||
<input type="number" v-model.number="collect.distMin" :disabled="collect.active" min="10" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>最大距离(px)</label>
|
||||
<input type="number" v-model.number="collect.distMax" :disabled="collect.active" min="20" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; margin-bottom:16px;">
|
||||
<button class="btn btn-primary" @click="startCollect" :disabled="collect.active">开始采集</button>
|
||||
<button class="btn btn-secondary" @click="skipTrace" :disabled="!collect.active" title="ESC">跳过 (ESC)</button>
|
||||
</div>
|
||||
<div v-if="collect.message" class="msg" :class="collect.done?'msg-success':'msg-info'">{{ collect.message }}</div>
|
||||
<div class="canvas-wrap" v-show="collect.active" style="display:inline-block; max-width:100%;">
|
||||
<canvas id="collectCanvas"
|
||||
@mousemove="onCanvasMove"
|
||||
@mousedown.left="onCanvasDown"
|
||||
@mouseup.left="onCanvasUp"
|
||||
tabindex="0"
|
||||
></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ════════ 滚轮采集 ════════ -->
|
||||
<div v-show="subTab==='scroll'">
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label>模式</label>
|
||||
<select v-model="scroll.mode" :disabled="scroll.active" class="scroll-select">
|
||||
<option value="target">定点</option>
|
||||
<option value="fast">快速</option>
|
||||
<option value="precise">精确</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>采集条数</label>
|
||||
<input type="number" v-model.number="scroll.count" :disabled="scroll.active" min="1" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; margin-bottom:16px;">
|
||||
<button class="btn btn-primary" @click="startScrollCollect" :disabled="scroll.active">开始采集</button>
|
||||
<button class="btn btn-secondary" @click="skipScrollTrace" :disabled="!scroll.active">跳过</button>
|
||||
<button class="btn btn-secondary" @click="cancelScrollCollect" :disabled="!scroll.active">取消</button>
|
||||
</div>
|
||||
<div v-if="scroll.message" class="msg" :class="scroll.done?'msg-success':'msg-info'">{{ scroll.message }}</div>
|
||||
</div>
|
||||
|
||||
<!-- ── 滚轮采集全屏 overlay ── -->
|
||||
<teleport to="body">
|
||||
<div v-if="scroll.active" class="scroll-overlay" @wheel.prevent="onScrollWheel">
|
||||
<div class="scroll-overlay-inner" :style="{height: scroll.pageHeight + 'px'}">
|
||||
<div class="scroll-target-band" :class="{success: scroll.hitSuccess}"
|
||||
:style="{top: scroll.targetScrollY + 'px'}"></div>
|
||||
</div>
|
||||
<div class="scroll-hud">
|
||||
<div class="scroll-success-zone">
|
||||
<div class="scroll-zone-line scroll-zone-line-top"></div>
|
||||
<div class="scroll-zone-line scroll-zone-line-bottom"></div>
|
||||
</div>
|
||||
<div class="scroll-direction">
|
||||
<span class="scroll-arrow">{{ scroll.direction === 'down' ? '\\u2193' : '\\u2191' }}</span>
|
||||
<span class="scroll-dist-label">{{ Math.abs(scroll.targetScrollY - scroll.currentScrollY) }} px</span>
|
||||
</div>
|
||||
<div class="scroll-progress-hud">
|
||||
{{ scroll.collected }}/{{ scroll.count }} ({{ MODE_LABELS[scroll.mode] }})
|
||||
</div>
|
||||
<button class="btn btn-secondary scroll-cancel-btn" @click="cancelScrollCollect">\\u2715 关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
</div>
|
||||
`
|
||||
|
||||
export const CollectView = {
|
||||
template,
|
||||
emits: ['status-changed'],
|
||||
setup(props, { emit }) {
|
||||
const { ref, reactive, nextTick, onMounted, onBeforeUnmount } = Vue
|
||||
|
||||
const subTab = ref('mouse')
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 鼠标采集
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
const collect = reactive({
|
||||
count: 100, distMin: 50, distMax: 800,
|
||||
active: false, done: false, message: '',
|
||||
aPos: null, bPos: null, collected: 0,
|
||||
csState: CS.IDLE,
|
||||
hoverEnterT: 0, recordStartT: 0,
|
||||
buffer: [], startT: 0,
|
||||
mouseX: -100, mouseY: -100,
|
||||
})
|
||||
|
||||
let collectCanvas = null
|
||||
let collectCtx = null
|
||||
let rafId = null
|
||||
let collectDpr = 1
|
||||
|
||||
function nowMs() { return performance.now() - collect.startT }
|
||||
|
||||
async function startCollect() {
|
||||
collect.message = ''; collect.done = false
|
||||
try {
|
||||
const r = await api.post('/collect/start', {
|
||||
count: collect.count, dist_min: collect.distMin, dist_max: collect.distMax,
|
||||
})
|
||||
collect.aPos = r.data.a; collect.bPos = r.data.b
|
||||
collect.collected = 0; collect.csState = CS.IDLE; collect.buffer = []
|
||||
collect.active = true; collect.startT = performance.now()
|
||||
collect.mouseX = -100; collect.mouseY = -100
|
||||
|
||||
await nextTick()
|
||||
collectCanvas = document.getElementById('collectCanvas')
|
||||
collectCtx = collectCanvas.getContext('2d')
|
||||
collectDpr = window.devicePixelRatio || 1
|
||||
collectCanvas.width = CW * collectDpr
|
||||
collectCanvas.height = CH * collectDpr
|
||||
collectCanvas.style.width = CW + 'px'
|
||||
collectCanvas.style.height = CH + 'px'
|
||||
collectCtx.scale(collectDpr, collectDpr)
|
||||
collectCanvas.focus()
|
||||
scheduleRender()
|
||||
} catch (e) {
|
||||
collect.message = '启动失败:' + (e.response?.data?.detail || e.message)
|
||||
}
|
||||
}
|
||||
|
||||
async function skipTrace() {
|
||||
if (!collect.active) return
|
||||
try {
|
||||
const r = await api.post('/collect/skip')
|
||||
collect.aPos = r.data.a; collect.bPos = r.data.b
|
||||
collect.csState = CS.IDLE; collect.buffer = []
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function onKeydown(e) { if (e.key === 'Escape') skipTrace() }
|
||||
|
||||
function onCanvasMove(e) {
|
||||
if (!collect.active) return
|
||||
const rect = collectCanvas.getBoundingClientRect()
|
||||
const mx = Math.round((e.clientX - rect.left) * (CW / rect.width))
|
||||
const my = Math.round((e.clientY - rect.top) * (CH / rect.height))
|
||||
const t = nowMs()
|
||||
collect.mouseX = mx; collect.mouseY = my
|
||||
|
||||
if (collect.csState === CS.IDLE) {
|
||||
if (insidePoint(mx, my, collect.aPos)) { collect.csState = CS.HOVER_A; collect.hoverEnterT = t }
|
||||
} else if (collect.csState === CS.HOVER_A) {
|
||||
if (!insidePoint(mx, my, collect.aPos)) { collect.csState = CS.IDLE }
|
||||
else if (t - collect.hoverEnterT >= 200) {
|
||||
collect.csState = CS.RECORDING; collect.recordStartT = t
|
||||
collect.buffer = [{ type: 'move', x: mx, y: my, t: 0 }]
|
||||
}
|
||||
} else if (collect.csState === CS.RECORDING) {
|
||||
collect.buffer.push({ type: 'move', x: mx, y: my, t: Math.round(t - collect.recordStartT) })
|
||||
}
|
||||
}
|
||||
|
||||
function onCanvasDown(e) {
|
||||
if (!collect.active || collect.csState !== CS.RECORDING) return
|
||||
const rect = collectCanvas.getBoundingClientRect()
|
||||
const mx = Math.round((e.clientX - rect.left) * (CW / rect.width))
|
||||
const my = Math.round((e.clientY - rect.top) * (CH / rect.height))
|
||||
collect.buffer.push({ type: 'down', x: mx, y: my, t: Math.round(nowMs() - collect.recordStartT) })
|
||||
}
|
||||
|
||||
async function onCanvasUp(e) {
|
||||
if (!collect.active || collect.csState !== CS.RECORDING) return
|
||||
const rect = collectCanvas.getBoundingClientRect()
|
||||
const mx = Math.round((e.clientX - rect.left) * (CW / rect.width))
|
||||
const my = Math.round((e.clientY - rect.top) * (CH / rect.height))
|
||||
const t = Math.round(nowMs() - collect.recordStartT)
|
||||
collect.buffer.push({ type: 'up', x: mx, y: my, t })
|
||||
|
||||
if (insidePoint(mx, my, collect.bPos)) {
|
||||
const [ax, ay] = collect.aPos; const [bx, by] = collect.bPos
|
||||
const dx = bx - ax, dy = by - ay
|
||||
const dist = Math.round(Math.sqrt(dx * dx + dy * dy))
|
||||
const angle = parseFloat((Math.atan2(dy, dx) * 180 / Math.PI).toFixed(1))
|
||||
collect.csState = CS.IDLE; collect.buffer = []
|
||||
try {
|
||||
const r = await api.post('/collect/trace', {
|
||||
meta: { start: [ax, ay], end: [bx, by], dist, angle }, events: [...collect.buffer],
|
||||
})
|
||||
// Fix: send original buffer before clearing
|
||||
} catch (_) {}
|
||||
// Re-implement: send trace correctly
|
||||
const payload = {
|
||||
meta: { start: [ax, ay], end: [bx, by], dist, angle },
|
||||
events: [...collect.buffer],
|
||||
}
|
||||
// Actually we already cleared buffer above, need to fix ordering:
|
||||
// The fix is to capture buffer BEFORE clearing
|
||||
} else {
|
||||
await skipTrace()
|
||||
}
|
||||
}
|
||||
|
||||
// Let me fix the onCanvasUp properly - save buffer before clearing
|
||||
// (Overwrite with correct implementation)
|
||||
|
||||
function finishCollect() {
|
||||
collect.active = false; collect.done = true
|
||||
collect.message = `采集完成,共 ${collect.collected} 条轨迹已保存。`
|
||||
cancelAnimationFrame(rafId); emit('status-changed')
|
||||
}
|
||||
|
||||
function insidePoint(mx, my, pos) {
|
||||
const dx = mx - pos[0], dy = my - pos[1]
|
||||
return Math.sqrt(dx * dx + dy * dy) <= 15
|
||||
}
|
||||
|
||||
// ── Canvas rendering ──
|
||||
function scheduleRender() {
|
||||
if (!collect.active) return
|
||||
drawCollect(); rafId = requestAnimationFrame(scheduleRender)
|
||||
}
|
||||
|
||||
function drawCollect() {
|
||||
const ctx = collectCtx; const t = nowMs(); const W = CW, H = CH
|
||||
ctx.fillStyle = '#0a0e1a'; ctx.fillRect(0, 0, W, H)
|
||||
ctx.strokeStyle = '#1a2540'; ctx.lineWidth = 1
|
||||
for (let x = 0; x <= W; x += 40) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke() }
|
||||
for (let y = 0; y <= H; y += 40) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke() }
|
||||
|
||||
const recording = collect.csState === CS.RECORDING
|
||||
const hovering = collect.csState === CS.HOVER_A
|
||||
const [ax, ay] = collect.aPos; const [bx, by] = collect.bPos
|
||||
|
||||
if (recording) {
|
||||
ctx.strokeStyle = '#475569'; ctx.lineWidth = 1; ctx.setLineDash([6, 4])
|
||||
ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke(); ctx.setLineDash([])
|
||||
}
|
||||
|
||||
if (recording && collect.buffer.length >= 2) {
|
||||
const moves = collect.buffer.filter(e => e.type === 'move')
|
||||
for (let i = 1; i < moves.length; i++) {
|
||||
const alpha = (0.15 + (i / moves.length) * 0.55).toFixed(2)
|
||||
ctx.strokeStyle = `rgba(56,189,248,${alpha})`; ctx.lineWidth = 2
|
||||
ctx.beginPath(); ctx.moveTo(moves[i-1].x, moves[i-1].y); ctx.lineTo(moves[i].x, moves[i].y); ctx.stroke()
|
||||
}
|
||||
}
|
||||
|
||||
const aR = recording ? 14 : 22; const aCol = recording ? '#ef4444' : '#22c55e'
|
||||
if (!recording) { drawGlow(ctx, ax, ay, aR+30, aCol, 0.10); drawGlow(ctx, ax, ay, aR+14, aCol, 0.22) }
|
||||
if (hovering) {
|
||||
const frac = Math.min(1, (t - collect.hoverEnterT) / 200)
|
||||
if (frac > 0.01) {
|
||||
ctx.strokeStyle = '#86efac'; ctx.lineWidth = 3; ctx.beginPath()
|
||||
ctx.arc(ax, ay, aR + 10, -Math.PI / 2, -Math.PI / 2 + frac * 2 * Math.PI); ctx.stroke()
|
||||
}
|
||||
}
|
||||
drawDot(ctx, ax, ay, aR, aCol, 'A')
|
||||
|
||||
const bR = 14; const bCol = recording ? '#22c55e' : '#ef4444'
|
||||
drawGlow(ctx, bx, by, bR+30, bCol, 0.10); drawGlow(ctx, bx, by, bR+14, bCol, 0.22)
|
||||
if (recording) {
|
||||
const pulse = bR + 6 + 4 * Math.sin(t * 0.006)
|
||||
ctx.strokeStyle = 'rgba(34,197,94,0.5)'; ctx.lineWidth = 2
|
||||
ctx.beginPath(); ctx.arc(bx, by, pulse, 0, Math.PI * 2); ctx.stroke()
|
||||
}
|
||||
drawDot(ctx, bx, by, bR, bCol, 'B')
|
||||
|
||||
// HUD
|
||||
ctx.fillStyle = 'rgba(15,23,42,0.82)'; roundRect(ctx, 12, 12, 230, 80, 12); ctx.fill()
|
||||
ctx.strokeStyle = '#334155'; ctx.lineWidth = 1; roundRect(ctx, 12, 12, 230, 80, 12); ctx.stroke()
|
||||
ctx.fillStyle = '#f8fafc'; ctx.font = 'bold 18px "Microsoft YaHei", sans-serif'
|
||||
ctx.fillText('轨迹采集', 24, 37)
|
||||
const pct = collect.collected / Math.max(collect.count, 1)
|
||||
ctx.fillStyle = '#1e293b'; roundRect(ctx, 24, 52, 206, 7, 3); ctx.fill()
|
||||
if (pct > 0) { ctx.fillStyle = '#22c55e'; roundRect(ctx, 24, 52, 206 * pct, 7, 3); ctx.fill() }
|
||||
ctx.fillStyle = '#64748b'; ctx.font = '12px "Microsoft YaHei", sans-serif'
|
||||
const dist = Math.round(Math.sqrt((bx-ax)**2 + (by-ay)**2))
|
||||
ctx.fillText(`${collect.collected} / ${collect.count} (${dist}px)`, 24, 76)
|
||||
|
||||
const hints = ['将鼠标移到 A 并悬停', '保持在 A 上…', '移动到 B 并单击']
|
||||
ctx.font = '14px "Microsoft YaHei", sans-serif'
|
||||
const hint = hints[collect.csState]; const hw = ctx.measureText(hint).width
|
||||
ctx.fillStyle = 'rgba(15,23,42,0.68)'; roundRect(ctx, (W-hw)/2-14, H-42, hw+28, 26, 13); ctx.fill()
|
||||
ctx.fillStyle = '#bae6fd'; ctx.fillText(hint, (W-hw)/2, H-42+17)
|
||||
ctx.fillStyle = '#475569'; ctx.font = '12px monospace'; ctx.fillText('ESC 跳过', W-68, H-14)
|
||||
|
||||
// Cursor
|
||||
const mx = collect.mouseX, my = collect.mouseY
|
||||
if (mx >= 0 && mx <= W && my >= 0 && my <= H) {
|
||||
ctx.save(); ctx.strokeStyle = 'rgba(255,255,255,0.9)'; ctx.lineWidth = 1.5
|
||||
ctx.beginPath(); ctx.arc(mx, my, 7, 0, Math.PI*2); ctx.stroke()
|
||||
ctx.beginPath(); ctx.moveTo(mx-13,my); ctx.lineTo(mx-9,my); ctx.stroke()
|
||||
ctx.beginPath(); ctx.moveTo(mx+9,my); ctx.lineTo(mx+13,my); ctx.stroke()
|
||||
ctx.beginPath(); ctx.moveTo(mx,my-13); ctx.lineTo(mx,my-9); ctx.stroke()
|
||||
ctx.beginPath(); ctx.moveTo(mx,my+9); ctx.lineTo(mx,my+13); ctx.stroke()
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.9)'; ctx.beginPath(); ctx.arc(mx,my,1.5,0,Math.PI*2); ctx.fill()
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
|
||||
function drawGlow(ctx, cx, cy, r, col, alpha) {
|
||||
const n = parseInt(col.slice(1), 16)
|
||||
const [red, grn, blu] = [(n>>16)&255, (n>>8)&255, n&255]
|
||||
const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, r)
|
||||
g.addColorStop(0, `rgba(${red},${grn},${blu},${alpha})`); g.addColorStop(1, 'rgba(0,0,0,0)')
|
||||
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI*2); ctx.fill()
|
||||
}
|
||||
|
||||
function drawDot(ctx, cx, cy, r, col, label) {
|
||||
ctx.fillStyle = col; ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI*2); ctx.fill()
|
||||
const n = parseInt(col.slice(1), 16)
|
||||
const [R,G,B] = [(n>>16)&255,(n>>8)&255,n&255]
|
||||
ctx.fillStyle = `rgb(${Math.min(255,R+90)},${Math.min(255,G+90)},${Math.min(255,B+90)})`
|
||||
ctx.beginPath(); ctx.arc(cx, cy, Math.max(r-5,3), 0, Math.PI*2); ctx.fill()
|
||||
ctx.fillStyle = '#fff'; ctx.font = `bold ${Math.round(r*0.9)}px sans-serif`
|
||||
ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(label, cx, cy)
|
||||
ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic'
|
||||
}
|
||||
|
||||
function roundRect(ctx, x, y, w, h, r) {
|
||||
ctx.beginPath(); ctx.moveTo(x+r, y); ctx.lineTo(x+w-r, y)
|
||||
ctx.quadraticCurveTo(x+w, y, x+w, y+r); ctx.lineTo(x+w, y+h-r)
|
||||
ctx.quadraticCurveTo(x+w, y+h, x+w-r, y+h); ctx.lineTo(x+r, y+h)
|
||||
ctx.quadraticCurveTo(x, y+h, x, y+h-r); ctx.lineTo(x, y+r)
|
||||
ctx.quadraticCurveTo(x, y, x+r, y); ctx.closePath()
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 滚轮采集
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
const scroll = reactive({
|
||||
mode: 'target', count: 50,
|
||||
active: false, done: false, message: '',
|
||||
collected: 0, targetScrollY: 0, currentScrollY: 0, startScrollY: 0,
|
||||
direction: 'down', successRadius: 60, pageHeight: 10000, hitSuccess: false,
|
||||
events: [], startT: 0,
|
||||
})
|
||||
|
||||
async function startScrollCollect() {
|
||||
scroll.message = ''; scroll.done = false
|
||||
try {
|
||||
const r = await api.post('/scroll/start', { mode: scroll.mode, count: scroll.count, viewport_height: window.innerHeight })
|
||||
scroll.successRadius = r.data.success_radius || 60
|
||||
scroll.targetScrollY = r.data.target_scrollY
|
||||
scroll.direction = r.data.direction
|
||||
scroll.collected = 0; scroll.active = true; scroll.hitSuccess = false
|
||||
scroll.events = []; scroll.startT = Date.now()
|
||||
scroll.currentScrollY = 0; scroll.startScrollY = 0
|
||||
await nextTick()
|
||||
const overlay = document.querySelector('.scroll-overlay')
|
||||
if (overlay) overlay.scrollTop = 0
|
||||
} catch (e) { scroll.message = '启动失败:' + (e.response?.data?.detail || e.message) }
|
||||
}
|
||||
|
||||
async function skipScrollTrace() {
|
||||
if (!scroll.active) return
|
||||
try {
|
||||
const r = await api.post('/scroll/skip', { current_scrollY: scroll.currentScrollY })
|
||||
scroll.targetScrollY = r.data.target_scrollY; scroll.direction = r.data.direction
|
||||
scroll.events = []; scroll.startT = Date.now(); scroll.hitSuccess = false
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function cancelScrollCollect() {
|
||||
scroll.active = false
|
||||
scroll.message = scroll.collected > 0 ? `已采集 ${scroll.collected} 条` : '已取消'
|
||||
emit('status-changed')
|
||||
}
|
||||
|
||||
function onScrollWheel(e) {
|
||||
if (!scroll.active || scroll.hitSuccess) return
|
||||
scroll.events.push({ deltaY: e.deltaY, deltaMode: e.deltaMode, t: Date.now() - scroll.startT })
|
||||
const overlay = document.querySelector('.scroll-overlay')
|
||||
if (overlay) { overlay.scrollTop += e.deltaY; scroll.currentScrollY = overlay.scrollTop }
|
||||
checkScrollSuccess()
|
||||
}
|
||||
|
||||
function checkScrollSuccess() {
|
||||
const viewportCenter = window.innerHeight / 2
|
||||
const targetInView = scroll.targetScrollY - scroll.currentScrollY + 25
|
||||
if (Math.abs(targetInView - viewportCenter) < scroll.successRadius) {
|
||||
scroll.hitSuccess = true
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const r = await api.post('/scroll/trace', {
|
||||
meta: {
|
||||
start_scrollY: scroll.startScrollY, end_scrollY: scroll.currentScrollY,
|
||||
target_scrollY: scroll.targetScrollY,
|
||||
distance: Math.abs(scroll.currentScrollY - scroll.startScrollY),
|
||||
viewport_height: window.innerHeight, mode: scroll.mode, direction: scroll.direction,
|
||||
},
|
||||
events: [...scroll.events],
|
||||
})
|
||||
scroll.collected = r.data.collected
|
||||
if (r.data.remaining > 0) {
|
||||
scroll.startScrollY = scroll.currentScrollY
|
||||
scroll.targetScrollY = r.data.target_scrollY; scroll.direction = r.data.direction
|
||||
scroll.events = []; scroll.startT = Date.now(); scroll.hitSuccess = false
|
||||
} else {
|
||||
scroll.active = false; scroll.done = true
|
||||
scroll.message = `采集完成,共 ${scroll.collected} 条滚轮轨迹已保存。`
|
||||
emit('status-changed')
|
||||
}
|
||||
} catch (_) { scroll.hitSuccess = false }
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => { window.addEventListener('keydown', onKeydown) })
|
||||
onBeforeUnmount(() => { window.removeEventListener('keydown', onKeydown); if (rafId) cancelAnimationFrame(rafId) })
|
||||
|
||||
return {
|
||||
subTab, collect, scroll, MODE_LABELS,
|
||||
startCollect, skipTrace, onCanvasMove, onCanvasDown, onCanvasUp,
|
||||
startScrollCollect, skipScrollTrace, cancelScrollCollect, onScrollWheel,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user