2022-06-14 16:07:47 +08:00

633 lines
12 KiB
Vue

<template>
<div class="cl-upload__wrap">
<div
class="cl-upload"
:class="[
`cl-upload--${type}`,
{
'is-slot': $slots.default
}
]"
>
<!-- 列表 -->
<draggable
v-model="list"
v-bind="drag.options"
item-key="uid"
tag="div"
class="cl-upload__list"
@end="update"
>
<template #item="{ element: item, index }">
<el-upload
action=""
class="is-drag"
:accept="accept"
:show-file-list="false"
:http-request="
(req) => {
httpRequest(req, item);
}
"
:before-upload="
(file) => {
beforeUpload(file, item);
}
"
:headers="headers"
:disabled="disabled"
v-if="showFileList"
>
<div class="cl-upload__item" :class="[`is-${item.type}`]">
<!-- 图片 -->
<template v-if="item.type == 'image'">
<el-image :src="item.preload" fit="cover"></el-image>
</template>
<!-- 文件 -->
<template v-else>
<div class="cl-upload__icon">
<!-- 图标 -->
<el-icon :size="20"><document /></el-icon>
<!-- 扩展名 -->
<span>{{ extname(item.preload) }}</span>
</div>
<!-- 文件名 -->
<span class="cl-upload__name">{{ fileName(item.preload) }}</span>
<!-- 大小 -->
<span class="cl-upload__size">
{{ fileSize(item.size) }}
</span>
</template>
<!-- 工具 -->
<div class="cl-upload__actions">
<!-- 预览 -->
<span @click.stop="preview(item)" v-show="item.url">
<el-icon><zoom-in /></el-icon>
</span>
<!-- 删除 -->
<span @click.stop="remove(index)">
<el-icon><delete /></el-icon>
</span>
</div>
<!-- 进度条 -->
<div
class="cl-upload__progress"
v-if="item.progress > 0 && item.progress < 100"
>
<el-progress
:percentage="item.progress"
:show-text="false"
></el-progress>
</div>
<!-- 错误 -->
<div class="cl-upload__error" v-if="item.error">
{{ item.error }}
</div>
</div>
</el-upload>
</template>
<template #header>
<el-upload
ref="Upload"
action=""
class="un-drag"
:accept="accept"
:show-file-list="false"
:before-upload="beforeUpload"
:http-request="httpRequest"
:headers="headers"
:multiple="multiple"
:disabled="disabled"
>
<slot>
<template v-if="type == 'image' && isAdd">
<div class="cl-upload__item">
<el-icon :size="24"><picture-filled /></el-icon>
<span class="cl-upload__text">{{ text }}</span>
</div>
</template>
<template v-else-if="type == 'file'">
<div class="cl-upload__btn">
<el-button type="success">{{ text }}</el-button>
</div>
</template>
</slot>
</el-upload>
</template>
</draggable>
</div>
</div>
<cl-dialog v-model="pv.visible" title="图片预览" width="500px">
<img style="width: 100%" :src="pv.url" />
</cl-dialog>
</template>
<script lang="ts">
export default {
name: "cl-upload"
};
</script>
<script lang="ts" setup>
import { PictureFilled, ZoomIn, Delete, Document } from "@element-plus/icons-vue";
import { computed, ref, reactive, watch, PropType } from "vue";
import { useCool } from "/@/cool";
import { extname, module, uuid } from "/@/cool/utils";
import { isArray, isNumber } from "lodash";
import { ElMessage } from "element-plus";
import { useBase } from "/$/base";
import Draggable from "vuedraggable";
import { fileSize, fileName } from "../utils";
import dayjs from "dayjs";
interface Item {
url: string;
preload: string;
uid: number | string;
progress: number;
type?: string;
}
const props = defineProps({
modelValue: {
type: [String, Array],
default: () => []
},
type: {
type: String as PropType<"image" | "file">,
default: "image"
},
accept: String,
multiple: Boolean,
limit: Number,
limitSize: Number,
size: [String, Number, Array],
text: String,
prefixPath: {
type: String,
default: "app"
},
showFileList: {
type: Boolean,
default: true
},
drag: Boolean,
disabled: Boolean
});
const emit = defineEmits(["update:modelValue", "upload", "success", "error", "progress"]);
const { service } = useCool();
// 缓存
const { user } = useBase();
// 模块配置
const { options } = module.get("upload");
// el-upload
const Upload = ref<any>();
// 元素尺寸
const size = computed(() => {
const d = props.size || options.size;
return (isArray(d) ? d : [d, d]).map((e: string | number) => (isNumber(e) ? e + "px" : e));
});
// 最大上传数量
const limit = props.limit || options.limit.upload;
// 图片大小限制
const limitSize = props.limitSize || options.limit.size;
// 文案
const text = props.text || options.text;
// 请求头
const headers = computed(() => {
return {
Authorization: user.token
};
});
// 预览
const pv = reactive<any>({
visible: false,
url: ""
});
// 列表
const list = ref<Item[]>([]);
// 拖拽
const drag = reactive<any>({
options: {
group: "Upload",
animation: 300,
ghostClass: "Ghost",
dragClass: "Drag",
draggable: ".is-drag",
disabled: !props.drag
}
});
// 文件格式
const accept = computed(() => {
return props.accept || (props.type == "file" ? "*" : "image/*");
});
// 能否添加
const isAdd = computed(() => {
return props.multiple ? limit - list.value.length > 0 : list.value.length == 0;
});
// 上传前
function beforeUpload(file: any, item?: Item) {
if (file.size / 1024 / 1024 >= limitSize) {
ElMessage.error(`上传文件大小不能超过 ${limitSize}MB!`);
return false;
}
const d = {
type: file.type.includes("image") ? "image" : "file",
preload: "",
progress: 0,
url: "",
uid: file.uid,
size: file.size
};
d.preload = d.type == "image" ? window.webkitURL.createObjectURL(file) : file.name;
if (!item) {
if (isAdd.value) {
list.value.push(d);
} else {
list.value = [d];
}
} else {
Object.assign(item, d);
}
emit("upload", d);
return true;
}
// 移除
function remove(index: number) {
list.value.splice(index, 1);
update();
}
// 预览
function preview(item: Item) {
if (item.type == "image") {
pv.visible = true;
pv.url = item.url;
} else {
window.open(item.url);
}
}
// 文件上传请求
async function httpRequest(req: any, item?: any) {
if (!item) {
item = list.value.find((e) => e.uid == req.file.uid);
}
try {
// 文件名 uuid + filename
let fileName = uuid() + "_" + req.file.name;
const { mode, type } = await service.base.comm.uploadMode();
// 多种上传请求
return new Promise((resolve, reject) => {
async function next(params: any) {
const data = new FormData();
for (const i in params) {
if (!["host", "publicDomain", "fileKey", "uploadUrl", "preview"].includes(i)) {
data.append(i, params[i]);
}
}
if (mode == "local") {
data.append("key", fileName);
} else {
fileName = [props.prefixPath, dayjs().format("YYYY-MM-DD"), fileName]
.filter(Boolean)
.join("/");
data.append("key", fileName);
}
data.append("file", req.file);
// 上传
await service
.request({
url: params.host,
method: "POST",
headers: {
"Content-Type": "multipart/form-data"
},
timeout: 600000,
data,
onUploadProgress(e: any) {
item.progress = parseInt((e.loaded / e.total) * 100);
emit("progress", item);
},
proxy: mode == "local" ? true : false
})
.then((res) => {
if (mode === "local") {
item.url = res;
} else {
item.url = `${params.preview}/${fileName}`;
}
emit("success", item);
resolve(item.url);
update();
})
.catch((err) => {
ElMessage.error(err.message);
item.error = err.message;
emit("error", item);
reject(err);
});
}
if (mode == "local") {
next({
host: "/admin/base/comm/upload"
});
} else {
service.base.comm
.upload()
.then((res) => {
next({
host: res.uploadUrl,
preview: res.host || res.publicDomain,
...res
});
})
.catch(reject);
}
});
} catch (err) {
ElMessage.error("上传配置错误");
}
}
// 检测是否上传完
function check() {
return list.value.find((e) => e.progress != 100);
}
// 文件类型
function fileType(path: string) {
if (
["bmp", "jpg", "jpeg", "png", "tif", "gif", "svg", "webp"].includes(
extname(path).toLocaleLowerCase()
)
) {
return "image";
} else {
return "file";
}
}
// 更新
function update() {
const check = list.value.find((e) => !e.url);
if (!check) {
emit("update:modelValue", list.value.map((e: Item) => e.url).join(","));
}
}
// 获取
watch(
() => props.modelValue,
(val: any) => {
const arr = (isArray(val) ? val : (val || "").split(",")).filter(Boolean);
list.value = arr
.map((url: string) => {
const item = list.value.find((e) => url == e.url);
if (item) {
return item;
} else {
return {
type: fileType(url),
progress: 0,
uid: uuid(),
url,
preload: url
};
}
})
.filter((_: any, i: number) => {
return props.multiple ? true : i == 0;
});
},
{
immediate: true
}
);
// 导出
defineExpose({
isAdd,
list,
check,
remove,
upload(file: File) {
Upload.value.handleStart(file);
Upload.value.submit();
}
});
</script>
<style lang="scss">
.cl-upload {
display: flex;
flex-wrap: wrap;
line-height: normal;
.Ghost {
opacity: 0.7;
}
.Drag {
border: 1px dashed #000 !important;
background: #fff !important;
box-sizing: border-box;
}
.is-drag {
margin: 0 5px 5px 0;
}
.un-drag {
.cl-upload__item {
margin-right: 5px;
}
.cl-upload__btn {
margin-bottom: 5px;
}
}
&--file {
.cl-upload__list {
width: 100%;
.un-drag {
width: 100%;
}
}
}
&__list {
display: flex;
flex-wrap: wrap;
margin: 0 5px 0 0;
}
&__text {
font-size: 13px;
margin-top: 5px;
}
&__actions {
position: absolute;
display: none;
align-items: center;
justify-content: center;
z-index: 9;
height: 100%;
width: 100%;
background-color: rgba(0, 0, 0, 0.5);
span {
display: inline-flex;
align-items: center;
justify-content: center;
height: 30px;
width: 30px;
color: #fff;
font-size: 18px;
}
}
&__progress {
position: absolute;
bottom: 10px;
left: 10px;
width: calc(100% - 20px);
}
&__item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
height: v-bind("size[0]");
width: v-bind("size[1]");
color: #333;
box-sizing: border-box;
overflow: hidden;
user-select: none;
&:hover {
border-color: currentColor;
color: var(--color-primary);
.cl-upload__actions {
display: inline-flex;
}
}
&.is-file {
padding: 10px;
.cl-upload {
&__icon {
display: flex;
align-items: center;
span {
font-size: 13px;
text-transform: uppercase;
margin-left: 5px;
color: #666;
}
}
&__name {
height: 15px;
width: 100%;
margin-top: 5px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
text-align: center;
}
&__size {
position: absolute;
top: 5px;
right: 5px;
font-size: 12px;
line-height: 12px;
color: #999;
}
}
}
}
&__error {
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
font-size: 12px;
color: red;
padding: 10px;
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
box-sizing: border-box;
}
&.is-slot {
.cl-upload__list {
margin: 0;
}
.un-drag {
display: flex;
}
}
}
</style>