This commit is contained in:
icssoa 2025-08-11 14:42:26 +08:00
parent 79fd3b9093
commit 928848877b

View File

@ -1,642 +1,428 @@
<template> <template>
<div class="cl-upload__wrap" :class="[customClass]"> <el-button :icon="icon" :disabled="disabled" :type="type" @click="open">
<div {{ $t('导入') }}
class="cl-upload" </el-button>
:class="[
`cl-upload--${type}`, <cl-form ref="Form">
{ <template #slot-upload>
'is-disabled': disabled, <div v-if="!upload.filename" class="upload">
'is-multiple': multiple, <div class="tips" v-if="template">
'is-small': small <span>{{ tips }}</span>
} <el-button type="primary" text bg @click="download">{{
]" $t('下载模版')
> }}</el-button>
<template v-if="!drag"> </div>
<div v-if="type == 'file'" class="cl-upload__file-btn">
<el-upload <div class="inner">
<cl-upload
:ref="setRefs('upload')" :ref="setRefs('upload')"
:drag="drag" drag
action="" :limit-size="limitSize"
:accept="accept" :accept="accept"
:show-file-list="false"
:before-upload="onBeforeUpload"
:http-request="httpRequest"
:headers="headers"
:multiple="multiple"
:disabled="disabled" :disabled="disabled"
> :auto-upload="false"
<slot> :before-upload="onUpload"
<el-button type="success">{{ text }}</el-button> :size="[220, '100%']"
</slot> />
</el-upload> </div>
</div> </div>
</template> </template>
<!-- 列表 --> <template #slot-list>
<vue-draggable <div v-if="list.length" class="data-table">
v-if="showList" <div class="head">
v-model="list" <el-button type="success" @click="clear">{{ $t('重新上传') }}</el-button>
class="cl-upload__list" <el-button
tag="div" type="danger"
ghost-class="Ghost" :disabled="table.selection.length == 0"
drag-class="Drag" @click="table.del()"
item-key="uid"
:disabled="!draggable"
@end="update"
> >
<!-- 触发器 --> {{ $t('批量删除') }}
<template #footer> </el-button>
<div v-if="(type == 'image' || drag) && isAdd" class="cl-upload__footer">
<el-upload
:ref="setRefs('upload')"
action=""
:drag="drag"
:accept="accept"
:show-file-list="false"
:before-upload="onBeforeUpload"
:http-request="httpRequest"
:headers="headers"
:multiple="multiple"
:disabled="disabled"
>
<slot>
<!-- 拖拽方式 -->
<div v-if="drag" class="cl-upload__demo is-dragger">
<el-icon :size="46">
<upload-filled />
</el-icon>
<div>
{{
t('点击上传或将文件拖动到此处,文件大小限制{n}M', {
n: limitSize
})
}}
</div>
</div> </div>
<!-- 点击方式 --> <div class="cl-table">
<div v-else class="cl-upload__demo"> <el-table
<el-icon :size="36"> border
<component :is="icon" v-if="icon" /> :data="list"
<picture-filled v-else /> max-height="600px"
</el-icon> @selection-change="table.onSelectionChange"
<span v-if="text" class="text">{{ text }}</span> @row-click="
</div> row => {
</slot> row._edit = true;
</el-upload>
</div>
</template>
<!-- 列表 -->
<template #item="{ element: item, index }">
<el-upload
action=""
:accept="accept"
:show-file-list="false"
:http-request="
req => {
return httpRequest(req, item);
} }
" "
:before-upload="
file => {
onBeforeUpload(file, item);
}
"
:headers="headers"
:disabled="disabled"
> >
<slot name="item" :item="item" :index="index"> <el-table-column
<div class="cl-upload__item"> type="selection"
<upload-item width="60px"
:show-tag="showTag" align="center"
:item="item" fixed="left"
:list="list"
:disabled="disabled"
:deletable="deletable"
@remove="remove(index)"
/> />
<!-- 小图模式 --> <el-table-column
<el-icon :label="$t('序号')"
v-if="small" type="index"
class="cl-upload__item-remove" width="80px"
@click.stop="remove(index)" align="center"
fixed="left"
:index="table.onIndex"
/>
<el-table-column
v-for="item in table.header"
:key="item"
:prop="item"
:label="item"
min-width="160px"
align="center"
> >
<circle-close-filled /> <template #default="scope">
</el-icon> <span v-if="!scope.row._edit">{{ scope.row[item] }}</span>
</div>
</slot> <template v-else>
</el-upload> <el-input
v-model="scope.row[item]"
type="textarea"
clearable
:placeholder="item"
/>
</template> </template>
</vue-draggable> </template>
</el-table-column>
<el-table-column
:label="$t('操作')"
width="100px"
align="center"
fixed="right"
>
<template #default="scope">
<el-button
text
bg
type="danger"
@click.stop="table.del(scope.$index)"
>
{{ $t('删除') }}
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="pagination">
<el-pagination
v-model:current-page="pagination.page"
background
layout="total, prev, pager, next"
:total="upload.list.length"
:page-size="pagination.size"
@current-change="pagination.onCurrentChange"
/>
</div> </div>
</div> </div>
</template> </template>
</cl-form>
</template>
<script lang="ts" setup> <script lang="ts" setup>
defineOptions({ defineOptions({
name: 'cl-upload' name: 'cl-import-btn'
}); });
import { computed, ref, watch, type PropType, nextTick } from 'vue';
import { assign, isArray, isEmpty, isNumber } from 'lodash-es';
import VueDraggable from 'vuedraggable';
import { ElMessage } from 'element-plus';
import { PictureFilled, UploadFilled, CircleCloseFilled } from '@element-plus/icons-vue';
import { useForm } from '@cool-vue/crud'; import { useForm } from '@cool-vue/crud';
import { useCool } from '/@/cool'; import { ElMessage } from 'element-plus';
import { useBase } from '/$/base'; import { reactive, type PropType, computed } from 'vue';
import { uuid, isPromise } from '/@/cool/utils'; import * as XLSX from 'xlsx';
import { getUrls, getType } from '../utils'; import chardet from 'chardet';
import { useUpload } from '../hooks'; import { extname } from '/@/cool/utils';
import UploadItem from './upload-item/index.vue'; import { has } from 'lodash-es';
import { CrudProps } from '/#/crud';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useCool } from '/@/cool';
const props = defineProps({ const props = defineProps({
...CrudProps, onConfig: Function,
// onSubmit: Function,
modelValue: { template: {
type: [String, Array], type: String,
default: () => [] default: ''
},
tips: String,
limitSize: {
type: Number,
default: 10
}, },
//
type: { type: {
type: String as PropType<'image' | 'file'>, type: String as PropType<
default: 'image' 'default' | 'success' | 'warning' | 'info' | 'text' | 'primary' | 'danger'
>,
default: 'success'
}, },
// icon: String,
accept: String,
//
multiple: Boolean,
//
limit: Number,
//
limitSize: Number,
//
autoUpload: {
type: Boolean,
default: true
},
//
size: [String, Number, Array],
//
small: Boolean,
//
icon: null,
//
text: String,
//
showTag: {
type: Boolean,
default: true
},
//
showFileList: {
type: Boolean,
default: true
},
//
draggable: Boolean,
//
drag: Boolean,
//
disabled: Boolean, disabled: Boolean,
// accept: {
deletable: Boolean, type: String,
//
customClass: String,
//
beforeUpload: Function,
//
prefixPath: String
});
const emit = defineEmits(['update:modelValue', 'change', 'upload', 'success', 'error', 'progress']);
const { refs, setRefs } = useCool();
const { user } = useBase();
const Form = useForm();
const { options, toUpload } = useUpload();
const { t } = useI18n();
//
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 disabled = computed(() => {
return props.isDisabled || props.disabled;
});
//
const limit = props.limit || options.limit.upload;
//
const limitSize = props.limitSize || options.limit.size;
//
const text = computed(() => {
if (props.text !== undefined) {
return props.text;
} else {
switch (props.type) {
case 'file':
return t('选择文件');
case 'image':
return t('选择图片');
default: default:
return ''; 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel,text/csv'
}
} }
}); });
// const emit = defineEmits(['change']);
const headers = computed(() => {
return { const Form = useForm();
Authorization: user.token const { t } = useI18n();
}; const { refs, setRefs } = useCool();
//
const tips = computed(() => {
return props.tips || t('请按照模版填写信息');
}); });
// //
const list = ref<Upload.Item[]>([]); const upload = reactive({
filename: '',
file: null as File | null,
list: [] as any[]
});
// //
const showList = computed(() => { const pagination = reactive({
if (props.type == 'file') { size: 20,
return props.showFileList ? !isEmpty(list.value) : false; page: 1,
} else { onCurrentChange(page: number) {
return true; pagination.page = page;
} }
}); });
// //
const accept = computed(() => { const table = reactive({
return props.accept || (props.type == 'file' ? '' : 'image/*'); //
header: [] as string[],
//
selection: [] as any[],
//
del(index?: number) {
if (index !== undefined) {
upload.list.splice(index, 1);
} else {
upload.list = upload.list.filter(a => {
return !table.selection.includes(a._index);
});
}
},
//
onIndex(index: number) {
return index + 1 + (pagination.page - 1) * pagination.size;
},
//
onSelectionChange(arr: any[]) {
table.selection = arr.map(e => e._index);
}
}); });
// //
const isAdd = computed(() => { const list = computed(() => {
const len = list.value.length; return upload.list.filter((_, i) => {
return (
if (props.multiple && !disabled.value) { i >= (pagination.page - 1) * pagination.size && i < pagination.page * pagination.size
return limit - len > 0; );
} });
return len == 0;
}); });
//
async function onBeforeUpload(file: any, item?: Upload.Item) {
function next() {
const d = {
uid: file.uid,
size: file.size,
name: file.name,
type: getType(file.name),
progress: props.autoUpload ? 0 : 100, // 100%
url: '',
preload: '',
error: ''
};
//
if (d.type == 'image') {
if (file instanceof File) {
d.preload = window.webkitURL.createObjectURL(file);
}
}
//
emit('upload', d, file);
//
if (item) {
assign(item, d);
} else {
if (props.multiple) {
if (!isAdd.value) {
ElMessage.warning(t('最多只能上传{n}个文件', { n: limit }));
return false;
} else {
list.value.push(d);
}
} else {
list.value = [d];
}
}
return true;
}
//
if (file.size / 1024 / 1024 >= limitSize) {
ElMessage.error(t('上传文件大小不能超过 {n}MB!', { n: limitSize }));
return false;
}
//
if (props.beforeUpload) {
let r = props.beforeUpload(file, item, { next });
if (isPromise(r)) {
r.then(next).catch(() => null);
} else {
if (r) {
r = next();
}
}
return r;
} else {
return next();
}
}
//
function remove(index: number) {
list.value.splice(index, 1);
update();
}
// //
function clear() { function clear() {
list.value = []; upload.filename = '';
upload.file = null;
upload.list = [];
table.header = [];
table.selection = [];
refs.upload?.clear();
} }
// //
async function httpRequest(req: any, item?: Upload.Item) { function open() {
if (!item) {
item = list.value.find(e => e.uid == req.file.uid);
}
if (!item) {
return false;
}
//
toUpload(req.file, {
prefixPath: props.prefixPath,
onProgress(progress) {
item!.progress = progress;
emit('progress', item);
}
})
.then(res => {
assign(item!, res);
emit('success', item);
update();
})
.catch(err => {
item!.error = err.message;
emit('error', item);
});
}
//
function check() {
return list.value.find(e => !e.url);
}
//
function update() {
if (!check()) {
const urls = getUrls(list.value);
const val = props.multiple ? getUrls(list.value) : urls[0] || '';
//
emit('update:modelValue', val);
emit('change', val);
nextTick(() => {
if (props.prop) {
Form.value?.validateField(props.prop);
}
//
refs.upload?.clearFiles();
});
}
}
//
function upload(file: File) {
clear(); clear();
refs.upload?.clearFiles(); Form.value?.open({
title: t('导入'),
width: computed(() => (upload.filename ? '80%' : '800px')),
dialog: {
'close-on-press-escape': false
},
items: [
...(props.onConfig ? props.onConfig(Form) : []),
{
prop: 'file',
component: {
name: 'slot-upload'
},
hidden() {
return upload.filename;
}
},
{
component: {
name: 'slot-list'
}
}
],
op: {
saveButtonText: t('提交')
},
on: {
submit(_, { done, close }) {
if (!upload.filename) {
done();
return ElMessage.error(t('请选择文件'));
}
nextTick(() => { if (props.onSubmit) {
refs.upload?.handleStart(file); props.onSubmit(
refs.upload?.submit(); {
...upload,
..._
},
{ done, close }
);
} else {
ElMessage.error(t('[cl-import-btn] onSubmit is required'));
done();
}
}
}
}); });
} }
// //
watch( function onUpload(raw: File, _: any, { next }: any) {
() => props.modelValue, const reader = new FileReader();
(val: any[] | string) => { const ext = extname(raw.name);
if (check()) {
reader.onload = (event: any) => {
try {
let data = '';
if (ext == 'csv') {
const detected: any = chardet.detect(new Uint8Array(event.target.result));
const decoder = new TextDecoder(detected);
data = decoder.decode(event.target.result);
} else {
data = event.target.result;
}
const workbook = XLSX.read(data, { type: 'binary', raw: ext == 'csv' });
let json: any[] = [];
for (const sheet in workbook.Sheets) {
if (has(workbook.Sheets, sheet)) {
json = json.concat(
XLSX.utils.sheet_to_json(workbook.Sheets[sheet], {
raw: false,
dateNF: 'yyyy-mm-dd',
defval: ''
})
);
}
}
upload.list = json.map((e, i) => {
return {
...e,
_index: i
};
});
upload.filename = raw.name;
upload.file = raw;
const sheet = workbook.Sheets[Object.keys(workbook.Sheets)[0]];
for (const i in sheet) {
if (i[0] === '!') continue;
const row = i.match(/[0-9]+/)?.[0];
if (row == '1') {
table.header.push(sheet[i].v);
}
}
emit('change', json);
} catch (err) {
ElMessage.error(t('文件异常,请检查内容是否正确'));
clear();
}
};
if (ext == 'csv') {
reader.readAsArrayBuffer(raw);
} else {
reader.readAsBinaryString(raw);
}
next();
return false; return false;
} }
const urls = (isArray(val) ? val : [val]).filter(Boolean); //
function download() {
list.value = urls const link = document.createElement('a');
.map((url, index) => { link.setAttribute('href', props.template);
const old = list.value[index] || {}; link.setAttribute('download', '');
link.click();
return assign(
{
progress: 100,
uid: uuid()
},
old,
{
type: getType(url),
url,
preload: old.url == url ? old.preload : url //
} }
);
})
.filter((_, i) => {
return props.multiple ? true : i == 0;
});
},
{
immediate: true
}
);
//
defineExpose({ defineExpose({
isAdd, open,
list,
check,
clear, clear,
remove, Form
upload
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.cl-upload { .upload {
line-height: normal;
.Ghost {
.cl-upload__item {
border: 1px dashed var(--el-color-primary) !important;
}
}
&__file {
width: 100%;
&-btn {
width: fit-content;
}
}
&__list {
display: flex;
flex-wrap: wrap;
}
&__item,
&__demo {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.inner {
width: 100%;
:deep(.cl-upload) {
.cl-upload__footer,
.cl-upload__list,
.el-upload,
.is-drag {
width: 100% !important;
}
}
}
}
.tips {
display: flex;
justify-content: space-between;
align-items: center; align-items: center;
justify-content: center; margin-bottom: 20px;
height: v-bind('size[0]');
width: v-bind('size[1]');
background-color: var(--el-fill-color-light);
color: var(--el-text-color-regular);
border-radius: 8px;
cursor: pointer;
box-sizing: border-box;
position: relative;
user-select: none;
}
&__demo { & > span {
font-size: 13px; color: var(--el-color-warning);
.el-icon {
font-size: 46px;
}
.text {
margin-top: 5px;
}
&.is-dragger {
padding: 20px;
} }
} }
&__file-btn { .data-table {
& + .cl-upload__list { .head {
margin-bottom: 10px;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 10px; margin-top: 10px;
} }
} }
:deep(.el-upload) {
display: block;
.el-upload-dragger {
padding: 0;
border: 0;
background-color: transparent !important;
position: relative;
&.is-dragover {
&::after {
display: block;
content: '';
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
pointer-events: none;
border-radius: 8px;
box-sizing: border-box;
border: 1px dashed var(--el-color-primary);
}
}
}
}
&.is-disabled {
.cl-upload__demo {
color: var(--el-text-color-placeholder);
}
:deep(.cl-upload__item) {
cursor: not-allowed;
background-color: var(--el-disabled-bg-color);
}
}
&.is-multiple {
.cl-upload__list {
margin-bottom: -5px;
}
.cl-upload__item {
margin: 0 5px 5px 0;
}
.cl-upload__footer {
margin-bottom: 5px;
}
}
&.is-small {
.cl-upload__demo {
.el-icon {
font-size: 20px !important;
}
.text {
display: none;
}
}
.cl-upload__item-remove {
position: absolute;
right: 0px;
top: 0px;
color: var(--el-color-danger);
background-color: #fff;
border-radius: 100%;
}
:deep(.cl-upload-item) {
.cl-upload-item__progress-bar,
.cl-upload-item__actions,
.cl-upload-item__tag {
display: none;
}
.cl-upload-item__progress-value {
font-size: 12px;
}
}
}
&:not(.is-disabled) {
.cl-upload__demo {
&:hover {
color: var(--el-color-primary);
}
}
}
}
</style> </style>