2025-08-11 14:42:26 +08:00

429 lines
8.0 KiB
Vue

<template>
<el-button :icon="icon" :disabled="disabled" :type="type" @click="open">
{{ $t('导入') }}
</el-button>
<cl-form ref="Form">
<template #slot-upload>
<div v-if="!upload.filename" class="upload">
<div class="tips" v-if="template">
<span>{{ tips }}</span>
<el-button type="primary" text bg @click="download">{{
$t('下载模版')
}}</el-button>
</div>
<div class="inner">
<cl-upload
:ref="setRefs('upload')"
drag
:limit-size="limitSize"
:accept="accept"
:disabled="disabled"
:auto-upload="false"
:before-upload="onUpload"
:size="[220, '100%']"
/>
</div>
</div>
</template>
<template #slot-list>
<div v-if="list.length" class="data-table">
<div class="head">
<el-button type="success" @click="clear">{{ $t('重新上传') }}</el-button>
<el-button
type="danger"
:disabled="table.selection.length == 0"
@click="table.del()"
>
{{ $t('批量删除') }}
</el-button>
</div>
<div class="cl-table">
<el-table
border
:data="list"
max-height="600px"
@selection-change="table.onSelectionChange"
@row-click="
row => {
row._edit = true;
}
"
>
<el-table-column
type="selection"
width="60px"
align="center"
fixed="left"
/>
<el-table-column
:label="$t('序号')"
type="index"
width="80px"
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"
>
<template #default="scope">
<span v-if="!scope.row._edit">{{ scope.row[item] }}</span>
<template v-else>
<el-input
v-model="scope.row[item]"
type="textarea"
clearable
:placeholder="item"
/>
</template>
</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>
</template>
</cl-form>
</template>
<script lang="ts" setup>
defineOptions({
name: 'cl-import-btn'
});
import { useForm } from '@cool-vue/crud';
import { ElMessage } from 'element-plus';
import { reactive, type PropType, computed } from 'vue';
import * as XLSX from 'xlsx';
import chardet from 'chardet';
import { extname } from '/@/cool/utils';
import { has } from 'lodash-es';
import { useI18n } from 'vue-i18n';
import { useCool } from '/@/cool';
const props = defineProps({
onConfig: Function,
onSubmit: Function,
template: {
type: String,
default: ''
},
tips: String,
limitSize: {
type: Number,
default: 10
},
type: {
type: String as PropType<
'default' | 'success' | 'warning' | 'info' | 'text' | 'primary' | 'danger'
>,
default: 'success'
},
icon: String,
disabled: Boolean,
accept: {
type: String,
default:
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel,text/csv'
}
});
const emit = defineEmits(['change']);
const Form = useForm();
const { t } = useI18n();
const { refs, setRefs } = useCool();
// 提示信息
const tips = computed(() => {
return props.tips || t('请按照模版填写信息');
});
// 上传信息
const upload = reactive({
filename: '',
file: null as File | null,
list: [] as any[]
});
// 分页信息
const pagination = reactive({
size: 20,
page: 1,
onCurrentChange(page: number) {
pagination.page = page;
}
});
// 数据表格
const table = reactive({
// 表头
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 list = computed(() => {
return upload.list.filter((_, i) => {
return (
i >= (pagination.page - 1) * pagination.size && i < pagination.page * pagination.size
);
});
});
// 清空
function clear() {
upload.filename = '';
upload.file = null;
upload.list = [];
table.header = [];
table.selection = [];
refs.upload?.clear();
}
// 打开
function open() {
clear();
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('请选择文件'));
}
if (props.onSubmit) {
props.onSubmit(
{
...upload,
..._
},
{ done, close }
);
} else {
ElMessage.error(t('[cl-import-btn] onSubmit is required'));
done();
}
}
}
});
}
// 上传
function onUpload(raw: File, _: any, { next }: any) {
const reader = new FileReader();
const ext = extname(raw.name);
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;
}
// 下载模版
function download() {
const link = document.createElement('a');
link.setAttribute('href', props.template);
link.setAttribute('download', '');
link.click();
}
defineExpose({
open,
clear,
Form
});
</script>
<style lang="scss" scoped>
.upload {
display: flex;
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;
margin-bottom: 20px;
& > span {
color: var(--el-color-warning);
}
}
.data-table {
.head {
margin-bottom: 10px;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
}
</style>