From 5f49f94ca41fc970071be0e58b1b5edcaddaea21 Mon Sep 17 00:00:00 2001 From: NathanAlex7 <18898599977@163.com> Date: Tue, 15 Sep 2026 11:09:06 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=94=AF=E6=8C=81=E9=80=90=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E5=A1=AB=E5=86=99=E5=B9=B6=E4=BF=9D=E5=AD=98=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=E6=84=8F=E8=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/test_reviewer_workspace.cjs | 15 +++++++++++++++ scripts/verify.py | 1 + src/app/data-access/workspace.css | 1 + src/app/xingbang-workbench/page.tsx | 2 +- src/components/xingbang/ReviewerWorkspace.tsx | 9 +++++++-- 5 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 scripts/test_reviewer_workspace.cjs diff --git a/scripts/test_reviewer_workspace.cjs b/scripts/test_reviewer_workspace.cjs new file mode 100644 index 0000000..8171ccf --- /dev/null +++ b/scripts/test_reviewer_workspace.cjs @@ -0,0 +1,15 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); + +const root = path.resolve(__dirname, ".."); +const component = fs.readFileSync(path.join(root, "src/components/xingbang/ReviewerWorkspace.tsx"), "utf8"); +const page = fs.readFileSync(path.join(root, "src/app/xingbang-workbench/page.tsx"), "utf8"); + +assert.match(component, /aria-label=\{`\$\{topic\.title\}评审意见`\}/, "每个评审节点应提供独立意见输入框"); +assert.match(component, /onChange=\{\(event\)=>setDraft\(event\.target\.value\)\}/, "输入框应允许修改节点意见"); +assert.match(component, /draft\.trim\(\)\|\|"已复核本章节数据来源和AI检查结果"/, "正常节点应保存人工输入或明确的默认复核说明"); +assert.match(component, /disabled=\{busy\|\|requiredOpinion\}/, "需要说明依据的评审动作不应允许空意见提交"); +assert.match(page, /setReviewOpinion\]=useState\(""\)/, "常用模板默认不应冒充人工意见"); + +console.log("Reviewer workspace input behavior checks passed"); diff --git a/scripts/verify.py b/scripts/verify.py index 0da4b9f..18b76e2 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -208,6 +208,7 @@ def main() -> int: checks = [ ("repository-diff", [git, "diff", "--check"], ROOT), ("backend-unit", [project_python(), "-m", "unittest", "discover", "-s", ".", "-p", "test_*.py", "-v"], BACKEND), + ("reviewer-workspace-ui", [executable("node"), "scripts/test_reviewer_workspace.cjs"], ROOT), ("frontend-build", [npm, "run", "build"], ROOT), ("compose-local-config", [docker, "compose", "-f", "docker-compose.yml", "config", "--quiet"], ROOT), ("compose-aliyun-config", [docker, "compose", "--env-file", "deploy/aliyun.env.example", "-f", "deploy/docker-compose.aliyun.yml", "config", "--quiet"], ROOT), diff --git a/src/app/data-access/workspace.css b/src/app/data-access/workspace.css index 35957f5..321851a 100644 --- a/src/app/data-access/workspace.css +++ b/src/app/data-access/workspace.css @@ -9,3 +9,4 @@ .xb-full-review{padding:0;overflow:hidden}.xb-review-heading{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:24px 26px;background:linear-gradient(135deg,#153d2d,#1c6044);color:#fff}.xb-review-heading>div>span{font-size:10px;letter-spacing:1px;color:#88d6b0}.xb-review-heading h2{margin:8px 0 5px;font-size:22px}.xb-review-heading p{margin:0;max-width:780px;color:#c3dbcf;font-size:11px}.xb-review-progress{text-align:center;padding:11px 18px;border:1px solid #ffffff2b;border-radius:9px;background:#ffffff0d}.xb-review-progress strong{display:block;font-size:22px}.xb-review-progress small{display:block;margin-top:3px;color:#b9d8c8;font-size:9px}.xb-review-kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:1px;background:#e3e9e5;border-bottom:1px solid #e3e9e5}.xb-review-kpis article{display:flex;align-items:baseline;gap:8px;padding:15px 22px;background:#fff}.xb-review-kpis b{font-size:20px;color:#25694a}.xb-review-kpis span{font-size:10px;color:#728078}.xb-review-kpis .danger b{color:#a6473c}.xb-review-kpis .warning b{color:#a36b24}.xb-full-review>.xb-review-opinion{margin:20px 24px;max-width:580px}.xb-review-lanes{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;margin:0 24px;background:#dfe7e2;border:1px solid #dfe7e2;border-radius:8px 8px 0 0;overflow:hidden}.xb-review-lanes span{display:flex;align-items:center;gap:8px;padding:11px 16px;background:#edf4ef;color:#3c6750;font-size:11px;font-weight:700}.xb-topic-list{padding:0 24px}.xb-review-topic{margin:0;border:1px solid #e0e7e2;border-top:0;background:#fff}.xb-review-topic:last-child{border-radius:0 0 8px 8px}.xb-review-topic>summary{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:12px;padding:15px 17px;list-style:none;background:#f8faf9;color:#364d3f}.xb-review-topic>summary::-webkit-details-marker{display:none}.xb-review-topic.status-issue>summary{background:#fff1ee}.xb-review-topic.status-missing>summary{background:#fff7e9}.xb-review-topic.status-not_run>summary{background:#f4f5f5}.xb-topic-state svg{width:19px;color:#319164}.status-issue .xb-topic-state svg{color:#b44e41}.status-missing .xb-topic-state svg,.status-not_run .xb-topic-state svg{color:#b67b2e}.xb-review-topic>summary span>b,.xb-review-topic>summary span>small{display:block}.xb-review-topic>summary span>small{margin-top:4px;color:#7b8b81;font-size:10px}.xb-review-topic>summary>strong{font-size:10px;color:#4a6a57}.xb-topic-lanes{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1px;background:#e2e8e4;border-top:1px solid #e2e8e4}.xb-topic-lanes>section{min-width:0;padding:16px;background:#fff}.xb-topic-lanes h3{margin:0 0 12px;font-size:12px}.xb-source{margin:0 0 8px!important;border:1px solid #e5eae7;border-radius:7px}.xb-source>summary{display:flex;justify-content:space-between;gap:10px;padding:10px;list-style:none}.xb-source>summary span>b,.xb-source>summary span>small{display:block}.xb-source>summary span>small{display:flex;align-items:center;gap:3px;margin-top:4px;color:#78867e;font-size:9px}.xb-source>summary em{font-style:normal;font-size:9px;color:#27714d}.xb-source>p{display:grid;grid-template-columns:60px 1fr;gap:7px;margin:0!important;padding:4px 10px!important;font-size:10px!important;word-break:break-all}.xb-source>p:last-child{padding-bottom:10px!important}.xb-source>p>b{color:#849087}.xb-empty-source{display:flex;align-items:center;gap:7px;padding:13px;background:#fff6e9;color:#976323;font-size:10px}.xb-ai-state{padding:12px;border-radius:7px;background:#edf7f1}.xb-ai-state.status-issue{background:#fdebe8;color:#98453a}.xb-ai-state.status-missing,.xb-ai-state.status-not_run{background:#fff3df;color:#96601b}.xb-ai-state p{margin:4px 0 0!important;font-size:10px!important}.xb-ai-check{display:flex;gap:8px;padding:11px 0;border-bottom:1px solid #edf1ee}.xb-ai-check>span{height:21px;padding:4px 6px;border-radius:999px;background:#edf3ef;color:#397256;font-size:9px;white-space:nowrap}.xb-ai-check b{font-size:11px}.xb-ai-check p,.xb-ai-check small{display:block;margin:3px 0!important;color:#718078;font-size:9px!important}.xb-current-decision{padding:12px;border-radius:7px;background:#eef1fb;color:#40568e}.xb-current-decision p{margin:4px 0!important}.xb-current-decision small{font-size:9px;color:#7280a2}.xb-pending-decision{padding:12px!important;background:#f4f5f5;color:#7c8580}.xb-decision-actions{display:flex;flex-wrap:wrap;gap:7px;margin-top:12px}.xb-decision-actions .ra-button{padding:7px 9px;font-size:10px}.xb-trace-note{display:block;margin-top:11px;color:#859189;font-size:9px;line-height:1.6}.xb-review-hint{display:flex;align-items:center;gap:9px;margin:18px 24px;padding:13px;background:#fff5e5;color:#93601d;font-size:11px}.xb-full-review>.xb-review-final{margin:20px 24px 24px}@media(max-width:900px){.xb-review-lanes{display:none}.xb-topic-lanes{grid-template-columns:1fr}.xb-review-topic>summary{grid-template-columns:auto 1fr}.xb-review-topic>summary>strong{grid-column:2}.xb-review-kpis{grid-template-columns:1fr 1fr}}@media(max-width:600px){.xb-review-heading{align-items:flex-start;flex-direction:column}.xb-review-kpis{grid-template-columns:1fr}.xb-topic-list,.xb-full-review>.xb-review-final{margin-left:0;margin-right:0}.xb-topic-list{padding:0 12px}} .xb-pool-card{cursor:default}.xb-pool-card:hover{border-color:#dfe7e2;box-shadow:none}.xb-pool-card.just-submitted{border-color:#55a778;box-shadow:0 0 0 3px #55a7781f}.xb-pool-card.just-submitted>span{background:#dcf3e5;color:#17633f}.xb-pool-actions{display:flex;gap:8px;margin-top:14px}.xb-pool-actions .ra-button{display:inline-flex;align-items:center;gap:6px;text-decoration:none}.xb-pool-card footer b{display:flex;align-items:center;gap:5px;font-size:11px} .ra-heading-actions{display:flex;gap:10px;align-items:center}.ra-folder-input{display:none}@media(max-width:760px){.ra-heading-actions{width:100%;flex-wrap:wrap}} +.xb-decision-editor{display:block;margin-top:10px}.xb-decision-editor>span{display:block;margin-bottom:6px;color:#51675a;font-size:10px;font-weight:700}.xb-decision-editor textarea{box-sizing:border-box;width:100%;min-height:82px;padding:10px 11px;resize:vertical;border:1px solid #d9e2dc;border-radius:7px;background:#fff;color:#273c30;font:inherit;line-height:1.6;outline:none}.xb-decision-editor textarea:focus{border-color:#3b8d64;box-shadow:0 0 0 3px #3b8d6418}.xb-decision-editor>small{display:block;margin-top:4px;text-align:right;color:#8a958e;font-size:9px} diff --git a/src/app/xingbang-workbench/page.tsx b/src/app/xingbang-workbench/page.tsx index 9ae4f61..befcdf0 100644 --- a/src/app/xingbang-workbench/page.tsx +++ b/src/app/xingbang-workbench/page.tsx @@ -19,7 +19,7 @@ export default function XingbangWorkspace() { const [field, setField] = useState(fields[0][0]), [value, setValue] = useState(""), [reason, setReason] = useState(""); const [template, setTemplate] = useState("template.ai-pre-review"), [reportId, setReportId] = useState(""), [sectionEdits, setSectionEdits] = useState>({}); const [category, setCategory] = useState("rule_omission"), [feedbackNote, setFeedbackNote] = useState(""), [feedbackId, setFeedbackId] = useState(""); - const [reviewOpinion,setReviewOpinion]=useState("请补充最新材料并说明数据口径"); + const [reviewOpinion,setReviewOpinion]=useState(""); const [experience, setExperience] = useState({title: "", trigger: "", action: "", exceptions: "", scope: "project", team: "", industry: "", required_field: "collection.evidence", exception_field: "collection.evidence", expires_at: "2027-09-11", condition_field: "collection.dependency", condition_value: "true"}); const selected = useRef(""); const headers = (json = true) => {const token = sessionStorage.getItem("xingbang-access-token"), role=sessionStorage.getItem("xingbang-demo-role"); return {...(json ? {"Content-Type": "application/json"} : {}), ...(token ? {Authorization: `Bearer ${token}`} : {}), ...(role ? {"X-Xingbang-Demo-Role":role} : {})};}; diff --git a/src/components/xingbang/ReviewerWorkspace.tsx b/src/components/xingbang/ReviewerWorkspace.tsx index bdde9ad..4e02fca 100644 --- a/src/components/xingbang/ReviewerWorkspace.tsx +++ b/src/components/xingbang/ReviewerWorkspace.tsx @@ -1,5 +1,6 @@ "use client"; +import {useEffect, useState} from "react"; import {AlertTriangle, CheckCircle2, CircleDashed, Database, MapPin, ShieldAlert} from "lucide-react"; const AI_STATUS:Record={ @@ -26,7 +27,7 @@ export default function ReviewerWorkspace({topics,reviewed,blocking,busy,opinion return
评审员独立判断

全链路评审

每个结论自动绑定本次数据快照、材料定位和检查规则;AI只提示,最终结论由评审员选择。

{reviewed}/{total}必审领域已确认
{total}必审领域
{issues}AI发现异常
{incomplete}资料不足/未检查
{blocking.length}尚未闭环
- +
{topics.map((topic:any)=>)}
{!hasRun&&
请先在页面上方发起一次独立评审,系统才会冻结本次数据与规则版本。
} @@ -38,12 +39,16 @@ function ReviewTopic({topic,busy,opinion,onDecision}:any){ const status=AI_STATUS[topic.ai_status]||AI_STATUS.not_run; const needsAttention=topic.ai_status!=="pass"; const decision=topic.decision; + const [draft,setDraft]=useState(decision?.opinion||""); + useEffect(()=>{setDraft(decision?.opinion||"");},[decision?.id,decision?.opinion]); + useEffect(()=>{if(!decision&&opinion)setDraft(opinion);},[opinion,decision]); + const requiredOpinion=!draft.trim(); return
{topic.ai_status==="pass"?:}{topic.title}{status.label} · {topic.sources.length} 条数据来源 · {topic.checks.length} 项AI检查{decision?`已确认:${decision.decision}`:"待评审员确认"}

数据与来源

{topic.sources.length?topic.sources.map((source:any)=>
{source.source||"项目数据"}{source.locator||"已定位"}查看来源

数据项{source.field||"未标记"}

原始值{valueText(source.value)}

证据编号{source.id}

冻结快照{topic.snapshot_id||"尚未形成"}

):
没有可验证的数据来源
}

AI辅助判断

{status.label}

{status.note}

{topic.checks.map((check:any,index:number)=>
{CHECK_LABEL[check.result]||check.result}
{check.title}

{check.suggested_action||"无需额外处理"}

依据:{(check.basis_ids||[]).join("、")||"人工核查项"}
)}
-

评审员结论

{decision?
{decision.decision}

{decision.opinion||"已复核本章节数据和AI结果"}

{new Date(decision.updated_at).toLocaleString()} · 已绑定 {decision.evidence_ids?.length||0} 条证据
:

尚未形成人工结论

}
{topic.ai_status==="pass"?:<>}
保存后自动记录:评审员、时间、数据快照、证据编号、Skill/规则版本。
+

评审员结论

{decision&&
{decision.decision}

{decision.opinion||"已复核本章节数据和AI结果"}

{new Date(decision.updated_at).toLocaleString()} · 已绑定 {decision.evidence_ids?.length||0} 条证据
}