前端功能遗漏修复

main
kangwenjing 2026-04-08 09:09:59 +08:00
parent b13a2bfc87
commit 6e897552d0
8 changed files with 237 additions and 51 deletions

View File

@ -8,7 +8,7 @@ import type { MenuProps } from 'antd/es/menu';
import { DownOutlined, LogoutOutlined, MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined } from '@ant-design/icons';
import { useLocation, useNavigate } from 'react-router-dom';
import { removeToken } from '../../utils/auth';
import { usePermission } from '@/contexts/PermissionContext';
import { clearPermissionCache, usePermission } from '@/contexts/PermissionContext';
import './navbar.css';
interface AppNavbarProps {
@ -193,7 +193,8 @@ const isPinnedTab = (tab: OpenPageTab) => tab.pathname === '/index';
const AppNavbar: React.FC<AppNavbarProps> = ({ collapsed, onToggle }) => {
const navigate = useNavigate();
const location = useLocation();
const { userName, canAccessPath } = usePermission();
const { userName, currentUser, canAccessPath } = usePermission();
const displayName = String(currentUser.nickName || userName || '用户');
const activeTab = React.useMemo(
() => createOpenPageTab(location.pathname, location.search),
[location.pathname, location.search],
@ -201,6 +202,7 @@ const AppNavbar: React.FC<AppNavbarProps> = ({ collapsed, onToggle }) => {
const [openTabs, setOpenTabs] = React.useState<OpenPageTab[]>(() => getStoredOpenPageTabs(canAccessPath));
const handleLogout = () => {
clearPermissionCache();
removeToken();
navigate('/login');
};
@ -331,7 +333,7 @@ const AppNavbar: React.FC<AppNavbarProps> = ({ collapsed, onToggle }) => {
<Button type="text" className="app-navbar-user">
<Space size={10}>
<Avatar size={36} className="app-navbar-avatar" icon={<UserOutlined />} />
<span className="app-navbar-user-name">{userName || '用户'}</span>
<span className="app-navbar-user-name">{displayName}</span>
<DownOutlined className="app-navbar-chevron" />
</Space>
</Button>

View File

@ -79,6 +79,7 @@ const SUPER_PERMI = '*:*:*';
const ADMIN_ROLE = 'admin';
const PERMISSION_CACHE_KEY = 'pms_permission_cache_v1';
const PERMISSION_CACHE_TTL_MS = 5 * 60 * 1000;
const PERMISSION_CHANGED_EVENT = 'pms:permission-changed';
interface PermissionCacheState {
tokenFingerprint: string;
@ -162,13 +163,21 @@ const writePermissionCache = (state: Omit<PermissionCacheState, 'tokenFingerprin
}
};
const clearPermissionCache = () => {
export const clearPermissionCache = () => {
if (typeof window === 'undefined') {
return;
}
window.sessionStorage.removeItem(PERMISSION_CACHE_KEY);
};
export const notifyPermissionChanged = () => {
clearPermissionCache();
if (typeof window === 'undefined') {
return;
}
window.dispatchEvent(new Event(PERMISSION_CHANGED_EVENT));
};
const normalizePath = (rawPath: string) => {
const path = rawPath.split('?')[0]?.split('#')[0] ?? '';
if (!path) {
@ -453,6 +462,16 @@ export const PermissionProvider: React.FC<{ children: React.ReactNode }> = ({ ch
void refreshPermissions();
}, [refreshPermissions]);
useEffect(() => {
const handlePermissionChanged = () => {
void refreshPermissions();
};
window.addEventListener(PERMISSION_CHANGED_EVENT, handlePermissionChanged);
return () => {
window.removeEventListener(PERMISSION_CHANGED_EVENT, handlePermissionChanged);
};
}, [refreshPermissions]);
const roleSet = useMemo(() => new Set(roles), [roles]);
const permissionSet = useMemo(() => new Set(permissions), [permissions]);

View File

@ -7,6 +7,7 @@ import { login, getCodeImg } from '../../api/login';
import { TokenKey } from '../../utils/auth';
import type { LoginRequest } from '@/types/api';
import { notify } from '@/utils/notify';
import { clearPermissionCache } from '@/contexts/PermissionContext';
import './login.css';
interface LoginFormValues {
@ -86,6 +87,7 @@ const LoginPage = () => {
if (!tokenToSet) {
throw new Error('登录返回缺少 token');
}
clearPermissionCache();
Cookies.set(TokenKey, tokenToSet);
if (values.rememberMe) {
Cookies.set(REMEMBER_ME_KEY, 'true', { expires: 30 });

View File

@ -14,7 +14,7 @@ interface UserInfoProps {
const UserInfo = ({ user, onUpdated }: UserInfoProps) => {
const [form] = Form.useForm<UpdateUserProfilePayload>();
const navigate = useNavigate();
const { defaultRoutePath } = usePermission();
const { defaultRoutePath, refreshPermissions } = usePermission();
useEffect(() => {
form.setFieldsValue({
@ -38,6 +38,7 @@ const UserInfo = ({ user, onUpdated }: UserInfoProps) => {
await updateUserProfile(values);
notify.success('修改成功');
onUpdated(values);
await refreshPermissions();
} catch (error) {
console.error('Failed to update profile:', error);
}

View File

@ -29,6 +29,7 @@ import {
} from '@/api/system/role';
import PageBackButton from '@/components/PageBackButton';
import { parseTime } from '@/utils/ruoyi';
import { notifyPermissionChanged } from '@/contexts/PermissionContext';
import './system-admin.css';
interface UserRecord {
@ -182,6 +183,7 @@ const RoleAuthUserPage = () => {
userId: record.userId,
});
message.success('取消授权成功');
notifyPermissionChanged();
await refreshBoth();
} catch (error) {
console.error('Failed to cancel role auth:', error);
@ -204,6 +206,7 @@ const RoleAuthUserPage = () => {
});
message.success('批量取消授权成功');
setSelectedAllocatedKeys([]);
notifyPermissionChanged();
await refreshBoth();
} catch (error) {
console.error('Failed to cancel users in batch:', error);
@ -225,6 +228,7 @@ const RoleAuthUserPage = () => {
message.success('分配用户成功');
setSelectedUnallocatedKeys([]);
setActiveTab('allocated');
notifyPermissionChanged();
await refreshBoth();
} catch (error) {
console.error('Failed to assign users:', error);

View File

@ -17,6 +17,7 @@ import { parseTime } from '../../utils/ruoyi'; // Custom utility
import Permission from '@/components/Permission';
import ReadonlyAction from '@/components/Permission/ReadonlyAction';
import { useNavigate } from 'react-router-dom';
import { notifyPermissionChanged } from '@/contexts/PermissionContext';
import './system-admin.css';
const { RangePicker } = DatePicker;
@ -136,11 +137,40 @@ const RolePage: React.FC = () => {
return [];
};
const parseTreeLinkage = (value: unknown, fallback = true) => {
if (value === undefined || value === null || value === '') {
return fallback;
}
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'number') {
return value === 1;
}
if (typeof value === 'string') {
const normalized = value.trim().toLowerCase();
return normalized === 'true' || normalized === '1';
}
return Boolean(value);
};
const getTreeNodeKey = (node: any): React.Key | undefined => node?.id ?? node?.key;
const uniqueKeys = (keys: React.Key[]) => {
const keyMap = new Map<string, React.Key>();
keys.forEach((key) => {
if (key !== undefined && key !== null && String(key) !== '') {
keyMap.set(String(key), key);
}
});
return Array.from(keyMap.values());
};
const collectTreeKeys = (nodes: any[]): React.Key[] => {
const keys: React.Key[] = [];
const visit = (list: any[]) => {
list.forEach((node) => {
const key = node?.id ?? node?.key;
const key = getTreeNodeKey(node);
if (key !== undefined) {
keys.push(key);
}
@ -150,7 +180,67 @@ const RolePage: React.FC = () => {
});
};
visit(nodes ?? []);
return keys;
return uniqueKeys(keys);
};
const normalizeLinkedTreeSelection = (keys: React.Key[], nodes: any[]) => {
const checkedKeySet = new Set(uniqueKeys(keys).map((key) => String(key)));
const checkedKeys: React.Key[] = [];
const halfCheckedKeys: React.Key[] = [];
const hasCheckedDescendant = (node: any): boolean => {
const children = Array.isArray(node?.children) ? node.children : [];
return children.some((child: any) => {
const childKey = getTreeNodeKey(child);
return (
(childKey !== undefined && checkedKeySet.has(String(childKey))) ||
hasCheckedDescendant(child)
);
});
};
const allLeafDescendantsChecked = (node: any): boolean => {
const children = Array.isArray(node?.children) ? node.children : [];
if (children.length === 0) {
const key = getTreeNodeKey(node);
return key !== undefined && checkedKeySet.has(String(key));
}
return children.every((child: any) => allLeafDescendantsChecked(child));
};
const visit = (list: any[]) => {
list.forEach((node) => {
const key = getTreeNodeKey(node);
const children = Array.isArray(node?.children) ? node.children : [];
if (key !== undefined && checkedKeySet.has(String(key))) {
const isPartialParent = children.length > 0 && hasCheckedDescendant(node) && !allLeafDescendantsChecked(node);
if (isPartialParent) {
halfCheckedKeys.push(key);
} else {
checkedKeys.push(key);
}
}
if (children.length > 0) {
visit(children);
}
});
};
visit(nodes ?? []);
return {
checkedKeys: uniqueKeys(checkedKeys),
halfCheckedKeys: uniqueKeys(halfCheckedKeys),
};
};
const normalizeTreeSelection = (keys: React.Key[], nodes: any[], linkChildren: boolean) => {
if (!linkChildren) {
return {
checkedKeys: uniqueKeys(keys),
halfCheckedKeys: [],
};
}
return normalizeLinkedTreeSelection(keys, nodes);
};
const getList = useCallback(async () => {
@ -242,6 +332,7 @@ const RolePage: React.FC = () => {
try {
await changeRoleStatus(record.roleId, newStatus);
message.success(`${text}成功`);
notifyPermissionChanged();
getList();
} catch (error) {
message.error(`${text}失败`);
@ -301,11 +392,15 @@ const RolePage: React.FC = () => {
const roleResponse = await getRole(roleId);
const roleData = extractResponseData(roleResponse);
const nextMenuCheckStrictly = parseTreeLinkage(roleData.menuCheckStrictly, true);
const nextDeptCheckStrictly = parseTreeLinkage(roleData.deptCheckStrictly, true);
const nextMenuOptions = extractTreeNodes(roleMenuResponse, 'menus');
const nextMenuSelection = normalizeTreeSelection(extractCheckedKeys(roleMenuResponse), nextMenuOptions, nextMenuCheckStrictly);
setCurrentRole(roleData);
setMenuCheckStrictly(roleData.menuCheckStrictly === undefined ? true : !!roleData.menuCheckStrictly);
setDeptCheckStrictly(roleData.deptCheckStrictly === undefined ? true : !!roleData.deptCheckStrictly);
setMenuCheckedKeys(extractCheckedKeys(roleMenuResponse));
setMenuHalfCheckedKeys([]);
setMenuCheckStrictly(nextMenuCheckStrictly);
setDeptCheckStrictly(nextDeptCheckStrictly);
setMenuCheckedKeys(nextMenuSelection.checkedKeys);
setMenuHalfCheckedKeys(nextMenuSelection.halfCheckedKeys);
setAddEditModalTitle('修改角色');
setFormType('edit');
@ -335,6 +430,7 @@ const RolePage: React.FC = () => {
await delRole(roleIds.join(','));
message.success('删除成功');
setSelectedRowKeys([]);
notifyPermissionChanged();
getList();
} catch (error) {
message.error('删除失败');
@ -388,15 +484,19 @@ const RolePage: React.FC = () => {
const deptTreeResponse = await getDeptTreeselect(roleId); // Get dept tree with selected keys
const roleResponse = await getRole(roleId);
const roleData = extractResponseData(roleResponse);
const nextDeptCheckStrictly = parseTreeLinkage(roleData.deptCheckStrictly, true);
const nextDeptOptions = extractTreeNodes(deptTreeResponse, 'depts');
const nextDeptSelection = normalizeTreeSelection(extractCheckedKeys(deptTreeResponse), nextDeptOptions, nextDeptCheckStrictly);
setCurrentRole(roleData);
setDeptCheckStrictly(nextDeptCheckStrictly);
setDataScopeModalTitle('分配数据权限');
setDataScopeModalVisible(true);
dataScopeForm.setFieldsValue({
...roleData,
dataScope: roleData.dataScope?.toString(),
});
setDeptCheckedKeys(extractCheckedKeys(deptTreeResponse));
setDeptHalfCheckedKeys([]);
setDeptCheckedKeys(nextDeptSelection.checkedKeys);
setDeptHalfCheckedKeys(nextDeptSelection.halfCheckedKeys);
} catch (error) {
message.error('获取数据权限信息失败');
}
@ -408,7 +508,7 @@ const RolePage: React.FC = () => {
const submitRoleForm = async (values: any) => {
try {
const menuIds = [...menuCheckedKeys, ...menuHalfCheckedKeys];
const menuIds = uniqueKeys([...menuCheckedKeys, ...menuHalfCheckedKeys]);
const roleData = { ...currentRole, ...values, menuIds, menuCheckStrictly };
if (formType === 'edit') {
@ -419,6 +519,7 @@ const RolePage: React.FC = () => {
message.success('新增成功');
}
setAddEditModalVisible(false);
notifyPermissionChanged();
getList();
} catch (error) {
console.error('Submit role form failed:', error);
@ -430,13 +531,14 @@ const RolePage: React.FC = () => {
try {
let deptIds: React.Key[] = [];
if (values.dataScope === '2') {
deptIds = [...deptCheckedKeys, ...deptHalfCheckedKeys];
deptIds = uniqueKeys([...deptCheckedKeys, ...deptHalfCheckedKeys]);
}
const roleData = { ...currentRole, ...values, deptIds, deptCheckStrictly };
await dataScope(roleData);
message.success('分配数据权限成功');
setDataScopeModalVisible(false);
notifyPermissionChanged();
getList();
} catch (error) {
message.error('分配数据权限失败');
@ -477,27 +579,37 @@ const RolePage: React.FC = () => {
const handleMenuCheck = (checked: any, info: any) => {
if (Array.isArray(checked)) {
setMenuCheckedKeys(checked);
setMenuHalfCheckedKeys(info?.halfCheckedKeys ?? []);
setMenuCheckedKeys(uniqueKeys(checked));
setMenuHalfCheckedKeys(uniqueKeys(info?.halfCheckedKeys ?? []));
return;
}
setMenuCheckedKeys(checked?.checked ?? []);
setMenuHalfCheckedKeys(checked?.halfChecked ?? []);
setMenuCheckedKeys(uniqueKeys(checked?.checked ?? []));
setMenuHalfCheckedKeys(uniqueKeys(checked?.halfChecked ?? []));
};
const handleDeptCheck = (checked: any, info: any) => {
if (Array.isArray(checked)) {
setDeptCheckedKeys(checked);
setDeptHalfCheckedKeys(info?.halfCheckedKeys ?? []);
setDeptCheckedKeys(uniqueKeys(checked));
setDeptHalfCheckedKeys(uniqueKeys(info?.halfCheckedKeys ?? []));
return;
}
setDeptCheckedKeys(checked?.checked ?? []);
setDeptHalfCheckedKeys(checked?.halfChecked ?? []);
setDeptCheckedKeys(uniqueKeys(checked?.checked ?? []));
setDeptHalfCheckedKeys(uniqueKeys(checked?.halfChecked ?? []));
};
const handleCheckedTreeConnect = (value: boolean, type: 'menu' | 'dept') => {
if (type === 'menu') setMenuCheckStrictly(value);
if (type === 'dept') setDeptCheckStrictly(value);
if (type === 'menu') {
const nextSelection = normalizeTreeSelection([...menuCheckedKeys, ...menuHalfCheckedKeys], menuOptions, value);
setMenuCheckedKeys(nextSelection.checkedKeys);
setMenuHalfCheckedKeys(nextSelection.halfCheckedKeys);
setMenuCheckStrictly(value);
}
if (type === 'dept') {
const nextSelection = normalizeTreeSelection([...deptCheckedKeys, ...deptHalfCheckedKeys], deptOptions, value);
setDeptCheckedKeys(nextSelection.checkedKeys);
setDeptHalfCheckedKeys(nextSelection.halfCheckedKeys);
setDeptCheckStrictly(value);
}
};
const columns: TableColumnsType<any> = [

View File

@ -6,6 +6,7 @@ import { useParams } from 'react-router-dom';
import { getAuthRole, getUser, updateAuthRole } from '@/api/system/user';
import PageBackButton from '@/components/PageBackButton';
import { parseTime } from '@/utils/ruoyi';
import { notifyPermissionChanged } from '@/contexts/PermissionContext';
import './system-admin.css';
interface RoleRecord {
@ -94,6 +95,7 @@ const UserAuthRolePage = () => {
roleIds: selectedRoleIds.join(','),
});
message.success('分配角色成功');
notifyPermissionChanged();
await loadData();
} catch (error) {
console.error('Failed to save user roles:', error);

View File

@ -210,16 +210,56 @@ const normalizeFileList = (value: unknown): FileRecord[] =>
}))
: [];
const pickFirstValue = (source: Record<string, unknown>, keys: string[]) => {
for (const key of keys) {
const value = source[key];
if (value !== undefined && value !== null && String(value).trim() !== '') {
return value;
}
}
return undefined;
};
const extractRows = (value: unknown): unknown[] => {
if (Array.isArray(value)) {
return value;
}
if (!isObject(value)) {
return [];
}
const directArray = pickFirstValue(value, ['rows', 'records', 'list', 'projectList', 'projects']);
if (Array.isArray(directArray)) {
return directArray;
}
if (isObject(value.data)) {
return extractRows(value.data);
}
if (Array.isArray(value.data)) {
return value.data;
}
return [];
};
const normalizeProjectRows = (value: unknown): ProjectRow[] =>
Array.isArray(value)
? value
.filter((item) => isObject(item))
.map((item) => ({
.map((item) => {
const projectId = pickFirstValue(item, ['projectId', 'id', 'project_id', 'businessProjectId']);
const projectName = pickFirstValue(item, ['projectName', 'name', 'project_name', 'title']);
return {
...item,
projectId: item.projectId as string | number | undefined,
projectName: String(item.projectName ?? item.name ?? ''),
name: String(item.name ?? item.projectName ?? ''),
}))
projectId: projectId as string | number | undefined,
projectName: String(projectName ?? ''),
name: String(projectName ?? ''),
};
})
.filter((item) => item.projectId !== undefined && item.projectId !== null && String(item.projectId) !== '')
: [];
const PROJECT_BOUNDARY_KEYS: Array<keyof ProjectRow> = [
@ -287,7 +327,7 @@ const buildWorkLogPayload = (row: WorkLogRow): Record<string, unknown> => ({
fileList: normalizeFileList(row.fileList),
});
let inflightFallbackProjectListRequest: Promise<unknown> | null = null;
const inflightFallbackProjectListRequests = new Map<string, Promise<unknown>>();
const inflightWorklogUserProjectRequests = new Map<string, Promise<unknown>>();
const inflightCalendarRequests = new Map<string, Promise<unknown>>();
const inflightDayListRequests = new Map<string, Promise<unknown>>();
@ -307,15 +347,22 @@ const fetchUserProjectListRequest = async (userId: string) => {
return requestPromise;
};
const fetchFallbackProjectListRequest = async () => {
if (inflightFallbackProjectListRequest) {
return inflightFallbackProjectListRequest;
const fetchFallbackProjectListRequest = async (userId?: string) => {
const normalizedUserId = String(userId ?? '').trim();
const requestKey = normalizedUserId || '__current__';
const existingRequest = inflightFallbackProjectListRequests.get(requestKey);
if (existingRequest) {
return existingRequest;
}
const requestPromise = listProject({ pageNum: 1, pageSize: 10000 }).finally(() => {
inflightFallbackProjectListRequest = null;
const requestPromise = listProject({
pageNum: 1,
pageSize: 10000,
queryUserId: normalizedUserId || undefined,
}).finally(() => {
inflightFallbackProjectListRequests.delete(requestKey);
});
inflightFallbackProjectListRequest = requestPromise;
inflightFallbackProjectListRequests.set(requestKey, requestPromise);
return requestPromise;
};
@ -417,9 +464,9 @@ const WorkLogPage = () => {
}, [currentUser.userId, queryUserId]);
const disableTable = isReadOnlyByQuery || dayReadonly;
const canAddWorkLog = hasPermi('worklog/worklog:list');
const canEditWorkLog = hasPermi('worklog/worklog:list');
const canDeleteWorkLog = hasPermi('worklog/worklog:list');
const canAddWorkLog = hasPermi(['business:work:hour:add', 'business:work:hour:list', 'worklog/worklog:list']);
const canEditWorkLog = hasPermi(['business:work:hour:edit', 'business:work:hour:update', 'business:work:hour:list', 'worklog/worklog:list']);
const canDeleteWorkLog = hasPermi(['business:work:hour:remove', 'business:work:hour:list', 'worklog/worklog:list']);
const showDetailBack = useMemo(() => Boolean(queryUserId || queryProjectId), [queryProjectId, queryUserId]);
const displayUserName = queryNickName || currentUser.nickName || currentUser.userName || '当日日志';
const detailFallbackPath = useMemo(() => {
@ -533,21 +580,17 @@ const WorkLogPage = () => {
return;
}
const payload = normalizeResponseData(response);
const rows = isObject(payload) && Array.isArray(payload.rows) ? payload.rows : Array.isArray(payload) ? payload : [];
const rows = extractRows(payload);
const normalizedRows = normalizeProjectRows(rows);
if (!normalizedRows.some(hasProjectBoundaryInfo)) {
try {
const detailResponse = await fetchFallbackProjectListRequest();
const detailResponse = await fetchFallbackProjectListRequest(effectiveUserId);
if (requestId !== projectListRequestIdRef.current) {
return;
}
const detailPayload = normalizeResponseData(detailResponse);
const detailRows = isObject(detailPayload) && Array.isArray(detailPayload.rows)
? detailPayload.rows
: Array.isArray(detailPayload)
? detailPayload
: [];
const detailRows = extractRows(detailPayload);
setProjectList(mergeProjectRowsWithDetails(normalizedRows, normalizeProjectRows(detailRows)));
return;
} catch (detailError) {
@ -562,12 +605,12 @@ const WorkLogPage = () => {
}
try {
const response = await fetchFallbackProjectListRequest();
const response = await fetchFallbackProjectListRequest(effectiveUserId);
if (requestId !== projectListRequestIdRef.current) {
return;
}
const payload = normalizeResponseData(response);
const rows = isObject(payload) && Array.isArray(payload.rows) ? payload.rows : Array.isArray(payload) ? payload : [];
const rows = extractRows(payload);
setProjectList(normalizeProjectRows(rows));
} catch (error) {
if (requestId !== projectListRequestIdRef.current) {
@ -741,7 +784,7 @@ const WorkLogPage = () => {
}, [fetchDayList]);
const projectListFilter = useMemo(() => {
return projectList.filter((item) => {
const matchedProjects = projectList.filter((item) => {
const start = pickProjectBoundaryDate(item, [
'startDate',
'startTime',
@ -769,6 +812,7 @@ const WorkLogPage = () => {
}
return true;
});
return matchedProjects.length > 0 ? matchedProjects : projectList;
}, [projectList, selectedDayDate]);
const totalWorkTime = useMemo(() => {