| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394 |
- "use client";
- import { useEffect, useMemo, useState } from "react";
- import { AlertTriangle, ChevronDown, ChevronRight, Columns3, UnfoldVertical } from "lucide-react";
- import type { InspectorModule, InspectorWorkbenchProjection, RetrievalActivity, SourceBinding } from "@/lib/types";
- import { ComparisonRowCells } from "@/components/inspector/ComparisonRowCells";
- interface Props {
- data?: InspectorWorkbenchProjection;
- loading?: boolean;
- error?: string;
- onNavigate?: (activity: RetrievalActivity) => void;
- onActiveBindingChange?: (bindingId?: string) => void;
- clearActiveSignal?: number;
- }
- export function InspectorComparison({ data, loading, error, onActiveBindingChange, clearActiveSignal }: Props) {
- const [openModules, setOpenModules] = useState<Set<string>>(new Set());
- const [activeBindingId, setActiveBindingId] = useState<string>();
- const markers = useMemo(() => bindingMarkers(data), [data]);
- const moduleSignature = (data?.modules || []).map((module) => `${module.id}:${module.defaultOpen}`).join("|");
- useEffect(() => {
- setOpenModules(new Set((data?.modules || []).filter((module) => module.defaultOpen).map((module) => module.id)));
- setActiveBindingId(undefined);
- onActiveBindingChange?.(undefined);
- }, [data?.detailRef, moduleSignature, onActiveBindingChange]);
- useEffect(() => {
- if (clearActiveSignal === undefined) return;
- setActiveBindingId(undefined);
- }, [clearActiveSignal]);
- if (loading) return <ComparisonSkeleton />;
- if (error) return <ComparisonState title="数据来源读取失败" message={error} />;
- if (!data) return <ComparisonState title="没有可对照的数据" message="当前卡片没有统一来源记录。" />;
- const allOpen = data.modules.length > 0 && data.modules.every((module) => openModules.has(module.id));
- const activate = (bindingId: string, toggle = true) => {
- const next = toggle && activeBindingId === bindingId ? undefined : bindingId;
- setActiveBindingId(next);
- onActiveBindingChange?.(next);
- if (!next) return;
- requestAnimationFrame(() => document.querySelector<HTMLElement>(`.inspectorComparison [data-binding-id="${cssEscape(next)}"]`)?.scrollIntoView({ block: "nearest", behavior: "smooth" }));
- };
- const clearActive = () => { setActiveBindingId(undefined); onActiveBindingChange?.(undefined); };
- return <div className="inspectorComparison" data-completeness={data.completeness} onKeyDown={(event) => { if (event.key === "Escape" && activeBindingId) { event.preventDefault(); event.stopPropagation(); clearActive(); } }}>
- <div className="comparisonToolbar">
- <div><Columns3 aria-hidden="true" size={17} /><span>{data.modules.length} 个业务模块</span><b className={`comparisonCompleteness is-${data.completeness}`}>{completenessLabel(data.completeness)}</b></div>
- <div>{activeBindingId ? <button type="button" className="comparisonTextAction" onClick={clearActive}>清除突出</button> : null}<button type="button" className="comparisonTextAction" onClick={() => setOpenModules(allOpen ? new Set() : new Set(data.modules.map((module) => module.id)))}><UnfoldVertical aria-hidden="true" size={14} />{allOpen ? "折叠全部" : "展开全部"}</button></div>
- </div>
- {data.notices.length ? <div className="comparisonNotices">{data.notices.map((notice, index) => <p key={`${notice.code || "notice"}-${index}`} className={`is-${notice.level || "info"}`}><AlertTriangle aria-hidden="true" size={14} />{notice.message}</p>)}</div> : null}
- <div className="comparisonColumnHeaders" role="row"><span role="columnheader">业务详情中的内容</span><span role="columnheader">这一项的数据依据</span><span role="columnheader">对应的原始字段或原文</span></div>
- <div className="comparisonModules">{data.modules.map((module, index) => {
- const open = openModules.has(module.id);
- return <section key={module.id} className={`comparisonModule ${open ? "is-open" : ""}`} data-module-id={module.id}>
- <button type="button" className="comparisonModuleHeader" aria-expanded={open} onClick={() => setOpenModules((current) => { const next = new Set(current); next.has(module.id) ? next.delete(module.id) : next.add(module.id); return next; })}>
- {open ? <ChevronDown aria-hidden="true" size={16} /> : <ChevronRight aria-hidden="true" size={16} />}
- <span className="moduleMarker">M{index + 1}</span><strong>{module.title}</strong><small>{module.rows?.length || 0} 项业务内容</small>
- </button>
- {open ? <div className="comparisonItemRows">{(module.rows || []).map((row) => <ComparisonRowCells
- key={row.id}
- row={row}
- module={module}
- businessProjection={data.businessProjection}
- sources={data.sources}
- markers={markers}
- activeBindingId={activeBindingId}
- onActivate={activate}
- />)}</div> : null}
- </section>;
- })}</div>
- </div>;
- }
- export function allBindings(module: InspectorModule): SourceBinding[] {
- const seen = new Set<string>();
- return (module.bindings || []).filter((binding) => {
- if (seen.has(binding.id)) return false;
- seen.add(binding.id);
- return true;
- });
- }
- function bindingMarkers(data?: InspectorWorkbenchProjection) {
- const result = new Map<string, string>();
- let index = 1;
- for (const module of data?.modules || []) for (const binding of allBindings(module)) if (!result.has(binding.id)) result.set(binding.id, `S${index++}`);
- return result;
- }
- function completenessLabel(value: InspectorWorkbenchProjection["completeness"]) { return value === "complete" ? "来源完整" : value === "partial" ? "部分来源缺失" : "来源记录缺失"; }
- function cssEscape(value: string) { return typeof CSS !== "undefined" && CSS.escape ? CSS.escape(value) : value.replace(/[^a-zA-Z0-9_-]/g, "\\$&"); }
- function ComparisonState({ title, message }: { title: string; message: string }) { return <div className="comparisonState"><AlertTriangle size={22} /><h3>{title}</h3><p>{message}</p></div>; }
- function ComparisonSkeleton() { return <div className="comparisonSkeleton" aria-label="正在加载业务详情、数据依据和原始记录"><div /><span /><span /><span /><div /><span /><span /><span /></div>; }
|