Toggle navigation
Toggle navigation
This project
Loading...
Sign in
Hooke
/
custom_form
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Graphs
Network
Create a new issue
Commits
Issue Boards
Authored by
hookehuyr
2023-04-12 15:03:20 +0800
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
02ab19cd4faa1bb2ed3242d94d26ebc9f7a230f1
02ab19cd
1 parent
c31a67ee
✨ feat(图片上传控件): 样式和功能调整
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
255 additions
and
222 deletions
src/components/FileUploaderField/Uploader.js
src/components/FileUploaderField/index.vue
src/components/ImageUploaderField/index.vue
src/hooks/useComponentType.js
src/components/FileUploaderField/Uploader.js
View file @
02ab19c
...
...
@@ -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
}
...
...
src/components/FileUploaderField/index.vue
View file @
02ab19c
<!--
* @Date: 2022-08-31 16:16:49
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-04-12 1
0:59:48
* @LastEditTime: 2023-04-12 1
4: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 }}. {{ file.name }} {{ (file.size / 1024 / 1024).toFixed(2) }}MB</span>
<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;
...
...
src/components/ImageUploaderField/index.vue
View file @
02ab19c
<!--
* @Date: 2022-08-31 16:16:49
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-0
1-17 16:44:32
* @FilePath: /
data-table
/src/components/ImageUploaderField/index.vue
* @LastEditTime: 2023-0
4-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"> *</span
>
<
text v-if="item.component_props.required"> *</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">上传类型: {{ 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>
<van-overlay :show="loading">
<div class="wrapper" @click.stop>
<van-loading vertical color="#FFFFFF">上传中...</van-loading>
</nut-overlay>
<nut-toast :msg="toast_msg" v-model:visible="toast_show" :type="toast_type" />
</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 {
defaultFileList.value[defaultFileList.value.length - 1]['url'] = resImgObj.src;
defaultFileList.value[defaultFileList.value.length - 1]['type'] = 'image';
fileList.value.push({
name: imgFile.name,
url: resImgObj.src,
size: imgFile.size
});
options.onSuccess?.(resImgObj, options);
loading.value = false;
}
} else {
files.status = "";
files.message = "";
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({
// meta_id: imgUrl.meta_id,
name: files.file.name,
url: imgUrl.src,
// isImage: true,
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;
})
}
// 过滤非包含URL的图片
fileList.value = fileList.value.filter((item) => {
if (item.url) return item;
// 重复上传
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: "
imag
e_uploader",
key: "
fil
e_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 beforeDelete = (files) => {
// 上传失败回调
const uploadFailure = async ({ data, fileItem, option, responseText }) => {
// 真实上传失败才会提示
if (data) {
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
s
.url) return item;
if (item.url !== file.url) return item;
});
props.item.value = {
key: "
imag
e_uploader",
key: "
fil
e_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';
}
const onChange = ({ fileList }) => {
}
/
/ var uuid = s.join("");
// return uuid
;
// };
/
********** 上传七牛云获取文件地址 ***********/
const loading = ref(false)
;
const formCode = getUrlParams(location.href) ? getUrlParams(location.href).code : ''; // 表单code
// 上传
图片返回图片
URL
const handleUpload = async (file
s
) => {
// 上传
文件返回文件
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(
file
s.file
,
file,
async (err, md5) => {
if (err) {
console.log(err);
reject(err);
}
// 获取七牛token
const filename = file
s.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';
} else {
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_e
mpty
.value;
return !show_e
rror
.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;
...
...
src/hooks/useComponentType.js
View file @
02ab19c
...
...
@@ -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
}
...
...
Please
register
or
login
to post a comment