/** * 右栏 · 评查点详情卡片 * 展示单个评查点的完整详情,复用 renderConsistencyRule/renderOtherRule/renderModelRule 渲染逻辑 */ import { useState, useEffect, useRef, useMemo } from 'react'; import { createPortal } from 'react-dom'; import { toastService } from '~/components/ui/Toast'; import type { ReviewPoint, CharPosition, PdfBboxHighlight } from '../ReviewPointsList'; import { CorporateInfoModal } from '../../corporate-information'; import type { BusinessInfoResult, DishonestyResult } from '../../corporate-information'; import { queryCompanyInfo } from '~/api/corporate-information/qichacha'; interface ReviewPointDetailCardProps { reviewPoint: ReviewPoint; onReviewPointSelect: (id: string | number, page?: number, charPositions?: CharPosition[], value?: string, bboxHighlight?: PdfBboxHighlight) => void; onStatusChange: (id: string, editAuditStatusId: string | number, status: string, message: string) => void; fileFormat?: string; detailMode?: 'legacy' | 'leaudit'; } // ── 比较方法映射 ── const compareMethodMap: Record = { exact: '精确匹配', contains: '包含关系', semantic: '大模型语义匹配', }; const getCompareMethodText = (method?: string): string => { if (!method) return '相等'; const text = compareMethodMap[method] || method; return typeof text === 'string' ? text : String(text); }; const ruleTypeMap: Record = { exists: '有无判断', format: '格式判断', logic: '逻辑判断', regex: '正则表达式', }; const getRuleTypeText = (type?: string): string => { if (!type) return ''; return ruleTypeMap[type] || type; }; function normalizeActionContent(actionContent?: string | string[]): string { if (typeof actionContent === 'string') return actionContent; if (Array.isArray(actionContent)) { return actionContent .map(item => typeof item === 'string' ? item : JSON.stringify(item)) .filter(Boolean) .join('\n'); } return ''; } function getLeauditNote(reviewPoint: ReviewPoint): string { return reviewPoint.editAuditStatusMessage || normalizeActionContent(reviewPoint.actionContent) || reviewPoint.suggestion || ''; } function getLeauditRawFieldValue(value: ReviewPoint['content'][string]): unknown { if (value == null) return undefined; if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value; if (typeof value === 'object' && 'value' in value) return value.value; return value; } function isValidQuad(value: unknown): value is [number, number, number, number] { return Array.isArray(value) && value.length === 4 && value.every(item => typeof item === 'number' && Number.isFinite(item)); } function getLeauditTargetPage(reviewPoint: ReviewPoint, fieldKey: string): number | undefined { const contentPage = reviewPoint.contentPage?.[fieldKey]; const parsedContentPage = Number(contentPage); if (Number.isFinite(parsedContentPage) && parsedContentPage > 0) return parsedContentPage; const pageNum = reviewPoint.fieldPositions?.[fieldKey]?.page_num; if (typeof pageNum === 'number' && Number.isFinite(pageNum) && pageNum >= 0) return pageNum + 1; return undefined; } function getLeauditBboxHighlight(reviewPoint: ReviewPoint, fieldKey: string, page?: number): PdfBboxHighlight | undefined { const fieldPosition = reviewPoint.fieldPositions?.[fieldKey]; if (!fieldPosition || !isValidQuad(fieldPosition.bbox) || !isValidQuad(fieldPosition.page_box)) return undefined; return { fieldKey, bbox: [...fieldPosition.bbox] as [number, number, number, number], pageBox: [...fieldPosition.page_box] as [number, number, number, number], pageNum: fieldPosition.page_num, page, confidence: fieldPosition.confidence, matchMethod: fieldPosition.match_method, }; } // ── Tooltip 系统 ── let activeTooltip = { show: false, content: null as React.ReactNode, position: { top: 0, left: 0 }, ready: false }; function TooltipPortal() { const [tooltip, setTooltip] = useState(activeTooltip); useEffect(() => { const update = () => setTooltip({ ...activeTooltip }); window.addEventListener('tooltip-update', update); return () => window.removeEventListener('tooltip-update', update); }, []); if (!tooltip.show || !tooltip.content) return null; return createPortal(
{tooltip.content}
, document.body, ); } function showTooltip(content: React.ReactNode, position: { top: number; left: number }) { activeTooltip = { show: true, content, position, ready: false }; window.dispatchEvent(new Event('tooltip-update')); requestAnimationFrame(() => { const el = document.querySelector('.fixed.bg-white.shadow-lg.rounded-md') as HTMLElement; if (el) { const r = el.getBoundingClientRect(); let t = position.top, l = position.left; if (l - r.width < 0) l = r.width + 10; if (t - r.height / 2 < 0) t = r.height / 2 + 10; if (t + r.height / 2 > window.innerHeight) t = window.innerHeight - r.height / 2 - 10; activeTooltip.position = { top: t, left: l }; activeTooltip.ready = true; } else { activeTooltip.ready = true; } window.dispatchEvent(new Event('tooltip-update')); }); } function hideTooltip() { activeTooltip.show = false; activeTooltip.ready = false; window.dispatchEvent(new Event('tooltip-update')); } // ── ReactTableTooltip ── function renderReactTable(text: string) { const rows = text.split('\n').map(r => r.split('\t')); return ( {rows.map((row, i) => ( {row.map((cell, j) => )} ))}
{cell}
); } function renderMarkdownTable(text: string) { const lines = text.split('\n').filter(l => l.trim()); const rows: string[][] = []; for (const line of lines) { if (/^\s*\|[-\s:]+\|/.test(line)) continue; const cells = line.split('|').map(c => c.trim()).filter((_, i, a) => i > 0 && i < a.length); if (cells.length > 0) rows.push(cells); } if (rows.length < 1) return {text}; return ( {rows.map((row, i) => ( {row.map((cell, j) => )} ))}
{cell}
); } function renderPipeTable(text: string) { const lines = text.split('\n').filter(l => l.includes('|')); const rows = lines.map(l => l.split('|').map(c => c.trim()).filter(Boolean)); return ( {rows.map((row, i) => ( {row.map((cell, j) => )} ))}
{cell}
); } const ReactTableTooltip = ({ content }: { content: string }) => { const [showTip, setShowTip] = useState(false); const [rendered, setRendered] = useState(null); const ref = useRef(null); const isTabTable = content.includes('\t') && content.includes('\n'); const isMdTable = content.includes('|') && /\|[-\s:]+\|/.test(content); const isPipeTable = !isMdTable && content.includes('|') && content.includes('\n') && content.split('\n').filter(l => l.includes('|')).length >= 2; const isTableLike = isTabTable || isMdTable || isPipeTable; useEffect(() => { const check = () => { if (ref.current) setShowTip(isTableLike || ref.current.scrollHeight > ref.current.clientHeight); }; if (isMdTable) setRendered(renderMarkdownTable(content)); else if (isPipeTable) setRendered(renderPipeTable(content)); else if (isTabTable) setRendered(renderReactTable(content)); else setRendered(content); requestAnimationFrame(check); window.addEventListener('resize', check); return () => window.removeEventListener('resize', check); }, [content, isTableLike]); return (
showTip && showTooltip(
{rendered}
, { top: ref.current?.getBoundingClientRect().top || 0, left: ref.current?.getBoundingClientRect().left || 0 })} onMouseLeave={hideTooltip}>{rendered}
); }; // ── filterOtherRule ── interface MergedRule { fieldKey: string; fieldValue: { type: Record; }; } function filterOtherRule(reviewPoint: ReviewPoint): MergedRule[] { interface RuleFieldValue { page?: number | string; value?: string; char_positions?: CharPosition[]; type: Record } const allRule: Array<{ fieldKey: string; fieldValue: RuleFieldValue }> = []; for (const rule of reviewPoint.evaluatedPointResultsLog?.rules || []) { if ((rule.config as any).res !== reviewPoint.result) continue; if (rule.type === 'exists') { const config = rule.config as { res: boolean; fields: Record; logic?: string }; if (config.res) { Object.entries(config.fields).forEach(([key, fv]) => { if (fv.value && fv.value.trim() !== '') allRule.push({ fieldKey: key, fieldValue: { ...fv, type: { exists: true } } }); }); } else { Object.entries(config.fields).forEach(([key, fv]) => { const empty = !fv.value || fv.value.trim() === ''; allRule.push({ fieldKey: key, fieldValue: { ...fv, type: { exists: !empty } } }); }); } } if (rule.type === 'format') { const config = rule.config as { res: boolean; field: Record; formatType?: string; parameters?: string }; if (config.field) { const entries = Object.entries(config.field); if (entries.length > 0) { const [key, fv] = entries[0]; allRule.push({ fieldKey: key, fieldValue: { ...fv, type: { format: config.res } } }); } } } if (rule.type === 'logic') { const config = rule.config as { logic: string; res: boolean; conditions: Array<{ field: Record; value: string; operator: string; res: boolean }> }; if (config.conditions && Array.isArray(config.conditions)) { config.conditions.forEach(cond => { const entries = Object.entries(cond.field); if (entries.length > 0) { const [key, fv] = entries[0]; allRule.push({ fieldKey: key, fieldValue: { ...fv, type: { logic: cond.res } } }); } }); } } if (rule.type === 'regex') { const config = rule.config as { res: boolean; field: Record; pattern?: string }; if (config.field) { const entries = Object.entries(config.field); if (entries.length > 0) { const [key, fv] = entries[0]; allRule.push({ fieldKey: key, fieldValue: { ...fv, type: { regex: config.res } } }); } } } } const fieldKeyMap: Record = {}; allRule.forEach(item => { const typeKey = Object.keys(item.fieldValue.type)[0]; const typeValue = item.fieldValue.type[typeKey]; if (!fieldKeyMap[item.fieldKey]) fieldKeyMap[item.fieldKey] = { fieldKey: item.fieldKey, fieldValue: { type: {} } }; fieldKeyMap[item.fieldKey].fieldValue.type[typeKey] = { res: typeValue, page: item.fieldValue.page, value: item.fieldValue.value, char_positions: item.fieldValue.char_positions }; }); return Object.values(fieldKeyMap); } // ── renderOtherRule ── function RenderOtherRule({ rule, reviewPoint, onReviewPointSelect }: { rule: MergedRule; reviewPoint: ReviewPoint; onReviewPointSelect: (id: string | number, page?: number, charPos?: CharPosition[], value?: string, bboxHighlight?: PdfBboxHighlight) => void }) { const fieldKey = rule.fieldKey; const fieldValue = rule.fieldValue; const hasFailure = Object.values(fieldValue.type || {}).some(item => item.res === false); const overallResult = !hasFailure; const failedEntry = Object.entries(fieldValue.type || {}).find(([, item]) => item.res === false); const mainEntry = failedEntry || Object.entries(fieldValue.type || {})[0]; if (!mainEntry) return null; const [, mainVal] = mainEntry; const tooltipContent = (
{Object.entries(fieldValue.type || {}).map(([tk, tv]) => (
{getRuleTypeText(tk)}:
{tv.res ? '通过' : '不通过'}
))}
); return ( ); } // ── renderConsistencyRule ── type ChainItem = { field: string; data: { key: string; page: number; value: string; char_positions?: CharPosition[] }; res: boolean; compareMethod?: string }; function RenderConsistencyRule({ rule, reviewPoint, onReviewPointSelect }: { rule: Record; reviewPoint: ReviewPoint; onReviewPointSelect: (id: string | number, page?: number, charPos?: CharPosition[], value?: string, bboxHighlight?: PdfBboxHighlight) => void }) { if (reviewPoint.result !== (rule.res as boolean)) return null; const config = rule.config as { logic?: string; pairs?: Array<{ sourceField: Record; targetField: Record; res: boolean; compareMethod?: string }>; selectedFields?: string[] } | undefined; if (!config || !config.pairs || !Array.isArray(config.pairs) || config.pairs.length === 0) return null; const pairs = config.pairs; const chains = useMemo(() => { type CI = ChainItem; const result: Array> = []; const visited = new Set(); const fieldMap = new Map>(); pairs.forEach(pair => { const sKey = Object.keys(pair.sourceField)[0], tKey = Object.keys(pair.targetField)[0]; if (!fieldMap.has(sKey)) fieldMap.set(sKey, []); fieldMap.get(sKey)?.push({ targetField: tKey, data: { source: { key: sKey, ...pair.sourceField[sKey] }, target: { key: tKey, ...pair.targetField[tKey] } }, res: pair.res, compareMethod: pair.compareMethod }); }); const starts = new Set(); for (const [key] of fieldMap.entries()) { let isT = false; for (const p of pairs) { if (Object.keys(p.targetField)[0] === key) { isT = true; break; } } if (!isT) starts.add(key); } for (const sp of starts) { if (visited.has(sp)) continue; const chain: CI[] = []; let cur = sp; while (fieldMap.has(cur)) { const targets = fieldMap.get(cur); if (!targets || targets.length === 0) break; let next: typeof targets[0] | null = null; for (const t of targets) { if (!visited.has(t.targetField)) { next = t; break; } } if (!next) break; if (chain.length === 0) chain.push({ field: cur, data: next.data.source, res: next.res, compareMethod: next.compareMethod }); chain.push({ field: next.targetField, data: next.data.target, res: next.res, compareMethod: next.compareMethod }); visited.add(cur); visited.add(next.targetField); cur = next.targetField; } if (chain.length > 0) result.push(chain); } // isolated pairs for (const pair of pairs) { const sKey = Object.keys(pair.sourceField)[0], tKey = Object.keys(pair.targetField)[0]; if (!visited.has(sKey) || !visited.has(tKey)) { result.push([ { field: sKey, data: { key: sKey, ...pair.sourceField[sKey] }, res: pair.res }, { field: tKey, data: { key: tKey, ...pair.targetField[tKey] }, res: pair.res }, ]); visited.add(sKey); visited.add(tKey); } } return result; }, [pairs]); return (
{chains.map((chain: ChainItem[], ci: number) => { const res = chain[1]?.res ?? true; const itemCls = res ? 'comparison-item match' : 'comparison-item mismatch'; if (chain.length > 2) { return (
{ e.stopPropagation(); for (const item of chain) { if (item.data.page && typeof onReviewPointSelect === 'function') { onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions); break; } } }}>
{chain.map((item: ChainItem, idx: number) => ( {item.field} {idx < chain.length - 1 && {typeof chain[idx + 1]?.compareMethod === 'object' ? '' : getCompareMethodText(chain[idx + 1]?.compareMethod)}} ))}
{chain.map((item: ChainItem, idx: number) => ( ))}
{res ? : }
); } // Standard pair (2 elements) return (
{ const r = e.currentTarget.getBoundingClientRect(); showTooltip(
{chain.slice(1).map((item: ChainItem, i: number) =>
{typeof item.compareMethod === 'object' ? '' : `${getCompareMethodText(item.compareMethod)}:`}
{res ? '通过' : '不通过'}
)}
, { top: r.top + r.height / 2, left: r.left }); }} onMouseLeave={hideTooltip}> {res ? : }
); })}
); } // ── renderModelRule ── function RenderModelRule({ rule, reviewPoint, onReviewPointSelect, fileFormat }: { rule: Record; reviewPoint: ReviewPoint; onReviewPointSelect: (id: string | number, page?: number, charPos?: CharPosition[], value?: string, bboxHighlight?: PdfBboxHighlight) => void; fileFormat?: string }) { const config = rule.config as { model?: string; fields?: Record; ai_suggestion?: { summary?: string; analysis?: { failure_reason?: string; solution_approach?: string; rule_understanding?: string }; suggestions?: Record; generated_at?: string }; message?: string; res?: boolean } | undefined; if (config?.res !== reviewPoint.result) return null; if (!config) return null; const fieldElements: JSX.Element[] = []; if (config.fields) { Object.entries(config.fields).forEach(([key, value], index) => { const res = value.res !== undefined && value.res !== null ? value.res : value.value.trim() !== ''; fieldElements.push( ); }); } if (config.message) { const msg = typeof config.message === 'object' ? JSON.stringify(config.message) : String(config.message); fieldElements.push(
AI 评查意见

