| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172 |
- import type { DecisionDetailBlock, InspectorPresentation, InspectorSemanticMeta, InspectorVisualRole } from "@/lib/types";
- import { RichText } from "@/components/ui/RichText";
- import { ChevronRight } from "lucide-react";
- export function DecisionDetailRenderer({ blocks }: { blocks: DecisionDetailBlock[] }) {
- return <div className="decisionDetailBlocks">
- {blocks.map((block, index) => {
- if (block.type === "implementation-plan") return <ImplementationPlan key={`${block.title}-${index}`} block={block} />;
- if (block.type === "creative-process") return <CreativeProcess key={`${block.title}-${index}`} block={block} />;
- if (block.type === "evaluation-branches") return <EvaluationBranches key={`${block.title}-${index}`} block={block} />;
- if (block.type === "evaluation-matrix") return <EvaluationMatrix key={`${block.title}-${index}`} block={block} />;
- if (block.type === "comparison") return <section {...panelProps(block, "comparisonPanel", "section", "comparison")} key={`${block.title}-${index}`}><h3>{block.title}</h3><Comparison rows={block.rows} /></section>;
- if (block.type === "items" && block.collapsible) return <CollapsibleItems key={`${block.title}-${index}`} block={block} />;
- if (block.type === "items") return <section {...panelProps(block, "decisionItemsPanel", "section", "list")} key={`${block.title}-${index}`}><h3>{block.title}</h3><Items items={block.items} /></section>;
- if (block.type === "notice") return <section {...panelProps(block, `decisionNotice is-${block.level}`, "notice", "prose")} key={`${block.title || "notice"}-${index}`}><h3>{block.title || "记录说明"}</h3><RichText value={block.value} /></section>;
- if (block.type === "artifact") return <section {...panelProps(block, "", "section", "artifact")} key={`${block.title}-${index}`}><h3>{block.title}</h3><ul>{block.refs.map((ref) => <li key={ref.artifactRef}>{ref.label}</li>)}</ul></section>;
- return <SummaryBlock key={`${block.title}-${index}`} block={block} />;
- })}
- </div>;
- }
- type CreativeProcessBlock = Extract<DecisionDetailBlock, { type: "creative-process" }>;
- function CreativeProcess({ block }: { block: CreativeProcessBlock }) {
- return <section {...panelProps(block, "creativeProcessPanel", "section", "process")}>
- <header className="creativeProcessHeader"><div><h3>{block.title}</h3><p>{block.steps.length} 个真实执行步骤</p></div></header>
- <ol className="creativeProcessSteps">
- {block.steps.map((step) => <li key={`${step.eventRef || "step"}-${step.stepIndex}`}>
- <header><span>步骤 {String(step.stepIndex).padStart(2, "0")}</span><strong>{step.action}</strong></header>
- {step.summary && step.summary !== step.action ? <p className="creativeStepSummary">{step.summary}</p> : null}
- {step.plan ? <div className="creativeStepPlan"><small>执行计划</small><ReadableDecisionText value={step.plan} /></div> : null}
- {step.reasoning ? <details className="creativeStepReasoning"><summary><ChevronRight aria-hidden="true" size={15} />查看本步思考依据</summary><ReadableDecisionText value={step.reasoning} /></details> : null}
- </li>)}
- </ol>
- </section>;
- }
- type EvaluationBranchesBlock = Extract<DecisionDetailBlock, { type: "evaluation-branches" }>;
- function EvaluationBranches({ block }: { block: EvaluationBranchesBlock }) {
- return <section {...panelProps(block, "evaluationBranchesPanel", "section", "grouped-review")}>
- <h3>{block.title}</h3>
- <div className="evaluationBranchList">{block.branches.map((branch) => <article key={branch.subject}>
- <header><h4>{branch.subject}</h4>{branch.conclusion ? <span>{branch.conclusion}</span> : null}</header>
- {branch.achievements?.length ? <EvaluationList title="已达成" items={branch.achievements} tone="success" /> : null}
- {branch.problems?.length ? <EvaluationList title="需关注" items={branch.problems} tone="warning" /> : null}
- {branch.evidence ? <div className="evaluationEvidence"><strong>有据扎实度</strong><ReadableDecisionText value={branch.evidence} /></div> : null}
- </article>)}</div>
- </section>;
- }
- function EvaluationList({ title, items, tone }: { title: string; items: string[]; tone: "success" | "warning" }) {
- return <div className={`evaluationList is-${tone}`}><strong>{title}</strong><ul>{items.map((item, index) => <li key={`${title}-${index}`}><ReadableDecisionText value={item} /></li>)}</ul></div>;
- }
- type EvaluationMatrixBlock = Extract<DecisionDetailBlock, { type: "evaluation-matrix" }>;
- function EvaluationMatrix({ block }: { block: EvaluationMatrixBlock }) {
- const subjects = block.rows[0]?.candidates.map((item) => item.subject) || [];
- return <section {...panelProps(block, "evaluationMatrixPanel", "section", "matrix")}>
- <h3>{block.title}</h3>
- <div className="evaluationMatrixScroll"><table><thead><tr><th scope="col">评审标准</th>{subjects.map((subject) => <th scope="col" key={subject}>{subject}</th>)}<th scope="col">差异依据</th></tr></thead><tbody>{block.rows.map((row) => <tr key={row.criterion}><th scope="row">{row.criterion}</th>{subjects.map((subject) => <td key={subject}>{row.candidates.find((item) => item.subject === subject)?.value || "未记录"}</td>)}<td>{row.basis || "未记录"}</td></tr>)}</tbody></table></div>
- </section>;
- }
- type ImplementationPlanBlock = Extract<DecisionDetailBlock, { type: "implementation-plan" }>;
- function ImplementationPlan({ block }: { block: ImplementationPlanBlock }) {
- return <section {...panelProps(block, "implementationPlanPanel", "section", "plan")}>
- <header className="implementationPlanHeader">
- <div><h3>{block.title}</h3><p>{block.routes.length} 路实现方案</p></div>
- {block.mode ? <span>{block.mode}</span> : null}
- </header>
- <ol className="implementationRoutes">
- {block.routes.map((route) => <li key={route.pathIndex}>
- <header className="implementationRouteHeader">
- <strong>路线 {route.pathIndex}</strong>
- <span className="implementationRouteTags">{route.action ? <b>{route.action}</b> : null}{route.pathType ? <b>{route.pathType}</b> : null}</span>
- </header>
- <div className="implementationRouteBody">
- {route.target ? <PlanField label="作用范围" value={route.target} wide /> : null}
- {route.method ? <PlanField label="实施方法" value={route.method} /> : null}
- {route.emphasis ? <PlanField label="路线侧重" value={route.emphasis} /> : null}
- </div>
- </li>)}
- </ol>
- {block.reasoning ? <div className="implementationReasoning"><h4>为什么这样规划</h4><ReadableDecisionText value={block.reasoning} /></div> : null}
- </section>;
- }
- function PlanField({ label, value, wide = false }: { label: string; value: string; wide?: boolean }) {
- return <div className={`implementationPlanField ${wide ? "is-wide" : ""}`}><small>{label}</small><ReadableDecisionText value={value} /></div>;
- }
- type ItemsBlock = Extract<DecisionDetailBlock, { type: "items" }>;
- function CollapsibleItems({ block }: { block: ItemsBlock }) {
- return <section {...panelProps(block, "decisionDisclosurePanel", "evidence", "list")}>
- <details className="decisionDisclosure" open={block.defaultOpen || undefined}>
- <summary>
- <span className="decisionDisclosureLabel"><ChevronRight aria-hidden="true" size={16} strokeWidth={2.25} /><strong>{block.title}</strong></span>
- <span className="decisionDisclosureCount">共 {block.items.length} 项</span>
- </summary>
- <div className="decisionDisclosureBody"><Items items={block.items} /></div>
- </details>
- </section>;
- }
- type SummaryDetailBlock = Extract<DecisionDetailBlock, { type: "summary" }>;
- function SummaryBlock({ block }: { block: SummaryDetailBlock }) {
- const value = block.presentation === "document" ? normalizeDocumentMarkdown(block.value) : block.value;
- return <section {...panelProps(block, "decisionSummaryPanel", "section", "prose")}>
- <h3>{block.title}</h3>
- <ReadableDecisionText value={value} />
- </section>;
- }
- function normalizeDocumentMarkdown(value: string) {
- return value.replace(/\*\*(["“])([^"”\n]+)(["”])\*\*/g, "$1**$2**$3");
- }
- function panelProps(meta: InspectorSemanticMeta, extraClass: string, fallbackRole: InspectorVisualRole, fallbackPresentation: InspectorPresentation) {
- const role = meta.visualRole || fallbackRole;
- return {
- className: ["inspectorPanel", extraClass, `is-role-${role}`].filter(Boolean).join(" "),
- "data-visual-role": role,
- "data-presentation": meta.presentation || fallbackPresentation,
- };
- }
- function Items({ items }: { items: Array<string | Record<string, unknown>> }) {
- return <dl className="inspectorDetailItems">{items.map((item, index) => {
- if (typeof item === "string") return <div className="inspectorDetailItem" key={`${item}-${index}`}><dt>{String(index + 1).padStart(2, "0")}</dt><dd><ReadableDecisionText value={item} /></dd></div>;
- const severity = item.severity ? String(item.severity) : undefined;
- const label = item.label ? String(item.label)
- : item.subject ? String(item.subject)
- : severity ? `问题 · ${severity}`
- : `项目 ${index + 1}`;
- const value = String(item.value || item.conclusion || item.summary || "");
- const note = item.note ? String(item.note) : undefined;
- const achievements = stringList(item.achievements);
- const problems = stringList(item.problems);
- return <div className="inspectorDetailItem" key={`${label}-${index}`}><dt>{label}</dt><dd>{value ? <ReadableDecisionText value={value} /> : null}{note ? <small>{note}</small> : null}{achievements.length ? <NestedList title="已达成" items={achievements} /> : null}{problems.length ? <NestedList title="需关注" items={problems} /> : null}</dd></div>;
- })}</dl>;
- }
- function ReadableDecisionText({ value }: { value: string }) {
- return <RichText value={insertNumberedLineBreaks(value)} className="decisionReadableText" />;
- }
- function insertNumberedLineBreaks(value: string) {
- const markers = [...value.matchAll(/[ \t\u3000]+(\d+)\.(?=[ \t\u3000]+\S)/g)];
- if (markers.length < 2) return value;
- const numbers = markers.map((match) => Number(match[1]));
- if (numbers[0] !== 1 || numbers.some((number, index) => index > 0 && number !== numbers[index - 1] + 1)) return value;
- let markerIndex = 0;
- return value.replace(/[ \t\u3000]+(\d+\.(?=[ \t\u3000]+\S))/g, (_match, marker: string) => `${markerIndex++ === 0 ? "\n\n" : "\n"}${marker}`);
- }
- function Comparison({ rows }: { rows: Array<Record<string, unknown>> }) {
- const showHandling = rows.some((row) => Boolean(row.handling));
- return <div className={`decisionComparison ${showHandling ? "hasHandling" : ""}`}><div className="comparisonHeader"><b>候选方案</b><b>评审建议</b><b>最终决定</b>{showHandling ? <b>对建议的处理</b> : null}</div>{rows.map((row, index) => <div className="comparisonRow" key={`${row.candidate || index}`}><strong>{String(row.candidate || `方案 ${index + 1}`)}</strong><span>{String(row.recommendation || "未记录")}</span><span>{String(row.finalDecision || "未记录")}</span>{showHandling ? <span>{String(row.handling || "未明确说明")}</span> : null}</div>)}</div>;
- }
- function stringList(value: unknown): string[] {
- return Array.isArray(value) ? value.map((item) => typeof item === "string" ? item : String((item as Record<string, unknown>)?.summary || "")).filter(Boolean) : [];
- }
- function NestedList({ title, items }: { title: string; items: string[] }) {
- return <div className="decisionNestedList"><strong>{title}</strong><ul>{items.map((item, index) => <li key={`${item}-${index}`}><RichText value={item} /></li>)}</ul></div>;
- }
|