diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index c8641b8..3586290 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -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 = ({ 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 = ({ collapsed, onToggle }) => { const [openTabs, setOpenTabs] = React.useState(() => getStoredOpenPageTabs(canAccessPath)); const handleLogout = () => { + clearPermissionCache(); removeToken(); navigate('/login'); }; @@ -331,7 +333,7 @@ const AppNavbar: React.FC = ({ collapsed, onToggle }) => { diff --git a/src/contexts/PermissionContext.tsx b/src/contexts/PermissionContext.tsx index 3c7bad4..8c3decb 100644 --- a/src/contexts/PermissionContext.tsx +++ b/src/contexts/PermissionContext.tsx @@ -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 { +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]); diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index a79c8ee..04e1055 100644 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -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 }); diff --git a/src/pages/Profile/UserInfo.tsx b/src/pages/Profile/UserInfo.tsx index 1feffb5..ce93e2c 100644 --- a/src/pages/Profile/UserInfo.tsx +++ b/src/pages/Profile/UserInfo.tsx @@ -14,7 +14,7 @@ interface UserInfoProps { const UserInfo = ({ user, onUpdated }: UserInfoProps) => { const [form] = Form.useForm(); 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); } diff --git a/src/pages/system/RoleAuthUserPage.tsx b/src/pages/system/RoleAuthUserPage.tsx index 3f3bdfa..287fa9e 100644 --- a/src/pages/system/RoleAuthUserPage.tsx +++ b/src/pages/system/RoleAuthUserPage.tsx @@ -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); diff --git a/src/pages/system/RolePage.tsx b/src/pages/system/RolePage.tsx index 3f65a93..da22439 100644 --- a/src/pages/system/RolePage.tsx +++ b/src/pages/system/RolePage.tsx @@ -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(); + 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 = [ diff --git a/src/pages/system/UserAuthRolePage.tsx b/src/pages/system/UserAuthRolePage.tsx index 79dcb07..4190252 100644 --- a/src/pages/system/UserAuthRolePage.tsx +++ b/src/pages/system/UserAuthRolePage.tsx @@ -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); diff --git a/src/pages/worklog/WorkLogPage.tsx b/src/pages/worklog/WorkLogPage.tsx index f567f82..5c28bb3 100644 --- a/src/pages/worklog/WorkLogPage.tsx +++ b/src/pages/worklog/WorkLogPage.tsx @@ -210,16 +210,56 @@ const normalizeFileList = (value: unknown): FileRecord[] => })) : []; +const pickFirstValue = (source: Record, 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) => ({ - ...item, - projectId: item.projectId as string | number | undefined, - projectName: String(item.projectName ?? item.name ?? ''), - name: String(item.name ?? item.projectName ?? ''), - })) + .map((item) => { + const projectId = pickFirstValue(item, ['projectId', 'id', 'project_id', 'businessProjectId']); + const projectName = pickFirstValue(item, ['projectName', 'name', 'project_name', 'title']); + return { + ...item, + 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 = [ @@ -287,7 +327,7 @@ const buildWorkLogPayload = (row: WorkLogRow): Record => ({ fileList: normalizeFileList(row.fileList), }); -let inflightFallbackProjectListRequest: Promise | null = null; +const inflightFallbackProjectListRequests = new Map>(); const inflightWorklogUserProjectRequests = new Map>(); const inflightCalendarRequests = new Map>(); const inflightDayListRequests = new Map>(); @@ -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(() => {