前端功能遗漏修复
parent
b13a2bfc87
commit
6e897552d0
|
|
@ -8,7 +8,7 @@ import type { MenuProps } from 'antd/es/menu';
|
||||||
import { DownOutlined, LogoutOutlined, MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined } from '@ant-design/icons';
|
import { DownOutlined, LogoutOutlined, MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined } from '@ant-design/icons';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { removeToken } from '../../utils/auth';
|
import { removeToken } from '../../utils/auth';
|
||||||
import { usePermission } from '@/contexts/PermissionContext';
|
import { clearPermissionCache, usePermission } from '@/contexts/PermissionContext';
|
||||||
import './navbar.css';
|
import './navbar.css';
|
||||||
|
|
||||||
interface AppNavbarProps {
|
interface AppNavbarProps {
|
||||||
|
|
@ -193,7 +193,8 @@ const isPinnedTab = (tab: OpenPageTab) => tab.pathname === '/index';
|
||||||
const AppNavbar: React.FC<AppNavbarProps> = ({ collapsed, onToggle }) => {
|
const AppNavbar: React.FC<AppNavbarProps> = ({ collapsed, onToggle }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { userName, canAccessPath } = usePermission();
|
const { userName, currentUser, canAccessPath } = usePermission();
|
||||||
|
const displayName = String(currentUser.nickName || userName || '用户');
|
||||||
const activeTab = React.useMemo(
|
const activeTab = React.useMemo(
|
||||||
() => createOpenPageTab(location.pathname, location.search),
|
() => createOpenPageTab(location.pathname, location.search),
|
||||||
[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 [openTabs, setOpenTabs] = React.useState<OpenPageTab[]>(() => getStoredOpenPageTabs(canAccessPath));
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
|
clearPermissionCache();
|
||||||
removeToken();
|
removeToken();
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
};
|
};
|
||||||
|
|
@ -331,7 +333,7 @@ const AppNavbar: React.FC<AppNavbarProps> = ({ collapsed, onToggle }) => {
|
||||||
<Button type="text" className="app-navbar-user">
|
<Button type="text" className="app-navbar-user">
|
||||||
<Space size={10}>
|
<Space size={10}>
|
||||||
<Avatar size={36} className="app-navbar-avatar" icon={<UserOutlined />} />
|
<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" />
|
<DownOutlined className="app-navbar-chevron" />
|
||||||
</Space>
|
</Space>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ const SUPER_PERMI = '*:*:*';
|
||||||
const ADMIN_ROLE = 'admin';
|
const ADMIN_ROLE = 'admin';
|
||||||
const PERMISSION_CACHE_KEY = 'pms_permission_cache_v1';
|
const PERMISSION_CACHE_KEY = 'pms_permission_cache_v1';
|
||||||
const PERMISSION_CACHE_TTL_MS = 5 * 60 * 1000;
|
const PERMISSION_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||||
|
const PERMISSION_CHANGED_EVENT = 'pms:permission-changed';
|
||||||
|
|
||||||
interface PermissionCacheState {
|
interface PermissionCacheState {
|
||||||
tokenFingerprint: string;
|
tokenFingerprint: string;
|
||||||
|
|
@ -162,13 +163,21 @@ const writePermissionCache = (state: Omit<PermissionCacheState, 'tokenFingerprin
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearPermissionCache = () => {
|
export const clearPermissionCache = () => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.sessionStorage.removeItem(PERMISSION_CACHE_KEY);
|
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 normalizePath = (rawPath: string) => {
|
||||||
const path = rawPath.split('?')[0]?.split('#')[0] ?? '';
|
const path = rawPath.split('?')[0]?.split('#')[0] ?? '';
|
||||||
if (!path) {
|
if (!path) {
|
||||||
|
|
@ -453,6 +462,16 @@ export const PermissionProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
||||||
void refreshPermissions();
|
void refreshPermissions();
|
||||||
}, [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 roleSet = useMemo(() => new Set(roles), [roles]);
|
||||||
const permissionSet = useMemo(() => new Set(permissions), [permissions]);
|
const permissionSet = useMemo(() => new Set(permissions), [permissions]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { login, getCodeImg } from '../../api/login';
|
||||||
import { TokenKey } from '../../utils/auth';
|
import { TokenKey } from '../../utils/auth';
|
||||||
import type { LoginRequest } from '@/types/api';
|
import type { LoginRequest } from '@/types/api';
|
||||||
import { notify } from '@/utils/notify';
|
import { notify } from '@/utils/notify';
|
||||||
|
import { clearPermissionCache } from '@/contexts/PermissionContext';
|
||||||
import './login.css';
|
import './login.css';
|
||||||
|
|
||||||
interface LoginFormValues {
|
interface LoginFormValues {
|
||||||
|
|
@ -86,6 +87,7 @@ const LoginPage = () => {
|
||||||
if (!tokenToSet) {
|
if (!tokenToSet) {
|
||||||
throw new Error('登录返回缺少 token');
|
throw new Error('登录返回缺少 token');
|
||||||
}
|
}
|
||||||
|
clearPermissionCache();
|
||||||
Cookies.set(TokenKey, tokenToSet);
|
Cookies.set(TokenKey, tokenToSet);
|
||||||
if (values.rememberMe) {
|
if (values.rememberMe) {
|
||||||
Cookies.set(REMEMBER_ME_KEY, 'true', { expires: 30 });
|
Cookies.set(REMEMBER_ME_KEY, 'true', { expires: 30 });
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ interface UserInfoProps {
|
||||||
const UserInfo = ({ user, onUpdated }: UserInfoProps) => {
|
const UserInfo = ({ user, onUpdated }: UserInfoProps) => {
|
||||||
const [form] = Form.useForm<UpdateUserProfilePayload>();
|
const [form] = Form.useForm<UpdateUserProfilePayload>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { defaultRoutePath } = usePermission();
|
const { defaultRoutePath, refreshPermissions } = usePermission();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
|
|
@ -38,6 +38,7 @@ const UserInfo = ({ user, onUpdated }: UserInfoProps) => {
|
||||||
await updateUserProfile(values);
|
await updateUserProfile(values);
|
||||||
notify.success('修改成功');
|
notify.success('修改成功');
|
||||||
onUpdated(values);
|
onUpdated(values);
|
||||||
|
await refreshPermissions();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update profile:', error);
|
console.error('Failed to update profile:', error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import {
|
||||||
} from '@/api/system/role';
|
} from '@/api/system/role';
|
||||||
import PageBackButton from '@/components/PageBackButton';
|
import PageBackButton from '@/components/PageBackButton';
|
||||||
import { parseTime } from '@/utils/ruoyi';
|
import { parseTime } from '@/utils/ruoyi';
|
||||||
|
import { notifyPermissionChanged } from '@/contexts/PermissionContext';
|
||||||
import './system-admin.css';
|
import './system-admin.css';
|
||||||
|
|
||||||
interface UserRecord {
|
interface UserRecord {
|
||||||
|
|
@ -182,6 +183,7 @@ const RoleAuthUserPage = () => {
|
||||||
userId: record.userId,
|
userId: record.userId,
|
||||||
});
|
});
|
||||||
message.success('取消授权成功');
|
message.success('取消授权成功');
|
||||||
|
notifyPermissionChanged();
|
||||||
await refreshBoth();
|
await refreshBoth();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to cancel role auth:', error);
|
console.error('Failed to cancel role auth:', error);
|
||||||
|
|
@ -204,6 +206,7 @@ const RoleAuthUserPage = () => {
|
||||||
});
|
});
|
||||||
message.success('批量取消授权成功');
|
message.success('批量取消授权成功');
|
||||||
setSelectedAllocatedKeys([]);
|
setSelectedAllocatedKeys([]);
|
||||||
|
notifyPermissionChanged();
|
||||||
await refreshBoth();
|
await refreshBoth();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to cancel users in batch:', error);
|
console.error('Failed to cancel users in batch:', error);
|
||||||
|
|
@ -225,6 +228,7 @@ const RoleAuthUserPage = () => {
|
||||||
message.success('分配用户成功');
|
message.success('分配用户成功');
|
||||||
setSelectedUnallocatedKeys([]);
|
setSelectedUnallocatedKeys([]);
|
||||||
setActiveTab('allocated');
|
setActiveTab('allocated');
|
||||||
|
notifyPermissionChanged();
|
||||||
await refreshBoth();
|
await refreshBoth();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to assign users:', error);
|
console.error('Failed to assign users:', error);
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { parseTime } from '../../utils/ruoyi'; // Custom utility
|
||||||
import Permission from '@/components/Permission';
|
import Permission from '@/components/Permission';
|
||||||
import ReadonlyAction from '@/components/Permission/ReadonlyAction';
|
import ReadonlyAction from '@/components/Permission/ReadonlyAction';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { notifyPermissionChanged } from '@/contexts/PermissionContext';
|
||||||
import './system-admin.css';
|
import './system-admin.css';
|
||||||
|
|
||||||
const { RangePicker } = DatePicker;
|
const { RangePicker } = DatePicker;
|
||||||
|
|
@ -136,11 +137,40 @@ const RolePage: React.FC = () => {
|
||||||
return [];
|
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 collectTreeKeys = (nodes: any[]): React.Key[] => {
|
||||||
const keys: React.Key[] = [];
|
const keys: React.Key[] = [];
|
||||||
const visit = (list: any[]) => {
|
const visit = (list: any[]) => {
|
||||||
list.forEach((node) => {
|
list.forEach((node) => {
|
||||||
const key = node?.id ?? node?.key;
|
const key = getTreeNodeKey(node);
|
||||||
if (key !== undefined) {
|
if (key !== undefined) {
|
||||||
keys.push(key);
|
keys.push(key);
|
||||||
}
|
}
|
||||||
|
|
@ -150,7 +180,67 @@ const RolePage: React.FC = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
visit(nodes ?? []);
|
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 () => {
|
const getList = useCallback(async () => {
|
||||||
|
|
@ -242,6 +332,7 @@ const RolePage: React.FC = () => {
|
||||||
try {
|
try {
|
||||||
await changeRoleStatus(record.roleId, newStatus);
|
await changeRoleStatus(record.roleId, newStatus);
|
||||||
message.success(`${text}成功`);
|
message.success(`${text}成功`);
|
||||||
|
notifyPermissionChanged();
|
||||||
getList();
|
getList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(`${text}失败`);
|
message.error(`${text}失败`);
|
||||||
|
|
@ -301,11 +392,15 @@ const RolePage: React.FC = () => {
|
||||||
const roleResponse = await getRole(roleId);
|
const roleResponse = await getRole(roleId);
|
||||||
|
|
||||||
const roleData = extractResponseData(roleResponse);
|
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);
|
setCurrentRole(roleData);
|
||||||
setMenuCheckStrictly(roleData.menuCheckStrictly === undefined ? true : !!roleData.menuCheckStrictly);
|
setMenuCheckStrictly(nextMenuCheckStrictly);
|
||||||
setDeptCheckStrictly(roleData.deptCheckStrictly === undefined ? true : !!roleData.deptCheckStrictly);
|
setDeptCheckStrictly(nextDeptCheckStrictly);
|
||||||
setMenuCheckedKeys(extractCheckedKeys(roleMenuResponse));
|
setMenuCheckedKeys(nextMenuSelection.checkedKeys);
|
||||||
setMenuHalfCheckedKeys([]);
|
setMenuHalfCheckedKeys(nextMenuSelection.halfCheckedKeys);
|
||||||
setAddEditModalTitle('修改角色');
|
setAddEditModalTitle('修改角色');
|
||||||
setFormType('edit');
|
setFormType('edit');
|
||||||
|
|
||||||
|
|
@ -335,6 +430,7 @@ const RolePage: React.FC = () => {
|
||||||
await delRole(roleIds.join(','));
|
await delRole(roleIds.join(','));
|
||||||
message.success('删除成功');
|
message.success('删除成功');
|
||||||
setSelectedRowKeys([]);
|
setSelectedRowKeys([]);
|
||||||
|
notifyPermissionChanged();
|
||||||
getList();
|
getList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('删除失败');
|
message.error('删除失败');
|
||||||
|
|
@ -388,15 +484,19 @@ const RolePage: React.FC = () => {
|
||||||
const deptTreeResponse = await getDeptTreeselect(roleId); // Get dept tree with selected keys
|
const deptTreeResponse = await getDeptTreeselect(roleId); // Get dept tree with selected keys
|
||||||
const roleResponse = await getRole(roleId);
|
const roleResponse = await getRole(roleId);
|
||||||
const roleData = extractResponseData(roleResponse);
|
const roleData = extractResponseData(roleResponse);
|
||||||
|
const nextDeptCheckStrictly = parseTreeLinkage(roleData.deptCheckStrictly, true);
|
||||||
|
const nextDeptOptions = extractTreeNodes(deptTreeResponse, 'depts');
|
||||||
|
const nextDeptSelection = normalizeTreeSelection(extractCheckedKeys(deptTreeResponse), nextDeptOptions, nextDeptCheckStrictly);
|
||||||
setCurrentRole(roleData);
|
setCurrentRole(roleData);
|
||||||
|
setDeptCheckStrictly(nextDeptCheckStrictly);
|
||||||
setDataScopeModalTitle('分配数据权限');
|
setDataScopeModalTitle('分配数据权限');
|
||||||
setDataScopeModalVisible(true);
|
setDataScopeModalVisible(true);
|
||||||
dataScopeForm.setFieldsValue({
|
dataScopeForm.setFieldsValue({
|
||||||
...roleData,
|
...roleData,
|
||||||
dataScope: roleData.dataScope?.toString(),
|
dataScope: roleData.dataScope?.toString(),
|
||||||
});
|
});
|
||||||
setDeptCheckedKeys(extractCheckedKeys(deptTreeResponse));
|
setDeptCheckedKeys(nextDeptSelection.checkedKeys);
|
||||||
setDeptHalfCheckedKeys([]);
|
setDeptHalfCheckedKeys(nextDeptSelection.halfCheckedKeys);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('获取数据权限信息失败');
|
message.error('获取数据权限信息失败');
|
||||||
}
|
}
|
||||||
|
|
@ -408,7 +508,7 @@ const RolePage: React.FC = () => {
|
||||||
|
|
||||||
const submitRoleForm = async (values: any) => {
|
const submitRoleForm = async (values: any) => {
|
||||||
try {
|
try {
|
||||||
const menuIds = [...menuCheckedKeys, ...menuHalfCheckedKeys];
|
const menuIds = uniqueKeys([...menuCheckedKeys, ...menuHalfCheckedKeys]);
|
||||||
const roleData = { ...currentRole, ...values, menuIds, menuCheckStrictly };
|
const roleData = { ...currentRole, ...values, menuIds, menuCheckStrictly };
|
||||||
|
|
||||||
if (formType === 'edit') {
|
if (formType === 'edit') {
|
||||||
|
|
@ -419,6 +519,7 @@ const RolePage: React.FC = () => {
|
||||||
message.success('新增成功');
|
message.success('新增成功');
|
||||||
}
|
}
|
||||||
setAddEditModalVisible(false);
|
setAddEditModalVisible(false);
|
||||||
|
notifyPermissionChanged();
|
||||||
getList();
|
getList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Submit role form failed:', error);
|
console.error('Submit role form failed:', error);
|
||||||
|
|
@ -430,13 +531,14 @@ const RolePage: React.FC = () => {
|
||||||
try {
|
try {
|
||||||
let deptIds: React.Key[] = [];
|
let deptIds: React.Key[] = [];
|
||||||
if (values.dataScope === '2') {
|
if (values.dataScope === '2') {
|
||||||
deptIds = [...deptCheckedKeys, ...deptHalfCheckedKeys];
|
deptIds = uniqueKeys([...deptCheckedKeys, ...deptHalfCheckedKeys]);
|
||||||
}
|
}
|
||||||
const roleData = { ...currentRole, ...values, deptIds, deptCheckStrictly };
|
const roleData = { ...currentRole, ...values, deptIds, deptCheckStrictly };
|
||||||
|
|
||||||
await dataScope(roleData);
|
await dataScope(roleData);
|
||||||
message.success('分配数据权限成功');
|
message.success('分配数据权限成功');
|
||||||
setDataScopeModalVisible(false);
|
setDataScopeModalVisible(false);
|
||||||
|
notifyPermissionChanged();
|
||||||
getList();
|
getList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('分配数据权限失败');
|
message.error('分配数据权限失败');
|
||||||
|
|
@ -477,27 +579,37 @@ const RolePage: React.FC = () => {
|
||||||
|
|
||||||
const handleMenuCheck = (checked: any, info: any) => {
|
const handleMenuCheck = (checked: any, info: any) => {
|
||||||
if (Array.isArray(checked)) {
|
if (Array.isArray(checked)) {
|
||||||
setMenuCheckedKeys(checked);
|
setMenuCheckedKeys(uniqueKeys(checked));
|
||||||
setMenuHalfCheckedKeys(info?.halfCheckedKeys ?? []);
|
setMenuHalfCheckedKeys(uniqueKeys(info?.halfCheckedKeys ?? []));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setMenuCheckedKeys(checked?.checked ?? []);
|
setMenuCheckedKeys(uniqueKeys(checked?.checked ?? []));
|
||||||
setMenuHalfCheckedKeys(checked?.halfChecked ?? []);
|
setMenuHalfCheckedKeys(uniqueKeys(checked?.halfChecked ?? []));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeptCheck = (checked: any, info: any) => {
|
const handleDeptCheck = (checked: any, info: any) => {
|
||||||
if (Array.isArray(checked)) {
|
if (Array.isArray(checked)) {
|
||||||
setDeptCheckedKeys(checked);
|
setDeptCheckedKeys(uniqueKeys(checked));
|
||||||
setDeptHalfCheckedKeys(info?.halfCheckedKeys ?? []);
|
setDeptHalfCheckedKeys(uniqueKeys(info?.halfCheckedKeys ?? []));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setDeptCheckedKeys(checked?.checked ?? []);
|
setDeptCheckedKeys(uniqueKeys(checked?.checked ?? []));
|
||||||
setDeptHalfCheckedKeys(checked?.halfChecked ?? []);
|
setDeptHalfCheckedKeys(uniqueKeys(checked?.halfChecked ?? []));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCheckedTreeConnect = (value: boolean, type: 'menu' | 'dept') => {
|
const handleCheckedTreeConnect = (value: boolean, type: 'menu' | 'dept') => {
|
||||||
if (type === 'menu') setMenuCheckStrictly(value);
|
if (type === 'menu') {
|
||||||
if (type === 'dept') setDeptCheckStrictly(value);
|
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> = [
|
const columns: TableColumnsType<any> = [
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { useParams } from 'react-router-dom';
|
||||||
import { getAuthRole, getUser, updateAuthRole } from '@/api/system/user';
|
import { getAuthRole, getUser, updateAuthRole } from '@/api/system/user';
|
||||||
import PageBackButton from '@/components/PageBackButton';
|
import PageBackButton from '@/components/PageBackButton';
|
||||||
import { parseTime } from '@/utils/ruoyi';
|
import { parseTime } from '@/utils/ruoyi';
|
||||||
|
import { notifyPermissionChanged } from '@/contexts/PermissionContext';
|
||||||
import './system-admin.css';
|
import './system-admin.css';
|
||||||
|
|
||||||
interface RoleRecord {
|
interface RoleRecord {
|
||||||
|
|
@ -94,6 +95,7 @@ const UserAuthRolePage = () => {
|
||||||
roleIds: selectedRoleIds.join(','),
|
roleIds: selectedRoleIds.join(','),
|
||||||
});
|
});
|
||||||
message.success('分配角色成功');
|
message.success('分配角色成功');
|
||||||
|
notifyPermissionChanged();
|
||||||
await loadData();
|
await loadData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save user roles:', error);
|
console.error('Failed to save user roles:', error);
|
||||||
|
|
|
||||||
|
|
@ -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[] =>
|
const normalizeProjectRows = (value: unknown): ProjectRow[] =>
|
||||||
Array.isArray(value)
|
Array.isArray(value)
|
||||||
? value
|
? value
|
||||||
.filter((item) => isObject(item))
|
.filter((item) => isObject(item))
|
||||||
.map((item) => ({
|
.map((item) => {
|
||||||
...item,
|
const projectId = pickFirstValue(item, ['projectId', 'id', 'project_id', 'businessProjectId']);
|
||||||
projectId: item.projectId as string | number | undefined,
|
const projectName = pickFirstValue(item, ['projectName', 'name', 'project_name', 'title']);
|
||||||
projectName: String(item.projectName ?? item.name ?? ''),
|
return {
|
||||||
name: String(item.name ?? item.projectName ?? ''),
|
...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<keyof ProjectRow> = [
|
const PROJECT_BOUNDARY_KEYS: Array<keyof ProjectRow> = [
|
||||||
|
|
@ -287,7 +327,7 @@ const buildWorkLogPayload = (row: WorkLogRow): Record<string, unknown> => ({
|
||||||
fileList: normalizeFileList(row.fileList),
|
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 inflightWorklogUserProjectRequests = new Map<string, Promise<unknown>>();
|
||||||
const inflightCalendarRequests = new Map<string, Promise<unknown>>();
|
const inflightCalendarRequests = new Map<string, Promise<unknown>>();
|
||||||
const inflightDayListRequests = new Map<string, Promise<unknown>>();
|
const inflightDayListRequests = new Map<string, Promise<unknown>>();
|
||||||
|
|
@ -307,15 +347,22 @@ const fetchUserProjectListRequest = async (userId: string) => {
|
||||||
return requestPromise;
|
return requestPromise;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchFallbackProjectListRequest = async () => {
|
const fetchFallbackProjectListRequest = async (userId?: string) => {
|
||||||
if (inflightFallbackProjectListRequest) {
|
const normalizedUserId = String(userId ?? '').trim();
|
||||||
return inflightFallbackProjectListRequest;
|
const requestKey = normalizedUserId || '__current__';
|
||||||
|
const existingRequest = inflightFallbackProjectListRequests.get(requestKey);
|
||||||
|
if (existingRequest) {
|
||||||
|
return existingRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestPromise = listProject({ pageNum: 1, pageSize: 10000 }).finally(() => {
|
const requestPromise = listProject({
|
||||||
inflightFallbackProjectListRequest = null;
|
pageNum: 1,
|
||||||
|
pageSize: 10000,
|
||||||
|
queryUserId: normalizedUserId || undefined,
|
||||||
|
}).finally(() => {
|
||||||
|
inflightFallbackProjectListRequests.delete(requestKey);
|
||||||
});
|
});
|
||||||
inflightFallbackProjectListRequest = requestPromise;
|
inflightFallbackProjectListRequests.set(requestKey, requestPromise);
|
||||||
return requestPromise;
|
return requestPromise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -417,9 +464,9 @@ const WorkLogPage = () => {
|
||||||
}, [currentUser.userId, queryUserId]);
|
}, [currentUser.userId, queryUserId]);
|
||||||
|
|
||||||
const disableTable = isReadOnlyByQuery || dayReadonly;
|
const disableTable = isReadOnlyByQuery || dayReadonly;
|
||||||
const canAddWorkLog = hasPermi('worklog/worklog:list');
|
const canAddWorkLog = hasPermi(['business:work:hour:add', 'business:work:hour:list', 'worklog/worklog:list']);
|
||||||
const canEditWorkLog = hasPermi('worklog/worklog:list');
|
const canEditWorkLog = hasPermi(['business:work:hour:edit', 'business:work:hour:update', 'business:work:hour:list', 'worklog/worklog:list']);
|
||||||
const canDeleteWorkLog = hasPermi('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 showDetailBack = useMemo(() => Boolean(queryUserId || queryProjectId), [queryProjectId, queryUserId]);
|
||||||
const displayUserName = queryNickName || currentUser.nickName || currentUser.userName || '当日日志';
|
const displayUserName = queryNickName || currentUser.nickName || currentUser.userName || '当日日志';
|
||||||
const detailFallbackPath = useMemo(() => {
|
const detailFallbackPath = useMemo(() => {
|
||||||
|
|
@ -533,21 +580,17 @@ const WorkLogPage = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const payload = normalizeResponseData(response);
|
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);
|
const normalizedRows = normalizeProjectRows(rows);
|
||||||
|
|
||||||
if (!normalizedRows.some(hasProjectBoundaryInfo)) {
|
if (!normalizedRows.some(hasProjectBoundaryInfo)) {
|
||||||
try {
|
try {
|
||||||
const detailResponse = await fetchFallbackProjectListRequest();
|
const detailResponse = await fetchFallbackProjectListRequest(effectiveUserId);
|
||||||
if (requestId !== projectListRequestIdRef.current) {
|
if (requestId !== projectListRequestIdRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const detailPayload = normalizeResponseData(detailResponse);
|
const detailPayload = normalizeResponseData(detailResponse);
|
||||||
const detailRows = isObject(detailPayload) && Array.isArray(detailPayload.rows)
|
const detailRows = extractRows(detailPayload);
|
||||||
? detailPayload.rows
|
|
||||||
: Array.isArray(detailPayload)
|
|
||||||
? detailPayload
|
|
||||||
: [];
|
|
||||||
setProjectList(mergeProjectRowsWithDetails(normalizedRows, normalizeProjectRows(detailRows)));
|
setProjectList(mergeProjectRowsWithDetails(normalizedRows, normalizeProjectRows(detailRows)));
|
||||||
return;
|
return;
|
||||||
} catch (detailError) {
|
} catch (detailError) {
|
||||||
|
|
@ -562,12 +605,12 @@ const WorkLogPage = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetchFallbackProjectListRequest();
|
const response = await fetchFallbackProjectListRequest(effectiveUserId);
|
||||||
if (requestId !== projectListRequestIdRef.current) {
|
if (requestId !== projectListRequestIdRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const payload = normalizeResponseData(response);
|
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));
|
setProjectList(normalizeProjectRows(rows));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (requestId !== projectListRequestIdRef.current) {
|
if (requestId !== projectListRequestIdRef.current) {
|
||||||
|
|
@ -741,7 +784,7 @@ const WorkLogPage = () => {
|
||||||
}, [fetchDayList]);
|
}, [fetchDayList]);
|
||||||
|
|
||||||
const projectListFilter = useMemo(() => {
|
const projectListFilter = useMemo(() => {
|
||||||
return projectList.filter((item) => {
|
const matchedProjects = projectList.filter((item) => {
|
||||||
const start = pickProjectBoundaryDate(item, [
|
const start = pickProjectBoundaryDate(item, [
|
||||||
'startDate',
|
'startDate',
|
||||||
'startTime',
|
'startTime',
|
||||||
|
|
@ -769,6 +812,7 @@ const WorkLogPage = () => {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
return matchedProjects.length > 0 ? matchedProjects : projectList;
|
||||||
}, [projectList, selectedDayDate]);
|
}, [projectList, selectedDayDate]);
|
||||||
|
|
||||||
const totalWorkTime = useMemo(() => {
|
const totalWorkTime = useMemo(() => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue