xydata-tools-uniapp 1.0.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.
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # xydata-tools-uniapp
2
+
3
+ 鑫义科技 uni-app 组件库,从 `xydata-tools` 独立拆分而来,专注于 uni-app 端(微信小程序 / H5 / App)的组件能力。
4
+
5
+ ## 组件清单
6
+
7
+ - **MediaPicker** - 媒体(图片/视频)选择上传组件,支持图片水印、拖拽排序、只读预览等
8
+ - **FilePicker** - 文件选择上传组件
9
+
10
+ ## 本地开发
11
+
12
+ 1. 在项目根目录执行:
13
+
14
+ ```bash
15
+ yarn dev
16
+ ```
17
+
18
+ 2. 在 example-uniapp 下运行:
19
+
20
+ ```bash
21
+ yarn link xydata-tools-uniapp
22
+ ```
23
+
24
+ 3. 将 example-uniapp 添加到 H-Builder 中运行
25
+
26
+ > example-uniapp 的 `pages.json` 中 `easycom` 已配置好组件路径,会自动扫描引入。
27
+
28
+ ## 构建发布
29
+
30
+ ```bash
31
+ npm run prepublishOnly
32
+ ```
33
+
34
+ 发布 npm 包:
35
+
36
+ ```bash
37
+ npm config set registry https://registry.npmjs.org
38
+ npm publish -d
39
+ ```
40
+
41
+ 发布完成后,还原 npm 源(淘宝源):
42
+
43
+ ```bash
44
+ npm config set registry https://registry.npmmirror.com
45
+ ```
46
+
47
+ ## 目录结构
48
+
49
+ ```
50
+ src/
51
+ index.js # 组件库入口(导出 MediaPicker / FilePicker)
52
+ index.d.ts # 类型声明
53
+ components/uni-app/ # uni-app 组件源码
54
+ utils/ # 共享工具(watermark-shared 等)
55
+ assets/ # 组件图片资源
56
+ example-uniapp/ # 示例工程(HBuilder 直接运行)
57
+ docs/ # 组件文档
58
+ ```
59
+
60
+ ---
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,633 @@
1
+ <template>
2
+ <view class="file-picker-wrapper">
3
+ <view class="file-picker-box" v-if="!readonly">
4
+ <view class="upload-view">
5
+ <!-- 上传中状态 -->
6
+ <view class="file-item uploading-item" v-if="uploading">
7
+ <view class="file-info">
8
+ <view class="file-icon">
9
+ <image class="loading-icon" src="../../../assets/loading.png" />
10
+ </view>
11
+ <view class="file-content">
12
+ <text class="file-name">上传中 {{ uploadProgress }}%</text>
13
+ </view>
14
+ </view>
15
+ <view class="progress-bar">
16
+ <view class="progress-bar-inner" :style="{ width: uploadProgress + '%' }"></view>
17
+ </view>
18
+ </view>
19
+
20
+ <!-- 自定义上传按钮插槽 -->
21
+ <slot name="upload-button" :select="selectAndUploadFile" :count="fileList.length">
22
+ <!-- 默认上传按钮 -->
23
+ <button class="upload-btn" v-if="fileList.length < limit && !uploading" @click="selectAndUploadFile">
24
+ {{ buttonTitle }}
25
+ </button>
26
+ </slot>
27
+
28
+ <!-- 文件列表 -->
29
+ <view class="file-item" v-for="(file, index) in fileList" :key="index">
30
+ <view class="file-info" @click="handleFileClick(file)">
31
+ <view class="file-icon">
32
+ <image :src="getFileIcon(file.name)" mode="aspectFit" class="icon-image"></image>
33
+ </view>
34
+ <view class="file-content">
35
+ <text class="file-name">{{ file.name }}</text>
36
+ </view>
37
+ </view>
38
+ <view class="file-actions">
39
+ <view class="action-btn delete-btn" @click="removeFile(index)"> × </view>
40
+ </view>
41
+ </view>
42
+ </view>
43
+ </view>
44
+
45
+ <!-- 只读模式 -->
46
+ <view class="file-readonly" v-else-if="fileList.length">
47
+ <view class="file-item" v-for="(file, index) in fileList" :key="index">
48
+ <view class="file-info" @click="handleFileClick(file)">
49
+ <view class="file-icon">
50
+ <image :src="getFileIcon(file.name)" mode="aspectFit" class="icon-image"></image>
51
+ </view>
52
+ <view class="file-content">
53
+ <text class="file-name">{{ file.name }}</text>
54
+ </view>
55
+ </view>
56
+ </view>
57
+ </view>
58
+ </view>
59
+ </template>
60
+
61
+ <script>
62
+ import msWord from '../../../assets/file-icon/ms-word.png';
63
+ import msExcel from '../../../assets/file-icon/ms-excel.png';
64
+ import msPowerpoint from '../../../assets/file-icon/ms-powerpoint.png';
65
+ import pdfIcon from '../../../assets/file-icon/pdf.png';
66
+ import imageIcon from '../../../assets/file-icon/image.png';
67
+ import videoIcon from '../../../assets/file-icon/video.png';
68
+ import documentIcon from '../../../assets/file-icon/document.png';
69
+ import zipIcon from '../../../assets/file-icon/zip.png';
70
+
71
+ export default {
72
+ name: 'FilePicker',
73
+ // #ifdef VUE3
74
+ emits: ['change', 'update:modelValue', 'fileLimitReached', 'fileTypeInvalid', 'fileSizeExceed', 'uploadSuccess', 'uploadFail', 'progress', 'click'],
75
+ // #endif
76
+ props: {
77
+ // #ifdef VUE3
78
+ modelValue: {
79
+ type: Array,
80
+ default: () => []
81
+ },
82
+ // #endif
83
+ // #ifndef VUE3
84
+ value: {
85
+ type: Array,
86
+ default: () => []
87
+ },
88
+ // #endif
89
+ /** 上传地址 */
90
+ uploadUrl: {
91
+ type: String,
92
+ default: ''
93
+ },
94
+ /** 只读模式 */
95
+ readonly: {
96
+ type: Boolean,
97
+ default: false
98
+ },
99
+ /** 文件列表(兼容非v-model用法) */
100
+ files: {
101
+ type: Array,
102
+ default: () => []
103
+ },
104
+ /** 最大上传数量 */
105
+ limit: {
106
+ type: Number,
107
+ default: 10
108
+ },
109
+ /** 允许的文件扩展名 */
110
+ accept: {
111
+ type: Array,
112
+ default: () => ['doc', 'xls', 'ppt', 'pdf', 'docx', 'xlsx', 'pptx', 'zip', 'rar', 'png', 'jpg']
113
+ },
114
+ /** 文件大小限制(MB) */
115
+ maxSize: {
116
+ type: Number,
117
+ default: 20
118
+ },
119
+ /** 上传按钮文字 */
120
+ buttonTitle: {
121
+ type: String,
122
+ default: '上传附件'
123
+ }
124
+ },
125
+ data() {
126
+ return {
127
+ fileList: [],
128
+ uploading: false,
129
+ uploadProgress: 0,
130
+ actualUploadUrl: '' // 实际使用的上传地址
131
+ };
132
+ },
133
+ computed: {
134
+ // 判断是否为H5环境
135
+ isH5() {
136
+ // #ifdef H5
137
+ return true;
138
+ // #endif
139
+ // #ifndef H5
140
+ return false;
141
+ // #endif
142
+ },
143
+ /** 实际使用的上传地址 */
144
+ finalUploadUrl() {
145
+ return this.actualUploadUrl || this.uploadUrl || (getApp().globalData && getApp().globalData.uploadUrl) || '';
146
+ },
147
+ /** 统一读取外部值 */
148
+ internalValue() {
149
+ // #ifdef VUE3
150
+ return this.modelValue && this.modelValue.length ? this.modelValue : this.files;
151
+ // #endif
152
+ // #ifndef VUE3
153
+ return this.value && this.value.length ? this.value : this.files;
154
+ // #endif
155
+ }
156
+ },
157
+ watch: {
158
+ internalValue: {
159
+ handler(val) {
160
+ this.fileList = val || [];
161
+ },
162
+ deep: true,
163
+ immediate: true
164
+ }
165
+ },
166
+ mounted() {
167
+ this.fileList = this.internalValue || [];
168
+ // 初始化上传地址
169
+ if (!this.uploadUrl) {
170
+ this.actualUploadUrl = (getApp().globalData && getApp().globalData.uploadUrl) || '';
171
+ }
172
+ },
173
+ methods: {
174
+ // ==================== 事件监听辅助方法 ====================
175
+ /**
176
+ * 检查事件是否被监听
177
+ * @param {String} eventName - 事件名称
178
+ * @returns {Boolean} - 是否被监听
179
+ */
180
+ hasEventListener(eventName) {
181
+ // #ifdef VUE3
182
+ const onEventName = 'on' + eventName.charAt(0).toUpperCase() + eventName.slice(1);
183
+ return !!(this.$attrs[eventName] || this.$attrs[onEventName]);
184
+ // #endif
185
+ // #ifndef VUE3
186
+ return !!(this.$listeners && this.$listeners[eventName]);
187
+ // #endif
188
+ },
189
+
190
+ // ==================== 文件处理方法 ====================
191
+ /**
192
+ * 获取文件扩展名
193
+ */
194
+ getFileExtension(fileName) {
195
+ if (!fileName) return '';
196
+ const parts = fileName.split('.');
197
+ return parts.length > 1 ? parts.pop().toLowerCase() : '';
198
+ },
199
+
200
+ /**
201
+ * 获取文件图标
202
+ */
203
+ getFileIcon(fileName) {
204
+ const ext = this.getFileExtension(fileName);
205
+ switch (ext) {
206
+ case 'doc':
207
+ case 'docx':
208
+ return msWord;
209
+ case 'xls':
210
+ case 'xlsx':
211
+ return msExcel;
212
+ case 'ppt':
213
+ case 'pptx':
214
+ return msPowerpoint;
215
+ case 'pdf':
216
+ return pdfIcon;
217
+ case 'png':
218
+ case 'jpeg':
219
+ case 'jpg':
220
+ case 'gif':
221
+ return imageIcon;
222
+ case 'mp4':
223
+ case 'mov':
224
+ case 'avi':
225
+ case 'wmv':
226
+ case 'flv':
227
+ case 'mkv':
228
+ case 'webm':
229
+ case 'mpg':
230
+ case 'mpeg':
231
+ case 'm4v':
232
+ return videoIcon;
233
+ case 'rar':
234
+ case 'zip':
235
+ case '7z':
236
+ return zipIcon;
237
+ default:
238
+ return documentIcon;
239
+ }
240
+ },
241
+
242
+ /**
243
+ * 格式化文件大小
244
+ */
245
+ formatFileSize(bytes) {
246
+ if (!bytes) return '';
247
+ if (bytes < 1024) return bytes + 'B';
248
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + 'KB';
249
+ return (bytes / (1024 * 1024)).toFixed(2) + 'MB';
250
+ },
251
+
252
+ /**
253
+ * 选择并上传文件
254
+ */
255
+ selectAndUploadFile() {
256
+ if (!this.finalUploadUrl) {
257
+ uni.showToast({
258
+ title: '请先配置上传地址',
259
+ icon: 'none'
260
+ });
261
+ return;
262
+ }
263
+
264
+ if (this.fileList.length >= this.limit || this.uploading) {
265
+ if (this.fileList.length >= this.limit) {
266
+ this.$emit('fileLimitReached', {
267
+ current: this.fileList.length,
268
+ limit: this.limit
269
+ });
270
+ if (!this.hasEventListener('fileLimitReached')) {
271
+ uni.showToast({
272
+ title: `最多上传${this.limit}个文件`,
273
+ icon: 'none'
274
+ });
275
+ }
276
+ }
277
+ return;
278
+ }
279
+
280
+ // #ifdef H5
281
+ uni.chooseFile({
282
+ count: 1,
283
+ extension: this.accept,
284
+ success: (res) => {
285
+ this.validateAndUpload(res.tempFiles[0]);
286
+ }
287
+ });
288
+ // #endif
289
+
290
+ // #ifndef H5
291
+ uni.chooseMessageFile({
292
+ count: 1,
293
+ extension: this.accept,
294
+ success: (res) => {
295
+ this.validateAndUpload(res.tempFiles[0]);
296
+ }
297
+ });
298
+ // #endif
299
+ },
300
+
301
+ /**
302
+ * 验证并上传文件
303
+ */
304
+ validateAndUpload(file) {
305
+ const ext = this.getFileExtension(file.name);
306
+
307
+ if (this.accept && this.accept.length && !this.accept.includes(ext)) {
308
+ this.$emit('fileTypeInvalid', {
309
+ fileName: file.name,
310
+ fileType: ext,
311
+ acceptTypes: this.accept,
312
+ file: file
313
+ });
314
+ if (!this.hasEventListener('fileTypeInvalid')) {
315
+ uni.showToast({
316
+ title: `不支持${ext}格式`,
317
+ icon: 'none'
318
+ });
319
+ }
320
+ return;
321
+ }
322
+
323
+ const maxBytes = this.maxSize * 1024 * 1024;
324
+ if (file.size > maxBytes) {
325
+ this.$emit('fileSizeExceed', {
326
+ size: file.size,
327
+ maxSize: maxBytes,
328
+ fileName: file.name
329
+ });
330
+ if (!this.hasEventListener('fileSizeExceed')) {
331
+ uni.showToast({
332
+ title: `不能超过${this.maxSize}MB`,
333
+ icon: 'none'
334
+ });
335
+ }
336
+ return;
337
+ }
338
+
339
+ this.uploadFile(file);
340
+ },
341
+
342
+ /**
343
+ * 上传文件核心
344
+ */
345
+ uploadFile(file) {
346
+ this.uploading = true;
347
+ this.uploadProgress = 0;
348
+
349
+ const uploadTask = uni.uploadFile({
350
+ url: this.finalUploadUrl,
351
+ filePath: file.path,
352
+ name: 'file',
353
+ success: (uploadFileRes) => {
354
+ this.handleUploadSuccess(uploadFileRes, file);
355
+ },
356
+ fail: (err) => {
357
+ this.handleUploadError(err);
358
+ }
359
+ });
360
+
361
+ // 监听上传进度
362
+ uploadTask.onProgressUpdate((res) => {
363
+ this.uploadProgress = res.progress;
364
+ this.$emit('progress', res);
365
+ });
366
+ },
367
+
368
+ /**
369
+ * 处理上传成功
370
+ */
371
+ handleUploadSuccess(uploadFileRes, originalFile) {
372
+ try {
373
+ const result = JSON.parse(uploadFileRes.data);
374
+ console.log('文件上传成功:', result);
375
+
376
+ const fileInfo = {
377
+ fileName: result.data.fileName,
378
+ name: result.data.originFileName || originalFile.name,
379
+ url: result.data.pathUrl
380
+ };
381
+
382
+ this.fileList.push(fileInfo);
383
+ this.uploadProgress = 100;
384
+ this.uploading = false;
385
+ this.emitChange();
386
+
387
+ // 触发上传成功事件
388
+ this.$emit('uploadSuccess', {
389
+ file: fileInfo,
390
+ response: result
391
+ });
392
+
393
+ // 如果用户未监听事件,显示默认提示
394
+ if (!this.hasEventListener('uploadSuccess')) {
395
+ uni.showToast({
396
+ title: '上传成功',
397
+ icon: 'success'
398
+ });
399
+ }
400
+ } catch (e) {
401
+ this.handleUploadError(e);
402
+ }
403
+ },
404
+
405
+ /**
406
+ * 处理上传失败
407
+ */
408
+ handleUploadError(err) {
409
+ console.error('文件上传失败:', err);
410
+
411
+ // 触发上传失败事件
412
+ this.$emit('uploadFail', {
413
+ error: err,
414
+ message: '文件上传失败'
415
+ });
416
+
417
+ // 如果用户未监听事件,显示默认提示
418
+ if (!this.hasEventListener('uploadFail')) {
419
+ uni.showToast({
420
+ icon: 'none',
421
+ title: '文件上传失败'
422
+ });
423
+ }
424
+
425
+ this.uploading = false;
426
+ this.uploadProgress = 0;
427
+ },
428
+
429
+ /**
430
+ * 删除文件
431
+ */
432
+ removeFile(index) {
433
+ uni.showModal({
434
+ title: '提示',
435
+ content: '确定要删除该文件吗?',
436
+ success: (res) => {
437
+ if (res.confirm) {
438
+ this.fileList.splice(index, 1);
439
+ this.emitChange();
440
+ }
441
+ }
442
+ });
443
+ },
444
+
445
+ /**
446
+ * 处理文件点击
447
+ */
448
+ handleFileClick(file) {
449
+ if (this.hasEventListener('click')) {
450
+ this.$emit('click', file);
451
+ return;
452
+ }
453
+
454
+ if (!file.url) {
455
+ uni.showToast({
456
+ title: '文件地址无效',
457
+ icon: 'none'
458
+ });
459
+ return;
460
+ }
461
+
462
+ // #ifdef H5
463
+ // H5环境直接下载
464
+ this.downloadFileH5(file);
465
+ // #endif
466
+
467
+ // #ifndef H5
468
+ // 小程序等环境使用 uni.openDocument
469
+ this.openDocument(file);
470
+ // #endif
471
+ },
472
+
473
+ /**
474
+ * H5环境下载文件
475
+ */
476
+ downloadFileH5(file) {
477
+ uni.showLoading({
478
+ title: '下载中...'
479
+ });
480
+
481
+ // 使用 fetch 下载文件
482
+ fetch(file.url)
483
+ .then((res) => res.blob())
484
+ .then((blob) => {
485
+ // 创建 blob URL
486
+ const blobUrl = URL.createObjectURL(blob);
487
+ const a = document.createElement('a');
488
+ a.href = blobUrl;
489
+ a.download = file.name;
490
+ document.body.appendChild(a);
491
+ a.click();
492
+ document.body.removeChild(a);
493
+
494
+ // 释放 blob URL
495
+ setTimeout(() => {
496
+ URL.revokeObjectURL(blobUrl);
497
+ }, 100);
498
+
499
+ uni.hideLoading();
500
+ })
501
+ .catch((err) => {
502
+ console.error('下载文件失败:', err);
503
+ uni.hideLoading();
504
+ uni.showToast({
505
+ title: '下载失败',
506
+ icon: 'none'
507
+ });
508
+ });
509
+ },
510
+
511
+ /**
512
+ * 打开文档(小程序等)
513
+ */
514
+ openDocument(file) {
515
+ // 判断文件类型是否为可打开的文档类型
516
+ const ext = this.getFileExtension(file.name);
517
+ const supportedTypes = ['doc', 'xls', 'ppt', 'pdf', 'docx', 'xlsx', 'pptx'];
518
+ const isDocument = supportedTypes.includes(ext);
519
+
520
+ const imageTypes = ['png', 'jpeg', 'jpg', 'gif'];
521
+ console.log(ext);
522
+ if (imageTypes.includes(ext)) {
523
+ uni.previewImage({
524
+ urls: [file.url]
525
+ });
526
+ return;
527
+ }
528
+
529
+ // 显示加载提示
530
+ uni.showLoading({
531
+ title: isDocument ? '正在打开...' : '正在下载...'
532
+ });
533
+
534
+ // 下载文件
535
+ uni.downloadFile({
536
+ url: file.url,
537
+ success: (res) => {
538
+ if (res.statusCode === 200) {
539
+ if (isDocument) {
540
+ // 打开文档
541
+ uni.openDocument({
542
+ filePath: res.tempFilePath,
543
+ fileType: ext,
544
+ success: () => {
545
+ uni.hideLoading();
546
+ },
547
+ fail: (err) => {
548
+ console.error('打开文档失败:', err);
549
+ uni.hideLoading();
550
+ uni.showToast({
551
+ title: '打开文档失败',
552
+ icon: 'none'
553
+ });
554
+ }
555
+ });
556
+ } else {
557
+ // 保存文件到本地
558
+ uni.saveFile({
559
+ tempFilePath: res.tempFilePath,
560
+ success: (saveRes) => {
561
+ uni.hideLoading();
562
+ uni.showToast({
563
+ title: '已保存到本地',
564
+ icon: 'success'
565
+ });
566
+ },
567
+ fail: () => {
568
+ uni.hideLoading();
569
+ uni.showToast({
570
+ title: '保存失败',
571
+ icon: 'none'
572
+ });
573
+ }
574
+ });
575
+ }
576
+ } else {
577
+ uni.hideLoading();
578
+ uni.showToast({
579
+ title: '下载失败',
580
+ icon: 'none'
581
+ });
582
+ }
583
+ },
584
+ fail: (err) => {
585
+ console.error('下载失败:', err);
586
+ uni.hideLoading();
587
+ uni.showToast({
588
+ title: '下载失败',
589
+ icon: 'none'
590
+ });
591
+ }
592
+ });
593
+ },
594
+
595
+ /**
596
+ * 触发变更事件
597
+ */
598
+ emitChange() {
599
+ this.$emit('change', this.fileList);
600
+ // #ifdef VUE3
601
+ this.$emit('update:modelValue', this.fileList);
602
+ // #endif
603
+ // #ifndef VUE3
604
+ this.$emit('input', this.fileList);
605
+ // #endif
606
+ },
607
+
608
+ /**
609
+ * 获取文件列表
610
+ * @returns {Array} 文件列表
611
+ */
612
+ getFileList() {
613
+ return this.fileList;
614
+ },
615
+
616
+ /**
617
+ * 获取参数(不包含 url 字段)
618
+ * @returns {String} JSON 字符串
619
+ */
620
+ getParams() {
621
+ const params = this.fileList.map((file) => {
622
+ const { url, ...rest } = file;
623
+ return rest;
624
+ });
625
+ return JSON.stringify(params);
626
+ }
627
+ }
628
+ };
629
+ </script>
630
+
631
+ <style lang="scss">
632
+ @import './styles.scss';
633
+ </style>