fix: align rules list and review detail flows
This commit is contained in:
@@ -310,10 +310,14 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid
|
|||||||
if (mainType) params.set('mainType', mainType);
|
if (mainType) params.set('mainType', mainType);
|
||||||
} else if (isContractModule) {
|
} else if (isContractModule) {
|
||||||
params.set('documentType', '合同');
|
params.set('documentType', '合同');
|
||||||
params.set('mainType', '合同');
|
if (mainType) {
|
||||||
|
params.set('mainType', mainType);
|
||||||
|
}
|
||||||
} else if (effectiveSelectedModuleName.includes('公文')) {
|
} else if (effectiveSelectedModuleName.includes('公文')) {
|
||||||
params.set('documentType', '内部公文');
|
params.set('documentType', '内部公文');
|
||||||
params.set('mainType', '内部公文');
|
if (mainType) {
|
||||||
|
params.set('mainType', mainType);
|
||||||
|
}
|
||||||
} else if (effectiveSelectedModuleName) {
|
} else if (effectiveSelectedModuleName) {
|
||||||
params.set('documentType', effectiveSelectedModuleName);
|
params.set('documentType', effectiveSelectedModuleName);
|
||||||
params.set('mainType', effectiveSelectedModuleName);
|
params.set('mainType', effectiveSelectedModuleName);
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export interface CharPosition {
|
|||||||
* 用于展示单个评查结果
|
* 用于展示单个评查结果
|
||||||
*/
|
*/
|
||||||
export interface ReviewPoint {
|
export interface ReviewPoint {
|
||||||
id: string;
|
id: string | number;
|
||||||
documentId?: string;
|
documentId?: string;
|
||||||
pointId?: string;
|
pointId?: string;
|
||||||
editAuditStatusId?: string | number;
|
editAuditStatusId?: string | number;
|
||||||
@@ -2888,4 +2888,4 @@ export function ReviewPointsList({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ interface Statistics {
|
|||||||
interface RulesDirectoryProps {
|
interface RulesDirectoryProps {
|
||||||
reviewPoints: ReviewPoint[];
|
reviewPoints: ReviewPoint[];
|
||||||
statistics: Statistics;
|
statistics: Statistics;
|
||||||
activeReviewPointResultId: string | null;
|
activeReviewPointResultId: string | number | null;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
onRuleSelect: (id: string) => void;
|
onRuleSelect: (id: string | number) => void;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
* 右栏 · 详情面板
|
* 右栏 · 详情面板
|
||||||
* 包含 3 个选项卡(评查结果、抽取字段、文件信息)+ 底部操作栏
|
* 包含 3 个选项卡(评查结果、抽取字段、文件信息)+ 底部操作栏
|
||||||
*/
|
*/
|
||||||
import { useState } from 'react';
|
|
||||||
import type { ReviewPoint, CharPosition } from '../ReviewPointsList';
|
import type { ReviewPoint, CharPosition } from '../ReviewPointsList';
|
||||||
import { ReviewPointDetailCard } from './ReviewPointDetailCard';
|
import { ReviewPointDetailCard } from './ReviewPointDetailCard';
|
||||||
import { FileInfoPanel } from './FileInfoPanel';
|
import { FileInfoPanel } from './FileInfoPanel';
|
||||||
@@ -35,8 +34,8 @@ interface DetailPanelProps {
|
|||||||
reviewPoints: ReviewPoint[];
|
reviewPoints: ReviewPoint[];
|
||||||
fileInfo: FileInfoData;
|
fileInfo: FileInfoData;
|
||||||
reviewInfo: ReviewInfoData;
|
reviewInfo: ReviewInfoData;
|
||||||
onReviewPointSelect: (id: string, page?: number, charPositions?: CharPosition[], value?: string) => void;
|
onReviewPointSelect: (id: string | number, page?: number, charPositions?: CharPosition[], value?: string) => void;
|
||||||
onStatusChange: (id: string, editAuditStatusId: string | number, status: string, message: string) => void;
|
onStatusChange: (id: string | number, editAuditStatusId: string | number, status: string, message: string) => void;
|
||||||
onConfirmResults: () => void;
|
onConfirmResults: () => void;
|
||||||
onDownload: () => void;
|
onDownload: () => void;
|
||||||
auditStatus?: number;
|
auditStatus?: number;
|
||||||
@@ -46,16 +45,36 @@ interface DetailPanelProps {
|
|||||||
showComparisonButton?: boolean;
|
showComparisonButton?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExtractedFieldsPanel({ reviewPoints, onFieldClick }: { reviewPoints: ReviewPoint[]; onFieldClick: (page: number) => void }) {
|
type ExtractedFieldValue = {
|
||||||
const fields: Array<{ key: string; value: string; page?: number; pointName: string }> = [];
|
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) => {
|
reviewPoints.forEach((p) => {
|
||||||
if (p.content) {
|
if (p.content) {
|
||||||
Object.entries(p.content).forEach(([key, data]) => {
|
Object.entries(p.content).forEach(([key, data]) => {
|
||||||
const val = (data as any)?.value;
|
const fieldData = (data && typeof data === 'object' ? data : {}) as ExtractedFieldValue & { text?: string };
|
||||||
const page = (data as any)?.page;
|
const val = fieldData.value;
|
||||||
const text = typeof val === 'object' ? (val as any)?.text || JSON.stringify(val) : String(val || '');
|
const page = fieldData.page;
|
||||||
fields.push({ key, value: text, page: page ? Number(page) : undefined, pointName: p.pointName });
|
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}`}
|
key={`${f.key}-${i}`}
|
||||||
type="button"
|
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'}`}
|
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">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="text-[11px] text-slate-500 truncate font-medium">{f.key}</span>
|
<span className="text-[11px] text-slate-500 truncate font-medium">{f.key}</span>
|
||||||
@@ -180,11 +199,8 @@ export function DetailPanel({
|
|||||||
{activeTab === 'fields' && (
|
{activeTab === 'fields' && (
|
||||||
<ExtractedFieldsPanel
|
<ExtractedFieldsPanel
|
||||||
reviewPoints={reviewPoints}
|
reviewPoints={reviewPoints}
|
||||||
onFieldClick={(page) => {
|
onFieldClick={(pointId, page) => {
|
||||||
// 通过 activeReviewPoint 的 id 跳转页面
|
onReviewPointSelect(pointId, page);
|
||||||
if (activeReviewPoint) {
|
|
||||||
onReviewPointSelect(activeReviewPoint.id, page);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import type { ReviewPoint, CharPosition } from '../ReviewPointsList';
|
|||||||
|
|
||||||
interface ReviewPointDetailCardProps {
|
interface ReviewPointDetailCardProps {
|
||||||
reviewPoint: ReviewPoint;
|
reviewPoint: ReviewPoint;
|
||||||
onReviewPointSelect: (id: string, page?: number, charPositions?: CharPosition[], value?: string) => void;
|
onReviewPointSelect: (id: string | number, page?: number, charPositions?: CharPosition[], value?: string) => void;
|
||||||
onStatusChange: (id: string, editAuditStatusId: string | number, status: string, message: string) => void;
|
onStatusChange: (id: string | number, editAuditStatusId: string | number, status: string, message: string) => void;
|
||||||
fileFormat?: string;
|
fileFormat?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ function filterOtherRule(reviewPoint: ReviewPoint): MergedRule[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── renderOtherRule ──
|
// ── 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 fieldKey = rule.fieldKey;
|
||||||
const fieldValue = rule.fieldValue;
|
const fieldValue = rule.fieldValue;
|
||||||
const hasFailure = Object.values(fieldValue.type || {}).some(item => item.res === false);
|
const hasFailure = Object.values(fieldValue.type || {}).some(item => item.res === false);
|
||||||
@@ -273,7 +273,7 @@ function RenderOtherRule({ rule, reviewPoint, onReviewPointSelect }: { rule: Mer
|
|||||||
// ── renderConsistencyRule ──
|
// ── renderConsistencyRule ──
|
||||||
type ChainItem = { field: string; data: { key: string; page: number; value: string; char_positions?: CharPosition[] }; res: boolean; compareMethod?: string };
|
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;
|
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;
|
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;
|
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 ──
|
// ── 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;
|
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;
|
if (config?.res !== reviewPoint.result) return null;
|
||||||
@@ -434,6 +434,91 @@ function RenderModelRule({ rule, reviewPoint, onReviewPointSelect, fileFormat }:
|
|||||||
return <>{fieldElements}</>;
|
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 ──
|
// ── Main Component ──
|
||||||
export function ReviewPointDetailCard({ reviewPoint, onReviewPointSelect, onStatusChange, fileFormat }: ReviewPointDetailCardProps) {
|
export function ReviewPointDetailCard({ reviewPoint, onReviewPointSelect, onStatusChange, fileFormat }: ReviewPointDetailCardProps) {
|
||||||
const resolveManualNote = () => {
|
const resolveManualNote = () => {
|
||||||
@@ -519,7 +604,7 @@ export function ReviewPointDetailCard({ reviewPoint, onReviewPointSelect, onStat
|
|||||||
if (rule.type === 'ai') {
|
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 <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>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -180,9 +180,9 @@ const configs: Record<string, ApiConfig> = {
|
|||||||
// documentUrl: 'http://172.16.0.84:8073/docauditai/',
|
// documentUrl: 'http://172.16.0.84:8073/docauditai/',
|
||||||
// uploadUrl: 'http://172.16.0.84:8073/api/v2/documents',
|
// uploadUrl: 'http://172.16.0.84:8073/api/v2/documents',
|
||||||
|
|
||||||
collaboraUrl: 'http://172.16.0.58:9980',
|
// 公网访问 reviewsTest 时,iframe 不能再直连内网 Collabora,否则浏览器会拦截。
|
||||||
// collaboraUrl: 'http://nas.7bm.co:9980',
|
collaboraUrl: 'http://nas.7bm.co:9980',
|
||||||
appUrl: 'http://172.16.0.34:51703',
|
appUrl: 'http://nas.7bm.co:5173',
|
||||||
|
|
||||||
oauth: {
|
oauth: {
|
||||||
serverUrl: 'http://10.79.112.85', // IDaaS服务器地址
|
serverUrl: 'http://10.79.112.85', // IDaaS服务器地址
|
||||||
|
|||||||
+56
-118
@@ -26,10 +26,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
|
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
|
||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useNavigate, useLoaderData, useFetcher, useRevalidator } from "@remix-run/react";
|
import { useNavigate, useLoaderData, useFetcher } from "@remix-run/react";
|
||||||
import reviewsStyles from "~/styles/reviews.css?url";
|
import reviewsStyles from "~/styles/reviews.css?url";
|
||||||
import { getReviewPoints_fromApi, getUnifiedEvaluationResults, updateReviewResult, confirmReviewResults } from "~/api/evaluation_points/reviews";
|
import { getReviewPoints_fromApi, updateReviewResult, confirmReviewResults } from "~/api/evaluation_points/reviews";
|
||||||
import { toastService } from "~/components/ui/Toast";
|
import { toastService } from "~/components/ui/Toast";
|
||||||
import { Modal } from "~/components/ui/Modal";
|
import { Modal } from "~/components/ui/Modal";
|
||||||
import { UploadArea, type UploadAreaRef } from "~/components/ui/UploadArea";
|
import { UploadArea, type UploadAreaRef } from "~/components/ui/UploadArea";
|
||||||
@@ -155,6 +155,32 @@ interface ReviewData {
|
|||||||
aiAnalysis: AnalysisData;
|
aiAnalysis: AnalysisData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PreviewDocument = {
|
||||||
|
path?: string;
|
||||||
|
attachments?: Array<{
|
||||||
|
fileRole?: string;
|
||||||
|
ossUrl?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolvePreviewPath(document: PreviewDocument | null | undefined): string {
|
||||||
|
if (document?.path) {
|
||||||
|
return document.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryAttachment = Array.isArray(document?.attachments)
|
||||||
|
? document.attachments.find((item) => item?.fileRole === 'primary' && item?.ossUrl)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return primaryAttachment?.ossUrl || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePreviewExtension(document: PreviewDocument | null | undefined): string {
|
||||||
|
const path = resolvePreviewPath(document);
|
||||||
|
const suffix = path.split('.').pop();
|
||||||
|
return typeof suffix === 'string' ? suffix.toLowerCase() : '';
|
||||||
|
}
|
||||||
|
|
||||||
function buildReviewData(document: any, reviewPoints: ReviewPoint[], statistics: Statistics, reviewInfo: ReviewInfo): ReviewData | null {
|
function buildReviewData(document: any, reviewPoints: ReviewPoint[], statistics: Statistics, reviewInfo: ReviewInfo): ReviewData | null {
|
||||||
if (!document) {
|
if (!document) {
|
||||||
return null;
|
return null;
|
||||||
@@ -222,110 +248,26 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
// 获取用户会话信息
|
// 获取用户会话信息
|
||||||
const { getUserSession } = await import("~/api/login/auth.server");
|
const { getUserSession } = await import("~/api/login/auth.server");
|
||||||
const { userInfo, frontendJWT } = await getUserSession(request);
|
const { userInfo, frontendJWT } = await getUserSession(request);
|
||||||
|
// reviewsTest 正式链路统一只走新后端聚合接口,避免旧统一接口 404 噪音。
|
||||||
// 🆕 使用新的统一API获取评查点数据
|
const reviewData = await getReviewPoints_fromApi(id, request);
|
||||||
// 先尝试新的统一评查接口
|
if ('error' in reviewData && reviewData.error) {
|
||||||
const unifiedData = await getUnifiedEvaluationResults(id, request);
|
console.error("[Reviews Loader] 获取评查点数据错误:", reviewData.error);
|
||||||
|
return Response.json({ result: false, message: reviewData.error });
|
||||||
// 如果统一接口返回错误或 flow_type 为 legacy,直接走新后端聚合接口
|
|
||||||
if ('error' in unifiedData || !unifiedData.flow_type) {
|
|
||||||
console.log("[Reviews Loader] 统一接口不可用,直接尝试 review-points 聚合接口...");
|
|
||||||
const reviewData = await getReviewPoints_fromApi(id, request);
|
|
||||||
|
|
||||||
if ('error' in reviewData && reviewData.error) {
|
|
||||||
console.error("[Reviews Loader] 获取评查点数据错误:", reviewData.error);
|
|
||||||
return Response.json({ result: false, message: reviewData.error });
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('document' in reviewData && 'data' in reviewData && 'reviewInfo' in reviewData && 'stats' in reviewData) {
|
|
||||||
return Response.json({
|
|
||||||
previousRoute: previousRoute,
|
|
||||||
document: reviewData.document,
|
|
||||||
reviewPoints: reviewData.data,
|
|
||||||
reviewInfo: reviewData.reviewInfo,
|
|
||||||
statistics: reviewData.stats,
|
|
||||||
comparison_document: reviewData.comparison_document,
|
|
||||||
userInfo,
|
|
||||||
frontendJWT,
|
|
||||||
flowType: 'legacy',
|
|
||||||
scoredResults: null,
|
|
||||||
scoredSummary: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统一接口成功返回,判断流程类型
|
return Response.json({
|
||||||
if (unifiedData.flow_type === 'graphrag') {
|
previousRoute: previousRoute,
|
||||||
// 先获取文档基本信息(统一接口不返回文档内容)
|
document: reviewData.document,
|
||||||
const reviewData = await getReviewPoints_fromApi(id, request);
|
reviewPoints: reviewData.data,
|
||||||
|
reviewInfo: reviewData.reviewInfo,
|
||||||
// 合并已评查的 reviewPoints + 未涉及的评查点
|
statistics: reviewData.stats,
|
||||||
const existingPoints = ('data' in reviewData && !('error' in reviewData)) ? reviewData.data : [];
|
comparison_document: reviewData.comparison_document,
|
||||||
const notApplicablePoints = (unifiedData.results || [])
|
userInfo,
|
||||||
.filter((r: any) => r.result_type === 'not_applicable')
|
frontendJWT,
|
||||||
.map((r: any) => ({
|
flowType: 'legacy',
|
||||||
id: `na-${r.evaluation_point_id}`,
|
scoredResults: null,
|
||||||
documentId: id,
|
scoredSummary: null
|
||||||
pointId: r.evaluation_point_id,
|
});
|
||||||
editAuditStatusId: '',
|
|
||||||
editAuditStatus: '',
|
|
||||||
editAuditStatusMessage: '',
|
|
||||||
title: '该评查点未涉及',
|
|
||||||
pointName: r.name || '',
|
|
||||||
pointCode: r.code || '',
|
|
||||||
groupName: '',
|
|
||||||
status: 'notApplicable',
|
|
||||||
content: {},
|
|
||||||
contentPage: {},
|
|
||||||
suggestion: r.ai_suggestion || '该评查点未涉及',
|
|
||||||
result: null,
|
|
||||||
score: r.score || 0,
|
|
||||||
finalScore: null,
|
|
||||||
machineScore: 0,
|
|
||||||
postAction: '',
|
|
||||||
}));
|
|
||||||
const allReviewPoints = [...existingPoints, ...notApplicablePoints];
|
|
||||||
|
|
||||||
return Response.json({
|
|
||||||
previousRoute: previousRoute,
|
|
||||||
document: ('document' in reviewData && !('error' in reviewData)) ? reviewData.document : null,
|
|
||||||
reviewPoints: allReviewPoints,
|
|
||||||
reviewInfo: { reviewTime: unifiedData.evaluated_at, reviewModel: 'GraphRAG', ruleGroup: '', result: '', issueCount: unifiedData.summary?.total_points || 0 },
|
|
||||||
statistics: {
|
|
||||||
total: unifiedData.summary?.total_points || 0,
|
|
||||||
success: unifiedData.summary?.passed_count || 0,
|
|
||||||
warning: unifiedData.summary?.failed_count || 0,
|
|
||||||
error: 0,
|
|
||||||
notApplicable: unifiedData.summary?.not_applicable_count || 0,
|
|
||||||
score: unifiedData.summary?.total_score || 0
|
|
||||||
},
|
|
||||||
comparison_document: ('comparison_document' in reviewData && !('error' in reviewData)) ? reviewData.comparison_document : null,
|
|
||||||
userInfo,
|
|
||||||
frontendJWT,
|
|
||||||
flowType: 'graphrag',
|
|
||||||
scoredResults: unifiedData.results,
|
|
||||||
scoredSummary: unifiedData.summary
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// legacy 流程但统一接口可用,也统一走 review-points 聚合接口
|
|
||||||
const reviewData = await getReviewPoints_fromApi(id, request);
|
|
||||||
if ('error' in reviewData && reviewData.error) {
|
|
||||||
return Response.json({ result: false, message: reviewData.error });
|
|
||||||
}
|
|
||||||
return Response.json({
|
|
||||||
previousRoute: previousRoute,
|
|
||||||
document: reviewData.document,
|
|
||||||
reviewPoints: reviewData.data,
|
|
||||||
reviewInfo: reviewData.reviewInfo,
|
|
||||||
statistics: reviewData.stats,
|
|
||||||
comparison_document: reviewData.comparison_document,
|
|
||||||
userInfo,
|
|
||||||
frontendJWT,
|
|
||||||
flowType: 'legacy',
|
|
||||||
scoredResults: null,
|
|
||||||
scoredSummary: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Reviews Loader] 获取评查数据失败:', error);
|
console.error('[Reviews Loader] 获取评查数据失败:', error);
|
||||||
console.error('[Reviews Loader] 错误堆栈:', error instanceof Error ? error.stack : '无堆栈信息');
|
console.error('[Reviews Loader] 错误堆栈:', error instanceof Error ? error.stack : '无堆栈信息');
|
||||||
@@ -431,7 +373,7 @@ export default function ReviewDetails() {
|
|||||||
const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态
|
const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态
|
||||||
const [rightActiveTab, setRightActiveTab] = useState<'result' | 'fields' | 'info'>('result');
|
const [rightActiveTab, setRightActiveTab] = useState<'result' | 'fields' | 'info'>('result');
|
||||||
const [reviewData, setReviewData] = useState<ReviewData | null>(fallbackReviewData);
|
const [reviewData, setReviewData] = useState<ReviewData | null>(fallbackReviewData);
|
||||||
const [activeReviewPointResultId, setActiveReviewPointResultId] = useState<string | null>(null);
|
const [activeReviewPointResultId, setActiveReviewPointResultId] = useState<string | number | null>(null);
|
||||||
const [targetPage, setTargetPage] = useState<number | undefined>(undefined);
|
const [targetPage, setTargetPage] = useState<number | undefined>(undefined);
|
||||||
const [charPositions, setCharPositions] = useState<Array<{ box: number[][], char: string, score: number }> | undefined>(undefined);
|
const [charPositions, setCharPositions] = useState<Array<{ box: number[][], char: string, score: number }> | undefined>(undefined);
|
||||||
const [highlightValue, setHighlightValue] = useState<string | undefined>(undefined);
|
const [highlightValue, setHighlightValue] = useState<string | undefined>(undefined);
|
||||||
@@ -443,7 +385,7 @@ export default function ReviewDetails() {
|
|||||||
const [showCompareOverlay, setShowCompareOverlay] = useState(false);
|
const [showCompareOverlay, setShowCompareOverlay] = useState(false);
|
||||||
|
|
||||||
// 一键替换(DOCX Collabora 使用)
|
// 一键替换(DOCX Collabora 使用)
|
||||||
const [aiSuggestionReplace, setAiSuggestionReplace] = useState<{
|
const [aiSuggestionReplace] = useState<{
|
||||||
searchText: string;
|
searchText: string;
|
||||||
replaceText: string;
|
replaceText: string;
|
||||||
pageNumber: number;
|
pageNumber: number;
|
||||||
@@ -456,7 +398,8 @@ export default function ReviewDetails() {
|
|||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [showComparison, setShowComparison] = useState(false);
|
const [showComparison, setShowComparison] = useState(false);
|
||||||
const uploadAreaRef = useRef<UploadAreaRef>(null);
|
const uploadAreaRef = useRef<UploadAreaRef>(null);
|
||||||
const revalidator = useRevalidator();
|
const previewPath = resolvePreviewPath(document);
|
||||||
|
const previewExtension = resolvePreviewExtension(document);
|
||||||
|
|
||||||
// 结构比对按钮显示条件:fileInfo.type 包含 '1'
|
// 结构比对按钮显示条件:fileInfo.type 包含 '1'
|
||||||
const showComparisonButton = (document as any)?.type?.toString().includes('1');
|
const showComparisonButton = (document as any)?.type?.toString().includes('1');
|
||||||
@@ -524,7 +467,7 @@ export default function ReviewDetails() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 从左栏选择评查点
|
// 从左栏选择评查点
|
||||||
const handleRuleSelect = (id: string) => {
|
const handleRuleSelect = (id: string | number) => {
|
||||||
setActiveReviewPointResultId(id);
|
setActiveReviewPointResultId(id);
|
||||||
setRightActiveTab('result');
|
setRightActiveTab('result');
|
||||||
|
|
||||||
@@ -578,7 +521,7 @@ export default function ReviewDetails() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReviewPointSelect = (reviewPointId: string, page?: number, charPos?: Array<{ box: number[][], char: string, score: number }>, value?: string) => {
|
const handleReviewPointSelect = (reviewPointId: string | number, page?: number, charPos?: Array<{ box: number[][], char: string, score: number }>, value?: string) => {
|
||||||
// 如果点击的是相同的评查点,但有page参数,先重置targetPage以确保useEffect能够触发
|
// 如果点击的是相同的评查点,但有page参数,先重置targetPage以确保useEffect能够触发
|
||||||
if (reviewPointId === activeReviewPointResultId && page) {
|
if (reviewPointId === activeReviewPointResultId && page) {
|
||||||
setTargetPage(undefined);
|
setTargetPage(undefined);
|
||||||
@@ -600,11 +543,6 @@ export default function ReviewDetails() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理AI建议替换(保留空实现,右栏详情卡片中DOCX替换需要)
|
|
||||||
const handleAiSuggestionReplace = (_searchText: string, _replaceText: string, _pageNumber: number) => {
|
|
||||||
// PDF 文件不支持替换,暂不实现
|
|
||||||
};
|
|
||||||
|
|
||||||
// 刷新评审数据
|
// 刷新评审数据
|
||||||
// async function refreshReviewData(documentId: string) {
|
// async function refreshReviewData(documentId: string) {
|
||||||
// // 设置加载状态
|
// // 设置加载状态
|
||||||
@@ -942,9 +880,9 @@ export default function ReviewDetails() {
|
|||||||
{/* 中栏:PDF 预览 */}
|
{/* 中栏:PDF 预览 */}
|
||||||
{/* 中栏:文件预览(根据文件类型切换) */}
|
{/* 中栏:文件预览(根据文件类型切换) */}
|
||||||
<section className="flex flex-col min-h-0 bg-slate-100">
|
<section className="flex flex-col min-h-0 bg-slate-100">
|
||||||
{document?.path?.split('.').pop()?.toLowerCase() === 'docx' ? (
|
{previewExtension === 'docx' ? (
|
||||||
<DocxPreviewTest
|
<DocxPreviewTest
|
||||||
filePath={document?.path || ''}
|
filePath={previewPath}
|
||||||
targetPage={targetPage}
|
targetPage={targetPage}
|
||||||
charPositions={charPositions}
|
charPositions={charPositions}
|
||||||
activeReviewPointResultId={activeReviewPointResultId}
|
activeReviewPointResultId={activeReviewPointResultId}
|
||||||
@@ -955,7 +893,7 @@ export default function ReviewDetails() {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PdfPreviewTest
|
<PdfPreviewTest
|
||||||
filePath={document?.path || ''}
|
filePath={previewPath}
|
||||||
targetPage={targetPage}
|
targetPage={targetPage}
|
||||||
charPositions={charPositions}
|
charPositions={charPositions}
|
||||||
activeReviewPointResultId={activeReviewPointResultId}
|
activeReviewPointResultId={activeReviewPointResultId}
|
||||||
|
|||||||
@@ -61,6 +61,16 @@ function unique(values: string[]): string[] {
|
|||||||
return Array.from(new Set(values.filter(Boolean)));
|
return Array.from(new Set(values.filter(Boolean)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveDocumentScope(pack: Pick<RuleYamlPack, 'documentType' | 'mainType' | 'moduleType'>): string {
|
||||||
|
const values = [pack.documentType, pack.mainType, pack.moduleType].join(' ');
|
||||||
|
if (values.includes('合同')) return '合同';
|
||||||
|
if (values.includes('案卷') || values.includes('卷宗') || values.includes('行政处罚') || values.includes('行政许可')) {
|
||||||
|
return '案卷';
|
||||||
|
}
|
||||||
|
if (values.includes('公文')) return '内部公文';
|
||||||
|
return pack.documentType || pack.mainType || pack.moduleType || '未分类';
|
||||||
|
}
|
||||||
|
|
||||||
function riskColor(risk: string): TagColor {
|
function riskColor(risk: string): TagColor {
|
||||||
if (risk === 'high') return 'red';
|
if (risk === 'high') return 'red';
|
||||||
if (risk === 'medium') return 'orange';
|
if (risk === 'medium') return 'orange';
|
||||||
@@ -86,28 +96,36 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const packs = await loadRuleConfigPacks(request);
|
const packs = await loadRuleConfigPacks(request);
|
||||||
const documentTypes = unique(packs.map(pack => pack.documentType));
|
const packScopes = packs.map(pack => ({
|
||||||
const inferredDocumentType = packs.find(pack => pack.mainType === requestedFilters.mainType)?.documentType || '';
|
pack,
|
||||||
const currentDocumentType = documentTypes.includes(requestedFilters.documentType)
|
scope: resolveDocumentScope(pack),
|
||||||
? requestedFilters.documentType
|
}));
|
||||||
|
const documentTypes = unique(packScopes.map(item => item.scope));
|
||||||
|
const requestedDocumentType = requestedFilters.documentType;
|
||||||
|
const inferredDocumentType = requestedMainType
|
||||||
|
? packScopes.find(item => item.pack.mainType === requestedMainType)?.scope || ''
|
||||||
|
: '';
|
||||||
|
const currentDocumentType = documentTypes.includes(requestedDocumentType)
|
||||||
|
? requestedDocumentType
|
||||||
: inferredDocumentType || documentTypes[0] || '';
|
: inferredDocumentType || documentTypes[0] || '';
|
||||||
|
const scopedDocumentPacks = packScopes
|
||||||
|
.filter(item => item.scope === currentDocumentType)
|
||||||
|
.map(item => item.pack);
|
||||||
const scopedFilters = {
|
const scopedFilters = {
|
||||||
...requestedFilters,
|
...requestedFilters,
|
||||||
documentType: currentDocumentType,
|
documentType: currentDocumentType,
|
||||||
mainType: packs.some(pack => pack.documentType === currentDocumentType && pack.mainType === requestedFilters.mainType)
|
mainType: scopedDocumentPacks.some(pack => pack.mainType === requestedFilters.mainType)
|
||||||
? requestedFilters.mainType
|
? requestedFilters.mainType
|
||||||
: '',
|
: '',
|
||||||
subtype: packs.some(pack =>
|
subtype: scopedDocumentPacks.some(pack =>
|
||||||
pack.documentType === currentDocumentType &&
|
|
||||||
(!requestedFilters.mainType || pack.mainType === requestedFilters.mainType) &&
|
(!requestedFilters.mainType || pack.mainType === requestedFilters.mainType) &&
|
||||||
pack.subtype === requestedFilters.subtype
|
pack.subtype === requestedFilters.subtype
|
||||||
)
|
)
|
||||||
? requestedFilters.subtype
|
? requestedFilters.subtype
|
||||||
: ''
|
: ''
|
||||||
};
|
};
|
||||||
const scopedByMainTypePacks = packs.filter(pack =>
|
const scopedByMainTypePacks = scopedDocumentPacks.filter(pack =>
|
||||||
pack.documentType === scopedFilters.documentType &&
|
!scopedFilters.mainType || pack.mainType === scopedFilters.mainType
|
||||||
(!scopedFilters.mainType || pack.mainType === scopedFilters.mainType)
|
|
||||||
);
|
);
|
||||||
const subtypeOptions = unique(scopedByMainTypePacks.map(pack => pack.subtype));
|
const subtypeOptions = unique(scopedByMainTypePacks.map(pack => pack.subtype));
|
||||||
const ruleGroupSourcePacks = scopedFilters.subtype
|
const ruleGroupSourcePacks = scopedFilters.subtype
|
||||||
@@ -122,8 +140,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
...scopedFilters,
|
...scopedFilters,
|
||||||
ruleGroup: ruleGroupOptions.includes(requestedFilters.ruleGroup) ? requestedFilters.ruleGroup : ''
|
ruleGroup: ruleGroupOptions.includes(requestedFilters.ruleGroup) ? requestedFilters.ruleGroup : ''
|
||||||
};
|
};
|
||||||
const visiblePacks = packs.filter(pack =>
|
const visiblePacks = scopedDocumentPacks.filter(pack =>
|
||||||
pack.documentType === filters.documentType &&
|
|
||||||
(!filters.mainType || pack.mainType === filters.mainType) &&
|
(!filters.mainType || pack.mainType === filters.mainType) &&
|
||||||
(!filters.subtype || pack.subtype === filters.subtype)
|
(!filters.subtype || pack.subtype === filters.subtype)
|
||||||
);
|
);
|
||||||
@@ -196,7 +213,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
pageSize: filters.pageSize,
|
pageSize: filters.pageSize,
|
||||||
options: {
|
options: {
|
||||||
documentTypes,
|
documentTypes,
|
||||||
mainTypes: unique(packs.filter(pack => pack.documentType === filters.documentType).map(pack => pack.mainType)),
|
mainTypes: unique(scopedDocumentPacks.map(pack => pack.mainType)),
|
||||||
subtypes: subtypeOptions,
|
subtypes: subtypeOptions,
|
||||||
ruleGroups: ruleGroupOptions
|
ruleGroups: ruleGroupOptions
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,39 @@ export class WopiService {
|
|||||||
return fileId.replace(/\.\./g, '').replace(/^\//, '');
|
return fileId.replace(/\.\./g, '').replace(/^\//, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 某些后端文件代理不支持 HEAD,这里先尝试 HEAD,遇到 405 再回退到 GET。
|
||||||
|
*/
|
||||||
|
private async probeFileMetadata(fileUrl: string, frontendJWT: string) {
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `Bearer ${frontendJWT}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const headResponse = await fetch(fileUrl, {
|
||||||
|
method: 'HEAD',
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (headResponse.ok) {
|
||||||
|
return headResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headResponse.status !== 405) {
|
||||||
|
throw new Error(`文件探测失败: ${headResponse.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getResponse = await fetch(fileUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!getResponse.ok) {
|
||||||
|
throw new Error(`文件探测失败: ${getResponse.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getResponse;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CheckFileInfo - 返回文件元数据
|
* CheckFileInfo - 返回文件元数据
|
||||||
* @param fileId - 文件路径(例如:contracts/test.docx)
|
* @param fileId - 文件路径(例如:contracts/test.docx)
|
||||||
@@ -123,20 +156,12 @@ export class WopiService {
|
|||||||
// 清理文件路径
|
// 清理文件路径
|
||||||
const sanitizedFileId = this.sanitizeFileId(fileId);
|
const sanitizedFileId = this.sanitizeFileId(fileId);
|
||||||
|
|
||||||
// 通过 FastAPI 代理获取文件元数据(使用 HEAD 请求)
|
// 通过 FastAPI 代理获取文件元数据。
|
||||||
|
// 注意:当前后端文件路由对 HEAD 返回 405,不能再直接据此判定“文件不存在”。
|
||||||
const fileUrl = `${DOCUMENT_URL}${sanitizedFileId}`;
|
const fileUrl = `${DOCUMENT_URL}${sanitizedFileId}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(fileUrl, {
|
const response = await this.probeFileMetadata(fileUrl, tokenData.frontendJWT);
|
||||||
method: 'HEAD',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${tokenData.frontendJWT}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`文件不存在: ${sanitizedFileId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentLength = response.headers.get('Content-Length');
|
const contentLength = response.headers.get('Content-Length');
|
||||||
const lastModified = response.headers.get('Last-Modified');
|
const lastModified = response.headers.get('Last-Modified');
|
||||||
|
|||||||
Reference in New Issue
Block a user