2159 lines
69 KiB
HTML
2159 lines
69 KiB
HTML
|
|
<!doctype html>
|
|||
|
|
<html lang="zh-CN">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="utf-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|||
|
|
<title>AI 报销预审中台 - macOS Prototype</title>
|
|||
|
|
<style>
|
|||
|
|
:root {
|
|||
|
|
--bg: #eef1f4;
|
|||
|
|
--window: rgba(248, 249, 251, 0.92);
|
|||
|
|
--sidebar: rgba(233, 237, 242, 0.78);
|
|||
|
|
--surface: #ffffff;
|
|||
|
|
--surface-2: #f7f8fa;
|
|||
|
|
--line: #d8dde5;
|
|||
|
|
--line-strong: #c1c8d2;
|
|||
|
|
--text: #14171c;
|
|||
|
|
--muted: #657083;
|
|||
|
|
--soft: #8792a3;
|
|||
|
|
--blue: #156df7;
|
|||
|
|
--blue-soft: #e8f1ff;
|
|||
|
|
--green: #11845b;
|
|||
|
|
--green-soft: #e8f7ef;
|
|||
|
|
--amber: #a55f00;
|
|||
|
|
--amber-soft: #fff3dc;
|
|||
|
|
--red: #c7352b;
|
|||
|
|
--red-soft: #fff0ee;
|
|||
|
|
--purple: #6b4fd8;
|
|||
|
|
--purple-soft: #f0edff;
|
|||
|
|
--shadow: 0 24px 80px rgba(31, 39, 51, 0.22);
|
|||
|
|
--shadow-soft: 0 10px 30px rgba(31, 39, 51, 0.10);
|
|||
|
|
--radius: 8px;
|
|||
|
|
--font: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Segoe UI", sans-serif;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
* {
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
body {
|
|||
|
|
margin: 0;
|
|||
|
|
min-height: 100dvh;
|
|||
|
|
color: var(--text);
|
|||
|
|
font-family: var(--font);
|
|||
|
|
background:
|
|||
|
|
linear-gradient(135deg, rgba(255,255,255,0.72), rgba(229,235,244,0.72)),
|
|||
|
|
linear-gradient(90deg, #e8eef4, #f4f1ee 48%, #e9f0ed);
|
|||
|
|
letter-spacing: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
button,
|
|||
|
|
input,
|
|||
|
|
textarea,
|
|||
|
|
select {
|
|||
|
|
font: inherit;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
button {
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
button:focus-visible,
|
|||
|
|
input:focus-visible,
|
|||
|
|
textarea:focus-visible,
|
|||
|
|
select:focus-visible,
|
|||
|
|
[tabindex]:focus-visible {
|
|||
|
|
outline: 3px solid rgba(21, 109, 247, 0.25);
|
|||
|
|
outline-offset: 2px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.desktop {
|
|||
|
|
min-height: 100dvh;
|
|||
|
|
display: grid;
|
|||
|
|
place-items: center;
|
|||
|
|
padding: 28px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.app-window {
|
|||
|
|
width: min(1480px, 100%);
|
|||
|
|
height: min(940px, calc(100dvh - 56px));
|
|||
|
|
min-height: 720px;
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-rows: 48px 1fr;
|
|||
|
|
overflow: hidden;
|
|||
|
|
border: 1px solid rgba(255,255,255,0.72);
|
|||
|
|
border-radius: 18px;
|
|||
|
|
background: var(--window);
|
|||
|
|
box-shadow: var(--shadow);
|
|||
|
|
backdrop-filter: blur(22px) saturate(1.12);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.titlebar {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 172px 1fr auto;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 18px;
|
|||
|
|
padding: 0 16px;
|
|||
|
|
border-bottom: 1px solid rgba(196, 204, 214, 0.78);
|
|||
|
|
background: rgba(246, 248, 250, 0.78);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.traffic {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.traffic span {
|
|||
|
|
width: 12px;
|
|||
|
|
height: 12px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.10);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.traffic span:nth-child(1) { background: #ff5f57; }
|
|||
|
|
.traffic span:nth-child(2) { background: #ffbd2e; }
|
|||
|
|
.traffic span:nth-child(3) { background: #28c840; }
|
|||
|
|
|
|||
|
|
.toolbar {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
gap: 10px;
|
|||
|
|
min-width: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.search {
|
|||
|
|
width: min(520px, 100%);
|
|||
|
|
height: 32px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
padding: 0 12px;
|
|||
|
|
color: var(--muted);
|
|||
|
|
border: 1px solid rgba(192, 199, 210, 0.78);
|
|||
|
|
border-radius: 7px;
|
|||
|
|
background: rgba(255,255,255,0.7);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.search input {
|
|||
|
|
width: 100%;
|
|||
|
|
border: 0;
|
|||
|
|
outline: 0;
|
|||
|
|
background: transparent;
|
|||
|
|
color: var(--text);
|
|||
|
|
font-size: 13px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.title-actions {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mac-chip {
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
min-height: 28px;
|
|||
|
|
gap: 8px;
|
|||
|
|
padding: 4px 10px;
|
|||
|
|
border: 1px solid var(--line);
|
|||
|
|
border-radius: 7px;
|
|||
|
|
background: rgba(255,255,255,0.64);
|
|||
|
|
color: #2c3340;
|
|||
|
|
font-size: 12px;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.avatar {
|
|||
|
|
width: 28px;
|
|||
|
|
height: 28px;
|
|||
|
|
display: grid;
|
|||
|
|
place-items: center;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: #20242b;
|
|||
|
|
color: #fff;
|
|||
|
|
font-size: 12px;
|
|||
|
|
font-weight: 700;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.app-body {
|
|||
|
|
min-height: 0;
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 246px minmax(0, 1fr) 336px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sidebar {
|
|||
|
|
min-height: 0;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
padding: 18px 12px;
|
|||
|
|
border-right: 1px solid rgba(196, 204, 214, 0.72);
|
|||
|
|
background: var(--sidebar);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.brand {
|
|||
|
|
padding: 4px 8px 18px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.brand h1 {
|
|||
|
|
margin: 0 0 5px;
|
|||
|
|
font-size: 17px;
|
|||
|
|
line-height: 1.25;
|
|||
|
|
letter-spacing: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.brand p {
|
|||
|
|
margin: 0;
|
|||
|
|
color: var(--muted);
|
|||
|
|
font-size: 12px;
|
|||
|
|
line-height: 1.45;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.nav-section {
|
|||
|
|
display: grid;
|
|||
|
|
gap: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.nav-title {
|
|||
|
|
padding: 16px 8px 6px;
|
|||
|
|
color: var(--soft);
|
|||
|
|
font-size: 11px;
|
|||
|
|
font-weight: 700;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.nav-item {
|
|||
|
|
width: 100%;
|
|||
|
|
min-height: 40px;
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 24px 1fr auto;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 9px;
|
|||
|
|
padding: 8px 10px;
|
|||
|
|
border: 0;
|
|||
|
|
border-radius: 7px;
|
|||
|
|
background: transparent;
|
|||
|
|
color: #293241;
|
|||
|
|
text-align: left;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.nav-item:hover {
|
|||
|
|
background: rgba(255,255,255,0.58);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.nav-item.active {
|
|||
|
|
background: rgba(255,255,255,0.86);
|
|||
|
|
box-shadow: 0 1px 2px rgba(31, 39, 51, 0.08);
|
|||
|
|
color: var(--blue);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.nav-item svg {
|
|||
|
|
width: 18px;
|
|||
|
|
height: 18px;
|
|||
|
|
stroke-width: 2;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.nav-item .label {
|
|||
|
|
min-width: 0;
|
|||
|
|
font-size: 13px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.nav-badge {
|
|||
|
|
min-width: 22px;
|
|||
|
|
height: 20px;
|
|||
|
|
display: grid;
|
|||
|
|
place-items: center;
|
|||
|
|
padding: 0 6px;
|
|||
|
|
border-radius: 999px;
|
|||
|
|
background: #edf0f5;
|
|||
|
|
color: var(--muted);
|
|||
|
|
font-size: 11px;
|
|||
|
|
font-variant-numeric: tabular-nums;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sidebar-footer {
|
|||
|
|
margin-top: auto;
|
|||
|
|
display: grid;
|
|||
|
|
gap: 10px;
|
|||
|
|
padding: 12px 8px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mini-meter {
|
|||
|
|
display: grid;
|
|||
|
|
gap: 8px;
|
|||
|
|
padding: 12px;
|
|||
|
|
border: 1px solid rgba(205, 211, 220, 0.76);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
background: rgba(255,255,255,0.54);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mini-meter strong {
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.meter-row {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
color: var(--muted);
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.bar {
|
|||
|
|
height: 7px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
border-radius: 999px;
|
|||
|
|
background: #dde3ec;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.bar span {
|
|||
|
|
display: block;
|
|||
|
|
width: var(--value, 35%);
|
|||
|
|
height: 100%;
|
|||
|
|
border-radius: inherit;
|
|||
|
|
background: linear-gradient(90deg, var(--blue), #3c9a6d);
|
|||
|
|
transition: width 260ms ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.main {
|
|||
|
|
min-width: 0;
|
|||
|
|
min-height: 0;
|
|||
|
|
overflow: auto;
|
|||
|
|
padding: 28px;
|
|||
|
|
background:
|
|||
|
|
linear-gradient(180deg, rgba(255,255,255,0.50), rgba(247,249,252,0.74)),
|
|||
|
|
var(--bg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.right-panel {
|
|||
|
|
min-width: 0;
|
|||
|
|
min-height: 0;
|
|||
|
|
overflow: auto;
|
|||
|
|
padding: 22px 18px;
|
|||
|
|
border-left: 1px solid rgba(196, 204, 214, 0.72);
|
|||
|
|
background: rgba(246, 248, 250, 0.78);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.view {
|
|||
|
|
display: none;
|
|||
|
|
animation: appear 190ms ease-out;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.view.active {
|
|||
|
|
display: block;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes appear {
|
|||
|
|
from {
|
|||
|
|
opacity: 0;
|
|||
|
|
transform: translateY(6px);
|
|||
|
|
}
|
|||
|
|
to {
|
|||
|
|
opacity: 1;
|
|||
|
|
transform: translateY(0);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (prefers-reduced-motion: reduce) {
|
|||
|
|
*, *::before, *::after {
|
|||
|
|
animation-duration: 1ms !important;
|
|||
|
|
transition-duration: 1ms !important;
|
|||
|
|
scroll-behavior: auto !important;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page-head {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
gap: 18px;
|
|||
|
|
margin-bottom: 22px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.eyebrow {
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
color: var(--blue);
|
|||
|
|
font-size: 12px;
|
|||
|
|
font-weight: 700;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page-head h2 {
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: clamp(25px, 3vw, 34px);
|
|||
|
|
line-height: 1.14;
|
|||
|
|
letter-spacing: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page-head p {
|
|||
|
|
max-width: 760px;
|
|||
|
|
margin: 8px 0 0;
|
|||
|
|
color: var(--muted);
|
|||
|
|
font-size: 14px;
|
|||
|
|
line-height: 1.65;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.headline-actions {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 10px;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
justify-content: flex-end;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn {
|
|||
|
|
min-height: 36px;
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
padding: 8px 13px;
|
|||
|
|
border: 1px solid var(--line);
|
|||
|
|
border-radius: 7px;
|
|||
|
|
background: var(--surface);
|
|||
|
|
color: #222936;
|
|||
|
|
font-size: 13px;
|
|||
|
|
font-weight: 700;
|
|||
|
|
box-shadow: 0 1px 1px rgba(31, 39, 51, 0.04);
|
|||
|
|
transition: transform 140ms ease, box-shadow 140ms ease, background 140ms ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn:hover {
|
|||
|
|
background: #f9fafc;
|
|||
|
|
box-shadow: var(--shadow-soft);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn:active {
|
|||
|
|
transform: scale(0.98);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn.primary {
|
|||
|
|
border-color: #075fdc;
|
|||
|
|
background: linear-gradient(180deg, #2c86ff, #0f67ef);
|
|||
|
|
color: #fff;
|
|||
|
|
box-shadow: 0 10px 18px rgba(21, 109, 247, 0.24);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn.primary:hover {
|
|||
|
|
background: linear-gradient(180deg, #3b90ff, #166df1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn.ghost {
|
|||
|
|
background: transparent;
|
|||
|
|
box-shadow: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn.danger {
|
|||
|
|
border-color: #f1beb8;
|
|||
|
|
background: var(--red-soft);
|
|||
|
|
color: var(--red);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn:disabled {
|
|||
|
|
cursor: not-allowed;
|
|||
|
|
opacity: 0.48;
|
|||
|
|
box-shadow: none;
|
|||
|
|
transform: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn svg,
|
|||
|
|
.icon-btn svg,
|
|||
|
|
.search svg {
|
|||
|
|
width: 16px;
|
|||
|
|
height: 16px;
|
|||
|
|
stroke-width: 2;
|
|||
|
|
flex: 0 0 auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.icon-btn {
|
|||
|
|
width: 36px;
|
|||
|
|
height: 36px;
|
|||
|
|
display: inline-grid;
|
|||
|
|
place-items: center;
|
|||
|
|
border: 1px solid var(--line);
|
|||
|
|
border-radius: 7px;
|
|||
|
|
background: var(--surface);
|
|||
|
|
color: #293241;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.grid-2 {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: minmax(0, 1.08fr) minmax(320px, 0.92fr);
|
|||
|
|
gap: 18px;
|
|||
|
|
align-items: start;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.grid-3 {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|||
|
|
gap: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.card {
|
|||
|
|
border: 1px solid var(--line);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
background: rgba(255,255,255,0.88);
|
|||
|
|
box-shadow: 0 1px 1px rgba(31, 39, 51, 0.03);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.card.pad {
|
|||
|
|
padding: 18px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.card-title {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
gap: 12px;
|
|||
|
|
margin-bottom: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.card-title h3 {
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: 15px;
|
|||
|
|
line-height: 1.3;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.card-title p {
|
|||
|
|
margin: 3px 0 0;
|
|||
|
|
color: var(--muted);
|
|||
|
|
font-size: 12px;
|
|||
|
|
line-height: 1.45;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.intent-box {
|
|||
|
|
display: grid;
|
|||
|
|
gap: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.intent-box textarea {
|
|||
|
|
width: 100%;
|
|||
|
|
min-height: 150px;
|
|||
|
|
resize: vertical;
|
|||
|
|
padding: 14px;
|
|||
|
|
border: 1px solid var(--line-strong);
|
|||
|
|
border-radius: 8px;
|
|||
|
|
background: #fff;
|
|||
|
|
color: var(--text);
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.quick-actions {
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
gap: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.quick {
|
|||
|
|
min-height: 32px;
|
|||
|
|
border: 1px solid #cbd4df;
|
|||
|
|
border-radius: 999px;
|
|||
|
|
padding: 6px 11px;
|
|||
|
|
background: #fff;
|
|||
|
|
color: #2d3748;
|
|||
|
|
font-size: 12px;
|
|||
|
|
font-weight: 700;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.quick:hover {
|
|||
|
|
background: var(--blue-soft);
|
|||
|
|
border-color: #a9c8ff;
|
|||
|
|
color: var(--blue);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat {
|
|||
|
|
display: grid;
|
|||
|
|
gap: 8px;
|
|||
|
|
padding: 16px;
|
|||
|
|
border: 1px solid var(--line);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
background: var(--surface);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat span {
|
|||
|
|
color: var(--muted);
|
|||
|
|
font-size: 12px;
|
|||
|
|
font-weight: 700;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat strong {
|
|||
|
|
font-size: 28px;
|
|||
|
|
line-height: 1;
|
|||
|
|
font-variant-numeric: tabular-nums;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat small {
|
|||
|
|
color: var(--muted);
|
|||
|
|
font-size: 12px;
|
|||
|
|
line-height: 1.45;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dropzone {
|
|||
|
|
min-height: 238px;
|
|||
|
|
display: grid;
|
|||
|
|
place-items: center;
|
|||
|
|
padding: 24px;
|
|||
|
|
border: 1.5px dashed #aeb8c7;
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
background:
|
|||
|
|
linear-gradient(180deg, rgba(255,255,255,0.92), rgba(246,248,251,0.92));
|
|||
|
|
text-align: center;
|
|||
|
|
transition: border-color 160ms ease, background 160ms ease, transform 160ms ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dropzone.drag {
|
|||
|
|
border-color: var(--blue);
|
|||
|
|
background: var(--blue-soft);
|
|||
|
|
transform: translateY(-1px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dropzone input {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.drop-inner {
|
|||
|
|
display: grid;
|
|||
|
|
justify-items: center;
|
|||
|
|
gap: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.big-icon {
|
|||
|
|
width: 52px;
|
|||
|
|
height: 52px;
|
|||
|
|
display: grid;
|
|||
|
|
place-items: center;
|
|||
|
|
border-radius: 14px;
|
|||
|
|
background: #ecf2fb;
|
|||
|
|
color: var(--blue);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.big-icon svg {
|
|||
|
|
width: 26px;
|
|||
|
|
height: 26px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dropzone h3 {
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: 17px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dropzone p {
|
|||
|
|
max-width: 420px;
|
|||
|
|
margin: 0;
|
|||
|
|
color: var(--muted);
|
|||
|
|
line-height: 1.55;
|
|||
|
|
font-size: 13px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.file-list,
|
|||
|
|
.task-list,
|
|||
|
|
.check-list,
|
|||
|
|
.timeline,
|
|||
|
|
.agent-list {
|
|||
|
|
display: grid;
|
|||
|
|
gap: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.file-row,
|
|||
|
|
.task-row,
|
|||
|
|
.check-row,
|
|||
|
|
.audit-row,
|
|||
|
|
.agent-row {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: auto 1fr auto;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 12px;
|
|||
|
|
padding: 12px;
|
|||
|
|
border: 1px solid var(--line);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
background: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.file-thumb {
|
|||
|
|
width: 48px;
|
|||
|
|
height: 60px;
|
|||
|
|
display: grid;
|
|||
|
|
place-items: center;
|
|||
|
|
border: 1px solid #ccd4df;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
background:
|
|||
|
|
linear-gradient(180deg, #ffffff, #f0f3f7);
|
|||
|
|
color: var(--muted);
|
|||
|
|
font-size: 10px;
|
|||
|
|
font-weight: 800;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.file-thumb.ticket {
|
|||
|
|
background:
|
|||
|
|
linear-gradient(90deg, transparent 12px, rgba(21,109,247,0.13) 12px, rgba(21,109,247,0.13) 13px, transparent 13px),
|
|||
|
|
linear-gradient(180deg, #fff, #edf5ff);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.file-row strong,
|
|||
|
|
.task-row strong,
|
|||
|
|
.agent-row strong {
|
|||
|
|
display: block;
|
|||
|
|
font-size: 13px;
|
|||
|
|
line-height: 1.35;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.file-row span,
|
|||
|
|
.task-row span,
|
|||
|
|
.agent-row span,
|
|||
|
|
.audit-row span {
|
|||
|
|
display: block;
|
|||
|
|
margin-top: 3px;
|
|||
|
|
color: var(--muted);
|
|||
|
|
font-size: 12px;
|
|||
|
|
line-height: 1.35;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tag {
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
min-height: 24px;
|
|||
|
|
padding: 3px 8px;
|
|||
|
|
border: 1px solid transparent;
|
|||
|
|
border-radius: 999px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
font-weight: 800;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tag.blue { background: var(--blue-soft); color: var(--blue); border-color: #c4dcff; }
|
|||
|
|
.tag.green { background: var(--green-soft); color: var(--green); border-color: #bee8ce; }
|
|||
|
|
.tag.amber { background: var(--amber-soft); color: var(--amber); border-color: #ffd992; }
|
|||
|
|
.tag.red { background: var(--red-soft); color: var(--red); border-color: #ffc9c3; }
|
|||
|
|
.tag.purple { background: var(--purple-soft); color: var(--purple); border-color: #d9d0ff; }
|
|||
|
|
.tag.gray { background: #eef1f5; color: #546071; border-color: #d9dfe7; }
|
|||
|
|
|
|||
|
|
.progress-shell {
|
|||
|
|
display: none;
|
|||
|
|
gap: 12px;
|
|||
|
|
margin-top: 16px;
|
|||
|
|
padding: 14px;
|
|||
|
|
border: 1px solid #c9d8f1;
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
background: #f4f8ff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.progress-shell.active {
|
|||
|
|
display: grid;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.progress-text {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
gap: 12px;
|
|||
|
|
color: #315078;
|
|||
|
|
font-size: 13px;
|
|||
|
|
font-weight: 700;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.table-wrap {
|
|||
|
|
overflow: auto;
|
|||
|
|
border: 1px solid var(--line);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
background: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
table {
|
|||
|
|
width: 100%;
|
|||
|
|
border-collapse: collapse;
|
|||
|
|
min-width: 820px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
th,
|
|||
|
|
td {
|
|||
|
|
padding: 12px 12px;
|
|||
|
|
border-bottom: 1px solid #edf0f4;
|
|||
|
|
text-align: left;
|
|||
|
|
vertical-align: middle;
|
|||
|
|
font-size: 13px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
th {
|
|||
|
|
position: sticky;
|
|||
|
|
top: 0;
|
|||
|
|
z-index: 1;
|
|||
|
|
background: #f7f8fa;
|
|||
|
|
color: #5f6b7d;
|
|||
|
|
font-size: 12px;
|
|||
|
|
font-weight: 800;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
tr:last-child td {
|
|||
|
|
border-bottom: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
td input {
|
|||
|
|
width: 112px;
|
|||
|
|
min-height: 32px;
|
|||
|
|
padding: 5px 8px;
|
|||
|
|
border: 1px solid transparent;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
background: #f6f8fb;
|
|||
|
|
color: #1b2029;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
td input:focus {
|
|||
|
|
border-color: #9ec0ff;
|
|||
|
|
background: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.field-grid {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|||
|
|
gap: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.field {
|
|||
|
|
display: grid;
|
|||
|
|
gap: 7px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.field label {
|
|||
|
|
color: #5d6878;
|
|||
|
|
font-size: 12px;
|
|||
|
|
font-weight: 800;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.field input,
|
|||
|
|
.field select,
|
|||
|
|
.field textarea {
|
|||
|
|
width: 100%;
|
|||
|
|
min-height: 38px;
|
|||
|
|
padding: 8px 10px;
|
|||
|
|
border: 1px solid var(--line);
|
|||
|
|
border-radius: 7px;
|
|||
|
|
background: #fff;
|
|||
|
|
color: var(--text);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.field textarea {
|
|||
|
|
min-height: 118px;
|
|||
|
|
resize: vertical;
|
|||
|
|
line-height: 1.55;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.ai-note {
|
|||
|
|
display: grid;
|
|||
|
|
gap: 10px;
|
|||
|
|
padding: 14px;
|
|||
|
|
border: 1px solid #d9d0ff;
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
background: var(--purple-soft);
|
|||
|
|
color: #3d317a;
|
|||
|
|
font-size: 13px;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.receipt-strip {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|||
|
|
gap: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.receipt {
|
|||
|
|
min-height: 138px;
|
|||
|
|
display: grid;
|
|||
|
|
align-content: space-between;
|
|||
|
|
padding: 12px;
|
|||
|
|
border: 1px solid var(--line);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
background:
|
|||
|
|
linear-gradient(180deg, #fff, #f5f7fa);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.receipt .top-line {
|
|||
|
|
height: 8px;
|
|||
|
|
width: 58%;
|
|||
|
|
border-radius: 999px;
|
|||
|
|
background: #c7d2e1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.receipt .lines {
|
|||
|
|
display: grid;
|
|||
|
|
gap: 7px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.receipt .lines span {
|
|||
|
|
height: 6px;
|
|||
|
|
border-radius: 999px;
|
|||
|
|
background: #e0e6ee;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.receipt .lines span:nth-child(2) { width: 76%; }
|
|||
|
|
.receipt .lines span:nth-child(3) { width: 54%; }
|
|||
|
|
|
|||
|
|
.receipt strong {
|
|||
|
|
color: #303846;
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.result-hero {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: auto 1fr auto;
|
|||
|
|
gap: 18px;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 18px;
|
|||
|
|
border: 1px solid var(--line);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
background: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.score-ring {
|
|||
|
|
width: 92px;
|
|||
|
|
height: 92px;
|
|||
|
|
display: grid;
|
|||
|
|
place-items: center;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background:
|
|||
|
|
conic-gradient(var(--ring-color, #f0a429) var(--ring, 62%), #e9edf3 0);
|
|||
|
|
color: #1e2530;
|
|||
|
|
font-size: 23px;
|
|||
|
|
font-weight: 900;
|
|||
|
|
font-variant-numeric: tabular-nums;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.score-ring::before {
|
|||
|
|
content: "";
|
|||
|
|
position: absolute;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.score-inner {
|
|||
|
|
width: 70px;
|
|||
|
|
height: 70px;
|
|||
|
|
display: grid;
|
|||
|
|
place-items: center;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.rule-card {
|
|||
|
|
display: grid;
|
|||
|
|
gap: 12px;
|
|||
|
|
padding: 14px;
|
|||
|
|
border: 1px solid var(--line);
|
|||
|
|
border-left: 4px solid var(--severity, #f0a429);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
background: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.rule-head {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
gap: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.rule-head h3 {
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: 15px;
|
|||
|
|
line-height: 1.35;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.rule-card p {
|
|||
|
|
margin: 0;
|
|||
|
|
color: #556174;
|
|||
|
|
font-size: 13px;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
details {
|
|||
|
|
color: #465366;
|
|||
|
|
font-size: 13px;
|
|||
|
|
line-height: 1.55;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
summary {
|
|||
|
|
cursor: pointer;
|
|||
|
|
color: var(--blue);
|
|||
|
|
font-weight: 800;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.supplement-item {
|
|||
|
|
display: grid;
|
|||
|
|
gap: 12px;
|
|||
|
|
padding: 14px;
|
|||
|
|
border: 1px solid var(--line);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
background: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.supplement-item header {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
gap: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.supplement-item h3 {
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.supplement-item p {
|
|||
|
|
margin: 5px 0 0;
|
|||
|
|
color: var(--muted);
|
|||
|
|
font-size: 13px;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.segmented {
|
|||
|
|
display: inline-grid;
|
|||
|
|
grid-auto-flow: column;
|
|||
|
|
gap: 2px;
|
|||
|
|
padding: 3px;
|
|||
|
|
border: 1px solid var(--line);
|
|||
|
|
border-radius: 8px;
|
|||
|
|
background: #eef1f5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.segmented button {
|
|||
|
|
min-height: 30px;
|
|||
|
|
border: 0;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
padding: 5px 10px;
|
|||
|
|
background: transparent;
|
|||
|
|
color: var(--muted);
|
|||
|
|
font-size: 12px;
|
|||
|
|
font-weight: 800;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.segmented button.active {
|
|||
|
|
background: #fff;
|
|||
|
|
color: var(--blue);
|
|||
|
|
box-shadow: 0 1px 2px rgba(31, 39, 51, 0.08);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.summary-line {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
gap: 16px;
|
|||
|
|
padding: 12px 0;
|
|||
|
|
border-bottom: 1px solid #edf0f4;
|
|||
|
|
color: var(--muted);
|
|||
|
|
font-size: 13px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.summary-line:last-child {
|
|||
|
|
border-bottom: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.summary-line strong {
|
|||
|
|
color: var(--text);
|
|||
|
|
font-variant-numeric: tabular-nums;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.panel-card {
|
|||
|
|
display: grid;
|
|||
|
|
gap: 14px;
|
|||
|
|
padding: 14px;
|
|||
|
|
margin-bottom: 14px;
|
|||
|
|
border: 1px solid var(--line);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
background: rgba(255,255,255,0.72);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.panel-card h3 {
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.panel-card p {
|
|||
|
|
margin: 0;
|
|||
|
|
color: var(--muted);
|
|||
|
|
font-size: 12px;
|
|||
|
|
line-height: 1.55;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.agent-row {
|
|||
|
|
grid-template-columns: 24px 1fr auto;
|
|||
|
|
padding: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-dot {
|
|||
|
|
width: 10px;
|
|||
|
|
height: 10px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: #b4bdca;
|
|||
|
|
box-shadow: 0 0 0 4px rgba(180,189,202,0.14);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-dot.done { background: var(--green); box-shadow: 0 0 0 4px rgba(17,132,91,0.14); }
|
|||
|
|
.status-dot.run { background: var(--blue); box-shadow: 0 0 0 4px rgba(21,109,247,0.14); }
|
|||
|
|
.status-dot.warn { background: #d98b12; box-shadow: 0 0 0 4px rgba(217,139,18,0.14); }
|
|||
|
|
|
|||
|
|
.timeline {
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.audit-row {
|
|||
|
|
grid-template-columns: 92px 1fr auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.audit-row time {
|
|||
|
|
color: var(--muted);
|
|||
|
|
font-size: 12px;
|
|||
|
|
font-variant-numeric: tabular-nums;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.empty {
|
|||
|
|
padding: 28px;
|
|||
|
|
border: 1px dashed #c5ccd7;
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
background: rgba(255,255,255,0.56);
|
|||
|
|
color: var(--muted);
|
|||
|
|
text-align: center;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.toast {
|
|||
|
|
position: fixed;
|
|||
|
|
right: 28px;
|
|||
|
|
bottom: 28px;
|
|||
|
|
z-index: 5;
|
|||
|
|
max-width: min(420px, calc(100vw - 56px));
|
|||
|
|
display: none;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
gap: 12px;
|
|||
|
|
padding: 14px 16px;
|
|||
|
|
border: 1px solid #bcd4ff;
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
background: rgba(255,255,255,0.95);
|
|||
|
|
box-shadow: var(--shadow-soft);
|
|||
|
|
color: #22314a;
|
|||
|
|
backdrop-filter: blur(16px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.toast.active {
|
|||
|
|
display: flex;
|
|||
|
|
animation: appear 160ms ease-out;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.toast strong {
|
|||
|
|
display: block;
|
|||
|
|
font-size: 13px;
|
|||
|
|
margin-bottom: 2px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.toast span {
|
|||
|
|
display: block;
|
|||
|
|
color: var(--muted);
|
|||
|
|
font-size: 12px;
|
|||
|
|
line-height: 1.45;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sr-only {
|
|||
|
|
position: absolute;
|
|||
|
|
width: 1px;
|
|||
|
|
height: 1px;
|
|||
|
|
padding: 0;
|
|||
|
|
margin: -1px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
clip: rect(0,0,0,0);
|
|||
|
|
white-space: nowrap;
|
|||
|
|
border: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 1180px) {
|
|||
|
|
.app-body {
|
|||
|
|
grid-template-columns: 82px minmax(0, 1fr);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.right-panel {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.brand p,
|
|||
|
|
.nav-title,
|
|||
|
|
.nav-item .label,
|
|||
|
|
.nav-badge,
|
|||
|
|
.sidebar-footer {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.brand h1 {
|
|||
|
|
font-size: 12px;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.nav-item {
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
justify-items: center;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 860px) {
|
|||
|
|
.desktop {
|
|||
|
|
padding: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.app-window {
|
|||
|
|
height: 100dvh;
|
|||
|
|
min-height: 100dvh;
|
|||
|
|
border-radius: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.titlebar {
|
|||
|
|
grid-template-columns: auto 1fr;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.title-actions {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.app-body {
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sidebar {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.main {
|
|||
|
|
padding: 18px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.grid-2,
|
|||
|
|
.grid-3,
|
|||
|
|
.field-grid,
|
|||
|
|
.receipt-strip {
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page-head {
|
|||
|
|
display: grid;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.headline-actions {
|
|||
|
|
justify-content: flex-start;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.result-hero {
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div class="desktop">
|
|||
|
|
<div class="app-window" role="application" aria-label="AI 报销预审中台原型">
|
|||
|
|
<header class="titlebar">
|
|||
|
|
<div class="traffic" aria-hidden="true"><span></span><span></span><span></span></div>
|
|||
|
|
<div class="toolbar">
|
|||
|
|
<label class="search" aria-label="搜索任务、票据、规则">
|
|||
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path d="m21 21-4.35-4.35"></path><circle cx="11" cy="11" r="7"></circle></svg>
|
|||
|
|
<input type="search" placeholder="搜索任务、票据、规则或审计日志">
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
<div class="title-actions">
|
|||
|
|
<span class="mac-chip">演示环境 · Mock OCR</span>
|
|||
|
|
<span class="avatar" aria-label="当前用户:林">林</span>
|
|||
|
|
</div>
|
|||
|
|
</header>
|
|||
|
|
|
|||
|
|
<div class="app-body">
|
|||
|
|
<aside class="sidebar" aria-label="主导航">
|
|||
|
|
<div class="brand">
|
|||
|
|
<h1>X-Financial</h1>
|
|||
|
|
<p>AI 报销预审中台 · 差旅 MVP</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<nav class="nav-section" id="nav"></nav>
|
|||
|
|
|
|||
|
|
<div class="sidebar-footer">
|
|||
|
|
<div class="mini-meter">
|
|||
|
|
<strong>本月报销健康度</strong>
|
|||
|
|
<div class="meter-row"><span>规则通过率</span><b id="sidePassRate">86%</b></div>
|
|||
|
|
<div class="bar" aria-hidden="true"><span id="sidePassBar" style="--value: 86%"></span></div>
|
|||
|
|
</div>
|
|||
|
|
<button class="btn ghost" id="resetDemo" type="button">重置演示数据</button>
|
|||
|
|
</div>
|
|||
|
|
</aside>
|
|||
|
|
|
|||
|
|
<main class="main" id="main">
|
|||
|
|
<section class="view active" id="view-home" data-view="home">
|
|||
|
|
<div class="page-head">
|
|||
|
|
<div>
|
|||
|
|
<span class="eyebrow">AI 操作层</span>
|
|||
|
|
<h2>把一段报销意图变成可预审的差旅报销单</h2>
|
|||
|
|
<p>面向员工的入口页:输入报销意图,系统创建任务并引导上传发票、火车票、机票行程单等材料。</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="headline-actions">
|
|||
|
|
<button class="btn" type="button" data-go="audit">查看审计</button>
|
|||
|
|
<button class="btn primary" type="button" id="createTaskTop">新建报销</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="grid-2">
|
|||
|
|
<div class="card pad intent-box">
|
|||
|
|
<div class="card-title">
|
|||
|
|
<div>
|
|||
|
|
<h3>报销意图</h3>
|
|||
|
|
<p>自然语言输入会交给受理 Agent 提取出差地点、时间、费用类型和上下文。</p>
|
|||
|
|
</div>
|
|||
|
|
<span class="tag blue">IntakeAgent</span>
|
|||
|
|
</div>
|
|||
|
|
<textarea id="intentInput" aria-label="报销意图">我上周去上海拜访客户,报销高铁、住宿和市内交通,项目是 CRM 升级交付,成本中心是销售华东。</textarea>
|
|||
|
|
<div class="quick-actions" aria-label="常用报销类型">
|
|||
|
|
<button class="quick" type="button" data-intent="我去北京参加客户复盘,需要报销机票、酒店和餐饮,项目为北区金融客户拓展。">报差旅</button>
|
|||
|
|
<button class="quick" type="button" data-intent="帮我看看这张增值税发票和酒店流水能不能报销,可能有住宿标准超额。">看发票能不能报</button>
|
|||
|
|
<button class="quick" type="button" data-intent="补充上次杭州出差的出租车票和行程说明,希望重新预审。">补充材料</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="headline-actions">
|
|||
|
|
<button class="btn primary" type="button" id="createTask">创建任务并上传材料</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="card pad">
|
|||
|
|
<div class="card-title">
|
|||
|
|
<div>
|
|||
|
|
<h3>最近任务</h3>
|
|||
|
|
<p>用于返回草稿、查看预审结果或审计轨迹。</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="task-list">
|
|||
|
|
<button class="task-row" type="button" data-go="precheck">
|
|||
|
|
<span class="file-thumb ticket">TRIP</span>
|
|||
|
|
<span><strong>上海客户拜访差旅</strong><span>需补件 · 酒店流水缺失 · ¥4,836.50</span></span>
|
|||
|
|
<span class="tag amber">待处理</span>
|
|||
|
|
</button>
|
|||
|
|
<button class="task-row" type="button" data-go="confirm">
|
|||
|
|
<span class="file-thumb">VAT</span>
|
|||
|
|
<span><strong>北京售前支持</strong><span>预审通过 · 等待确认提交 · ¥2,418.00</span></span>
|
|||
|
|
<span class="tag green">通过</span>
|
|||
|
|
</button>
|
|||
|
|
<button class="task-row" type="button" data-go="audit">
|
|||
|
|
<span class="file-thumb">LOG</span>
|
|||
|
|
<span><strong>广州渠道会议</strong><span>已同步到 expense_system · 8 条审计记录</span></span>
|
|||
|
|
<span class="tag gray">归档</span>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="grid-3" style="margin-top: 18px;">
|
|||
|
|
<div class="stat"><span>今日自动识别</span><strong>48</strong><small>发票、火车票、行程单合计</small></div>
|
|||
|
|
<div class="stat"><span>需人工补件</span><strong>7</strong><small>主要来自住宿流水和费用日期异常</small></div>
|
|||
|
|
<div class="stat"><span>平均预审耗时</span><strong>42s</strong><small>Mock OCR 环境下的端到端耗时</small></div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section class="view" id="view-upload" data-view="upload">
|
|||
|
|
<div class="page-head">
|
|||
|
|
<div>
|
|||
|
|
<span class="eyebrow">材料收集</span>
|
|||
|
|
<h2>上传票据并启动 OCR / Agent 编排</h2>
|
|||
|
|
<p>支持多文件拖拽,文件类型限定为 PDF、JPG、PNG。开始识别后进入 CREATED → MATERIAL_COLLECTING → PARSING → DRAFT_GENERATED。</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="headline-actions">
|
|||
|
|
<button class="btn" type="button" data-go="home">返回入口</button>
|
|||
|
|
<button class="btn primary" type="button" id="runAgentBtn">开始识别</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="grid-2">
|
|||
|
|
<div class="card pad">
|
|||
|
|
<div class="card-title">
|
|||
|
|
<div>
|
|||
|
|
<h3>票据上传</h3>
|
|||
|
|
<p>拖入真实文件会显示文件名;不选择文件也可以使用内置样例继续演示。</p>
|
|||
|
|
</div>
|
|||
|
|
<span class="tag blue">PDF / JPG / PNG</span>
|
|||
|
|
</div>
|
|||
|
|
<label class="dropzone" id="dropzone">
|
|||
|
|
<input id="fileInput" type="file" multiple accept=".pdf,.jpg,.jpeg,.png">
|
|||
|
|
<span class="drop-inner">
|
|||
|
|
<span class="big-icon" aria-hidden="true">
|
|||
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 16V4"></path><path d="m7 9 5-5 5 5"></path><path d="M20 16.5V19a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2.5"></path></svg>
|
|||
|
|
</span>
|
|||
|
|
<span>
|
|||
|
|
<h3>拖拽票据到这里</h3>
|
|||
|
|
<p>建议上传增值税发票、火车票、酒店流水和行程单,用于验证附件完整性与费用类型匹配规则。</p>
|
|||
|
|
</span>
|
|||
|
|
<span class="btn" aria-hidden="true">选择文件</span>
|
|||
|
|
</span>
|
|||
|
|
</label>
|
|||
|
|
<div class="progress-shell" id="progressShell">
|
|||
|
|
<div class="progress-text"><span id="progressLabel">准备启动 Agent</span><span id="progressValue">0%</span></div>
|
|||
|
|
<div class="bar"><span id="progressBar" style="--value: 0%"></span></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="card pad">
|
|||
|
|
<div class="card-title">
|
|||
|
|
<div>
|
|||
|
|
<h3>已上传文件</h3>
|
|||
|
|
<p>OCR 结果会在草稿页以低置信度标注和可编辑字段展示。</p>
|
|||
|
|
</div>
|
|||
|
|
<select id="docType" aria-label="票据类型">
|
|||
|
|
<option>自动识别类型</option>
|
|||
|
|
<option>增值税发票</option>
|
|||
|
|
<option>火车票</option>
|
|||
|
|
<option>机票行程单</option>
|
|||
|
|
<option>酒店流水</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div class="file-list" id="fileList"></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section class="view" id="view-draft" data-view="draft">
|
|||
|
|
<div class="page-head">
|
|||
|
|
<div>
|
|||
|
|
<span class="eyebrow">影子报销账本</span>
|
|||
|
|
<h2>OCR 已生成报销草稿,关键字段可人工修正</h2>
|
|||
|
|
<p>草稿页对应 ShadowReimbursement + ReimbursementItem,编辑后再执行规则引擎预审。</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="headline-actions">
|
|||
|
|
<button class="btn" type="button" data-go="upload">返回上传</button>
|
|||
|
|
<button class="btn primary" type="button" id="runPrecheckBtn">执行预审</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="card pad" style="margin-bottom: 18px;">
|
|||
|
|
<div class="card-title">
|
|||
|
|
<div>
|
|||
|
|
<h3>报销人信息</h3>
|
|||
|
|
<p>Agent 从意图和组织配置中补齐部门、成本中心、项目。</p>
|
|||
|
|
</div>
|
|||
|
|
<span class="tag purple">AI 自动生成</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="field-grid">
|
|||
|
|
<div class="field"><label for="employee">报销人</label><input id="employee" value="林一舟"></div>
|
|||
|
|
<div class="field"><label for="department">部门</label><input id="department" value="销售华东"></div>
|
|||
|
|
<div class="field"><label for="costCenter">成本中心</label><input id="costCenter" value="CC-SH-SALES"></div>
|
|||
|
|
<div class="field"><label for="project">项目</label><input id="project" value="CRM 升级交付"></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="grid-2">
|
|||
|
|
<div class="card pad">
|
|||
|
|
<div class="card-title">
|
|||
|
|
<div>
|
|||
|
|
<h3>费用明细</h3>
|
|||
|
|
<p>金额可直接编辑,风险标签会在预审后刷新。</p>
|
|||
|
|
</div>
|
|||
|
|
<span class="tag gray" id="draftStatus">待预审</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="table-wrap">
|
|||
|
|
<table aria-label="费用明细表">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>费用类型</th>
|
|||
|
|
<th>金额</th>
|
|||
|
|
<th>税额</th>
|
|||
|
|
<th>发生日期</th>
|
|||
|
|
<th>城市</th>
|
|||
|
|
<th>商户</th>
|
|||
|
|
<th>风险</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody id="expenseBody"></tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="card pad">
|
|||
|
|
<div class="card-title">
|
|||
|
|
<div>
|
|||
|
|
<h3>AI 识别标注</h3>
|
|||
|
|
<p>低置信度字段和附件缺口会在这里提示。</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="ai-note">
|
|||
|
|
<strong>ParseAgent 发现 2 个需要关注的点</strong>
|
|||
|
|
<span>住宿金额高于上海 T2 标准 18%,且酒店流水附件未找到。出租车票日期与出差日期匹配,但商户字段置信度 0.71。</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="receipt-strip" style="margin-top: 14px;">
|
|||
|
|
<div class="receipt"><div class="top-line"></div><div class="lines"><span></span><span></span><span></span></div><strong>高铁票 · 可信</strong></div>
|
|||
|
|
<div class="receipt"><div class="top-line"></div><div class="lines"><span></span><span></span><span></span></div><strong>酒店发票 · 需流水</strong></div>
|
|||
|
|
<div class="receipt"><div class="top-line"></div><div class="lines"><span></span><span></span><span></span></div><strong>出租车票 · 低置信</strong></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section class="view" id="view-precheck" data-view="precheck">
|
|||
|
|
<div class="page-head">
|
|||
|
|
<div>
|
|||
|
|
<span class="eyebrow">规则预审</span>
|
|||
|
|
<h2 id="precheckTitle">预审结果:需补件后再提交</h2>
|
|||
|
|
<p>规则引擎执行 6 条 MVP 核心规则,并由 ExplainAgent 转化为面向用户的解释和修改建议。</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="headline-actions">
|
|||
|
|
<button class="btn" type="button" data-go="draft">回到草稿</button>
|
|||
|
|
<button class="btn" type="button" data-go="supplement" id="supplementJump">一键补件</button>
|
|||
|
|
<button class="btn primary" type="button" data-go="confirm" id="confirmJump">确认提交</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="result-hero" style="margin-bottom: 18px;">
|
|||
|
|
<div class="score-ring" id="scoreRing"><div class="score-inner" id="scoreText">62</div></div>
|
|||
|
|
<div>
|
|||
|
|
<h3 id="overallTitle" style="margin: 0 0 6px;">发现 2 个风险项和 1 个缺件项</h3>
|
|||
|
|
<p id="overallDesc" style="margin: 0; color: var(--muted); line-height: 1.6;">当前可以继续补充酒店流水,并调整住宿金额或补充审批说明。补件后可重新预审。</p>
|
|||
|
|
</div>
|
|||
|
|
<span class="tag amber" id="overallTag">需补件</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="grid-2">
|
|||
|
|
<div class="card pad">
|
|||
|
|
<div class="card-title">
|
|||
|
|
<div>
|
|||
|
|
<h3>命中规则</h3>
|
|||
|
|
<p>每条规则展示问题、制度依据和建议动作。</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="file-list" id="ruleList"></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="card pad">
|
|||
|
|
<div class="card-title">
|
|||
|
|
<div>
|
|||
|
|
<h3>通过项</h3>
|
|||
|
|
<p>通过项用来建立用户信心,减少重复沟通。</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="check-list" id="passList"></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section class="view" id="view-supplement" data-view="supplement">
|
|||
|
|
<div class="page-head">
|
|||
|
|
<div>
|
|||
|
|
<span class="eyebrow">补件交互</span>
|
|||
|
|
<h2>按规则命中项补充材料和说明</h2>
|
|||
|
|
<p>补件页支持补充附件、补充说明、修改字段三类动作,提交后回到预审结果页重新计算风险。</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="headline-actions">
|
|||
|
|
<button class="btn" type="button" data-go="precheck">返回预审</button>
|
|||
|
|
<button class="btn primary" type="button" id="submitSupplement">提交补件并重新预审</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="grid-2">
|
|||
|
|
<div class="card pad">
|
|||
|
|
<div class="card-title">
|
|||
|
|
<div>
|
|||
|
|
<h3>待补件清单</h3>
|
|||
|
|
<p>由 ExplainAgent 根据缺件类规则自动创建。</p>
|
|||
|
|
</div>
|
|||
|
|
<span class="tag amber">2 项</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="file-list">
|
|||
|
|
<div class="supplement-item">
|
|||
|
|
<header>
|
|||
|
|
<div>
|
|||
|
|
<h3>补充酒店流水</h3>
|
|||
|
|
<p>住宿费必须提供酒店水单或入住明细,用于核对入住日期、房费和实际支付金额。</p>
|
|||
|
|
</div>
|
|||
|
|
<span class="tag amber">补充附件</span>
|
|||
|
|
</header>
|
|||
|
|
<button class="btn" type="button" id="mockAttach">添加酒店流水样例</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="supplement-item">
|
|||
|
|
<header>
|
|||
|
|
<div>
|
|||
|
|
<h3>说明住宿标准超额原因</h3>
|
|||
|
|
<p>上海 T2 住宿标准为 ¥650/晚,当前票据折算 ¥768/晚。可说明客户指定酒店或会务协议价。</p>
|
|||
|
|
</div>
|
|||
|
|
<span class="tag purple">补充说明</span>
|
|||
|
|
</header>
|
|||
|
|
<div class="field">
|
|||
|
|
<label for="supplementText">说明内容</label>
|
|||
|
|
<textarea id="supplementText">客户临时要求在会场附近住宿,酒店为客户协议推荐,超额部分已由直属经理口头确认。</textarea>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="card pad">
|
|||
|
|
<div class="card-title">
|
|||
|
|
<div>
|
|||
|
|
<h3>补件预览</h3>
|
|||
|
|
<p>提交后系统会自动触发 MATERIAL_COLLECTING → PRECHECKING。</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="file-list" id="supplementPreview">
|
|||
|
|
<div class="empty">尚未添加补件附件。可以直接提交说明,系统会用样例酒店流水完成演示。</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section class="view" id="view-confirm" data-view="confirm">
|
|||
|
|
<div class="page-head">
|
|||
|
|
<div>
|
|||
|
|
<span class="eyebrow">提交确认</span>
|
|||
|
|
<h2>确认最终报销单并模拟同步后端系统</h2>
|
|||
|
|
<p>预审通过后,SyncAgent 将影子账本映射为标准报销单,并写入同步记录和审计日志。</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="headline-actions">
|
|||
|
|
<button class="btn" type="button" data-go="precheck">返回预审</button>
|
|||
|
|
<button class="btn primary" type="button" id="submitFinal">确认提交</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="grid-2">
|
|||
|
|
<div class="card pad">
|
|||
|
|
<div class="card-title">
|
|||
|
|
<div>
|
|||
|
|
<h3>最终摘要</h3>
|
|||
|
|
<p>提交前不可编辑,若需调整请回到草稿。</p>
|
|||
|
|
</div>
|
|||
|
|
<span class="tag green" id="confirmState">预审通过</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="summary-line"><span>报销人</span><strong>林一舟 · 销售华东</strong></div>
|
|||
|
|
<div class="summary-line"><span>报销事由</span><strong>上海客户拜访差旅</strong></div>
|
|||
|
|
<div class="summary-line"><span>费用总额</span><strong id="confirmTotal">¥0.00</strong></div>
|
|||
|
|
<div class="summary-line"><span>附件数量</span><strong id="confirmDocs">4 份</strong></div>
|
|||
|
|
<div class="summary-line"><span>同步目标</span><strong>expense_system</strong></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="card pad">
|
|||
|
|
<div class="card-title">
|
|||
|
|
<div>
|
|||
|
|
<h3>同步设置</h3>
|
|||
|
|
<p>MVP 默认使用 mock adapter,展示提交中、已同步、同步失败重试状态。</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="segmented" aria-label="同步目标系统">
|
|||
|
|
<button class="active" type="button">expense_system</button>
|
|||
|
|
<button type="button">ERP</button>
|
|||
|
|
<button type="button">仅归档</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="progress-shell" id="syncShell">
|
|||
|
|
<div class="progress-text"><span id="syncLabel">等待提交</span><span id="syncValue">0%</span></div>
|
|||
|
|
<div class="bar"><span id="syncBar" style="--value: 0%"></span></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section class="view" id="view-audit" data-view="audit">
|
|||
|
|
<div class="page-head">
|
|||
|
|
<div>
|
|||
|
|
<span class="eyebrow">审计日志</span>
|
|||
|
|
<h2>关键操作均写入可追溯时间线</h2>
|
|||
|
|
<p>记录文件上传、OCR 识别、Agent 调用、规则命中、用户补件、用户确认和后端同步。</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="headline-actions">
|
|||
|
|
<div class="segmented" aria-label="日志筛选">
|
|||
|
|
<button class="active" type="button">全部</button>
|
|||
|
|
<button type="button">Agent</button>
|
|||
|
|
<button type="button">规则</button>
|
|||
|
|
<button type="button">同步</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="card pad">
|
|||
|
|
<div class="timeline" id="auditList"></div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
</main>
|
|||
|
|
|
|||
|
|
<aside class="right-panel" aria-label="AI 状态面板">
|
|||
|
|
<div class="panel-card">
|
|||
|
|
<h3>当前任务</h3>
|
|||
|
|
<p id="taskSummary">上海客户拜访差旅 · TASK-20260424-018</p>
|
|||
|
|
<div class="summary-line"><span>状态</span><strong id="taskState">CREATED</strong></div>
|
|||
|
|
<div class="summary-line"><span>总金额</span><strong id="panelTotal">¥0.00</strong></div>
|
|||
|
|
<div class="summary-line"><span>风险等级</span><strong id="panelRisk">未预审</strong></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="panel-card">
|
|||
|
|
<h3>Agent 编排</h3>
|
|||
|
|
<div class="agent-list" id="agentList"></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="panel-card">
|
|||
|
|
<h3>核心规则</h3>
|
|||
|
|
<div class="check-list">
|
|||
|
|
<div class="check-row"><span class="status-dot done"></span><span><strong>必填字段校验</strong><span>事由、日期、城市、金额完整</span></span><span class="tag green">通过</span></div>
|
|||
|
|
<div class="check-row"><span class="status-dot warn"></span><span><strong>附件完整性</strong><span>住宿费需要酒店流水</span></span><span class="tag amber">待补件</span></div>
|
|||
|
|
<div class="check-row"><span class="status-dot warn"></span><span><strong>金额标准</strong><span>上海住宿超 T2 标准</span></span><span class="tag amber">说明</span></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</aside>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
const icons = {
|
|||
|
|
home: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m3 11 9-8 9 8"></path><path d="M5 10v10h14V10"></path><path d="M9 20v-6h6v6"></path></svg>',
|
|||
|
|
upload: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 16V4"></path><path d="m7 9 5-5 5 5"></path><path d="M20 16.5V19a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2.5"></path></svg>',
|
|||
|
|
draft: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M6 2h9l5 5v15H6z"></path><path d="M14 2v6h6"></path><path d="M9 13h6"></path><path d="M9 17h6"></path></svg>',
|
|||
|
|
precheck: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"></path></svg>',
|
|||
|
|
supplement: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 5v14"></path><path d="M5 12h14"></path><path d="M4 4h16v16H4z"></path></svg>',
|
|||
|
|
confirm: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M9 12l2 2 4-5"></path><circle cx="12" cy="12" r="9"></circle></svg>',
|
|||
|
|
audit: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M4 4h16v16H4z"></path><path d="M8 8h8"></path><path d="M8 12h8"></path><path d="M8 16h5"></path></svg>'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const navItems = [
|
|||
|
|
{ id: 'home', label: '报销入口', badge: '1', icon: icons.home },
|
|||
|
|
{ id: 'upload', label: '票据上传', badge: '2', icon: icons.upload },
|
|||
|
|
{ id: 'draft', label: '报销草稿', badge: '3', icon: icons.draft },
|
|||
|
|
{ id: 'precheck', label: '预审结果', badge: '4', icon: icons.precheck },
|
|||
|
|
{ id: 'supplement', label: '补件交互', badge: '5', icon: icons.supplement },
|
|||
|
|
{ id: 'confirm', label: '提交确认', badge: '6', icon: icons.confirm },
|
|||
|
|
{ id: 'audit', label: '审计日志', badge: 'log', icon: icons.audit }
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const baseFiles = [
|
|||
|
|
{ name: '上海-北京南高铁票.pdf', type: '火车票', size: '436 KB', confidence: '0.96' },
|
|||
|
|
{ name: '上海酒店增值税发票.jpg', type: '增值税发票', size: '812 KB', confidence: '0.93' },
|
|||
|
|
{ name: '市内交通出租车票.png', type: '出租车票', size: '288 KB', confidence: '0.71' }
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const expenses = [
|
|||
|
|
{ type: '城际交通', amount: 1126.5, tax: 0, date: '2026-04-16', city: '上海', vendor: '中国铁路', risk: 'low' },
|
|||
|
|
{ type: '住宿费', amount: 2304, tax: 130.42, date: '2026-04-16', city: '上海', vendor: '衡山商务酒店', risk: 'medium' },
|
|||
|
|
{ type: '市内交通', amount: 286, tax: 0, date: '2026-04-17', city: '上海', vendor: '出租车票据', risk: 'medium' },
|
|||
|
|
{ type: '餐饮费', amount: 1120, tax: 63.4, date: '2026-04-17', city: '上海', vendor: '客户接待餐厅', risk: 'low' }
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
let state = {
|
|||
|
|
view: 'home',
|
|||
|
|
files: [...baseFiles],
|
|||
|
|
supplemented: false,
|
|||
|
|
submitted: false,
|
|||
|
|
taskStatus: 'NEED_SUPPLEMENT',
|
|||
|
|
risk: '需补件',
|
|||
|
|
progress: 0,
|
|||
|
|
audit: [
|
|||
|
|
['09:12', '任务创建', '林一舟创建差旅报销任务,意图已进入受理 Agent。', '用户'],
|
|||
|
|
['09:14', '文件上传', '上传高铁票、酒店发票、出租车票共 3 份。', '文件'],
|
|||
|
|
['09:15', 'OCR 识别', 'Mock OCR 提取 4 条费用明细,1 个字段低置信。', 'Agent'],
|
|||
|
|
['09:16', '规则命中', '附件完整性和住宿金额标准需要处理。', '规则']
|
|||
|
|
],
|
|||
|
|
agents: [
|
|||
|
|
{ name: 'IntakeAgent', desc: '解析报销意图与上下文', status: 'done' },
|
|||
|
|
{ name: 'ParseAgent', desc: 'OCR 识别并生成草稿', status: 'done' },
|
|||
|
|
{ name: 'RuleCheckAgent', desc: '执行 6 条预审规则', status: 'warn' },
|
|||
|
|
{ name: 'ExplainAgent', desc: '生成解释和补件请求', status: 'warn' },
|
|||
|
|
{ name: 'SyncAgent', desc: '等待用户确认提交', status: 'idle' }
|
|||
|
|
]
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const $ = (selector) => document.querySelector(selector);
|
|||
|
|
const $$ = (selector) => [...document.querySelectorAll(selector)];
|
|||
|
|
|
|||
|
|
function money(value) {
|
|||
|
|
return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(value);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function totalAmount() {
|
|||
|
|
return expenses.reduce((sum, item) => sum + Number(item.amount || 0), 0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function toast(title, body) {
|
|||
|
|
const node = $('#toast');
|
|||
|
|
node.innerHTML = '<div class="status-dot done"></div><div><strong>' + title + '</strong><span>' + body + '</span></div>';
|
|||
|
|
node.classList.add('active');
|
|||
|
|
clearTimeout(node.timer);
|
|||
|
|
node.timer = setTimeout(() => node.classList.remove('active'), 3600);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function addAudit(action, detail, actor = '系统') {
|
|||
|
|
const now = new Date();
|
|||
|
|
const time = now.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
|||
|
|
state.audit.unshift([time, action, detail, actor]);
|
|||
|
|
renderAudit();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderNav() {
|
|||
|
|
const nav = $('#nav');
|
|||
|
|
nav.innerHTML = '<div class="nav-title">工作流</div>' + navItems.map(item => `
|
|||
|
|
<button class="nav-item ${state.view === item.id ? 'active' : ''}" type="button" data-go="${item.id}" aria-label="${item.label}">
|
|||
|
|
${item.icon}
|
|||
|
|
<span class="label">${item.label}</span>
|
|||
|
|
<span class="nav-badge">${item.badge}</span>
|
|||
|
|
</button>
|
|||
|
|
`).join('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function go(view) {
|
|||
|
|
state.view = view;
|
|||
|
|
$$('.view').forEach(node => node.classList.toggle('active', node.dataset.view === view));
|
|||
|
|
renderNav();
|
|||
|
|
renderPanel();
|
|||
|
|
if (view === 'precheck') renderPrecheck();
|
|||
|
|
if (view === 'confirm') renderConfirm();
|
|||
|
|
if (view === 'audit') renderAudit();
|
|||
|
|
$('#main').scrollTo({ top: 0, behavior: 'smooth' });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderFiles() {
|
|||
|
|
const list = $('#fileList');
|
|||
|
|
if (!state.files.length) {
|
|||
|
|
list.innerHTML = '<div class="empty">还没有上传文件。拖拽票据或点击上传区域选择文件。</div>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
list.innerHTML = state.files.map((file, index) => `
|
|||
|
|
<div class="file-row">
|
|||
|
|
<span class="file-thumb ${file.type.includes('票') ? 'ticket' : ''}">${file.type.slice(0, 2)}</span>
|
|||
|
|
<span><strong>${file.name}</strong><span>${file.type} · ${file.size} · OCR 置信度 ${file.confidence}</span></span>
|
|||
|
|
<button class="icon-btn" type="button" aria-label="移除 ${file.name}" data-remove-file="${index}">
|
|||
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
`).join('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function riskTag(risk) {
|
|||
|
|
if (risk === 'low') return '<span class="tag green">低</span>';
|
|||
|
|
if (risk === 'medium') return '<span class="tag amber">中</span>';
|
|||
|
|
if (risk === 'high') return '<span class="tag red">高</span>';
|
|||
|
|
return '<span class="tag gray">待定</span>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderExpenses() {
|
|||
|
|
$('#expenseBody').innerHTML = expenses.map((item, index) => `
|
|||
|
|
<tr>
|
|||
|
|
<td>${item.type}</td>
|
|||
|
|
<td><input aria-label="${item.type}金额" data-amount="${index}" value="${item.amount}"></td>
|
|||
|
|
<td>${money(item.tax)}</td>
|
|||
|
|
<td>${item.date}</td>
|
|||
|
|
<td>${item.city}</td>
|
|||
|
|
<td>${item.vendor}</td>
|
|||
|
|
<td>${riskTag(state.supplemented ? 'low' : item.risk)}</td>
|
|||
|
|
</tr>
|
|||
|
|
`).join('');
|
|||
|
|
renderPanel();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderPrecheck() {
|
|||
|
|
const passed = state.supplemented;
|
|||
|
|
$('#confirmJump').disabled = !passed;
|
|||
|
|
$('#supplementJump').disabled = passed;
|
|||
|
|
$('#precheckTitle').textContent = passed ? '预审结果:已通过,可确认提交' : '预审结果:需补件后再提交';
|
|||
|
|
$('#overallTitle').textContent = passed ? '6 条核心规则全部通过' : '发现 2 个风险项和 1 个缺件项';
|
|||
|
|
$('#overallDesc').textContent = passed ? '补充酒店流水和超标说明后,规则引擎已重新计算为可提交状态。' : '当前可以继续补充酒店流水,并调整住宿金额或补充审批说明。补件后可重新预审。';
|
|||
|
|
$('#overallTag').className = 'tag ' + (passed ? 'green' : 'amber');
|
|||
|
|
$('#overallTag').textContent = passed ? '通过' : '需补件';
|
|||
|
|
$('#scoreText').textContent = passed ? '96' : '62';
|
|||
|
|
$('#scoreRing').style.setProperty('--ring', passed ? '96%' : '62%');
|
|||
|
|
$('#scoreRing').style.setProperty('--ring-color', passed ? '#11845b' : '#f0a429');
|
|||
|
|
state.risk = passed ? '低风险' : '需补件';
|
|||
|
|
state.taskStatus = passed ? 'PENDING_USER_CONFIRM' : 'NEED_SUPPLEMENT';
|
|||
|
|
|
|||
|
|
const rules = passed ? [
|
|||
|
|
{ title: '住宿附件完整性已通过', code: 'ATTACHMENT_CHECK', level: 'green', line: '#11845b', desc: '已补充酒店流水,入住日期、房费和支付金额可核对。', suggestion: '可进入提交确认。' }
|
|||
|
|
] : [
|
|||
|
|
{ title: '住宿费缺少酒店流水', code: 'ATTACHMENT_CHECK', level: 'amber', line: '#d98b12', desc: '住宿费必须提供酒店水单或入住明细,当前仅识别到增值税发票。', suggestion: '补充酒店流水,或上传包含入住明细的订单截图。' },
|
|||
|
|
{ title: '住宿金额超过城市标准', code: 'AMOUNT_LIMIT', level: 'amber', line: '#d98b12', desc: '上海 T2 住宿标准为 ¥650/晚,当前折算 ¥768/晚。', suggestion: '调整金额,或补充客户指定酒店、会务协议价等审批说明。' },
|
|||
|
|
{ title: '出租车票商户字段置信度偏低', code: 'OCR_LOW_CONFIDENCE', level: 'purple', line: '#6b4fd8', desc: 'OCR 对商户字段置信度为 0.71,不影响提交但建议人工确认。', suggestion: '在草稿页检查商户字段,必要时手动修正。' }
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
$('#ruleList').innerHTML = rules.map(rule => `
|
|||
|
|
<article class="rule-card" style="--severity:${rule.line}">
|
|||
|
|
<div class="rule-head">
|
|||
|
|
<div>
|
|||
|
|
<h3>${rule.title}</h3>
|
|||
|
|
<p>${rule.code}</p>
|
|||
|
|
</div>
|
|||
|
|
<span class="tag ${rule.level}">${rule.level === 'green' ? '通过' : '命中'}</span>
|
|||
|
|
</div>
|
|||
|
|
<p>${rule.desc}</p>
|
|||
|
|
<details>
|
|||
|
|
<summary>查看制度依据和建议</summary>
|
|||
|
|
<p>制度依据:差旅报销制度 7.2,住宿费需要可核对入住明细;城市/职级标准按金额标准表执行。</p>
|
|||
|
|
<p>修改建议:${rule.suggestion}</p>
|
|||
|
|
</details>
|
|||
|
|
</article>
|
|||
|
|
`).join('');
|
|||
|
|
|
|||
|
|
const passItems = [
|
|||
|
|
['必填字段校验', '报销人、部门、事由、金额、发生日期完整。'],
|
|||
|
|
['重复发票检查', '未发现 invoice_code + invoice_number + amount 重复。'],
|
|||
|
|
['费用日期合理性', '费用发生日期在出差期间内。'],
|
|||
|
|
['费用类型匹配', '票据类型与费用类型匹配。']
|
|||
|
|
];
|
|||
|
|
$('#passList').innerHTML = passItems.map(item => `
|
|||
|
|
<div class="check-row">
|
|||
|
|
<span class="status-dot done"></span>
|
|||
|
|
<span><strong>${item[0]}</strong><span>${item[1]}</span></span>
|
|||
|
|
<span class="tag green">通过</span>
|
|||
|
|
</div>
|
|||
|
|
`).join('');
|
|||
|
|
renderPanel();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderConfirm() {
|
|||
|
|
$('#confirmTotal').textContent = money(totalAmount());
|
|||
|
|
$('#confirmDocs').textContent = (state.files.length + (state.supplemented ? 1 : 0)) + ' 份';
|
|||
|
|
$('#confirmState').textContent = state.submitted ? '已同步' : '预审通过';
|
|||
|
|
$('#confirmState').className = 'tag ' + (state.submitted ? 'blue' : 'green');
|
|||
|
|
renderPanel();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderAudit() {
|
|||
|
|
$('#auditList').innerHTML = state.audit.map(row => `
|
|||
|
|
<div class="audit-row">
|
|||
|
|
<time>${row[0]}</time>
|
|||
|
|
<span><strong>${row[1]}</strong><span>${row[2]}</span></span>
|
|||
|
|
<span class="tag gray">${row[3]}</span>
|
|||
|
|
</div>
|
|||
|
|
`).join('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderPanel() {
|
|||
|
|
$('#taskState').textContent = state.taskStatus;
|
|||
|
|
$('#panelTotal').textContent = money(totalAmount());
|
|||
|
|
$('#panelRisk').textContent = state.risk;
|
|||
|
|
$('#sidePassRate').textContent = state.supplemented ? '96%' : '86%';
|
|||
|
|
$('#sidePassBar').style.setProperty('--value', state.supplemented ? '96%' : '86%');
|
|||
|
|
$('#agentList').innerHTML = state.agents.map(agent => `
|
|||
|
|
<div class="agent-row">
|
|||
|
|
<span class="status-dot ${agent.status === 'done' ? 'done' : agent.status === 'run' ? 'run' : agent.status === 'warn' ? 'warn' : ''}"></span>
|
|||
|
|
<span><strong>${agent.name}</strong><span>${agent.desc}</span></span>
|
|||
|
|
<span class="tag ${agent.status === 'done' ? 'green' : agent.status === 'run' ? 'blue' : agent.status === 'warn' ? 'amber' : 'gray'}">${agent.status === 'done' ? '完成' : agent.status === 'run' ? '运行' : agent.status === 'warn' ? '待处理' : '等待'}</span>
|
|||
|
|
</div>
|
|||
|
|
`).join('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function startRecognition() {
|
|||
|
|
if (!state.files.length) {
|
|||
|
|
state.files = [...baseFiles];
|
|||
|
|
renderFiles();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const steps = [
|
|||
|
|
['MATERIAL_COLLECTING', '校验文件类型和票据类型', 18],
|
|||
|
|
['PARSING', '调用 Mock OCR 提取票据字段', 46],
|
|||
|
|
['DRAFT_GENERATED', 'ParseAgent 汇总费用明细', 76],
|
|||
|
|
['DRAFT_GENERATED', '写入影子报销账本', 100]
|
|||
|
|
];
|
|||
|
|
const shell = $('#progressShell');
|
|||
|
|
shell.classList.add('active');
|
|||
|
|
$('#runAgentBtn').disabled = true;
|
|||
|
|
|
|||
|
|
let index = 0;
|
|||
|
|
const tick = () => {
|
|||
|
|
const step = steps[index];
|
|||
|
|
state.taskStatus = step[0];
|
|||
|
|
$('#progressLabel').textContent = step[1];
|
|||
|
|
$('#progressValue').textContent = step[2] + '%';
|
|||
|
|
$('#progressBar').style.setProperty('--value', step[2] + '%');
|
|||
|
|
state.agents = state.agents.map(agent => {
|
|||
|
|
if (agent.name === 'ParseAgent') return { ...agent, status: step[2] < 100 ? 'run' : 'done' };
|
|||
|
|
return agent;
|
|||
|
|
});
|
|||
|
|
renderPanel();
|
|||
|
|
index += 1;
|
|||
|
|
if (index < steps.length) {
|
|||
|
|
setTimeout(tick, 560);
|
|||
|
|
} else {
|
|||
|
|
$('#runAgentBtn').disabled = false;
|
|||
|
|
addAudit('Agent 调用', '受理 Agent 和解析 Agent 完成,生成影子报销草稿。', 'Agent');
|
|||
|
|
toast('草稿已生成', 'OCR 识别完成,已进入报销草稿页。');
|
|||
|
|
renderExpenses();
|
|||
|
|
go('draft');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
tick();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function runPrecheck() {
|
|||
|
|
state.taskStatus = state.supplemented ? 'PENDING_USER_CONFIRM' : 'NEED_SUPPLEMENT';
|
|||
|
|
state.agents = state.agents.map(agent => {
|
|||
|
|
if (agent.name === 'RuleCheckAgent') return { ...agent, status: state.supplemented ? 'done' : 'warn' };
|
|||
|
|
if (agent.name === 'ExplainAgent') return { ...agent, status: state.supplemented ? 'done' : 'warn' };
|
|||
|
|
if (agent.name === 'SyncAgent') return { ...agent, status: 'idle' };
|
|||
|
|
return agent;
|
|||
|
|
});
|
|||
|
|
$('#draftStatus').textContent = state.supplemented ? '预审通过' : '需补件';
|
|||
|
|
$('#draftStatus').className = 'tag ' + (state.supplemented ? 'green' : 'amber');
|
|||
|
|
addAudit('规则预审', state.supplemented ? '6 条核心规则通过,等待用户确认提交。' : '命中附件完整性和金额标准规则,进入补件流程。', '规则');
|
|||
|
|
toast('预审已完成', state.supplemented ? '当前报销单可以提交。' : '发现待补件项,已生成解释和建议。');
|
|||
|
|
renderExpenses();
|
|||
|
|
go('precheck');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function submitSupplement() {
|
|||
|
|
state.supplemented = true;
|
|||
|
|
state.files.push({ name: '上海酒店流水-补件.pdf', type: '酒店流水', size: '524 KB', confidence: '0.98' });
|
|||
|
|
state.taskStatus = 'PRECHECKING';
|
|||
|
|
state.risk = '低风险';
|
|||
|
|
state.agents = state.agents.map(agent => {
|
|||
|
|
if (['RuleCheckAgent', 'ExplainAgent'].includes(agent.name)) return { ...agent, status: 'done' };
|
|||
|
|
return agent;
|
|||
|
|
});
|
|||
|
|
addAudit('用户补件', '补充酒店流水和住宿超标说明,触发重新预审。', '用户');
|
|||
|
|
toast('补件已提交', '规则引擎已重新计算,预审状态更新为通过。');
|
|||
|
|
renderFiles();
|
|||
|
|
renderExpenses();
|
|||
|
|
go('precheck');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function submitFinal() {
|
|||
|
|
const shell = $('#syncShell');
|
|||
|
|
shell.classList.add('active');
|
|||
|
|
$('#submitFinal').disabled = true;
|
|||
|
|
const steps = [
|
|||
|
|
['生成标准报销单', 28],
|
|||
|
|
['调用 mock sync adapter', 62],
|
|||
|
|
['写入同步记录', 88],
|
|||
|
|
['已同步到 expense_system', 100]
|
|||
|
|
];
|
|||
|
|
let index = 0;
|
|||
|
|
const tick = () => {
|
|||
|
|
const step = steps[index];
|
|||
|
|
state.taskStatus = step[1] === 100 ? 'SYNCED' : 'SUBMITTING';
|
|||
|
|
$('#syncLabel').textContent = step[0];
|
|||
|
|
$('#syncValue').textContent = step[1] + '%';
|
|||
|
|
$('#syncBar').style.setProperty('--value', step[1] + '%');
|
|||
|
|
state.agents = state.agents.map(agent => agent.name === 'SyncAgent' ? { ...agent, status: step[1] === 100 ? 'done' : 'run' } : agent);
|
|||
|
|
renderPanel();
|
|||
|
|
index += 1;
|
|||
|
|
if (index < steps.length) {
|
|||
|
|
setTimeout(tick, 520);
|
|||
|
|
} else {
|
|||
|
|
state.submitted = true;
|
|||
|
|
$('#submitFinal').disabled = false;
|
|||
|
|
addAudit('用户确认', '用户确认提交最终报销单。', '用户');
|
|||
|
|
addAudit('后端同步', 'SyncAgent 已将报销单同步到 expense_system。', '系统');
|
|||
|
|
toast('同步完成', '报销单已模拟同步到后端系统。');
|
|||
|
|
renderConfirm();
|
|||
|
|
go('audit');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
tick();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function bindEvents() {
|
|||
|
|
document.addEventListener('click', (event) => {
|
|||
|
|
const goTarget = event.target.closest('[data-go]');
|
|||
|
|
if (goTarget && !goTarget.disabled) go(goTarget.dataset.go);
|
|||
|
|
|
|||
|
|
const quick = event.target.closest('[data-intent]');
|
|||
|
|
if (quick) $('#intentInput').value = quick.dataset.intent;
|
|||
|
|
|
|||
|
|
const remove = event.target.closest('[data-remove-file]');
|
|||
|
|
if (remove) {
|
|||
|
|
state.files.splice(Number(remove.dataset.removeFile), 1);
|
|||
|
|
renderFiles();
|
|||
|
|
addAudit('文件移除', '用户从上传列表移除 1 份材料。', '用户');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
$('#createTask').addEventListener('click', () => {
|
|||
|
|
state.taskStatus = 'CREATED';
|
|||
|
|
addAudit('任务创建', '根据自然语言意图创建新的差旅报销任务。', '用户');
|
|||
|
|
toast('任务已创建', '请继续上传票据材料。');
|
|||
|
|
go('upload');
|
|||
|
|
});
|
|||
|
|
$('#createTaskTop').addEventListener('click', () => $('#createTask').click());
|
|||
|
|
$('#runAgentBtn').addEventListener('click', startRecognition);
|
|||
|
|
$('#runPrecheckBtn').addEventListener('click', runPrecheck);
|
|||
|
|
$('#submitSupplement').addEventListener('click', submitSupplement);
|
|||
|
|
$('#submitFinal').addEventListener('click', submitFinal);
|
|||
|
|
|
|||
|
|
$('#mockAttach').addEventListener('click', () => {
|
|||
|
|
$('#supplementPreview').innerHTML = `
|
|||
|
|
<div class="file-row">
|
|||
|
|
<span class="file-thumb">HTL</span>
|
|||
|
|
<span><strong>上海酒店流水-补件.pdf</strong><span>酒店流水 · 524 KB · OCR 置信度 0.98</span></span>
|
|||
|
|
<span class="tag green">已添加</span>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
toast('附件已添加', '酒店流水已加入补件预览。');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
$('#resetDemo').addEventListener('click', () => {
|
|||
|
|
state.supplemented = false;
|
|||
|
|
state.submitted = false;
|
|||
|
|
state.files = [...baseFiles];
|
|||
|
|
state.taskStatus = 'NEED_SUPPLEMENT';
|
|||
|
|
state.risk = '需补件';
|
|||
|
|
state.agents = [
|
|||
|
|
{ name: 'IntakeAgent', desc: '解析报销意图与上下文', status: 'done' },
|
|||
|
|
{ name: 'ParseAgent', desc: 'OCR 识别并生成草稿', status: 'done' },
|
|||
|
|
{ name: 'RuleCheckAgent', desc: '执行 6 条预审规则', status: 'warn' },
|
|||
|
|
{ name: 'ExplainAgent', desc: '生成解释和补件请求', status: 'warn' },
|
|||
|
|
{ name: 'SyncAgent', desc: '等待用户确认提交', status: 'idle' }
|
|||
|
|
];
|
|||
|
|
renderAll();
|
|||
|
|
toast('演示已重置', '数据已恢复为初始待补件状态。');
|
|||
|
|
go('home');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
$('#fileInput').addEventListener('change', (event) => {
|
|||
|
|
const files = [...event.target.files].map(file => ({
|
|||
|
|
name: file.name,
|
|||
|
|
type: $('#docType').value === '自动识别类型' ? '待识别' : $('#docType').value,
|
|||
|
|
size: Math.max(1, Math.round(file.size / 1024)) + ' KB',
|
|||
|
|
confidence: '待 OCR'
|
|||
|
|
}));
|
|||
|
|
state.files.push(...files);
|
|||
|
|
addAudit('文件上传', '用户上传 ' + files.length + ' 份材料。', '用户');
|
|||
|
|
renderFiles();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const dropzone = $('#dropzone');
|
|||
|
|
['dragenter', 'dragover'].forEach(name => {
|
|||
|
|
dropzone.addEventListener(name, (event) => {
|
|||
|
|
event.preventDefault();
|
|||
|
|
dropzone.classList.add('drag');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
['dragleave', 'drop'].forEach(name => {
|
|||
|
|
dropzone.addEventListener(name, (event) => {
|
|||
|
|
event.preventDefault();
|
|||
|
|
dropzone.classList.remove('drag');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
dropzone.addEventListener('drop', (event) => {
|
|||
|
|
const files = [...event.dataTransfer.files].map(file => ({
|
|||
|
|
name: file.name,
|
|||
|
|
type: $('#docType').value === '自动识别类型' ? '待识别' : $('#docType').value,
|
|||
|
|
size: Math.max(1, Math.round(file.size / 1024)) + ' KB',
|
|||
|
|
confidence: '待 OCR'
|
|||
|
|
}));
|
|||
|
|
if (files.length) {
|
|||
|
|
state.files.push(...files);
|
|||
|
|
addAudit('文件上传', '用户拖拽上传 ' + files.length + ' 份材料。', '用户');
|
|||
|
|
renderFiles();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.addEventListener('input', (event) => {
|
|||
|
|
const input = event.target.closest('[data-amount]');
|
|||
|
|
if (!input) return;
|
|||
|
|
expenses[Number(input.dataset.amount)].amount = Number(input.value || 0);
|
|||
|
|
renderPanel();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderAll() {
|
|||
|
|
renderNav();
|
|||
|
|
renderFiles();
|
|||
|
|
renderExpenses();
|
|||
|
|
renderPrecheck();
|
|||
|
|
renderConfirm();
|
|||
|
|
renderAudit();
|
|||
|
|
renderPanel();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bindEvents();
|
|||
|
|
renderAll();
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|