@jari-ace/element-plus-component 0.3.3 → 0.4.0

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.
Files changed (36) hide show
  1. package/dist/components/upload/FilePreviewer.vue.d.ts +59 -0
  2. package/dist/components/upload/FilePreviewer.vue.d.ts.map +1 -0
  3. package/dist/components/upload/FilePreviewer.vue.js +169 -0
  4. package/dist/components/upload/FilePreviewer.vue.js.map +1 -0
  5. package/dist/components/upload/JaUploader.vue.d.ts +8 -0
  6. package/dist/components/upload/JaUploader.vue.d.ts.map +1 -1
  7. package/dist/components/upload/index.d.ts +35 -0
  8. package/dist/components/upload/index.d.ts.map +1 -1
  9. package/dist/components/upload/index.js +2 -0
  10. package/dist/components/upload/index.js.map +1 -1
  11. package/dist/components/upload/uploader.vue.d.ts +5 -0
  12. package/dist/components/upload/uploader.vue.d.ts.map +1 -1
  13. package/dist/components/upload/uploader.vue.js +14 -4
  14. package/dist/components/upload/uploader.vue.js.map +1 -1
  15. package/dist/components/userPicker/src/JaUserList.vue.d.ts +4 -0
  16. package/dist/components/userPicker/src/JaUserList.vue.d.ts.map +1 -1
  17. package/dist/components/userPicker/src/JaUserList.vue.js +9 -1
  18. package/dist/components/userPicker/src/JaUserList.vue.js.map +1 -1
  19. package/dist/components/userPicker/src/UserPicker.vue.d.ts +10 -0
  20. package/dist/components/userPicker/src/UserPicker.vue.d.ts.map +1 -1
  21. package/dist/components/userPicker/src/UserPicker.vue.js +46 -5
  22. package/dist/components/userPicker/src/UserPicker.vue.js.map +1 -1
  23. package/dist/components/userSelectDialog/src/userSelectDialog.vue.d.ts +19 -2
  24. package/dist/components/userSelectDialog/src/userSelectDialog.vue.d.ts.map +1 -1
  25. package/dist/components/userSelectDialog/src/userSelectDialog.vue.js +58 -12
  26. package/dist/components/userSelectDialog/src/userSelectDialog.vue.js.map +1 -1
  27. package/lib/index.css +1 -1
  28. package/lib/index.js +6654 -6476
  29. package/lib/index.umd.cjs +31 -26
  30. package/package.json +1 -1
  31. package/packages/components/upload/FilePreviewer.vue +245 -0
  32. package/packages/components/upload/index.ts +2 -0
  33. package/packages/components/upload/uploader.vue +68 -55
  34. package/packages/components/userPicker/src/JaUserList.vue +14 -2
  35. package/packages/components/userPicker/src/UserPicker.vue +47 -4
  36. package/packages/components/userSelectDialog/src/userSelectDialog.vue +44 -7
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jari-ace/element-plus-component",
3
3
  "private": false,
4
- "version": "0.3.3",
4
+ "version": "0.4.0",
5
5
  "main": "lib/index.umd.cjs",
6
6
  "module": "lib/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -0,0 +1,245 @@
