hookehuyr

新增上传文件控件

1 +<!--
2 + * @Date: 2022-08-31 16:16:49
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2022-11-21 14:54:30
5 + * @FilePath: /data-table/src/components/ImageUploaderField/index.vue
6 + * @Description: 文件上传控件
7 +-->
8 +<template>
9 + <div class="file-uploader-field">
10 + <div class="label">
11 + {{ item.component_props.label
12 + }}<span v-if="item.component_props.required">&nbsp;*</span>
13 + </div>
14 + <div style="font-size: 0.9rem; padding-top: 0.5rem">
15 + <p
16 + v-for="file in fileList"
17 + :key="index"
18 + style="padding-left: 1rem; margin-bottom: 0.5rem"
19 + >
20 + {{ file.filename }}&nbsp;&nbsp;<span
21 + style="color: #e32525"
22 + @click="beforeDelete(file)"
23 + >移除</span
24 + >
25 + </p>
26 + </div>
27 + <div style="padding: 1rem">
28 + <van-uploader
29 + :name="item.name"
30 + upload-icon="add"
31 + accept="*"
32 + :before-read="beforeRead"
33 + :after-read="afterRead"
34 + :before-delete="beforeDelete"
35 + :multiple="item.component_props.multiple"
36 + >
37 + <van-button icon="plus" type="primary">上传文件</van-button>
38 + </van-uploader>
39 + </div>
40 + <!-- <div class="type-text">上传格式:{{ type_text }}</div> -->
41 + <div
42 + v-if="show_empty"
43 + class="van-field__error-message"
44 + style="padding: 0 1rem 1rem 1rem"
45 + >
46 + 文件上传不能为空
47 + </div>
48 + <van-divider />
49 + </div>
50 +
51 + <van-overlay :show="loading">
52 + <div class="wrapper" @click.stop>
53 + <van-loading vertical color="#FFFFFF">上传中...</van-loading>
54 + </div>
55 + </van-overlay>
56 +</template>
57 +
58 +<script setup>
59 +/**
60 + * 文件上传
61 + * @param name[String] 组件名称
62 + * @param file_type[Array] 文件上传类型
63 + * @param multiple[Boolean] 文件多选
64 + */
65 +import { showSuccessToast, showFailToast } from "vant";
66 +import _ from "lodash";
67 +import { v4 as uuidv4 } from "uuid";
68 +import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from "@/api/common";
69 +import BMF from "browser-md5-file";
70 +import { useRoute } from "vue-router";
71 +import axios from "axios";
72 +import { getEtag } from "@/utils/qetag.js"; // 生成hash值
73 +
74 +const $route = useRoute();
75 +const props = defineProps({
76 + item: Object,
77 +});
78 +const emit = defineEmits(["active"]);
79 +const show_empty = ref(false);
80 +
81 +// 文件类型中文页面显示
82 +const type_text = computed(() => {
83 + return props.item.component_props.file_type;
84 +});
85 +// 上传文件集合
86 +const fileList = ref([
87 + // { url: "https://fastly.jsdelivr.net/npm/@vant/assets/leaf.jpeg" },
88 + // Uploader 根据文件后缀来判断是否为文件文件
89 + // 如果文件 URL 中不包含类型信息,可以添加 isImage 标记来声明
90 + // { url: 'https://cloud-image', isImage: true },
91 +]);
92 +
93 +// 上传前置处理
94 +const beforeRead = (file) => {
95 + // 类型限制
96 + // const file_types = _.map(
97 + // props.item.component_props.file_type.split("/"),
98 + // (item) => `video/${item}`
99 + // );
100 +
101 + let flag = true;
102 + // if (_.isArray(file)) {
103 + // // 多张文件
104 + // const types = _.difference(_.uniq(_.map(file, (item) => item.type)), file_types); // 数组返回不能上传的类型
105 + // if (types.length) {
106 + // flag = false;
107 + // showFailToast("请上传指定格式文件");
108 + // }
109 + // } else {
110 + // if (!_.includes(file_types, file.type)) {
111 + // showFailToast("请上传指定格式文件");
112 + // flag = false;
113 + // }
114 + // }
115 + return flag;
116 +};
117 +
118 +// 文件读取完成后的回调函数
119 +const afterRead = async (files) => {
120 + if (Array.isArray(files)) {
121 + // 多张文件上传files是一个数组
122 + muliUpload(files);
123 + } else {
124 + const imgUrl = await handleUpload(files);
125 + // 上传失败提示
126 + if (!imgUrl.src) {
127 + files.status = "failed";
128 + files.message = "上传失败";
129 + loading.value = false;
130 + } else {
131 + files.status = "";
132 + files.message = "";
133 + fileList.value.push({
134 + // meta_id: imgUrl.meta_id,
135 + filename: files.file.name,
136 + url: imgUrl.src,
137 + // isImage: true,
138 + });
139 + loading.value = false;
140 + }
141 + }
142 + // 过滤非包含URL的文件
143 + fileList.value = fileList.value.filter((item) => {
144 + if (item.url) return item;
145 + });
146 + props.item.value = {
147 + key: "file_uploader",
148 + filed_name: props.item.key,
149 + // value: fileList.value.map((item) => item.url),
150 + value: fileList.value,
151 + };
152 + show_empty.value = false;
153 + // 完整数据回调到表单上
154 + emit("active", props.item.value);
155 + console.warn(fileList.value);
156 +};
157 +
158 +// 文件删除前的回调函数
159 +const beforeDelete = (files) => {
160 + fileList.value = fileList.value.filter((item) => {
161 + if (item.url !== files.url) return item;
162 + });
163 + props.item.value = {
164 + key: "file_uploader",
165 + filed_name: props.item.key,
166 + // value: fileList.value.map((item) => item.url),
167 + value: fileList.value,
168 + };
169 + // 完整数据回调到表单上
170 + emit("active", props.item.value);
171 +};
172 +
173 +/********** 上传七牛云获取文件地址 ***********/
174 +const loading = ref(false);
175 +const formCode = $route.query.code; // 表单code
176 +
177 +// 上传文件返回文件URL
178 +const handleUpload = async (files) => {
179 + loading.value = true;
180 + // 获取HASH值
181 + // const hash = getEtag(files.content);
182 + return new Promise((resolve, reject) => {
183 + // 获取MD5值
184 + const bmf = new BMF();
185 + bmf.md5(
186 + files.file,
187 + async (err, md5) => {
188 + if (err) {
189 + console.log(err);
190 + reject(err);
191 + }
192 + // 获取七牛token
193 + const filename = files.file.name; // 真实文件名
194 + const getToken = await qiniuTokenAPI({
195 + name: filename,
196 + hash: md5,
197 + });
198 + // 文件上传七牛云
199 + let imgUrl = "";
200 + // 第一次上传
201 + if (getToken.token) {
202 + files.status = "uploading";
203 + files.message = "上传中...";
204 + // 返回数据库真实文件地址
205 + imgUrl = await uploadQiniu(files.file, getToken.token, filename, md5);
206 + }
207 + // 重复上传
208 + if (getToken.data) {
209 + imgUrl = getToken.data;
210 + }
211 + resolve(imgUrl);
212 + },
213 + (process) => {
214 + //计算进度
215 + }
216 + );
217 + });
218 +};
219 +
220 +// 多选文件上传遍历
221 +var muliUpload = async (files) => {
222 + for (let item of files) {
223 + const res = await handleUpload(item);
224 + // 上传失败提示
225 + if (!res.src) {
226 + item.status = "failed";
227 + item.message = "上传失败";
228 + loading.value = false;
229 + } else {
230 + item.status = "";
231 + item.message = "";
232 + fileList.value.push({
233 + // meta_id: res.meta_id,
234 + filename: item.file.name,
235 + url: res.src,
236 + // isImage: true,
237 + });
238 + loading.value = false;
239 + }
240 + }
241 +};
242 +
243 +// 生成数据库真实文件地址
244 +const uploadQiniu = async (file, token, name, md5) => {
245 + // let affix = uuidv4();
246 + let fileName = `uploadForm/${formCode}/${md5}.${name.split(".")[1]}`;
247 + let formData = new FormData();
248 + formData.append("file", file); // 通过append向form对象添加数据
249 + formData.append("token", token);
250 + formData.append("key", fileName);
251 + let config = {
252 + headers: { "Content-Type": "multipart/form-data" },
253 + };
254 + // 自拍文件上传七牛服务器
255 + const { filekey, hash, image_info } = await qiniuUploadAPI(
256 + "http://upload.qiniu.com/",
257 + formData,
258 + config
259 + );
260 + if (filekey) {
261 + // 保存文件
262 + const { data } = await saveFileAPI({
263 + name,
264 + filekey,
265 + hash: md5,
266 + // format: image_info.format,
267 + // height: image_info.height,
268 + // width: image_info.width,
269 + });
270 + return data;
271 + }
272 +};
273 +
274 +/****************** END *******************/
275 +
276 +// 校验模块
277 +const validFileUploader = () => {
278 + // 必填项 未上传文件
279 + if (props.item.component_props.required && !fileList.value.length) {
280 + show_empty.value = true;
281 + } else {
282 + show_empty.value = false;
283 + }
284 + return !show_empty.value;
285 +};
286 +
287 +defineExpose({ validFileUploader });
288 +</script>
289 +
290 +<style lang="less" scoped>
291 +.file-uploader-field {
292 + .label {
293 + padding: 1rem 1rem 0 1rem;
294 + font-size: 0.9rem;
295 + font-weight: bold;
296 +
297 + span {
298 + color: red;
299 + }
300 + }
301 +
302 + .type-text {
303 + font-size: 0.9rem;
304 + margin-left: 1rem;
305 + padding-bottom: 1rem;
306 + color: gray;
307 + }
308 +}
309 +
310 +.wrapper {
311 + display: flex;
312 + align-items: center;
313 + justify-content: center;
314 + height: 100%;
315 +}
316 +
317 +.block {
318 + width: 120px;
319 + height: 120px;
320 + background-color: #fff;
321 +}
322 +</style>
...@@ -10,6 +10,7 @@ import DatePickerField from '@/components/DatePickerField/index.vue' ...@@ -10,6 +10,7 @@ import DatePickerField from '@/components/DatePickerField/index.vue'
10 import TimePickerField from '@/components/TimePickerField/index.vue' 10 import TimePickerField from '@/components/TimePickerField/index.vue'
11 import DateTimePickerField from '@/components/DateTimePickerField/index.vue' 11 import DateTimePickerField from '@/components/DateTimePickerField/index.vue'
12 import ImageUploaderField from '@/components/ImageUploaderField/index.vue' 12 import ImageUploaderField from '@/components/ImageUploaderField/index.vue'
13 +import FileUploaderField from '@/components/FileUploaderField/index.vue'
13 import PhoneField from '@/components/PhoneField/index.vue' 14 import PhoneField from '@/components/PhoneField/index.vue'
14 import EmailField from '@/components/EmailField/index.vue' 15 import EmailField from '@/components/EmailField/index.vue'
15 import SignField from '@/components/SignField/index.vue' 16 import SignField from '@/components/SignField/index.vue'
...@@ -98,6 +99,9 @@ export function createComponentType(data) { ...@@ -98,6 +99,9 @@ export function createComponentType(data) {
98 if (item.component_props.name === 'image_uploader') { 99 if (item.component_props.name === 'image_uploader') {
99 item.component = ImageUploaderField; 100 item.component = ImageUploaderField;
100 } 101 }
102 + if (item.component_props.name === 'file_uploader') {
103 + item.component = FileUploaderField;
104 + }
101 if (item.component_props.name === 'phone') { 105 if (item.component_props.name === 'phone') {
102 item.name = item.key; 106 item.name = item.key;
103 item.component = PhoneField; 107 item.component = PhoneField;
......
...@@ -169,8 +169,9 @@ onMounted(async () => { ...@@ -169,8 +169,9 @@ onMounted(async () => {
169 }); 169 });
170 170
171 // 处理没有绑定值的组件的赋值 171 // 处理没有绑定值的组件的赋值
172 -// 图片上传,电子签名,评分组件 172 +// 图片上传,文件上传,电子签名,评分组件
173 const image_uploader = ref(null); 173 const image_uploader = ref(null);
174 +const file_uploader = ref(null);
174 const sign = ref(null); 175 const sign = ref(null);
175 const rate_picker = ref(null); 176 const rate_picker = ref(null);
176 177
...@@ -179,6 +180,9 @@ const onActive = (item) => { ...@@ -179,6 +180,9 @@ const onActive = (item) => {
179 if (item.key === "image_uploader") { 180 if (item.key === "image_uploader") {
180 postData.value[item.filed_name] = item.value; 181 postData.value[item.filed_name] = item.value;
181 } 182 }
183 + if (item.key === "file_uploader") {
184 + postData.value[item.filed_name] = item.value;
185 + }
182 if (item.key === "sign") { 186 if (item.key === "sign") {
183 postData.value[item.filed_name] = item.value; 187 postData.value[item.filed_name] = item.value;
184 } 188 }
...@@ -205,6 +209,18 @@ const validOther = () => { ...@@ -205,6 +209,18 @@ const validOther = () => {
205 } 209 }
206 }); 210 });
207 } 211 }
212 + if (file_uploader.value) {
213 + // 文件上传
214 + file_uploader.value.forEach((item, index) => {
215 + if (!file_uploader.value[index].validFileUploader()) {
216 + valid = {
217 + status: file_uploader.value[index].validFileUploader(),
218 + key: "file_uploader",
219 + };
220 + return false;
221 + }
222 + });
223 + }
208 if (sign.value) { 224 if (sign.value) {
209 // 电子签名 225 // 电子签名
210 sign.value.forEach((item, index) => { 226 sign.value.forEach((item, index) => {
......