hookehuyr

✨ feat(图片上传控件): 样式和功能调整

......@@ -202,6 +202,10 @@ const _sfc_main = create({
type: Array,
default: () => ['image', 'video', 'mix'],
},
imageType: {
type: Array,
default: () => [],
},
camera: {
type: String,
default: 'back',
......@@ -248,6 +252,7 @@ const _sfc_main = create({
'delete',
'update:fileList',
'file-item-click',
'image-type-error',
],
setup(props, { emit }) {
const fileList = reactive(props.fileList)
......@@ -475,9 +480,18 @@ const _sfc_main = create({
const maximize = props.maximize * 1
const oversizes = new Array()
files = files.filter((file) => {
if (Taro.getEnv() != 'WEAPP') {
file.type = file.originalFileObj.name.split('.').pop()
} else {
file.type = file.tempFilePath.split('.').pop()
}
if (file.size > maximize) {
oversizes.push(file)
return false
} else if (!props.imageType.includes(file.type)) {
// 控制文件类型上传
emit('image-type-error', file.type)
return false
} else {
return true
}
......
<!--
* @Date: 2022-08-31 16:16:49
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-04-12 10:59:48
* @LastEditTime: 2023-04-12 14:19:07
* @FilePath: /custom_form/src/components/FileUploaderField/index.vue
* @Description: 文件上传控件
-->
......@@ -20,19 +20,6 @@
v-html="item.component_props.note"
style="font-size: 13px; margin-left: 1rem; color: gray; padding-bottom: 0.5rem; padding-top: 0.25rem; white-space: pre-wrap;"
/>
<!-- <div>
<p
v-for="(file, index) in fileList"
:key="index"
style="padding-left: 1rem; margin-bottom: 0.5rem"
>
<p style="font-size: 1rem; word-break: break-all; margin-right: 0.75rem;">
<span>{{ index + 1 }}.&nbsp;{{ file.name }}&nbsp;&nbsp;{{ (file.size / 1024 / 1024).toFixed(2) }}MB</span>
&nbsp;&nbsp;
<span style="color: #e32525; font-size: 0.85rem" @click="beforeDelete(file)">移除</span>
</p>
</p>
</div> -->
<div style="padding: 1rem; padding-top: 0.5rem;">
<nut-uploader
:name="item.name"
......@@ -49,7 +36,7 @@
@failure="uploadFailure"
@delete="onDelete"
@change="onChange"
@file-item-click="fileItemClick"
@image-type-error="imageTypeError"
>
<nut-button shape="square" type="primary">
<template #icon>
......@@ -96,9 +83,12 @@ const HideShow = computed(() => {
const emit = defineEmits(["active"]);
// // 固定类型限制
// const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'psd', 'tif'];
// // 文件类型中文页面显示
// const type_text = computed(() => {
// return props.item.component_props.file_type;
// return imageTypes.join('/');
// });
// 上传文件集合
......@@ -121,12 +111,18 @@ const onOversize = (files) => {
toast_type.value = 'warn';
};
const onStart = (options) => {
// console.warn(options);
}
// 自定义上传逻辑
const beforeXhrUpload = async (xhr, options) => {
const imgObj = defaultFileList.value[defaultFileList.value.length - 1];
// 判断上传文件格式
imgObj.type = imgObj.type ? imgObj.type : imgObj.name.split(".").pop();
// H5环境
if (process.env.TARO_ENV === 'h5') {
// 把本地路径转换成file实体
const imgObj = defaultFileList.value[defaultFileList.value.length - 1];
const imgBlob = await fetch(imgObj.url).then(r => r.blob());
const imgFile = new File([imgBlob], imgObj.name , { type: imgObj.type });
// 上传返回file数据结构
......@@ -146,7 +142,6 @@ const beforeXhrUpload = async (xhr, options) => {
loading.value = false;
}
} else {
const imgObj = defaultFileList.value[defaultFileList.value.length - 1];
const fs = Taro.getFileSystemManager()
fs.getFileInfo({
filePath: imgObj.url,
......@@ -250,9 +245,12 @@ const onDelete = ({ file }) => {
// 完整数据回调到表单上
emit("active", props.item.value);
}
// 上传成功,点击队列项回调
const fileItemClick = (fileItem) => {
console.warn(fileItem);
//
const imageTypeError = (file) => {
toast_msg.value = '请上传指定格式'
toast_show.value = true;
toast_type.value = 'warn';
}
const onChange = ({ fileList }) => {
......@@ -379,7 +377,7 @@ defineExpose({ validFileUploader });
}
.type-text {
font-size: 0.9rem;
font-size: 13px;
margin-left: 1rem;
padding-bottom: 1rem;
color: gray;
......
<!--
* @Date: 2022-08-31 16:16:49
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-01-17 16:44:32
* @FilePath: /data-table/src/components/ImageUploaderField/index.vue
* @LastEditTime: 2023-04-12 15:01:26
* @FilePath: /custom_form/src/components/ImageUploaderField/index.vue
* @Description: 图片上传控件
-->
<template>
<div v-if="HideShow" class="image-uploader-field">
<div class="label">
<span v-if="item.component_props.required">&nbsp;*</span>
<text v-if="item.component_props.required">&nbsp;*</text>
{{ item.component_props.label }}
</div>
<div
v-if="item.component_props.note"
v-html="item.component_props.note"
style="font-size: 0.9rem; margin-left: 1rem; color: gray; padding-bottom: 0.5rem; padding-top: 0.25rem; white-space: pre;"
/>
<div style="padding: 1rem">
<van-uploader
<div style="font-size: 12px; color: red; margin-left: 20px;">
<text>最大图片数为 {{ item.component_props.max_count }} 张</text>,
<text>单个图片最大体积 {{ item.component_props.max_size }} MB</text>
</div>
<div v-if="item.component_props.note" v-html="item.component_props.note"
style="font-size: 13px; margin-left: 1rem; color: gray; padding-bottom: 0.5rem; padding-top: 0.25rem; white-space: pre-wrap;" />
<div style="padding: 1rem; padding-top: 0.5rem;">
<nut-uploader
:name="item.name"
upload-icon="add"
:before-read="beforeRead"
:after-read="afterRead"
:before-delete="beforeDelete"
v-model="fileList"
:multiple="item.component_props.max_size > 1"
/>
v-model:file-list="defaultFileList"
:maximum="item.component_props.max_count"
:multiple="item.component_props.max_count > 1"
:size-type="['compressed']"
:image-type="imageTypes"
:maximize="max_size"
:before-xhr-upload="beforeXhrUpload"
@oversize="onOversize"
@success="uploadSuccess"
@failure="uploadFailure"
@delete="onDelete"
@change="onChange"
@image-type-error="imageTypeError">
</nut-uploader>
</div>
<div class="type-text">上传类型:&nbsp;{{ type_text }}</div>
<div
v-if="show_empty"
class="van-field__error-message"
style="padding: 0 1rem 1rem 1rem"
>
图片上传不能为空
<div class="type-text">上传格式:{{ type_text }}</div>
<div v-if="show_error" style="padding: 5px 20px; color: red; font-size: 12px;">
{{ error_msg }}
</div>
<van-divider />
<nut-divider :style="{ color: '#ebedf0' }" />
<nut-overlay v-model:visible="loading">
<div class="wrapper" style="color: white; font-size: 15px;">
<Loading />
上传中...
</div>
</nut-overlay>
<nut-toast :msg="toast_msg" v-model:visible="toast_show" :type="toast_type" />
</div>
<van-overlay :show="loading">
<div class="wrapper" @click.stop>
<van-loading vertical color="#FFFFFF">上传中...</van-loading>
</div>
</van-overlay>
</template>
<script setup>
/**
* 图片上传
* @param name[String] 组件名称
* @param image_type[Array] 图片上传类型
* @param multiple[Boolean] 图片多选
*/
import { showSuccessToast, showFailToast, showToast } from "vant";
import _ from "lodash";
import { v4 as uuidv4 } from "uuid";
import { ref, computed, watch, onMounted, reactive } from "vue";
import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from "@/api/common";
import BMF from "browser-md5-file";
import { useRoute } from "vue-router";
import axios from "axios";
import { getEtag } from "@/utils/qetag.js"; // 生成hash值
import { getUrlParams } from "@/utils/tools";
import { Uploader, Loading } from '@nutui/icons-vue-taro';
import Taro from '@tarojs/taro'
const $route = useRoute();
const props = defineProps({
item: Object,
});
......@@ -69,156 +65,204 @@ const props = defineProps({
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
const emit = defineEmits(["active"]);
const show_empty = ref(false);
// 固定类型限制
const imageTypes = "jpg/jpeg/png/gif/bmp/psd/tif";
const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'psd', 'tif'];
// 文件类型中文页面显示
const type_text = computed(() => {
// return props.item.component_props.image_type;
return imageTypes;
return imageTypes.join('/');
});
// 上传图片集合
const fileList = ref([
// { url: "https://fastly.jsdelivr.net/npm/@vant/assets/leaf.jpeg" },
// Uploader 根据文件后缀来判断是否为图片文件
// 如果图片 URL 中不包含类型信息,可以添加 isImage 标记来声明
// { url: 'https://cloud-image', isImage: true },
]);
// 上传前置处理
const beforeRead = (file) => {
// 类型限制
// const image_types = _.map(
// props.item.component_props.image_type.split("/"),
// (item) => `image/${item}`
// );
const image_types = _.map(imageTypes.split("/"), (item) => `image/${item}`);
// 上传文件集合
const fileList = ref([]);
const defaultFileList = ref([])
let flag = true;
if (_.isArray(file)) {
// 多张图片
const types = _.difference(_.uniq(_.map(file, (item) => item.type)), image_types); // 数组返回不能上传的类型
if (types.length) {
flag = false;
showFailToast("请上传指定格式图片");
}
if (fileList.value.length + file.length > props.item.component_props.max_count) {
// 数量限制
flag = false;
showToast(`最大上传数量为${props.item.component_props.max_count}张`);
}
} else {
if (!_.includes(image_types, file.type)) {
showFailToast("请上传指定格式图片");
flag = false;
}
if (fileList.value.length + 1 > props.item.component_props.max_count) {
// 数量限制
flag = false;
showToast(`最大上传数量为${props.item.component_props.max_count}张`);
}
if ((file.size / 1024 / 1024).toFixed(2) > props.item.component_props.max_size) {
// 体积限制
flag = false;
showToast(
`最大文件体积为${props.item.component_props.max_size}MB`
);
}
}
return flag;
// 上传文件体积
const max_size = computed(() => {
return props.item.component_props.max_size * 1024 * 1024
})
const toast_msg = ref('');
const toast_show = ref(false);
const toast_type = ref('success');
// 超过体积大小回调
const onOversize = (files) => {
toast_msg.value = `最大图片体积为${props.item.component_props.max_size}MB`
toast_show.value = true;
toast_type.value = 'warn';
};
// 文件读取完成后的回调函数
const afterRead = async (files) => {
if (Array.isArray(files)) {
// 多张图片上传files是一个数组
muliUpload(files);
} else {
const imgUrl = await handleUpload(files);
const onStart = (options) => {
// console.warn(options);
}
// 自定义上传逻辑
const beforeXhrUpload = async (xhr, options) => {
const imgObj = defaultFileList.value[defaultFileList.value.length - 1];
// 判断上传文件格式
imgObj.type = imgObj.type ? imgObj.type : imgObj.name.split(".").pop();
// H5环境
if (process.env.TARO_ENV === 'h5') {
// 把本地路径转换成file实体
const imgBlob = await fetch(imgObj.url).then(r => r.blob());
const imgFile = new File([imgBlob], imgObj.name, { type: imgObj.type });
// 上传返回file数据结构
const resImgObj = await handleUpload(imgFile);
// 上传失败提示
if (!imgUrl.src) {
files.status = "failed";
files.message = "上传失败";
if (!resImgObj) {
options.onFailure?.(resImgObj, options);
loading.value = false;
} else {
files.status = "";
files.message = "";
defaultFileList.value[defaultFileList.value.length - 1]['url'] = resImgObj.src;
defaultFileList.value[defaultFileList.value.length - 1]['type'] = 'image';
fileList.value.push({
// meta_id: imgUrl.meta_id,
name: files.file.name,
url: imgUrl.src,
// isImage: true,
name: imgFile.name,
url: resImgObj.src,
size: imgFile.size
});
options.onSuccess?.(resImgObj, options);
loading.value = false;
}
} else {
const fs = Taro.getFileSystemManager()
fs.getFileInfo({
filePath: imgObj.url,
success: async (res) => {
const file_info = res;
let suffix = /\.[^\.]+$/.exec(imgObj.name); // 获取后缀
// 获取七牛token
const filename = imgObj.name; // 真实文件名
const getToken = await qiniuTokenAPI({
name: filename,
hash: file_info.digest,
});
// 文件上传七牛云
// 第一次上传
if (getToken.token) {
loading.value = true;
// 自拍图片上传七牛服务器
Taro.uploadFile({
url: 'https://up.qbox.me',
filePath: imgObj.url,
name: `file`,
formData: {
token: getToken.token,
key: `uploadForm/${formCode}/${file_info.digest}${suffix}`
},
})
.then(async (res) => {
res.data = JSON.parse(res.data);
if (res.data.filekey) {
// 保存文件
const { data } = await saveFileAPI({
name: filename,
filekey: res.data.filekey,
hash: file_info.digest,
});
defaultFileList.value[defaultFileList.value.length - 1]['url'] = data.src;
defaultFileList.value[defaultFileList.value.length - 1]['type'] = 'image';
// 加入上传成功队列
fileList.value.push({
name: filename,
url: data.src,
size: file_info.size
});
options.onSuccess?.(data, options);
loading.value = false;
}
})
.catch((error) => {
console.error(error)
options.onFailure?.(error, options);
loading.value = false;
})
}
// 重复上传
if (getToken.data) {
// 加入上传成功队列
fileList.value.push({
name: filename,
url: getToken.data.src,
size: file_info.size
});
options.onSuccess?.(getToken.data, options);
}
}
})
}
// 过滤非包含URL的图片
fileList.value = fileList.value.filter((item) => {
if (item.url) return item;
});
}
// 上传成功回调
const uploadSuccess = async ({ data, fileItem, option, responseText }) => {
props.item.value = {
key: "image_uploader",
key: "file_uploader",
filed_name: props.item.key,
// value: fileList.value.map((item) => item.url),
value: fileList.value,
};
show_empty.value = false;
// 完整数据回调到表单上
emit("active", props.item.value);
// 校验数据
validImageUploader();
};
// 上传失败回调
const uploadFailure = async ({ data, fileItem, option, responseText }) => {
// 真实上传失败才会提示
if (data) {
console.error("上传失败", "fail");
toast_msg.value = '上传失败,请重新尝试!'
toast_show.value = true;
toast_type.value = 'fail';
}
};
// 文件删除前的回调函数
const beforeDelete = (files) => {
// 删除上传队列回调
const onDelete = ({ file }) => {
fileList.value = fileList.value.filter((item) => {
if (item.url !== files.url) return item;
if (item.url !== file.url) return item;
});
props.item.value = {
key: "image_uploader",
key: "file_uploader",
filed_name: props.item.key,
// value: fileList.value.map((item) => item.url),
value: fileList.value,
};
// 完整数据回调到表单上
emit("active", props.item.value);
};
}
/********** 上传七牛云获取图片地址 ***********/
const loading = ref(false);
const formCode = $route.query.code; // 表单code
// const uuid = () => {
// let s = [];
// let hexDigits = "0123456789abcdef";
// for (var i = 0; i < 36; i++) {
// s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
// }
// s[14] = "4";
// s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
// s[8] = s[13] = s[18] = s[23] = "-";
//
const imageTypeError = (file) => {
toast_msg.value = '请上传指定格式'
toast_show.value = true;
toast_type.value = 'warn';
}
// var uuid = s.join("");
// return uuid;
// };
const onChange = ({ fileList }) => {
}
// 上传图片返回图片URL
const handleUpload = async (files) => {
/********** 上传七牛云获取文件地址 ***********/
const loading = ref(false);
const formCode = getUrlParams(location.href) ? getUrlParams(location.href).code : ''; // 表单code
// 上传文件返回文件URL
const handleUpload = async (file) => {
loading.value = true;
// 获取HASH值
// const hash = getEtag(files.content);
return new Promise((resolve, reject) => {
// 获取MD5值
const bmf = new BMF();
bmf.md5(
files.file,
file,
async (err, md5) => {
if (err) {
console.log(err);
reject(err);
}
// 获取七牛token
const filename = files.file.name; // 真实文件名
const filename = file.name; // 真实文件名
const getToken = await qiniuTokenAPI({
name: filename,
hash: md5,
......@@ -227,10 +271,8 @@ const handleUpload = async (files) => {
let imgUrl = "";
// 第一次上传
if (getToken.token) {
files.status = "uploading";
files.message = "上传中...";
// 返回数据库真实图片地址
imgUrl = await uploadQiniu(files.file, getToken.token, filename, md5);
// 返回数据库真实文件地址
imgUrl = await uploadQiniu(file, getToken.token, filename, md5);
}
// 重复上传
if (getToken.data) {
......@@ -245,39 +287,7 @@ const handleUpload = async (files) => {
});
};
// 多选图片上传遍历
var muliUpload = async (files) => {
for (let item of files) {
const res = await handleUpload(item);
// 上传失败提示
if (!res.src) {
item.status = "failed";
item.message = "上传失败";
loading.value = false;
} else {
item.status = "";
item.message = "";
fileList.value.push({
// meta_id: res.meta_id,
name: item.file.name,
url: res.src,
// isImage: true,
});
loading.value = false;
}
}
};
const getType = (file, name) => {
var index1 = name.lastIndexOf(".");
var index2 = file.length;
var type = file.substring(index1, index2).toUpperCase();
return type;
}
// 生成数据库真实图片地址
// 生成数据库真实文件地址
const uploadQiniu = async (file, token, name, md5) => {
let suffix = /\.[^\.]+$/.exec(name); // 获取后缀
// let affix = uuidv4();
......@@ -289,27 +299,32 @@ const uploadQiniu = async (file, token, name, md5) => {
let config = {
headers: { "Content-Type": "multipart/form-data" },
};
// 自拍图片上传七牛服务器
// 自拍文件上传七牛服务器
let qiniuUploadUrl;
if (window.location.protocol === 'https:') {
qiniuUploadUrl = 'https://up.qbox.me';
qiniuUploadUrl = 'https://up.qbox.me';
} else {
qiniuUploadUrl = 'http://upload.qiniu.com';
qiniuUploadUrl = 'http://upload.qiniu.com';
}
const { filekey, hash, image_info } = await qiniuUploadAPI(
const uploadData = await qiniuUploadAPI(
qiniuUploadUrl,
formData,
config
);
if (filekey) {
// 保存图片
// 上传失败处理
if (!uploadData) {
loading.value = false;
return false;
}
if (uploadData.filekey) {
// 保存文件
const { data } = await saveFileAPI({
name,
filekey,
filekey: uploadData.filekey,
hash: md5,
// format: image_info.format,
height: image_info.height,
width: image_info.width,
// height: image_info.height,
// width: image_info.width,
});
return data;
}
......@@ -317,34 +332,40 @@ const uploadQiniu = async (file, token, name, md5) => {
/****************** END *******************/
const show_error = ref(false);
const error_msg = ref('');
// 校验模块
const validImageUploader = () => {
// 必填项 未上传图片
// 必填项 未上传文件
if (props.item.component_props.required && !fileList.value.length) {
show_empty.value = true;
show_error.value = true;
error_msg.value = '必填项不能为空'
} else {
show_empty.value = false;
show_error.value = false;
error_msg.value = ''
}
return !show_empty.value;
return !show_error.value;
};
defineExpose({ validImageUploader });
</script>
<style lang="less" scoped>
<style lang="less">
.image-uploader-field {
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
margin-left: 1rem;
padding-bottom: 20px;
font-size: 26px;
font-weight: bold;
span {
text {
color: red;
}
}
.type-text {
font-size: 0.9rem;
font-size: 22px;
margin-left: 1rem;
padding-bottom: 1rem;
color: gray;
......
......@@ -8,7 +8,7 @@ import AreaPickerField from '@/components/AreaPickerField/index.vue'
import DatePickerField from '@/components/DatePickerField/index.vue'
import TimePickerField from '@/components/TimePickerField/index.vue'
import DateTimePickerField from '@/components/DateTimePickerField/index.vue'
// import ImageUploaderField from '@/components/ImageUploaderField/index.vue'
import ImageUploaderField from '@/components/ImageUploaderField/index.vue'
import FileUploaderField from '@/components/FileUploaderField/index.vue'
import PhoneField from '@/components/PhoneField/index.vue'
import EmailField from '@/components/EmailField/index.vue'
......@@ -105,9 +105,9 @@ export function createComponentType(data) {
if (item.component_props.tag === 'datetime') {
item.component = DateTimePickerField
}
// if (item.component_props.tag === 'image_uploader') {
// item.component = ImageUploaderField
// }
if (item.component_props.tag === 'image_uploader') {
item.component = ImageUploaderField
}
if (item.component_props.tag === 'file_uploader') {
item.component = FileUploaderField
}
......