1
+ <!--
2
+ - 版权所有 (c) 2026 江苏杰瑞信息科技有限公司
3
+ -
4
+ - 此文件属于JARI.ACE平台项目,修改、分发必须遵守杰瑞信科商业软件许可协议
5
+ -
6
+ -->
7
+
8
+ <!--
9
+ * FilePreviewer 组件
10
+ *
11
+ * 用途:用于根据 attachToken 预览附件列表中的第一个可预览文件
12
+ *
13
+ * 功能特点:
14
+ * - 自动获取附件列表并识别可预览文件
15
+ * - 支持多种文件格式预览(PDF、Office文档、图片等)
16
+ * - 使用 Element Plus 按钮组件,支持完整的外观自定义
17
+ * - 集成 PdfViewerModal 进行文件预览
18
+ *
19
+ * 使用示例:
20
+ *
21
+ * 基础用法:
22
+ * <FilePreviewer attachToken="your-attach-token" />
23
+ *
24
+ * 自定义按钮外观:
25
+ * <FilePreviewer
26
+ * attachToken="your-attach-token"
27
+ * buttonType="primary"
28
+ * buttonSize="medium"
29
+ * buttonText="查看文件"
30
+ * buttonIcon="View"
31
+ * />
32
+ *
33
+ * 高级自定义:
34
+ * <FilePreviewer
35
+ * attachToken="your-attach-token"
36
+ * buttonClass="my-custom-button"
37
+ * buttonRound
38
+ * buttonPlain={false}
39
+ * />
40
+ -->
41
+
42
+ <script setup lang="ts">
43
+ import {createAxiosWithoutCache, type FileInfo, useFilesApi} from "@jari-ace/app-bolts";
44
+ import {computed, ref} from "vue";
45
+ import {ElButton} from "element-plus";
46
+ import PdfViewerModal from "./pdf-viewer/PdfViewerModal.vue";
47
+
48
+ /**
49
+ * FilePreviewer 组件属性定义
50
+ *
51
+ * @property {string} attachToken - 附件令牌,用于获取文件列表(必需)
52
+ * @property {string} [buttonType='warning'] - 按钮类型,可选值:primary/success/warning/danger/info
53
+ * @property {string} [buttonSize='small'] - 按钮尺寸,可选值:large/medium/small
54
+ * @property {boolean} [buttonPlain=true] - 是否使用朴素按钮样式
55
+ * @property {boolean} [buttonRound=false] - 是否圆角按钮
56
+ * @property {boolean} [buttonCircle=false] - 是否圆形按钮
57
+ * @property {boolean} [buttonDisabled=false] - 按钮是否禁用
58
+ * @property {any} [buttonIcon] - 按钮图标,可以是 Element Plus 图标组件
59
+ * @property {string} [buttonText='预览'] - 按钮文本内容
60
+ * @property {string} [buttonClass] - 自定义按钮 CSS 类名
61
+ */
62
+ const props = defineProps<{
63
+ /**
64
+ * 附件令牌
65
+ */
66
+ attachToken: string,
67
+ /**
68
+ * 按钮类型
69
+ */
70
+ buttonType?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'text' | 'default',
71
+ /**
72
+ * 按钮尺寸
73
+ */
74
+ buttonSize?: string,
75
+ /**
76
+ * 按钮是否朴素
77
+ */
78
+ buttonPlain?: boolean,
79
+ /**
80
+ * 按钮是否圆角
81
+ */
82
+ buttonRound?: boolean,
83
+ /**
84
+ * 按钮是否圆形
85
+ */
86
+ buttonCircle?: boolean,
87
+ /**
88
+ * 按钮是否禁用
89
+ */
90
+ buttonDisabled?: boolean,
91
+ /**
92
+ * 按钮图标
93
+ */
94
+ buttonIcon?: any,
95
+ /**
96
+ * 按钮文本
97
+ */
98
+ buttonText?: string,
99
+ /**
100
+ * 按钮类名
101
+ */
102
+ buttonClass?: string,
103
+ }>();
104
+
105
+ /**
106
+ * 组件内部状态管理
107
+ */
108
+ const axios = createAxiosWithoutCache();
109
+ const api = useFilesApi(axios);
110
+ const files = ref<FileInfo[]>([]); // 存储文件列表
111
+ const pdfViewerVisible = ref(false); // 控制 PDF 查看器模态框显示
112
+ const pdfSrc = ref(""); // PDF 文件源地址
113
+ const loading = ref(false); // 加载状态
114
+
115
+ /**
116
+ * 按钮属性计算属性
117
+ * 用于处理 Element Plus 按钮的类型安全
118
+ */
119
+ const buttonProps = computed(() => (
120
+ {
121
+ type: (
122
+ props.buttonType || 'warning'
123
+ ) as any,
124
+ size: (
125
+ props.buttonSize || 'small'
126
+ ) as any
127
+ }
128
+ ));
129
+
130
+ /**
131
+ * 可预览文件扩展名列表
132
+ * 支持多种文档格式:Office文档、PDF、图片、文本等
133
+ */
134
+ const previewableFileExts = [".doc", ".docx", ".xls", ".xlsx", ".wps", ".et",
135
+ ".csv", ".txt", ".rtf", ".odt", "ods", ".mht", ".ppt", ".pptx", ".odp", ".pdf", "xps", "."];
136
+
137
+ /**
138
+ * 计算属性:检查是否存在可预览的文件
139
+ * 遍历文件列表,检查是否有文件扩展名在可预览列表中
140
+ */
141
+ const hasPreviewableFiles = computed(() => {
142
+ return files.value.some(file => previewableFileExts.some(ext => file.fileName.endsWith(ext)));
143
+ });
144
+
145
+ /**
146
+ * 计算属性:获取第一个可预览的文件
147
+ * 用于确定要预览哪个文件
148
+ */
149
+ const firstPreviewableFile = computed(() => {
150
+ return files.value.find(file => previewableFileExts.some(ext => file.fileName.endsWith(ext)));
151
+ });
152
+
153
+ /**
154
+ * 加载文件列表
155
+ * 根据 attachToken 从服务器获取关联的文件列表
156
+ *
157
+ * @async
158
+ * @returns {Promise<void>}
159
+ */
160
+ async function loadFileInfos() {
161
+ if (!props.attachToken) {
162
+ files.value = [];
163
+ return;
164
+ }
165
+
166
+ try {
167
+ loading.value = true;
168
+ files.value = await api.getFileList(props.attachToken);
169
+ } catch (error) {
170
+ console.error('Failed to load file list:', error);
171
+ files.value = [];
172
+ } finally {
173
+ loading.value = false;
174
+ }
175
+ }
176
+
177
+ /**
178
+ * 预览第一个可预览的文件
179
+ * 构建预览 URL 并显示 PDF 查看器模态框
180
+ *
181
+ * @returns {void}
182
+ */
183
+ function previewFirstFile() {
184
+ const file = firstPreviewableFile.value;
185
+ if (!file) {
186
+ console.warn('No previewable files found');
187
+ return;
188
+ }
189
+
190
+ // 构建预览 URL,使用文件 token 或 ID
191
+ pdfSrc.value = new URL("uploads/preview/" + (
192
+ file.token || file.id
193
+ ), location.origin).toString();
194
+ pdfViewerVisible.value = true;
195
+ }
196
+
197
+ /**
198
+ * 组件初始化
199
+ * 自动加载文件列表
200
+ */
201
+ loadFileInfos();
202
+ </script>
203
+
204
+ <template>
205
+ <div class="file-previewer">
206
+ <!--
207
+ * 预览按钮
208
+ * 支持完整的 Element Plus 按钮属性自定义
209
+ * 当没有可预览文件时会自动禁用
210
+ * 点击时触发 previewFirstFile 函数
211
+ -->
212
+ <ElButton
213
+ :type="buttonProps.type"
214
+ :size="buttonProps.size"
215
+ :plain="buttonPlain !== undefined ? buttonPlain : true"
216
+ :round="buttonRound"
217
+ :circle="buttonCircle"
218
+ :loading="loading"
219
+ :disabled="buttonDisabled || !hasPreviewableFiles"
220
+ :icon="buttonIcon"
221
+ :class="buttonClass"
222
+ @click="previewFirstFile"
223
+ link>
224
+ {{ buttonText || '预览' }}
225
+ </ElButton>
226
+
227
+ <!--
228
+ * PDF 查看器模态框
229
+ * 用于显示可预览文件的内容
230
+ * 支持全屏显示和响应式布局
231
+ -->
232
+ <PdfViewerModal :src="pdfSrc" v-model="pdfViewerVisible"></PdfViewerModal>
233
+ </div>
234
+ </template>
235
+
236
+ <style lang="scss" scoped>
237
+ /**
238
+ * FilePreviewer 组件样式
239
+ * 使用内联块级显示,使按钮可以与其他元素并排显示
240
+ * 保持简洁的样式,让按钮本身的样式起主导作用
241
+ */
242
+ .file-previewer {
243
+ display: inline-block;
244
+ }
245
+ </style>
@@ -1,6 +1,8 @@
1
1
  import { withInstall } from '../../utils/install'
