feat: add permission-based access control and improve alert table UI

- Implement PermissionGuard component for route-level permission enforcement
- Add tree-based permission checkbox group in role management popup
- Gate SSE connection, unread count fetch, and rule edit actions on user permissions
- Add skip401Refresh option and auto token refresh on 401 response
- Improve alert table with fixed columns, explicit widths, and text ellipsis
- Add i18n entries for permission guide and rule menu descriptions (en/zh)
- Add expo-api nginx proxy configuration
- Add .claude to .gitignore

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 18:04:07 +08:00
parent 710da86521
commit 43d6e9207a
20 changed files with 742 additions and 48 deletions

View File

@@ -1,19 +1,31 @@
import {
ModalForm,
ProForm,
ProFormCheckbox,
ProFormDependency,
// ProFormDigit,
ProFormText,
} from '@ant-design/pro-components';
// import { useAccess } from '@umijs/max';
import { ColorPicker, Form, Space } from 'antd';
import { useMemo } from 'react';
import MyTag from '@/components/MyTag';
import useUserInfo from '@/hooks/useUserInfo';
import { getRoleDetail } from '@/services/api';
import { $t } from '@/utils/i18n';
// import { preventScientificNotation } from '@/utils';
import useCardList from './cardList';
import PermissionCheckboxGroup from './components/PermissionCheckboxGroup';
function flattenAllIds(nodes: API.MenuListItem[]): (string | number)[] {
const ids: (string | number)[] = [];
const walk = (items: API.MenuListItem[]) => {
items.forEach((item) => {
ids.push(item.id);
if (item.children?.length) walk(item.children);
});
};
walk(nodes);
return ids;
}
type RolesListItem = Partial<API.RolesListItem>;
export default (props: {
@@ -22,8 +34,16 @@ export default (props: {
formValues: Partial<RolesListItem | undefined>;
onSubmit: (values: RolesListItem) => void;
onDone: () => void;
menuTree?: API.MenuListItem[];
}) => {
const { visible, formValues, onSubmit, onDone, action } = props;
const {
visible,
formValues,
onSubmit,
onDone,
action,
menuTree: propMenuTree,
} = props;
const { userInfo } = useUserInfo();
// const { isGlobalCompany } = useAccess();
@@ -31,9 +51,14 @@ export default (props: {
// const level = userRoleInfo?.level || (isGlobalCompany ? 100 : 80);
const { getMenuMetaByPermissions } = useCardList();
const menuList = getMenuMetaByPermissions(
userInfo?.menuList || ([] as API.MenuListItem[]),
);
const menuTree = useMemo(() => {
const source = propMenuTree?.length
? propMenuTree
: getMenuMetaByPermissions(userInfo?.menuList || []);
return source as API.MenuListItem[];
}, [propMenuTree, userInfo?.menuList]);
const actionMap = {
add: $t('table.action.add'),
view: $t('table.action.view'),
@@ -46,7 +71,7 @@ export default (props: {
initialValues={formValues?.id ? formValues : { color: '#3b82f6' }}
open={visible}
onFinish={async (values: any) => {
values.menuIds = values.menuIds.join(',') || '';
values.menuIds = (values.menuIds || []).join(',') || '';
return onSubmit(values);
}}
modalProps={{
@@ -61,12 +86,10 @@ export default (props: {
} = await getRoleDetail({
id: formValues.id,
});
const menuIds = (data?.menuList || []).map(
(item: API.MenuListItem) => item.id,
);
const allSelectedIds = flattenAllIds(data?.menuList || []);
return {
...formValues,
menuIds,
menuIds: allSelectedIds,
};
}
return formValues || {};
@@ -114,23 +137,16 @@ export default (props: {
</ProFormDependency>
</Space>
</ProForm.Item>
<ProFormCheckbox.Group
<ProForm.Item
name="menuIds"
label={$t('form.role.menuIds')}
rules={[{ required: true, message: $t('form.required') }]}
options={menuList.map((item: API.MenuListItem) => ({
label: `${item.meta?.icon || ''} ${item.meta?.title}${item?.meta?.describe}`,
value: item.id,
}))}
fieldProps={{
style: {
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: '8px 16px',
alignItems: 'center',
},
}}
/>
>
<PermissionCheckboxGroup
menuTree={menuTree}
disabled={action === 'view'}
/>
</ProForm.Item>
<ProFormText hidden name="id" label="ID" />
</ModalForm>
);