| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- "use client";
- import { useMemo, useState } from "react";
- import { AlertTriangle, ArrowDown, Check, ChevronRight, CircleDashed, Database, FileOutput, GitBranch, Link2, ScrollText } from "lucide-react";
- import { RichText } from "@/components/ui/RichText";
- import { ValueView } from "@/components/ui/ValueView";
- import type { CardBusinessData, CardDataCompleteness, CardDataField, CardDataSourceKind, RuntimeUnit } from "@/lib/types";
- interface Props {
- data?: CardBusinessData;
- loading?: boolean;
- error?: string;
- missingRef?: boolean;
- }
- const sourceLabels: Record<CardDataSourceKind, string> = {
- database: "数据库",
- "runtime-event": "运行事件",
- artifact: "产物",
- calculation: "计算生成",
- "log-anchor": "日志锚点",
- };
- const relationLabels: Record<CardBusinessData["relationKind"], string> = {
- "single-event": "单次运行事件",
- "event-sequence": "事件序列",
- "business-record": "业务记录",
- aggregate: "多条记录聚合",
- calculated: "计算生成",
- missing: "运行关系缺失",
- };
- const completenessLabels: Record<CardDataCompleteness, string> = {
- complete: "记录完整",
- partial: "部分记录",
- missing: "记录缺失",
- };
- export function DataFlowRenderer({ data, loading, error, missingRef }: Props) {
- if (loading) return <DataFlowSkeleton />;
- if (error) return <DataFlowState title="数据流暂时不可用" description={error} />;
- if (missingRef) return <DataFlowState title="没有稳定的数据定位" description="这张摘要卡没有独立业务详情,不会推断或拼接它与运行记录的关系。" />;
- if (!data) return <DataFlowState title="还没有数据流记录" description="打开其他卡片,查看其业务输入、运行过程和业务输出。" />;
- const sourceIndex = new Map<string, number>();
- [...data.businessInputs, ...data.businessOutputs].forEach((field) => {
- if (!sourceIndex.has(field.id)) sourceIndex.set(field.id, sourceIndex.size + 1);
- });
- data.runtime.units.forEach((unit) => {
- if (!sourceIndex.has(unit.id)) sourceIndex.set(unit.id, sourceIndex.size + 1);
- });
- return <div className="dataFlow" aria-label="卡片数据流">
- <header className="dataFlowOverview">
- <div className="dataFlowOverviewIcon" aria-hidden="true"><GitBranch size={18} /></div>
- <div>
- <span>{data.cardKind || "业务卡片"}</span>
- <h3>数据关系与完整性</h3>
- </div>
- <div className="dataFlowOverviewBadges">
- <span>{relationLabels[data.relationKind] || data.relationKind}</span>
- <span className={`is-${data.completeness}`}>{completenessLabels[data.completeness]}</span>
- </div>
- </header>
- {data.notices.length ? <div className="dataFlowNotices" aria-label="数据说明">
- {data.notices.map((notice, index) => <p key={`${notice.message}-${index}`} className={`is-${notice.level}`}><AlertTriangle size={14} aria-hidden="true" />{notice.message}</p>)}
- </div> : null}
- <FlowSection stage="input" title="业务输入" description="这一步开始前,可确认获得的信息。">
- <FieldList fields={data.businessInputs} sourceIndex={sourceIndex} empty="没有记录到可确认的业务输入。" />
- </FlowSection>
- <FlowConnector />
- <FlowSection stage="runtime" title="运行过程 / I/O" description={data.runtime.summary || "本次运行没有留下过程概要。"}>
- {data.runtime.units.length ? <div className="runtimeUnitList">
- {data.runtime.units.map((unit, index) => <RuntimeUnitDisclosure key={unit.id || index} unit={unit} index={index + 1} sourceNumber={sourceIndex.get(unit.id)} />)}
- </div> : <EmptyLine>没有保存可下钻的运行单元。</EmptyLine>}
- {data.runtime.calculation ? <div className="dataFlowCalculation"><strong>聚合方式</strong><LongValue value={data.runtime.calculation} /></div> : null}
- </FlowSection>
- <FlowConnector />
- <FlowSection stage="output" title="业务输出" description="运行完成后实际形成或保存的结果。">
- <FieldList fields={data.businessOutputs} sourceIndex={sourceIndex} empty="没有记录到可确认的业务输出。" />
- </FlowSection>
- <FlowConnector muted />
- <DisplayUse data={data} sourceIndex={sourceIndex} />
- </div>;
- }
- function FlowSection({ stage, title, description, children }: { stage: "input" | "runtime" | "output"; title: string; description: string; children: React.ReactNode }) {
- return <section className="dataFlowStage" data-stage={stage} aria-labelledby={`data-flow-${stage}`}>
- <header>
- <span className="dataFlowStageIcon" aria-hidden="true">{stage === "input" ? <Database size={16} /> : stage === "runtime" ? <CircleDashed size={16} /> : <FileOutput size={16} />}</span>
- <div><h3 id={`data-flow-${stage}`}>{title}</h3><p>{description}</p></div>
- </header>
- {children}
- </section>;
- }
- function FlowConnector({ muted = false }: { muted?: boolean }) {
- return <div className={`dataFlowConnector ${muted ? "is-muted" : ""}`} aria-hidden="true"><ArrowDown size={14} /></div>;
- }
- function FieldList({ fields, sourceIndex, empty }: { fields: CardDataField[]; sourceIndex: Map<string, number>; empty: string }) {
- if (!fields.length) return <EmptyLine>{empty}</EmptyLine>;
- return <div className="dataFieldList">
- {fields.map((field, index) => <article className="dataField" key={`${field.id}-${index}`}>
- <header>
- <SourceMarker number={sourceIndex.get(field.id)} />
- <h4>{field.label}</h4>
- <span className={`dataCompleteness is-${field.completeness}`}>{completenessLabels[field.completeness]}</span>
- </header>
- <LongValue value={field.value} />
- <footer>
- <SourceBadge kind={field.source.kind} />
- <span>{field.source.label}</span>
- {field.source.ref ? <code>{field.source.ref}</code> : null}
- {field.source.fieldPath ? <code>{field.source.fieldPath}</code> : null}
- <span className="dataRelation">{fieldRelationLabel(field)}</span>
- </footer>
- </article>)}
- </div>;
- }
- function RuntimeUnitDisclosure({ unit, index, sourceNumber }: { unit: RuntimeUnit; index: number; sourceNumber?: number }) {
- const [open, setOpen] = useState(false);
- const title = unit.label || `运行单元 ${index}`;
- return <details className="runtimeUnit" open={open}>
- <summary aria-expanded={open} onClick={(event) => { event.preventDefault(); setOpen((current) => !current); }}>
- <ChevronRight className="dataDisclosureChevron" size={15} aria-hidden="true" />
- <SourceMarker number={sourceNumber} />
- <strong>{title}</strong>
- {unit.status ? <span className={`runtimeStatus is-${statusClass(unit.status)}`}>{unit.status}</span> : null}
- {typeof unit.durationMs === "number" ? <small>{formatDuration(unit.durationMs)}</small> : null}
- </summary>
- <div className="runtimeUnitBody">
- {unit.summary ? <LongValue value={unit.summary} /> : null}
- {unit.input !== undefined ? <RuntimeValue label="输入" value={unit.input} /> : null}
- {unit.output !== undefined ? <RuntimeValue label="输出" value={unit.output} /> : null}
- {unit.eventRef ? <p className="runtimeEventRef"><span>运行事件</span><code>{unit.eventRef}</code></p> : null}
- {unit.completeness ? <p className="runtimeCompleteness"><Check size={13} aria-hidden="true" />{completenessLabels[unit.completeness]}</p> : null}
- </div>
- </details>;
- }
- function RuntimeValue({ label, value }: { label: string; value: unknown }) {
- return <section className="runtimeValue"><h5>{label}</h5><LongValue value={value} /></section>;
- }
- function DisplayUse({ data, sourceIndex }: { data: CardBusinessData; sourceIndex: Map<string, number> }) {
- const { businessModules, cardFields, unusedSourceIds } = data.displayUse;
- return <section className="displayUse" aria-labelledby="data-flow-display-use">
- <header><span className="dataFlowStageIcon" aria-hidden="true"><Link2 size={16} /></span><div><h3 id="data-flow-display-use">页面如何使用这些数据</h3><p>同一编号把原始字段、业务详情模块与卡片摘要对应起来。</p></div></header>
- <div className="displayUseGroups">
- <MappingGroup title="业务详情" items={businessModules.map((module) => ({ key: module.id, title: module.title, sourceIds: module.sourceIds }))} sourceIndex={sourceIndex} />
- <MappingGroup title="卡片摘要" items={cardFields.map((field) => ({ key: field.key, title: field.label, sourceIds: field.sourceIds, note: field.transform }))} sourceIndex={sourceIndex} />
- </div>
- {unusedSourceIds.length ? <div className="unusedSources"><strong>未用于页面展示</strong><SourceReferences ids={unusedSourceIds} sourceIndex={sourceIndex} /></div> : null}
- </section>;
- }
- function MappingGroup({ title, items, sourceIndex }: { title: string; items: Array<{ key: string; title: string; sourceIds: string[]; note?: string }>; sourceIndex: Map<string, number> }) {
- return <section className="mappingGroup"><h4>{title}</h4>{items.length ? <ul>{items.map((item) => <li key={item.key}><div><strong>{item.title}</strong>{item.note ? <small>{item.note}</small> : null}</div><SourceReferences ids={item.sourceIds} sourceIndex={sourceIndex} /></li>)}</ul> : <EmptyLine>没有配置展示映射。</EmptyLine>}</section>;
- }
- function SourceReferences({ ids, sourceIndex }: { ids: string[]; sourceIndex: Map<string, number> }) {
- if (!ids.length) return <span className="sourceReferenceEmpty">无直接来源</span>;
- return <span className="sourceReferences">{ids.map((id) => <span key={id} title={id}>F{sourceIndex.get(id) || "?"}</span>)}</span>;
- }
- function SourceMarker({ number }: { number?: number }) {
- return <span className="sourceMarker" title={number ? `数据字段 F${number}` : "运行单元"}>{number ? `F${number}` : "I/O"}</span>;
- }
- function SourceBadge({ kind }: { kind: CardDataSourceKind }) {
- const Icon = kind === "database" ? Database : kind === "artifact" ? FileOutput : kind === "log-anchor" ? ScrollText : kind === "runtime-event" ? CircleDashed : GitBranch;
- return <span className={`dataSourceBadge is-${kind}`}><Icon size={12} aria-hidden="true" />{sourceLabels[kind]}</span>;
- }
- function LongValue({ value }: { value: unknown }) {
- const { text, structured } = useMemo(() => displayValue(value), [value]);
- const [open, setOpen] = useState(false);
- if (text.length <= 800) return structured ? <ValueView value={value} /> : <RichText className="dataFlowText" value={text || "(空)"} />;
- const preview = `${text.slice(0, 240).trimEnd()}…`;
- return <details className="longDataValue" open={open}>
- <summary aria-expanded={open} onClick={(event) => { event.preventDefault(); setOpen((current) => !current); }}>
- <span className="longDataSummaryLine"><ChevronRight className="dataDisclosureChevron" size={15} aria-hidden="true" /><span>{open ? "收起完整内容" : "展开完整内容"}</span><small>{text.length.toLocaleString("zh-CN")} 字符</small></span>
- {!open ? <span className="longDataPreview">{preview}</span> : null}
- </summary>
- {open ? structured ? <ValueView value={value} /> : <RichText className="dataFlowText" value={text} /> : null}
- </details>;
- }
- function EmptyLine({ children }: { children: React.ReactNode }) { return <p className="dataFlowEmpty">{children}</p>; }
- function DataFlowState({ title, description }: { title: string; description: string }) {
- return <div className="dataFlowState" role="status"><GitBranch size={20} aria-hidden="true" /><h3>{title}</h3><p>{description}</p></div>;
- }
- function DataFlowSkeleton() {
- return <div className="dataFlowSkeleton" aria-label="正在加载数据流"><div /><span /><span /><div /><span /><span /><div /><span /></div>;
- }
- function displayValue(value: unknown) {
- if (typeof value === "string") return { text: value, structured: false };
- if (value === undefined) return { text: "(未记录)", structured: false };
- if (value === null) return { text: "(空)", structured: false };
- try { return { text: JSON.stringify(value, null, 2), structured: true }; }
- catch { return { text: String(value), structured: false }; }
- }
- function fieldRelationLabel(field: CardDataField) {
- if (field.relation === "available-upstream") return "可确认是上游信息,但无法确认是否被本次决策采用";
- return ({
- "direct-input": "本次运行的直接输入",
- "previous-result": "上一步形成的结果",
- "standing-constraint": "持续生效的约束",
- "explicit-basis": "记录明确采用的依据",
- "persisted-output": "已保存的业务输出",
- } as Record<string, string>)[field.relation] || field.relation;
- }
- function statusClass(value: string) {
- const normalized = value.toLowerCase();
- if (["success", "completed", "hit", "passed"].includes(normalized)) return "success";
- if (["failed", "failure", "error"].includes(normalized)) return "failure";
- if (["running", "processing"].includes(normalized)) return "running";
- return "neutral";
- }
- function formatDuration(value: number) {
- if (value < 1000) return `${value}ms`;
- if (value < 60_000) return `${(value / 1000).toFixed(value < 10_000 ? 1 : 0)}s`;
- return `${Math.floor(value / 60_000)}m ${Math.round((value % 60_000) / 1000)}s`;
- }
|