@jiangood/open-admin 3.0.10 → 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 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.10"
52
+ "version": "3.0.12"
52
53
  }
@@ -142,6 +142,9 @@ export class DownloadModal extends React.Component<DownloadModalProps, ModalStat
142
142
  }
143
143
  filename = parsedName;
144
144
  }
145
+ if (filename && filename !== this.state.fileName) {
146
+ this.setState({fileName: filename});
147
+ }
145
148
 
146
149
  const url = window.URL.createObjectURL(new Blob([blob]));
147
150
  const link = document.createElement('a');
@@ -278,8 +281,7 @@ export class DownloadModal extends React.Component<DownloadModalProps, ModalStat
278
281
  <div style={{padding: '20px 0'}}>
279
282
  {/* 文件名 */}
280
283
  <div style={{marginBottom: 16, fontSize: 15, fontWeight: 500, color: '#333'}}>
281
- <DownloadOutlined style={{marginRight: 8}}/>
282
- {fileName || '未知文件'}
284
+ 文件名:{fileName || '未知文件'}
283
285
  </div>
284
286
 
285
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;
@@ -16,4 +16,5 @@ export * from './FieldSysOrgTreeSelect'
16
16
  export * from './FieldSysOrgTree'
17
17
  export * from './FieldPercent'
18
18
  export * from './FieldUploadFile';
19
+ export * from './FieldUploadImage';
19
20
  export * from '../biz/OrgTree';
@@ -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 = urlList.map((url) => (
48
+ const imgs = thumbList.map((thumb, i) => (
45
49
  <img
46
50
  style={{ display: 'inline-block' }}
47
- key={url}
48
- src={url}
49
- onClick={() => this.setState({modalOpen: true, previewUrl: url})}
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
  />
@@ -91,6 +91,7 @@ export class Layouts extends React.Component<LayoutsProps> {
91
91
  GlobalData.setSiteArticles(data.siteArticles);
92
92
 
93
93
  if (data.needUpdatePwd) {
94
+ this.setState({loginChecked: true});
94
95
  history.push('/standalone/forceUpdatePwd');
95
96
  return;
96
97
  }
@@ -6,7 +6,7 @@ import {
6
6
  FieldBoolean,
7
7
  FieldDictSelect,
8
8
  FieldEditor,
9
- FieldUploadFile,
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
- <FieldUploadFile maxCount={1} cropImage accept='image/*'/>
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
- render() {
89
- return <Page title="文件管理" description="管理系统上传文件">
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
- <PermActions>
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
- <Modal open={this.state.formOpen} title='上传文件'
124
- width={800}
125
- onCancel={() => {
126
- this.setState({formOpen: false})
127
- this.tableRef.current.reload()
128
- }}
129
- footer={null}
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, StringUtils, UrlUtils, ValueType} from "../../../framework";
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] 加载作业类选项失败:', e);
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] 加载作业参数字段失败:', e);
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] 删除作业失败:', e);
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] 触发作业失败:', e);
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: 'jobClass',
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="作业调度" description="管理定时作业任务">
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='作业调度' width={800}
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='org.quartz.Job接口,参考io.tmgg.job.builtin.DemoJob'>
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>作业参数</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
- if (StringUtils.contains(label, " ")) { // 取中文名部门设置为name
315
- this.modalRef.current.formInstance.setFieldValue("name", label.split(" ")[1])
313
+ const match = label.match(/[((](.*?)[))]/);
314
+ if (match) { // 取括号内中文描述设置为name
315
+ this.modalRef.current.formInstance.setFieldValue("name", match[1])
316
316
  }
317
317
  }
318
318
  }
@@ -157,6 +157,7 @@ export default class extends React.Component {
157
157
  </div>
158
158
  {selectedOrg && (
159
159
  <Descriptions size='small' column={2}>
160
+ <Descriptions.Item label="唯一标识">{selectedOrg.id}</Descriptions.Item>
160
161
  <Descriptions.Item label="名称">{selectedOrg.name}</Descriptions.Item>
161
162
  <Descriptions.Item label="类型">{selectedOrg.typeLabel}</Descriptions.Item>
162
163
  <Descriptions.Item label="上级机构">{selectedOrg.parentName || '-'}</Descriptions.Item>
@@ -26,15 +26,7 @@ export default class extends React.Component {
26
26
  }
27
27
 
28
28
  pwdValidator = (rule, value) => {
29
- return new Promise((resolve, reject) => {
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() {