hookehuyr

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

......@@ -49,6 +49,8 @@ declare module '@vue/runtime-core' {
NutSwiper: typeof import('@nutui/nutui-taro')['Swiper']
NutSwiperItem: typeof import('@nutui/nutui-taro')['SwiperItem']
NutTextarea: typeof import('@nutui/nutui-taro')['Textarea']
NutToast: typeof import('@nutui/nutui-taro')['Toast']
NutUploader: typeof import('@nutui/nutui-taro')['Uploader']
PhoneField: typeof import('./src/components/PhoneField/index.vue')['default']
PickerField: typeof import('./src/components/PickerField/index.vue')['default']
RadioField: typeof import('./src/components/RadioField/index.vue')['default']
......
......@@ -38,7 +38,7 @@
"dependencies": {
"@babel/runtime": "^7.7.7",
"@nutui/icons-vue-taro": "^0.0.9",
"@nutui/nutui-taro": "^4.0.4",
"@nutui/nutui-taro": "^4.0.5",
"@tarojs/components": "3.6.2",
"@tarojs/extend": "^3.6.2",
"@tarojs/helper": "3.6.2",
......@@ -57,6 +57,7 @@
"@tarojs/taro": "3.6.2",
"@vant/area-data": "^1.4.1",
"axios-miniprogram": "^2.0.0-rc-2",
"browser-md5-file": "^1.1.1",
"dayjs": "^1.11.7",
"pinia": "2.0.10",
"taro-plugin-pinia": "^1.0.0",
......
var __defProp = Object.defineProperty
var __defNormalProp = (obj, key, value) =>
key in obj
? __defProp(obj, key, {
enumerable: true,
configurable: true,
writable: true,
value,
})
: (obj[key] = value)
var __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== 'symbol' ? key + '' : key, value)
return value
}
import {
reactive,
computed,
resolveComponent,
openBlock,
createElementBlock,
normalizeClass,
renderSlot,
createTextVNode,
createBlock,
createCommentVNode,
Fragment,
renderList,
createElementVNode,
toDisplayString,
createVNode,
} from 'vue'
import { c as createComponent } from './component-25dcca32.js'
import { f as funInterceptor } from './interceptor-157a0193.js'
import Progress from './Progress.js'
import Button from './Button.js'
import Taro from '@tarojs/taro'
import { Photograph, Failure, Loading, Del, Link } from '@nutui/icons-vue-taro'
import { _ as _export_sfc } from './_plugin-vue_export-helper-cc2b3d55.js'
import '../locale/lang'
class UploadOptions {
constructor() {
__publicField(this, 'url', '')
__publicField(this, 'name', 'file')
__publicField(this, 'fileType', 'image')
__publicField(this, 'formData')
__publicField(this, 'sourceFile')
__publicField(this, 'method', 'post')
__publicField(this, 'xhrState', 200)
__publicField(this, 'timeout', 30 * 1e3)
__publicField(this, 'headers', {})
__publicField(this, 'withCredentials', false)
__publicField(this, 'onStart')
__publicField(this, 'taroFilePath')
__publicField(this, 'onProgress')
__publicField(this, 'onSuccess')
__publicField(this, 'onFailure')
__publicField(this, 'beforeXhrUpload')
}
}
class Uploader {
constructor(options) {
__publicField(this, 'options')
this.options = options
}
upload() {
var _a
const options = this.options
const xhr = new XMLHttpRequest()
xhr.timeout = options.timeout
if (xhr.upload) {
xhr.upload.addEventListener(
'progress',
(e) => {
var _a2
;(_a2 = options.onProgress) == null
? void 0
: _a2.call(options, e, options)
},
false,
)
xhr.onreadystatechange = () => {
var _a2, _b
if (xhr.readyState === 4) {
if (xhr.status == options.xhrState) {
;(_a2 = options.onSuccess) == null
? void 0
: _a2.call(options, xhr.responseText, options)
} else {
;(_b = options.onFailure) == null
? void 0
: _b.call(options, xhr.responseText, options)
}
}
}
xhr.withCredentials = options.withCredentials
xhr.open(options.method, options.url, true)
for (const [key, value] of Object.entries(options.headers)) {
xhr.setRequestHeader(key, value)
}
;(_a = options.onStart) == null ? void 0 : _a.call(options, options)
if (options.beforeXhrUpload) {
options.beforeXhrUpload(xhr, options)
} else {
xhr.send(options.formData)
}
} else {
console.warn('浏览器不支持 XMLHttpRequest')
}
}
}
class UploaderTaro extends Uploader {
constructor(options) {
super(options)
}
uploadTaro(uploadFile, env) {
var _a
const options = this.options
if (env === 'WEB') {
this.upload()
} else {
if (options.beforeXhrUpload) {
options.beforeXhrUpload(uploadFile, options)
} else {
const uploadTask = uploadFile({
url: options.url,
filePath: options.taroFilePath,
fileType: options.fileType,
header: {
'Content-Type': 'multipart/form-data',
...options.headers,
},
//
formData: options.formData,
name: options.name,
success(response) {
var _a2, _b
if (options.xhrState == response.statusCode) {
;(_a2 = options.onSuccess) == null
? void 0
: _a2.call(options, response, options)
} else {
;(_b = options.onFailure) == null
? void 0
: _b.call(options, response, options)
}
},
fail(e) {
var _a2
;(_a2 = options.onFailure) == null
? void 0
: _a2.call(options, e, options)
},
})
;(_a = options.onStart) == null ? void 0 : _a.call(options, options)
uploadTask.progress((res) => {
var _a2
;(_a2 = options.onProgress) == null
? void 0
: _a2.call(options, res, options)
})
}
}
}
}
const { translate: translate$1 } = createComponent('uploader')
class FileItem {
constructor() {
__publicField(this, 'status', 'ready')
__publicField(this, 'message', translate$1('ready'))
__publicField(this, 'uid', /* @__PURE__ */ new Date().getTime().toString())
__publicField(this, 'name')
__publicField(this, 'url')
__publicField(this, 'type')
__publicField(this, 'path')
__publicField(this, 'percentage', 0)
__publicField(this, 'formData', {})
}
}
const { componentName, create, translate } = createComponent('uploader')
const _sfc_main = create({
components: {
[Progress.name]: Progress,
[Button.name]: Button,
Photograph,
Failure,
Loading,
Del,
Link,
},
props: {
name: { type: String, default: 'file' },
url: { type: String, default: '' },
sizeType: {
type: Array,
default: () => ['original', 'compressed'],
},
sourceType: {
type: Array,
default: () => ['album', 'camera'],
},
mediaType: {
type: Array,
default: () => ['image', 'video', 'mix'],
},
camera: {
type: String,
default: 'back',
},
timeout: { type: [Number, String], default: 1e3 * 30 },
// defaultFileList: { type: Array, default: () => new Array<FileItem>() },
fileList: { type: Array, default: () => [] },
isPreview: { type: Boolean, default: true },
// picture、list
listType: { type: String, default: 'picture' },
isDeletable: { type: Boolean, default: true },
method: { type: String, default: 'post' },
capture: { type: Boolean, default: false },
maximize: { type: [Number, String], default: Number.MAX_VALUE },
maximum: { type: [Number, String], default: 9 },
clearInput: { type: Boolean, default: true },
accept: { type: String, default: '*' },
headers: { type: Object, default: {} },
data: { type: Object, default: {} },
xhrState: { type: [Number, String], default: 200 },
multiple: { type: Boolean, default: true },
disabled: { type: Boolean, default: false },
autoUpload: { type: Boolean, default: true },
maxDuration: { type: Number, default: 10 },
beforeXhrUpload: {
type: Function,
default: null,
},
beforeDelete: {
type: Function,
default: (file, files) => {
return true
},
},
onChange: { type: Function },
},
emits: [
'start',
'progress',
'oversize',
'success',
'failure',
'change',
'delete',
'update:fileList',
'file-item-click',
],
setup(props, { emit }) {
const fileList = reactive(props.fileList)
let uploadQueue = []
const classes = computed(() => {
const prefixCls = componentName
return {
[prefixCls]: true,
}
})
const chooseImage = () => {
if (props.disabled) {
return
}
if (Taro.getEnv() == 'WEB') {
let el = document.getElementById('taroChooseImage')
if (el) {
el == null ? void 0 : el.setAttribute('accept', props.accept)
} else {
const obj = document.createElement('input')
obj.setAttribute('type', 'file')
obj.setAttribute('id', 'taroChooseImage')
obj.setAttribute('accept', props.accept)
obj.setAttribute(
'style',
'position: fixed; top: -4000px; left: -3000px; z-index: -300;',
)
document.body.appendChild(obj)
}
}
if (Taro.getEnv() == 'WEAPP') {
// 上传文件类型
if (props.mediaType.includes('file')) {
wx.chooseMessageFile({
/** 最多可以选择的文件个数 */
count: props.multiple
? props.maximum * 1 - props.fileList.length
: 1,
type: 'file',
/** 接口调用失败的回调函数 */
fail: (res) => {
emit('failure', res)
},
/** 接口调用成功的回调函数 */
success: onChangeFile,
})
} else {
Taro.chooseMedia({
/** 最多可以选择的文件个数 */
count: props.multiple
? props.maximum * 1 - props.fileList.length
: 1,
/** 文件类型 */
mediaType: props.mediaType,
/** 图片和视频选择的来源 */
sourceType: props.sourceType,
/** 拍摄视频最长拍摄时间,单位秒。时间范围为 3s 至 30s 之间 */
maxDuration: props.maxDuration,
/** 仅对 mediaType 为 image 时有效,是否压缩所选文件 */
sizeType: [],
/** 仅在 sourceType 为 camera 时生效,使用前置或后置摄像头 */
camera: props.camera,
/** 接口调用失败的回调函数 */
fail: (res) => {
emit('failure', res)
},
/** 接口调用成功的回调函数 */
success: onChangeMedia,
})
}
} else {
Taro.chooseImage({
// 选择数量
count: props.multiple ? props.maximum * 1 - props.fileList.length : 1,
// 可以指定是原图还是压缩图,默认二者都有
sizeType: props.sizeType,
sourceType: props.sourceType,
success: onChangeImage,
fail: (res) => {
emit('failure', res)
},
})
}
}
const onChangeMedia = (res) => {
const { type, tempFiles } = res
const _files = filterFiles(tempFiles)
readFile(_files)
emit('change', {
fileList,
})
}
const onChangeFile = (res) => {
const { type, tempFiles } = res
const _files = filterFiles(tempFiles)
readFile(_files)
emit('change', {
fileList,
})
}
const onChangeImage = (res) => {
const { tempFilePaths, tempFiles } = res
const _files = filterFiles(tempFiles)
readFile(_files)
emit('change', {
fileList,
})
}
const fileItemClick = (fileItem) => {
emit('file-item-click', { fileItem })
}
const executeUpload = (fileItem, index) => {
const uploadOption = new UploadOptions()
uploadOption.name = props.name
uploadOption.url = props.url
uploadOption.fileType = fileItem.type
uploadOption.formData = fileItem.formData
uploadOption.timeout = props.timeout * 1
uploadOption.method = props.method
uploadOption.xhrState = props.xhrState
uploadOption.method = props.method
uploadOption.headers = props.headers
uploadOption.taroFilePath = fileItem.path
uploadOption.beforeXhrUpload = props.beforeXhrUpload
uploadOption.onStart = (option) => {
fileItem.status = 'ready'
fileItem.message = translate('readyUpload')
clearUploadQueue(index)
emit('start', option)
}
uploadOption.onProgress = (event, option) => {
fileItem.status = 'uploading'
fileItem.message = translate('uploading')
fileItem.percentage = event.progress
emit('progress', { event, option, percentage: fileItem.percentage })
}
uploadOption.onSuccess = (data, option) => {
fileItem.status = 'success'
fileItem.message = translate('success')
emit('success', {
data,
responseText: data,
option,
fileItem,
})
emit('update:fileList', fileList)
}
uploadOption.onFailure = (data, option) => {
fileItem.status = 'error'
fileItem.message = translate('error')
emit('failure', {
data,
responseText: data,
option,
fileItem,
})
}
let task = new UploaderTaro(uploadOption)
if (props.autoUpload) {
task.uploadTaro(Taro.uploadFile, Taro.getEnv())
} else {
uploadQueue.push(
new Promise((resolve, reject) => {
resolve(task)
}),
)
}
}
const clearUploadQueue = (index = -1) => {
if (index > -1) {
uploadQueue.splice(index, 1)
} else {
uploadQueue = []
fileList.splice(0, fileList.length)
}
}
const submit = () => {
Promise.all(uploadQueue).then((res) => {
res.forEach((i) => i.uploadTaro(Taro.uploadFile, Taro.getEnv()))
})
}
const readFile = (files) => {
files.forEach((file, index) => {
var _a, _b
let fileType = file.type
let filepath = file.tempFilePath || file.path
const fileItem = reactive(new FileItem())
if (file.fileType) {
fileType = file.fileType
} else {
const imgReg = /\.(png|jpeg|jpg|webp|gif)$/i
if (
!fileType &&
(imgReg.test(filepath) || filepath.includes('data:image'))
) {
fileType = 'image'
}
}
fileItem.path = filepath
fileItem.name = file.name ? file.name : filepath
fileItem.status = 'ready'
fileItem.message = translate('waitingUpload')
fileItem.type = fileType
if (Taro.getEnv() == 'WEB') {
const formData = new FormData()
for (const [key, value] of Object.entries(props.data)) {
formData.append(key, value)
}
formData.append(props.name, file.originalFileObj)
fileItem.name = (_a = file.originalFileObj) == null ? void 0 : _a.name
fileItem.type = (_b = file.originalFileObj) == null ? void 0 : _b.type
fileItem.formData = formData
} else {
fileItem.formData = props.data
}
if (props.isPreview) {
fileItem.url = fileType == 'video' ? file.thumbTempFilePath : filepath
}
fileList.push(fileItem)
executeUpload(fileItem, index)
})
}
const filterFiles = (files) => {
const maximum = props.maximum * 1
const maximize = props.maximize * 1
const oversizes = new Array()
files = files.filter((file) => {
if (file.size > maximize) {
oversizes.push(file)
return false
} else {
return true
}
})
if (oversizes.length) {
emit('oversize', oversizes)
}
let currentFileLength = files.length + fileList.length
if (currentFileLength > maximum) {
files.splice(files.length - (currentFileLength - maximum))
}
return files
}
const deleted = (file, index) => {
fileList.splice(index, 1)
emit('delete', {
file,
fileList,
index,
})
}
const onDelete = (file, index) => {
clearUploadQueue(index)
funInterceptor(props.beforeDelete, {
args: [file, fileList],
done: () => deleted(file, index),
})
}
return {
onDelete,
fileList,
classes,
chooseImage,
fileItemClick,
clearUploadQueue,
submit,
}
},
})
const _hoisted_1 = {
key: 0,
class: 'nut-uploader__slot',
}
const _hoisted_2 = {
key: 0,
class: 'nut-uploader__preview-img',
}
const _hoisted_3 = {
key: 0,
class: 'nut-uploader__preview__progress',
}
const _hoisted_4 = { class: 'nut-uploader__preview__progress__msg' }
const _hoisted_5 = ['onClick']
const _hoisted_6 = ['onClick', 'src']
const _hoisted_7 = {
key: 3,
class: 'nut-uploader__preview-img__file',
}
const _hoisted_8 = ['onClick']
const _hoisted_9 = { class: 'file__name_tips' }
const _hoisted_10 = { class: 'tips' }
const _hoisted_11 = {
key: 1,
class: 'nut-uploader__preview-list',
}
const _hoisted_12 = ['onClick']
const _hoisted_13 = { class: 'file__name_tips' }
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_nut_button = resolveComponent('nut-button')
const _component_Failure = resolveComponent('Failure')
const _component_Loading = resolveComponent('Loading')
const _component_Link = resolveComponent('Link')
const _component_Del = resolveComponent('Del')
const _component_nut_progress = resolveComponent('nut-progress')
const _component_Photograph = resolveComponent('Photograph')
return (
openBlock(),
createElementBlock(
'view',
{
class: normalizeClass(_ctx.classes),
},
[
_ctx.$slots.default
? (openBlock(),
createElementBlock('view', _hoisted_1, [
renderSlot(_ctx.$slots, 'default'),
createTextVNode(),
Number(_ctx.maximum) - _ctx.fileList.length
? (openBlock(),
createBlock(
_component_nut_button,
{
key: 0,
class: 'nut-uploader__input',
onClick: _ctx.chooseImage,
},
null,
8,
['onClick'],
))
: createCommentVNode('', true),
]))
: createCommentVNode('', true),
createTextVNode(),
(openBlock(true),
createElementBlock(
Fragment,
null,
renderList(_ctx.fileList, (item, index) => {
return (
openBlock(),
createElementBlock(
'view',
{
class: normalizeClass([
'nut-uploader__preview',
[_ctx.listType],
]),
key: item.uid,
},
[
_ctx.listType == 'picture' && !_ctx.$slots.default
? (openBlock(),
createElementBlock('view', _hoisted_2, [
item.status != 'success'
? (openBlock(),
createElementBlock('view', _hoisted_3, [
item.status != 'ready'
? (openBlock(),
createElementBlock(
Fragment,
{ key: 0 },
[
item.status == 'error'
? (openBlock(),
createBlock(_component_Failure, {
key: 0,
color: '#fff',
}))
: (openBlock(),
createBlock(_component_Loading, {
key: 1,
name: 'loading',
color: '#fff',
})),
],
64,
))
: createCommentVNode('', true),
createTextVNode(),
createElementVNode(
'view',
_hoisted_4,
toDisplayString(item.message),
1,
),
]))
: createCommentVNode('', true),
createTextVNode(),
_ctx.isDeletable
? (openBlock(),
createElementBlock(
'view',
{
key: 1,
class: 'close',
onClick: ($event) => _ctx.onDelete(item, index),
},
[
renderSlot(
_ctx.$slots,
'delete-icon',
{},
() => [createVNode(_component_Failure)],
),
],
8,
_hoisted_5,
))
: createCommentVNode('', true),
createTextVNode(),
['image', 'video'].includes(item.type) && item.url
? (openBlock(),
createElementBlock(
'img',
{
key: 2,
class: 'nut-uploader__preview-img__c',
mode: 'aspectFit',
onClick: ($event) => _ctx.fileItemClick(item),
src: item.url,
},
null,
8,
_hoisted_6,
))
: (openBlock(),
createElementBlock('view', _hoisted_7, [
createElementVNode(
'view',
{
class:
'nut-uploader__preview-img__file__name',
onClick: ($event) => _ctx.fileItemClick(item),
},
[
createElementVNode(
'view',
_hoisted_9,
toDisplayString(item.name),
1,
),
],
8,
_hoisted_8,
),
])),
createTextVNode(),
createElementVNode(
'view',
_hoisted_10,
toDisplayString(item.name),
1,
),
]))
: _ctx.listType == 'list'
? (openBlock(),
createElementBlock('view', _hoisted_11, [
createElementVNode(
'view',
{
class: normalizeClass([
'nut-uploader__preview-img__file__name',
[item.status],
]),
onClick: ($event) => _ctx.fileItemClick(item),
},
[
createVNode(_component_Link, {
class: 'nut-uploader__preview-img__file__link',
}),
createTextVNode(),
createElementVNode(
'view',
_hoisted_13,
toDisplayString(item.name),
1,
),
createTextVNode(),
_ctx.isDeletable
? (openBlock(),
createBlock(
_component_Del,
{
key: 0,
color: '#808080',
class:
'nut-uploader__preview-img__file__del',
onClick: ($event) =>
_ctx.onDelete(item, index),
},
null,
8,
['onClick'],
))
: createCommentVNode('', true),
],
10,
_hoisted_12,
),
createTextVNode(),
item.status == 'uploading'
? (openBlock(),
createBlock(
_component_nut_progress,
{
key: 0,
size: 'small',
percentage: item.percentage,
'stroke-color':
'linear-gradient(270deg, rgba(18,126,255,1) 0%,rgba(32,147,255,1) 32.815625%,rgba(13,242,204,1) 100%)',
'show-text': false,
},
null,
8,
['percentage'],
))
: createCommentVNode('', true),
]))
: createCommentVNode('', true),
],
2,
)
)
}),
128,
)),
createTextVNode(),
_ctx.listType == 'picture' &&
!_ctx.$slots.default &&
Number(_ctx.maximum) - _ctx.fileList.length
? (openBlock(),
createElementBlock(
'view',
{
key: 1,
class: normalizeClass([
'nut-uploader__upload',
[_ctx.listType],
]),
},
[
renderSlot(_ctx.$slots, 'upload-icon', {}, () => [
createVNode(_component_Photograph, { color: '#808080' }),
]),
createTextVNode(),
createVNode(
_component_nut_button,
{
class: normalizeClass([
'nut-uploader__input',
{ disabled: _ctx.disabled },
]),
onClick: _ctx.chooseImage,
},
null,
8,
['class', 'onClick'],
),
],
2,
))
: createCommentVNode('', true),
],
2,
)
)
}
const index_taro = /* @__PURE__ */ _export_sfc(_sfc_main, [
['render', _sfc_render],
])
export { index_taro as default }
<!--
* @Date: 2022-08-31 16:16:49
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-02-10 11:17:21
* @FilePath: /data-table/src/components/FileUploaderField/index.vue
* @LastEditTime: 2023-04-12 09:54:33
* @FilePath: /custom_form/src/components/FileUploaderField/index.vue
* @Description: 文件上传控件
-->
<template>
<div v-if="HideShow" class="file-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 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: 0.9rem; margin-left: 1rem; color: gray; padding-bottom: 0.5rem; padding-top: 0.25rem; white-space: pre-wrap;"
/>
<div>
<!-- <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.filename }}&nbsp;&nbsp;{{ (file.size / 1024 / 1024).toFixed(2) }}MB</span>
<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">
<van-uploader
</div> -->
<div style="padding: 1rem; padding-top: 0.5rem;">
<nut-uploader
:name="item.name"
upload-icon="add"
accept="*"
:before-read="beforeRead"
:after-read="afterRead"
:before-delete="beforeDelete"
: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']"
:media-type="['file']"
:maximize="max_size"
list-type="list"
:before-xhr-upload="beforeXhrUpload"
@oversize="onOversize"
@success="uploadSuccess"
@failure="uploadFailure"
@delete="onDelete"
@change="onChange"
@file-item-click="fileItemClick"
>
<van-button icon="plus" type="primary">上传文件</van-button>
</van-uploader>
<nut-button shape="square" type="primary">
<template #icon>
<Uploader />
</template>
上传文件
</nut-button>
</nut-uploader>
</div>
<!-- <div class="type-text">上传格式:{{ type_text }}</div> -->
<div
v-if="show_empty"
class="van-field__error-message"
style="padding: 0 1rem 1rem 1rem"
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 file_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,
});
......@@ -84,20 +93,166 @@ const props = defineProps({
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
const emit = defineEmits(["active"]);
const show_empty = ref(false);
// 文件类型中文页面显示
const type_text = computed(() => {
return props.item.component_props.file_type;
});
// // 文件类型中文页面显示
// const type_text = computed(() => {
// return props.item.component_props.file_type;
// });
// 上传文件集合
const fileList = ref([
// { url: "https://fastly.jsdelivr.net/npm/@vant/assets/leaf.jpeg" },
// Uploader 根据文件后缀来判断是否为文件文件
// 如果文件 URL 中不包含类型信息,可以添加 isImage 标记来声明
// { url: 'https://cloud-image', isImage: true },
]);
const fileList = ref([]);
const defaultFileList = ref([])
// 上传文件体积
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 beforeXhrUpload = async (xhr, options) => {
// 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数据结构
const resImgObj = await handleUpload(imgFile);
// 上传失败提示
if (!resImgObj.src) {
options.onFailure?.(resImgObj, options);
loading.value = false;
} else {
defaultFileList.value[defaultFileList.value.length - 1]['url'] = resImgObj.src;
fileList.value.push({
name: imgFile.name,
url: resImgObj.src,
size: imgFile.size
});
options.onSuccess?.(resImgObj, options);
loading.value = false;
}
} else {
const imgObj = defaultFileList.value[defaultFileList.value.length - 1];
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) {
// 自拍图片上传七牛服务器
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,
// format: image_info.format,
// height: image_info.height,
// width: image_info.width,
});
// 加入上传成功队列
fileList.value.push({
name: filename,
url: data.src,
size: file_info.size
});
options.onSuccess?.(data, options);
}
})
.catch((error) => {
console.error(error)
})
}
// 重复上传
if (getToken.data) {
// 加入上传成功队列
fileList.value.push({
name: filename,
url: getToken.data.src,
size: file_info.size
});
options.onSuccess?.(getToken.data, options);
}
}
})
}
}
// 上传成功回调
const uploadSuccess = async ({ data, fileItem, option, responseText }) => {
props.item.value = {
key: "file_uploader",
filed_name: props.item.key,
value: fileList.value,
};
// 完整数据回调到表单上
emit("active", props.item.value);
// 校验数据
validFileUploader();
};
// 上传失败回调
const uploadFailure = async ({ data, fileItem, option, responseText }) => {
console.error("上传失败", "fail");
toast_msg.value = '上传失败,请重新尝试!'
toast_show.value = true;
toast_type.value = 'fail';
};
// 删除上传队列回调
const onDelete = ({ file }) => {
fileList.value = fileList.value.filter((item) => {
if (item.url !== file.url) return item;
});
props.item.value = {
key: "file_uploader",
filed_name: props.item.key,
value: fileList.value,
};
// 完整数据回调到表单上
emit("active", props.item.value);
}
// 上传成功,点击队列项回调
const fileItemClick = (fileItem) => {
console.warn(fileItem);
}
const onChange = ({ fileList }) => {
}
// 上传前置处理
const beforeRead = (file) => {
......@@ -141,82 +296,25 @@ const beforeRead = (file) => {
return flag;
};
// 文件读取完成后的回调函数
const afterRead = async (files) => {
if (Array.isArray(files)) {
// 多张文件上传files是一个数组
muliUpload(files);
} else {
const imgUrl = await handleUpload(files);
// 上传失败提示
if (!imgUrl.src) {
files.status = "failed";
files.message = "上传失败";
loading.value = false;
} else {
files.status = "";
files.message = "";
fileList.value.push({
// meta_id: imgUrl.meta_id,
name: files.file.name,
url: imgUrl.src,
size: files.file.size
// isImage: true,
});
loading.value = false;
}
}
// 过滤非包含URL的文件
fileList.value = fileList.value.filter((item) => {
if (item.url) return item;
});
props.item.value = {
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);
};
// 文件删除前的回调函数
const beforeDelete = (files) => {
fileList.value = fileList.value.filter((item) => {
if (item.url !== files.url) return item;
});
props.item.value = {
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 formCode = getUrlParams(location.href) ? getUrlParams(location.href).code : ''; // 表单code
// 上传文件返回文件URL
const handleUpload = async (files) => {
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,
......@@ -225,10 +323,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) {
......@@ -243,30 +339,6 @@ 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,
size: files.file.size
// isImage: true,
});
loading.value = false;
}
}
};
// 生成数据库真实文件地址
const uploadQiniu = async (file, token, name, md5) => {
let suffix = /\.[^\.]+$/.exec(name); // 获取后缀
......@@ -307,28 +379,34 @@ const uploadQiniu = async (file, token, name, md5) => {
/****************** END *******************/
const show_error = ref(false);
const error_msg = ref('');
// 校验模块
const validFileUploader = () => {
// 必填项 未上传文件
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({ validFileUploader });
</script>
<style lang="less" scoped>
<style lang="less">
.file-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;
}
}
......
......@@ -9,7 +9,7 @@ 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 FileUploaderField from '@/components/FileUploaderField/index.vue'
import FileUploaderField from '@/components/FileUploaderField/index.vue'
import PhoneField from '@/components/PhoneField/index.vue'
import EmailField from '@/components/EmailField/index.vue'
// import SignField from '@/components/SignField/index.vue'
......@@ -108,9 +108,9 @@ export function createComponentType(data) {
// if (item.component_props.tag === 'image_uploader') {
// item.component = ImageUploaderField
// }
// if (item.component_props.tag === 'file_uploader') {
// item.component = FileUploaderField
// }
if (item.component_props.tag === 'file_uploader') {
item.component = FileUploaderField
}
if (item.component_props.tag === 'phone') {
item.name = item.key
item.component = PhoneField
......
......@@ -1359,10 +1359,10 @@
resolved "https://mirrors.cloud.tencent.com/npm/@nutui/icons-vue-taro/-/icons-vue-taro-0.0.9.tgz#b5223eb01e2b987fdbe460e5d0439a66481e54f1"
integrity sha512-10VYAtFC+o1X0anGs+y2PgF1NWMeLFz2JVMRw4BWLg6wbtVbYy9wukLxyGhZC6Yf6t39DcwaGVda8paV7K6/Ew==
"@nutui/nutui-taro@^4.0.4":
version "4.0.4"
resolved "https://mirrors.cloud.tencent.com/npm/@nutui/nutui-taro/-/nutui-taro-4.0.4.tgz#c5b65431ece527e3e531bc7923ad0a7b499daeeb"
integrity sha512-v9XyXidgiRgZTH5JofgjhJTGGvQDXhOqkLb/uRjlR2c9eQBSU653rlTO82VjUWOpCterYDxtqXswUZIf+PRbfQ==
"@nutui/nutui-taro@^4.0.5":
version "4.0.5"
resolved "https://mirrors.cloud.tencent.com/npm/@nutui/nutui-taro/-/nutui-taro-4.0.5.tgz#67c88d582e921641e81432ae1fd136e10e5bc1ea"
integrity sha512-j7+fKtRarfphYJ0uosdalIWvf3k2+x8eDsmBSSnPDyGEeRHtXqGIw8MIxy8wL5UUnADP+6JVK339vC28oo436g==
dependencies:
"@nutui/icons-vue-taro" "^0.0.9"
sass "^1.50.0"
......@@ -3546,6 +3546,13 @@ braces@^3.0.2, braces@~3.0.2:
dependencies:
fill-range "^7.0.1"
browser-md5-file@^1.1.1:
version "1.1.1"
resolved "https://mirrors.cloud.tencent.com/npm/browser-md5-file/-/browser-md5-file-1.1.1.tgz#247d63527f662d9667adecbe61808b4961b90dc6"
integrity sha512-9h2UViTtZPhBa7oHvp5mb7MvJaX5OKEPUsplDwJ800OIV+In7BOR3RXOMB78obn2iQVIiS3WkVLhG7Zu1EMwbw==
dependencies:
spark-md5 "^2.0.2"
browser-process-hrtime@^1.0.0:
version "1.0.0"
resolved "https://mirrors.cloud.tencent.com/npm/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
......@@ -11519,6 +11526,11 @@ sourcemap-codec@^1.4.8:
resolved "https://mirrors.cloud.tencent.com/npm/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
spark-md5@^2.0.2:
version "2.0.2"
resolved "https://mirrors.cloud.tencent.com/npm/spark-md5/-/spark-md5-2.0.2.tgz#37b763847763ae7e7acef2ca5233d01e649a78b7"
integrity sha1-N7djhHdjrn56zvLKUjPQHmSaeLc=
spdx-correct@^3.0.0:
version "3.2.0"
resolved "https://mirrors.cloud.tencent.com/npm/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c"
......