|
|
@@ -1,105 +1,319 @@
|
|
|
<template>
|
|
|
- <u-upload :action="action" :fileList="fileListData"
|
|
|
- v-bind="$attrs"
|
|
|
- @on-remove="onRemove"
|
|
|
- @on-success="afterRead"
|
|
|
- @on-list-change="onListChange">
|
|
|
- <slot></slot>
|
|
|
- </u-upload>
|
|
|
+ <u-upload
|
|
|
+ :name="name"
|
|
|
+ :width="width"
|
|
|
+ :height="height"
|
|
|
+ :accept="accept"
|
|
|
+ :fileList="fileList"
|
|
|
+ :multiple="multiple"
|
|
|
+ :maxCount="maxCount"
|
|
|
+ :disabled="disabled"
|
|
|
+ v-if="$slots.content"
|
|
|
+ @afterRead="afterRead"
|
|
|
+ @delete="onDelete">
|
|
|
+ <slot name="content" :data="fileList"></slot>
|
|
|
+ </u-upload>
|
|
|
+ <u-upload
|
|
|
+ :name="name"
|
|
|
+ :width="width"
|
|
|
+ :height="height"
|
|
|
+ :accept="accept"
|
|
|
+ :fileList="fileList"
|
|
|
+ :multiple="multiple"
|
|
|
+ :maxCount="maxCount"
|
|
|
+ :disabled="disabled"
|
|
|
+ v-else
|
|
|
+ @afterRead="afterRead"
|
|
|
+ @delete="onDelete">
|
|
|
+ </u-upload>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
+// import baseUrl from '@/common/config.js';
|
|
|
import configService from '@/common/service/config.service';
|
|
|
export default {
|
|
|
- name: "",
|
|
|
- props: {
|
|
|
- api: {
|
|
|
- type: String,
|
|
|
- default: '/sys/common/upload'
|
|
|
- },
|
|
|
- fileList: {
|
|
|
- type: Array,
|
|
|
- default: () => []
|
|
|
- },
|
|
|
- },
|
|
|
- data() {
|
|
|
- return {
|
|
|
- fileListData: [],
|
|
|
- };
|
|
|
- },
|
|
|
- watch: {
|
|
|
- fileList: {
|
|
|
- immediate: true,
|
|
|
- handler(n) {
|
|
|
- if (n.length > 0) {
|
|
|
- this.fileListData = n;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
- computed: {
|
|
|
- action() {
|
|
|
- return configService.apiUrl + this.api
|
|
|
- }
|
|
|
- },
|
|
|
- mounted() {
|
|
|
-
|
|
|
- },
|
|
|
- beforeDestroy() {
|
|
|
-
|
|
|
- },
|
|
|
- methods: {
|
|
|
- afterRead(event, index, list) {
|
|
|
- // url是组件的固定字段
|
|
|
- this.fileListData[index] = {
|
|
|
- ...event,
|
|
|
- url: list[index].url
|
|
|
- };
|
|
|
- this.$emit('update:fileList', this.fileListData);
|
|
|
- this.$emit('complete', this.fileListData);
|
|
|
- },
|
|
|
- onRemove(index, lists) {
|
|
|
- this.fileListData.splice(index, 1);
|
|
|
- this.$emit('update:fileList', this.fileListData);
|
|
|
- this.$emit('complete', this.fileListData);
|
|
|
- },
|
|
|
- // onListChange早于onSuccess
|
|
|
- onListChange(lists) {
|
|
|
- // this.$emit('complete', lists);
|
|
|
- },
|
|
|
- // 手动上传
|
|
|
- uploadFilePromise(url) {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- let a = uni.uploadFile({
|
|
|
- url: this.action,
|
|
|
- name: "file",
|
|
|
- header:{
|
|
|
- 'AppType': '5',
|
|
|
- 'X-Access-Token': uni.getStorageSync('token'),
|
|
|
- },
|
|
|
- filePath: url,
|
|
|
- // formData: {
|
|
|
- // user: "test",
|
|
|
- // },
|
|
|
- success: (res) => {
|
|
|
- uni.hideLoading();
|
|
|
- let data = JSON.parse(res.data);
|
|
|
- if (data.success) {
|
|
|
- resolve(data);
|
|
|
- } else {
|
|
|
- uni.showToast({
|
|
|
- icon: 'none',
|
|
|
- title: data.message || '文件导入失败',
|
|
|
- });
|
|
|
- }
|
|
|
- },
|
|
|
- });
|
|
|
- });
|
|
|
- },
|
|
|
- }
|
|
|
+ name: "",
|
|
|
+ props: {
|
|
|
+ api: {
|
|
|
+ type: String,
|
|
|
+ default: '/sys/common/upload'
|
|
|
+ },
|
|
|
+ name: {
|
|
|
+ type: String,
|
|
|
+ default: uni.$u.props.upload.name
|
|
|
+ },
|
|
|
+ accept: {
|
|
|
+ type: String,
|
|
|
+ default: uni.$u.props.upload.accept
|
|
|
+ },
|
|
|
+ width: {
|
|
|
+ type: [String, Number],
|
|
|
+ default: uni.$u.props.upload.width
|
|
|
+ },
|
|
|
+ height: {
|
|
|
+ type: [String, Number],
|
|
|
+ default: uni.$u.props.upload.height
|
|
|
+ },
|
|
|
+ maxCount: {
|
|
|
+ type: Number,
|
|
|
+ default: 5
|
|
|
+ },
|
|
|
+ multiple: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true
|
|
|
+ },
|
|
|
+ disabled: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ },
|
|
|
+ // 初始图片列表
|
|
|
+ initList: {
|
|
|
+ type: Array,
|
|
|
+ default: () => []
|
|
|
+ }
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ fileList: [],
|
|
|
+ };
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ // 更新fileList
|
|
|
+ initList: {
|
|
|
+ handler(newVal) {
|
|
|
+ if (newVal && newVal.length > 0) {
|
|
|
+ this.fileList = newVal.map(item => ({
|
|
|
+ ...item,
|
|
|
+ status: 'success',
|
|
|
+ message: ''
|
|
|
+ }));
|
|
|
+ } else {
|
|
|
+ this.fileList = [];
|
|
|
+ }
|
|
|
+ },
|
|
|
+ immediate: true,
|
|
|
+ deep: true
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ // 上传后的处理函数
|
|
|
+ async afterRead(event) {
|
|
|
+ // 当设置 multiple 为 true 时, file 为数组格式,否则为对象格式
|
|
|
+ let lists = [].concat(event.file);
|
|
|
+ let fileListLen = this[`fileList`].length;
|
|
|
+
|
|
|
+ // 遍历文件,对图片进行压缩(支持H5、App、小程序三端)
|
|
|
+ // 支持两种判断方式:1. MIME类型以image/开头 2. 文件后缀为.jpg
|
|
|
+ for (let i = 0; i < lists.length; i++) {
|
|
|
+ const item = lists[i];
|
|
|
+ const isImageType = item.type && item.type.startsWith('image');
|
|
|
+ const isJpgFile = item.name && (item.name.toLowerCase().endsWith('.jpg') || item.name.toLowerCase().endsWith('.jpeg'));
|
|
|
+ if (isImageType || isJpgFile) {
|
|
|
+ // 压缩后可能是 File 对象(H5)或 URL 字符串(App/小程序)
|
|
|
+ const compressedResult = await this.compressImage(item.url);
|
|
|
+ // 保存原始 URL 用于预览,压缩结果用于上传
|
|
|
+ lists[i].compressedFile = compressedResult;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ lists.map((item) => {
|
|
|
+ this[`fileList`].push({
|
|
|
+ ...item,
|
|
|
+ status: "uploading",
|
|
|
+ message: "上传中",
|
|
|
+ localUrl: item.url, // 保存原始本地路径用于预览
|
|
|
+ });
|
|
|
+ });
|
|
|
+ // 无预览时,使用toast
|
|
|
+ if (!this.$attrs.previewImage) {
|
|
|
+ uni.showLoading({
|
|
|
+ title: '上传中...'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ for (let i = 0; i < lists.length; i++) {
|
|
|
+ // 使用压缩后的文件或原文件上传
|
|
|
+ const uploadTarget = lists[i].compressedFile || lists[i].url;
|
|
|
+ const result = await this.uploadFilePromise(uploadTarget);
|
|
|
+ let item = this[`fileList`][fileListLen];
|
|
|
+ this[`fileList`].splice(
|
|
|
+ fileListLen,
|
|
|
+ 1,
|
|
|
+ Object.assign(item, {
|
|
|
+ status: "success",
|
|
|
+ message: "",
|
|
|
+ url: configService.apiUrl + '/' + result,
|
|
|
+ localUrl: item.localUrl
|
|
|
+ })
|
|
|
+ );
|
|
|
+ fileListLen++;
|
|
|
+ // 无预览时,使用toast
|
|
|
+ if (!this.$attrs.previewImage) {
|
|
|
+ uni.hideLoading();
|
|
|
+ uni.showToast({
|
|
|
+ icon: 'none',
|
|
|
+ title: result.message || '上传完成',
|
|
|
+ duration: 2000
|
|
|
+ });
|
|
|
+ this.$emit('complete');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.$emit('fileList', this.fileList);
|
|
|
+ },
|
|
|
+ // 图片压缩 - 支持H5、App、小程序三端
|
|
|
+ // 使用条件编译区分平台
|
|
|
+ async compressImage(url) {
|
|
|
+ // #ifdef H5
|
|
|
+ // H5 环境直接使用 Canvas 压缩
|
|
|
+ try {
|
|
|
+ return await this.canvasCompress(url, 0.8);
|
|
|
+ } catch (err) {
|
|
|
+ console.warn('Canvas 压缩失败,使用原图片:', err);
|
|
|
+ return url;
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifndef H5
|
|
|
+ // App 和小程序使用 uni.compressImage
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ uni.compressImage({
|
|
|
+ src: url,
|
|
|
+ quality: 0.8,
|
|
|
+ success: (res) => {
|
|
|
+ resolve(res.tempFilePath);
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.warn('uni.compressImage 失败,使用原图片:', err);
|
|
|
+ resolve(url);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ // #endif
|
|
|
+ },
|
|
|
+ // Canvas 图片压缩(H5 专用)
|
|
|
+ canvasCompress(url, quality) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ const img = new Image();
|
|
|
+ img.crossOrigin = 'anonymous';
|
|
|
+ img.onload = () => {
|
|
|
+ const canvas = document.createElement('canvas');
|
|
|
+ const ctx = canvas.getContext('2d');
|
|
|
+
|
|
|
+ // 计算压缩后的尺寸(保持宽高比)
|
|
|
+ const maxWidth = 1200;
|
|
|
+ const maxHeight = 1200;
|
|
|
+ let width = img.width;
|
|
|
+ let height = img.height;
|
|
|
+
|
|
|
+ if (width > maxWidth) {
|
|
|
+ height = (maxWidth / width) * height;
|
|
|
+ width = maxWidth;
|
|
|
+ }
|
|
|
+ if (height > maxHeight) {
|
|
|
+ width = (maxHeight / height) * width;
|
|
|
+ height = maxHeight;
|
|
|
+ }
|
|
|
+
|
|
|
+ canvas.width = width;
|
|
|
+ canvas.height = height;
|
|
|
+ ctx.drawImage(img, 0, 0, width, height);
|
|
|
+
|
|
|
+ // 将 Canvas 转换为 File 对象
|
|
|
+ canvas.toBlob((blob) => {
|
|
|
+ if (blob) {
|
|
|
+ // 确保文件名以 .jpg 结尾
|
|
|
+ const originalName = url.split('/').pop() || 'image';
|
|
|
+ const fileName = originalName.endsWith('.jpg') || originalName.endsWith('.jpeg')
|
|
|
+ ? originalName
|
|
|
+ : originalName + '.jpg';
|
|
|
+ const file = new File([blob], fileName, { type: 'image/jpeg' });
|
|
|
+ resolve(file);
|
|
|
+ } else {
|
|
|
+ reject('Blob 创建失败');
|
|
|
+ }
|
|
|
+ }, 'image/jpeg', quality);
|
|
|
+ };
|
|
|
+ img.onerror = (err) => {
|
|
|
+ reject('图片加载失败: ' + err);
|
|
|
+ };
|
|
|
+ img.src = url;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ uploadFilePromise(fileOrUrl) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ // #ifdef H5
|
|
|
+ // H5 环境使用原生 FormData 上传
|
|
|
+ if (fileOrUrl instanceof File) {
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append('file', fileOrUrl);
|
|
|
+ formData.append('biz', 'temp');
|
|
|
+
|
|
|
+ fetch( configService.apiUrl + this.api, {
|
|
|
+ method: 'POST',
|
|
|
+ body: formData,
|
|
|
+ headers: {
|
|
|
+ 'Accept': 'application/json'
|
|
|
+ }
|
|
|
+ }).then(response => response.json())
|
|
|
+ .then(data => {
|
|
|
+ if (data.success) {
|
|
|
+ resolve(data.message);
|
|
|
+ } else {
|
|
|
+ reject(data.message);
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(err => {
|
|
|
+ reject('上传失败: ' + err);
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // App 和小程序使用 uni.uploadFile
|
|
|
+ uni.uploadFile({
|
|
|
+ url: configService.apiUrl + this.api,
|
|
|
+ name: "file",
|
|
|
+ formData: {
|
|
|
+ 'biz': 'temp',
|
|
|
+ },
|
|
|
+ filePath: fileOrUrl,
|
|
|
+ success: (res) => {
|
|
|
+ let data = JSON.parse(res.data);
|
|
|
+ if (data.success) {
|
|
|
+ resolve(data.message);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ reject(err);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ },
|
|
|
+ onDelete(event) {
|
|
|
+ uni.showModal({
|
|
|
+ title: '提示',
|
|
|
+ content: '确定要删除?',
|
|
|
+ cancelText: '取消',
|
|
|
+ confirmText: '确定',
|
|
|
+ success: res => {
|
|
|
+ if (res.confirm) {
|
|
|
+ this[`fileList`].splice(event.index, 1);
|
|
|
+ this.$emit('fileList', this.fileList);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ }
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
-
|
|
|
</style>
|