2309 lines
84 KiB
HTML
2309 lines
84 KiB
HTML
<!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-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;
|
||
}
|
||
|
||
.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 {
|
||
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 {
|
||
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: "正式版被高风险与冲突字段拦截" },
|
||
],
|
||
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: "是" },
|
||
],
|
||
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: "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");
|
||
|
||
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 renderWorkspacePage() {
|
||
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">
|
||
${data.tasks.map(task => `
|
||
<div class="task-card ${task.id === "risk" ? "active" : ""}" data-page-jump="${task.id}">
|
||
<span class="tag ${task.status === "已阻断" ? "high" : ""}">${task.status}</span>
|
||
<h4>${task.name}</h4>
|
||
<p>${task.detail}</p>
|
||
<div class="task-meta">
|
||
<span>${task.metric}</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 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>
|
||
<div class="timeline">
|
||
${data.importTimeline.map((item, idx) => `
|
||
<div class="timeline-step">
|
||
<div class="step-no">${idx + 1}</div>
|
||
<div>
|
||
<strong>${item.step}</strong>
|
||
<div class="muted" style="margin-top: 6px;">${item.detail}</div>
|
||
</div>
|
||
<span class="tag ${item.status.includes("部分") ? "medium" : ""}">${item.status}</span>
|
||
</div>
|
||
`).join("")}
|
||
</div>
|
||
</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,
|
||
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) {
|
||
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);
|
||
});
|
||
}
|
||
});
|
||
|
||
document.getElementById("openDrawerBtn").addEventListener("click", () => openGovernance("rules"));
|
||
document.getElementById("closeDrawerBtn").addEventListener("click", closeGovernance);
|
||
overlay.addEventListener("click", closeGovernance);
|
||
|
||
document.getElementById("draftBtn")?.addEventListener("click", () => {
|
||
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>
|
||
`;
|
||
});
|
||
|
||
document.getElementById("formalBtn")?.addEventListener("click", () => {
|
||
alert("正式导出已被阻断:当前批次存在高风险和冲突字段,请先完成整改。");
|
||
});
|
||
|
||
document.getElementById("sendBtn")?.addEventListener("click", () => {
|
||
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}
|
||
`;
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|