{msg}

); } return <>{fieldElements}; } // ── Main Component ── function stringifyUnknown(value: unknown): string { if (typeof value === 'string') return value; if (typeof value === 'number' || typeof value === 'boolean') return String(value); if (value == null) return ''; try { return JSON.stringify(value, null, 2); } catch { return String(value); } } function getLeauditFieldText(value: ReviewPoint['content'][string]): string { if (value == null) return '未填写'; if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { return String(value); } if (typeof value === 'object' && value && 'value' in value) { return stringifyUnknown(value.value); } return stringifyUnknown(value); } function parseMissingArrayString(rawText: string): string[] { const match = rawText.match(/missing[\w-]*\s*:\s*\[([\s\S]*?)\]/i) || rawText.match(/\[([\s\S]*?)\]/); if (!match) return []; const innerText = match[1].trim(); if (!innerText) return []; const quotedItems = Array.from(innerText.matchAll(/['"]([^'"]+)['"]/g)) .map(item => item[1].trim()) .filter(Boolean); if (quotedItems.length > 0) { return quotedItems; } return innerText .split(',') .map(item => item.trim().replace(/^['"]|['"]$/g, '')) .filter(Boolean); } function getLeauditMissingItems(reviewPoint: ReviewPoint): string[] { const textCandidates = [ reviewPoint.skipReason, typeof reviewPoint.evaluatedPointResultsLog?.skip_reason === 'string' ? reviewPoint.evaluatedPointResultsLog.skip_reason : '', reviewPoint.suggestion, ].filter(Boolean) as string[]; for (const text of textCandidates) { if (!/missing[\w-]*\s*:/i.test(text)) continue; const items = parseMissingArrayString(text); if (items.length > 0) return items; } return []; } function normalizeAiResponseItems(value: unknown, options?: { hideNone?: boolean }): string[] { const hideNone = options?.hideNone === true; if (Array.isArray(value)) { return value .map(item => String(item).trim()) .filter(item => item && (!hideNone || item !== '无')); } if (typeof value === 'string') { const trimmed = value.trim(); if (!trimmed) return []; if (hideNone && trimmed === '无') return []; return [trimmed]; } return []; } function LeauditReviewPointDetailCard({ reviewPoint, onReviewPointSelect, onStatusChange }: { reviewPoint: ReviewPoint; onReviewPointSelect: (id: string | number, page?: number, charPos?: CharPosition[], value?: string, bboxHighlight?: PdfBboxHighlight) => void; onStatusChange: (id: string, editAuditStatusId: string | number, status: string, message: string) => void; }) { const [manualNote, setManualNote] = useState(() => getLeauditNote(reviewPoint)); const [corporateModalVisible, setCorporateModalVisible] = useState(false); const [corporateCompanyName, setCorporateCompanyName] = useState(''); const [corporateLoading, setCorporateLoading] = useState(false); const [corporateBusinessInfo, setCorporateBusinessInfo] = useState(null); const [corporateDishonestyInfo, setCorporateDishonestyInfo] = useState(null); const [corporateError, setCorporateError] = useState(null); const [corporateUpdatedAt, setCorporateUpdatedAt] = useState(null); useEffect(() => { setManualNote(getLeauditNote(reviewPoint)); }, [reviewPoint.id, reviewPoint.editAuditStatusMessage, reviewPoint.actionContent, reviewPoint.suggestion]); const stages = Array.isArray(reviewPoint.evaluatedPointResultsLog?.stages) ? (reviewPoint.evaluatedPointResultsLog.stages as Array>) : []; const missingItems = getLeauditMissingItems(reviewPoint); const legalBasisList = Array.isArray(reviewPoint.legalBasis) ? reviewPoint.legalBasis : reviewPoint.legalBasis?.articles?.map(item => typeof item === 'string' ? item : (item.name || item.content || stringifyUnknown(item))) || []; const riskLabelMap: Record = { high: { cls: 'bg-red-50 text-red-700 border-red-200', label: '高风险' }, medium: { cls: 'bg-amber-50 text-amber-700 border-amber-200', label: '中风险' }, low: { cls: 'bg-emerald-50 text-emerald-700 border-emerald-200', label: '低风险' }, }; const riskMeta = riskLabelMap[reviewPoint.riskLevel || ''] || { cls: 'bg-slate-100 text-slate-600 border-slate-200', label: '未知风险', }; const configConfidence = reviewPoint.evaluationConfig && typeof reviewPoint.evaluationConfig === 'object' ? reviewPoint.evaluationConfig.confidence : undefined; const confidencePct = typeof reviewPoint.confidence === 'number' ? `${Math.round(reviewPoint.confidence * 100)}%` : typeof configConfidence === 'number' ? `${Math.round(configConfidence * 100)}%` : null; const isPass = reviewPoint.status === 'success' && reviewPoint.result === true; const isWarning = reviewPoint.status === 'warning' || (reviewPoint.ruleStatus || '').startsWith('skipped_'); const statusChip = isPass ? { cls: 'bg-emerald-50 text-emerald-700 border-emerald-200', icon: 'ri-checkbox-circle-fill', label: '通过' } : isWarning ? { cls: 'bg-amber-50 text-amber-700 border-amber-200', icon: 'ri-error-warning-fill', label: '提醒' } : { cls: 'bg-red-50 text-red-700 border-red-200', icon: 'ri-close-circle-fill', label: '不通过' }; const summaryText = isPass ? (reviewPoint.passMessage || reviewPoint.suggestion || '校验通过') : isWarning ? (reviewPoint.skipReason || reviewPoint.suggestion || '当前规则未执行或需人工关注') : (reviewPoint.failMessage || reviewPoint.suggestion || '发现问题,请处理'); const partyANameRaw = getLeauditRawFieldValue(reviewPoint.content?.['甲方名称']); const partyBNameRaw = getLeauditRawFieldValue(reviewPoint.content?.['乙方名称']); const partyAName = typeof partyANameRaw === 'string' ? partyANameRaw.trim() : String(partyANameRaw || '').trim(); const partyBName = typeof partyBNameRaw === 'string' ? partyBNameRaw.trim() : String(partyBNameRaw || '').trim(); const shouldShowEnterpriseButtons = reviewPoint.groupName?.trim() === '合同主体'; const handleCorporateInfoClick = async (companyName: string, forceRefresh = false) => { if (!companyName) { toastService.warning('企业名称为空,无法查询'); return; } setCorporateModalVisible(true); setCorporateCompanyName(companyName); setCorporateLoading(true); setCorporateError(null); setCorporateBusinessInfo(null); setCorporateDishonestyInfo(null); setCorporateUpdatedAt(null); try { const response = await queryCompanyInfo({ keyword: companyName, force_refresh: forceRefresh }); if (response.success && response.data) { setCorporateBusinessInfo(response.data.enterprise); setCorporateUpdatedAt(response.data.updated_at); if (response.data.dishonesty) { setCorporateDishonestyInfo({ VerifyResult: response.data.dishonesty.VerifyResult, Data: response.data.dishonesty.Data || [], }); } } else { setCorporateError(response.message || '查询失败'); } } catch (error) { console.error('查询企业信息失败:', error); setCorporateError(error instanceof Error ? error.message : '查询失败'); } finally { setCorporateLoading(false); } }; const handleCorporateForceRefresh = async () => { if (corporateCompanyName) { await handleCorporateInfoClick(corporateCompanyName, true); } }; const handleCloseCorporateModal = () => { setCorporateModalVisible(false); setCorporateCompanyName(''); setCorporateBusinessInfo(null); setCorporateDishonestyInfo(null); setCorporateError(null); setCorporateUpdatedAt(null); }; const renderFieldCard = (fieldKey: string, fieldValue: string) => { const page = getLeauditTargetPage(reviewPoint, fieldKey); const bboxHighlight = getLeauditBboxHighlight(reviewPoint, fieldKey, page); const enterpriseButton = shouldShowEnterpriseButtons && fieldKey === '甲方名称' && partyAName ? renderEnterpriseInfoButton('甲方企业信息', partyAName) : shouldShowEnterpriseButtons && fieldKey === '乙方名称' && partyBName ? renderEnterpriseInfoButton('乙方企业信息', partyBName) : null; return ( ); }; const renderEnterpriseInfoButton = (label: string, companyName: string) => ( ); const renderStageContent = (stage: Record, index: number) => { const detail = (stage.detail || {}) as Record; const checkType = String(stage.check_type || 'unknown'); const passed = stage.passed === true; const hasPassedState = typeof stage.passed === 'boolean'; const stageCardClass = passed ? 'border-emerald-200 bg-emerald-50/60' : 'border-slate-200 bg-slate-50/60'; const stageBadgeClass = passed ? 'text-emerald-700' : 'text-slate-600'; const stageLabelMap: Record = { required: '字段必填', match: '一致性比对', ai: 'AI 评查', contains: '包含校验', compare: '比较校验', }; const stageDisplayName = typeof stage.check_type_chinese === 'string' && stage.check_type_chinese.trim() ? stage.check_type_chinese.trim() : (stageLabelMap[checkType] || checkType); const stageReason = typeof stage.reason === 'string' ? stage.reason.trim() : ''; const getStageDisplayValue = (value: unknown) => { if (value == null || value === '') return '—'; if (typeof value === 'string') return value; if (typeof value === 'number' || typeof value === 'boolean') return String(value); return stringifyUnknown(value); }; const renderStageInfoRow = ( label: string, value: unknown, options?: { valueClassName?: string; mono?: boolean }, ) => (
{label}
{getStageDisplayValue(value)}
); if (checkType === 'required') { const fields = Array.isArray(detail.fields) ? detail.fields.map(item => String(item)) : []; const missing = Array.isArray(detail.missing) ? detail.missing.map(item => String(item)) : []; return (
{stageDisplayName}
{passed ? '通过' : '缺失'}
{fields.length > 0 &&
{`命中字段:${fields.join('、')}`}
} {missing.length > 0 &&
{`缺失字段:${missing.join('、')}`}
}
); } if (checkType === 'match') { const failures = Array.isArray(detail.failures) ? detail.failures.map(item => item as Record) : []; return (
{stageDisplayName}
{passed ? '通过' : '不一致'}
{failures.length > 0 ? (
{failures.map((failure, failureIndex) => { const leftField = String(failure.a || '左侧字段'); const rightField = String(failure.b || '右侧字段'); const leftValue = failure.a_value == null ? '—' : String(failure.a_value); const rightValue = failure.b_value == null ? '—' : String(failure.b_value); return (
{`差异项 ${failureIndex + 1}`}
{leftField}
{failure.a_value == null ? ( {'未填写'} ) : (
{leftValue}
)}
{rightField}
{failure.b_value == null ? ( {'未填写'} ) : (
{rightValue}
)}
); })}
) : (
{'未发现不一致项'}
)}
); } if (checkType === 'compare') { const opMap: Record = { '>=': '≥', '<=': '≤', '!=': '≠', '<>': '≠', '==': '=', '>': '>', '<': '<', '=': '=', }; const displayOp = opMap[String(detail.op)] || String(detail.op); const fmtNum = (v: unknown) => { if (v == null || String(v).trim() === '') return getStageDisplayValue(v); const n = Number(v); return !isNaN(n) ? n.toLocaleString('zh-CN') : getStageDisplayValue(v); }; const buildOperand = (field: unknown, value: unknown) => { const fieldStr = getStageDisplayValue(field); if (value == null || value === '') return fieldStr; return `${fieldStr}(${fmtNum(value)})`; }; const leftOperand = buildOperand(detail.left, detail.left_value); const rightDisplay = typeof detail.right === 'number' ? fmtNum(detail.right) : buildOperand(detail.right, detail.right_value); return (
{stageDisplayName}
{passed ? '通过' : '未通过'}
{leftOperand} {displayOp} {rightDisplay}
{stageReason && (
{stageReason}
)}
); } if (checkType === 'ai') { const response = (detail.response || {}) as Record; const reasonText = typeof response.reason === 'string' ? response.reason.trim() : ''; const strengthItems = normalizeAiResponseItems(response.strengths); const suggestionItems = normalizeAiResponseItems(response.suggestion, { hideNone: true }); const dividerClass = passed ? 'border-emerald-200/70' : 'border-fuchsia-200/70'; return (
{'AI 评查意见'}
{reasonText && (
{reasonText}
)} {strengthItems.length > 0 && (
{'亮点'} {strengthItems.length}
    {strengthItems.map((item, itemIndex) => (
  • {item}
  • ))}
)} {suggestionItems.length > 0 && (
{'修改建议'} {suggestionItems.length}
    {suggestionItems.map((item, itemIndex) => (
  • {'•'} {item}
  • ))}
)}
); } return (
{stageDisplayName}
{hasPassedState && ( {passed ? '通过' : '未通过'} )}
{/* {renderStageInfoRow('阶段类型', stageDisplayName)} */} {/* {hasPassedState && renderStageInfoRow('结果', passed ? '通过' : '未通过', { valueClassName: passed ? 'text-emerald-700' : 'text-red-700' })} */} {stageReason && renderStageInfoRow('原因', stageReason)}
); }; return ( <>
{reviewPoint.pointId && ( {reviewPoint.pointId} )}

{reviewPoint.pointName}

{statusChip.label} {riskMeta.label} {reviewPoint.postAction === 'manual' && ( 需人工 )}
{reviewPoint.score != null && 得分 {reviewPoint.finalScore ?? reviewPoint.machineScore ?? reviewPoint.score}/{reviewPoint.score}} {confidencePct && 置信度 {confidencePct}}
{Object.keys(reviewPoint.content || {}).length > 0 && (
命中字段 {Object.keys(reviewPoint.content).length}
{Object.entries(reviewPoint.content).map(([key, value]) => renderFieldCard(key, getLeauditFieldText(value)))}
)} {missingItems.length > 0 && (
缺失项 {missingItems.length}
{missingItems.map(item => (
{item}
缺失
))}
)} {stages.length > 0 && (
阶段结果 {stages.length}
{stages.map((stage, index) => renderStageContent(stage, index))}
)} {!isWarning && (
{isPass ? '校验结果' : '问题说明'}
{summaryText}
)} {legalBasisList.length > 0 && (
法律依据
{legalBasisList.map((item, index) => ( {item} ))}
)} {reviewPoint.postAction === 'manual' && (
审核意见