2
2
  import Upload from './uploader.vue'
3
3
  import _JaUploader from './JaUploader.vue'
4
+ import _FilePreviewer from './FilePreviewer.vue'
4
5
  export const JaUploader = withInstall(_JaUploader);
5
6
  export const JaUploaderRaw = withInstall(Upload);
7
+ export const JaFilePreviewer = withInstall(_FilePreviewer);
6
8
  export * from './types'
@@ -6,21 +6,21 @@ import {
6
6
  useLoginUser,
7
7
  type ClassificationLevel, type UploadInitParams, useLoading
8
8
  } from "@jari-ace/app-bolts";
9
- import {nextTick, onMounted, onUnmounted, ref, watch} from "vue";
10
- import {ArrowDown, Download, Upload} from "@element-plus/icons-vue";
9
+ import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
10
+ import { ArrowDown, Download, Upload } from "@element-plus/icons-vue";
11
11
  import {
12
12
  useSystemClassificationLevelMap,
13
13
  useSystemClassificationLevels
14
14
  } from "../../hooks/useClassificationLevels";
15
15
  import Ace = Jari.Ace;
16
- import {ElMessageBox} from "element-plus";
16
+ import { ElMessageBox } from "element-plus";
17
17
  import "@uppy/core/css/style.min.css";
