feat: 完成三角色工作台及金租对公Skills全流程演示

This commit is contained in:
NathanAlex7
2026-09-15 10:32:24 +08:00
parent 5f0b1f9409
commit 47ec090345
58 changed files with 2534 additions and 609 deletions
@@ -0,0 +1,49 @@
"use client";
import {AlertTriangle, CheckCircle2, CircleDashed, Database, MapPin, ShieldAlert} from "lucide-react";
const AI_STATUS:Record<string,{label:string;note:string}>={
pass:{label:"未发现异常",note:"AI检查正常,仍需评审员按本章节确认"},
issue:{label:"发现异常",note:"请结合原始数据作出独立判断"},
missing:{label:"资料不足",note:"缺少数据或口径,不能视为检查通过"},
not_run:{label:"尚未检查",note:"请先发起独立评审"},
};
const CHECK_LABEL:Record<string,string>={pass:"正常",issue:"异常",unknown:"资料不足",error:"执行失败",not_applicable:"不适用"};
function valueText(value:any){
if(value===null||value===undefined||value==="")return "未提供";
if(typeof value==="object")return Object.entries(value).map(([key,item])=>`${key}:${String(item)}`).join(";");
return String(value);
}
export default function ReviewerWorkspace({topics,reviewed,blocking,busy,opinion,setOpinion,onDecision,onComplete,hasRun}:any){
const total=topics.length;
const issues=topics.filter((item:any)=>item.ai_status==="issue").length;
const incomplete=topics.filter((item:any)=>["missing","not_run"].includes(item.ai_status)).length;
const pending=topics.filter((item:any)=>!item.decision).map((item:any)=>item.title);
const canApprove=hasRun&&total>0&&reviewed===total&&!blocking.length;
return <section className="xb-panel xb-wide xb-full-review" id="full-review">
<header className="xb-review-heading"><div><span>评审员独立判断</span><h2>全链路评审</h2><p>每个结论自动绑定本次数据快照、材料定位和检查规则;AI只提示,最终结论由评审员选择。</p></div><div className="xb-review-progress"><strong>{reviewed}/{total}</strong><small>必审领域已确认</small></div></header>
<div className="xb-review-kpis"><article><b>{total}</b><span>必审领域</span></article><article className="danger"><b>{issues}</b><span>AI发现异常</span></article><article className="warning"><b>{incomplete}</b><span>资料不足/未检查</span></article><article><b>{blocking.length}</b><span>尚未闭环</span></article></div>
<label className="ra-field xb-review-opinion"><span>常用意见(异常处理时直接套用,可修改)</span><select value={opinion} onChange={(event)=>setOpinion(event.target.value)}><option>请补充最新材料并说明数据口径</option><option>风险已有缓释措施,整体可控</option><option>AI判断不适用于本项目,以人工核实为准</option><option>请补充现场核实记录</option><option>该事项不适用于本项目</option></select></label>
<div className="xb-review-lanes" aria-hidden="true"><span><Database size={15}/>数据与来源</span><span><ShieldAlert size={15}/>AI辅助判断</span><span><CheckCircle2 size={15}/>评审员结论</span></div>
<div className="xb-topic-list">{topics.map((topic:any)=><ReviewTopic key={topic.id} topic={topic} busy={busy} opinion={opinion} onDecision={onDecision}/>)}</div>
{!hasRun&&<div className="xb-review-hint"><CircleDashed size={18}/><span>请先在页面上方发起一次独立评审,系统才会冻结本次数据与规则版本。</span></div>}
<div className="xb-review-final"><div><CheckCircle2 size={21}/><span><b>完成全链路核查</b><small>{pending.length?`待确认:${pending.join("、")}`:blocking.length?`尚未闭环:${blocking.map((item:any)=>item.title).join("、")}`:"所有必审领域均已形成可追溯结论"}</small></span></div><button className="ra-button" disabled={busy||!hasRun} onClick={()=>onComplete("退回")}>退回补充材料</button><button className="ra-button primary" disabled={busy||!canApprove} onClick={()=>onComplete("批准")}>批准并生成评审报告</button></div>
</section>;
}
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;
return <details className={`xb-review-topic status-${topic.ai_status}`} open={needsAttention||!decision}>
<summary><span className="xb-topic-state">{topic.ai_status==="pass"?<CheckCircle2/>:<AlertTriangle/>}</span><span><b>{topic.title}</b><small>{status.label} · {topic.sources.length} 条数据来源 · {topic.checks.length} 项AI检查</small></span><strong>{decision?`已确认:${decision.decision}`:"待评审员确认"}</strong></summary>
<div className="xb-topic-lanes">
<section><h3>数据与来源</h3>{topic.sources.length?topic.sources.map((source:any)=><details className="xb-source" key={source.id}><summary><span><b>{source.source||"项目数据"}</b><small><MapPin size={12}/>{source.locator||"已定位"}</small></span><em>查看来源</em></summary><p><b>数据项</b>{source.field||"未标记"}</p><p><b>原始值</b>{valueText(source.value)}</p><p><b>证据编号</b>{source.id}</p><p><b>冻结快照</b>{topic.snapshot_id||"尚未形成"}</p></details>):<div className="xb-empty-source"><AlertTriangle size={17}/>没有可验证的数据来源</div>}</section>
<section><h3>AI辅助判断</h3><div className={`xb-ai-state status-${topic.ai_status}`}><b>{status.label}</b><p>{status.note}</p></div>{topic.checks.map((check:any,index:number)=><article className="xb-ai-check" key={check.code||index}><span>{CHECK_LABEL[check.result]||check.result}</span><div><b>{check.title}</b><p>{check.suggested_action||"无需额外处理"}</p><small>依据:{(check.basis_ids||[]).join("、")||"人工核查项"}</small></div></article>)}</section>
<section><h3>评审员结论</h3>{decision?<div className="xb-current-decision"><b>{decision.decision}</b><p>{decision.opinion||"已复核本章节数据和AI结果"}</p><small>{new Date(decision.updated_at).toLocaleString()} · 已绑定 {decision.evidence_ids?.length||0} 条证据</small></div>:<p className="xb-pending-decision">尚未形成人工结论</p>}<div className="xb-decision-actions">{topic.ai_status==="pass"?<button className="ra-button primary" disabled={busy} onClick={()=>onDecision(topic.id,"确认无异常","已复核本章节数据来源和AI检查结果")}>确认本章节</button>:<><button className="ra-button" disabled={busy} onClick={()=>onDecision(topic.id,"确认风险可控",opinion)}>风险可控</button><button className="ra-button" disabled={busy} onClick={()=>onDecision(topic.id,"要求补件",opinion)}>要求补件</button><button className="ra-button" disabled={busy} onClick={()=>onDecision(topic.id,"转现场核查",opinion)}>现场核查</button><button className="ra-button" disabled={busy} onClick={()=>onDecision(topic.id,"不采纳AI判断",opinion)}>不采纳AI</button><button className="ra-button" disabled={busy} onClick={()=>onDecision(topic.id,"不适用",opinion)}>不适用</button></>}</div><small className="xb-trace-note">保存后自动记录:评审员、时间、数据快照、证据编号、Skill/规则版本。</small></section>
</div>
</details>;
}