feat: align frontend document and rule management flows
This commit is contained in:
+103
-62
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user