feat: 1. 完善全局路由的访问权限的验证。 2. 完善接口返回的树形路由结构 3.优化评查点列表的查询,改用表连接的方式,废弃使用数据库的rpc函数,同时进行地区隔离和权限隔离。

4. 删除冗余的评查文件列表。      5.完善上传文档 页面初始化查询数据的时候 查询文件类型(改成动态指定)  6. 添加获取入口模块的查询接口。    7.完善服务端中判断token的有效性,失效则跳转到登录页。
8. 重构layout和sidebar的页面,改成由动态权限路由来渲染对应的菜单栏。       9.重构入口页面,通过动态查询根据不同地区的人返回不同的入口。
This commit is contained in:
2025-11-20 01:35:30 +08:00
parent adfb84a31d
commit 2edde8a8ab
23 changed files with 1201 additions and 2154 deletions
+113 -79
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData, useSearchParams, Link, useNavigate, useFetcher, useRouteLoaderData, useLocation } from "@remix-run/react";
import { Button } from '~/components/ui/Button';
@@ -38,6 +38,10 @@ export const meta: MetaFunction = () => {
];
};
export const handle = {
breadcrumb: "评查点列表"
};
// 声明loader返回的数据类型
export type LoaderData = {
rules: Rule[];
@@ -70,9 +74,17 @@ interface ActionResponse {
}
function mapApiRuleToModel(apiRule: ApiRule): Rule {
// 🔑 清洗评查点编码:移除最后一个 '--' 及其后面的字符
// 例如:'code-mis--mz' --> 'code-mis', 'code-mbs--alsi--gz' --> 'code-mbs--alsi'
let cleanedCode = apiRule.code;
const lastDoubleHyphenIndex = cleanedCode.lastIndexOf('--');
if (lastDoubleHyphenIndex !== -1) {
cleanedCode = cleanedCode.substring(0, lastDoubleHyphenIndex);
}
return {
id: apiRule.id,
code: apiRule.code,
code: cleanedCode,
name: apiRule.name,
ruleType: apiRule.ruleType as RuleType, // 类型转换
ruleGroupId: apiRule.groupId,
@@ -105,22 +117,13 @@ export async function loader({ request }: LoaderFunctionArgs) {
const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT } = await getUserSession(request);
// 获取评查点类型列表,供前端筛选使用
const typeResponse = await getRuleTypes(undefined, frontendJWT);
if (typeResponse.error) {
console.error('获取评查点类型失败:', typeResponse.error);
}
const ruleTypes = typeResponse.error ? [] : typeResponse.data;
// 返回初始空数据,客户端将根据 sessionStorage 中的 reviewType 加载实际数据
// 返回初始空数据,客户端将根据 sessionStorage 中的 documentTypeIds 加载实际数据
return Response.json({
rules: [],
totalCount: 0,
currentPage: params.page,
pageSize: params.pageSize,
ruleTypes,
ruleTypes: [], // 服务端无法访问 sessionStorage,客户端加载
initialLoad: true,
frontendJWT
}, {
@@ -198,16 +201,16 @@ export default function RulesIndex() {
// 状态管理
const [ruleGroups, setRuleGroups] = useState<RuleGroup[]>([]);
const [loadingGroups, setLoadingGroups] = useState(false);
const [reviewType, setReviewType] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [filteredRules, setFilteredRules] = useState<Rule[]>(initialRules);
const [filteredTotalCount, setFilteredTotalCount] = useState<number>(initialTotalCount);
const [ruleTypes, setRuleTypes] = useState<ApiRuleType[]>(initialRuleTypes);
// 添加一个路由变化计数器
const [routeChangeCount, setRouteChangeCount] = useState(0);
// 添加一个状态来跟踪是否执行了删除操作
const [isDeleting, setIsDeleting] = useState(false);
// 使用 ref 跟踪是否正在加载数据,避免重复加载
const isLoadingRef = useRef(false);
// 查询参数记忆 key 与保存/恢复
const SEARCH_PARAMS_STORAGE_KEY = 'rules.searchParams';
@@ -231,19 +234,13 @@ export default function RulesIndex() {
// 获取当前的ruleType值
const ruleTypeParam = searchParams.get('ruleType');
// 追踪路由变化
useEffect(() => {
// console.log("路由变化:", location.key);
setRouteChangeCount(prev => prev + 1);
}, [location]);
// 判断是否禁用规则组选择
const isRuleGroupSelectDisabled = loadingGroups || !ruleTypeParam || ruleGroups.length === 0;
// 检查用户是否为开发者角色
const userRole = rootData?.userRole || 'common';
const isDeveloper = userRole === 'admin';
const isDeveloper = userRole.includes('admin');
// 调试日志
// console.log("🔑 [Rules List] rootData:", rootData);
@@ -261,56 +258,93 @@ export default function RulesIndex() {
useEffect(() => {
if(loaderData.error) {
toastService.error(loaderData.error);
}else if(loaderData.ruleTypes.length === 0){
toastService.error("评查点类型数据为空");
}
}, [loaderData.error,loaderData.ruleTypes]);
// ❌ 不再检查 loaderData.ruleTypes,因为服务端永远返回空数组
// 如果需要检查评查点类型数据,应该在 fetchData 完成后检查状态 ruleTypes
}, [loaderData.error]);
// 客户端数据加载函数
const fetchData = useCallback(async () => {
try {
// 从sessionStorage获取reviewType
const storedReviewType = typeof window !== 'undefined' ? sessionStorage.getItem('reviewType') : null;
const typeToUse = reviewType || storedReviewType;
if (!typeToUse) {
console.warn('无法加载评查点数据:未找到reviewType');
// 🔑 如果正在加载,避免重复调用
if (isLoadingRef.current) {
console.log('📋 [fetchData] 正在加载中,跳过重复调用');
return;
}
// console.log("fetchData被调用,加载评查点数据", typeToUse);
// 🔑 从 sessionStorage 获取 documentTypeIds
const typeIdsStr = typeof window !== 'undefined' ? sessionStorage.getItem('documentTypeIds') : null;
const documentTypeIds = typeIdsStr ? JSON.parse(typeIdsStr) : null;
if (!documentTypeIds || documentTypeIds.length === 0) {
console.warn('无法加载评查点数据:未找到 documentTypeIds');
return;
}
isLoadingRef.current = true;
// 🔑 从 localStorage 获取 user_info 中的 area
let userArea: string | undefined = undefined;
if (typeof window !== 'undefined') {
try {
const userInfoStr = localStorage.getItem('user_info');
if (userInfoStr) {
const userInfo = JSON.parse(userInfoStr);
userArea = userInfo.area;
console.log("📋 [fetchData] 从 localStorage 获取到用户地区:", userArea);
}
} catch (error) {
console.error('解析 user_info 失败:', error);
}
}
console.log("📋 [fetchData] 开始加载评查点数据, documentTypeIds:", documentTypeIds, "area:", userArea);
setLoading(true);
// 获取评查点类型
// 🔑 获取评查点类型(通过 documentTypeIds
let loadedRuleTypes: ApiRuleType[] = [];
try {
const typeResponse = await getRuleTypes(typeToUse, loaderData.frontendJWT);
const typeResponse = await getRuleTypes(documentTypeIds, loaderData.frontendJWT);
if (typeResponse.data) {
setRuleTypes(typeResponse.data);
loadedRuleTypes = typeResponse.data;
setRuleTypes(loadedRuleTypes);
console.log("📋 [fetchData] 获取到评查点类型:", loadedRuleTypes);
}
} catch (error) {
console.error('加载评查点类型失败:', error);
}
// 构建查询参数
// 🔑 当选择"全部"或未选择评查点类型时,使用下拉框中所有评查点类型的 id 组合
let finalRuleType: string | undefined = undefined;
if (ruleTypeParam && ruleTypeParam !== 'all') {
// 选择了具体的评查点类型
finalRuleType = ruleTypeParam;
} else if (loadedRuleTypes && loadedRuleTypes.length > 0) {
// 选择"全部"或未选择,使用刚加载的评查点类型的 id
finalRuleType = loadedRuleTypes.map(type => type.id).join(',');
console.log("📋 [fetchData] 选择全部类型,使用 loadedRuleTypes 的 id 组合:", finalRuleType);
}
const queryParams = {
ruleType: ruleTypeParam || undefined,
ruleType: finalRuleType,
groupId: searchParams.get('groupId') || undefined,
isActive: searchParams.get('isActive') ? searchParams.get('isActive') === 'true' : undefined,
keyword: searchParams.get('keyword') || undefined,
area: userArea, // 添加地区过滤
page: currentPage,
pageSize,
reviewType: typeToUse,
token: loaderData.frontendJWT
};
// 调用 API 获取数据
const response = await getRulesList(queryParams);
if (response.data) {
const apiRules = response.data.rules || [];
const total = response.data.totalCount || 0;
const mappedRules = apiRules.map((apiRule: ApiRule) => mapApiRuleToModel(apiRule));
setFilteredRules(mappedRules);
setFilteredTotalCount(total);
}
@@ -319,8 +353,9 @@ export default function RulesIndex() {
toastService.error('加载评查点列表失败');
} finally {
setLoading(false);
isLoadingRef.current = false;
}
}, [reviewType, ruleTypeParam, searchParams, currentPage, pageSize]);
}, [ruleTypeParam, searchParams, currentPage, pageSize, loaderData.frontendJWT]);
// 当评查点类型变化时,加载对应的规则组
useEffect(() => {
@@ -380,55 +415,54 @@ export default function RulesIndex() {
}
}, [fetcher.data, fetcher.state, fetchData, isDeleting]);
// 在组件挂载时从 sessionStorage 获取 reviewType 并加载数据
// 在组件挂载时从 sessionStorage 获取 documentTypeIds 并加载数据
useEffect(() => {
try {
if (typeof window !== 'undefined') {
const storedReviewType = sessionStorage.getItem('reviewType');
// console.log("组件挂载,从sessionStorage获取reviewType:", storedReviewType);
if (storedReviewType !== reviewType) {
setReviewType(storedReviewType);
}
// 无论如何,都加载数据,不依赖于reviewType的变化
if (storedReviewType) {
// 使用setTimeout确保该操作在其他状态更新之后执行
const typeIdsStr = sessionStorage.getItem('documentTypeIds');
const documentTypeIds = typeIdsStr ? JSON.parse(typeIdsStr) : null;
console.log("📋 组件挂载,从 sessionStorage 获取 documentTypeIds:", documentTypeIds);
// 如果有 documentTypeIds,加载数据
if (documentTypeIds && documentTypeIds.length > 0) {
// 使用 setTimeout 确保该操作在其他状态更新之后执行
setTimeout(() => {
fetchData();
}, 0);
}
}
} catch (error) {
console.error('获取 sessionStorage 中的 reviewType 失败:', error);
console.error('获取 sessionStorage 中的 documentTypeIds 失败:', error);
}
}, [initialLoad, fetchData]);
// 监听路由变化,每次路由到此页面时刷新数据
useEffect(() => {
if (routeChangeCount > 0) {
// console.log("路由变化触发数据刷新,计数:", routeChangeCount);
const storedReviewType = typeof window !== 'undefined' ? sessionStorage.getItem('reviewType') : null;
// console.log("storedReviewType:", storedReviewType);
if (storedReviewType) {
if (storedReviewType !== reviewType) {
setReviewType(storedReviewType);
}
// 使用setTimeout确保该操作在其他状态更新之后执行
setTimeout(() => {
fetchData();
}, 0);
}
}
}, [routeChangeCount, fetchData]);
// 注释掉重复的路由监听逻辑,避免与searchParams监听重复触发
// useEffect(() => {
// if (routeChangeCount > 0) {
// console.log("📋 路由变化触发数据刷新,计数:", routeChangeCount);
// const typeIdsStr = typeof window !== 'undefined' ? sessionStorage.getItem('documentTypeIds') : null;
// const documentTypeIds = typeIdsStr ? JSON.parse(typeIdsStr) : null;
// console.log("📋 documentTypeIds:", documentTypeIds);
// if (documentTypeIds && documentTypeIds.length > 0) {
// // 使用 setTimeout 确保该操作在其他状态更新之后执行
// setTimeout(() => {
// fetchData();
// }, 0);
// }
// }
// }, [routeChangeCount, fetchData]);
// 监听 URL 参数变化,重新获取数据
useEffect(() => {
if (reviewType) {
// 检查是否有 documentTypeIds
const typeIdsStr = typeof window !== 'undefined' ? sessionStorage.getItem('documentTypeIds') : null;
const documentTypeIds = typeIdsStr ? JSON.parse(typeIdsStr) : null;
if (documentTypeIds && documentTypeIds.length > 0) {
fetchData();
}
}, [searchParams, fetchData, reviewType]);
}, [searchParams, fetchData]);
// 筛选评查点
const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) => {