@jiangood/open-admin 3.0.11 → 3.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/framework/components/DownloadModal/index.tsx +1 -2
- package/src/framework/fields/FieldUploadFile/index.tsx +1 -18
- package/src/framework/fields/FieldUploadImage/index.tsx +334 -0
- package/src/framework/fields/index.ts +1 -0
- package/src/framework/views/ViewImage/index.tsx +8 -4
- package/src/layouts/index.tsx +1 -0
- package/src/pages/system/article/index.jsx +2 -2
- package/src/pages/system/file/index.jsx +94 -32
- package/src/pages/system/job/index.jsx +16 -16
- package/src/pages/userCenter/index.jsx +1 -9
package/package.json
CHANGED
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
"@ant-design/icons": "^6.0.0",
|
|
15
15
|
"@tinymce/tinymce-react": "^6.0.0",
|
|
16
16
|
"antd": "^6.0.0",
|
|
17
|
-
"antd-img-crop": "^4.23.0",
|
|
18
17
|
"axios": "^1.13.2",
|
|
18
|
+
"compressorjs": "^1.2.1",
|
|
19
|
+
"cropperjs": "^1.6.2",
|
|
19
20
|
"dayjs": "^1.11.13",
|
|
20
21
|
"lodash": "^4.17.23",
|
|
21
22
|
"qs": "^6.14.0",
|
|
@@ -48,5 +49,5 @@
|
|
|
48
49
|
"test:e2e": "playwright test",
|
|
49
50
|
"test:e2e:ui": "playwright test --ui"
|
|
50
51
|
},
|
|
51
|
-
"version": "3.0.
|
|
52
|
+
"version": "3.0.12"
|
|
52
53
|
}
|
|
@@ -281,8 +281,7 @@ export class DownloadModal extends React.Component<DownloadModalProps, ModalStat
|
|
|
281
281
|
<div style={{padding: '20px 0'}}>
|
|
282
282
|
{/* 文件名 */}
|
|
283
283
|
<div style={{marginBottom: 16, fontSize: 15, fontWeight: 500, color: '#333'}}>
|
|
284
|
-
|
|
285
|
-
{fileName || '未知文件'}
|
|
284
|
+
文件名:{fileName || '未知文件'}
|
|
286
285
|
</div>
|
|
287
286
|
|
|
288
287
|
{/* 下载中 */}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import ImgCrop from "antd-img-crop";
|
|
3
|
-
import type {ImgCropProps} from "antd-img-crop";
|
|
4
2
|
import {message, Modal, Upload} from "antd";
|
|
5
3
|
import type {UploadChangeParam, UploadFile, UploadProps} from "antd";
|
|
6
4
|
import { UploadOutlined } from "@ant-design/icons";
|
|
@@ -14,10 +12,6 @@ export type SysUploadFile = UploadFile & { objectName?: string };
|
|
|
14
12
|
export interface FieldUploadFileProps extends FieldProps<string> {
|
|
15
13
|
/** 最大上传数量,默认 1 */
|
|
16
14
|
maxCount?: number;
|
|
17
|
-
/** 是否启用上传前裁切图片 */
|
|
18
|
-
cropImage?: boolean;
|
|
19
|
-
/** 裁切配置(antd-img-crop 的 cropperProps) */
|
|
20
|
-
cropperProps?: ImgCropProps['cropperProps'];
|
|
21
15
|
/** 上传列表的内建样式,默认 picture-card */
|
|
22
16
|
listType?: UploadProps['listType'];
|
|
23
17
|
/** 接受的文件类型,如 image/* */
|
|
@@ -30,7 +24,6 @@ export interface FieldUploadFileProps extends FieldProps<string> {
|
|
|
30
24
|
|
|
31
25
|
interface FieldUploadFileState {
|
|
32
26
|
maxCount: number;
|
|
33
|
-
cropImage: boolean;
|
|
34
27
|
fileList: SysUploadFile[];
|
|
35
28
|
/** 逗号分隔的文件 objectName */
|
|
36
29
|
value: string | null;
|
|
@@ -39,16 +32,13 @@ interface FieldUploadFileState {
|
|
|
39
32
|
}
|
|
40
33
|
|
|
41
34
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* 可参考 react-easy-crop
|
|
35
|
+
* 通用文件上传(不含图片压缩/裁切,图片上传请使用 FieldUploadImage)
|
|
45
36
|
*/
|
|
46
37
|
export class FieldUploadFile extends React.Component<FieldUploadFileProps, FieldUploadFileState & { errorTitle?: string; errorContent?: string; previewObjectName?: string }> {
|
|
47
38
|
|
|
48
39
|
state: FieldUploadFileState & { errorTitle?: string; errorContent?: string; previewObjectName?: string } = {
|
|
49
40
|
// 传入的参数
|
|
50
41
|
maxCount: 1,
|
|
51
|
-
cropImage: false,
|
|
52
42
|
visibility: 'public',
|
|
53
43
|
|
|
54
44
|
// 内部参数
|
|
@@ -65,7 +55,6 @@ export class FieldUploadFile extends React.Component<FieldUploadFileProps, Field
|
|
|
65
55
|
componentDidUpdate(prevProps: FieldUploadFileProps) {
|
|
66
56
|
const next: Partial<FieldUploadFileState> = {};
|
|
67
57
|
if (this.props.maxCount !== prevProps.maxCount) next.maxCount = this.props.maxCount;
|
|
68
|
-
if (this.props.cropImage !== prevProps.cropImage) next.cropImage = this.props.cropImage;
|
|
69
58
|
if (this.props.visibility !== prevProps.visibility) next.visibility = this.props.visibility;
|
|
70
59
|
|
|
71
60
|
const prevValue = prevProps.value ?? null;
|
|
@@ -142,12 +131,6 @@ export class FieldUploadFile extends React.Component<FieldUploadFileProps, Field
|
|
|
142
131
|
};
|
|
143
132
|
|
|
144
133
|
render() {
|
|
145
|
-
if (this.state.cropImage) {
|
|
146
|
-
return <ImgCrop cropperProps={this.props.cropperProps} modalTitle={'裁剪图片'} fillColor={null}>
|
|
147
|
-
{this.getUpload()}
|
|
148
|
-
</ImgCrop>;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
134
|
return <>
|
|
152
135
|
{this.getUpload()}
|
|
153
136
|
<Modal open={!!this.state.errorTitle} title={this.state.errorTitle} okText="确定"
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import React, {useCallback, useEffect, useRef, useState} from "react";
|
|
2
|
+
import {Alert, Button, Modal, Space, Upload, message} from "antd";
|
|
3
|
+
import {DeleteOutlined, PlusOutlined, ScissorOutlined} from "@ant-design/icons";
|
|
4
|
+
import Compressor from "compressorjs";
|
|
5
|
+
import Cropper from "cropperjs";
|
|
6
|
+
import "cropperjs/dist/cropper.css";
|
|
7
|
+
import {HttpUtils} from "../../utils";
|
|
8
|
+
import {UrlUtils} from "../../utils";
|
|
9
|
+
import type {FieldProps} from '../types';
|
|
10
|
+
|
|
11
|
+
interface FieldUploadImageProps extends FieldProps<string> {
|
|
12
|
+
/** 最大上传数量,默认 1 */
|
|
13
|
+
maxCount?: number;
|
|
14
|
+
/** 压缩图目标宽度(强制目标比例),默认 800 */
|
|
15
|
+
targetWidth?: number;
|
|
16
|
+
/** 压缩图目标高度(强制目标比例),默认 600 */
|
|
17
|
+
targetHeight?: number;
|
|
18
|
+
/** 缩略图最长边,默认 300 */
|
|
19
|
+
thumbWidth?: number;
|
|
20
|
+
/** 压缩质量,默认 0.8 */
|
|
21
|
+
quality?: number;
|
|
22
|
+
/** 文件可见性,默认 public */
|
|
23
|
+
visibility?: 'public' | 'private';
|
|
24
|
+
/** 接受的文件类型 */
|
|
25
|
+
accept?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface Dims {
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 弹窗内预览结果:压缩图 + 缩略图 */
|
|
34
|
+
interface PreviewResult {
|
|
35
|
+
cUrl: string;
|
|
36
|
+
tUrl: string;
|
|
37
|
+
cFile: File;
|
|
38
|
+
tFile: File;
|
|
39
|
+
cSize: number;
|
|
40
|
+
tSize: number;
|
|
41
|
+
cdims: Dims;
|
|
42
|
+
tdims: Dims;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatSize(bytes: number): string {
|
|
46
|
+
if (bytes < 1024) return bytes + ' B';
|
|
47
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
48
|
+
return (bytes / 1024 / 1024).toFixed(2) + ' MB';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readDims(url: string): Promise<Dims> {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
const img = new Image();
|
|
54
|
+
img.onload = () => resolve({width: img.naturalWidth, height: img.naturalHeight});
|
|
55
|
+
img.onerror = () => resolve({width: 0, height: 0});
|
|
56
|
+
img.src = url;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function compressToFile(source: Blob, maxWidth: number, maxHeight: number, quality: number): Promise<File> {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
new Compressor(source, {
|
|
63
|
+
maxWidth,
|
|
64
|
+
maxHeight,
|
|
65
|
+
quality,
|
|
66
|
+
success(result) {
|
|
67
|
+
const file = result instanceof File ? result : new File([result], 'image.jpg', {type: result.type || 'image/jpeg'});
|
|
68
|
+
resolve(file);
|
|
69
|
+
},
|
|
70
|
+
error: reject,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function FieldUploadImage(props: FieldUploadImageProps) {
|
|
76
|
+
const {
|
|
77
|
+
value, onChange, maxCount = 1, targetWidth = 800, targetHeight = 600,
|
|
78
|
+
thumbWidth = 300, quality = 0.8, visibility = 'public', accept = 'image/*',
|
|
79
|
+
} = props;
|
|
80
|
+
|
|
81
|
+
const [objectNames, setObjectNames] = useState<string[]>(() => (value ? value.split(',') : []));
|
|
82
|
+
const [modalOpen, setModalOpen] = useState(false);
|
|
83
|
+
const [originalUrl, setOriginalUrl] = useState<string>();
|
|
84
|
+
const [originalDims, setOriginalDims] = useState<Dims>();
|
|
85
|
+
const [originalSize, setOriginalSize] = useState(0);
|
|
86
|
+
const [preview, setPreview] = useState<PreviewResult>();
|
|
87
|
+
const [needsCrop, setNeedsCrop] = useState(false);
|
|
88
|
+
const [cropperActive, setCropperActive] = useState(false);
|
|
89
|
+
const [uploading, setUploading] = useState(false);
|
|
90
|
+
const [fullPreviewUrl, setFullPreviewUrl] = useState<string>();
|
|
91
|
+
|
|
92
|
+
const imgRef = useRef<HTMLImageElement>(null);
|
|
93
|
+
const cropperRef = useRef<Cropper>();
|
|
94
|
+
const selectedFileRef = useRef<File>();
|
|
95
|
+
const previewUrlsRef = useRef<string[]>([]);
|
|
96
|
+
|
|
97
|
+
const targetRatio = targetWidth / targetHeight;
|
|
98
|
+
|
|
99
|
+
// 父组件 value 变化时同步
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
const parsed = value ? value.split(',') : [];
|
|
102
|
+
setObjectNames(parsed);
|
|
103
|
+
}, [value]);
|
|
104
|
+
|
|
105
|
+
const revokePreviewUrls = useCallback(() => {
|
|
106
|
+
previewUrlsRef.current.forEach((u) => URL.revokeObjectURL(u));
|
|
107
|
+
previewUrlsRef.current = [];
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
110
|
+
const closeModal = useCallback(() => {
|
|
111
|
+
if (cropperRef.current) {
|
|
112
|
+
cropperRef.current.destroy();
|
|
113
|
+
cropperRef.current = undefined;
|
|
114
|
+
}
|
|
115
|
+
setModalOpen(false);
|
|
116
|
+
setCropperActive(false);
|
|
117
|
+
setNeedsCrop(false);
|
|
118
|
+
setUploading(false);
|
|
119
|
+
setOriginalUrl(undefined);
|
|
120
|
+
setOriginalDims(undefined);
|
|
121
|
+
setOriginalSize(0);
|
|
122
|
+
setPreview(undefined);
|
|
123
|
+
if (originalUrl) URL.revokeObjectURL(originalUrl);
|
|
124
|
+
revokePreviewUrls();
|
|
125
|
+
}, [originalUrl, revokePreviewUrls]);
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 基于源文件生成压缩图 + 缩略图(含各自尺寸/大小),用于弹窗展示
|
|
129
|
+
*/
|
|
130
|
+
const regenerate = useCallback(async (source: Blob) => {
|
|
131
|
+
const cFile = await compressToFile(source, targetWidth, targetHeight, quality);
|
|
132
|
+
const tFile = await compressToFile(cFile, thumbWidth, thumbWidth, quality);
|
|
133
|
+
const cUrl = URL.createObjectURL(cFile);
|
|
134
|
+
const tUrl = URL.createObjectURL(tFile);
|
|
135
|
+
const cdims = await readDims(cUrl);
|
|
136
|
+
const tdims = await readDims(tUrl);
|
|
137
|
+
revokePreviewUrls();
|
|
138
|
+
previewUrlsRef.current = [cUrl, tUrl];
|
|
139
|
+
setPreview({cUrl, tUrl, cFile, tFile, cSize: cFile.size, tSize: tFile.size, cdims, tdims});
|
|
140
|
+
}, [quality, revokePreviewUrls, targetHeight, targetWidth, thumbWidth]);
|
|
141
|
+
|
|
142
|
+
const handleBeforeUpload = useCallback(async (file: File) => {
|
|
143
|
+
if (objectNames.length >= maxCount) {
|
|
144
|
+
message.warning('已达到最大上传数量');
|
|
145
|
+
return Upload.LIST_IGNORE;
|
|
146
|
+
}
|
|
147
|
+
if (!file.type.startsWith('image/')) {
|
|
148
|
+
message.error('请选择图片文件');
|
|
149
|
+
return Upload.LIST_IGNORE;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
selectedFileRef.current = file;
|
|
153
|
+
const url = URL.createObjectURL(file);
|
|
154
|
+
setOriginalUrl(url);
|
|
155
|
+
setOriginalSize(file.size);
|
|
156
|
+
setModalOpen(true);
|
|
157
|
+
setCropperActive(false);
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
// EXIF 方向感知地读取原始尺寸
|
|
161
|
+
const bitmap = await createImageBitmap(file, {imageOrientation: 'from-image'});
|
|
162
|
+
const dims = {width: bitmap.width, height: bitmap.height};
|
|
163
|
+
bitmap.close();
|
|
164
|
+
setOriginalDims(dims);
|
|
165
|
+
const ratioDiff = Math.abs(dims.width / dims.height - targetRatio);
|
|
166
|
+
setNeedsCrop(ratioDiff / targetRatio > 0.02);
|
|
167
|
+
} catch (e) {
|
|
168
|
+
message.error('读取图片失败');
|
|
169
|
+
closeModal();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await regenerate(file);
|
|
174
|
+
return Upload.LIST_IGNORE;
|
|
175
|
+
}, [closeModal, maxCount, objectNames.length, regenerate, targetRatio]);
|
|
176
|
+
|
|
177
|
+
const startCrop = useCallback(() => {
|
|
178
|
+
setCropperActive(true);
|
|
179
|
+
setTimeout(() => {
|
|
180
|
+
const el = imgRef.current;
|
|
181
|
+
if (!el) return;
|
|
182
|
+
if (cropperRef.current) cropperRef.current.destroy();
|
|
183
|
+
cropperRef.current = new Cropper(el, {
|
|
184
|
+
aspectRatio: targetRatio,
|
|
185
|
+
viewMode: 1,
|
|
186
|
+
autoCropArea: 1,
|
|
187
|
+
dragMode: 'move',
|
|
188
|
+
});
|
|
189
|
+
}, 0);
|
|
190
|
+
}, [targetRatio]);
|
|
191
|
+
|
|
192
|
+
const confirmCrop = useCallback(() => {
|
|
193
|
+
const cropper = cropperRef.current;
|
|
194
|
+
const file = selectedFileRef.current;
|
|
195
|
+
if (!cropper || !file) return;
|
|
196
|
+
const canvas = cropper.getCroppedCanvas();
|
|
197
|
+
const mime = file.type || 'image/jpeg';
|
|
198
|
+
canvas.toBlob(async (blob) => {
|
|
199
|
+
if (cropperRef.current) {
|
|
200
|
+
cropperRef.current.destroy();
|
|
201
|
+
cropperRef.current = undefined;
|
|
202
|
+
}
|
|
203
|
+
setCropperActive(false);
|
|
204
|
+
setNeedsCrop(false);
|
|
205
|
+
if (blob) {
|
|
206
|
+
const croppedFile = new File([blob], 'cropped.jpg', {type: mime});
|
|
207
|
+
await regenerate(croppedFile);
|
|
208
|
+
}
|
|
209
|
+
}, mime, quality);
|
|
210
|
+
}, [quality, regenerate]);
|
|
211
|
+
|
|
212
|
+
const cancelCrop = useCallback(() => {
|
|
213
|
+
if (cropperRef.current) {
|
|
214
|
+
cropperRef.current.destroy();
|
|
215
|
+
cropperRef.current = undefined;
|
|
216
|
+
}
|
|
217
|
+
setCropperActive(false);
|
|
218
|
+
}, []);
|
|
219
|
+
|
|
220
|
+
const handleConfirm = useCallback(async () => {
|
|
221
|
+
if (needsCrop || !preview) return;
|
|
222
|
+
setUploading(true);
|
|
223
|
+
try {
|
|
224
|
+
const fd = new FormData();
|
|
225
|
+
fd.append('file', preview.cFile);
|
|
226
|
+
fd.append('thumb', preview.tFile);
|
|
227
|
+
fd.append('visibility', visibility);
|
|
228
|
+
const rs = await HttpUtils.post('admin/sysFile/uploadImage', fd, null, {headers: {'Content-Type': 'multipart/form-data'}});
|
|
229
|
+
const newNames = [...objectNames, rs.objectName];
|
|
230
|
+
setObjectNames(newNames);
|
|
231
|
+
onChange?.(newNames.join(','));
|
|
232
|
+
closeModal();
|
|
233
|
+
} catch (e) {
|
|
234
|
+
message.error(HttpUtils.extractErrorMessage(e));
|
|
235
|
+
} finally {
|
|
236
|
+
setUploading(false);
|
|
237
|
+
}
|
|
238
|
+
}, [closeModal, needsCrop, objectNames, onChange, preview, visibility]);
|
|
239
|
+
|
|
240
|
+
const removeImage = useCallback((name: string) => {
|
|
241
|
+
const newNames = objectNames.filter((n) => n !== name);
|
|
242
|
+
setObjectNames(newNames);
|
|
243
|
+
onChange?.(newNames.join(','));
|
|
244
|
+
}, [objectNames, onChange]);
|
|
245
|
+
|
|
246
|
+
const renderPanel = (title: string, url: string | undefined, dims: Dims | undefined, size: number | undefined) => (
|
|
247
|
+
<div style={{flex: 1, textAlign: 'center'}}>
|
|
248
|
+
<div style={{fontWeight: 600, marginBottom: 8}}>{title}</div>
|
|
249
|
+
{url ? (
|
|
250
|
+
<img src={url} style={{maxWidth: '100%', maxHeight: 160, display: 'block', margin: '0 auto'}}/>
|
|
251
|
+
) : (
|
|
252
|
+
<div style={{height: 120, lineHeight: '120px', color: '#999'}}>生成中...</div>
|
|
253
|
+
)}
|
|
254
|
+
<div style={{color: '#666', marginTop: 8}}>
|
|
255
|
+
{dims ? `${dims.width} x ${dims.height}` : '--'}
|
|
256
|
+
{size != null && size > 0 && <span> · {formatSize(size)}</span>}
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<>
|
|
263
|
+
<div>
|
|
264
|
+
{objectNames.map((name) => (
|
|
265
|
+
<div key={name} style={{position: 'relative', display: 'inline-block', marginRight: 8, verticalAlign: 'top'}}>
|
|
266
|
+
<img
|
|
267
|
+
src={UrlUtils.contextPath(`/file/${name}?thumb=1`)}
|
|
268
|
+
width={80}
|
|
269
|
+
height={80}
|
|
270
|
+
style={{objectFit: 'cover', borderRadius: 4, cursor: 'pointer'}}
|
|
271
|
+
onClick={() => setFullPreviewUrl(UrlUtils.contextPath(`/file/${name}`))}
|
|
272
|
+
alt={name}
|
|
273
|
+
/>
|
|
274
|
+
<Button
|
|
275
|
+
size="small"
|
|
276
|
+
danger
|
|
277
|
+
icon={<DeleteOutlined/>}
|
|
278
|
+
style={{position: 'absolute', top: -8, right: -8}}
|
|
279
|
+
onClick={() => removeImage(name)}
|
|
280
|
+
/>
|
|
281
|
+
</div>
|
|
282
|
+
))}
|
|
283
|
+
{objectNames.length < maxCount && (
|
|
284
|
+
<Upload accept={accept} showUploadList={false} beforeUpload={handleBeforeUpload} multiple={false}>
|
|
285
|
+
<Button icon={<PlusOutlined/>}>选择图片</Button>
|
|
286
|
+
</Upload>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<Modal
|
|
291
|
+
open={modalOpen}
|
|
292
|
+
title="图片预览"
|
|
293
|
+
width={900}
|
|
294
|
+
okText="确定"
|
|
295
|
+
cancelText="取消"
|
|
296
|
+
onOk={handleConfirm}
|
|
297
|
+
onCancel={closeModal}
|
|
298
|
+
confirmLoading={uploading}
|
|
299
|
+
okButtonProps={{disabled: needsCrop}}
|
|
300
|
+
>
|
|
301
|
+
{cropperActive ? (
|
|
302
|
+
<div>
|
|
303
|
+
{originalUrl && <img ref={imgRef} src={originalUrl} style={{maxWidth: '100%'}} alt="待裁切"/>}
|
|
304
|
+
<Space style={{marginTop: 12}}>
|
|
305
|
+
<Button type="primary" onClick={confirmCrop}>确认裁切</Button>
|
|
306
|
+
<Button onClick={cancelCrop}>取消裁切</Button>
|
|
307
|
+
</Space>
|
|
308
|
+
</div>
|
|
309
|
+
) : (
|
|
310
|
+
<>
|
|
311
|
+
{needsCrop && (
|
|
312
|
+
<Alert type="warning" showIcon
|
|
313
|
+
message="图片比例与目标尺寸不符,请点击下方「裁切」按钮按比例处理后再确定"
|
|
314
|
+
style={{marginBottom: 12}}/>
|
|
315
|
+
)}
|
|
316
|
+
<div style={{display: 'flex', gap: 16}}>
|
|
317
|
+
{renderPanel('原图', originalUrl, originalDims, originalSize)}
|
|
318
|
+
{renderPanel('压缩图', preview?.cUrl, preview?.cdims, preview?.cSize)}
|
|
319
|
+
{renderPanel('缩略图', preview?.tUrl, preview?.tdims, preview?.tSize)}
|
|
320
|
+
</div>
|
|
321
|
+
<Button icon={<ScissorOutlined/>} onClick={startCrop} style={{marginTop: 12}}>裁切</Button>
|
|
322
|
+
</>
|
|
323
|
+
)}
|
|
324
|
+
</Modal>
|
|
325
|
+
|
|
326
|
+
<Modal open={!!fullPreviewUrl} title="图片预览" width="70vw" footer={null}
|
|
327
|
+
onCancel={() => setFullPreviewUrl(undefined)}>
|
|
328
|
+
{fullPreviewUrl && <img src={fullPreviewUrl} style={{maxWidth: '100%'}} alt="预览"/>}
|
|
329
|
+
</Modal>
|
|
330
|
+
</>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export default FieldUploadImage;
|
|
@@ -22,31 +22,35 @@ export class ViewImage extends React.Component {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const urlList = [];
|
|
25
|
+
const thumbList = [];
|
|
25
26
|
for (let v of vs) {
|
|
26
27
|
const isAbsUrl = v.startsWith('http');
|
|
27
28
|
const isDataUrl = v.startsWith('data:');
|
|
28
29
|
const isObjectName = v.startsWith('public/') || v.startsWith('private/');
|
|
29
30
|
if (isAbsUrl || isDataUrl) {
|
|
30
31
|
urlList.push(v);
|
|
32
|
+
thumbList.push(v);
|
|
31
33
|
continue;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
if (isObjectName) {
|
|
35
37
|
urlList.push(UrlUtils.contextPath('/file/' + v));
|
|
38
|
+
thumbList.push(UrlUtils.contextPath('/file/' + v + '?thumb=1'));
|
|
36
39
|
continue;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
urlList.push(UrlUtils.contextPath(v));
|
|
43
|
+
thumbList.push(UrlUtils.contextPath(v));
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
const closeModal = () => this.setState({modalOpen: false});
|
|
43
47
|
|
|
44
|
-
const imgs =
|
|
48
|
+
const imgs = thumbList.map((thumb, i) => (
|
|
45
49
|
<img
|
|
46
50
|
style={{ display: 'inline-block' }}
|
|
47
|
-
key={
|
|
48
|
-
src={
|
|
49
|
-
onClick={() => this.setState({modalOpen: true, previewUrl:
|
|
51
|
+
key={urlList[i]}
|
|
52
|
+
src={thumb}
|
|
53
|
+
onClick={() => this.setState({modalOpen: true, previewUrl: urlList[i]})}
|
|
50
54
|
width={60}
|
|
51
55
|
height={60}
|
|
52
56
|
/>
|
package/src/layouts/index.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
FieldBoolean,
|
|
7
7
|
FieldDictSelect,
|
|
8
8
|
FieldEditor,
|
|
9
|
-
|
|
9
|
+
FieldUploadImage,
|
|
10
10
|
FormModal,
|
|
11
11
|
HttpUtils,
|
|
12
12
|
Page,
|
|
@@ -136,7 +136,7 @@ export default class extends React.Component {
|
|
|
136
136
|
</Form.Item>
|
|
137
137
|
|
|
138
138
|
<Form.Item label='主图' name='mainImage'>
|
|
139
|
-
<
|
|
139
|
+
<FieldUploadImage maxCount={1}/>
|
|
140
140
|
</Form.Item>
|
|
141
141
|
|
|
142
142
|
<Form.Item label='内容' name='content'>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import {Button, Form, Input, Modal} from 'antd'
|
|
2
|
+
import {Button, Empty, Form, Input, Modal, Pagination, Tabs} from 'antd'
|
|
3
3
|
import {CloudUploadOutlined} from "@ant-design/icons";
|
|
4
4
|
|
|
5
5
|
import {PermActions, DictUtils, FieldDateRange, FieldDictSelect, FieldUploadFile, HttpUtils, Page, ProTable, UrlUtils} from "../../../framework";
|
|
@@ -8,7 +8,12 @@ export default class extends React.Component {
|
|
|
8
8
|
|
|
9
9
|
state = {
|
|
10
10
|
formOpen: false,
|
|
11
|
-
formValues: {}
|
|
11
|
+
formValues: {},
|
|
12
|
+
gridPage: 1,
|
|
13
|
+
gridList: [],
|
|
14
|
+
gridTotal: 0,
|
|
15
|
+
gridLoading: false,
|
|
16
|
+
previewUrl: undefined,
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
tableRef = React.createRef()
|
|
@@ -20,6 +25,16 @@ export default class extends React.Component {
|
|
|
20
25
|
})
|
|
21
26
|
}
|
|
22
27
|
|
|
28
|
+
loadGrid = page => {
|
|
29
|
+
const p = page || this.state.gridPage
|
|
30
|
+
this.setState({gridLoading: true})
|
|
31
|
+
HttpUtils.get('admin/sysFile/page', {page: p, size: 50, type: 'image'}).then(rs => {
|
|
32
|
+
this.setState({gridList: rs.content || [], gridTotal: Number(rs.totalElements || 0), gridPage: p})
|
|
33
|
+
}).finally(() => {
|
|
34
|
+
this.setState({gridLoading: false})
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
columns = [
|
|
24
39
|
{
|
|
25
40
|
title: '原始名称',
|
|
@@ -85,14 +100,32 @@ export default class extends React.Component {
|
|
|
85
100
|
},
|
|
86
101
|
]
|
|
87
102
|
|
|
88
|
-
|
|
89
|
-
|
|
103
|
+
renderUploadModal = () => (
|
|
104
|
+
<Modal open={this.state.formOpen} title='上传文件'
|
|
105
|
+
width={800}
|
|
106
|
+
onCancel={() => {
|
|
107
|
+
this.setState({formOpen: false})
|
|
108
|
+
this.tableRef.current.reload()
|
|
109
|
+
}}
|
|
110
|
+
footer={null}
|
|
111
|
+
destroyOnHidden
|
|
112
|
+
>
|
|
113
|
+
<Form ref={this.formRef}
|
|
114
|
+
initialValues={this.state.formValues}
|
|
115
|
+
>
|
|
116
|
+
<Form.Item name='文件'>
|
|
117
|
+
<FieldUploadFile accept="*/*"/>
|
|
118
|
+
</Form.Item>
|
|
119
|
+
</Form>
|
|
120
|
+
</Modal>
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
renderTable = () => (
|
|
124
|
+
<>
|
|
90
125
|
<ProTable
|
|
91
126
|
actionRef={this.tableRef}
|
|
92
127
|
toolBarRender={() => (
|
|
93
|
-
<
|
|
94
|
-
<Button type='primary' perm='sys-file:upload' icon={<CloudUploadOutlined/>} onClick={() => this.setState({formOpen: true})}>上传文件</Button>
|
|
95
|
-
</PermActions>
|
|
128
|
+
<Button type='primary' icon={<CloudUploadOutlined/>} onClick={() => this.setState({formOpen: true})}>上传文件</Button>
|
|
96
129
|
)}
|
|
97
130
|
request={(params) => {
|
|
98
131
|
return HttpUtils.get('admin/sysFile/page', params);
|
|
@@ -110,39 +143,68 @@ export default class extends React.Component {
|
|
|
110
143
|
<Form.Item label='状态' name='status'>
|
|
111
144
|
<FieldDictSelect typeCode='fileStatus'/>
|
|
112
145
|
</Form.Item>
|
|
113
|
-
<Form.Item label='类型' name='type'>
|
|
114
|
-
<FieldDictSelect typeCode='materialType'/>
|
|
115
|
-
</Form.Item>
|
|
116
146
|
<Form.Item label='上传时间' name='dateRange'>
|
|
117
147
|
<FieldDateRange/>
|
|
118
148
|
</Form.Item>
|
|
119
149
|
</>
|
|
120
150
|
)}
|
|
121
151
|
/>
|
|
152
|
+
{this.renderUploadModal()}
|
|
153
|
+
</>
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
renderImageGrid = () => {
|
|
157
|
+
const {gridList, gridTotal, gridPage, gridLoading, previewUrl} = this.state
|
|
158
|
+
return (
|
|
159
|
+
<div style={{minHeight: 200, position: 'relative'}}>
|
|
160
|
+
{gridList.length === 0 && !gridLoading && <Empty style={{marginTop: 80}} description="暂无图片"/>}
|
|
161
|
+
<div style={{display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', gap: 8}}>
|
|
162
|
+
{gridList.map(item => (
|
|
163
|
+
<div key={item.objectName}
|
|
164
|
+
style={{borderRadius: 4, overflow: 'hidden', cursor: 'pointer', border: '1px solid #eee'}}
|
|
165
|
+
onClick={() => this.setState({previewUrl: UrlUtils.contextPath('/file/' + item.objectName)})}>
|
|
166
|
+
<img
|
|
167
|
+
src={UrlUtils.contextPath('/file/' + item.objectName + '?thumb=1')}
|
|
168
|
+
style={{width: '100%', height: 120, objectFit: 'cover', display: 'block'}}
|
|
169
|
+
alt={item.originName || item.objectName}
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
))}
|
|
173
|
+
</div>
|
|
174
|
+
{gridTotal > 0 && (
|
|
175
|
+
<div style={{display: 'flex', justifyContent: 'flex-end', marginTop: 16}}>
|
|
176
|
+
<Pagination
|
|
177
|
+
current={gridPage}
|
|
178
|
+
pageSize={50}
|
|
179
|
+
total={gridTotal}
|
|
180
|
+
showTotal={(total) => `共 ${total} 张`}
|
|
181
|
+
onChange={(page) => this.loadGrid(page)}
|
|
182
|
+
/>
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
122
185
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
destroyOnHidden
|
|
131
|
-
>
|
|
132
|
-
<Form ref={this.formRef}
|
|
133
|
-
initialValues={this.state.formValues}
|
|
134
|
-
>
|
|
135
|
-
<Form.Item name='文件'>
|
|
136
|
-
<FieldUploadFile accept="*/*"/>
|
|
137
|
-
</Form.Item>
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
</Form>
|
|
141
|
-
</Modal>
|
|
186
|
+
<Modal open={!!previewUrl} title="图片预览" width="70vw" footer={null}
|
|
187
|
+
onCancel={() => this.setState({previewUrl: undefined})}>
|
|
188
|
+
{previewUrl && <img src={previewUrl} style={{maxWidth: '100%'}} alt="预览"/>}
|
|
189
|
+
</Modal>
|
|
190
|
+
</div>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
142
193
|
|
|
194
|
+
render() {
|
|
195
|
+
return <Page title="文件管理" description="管理系统上传文件">
|
|
196
|
+
<Tabs
|
|
197
|
+
defaultActiveKey="table"
|
|
198
|
+
onChange={(key) => {
|
|
199
|
+
if (key === 'image' && this.state.gridList.length === 0) {
|
|
200
|
+
this.loadGrid(1)
|
|
201
|
+
}
|
|
202
|
+
}}
|
|
203
|
+
items={[
|
|
204
|
+
{key: 'table', label: '全部', children: this.renderTable()},
|
|
205
|
+
{key: 'image', label: '图片', children: this.renderImageGrid()},
|
|
206
|
+
]}
|
|
207
|
+
/>
|
|
143
208
|
</Page>
|
|
144
209
|
}
|
|
145
210
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
Tag
|
|
14
14
|
} from 'antd'
|
|
15
15
|
import {PlusOutlined} from "@ant-design/icons";
|
|
16
|
-
import {FormModal, HttpUtils, Page, PermActions, ProTable,
|
|
16
|
+
import {FormModal, HttpUtils, Page, PermActions, ProTable, UrlUtils, ValueType} from "../../../framework";
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
const cronOptions = [
|
|
@@ -61,7 +61,7 @@ export default class extends React.Component {
|
|
|
61
61
|
HttpUtils.get('admin/job/job-class-options').then(rs => {
|
|
62
62
|
this.setState({jobClassOptions: rs})
|
|
63
63
|
}).catch(e => {
|
|
64
|
-
console.error('[Job]
|
|
64
|
+
console.error('[Job] 加载任务类选项失败:', e);
|
|
65
65
|
})
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -79,7 +79,7 @@ export default class extends React.Component {
|
|
|
79
79
|
HttpUtils.post("admin/job/get-job-param-fields", jobData || {}, {className}).then(rs => {
|
|
80
80
|
this.setState({paramList: rs})
|
|
81
81
|
}).catch(e => {
|
|
82
|
-
console.error('[Job]
|
|
82
|
+
console.error('[Job] 加载任务参数字段失败:', e);
|
|
83
83
|
})
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -91,12 +91,12 @@ export default class extends React.Component {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
handleDelete = row => {
|
|
94
|
-
const hide = message.loading("
|
|
94
|
+
const hide = message.loading("删除任务中...")
|
|
95
95
|
HttpUtils.post('admin/job/delete', {id: row.id}).then(rs => {
|
|
96
96
|
hide();
|
|
97
97
|
this.tableRef.current.reload();
|
|
98
98
|
}).catch(e => {
|
|
99
|
-
console.error('[Job]
|
|
99
|
+
console.error('[Job] 删除任务失败:', e);
|
|
100
100
|
hide();
|
|
101
101
|
})
|
|
102
102
|
}
|
|
@@ -105,7 +105,7 @@ export default class extends React.Component {
|
|
|
105
105
|
HttpUtils.post('admin/job/trigger-job', {id: row.id}).then(rs => {
|
|
106
106
|
this.tableRef.current.reload();
|
|
107
107
|
}).catch(e => {
|
|
108
|
-
console.error('[Job]
|
|
108
|
+
console.error('[Job] 触发任务失败:', e);
|
|
109
109
|
})
|
|
110
110
|
}
|
|
111
111
|
|
|
@@ -117,8 +117,7 @@ export default class extends React.Component {
|
|
|
117
117
|
},
|
|
118
118
|
{
|
|
119
119
|
title: '执行类',
|
|
120
|
-
dataIndex: '
|
|
121
|
-
|
|
120
|
+
dataIndex: 'jobClassName',
|
|
122
121
|
},
|
|
123
122
|
|
|
124
123
|
{
|
|
@@ -181,7 +180,7 @@ export default class extends React.Component {
|
|
|
181
180
|
|
|
182
181
|
|
|
183
182
|
render() {
|
|
184
|
-
return <Page title="
|
|
183
|
+
return <Page title="定时任务" description="管理定时任务">
|
|
185
184
|
<ProTable
|
|
186
185
|
actionRef={this.tableRef}
|
|
187
186
|
toolBarRender={() => (
|
|
@@ -205,11 +204,11 @@ export default class extends React.Component {
|
|
|
205
204
|
/>
|
|
206
205
|
|
|
207
206
|
|
|
208
|
-
<FormModal ref={this.modalRef} title='
|
|
207
|
+
<FormModal ref={this.modalRef} title='定时任务' width={800}
|
|
209
208
|
onFinish={this.onFinish}
|
|
210
209
|
onValuesChange={this.onValuesChange}>
|
|
211
210
|
<Form.Item label='执行类' name='jobClass' rules={[{required: true}]}
|
|
212
|
-
tooltip='
|
|
211
|
+
tooltip='继承 BaseJob,参考 HelloWorldJob'>
|
|
213
212
|
<Select options={this.state.jobClassOptions}/>
|
|
214
213
|
</Form.Item>
|
|
215
214
|
<Form.Item label='名称' name='name' rules={[{required: true}]}>
|
|
@@ -227,7 +226,7 @@ export default class extends React.Component {
|
|
|
227
226
|
|
|
228
227
|
{this.state.paramList?.map(p => (
|
|
229
228
|
<div key={p.name}>
|
|
230
|
-
<Divider
|
|
229
|
+
<Divider>任务参数</Divider>
|
|
231
230
|
<Form.Item label={p.label}
|
|
232
231
|
name={['jobData', p.name]}
|
|
233
232
|
key={p.name}
|
|
@@ -242,7 +241,7 @@ export default class extends React.Component {
|
|
|
242
241
|
))}
|
|
243
242
|
</FormModal>
|
|
244
243
|
|
|
245
|
-
<Modal title='
|
|
244
|
+
<Modal title='定时任务状态'
|
|
246
245
|
open={this.state.statusOpen}
|
|
247
246
|
onCancel={() => this.setState({statusOpen: false})}
|
|
248
247
|
footer={null}
|
|
@@ -252,7 +251,7 @@ export default class extends React.Component {
|
|
|
252
251
|
|
|
253
252
|
</Modal>
|
|
254
253
|
|
|
255
|
-
<Modal title='
|
|
254
|
+
<Modal title='定时任务记录'
|
|
256
255
|
open={this.state.executeRecordOpen}
|
|
257
256
|
onCancel={() => this.setState({executeRecordOpen: false})}
|
|
258
257
|
footer={null}
|
|
@@ -311,8 +310,9 @@ export default class extends React.Component {
|
|
|
311
310
|
const option = this.state.jobClassOptions.find(o => o.value === changed.jobClass)
|
|
312
311
|
if (option) {
|
|
313
312
|
const {label} = option;
|
|
314
|
-
|
|
315
|
-
|
|
313
|
+
const match = label.match(/[((](.*?)[))]/);
|
|
314
|
+
if (match) { // 取括号内中文描述设置为name
|
|
315
|
+
this.modalRef.current.formInstance.setFieldValue("name", match[1])
|
|
316
316
|
}
|
|
317
317
|
}
|
|
318
318
|
}
|
|
@@ -26,15 +26,7 @@ export default class extends React.Component {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
pwdValidator = (rule, value) => {
|
|
29
|
-
return
|
|
30
|
-
HttpUtils.get("admin/sysUser/pwd-strength", {password: value}, {showError: false}).then(response => {
|
|
31
|
-
const rs = response.data
|
|
32
|
-
if (!rs.success) {
|
|
33
|
-
reject(rs.message)
|
|
34
|
-
}
|
|
35
|
-
resolve()
|
|
36
|
-
})
|
|
37
|
-
})
|
|
29
|
+
return HttpUtils.get("admin/sysUser/pwd-strength", {password: value}, {showError: false})
|
|
38
30
|
}
|
|
39
31
|
|
|
40
32
|
render() {
|