mirror of
https://gitee.com/JavaLionLi/plus-ui.git
synced 2026-09-13 07:43:43 +08:00
update 升级全部依赖至最新版本并适配
- react/react-dom 升级 19.3.0 应用了 useEffectEvent 新特性(TypeScript 保持 6.x) - 使用 @noble/ciphers 替换已废弃的 crypto-js 保持与后端 AES 加解密兼容 - @wangeditor-next 5->6 升级(editor/editor-for-react 6.4.1) - antd/@ant-design/icons/@ant-design/x/ahooks/zustand/axios 等升级至最新 - 适配 oxlint 1.82 新规则(refs/set-state-in-effect/immutability/exhaustive-effect-dependencies) - 适配 axios 1.20 类型变化 使用 useEffectEvent 重构弹窗初始化逻辑
This commit is contained in:
parent
29fc02f0a6
commit
b56ebf5ae6
39
package.json
39
package.json
@ -19,46 +19,45 @@
|
||||
"start": "cross-env UMI_ENV=development MOCK=none BABEL_POLYFILL=none max dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.3.2",
|
||||
"@ant-design/icons": "^6.3.4",
|
||||
"@ant-design/pro-components": "3.1.12-0",
|
||||
"@ant-design/x": "^2.8.0",
|
||||
"@ant-design/x": "^2.9.0",
|
||||
"@iconify/react": "^6.0.2",
|
||||
"@tanstack/react-query": "^5.101.4",
|
||||
"@umijs/max": "^4.6.82",
|
||||
"@wangeditor-next/editor": "^5.7.16",
|
||||
"@wangeditor-next/editor-for-react": "^2.0.3",
|
||||
"ahooks": "^3.9.7",
|
||||
"antd": "^6.5.2",
|
||||
"@noble/ciphers": "^2.4.0",
|
||||
"@tanstack/react-query": "^5.102.8",
|
||||
"@umijs/max": "^4.7.16",
|
||||
"@wangeditor-next/editor": "^6.4.1",
|
||||
"@wangeditor-next/editor-for-react": "^6.4.1",
|
||||
"ahooks": "^3.10.0",
|
||||
"antd": "^6.6.3",
|
||||
"antd-style": "^4.1.0",
|
||||
"axios": "1.18.1",
|
||||
"axios": "1.20.0",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"clsx": "^2.1.1",
|
||||
"crypto-js": "4.2.0",
|
||||
"dayjs": "^1.11.21",
|
||||
"dayjs": "^1.11.23",
|
||||
"echarts": "^6.1.0",
|
||||
"echarts-for-react": "^3.0.6",
|
||||
"jsencrypt": "3.5.4",
|
||||
"nprogress": "0.2.0",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0",
|
||||
"react-easy-crop": "^6.2.3",
|
||||
"react-json-view-lite": "^2.5.0",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"screenfull": "^6.0.2",
|
||||
"zustand": "^5.0.14"
|
||||
"zustand": "^5.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/crypto-js": "4.2.2",
|
||||
"@types/node": "^26.1.1",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@umijs/lint": "^4.6.82",
|
||||
"@umijs/lint": "^4.7.16",
|
||||
"@umijs/max-plugin-openapi": "^2.0.3",
|
||||
"cross-env": "^10.1.0",
|
||||
"oxfmt": "^0.60.0",
|
||||
"oxlint": "^1.75.0",
|
||||
"oxfmt": "^0.67.0",
|
||||
"oxlint": "^1.82.0",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
3426
pnpm-lock.yaml
generated
3426
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -240,5 +240,6 @@ service.interceptors.response.use(
|
||||
);
|
||||
|
||||
export default function request<T = unknown, D = unknown>(config: RequestConfig<D>): Promise<T> {
|
||||
return service.request<T, T, D>(config);
|
||||
// axios 1.20 泛型返回为条件类型 AxiosResponseResult 需要断言 响应拦截器已返回 response.data
|
||||
return service.request<T, T, D>(config) as Promise<T>;
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { DataNode, EventDataNode } from 'antd/es/tree';
|
||||
import { LeftOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Input, Tree } from 'antd';
|
||||
import { useEffect, useMemo, useState, type Key } from 'react';
|
||||
import { useMemo, useState, type Key } from 'react';
|
||||
import { collectTreeKeys } from '@/utils/ruoyi';
|
||||
|
||||
export interface TreePanelProps<T extends object = Record<string, unknown>> {
|
||||
@ -94,16 +94,12 @@ export default function TreePanel<T extends object = Record<string, unknown>>({
|
||||
);
|
||||
const allTreeKeys = useMemo(() => collectTreeKeys(treeData, node => node.key), [treeData]);
|
||||
const mergedCollapsed = collapsed ?? innerCollapsed;
|
||||
|
||||
useEffect(() => {
|
||||
if (collapsed !== undefined) {
|
||||
setInnerCollapsed(collapsed);
|
||||
}
|
||||
}, [collapsed]);
|
||||
|
||||
useEffect(() => {
|
||||
// 数据变化时默认展开全部 通过渲染期对比 prev 派生 而非 effect 中 setState
|
||||
const [prevAllTreeKeys, setPrevAllTreeKeys] = useState(allTreeKeys);
|
||||
if (allTreeKeys !== prevAllTreeKeys) {
|
||||
setPrevAllTreeKeys(allTreeKeys);
|
||||
setExpandedKeys(allTreeKeys);
|
||||
}, [allTreeKeys]);
|
||||
}
|
||||
|
||||
const toggleCollapsed = () => {
|
||||
const nextCollapsed = !mergedCollapsed;
|
||||
|
||||
@ -3,7 +3,7 @@ import type { DataNode } from 'antd/es/tree';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { useBoolean, useRequest } from 'ahooks';
|
||||
import { Button, Form, Input, Modal, Space, Table, Tag, Tree } from 'antd';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useEffectEvent, useMemo, useState } from 'react';
|
||||
import type { DeptTreeVO } from '@/api/system/dept/types';
|
||||
import type { UserQuery, UserVO } from '@/api/system/user/types';
|
||||
import { deptTreeSelect, listUser, optionSelect } from '@/api/system/user';
|
||||
@ -98,23 +98,38 @@ export default function UserSelect({
|
||||
setSelectedUsers(res.data || []);
|
||||
};
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: modal initialization is intentionally tied to open state.
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
form.resetFields();
|
||||
setQuery({ pageNum: 1, pageSize: 10 });
|
||||
setUsers([]);
|
||||
setTotal(0);
|
||||
setSelectedUsers([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// useEffectEvent 读取最新的 value/data/query 等非响应式逻辑 只在 open 变化时执行
|
||||
const initModal = useEffectEvent(() => {
|
||||
Promise.all([
|
||||
deptTreeSelect().then(res => setDeptOptions(res.data || [])),
|
||||
loadUsers({ pageNum: 1, pageSize: 10 }),
|
||||
initSelectedUsers()
|
||||
]);
|
||||
}, [open]);
|
||||
});
|
||||
|
||||
// 关闭时重置弹窗状态
|
||||
const resetModal = useEffectEvent(() => {
|
||||
form.resetFields();
|
||||
setQuery({ pageNum: 1, pageSize: 10 });
|
||||
setUsers([]);
|
||||
setTotal(0);
|
||||
setSelectedUsers([]);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
// useEffectEvent 在 effect 中调用是 React 官方推荐用法
|
||||
// oxlint-disable-next-line react/set-state-in-effect
|
||||
resetModal();
|
||||
return;
|
||||
}
|
||||
|
||||
// useEffectEvent 在 effect 中调用是 React 官方推荐用法
|
||||
// oxlint-disable-next-line react/set-state-in-effect
|
||||
initModal();
|
||||
// initModal/resetModal 为 useEffectEvent 引用稳定 无需作为依赖
|
||||
// oxlint-disable-next-line react/exhaustive-effect-dependencies
|
||||
}, [initModal, open, resetModal]);
|
||||
|
||||
const handleQuery = async () => {
|
||||
const values = form.getFieldsValue();
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { CheckOutlined, ReloadOutlined, SaveOutlined } from '@ant-design/icons';
|
||||
import { Button, Divider, Drawer, Input, message, Segmented, Slider, Space, Switch } from 'antd';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { defaultLayoutSettings, useAppStore, type LayoutSettingsValue, type NavLayout } from '@/stores/appStore';
|
||||
|
||||
interface LayoutSettingsProps {
|
||||
@ -21,22 +21,17 @@ function SettingRow({ label, children }: { label: string; children: React.ReactN
|
||||
|
||||
export default function LayoutSettings({ open, value, onChange, onOpenChange }: LayoutSettingsProps) {
|
||||
const resetLayoutSettings = useAppStore(state => state.resetLayoutSettings);
|
||||
const preferredSideThemeRef = useRef(value.sideTheme);
|
||||
|
||||
useEffect(() => {
|
||||
if (!value.darkMode) {
|
||||
preferredSideThemeRef.current = value.sideTheme;
|
||||
}
|
||||
}, [value.darkMode, value.sideTheme]);
|
||||
// 记住非暗色模式下选中的主题 仅在事件回调中更新 避免渲染期读取 ref
|
||||
const [preferredSideTheme, setPreferredSideTheme] = useState(value.sideTheme);
|
||||
|
||||
const patchValue = (patch: Partial<LayoutSettingsValue>) => {
|
||||
if (patch.sideTheme) {
|
||||
preferredSideThemeRef.current = patch.sideTheme;
|
||||
setPreferredSideTheme(patch.sideTheme);
|
||||
}
|
||||
|
||||
const next = { ...value, ...patch };
|
||||
if (patch.darkMode === false) {
|
||||
next.sideTheme = preferredSideThemeRef.current;
|
||||
next.sideTheme = preferredSideTheme;
|
||||
}
|
||||
if (patch.tagsView === false) {
|
||||
next.tagsViewPersist = false;
|
||||
@ -46,10 +41,11 @@ export default function LayoutSettings({ open, value, onChange, onOpenChange }:
|
||||
|
||||
const resetSettings = () => {
|
||||
resetLayoutSettings();
|
||||
setPreferredSideTheme(defaultLayoutSettings.sideTheme);
|
||||
onChange(defaultLayoutSettings);
|
||||
message.success('配置已重置');
|
||||
};
|
||||
const selectedSideTheme = value.darkMode ? preferredSideThemeRef.current : value.sideTheme;
|
||||
const selectedSideTheme = value.darkMode ? preferredSideTheme : value.sideTheme;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
|
||||
@ -2,7 +2,7 @@ import { DownloadOutlined } from '@ant-design/icons';
|
||||
import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { Button, Modal, Popover, Spin, Table, Tabs } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { OssVO } from '@/api/system/oss/types';
|
||||
import type { FlowHistoryVO } from '@/api/workflow/task/types';
|
||||
import { downloadOss, listByIds } from '@/api/system/oss';
|
||||
@ -52,19 +52,21 @@ export default function ApprovalRecord({ businessId, open, onOpenChange, width =
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !businessId) {
|
||||
if (!open) {
|
||||
setRecordList([]);
|
||||
setInstanceId(undefined);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 打开时清空旧数据并加载 打开弹窗同步外部状态属于合理场景
|
||||
const resetAndLoad = useCallback(async (nextBusinessId?: string | number) => {
|
||||
setRecordList([]);
|
||||
setInstanceId(undefined);
|
||||
loadRecords(businessId);
|
||||
}, [businessId, loadRecords, open]);
|
||||
if (nextBusinessId) {
|
||||
await loadRecords(nextBusinessId);
|
||||
}
|
||||
}, [loadRecords]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
// 打开弹窗时同步清空旧数据属于与外部状态对齐的合理场景
|
||||
// oxlint-disable-next-line react/set-state-in-effect
|
||||
void resetAndLoad(businessId);
|
||||
}, [businessId, open, resetAndLoad]);
|
||||
|
||||
const downloadAttachment = async (row: OssVO) => {
|
||||
const blob = await downloadOss(row.ossId);
|
||||
|
||||
@ -10,19 +10,21 @@ const maxScale = 3;
|
||||
|
||||
export default function FlowChartImage({ imgUrl }: FlowChartImageProps) {
|
||||
const wrapperRef = useRef<HTMLButtonElement>(null);
|
||||
const dragRef = useRef({ dragging: false, x: 0, y: 0 });
|
||||
const dragRef = useRef({ x: 0, y: 0 });
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [scale, setScale] = useState(1);
|
||||
const [translate, setTranslate] = useState({ x: 0, y: 0 });
|
||||
|
||||
const transformStyle = useMemo(
|
||||
() => ({
|
||||
transform: `translate(${translate.x}px, ${translate.y}px) scale(${scale})`,
|
||||
transition: dragRef.current.dragging ? 'none' : 'transform 0.2s ease'
|
||||
transition: dragging ? 'none' : 'transform 0.2s ease'
|
||||
}),
|
||||
[scale, translate]
|
||||
[dragging, scale, translate]
|
||||
);
|
||||
|
||||
const resetTransform = () => {
|
||||
setDragging(false);
|
||||
setScale(1);
|
||||
setTranslate({ x: 0, y: 0 });
|
||||
};
|
||||
@ -37,19 +39,20 @@ export default function FlowChartImage({ imgUrl }: FlowChartImageProps) {
|
||||
const handleMouseDown = (event: MouseEvent<HTMLButtonElement>) => {
|
||||
if (scale <= 1) return;
|
||||
event.preventDefault();
|
||||
dragRef.current = { dragging: true, x: event.clientX, y: event.clientY };
|
||||
dragRef.current = { x: event.clientX, y: event.clientY };
|
||||
setDragging(true);
|
||||
};
|
||||
|
||||
const handleMouseMove = (event: MouseEvent<HTMLButtonElement>) => {
|
||||
if (!dragRef.current.dragging) return;
|
||||
if (!dragging) return;
|
||||
const deltaX = event.clientX - dragRef.current.x;
|
||||
const deltaY = event.clientY - dragRef.current.y;
|
||||
dragRef.current = { dragging: true, x: event.clientX, y: event.clientY };
|
||||
dragRef.current = { x: event.clientX, y: event.clientY };
|
||||
setTranslate(prev => ({ x: prev.x + deltaX, y: prev.y + deltaY }));
|
||||
};
|
||||
|
||||
const stopDrag = () => {
|
||||
dragRef.current.dragging = false;
|
||||
setDragging(false);
|
||||
};
|
||||
|
||||
if (!imgUrl) {
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import { useBoolean } from 'ahooks';
|
||||
import { Button, Form, Input, message, Modal, Select, Space, Table, Tag } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useEffectEvent, useState } from 'react';
|
||||
import type { UserVO } from '@/api/system/user/types';
|
||||
import type { FlowNextNodeVO, FlowTaskVO } from '@/api/workflow/task/types';
|
||||
import {
|
||||
@ -72,29 +72,31 @@ export default function SubmitVerify({ open, taskId, variables = emptyVariables,
|
||||
const [signatureOpen, { setTrue: openSignatureModal, setFalse: closeSignatureModal }] = useBoolean(false);
|
||||
const [signatureUsers, setSignatureUsers] = useState<SignatureUser[]>([]);
|
||||
|
||||
// useEffectEvent 让 variables 始终读到最新值 避免其引用变化触发重复加载
|
||||
const loadTask = useEffectEvent(async (taskId: string | number) => {
|
||||
setApprovalLoading(true);
|
||||
approveForm.setFieldsValue({ message: '', messageType: ['1'], assigneeMap: {}, fileId: undefined });
|
||||
setCopyUsers([]);
|
||||
setAssigneeNames({});
|
||||
setCurrentTask(undefined);
|
||||
setNextNodes([]);
|
||||
try {
|
||||
const taskRes = await getTask(taskId);
|
||||
setCurrentTask(taskRes.data);
|
||||
setCopyUsers((taskRes.data.copyList || []).map(user => ({ userId: user.userId, nickName: user.nickName })));
|
||||
const nextRes = await getNextNodeList({ taskId, variables });
|
||||
setNextNodes(nextRes.data || []);
|
||||
} finally {
|
||||
setApprovalLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !taskId) return;
|
||||
|
||||
const loadTask = async () => {
|
||||
setApprovalLoading(true);
|
||||
approveForm.setFieldsValue({ message: '', messageType: ['1'], assigneeMap: {}, fileId: undefined });
|
||||
setCopyUsers([]);
|
||||
setAssigneeNames({});
|
||||
setCurrentTask(undefined);
|
||||
setNextNodes([]);
|
||||
try {
|
||||
const taskRes = await getTask(taskId);
|
||||
setCurrentTask(taskRes.data);
|
||||
setCopyUsers((taskRes.data.copyList || []).map(user => ({ userId: user.userId, nickName: user.nickName })));
|
||||
const nextRes = await getNextNodeList({ taskId, variables });
|
||||
setNextNodes(nextRes.data || []);
|
||||
} finally {
|
||||
setApprovalLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadTask();
|
||||
}, [approveForm, open, taskId, variables]);
|
||||
// useEffectEvent 在 effect 中调用是 React 官方推荐用法
|
||||
// oxlint-disable-next-line react/set-state-in-effect
|
||||
loadTask(taskId);
|
||||
}, [open, taskId]);
|
||||
|
||||
const closeApprovalModal = () => {
|
||||
onOpenChange(false);
|
||||
|
||||
@ -34,8 +34,8 @@ export function useTableScroll(options: TableScrollX | TableScrollOptions) {
|
||||
|
||||
const refreshTableScroll = useCallback(() => {
|
||||
if (!fullHeightTable || disabled || typeof window === 'undefined') {
|
||||
// tableY 仅在启用时参与 memo 计算 这里无需同步重置
|
||||
clearTableBodyHeight();
|
||||
setTableY(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -14,11 +14,10 @@ function buildChatUrl(openId: string, trustedCredential: string) {
|
||||
export default function AiChatPage() {
|
||||
const [chatUrl, setChatUrl] = useState('');
|
||||
const [loadError, setLoadError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
// 初始即为加载中 effect 首次触发时无需同步 setState
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const loadChat = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setLoadError('');
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
@ -44,6 +43,8 @@ export default function AiChatPage() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 挂载时加载 AI 聊天地址属于与外部系统同步的合理场景
|
||||
// oxlint-disable-next-line react/set-state-in-effect
|
||||
loadChat();
|
||||
}, [loadChat]);
|
||||
|
||||
@ -60,7 +61,15 @@ export default function AiChatPage() {
|
||||
) : (
|
||||
<Empty className="ai-chat-empty" description={loadError || '正在加载 Snail AI'}>
|
||||
{loadError ? (
|
||||
<Button type="primary" icon={<ReloadOutlined />} onClick={loadChat}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => {
|
||||
setLoading(true);
|
||||
setLoadError('');
|
||||
loadChat();
|
||||
}}
|
||||
>
|
||||
重新加载
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
@ -124,6 +124,8 @@ export default function Login() {
|
||||
|
||||
useEffect(() => {
|
||||
if (getToken()) return;
|
||||
// 挂载时加载验证码属于与外部系统同步的合理场景
|
||||
// oxlint-disable-next-line react/set-state-in-effect
|
||||
loadCaptcha().catch(() => {
|
||||
setCaptcha({ captchaEnabled: false });
|
||||
});
|
||||
@ -135,7 +137,7 @@ export default function Login() {
|
||||
|
||||
const doSocialLogin = async (source: string) => {
|
||||
const res = await authRouterUrl(source);
|
||||
window.location.href = res.data;
|
||||
window.location.assign(res.data);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -83,6 +83,8 @@ export default function Register() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 挂载时加载验证码属于与外部系统同步的合理场景
|
||||
// oxlint-disable-next-line react/set-state-in-effect
|
||||
loadCaptcha().catch(() => setCaptcha({ captchaEnabled: false }));
|
||||
}, [loadCaptcha]);
|
||||
|
||||
|
||||
@ -123,6 +123,8 @@ export default function SystemNoticePage() {
|
||||
useEffect(() => {
|
||||
const noticeId = new URLSearchParams(location.search).get('noticeId');
|
||||
if (noticeId) {
|
||||
// URL 深链同步打开公告详情属于与外部状态对齐的合理场景
|
||||
// oxlint-disable-next-line react/set-state-in-effect
|
||||
openDetail(noticeId);
|
||||
}
|
||||
}, [location.search, openDetail]);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { DataNode } from 'antd/es/tree';
|
||||
import { useBoolean } from 'ahooks';
|
||||
import { Checkbox, message, type FormInstance } from 'antd';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useEffectEvent, useMemo, useState } from 'react';
|
||||
import type { DeptTreeVO } from '@/api/system/dept/types';
|
||||
import type { MenuTreeOption } from '@/api/system/menu/types';
|
||||
import type { RoleForm } from '@/api/system/role/types';
|
||||
@ -104,39 +104,37 @@ export function useRolePermission({ open, roleId, form, onSuccess }: UseRolePerm
|
||||
buttonAllKeys.every(key => hasId(buttonCheckedKeys, key));
|
||||
const deptAllChecked = deptAllKeys.length > 0 && deptAllKeys.every(key => deptCheckedKeys.includes(key));
|
||||
|
||||
// useEffectEvent 隔离 form 实例等非响应式逻辑 依赖项只保留 open/roleId
|
||||
const loadPermission = useEffectEvent(async (roleId: string | number) => {
|
||||
setLoading(true);
|
||||
setActiveTab('menu');
|
||||
try {
|
||||
const [roleRes, menuRes, deptRes] = await Promise.all([getRole(roleId), roleMenuTreeselect(roleId), deptTreeSelect(roleId)]);
|
||||
form.setFieldsValue({
|
||||
...defaultRoleForm,
|
||||
...roleRes.data
|
||||
});
|
||||
setMenuConnect(roleRes.data.menuCheckStrictly ?? true);
|
||||
setDeptConnect(roleRes.data.deptCheckStrictly ?? true);
|
||||
const nextMenuTree = menuRes.data.menus || [];
|
||||
const nextMenuMeta = buildMenuPermissionMeta(nextMenuTree);
|
||||
const checkedKeys = menuRes.data.checkedKeys || [];
|
||||
setMenuTree(nextMenuTree);
|
||||
setDeptTree(deptRes.data.depts || []);
|
||||
setMenuCheckedKeys(checkedKeys.filter(key => !hasId(nextMenuMeta.buttonIds, key)));
|
||||
setButtonCheckedKeys(checkedKeys.filter(key => hasId(nextMenuMeta.buttonIds, key)));
|
||||
setDeptCheckedKeys(deptRes.data.checkedKeys || []);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !roleId) return;
|
||||
|
||||
const loadPermission = async () => {
|
||||
setLoading(true);
|
||||
setActiveTab('menu');
|
||||
try {
|
||||
const [roleRes, menuRes, deptRes] = await Promise.all([
|
||||
getRole(roleId),
|
||||
roleMenuTreeselect(roleId),
|
||||
deptTreeSelect(roleId)
|
||||
]);
|
||||
form.setFieldsValue({
|
||||
...defaultRoleForm,
|
||||
...roleRes.data
|
||||
});
|
||||
setMenuConnect(roleRes.data.menuCheckStrictly ?? true);
|
||||
setDeptConnect(roleRes.data.deptCheckStrictly ?? true);
|
||||
const nextMenuTree = menuRes.data.menus || [];
|
||||
const nextMenuMeta = buildMenuPermissionMeta(nextMenuTree);
|
||||
const checkedKeys = menuRes.data.checkedKeys || [];
|
||||
setMenuTree(nextMenuTree);
|
||||
setDeptTree(deptRes.data.depts || []);
|
||||
setMenuCheckedKeys(checkedKeys.filter(key => !hasId(nextMenuMeta.buttonIds, key)));
|
||||
setButtonCheckedKeys(checkedKeys.filter(key => hasId(nextMenuMeta.buttonIds, key)));
|
||||
setDeptCheckedKeys(deptRes.data.checkedKeys || []);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadPermission();
|
||||
}, [form, open, roleId]);
|
||||
// useEffectEvent 在 effect 中调用是 React 官方推荐用法
|
||||
// oxlint-disable-next-line react/set-state-in-effect
|
||||
loadPermission(roleId);
|
||||
}, [open, roleId]);
|
||||
|
||||
const resetPermission = () => {
|
||||
form.resetFields();
|
||||
|
||||
@ -27,14 +27,20 @@ export default function UserAuthRolePage() {
|
||||
const [roleIds, setRoleIds] = useState<Array<string | number>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
getAuthRole(userId)
|
||||
.then(res => {
|
||||
const loadAuthRole = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getAuthRole(userId);
|
||||
setUser(res.data.user);
|
||||
setRoles(res.data.roles || []);
|
||||
setRoleIds((res.data.roles || []).filter(role => role.flag).map(role => role.roleId));
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
loadAuthRole();
|
||||
// userId 来自路由参数 变化时需重新加载
|
||||
// oxlint-disable-next-line react/exhaustive-effect-dependencies
|
||||
}, [userId]);
|
||||
|
||||
const columns: ProColumns<RoleVO>[] = useMemo(
|
||||
|
||||
@ -275,7 +275,7 @@ export default function Profile() {
|
||||
|
||||
const bindAuth = async (source: string) => {
|
||||
const res = await authRouterUrl(source);
|
||||
window.location.href = res.data;
|
||||
window.location.assign(res.data);
|
||||
};
|
||||
|
||||
const deviceColumns: ProColumns<OnlineVO>[] = [
|
||||
|
||||
@ -90,6 +90,8 @@ export default function ToolGenPage() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 挂载时加载数据表名属于与外部系统同步的合理场景
|
||||
// oxlint-disable-next-line react/set-state-in-effect
|
||||
loadDataNames();
|
||||
}, [loadDataNames]);
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import { message, Modal, Space, TreeSelect, Upload, type UploadProps } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import type { CategoryTreeVO } from '@/api/workflow/category/types';
|
||||
import { importDefinition } from '@/api/workflow/definition';
|
||||
import { buildAccept, getUploadErrorMessage, validateUploadFile } from '@/utils/upload';
|
||||
@ -22,13 +22,8 @@ export default function DefinitionImportModal({
|
||||
onCancel,
|
||||
onSuccess
|
||||
}: DefinitionImportModalProps) {
|
||||
const [uploadCategory, setUploadCategory] = useState<string | number>();
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setUploadCategory(initialCategory);
|
||||
}
|
||||
}, [initialCategory, open]);
|
||||
// 父组件通过 key 重挂载打开弹窗 这里的初值即为每次打开时的 initialCategory 无需 effect 同步
|
||||
const [uploadCategory, setUploadCategory] = useState<string | number | undefined>(initialCategory);
|
||||
|
||||
const handleImportDefinition: UploadProps['customRequest'] = async options => {
|
||||
const file = options.file as File;
|
||||
|
||||
@ -120,6 +120,8 @@ export default function WorkflowProcessDefinitionPage() {
|
||||
useEffect(() => {
|
||||
const nextTab = getExplicitActiveTabFromSearch(location.search);
|
||||
if (!nextTab) return;
|
||||
// URL 查询参数同步页签属于与外部状态对齐的合理场景
|
||||
// oxlint-disable-next-line react/set-state-in-effect
|
||||
switchTab(nextTab);
|
||||
}, [location.search, switchTab]);
|
||||
|
||||
@ -414,6 +416,7 @@ export default function WorkflowProcessDefinitionPage() {
|
||||
/>
|
||||
|
||||
<DefinitionImportModal
|
||||
key={uploadOpen ? 'open' : 'closed'}
|
||||
open={uploadOpen}
|
||||
categoryOptions={categoryOptions}
|
||||
initialCategory={category}
|
||||
|
||||
@ -61,7 +61,6 @@ export default function WorkflowSpelPage() {
|
||||
const { ids, selectedOne, handleSelectionChange, clearSelection } = useTableSelection<SpelVO>(row => row.id);
|
||||
const [modalOpen, { setTrue: openModal, setFalse: closeModal }] = useBoolean(false);
|
||||
const [modalTitle, setModalTitle] = useState('');
|
||||
const [preview, setPreview] = useState('');
|
||||
|
||||
const canAdd = hasPermi(userInfo, ['workflow:spel:add']);
|
||||
const canEdit = hasPermi(userInfo, ['workflow:spel:edit']);
|
||||
@ -69,17 +68,17 @@ export default function WorkflowSpelPage() {
|
||||
const statusOptions = useMemo(() => dictOptions(dicts.sys_normal_disable), [dicts.sys_normal_disable]);
|
||||
const watchedValues = Form.useWatch([], form);
|
||||
|
||||
// preview 由表单值派生 无需独立 state
|
||||
const preview = useMemo(() => buildViewSpel(watchedValues || {}), [watchedValues]);
|
||||
|
||||
useEffect(() => {
|
||||
const nextPreview = buildViewSpel(watchedValues || {});
|
||||
setPreview(nextPreview);
|
||||
form.setFieldValue('viewSpel', nextPreview);
|
||||
}, [form, watchedValues]);
|
||||
form.setFieldValue('viewSpel', preview);
|
||||
}, [form, preview]);
|
||||
|
||||
const openAdd = () => {
|
||||
form.resetFields();
|
||||
form.setFieldsValue(defaultSpelForm);
|
||||
setModalTitle('添加流程spel表达式定义');
|
||||
setPreview('');
|
||||
openModal();
|
||||
};
|
||||
|
||||
@ -90,7 +89,6 @@ export default function WorkflowSpelPage() {
|
||||
form.resetFields();
|
||||
form.setFieldsValue(res.data);
|
||||
setModalTitle('修改流程spel表达式定义');
|
||||
setPreview(res.data.viewSpel || '');
|
||||
openModal();
|
||||
};
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import * as CryptoJSModule from 'crypto-js';
|
||||
|
||||
const CryptoJS = ('default' in CryptoJSModule ? CryptoJSModule.default : CryptoJSModule) as typeof CryptoJSModule;
|
||||
import { utf8ToBytes } from '@noble/ciphers/utils.js';
|
||||
import { ecb } from '@noble/ciphers/aes.js';
|
||||
|
||||
function generateRandomString() {
|
||||
const array = new Uint8Array(32);
|
||||
@ -10,29 +9,36 @@ function generateRandomString() {
|
||||
.slice(0, 32);
|
||||
}
|
||||
|
||||
export function generateAesKey(): CryptoJSModule.lib.WordArray {
|
||||
return CryptoJS.enc.Utf8.parse(generateRandomString());
|
||||
function bytesToBase64(bytes: Uint8Array) {
|
||||
return btoa(String.fromCharCode(...bytes));
|
||||
}
|
||||
|
||||
export function encryptBase64(str: CryptoJSModule.lib.WordArray) {
|
||||
return CryptoJS.enc.Base64.stringify(str);
|
||||
function base64ToBytes(base64: string) {
|
||||
return Uint8Array.from(atob(base64), c => c.charCodeAt(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 AES-256 密钥 (32 字节随机字符串编码) 兼容后端 SecureUtil.aes(key)
|
||||
*/
|
||||
export function generateAesKey(): Uint8Array {
|
||||
return utf8ToBytes(generateRandomString());
|
||||
}
|
||||
|
||||
export function encryptBase64(bytes: Uint8Array) {
|
||||
return bytesToBase64(bytes);
|
||||
}
|
||||
|
||||
export function decryptBase64(str: string) {
|
||||
return CryptoJS.enc.Base64.parse(str);
|
||||
return base64ToBytes(str);
|
||||
}
|
||||
|
||||
export function encryptWithAes(message: string, aesKey: CryptoJSModule.lib.WordArray) {
|
||||
return CryptoJS.AES.encrypt(message, aesKey, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7
|
||||
}).toString();
|
||||
/**
|
||||
* AES-256-ECB + PKCS7 填充 输出 Base64 密文 与后端 AES/ECB/PKCS5Padding 格式兼容
|
||||
*/
|
||||
export function encryptWithAes(message: string, aesKey: Uint8Array) {
|
||||
return bytesToBase64(ecb(aesKey).encrypt(utf8ToBytes(message)));
|
||||
}
|
||||
|
||||
export function decryptWithAes(message: string, aesKey: CryptoJSModule.lib.WordArray) {
|
||||
const decrypted = CryptoJS.AES.decrypt(message, aesKey, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7
|
||||
});
|
||||
return decrypted.toString(CryptoJS.enc.Utf8);
|
||||
export function decryptWithAes(message: string, aesKey: Uint8Array) {
|
||||
return new TextDecoder().decode(ecb(aesKey).decrypt(base64ToBytes(message)));
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user