18
18
  import "@uppy/dashboard/css/style.min.css";
19
19
  import "@uppy/audio/css/style.min.css";
20
20
  import "@uppy/screen-capture/css/style.min.css";
21
21
  import "@uppy/webcam/css/style.min.css";
22
22
  import "@uppy/image-editor/css/style.min.css";
23
- import Uppy, {type Meta, type UppyFile} from "@uppy/core";
23
+ import Uppy, { type Meta, type UppyFile } from "@uppy/core";
24
24
  import Tus from "@uppy/tus";
25
25
  import Webcam from "@uppy/webcam";
26
26
  import ScreenCapture from "@uppy/screen-capture";
@@ -41,7 +41,7 @@ import {
41
41
  ElIcon,
42
42
  ElTag
43
43
  } from "element-plus";
44
- import type {AceFile} from "./types";
44
+ import type { AceFile } from "./types";
45
45
 
46
46
  const props = defineProps<{
47
47
  /**
@@ -138,8 +138,14 @@ const emits = defineEmits<{
138
138
  * 单个文件上传失败事件
139
139
  */
140
140
  uploadError: [file: AceFile, error: Error]
141
-
142
-
141
+ /**
142
+ * 下载完成事件
143
+ */
144
+ downloaded: [file: FileInfo]
145
+ /**
146
+ * 打包下载完成事件
147
+ */
148
+ zipDownloaded: [file: FileInfo[]]
143
149
  }>();
144
150
  let uppy: Uppy | undefined;
145
151
  curAttachToken.value = props.attachToken;
