fix: align rules list and review detail flows
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
* 右栏 · 详情面板
|
||||
* 包含 3 个选项卡(评查结果、抽取字段、文件信息)+ 底部操作栏
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import type { ReviewPoint, CharPosition } from '../ReviewPointsList';
|
||||
import { ReviewPointDetailCard } from './ReviewPointDetailCard';
|
||||
import { FileInfoPanel } from './FileInfoPanel';
|
||||
@@ -35,8 +34,8 @@ interface DetailPanelProps {
|
||||
reviewPoints: ReviewPoint[];
|
||||
fileInfo: FileInfoData;
|
||||
reviewInfo: ReviewInfoData;
|
||||
onReviewPointSelect: (id: string, page?: number, charPositions?: CharPosition[], value?: string) => void;
|
||||
onStatusChange: (id: string, editAuditStatusId: string | number, status: string, message: string) => void;
|
||||
onReviewPointSelect: (id: string | number, page?: number, charPositions?: CharPosition[], value?: string) => void;
|
||||
onStatusChange: (id: string | number, editAuditStatusId: string | number, status: string, message: string) => void;
|
||||
onConfirmResults: () => void;
|
||||
onDownload: () => void;
|
||||
auditStatus?: number;
|
||||
@@ -46,16 +45,36 @@ interface DetailPanelProps {
|
||||
showComparisonButton?: boolean;
|
||||
}
|
||||
|
||||
function ExtractedFieldsPanel({ reviewPoints, onFieldClick }: { reviewPoints: ReviewPoint[]; onFieldClick: (page: number) => void }) {
|
||||
const fields: Array<{ key: string; value: string; page?: number; pointName: string }> = [];
|
||||
type ExtractedFieldValue = {
|
||||
value?: unknown;
|
||||
page?: number | string;
|
||||
};
|
||||
|
||||
function ExtractedFieldsPanel({
|
||||
reviewPoints,
|
||||
onFieldClick,
|
||||
}: {
|
||||
reviewPoints: ReviewPoint[];
|
||||
onFieldClick: (pointId: string | number, page: number) => void;
|
||||
}) {
|
||||
const fields: Array<{ key: string; value: string; page?: number; pointName: string; pointId: string | number }> = [];
|
||||
|
||||
reviewPoints.forEach((p) => {
|
||||
if (p.content) {
|
||||
Object.entries(p.content).forEach(([key, data]) => {
|
||||
const val = (data as any)?.value;
|
||||
const page = (data as any)?.page;
|
||||
const text = typeof val === 'object' ? (val as any)?.text || JSON.stringify(val) : String(val || '');
|
||||
fields.push({ key, value: text, page: page ? Number(page) : undefined, pointName: p.pointName });
|
||||
const fieldData = (data && typeof data === 'object' ? data : {}) as ExtractedFieldValue & { text?: string };
|
||||
const val = fieldData.value;
|
||||
const page = fieldData.page;
|
||||
const text = typeof val === 'object' && val !== null
|
||||
? ('text' in (val as Record<string, unknown>) ? String((val as Record<string, unknown>).text || '') : JSON.stringify(val))
|
||||
: String(val || '');
|
||||
fields.push({
|
||||
key,
|
||||
value: text,
|
||||
page: page ? Number(page) : undefined,
|
||||
pointName: p.pointName,
|
||||
pointId: p.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -74,7 +93,7 @@ function ExtractedFieldsPanel({ reviewPoints, onFieldClick }: { reviewPoints: Re
|
||||
key={`${f.key}-${i}`}
|
||||
type="button"
|
||||
className={`w-full border border-slate-200 rounded-md text-left hover:bg-slate-50 transition p-2.5 ${f.page ? 'cursor-pointer' : 'cursor-default opacity-70'}`}
|
||||
onClick={() => f.page && onFieldClick(f.page)}
|
||||
onClick={() => f.page && onFieldClick(f.pointId, f.page)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[11px] text-slate-500 truncate font-medium">{f.key}</span>
|
||||
@@ -180,11 +199,8 @@ export function DetailPanel({
|
||||
{activeTab === 'fields' && (
|
||||
<ExtractedFieldsPanel
|
||||
reviewPoints={reviewPoints}
|
||||
onFieldClick={(page) => {
|
||||
// 通过 activeReviewPoint 的 id 跳转页面
|
||||
if (activeReviewPoint) {
|
||||
onReviewPointSelect(activeReviewPoint.id, page);
|
||||
}
|
||||
onFieldClick={(pointId, page) => {
|
||||
onReviewPointSelect(pointId, page);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -9,8 +9,8 @@ import type { ReviewPoint, CharPosition } from '../ReviewPointsList';
|
||||
|
||||
interface ReviewPointDetailCardProps {
|
||||
reviewPoint: ReviewPoint;
|
||||
onReviewPointSelect: (id: string, page?: number, charPositions?: CharPosition[], value?: string) => void;
|
||||
onStatusChange: (id: string, editAuditStatusId: string | number, status: string, message: string) => void;
|
||||
onReviewPointSelect: (id: string | number, page?: number, charPositions?: CharPosition[], value?: string) => void;
|
||||
onStatusChange: (id: string | number, editAuditStatusId: string | number, status: string, message: string) => void;
|
||||
fileFormat?: string;
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ function filterOtherRule(reviewPoint: ReviewPoint): MergedRule[] {
|
||||
}
|
||||
|
||||
// ── renderOtherRule ──
|
||||
function RenderOtherRule({ rule, reviewPoint, onReviewPointSelect }: { rule: MergedRule; reviewPoint: ReviewPoint; onReviewPointSelect: (id: string, page?: number, charPos?: CharPosition[], value?: string) => void }) {
|
||||
function RenderOtherRule({ rule, reviewPoint, onReviewPointSelect }: { rule: MergedRule; reviewPoint: ReviewPoint; onReviewPointSelect: (id: string | number, page?: number, charPos?: CharPosition[], value?: string) => void }) {
|
||||
const fieldKey = rule.fieldKey;
|
||||
const fieldValue = rule.fieldValue;
|
||||
const hasFailure = Object.values(fieldValue.type || {}).some(item => item.res === false);
|
||||
@@ -273,7 +273,7 @@ function RenderOtherRule({ rule, reviewPoint, onReviewPointSelect }: { rule: Mer
|
||||
// ── 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<string, unknown>; reviewPoint: ReviewPoint; onReviewPointSelect: (id: string, page?: number, charPos?: CharPosition[], value?: string) => void }) {
|
||||
function RenderConsistencyRule({ rule, reviewPoint, onReviewPointSelect }: { rule: Record<string, unknown>; reviewPoint: ReviewPoint; onReviewPointSelect: (id: string | number, page?: number, charPos?: CharPosition[], value?: string) => void }) {
|
||||
if (reviewPoint.result !== (rule.res as boolean)) return null;
|
||||
const config = rule.config as { logic?: string; pairs?: Array<{ sourceField: Record<string, { page: number; value: string; char_positions?: CharPosition[] }>; targetField: Record<string, { page: number; value: string; char_positions?: CharPosition[] }>; res: boolean; compareMethod?: string }>; selectedFields?: string[] } | undefined;
|
||||
if (!config || !config.pairs || !Array.isArray(config.pairs) || config.pairs.length === 0) return null;
|
||||
@@ -389,7 +389,7 @@ function RenderConsistencyRule({ rule, reviewPoint, onReviewPointSelect }: { rul
|
||||
}
|
||||
|
||||
// ── renderModelRule ──
|
||||
function RenderModelRule({ rule, reviewPoint, onReviewPointSelect, fileFormat }: { rule: Record<string, unknown>; reviewPoint: ReviewPoint; onReviewPointSelect: (id: string, page?: number, charPos?: CharPosition[], value?: string) => void; fileFormat?: string }) {
|
||||
function RenderModelRule({ rule, reviewPoint, onReviewPointSelect, fileFormat }: { rule: Record<string, unknown>; reviewPoint: ReviewPoint; onReviewPointSelect: (id: string | number, page?: number, charPos?: CharPosition[], value?: string) => void; fileFormat?: string }) {
|
||||
const config = rule.config as { model?: string; fields?: Record<string, { page: number | string; value: string; char_positions?: CharPosition[]; res?: boolean }>; ai_suggestion?: { summary?: string; analysis?: { failure_reason?: string; solution_approach?: string; rule_understanding?: string }; suggestions?: Record<string, { reason: string; source: { page: number | null; type: string; field: string | null }; priority: string; confidence: number; suggested_value: string | null }>; generated_at?: string }; message?: string; res?: boolean } | undefined;
|
||||
|
||||
if (config?.res !== reviewPoint.result) return null;
|
||||
@@ -434,6 +434,91 @@ function RenderModelRule({ rule, reviewPoint, onReviewPointSelect, fileFormat }:
|
||||
return <>{fieldElements}</>;
|
||||
}
|
||||
|
||||
function RenderGenericRule({
|
||||
rule,
|
||||
reviewPoint,
|
||||
onReviewPointSelect,
|
||||
}: {
|
||||
rule: Record<string, unknown>;
|
||||
reviewPoint: ReviewPoint;
|
||||
onReviewPointSelect: (id: string | number, page?: number, charPos?: CharPosition[], value?: string) => void;
|
||||
}) {
|
||||
const config = (rule.config && typeof rule.config === 'object' ? rule.config : {}) as Record<string, unknown>;
|
||||
const detail = (config.detail && typeof config.detail === 'object' ? config.detail : {}) as Record<string, unknown>;
|
||||
const fieldNames = Array.isArray(detail.fields)
|
||||
? detail.fields.map((field) => String(field))
|
||||
: Array.isArray((config as any).fields)
|
||||
? (config as any).fields.map((field: unknown) => String(field))
|
||||
: [];
|
||||
const reason = [config.reason, detail.reason, reviewPoint.failMessage, reviewPoint.passMessage]
|
||||
.find((item) => typeof item === 'string' && item.trim()) as string | undefined;
|
||||
const passed = typeof rule.res === 'boolean' ? rule.res : reviewPoint.result === true;
|
||||
const checkType = typeof config.check_type === 'string' ? config.check_type : '';
|
||||
const primitiveType = typeof config.primitive_type === 'string' ? config.primitive_type : '';
|
||||
const badgeText = checkType || primitiveType || '规则检查';
|
||||
|
||||
const jumpToField = (fieldName: string) => {
|
||||
const fieldData = reviewPoint.content?.[fieldName];
|
||||
const page = fieldData?.page || reviewPoint.contentPage?.[fieldName];
|
||||
const normalizedPage = page ? Number(page) : undefined;
|
||||
if (normalizedPage && Number.isFinite(normalizedPage)) {
|
||||
onReviewPointSelect(
|
||||
reviewPoint.id,
|
||||
normalizedPage,
|
||||
fieldData?.char_positions,
|
||||
typeof fieldData?.value === 'string' ? fieldData.value : undefined,
|
||||
);
|
||||
return;
|
||||
}
|
||||
toastService.info(`${fieldName} 当前没有可定位页码`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`mb-3 rounded-md border ${passed ? 'border-emerald-200 bg-emerald-50/60' : 'border-amber-200 bg-amber-50/70'} p-3`}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="text-[11px] font-medium text-slate-600">{badgeText}</div>
|
||||
<span className={`inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10.5px] ${passed ? 'bg-emerald-100 text-emerald-700' : 'bg-amber-100 text-amber-700'}`}>
|
||||
<i className={passed ? 'ri-checkbox-circle-line' : 'ri-error-warning-line'} />
|
||||
{passed ? '通过' : '未通过'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{reason && (
|
||||
<div className="mt-2 text-[12px] leading-5 text-slate-700">
|
||||
{reason}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{fieldNames.length > 0 && (
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{fieldNames.map((fieldName) => {
|
||||
const fieldData = reviewPoint.content?.[fieldName];
|
||||
const fieldValue = fieldData?.value;
|
||||
const displayValue =
|
||||
typeof fieldValue === 'string'
|
||||
? fieldValue
|
||||
: fieldValue == null
|
||||
? '未抽取到值'
|
||||
: JSON.stringify(fieldValue);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={fieldName}
|
||||
type="button"
|
||||
className="min-w-0 flex-1 rounded border border-slate-200 bg-white px-2.5 py-2 text-left hover:border-[#00684a] hover:bg-[#f6fffb]"
|
||||
onClick={() => jumpToField(fieldName)}
|
||||
>
|
||||
<div className="text-[11px] font-medium text-slate-500">{fieldName}</div>
|
||||
<div className="mt-1 text-[12px] leading-5 text-slate-700 break-all">{displayValue}</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main Component ──
|
||||
export function ReviewPointDetailCard({ reviewPoint, onReviewPointSelect, onStatusChange, fileFormat }: ReviewPointDetailCardProps) {
|
||||
const resolveManualNote = () => {
|
||||
@@ -519,7 +604,7 @@ export function ReviewPointDetailCard({ reviewPoint, onReviewPointSelect, onStat
|
||||
if (rule.type === 'ai') {
|
||||
return <div key={`rule-${i}`}>{otherRules.length > 0 && <div className="bg-gray-50 rounded border border-gray-200 text-xs mb-3" />}<RenderModelRule rule={rule} reviewPoint={reviewPoint} onReviewPointSelect={onReviewPointSelect} fileFormat={fileFormat} /></div>;
|
||||
}
|
||||
return null;
|
||||
return <RenderGenericRule key={`rule-${i}`} rule={rule} reviewPoint={reviewPoint} onReviewPointSelect={onReviewPointSelect} />;
|
||||
})}
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user