@ebiz/designer-components 0.1.15 → 0.1.16

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ebiz/designer-components",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,417 @@
1
+ <template>
2
+ <div class="ebiz-file-list">
3
+ <div v-if="computedFiles.length === 0" class="empty-state">
4
+ <t-icon name="file" size="48px" />
5
+ <p>暂无文件</p>
6
+ </div>
7
+ <div v-else class="file-list-container">
8
+ <div
9
+ v-for="(file, index) in computedFiles"
10
+ :key="index"
11
+ class="file-item"
12
+ @click="handleFileClick(file)"
13
+ >
14
+ <!-- 图片文件直接显示缩略图 -->
15
+ <div v-if="isImage(file.extension)" class="file-icon">
16
+ <t-image
17
+ :src="file.url"
18
+ :alt="file.name"
19
+ fit="cover"
20
+ loading="lazy"
21
+ :style="{ width: imageSize, height: imageSize, borderRadius: '4px' }"
22
+ @error="handleImageError"
23
+ />
24
+ </div>
25
+
26
+ <!-- 非图片文件显示图标 -->
27
+ <div v-else class="file-icon">
28
+ <t-icon :name="getFileIcon(file.extension)" :size="imageSize" :style="{ color: getFileColor(file.extension) }" />
29
+ </div>
30
+
31
+ <div class="file-info">
32
+ <div class="file-name" :title="file.name">{{ file.name }}</div>
33
+ <div class="file-meta">
34
+ <span v-if="showFileType" class="file-type">{{ file.extension?.toUpperCase() || '未知' }}</span>
35
+ <span v-if="showFileSize && file.size" class="file-size">{{ formatFileSize(file.size) }}</span>
36
+ </div>
37
+ </div>
38
+
39
+ <!-- 操作按钮 -->
40
+ <div v-if="showActions" class="file-actions">
41
+ <t-button
42
+ size="small"
43
+ variant="text"
44
+ @click.stop="handleDownload(file)"
45
+ :title="isImage(file.extension) ? '查看' : '下载'"
46
+ >
47
+ <t-icon :name="isImage(file.extension) ? 'view-list' : 'download'" />
48
+ </t-button>
49
+ <t-button
50
+ v-if="allowDelete"
51
+ size="small"
52
+ variant="text"
53
+ theme="danger"
54
+ @click.stop="handleDelete(file, index)"
55
+ title="删除"
56
+ >
57
+ <t-icon name="delete" />
58
+ </t-button>
59
+ </div>
60
+ </div>
61
+ </div>
62
+
63
+ <!-- 图片预览弹窗 -->
64
+ <t-image-viewer
65
+ v-model:visible="previewVisible"
66
+ :images="previewImages"
67
+ :index="previewIndex"
68
+ :close-on-esc="true"
69
+ @close="previewVisible = false"
70
+ />
71
+ </div>
72
+ </template>
73
+
74
+ <script>
75
+ export default {
76
+ name: "EbizFileList"
77
+ }
78
+ </script>
79
+
80
+ <script setup>
81
+ import { defineProps, defineEmits, computed, ref } from 'vue';
82
+ import { Image as TImage, ImageViewer as TImageViewer, Icon as TIcon, Button as TButton } from 'tdesign-vue-next';
83
+
84
+ const props = defineProps({
85
+ // 文件列表数据,支持数组或字符串(逗号分隔)
86
+ files: {
87
+ type: [Array, String],
88
+ default: () => []
89
+ },
90
+ // 图片尺寸
91
+ imageSize: {
92
+ type: String,
93
+ default: '50px'
94
+ },
95
+ // 是否显示文件大小
96
+ showFileSize: {
97
+ type: Boolean,
98
+ default: true
99
+ },
100
+ // 是否显示文件类型
101
+ showFileType: {
102
+ type: Boolean,
103
+ default: true
104
+ },
105
+ // 是否显示操作按钮
106
+ showActions: {
107
+ type: Boolean,
108
+ default: true
109
+ },
110
+ // 是否允许删除
111
+ allowDelete: {
112
+ type: Boolean,
113
+ default: false
114
+ },
115
+ // 文件基础URL(用于相对路径)
116
+ baseUrl: {
117
+ type: String,
118
+ default: ''
119
+ }
120
+ });
121
+
122
+ const emit = defineEmits(['file-click', 'file-download', 'file-delete', 'file-preview']);
123
+
124
+ // 预览相关状态
125
+ const previewVisible = ref(false);
126
+ const previewImages = ref([]);
127
+ const previewIndex = ref(0);
128
+
129
+ // 获取完整URL
130
+ const getFullUrl = (url) => {
131
+ if (!url) return '';
132
+ if (url.startsWith('http') || url.startsWith('data:')) return url;
133
+ return props.baseUrl + url;
134
+ };
135
+
136
+ // 提取文件名
137
+ const extractFileName = (url) => {
138
+ if (!url) return '未知文件';
139
+ const parts = url.split('/');
140
+ return parts[parts.length - 1] || '未知文件';
141
+ };
142
+
143
+ // 获取文件扩展名
144
+ const getFileExtension = (filename) => {
145
+ if (!filename) return '';
146
+ const parts = filename.split('.');
147
+ return parts.length > 1 ? parts.pop().toLowerCase() : '';
148
+ };
149
+
150
+ // 解析文件信息
151
+ const parseFileInfo = (url) => {
152
+ const fullUrl = getFullUrl(url);
153
+ const fileName = extractFileName(url);
154
+ const extension = getFileExtension(fileName);
155
+
156
+ return {
157
+ name: fileName,
158
+ url: fullUrl,
159
+ extension,
160
+ size: null
161
+ };
162
+ };
163
+
164
+ // 计算文件列表
165
+ const computedFiles = computed(() => {
166
+ let fileList = [];
167
+
168
+ if (typeof props.files === 'string') {
169
+ // 字符串格式,按逗号分割
170
+ const urls = props.files.split(',').map(url => url.trim()).filter(url => url);
171
+ fileList = urls.map(url => parseFileInfo(url));
172
+ } else if (Array.isArray(props.files)) {
173
+ // 数组格式
174
+ fileList = props.files.map(file => {
175
+ if (typeof file === 'string') {
176
+ return parseFileInfo(file);
177
+ } else {
178
+ return {
179
+ name: file.name || extractFileName(file.url),
180
+ url: getFullUrl(file.url),
181
+ size: file.size,
182
+ extension: file.extension || getFileExtension(file.url || file.name),
183
+ ...file
184
+ };
185
+ }
186
+ });
187
+ }
188
+
189
+ return fileList;
190
+ });
191
+
192
+ // 判断是否为图片
193
+ const isImage = (extension) => {
194
+ const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'];
195
+ return imageExtensions.includes(extension?.toLowerCase());
196
+ };
197
+
198
+ // 获取文件图标
199
+ const getFileIcon = (extension) => {
200
+ const iconMap = {
201
+ // 文档类
202
+ 'pdf': 'file-pdf',
203
+ 'doc': 'file-word',
204
+ 'docx': 'file-word',
205
+ 'xls': 'file-excel',
206
+ 'xlsx': 'file-excel',
207
+ 'ppt': 'file-powerpoint',
208
+ 'pptx': 'file-powerpoint',
209
+ 'txt': 'file-text',
210
+ 'rtf': 'file-text',
211
+ // 代码类
212
+ 'js': 'file-code',
213
+ 'ts': 'file-code',
214
+ 'html': 'file-code',
215
+ 'css': 'file-code',
216
+ 'json': 'file-code',
217
+ 'xml': 'file-code',
218
+ // 压缩类
219
+ 'zip': 'file-zip',
220
+ 'rar': 'file-zip',
221
+ '7z': 'file-zip',
222
+ 'tar': 'file-zip',
223
+ 'gz': 'file-zip',
224
+ // 音视频类
225
+ 'mp3': 'file-music',
226
+ 'wav': 'file-music',
227
+ 'mp4': 'file-video',
228
+ 'avi': 'file-video',
229
+ 'mov': 'file-video',
230
+ 'wmv': 'file-video'
231
+ };
232
+
233
+ return iconMap[extension?.toLowerCase()] || 'file';
234
+ };
235
+
236
+ // 获取文件颜色
237
+ const getFileColor = (extension) => {
238
+ const colorMap = {
239
+ 'pdf': '#FF4757',
240
+ 'doc': '#2E5BFF',
241
+ 'docx': '#2E5BFF',
242
+ 'xls': '#00A854',
243
+ 'xlsx': '#00A854',
244
+ 'ppt': '#FF8C42',
245
+ 'pptx': '#FF8C42',
246
+ 'txt': '#666666',
247
+ 'zip': '#9C27B0',
248
+ 'rar': '#9C27B0',
249
+ 'mp3': '#FF9800',
250
+ 'mp4': '#795548'
251
+ };
252
+
253
+ return colorMap[extension?.toLowerCase()] || '#666666';
254
+ };
255
+
256
+ // 格式化文件大小
257
+ const formatFileSize = (bytes) => {
258
+ if (!bytes) return '';
259
+ const units = ['B', 'KB', 'MB', 'GB'];
260
+ let size = bytes;
261
+ let unitIndex = 0;
262
+
263
+ while (size >= 1024 && unitIndex < units.length - 1) {
264
+ size /= 1024;
265
+ unitIndex++;
266
+ }
267
+
268
+ return `${size.toFixed(1)} ${units[unitIndex]}`;
269
+ };
270
+
271
+ // 是否可在线预览
272
+ const canPreviewOnline = (extension) => {
273
+ const onlinePreviewExtensions = ['pdf', 'txt', 'html', 'xml', 'json'];
274
+ return onlinePreviewExtensions.includes(extension?.toLowerCase());
275
+ };
276
+
277
+ // 文件下载
278
+ const handleDownload = (file) => {
279
+ const link = document.createElement('a');
280
+ link.href = file.url;
281
+ link.download = file.name;
282
+ link.target = '_blank';
283
+ document.body.appendChild(link);
284
+ link.click();
285
+ document.body.removeChild(link);
286
+
287
+ emit('file-download', file);
288
+ };
289
+
290
+ // 图片预览
291
+ const handlePreview = (file) => {
292
+ const imageFiles = computedFiles.value.filter(f => isImage(f.extension));
293
+ previewImages.value = imageFiles.map(f => f.url);
294
+ previewIndex.value = imageFiles.findIndex(f => f.url === file.url);
295
+ previewVisible.value = true;
296
+
297
+ emit('file-preview', file);
298
+ };
299
+
300
+ // 文件点击处理
301
+ const handleFileClick = (file) => {
302
+ emit('file-click', file);
303
+
304
+ if (isImage(file.extension)) {
305
+ // 图片文件,显示预览
306
+ handlePreview(file);
307
+ } else if (canPreviewOnline(file.extension)) {
308
+ // 可在线预览的文件
309
+ window.open(file.url, '_blank');
310
+ } else {
311
+ // 其他文件,触发下载
312
+ handleDownload(file);
313
+ }
314
+ };
315
+
316
+ // 文件删除
317
+ const handleDelete = (file, index) => {
318
+ emit('file-delete', file, index);
319
+ };
320
+
321
+ // 图片加载错误处理
322
+ const handleImageError = () => {
323
+ // 图片加载失败时的处理
324
+ };
325
+ </script>
326
+
327
+ <style lang="less" scoped>
328
+ .ebiz-file-list {
329
+ width: 100%;
330
+
331
+ .empty-state {
332
+ display: flex;
333
+ flex-direction: column;
334
+ align-items: center;
335
+ justify-content: center;
336
+ padding: 40px;
337
+ color: #999;
338
+
339
+ p {
340
+ margin: 8px 0 0 0;
341
+ font-size: 14px;
342
+ }
343
+ }
344
+
345
+ .file-list-container {
346
+ display: flex;
347
+ flex-direction: column;
348
+ gap: 8px;
349
+ }
350
+
351
+ .file-item {
352
+ display: flex;
353
+ align-items: center;
354
+ padding: 8px 12px;
355
+ border: 1px solid #e6e6e6;
356
+ border-radius: 6px;
357
+ cursor: pointer;
358
+ transition: all 0.3s;
359
+ background: #fff;
360
+
361
+ &:hover {
362
+ border-color: #0052d9;
363
+ box-shadow: 0 2px 8px rgba(0, 82, 217, 0.15);
364
+
365
+ .file-actions {
366
+ opacity: 1;
367
+ }
368
+ }
369
+ }
370
+
371
+ .file-icon {
372
+ margin-right: 12px;
373
+ flex-shrink: 0;
374
+ display: flex;
375
+ align-items: center;
376
+ justify-content: center;
377
+ width: v-bind(imageSize);
378
+ height: v-bind(imageSize);
379
+ }
380
+
381
+ .file-info {
382
+ flex: 1;
383
+ min-width: 0;
384
+
385
+ .file-name {
386
+ font-size: 14px;
387
+ font-weight: 500;
388
+ color: #333;
389
+ margin-bottom: 3px;
390
+ overflow: hidden;
391
+ text-overflow: ellipsis;
392
+ white-space: nowrap;
393
+ }
394
+
395
+ .file-meta {
396
+ display: flex;
397
+ gap: 8px;
398
+
399
+ .file-size,
400
+ .file-type {
401
+ font-size: 12px;
402
+ color: #999;
403
+ }
404
+ }
405
+ }
406
+
407
+ .file-actions {
408
+ display: flex;
409
+ gap: 4px;
410
+ opacity: 0;
411
+ transition: opacity 0.3s;
412
+ flex-shrink: 0;
413
+ }
414
+ }
415
+
416
+
417
+ </style>
package/src/index.js CHANGED
@@ -95,6 +95,7 @@ import EbizMeetingRoomSelector from './components/EbizMeetingRoomSelector.vue'
95
95
  import EbizMobileMeetingRoomSelector from './components/EbizMobileMeetingRoomSelector.vue'
96
96
  import EbizDropdown from './components/EbizDropdown.vue'
97
97
  import EbizDropdownItem from './components/EbizDropdownItem.vue'
98
+ import EbizFileList from './components/EbizFileList.vue'
98
99
 
99
100
  // 导出组件
100
101
  export {
@@ -223,6 +224,8 @@ export {
223
224
  EbizDropdown,
224
225
  // 下拉菜单项组件
225
226
  EbizDropdownItem,
227
+ // 文件列表组件
228
+ EbizFileList,
226
229
  // 会议室选择器组件
227
230
  EbizMeetingRoomSelector,
228
231
  // 新增 EbizMobileMeetingRoomSelector 组件
@@ -376,6 +376,12 @@ const routes = [
376
376
  name: 'MobileMeetingRoomSelector',
377
377
  component: () => import('../views/EbizMobileMeetingRoomSelectorDemo.vue'),
378
378
  meta: { title: 'Ebiz移动端会议室选择器组件示例' }
379
+ },
380
+ {
381
+ path: '/file-list-demo',
382
+ name: 'FileListDemo',
383
+ component: () => import('../views/EbizFileListDemo.vue'),
384
+ meta: { title: 'Ebiz文件列表组件示例' }
379
385
  }
380
386
  ]
381
387