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:
疯狂的狮子Li 2026-09-10 12:05:28 +08:00
parent 29fc02f0a6
commit b56ebf5ae6
22 changed files with 2126 additions and 1685 deletions

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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>;
}

View File

@ -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;

View File

@ -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();

View File

@ -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

View File

@ -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);

View File

@ -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) {

View File

@ -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);

View File

@ -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;
}

View File

@ -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}

View File

@ -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 (

View File

@ -83,6 +83,8 @@ export default function Register() {
}, []);
useEffect(() => {
// 挂载时加载验证码属于与外部系统同步的合理场景
// oxlint-disable-next-line react/set-state-in-effect
loadCaptcha().catch(() => setCaptcha({ captchaEnabled: false }));
}, [loadCaptcha]);

View File

@ -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]);

View File

@ -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();

View File

@ -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(

View File

@ -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>[] = [

View File

@ -90,6 +90,8 @@ export default function ToolGenPage() {
};
useEffect(() => {
// 挂载时加载数据表名属于与外部系统同步的合理场景
// oxlint-disable-next-line react/set-state-in-effect
loadDataNames();
}, [loadDataNames]);

View File

@ -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;

View 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}

View File

@ -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();
};

View File

@ -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)));
}