DecisionDetailRenderer.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import type { DecisionDetailBlock, InspectorPresentation, InspectorSemanticMeta, InspectorVisualRole } from "@/lib/types";
  2. import { RichText } from "@/components/ui/RichText";
  3. import { ChevronRight } from "lucide-react";
  4. export function DecisionDetailRenderer({ blocks }: { blocks: DecisionDetailBlock[] }) {
  5. return <div className="decisionDetailBlocks">
  6. {blocks.map((block, index) => {
  7. if (block.type === "implementation-plan") return <ImplementationPlan key={`${block.title}-${index}`} block={block} />;
  8. if (block.type === "creative-process") return <CreativeProcess key={`${block.title}-${index}`} block={block} />;
  9. if (block.type === "evaluation-branches") return <EvaluationBranches key={`${block.title}-${index}`} block={block} />;
  10. if (block.type === "evaluation-matrix") return <EvaluationMatrix key={`${block.title}-${index}`} block={block} />;
  11. if (block.type === "comparison") return <section {...panelProps(block, "comparisonPanel", "section", "comparison")} key={`${block.title}-${index}`}><h3>{block.title}</h3><Comparison rows={block.rows} /></section>;
  12. if (block.type === "items" && block.collapsible) return <CollapsibleItems key={`${block.title}-${index}`} block={block} />;
  13. if (block.type === "items") return <section {...panelProps(block, "decisionItemsPanel", "section", "list")} key={`${block.title}-${index}`}><h3>{block.title}</h3><Items items={block.items} /></section>;
  14. 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>;
  15. 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>;
  16. return <SummaryBlock key={`${block.title}-${index}`} block={block} />;
  17. })}
  18. </div>;
  19. }
  20. type CreativeProcessBlock = Extract<DecisionDetailBlock, { type: "creative-process" }>;
  21. function CreativeProcess({ block }: { block: CreativeProcessBlock }) {
  22. return <section {...panelProps(block, "creativeProcessPanel", "section", "process")}>
  23. <header className="creativeProcessHeader"><div><h3>{block.title}</h3><p>{block.steps.length} 个真实执行步骤</p></div></header>
  24. <ol className="creativeProcessSteps">
  25. {block.steps.map((step) => <li key={`${step.eventRef || "step"}-${step.stepIndex}`}>
  26. <header><span>步骤 {String(step.stepIndex).padStart(2, "0")}</span><strong>{step.action}</strong></header>
  27. {step.summary && step.summary !== step.action ? <p className="creativeStepSummary">{step.summary}</p> : null}
  28. {step.plan ? <div className="creativeStepPlan"><small>执行计划</small><ReadableDecisionText value={step.plan} /></div> : null}
  29. {step.reasoning ? <details className="creativeStepReasoning"><summary><ChevronRight aria-hidden="true" size={15} />查看本步思考依据</summary><ReadableDecisionText value={step.reasoning} /></details> : null}
  30. </li>)}
  31. </ol>
  32. </section>;
  33. }
  34. type EvaluationBranchesBlock = Extract<DecisionDetailBlock, { type: "evaluation-branches" }>;
  35. function EvaluationBranches({ block }: { block: EvaluationBranchesBlock }) {
  36. return <section {...panelProps(block, "evaluationBranchesPanel", "section", "grouped-review")}>
  37. <h3>{block.title}</h3>
  38. <div className="evaluationBranchList">{block.branches.map((branch) => <article key={branch.subject}>
  39. <header><h4>{branch.subject}</h4>{branch.conclusion ? <span>{branch.conclusion}</span> : null}</header>
  40. {branch.achievements?.length ? <EvaluationList title="已达成" items={branch.achievements} tone="success" /> : null}
  41. {branch.problems?.length ? <EvaluationList title="需关注" items={branch.problems} tone="warning" /> : null}
  42. {branch.evidence ? <div className="evaluationEvidence"><strong>有据扎实度</strong><ReadableDecisionText value={branch.evidence} /></div> : null}
  43. </article>)}</div>
  44. </section>;
  45. }
  46. function EvaluationList({ title, items, tone }: { title: string; items: string[]; tone: "success" | "warning" }) {
  47. return <div className={`evaluationList is-${tone}`}><strong>{title}</strong><ul>{items.map((item, index) => <li key={`${title}-${index}`}><ReadableDecisionText value={item} /></li>)}</ul></div>;
  48. }
  49. type EvaluationMatrixBlock = Extract<DecisionDetailBlock, { type: "evaluation-matrix" }>;
  50. function EvaluationMatrix({ block }: { block: EvaluationMatrixBlock }) {
  51. const subjects = block.rows[0]?.candidates.map((item) => item.subject) || [];
  52. return <section {...panelProps(block, "evaluationMatrixPanel", "section", "matrix")}>
  53. <h3>{block.title}</h3>
  54. <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>
  55. </section>;
  56. }
  57. type ImplementationPlanBlock = Extract<DecisionDetailBlock, { type: "implementation-plan" }>;
  58. function ImplementationPlan({ block }: { block: ImplementationPlanBlock }) {
  59. return <section {...panelProps(block, "implementationPlanPanel", "section", "plan")}>
  60. <header className="implementationPlanHeader">
  61. <div><h3>{block.title}</h3><p>{block.routes.length} 路实现方案</p></div>
  62. {block.mode ? <span>{block.mode}</span> : null}
  63. </header>
  64. <ol className="implementationRoutes">
  65. {block.routes.map((route) => <li key={route.pathIndex}>
  66. <header className="implementationRouteHeader">
  67. <strong>路线 {route.pathIndex}</strong>
  68. <span className="implementationRouteTags">{route.action ? <b>{route.action}</b> : null}{route.pathType ? <b>{route.pathType}</b> : null}</span>
  69. </header>
  70. <div className="implementationRouteBody">
  71. {route.target ? <PlanField label="作用范围" value={route.target} wide /> : null}
  72. {route.method ? <PlanField label="实施方法" value={route.method} /> : null}
  73. {route.emphasis ? <PlanField label="路线侧重" value={route.emphasis} /> : null}
  74. </div>
  75. </li>)}
  76. </ol>
  77. {block.reasoning ? <div className="implementationReasoning"><h4>为什么这样规划</h4><ReadableDecisionText value={block.reasoning} /></div> : null}
  78. </section>;
  79. }
  80. function PlanField({ label, value, wide = false }: { label: string; value: string; wide?: boolean }) {
  81. return <div className={`implementationPlanField ${wide ? "is-wide" : ""}`}><small>{label}</small><ReadableDecisionText value={value} /></div>;
  82. }
  83. type ItemsBlock = Extract<DecisionDetailBlock, { type: "items" }>;
  84. function CollapsibleItems({ block }: { block: ItemsBlock }) {
  85. return <section {...panelProps(block, "decisionDisclosurePanel", "evidence", "list")}>
  86. <details className="decisionDisclosure" open={block.defaultOpen || undefined}>
  87. <summary>
  88. <span className="decisionDisclosureLabel"><ChevronRight aria-hidden="true" size={16} strokeWidth={2.25} /><strong>{block.title}</strong></span>
  89. <span className="decisionDisclosureCount">共 {block.items.length} 项</span>
  90. </summary>
  91. <div className="decisionDisclosureBody"><Items items={block.items} /></div>
  92. </details>
  93. </section>;
  94. }
  95. type SummaryDetailBlock = Extract<DecisionDetailBlock, { type: "summary" }>;
  96. function SummaryBlock({ block }: { block: SummaryDetailBlock }) {
  97. const value = block.presentation === "document" ? normalizeDocumentMarkdown(block.value) : block.value;
  98. return <section {...panelProps(block, "decisionSummaryPanel", "section", "prose")}>
  99. <h3>{block.title}</h3>
  100. <ReadableDecisionText value={value} />
  101. </section>;
  102. }
  103. function normalizeDocumentMarkdown(value: string) {
  104. return value.replace(/\*\*(["“])([^"”\n]+)(["”])\*\*/g, "$1**$2**$3");
  105. }
  106. function panelProps(meta: InspectorSemanticMeta, extraClass: string, fallbackRole: InspectorVisualRole, fallbackPresentation: InspectorPresentation) {
  107. const role = meta.visualRole || fallbackRole;
  108. return {
  109. className: ["inspectorPanel", extraClass, `is-role-${role}`].filter(Boolean).join(" "),
  110. "data-visual-role": role,
  111. "data-presentation": meta.presentation || fallbackPresentation,
  112. };
  113. }
  114. function Items({ items }: { items: Array<string | Record<string, unknown>> }) {
  115. return <dl className="inspectorDetailItems">{items.map((item, index) => {
  116. 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>;
  117. const severity = item.severity ? String(item.severity) : undefined;
  118. const label = item.label ? String(item.label)
  119. : item.subject ? String(item.subject)
  120. : severity ? `问题 · ${severity}`
  121. : `项目 ${index + 1}`;
  122. const value = String(item.value || item.conclusion || item.summary || "");
  123. const note = item.note ? String(item.note) : undefined;
  124. const achievements = stringList(item.achievements);
  125. const problems = stringList(item.problems);
  126. 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>;
  127. })}</dl>;
  128. }
  129. function ReadableDecisionText({ value }: { value: string }) {
  130. return <RichText value={insertNumberedLineBreaks(value)} className="decisionReadableText" />;
  131. }
  132. function insertNumberedLineBreaks(value: string) {
  133. const markers = [...value.matchAll(/[ \t\u3000]+(\d+)\.(?=[ \t\u3000]+\S)/g)];
  134. if (markers.length < 2) return value;
  135. const numbers = markers.map((match) => Number(match[1]));
  136. if (numbers[0] !== 1 || numbers.some((number, index) => index > 0 && number !== numbers[index - 1] + 1)) return value;
  137. let markerIndex = 0;
  138. return value.replace(/[ \t\u3000]+(\d+\.(?=[ \t\u3000]+\S))/g, (_match, marker: string) => `${markerIndex++ === 0 ? "\n\n" : "\n"}${marker}`);
  139. }
  140. function Comparison({ rows }: { rows: Array<Record<string, unknown>> }) {
  141. const showHandling = rows.some((row) => Boolean(row.handling));
  142. 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>;
  143. }
  144. function stringList(value: unknown): string[] {
  145. return Array.isArray(value) ? value.map((item) => typeof item === "string" ? item : String((item as Record<string, unknown>)?.summary || "")).filter(Boolean) : [];
  146. }
  147. function NestedList({ title, items }: { title: string; items: string[] }) {
  148. return <div className="decisionNestedList"><strong>{title}</strong><ul>{items.map((item, index) => <li key={`${item}-${index}`}><RichText value={item} /></li>)}</ul></div>;
  149. }