Files
DEMO-AGENT/docs/原型设计/registration-prototype-demo.html

2707 lines
100 KiB
HTML
Raw 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.0" />
<title>注册审核平台原型演示站</title>
<style>
:root {
--bg: #eef2f5;
--bg-strong: #d9e1e8;
--panel: #ffffff;
--panel-strong: #ffffff;
--ink: #132535;
--muted: #627588;
--line: #d7e0e7;
--line-strong: #c4d0da;
--navy: #173a58;
--navy-strong: #112b42;
--teal: #245d69;
--accent: #b77a39;
--danger: #a94731;
--success: #2f6a4b;
--warning: #b8802f;
--shadow: 0 8px 18px rgba(23, 41, 61, 0.06);
--shadow-strong: 0 12px 24px rgba(18, 35, 53, 0.08);
--radius-xl: 10px;
--radius-lg: 8px;
--radius-md: 6px;
--radius-sm: 4px;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Microsoft YaHei", "PingFang SC", "Segoe UI", sans-serif;
color: var(--ink);
background:
linear-gradient(180deg, #f5f7f9 0%, #edf2f5 100%);
}
a {
color: inherit;
text-decoration: none;
}
button {
font: inherit;
cursor: pointer;
border: 0;
}
.app-shell {
min-height: 100vh;
display: grid;
grid-template-columns: 280px 1fr;
}
.sidebar {
position: sticky;
top: 0;
height: 100vh;
padding: 18px 14px;
background: linear-gradient(180deg, #173754 0%, #122c43 100%);
color: rgba(255, 255, 255, 0.94);
display: flex;
flex-direction: column;
gap: 12px;
border-right: 1px solid rgba(255, 255, 255, 0.08);
}
.brand {
padding: 16px;
border-radius: var(--radius-lg);
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.brand h1 {
margin: 0 0 8px;
font-size: 24px;
line-height: 1.2;
}
.brand p,
.sidebar-note,
.nav-label,
.muted {
color: rgba(255, 255, 255, 0.72);
}
.nav-list {
display: grid;
gap: 10px;
}
.nav-btn,
.governance-tab,
.pill,
.secondary-btn,
.primary-btn,
.mini-btn {
transition: all 0.18s ease;
}
.nav-btn {
width: 100%;
padding: 12px 14px;
border-radius: var(--radius-md);
background: rgba(255, 255, 255, 0.03);
color: white;
text-align: left;
border: 1px solid transparent;
}
.nav-btn small {
display: block;
margin-top: 4px;
color: rgba(255, 255, 255, 0.66);
font-size: 12px;
}
.nav-btn.active,
.nav-btn:hover {
background: rgba(255, 255, 255, 0.09);
border-color: rgba(255, 255, 255, 0.12);
transform: none;
}
.sidebar-summary {
margin-top: auto;
padding: 16px;
border-radius: var(--radius-lg);
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.main {
padding: 16px 18px 24px;
}
.topbar {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: center;
padding: 16px 18px;
border-radius: var(--radius-xl);
background: #ffffff;
border: 1px solid var(--line);
box-shadow: var(--shadow);
margin-bottom: 16px;
}
.topbar h2 {
margin: 0 0 6px;
font-size: 22px;
letter-spacing: 0.01em;
}
.status-row,
.toolbar,
.card-actions,
.chip-row,
.split-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.status-chip,
.tag,
.pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
}
.status-chip {
background: #eef3f8;
color: #264764;
border: 1px solid #d6e0ea;
}
.tag {
background: #f6f8fa;
color: var(--muted);
border: 1px solid var(--line);
}
.tag.high,
.risk-high {
background: #fae6df;
color: var(--danger);
}
.tag.medium {
background: #fff1de;
color: var(--warning);
}
.tag.low {
background: #e5f4ea;
color: var(--success);
}
.primary-btn,
.secondary-btn {
padding: 10px 14px;
border-radius: var(--radius-md);
font-weight: 700;
}
.primary-btn {
background: linear-gradient(180deg, #214b74 0%, #163754 100%);
color: white;
box-shadow: none;
border: 1px solid #153754;
}
.secondary-btn,
.mini-btn {
background: white;
color: var(--ink);
border: 1px solid var(--line-strong);
}
.mini-btn {
padding: 7px 10px;
border-radius: var(--radius-md);
font-size: 12px;
font-weight: 700;
}
.page {
display: none;
animation: fadeIn 0.24s ease;
}
.page.active {
display: block;
}
.hero {
display: grid;
grid-template-columns: 1.35fr 1fr;
gap: 18px;
margin-bottom: 16px;
}
.panel {
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius-xl);
box-shadow: var(--shadow);
padding: 18px;
}
.panel h3,
.panel h4 {
margin: 0 0 12px;
}
.lead {
color: var(--muted);
line-height: 1.7;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px;
margin-bottom: 16px;
}
.metric-card,
.task-card,
.info-card {
border-radius: var(--radius-lg);
padding: 16px;
background: var(--panel-strong);
border: 1px solid var(--line);
}
.metric-card strong {
display: block;
font-size: 28px;
margin-top: 8px;
line-height: 1;
color: var(--navy-strong);
}
.section-title {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 12px;
margin-bottom: 14px;
}
.section-title h3,
.section-title h4 {
margin: 0;
}
.section-title p {
margin: 4px 0 0;
color: var(--muted);
}
.task-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px;
margin-bottom: 12px;
}
.task-card {
position: relative;
overflow: hidden;
cursor: pointer;
}
.task-card.active {
border-color: #8da4b8;
box-shadow: none;
background: #fbfdff;
}
.task-card.active::after {
content: "";
position: absolute;
inset: 0;
border-top: 3px solid var(--navy);
border-radius: inherit;
}
.task-card h4 {
margin: 0 0 8px;
font-size: 16px;
}
.task-card p {
margin: 0 0 14px;
color: var(--muted);
min-height: 36px;
line-height: 1.5;
font-size: 12px;
}
.task-status-line {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.task-note {
color: var(--muted);
font-size: 12px;
line-height: 1.6;
min-height: 40px;
}
.task-meta {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--muted);
font-size: 12px;
font-weight: 700;
}
.two-col {
display: grid;
grid-template-columns: 1.15fr 0.85fr;
gap: 18px;
}
.tri-col {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 18px;
}
.split {
display: grid;
grid-template-columns: 1.3fr 0.9fr;
gap: 18px;
}
.subsplit {
display: grid;
grid-template-columns: 0.9fr 1.1fr;
gap: 18px;
}
.tree,
.timeline,
.issue-list,
.check-list,
.governance-list {
display: grid;
gap: 10px;
}
.tree-node,
.timeline-step,
.issue-item,
.check-item,
.governance-row {
padding: 12px;
border-radius: var(--radius-md);
background: #f8fafb;
border: 1px solid #dbe4eb;
}
.tree-node button {
background: transparent;
padding: 0;
color: var(--ink);
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
}
.tree-children {
margin-top: 10px;
padding-left: 12px;
border-left: 2px solid #dfe5eb;
display: none;
gap: 8px;
}
.tree-node.expanded .tree-children {
display: grid;
}
.timeline-step {
display: grid;
grid-template-columns: 36px 1fr auto;
gap: 12px;
align-items: start;
}
.step-no {
width: 36px;
height: 36px;
border-radius: 50%;
display: grid;
place-items: center;
background: #dbe7f2;
color: var(--navy);
font-weight: 800;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
th,
td {
padding: 12px 10px;
text-align: left;
border-bottom: 1px solid #e6edf3;
vertical-align: top;
}
thead th {
color: var(--muted);
font-weight: 700;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
tbody tr:hover {
background: rgba(21, 61, 98, 0.03);
}
.evidence {
padding: 14px;
border-radius: var(--radius-md);
background: #f5f8fb;
border: 1px solid #d6e1ea;
line-height: 1.7;
color: #2f4358;
}
.card-stack {
display: grid;
gap: 14px;
}
.phase-board {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.phase-card {
padding: 14px;
border-radius: var(--radius-lg);
background: #f8fafb;
border: 1px solid #dbe4eb;
min-height: 142px;
display: grid;
gap: 10px;
align-content: start;
}
.phase-card.running {
background: #eef4fb;
border-color: #bfd2e3;
box-shadow: inset 0 0 0 1px rgba(32, 74, 112, 0.06);
}
.phase-card.completed {
background: #eff8f2;
border-color: #c8dfcf;
}
.phase-card.blocked {
background: #fff3ef;
border-color: #efc7bc;
}
.phase-card.pending {
background: #f8fafb;
border-color: #dbe4eb;
}
.phase-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 10px;
}
.phase-card strong {
font-size: 15px;
line-height: 1.4;
}
.phase-state {
font-size: 12px;
font-weight: 700;
color: var(--muted);
line-height: 1.4;
}
.phase-note {
color: var(--muted);
font-size: 12px;
line-height: 1.7;
}
.phase-card.running .phase-state {
color: var(--navy);
}
.phase-card.completed .phase-state {
color: var(--success);
}
.phase-card.blocked .phase-state {
color: var(--danger);
}
.knowledge-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
}
.knowledge-card {
padding: 16px;
border-radius: var(--radius-lg);
background: var(--panel-strong);
border: 1px solid var(--line);
display: grid;
gap: 12px;
}
.knowledge-card p {
margin: 0;
color: var(--muted);
line-height: 1.7;
font-size: 13px;
}
.knowledge-stat {
font-size: 28px;
font-weight: 800;
color: var(--navy-strong);
line-height: 1;
}
.knowledge-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.upload-zone {
border: 1px dashed #a7bacb;
background: linear-gradient(180deg, #f9fbfc 0%, #f3f7fa 100%);
border-radius: var(--radius-lg);
padding: 20px;
display: grid;
gap: 12px;
}
.upload-zone strong {
font-size: 16px;
}
.upload-list {
display: grid;
gap: 10px;
}
.upload-item {
padding: 12px 14px;
border-radius: var(--radius-md);
border: 1px solid #dbe4eb;
background: #f8fafb;
display: grid;
grid-template-columns: 1fr auto;
gap: 12px;
align-items: center;
}
.upload-item strong {
display: block;
margin-bottom: 6px;
}
.upload-item .muted {
color: #5f7285;
line-height: 1.6;
}
.alert {
padding: 14px 16px;
border-radius: var(--radius-md);
border: 1px solid;
font-size: 14px;
line-height: 1.65;
}
.alert.danger {
background: #fff3ef;
border-color: #f1c7bb;
color: #81341f;
}
.alert.warning {
background: #fff8ec;
border-color: #f0dbb5;
color: #8d6120;
}
.alert.success {
background: #eef9f2;
border-color: #cde9d5;
color: #21583c;
}
.drawer {
position: fixed;
top: 0;
right: -720px;
width: min(720px, 100vw);
height: 100vh;
background: #f7fafc;
box-shadow: -16px 0 24px rgba(14, 29, 43, 0.12);
border-left: 1px solid var(--line);
transition: right 0.28s ease;
z-index: 20;
display: flex;
flex-direction: column;
}
.drawer.open {
right: 0;
}
.drawer-header,
.drawer-body {
padding: 20px;
}
.drawer-header {
border-bottom: 1px solid var(--line);
display: flex;
justify-content: space-between;
align-items: center;
}
.drawer-body {
overflow: auto;
display: grid;
gap: 18px;
}
.governance-tabs {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.governance-tab {
padding: 10px 12px;
border-radius: 999px;
background: white;
border: 1px solid var(--line-strong);
color: var(--muted);
font-weight: 700;
}
.governance-tab.active {
background: #163754;
color: white;
border-color: #163754;
}
.governance-panel {
display: none;
}
.governance-panel.active {
display: block;
}
.crud-bar {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 14px;
}
.message-card {
border-radius: var(--radius-lg);
background: #ffffff;
border: 1px solid #d6e0e7;
padding: 18px;
box-shadow: none;
}
.message-card .card-head {
display: flex;
justify-content: space-between;
align-items: start;
gap: 16px;
margin-bottom: 16px;
}
.mention {
display: inline-flex;
padding: 6px 10px;
border-radius: 999px;
background: #e9f2ff;
color: #1c4f88;
font-size: 12px;
font-weight: 700;
}
.download-box {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
padding: 16px;
border-radius: var(--radius-md);
background: #f7fafc;
border: 1px solid #cfdae3;
}
.copy-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.workspace-main {
display: grid;
grid-template-columns: 1.5fr 0.95fr;
gap: 16px;
margin-top: 16px;
}
.agent-shell {
display: grid;
gap: 12px;
}
.prompt-bar {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.prompt-chip {
padding: 8px 12px;
border-radius: var(--radius-md);
background: #f3f6f9;
border: 1px solid var(--line);
color: var(--navy-strong);
font-size: 12px;
font-weight: 700;
}
.conversation-board {
display: grid;
gap: 12px;
}
.message {
border: 1px solid var(--line);
border-radius: var(--radius-lg);
background: #ffffff;
overflow: hidden;
}
.message.user {
border-color: #cdd9e2;
background: #f7fafc;
}
.message.agent {
border-color: #cfdae3;
background: #ffffff;
}
.message.tool {
border-color: #d7dfe6;
background: #fbfcfd;
}
.message.system {
border-color: #d9e1e8;
background: #f6f8fa;
}
.message-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-bottom: 1px solid #e6edf2;
background: #f7fafc;
}
.message.user .message-head {
background: #eef4f8;
}
.message.agent .message-head {
background: #f5f8fb;
}
.message-body {
padding: 14px;
line-height: 1.75;
color: #314659;
}
.message-body p {
margin: 0 0 10px;
}
.message-body p:last-child {
margin-bottom: 0;
}
.trace-grid {
display: grid;
gap: 10px;
margin-top: 12px;
}
.trace-item {
padding: 12px;
border-radius: var(--radius-md);
border: 1px solid #dbe4eb;
background: #f8fafb;
}
.trace-item strong {
display: block;
margin-bottom: 6px;
}
.review-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.review-btn {
padding: 8px 12px;
border-radius: var(--radius-md);
border: 1px solid var(--line-strong);
background: #ffffff;
color: var(--ink);
font-size: 12px;
font-weight: 700;
}
.review-btn.primary {
background: #173754;
border-color: #173754;
color: #ffffff;
}
.page-agent-grid {
display: grid;
grid-template-columns: 1.35fr 0.95fr;
gap: 16px;
margin-top: 16px;
}
.page-agent-side {
display: grid;
gap: 16px;
}
.agent-mini-panel {
display: grid;
gap: 12px;
}
.agent-q-list {
display: grid;
gap: 8px;
}
.agent-q-item {
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: var(--radius-md);
background: #f8fafb;
font-size: 13px;
color: #314659;
}
.list-inline {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.overlay {
position: fixed;
inset: 0;
background: rgba(8, 18, 28, 0.28);
opacity: 0;
pointer-events: none;
transition: opacity 0.24s ease;
z-index: 10;
}
.overlay.show {
opacity: 1;
pointer-events: auto;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 1240px) {
.app-shell {
grid-template-columns: 1fr;
}
.sidebar {
position: relative;
height: auto;
}
.hero,
.two-col,
.tri-col,
.split,
.subsplit,
.workspace-main,
.page-agent-grid {
grid-template-columns: 1fr;
}
.metric-grid,
.task-grid,
.copy-grid,
.phase-board,
.knowledge-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 760px) {
.main {
padding: 16px;
}
.topbar,
.panel {
padding: 16px;
}
.metric-grid,
.task-grid,
.copy-grid,
.phase-board,
.knowledge-grid {
grid-template-columns: 1fr;
}
.drawer {
width: 100vw;
}
}
</style>
</head>
<body>
<div class="overlay" id="overlay"></div>
<div class="drawer" id="governanceDrawer">
<div class="drawer-header">
<div>
<h3 style="margin: 0 0 6px;">知识库与治理台</h3>
<div class="muted" style="color: #5f7285;">统一展示法规规则、RAG、字段 Schema、模板映射、责任人与飞书配置 CRUD。</div>
</div>
<button class="secondary-btn" id="closeDrawerBtn">关闭</button>
</div>
<div class="drawer-body">
<div class="governance-tabs" id="governanceTabs"></div>
<div id="governancePanels"></div>
</div>
</div>
<div class="app-shell">
<aside class="sidebar">
<div class="brand">
<div class="nav-label">Demo Prototype</div>
<h1>注册审核平台</h1>
<p style="margin: 0; line-height: 1.7;">资料包治理、法规核查、字段池、一致性、风险预警、Word 导出、飞书协同一体化演示。</p>
</div>
<div class="nav-list" id="navList"></div>
<div class="sidebar-summary">
<div class="nav-label">当前批次</div>
<div style="font-size: 20px; font-weight: 800; margin: 8px 0;">SUB-20260603-001</div>
<div class="sidebar-note" style="line-height: 1.7;">产品:新型冠状病毒 2019-nCoV 核酸检测试剂盒</div>
<div class="list-inline" style="margin-top: 12px;">
<span class="tag high">高风险</span>
<span class="tag">CH1~CH6</span>
<span class="tag">registration</span>
</div>
</div>
</aside>
<main class="main">
<header class="topbar">
<div>
<h2 id="pageTitle">审核任务工作台</h2>
<div class="status-row">
<span class="status-chip">批次 SUB-20260603-001</span>
<span class="status-chip">流程 registration</span>
<span class="status-chip">当前结论 不通过</span>
</div>
</div>
<div class="toolbar">
<button class="secondary-btn" id="openDrawerBtn">打开治理台</button>
<button class="primary-btn" data-page-target="notification">查看飞书通知</button>
</div>
</header>
<div id="pages"></div>
</main>
</div>
<script>
const data = {
batch: {
id: "SUB-20260603-001",
workflow: "registration",
productName: "新型冠状病毒 2019-nCoV 核酸检测试剂盒",
applicant: "示例生物科技(上海)有限公司",
stage: "风险预警已完成,正式导出受阻",
riskLevel: "高",
passStatus: "不通过",
totalDocs: 18,
totalPages: 236,
},
metrics: [
{ label: "资料齐套率", value: "87%", note: "CH1 存在 1 项缺失" },
{ label: "字段抽取完成度", value: "92%", note: "1 个字段待复核" },
{ label: "一致性通过率", value: "78%", note: "产品名称存在冲突" },
{ label: "导出可用状态", value: "草稿", note: "正式版被高风险拦截" },
],
tasks: [
{ id: "import", name: "资料包导入", status: "已完成", metric: "18 份文件", detail: "支持批量文件、文件夹和压缩包导入" },
{ id: "import", name: "目录汇总", status: "已完成", metric: "236 页", detail: "章节点识别到 CH1 ~ CH6" },
{ id: "completeness", name: "法规完整性检查", status: "已完成", metric: "1 缺失 / 1 错放", detail: "基于规则包和法规证据输出结论" },
{ id: "fields", name: "字段抽取", status: "已完成", metric: "12 / 13", detail: "字段池已建立1 个字段待复核" },
{ id: "consistency", name: "一致性核查", status: "已完成", metric: "1 冲突 / 1 混档", detail: "说明书与申请表存在产品名称冲突" },
{ id: "risk", name: "风险预警", status: "已完成", metric: "高风险", detail: "综合结论不通过,建议先整改" },
{ id: "word", name: "Word 回填导出", status: "已阻断", metric: "仅草稿可导出", detail: "正式版被高风险与冲突字段拦截" },
],
taskStatusMap: {
pending: "待执行",
running: "执行中",
completed: "已完成",
blocked: "已阻断"
},
processFlows: {
import: [
{ key: "upload", title: "资料上传", state: "completed", stateText: "上传完成", note: "18 份文件和 1 个压缩包已建立批次。" },
{ key: "parse", title: "文档解析", state: "running", stateText: "解析中", note: "正在提取目录结构、页数与文档元数据。" },
{ key: "recognize", title: "章节点识别", state: "pending", stateText: "待执行", note: "待文档解析完成后自动识别 CH 章节节点。" },
{ key: "summary", title: "目录汇总", state: "pending", stateText: "待执行", note: "将生成 registration_overview_report 并写入工作台。" }
],
completeness: [
{ key: "load-rules", title: "加载规则包", state: "completed", stateText: "已完成", note: "已装载 NMPA IVD 注册资料规则包。" },
{ key: "compare-items", title: "必交项比对", state: "running", stateText: "核查中", note: "对照 CH1 资料要求检查缺失项与错放项。" },
{ key: "evidence", title: "法规证据关联", state: "pending", stateText: "待执行", note: "待生成命中法规条款与证据摘要。" },
{ key: "risk-tag", title: "完整性风险判定", state: "pending", stateText: "待执行", note: "将输出完整性风险等级和责任角色。" }
],
fields: [
{ key: "ocr", title: "文本准备", state: "completed", stateText: "已完成", note: "OCR / 文本抽取结果已就绪。" },
{ key: "extract", title: "字段抽取", state: "running", stateText: "抽取中", note: "按字段 Schema 从说明书与申请表中抽取字段。" },
{ key: "pool", title: "字段池写入", state: "pending", stateText: "待执行", note: "将沉淀字段值、来源、置信度与回填标记。" },
{ key: "review", title: "待复核标记", state: "pending", stateText: "待执行", note: "对低置信度字段追加人工复核状态。" }
],
consistency: [
{ key: "load-pool", title: "加载字段池", state: "completed", stateText: "已完成", note: "已读取统一字段池和强一致字段定义。" },
{ key: "compare", title: "来源对比", state: "running", stateText: "核查中", note: "正在比较说明书、申请表和章节文档取值。" },
{ key: "mix-risk", title: "混档风险分析", state: "pending", stateText: "待执行", note: "将输出疑似混档与来源偏差解释。" },
{ key: "conflict", title: "冲突结论生成", state: "pending", stateText: "待执行", note: "生成冲突字段列表和建议采用值。" }
],
risk: [
{ key: "collect", title: "风险汇总", state: "completed", stateText: "已完成", note: "已汇总完整性、一致性和待复核结论。" },
{ key: "grade", title: "风险评级", state: "running", stateText: "判定中", note: "按高 / 中 / 低规则综合判定当前批次。" },
{ key: "rectify", title: "整改建议生成", state: "pending", stateText: "待执行", note: "将结合责任角色输出整改动作。" },
{ key: "gate", title: "导出闸门决策", state: "pending", stateText: "待执行", note: "将决定是否允许正式版回填导出。" }
],
word: [
{ key: "mapping", title: "模板映射加载", state: "completed", stateText: "已完成", note: "已读取 Word 模板占位符和字段映射。" },
{ key: "fill", title: "字段回填", state: "running", stateText: "回填中", note: "正在把可回填字段写入草稿模板。" },
{ key: "block", title: "拦截校验", state: "blocked", stateText: "已阻断", note: "正式版被高风险和冲突字段命中拦截。" },
{ key: "download", title: "导出结果生成", state: "pending", stateText: "待执行", note: "仅草稿下载入口会在拦截后继续保留。" }
],
notification: [
{ key: "compose", title: "消息编排", state: "completed", stateText: "已完成", note: "已生成飞书交互卡片内容。" },
{ key: "mention", title: "责任人映射", state: "running", stateText: "匹配中", note: "正在匹配章节责任人与风险责任人账号。" },
{ key: "link", title: "Web 详情链接", state: "pending", stateText: "待执行", note: "待拼装批次详情跳转链接。" },
{ key: "send", title: "发送回执", state: "pending", stateText: "待执行", note: "发送后将写入 message_id 和 sent_at。" }
]
},
importTimeline: [
{ step: "创建批次", status: "已完成", detail: "创建 SUB-20260603-001来源角色为 submission。" },
{ step: "文件校验", status: "已完成", detail: "18 份文件通过校验1 份 DOC 标记待复核。" },
{ step: "压缩包解包", status: "已完成", detail: "保留原始相对路径,识别到多层目录结构。" },
{ step: "页数统计", status: "部分完成", detail: "PDF / DOCX 精确统计1 份 DOC 页数待复核。" },
{ step: "章节点识别", status: "已完成", detail: "识别 CH1.2、CH1.4、CH1.11.5 等章节节点。" },
{ step: "目录汇总", status: "已完成", detail: "生成 registration_overview_report。" },
],
importDocs: [
{ path: "第1章 监管信息/CH1.2 监管信息目录.docx", type: "DOCX", pages: "3", confidence: "精确", chapter: "CH1.2", name: "监管信息目录", status: "已汇总", hit: "是" },
{ path: "第1章 监管信息/CH1.4 申请表.docx", type: "DOCX", pages: "5", confidence: "精确", chapter: "CH1.4", name: "申请表", status: "已汇总", hit: "是" },
{ path: "第1章 监管信息/CH1.9 产品申报前沟通的说明.doc", type: "DOC", pages: "待复核", confidence: "低", chapter: "CH1.9", name: "沟通说明", status: "待复核", hit: "是" },
{ path: "说明书/目标产品说明书.docx", type: "DOCX", pages: "18", confidence: "精确", chapter: "CH3", name: "产品说明书", status: "已汇总", hit: "是" },
],
knowledgeBase: {
summary: [
{ label: "知识库资料数", value: "27", note: "法规、模板、示例资料统一管理" },
{ label: "待入库任务", value: "3", note: "含 1 个法规包更新和 2 个业务资料" },
{ label: "有效切片数", value: "486", note: "支持 RAG 召回与规则辅助解释" },
{ label: "启用规则包", value: "4", note: "注册审核与一致性规则均已启用" }
],
uploadQueue: [
{ name: "体外诊断试剂注册申报资料要求及说明_2026修订版.docx", type: "法规资料", owner: "法规管理员", state: "文档解析中", detail: "已上传,正在提取章节标题与法规条款。" },
{ name: "说明书模板映射补充包.zip", type: "模板资料", owner: "数据治理专员", state: "解析完成", detail: "已识别 12 个占位符映射,待进入数据处理。" },
{ name: "注册申报常见问题汇编.docx", type: "业务知识", owner: "注册经理", state: "数据处理中", detail: "正在生成切片、关键词和召回标签。" },
{ name: "临床评价参考示例.pdf", type: "示例资料", owner: "医学专员", state: "处理完成", detail: "已入知识库并开放检索使用。" }
],
cards: [
{ title: "法规规则包", value: "4", desc: "维护完整性检查、一致性判断和风险等级规则。", actions: ["新增规则包", "上传新版", "停用旧版"] },
{ title: "RAG 文档源", value: "27", desc: "手动上传法规原文、模板文档、示例资料和内部 SOP。", actions: ["上传文档", "编辑标签", "重新入库"] },
{ title: "切片与召回", value: "486", desc: "查看切片状态、命中场景、阈值和是否可召回。", actions: ["新增切片", "拆分切片", "重建向量"] },
{ title: "字段 Schema", value: "38", desc: "统一字段标准、是否可回填、强一致约束和来源优先级。", actions: ["新增字段", "编辑字段", "复制版本"] },
{ title: "模板与映射", value: "9", desc: "维护 Word 模板、占位符映射和导出阻断约束。", actions: ["上传模板", "编辑映射", "预览模板"] },
{ title: "通知与责任人", value: "12", desc: "维护责任角色映射、飞书群聊、机器人和消息模板。", actions: ["新增映射", "发送测试", "查看回执"] }
]
},
completeness: {
summary: [
{ label: "必交项", value: "8" },
{ label: "已提供", value: "6" },
{ label: "缺失项", value: "1" },
{ label: "错放项", value: "1" },
],
issues: [
{ type: "缺失", chapter: "CH1.11.4", name: "授权书 / 声明类文件", risk: "高", detail: "未在当前资料包中识别到对应文档。" },
{ type: "错放", chapter: "CH1.9", name: "产品申报前沟通说明", risk: "中", detail: "命中文档存在,但路径和目录位置疑似不规范。" },
{ type: "待复核", chapter: "CH1.11", name: "DOC 页数结果", risk: "待确认", detail: "页数无法精确统计,需要人工确认。" },
],
evidence: "依据《体外诊断试剂注册申报资料要求及说明》CH1 监管信息章节要求,授权声明类资料应作为必交项提交;当前目录未发现对应文件,因此判定为缺失。",
},
fields: {
summary: [
{ label: "目标字段", value: "13" },
{ label: "已抽取", value: "12" },
{ label: "待复核", value: "1" },
{ label: "可回填", value: "9" },
],
items: [
{ key: "product_name", label: "产品名称", value: "新型冠状病毒 2019-nCoV 核酸检测试剂盒", raw: "新型冠状病毒2019-nCoV核酸检测试剂盒", source: "目标产品说明书.docx", location: "一、产品名称", method: "标题规则", confidence: "高", conflict: "待核查", review: "否", fillable: "是" },
{ key: "detection_target", label: "检测靶标", value: "2019-nCoV ORF1ab / N 基因", raw: "ORF1ab、N", source: "目标产品说明书.docx", location: "产品用途", method: "LLM 归纳", confidence: "中", conflict: "待核查", review: "否", fillable: "是" },
{ key: "storage_condition", label: "储存条件", value: "-20℃ 以下避光保存", raw: "建议冷冻保存", source: "申请表.docx", location: "表 2-储运条件", method: "表格抽取", confidence: "低", conflict: "待核查", review: "是", fillable: "是" },
{ key: "intended_use", label: "适用范围", value: "用于新型冠状病毒核酸定性检测", raw: "用于...定性检测", source: "目标产品说明书.docx", location: "适用范围", method: "段落归纳", confidence: "高", conflict: "待核查", review: "否", fillable: "是" },
]
},
consistency: {
summary: [
{ label: "核查字段", value: "6" },
{ label: "一致通过", value: "4" },
{ label: "冲突字段", value: "1" },
{ label: "混档风险", value: "1" },
],
conflicts: [
{ field: "产品名称", result: "冲突", values: "2", docs: "说明书 / 申请表", risk: "高", mixed: "是" },
{ field: "储存条件", result: "待复核", values: "1", docs: "申请表", risk: "待确认", mixed: "否" },
],
compare: [
{ doc: "目标产品说明书.docx", value: "新型冠状病毒 2019-nCoV 核酸检测试剂盒", chapter: "产品名称", page: "P1", normalized: "新型冠状病毒 2019-nCoV 核酸检测试剂盒" },
{ doc: "CH1.4 申请表.docx", value: "新型冠状病毒核酸检测试剂盒", chapter: "产品基本信息", page: "P2", normalized: "新型冠状病毒核酸检测试剂盒" },
]
},
risk: {
summary: [
{ label: "风险总数", value: "6" },
{ label: "高风险", value: "2" },
{ label: "中风险", value: "2" },
{ label: "低风险", value: "1" },
{ label: "待复核", value: "1" },
],
items: [
{ type: "缺失必交资料", level: "高", desc: "CH1.11.4 未识别到授权 / 声明类文件。", source: "完整性报告", doc: "CH1", advice: "补充资料后重跑完整性检查。", owner: "注册申报负责人" },
{ type: "产品名称冲突", level: "高", desc: "说明书与申请表产品名称不一致。", source: "一致性报告", doc: "说明书 / 申请表", advice: "先确认是否混入其他产品资料。", owner: "注册资料负责人" },
{ type: "资料错放", level: "中", desc: "沟通说明目录位置疑似不规范。", source: "完整性报告", doc: "CH1.9", advice: "调整目录结构并复核。", owner: "注册资料负责人" },
]
},
word: {
template: "registration_application_form_v1",
version: "2026-06-03",
exportStatus: "draft_generated",
filled: [
{ placeholder: "{{ product_name }}", field: "产品名称", value: "新型冠状病毒 2019-nCoV 核酸检测试剂盒", source: "说明书", status: "已回填", required: "是" },
{ placeholder: "{{ intended_use }}", field: "适用范围", value: "用于新型冠状病毒核酸定性检测", source: "说明书", status: "已回填", required: "是" },
{ placeholder: "{{ detection_target }}", field: "检测靶标", value: "2019-nCoV ORF1ab / N 基因", source: "说明书", status: "已回填", required: "否" },
],
blocked: [
{ reason: "高风险拦截", field: "正式版导出", detail: "当前批次总体风险等级为高,禁止正式导出。" },
{ reason: "冲突字段拦截", field: "产品名称", detail: "字段存在冲突,需人工确认推荐值。" },
{ reason: "待复核字段拦截", field: "储存条件", detail: "字段置信度低,需人工确认后方可正式出具。" },
]
},
notification: {
mentions: ["@注册资料负责人", "@注册申报负责人"],
receipt: { id: "om_demo_message_20260603_103000", time: "2026-06-03 10:30:00" },
webLink: "http://localhost:8000/audit/1001/",
},
agent: {
quickPrompts: [
"检查当前资料完整性",
"抽取本批次核心字段",
"解释产品名称冲突原因",
"生成整改建议和责任人分发"
],
workspaceConversation: [
{
role: "user",
title: "用户指令",
meta: "10:05 · 审核任务工作台",
body: [
"请基于当前批次资料,按注册申报流程完成完整性检查、字段抽取、一致性核查,并给出风险结论。"
]
},
{
role: "agent",
title: "Agent 计划",
meta: "10:05 · orchestration",
body: [
"我将按 5 个阶段执行:读取资料包目录 -> 加载法规规则包 -> 抽取统一字段池 -> 执行强一致比对 -> 汇总风险并判断是否允许导出。",
"当前建议优先关注 CH1 监管信息和说明书相关字段。"
]
},
{
role: "tool",
title: "工具调用记录",
meta: "10:06 · tools",
body: [
"已调用 `scan_submission_package`、`check_nmpa_completeness`、`extract_product_fields`、`compare_field_consistency`、`build_risk_alerts`。",
"RAG 命中来源包括《体外诊断试剂注册申报资料要求及说明》与目标产品说明书切片。"
]
},
{
role: "system",
title: "结构化结论",
meta: "10:09 · result",
body: [
"完整性检查:发现 1 个缺失项、1 个错放项。",
"字段抽取13 个目标字段中 12 个已抽取1 个待复核。",
"一致性核查:产品名称存在冲突,疑似混档。",
"综合风险:高风险,不允许正式导出,仅允许生成草稿。"
]
}
],
workspaceTrace: [
{ label: "当前任务", value: "注册申报审核闭环执行" },
{ label: "命中规则", value: "nmpa_ivd_registration_v1 / ivd_strict_consistency_v1" },
{ label: "工具链路", value: "资料扫描 -> 完整性检查 -> 字段抽取 -> 一致性 -> 风险汇总" },
{ label: "人工复核点", value: "储存条件字段 / 产品名称冲突 / CH1 缺失项确认" }
],
pagePanels: {
completeness: {
question: "为什么 CH1.11.4 会被判定为缺失?",
answer: "Agent 先读取目录汇总结果,再将 CH1 章节与法规规则包逐项匹配。规则要求 CH1.11.4 属于必交声明类资料但当前资料包未识别到有效命中文档因此判定为缺失。RAG 证据仅用于引用法规原文,不改变规则结论。",
trace: [
"规则命中CH1.11.4 -> 必交项 -> 默认高风险",
"证据来源:法规要求说明 / CH1 监管信息章节",
"建议动作:补充资料后重跑完整性检查"
],
actions: ["确认缺失项", "标记疑似命中", "发起补件通知"]
},
fields: {
question: "储存条件为什么是待复核,而不是直接回填?",
answer: "该字段来自申请表表格抽取原文字段表述较模糊和说明书标准口径未形成稳定一对一映射。Agent 将其保留在字段池中,但置信度标记为低,并要求人工确认后才能进入正式导出。",
trace: [
"抽取方式:表格抽取 + 标准化",
"字段状态manual_review_required = true",
"影响范围:正式导出时触发阻断"
],
actions: ["采用推荐值", "驳回推荐值", "重新抽取"]
},
consistency: {
question: "产品名称冲突为什么会被上升为混档风险?",
answer: "因为当前审核范围内的说明书与申请表属于同一批次业务资料,而强一致规则要求产品名称必须完全一致。两份文档标准化后仍不一致,因此 Agent 将其识别为字段冲突,并进一步提示疑似跨产品资料混入。",
trace: [
"强一致字段product_name",
"来源文档:目标产品说明书 / CH1.4 申请表",
"处理建议:先确认审核范围,再继续导出"
],
actions: ["确认混档风险", "排除文档后重跑", "采用说明书版本"]
},
risk: {
question: "为什么最终是不通过,而不是待复核?",
answer: "风险规则中配置了 high_risk_policy = fail。当前批次同时存在缺失必交资料和产品名称冲突两个高风险项因此 Agent 直接给出不通过结论,而不是仅提示待复核。",
trace: [
"风险规则ivd_registration_risk_v1",
"高风险项:缺失必交资料 / 产品名称冲突",
"下游影响:正式导出阻断 / 飞书通知责任人"
],
actions: ["确认风险结论", "生成整改任务", "推送飞书通知"]
},
word: {
question: "为什么只能生成草稿版,不能正式导出?",
answer: "Word 导出步骤会读取一致性核查和风险预警结果。当前批次存在高风险和待复核字段因此回填拦截检查命中正式导出阻断条件Agent 只允许生成带审阅用途的草稿版。",
trace: [
"模板registration_application_form_v1",
"阻断原因:高风险 + 冲突字段 + 待复核字段",
"导出策略allow_draft_when_blocked = true"
],
actions: ["生成草稿", "查看阻断详情", "整改后重试"]
},
notification: {
question: "飞书通知里为什么要同时 @ 两个责任人?",
answer: "因为责任人映射同时命中了章节缺失和风险类型冲突两类规则。CH1 缺失项对应注册申报负责人,产品名称冲突对应注册资料负责人,所以 Agent 在同一张消息卡片中同时 @ 两位责任人,并附上 Web 详情入口。",
trace: [
"责任人映射CH1 -> 注册资料负责人",
"风险映射missing_required_document -> 注册申报负责人",
"通知载荷interactive_card + Web detail URL"
],
actions: ["发送通知", "切换消息模板", "查看回执日志"]
}
}
},
governance: [
{
id: "rules",
title: "法规规则包",
intro: "维护完整性检查、强一致规则和风险映射的结构化规则包。",
actions: ["新增规则包", "编辑规则包", "复制新版本", "启用/停用", "删除草稿"],
columns: ["名称", "适用流程", "版本", "状态", "最近更新时间", "维护人"],
rows: [
["nmpa_ivd_registration_v1", "registration", "2026-06-03", "启用中", "2026-06-03 09:00", "法规管理员"],
["ivd_strict_consistency_v1", "registration", "2026-06-03", "启用中", "2026-06-03 09:15", "数据治理专员"],
],
detailTitle: "CH1 章节要求详情",
detailBody: "章 -> 条 -> 要求项 -> 模板字段四级结构已建立。CH1.11.4 当前被标记为必交项,风险等级默认高。"
},
{
id: "sources",
title: "RAG 文档源",
intro: "维护法规原文、模板文档和业务资料的向量入库来源。",
actions: ["上传文档源", "替换版本", "编辑元数据", "停用文档源", "删除失效源", "重新入库"],
columns: ["文档名称", "类别", "版本", "状态", "切片数", "最近同步"],
rows: [
["体外诊断试剂注册申报资料要求及说明.doc", "法规", "2026-06-03", "已入库", "124", "2026-06-03 08:40"],
["目标产品说明书.docx", "业务资料", "演示版", "已入库", "36", "2026-06-03 09:20"],
],
detailTitle: "文档源详情",
detailBody: "支持查看版本、切片数量、召回命中场景和重新入库入口。HTML 中以详情侧栏模拟。"
},
{
id: "chunks",
title: "RAG 切片",
intro: "管理法规与业务文档切片,支持查看、编辑、合并、拆分和删除。",
actions: ["新增手工切片", "编辑切片", "合并切片", "拆分切片", "删除切片", "重建向量", "调整阈值"],
columns: ["切片ID", "所属文档", "章节", "摘要", "长度", "状态"],
rows: [
["chunk-001", "法规要求说明", "CH1", "声明类资料提交要求", "438", "可召回"],
["chunk-104", "目标产品说明书", "适用范围", "定性检测场景描述", "286", "可召回"],
],
detailTitle: "切片预览",
detailBody: "切片预览区显示原文片段、命中历史、召回阈值和适用场景。"
},
{
id: "schema",
title: "字段 Schema",
intro: "维护统一字段池标准、是否可回填和是否强一致。",
actions: ["新增字段", "编辑字段", "启停字段", "删除草稿", "复制版本"],
columns: ["字段编码", "中文名", "字段类型", "可回填", "强一致", "状态"],
rows: [
["product_name", "产品名称", "string", "是", "是", "启用中"],
["storage_condition", "储存条件", "text", "是", "否", "启用中"],
],
detailTitle: "字段详情",
detailBody: "详情区展示字段来源优先级、页面使用范围和回填约束。"
},
{
id: "template",
title: "Word 模板与字段映射",
intro: "维护申报表格和对照清单模板,以及占位符到字段池的映射关系。",
actions: ["上传模板", "编辑模板", "编辑占位符映射", "启用/停用版本", "删除未发布版本", "预览模板"],
columns: ["模板名称", "输出类型", "版本", "占位符数", "状态", "更新时间"],
rows: [
["registration_application_form_v1", "application_form", "2026-06-03", "24", "启用中", "2026-06-03 09:45"],
["registration_checklist_v1", "checklist", "2026-06-03", "18", "草稿", "2026-06-03 09:50"],
],
detailTitle: "映射详情",
detailBody: "支持查看占位符、必填标记、阻断条件影响和模板预览。"
},
{
id: "owner",
title: "责任人映射",
intro: "按章节或风险类型维护责任角色与飞书账号映射。",
actions: ["新增映射", "编辑映射", "启停映射", "删除映射", "批量导入"],
columns: ["映射类型", "章节/风险类型", "责任角色", "飞书用户ID", "状态", "备注"],
rows: [
["章节", "CH1", "注册资料负责人", "ou_demo_owner", "启用中", "监管信息章"],
["风险类型", "missing_required_document", "注册申报负责人", "ou_demo_registration_owner", "启用中", "缺失资料通知"],
],
detailTitle: "映射影响范围",
detailBody: "点击后联动查看会影响哪些风险项与飞书 @ 标签。"
},
{
id: "feishu",
title: "飞书通知配置",
intro: "维护飞书群聊、机器人、消息模板和 Web 详情链接模板。",
actions: ["新增配置", "编辑配置", "切换消息模板", "启用/停用", "删除草稿", "发送测试消息"],
columns: ["配置名称", "群聊/机器人", "消息模板", "Web链接模板", "状态", "测试结果"],
rows: [
["风控通知卡片", "oc_demo_chat", "interactive_card", "/audit/{batch_id}/", "启用中", "最近测试成功"],
["导出状态通知", "oc_demo_chat", "summary_card", "/documents/exports/{file_id}/", "草稿", "未测试"],
],
detailTitle: "消息模板预览",
detailBody: "可查看 interactive card 预览、发送回执和失败日志。"
}
]
};
const navConfig = [
{ id: "workspace", name: "审核任务工作台", subtitle: "7 个流程任务卡片和执行状态" },
{ id: "knowledge", name: "知识库管理页", subtitle: "手动上传资料、入库状态、治理动作" },
{ id: "import", name: "资料包导入页", subtitle: "上传、目录、页数、章节点" },
{ id: "completeness", name: "法规完整性检查页", subtitle: "缺失项、错放项、法规依据" },
{ id: "fields", name: "字段抽取与字段池页", subtitle: "字段表、来源、置信度、待复核" },
{ id: "consistency", name: "一致性核查页", subtitle: "冲突字段、来源对比、混档风险" },
{ id: "risk", name: "风险预警页", subtitle: "总风险等级、是否通过、整改建议" },
{ id: "word", name: "Word 回填导出页", subtitle: "回填字段、拦截项、导出状态" },
{ id: "notification", name: "飞书通知视图", subtitle: "消息卡片、责任人 @、Web 详情链接" },
];
const pagesRoot = document.getElementById("pages");
const navRoot = document.getElementById("navList");
const governanceTabsRoot = document.getElementById("governanceTabs");
const governancePanelsRoot = document.getElementById("governancePanels");
const pageTitle = document.getElementById("pageTitle");
const governanceDrawer = document.getElementById("governanceDrawer");
const overlay = document.getElementById("overlay");
let activePageId = "workspace";
function metricCards(items) {
return `<div class="metric-grid">${items.map(item => `
<div class="metric-card">
<div class="muted">${item.label}</div>
<strong>${item.value}</strong>
<div class="muted" style="margin-top: 8px;">${item.note || ""}</div>
</div>
`).join("")}</div>`;
}
function renderAgentConversation(messages) {
return `
<div class="conversation-board">
${messages.map(message => `
<div class="message ${message.role}">
<div class="message-head">
<strong>${message.title}</strong>
<span class="tag">${message.meta}</span>
</div>
<div class="message-body">
${message.body.map(line => `<p>${line}</p>`).join("")}
</div>
</div>
`).join("")}
</div>
`;
}
function renderTraceItems(items) {
return `
<div class="trace-grid">
${items.map(item => `
<div class="trace-item">
<strong>${item.label || item}</strong>
${item.value ? `<div class="muted" style="color:#5f7285;">${item.value}</div>` : ""}
</div>
`).join("")}
</div>
`;
}
function renderAgentSidePanel(key, title) {
const panel = data.agent.pagePanels[key];
if (!panel) return "";
return `
<div class="page-agent-side">
<div class="panel">
<div class="section-title">
<div>
<h3>Agent 页面追问</h3>
<p>${title}中的解释型对话与证据追溯。</p>
</div>
</div>
<div class="agent-mini-panel">
<div class="message user">
<div class="message-head">
<strong>追问</strong>
<span class="tag">用户</span>
</div>
<div class="message-body">
<p>${panel.question}</p>
</div>
</div>
<div class="message agent">
<div class="message-head">
<strong>Agent 解释</strong>
<span class="tag">当前页面上下文</span>
</div>
<div class="message-body">
<p>${panel.answer}</p>
</div>
</div>
</div>
</div>
<div class="panel">
<div class="section-title">
<div>
<h3>执行轨迹</h3>
<p>说明这个结论是怎么来的。</p>
</div>
</div>
<div class="agent-q-list">
${panel.trace.map(item => `<div class="agent-q-item">${item}</div>`).join("")}
</div>
<div class="review-actions">
${panel.actions.map((action, index) => `<button class="review-btn ${index === 0 ? "primary" : ""}">${action}</button>`).join("")}
</div>
</div>
</div>
`;
}
function renderProcessFlow(flowKey) {
const items = data.processFlows[flowKey] || [];
return `
<div class="phase-board">
${items.map(item => `
<div class="phase-card ${item.state}">
<div class="phase-head">
<strong>${item.title}</strong>
<span class="phase-state">${item.stateText}</span>
</div>
<span class="tag ${item.state === "blocked" ? "high" : item.state === "running" ? "medium" : item.state === "completed" ? "low" : ""}">
${data.taskStatusMap[item.state] || item.stateText}
</span>
<div class="phase-note">${item.note}</div>
</div>
`).join("")}
</div>
`;
}
function buildLiveTasks(currentPageId) {
return data.tasks.map(task => {
const flow = data.processFlows[task.id];
if (!flow) {
return task;
}
const blocked = flow.find(item => item.state === "blocked");
const running = currentPageId === task.id ? flow.find(item => item.state === "running") : null;
const pendingCount = flow.filter(item => item.state === "pending").length;
const completedCount = flow.filter(item => item.state === "completed").length;
if (blocked) {
return {
...task,
status: "已阻断",
metric: blocked.stateText,
detail: blocked.note
};
}
if (running) {
return {
...task,
status: running.stateText,
metric: `${completedCount}/${flow.length} 已完成`,
detail: running.note
};
}
if (pendingCount === 0) {
return {
...task,
status: "已完成",
metric: `${flow.length}/${flow.length} 已完成`,
detail: task.detail
};
}
return {
...task,
status: "待执行",
metric: `${completedCount}/${flow.length} 已完成`,
detail: task.detail
};
});
}
function renderWorkspacePage() {
const liveTasks = buildLiveTasks(activePageId);
return `
<section class="page active" data-page="workspace">
<div class="hero">
<div class="panel">
<div class="section-title">
<div>
<h3>审核任务工作台</h3>
<p>先用一页看清当前批次进展、阻断点和推荐下一步。</p>
</div>
<span class="tag high">总体结论:${data.batch.passStatus}</span>
</div>
<div class="lead">
当前批次 <strong>${data.batch.id}</strong> 已完成资料包导入、完整性检查、字段抽取、一致性核查和风险预警。由于存在 CH1 必交资料缺失和产品名称冲突,系统仅允许草稿导出,不允许正式版直接出具。
</div>
<div class="chip-row" style="margin-top: 16px;">
<span class="tag">产品:${data.batch.productName}</span>
<span class="tag">申请人:${data.batch.applicant}</span>
<span class="tag high">风险等级:${data.batch.riskLevel}</span>
</div>
</div>
<div class="panel">
<div class="section-title">
<div>
<h3>当前批次状态</h3>
<p>${data.batch.stage}</p>
</div>
</div>
<div class="card-stack">
<div class="alert danger">CH1.11.4 缺失必交资料,完整性检查判定为高风险。</div>
<div class="alert warning">说明书与申请表产品名称不一致,存在混档风险。</div>
<div class="alert success">Word 草稿版已允许生成,可用于内部校对。</div>
</div>
</div>
</div>
${metricCards(data.metrics)}
<div class="panel">
<div class="section-title">
<div>
<h3>七个流程任务卡片</h3>
<p>点击卡片切换流程页面后,状态会实时更新为当前动作。</p>
</div>
</div>
<div class="task-grid">
${liveTasks.map(task => `
<div class="task-card ${task.id === activePageId ? "active" : ""}" data-page-jump="${task.id}">
<div class="task-status-line">
<span class="tag ${task.status === "已阻断" ? "high" : task.status.includes("中") ? "medium" : ""}">${task.status}</span>
<span class="muted">${task.metric}</span>
</div>
<h4>${task.name}</h4>
<div class="task-note">${task.detail}</div>
<div class="task-meta">
<span>进入流程</span>
<span>查看详情</span>
</div>
</div>
`).join("")}
</div>
</div>
<div class="two-col">
<div class="panel">
<div class="section-title">
<div>
<h3>风险总览</h3>
<p>把缺失、冲突、待复核和导出阻断统一到一处。</p>
</div>
</div>
<table>
<thead>
<tr>
<th>风险类型</th>
<th>等级</th>
<th>问题描述</th>
<th>责任角色</th>
</tr>
</thead>
<tbody>
${data.risk.items.map(item => `
<tr>
<td>${item.type}</td>
<td><span class="tag ${item.level === "高" ? "high" : item.level === "中" ? "medium" : "low"}">${item.level}</span></td>
<td>${item.desc}</td>
<td>${item.owner}</td>
</tr>
`).join("")}
</tbody>
</table>
</div>
<div class="panel">
<div class="section-title">
<div>
<h3>推荐下一步</h3>
<p>演示时建议按这组顺序点击。</p>
</div>
</div>
<div class="check-list">
<div class="check-item">1. 返回资料包导入页解释目录与页数识别。</div>
<div class="check-item">2. 进入法规完整性检查页展示 CH1 缺失项和法规依据。</div>
<div class="check-item">3. 打开字段池页,说明结构化字段如何沉淀。</div>
<div class="check-item">4. 打开风险预警页,解释为什么正式导出被拦截。</div>
</div>
</div>
</div>
<div class="workspace-main">
<div class="agent-shell">
<div class="panel">
<div class="section-title">
<div>
<h3>Agent 主对话区</h3>
<p>体现用户指令、Agent 计划、工具调用和结构化结论的多轮协作过程。</p>
</div>
</div>
<div class="prompt-bar">
${data.agent.quickPrompts.map(prompt => `<span class="prompt-chip">${prompt}</span>`).join("")}
</div>
<div style="margin-top:12px;">
${renderAgentConversation(data.agent.workspaceConversation)}
</div>
</div>
</div>
<div class="page-agent-side">
<div class="panel">
<div class="section-title">
<div>
<h3>Agent 执行轨迹</h3>
<p>把这次审核从“聊天”升级为“可追踪执行”。</p>
</div>
</div>
${renderTraceItems(data.agent.workspaceTrace)}
</div>
<div class="panel">
<div class="section-title">
<div>
<h3>人工复核协同</h3>
<p>支持人机共审,而不是单向回答。</p>
</div>
</div>
<div class="review-actions">
<button class="review-btn primary">确认当前结论</button>
<button class="review-btn">采用说明书值</button>
<button class="review-btn">重新执行一致性核查</button>
<button class="review-btn">生成整改任务</button>
</div>
</div>
</div>
</div>
</section>
`;
}
function renderKnowledgePage() {
return `
<section class="page" data-page="knowledge">
${metricCards(data.knowledgeBase.summary)}
<div class="hero">
<div class="panel">
<div class="section-title">
<div>
<h3>知识库管理</h3>
<p>手动上传法规、模板、示例资料和内部业务知识,统一进入 Agent 可用知识库。</p>
</div>
<button class="secondary-btn" data-open-governance="sources">打开治理中心</button>
</div>
<div class="upload-zone">
<strong>手动上传知识库资料</strong>
<div class="lead">支持批量上传 <code>pdf / docx / doc / zip</code>,并按“法规资料 / 模板资料 / 示例资料 / 内部 SOP”进行分类。</div>
<div class="split-actions">
<button class="primary-btn">上传法规资料</button>
<button class="secondary-btn">上传业务资料</button>
<button class="secondary-btn">导入模板压缩包</button>
</div>
</div>
</div>
<div class="panel">
<div class="section-title">
<div>
<h3>入库处理状态</h3>
<p>通过卡片实时展示每份资料的处理动作。</p>
</div>
</div>
<div class="upload-list">
${data.knowledgeBase.uploadQueue.map(item => `
<div class="upload-item">
<div>
<strong>${item.name}</strong>
<div class="muted">${item.type} · 责任人:${item.owner}<br />${item.detail}</div>
</div>
<span class="tag ${item.state.includes("处理中") || item.state.includes("解析中") ? "medium" : item.state.includes("完成") ? "low" : ""}">${item.state}</span>
</div>
`).join("")}
</div>
</div>
</div>
<div class="panel">
<div class="section-title">
<div>
<h3>知识库能力卡片</h3>
<p>原有治理内容收拢为卡片式入口,适合演示 Agent 依赖的知识管理能力。</p>
</div>
</div>
<div class="knowledge-grid">
${data.knowledgeBase.cards.map(card => `
<div class="knowledge-card">
<div class="muted">${card.title}</div>
<div class="knowledge-stat">${card.value}</div>
<p>${card.desc}</p>
<div class="knowledge-actions">
${card.actions.map(action => `<button class="mini-btn">${action}</button>`).join("")}
</div>
</div>
`).join("")}
</div>
</div>
<div class="two-col" style="margin-top: 18px;">
<div class="panel">
<div class="section-title">
<div>
<h3>入库动作流</h3>
<p>模拟上传后的后台流水线动作。</p>
</div>
</div>
${renderProcessFlow("import")}
</div>
<div class="panel">
<div class="section-title">
<div>
<h3>Agent 如何使用知识库</h3>
<p>上传并入库后,后续页面会自动引用这些知识进行解释和核查。</p>
</div>
</div>
<div class="check-list">
<div class="check-item">法规资料进入 RAG 文档源,用于完整性检查时的法规依据解释。</div>
<div class="check-item">模板资料进入模板映射中心,用于 Word 回填与导出拦截。</div>
<div class="check-item">业务示例进入示例知识库,用于字段抽取提示和冲突解释增强。</div>
<div class="check-item">责任人和通知配置会驱动飞书通知中的 @ 与 Web 详情链接。</div>
</div>
</div>
</div>
</section>
`;
}
function renderImportPage() {
return `
<section class="page" data-page="import">
${metricCards([
{ label: "导入文件数", value: "18", note: "含压缩包解包后文件" },
{ label: "已识别页数", value: "236", note: "DOCX / PDF 精确统计" },
{ label: "章节点命中", value: "14", note: "覆盖 CH1 ~ CH6" },
{ label: "待复核异常", value: "3", note: "DOC 页数 / 错放 / OCR" },
])}
<div class="tri-col">
<div class="panel">
<div class="section-title">
<div>
<h3>资料包导入入口</h3>
<p>支持批量文件、文件夹和压缩包导入。</p>
</div>
</div>
<div class="card-stack">
<div class="info-card">
<strong>拖拽上传区</strong>
<div class="lead" style="margin-top: 8px;">支持 <code>pdf / docx / doc / zip / rar / 7z</code>,保留压缩包内原始相对路径。</div>
</div>
<div class="split-actions">
<button class="primary-btn">上传批量文件</button>
<button class="secondary-btn">导入压缩包</button>
</div>
</div>
</div>
<div class="panel">
<div class="section-title">
<div>
<h3>处理流水线</h3>
<p>进入本流程后,状态卡片会切换到当前执行动作。</p>
</div>
</div>
${renderProcessFlow("import")}
</div>
<div class="panel">
<div class="section-title">
<div>
<h3>异常与待复核箱</h3>
<p>把资料问题前置暴露。</p>
</div>
</div>
<div class="card-stack">
<div class="alert warning">1 份 DOC 页数无法精确统计,需人工复核。</div>
<div class="alert danger">1 份沟通记录疑似目录错放。</div>
<div class="alert warning">1 份扫描件建议补 OCR 或手工确认文本质量。</div>
</div>
</div>
</div>
<div class="split" style="margin-top: 18px;">
<div class="panel">
<div class="section-title">
<div>
<h3>目录树</h3>
<p>点击节点可展开目录层级。</p>
</div>
<button class="secondary-btn" data-open-governance="sources">查看 RAG 文档源</button>
</div>
<div class="tree">
<div class="tree-node expanded">
<button type="button" class="tree-toggle">▾ 第1章 监管信息</button>
<div class="tree-children" style="display:grid;">
<div class="tree-node-child">CH1.2 监管信息目录.docx</div>
<div class="tree-node-child">CH1.4 申请表.docx</div>
<div class="tree-node-child">CH1.9 产品申报前沟通的说明.doc</div>
</div>
</div>
<div class="tree-node">
<button type="button" class="tree-toggle">▸ 第3章 产品技术要求与说明书</button>
<div class="tree-children">
<div class="tree-node-child">目标产品说明书.docx</div>
</div>
</div>
<div class="tree-node">
<button type="button" class="tree-toggle">▸ 第4章 性能与临床资料</button>
<div class="tree-children">
<div class="tree-node-child">临床资料包.zip</div>
</div>
</div>
</div>
</div>
<div class="panel">
<div class="section-title">
<div>
<h3>目录汇总表</h3>
<p>页面 mock 基于 registration_overview_report 渲染。</p>
</div>
</div>
<table>
<thead>
<tr>
<th>相对路径</th>
<th>类型</th>
<th>页数</th>
<th>章节点</th>
<th>状态</th>
</tr>
</thead>
<tbody>
${data.importDocs.map(doc => `
<tr>
<td>${doc.path}</td>
<td>${doc.type}</td>
<td>${doc.pages} / ${doc.confidence}</td>
<td>${doc.chapter}</td>
<td><span class="tag ${doc.status === "待复核" ? "medium" : ""}">${doc.status}</span></td>
</tr>
`).join("")}
</tbody>
</table>
</div>
</div>
</section>
`;
}
function renderCompletenessPage() {
return `
<section class="page" data-page="completeness">
${metricCards(data.completeness.summary.map(item => ({ label: item.label, value: item.value, note: "" })))}
<div class="page-agent-grid">
<div>
<div class="split">
<div class="panel">
<div class="section-title">
<div>
<h3>法规目录树</h3>
<p>按章节展示匹配情况和缺失项。</p>
</div>
<button class="secondary-btn" data-open-governance="rules">查看规则包</button>
</div>
<div class="tree">
<div class="tree-node expanded">
<button type="button" class="tree-toggle">▾ CH1 监管信息</button>
<div class="tree-children" style="display:grid;">
<div class="tree-node-child">CH1.2 已匹配</div>
<div class="tree-node-child">CH1.4 已匹配</div>
<div class="tree-node-child">CH1.9 疑似错放</div>
<div class="tree-node-child">CH1.11.4 缺失</div>
</div>
</div>
<div class="tree-node">
<button type="button" class="tree-toggle">▸ CH2 产品综述资料</button>
<div class="tree-children">
<div class="tree-node-child">待展开</div>
</div>
</div>
</div>
</div>
<div class="panel">
<div class="section-title">
<div>
<h3>缺失项 / 错放项</h3>
<p>聚焦当前最值得讲解的完整性问题。</p>
</div>
</div>
<div class="issue-list">
${data.completeness.issues.map(item => `
<div class="issue-item">
<div class="split-actions" style="justify-content: space-between;">
<strong>${item.chapter} ${item.name}</strong>
<span class="tag ${item.risk === "高" ? "high" : item.risk === "中" ? "medium" : ""}">${item.type} / ${item.risk}</span>
</div>
<div class="muted" style="margin-top: 8px; line-height: 1.7;">${item.detail}</div>
</div>
`).join("")}
</div>
</div>
</div>
<div class="two-col" style="margin-top: 18px;">
<div class="panel">
<div class="section-title">
<div>
<h3>法规依据</h3>
<p>RAG 负责给证据,规则负责做结论。</p>
</div>
<button class="secondary-btn" data-open-governance="chunks">查看切片</button>
</div>
<div class="evidence">${data.completeness.evidence}</div>
</div>
<div class="panel">
<div class="section-title">
<div>
<h3>处理建议</h3>
<p>风险等级与责任角色已结构化。</p>
</div>
</div>
<table>
<thead>
<tr>
<th>问题类型</th>
<th>等级</th>
<th>建议动作</th>
<th>责任角色</th>
</tr>
</thead>
<tbody>
<tr>
<td>缺失</td>
<td><span class="tag high">高</span></td>
<td>补充授权 / 声明资料后重跑完整性检查。</td>
<td>注册申报负责人</td>
</tr>
<tr>
<td>错放</td>
<td><span class="tag medium">中</span></td>
<td>调整 CH1.9 文档路径并重新确认目录结构。</td>
<td>注册资料负责人</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
${renderAgentSidePanel("completeness", "法规完整性检查")}
</div>
</section>
`;
}
function renderFieldsPage() {
return `
<section class="page" data-page="fields">
${metricCards(data.fields.summary.map(item => ({ label: item.label, value: item.value, note: "" })))}
<div class="page-agent-grid">
<div>
<div class="panel">
<div class="section-title">
<div>
<h3>字段池主表</h3>
<p>统一沉淀字段事实、来源证据、置信度和回填可用性。</p>
</div>
<div class="toolbar">
<span class="tag">全部字段</span>
<span class="tag">可回填</span>
<span class="tag medium">待复核</span>
<button class="secondary-btn" data-open-governance="schema">维护字段 Schema</button>
</div>
</div>
<table>
<thead>
<tr>
<th>字段编码</th>
<th>中文名</th>
<th>标准值</th>
<th>来源文档</th>
<th>抽取方式</th>
<th>置信度</th>
<th>待复核</th>
<th>可回填</th>
</tr>
</thead>
<tbody>
${data.fields.items.map(item => `
<tr>
<td>${item.key}</td>
<td>${item.label}</td>
<td>${item.value}</td>
<td>${item.source}<br><span class="muted">${item.location}</span></td>
<td>${item.method}</td>
<td><span class="tag ${item.confidence === "低" ? "medium" : ""}">${item.confidence}</span></td>
<td>${item.review}</td>
<td>${item.fillable}</td>
</tr>
`).join("")}
</tbody>
</table>
</div>
<div class="two-col" style="margin-top: 18px;">
<div class="panel">
<div class="section-title">
<div>
<h3>待复核字段</h3>
<p>突出需要人工确认的低置信度字段。</p>
</div>
</div>
<div class="alert warning">储存条件字段来自申请表表格抽取,原文为“建议冷冻保存”,标准化值为“-20℃ 以下避光保存”,需人工确认。</div>
</div>
<div class="panel">
<div class="section-title">
<div>
<h3>来源证据预览</h3>
<p>说明字段不是无来源生成。</p>
</div>
</div>
<div class="evidence">
来源文档:目标产品说明书.docx<br />
章节:一、产品名称<br />
抽取规则:标题规则 + 正文标准化<br />
证据片段:“产品名称:新型冠状病毒 2019-nCoV 核酸检测试剂盒”
</div>
</div>
</div>
</div>
${renderAgentSidePanel("fields", "字段抽取与字段池")}
</div>
</section>
`;
}
function renderConsistencyPage() {
return `
<section class="page" data-page="consistency">
${metricCards(data.consistency.summary.map(item => ({ label: item.label, value: item.value, note: "" })))}
<div class="page-agent-grid">
<div>
<div class="two-col">
<div class="panel">
<div class="section-title">
<div>
<h3>冲突字段主表</h3>
<p>对同一审核范围内的字段做严格一致性比对。</p>
</div>
<button class="secondary-btn" data-open-governance="rules">维护强一致规则</button>
</div>
<table>
<thead>
<tr>
<th>字段名</th>
<th>结果</th>
<th>冲突值</th>
<th>来源文档</th>
<th>风险等级</th>
<th>混档风险</th>
</tr>
</thead>
<tbody>
${data.consistency.conflicts.map(item => `
<tr>
<td>${item.field}</td>
<td><span class="tag ${item.result === "冲突" ? "high" : "medium"}">${item.result}</span></td>
<td>${item.values}</td>
<td>${item.docs}</td>
<td>${item.risk}</td>
<td>${item.mixed}</td>
</tr>
`).join("")}
</tbody>
</table>
</div>
<div class="panel">
<div class="section-title">
<div>
<h3>来源对比视图</h3>
<p>点击字段可联动来源比对,此处展示产品名称冲突。</p>
</div>
</div>
<div class="card-stack">
${data.consistency.compare.map(item => `
<div class="info-card">
<strong>${item.doc}</strong>
<div class="muted" style="margin-top: 8px;">${item.chapter} · ${item.page}</div>
<div style="margin-top: 10px; line-height: 1.7;">原始值:${item.value}</div>
<div class="muted" style="margin-top: 8px;">标准化:${item.normalized}</div>
</div>
`).join("")}
</div>
</div>
</div>
<div class="panel" style="margin-top: 18px;">
<div class="section-title">
<div>
<h3>混档风险提示</h3>
<p>把字段冲突上升为可执行的风险结论。</p>
</div>
</div>
<div class="alert danger">说明书与申请表中的产品名称存在文本不一致,系统判定为疑似跨产品资料混入,建议先确认当前审核范围再继续回填和导出。</div>
</div>
</div>
${renderAgentSidePanel("consistency", "一致性核查")}
</div>
</section>
`;
}
function renderRiskPage() {
return `
<section class="page" data-page="risk">
${metricCards(data.risk.summary.map(item => ({ label: item.label, value: item.value, note: "" })))}
<div class="page-agent-grid">
<div>
<div class="hero">
<div class="panel">
<div class="section-title">
<div>
<h3>综合风险结论</h3>
<p>把完整性、字段抽取和一致性问题统一归并。</p>
</div>
<span class="tag high">是否通过:${data.batch.passStatus}</span>
</div>
<div class="card-stack">
<div class="alert danger">最高风险等级:高。正式版导出被阻断。</div>
<div class="alert warning">当前需优先处理 CH1 缺失资料与产品名称冲突问题。</div>
<div class="alert success">可先生成草稿版供内部核对使用。</div>
</div>
</div>
<div class="panel">
<div class="section-title">
<div>
<h3>责任角色分布</h3>
<p>风险项已经绑定责任角色,便于协同。</p>
</div>
<button class="secondary-btn" data-open-governance="owner">维护责任人映射</button>
</div>
<div class="check-list">
<div class="check-item">注册申报负责人2 个待处理高风险</div>
<div class="check-item">注册资料负责人2 个待处理问题</div>
<div class="check-item">临床注册负责人0 个高优先级问题</div>
</div>
</div>
</div>
<div class="panel">
<div class="section-title">
<div>
<h3>风险清单</h3>
<p>适合演示时逐条解释“问题 -> 依据 -> 动作 -> 责任人”。</p>
</div>
</div>
<table>
<thead>
<tr>
<th>风险类型</th>
<th>等级</th>
<th>问题描述</th>
<th>来源报告</th>
<th>整改建议</th>
<th>责任角色</th>
</tr>
</thead>
<tbody>
${data.risk.items.map(item => `
<tr>
<td>${item.type}</td>
<td><span class="tag ${item.level === "高" ? "high" : item.level === "中" ? "medium" : "low"}">${item.level}</span></td>
<td>${item.desc}</td>
<td>${item.source}</td>
<td>${item.advice}</td>
<td>${item.owner}</td>
</tr>
`).join("")}
</tbody>
</table>
</div>
</div>
${renderAgentSidePanel("risk", "风险预警")}
</div>
</section>
`;
}
function renderWordPage() {
return `
<section class="page" data-page="word">
${metricCards([
{ label: "模板版本", value: data.word.version, note: data.word.template },
{ label: "已回填字段", value: "4 / 5", note: "重点字段已生成草稿" },
{ label: "阻断项", value: "3", note: "高风险 / 冲突 / 待复核" },
{ label: "导出状态", value: "草稿已生成", note: "正式版被阻断" },
])}
<div class="page-agent-grid">
<div>
<div class="split">
<div class="panel">
<div class="section-title">
<div>
<h3>回填字段表</h3>
<p>从字段池到模板占位符的映射一目了然。</p>
</div>
<button class="secondary-btn" data-open-governance="template">维护模板与映射</button>
</div>
<table>
<thead>
<tr>
<th>占位符</th>
<th>字段名</th>
<th>字段值</th>
<th>来源</th>
<th>状态</th>
<th>必填</th>
</tr>
</thead>
<tbody>
${data.word.filled.map(item => `
<tr>
<td>${item.placeholder}</td>
<td>${item.field}</td>
<td>${item.value}</td>
<td>${item.source}</td>
<td><span class="tag low">${item.status}</span></td>
<td>${item.required}</td>
</tr>
`).join("")}
</tbody>
</table>
</div>
<div class="panel">
<div class="section-title">
<div>
<h3>拦截项</h3>
<p>说明为什么正式导出没有直接放行。</p>
</div>
</div>
<div class="card-stack">
${data.word.blocked.map(item => `
<div class="alert ${item.reason.includes("高风险") ? "danger" : "warning"}">
<strong>${item.reason}</strong><br />
${item.field}${item.detail}
</div>
`).join("")}
</div>
<div class="split-actions" style="margin-top: 16px;">
<button class="primary-btn" id="draftBtn">生成草稿</button>
<button class="secondary-btn" id="formalBtn">尝试正式导出</button>
</div>
</div>
</div>
<div class="panel" style="margin-top: 18px;">
<div class="section-title">
<div>
<h3>导出记录与下载入口</h3>
<p>展示输出状态、版式校验结果和下载入口。</p>
</div>
</div>
<div class="download-box" id="downloadBox">
<div>
<strong>注册申报表格_回填草稿.docx</strong>
<div class="muted" style="margin-top: 8px;">状态:草稿已生成 · 版式校验:通过 · 时间2026-06-03 10:20</div>
</div>
<button class="primary-btn">下载草稿</button>
</div>
</div>
</div>
${renderAgentSidePanel("word", "Word 回填导出")}
</div>
</section>
`;
}
function renderNotificationPage() {
return `
<section class="page" data-page="notification">
<div class="page-agent-grid">
<div>
<div class="split">
<div class="panel">
<div class="section-title">
<div>
<h3>飞书消息卡片预览</h3>
<p>把风险摘要与导出状态转成可直接发送的协同消息。</p>
</div>
<button class="secondary-btn" data-open-governance="feishu">维护飞书配置</button>
</div>
<div class="message-card">
<div class="card-head">
<div>
<div class="muted">interactive card</div>
<h4 style="margin: 8px 0 0;">批次 ${data.batch.id} 风险预警通知</h4>
</div>
<span class="tag high">${data.batch.passStatus}</span>
</div>
<div class="lead" style="margin-bottom: 14px;">
当前批次存在 CH1 必交资料缺失和产品名称冲突,已阻断正式导出,仅生成草稿版供内部复核。
</div>
<div class="list-inline" style="margin-bottom: 14px;">
${data.notification.mentions.map(item => `<span class="mention">${item}</span>`).join("")}
</div>
<div class="card-stack">
<div class="info-card">高风险CH1.11.4 缺失必交资料</div>
<div class="info-card">高风险:产品名称冲突,疑似混档</div>
<div class="info-card">导出状态:仅草稿版允许下载</div>
</div>
<div class="split-actions" style="margin-top: 18px;">
<button class="primary-btn" id="sendBtn">发送通知</button>
<button class="secondary-btn">查看 Web 详情</button>
</div>
</div>
</div>
<div class="panel">
<div class="section-title">
<div>
<h3>通知配置与回执</h3>
<p>展示责任人映射、消息模板和发送状态。</p>
</div>
</div>
<div class="card-stack">
<div class="info-card">
<strong>责任人映射</strong>
<div class="muted" style="margin-top: 8px;">注册资料负责人 -> ou_demo_owner<br />注册申报负责人 -> ou_demo_registration_owner</div>
</div>
<div class="info-card">
<strong>Web 详情链接</strong>
<div class="muted" style="margin-top: 8px;">${data.notification.webLink}</div>
</div>
<div class="alert success" id="receiptBox">
当前为未发送预览状态。点击“发送通知”后展示回执。
</div>
</div>
</div>
</div>
</div>
${renderAgentSidePanel("notification", "飞书通知视图")}
</div>
</section>
`;
}
const pageTemplates = {
workspace: renderWorkspacePage,
knowledge: renderKnowledgePage,
import: renderImportPage,
completeness: renderCompletenessPage,
fields: renderFieldsPage,
consistency: renderConsistencyPage,
risk: renderRiskPage,
word: renderWordPage,
notification: renderNotificationPage,
};
function renderNav() {
navRoot.innerHTML = navConfig.map(item => `
<button class="nav-btn ${item.id === "workspace" ? "active" : ""}" data-page-target="${item.id}">
${item.name}
<small>${item.subtitle}</small>
</button>
`).join("");
}
function renderPages() {
pagesRoot.innerHTML = Object.entries(pageTemplates)
.map(([id, tpl]) => tpl(id))
.join("");
}
function renderGovernance() {
governanceTabsRoot.innerHTML = data.governance.map((item, idx) => `
<button class="governance-tab ${idx === 0 ? "active" : ""}" data-governance-target="${item.id}">${item.title}</button>
`).join("");
governancePanelsRoot.innerHTML = data.governance.map((item, idx) => `
<section class="governance-panel ${idx === 0 ? "active" : ""}" data-governance-panel="${item.id}">
<div class="panel" style="padding: 18px;">
<div class="section-title">
<div>
<h4>${item.title}</h4>
<p>${item.intro}</p>
</div>
</div>
<div class="crud-bar">
${item.actions.map(action => `<button class="mini-btn">${action}</button>`).join("")}
</div>
<table>
<thead>
<tr>${item.columns.map(col => `<th>${col}</th>`).join("")}</tr>
</thead>
<tbody>
${item.rows.map(row => `<tr>${row.map(value => `<td>${value}</td>`).join("")}</tr>`).join("")}
</tbody>
</table>
</div>
<div class="panel" style="padding: 18px;">
<div class="section-title">
<div>
<h4>${item.detailTitle}</h4>
<p>详情区 / 编辑抽屉 / 配置弹窗的 mock 内容</p>
</div>
</div>
<div class="evidence">${item.detailBody}</div>
</div>
</section>
`).join("");
}
function setActivePage(pageId) {
activePageId = pageId;
renderPages();
document.querySelectorAll(".page").forEach(page => {
page.classList.toggle("active", page.dataset.page === pageId);
});
document.querySelectorAll(".nav-btn").forEach(btn => {
btn.classList.toggle("active", btn.dataset.pageTarget === pageId);
});
document.querySelectorAll(".task-card").forEach(card => {
card.classList.toggle("active", card.dataset.pageJump === pageId);
});
const match = navConfig.find(item => item.id === pageId);
pageTitle.textContent = match ? match.name : "注册审核平台";
window.scrollTo({ top: 0, behavior: "smooth" });
}
function openGovernance(targetId) {
governanceDrawer.classList.add("open");
overlay.classList.add("show");
if (targetId) {
document.querySelectorAll(".governance-tab").forEach(tab => {
tab.classList.toggle("active", tab.dataset.governanceTarget === targetId);
});
document.querySelectorAll(".governance-panel").forEach(panel => {
panel.classList.toggle("active", panel.dataset.governancePanel === targetId);
});
}
}
function closeGovernance() {
governanceDrawer.classList.remove("open");
overlay.classList.remove("show");
}
renderNav();
renderPages();
renderGovernance();
document.addEventListener("click", (event) => {
const pageBtn = event.target.closest("[data-page-target]");
if (pageBtn) {
setActivePage(pageBtn.dataset.pageTarget);
}
const jumpCard = event.target.closest("[data-page-jump]");
if (jumpCard) {
setActivePage(jumpCard.dataset.pageJump);
}
const treeToggle = event.target.closest(".tree-toggle");
if (treeToggle) {
treeToggle.parentElement.classList.toggle("expanded");
const expanded = treeToggle.parentElement.classList.contains("expanded");
treeToggle.textContent = treeToggle.textContent.replace(expanded ? "▸" : "▾", expanded ? "▾" : "▸");
}
const govBtn = event.target.closest("[data-open-governance]");
if (govBtn) {
openGovernance(govBtn.dataset.openGovernance);
}
const govTab = event.target.closest("[data-governance-target]");
if (govTab) {
const id = govTab.dataset.governanceTarget;
document.querySelectorAll(".governance-tab").forEach(tab => {
tab.classList.toggle("active", tab.dataset.governanceTarget === id);
});
document.querySelectorAll(".governance-panel").forEach(panel => {
panel.classList.toggle("active", panel.dataset.governancePanel === id);
});
}
if (event.target.closest("#draftBtn")) {
document.getElementById("downloadBox").innerHTML = `
<div>
<strong>注册申报表格_回填草稿_v2.docx</strong>
<div class="muted" style="margin-top: 8px;">状态:草稿已重新生成 · 版式校验:通过 · 时间2026-06-03 10:42</div>
</div>
<button class="primary-btn">下载新草稿</button>
`;
}
if (event.target.closest("#formalBtn")) {
alert("正式导出已被阻断:当前批次存在高风险和冲突字段,请先完成整改。");
}
if (event.target.closest("#sendBtn")) {
document.getElementById("receiptBox").className = "alert success";
document.getElementById("receiptBox").innerHTML = `
已发送至飞书群聊 oc_demo_chat<br />
message_id${data.notification.receipt.id}<br />
sent_at${data.notification.receipt.time}
`;
}
});
document.getElementById("openDrawerBtn").addEventListener("click", () => openGovernance("rules"));
document.getElementById("closeDrawerBtn").addEventListener("click", closeGovernance);
overlay.addEventListener("click", closeGovernance);
</script>
</body>
</html>