feat: align frontend document and rule management flows

This commit is contained in:
wren
2026-05-06 09:40:37 +08:00
parent 8a5044b024
commit c54f84382b
41 changed files with 4239 additions and 2903 deletions
+103 -62
View File
@@ -1,4 +1,4 @@
import { useState, useEffect, useRef, useCallback } from "react";
import { Fragment, useState, useEffect, useRef, useCallback } from "react";
import { useSearchParams, useLoaderData, useFetcher, useNavigate,Link } from "@remix-run/react";
import { type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
import { Card } from "~/components/ui/Card";
@@ -13,6 +13,7 @@ import { TableRowSkeleton, LoadingIndicator, NumberSkeleton } from '~/components
import documentsIndexStyles from "~/styles/pages/documents_index.css?url";
import documentVersionStyles from "~/styles/components/document-version.css?url";
import { getDocumentTypesByIds, deleteDocument, getDocumentsListFromAPI, type DocumentUI, type DocumentVersionUI } from "~/api/files/documents";
import { getDocumentTypes } from "~/api/document-types/document-types";
// import { IssuesDiff } from "~/components/ui/IssuesDiff";
import { ResultStats } from "~/components/ui/ResultStats";
import { updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files";
@@ -57,7 +58,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
// label: type.name
// }));
// 初始返回空数据,将在客户端根据 sessionStorage 中的 documentTypeIds 加载实际数据
// 初始返回空数据,客户端根据入口模块/类型作用域加载实际数据
return Response.json({
documents: [],
total: 0,
@@ -76,6 +77,11 @@ interface ActionResponse {
message: string;
}
interface DocumentListScope {
selectedModuleId: number | null;
documentTypeIds: number[];
}
// 审核状态筛选选项
const auditStatusOptions = [
// { value: "", label: "全部" },
@@ -186,12 +192,13 @@ export default function DocumentsIndex() {
const navigate = useNavigate();
// 权限控制
const { hasPermission } = usePermission();
const canView = hasPermission('document:document:view');
const canUpdate = hasPermission('document:document:update');
const { hasAnyPermission } = usePermission();
const canView = hasAnyPermission(['documents:list:read', 'documents:detail:read']);
const canUpdate = hasAnyPermission(['documents:update:write', 'documents:detail:read', 'documents:list:read']);
// 存储从 sessionStorage 获取的 documentTypeIds
const [documentTypeIds, setDocumentTypeIds] = useState<number[] | null>(null);
// 文档列表现在优先走入口模块作用域,documentTypeIds 仅保留兼容旧首页缓存。
const [listScope, setListScope] = useState<DocumentListScope>({ selectedModuleId: null, documentTypeIds: [] });
const [scopeReady, setScopeReady] = useState(false);
// 添加页面加载状态管理
const [isLoadingData, setIsLoadingData] = useState(true);
@@ -275,8 +282,40 @@ export default function DocumentsIndex() {
const pageSize = parseInt(searchParams.get("pageSize") || "10", 10);
const readListScopeFromSession = useCallback((): DocumentListScope => {
if (typeof window === 'undefined') {
return { selectedModuleId: null, documentTypeIds: [] };
}
let selectedModuleId: number | null = null;
const selectedModuleIdStr = sessionStorage.getItem('selectedModuleId');
if (selectedModuleIdStr) {
const parsedModuleId = Number(selectedModuleIdStr);
if (Number.isFinite(parsedModuleId) && parsedModuleId > 0) {
selectedModuleId = parsedModuleId;
}
}
let documentTypeIds: number[] = [];
const typeIdsStr = sessionStorage.getItem('documentTypeIds');
if (typeIdsStr) {
try {
const parsedTypeIds = JSON.parse(typeIdsStr);
if (Array.isArray(parsedTypeIds)) {
documentTypeIds = parsedTypeIds
.map((item) => Number(item))
.filter((item) => Number.isFinite(item) && item > 0);
}
} catch (error) {
console.error('解析 sessionStorage.documentTypeIds 失败:', error);
}
}
return { selectedModuleId, documentTypeIds };
}, []);
// 客户端数据请求
const fetchData = useCallback(async (typeIds: number[]) => {
const fetchData = useCallback(async (scope: DocumentListScope) => {
setIsLoadingData(true);
loadingBarService.show();
@@ -290,7 +329,12 @@ export default function DocumentsIndex() {
return;
}
console.log('🔑 [fetchData] 文档类型IDs:', typeIds);
const selectedTypeId = Number(documentType);
const scopedTypeIds = documentType && Number.isFinite(selectedTypeId) && selectedTypeId > 0
? [selectedTypeId]
: scope.documentTypeIds;
console.log('🔑 [fetchData] 文档列表作用域:', scope);
// 调用新的 API 函数
const result = await getDocumentsListFromAPI({
@@ -298,7 +342,8 @@ export default function DocumentsIndex() {
pageSize: pageSize,
name: search || undefined,
documentNumber: documentNumber || undefined,
documentTypeIds: documentType ? [parseInt(documentType, 10)] : typeIds, // 如果有单独选择的类型,优先使用
documentTypeIds: scopedTypeIds,
entryModuleId: scope.selectedModuleId || undefined,
auditStatus: auditStatus || undefined,
fileStatus: fileStatus || undefined,
dateFrom: dateFrom || undefined,
@@ -320,14 +365,26 @@ export default function DocumentsIndex() {
setDocuments(result.data.documents);
setTotal(result.data.total);
// 获取经过过滤的文档类型列表
const filteredTypesResponse = await getDocumentTypesByIds(typeIds, jwtToken);
// 文档类型下拉与列表作用域保持一致:优先入口模块,其次兼容旧缓存 typeIds。
const filteredTypesResponse = scope.selectedModuleId
? await getDocumentTypes({ entry_module_id: scope.selectedModuleId, page: 1, pageSize: 200 }, jwtToken)
: await getDocumentTypesByIds(scope.documentTypeIds, jwtToken);
if (filteredTypesResponse.data?.types?.length) {
const typeNameMap = new Map(
filteredTypesResponse.data.types.map((type) => [String(type.id), type.name])
);
const filteredOptions = filteredTypesResponse.data.types.map(type => ({
value: type.id,
label: type.name
}));
setFilteredDocumentTypeOptions(filteredOptions);
setDocuments(
result.data.documents.map((doc) => ({
...doc,
typeName: typeNameMap.get(doc.type) || doc.typeName,
}))
);
} else {
const fallbackOptions = Array.from(
new Map(
@@ -337,6 +394,7 @@ export default function DocumentsIndex() {
).values()
);
setFilteredDocumentTypeOptions(fallbackOptions);
setDocuments(result.data.documents);
}
} catch (error) {
@@ -349,41 +407,27 @@ export default function DocumentsIndex() {
}
}, [search, documentNumber, documentType, auditStatus, fileStatus, dateFrom, dateTo, currentPage, pageSize, loaderData.frontendJWT]);
// 在组件挂载时从 sessionStorage 获取 documentTypeIds 并加载数据
// 在组件挂载时建立页面作用域并加载数据
useEffect(() => {
try {
if (typeof window !== 'undefined') {
const typeIdsStr = sessionStorage.getItem('documentTypeIds');
if (typeIdsStr) {
const typeIds = JSON.parse(typeIdsStr) as number[];
console.log('📋 [useEffect] 从 sessionStorage 获取文档类型IDs:', typeIds);
setDocumentTypeIds(typeIds);
// 加载数据(fetchData 中会自动获取并设置过滤后的文档类型选项)
fetchData(typeIds);
} else {
console.warn('⚠️ [useEffect] sessionStorage 中没有 documentTypeIds');
// 没有 documentTypeIds 时,标记初始化完成但无数据
setIsLoadingData(false);
setHasInitialized(true);
loadingBarService.hide();
}
}
const nextScope = readListScopeFromSession();
setListScope(nextScope);
setScopeReady(true);
} catch (error) {
console.error('❌ [useEffect] 获取 sessionStorage 中的 documentTypeIds 失败:', error);
console.error('❌ [useEffect] 初始化文档列表作用域失败:', error);
// 出错时也标记初始化完成
setIsLoadingData(false);
setHasInitialized(true);
setScopeReady(true);
loadingBarService.hide();
}
}, [fetchData]);
}, [readListScopeFromSession]);
// 监听 URL 参数变化,重新获取数据
useEffect(() => {
if (documentTypeIds) {
fetchData(documentTypeIds);
}
}, [searchParams, fetchData, documentTypeIds]);
if (!scopeReady) return;
fetchData(listScope);
}, [searchParams, fetchData, listScope, scopeReady]);
// 监听 documents 数据变化,自动修正不一致的展开状态
useEffect(() => {
@@ -439,15 +483,13 @@ export default function DocumentsIndex() {
if (fetcher.data.result) {
toastService.success(fetcher.data.message);
// 删除成功后重新加载数据
if (documentTypeIds) {
fetchData(documentTypeIds);
}
fetchData(listScope);
} else if (fetcher.data.message) {
toastService.error(fetcher.data.message);
// 删除失败只显示错误信息,不刷新数据
}
}
}, [fetcher.data, fetcher.state, fetchData, documentTypeIds, isDeleting]);
}, [fetcher.data, fetcher.state, fetchData, listScope, isDeleting]);
// 分页处理函数
const handlePageChange = (page: number) => {
@@ -921,9 +963,7 @@ export default function DocumentsIndex() {
setSelectedDocumentVersion(null);
// 刷新文档列表
if (documentTypeIds && documentTypeIds.length > 0) {
fetchData(documentTypeIds);
}
fetchData(listScope);
} catch (error) {
console.error('【附件追加】上传失败:', error);
@@ -993,9 +1033,7 @@ export default function DocumentsIndex() {
setSelectedDocumentVersion(null);
// 刷新文档列表
if (documentTypeIds && documentTypeIds.length > 0) {
fetchData(documentTypeIds);
}
fetchData(listScope);
} catch (error) {
console.error('【合同模板上传】上传失败:', error);
@@ -1178,7 +1216,7 @@ export default function DocumentsIndex() {
className="text-xs px-2 py-1 h-7 mr-1 hover:underline"
>
<i className="ri-eye-line"></i>
</Link>
)}
{/* 修改按钮 - 需要 document:document:view 权限 */}
@@ -1316,6 +1354,11 @@ export default function DocumentsIndex() {
fileType={record.fileType}
colorMode="light"
/>
{record.groupName && (
<span className="text-xs bg-emerald-50 text-emerald-700 border border-emerald-200 px-2 py-[2px] rounded">
{record.groupName}
</span>
)}
{record.isTest && (
<span className="text-xs bg-gray-100 text-gray-500 px-1 rounded"></span>
)}
@@ -1379,14 +1422,14 @@ export default function DocumentsIndex() {
width:"18%",
render: (_: unknown, record: DocumentUI) => (
<ResultStats
passCount={record.pass_count}
warningCount={record.warning_count}
errorCount={record.error_count}
manualCount={record.manual_count}
previousPassCount={record.previous_pass_count}
previousWarningCount={record.previous_warning_count}
previousErrorCount={record.previous_error_count}
previousManualCount={record.previous_manual_count}
passCount={record.pass_count ?? null}
warningCount={record.warning_count ?? null}
errorCount={record.error_count ?? null}
manualCount={record.manual_count ?? null}
previousPassCount={record.previous_pass_count ?? null}
previousWarningCount={record.previous_warning_count ?? null}
previousErrorCount={record.previous_error_count ?? null}
previousManualCount={record.previous_manual_count ?? null}
warningMessages={record.warning_messages}
errorMessages={record.error_messages}
manualMessages={record.manual_messages}
@@ -1436,7 +1479,7 @@ export default function DocumentsIndex() {
className="text-xs px-2 py-1 h-7 mr-1 hover:underline"
>
<i className="ri-eye-line"></i>
</Link>
)}
</>
@@ -1547,9 +1590,7 @@ export default function DocumentsIndex() {
type="primary"
icon="ri-search-line"
onClick={() => {
if (documentTypeIds) {
fetchData(documentTypeIds);
}
fetchData(listScope);
}}
className="mr-2"
>
@@ -1673,7 +1714,7 @@ export default function DocumentsIndex() {
</thead>
<tbody>
{documents.map((doc) => (
<>
<Fragment key={`doc-group-${doc.id}`}>
{/* 主文档行 */}
<tr
key={doc.id}
@@ -1705,7 +1746,7 @@ export default function DocumentsIndex() {
>
{columns.map((col, index) => (
<td key={col.key || index} className="px-4 py-3 text-sm">
{col.render ? col.render(null, doc, index) : (doc as any)[col.key]}
{col.render ? col.render(null, doc) : (doc as any)[col.key]}
</td>
))}
</tr>
@@ -1738,7 +1779,7 @@ export default function DocumentsIndex() {
)}
</>
)}
</>
</Fragment>
))}
</tbody>
</table>