- data/efficiency-samples/ 23 个多语言样例 + data/manual-review-tool.html 人工审核工具 - tests/measure/measure-plugin-samples.mjs 四语言插件侧测量脚本 - tests/measure/compare-a3.mjs 聚合对比 + results(插件实测/人工基线/comparison) - performance-comparison.md 填实基线对比与结论;README 效果总结由占位改为真实数据
231 lines
28 KiB
HTML
231 lines
28 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN"><head>
|
|
<meta charset="UTF-8">
|
|
<title>人工审核记录工具 · A3 实验</title>
|
|
<style>
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body { font-family: "Segoe UI", "Noto Sans CJK SC", "Microsoft YaHei", sans-serif; background: #1e1e2e; color: #cdd6f4; display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
|
|
header { padding: 10px 16px; background: #181825; border-bottom: 1px solid #313244; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
header h1 { font-size: 15px; color: #89b4fa; margin-right: 8px; }
|
|
header .field { display: flex; align-items: center; gap: 4px; font-size: 13px; }
|
|
header input, header select { background: #313244; color: #cdd6f4; border: 1px solid #45475a; border-radius: 4px; padding: 4px 8px; font-size: 13px; }
|
|
#totalTimer { font-family: Consolas, monospace; font-size: 16px; color: #a6e3a1; margin-left: auto; }
|
|
.btn { cursor: pointer; border: none; border-radius: 4px; padding: 6px 14px; font-size: 13px; background: #45475a; color: #cdd6f4; }
|
|
.btn:hover { background: #585b70; }
|
|
.btn.primary { background: #89b4fa; color: #1e1e2e; font-weight: 600; }
|
|
.btn.green { background: #a6e3a1; color: #1e1e2e; font-weight: 600; }
|
|
.btn.red { background: #f38ba8; color: #1e1e2e; }
|
|
main { display: flex; flex: 1; min-height: 0; }
|
|
#sidebar { width: 240px; background: #181825; border-right: 1px solid #313244; overflow-y: auto; padding: 8px; }
|
|
#sidebar .group-title { font-size: 12px; color: #6c7086; text-transform: uppercase; letter-spacing: 1px; margin: 10px 4px 4px; }
|
|
.file-item { padding: 7px 10px; border-radius: 6px; cursor: pointer; font-size: 13px; display: flex; align-items: center; gap: 6px; margin-bottom: 2px; }
|
|
.file-item:hover { background: #313244; }
|
|
.file-item.active { background: #45475a; }
|
|
.file-item .dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
|
|
.dot.todo { background: #6c7086; }
|
|
.dot.doing { background: #f9e2af; box-shadow: 0 0 6px #f9e2af; }
|
|
.dot.done { background: #a6e3a1; }
|
|
.file-item .cnt { margin-left: auto; font-size: 11px; color: #a6adc8; background: #313244; border-radius: 8px; padding: 1px 6px; }
|
|
#content { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
#codePanel { flex: 1; overflow: auto; background: #11111b; margin: 8px; border-radius: 8px; border: 1px solid #313244; padding: 10px 0; font-family: Consolas, "Courier New", monospace; font-size: 13px; line-height: 1.55; }
|
|
.code-line { display: flex; white-space: pre; }
|
|
.code-line:hover { background: #181825; }
|
|
.code-line .ln { flex: none; width: 48px; text-align: right; padding-right: 12px; color: #585b70; user-select: none; cursor: pointer; }
|
|
.code-line .ln:hover { color: #89b4fa; text-decoration: underline; }
|
|
.code-line .ln.marked { color: #f38ba8; font-weight: 700; }
|
|
.code-line .code { color: #cdd6f4; }
|
|
#issueBar { background: #181825; border-top: 1px solid #313244; padding: 10px 16px; }
|
|
#issueBar .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
#issueBar label { font-size: 12px; color: #a6adc8; }
|
|
#issueBar input, #issueBar select { background: #313244; color: #cdd6f4; border: 1px solid #45475a; border-radius: 4px; padding: 5px 8px; font-size: 13px; }
|
|
#noteInput { flex: 1; min-width: 240px; }
|
|
#issueList { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px; }
|
|
.issue-chip { font-size: 12px; background: #313244; border: 1px solid #45475a; border-radius: 12px; padding: 3px 10px; display: inline-flex; gap: 6px; align-items: center; }
|
|
.issue-chip .sev { font-weight: 700; }
|
|
.sev.error { color: #f38ba8; } .sev.warning { color: #f9e2af; } .sev.info { color: #89b4fa; }
|
|
.issue-chip .del { cursor: pointer; color: #6c7086; } .issue-chip .del:hover { color: #f38ba8; }
|
|
#fileTimer { font-family: Consolas, monospace; color: #f9e2af; font-size: 14px; }
|
|
.status { font-size: 12px; color: #a6adc8; }
|
|
#toast { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: #a6e3a1; color: #1e1e2e; padding: 8px 20px; border-radius: 6px; font-size: 13px; display: none; z-index: 99; }
|
|
.hint { font-size: 11px; color: #6c7086; margin-top: 6px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>人工审核记录 · A3</h1>
|
|
<div class="field">受试者 <input id="reviewer" placeholder="编号/昵称" style="width:110px"></div>
|
|
<div class="field">经验 <select id="level"><option value="junior">初级 (<2年)</option><option value="mid">中级 (2-5年)</option><option value="senior">高级 (>5年)</option></select></div>
|
|
<button class="btn green" id="exportBtn">导出 JSON</button>
|
|
<button class="btn red" id="resetBtn">清空全部数据</button>
|
|
<div id="totalTimer">00:00:00</div>
|
|
</header>
|
|
<main>
|
|
<div id="sidebar"></div>
|
|
<div id="content">
|
|
<div id="codePanel"></div>
|
|
<div id="issueBar">
|
|
<div class="row">
|
|
<button class="btn primary" id="startBtn">▶ 开始审查此文件</button>
|
|
<button class="btn" id="finishBtn" disabled="">⏹ 完成</button>
|
|
<span id="fileTimer">本文件 00:00</span>
|
|
<span class="status" id="fileStatus"></span>
|
|
</div>
|
|
<div class="row" style="margin-top:8px">
|
|
<label>行号 <input id="lineInput" type="number" min="1" style="width:70px"></label>
|
|
<label>严重度 <select id="sevInput"><option value="error">error</option><option value="warning" selected="">warning</option><option value="info">info</option></select></label>
|
|
<label>问题描述 <input id="noteInput" placeholder="例:使用了 == 应为 ==="></label>
|
|
<button class="btn" id="addIssueBtn">+ 记录问题</button>
|
|
</div>
|
|
<div id="issueList"></div>
|
|
<div class="hint">提示:点击代码左侧行号可快速填入行号。先点"开始审查"再读代码,读完点"完成"停止计时。中途休息请先点"完成"。</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
<div id="toast"></div>
|
|
<script>
|
|
const DATA = {"generatedAt":"2026-08-22T04:39:38.089Z","files":[{"rel":"data/efficiency-samples/js/sample-01.js","lang":"JavaScript","languageId":"javascript","code":"// sample-1: order service module\nvar config = { retries: 3 };\n\nfunction orderProcess(items) {\n var total = 0;\n for (var i = 0; i < items.length; i++) {\n total += items[i].amount;\n }\n if (total == 0) {\n return null;\n }\n return total;\n}\n\nfunction orderValidate(record) {\n var shadowed = record.id;\n function inner() {\n var shadowed = record.alt;\n return shadowed;\n }\n if (record.value === record.value) {\n inner();\n }\n return shadowed;\n}\n\nfunction orderUnsafe(input) {\n var unused = computeLegacy(input);\n eval(input.expression);\n debugger;\n}\n\nfunction computeLegacy(x) {\n if (x.pending) {}\n x.count = x.count;\n return x;\n}\n\nmodule.exports = { orderProcess, orderValidate, orderUnsafe, config };\n"},{"rel":"data/efficiency-samples/js/sample-02.js","lang":"JavaScript","languageId":"javascript","code":"// sample-2: inventory service module\nvar config = { retries: 3 };\n\nfunction inventoryProcess(items) {\n var total = 0;\n for (var i = 0; i < items.length; i++) {\n total += items[i].amount;\n }\n if (total == 0) {\n return null;\n }\n return total;\n}\n\nfunction inventoryValidate(record) {\n var shadowed = record.id;\n function inner() {\n var shadowed = record.alt;\n return shadowed;\n }\n if (record.value === record.value) {\n inner();\n }\n return shadowed;\n}\n\nfunction inventoryUnsafe(input) {\n var unused = computeLegacy(input);\n eval(input.expression);\n debugger;\n}\n\nfunction computeLegacy(x) {\n if (x.pending) {}\n x.count = x.count;\n return x;\n}\n\nmodule.exports = { inventoryProcess, inventoryValidate, inventoryUnsafe, config };\n"},{"rel":"data/efficiency-samples/js/sample-03.js","lang":"JavaScript","languageId":"javascript","code":"// sample-3: billing service module\nvar config = { retries: 3 };\n\nfunction billingProcess(items) {\n var total = 0;\n for (var i = 0; i < items.length; i++) {\n total += items[i].amount;\n }\n if (total == 0) {\n return null;\n }\n return total;\n}\n\nfunction billingValidate(record) {\n var shadowed = record.id;\n function inner() {\n var shadowed = record.alt;\n return shadowed;\n }\n if (record.value === record.value) {\n inner();\n }\n return shadowed;\n}\n\nfunction billingUnsafe(input) {\n var unused = computeLegacy(input);\n eval(input.expression);\n debugger;\n}\n\nfunction computeLegacy(x) {\n if (x.pending) {}\n x.count = x.count;\n return x;\n}\n\nmodule.exports = { billingProcess, billingValidate, billingUnsafe, config };\n"},{"rel":"data/efficiency-samples/js/sample-04.js","lang":"JavaScript","languageId":"javascript","code":"// sample-4: customer service module\nvar config = { retries: 3 };\n\nfunction customerProcess(items) {\n var total = 0;\n for (var i = 0; i < items.length; i++) {\n total += items[i].amount;\n }\n if (total == 0) {\n return null;\n }\n return total;\n}\n\nfunction customerValidate(record) {\n var shadowed = record.id;\n function inner() {\n var shadowed = record.alt;\n return shadowed;\n }\n if (record.value === record.value) {\n inner();\n }\n return shadowed;\n}\n\nfunction customerUnsafe(input) {\n var unused = computeLegacy(input);\n eval(input.expression);\n debugger;\n}\n\nfunction computeLegacy(x) {\n if (x.pending) {}\n x.count = x.count;\n return x;\n}\n\nmodule.exports = { customerProcess, customerValidate, customerUnsafe, config };\n"},{"rel":"data/efficiency-samples/js/sample-05.js","lang":"JavaScript","languageId":"javascript","code":"// sample-5: shipping service module\nvar config = { retries: 3 };\n\nfunction shippingProcess(items) {\n var total = 0;\n for (var i = 0; i < items.length; i++) {\n total += items[i].amount;\n }\n if (total == 0) {\n return null;\n }\n return total;\n}\n\nfunction shippingValidate(record) {\n var shadowed = record.id;\n function inner() {\n var shadowed = record.alt;\n return shadowed;\n }\n if (record.value === record.value) {\n inner();\n }\n return shadowed;\n}\n\nfunction shippingUnsafe(input) {\n var unused = computeLegacy(input);\n eval(input.expression);\n debugger;\n}\n\nfunction computeLegacy(x) {\n if (x.pending) {}\n x.count = x.count;\n return x;\n}\n\nmodule.exports = { shippingProcess, shippingValidate, shippingUnsafe, config };\n"},{"rel":"data/efficiency-samples/js/sample-06.js","lang":"JavaScript","languageId":"javascript","code":"// sample-6: coupon service module\nvar config = { retries: 3 };\n\nfunction couponProcess(items) {\n var total = 0;\n for (var i = 0; i < items.length; i++) {\n total += items[i].amount;\n }\n if (total == 0) {\n return null;\n }\n return total;\n}\n\nfunction couponValidate(record) {\n var shadowed = record.id;\n function inner() {\n var shadowed = record.alt;\n return shadowed;\n }\n if (record.value === record.value) {\n inner();\n }\n return shadowed;\n}\n\nfunction couponUnsafe(input) {\n var unused = computeLegacy(input);\n eval(input.expression);\n debugger;\n}\n\nfunction computeLegacy(x) {\n if (x.pending) {}\n x.count = x.count;\n return x;\n}\n\nmodule.exports = { couponProcess, couponValidate, couponUnsafe, config };\n"},{"rel":"data/efficiency-samples/js/sample-07.js","lang":"JavaScript","languageId":"javascript","code":"// sample-7: refund service module\nvar config = { retries: 3 };\n\nfunction refundProcess(items) {\n var total = 0;\n for (var i = 0; i < items.length; i++) {\n total += items[i].amount;\n }\n if (total == 0) {\n return null;\n }\n return total;\n}\n\nfunction refundValidate(record) {\n var shadowed = record.id;\n function inner() {\n var shadowed = record.alt;\n return shadowed;\n }\n if (record.value === record.value) {\n inner();\n }\n return shadowed;\n}\n\nfunction refundUnsafe(input) {\n var unused = computeLegacy(input);\n eval(input.expression);\n debugger;\n}\n\nfunction computeLegacy(x) {\n if (x.pending) {}\n x.count = x.count;\n return x;\n}\n\nmodule.exports = { refundProcess, refundValidate, refundUnsafe, config };\n"},{"rel":"data/efficiency-samples/js/sample-08.js","lang":"JavaScript","languageId":"javascript","code":"// sample-8: invoice service module\nvar config = { retries: 3 };\n\nfunction invoiceProcess(items) {\n var total = 0;\n for (var i = 0; i < items.length; i++) {\n total += items[i].amount;\n }\n if (total == 0) {\n return null;\n }\n return total;\n}\n\nfunction invoiceValidate(record) {\n var shadowed = record.id;\n function inner() {\n var shadowed = record.alt;\n return shadowed;\n }\n if (record.value === record.value) {\n inner();\n }\n return shadowed;\n}\n\nfunction invoiceUnsafe(input) {\n var unused = computeLegacy(input);\n eval(input.expression);\n debugger;\n}\n\nfunction computeLegacy(x) {\n if (x.pending) {}\n x.count = x.count;\n return x;\n}\n\nmodule.exports = { invoiceProcess, invoiceValidate, invoiceUnsafe, config };\n"},{"rel":"data/efficiency-samples/java/InventoryService.java","lang":"Java","languageId":"java","code":"public class InventoryService {\n\n private static final String DEFAULT_USER = \"guest\";\n\n public void register(String userName) {\n String password = \"admin123\";\n System.out.println(\"register user: \" + userName);\n try {\n persist(userName);\n } catch (Exception e) {\n }\n int leftover = computeChecksum(userName);\n audit(DEFAULT_USER);\n }\n\n private int computeChecksum(String value) {\n String copy = value.toString();\n return copy.length();\n }\n\n private void persist(String userName) {\n // placeholder persistence\n }\n\n private void audit(String actor) {\n System.out.println(\"audit by \" + actor);\n }\n}\n"},{"rel":"data/efficiency-samples/java/NotifyService.java","lang":"Java","languageId":"java","code":"public class NotifyService {\n\n private static final String DEFAULT_USER = \"guest\";\n\n public void register(String userName) {\n String password = \"admin123\";\n System.out.println(\"register user: \" + userName);\n try {\n persist(userName);\n } catch (Exception e) {\n }\n int leftover = computeChecksum(userName);\n audit(DEFAULT_USER);\n }\n\n private int computeChecksum(String value) {\n String copy = value.toString();\n return copy.length();\n }\n\n private void persist(String userName) {\n // placeholder persistence\n }\n\n private void audit(String actor) {\n System.out.println(\"audit by \" + actor);\n }\n}\n"},{"rel":"data/efficiency-samples/java/OrderService.java","lang":"Java","languageId":"java","code":"public class OrderService {\n\n private static final String DEFAULT_USER = \"guest\";\n\n public void register(String userName) {\n String password = \"admin123\";\n System.out.println(\"register user: \" + userName);\n try {\n persist(userName);\n } catch (Exception e) {\n }\n int leftover = computeChecksum(userName);\n audit(DEFAULT_USER);\n }\n\n private int computeChecksum(String value) {\n String copy = value.toString();\n return copy.length();\n }\n\n private void persist(String userName) {\n // placeholder persistence\n }\n\n private void audit(String actor) {\n System.out.println(\"audit by \" + actor);\n }\n}\n"},{"rel":"data/efficiency-samples/java/PaymentService.java","lang":"Java","languageId":"java","code":"public class PaymentService {\n\n private static final String DEFAULT_USER = \"guest\";\n\n public void register(String userName) {\n String password = \"admin123\";\n System.out.println(\"register user: \" + userName);\n try {\n persist(userName);\n } catch (Exception e) {\n }\n int leftover = computeChecksum(userName);\n audit(DEFAULT_USER);\n }\n\n private int computeChecksum(String value) {\n String copy = value.toString();\n return copy.length();\n }\n\n private void persist(String userName) {\n // placeholder persistence\n }\n\n private void audit(String actor) {\n System.out.println(\"audit by \" + actor);\n }\n}\n"},{"rel":"data/efficiency-samples/java/ReportService.java","lang":"Java","languageId":"java","code":"public class ReportService {\n\n private static final String DEFAULT_USER = \"guest\";\n\n public void register(String userName) {\n String password = \"admin123\";\n System.out.println(\"register user: \" + userName);\n try {\n persist(userName);\n } catch (Exception e) {\n }\n int leftover = computeChecksum(userName);\n audit(DEFAULT_USER);\n }\n\n private int computeChecksum(String value) {\n String copy = value.toString();\n return copy.length();\n }\n\n private void persist(String userName) {\n // placeholder persistence\n }\n\n private void audit(String actor) {\n System.out.println(\"audit by \" + actor);\n }\n}\n"},{"rel":"data/efficiency-samples/java/UserService.java","lang":"Java","languageId":"java","code":"public class UserService {\n\n private static final String DEFAULT_USER = \"guest\";\n\n public void register(String userName) {\n String password = \"admin123\";\n System.out.println(\"register user: \" + userName);\n try {\n persist(userName);\n } catch (Exception e) {\n }\n int leftover = computeChecksum(userName);\n audit(DEFAULT_USER);\n }\n\n private int computeChecksum(String value) {\n String copy = value.toString();\n return copy.length();\n }\n\n private void persist(String userName) {\n // placeholder persistence\n }\n\n private void audit(String actor) {\n System.out.println(\"audit by \" + actor);\n }\n}\n"},{"rel":"data/efficiency-samples/css/sample-01.css","lang":"CSS","languageId":"css","code":"/* header panel styles */\n.header-card {\n color: #FFFFFF;\n background: #FFF1A2B;\n margin: 0px;\n padding: 12px 12px;\n -webkit-border-radius: 6px;\n border: 1px solid #dddddd;\n}\n\n.header-title {\n font-size: 16px;\n font-weight: 700;\n color: #333333;\n color: #444444;\n}\n\n.header-actions {\n display: flex;\n gap: 8px;\n -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.10);\n}\n"},{"rel":"data/efficiency-samples/css/sample-02.css","lang":"CSS","languageId":"css","code":"/* sidebar panel styles */\n.sidebar-card {\n color: #FFFFFF;\n background: #FFF1A2B;\n margin: 0px;\n padding: 12px 12px;\n -webkit-border-radius: 6px;\n border: 1px solid #dddddd;\n}\n\n.sidebar-title {\n font-size: 16px;\n font-weight: 700;\n color: #333333;\n color: #444444;\n}\n\n.sidebar-actions {\n display: flex;\n gap: 8px;\n -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.10);\n}\n"},{"rel":"data/efficiency-samples/css/sample-03.css","lang":"CSS","languageId":"css","code":"/* footer panel styles */\n.footer-card {\n color: #FFFFFF;\n background: #FFF1A2B;\n margin: 0px;\n padding: 12px 12px;\n -webkit-border-radius: 6px;\n border: 1px solid #dddddd;\n}\n\n.footer-title {\n font-size: 16px;\n font-weight: 700;\n color: #333333;\n color: #444444;\n}\n\n.footer-actions {\n display: flex;\n gap: 8px;\n -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.10);\n}\n"},{"rel":"data/efficiency-samples/css/sample-04.css","lang":"CSS","languageId":"css","code":"/* modal panel styles */\n.modal-card {\n color: #FFFFFF;\n background: #FFF1A2B;\n margin: 0px;\n padding: 12px 12px;\n -webkit-border-radius: 6px;\n border: 1px solid #dddddd;\n}\n\n.modal-title {\n font-size: 16px;\n font-weight: 700;\n color: #333333;\n color: #444444;\n}\n\n.modal-actions {\n display: flex;\n gap: 8px;\n -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.10);\n}\n"},{"rel":"data/efficiency-samples/css/sample-05.css","lang":"CSS","languageId":"css","code":"/* toast panel styles */\n.toast-card {\n color: #FFFFFF;\n background: #FFF1A2B;\n margin: 0px;\n padding: 12px 12px;\n -webkit-border-radius: 6px;\n border: 1px solid #dddddd;\n}\n\n.toast-title {\n font-size: 16px;\n font-weight: 700;\n color: #333333;\n color: #444444;\n}\n\n.toast-actions {\n display: flex;\n gap: 8px;\n -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.10);\n}\n"},{"rel":"data/efficiency-samples/sql/sample-01.sql","lang":"SQL","languageId":"sql","code":"-- daily-orders report query\nselect u.user_id,u.user_name,o.order_id,o.amount\nfrom t_user u\njoin t_order o on u.user_id = o.user_id\nwhere u.status = 'active'\n and o.created_at > sysdate - 30\n and o.amount > 100\nORDER BY o.amount desc\n"},{"rel":"data/efficiency-samples/sql/sample-02.sql","lang":"SQL","languageId":"sql","code":"-- monthly-revenue summary query\nSELECT d.dept_name, COUNT(*) AS total\nFROM t_order o\nJOIN t_user u ON o.user_id = u.user_id\nJOIN t_dept d ON u.dept_id = d.dept_id\nWHERE o.status = 'paid'\nGROUP BY d.dept_name\nhaving total > 5\n"},{"rel":"data/efficiency-samples/sql/sample-03.sql","lang":"SQL","languageId":"sql","code":"-- active-users report query\nselect u.user_id,u.user_name,o.order_id,o.amount\nfrom t_user u\njoin t_order o on u.user_id = o.user_id\nwhere u.status = 'active'\n and o.created_at > sysdate - 30\n and o.amount > 100\nORDER BY o.amount desc\n"},{"rel":"data/efficiency-samples/sql/sample-04.sql","lang":"SQL","languageId":"sql","code":"-- top-departments summary query\nSELECT d.dept_name, COUNT(*) AS total\nFROM t_order o\nJOIN t_user u ON o.user_id = u.user_id\nJOIN t_dept d ON u.dept_id = d.dept_id\nWHERE o.status = 'paid'\nGROUP BY d.dept_name\nhaving total > 5\n"}]};
|
|
const LS_KEY = 'a3-manual-review-v1';
|
|
const state = load() || { reviewer: '', level: 'mid', files: {} };
|
|
// state.files[rel] = { elapsedMs, startedAt, done, issues: [{line, severity, note, at}] }
|
|
const $ = (id) => document.getElementById(id);
|
|
let current = DATA.files[0]?.rel ?? null;
|
|
let tickHandle = setInterval(renderTimers, 1000);
|
|
|
|
function load() { try { return JSON.parse(localStorage.getItem(LS_KEY)); } catch { return null; } }
|
|
function save() { localStorage.setItem(LS_KEY, JSON.stringify(state)); }
|
|
function fstate(rel) { return state.files[rel] ??= { elapsedMs: 0, startedAt: null, done: false, issues: [] }; }
|
|
function fmtClock(ms) {
|
|
const s = Math.floor(ms / 1000);
|
|
const h = String(Math.floor(s / 3600)).padStart(2, '0');
|
|
return h + ':' + String(Math.floor((s % 3600) / 60)).padStart(2, '0') + ':' + String(s % 60).padStart(2, '0');
|
|
}
|
|
function fmtShort(ms) {
|
|
const s = Math.floor(ms / 1000);
|
|
return String(Math.floor(s / 60)).padStart(2, '0') + ':' + String(s % 60).padStart(2, '0');
|
|
}
|
|
function totalElapsed() {
|
|
let t = 0;
|
|
for (const f of Object.values(state.files)) { t += f.elapsedMs + (f.startedAt ? Date.now() - f.startedAt : 0); }
|
|
return t;
|
|
}
|
|
function esc(s) { return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
|
|
function renderSidebar() {
|
|
const el = $('sidebar'); el.innerHTML = '';
|
|
const groups = {};
|
|
for (const f of DATA.files) { (groups[f.lang] ??= []).push(f); }
|
|
for (const [lang, fs] of Object.entries(groups)) {
|
|
const t = document.createElement('div'); t.className = 'group-title'; t.textContent = lang; el.appendChild(t);
|
|
for (const f of fs) {
|
|
const st = fstate(f.rel);
|
|
const item = document.createElement('div');
|
|
item.className = 'file-item' + (f.rel === current ? ' active' : '');
|
|
const dotCls = st.done ? 'done' : (st.startedAt ? 'doing' : (st.elapsedMs > 0 || st.issues.length ? 'doing' : 'todo'));
|
|
item.innerHTML = '<span class="dot ' + dotCls + '"></span><span>' + f.rel.split('/').pop() + '</span>' +
|
|
(st.issues.length ? '<span class="cnt">' + st.issues.length + ' 题目</span>' : '');
|
|
item.onclick = () => switchFile(f.rel);
|
|
el.appendChild(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
function renderCode() {
|
|
const f = DATA.files.find((x) => x.rel === current);
|
|
const panel = $('codePanel'); panel.innerHTML = '';
|
|
if (!f) { panel.textContent = '(无文件)'; return; }
|
|
const st = fstate(f.rel);
|
|
const markedLines = new Set(st.issues.map((i) => i.line));
|
|
const lines = f.code.split('\n');
|
|
lines.forEach((text, i) => {
|
|
const row = document.createElement('div'); row.className = 'code-line';
|
|
const ln = document.createElement('span');
|
|
ln.className = 'ln' + (markedLines.has(i + 1) ? ' marked' : '');
|
|
ln.textContent = String(i + 1);
|
|
ln.onclick = () => { $('lineInput').value = i + 1; $('noteInput').focus(); };
|
|
const code = document.createElement('span'); code.className = 'code'; code.textContent = text || ' ';
|
|
row.appendChild(ln); row.appendChild(code); panel.appendChild(row);
|
|
});
|
|
panel.scrollTop = 0;
|
|
}
|
|
|
|
function renderIssueBar() {
|
|
const st = fstate(current);
|
|
$('startBtn').disabled = !!st.startedAt;
|
|
$('finishBtn').disabled = !st.startedAt;
|
|
$('fileStatus').textContent = st.done ? '已完成' : (st.startedAt ? '审查中…' : '未开始');
|
|
const list = $('issueList'); list.innerHTML = '';
|
|
for (let idx = 0; idx < st.issues.length; idx++) {
|
|
const it = st.issues[idx];
|
|
const chip = document.createElement('span'); chip.className = 'issue-chip';
|
|
chip.innerHTML = '<span class="sev ' + it.severity + '">' + it.severity + '</span> L' + it.line +
|
|
' <span>' + esc(it.note || '(未描述)') + '</span> <span class="del" title="删除">✕</span>';
|
|
chip.querySelector('.del').onclick = () => { st.issues.splice(idx, 1); save(); renderAll(); };
|
|
list.appendChild(chip);
|
|
}
|
|
}
|
|
|
|
function renderTimers() {
|
|
$('totalTimer').textContent = fmtClock(totalElapsed());
|
|
const st = current && fstate(current);
|
|
if (st) { $('fileTimer').textContent = '本文件 ' + fmtShort(st.elapsedMs + (st.startedAt ? Date.now() - st.startedAt : 0)); }
|
|
}
|
|
|
|
function renderAll() { renderSidebar(); renderCode(); renderIssueBar(); renderTimers(); }
|
|
|
|
function switchFile(rel) {
|
|
const prev = fstate(current);
|
|
if (prev.startedAt) { prev.elapsedMs += Date.now() - prev.startedAt; prev.startedAt = null; } // 切换视为暂停
|
|
current = rel; save(); renderAll();
|
|
}
|
|
|
|
$('startBtn').onclick = () => { const st = fstate(current); if (!st.startedAt) { st.startedAt = Date.now(); st.done = false; save(); renderAll(); } };
|
|
$('finishBtn').onclick = () => {
|
|
const st = fstate(current);
|
|
if (st.startedAt) { st.elapsedMs += Date.now() - st.startedAt; st.startedAt = null; }
|
|
st.done = true; save(); renderAll();
|
|
};
|
|
$('addIssueBtn').onclick = () => {
|
|
const line = parseInt($('lineInput').value, 10);
|
|
const note = $('noteInput').value.trim();
|
|
if (!line || line < 1) { toast('请填写行号(可点击代码行号)'); return; }
|
|
if (!note) { toast('请填写问题描述'); return; }
|
|
const st = fstate(current);
|
|
st.issues.push({ line, severity: $('sevInput').value, note, at: new Date().toISOString() });
|
|
$('lineInput').value = ''; $('noteInput').value = '';
|
|
save(); renderAll(); toast('已记录 L' + line);
|
|
};
|
|
$('reviewer').oninput = (e) => { state.reviewer = e.target.value; save(); };
|
|
$('level').onchange = (e) => { state.level = e.target.value; save(); };
|
|
|
|
$('exportBtn').onclick = () => {
|
|
state.reviewer = $('reviewer').value; state.level = $('level').value;
|
|
for (const st of Object.values(state.files)) {
|
|
if (st.startedAt) { st.elapsedMs += Date.now() - st.startedAt; st.startedAt = null; }
|
|
}
|
|
save();
|
|
const out = { experiment: 'A3 人工审核 vs 插件审核', exportedAt: new Date().toISOString(), reviewer: state.reviewer, level: state.level, totalElapsedMs: totalElapsed(), files: state.files };
|
|
const blob = new Blob([JSON.stringify(out, null, 2)], { type: 'application/json' });
|
|
const a = document.createElement('a');
|
|
a.href = URL.createObjectURL(blob);
|
|
a.download = 'manual-review-' + (state.reviewer || 'anon') + '.json';
|
|
a.click(); URL.revokeObjectURL(a.href);
|
|
toast('已导出,请交给实验组织者');
|
|
};
|
|
$('resetBtn').onclick = () => {
|
|
if (!confirm('确定清空全部记录?此操作不可恢复。')) return;
|
|
localStorage.removeItem(LS_KEY); location.reload();
|
|
};
|
|
|
|
function toast(msg) {
|
|
const t = $('toast'); t.textContent = msg; t.style.display = 'block';
|
|
setTimeout(() => { t.style.display = 'none'; }, 1600);
|
|
}
|
|
|
|
// 恢复受试者信息
|
|
if (state.reviewer) { $('reviewer').value = state.reviewer; }
|
|
if (state.level) { $('level').value = state.level; }
|
|
renderAll();
|
|
</script>
|
|
|
|
|
|
</body></html> |