feat: update audit platform workspace

This commit is contained in:
wren
2026-05-25 09:50:01 +08:00
parent ba8e93c0d3
commit 68d0b4c878
73 changed files with 12196 additions and 367 deletions
@@ -0,0 +1,971 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>评查详情纵向抽屉原型</title>
<style>
:root {
--primary: #00684a;
--primary-soft: rgba(0, 104, 74, 0.08);
--border: #d9e1e7;
--text: #172033;
--muted: #64748b;
--bg: #eef2f5;
--panel: #ffffff;
--danger: #dc2626;
--warn: #d97706;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
color: var(--text);
background: var(--bg);
}
button {
font: inherit;
}
.app {
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.topbar {
height: 52px;
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 16px;
padding: 0 18px;
border-bottom: 1px solid var(--border);
background: var(--panel);
}
.title {
min-width: 0;
flex: 1;
}
.title strong {
display: block;
font-size: 15px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.title span {
display: block;
margin-top: 2px;
font-size: 11px;
color: var(--muted);
}
.mode-switch {
display: inline-flex;
padding: 3px;
border: 1px solid var(--border);
border-radius: 7px;
background: #f8fafc;
}
.mode-switch button,
.top-action {
border: 0;
background: transparent;
cursor: pointer;
border-radius: 5px;
color: var(--muted);
}
.mode-switch button {
height: 28px;
padding: 0 12px;
font-size: 12px;
}
.mode-switch button.active {
background: var(--primary);
color: #fff;
}
.top-actions {
display: flex;
align-items: center;
gap: 8px;
}
.top-action {
height: 32px;
padding: 0 11px;
border: 1px solid var(--border);
background: #fff;
font-size: 12px;
}
.top-action.primary {
border-color: var(--primary);
background: var(--primary);
color: #fff;
}
.workspace {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: 260px minmax(0, 1fr) 58px;
gap: 0;
overflow: hidden;
}
.rules {
min-width: 0;
border-right: 1px solid var(--border);
background: #fbfcfd;
display: flex;
flex-direction: column;
}
.rules-head {
flex: 0 0 auto;
height: 48px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 14px;
border-bottom: 1px solid var(--border);
font-size: 13px;
font-weight: 650;
}
.rules-list {
flex: 1;
min-height: 0;
overflow: auto;
padding: 8px;
}
.rule-item {
width: 100%;
min-height: 46px;
display: flex;
align-items: center;
gap: 9px;
margin-bottom: 6px;
padding: 8px 9px;
border: 1px solid transparent;
border-radius: 7px;
background: transparent;
text-align: left;
cursor: pointer;
}
.rule-item.active {
border-color: rgba(0, 104, 74, 0.22);
background: var(--primary-soft);
}
.rule-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--primary);
flex: 0 0 auto;
}
.rule-dot.error {
background: var(--danger);
}
.rule-dot.warn {
background: var(--warn);
}
.rule-text {
min-width: 0;
flex: 1;
}
.rule-text strong {
display: block;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.rule-text span {
display: block;
margin-top: 2px;
font-size: 10.5px;
color: var(--muted);
}
.preview {
min-width: 0;
min-height: 0;
position: relative;
background: #dfe6ec;
overflow: hidden;
display: flex;
flex-direction: column;
}
.preview-toolbar {
height: 40px;
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
border-bottom: 1px solid #cbd5df;
background: #f8fafc;
font-size: 12px;
color: var(--muted);
}
.pages {
flex: 1;
min-height: 0;
overflow: auto;
padding: 24px 0 80px;
}
.page {
width: min(760px, calc(100% - 64px));
min-height: 900px;
margin: 0 auto 22px;
padding: 52px 58px;
background: #fff;
box-shadow: 0 14px 42px rgba(15, 23, 42, 0.12);
line-height: 1.8;
font-size: 14px;
}
.page h2 {
margin: 0 0 28px;
text-align: center;
font-size: 20px;
letter-spacing: 0;
}
.highlight {
background: rgba(0, 104, 74, 0.13);
outline: 1px solid rgba(0, 104, 74, 0.45);
}
.rail {
border-left: 1px solid var(--border);
background: #fff;
display: flex;
flex-direction: column;
align-items: center;
padding: 8px 6px;
gap: 6px;
z-index: 20;
}
.rail-button {
width: 44px;
min-height: 48px;
border: 1px solid transparent;
border-radius: 7px;
background: transparent;
color: var(--muted);
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
position: relative;
}
.rail-button:hover,
.rail-button.active {
border-color: rgba(0, 104, 74, 0.18);
background: var(--primary-soft);
color: var(--primary);
}
.rail-button .icon {
font-size: 17px;
line-height: 1;
}
.rail-button .label {
font-size: 10px;
writing-mode: vertical-rl;
letter-spacing: 0;
line-height: 1.1;
max-height: 54px;
white-space: nowrap;
}
.badge {
position: absolute;
top: 3px;
right: 3px;
min-width: 16px;
height: 16px;
padding: 0 4px;
display: grid;
place-items: center;
border-radius: 999px;
background: var(--danger);
color: #fff;
font-size: 10px;
}
.drawer-backdrop {
position: fixed;
inset: 52px 58px 0 0;
background: rgba(15, 23, 42, 0.18);
opacity: 0;
pointer-events: none;
transition: opacity 160ms ease;
z-index: 25;
}
.drawer-backdrop.open {
opacity: 1;
pointer-events: auto;
}
.drawer {
position: fixed;
top: 52px;
right: 58px;
bottom: 0;
width: 460px;
max-width: calc(100vw - 120px);
display: flex;
flex-direction: column;
border-left: 1px solid var(--border);
background: #fff;
box-shadow: -18px 0 36px rgba(15, 23, 42, 0.18);
transform: translateX(calc(100% + 24px));
transition: transform 180ms ease;
z-index: 30;
}
.drawer.open {
transform: translateX(0);
}
.drawer.wide {
width: min(980px, calc(100vw - 116px));
}
.drawer-head {
height: 48px;
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 10px;
padding: 0 14px;
border-bottom: 1px solid var(--border);
}
.drawer-title {
min-width: 0;
flex: 1;
}
.drawer-title strong {
display: block;
font-size: 14px;
}
.drawer-title span {
display: block;
margin-top: 1px;
font-size: 11px;
color: var(--muted);
}
.icon-button {
width: 30px;
height: 30px;
border: 1px solid var(--border);
border-radius: 6px;
background: #fff;
color: var(--muted);
cursor: pointer;
}
.drawer-body {
flex: 1;
min-height: 0;
overflow: auto;
padding: 14px;
}
.drawer-foot {
flex: 0 0 auto;
min-height: 52px;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
border-top: 1px solid var(--border);
background: #f8fafc;
}
.btn {
height: 34px;
border: 1px solid var(--border);
border-radius: 6px;
padding: 0 12px;
background: #fff;
color: var(--text);
font-size: 12px;
cursor: pointer;
}
.btn.primary {
border-color: var(--primary);
background: var(--primary);
color: #fff;
}
.btn.ghost-primary {
border-color: rgba(0, 104, 74, 0.35);
color: var(--primary);
background: #fff;
}
.card {
border: 1px solid var(--border);
border-radius: 8px;
background: #fff;
overflow: hidden;
margin-bottom: 12px;
}
.card-head {
padding: 10px 12px;
border-bottom: 1px solid #edf2f7;
font-size: 13px;
font-weight: 650;
}
.card-body {
padding: 12px;
font-size: 12px;
color: #334155;
line-height: 1.7;
}
.status-row {
display: grid;
grid-template-columns: 90px 1fr;
gap: 8px;
margin-bottom: 8px;
}
.status-row span:first-child {
color: var(--muted);
}
.field-item,
.opinion-item {
padding: 10px 0;
border-bottom: 1px solid #edf2f7;
}
.field-item:last-child,
.opinion-item:last-child {
border-bottom: 0;
}
.field-item strong,
.opinion-item strong {
display: block;
font-size: 12px;
margin-bottom: 4px;
}
.field-item span,
.opinion-item span {
color: var(--muted);
font-size: 11px;
}
.compare-grid {
min-height: calc(100vh - 180px);
display: grid;
grid-template-columns: 1fr 1fr;
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
.compare-pane {
min-width: 0;
background: #fff;
display: flex;
flex-direction: column;
}
.compare-pane + .compare-pane {
border-left: 1px solid var(--border);
}
.compare-pane h4 {
margin: 0;
padding: 10px 12px;
border-bottom: 1px solid #edf2f7;
font-size: 12px;
color: var(--muted);
background: #f8fafc;
}
.compare-text {
flex: 1;
min-height: 0;
padding: 16px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
line-height: 1.8;
white-space: pre-wrap;
overflow: auto;
}
.diff-del {
background: #fee2e2;
color: #991b1b;
}
.diff-add {
background: #dcfce7;
color: #166534;
}
.toast {
position: fixed;
left: 50%;
bottom: 22px;
transform: translateX(-50%) translateY(20px);
min-width: 220px;
padding: 9px 12px;
border-radius: 7px;
background: #172033;
color: #fff;
font-size: 12px;
opacity: 0;
pointer-events: none;
transition: opacity 160ms ease, transform 160ms ease;
z-index: 50;
text-align: center;
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
@media (max-width: 900px) {
.workspace {
grid-template-columns: 0 minmax(0, 1fr) 54px;
}
.rules {
display: none;
}
.drawer-backdrop {
inset: 52px 54px 0 0;
}
.drawer,
.drawer.wide {
right: 54px;
width: calc(100vw - 54px);
max-width: calc(100vw - 54px);
}
.topbar {
gap: 8px;
padding: 0 10px;
}
.top-actions {
display: none;
}
}
</style>
</head>
<body>
<div class="app">
<header class="topbar">
<div class="title">
<strong id="pageTitle">合同评查详情 - 采购合同示例.docx</strong>
<span id="pageMeta">合同编号:HT-2026-0523 | DOCX | 18页 | 当前布局为预览原型</span>
</div>
<div class="mode-switch" aria-label="切换页面模式">
<button id="normalMode" class="active" type="button">普通评查</button>
<button id="crossMode" type="button">交叉评查</button>
</div>
<div class="top-actions">
<button class="top-action" type="button" data-toast="返回上一页">返回</button>
<button class="top-action" type="button" data-toast="开始下载文档">下载</button>
<button id="confirmButton" class="top-action primary" type="button" data-toast="确认评查结果">确认评查结果</button>
</div>
</header>
<main class="workspace">
<aside class="rules">
<div class="rules-head">
<span>评查点目录</span>
<span style="font-size: 11px; color: var(--muted);">29</span>
</div>
<div class="rules-list">
<button class="rule-item active" type="button">
<span class="rule-dot error"></span>
<span class="rule-text"><strong>合同主体信息不完整</strong><span>第 2 页 | 风险项</span></span>
</button>
<button class="rule-item" type="button">
<span class="rule-dot warn"></span>
<span class="rule-text"><strong>付款节点表述不一致</strong><span>第 5 页 | 待复核</span></span>
</button>
<button class="rule-item" type="button">
<span class="rule-dot"></span>
<span class="rule-text"><strong>违约责任条款完整</strong><span>第 8 页 | 通过</span></span>
</button>
<button class="rule-item" type="button">
<span class="rule-dot warn"></span>
<span class="rule-text"><strong>争议解决方式需确认</strong><span>第 12 页 | 待复核</span></span>
</button>
<button class="rule-item" type="button">
<span class="rule-dot"></span>
<span class="rule-text"><strong>签章区域格式符合要求</strong><span>第 18 页 | 通过</span></span>
</button>
</div>
</aside>
<section class="preview">
<div class="preview-toolbar">
<span>文档预览</span>
<span>缩放 100% | 当前页 2 / 18</span>
</div>
<div class="pages">
<article class="page">
<h2>采购合同</h2>
<p>甲方:某某烟草公司</p>
<p>乙方:某某供应商有限公司</p>
<p class="highlight">经评查发现,乙方统一社会信用代码未在合同主体信息中完整展示,建议补充完整后再提交确认。</p>
<p>双方根据相关法律法规,经友好协商,就采购事项达成本合同。</p>
<p>付款方式:合同签订后支付 30%,验收通过后支付 60%,质保期满后支付 10%。</p>
<p>违约责任:任一方违反本合同约定,应承担相应违约责任。</p>
</article>
</div>
</section>
<nav id="rail" class="rail" aria-label="业务功能"></nav>
</main>
</div>
<div id="backdrop" class="drawer-backdrop"></div>
<aside id="drawer" class="drawer" aria-live="polite">
<header class="drawer-head">
<div class="drawer-title">
<strong id="drawerTitle">评查结果</strong>
<span id="drawerSubtitle">查看当前选中评查点的详情</span>
</div>
<button id="drawerClose" class="icon-button" type="button" aria-label="关闭">×</button>
</header>
<div id="drawerBody" class="drawer-body"></div>
<footer id="drawerFoot" class="drawer-foot"></footer>
</aside>
<div id="toast" class="toast"></div>
<script>
const state = {
mode: "normal",
activeTab: null,
hasTemplate: false,
};
const rail = document.getElementById("rail");
const drawer = document.getElementById("drawer");
const backdrop = document.getElementById("backdrop");
const drawerTitle = document.getElementById("drawerTitle");
const drawerSubtitle = document.getElementById("drawerSubtitle");
const drawerBody = document.getElementById("drawerBody");
const drawerFoot = document.getElementById("drawerFoot");
const toast = document.getElementById("toast");
const normalTabs = [
{ key: "result", label: "评查结果", icon: "✓", subtitle: "查看当前选中评查点的详情" },
{ key: "fields", label: "抽取字段", icon: "▦", subtitle: "查看文档字段抽取结果" },
{ key: "compare", label: "结构比对", icon: "⇄", subtitle: "对比原文和模板合同结构", wide: true },
{ key: "fileinfo", label: "文件信息", icon: "i", subtitle: "查看文件基础信息和评查信息" },
];
const crossTabs = [
{ key: "result", label: "评查结果", icon: "✓", subtitle: "查看当前选中评查点的详情" },
{ key: "fields", label: "抽取字段", icon: "▦", subtitle: "查看文档字段抽取结果" },
{ key: "compare", label: "结构比对", icon: "⇄", subtitle: "对比原文和模板合同结构", wide: true },
{ key: "crossOpinions", label: "交叉意见", icon: "☰", subtitle: "查看和提交评查点意见", badge: 3 },
{ key: "supplementOpinions", label: "补充意见", icon: "+", subtitle: "查看和提交补充意见", badge: 1 },
{ key: "fileinfo", label: "文件信息", icon: "i", subtitle: "查看文件基础信息和评查信息" },
];
function getTabs() {
return state.mode === "cross" ? crossTabs : normalTabs;
}
function showToast(message) {
toast.textContent = message;
toast.classList.add("show");
window.clearTimeout(showToast.timer);
showToast.timer = window.setTimeout(() => toast.classList.remove("show"), 1600);
}
function renderRail() {
rail.innerHTML = "";
getTabs().forEach((tab) => {
const button = document.createElement("button");
button.className = `rail-button ${state.activeTab === tab.key ? "active" : ""}`;
button.type = "button";
button.title = tab.label;
button.innerHTML = `
<span class="icon">${tab.icon}</span>
<span class="label">${tab.label}</span>
${tab.badge ? `<span class="badge">${tab.badge}</span>` : ""}
`;
button.addEventListener("click", () => openTab(tab.key));
rail.appendChild(button);
});
}
function openTab(key) {
const tab = getTabs().find((item) => item.key === key);
if (!tab) return;
state.activeTab = key;
drawerTitle.textContent = tab.label;
drawerSubtitle.textContent = tab.subtitle;
drawer.classList.toggle("wide", Boolean(tab.wide));
drawerBody.innerHTML = renderDrawerBody(key);
drawerFoot.innerHTML = renderDrawerFoot(key);
drawer.classList.add("open");
backdrop.classList.add("open");
renderRail();
bindDrawerActions();
}
function closeDrawer() {
state.activeTab = null;
drawer.classList.remove("open");
backdrop.classList.remove("open");
renderRail();
}
function renderDrawerBody(key) {
if (key === "result") {
return `
<section class="card">
<div class="card-head">合同主体信息不完整</div>
<div class="card-body">
<div class="status-row"><span>评查状态</span><strong style="color: var(--danger);">不通过</strong></div>
<div class="status-row"><span>定位页码</span><span>第 2 页</span></div>
<div class="status-row"><span>扣分</span><span>-3</span></div>
<p>乙方主体信息缺少统一社会信用代码,建议补齐主体身份信息。</p>
</div>
</section>
<section class="card">
<div class="card-head">AI 建议</div>
<div class="card-body">请补充乙方统一社会信用代码,并与营业执照信息保持一致。</div>
</section>
`;
}
if (key === "fields") {
return `
<section class="card">
<div class="card-head">抽取字段 29</div>
<div class="card-body">
<div class="field-item"><strong>甲方名称</strong><span>某某烟草公司 | 置信度 96% | 第 1 页</span></div>
<div class="field-item"><strong>乙方名称</strong><span>某某供应商有限公司 | 置信度 94% | 第 1 页</span></div>
<div class="field-item"><strong>乙方统一社会信用代码</strong><span style="color: var(--danger);">缺失 | 未定位</span></div>
<div class="field-item"><strong>合同金额</strong><span>1,580,000.00 元 | 置信度 91% | 第 4 页</span></div>
<div class="field-item"><strong>付款方式</strong><span>分阶段付款 | 置信度 88% | 第 5 页</span></div>
</div>
</section>
`;
}
if (key === "compare") {
const uploadLabel = state.hasTemplate ? "重新上传对比" : "上传模板对比";
return `
<section class="card">
<div class="card-body" style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
<span>${state.hasTemplate ? "已上传模板合同,当前显示结构差异。" : "当前文档类型暂未上传模板合同,可先上传模板后再对比。"}</span>
<button class="btn ghost-primary" type="button" data-action="upload-template">${uploadLabel}</button>
</div>
</section>
${state.hasTemplate ? `
<div class="compare-grid">
<section class="compare-pane">
<h4>原始版本</h4>
<div class="compare-text">第一条 合同主体
甲方:某某烟草公司
乙方:某某供应商有限公司
<span class="diff-del">乙方信用代码:未填写</span>
第二条 付款方式
合同签订后支付 30%。</div>
</section>
<section class="compare-pane">
<h4>模板版本</h4>
<div class="compare-text">第一条 合同主体
甲方:某某烟草公司
乙方:某某供应商有限公司
<span class="diff-add">乙方信用代码:应填写统一社会信用代码</span>
第二条 付款方式
合同签订后支付 30%。</div>
</section>
</div>
` : `
<section class="card">
<div class="card-body" style="text-align:center; padding:48px 20px;">
<div style="font-size:34px; color:#94a3b8;">⇄</div>
<strong style="display:block; margin-top:10px;">暂无可用模板,无法进行结构比对</strong>
<p style="margin:8px auto 0; max-width:420px; color:var(--muted);">上传模板后,这里会展示原始文档和模板合同的结构差异。</p>
</div>
</section>
`}
`;
}
if (key === "crossOpinions") {
return `
<section class="card">
<div class="card-head">交叉意见 3</div>
<div class="card-body">
<div class="opinion-item"><strong>梅州评查员</strong><span>建议将该项从“不通过”调整为“待复核”。</span></div>
<div class="opinion-item"><strong>省级复核员</strong><span>主体信息缺失属实,维持不通过。</span></div>
<div class="opinion-item"><strong>系统汇总</strong><span>当前 2 人赞同,1 人待投票。</span></div>
</div>
</section>
`;
}
if (key === "supplementOpinions") {
return `
<section class="card">
<div class="card-head">补充意见 1</div>
<div class="card-body">
<div class="opinion-item"><strong>补充风险</strong><span>建议额外检查供应商资质附件是否齐全。</span></div>
</div>
</section>
`;
}
return `
<section class="card">
<div class="card-head">文件信息</div>
<div class="card-body">
<div class="status-row"><span>文件名称</span><span>采购合同示例.docx</span></div>
<div class="status-row"><span>文件类型</span><span>合同</span></div>
<div class="status-row"><span>上传时间</span><span>2026/5/23 10:18:22</span></div>
<div class="status-row"><span>评查模型</span><span>LeAudit 默认模型</span></div>
<div class="status-row"><span>评查结果</span><span>存在风险项</span></div>
</div>
</section>
`;
}
function renderDrawerFoot(key) {
if (key === "compare") {
return `
<button class="btn" type="button" data-action="close">关闭</button>
<button class="btn ghost-primary" type="button" data-action="upload-template">${state.hasTemplate ? "重新上传对比" : "上传模板对比"}</button>
`;
}
if (key === "crossOpinions" || key === "supplementOpinions") {
return `
<button class="btn" type="button" data-action="close">关闭</button>
<button class="btn primary" type="button" data-toast="提交意见弹窗">提交意见</button>
`;
}
if (key === "result") {
return `
<button class="btn" type="button" data-toast="开始下载文档">下载</button>
<button class="btn primary" type="button" data-toast="${state.mode === "cross" ? "完成当前交叉评查" : "确认评查结果"}">${state.mode === "cross" ? "完成评查" : "确认评查结果"}</button>
`;
}
return `<button class="btn" type="button" data-action="close">关闭</button>`;
}
function bindDrawerActions() {
drawer.querySelectorAll("[data-action='close']").forEach((button) => {
button.addEventListener("click", closeDrawer);
});
drawer.querySelectorAll("[data-action='upload-template']").forEach((button) => {
button.addEventListener("click", () => {
state.hasTemplate = true;
showToast("模拟上传成功,已切换为重新上传对比");
openTab("compare");
});
});
drawer.querySelectorAll("[data-toast]").forEach((button) => {
button.addEventListener("click", () => showToast(button.dataset.toast));
});
}
function setMode(mode) {
state.mode = mode;
state.activeTab = null;
document.getElementById("normalMode").classList.toggle("active", mode === "normal");
document.getElementById("crossMode").classList.toggle("active", mode === "cross");
document.getElementById("pageTitle").textContent = mode === "cross"
? "交叉评查详情 - 采购合同示例.docx"
: "合同评查详情 - 采购合同示例.docx";
document.getElementById("pageMeta").textContent = mode === "cross"
? "任务:梅州地区交叉评查 | DOCX | 18页 | 当前布局为预览原型"
: "合同编号:HT-2026-0523 | DOCX | 18页 | 当前布局为预览原型";
document.getElementById("confirmButton").textContent = mode === "cross" ? "完成评查" : "确认评查结果";
closeDrawer();
renderRail();
}
document.getElementById("normalMode").addEventListener("click", () => setMode("normal"));
document.getElementById("crossMode").addEventListener("click", () => setMode("cross"));
document.getElementById("drawerClose").addEventListener("click", closeDrawer);
backdrop.addEventListener("click", closeDrawer);
document.querySelectorAll("[data-toast]").forEach((button) => {
button.addEventListener("click", () => showToast(button.dataset.toast));
});
document.querySelectorAll(".rule-item").forEach((button) => {
button.addEventListener("click", () => {
document.querySelectorAll(".rule-item").forEach((item) => item.classList.remove("active"));
button.classList.add("active");
showToast("已切换评查点");
if (state.activeTab === "result") openTab("result");
});
});
renderRail();
openTab("result");
</script>
</body>
</html>