@@ -150,6 +156,13 @@ onMounted(async () => {
150
156
  classificationLevels.value = await useSystemClassificationLevels();
151
157
  classificationLevelMap.value = await useSystemClassificationLevelMap();
152
158
  allowedClassificationLevels.value = classificationLevels.value.filter(l => l.value >= cl);
159
+ console.log(`附件控件密级调试:
160
+ 表单密级:`, props.classificationLevel, `
161
+ 用户密级:`, user.value?.classifiedLevel, `
162
+ 表单/用户密级比较后取最低密级:`, cl, `
163
+ 系统密级:`, classificationLevels.value, `
164
+ 最终可选密级:`, allowedClassificationLevels.value
165
+ )
153
166
  if (props.attachToken) {
154
167
  await initLoad();
155
168
  } else {
@@ -188,7 +201,7 @@ function createUppyInstance() {
188
201
  withCredentials: true,
189
202
  retryDelays: undefined,
190
203
  chunkSize: !uploadInitParams.value?.chunkSize || uploadInitParams.value?.chunkSize
191
- == 0 ? Infinity : uploadInitParams.value?.chunkSize,
204
+ == 0 ? Infinity : uploadInitParams.value?.chunkSize,
192
205
  parallelUploads: 1,
193
206
  headers: {
194
207
  aceAttachId: attachId.value!,
@@ -264,17 +277,17 @@ function createUppyInstance() {
264
277
  emits('upload', uploadID, files);
265
278
  }).on('progress', progress => {
266
279
  emits('progress', progress)
267
- }).on('upload-progress', file=> {
280
+ }).on('upload-progress', file => {
268
281
  emits('uploadProgress', file)
269
- }).on('upload-pause', (file, isPaused)=>{
282
+ }).on('upload-pause', (file, isPaused) => {
270
283
  emits("uploadPause", file, isPaused)
271
284
  }).on('upload-success', file => {
272
285
  emits('uploadSuccess', file)
273
- }).on('complete', (result)=> {
286
+ }).on('complete', (result) => {
274
287
  emits('complete', result.successful, result.failed)
275
- }).on('error', (error)=> {
288
+ }).on('error', (error) => {
276
289
  emits('error', error)
277
- }).on('upload-error', (file,error)=> {
290
+ }).on('upload-error', (file, error) => {
278
291
  emits('uploadError', file, error)
279
292
  })
280
293
  }
@@ -292,9 +305,9 @@ async function onUploadBtnClick() {
292
305
  if (dashboard) {
293
306
  dashboard.setOptions({
294
307
  note: (
295
- cfg.uploadNote ? cfg.uploadNote : ""
296
- )
297
- + `(正在上传${classificationLevelMap.value?.get(selectedFileClassificationLevel.value)}文件)`
308
+ cfg.uploadNote ? cfg.uploadNote : ""
309
+ )
310
+ + `(正在上传${classificationLevelMap.value?.get(selectedFileClassificationLevel.value)}文件)`
298
311
  });
299
312
  }
300
313
  dashboard?.openModal();
@@ -317,9 +330,9 @@ function handleClassificationLevelSelCmd(level: number) {
317
330
  if (cfg) {
318
331
  dashboard.setOptions({
319
332
  note: (
320
- cfg.uploadNote ? cfg.uploadNote : ""
321
- )
322
- + `(正在上传${classificationLevelMap.value?.get(selectedFileClassificationLevel.value)}文件)`
333
+ cfg.uploadNote ? cfg.uploadNote : ""
334
+ )
335
+ + `(正在上传${classificationLevelMap.value?.get(selectedFileClassificationLevel.value)}文件)`
323
336
  });
324
337
  }
325
338
  dashboard?.openModal();
@@ -357,8 +370,11 @@ function previewFile(fileToken: string) {
357
370
  pdfViewerVisible.value = true;
358
371
  }
359
372
 
360
- function downloadFile(fileToken: string) {
361
- api.downloadFile(fileToken);
373
+ async function downloadFile(fileToken: string) {
374
+ await api.downloadFile(fileToken);
375
+ var file = files.value.filter(f => fileToken === f.token)
376
+ if (file && file.length > 0)
377
+ emits('downloaded', file[0]);
362
378
  }
363
379
 
364
380
  function viewUppyDashboard() {
@@ -368,11 +384,12 @@ function viewUppyDashboard() {
368
384
  }
369
385
  }
370
386
 
371
- function downloadAll() {
387
+ async function downloadAll() {
372
388
  if (!curAttachToken.value) {
373
389
  return;
374
390
  }
375
- api.downloadZipFile(curAttachToken.value);
391
+ await api.downloadZipFile(curAttachToken.value);
392
+ emits('zipDownloaded', files.value);
376
393
  }
377
394
 
378
395
  function checkAllowUpload() {
@@ -451,42 +468,39 @@ watch(() => props.attachToken, () => {
451
468
  <template>
452
469
  <div class="ja-uploader">
453
470
  <div class="ja-uploader-tools">
454
- <el-button plain type="success" :icon="Upload" @click="onUploadBtnClick"
455
- :loading="loading" :disabled="loading" size="small"
456
- v-if="allowedClassificationLevels.length <= 1 && checkAllowUpload()">
471
+ <el-button plain type="success" :icon="Upload" @click="onUploadBtnClick" :loading="loading"
472
+ :disabled="loading" size="small" v-if="allowedClassificationLevels.length <= 1 && checkAllowUpload()">
457
473
  上传{{ uploadInitParams?.fileConfig.enableClassifiedLevel ? "(非密)" : "" }}
458
474
  </el-button>
459
- <el-button plain :icon="Download" @click="downloadAll" v-if="checkAllowDownload()"
460
- :loading="loading" :disabled="loading || files?.length === 0" size="small">
475
+ <el-button plain :icon="Download" @click="downloadAll" v-if="checkAllowDownload()" :loading="loading"
476
+ :disabled="loading || files?.length === 0" size="small">
461
477
  全部下载
462
478
  </el-button>
463
- <el-dropdown v-if="allowedClassificationLevels.length > 1 && checkAllowUpload()"
464
- @command="handleClassificationLevelSelCmd" size="small">
479
+ <el-dropdown v-if="allowedClassificationLevels.length > 1 && checkAllowUpload()"
480
+ @command="handleClassificationLevelSelCmd" size="small">
465
481
  <el-button type="success" size="small">
466
482
  上传
467
483
  <el-icon class="el-icon--right">
468
- <arrow-down/>
484
+ <arrow-down />
469
485
  </el-icon>
470
486
  </el-button>
471
487
  <template #dropdown>
472
488
  <el-dropdown-menu>
473
- <el-dropdown-item v-for="cl in allowedClassificationLevels"
474
- :key="cl.value"
475
- :command="cl.value">{{ cl.label }}
489
+ <el-dropdown-item v-for="cl in allowedClassificationLevels" :key="cl.value"
490
+ :command="cl.value">{{ cl.label }}
476
491
  </el-dropdown-item>
477
492
  </el-dropdown-menu>
478
493
  </template>
479
494
  </el-dropdown>
480
- <div class="container" @click="viewUppyDashboard"
481
- v-if="uploadingProgress < 100 && uploadingProgress > 0">
495
+ <div class="container" @click="viewUppyDashboard" v-if="uploadingProgress < 100 && uploadingProgress > 0">
482
496
  <p>
483
497
  <span class="loader-shimmer">正在上传,已完成{{ uploadingProgress }}%...</span>
484
498
  </p>
485
499
  </div>
486
500
  </div>
487
- <el-table :data="files" :show-header="true" style="width: 100%" empty-text="暂无文件"
488
- :height="props.height" :max-height="props.maxHeight">
489
- <el-table-column prop="fileName"/>
501
+ <el-table :data="files" :show-header="true" style="width: 100%" empty-text="暂无文件" :height="props.height"
502
+ :max-height="props.maxHeight">
503
+ <el-table-column prop="fileName" />
490
504
  <el-table-column prop="fileSize" width="100">
491
505
  <template #default="scope">
492
506
  {{ prettyBytes(scope.row.fileSize) }}
@@ -495,7 +509,7 @@ watch(() => props.attachToken, () => {
495
509
  <el-table-column prop="classifiedLevel" width="60">
496
510
  <template #default="scope">
497
511
  <el-tag
498
- :type="scope.row.classifiedLevel < 50? ( scope.row.classifiedLevel < 30 ? 'danger' : 'warning'): 'info'">
512
+ :type="scope.row.classifiedLevel < 50 ? (scope.row.classifiedLevel < 30 ? 'danger' : 'warning') : 'info'">
499
513
  {{ classificationLevelMap?.get(scope.row.classifiedLevel) }}
500
514
  </el-tag>
501
515
  </template>
@@ -503,13 +517,11 @@ watch(() => props.attachToken, () => {
503
517
  <el-table-column align="right" width="150" fixed="right">
504
518
  <template #default="scope">
505
519
  <el-button link type="warning" @click="previewFile(scope.row.token)"
506
- v-if="checkAllowPreview(scope.row)">预览
520
+ v-if="checkAllowPreview(scope.row)">预览
507
521
  </el-button>
508
- <el-button link type="primary" @click="downloadFile(scope.row.token)"
509
- v-if="checkAllowDownload()">下载
522
+ <el-button link type="primary" @click="downloadFile(scope.row.token)" v-if="checkAllowDownload()">下载
510
523
  </el-button>
511
- <el-button link type="danger" @click="delUploadedFile(scope.row)"
512
- v-if="checkAllowDelete()">删除
524
+ <el-button link type="danger" @click="delUploadedFile(scope.row)" v-if="checkAllowDelete()">删除
513
525
  </el-button>
514
526
  </template>
515
527
  </el-table-column>
@@ -552,7 +564,8 @@ watch(() => props.attachToken, () => {
552
564
  font-size: 0.9em;
553
565
  font-weight: 500;
554
566
  position: relative;
555
- overflow: hidden; /* 隐藏闪光效果的溢出 */
567
+ overflow: hidden;
568
+ /* 隐藏闪光效果的溢出 */
556
569
  vertical-align: middle;
557
570
  }
558
571
 
@@ -560,17 +573,17 @@ watch(() => props.attachToken, () => {
560
573
  content: '';
561
574
  position: absolute;
562
575
  top: 0;
563
- left: -100%; /* 从左侧外部开始 */
564
- width: 50%; /* 闪光宽度 */
576
+ left: -100%;
577
+ /* 从左侧外部开始 */
578
+ width: 50%;
579
+ /* 闪光宽度 */
565
580
  height: 100%;
566
581
 
567
582
  /* 闪光效果:透明 -> 亮白色 -> 透明 */
568
- background: linear-gradient(
569
- 90deg,
583
+ background: linear-gradient(90deg,
570
584
  transparent,
571
585
  rgba(255, 255, 255, 0.8),
572
- transparent
573
- );
586
+ transparent);
574
587
 
575
588
  /* 动画: 1.5秒, 缓入缓出, 无限循环 */
576
589
  animation: shimmer 1.5s ease-in-out infinite;
@@ -580,11 +593,11 @@ watch(() => props.attachToken, () => {
580
593
  0% {
581
594
  left: -100%;
582
595
  }
596
+
583
597
  100% {
584
- left: 150%; /* 移动到右侧外部 */
598
+ left: 150%;
599
+ /* 移动到右侧外部 */
585
600
  }
586
601
  }
587
602
  }
588
-
589
-
590
603
  </style>
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
 
3
- import {ElEmpty, ElIcon, ElScrollbar, vLoading} from "element-plus";
3
+ import {ElEmpty, ElIcon, ElScrollbar, vLoading, ElMessage} from "element-plus";
4
4
  import {Check} from "@element-plus/icons-vue";
5
5
  import {JaUserInfoTag} from "../../userTag";
6
6
  import {computed, nextTick, onUnmounted, ref, watch} from "vue";
@@ -14,7 +14,11 @@ const props = defineProps<{
14
14
  pageSize: number,
15
15
  height?: number | string,
16
16
  loading: boolean,
17
- users: UserReference[]
17
+ users: UserReference[],
18
+ /**
19
+ * 最多选择数量,为0则不限制
20
+ */
21
+ maxSelect?: number
18
22
  }>();
19
23
  const activeIndex = ref(0);
20
24
  const scrollbar = ref<InstanceType<typeof ElScrollbar>>();
@@ -50,6 +54,14 @@ function switchUserSelect(u: UserReference) {
50
54
  }
51
55
  const index = selectedUsers.value.findIndex(su => su.id == u.id);
52
56
  if (index < 0) {
57
+ if (props.maxSelect && props.maxSelect > 0 && selectedUsers.value.length >= props.maxSelect) {
58
+ ElMessage({
59
+ message: `已超过最大选择数量 ${props.maxSelect}`,
60
+ plain: true,
61
+ type: 'warning',
62
+ })
63
+ return;
64
+ }
53
65
  selectedUsers.value.push(u);
54
66
  } else {
55
67
  selectedUsers.value.splice(index, 1);