update: figure

This commit is contained in:
2026-03-20 10:49:50 +08:00
parent f18b8b1716
commit 3979ba4efd
2 changed files with 901 additions and 4 deletions

View File

@@ -0,0 +1,894 @@
<!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 {
--text: #1f2937;
--muted: #5b6472;
--panel: #ffffff;
--line: #d8e1ef;
--blue: #2563eb;
--blue-soft: #dbeafe;
--green: #16a34a;
--green-soft: #dcfce7;
--amber: #d97706;
--amber-soft: #fef3c7;
--red: #dc2626;
--red-soft: #fee2e2;
--purple: #7c3aed;
--purple-soft: #ede9fe;
--slate: #475569;
--slate-soft: #e2e8f0;
--shadow: 0 14px 30px rgba(15, 23, 42, 0.08);
--diagram-scale: 1;
--diagram-width: 1600px;
--diagram-height: 1060px;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
color: var(--text);
background:
radial-gradient(circle at top left, rgba(219, 234, 254, 0.9), transparent 28%),
linear-gradient(180deg, #eef4ff 0%, #f7faff 38%, #f5f7fb 100%);
}
.page {
max-width: 1920px;
margin: 0 auto;
padding: 18px 24px 30px;
}
.header {
display: grid;
grid-template-columns: 1.55fr 1fr;
gap: 18px;
margin-bottom: 18px;
}
.hero,
.summary,
.board,
.legend-panel {
background: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(148, 163, 184, 0.18);
border-radius: 22px;
box-shadow: var(--shadow);
}
.hero {
padding: 22px 24px;
}
.hero h1 {
margin: 0 0 10px;
font-size: 28px;
line-height: 1.2;
}
.hero p {
margin: 0;
color: var(--muted);
font-size: 14px;
line-height: 1.75;
}
.summary {
padding: 18px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
align-content: start;
}
.metric {
border: 1px solid #e7eef8;
border-radius: 16px;
background: linear-gradient(180deg, #ffffff 0%, #f7faff 100%);
padding: 14px 16px;
}
.metric .num {
font-size: 28px;
font-weight: 800;
margin-bottom: 3px;
}
.metric .label {
color: var(--muted);
font-size: 13px;
line-height: 1.5;
}
.board {
padding: 16px;
}
.board-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 14px;
}
.toolbar-desc {
color: var(--muted);
font-size: 13px;
line-height: 1.6;
}
.toolbar-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.toolbar-actions button {
appearance: none;
border: 1px solid #d8e2f0;
background: #fff;
border-radius: 999px;
padding: 8px 12px;
font-size: 13px;
font-weight: 700;
color: var(--text);
cursor: pointer;
transition: 0.2s ease;
}
.toolbar-actions button:hover {
background: #eff6ff;
border-color: #93c5fd;
color: var(--blue);
}
.toolbar-actions button.active {
background: #eff6ff;
border-color: var(--blue);
color: var(--blue);
}
.scale-indicator {
min-width: 78px;
text-align: right;
color: var(--muted);
font-size: 13px;
font-weight: 700;
}
.board-shell {
min-height: calc(1060px * var(--diagram-scale) + 24px);
border-radius: 24px;
overflow: auto;
border: 1px solid #dbe4f0;
background: linear-gradient(180deg, rgba(251, 253, 255, 0.96), rgba(245, 248, 252, 0.98));
padding: 12px;
}
.board-viewport {
width: 100%;
min-width: fit-content;
display: flex;
justify-content: center;
align-items: flex-start;
}
.board-inner {
position: relative;
width: var(--diagram-width);
height: var(--diagram-height);
transform-origin: top center;
transform: scale(var(--diagram-scale));
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(148, 163, 184, 0.18);
border-radius: 28px;
box-shadow: var(--shadow);
overflow: hidden;
}
.lane {
position: absolute;
left: 22px;
right: 22px;
border-radius: 18px;
border: 1px solid rgba(148, 163, 184, 0.18);
padding: 18px 18px 14px;
}
.lane-title {
position: absolute;
top: 12px;
left: 18px;
padding: 6px 10px;
border-radius: 999px;
font-size: 13px;
font-weight: 800;
color: #243244;
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(148, 163, 184, 0.22);
}
.lane-subtitle {
position: absolute;
top: 14px;
right: 18px;
color: #6b7280;
font-size: 12px;
font-weight: 600;
}
.lane-input { top: 86px; height: 118px; background: linear-gradient(90deg, #eef6ff, #f8fbff); }
.lane-sense { top: 224px; height: 192px; background: linear-gradient(90deg, #f0f9ff, #f8fbff); }
.lane-reason { top: 436px; height: 234px; background: linear-gradient(90deg, #f7f9ff, #fbfcff); }
.lane-review { top: 690px; height: 176px; background: linear-gradient(90deg, #f9fafb, #fcfdff); }
.lane-output { top: 886px; height: 150px; background: linear-gradient(90deg, #f6f3ff, #fbfaff); }
.node {
position: absolute;
width: 248px;
min-height: 120px;
border-radius: 20px;
background: var(--panel);
border: 1px solid #d9e2f0;
box-shadow: 0 10px 22px rgba(15, 23, 42, 0.08);
padding: 14px 16px;
z-index: 2;
cursor: grab;
user-select: none;
}
.node.dragging {
cursor: grabbing;
box-shadow: 0 18px 36px rgba(37, 99, 235, 0.18);
z-index: 5;
}
.node.large {
width: 278px;
min-height: 136px;
}
.node.memory {
width: 300px;
min-height: 146px;
border-style: dashed;
border-width: 2px;
box-shadow: 0 14px 30px rgba(37, 99, 235, 0.10);
}
.node.human {
width: 300px;
min-height: 140px;
border-width: 2px;
box-shadow: 0 14px 28px rgba(217, 119, 6, 0.14);
}
.node-title {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 18px;
font-weight: 800;
margin-bottom: 8px;
}
.icon {
width: 30px;
height: 30px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 800;
color: #fff;
}
.node p {
margin: 0 0 8px;
color: var(--muted);
font-size: 13.5px;
line-height: 1.62;
}
.chip-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.chip {
white-space: nowrap;
border-radius: 999px;
padding: 6px 8px;
border: 1px solid transparent;
font-size: 11px;
font-weight: 700;
line-height: 1;
}
.blue { border-color: #bfdbfe; background: var(--blue-soft); }
.green { border-color: #bbf7d0; background: var(--green-soft); }
.amber { border-color: #fde68a; background: var(--amber-soft); }
.red { border-color: #fecaca; background: var(--red-soft); }
.purple { border-color: #ddd6fe; background: var(--purple-soft); }
.slate { border-color: #cbd5e1; background: var(--slate-soft); }
svg.connector-layer {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 1;
pointer-events: none;
}
.legend {
margin-top: 18px;
display: grid;
grid-template-columns: 1.25fr 1fr;
gap: 18px;
}
.legend-panel {
padding: 18px 20px;
}
.legend-panel h3 {
margin: 0 0 14px;
font-size: 18px;
}
.legend-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 14px;
background: #f8fbff;
border: 1px solid #e5ecf6;
color: var(--muted);
font-size: 13px;
font-weight: 600;
}
.line-sample {
width: 32px;
height: 0;
border-top: 3px solid;
}
.line-main { border-color: var(--blue); }
.line-feedback { border-color: var(--green); border-top-style: dashed; }
.line-data { border-color: var(--purple); border-top-style: dotted; }
.notes {
margin: 0;
padding-left: 18px;
color: var(--muted);
font-size: 14px;
line-height: 1.75;
}
@media (max-width: 1400px) {
.page {
padding: 14px;
}
.header,
.legend {
grid-template-columns: 1fr;
}
.summary {
grid-template-columns: repeat(4, 1fr);
}
}
@media (max-width: 980px) {
.summary {
grid-template-columns: 1fr 1fr;
}
.toolbar-actions {
width: 100%;
}
.scale-indicator {
text-align: left;
}
}
</style>
</head>
<body>
<div class="page">
<div class="header">
<section class="hero">
<h1>电诈受害人资金核查多智能体协同流程图</h1>
<p>
本图聚焦“输入感知、证据解析、关联推理、人工复核、笔录与证据输出”全流程,
突出三类核心关系:主处理链路、关键词回溯形成的自我完善闭环,以及民警人工复核的
最终把关作用。页面已优化为可自适应缩放,适合浏览、截图和比赛汇报展示。
</p>
</section>
<section class="summary">
<div class="metric">
<div class="num">9</div>
<div class="label">核心智能体/功能代理</div>
</div>
<div class="metric">
<div class="num">1</div>
<div class="label">案件级记忆中心</div>
</div>
<div class="metric">
<div class="num">3</div>
<div class="label">关键闭环:主链路/反馈链/人机链</div>
</div>
<div class="metric">
<div class="num">22</div>
<div class="label">主要数据流与关系连线</div>
</div>
</section>
</div>
<section class="board">
<div class="board-toolbar">
<div class="toolbar-desc">
页面问题已修复:支持自动适配窗口、手动缩放查看、图面主体优先展示。若用于截图,建议先切到
<strong>100%</strong><strong>115%</strong>
</div>
<div class="toolbar-actions">
<button type="button" data-scale-mode="fit" class="active">适配窗口</button>
<button type="button" data-scale="0.9">90%</button>
<button type="button" data-scale="1">100%</button>
<button type="button" data-scale="1.15">115%</button>
<button type="button" data-scale="1.3">130%</button>
<span class="scale-indicator" id="scaleIndicator">缩放 100%</span>
</div>
</div>
<div class="board-shell" id="boardShell">
<div class="board-viewport">
<div class="board-inner">
<div class="lane lane-input">
<div class="lane-title">输入层</div>
<div class="lane-subtitle">案件信息、截图材料、受害人陈述进入系统</div>
</div>
<div class="lane lane-sense">
<div class="lane-title">感知层</div>
<div class="lane-subtitle">页面识别、字段提取、页型修正</div>
</div>
<div class="lane lane-reason">
<div class="lane-title">推理层</div>
<div class="lane-subtitle">归并去重、中转识别、资金路径恢复、金额认定</div>
</div>
<div class="lane lane-review">
<div class="lane-title">复核决策层</div>
<div class="lane-subtitle">人工审核、补问提示、认定确认</div>
</div>
<div class="lane lane-output">
<div class="lane-title">输出层</div>
<div class="lane-subtitle">笔录内容、证据索引、图表与报告输出</div>
</div>
<article id="node-input" class="node large" style="left: 70px; top: 110px;">
<div class="node-title">
<span class="icon" style="background: var(--slate);"></span>
多源案件输入
</div>
<p>受害人陈述、微信/支付宝/银行APP截图、交易凭证、补充说明。</p>
<div class="chip-row">
<span class="chip slate">案件基础信息</span>
<span class="chip slate">多APP截图</span>
<span class="chip slate">受害人描述</span>
</div>
</article>
<article id="node-case" class="node" style="left: 385px; top: 108px;">
<div class="node-title">
<span class="icon" style="background: var(--blue);">1</span>
案件受理智能体
</div>
<p>建立案件上下文,统一组织截图、人员、时间和案件编号等基础要素。</p>
<div class="chip-row">
<span class="chip blue">案件上下文</span>
<span class="chip blue">任务编排起点</span>
</div>
</article>
<article id="node-parse" class="node large" style="left: 690px; top: 240px;">
<div class="node-title">
<span class="icon" style="background: var(--blue);">2</span>
截图解析智能体
</div>
<p>完成截图页型识别、字段抽取和候选交易生成,输出初始结构化结果。</p>
<div class="chip-row">
<span class="chip blue">页型判断</span>
<span class="chip blue">字段抽取</span>
<span class="chip blue">候选交易</span>
</div>
</article>
<article id="node-feedback" class="node" style="left: 1030px; top: 245px;">
<div class="node-title">
<span class="icon" style="background: var(--green);">3</span>
关键词回溯优化智能体
</div>
<p>利用后续高置信证据回溯“支付成功、账单详情、订单号、退款、充值”等关键词,反向修正页型判断。</p>
<div class="chip-row">
<span class="chip green">自我完善闭环</span>
<span class="chip green">OCR校正</span>
</div>
</article>
<article id="node-match" class="node large" style="left: 250px; top: 460px;">
<div class="node-title">
<span class="icon" style="background: var(--purple);">4</span>
跨平台关联智能体
</div>
<p>基于订单号、金额、时间窗口、账户尾号、对手方相似性完成归并去重。</p>
<div class="chip-row">
<span class="chip purple">统一交易视图</span>
<span class="chip purple">去重结果</span>
</div>
</article>
<article id="node-flow" class="node large" style="left: 585px; top: 460px;">
<div class="node-title">
<span class="icon" style="background: var(--amber);">5</span>
资金路径分析智能体
</div>
<p>识别本人账户中转、恢复流向关系、生成交易时间轴与资金流图基础数据。</p>
<div class="chip-row">
<span class="chip amber">中转识别</span>
<span class="chip amber">路径恢复</span>
<span class="chip amber">流图数据</span>
</div>
</article>
<article id="node-assess" class="node large" style="left: 920px; top: 460px;">
<div class="node-title">
<span class="icon" style="background: var(--red);">6</span>
金额认定智能体
</div>
<p>对交易进行高/中/低置信分层认定,生成认定理由、排除说明和待补问点位。</p>
<div class="chip-row">
<span class="chip red">高/中/低置信</span>
<span class="chip red">认定理由</span>
<span class="chip red">排除说明</span>
</div>
</article>
<article id="node-inquiry" class="node" style="left: 1270px; top: 500px;">
<div class="node-title">
<span class="icon" style="background: var(--purple);">7</span>
问询辅助智能体
</div>
<p>围绕中置信和待复核记录生成补问建议,辅助完善证据链和笔录提问重点。</p>
<div class="chip-row">
<span class="chip purple">追问建议</span>
<span class="chip purple">缺口提示</span>
</div>
</article>
<article id="node-review" class="node human" style="left: 690px; top: 715px;">
<div class="node-title">
<span class="icon" style="background: var(--amber);"></span>
民警人工复核
</div>
<p>对系统给出的认定结论进行确认、排除、补充核实,形成最终可办案结论。</p>
<div class="chip-row">
<span class="chip amber">确认</span>
<span class="chip amber">排除</span>
<span class="chip amber">补充核实</span>
</div>
</article>
<article id="node-record" class="node large" style="left: 440px; top: 910px;">
<div class="node-title">
<span class="icon" style="background: var(--green);">8</span>
笔录辅助智能体
</div>
<p>将已确认交易自动整理为可直接插入笔录的内容,包括时间、金额、渠道、对象和需补充说明点。</p>
<div class="chip-row">
<span class="chip green">笔录片段</span>
<span class="chip green">内容直插</span>
<span class="chip green">减轻负担</span>
</div>
</article>
<article id="node-report" class="node large" style="left: 935px; top: 910px;">
<div class="node-title">
<span class="icon" style="background: var(--purple);">9</span>
证据输出智能体
</div>
<p>输出交易明细、资金流图、时间轴、证据索引、Excel/PDF报告和审计快照。</p>
<div class="chip-row">
<span class="chip purple">证据清单</span>
<span class="chip purple">报告导出</span>
<span class="chip purple">快照留痕</span>
</div>
</article>
<article id="node-memory" class="node memory" style="left: 90px; top: 698px;">
<div class="node-title">
<span class="icon" style="background: var(--blue);"></span>
案件记忆中心
</div>
<p>统一沉淀案件信息、截图、候选交易、去重结果、认定结果、复核状态、笔录片段和报告快照。</p>
<div class="chip-row">
<span class="chip blue">共享上下文</span>
<span class="chip blue">持续更新</span>
<span class="chip blue">结果回写</span>
</div>
</article>
<svg class="connector-layer" viewBox="0 0 1600 1060" preserveAspectRatio="none" aria-hidden="true">
<defs>
<marker id="arrowBlue" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#2563eb"></path>
</marker>
<marker id="arrowGreen" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"></path>
</marker>
<marker id="arrowPurple" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#7c3aed"></path>
</marker>
<marker id="arrowAmber" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#d97706"></path>
</marker>
</defs>
<g id="connectorPaths"></g>
<g id="connectorLabels"></g>
</svg>
</div>
</div>
</div>
</section>
<section class="legend">
<div class="legend-panel">
<h3>图例与阅读方式</h3>
<div class="legend-grid">
<div class="legend-item"><span class="line-sample line-main"></span>主处理流程</div>
<div class="legend-item"><span class="line-sample line-feedback"></span>反馈优化闭环</div>
<div class="legend-item"><span class="line-sample line-data"></span>共享数据/记忆回写</div>
</div>
<ul class="notes" style="margin-top: 16px;">
<li>蓝色实线展示“截图输入到认定输出”的核心办案流程。</li>
<li>绿色虚线闭环重点体现“证据识别后关键词回溯”的自我完善能力。</li>
<li>橙色人工复核节点体现“机器先判、民警把关”的警务人机协同机制。</li>
<li>输出层同时突出“笔录直接插入”和“证据化导出”两个实战落点。</li>
</ul>
</div>
<div class="legend-panel">
<h3>本次修复点</h3>
<ul class="notes">
<li>修正统计卡片数量错误,核心智能体数量由 8 改为 9。</li>
<li>新增“适配窗口/手动缩放”查看控制,解决普通笔记本屏幕下图面过小问题。</li>
<li>优化主图容器,突出图面主体,减少页面留白对可读性的影响。</li>
<li>提升节点文字、说明文字和连线标签对比度,更适合截图和投屏展示。</li>
</ul>
</div>
</section>
</div>
<script>
(function () {
const root = document.documentElement;
const shell = document.getElementById("boardShell");
const indicator = document.getElementById("scaleIndicator");
const buttons = Array.from(document.querySelectorAll("[data-scale], [data-scale-mode]"));
const baseWidth = 1600;
let mode = "fit";
let manualScale = 1;
const boardInner = document.querySelector(".board-inner");
const connectorPaths = document.getElementById("connectorPaths");
const connectorLabels = document.getElementById("connectorLabels");
const draggableNodes = Array.from(document.querySelectorAll(".node"));
const connectors = [
{ from: "node-input", to: "node-case", start: "right", end: "left", color: "#2563eb", marker: "url(#arrowBlue)", width: 3.5, label: "案件信息/截图材料", lx: 0, ly: -14 },
{ from: "node-case", to: "node-parse", start: "right", end: "top", color: "#2563eb", marker: "url(#arrowBlue)", width: 3.5, label: "案件上下文", lx: 10, ly: -12 },
{ from: "node-parse", to: "node-match", start: "bottom", end: "top", color: "#2563eb", marker: "url(#arrowBlue)", width: 3.5, label: "候选交易/页型结果", lx: -48, ly: -12, bend: 110 },
{ from: "node-match", to: "node-flow", start: "right", end: "left", color: "#2563eb", marker: "url(#arrowBlue)", width: 3.5, label: "统一交易视图", lx: 0, ly: -12 },
{ from: "node-flow", to: "node-assess", start: "right", end: "left", color: "#2563eb", marker: "url(#arrowBlue)", width: 3.5, label: "路径结果/中转标记", lx: 0, ly: -12 },
{ from: "node-assess", to: "node-review", start: "bottom", end: "top", color: "#2563eb", marker: "url(#arrowBlue)", width: 3.5, label: "认定结果", lx: 0, ly: -10, bend: 96 },
{ from: "node-assess", to: "node-inquiry", start: "right", end: "left", color: "#7c3aed", marker: "url(#arrowPurple)", width: 3, dash: "7 6", label: "待补问点位", lx: 6, ly: -12 },
{ from: "node-inquiry", to: "node-review", start: "bottom", end: "right", color: "#7c3aed", marker: "url(#arrowPurple)", width: 3, dash: "7 6", label: "问询建议", lx: 38, ly: -12, bend: 120 },
{ from: "node-assess", to: "node-feedback", start: "top", end: "bottom", color: "#16a34a", marker: "url(#arrowGreen)", width: 3.5, dash: "8 7", label: "高置信字段/后验信号", lx: 46, ly: -14, bend: 90 },
{ from: "node-feedback", to: "node-parse", start: "left", end: "right", color: "#16a34a", marker: "url(#arrowGreen)", width: 3.5, dash: "8 7", label: "关键词回溯修正", lx: 10, ly: -12 },
{ from: "node-review", to: "node-record", start: "bottom", end: "top", color: "#d97706", marker: "url(#arrowAmber)", width: 3.5, label: "已确认结论", lx: -22, ly: -12, bend: 78 },
{ from: "node-review", to: "node-report", start: "bottom", end: "top", color: "#7c3aed", marker: "url(#arrowPurple)", width: 3.5, label: "证据索引/导出数据", lx: 28, ly: -12, bend: 78 },
{ from: "node-case", to: "node-memory", start: "left", end: "top", color: "#7c3aed", marker: "url(#arrowPurple)", width: 2.6, dash: "3 7", label: "结果回写/共享上下文", lx: -86, ly: 18, bend: 120 },
{ from: "node-parse", to: "node-memory", start: "left", end: "top", color: "#7c3aed", marker: "url(#arrowPurple)", width: 2.6, dash: "3 7", bend: 170 },
{ from: "node-match", to: "node-memory", start: "left", end: "right", color: "#7c3aed", marker: "url(#arrowPurple)", width: 2.6, dash: "3 7", bend: 70 },
{ from: "node-flow", to: "node-memory", start: "left", end: "right", color: "#7c3aed", marker: "url(#arrowPurple)", width: 2.6, dash: "3 7", bend: 130 },
{ from: "node-review", to: "node-memory", start: "left", end: "right", color: "#7c3aed", marker: "url(#arrowPurple)", width: 2.6, dash: "3 7", bend: 86 },
{ from: "node-record", to: "node-memory", start: "left", end: "bottom", color: "#7c3aed", marker: "url(#arrowPurple)", width: 2.6, dash: "3 7", bend: 120 },
{ from: "node-report", to: "node-memory", start: "left", end: "bottom", color: "#7c3aed", marker: "url(#arrowPurple)", width: 2.6, dash: "3 7", bend: 220 }
];
function setActiveButton(target) {
buttons.forEach((btn) => btn.classList.remove("active"));
if (target) target.classList.add("active");
}
function computeFitScale() {
const available = shell.clientWidth - 24;
const fit = available / baseWidth;
return Math.max(0.68, Math.min(1, fit));
}
function applyScale(value) {
root.style.setProperty("--diagram-scale", String(value));
indicator.textContent = `缩放 ${Math.round(value * 100)}%`;
}
function updateScale() {
applyScale(mode === "fit" ? computeFitScale() : manualScale);
}
function getNodeRect(id) {
const node = document.getElementById(id);
return {
el: node,
left: parseFloat(node.style.left || "0"),
top: parseFloat(node.style.top || "0"),
width: node.offsetWidth,
height: node.offsetHeight
};
}
function getAnchorPoint(rect, anchor) {
switch (anchor) {
case "left":
return { x: rect.left, y: rect.top + rect.height / 2, dx: -1, dy: 0 };
case "right":
return { x: rect.left + rect.width, y: rect.top + rect.height / 2, dx: 1, dy: 0 };
case "top":
return { x: rect.left + rect.width / 2, y: rect.top, dx: 0, dy: -1 };
case "bottom":
default:
return { x: rect.left + rect.width / 2, y: rect.top + rect.height, dx: 0, dy: 1 };
}
}
function cubicPoint(p0, p1, p2, p3, t) {
const mt = 1 - t;
return {
x: mt ** 3 * p0.x + 3 * mt ** 2 * t * p1.x + 3 * mt * t ** 2 * p2.x + t ** 3 * p3.x,
y: mt ** 3 * p0.y + 3 * mt ** 2 * t * p1.y + 3 * mt * t ** 2 * p2.y + t ** 3 * p3.y
};
}
function renderConnectors() {
connectorPaths.innerHTML = "";
connectorLabels.innerHTML = "";
connectors.forEach((conn) => {
const fromRect = getNodeRect(conn.from);
const toRect = getNodeRect(conn.to);
const start = getAnchorPoint(fromRect, conn.start);
const end = getAnchorPoint(toRect, conn.end);
const bend = conn.bend || 84;
const cp1 = {
x: start.x + start.dx * bend,
y: start.y + start.dy * bend
};
const cp2 = {
x: end.x + end.dx * bend,
y: end.y + end.dy * bend
};
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", `M ${start.x} ${start.y} C ${cp1.x} ${cp1.y}, ${cp2.x} ${cp2.y}, ${end.x} ${end.y}`);
path.setAttribute("fill", "none");
path.setAttribute("stroke", conn.color);
path.setAttribute("stroke-width", String(conn.width));
if (conn.dash) path.setAttribute("stroke-dasharray", conn.dash);
path.setAttribute("marker-end", conn.marker);
connectorPaths.appendChild(path);
if (conn.label) {
const mid = cubicPoint(start, cp1, cp2, end, 0.5);
const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
label.setAttribute("x", String(mid.x + (conn.lx || 0)));
label.setAttribute("y", String(mid.y + (conn.ly || 0)));
label.setAttribute("font-size", "12.5");
label.setAttribute("fill", conn.color);
label.setAttribute("font-weight", "800");
label.textContent = conn.label;
connectorLabels.appendChild(label);
}
});
}
buttons.forEach((button) => {
button.addEventListener("click", () => {
if (button.dataset.scaleMode === "fit") {
mode = "fit";
setActiveButton(button);
updateScale();
return;
}
mode = "manual";
manualScale = Number(button.dataset.scale || "1");
setActiveButton(button);
updateScale();
});
});
window.addEventListener("resize", () => {
if (mode === "fit") updateScale();
renderConnectors();
});
draggableNodes.forEach((node) => {
let dragging = false;
let startX = 0;
let startY = 0;
let originLeft = 0;
let originTop = 0;
node.addEventListener("mousedown", (event) => {
if (event.button !== 0) return;
dragging = true;
startX = event.clientX;
startY = event.clientY;
originLeft = parseFloat(node.style.left || "0");
originTop = parseFloat(node.style.top || "0");
node.classList.add("dragging");
event.preventDefault();
});
window.addEventListener("mousemove", (event) => {
if (!dragging) return;
const currentScale = parseFloat(
getComputedStyle(document.documentElement).getPropertyValue("--diagram-scale")
) || 1;
const deltaX = (event.clientX - startX) / currentScale;
const deltaY = (event.clientY - startY) / currentScale;
const maxLeft = boardInner.clientWidth - node.offsetWidth - 12;
const maxTop = boardInner.clientHeight - node.offsetHeight - 12;
const nextLeft = Math.min(Math.max(12, originLeft + deltaX), maxLeft);
const nextTop = Math.min(Math.max(12, originTop + deltaY), maxTop);
node.style.left = `${nextLeft}px`;
node.style.top = `${nextTop}px`;
renderConnectors();
});
window.addEventListener("mouseup", () => {
if (!dragging) return;
dragging = false;
node.classList.remove("dragging");
});
});
updateScale();
renderConnectors();
})();
</script>
</body>
</html>

View File

@@ -59,10 +59,12 @@ const Transactions: React.FC = () => {
const [detail, setDetail] = useState<TransactionRecord | null>(null);
const [editableDetail, setEditableDetail] = useState<TransactionRecord | null>(null);
const [markOverrides, setMarkOverrides] = useState<Record<string, 'duplicate' | 'transit' | 'valid'>>({});
const [autoRefreshUntil, setAutoRefreshUntil] = useState<number>(0);
const { data: txData } = useQuery({
queryKey: ['transactions', id],
queryFn: () => fetchTransactions(id),
refetchInterval: () => (Date.now() < autoRefreshUntil ? 2000 : false),
});
const saveTxMutation = useMutation({
mutationFn: (params: {
@@ -84,7 +86,7 @@ const Transactions: React.FC = () => {
message.open({
key: 'transactions-analysis',
type: 'loading',
content: '正在执行资金分析...',
content: '正在提交案件分析任务...',
duration: 0,
});
},
@@ -92,8 +94,9 @@ const Transactions: React.FC = () => {
message.open({
key: 'transactions-analysis',
type: 'success',
content: res.message || '资金分析任务已提交',
content: res.message || '案件分析任务已提交',
});
setAutoRefreshUntil(Date.now() + 60_000);
qc.invalidateQueries({ queryKey: ['transactions', id] });
qc.invalidateQueries({ queryKey: ['assessments', id] });
qc.invalidateQueries({ queryKey: ['flows', id] });
@@ -104,7 +107,7 @@ const Transactions: React.FC = () => {
message.open({
key: 'transactions-analysis',
type: 'error',
content: '资金分析执行失败',
content: '案件分析提交失败',
});
},
});
@@ -402,7 +405,7 @@ const Transactions: React.FC = () => {
loading={analysisMutation.isPending}
onClick={() => analysisMutation.mutate()}
>
{analysisMutation.isPending ? '分析中...' : '执行资金分析'}
{analysisMutation.isPending ? '分析中...' : '执行案件分析'}
</Button>
</Space>
}