Files
2026-07-07 07:27:13 +08:00

946 lines
31 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Issue Hub AI 设计定稿审阅入口</title>
<style>
:root {
color-scheme: light;
--bg: #f4f6f8;
--surface: #ffffff;
--surface-soft: #f8fafc;
--line: #dce3ea;
--line-strong: #c9d4df;
--text: #17202a;
--muted: #657282;
--muted-2: #8a96a3;
--accent: #1769e0;
--accent-soft: #e8f1ff;
--ok: #147d55;
--ok-soft: #e7f7ef;
--warn: #ad5b00;
--warn-soft: #fff3df;
--danger: #b42318;
--shadow: 0 12px 32px rgba(21, 32, 43, 0.08);
font-family: "Microsoft YaHei", "PingFang SC", "Segoe UI", Arial, sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-size: 14px;
line-height: 1.55;
}
button,
input,
textarea {
font: inherit;
}
.shell {
width: min(1760px, calc(100% - 40px));
margin: 0 auto;
padding: 24px 0 48px;
}
.topbar {
position: sticky;
top: 0;
z-index: 20;
margin: -24px calc((40px - 100vw) / 2) 18px;
padding: 16px max(20px, calc((100vw - 1760px) / 2 + 20px));
background: rgba(244, 246, 248, 0.94);
border-bottom: 1px solid rgba(201, 212, 223, 0.75);
backdrop-filter: blur(12px);
}
.topbar-inner {
display: grid;
grid-template-columns: minmax(280px, 1fr) auto;
gap: 18px;
align-items: center;
max-width: 1760px;
margin: 0 auto;
}
h1 {
margin: 0;
font-size: 22px;
line-height: 1.2;
letter-spacing: 0;
}
.subtitle {
margin: 6px 0 0;
color: var(--muted);
font-size: 13px;
}
.summary {
display: grid;
grid-template-columns: repeat(4, minmax(108px, 1fr));
gap: 8px;
min-width: min(640px, 100%);
}
.metric {
padding: 10px 12px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: 0 4px 18px rgba(21, 32, 43, 0.05);
}
.metric span {
display: block;
color: var(--muted);
font-size: 12px;
white-space: nowrap;
}
.metric strong {
display: block;
margin-top: 2px;
font-size: 16px;
line-height: 1.25;
white-space: nowrap;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
padding: 10px 12px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: var(--shadow);
}
.filters {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.filter-btn {
min-height: 34px;
padding: 6px 12px;
color: var(--muted);
background: var(--surface-soft);
border: 1px solid var(--line);
border-radius: 7px;
cursor: pointer;
transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
}
.filter-btn:hover,
.filter-btn.active {
color: var(--accent);
background: var(--accent-soft);
border-color: #9ec5ff;
}
.toolbar-note {
color: var(--muted);
font-size: 13px;
}
.toolbar-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: flex-end;
}
.toolbar-actions button {
min-height: 34px;
padding: 6px 12px;
color: #243244;
background: var(--surface-soft);
border: 1px solid var(--line);
border-radius: 7px;
cursor: pointer;
}
.toolbar-actions button:hover,
.toolbar-actions button:focus-visible {
color: var(--accent);
border-color: #9ec5ff;
}
.page-list {
display: grid;
gap: 14px;
}
.review-card {
overflow: hidden;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: var(--shadow);
}
.review-card.hidden {
display: none;
}
.card-head {
display: grid;
grid-template-columns: minmax(220px, 1fr) auto;
gap: 12px;
align-items: start;
padding: 14px 16px 12px;
border-bottom: 1px solid var(--line);
background: linear-gradient(180deg, #ffffff 0%, #fbfcfe 100%);
}
.title-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
h2 {
margin: 0;
font-size: 18px;
line-height: 1.25;
letter-spacing: 0;
}
.route {
margin-top: 6px;
color: var(--muted);
font-size: 13px;
}
code {
padding: 1px 5px;
background: #eef2f6;
border-radius: 5px;
color: #334155;
font-family: Consolas, "SFMono-Regular", monospace;
font-size: 12px;
}
.badge {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 2px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.badge.review {
color: var(--ok);
background: var(--ok-soft);
border: 1px solid #b7e5cc;
}
.badge.decision {
color: var(--warn);
background: var(--warn-soft);
border: 1px solid #ffd9a8;
}
.badge.candidate {
color: var(--accent);
background: var(--accent-soft);
border: 1px solid #9ec5ff;
}
.score-grid {
display: grid;
grid-template-columns: repeat(3, minmax(72px, auto));
gap: 8px;
}
.score {
min-width: 84px;
padding: 7px 9px;
background: var(--surface-soft);
border: 1px solid var(--line);
border-radius: 7px;
text-align: right;
}
.score span {
display: block;
color: var(--muted-2);
font-size: 11px;
}
.score strong {
font-size: 14px;
}
.image-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1px;
background: var(--line);
}
.shot {
min-width: 0;
background: var(--surface-soft);
}
.shot-label {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
height: 34px;
padding: 0 10px;
color: var(--muted);
background: #f8fafc;
border-bottom: 1px solid var(--line);
font-size: 12px;
font-weight: 700;
}
.shot-label a {
color: var(--accent);
text-decoration: none;
font-weight: 600;
}
.shot-frame {
display: flex;
align-items: flex-start;
justify-content: center;
min-height: 224px;
padding: 8px;
background:
linear-gradient(45deg, #f1f5f9 25%, transparent 25%),
linear-gradient(-45deg, #f1f5f9 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #f1f5f9 75%),
linear-gradient(-45deg, transparent 75%, #f1f5f9 75%);
background-color: #ffffff;
background-size: 18px 18px;
background-position: 0 0, 0 9px, 9px -9px, -9px 0;
}
.shot img {
display: block;
width: 100%;
max-height: 430px;
object-fit: contain;
background: #fff;
border: 1px solid rgba(201, 212, 223, 0.72);
border-radius: 4px;
}
.card-foot {
display: grid;
grid-template-columns: minmax(260px, 1.15fr) minmax(260px, 0.85fr);
gap: 14px;
padding: 12px 16px 14px;
border-top: 1px solid var(--line);
}
.focus-title,
.decision-title {
margin: 0 0 7px;
color: var(--muted);
font-size: 12px;
font-weight: 800;
letter-spacing: 0;
}
.focus-text {
margin: 0;
color: #344054;
font-size: 13px;
}
.key-issue {
margin-top: 8px;
padding: 8px 10px;
color: var(--danger);
background: #fff1f0;
border: 1px solid #ffd0cc;
border-radius: 7px;
font-size: 13px;
}
.decision-box {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.choice {
display: flex;
align-items: center;
gap: 7px;
min-height: 36px;
padding: 7px 9px;
color: #344054;
background: var(--surface-soft);
border: 1px solid var(--line);
border-radius: 7px;
cursor: pointer;
user-select: none;
}
.choice input {
width: 15px;
height: 15px;
margin: 0;
accent-color: var(--accent);
}
.review-note {
width: 100%;
min-height: 74px;
margin-top: 10px;
padding: 9px 10px;
color: #344054;
background: #fff;
border: 1px solid var(--line);
border-radius: 7px;
resize: vertical;
}
.review-status {
margin-top: 7px;
color: var(--muted);
font-size: 12px;
}
.empty {
display: none;
padding: 32px;
color: var(--muted);
text-align: center;
background: var(--surface);
border: 1px dashed var(--line-strong);
border-radius: 8px;
}
@media (max-width: 1180px) {
.topbar-inner,
.card-head,
.card-foot {
grid-template-columns: 1fr;
}
.summary {
min-width: 0;
}
}
@media (max-width: 900px) {
.shell {
width: min(100% - 24px, 1760px);
padding-bottom: 32px;
}
.topbar {
margin-left: calc((24px - 100vw) / 2);
margin-right: calc((24px - 100vw) / 2);
}
.summary,
.score-grid,
.decision-box {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.image-grid {
grid-template-columns: 1fr;
}
.shot img {
max-height: none;
}
}
@media (max-width: 560px) {
.summary,
.score-grid,
.decision-box {
grid-template-columns: 1fr;
}
.toolbar {
align-items: stretch;
}
.filters,
.filter-btn,
.toolbar-actions,
.toolbar-actions button {
width: 100%;
}
.filter-btn {
justify-content: center;
}
}
</style>
</head>
<body>
<header class="topbar">
<div class="topbar-inner">
<div>
<h1>Issue Hub AI 设计定稿审阅入口</h1>
<p class="subtitle">用于人工定稿:并排核对设计图、当前实现截图和视觉 diff,记录页面级结论。</p>
</div>
<section class="summary" aria-label="状态摘要">
<div class="metric"><span>报告时间</span><strong>2026-06-18T04:47:26.995Z</strong></div>
<div class="metric"><span>页面总数</span><strong>12 页</strong></div>
<div class="metric"><span>可人工复核</span><strong>4 页</strong></div>
<div class="metric"><span>定稿候选</span><strong>8 页</strong></div>
<div class="metric"><span>需人工决策</span><strong>0 页</strong></div>
</section>
</div>
</header>
<main class="shell">
<section class="toolbar" aria-label="筛选工具">
<div class="filters" role="group" aria-label="页面筛选">
<button class="filter-btn active" type="button" data-filter="all">全部 <span data-count="all">12</span></button>
<button class="filter-btn" type="button" data-filter="review">可复核 <span data-count="review">4</span></button>
<button class="filter-btn" type="button" data-filter="candidate">定稿候选 <span data-count="candidate">8</span></button>
<button class="filter-btn" type="button" data-filter="decision">需决策 <span data-count="decision">0</span></button>
</div>
<div class="toolbar-actions">
<div class="toolbar-note">审阅结论会保存在当前浏览器,可导出为定稿记录。</div>
<button type="button" id="exportReview">导出定稿记录</button>
<button type="button" id="clearReview">清空记录</button>
</div>
</section>
<section class="page-list" id="pageList" aria-label="页面审阅列表"></section>
<div class="empty" id="emptyState">当前筛选没有可展示页面。</div>
</main>
<template id="cardTemplate">
<article class="review-card">
<div class="card-head">
<div>
<div class="title-row">
<h2></h2>
<span class="badge"></span>
</div>
<div class="route"></div>
</div>
<div class="score-grid">
<div class="score"><span>avg</span><strong data-field="avg"></strong></div>
<div class="score"><span>RMS</span><strong data-field="rms"></strong></div>
<div class="score"><span>变更像素</span><strong data-field="pixels"></strong></div>
</div>
</div>
<div class="image-grid">
<figure class="shot">
<figcaption class="shot-label"><span>设计图</span><a data-link="design" target="_blank" rel="noreferrer">打开</a></figcaption>
<div class="shot-frame"><img data-img="design" alt=""></div>
</figure>
<figure class="shot">
<figcaption class="shot-label"><span>实现截图</span><a data-link="implementation" target="_blank" rel="noreferrer">打开</a></figcaption>
<div class="shot-frame"><img data-img="implementation" alt=""></div>
</figure>
<figure class="shot">
<figcaption class="shot-label"><span>diff 图</span><a data-link="diff" target="_blank" rel="noreferrer">打开</a></figcaption>
<div class="shot-frame"><img data-img="diff" alt=""></div>
</figure>
</div>
<div class="card-foot">
<div>
<p class="focus-title">人工关注点</p>
<p class="focus-text"></p>
<div class="key-issue" hidden></div>
</div>
<div>
<p class="decision-title">审阅结论</p>
<div class="decision-box" role="group" aria-label="审阅结论">
<label class="choice"><input type="radio" value="pass">通过</label>
<label class="choice"><input type="radio" value="pass_with_note">通过但备注</label>
<label class="choice"><input type="radio" value="rework">带目标返修</label>
</div>
<textarea class="review-note" placeholder="记录人工备注、返修目标、不可破坏的功能和复验命令。"></textarea>
<div class="review-status">尚未记录</div>
</div>
</div>
</article>
</template>
<script>
const pages = [
{
name: "中心调度台",
route: "/#/center-dashboard",
file: "dispatcher-01-center-dashboard.png",
status: "candidate",
statusText: "定稿候选",
avg: "19.94",
rms: "47.78",
pixels: "17.7%",
focus: "核对右侧 AI 推荐检查器底部负载卡与动作区、隐藏侧栏底部快捷区,以及中心表格密度是否仍满足调度扫描效率。",
issue: "定稿候选:核心骨架、真实分页、每页条数、行级派发面板、批量操作和右侧 AI 推荐检查器已形成稳定功能闭环;动态业务样例作为可接受偏差备注。",
suggestedDecision: "pass_with_note",
suggestedNote: "定稿候选:接受当前真实分页、每页条数、选中计数、动态 footer、行级更多派发操作、右侧 AI 推荐检查器动态样例和侧栏底部状态策略;不再压缩 KPI / 表格 / 检查器密度,不再改 KPI 图标、刷新图标、自定义复选框、人员负载圆环或相似问题卡密度。"
},
{
name: "问题池",
route: "/#/issue-pool",
file: "dispatcher-02-issue-pool.png",
status: "candidate",
statusText: "定稿候选",
avg: "17.18",
rms: "43.47",
pixels: "14.1%",
focus: "复核主导航、问题池列表密度、筛选区和右侧操作区的空间关系;动态数据和图标细节不作为单独阻塞项。",
issue: "定稿候选:真实分页、提交人筛选和导出闭环已稳定;右侧 AI 洞察由图表型面板简化为文本洞察区作为产品化偏差备注。",
suggestedDecision: "pass_with_note",
suggestedNote: "通过但备注:接受右侧 AI 洞察简化为文本洞察区,保留真实分页、提交人筛选、CSV 导出、批量派发和批量治理闭环;不再为了图表装饰牺牲问题池扫描效率。"
},
{
name: "部门队列",
route: "/#/department-queue",
file: "dispatcher-03-department-queue.png",
status: "review",
statusText: "可人工复核",
avg: "17.42",
rms: "41.57",
pixels: "13.9%",
focus: "复核部门队列的标题、KPI、筛选和列表层级;重点看顶部右侧与中下部表格区域是否清晰稳定。",
suggestedDecision: "pass"
},
{
name: "提醒升级",
route: "/#/escalation-center",
file: "dispatcher-04-escalation-center.png",
status: "candidate",
statusText: "定稿候选",
avg: "18.62",
rms: "45.94",
pixels: "15.7%",
focus: "复核升级提醒的风险状态、表格密度、右侧升级链路与规则信息;只记录影响识读或业务判断的差异。",
issue: "定稿候选:已按返修目标恢复风险分组表格密度,并补齐右侧升级链路、提醒规则、最近动作和备注区;自动 diff 差异作为结构升级后的诊断备注。",
suggestedDecision: "pass_with_note",
suggestedNote: "通过但备注:采用当前风险筛选条、分组表格、升级链路、提醒规则、最近动作和备注区定稿;接受动态风险候选数量、SLA 数据和右侧链路文案与静态设计稿不同。"
},
{
name: "知识复盘",
route: "/#/knowledge-review",
file: "dispatcher-05-knowledge-review.png",
status: "candidate",
statusText: "定稿候选",
avg: "18.92",
rms: "44.57",
pixels: "14.9%",
focus: "核对右侧 AI 摘要在可编辑、可保存状态下的设计感,底部动作条清晰度,以及中部表格行高和对齐。",
issue: "关键问题:右侧摘要、底部动作区和中部表格对齐是否按当前实现定稿。",
suggestedDecision: "pass_with_note",
suggestedNote: "定稿候选:采用当前可编辑 AI 复盘工作区,保留真实分页、每页条数、摘要编辑保存和三类知识动作;不再继续调整摘要宽度、行高、按钮 gap、padding、筛选图标或头像锚点。"
},
{
name: "规则配置",
route: "/#/rule-config",
file: "dispatcher-06-rule-config.png",
status: "review",
statusText: "可人工复核",
avg: "17.81",
rms: "42.33",
pixels: "15.5%",
focus: "复核规则配置的表单密度、规则列表、状态徽标和底部右侧热点;关注配置操作是否足够清楚。",
suggestedDecision: "pass"
},
{
name: "管理看板",
route: "/#/analytics-board",
file: "dispatcher-07-analytics-board.png",
status: "candidate",
statusText: "定稿候选",
avg: "19.95",
rms: "44.41",
pixels: "19.2%",
focus: "核对顶部趋势与状态分布比例、middle-left 排行区密度、自定义布局入口和导出能力是否符合最终产品预期。",
issue: "定稿候选:五个 KPI、趋势、状态分布、AI 洞察、Top5、效率趋势、SLA 风险、本周对比和导出 / 下钻 / 自定义看板已形成稳定功能闭环。",
suggestedDecision: "pass_with_note",
suggestedNote: "定稿候选:接受首屏设计量级展示选择器、真实导出/下钻/刷新记录、右上操作入口、中部榜单当前密度和右侧 AI 洞察完整正文;不再裁剪洞察正文、不再改趋势/状态比例、效率趋势高度、榜单行高、RankingPanel 轨道、KPI 图标、tooltip 或风险色。"
},
{
name: "我的待办",
route: "/#/client-workbench",
file: "client-01-workbench.png",
status: "candidate",
statusText: "定稿候选",
avg: "17.72",
rms: "44.08",
pixels: "14.7%",
focus: "复核客户端工作台的信息层级、待办列表、右侧区域和底部热点;业务样例差异可备注但不单独阻塞。",
issue: "定稿候选:任务处理主路径、详情面板和操作优先级清楚;右侧相似问题区在当前首屏被详情与操作区挤出作为可接受偏差备注。",
suggestedDecision: "pass_with_note",
suggestedNote: "通过但备注:接受当前详情与操作区优先于右侧相似问题区的首屏取舍,保留我的待办分页、筛选排序、关注、复制链接和处理动作闭环。"
},
{
name: "提交问题",
route: "/#/submit-issue",
file: "client-02-submit-issue.png",
status: "review",
statusText: "可人工复核",
avg: "14.16",
rms: "37.39",
pixels: "11.3%",
focus: "复核提交表单、底部动作区、顶部左侧导航和移动端补充截图;关注录入流程是否清楚而不是逐像素贴图。",
suggestedDecision: "pass"
},
{
name: "处理人详情",
route: "/#/handler-detail",
file: "client-03-issue-detail-handler.png",
status: "candidate",
statusText: "定稿候选",
avg: "18.22",
rms: "43.82",
pixels: "16.2%",
focus: "核对顶栏用户区、通知和帮助区、右侧下一步动作视觉权重,以及左侧问题摘要业务样例是否需要产品统一。",
issue: "定稿候选:顶栏、右栏动作、时间线、评论工具和三栏骨架已形成稳定功能闭环;业务样例和头像细节作为可接受偏差备注。",
suggestedDecision: "pass_with_note",
suggestedNote: "定稿候选:接受当前真实演示数据、顶栏头像/通知/帮助细节、评论工具图标入口、单一右栏分组结构、19px 时间线圆点、250px / 1fr / 400px 三栏比例和右侧主按钮渐变样式;不再拆右栏独立白卡、不再调整三栏比例、不再压缩时间线圆点或重做头像/按钮色彩。"
},
{
name: "提交人追踪",
route: "/#/submitter-tracking",
file: "client-04-submitter-tracking.png",
status: "candidate",
statusText: "定稿候选",
avg: "17.91",
rms: "43.59",
pixels: "14.3%",
focus: "复核提交人追踪的时间线、状态反馈、右侧区域和底部右侧热点;确保长文本与状态徽标没有遮挡。",
issue: "定稿候选:三栏追踪结构、真实分页、排序筛选、负责人资料和附件下载稳定;默认样例、数量、头像和附件数据差异作为真实演示数据偏差备注。",
suggestedDecision: "pass_with_note",
suggestedNote: "通过但备注:接受默认选中样例、记录数量、头像和附件数据与设计稿不同,保留三栏追踪、分页、排序筛选、详情展开、负责人资料和附件下载闭环。"
},
{
name: "移动端提交 / 补充",
route: "/#/mobile-submit",
file: "client-05-mobile-submit.png",
status: "review",
statusText: "可人工复核",
avg: "17.87",
rms: "44.91",
pixels: "12.9%",
focus: "复核移动端提交流程、窄屏布局、顶部右侧热点和主操作可触达性;动态表单数据可作为备注处理。",
suggestedDecision: "pass"
}
];
const pageList = document.querySelector("#pageList");
const emptyState = document.querySelector("#emptyState");
const template = document.querySelector("#cardTemplate");
const filters = document.querySelectorAll(".filter-btn");
const exportButton = document.querySelector("#exportReview");
const clearButton = document.querySelector("#clearReview");
const storageKey = "issue-hub-ai:design-freeze-review:v1";
const decisionLabels = {
pass: "通过",
pass_with_note: "通过但备注",
rework: "带目标返修"
};
function loadReviewState() {
try {
return JSON.parse(localStorage.getItem(storageKey) || "{}");
} catch {
return {};
}
}
function saveReviewState(state) {
localStorage.setItem(storageKey, JSON.stringify(state));
}
let reviewState = loadReviewState();
function pathFor(type, file) {
if (type === "design") return "images2/" + file;
if (type === "implementation") return "implementation-screenshots/" + file;
return "visual-diff/" + file.replace(".png", "-diff.png");
}
function render() {
const fragment = document.createDocumentFragment();
pages.forEach((page) => {
const node = template.content.firstElementChild.cloneNode(true);
const stored = reviewState[page.file] || {};
const initialDecision = stored.decision || page.suggestedDecision || "";
const initialNote = stored.note || page.suggestedNote || "";
node.dataset.status = page.status;
node.dataset.file = page.file;
node.querySelector("h2").textContent = page.name;
node.querySelector(".route").innerHTML = "<code>" + page.route + "</code> / <code>" + page.file + "</code>";
const badge = node.querySelector(".badge");
badge.textContent = page.statusText;
badge.classList.add(page.status);
node.querySelector("[data-field='avg']").textContent = page.avg;
node.querySelector("[data-field='rms']").textContent = page.rms;
node.querySelector("[data-field='pixels']").textContent = page.pixels;
node.querySelector(".focus-text").textContent = page.focus;
if (page.issue) {
const issue = node.querySelector(".key-issue");
issue.hidden = false;
issue.textContent = page.issue;
}
["design", "implementation", "diff"].forEach((type) => {
const src = pathFor(type, page.file);
const img = node.querySelector("[data-img='" + type + "']");
const link = node.querySelector("[data-link='" + type + "']");
img.src = src;
img.alt = page.name + " " + (type === "design" ? "设计图" : type === "implementation" ? "实现截图" : "diff 图");
link.href = src;
});
const note = node.querySelector(".review-note");
const reviewStatus = node.querySelector(".review-status");
const choices = node.querySelectorAll(".decision-box input");
note.value = initialNote;
choices.forEach((input) => {
input.name = "review-" + page.file;
input.checked = input.value === initialDecision;
input.addEventListener("change", () => {
updateReviewState(page, node);
});
});
note.addEventListener("input", () => {
updateReviewState(page, node);
});
renderReviewStatus(page, node, reviewStatus);
fragment.appendChild(node);
});
pageList.appendChild(fragment);
}
function currentDecision(card) {
const checked = card.querySelector(".decision-box input:checked");
return checked ? checked.value : "";
}
function renderReviewStatus(page, card, target = card.querySelector(".review-status")) {
const decision = currentDecision(card);
const note = card.querySelector(".review-note").value.trim();
if (!decision && !note) {
target.textContent = "尚未记录";
return;
}
const source = reviewState[page.file] ? "已保存" : "建议";
target.textContent = source + "" + (decisionLabels[decision] || "未选择结论") + (note ? ",含备注" : "");
}
function updateReviewState(page, card) {
const decision = currentDecision(card);
const note = card.querySelector(".review-note").value.trim();
if (!decision && !note) {
delete reviewState[page.file];
} else {
reviewState[page.file] = {
page: page.name,
route: page.route,
file: page.file,
status: page.statusText,
decision,
decisionLabel: decisionLabels[decision] || "",
note,
updatedAt: new Date().toISOString()
};
}
saveReviewState(reviewState);
renderReviewStatus(page, card);
}
function applyFilter(filter) {
let visible = 0;
document.querySelectorAll(".review-card").forEach((card) => {
const show = filter === "all" || card.dataset.status === filter;
card.classList.toggle("hidden", !show);
if (show) visible += 1;
});
emptyState.style.display = visible ? "none" : "block";
filters.forEach((button) => {
button.classList.toggle("active", button.dataset.filter === filter);
});
}
filters.forEach((button) => {
button.addEventListener("click", () => applyFilter(button.dataset.filter));
});
exportButton.addEventListener("click", () => {
const decisions = Array.from(document.querySelectorAll(".review-card")).map((card) => {
const page = pages.find((item) => item.file === card.dataset.file);
const decision = currentDecision(card);
const note = card.querySelector(".review-note").value.trim();
if (!page || (!decision && !note)) return null;
return reviewState[page.file] || {
page: page.name,
route: page.route,
file: page.file,
status: page.statusText,
decision,
decisionLabel: decisionLabels[decision] || "",
note,
updatedAt: new Date().toISOString(),
source: page.suggestedDecision ? "suggested" : "current"
};
}).filter(Boolean);
const payload = {
exportedAt: new Date().toISOString(),
reportTime: "2026-06-18T04:47:26.995Z",
storageKey,
decisions
};
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "issue-hub-ai-design-freeze-review.json";
link.click();
URL.revokeObjectURL(link.href);
});
clearButton.addEventListener("click", () => {
if (!confirm("确定清空当前浏览器中的定稿审阅记录吗?")) return;
reviewState = {};
saveReviewState(reviewState);
document.querySelectorAll(".review-card").forEach((card) => {
const page = pages.find((item) => item.file === card.dataset.file);
card.querySelectorAll(".decision-box input").forEach((input) => {
input.checked = false;
});
card.querySelector(".review-note").value = "";
renderReviewStatus(page, card);
});
});
render();
</script>
</body>
</html>