mirror of
https://gitee.com/JavaLionLi/plus-ui.git
synced 2026-09-13 07:43:43 +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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.2.3",
|
||||
"@ant-design/pro-components": "^3.1.12-0",
|
||||
"@ant-design/icons": "^6.2.5",
|
||||
"@ant-design/pro-components": "3.1.12-0",
|
||||
"@ant-design/x": "^2.7.0",
|
||||
"@iconify/react": "^6.0.2",
|
||||
"@tanstack/react-query": "^5.100.6",
|
||||
"@umijs/max": "^4.6.51",
|
||||
"@wangeditor-next/editor": "^5.6.4",
|
||||
"@wangeditor-next/editor-for-react": "^1.0.10",
|
||||
"@tanstack/react-query": "^5.101.0",
|
||||
"@umijs/max": "^4.6.61",
|
||||
"@wangeditor-next/editor": "^5.7.10",
|
||||
"@wangeditor-next/editor-for-react": "^2.0.2",
|
||||
"ahooks": "^3.9.7",
|
||||
"antd": "^6.4.3",
|
||||
"antd-style": "^4.1.0",
|
||||
"axios": "1.16.1",
|
||||
"axios": "1.17.0",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"clsx": "^2.1.1",
|
||||
"cropperjs": "^1.6.2",
|
||||
"crypto-js": "4.2.0",
|
||||
"dayjs": "^1.11.20",
|
||||
"echarts": "^6.0.0",
|
||||
"echarts-for-react": "^3.0.2",
|
||||
"dayjs": "^1.11.21",
|
||||
"echarts": "^6.1.0",
|
||||
"echarts-for-react": "^3.0.6",
|
||||
"jsencrypt": "3.5.4",
|
||||
"nprogress": "0.2.0",
|
||||
"react": "^19.2.5",
|
||||
"react-cropper": "^2.3.3",
|
||||
"react-dom": "^19.2.5",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-easy-crop": "^5.5.7",
|
||||
"react-json-view-lite": "^2.5.0",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"screenfull": "^6.0.2",
|
||||
@ -48,16 +47,16 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/crypto-js": "4.2.2",
|
||||
"@types/node": "^25.9.1",
|
||||
"@types/node": "^25.9.2",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@umijs/lint": "^4.6.51",
|
||||
"@umijs/lint": "^4.6.61",
|
||||
"@umijs/max-plugin-openapi": "^2.0.3",
|
||||
"cross-env": "^10.1.0",
|
||||
"oxfmt": "^0.51.0",
|
||||
"oxlint": "^1.66.0",
|
||||
"oxfmt": "^0.53.0",
|
||||
"oxlint": "^1.68.0",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"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 '@wangeditor-next/editor/dist/css/style.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';
|
||||
|
||||
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 (
|
||||
<PageContainer title="缓存监控">
|
||||
<Spin spinning={loading}>
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<Space orientation="vertical" size={12} style={{ width: '100%' }}>
|
||||
<ProCard
|
||||
title="缓存概览"
|
||||
extra={
|
||||
|
||||
@ -88,7 +88,7 @@ export default function UserImportModal({ open, onClose, onSuccess }: UserImport
|
||||
<p className="ant-upload-text">将文件拖到此处,或点击上传</p>
|
||||
<p className="ant-upload-hint">仅允许导入 xls、xlsx 格式文件。</p>
|
||||
</Upload.Dragger>
|
||||
<Space direction="vertical" style={{ marginTop: 12 }}>
|
||||
<Space orientation="vertical" style={{ marginTop: 12 }}>
|
||||
<Checkbox checked={updateSupport} onChange={event => setUpdateSupport(event.target.checked)}>
|
||||
是否更新已经存在的用户数据
|
||||
</Checkbox>
|
||||
|
||||
@ -13,13 +13,14 @@ import {
|
||||
Popconfirm,
|
||||
Radio,
|
||||
Row,
|
||||
Slider,
|
||||
Space,
|
||||
Tabs,
|
||||
Upload,
|
||||
type UploadProps
|
||||
} from 'antd';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import Cropper, { type ReactCropperElement } from 'react-cropper';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import Cropper, { type Area, type Point } from 'react-easy-crop';
|
||||
import type { OnlineVO } from '@/api/monitor/online/types';
|
||||
import type { SocialAuthVO } from '@/api/system/social/types';
|
||||
import type { UserInfoVO, UserProfileForm } from '@/api/system/user/types';
|
||||
@ -48,6 +49,8 @@ const socialProviders = [
|
||||
{ source: 'github', label: 'GitHub', icon: githubIcon }
|
||||
];
|
||||
const avatarFileTypes = ['png', 'jpg', 'jpeg'];
|
||||
const avatarOutputSize = 200;
|
||||
const initialAvatarCrop: Point = { x: 0, y: 0 };
|
||||
|
||||
function socialProvider(source?: string) {
|
||||
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) {
|
||||
if (Array.isArray(payload)) return payload;
|
||||
@ -91,10 +145,17 @@ export default function Profile() {
|
||||
const [avatarCropOpen, setAvatarCropOpen] = useState(false);
|
||||
const [avatarCropUrl, setAvatarCropUrl] = useState('');
|
||||
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 genderOptions = useMemo(() => dictOptions(dicts.sys_user_gender), [dicts.sys_user_gender]);
|
||||
|
||||
const onAvatarCropComplete = useCallback((_croppedArea: Area, croppedAreaPixels: Area) => {
|
||||
setAvatarCroppedAreaPixels(croppedAreaPixels);
|
||||
}, []);
|
||||
|
||||
const loadProfile = useCallback(async () => {
|
||||
await withLoading(async () => {
|
||||
const [profileRes, authRes, onlineRes] = await Promise.all([getUserProfile(), getAuthList(), getOnline()]);
|
||||
@ -143,6 +204,10 @@ export default function Profile() {
|
||||
window.URL.revokeObjectURL(avatarCropUrl);
|
||||
}
|
||||
setAvatarFileName(file.name || 'avatar.png');
|
||||
setAvatarCrop(initialAvatarCrop);
|
||||
setAvatarZoom(1);
|
||||
setAvatarRotation(0);
|
||||
setAvatarCroppedAreaPixels(undefined);
|
||||
setAvatarCropUrl(window.URL.createObjectURL(file));
|
||||
setAvatarCropOpen(true);
|
||||
return Upload.LIST_IGNORE;
|
||||
@ -155,22 +220,24 @@ export default function Profile() {
|
||||
}
|
||||
setAvatarCropOpen(false);
|
||||
setAvatarCropUrl('');
|
||||
setAvatarCrop(initialAvatarCrop);
|
||||
setAvatarZoom(1);
|
||||
setAvatarRotation(0);
|
||||
setAvatarCroppedAreaPixels(undefined);
|
||||
};
|
||||
|
||||
const submitAvatarCrop = async () => {
|
||||
const cropper = cropperRef.current?.cropper;
|
||||
const canvas = cropper?.getCroppedCanvas({ width: 200, height: 200, imageSmoothingQuality: 'high' });
|
||||
if (!canvas) {
|
||||
if (!avatarCropUrl || !avatarCroppedAreaPixels) {
|
||||
message.warning('请先选择头像图片');
|
||||
return;
|
||||
}
|
||||
const blob = await new Promise<Blob | null>(resolve => canvas.toBlob(resolve, 'image/png'));
|
||||
const blob = await getCroppedAvatarBlob(avatarCropUrl, avatarCroppedAreaPixels, avatarRotation);
|
||||
if (!blob) {
|
||||
message.error('头像裁剪失败');
|
||||
return;
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('file', blob, avatarFileName || 'avatar.png');
|
||||
formData.append('file', blob, `${avatarUploadFileName(avatarFileName)}.png`);
|
||||
await withAvatarUploading(async () => {
|
||||
try {
|
||||
const res = await uploadOss(formData);
|
||||
@ -260,7 +327,7 @@ export default function Profile() {
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} lg={7}>
|
||||
<Card title="个人信息">
|
||||
<Space direction="vertical" size={18} style={{ width: '100%' }}>
|
||||
<Space orientation="vertical" size={18} style={{ width: '100%' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Avatar size={112} src={user.avatar || defaultAvatar} />
|
||||
<div style={{ marginTop: 12 }}>
|
||||
@ -385,7 +452,7 @@ export default function Profile() {
|
||||
key: 'thirdParty',
|
||||
label: '第三方应用',
|
||||
children: (
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<Space orientation="vertical" size={16} style={{ width: '100%' }}>
|
||||
<ProTable<SocialAuthVO>
|
||||
rowKey="id"
|
||||
columns={authColumns}
|
||||
@ -440,24 +507,31 @@ export default function Profile() {
|
||||
>
|
||||
{avatarCropUrl && (
|
||||
<div className="avatar-cropper-react">
|
||||
<Cropper
|
||||
ref={cropperRef}
|
||||
src={avatarCropUrl}
|
||||
viewMode={1}
|
||||
aspectRatio={1}
|
||||
guides
|
||||
background={false}
|
||||
autoCropArea={0.8}
|
||||
dragMode="move"
|
||||
style={{ height: 360, width: '100%' }}
|
||||
/>
|
||||
<Space style={{ marginTop: 12 }}>
|
||||
<Button icon={<RotateLeftOutlined />} onClick={() => cropperRef.current?.cropper.rotate(-90)}>
|
||||
左旋转
|
||||
</Button>
|
||||
<Button icon={<RotateRightOutlined />} onClick={() => cropperRef.current?.cropper.rotate(90)}>
|
||||
右旋转
|
||||
</Button>
|
||||
<div style={{ position: 'relative', height: 360, width: '100%', background: '#111', borderRadius: 6 }}>
|
||||
<Cropper
|
||||
image={avatarCropUrl}
|
||||
crop={avatarCrop}
|
||||
zoom={avatarZoom}
|
||||
rotation={avatarRotation}
|
||||
aspect={1}
|
||||
cropShape="round"
|
||||
showGrid={false}
|
||||
onCropChange={setAvatarCrop}
|
||||
onZoomChange={setAvatarZoom}
|
||||
onRotationChange={setAvatarRotation}
|
||||
onCropComplete={onAvatarCropComplete}
|
||||
/>
|
||||
</div>
|
||||
<Space orientation="vertical" size={12} style={{ marginTop: 12, width: '100%' }}>
|
||||
<Space>
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -68,7 +68,7 @@ export default function ImportTableModal({
|
||||
|
||||
return (
|
||||
<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
|
||||
showSearch
|
||||
value={currentDataName}
|
||||
|
||||
@ -58,7 +58,7 @@ export default function DefinitionImportModal({
|
||||
|
||||
return (
|
||||
<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
|
||||
value={uploadCategory}
|
||||
onChange={setUploadCategory}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user