Showing
3 changed files
with
343 additions
and
1 deletions
src/components/FileUploaderField/index.vue
0 → 100644
| 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"> *</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 }} <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) => { | ... | ... |
-
Please register or login to post a comment