// ==UserScript== // @name 车位线标注验收 // @namespace http://tampermonkey.net/ // @version 1.0 // @description 拦截标注数据验收,根据业务规则精准校验,支持点击高亮定位与窗口拖动 // @author CC // @match http://113.207.49.80:8080/* // @grant none // ==/UserScript== (function() { 'use strict'; const originalOpen = XMLHttpRequest.prototype.open; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url, ...args) { this._hookData = { method, url }; return originalOpen.apply(this, [method, url, ...args]); }; XMLHttpRequest.prototype.send = function(...args) { if (this._hookData && this._hookData.url.includes('/api/task/get-mark-data')) { this.addEventListener('load', function() { if (this.status === 200) { try { const responseData = JSON.parse(this.responseText); if (responseData.code === 0 && responseData.data && responseData.data.markData) { validateAndReport(responseData.data.markData); } } catch (e) { console.error('[验收助手] 解析JSON失败:', e); } } }); } return originalSend.apply(this, args); } function validateAndReport(markData) { const marks = markData.marks || []; const errors = []; // 1. 数据预处理与分组 const groups = {}; const pointList = []; let mainIndex = 1; marks.forEach((mark) => { const gid = mark.groupId; if (mark.type === 'point' && gid !== undefined) { if (!groups[gid]) { groups[gid] = { mainIndex: mainIndex++, points: [], rect: null }; } const subIndex = groups[gid].points.length + 1; mark._displayLabel = `#${groups[gid].mainIndex}-${subIndex}`; mark._mainIndex = groups[gid].mainIndex; groups[gid].points.push(mark); pointList.push(mark); } else if (mark.type === 'rect' && gid !== undefined && mark.pselect === '车位') { if (groups[gid]) { mark._displayLabel = `#${groups[gid].mainIndex}-${groups[gid].points.length + 1}`; mark._mainIndex = groups[gid].mainIndex; groups[gid].rect = mark; } } else { mark._displayLabel = `#${mainIndex}`; mark._mainIndex = mainIndex++; } }); // 2. 执行业务规则校验 // 规则1:一个完整库位,一定是1.2.3.4点+车位整体框,且车位整块框一定是完整库位 for (const gid in groups) { const group = groups[gid]; if (group.points.length !== 4) { errors.push({ type: '结构错误', label: `#${group.mainIndex}`, name: '车位组', markId: (group.rect || group.points[0]).markId, msg: `应含4个角点,实际包含 ${group.points.length} 个` }); } if (!group.rect) { errors.push({ type: '结构错误', label: `#${group.mainIndex}`, name: '车位组', markId: group.points[0].markId, msg: '缺少车位整体框' }); } else { const category = group.rect.attrs && group.rect.attrs.category && group.rect.attrs.category[0]; if (category !== 'lots') { errors.push({ type: '属性错误', label: `#${group.mainIndex}`, name: '车位框', markId: group.rect.markId, msg: `车位整块框必须是完整库位(lots),当前为 ${category || '空'}` }); } } } // 规则2:所有车位线属性中,车位线的宽度一定是8-12像素 marks.forEach(mark => { if (mark.type === 'rect' && mark.pselect === '车位线') { const p = mark.point || {}; const w = Math.abs((p.right || 0) - (p.left || 0)); const h = Math.abs((p.bottom || 0) - (p.top || 0)); const minSide = Math.min(w, h); if (minSide < 8 || minSide > 12) { errors.push({ type: '属性错误', label: mark._displayLabel, name: '车位线', markId: mark.markId, msg: `车位线宽度应为8-12px,当前为 ${minSide.toFixed(1)}px` }); } } }); // 规则3:车位整块框和1.2点的距离不会太远 (设定阈值为100像素) for (const gid in groups) { const group = groups[gid]; if (group.rect && group.points.length >= 2) { const rectP = group.rect.point || {}; const rectCx = ((rectP.left || 0) + (rectP.right || 0)) / 2; const rectCy = ((rectP.top || 0) + (rectP.bottom || 0)) / 2; group.points.slice(0, 2).forEach(p => { const pt = p.point || {}; const dist = Math.sqrt(Math.pow(rectCx - parseFloat(pt.x || 0), 2) + Math.pow(rectCy - parseFloat(pt.y || 0), 2)); if (dist > 100) { errors.push({ type: '逻辑错误', label: p._displayLabel, name: '角点', markId: p.markId, msg: `角点距离车位框中心过远 (${dist.toFixed(1)}px)` }); } }); } } // 规则4:车位内1和2点任意一点属性为不可见时,车位整块框的属性一定会选择不可辨别 for (const gid in groups) { const group = groups[gid]; if (group.rect && group.points.length >= 2) { const p1Vis = group.points[0].attrs && group.points[0].attrs.visibility && group.points[0].attrs.visibility[0]; const p2Vis = group.points[1].attrs && group.points[1].attrs.visibility && group.points[1].attrs.visibility[0]; if (p1Vis === 'invisible_p' || p2Vis === 'invisible_p') { const status = group.rect.attrs && group.rect.attrs.status && group.rect.attrs.status[0]; if (status !== 'unrecognizable') { errors.push({ type: '逻辑错误', label: `#${group.mainIndex}`, name: '车位框', markId: group.rect.markId, msg: '1或2点不可见时,车位框属性必须为不可辨别' }); } } } } // 规则5:点的属性中,不可见和其他一定是对应的 pointList.forEach(p => { const vis = p.attrs && p.attrs.visibility && p.attrs.visibility[0]; const pType = p.attrs && p.attrs.points_type && p.attrs.points_type[0]; if (vis === 'invisible_p' && pType !== 'other') { errors.push({ type: '属性错误', label: p._displayLabel, name: '角点', markId: p.markId, msg: '点属性不可见时,点类型必须为其他(other)' }); } if (vis === 'visible_p' && pType === 'other') { errors.push({ type: '属性错误', label: p._displayLabel, name: '角点', markId: p.markId, msg: '点属性可见时,点类型不能为其他(other)' }); } }); // 规则6:砖线一定对应颜色是其他 marks.forEach(mark => { if (mark.type === 'rect' && mark.pselect === '车位线') { const category = mark.attrs && mark.attrs.category && mark.attrs.category[0]; const color = mark.attrs && mark.attrs.color && mark.attrs.color[0]; if (category === 'brick_line' && color !== 'other') { errors.push({ type: '属性错误', label: mark._displayLabel, name: '车位线', markId: mark.markId, msg: `砖线(brick_line)颜色必须为其他(other),当前为 ${color || '空'}` }); } } }); renderReport(errors); } function initDrag(panel) { const header = panel.querySelector('#val-header'); let isDragging = false; let startX, startY, initialLeft, initialTop; header.addEventListener('mousedown', (e) => { if (e.target.classList.contains('val-btn')) return; isDragging = true; startX = e.clientX; startY = e.clientY; const rect = panel.getBoundingClientRect(); initialLeft = rect.left; initialTop = rect.top; panel.style.transition = 'none'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; panel.style.left = `${initialLeft + dx}px`; panel.style.top = `${initialTop + dy}px`; panel.style.right = 'auto'; }); document.addEventListener('mouseup', () => { isDragging = false; panel.style.transition = ''; }); } function renderReport(errors) { if (!document.getElementById('validate-css')) { const style = document.createElement('style'); style.id = 'validate-css'; style.textContent = ` #val-panel { position: fixed; top: 10px; right: 10px; width: 450px; background: rgba(17, 24, 39, 0.95); color: #fff; border-radius: 8px; z-index: 99999; font-family: sans-serif; box-shadow: 0 4px 20px rgba(0,0,0,0.6); border: 1px solid #333; display: flex; flex-direction: column; max-height: 90vh; } #val-header { padding: 12px 15px; background: #1f2937; border-bottom: 1px solid #333; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; } #val-header h3 { margin: 0; font-size: 15px; color: #60a5fa; } .val-btn { background: transparent; border: 1px solid #4b5563; color: #9ca3af; padding: 2px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; margin-left: 5px; } .val-btn:hover { background: #374151; color: #fff; } #val-stats { display: flex; padding: 10px 15px; background: #111827; border-bottom: 1px solid #333; font-size: 12px; } .stat-item { margin-right: 15px; color: #9ca3af; } .stat-num { font-weight: bold; margin-right: 4px; } .stat-struct .stat-num { color: #f87171; } .stat-attr .stat-num { color: #fbbf24; } .stat-logic .stat-num { color: #a78bfa; } #val-body { padding: 10px 15px; overflow-y: auto; max-height: 60vh; } .err-item { padding: 8px; margin-bottom: 6px; background: #1f2937; border-radius: 4px; font-size: 12px; border-left: 3px solid #ef4444; cursor: pointer; transition: 0.2s; display: flex; align-items: center; } .err-item:hover { background: #374151; } .err-item.attr-err { border-left-color: #f59e0b; } .err-item.logic-err { border-left-color: #8b5cf6; } .err-type { font-weight: bold; margin-right: 6px; flex-shrink: 0; } .struct-err .err-type { color: #f87171; } .attr-err .err-type { color: #fbbf24; } .logic-err .err-type { color: #a78bfa; } .err-name { color: #60a5fa; margin-right: 6px; flex-shrink: 0; } .err-label { color: #34d399; font-weight: bold; margin-right: 8px; background: rgba(52,211,153,0.1); padding: 1px 4px; border-radius: 2px; flex-shrink: 0; } .err-msg { color: #d1d5db; } .val-success { text-align: center; color: #34d399; padding: 30px 0; font-size: 16px; font-weight: bold; } `; document.head.appendChild(style); } let panel = document.getElementById('val-panel'); let isNewPanel = false; if (!panel) { panel = document.createElement('div'); panel.id = 'val-panel'; document.body.appendChild(panel); isNewPanel = true; } const structCount = errors.filter(e => e.type === '结构错误').length; const attrCount = errors.filter(e => e.type === '属性错误').length; const logicCount = errors.filter(e => e.type === '逻辑错误').length; let html = `

🛑 验收报告

结构错误: ${structCount}
属性错误: ${attrCount}
逻辑错误: ${logicCount}
`; if (errors.length === 0) { html += `
✅ 验收通过,未发现异常!
`; } else { errors.forEach(err => { const cls = err.type === '属性错误' ? 'attr-err' : (err.type === '逻辑错误' ? 'logic-err' : 'struct-err'); html += `
[${err.type}] ${err.label} ${err.name} ${err.msg}
`; }); } html += `
`; panel.innerHTML = html; panel.style.display = 'flex'; if (isNewPanel) { initDrag(panel); } panel.querySelectorAll('.err-item').forEach(item => { item.addEventListener('click', () => { const markId = item.getAttribute('data-markid'); locateInSidebar(markId); }); }); } function locateInSidebar(markId) { if (!markId) return; const targetItem = document.querySelector(`li[data-markid="${markId}"]`); if (targetItem) { targetItem.scrollIntoView({ behavior: 'smooth', block: 'center' }); targetItem.style.transition = 'background 0.3s'; targetItem.style.background = 'rgba(239, 68, 68, 0.5)'; setTimeout(() => { targetItem.style.background = ''; }, 3000); } else { alert(`无法定位到侧栏项,请检查页面是否已加载完全。`); } } })();