mirror of
https://gitee.com/JavaLionLi/plus-ui.git
synced 2026-09-14 16:17:31 +08:00
update: 升级前端主要依赖并替换头像裁剪组件
- 升级主要前端依赖
- React / React DOM
- Umi Max / Umi lint
- Ant Design Icons / ProComponents
- WangEditor
- Axios
- ECharts
- TanStack Query
- oxlint / oxfmt
- 相关 TypeScript 类型包
- 替换头像裁剪实现 react-easy-crop
- 适配 Ant Design 6 废弃 API
This commit is contained in:
parent
d5e1b42ad1
commit
eafce61dac
37
package.json
37
package.json
@ -17,30 +17,29 @@
|
|||||||
"start": "cross-env UMI_ENV=development MOCK=none BABEL_POLYFILL=none max dev"
|
"start": "cross-env UMI_ENV=development MOCK=none BABEL_POLYFILL=none max dev"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^6.2.3",
|
"@ant-design/icons": "^6.2.5",
|
||||||
"@ant-design/pro-components": "^3.1.12-0",
|
"@ant-design/pro-components": "3.1.12-0",
|
||||||
"@ant-design/x": "^2.7.0",
|
"@ant-design/x": "^2.7.0",
|
||||||
"@iconify/react": "^6.0.2",
|
"@iconify/react": "^6.0.2",
|
||||||
"@tanstack/react-query": "^5.100.6",
|
"@tanstack/react-query": "^5.101.0",
|
||||||
"@umijs/max": "^4.6.51",
|
"@umijs/max": "^4.6.61",
|
||||||
"@wangeditor-next/editor": "^5.6.4",
|
"@wangeditor-next/editor": "^5.7.10",
|
||||||
"@wangeditor-next/editor-for-react": "^1.0.10",
|
"@wangeditor-next/editor-for-react": "^2.0.2",
|
||||||
"ahooks": "^3.9.7",
|
"ahooks": "^3.9.7",
|
||||||
"antd": "^6.4.3",
|
"antd": "^6.4.3",
|
||||||
"antd-style": "^4.1.0",
|
"antd-style": "^4.1.0",
|
||||||
"axios": "1.16.1",
|
"axios": "1.17.0",
|
||||||
"browser-image-compression": "^2.0.2",
|
"browser-image-compression": "^2.0.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cropperjs": "^1.6.2",
|
|
||||||
"crypto-js": "4.2.0",
|
"crypto-js": "4.2.0",
|
||||||
"dayjs": "^1.11.20",
|
"dayjs": "^1.11.21",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.1.0",
|
||||||
"echarts-for-react": "^3.0.2",
|
"echarts-for-react": "^3.0.6",
|
||||||
"jsencrypt": "3.5.4",
|
"jsencrypt": "3.5.4",
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.7",
|
||||||
"react-cropper": "^2.3.3",
|
"react-dom": "^19.2.7",
|
||||||
"react-dom": "^19.2.5",
|
"react-easy-crop": "^5.5.7",
|
||||||
"react-json-view-lite": "^2.5.0",
|
"react-json-view-lite": "^2.5.0",
|
||||||
"react-syntax-highlighter": "^16.1.1",
|
"react-syntax-highlighter": "^16.1.1",
|
||||||
"screenfull": "^6.0.2",
|
"screenfull": "^6.0.2",
|
||||||
@ -48,16 +47,16 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/crypto-js": "4.2.2",
|
"@types/crypto-js": "4.2.2",
|
||||||
"@types/node": "^25.9.1",
|
"@types/node": "^25.9.2",
|
||||||
"@types/nprogress": "0.2.3",
|
"@types/nprogress": "0.2.3",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.17",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"@umijs/lint": "^4.6.51",
|
"@umijs/lint": "^4.6.61",
|
||||||
"@umijs/max-plugin-openapi": "^2.0.3",
|
"@umijs/max-plugin-openapi": "^2.0.3",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"oxfmt": "^0.51.0",
|
"oxfmt": "^0.53.0",
|
||||||
"oxlint": "^1.66.0",
|
"oxlint": "^1.68.0",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
2804
pnpm-lock.yaml
generated
2804
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
|||||||
import 'antd/dist/reset.css';
|
import 'antd/dist/reset.css';
|
||||||
import '@wangeditor-next/editor/dist/css/style.css';
|
import '@wangeditor-next/editor/dist/css/style.css';
|
||||||
import 'react-json-view-lite/dist/index.css';
|
import 'react-json-view-lite/dist/index.css';
|
||||||
import 'cropperjs/dist/cropper.css';
|
import 'react-easy-crop/react-easy-crop.css';
|
||||||
import './assets/styles/index.less';
|
import './assets/styles/index.less';
|
||||||
|
|||||||
2
src/pages/monitor/cache/index.tsx
vendored
2
src/pages/monitor/cache/index.tsx
vendored
@ -109,7 +109,7 @@ export default function MonitorCachePage() {
|
|||||||
return (
|
return (
|
||||||
<PageContainer title="缓存监控">
|
<PageContainer title="缓存监控">
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
<Space orientation="vertical" size={12} style={{ width: '100%' }}>
|
||||||
<ProCard
|
<ProCard
|
||||||
title="缓存概览"
|
title="缓存概览"
|
||||||
extra={
|
extra={
|
||||||
|
|||||||
@ -88,7 +88,7 @@ export default function UserImportModal({ open, onClose, onSuccess }: UserImport
|
|||||||
<p className="ant-upload-text">将文件拖到此处,或点击上传</p>
|
<p className="ant-upload-text">将文件拖到此处,或点击上传</p>
|
||||||
<p className="ant-upload-hint">仅允许导入 xls、xlsx 格式文件。</p>
|
<p className="ant-upload-hint">仅允许导入 xls、xlsx 格式文件。</p>
|
||||||
</Upload.Dragger>
|
</Upload.Dragger>
|
||||||
<Space direction="vertical" style={{ marginTop: 12 }}>
|
<Space orientation="vertical" style={{ marginTop: 12 }}>
|
||||||
<Checkbox checked={updateSupport} onChange={event => setUpdateSupport(event.target.checked)}>
|
<Checkbox checked={updateSupport} onChange={event => setUpdateSupport(event.target.checked)}>
|
||||||
是否更新已经存在的用户数据
|
是否更新已经存在的用户数据
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
|||||||
@ -13,13 +13,14 @@ import {
|
|||||||
Popconfirm,
|
Popconfirm,
|
||||||
Radio,
|
Radio,
|
||||||
Row,
|
Row,
|
||||||
|
Slider,
|
||||||
Space,
|
Space,
|
||||||
Tabs,
|
Tabs,
|
||||||
Upload,
|
Upload,
|
||||||
type UploadProps
|
type UploadProps
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import Cropper, { type ReactCropperElement } from 'react-cropper';
|
import Cropper, { type Area, type Point } from 'react-easy-crop';
|
||||||
import type { OnlineVO } from '@/api/monitor/online/types';
|
import type { OnlineVO } from '@/api/monitor/online/types';
|
||||||
import type { SocialAuthVO } from '@/api/system/social/types';
|
import type { SocialAuthVO } from '@/api/system/social/types';
|
||||||
import type { UserInfoVO, UserProfileForm } from '@/api/system/user/types';
|
import type { UserInfoVO, UserProfileForm } from '@/api/system/user/types';
|
||||||
@ -48,6 +49,8 @@ const socialProviders = [
|
|||||||
{ source: 'github', label: 'GitHub', icon: githubIcon }
|
{ source: 'github', label: 'GitHub', icon: githubIcon }
|
||||||
];
|
];
|
||||||
const avatarFileTypes = ['png', 'jpg', 'jpeg'];
|
const avatarFileTypes = ['png', 'jpg', 'jpeg'];
|
||||||
|
const avatarOutputSize = 200;
|
||||||
|
const initialAvatarCrop: Point = { x: 0, y: 0 };
|
||||||
|
|
||||||
function socialProvider(source?: string) {
|
function socialProvider(source?: string) {
|
||||||
return socialProviders.find(item => item.source === source);
|
return socialProviders.find(item => item.source === source);
|
||||||
@ -72,6 +75,57 @@ function socialProviderNode(source?: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createImage(url: string) {
|
||||||
|
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||||
|
const image = new window.Image();
|
||||||
|
image.addEventListener('load', () => resolve(image));
|
||||||
|
image.addEventListener('error', error => reject(error));
|
||||||
|
image.src = url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRadianAngle(degreeValue: number) {
|
||||||
|
return (degreeValue * Math.PI) / 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotateSize(width: number, height: number, rotation: number) {
|
||||||
|
const rotRad = getRadianAngle(rotation);
|
||||||
|
return {
|
||||||
|
width: Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),
|
||||||
|
height: Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCroppedAvatarBlob(imageSrc: string, crop: Area, rotation: number) {
|
||||||
|
const image = await createImage(imageSrc);
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return null;
|
||||||
|
|
||||||
|
const rotRad = getRadianAngle(rotation);
|
||||||
|
const { width: bBoxWidth, height: bBoxHeight } = rotateSize(image.naturalWidth, image.naturalHeight, rotation);
|
||||||
|
|
||||||
|
canvas.width = bBoxWidth;
|
||||||
|
canvas.height = bBoxHeight;
|
||||||
|
ctx.translate(bBoxWidth / 2, bBoxHeight / 2);
|
||||||
|
ctx.rotate(rotRad);
|
||||||
|
ctx.translate(-image.naturalWidth / 2, -image.naturalHeight / 2);
|
||||||
|
ctx.drawImage(image, 0, 0);
|
||||||
|
|
||||||
|
const croppedCanvas = document.createElement('canvas');
|
||||||
|
const croppedCtx = croppedCanvas.getContext('2d');
|
||||||
|
if (!croppedCtx) return null;
|
||||||
|
|
||||||
|
croppedCanvas.width = avatarOutputSize;
|
||||||
|
croppedCanvas.height = avatarOutputSize;
|
||||||
|
croppedCtx.drawImage(canvas, crop.x, crop.y, crop.width, crop.height, 0, 0, avatarOutputSize, avatarOutputSize);
|
||||||
|
|
||||||
|
return new Promise<Blob | null>(resolve => croppedCanvas.toBlob(resolve, 'image/png'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function avatarUploadFileName(fileName: string) {
|
||||||
|
return fileName.replace(/\.[^.]+$/, '') || 'avatar';
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeOnlineDevices(payload: OnlineVO[] | { rows?: OnlineVO[] } | undefined) {
|
function normalizeOnlineDevices(payload: OnlineVO[] | { rows?: OnlineVO[] } | undefined) {
|
||||||
if (Array.isArray(payload)) return payload;
|
if (Array.isArray(payload)) return payload;
|
||||||
@ -91,10 +145,17 @@ export default function Profile() {
|
|||||||
const [avatarCropOpen, setAvatarCropOpen] = useState(false);
|
const [avatarCropOpen, setAvatarCropOpen] = useState(false);
|
||||||
const [avatarCropUrl, setAvatarCropUrl] = useState('');
|
const [avatarCropUrl, setAvatarCropUrl] = useState('');
|
||||||
const [avatarFileName, setAvatarFileName] = useState('avatar.png');
|
const [avatarFileName, setAvatarFileName] = useState('avatar.png');
|
||||||
const cropperRef = useRef<ReactCropperElement>(null);
|
const [avatarCrop, setAvatarCrop] = useState<Point>(initialAvatarCrop);
|
||||||
|
const [avatarZoom, setAvatarZoom] = useState(1);
|
||||||
|
const [avatarRotation, setAvatarRotation] = useState(0);
|
||||||
|
const [avatarCroppedAreaPixels, setAvatarCroppedAreaPixels] = useState<Area>();
|
||||||
const user = profile?.user || {};
|
const user = profile?.user || {};
|
||||||
const genderOptions = useMemo(() => dictOptions(dicts.sys_user_gender), [dicts.sys_user_gender]);
|
const genderOptions = useMemo(() => dictOptions(dicts.sys_user_gender), [dicts.sys_user_gender]);
|
||||||
|
|
||||||
|
const onAvatarCropComplete = useCallback((_croppedArea: Area, croppedAreaPixels: Area) => {
|
||||||
|
setAvatarCroppedAreaPixels(croppedAreaPixels);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const loadProfile = useCallback(async () => {
|
const loadProfile = useCallback(async () => {
|
||||||
await withLoading(async () => {
|
await withLoading(async () => {
|
||||||
const [profileRes, authRes, onlineRes] = await Promise.all([getUserProfile(), getAuthList(), getOnline()]);
|
const [profileRes, authRes, onlineRes] = await Promise.all([getUserProfile(), getAuthList(), getOnline()]);
|
||||||
@ -143,6 +204,10 @@ export default function Profile() {
|
|||||||
window.URL.revokeObjectURL(avatarCropUrl);
|
window.URL.revokeObjectURL(avatarCropUrl);
|
||||||
}
|
}
|
||||||
setAvatarFileName(file.name || 'avatar.png');
|
setAvatarFileName(file.name || 'avatar.png');
|
||||||
|
setAvatarCrop(initialAvatarCrop);
|
||||||
|
setAvatarZoom(1);
|
||||||
|
setAvatarRotation(0);
|
||||||
|
setAvatarCroppedAreaPixels(undefined);
|
||||||
setAvatarCropUrl(window.URL.createObjectURL(file));
|
setAvatarCropUrl(window.URL.createObjectURL(file));
|
||||||
setAvatarCropOpen(true);
|
setAvatarCropOpen(true);
|
||||||
return Upload.LIST_IGNORE;
|
return Upload.LIST_IGNORE;
|
||||||
@ -155,22 +220,24 @@ export default function Profile() {
|
|||||||
}
|
}
|
||||||
setAvatarCropOpen(false);
|
setAvatarCropOpen(false);
|
||||||
setAvatarCropUrl('');
|
setAvatarCropUrl('');
|
||||||
|
setAvatarCrop(initialAvatarCrop);
|
||||||
|
setAvatarZoom(1);
|
||||||
|
setAvatarRotation(0);
|
||||||
|
setAvatarCroppedAreaPixels(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitAvatarCrop = async () => {
|
const submitAvatarCrop = async () => {
|
||||||
const cropper = cropperRef.current?.cropper;
|
if (!avatarCropUrl || !avatarCroppedAreaPixels) {
|
||||||
const canvas = cropper?.getCroppedCanvas({ width: 200, height: 200, imageSmoothingQuality: 'high' });
|
|
||||||
if (!canvas) {
|
|
||||||
message.warning('请先选择头像图片');
|
message.warning('请先选择头像图片');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const blob = await new Promise<Blob | null>(resolve => canvas.toBlob(resolve, 'image/png'));
|
const blob = await getCroppedAvatarBlob(avatarCropUrl, avatarCroppedAreaPixels, avatarRotation);
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
message.error('头像裁剪失败');
|
message.error('头像裁剪失败');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', blob, avatarFileName || 'avatar.png');
|
formData.append('file', blob, `${avatarUploadFileName(avatarFileName)}.png`);
|
||||||
await withAvatarUploading(async () => {
|
await withAvatarUploading(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await uploadOss(formData);
|
const res = await uploadOss(formData);
|
||||||
@ -260,7 +327,7 @@ export default function Profile() {
|
|||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col xs={24} lg={7}>
|
<Col xs={24} lg={7}>
|
||||||
<Card title="个人信息">
|
<Card title="个人信息">
|
||||||
<Space direction="vertical" size={18} style={{ width: '100%' }}>
|
<Space orientation="vertical" size={18} style={{ width: '100%' }}>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<Avatar size={112} src={user.avatar || defaultAvatar} />
|
<Avatar size={112} src={user.avatar || defaultAvatar} />
|
||||||
<div style={{ marginTop: 12 }}>
|
<div style={{ marginTop: 12 }}>
|
||||||
@ -385,7 +452,7 @@ export default function Profile() {
|
|||||||
key: 'thirdParty',
|
key: 'thirdParty',
|
||||||
label: '第三方应用',
|
label: '第三方应用',
|
||||||
children: (
|
children: (
|
||||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
<Space orientation="vertical" size={16} style={{ width: '100%' }}>
|
||||||
<ProTable<SocialAuthVO>
|
<ProTable<SocialAuthVO>
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
columns={authColumns}
|
columns={authColumns}
|
||||||
@ -440,24 +507,31 @@ export default function Profile() {
|
|||||||
>
|
>
|
||||||
{avatarCropUrl && (
|
{avatarCropUrl && (
|
||||||
<div className="avatar-cropper-react">
|
<div className="avatar-cropper-react">
|
||||||
<Cropper
|
<div style={{ position: 'relative', height: 360, width: '100%', background: '#111', borderRadius: 6 }}>
|
||||||
ref={cropperRef}
|
<Cropper
|
||||||
src={avatarCropUrl}
|
image={avatarCropUrl}
|
||||||
viewMode={1}
|
crop={avatarCrop}
|
||||||
aspectRatio={1}
|
zoom={avatarZoom}
|
||||||
guides
|
rotation={avatarRotation}
|
||||||
background={false}
|
aspect={1}
|
||||||
autoCropArea={0.8}
|
cropShape="round"
|
||||||
dragMode="move"
|
showGrid={false}
|
||||||
style={{ height: 360, width: '100%' }}
|
onCropChange={setAvatarCrop}
|
||||||
/>
|
onZoomChange={setAvatarZoom}
|
||||||
<Space style={{ marginTop: 12 }}>
|
onRotationChange={setAvatarRotation}
|
||||||
<Button icon={<RotateLeftOutlined />} onClick={() => cropperRef.current?.cropper.rotate(-90)}>
|
onCropComplete={onAvatarCropComplete}
|
||||||
左旋转
|
/>
|
||||||
</Button>
|
</div>
|
||||||
<Button icon={<RotateRightOutlined />} onClick={() => cropperRef.current?.cropper.rotate(90)}>
|
<Space orientation="vertical" size={12} style={{ marginTop: 12, width: '100%' }}>
|
||||||
右旋转
|
<Space>
|
||||||
</Button>
|
<Button icon={<RotateLeftOutlined />} onClick={() => setAvatarRotation(value => value - 90)}>
|
||||||
|
左旋转
|
||||||
|
</Button>
|
||||||
|
<Button icon={<RotateRightOutlined />} onClick={() => setAvatarRotation(value => value + 90)}>
|
||||||
|
右旋转
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
<Slider min={1} max={3} step={0.1} value={avatarZoom} onChange={setAvatarZoom} />
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -68,7 +68,7 @@ export default function ImportTableModal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal title="导入表" open={open} width={900} onOk={submit} onCancel={closeModal} destroyOnHidden>
|
<Modal title="导入表" open={open} width={900} onOk={submit} onCancel={closeModal} destroyOnHidden>
|
||||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
<Space orientation="vertical" size={12} style={{ width: '100%' }}>
|
||||||
<Select
|
<Select
|
||||||
showSearch
|
showSearch
|
||||||
value={currentDataName}
|
value={currentDataName}
|
||||||
|
|||||||
@ -58,7 +58,7 @@ export default function DefinitionImportModal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal title="部署流程文件" open={open} footer={null} onCancel={onCancel} destroyOnHidden>
|
<Modal title="部署流程文件" open={open} footer={null} onCancel={onCancel} destroyOnHidden>
|
||||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
<Space orientation="vertical" size={12} style={{ width: '100%' }}>
|
||||||
<TreeSelect
|
<TreeSelect
|
||||||
value={uploadCategory}
|
value={uploadCategory}
|
||||||
onChange={setUploadCategory}
|
onChange={setUploadCategory}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user