|
|
<script lang="ts" setup>
|
|
|
import { reactive, ref, unref } from "vue";
|
|
|
import * as XLSX from "xlsx";
|
|
|
import { arrayEquals } from "@/utils/index";
|
|
|
import { generateUuid } from "@/utils/uuid";
|
|
|
|
|
|
const props = defineProps<{
|
|
|
onSuccess: Function;
|
|
|
headerConfig: string[];
|
|
|
}>();
|
|
|
|
|
|
interface ExcelData {
|
|
|
header: string[] | null;
|
|
|
content: any[] | null;
|
|
|
}
|
|
|
|
|
|
interface ParseResults {
|
|
|
fileName: string;
|
|
|
results: any[];
|
|
|
uuid: string;
|
|
|
}
|
|
|
|
|
|
const cardStyle = {
|
|
|
"margin-bottom":"364px",
|
|
|
width: "484px",
|
|
|
"--n-padding-bottom": "10px",
|
|
|
"--n-padding-left": "0px",
|
|
|
};
|
|
|
const inputRef = ref(null);
|
|
|
let loading = false;
|
|
|
const excelData: ExcelData = { header: null, content: null };
|
|
|
const excelDatas: ParseResults[] = reactive([]);
|
|
|
|
|
|
function generateData(content) {
|
|
|
excelData.header = props.headerConfig;
|
|
|
excelData.content = content;
|
|
|
props.onSuccess && props.onSuccess(excelData);
|
|
|
}
|
|
|
|
|
|
function handleDrop(e) {
|
|
|
e.stopPropagation();
|
|
|
e.preventDefault();
|
|
|
if (loading) return;
|
|
|
const files = e.dataTransfer.files;
|
|
|
const rawFiles = Array.from(files);
|
|
|
// eslint-disable-next-line dot-notation
|
|
|
const $message = window["$message"];
|
|
|
|
|
|
if (!isExcel(rawFiles)) {
|
|
|
$message.error("Only supports upload .xlsx, .xls, .csv suffix files");
|
|
|
return false;
|
|
|
}
|
|
|
uploadFiles(rawFiles);
|
|
|
e.stopPropagation();
|
|
|
e.preventDefault();
|
|
|
}
|
|
|
|
|
|
async function uploadFiles(files) {
|
|
|
const inputEl: HTMLInputElement | null = unref(inputRef);
|
|
|
inputEl!.value = "";
|
|
|
|
|
|
loading = true;
|
|
|
|
|
|
for (const file of files) {
|
|
|
const fileData = await readFileData(file);
|
|
|
const message = validate(fileData);
|
|
|
// TODO
|
|
|
if (message === undefined || true) {
|
|
|
const uuid = generateUuid();
|
|
|
|
|
|
excelDatas.push({
|
|
|
fileName: file.name,
|
|
|
results: (fileData as any).results,
|
|
|
uuid,
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
loading = false;
|
|
|
}
|
|
|
|
|
|
function commitData() {
|
|
|
const mergeResults: any[] = [];
|
|
|
|
|
|
if (excelDatas.length === 0) return;
|
|
|
|
|
|
excelDatas.forEach((item) => {
|
|
|
mergeResults.push(...item.results);
|
|
|
});
|
|
|
|
|
|
generateData(mergeResults);
|
|
|
}
|
|
|
|
|
|
function validate(fileData) {
|
|
|
const { header } = fileData;
|
|
|
|
|
|
// 校验表头是否匹配
|
|
|
const equal = arrayEquals(header, props.headerConfig);
|
|
|
|
|
|
if (!equal) return "表头不匹配";
|
|
|
|
|
|
// TODO:校验值是否匹配
|
|
|
}
|
|
|
|
|
|
function readFileData(file) {
|
|
|
return new Promise((resolve, reject) => {
|
|
|
const reader = new FileReader();
|
|
|
reader.onload = (e) => {
|
|
|
const data = e.target!.result;
|
|
|
const workbook = XLSX.read(data, { type: "array" });
|
|
|
const firstSheetName = workbook.SheetNames[0];
|
|
|
const worksheet = workbook.Sheets[firstSheetName];
|
|
|
const header = getHeaderRow(worksheet);
|
|
|
const results = XLSX.utils.sheet_to_json(worksheet);
|
|
|
resolve({ header, results });
|
|
|
};
|
|
|
reader.readAsArrayBuffer(file);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
function handleDragover(e) {
|
|
|
e.stopPropagation();
|
|
|
e.preventDefault();
|
|
|
e.dataTransfer.dropEffect = "copy";
|
|
|
}
|
|
|
|
|
|
function handleUpload() {
|
|
|
(inputRef.value as any).click();
|
|
|
}
|
|
|
|
|
|
function handleClick(e) {
|
|
|
const files = e.target.files;
|
|
|
const rawFiles = Array.from(files);
|
|
|
uploadFiles(rawFiles);
|
|
|
}
|
|
|
|
|
|
function getHeaderRow(sheet) {
|
|
|
const headers: string[] = [];
|
|
|
const range = XLSX.utils.decode_range(sheet["!ref"]);
|
|
|
let C;
|
|
|
const R = range.s.r;
|
|
|
/* start in the first row */
|
|
|
for (C = range.s.c; C <= range.e.c; ++C) {
|
|
|
/* walk every column in the range */
|
|
|
const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })];
|
|
|
/* find the cell in the first row */
|
|
|
let hdr = `UNKNOWN ${C}`; // <-- replace with your desired default
|
|
|
if (cell && cell.t) hdr = XLSX.utils.format_cell(cell);
|
|
|
headers.push(hdr);
|
|
|
}
|
|
|
return headers;
|
|
|
}
|
|
|
|
|
|
function isExcel(files) {
|
|
|
return files.every((file) => {
|
|
|
return /\.(xlsx|xls|csv)$/.test(file.name);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
const show = ref(false);
|
|
|
|
|
|
function showModal() {
|
|
|
show.value = true;
|
|
|
}
|
|
|
|
|
|
function closeModal() {
|
|
|
show.value = false;
|
|
|
}
|
|
|
|
|
|
async function handleSumbit(e: MouseEvent) {
|
|
|
e.preventDefault();
|
|
|
commitData();
|
|
|
closeModal();
|
|
|
}
|
|
|
|
|
|
defineExpose({
|
|
|
showModal,
|
|
|
});
|
|
|
|
|
|
function removeHandler(id: string) {
|
|
|
const index = excelDatas.findIndex((item) => item.uuid === id);
|
|
|
excelDatas.splice(index, 1);
|
|
|
}
|
|
|
|
|
|
function afterLeave() {
|
|
|
excelDatas.length = 0;
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
<n-modal v-model:show="show" transform-origin="center" @after-leave="afterLeave">
|
|
|
<n-card
|
|
|
:style="cardStyle"
|
|
|
:bordered="false"
|
|
|
size="huge"
|
|
|
role="dialog"
|
|
|
aria-modal="true"
|
|
|
>
|
|
|
<div class="wrapper">
|
|
|
<div class="wrapper-header">
|
|
|
<span class="wrapper-left">批量导入</span>
|
|
|
<div class="wrapper-right">
|
|
|
<div class="wrapper-right-close" @pointerdown="closeModal">
|
|
|
<div class="wrapper-right-icon" />
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="topline"></div>
|
|
|
<n-divider />
|
|
|
<div class="wrapper-content">
|
|
|
<div
|
|
|
class="wrapper-content-dragger"
|
|
|
@drop="handleDrop"
|
|
|
@dragover="handleDragover"
|
|
|
@dragenter="handleDragover"
|
|
|
>
|
|
|
<input
|
|
|
ref="inputRef"
|
|
|
class="excel-upload-input"
|
|
|
type="file"
|
|
|
accept=".xlsx, .xls,.csv"
|
|
|
@change="handleClick"
|
|
|
/>
|
|
|
<SvgIcon
|
|
|
style="margin-top: 32px; margin-bottom: 13px"
|
|
|
size="45"
|
|
|
name="upload"
|
|
|
@click="handleUpload"
|
|
|
/>
|
|
|
<span class="wrapper-tip1">点击或拖拽审批文件到这里上传</span>
|
|
|
<span style="margin-top: 3px; margin-bottom: 19px" class="wrapper-tip2"
|
|
|
>支持上传格式:.xls .xlsx .csv的文件</span
|
|
|
>
|
|
|
</div>
|
|
|
<div
|
|
|
v-for="(item, index) in excelDatas"
|
|
|
:key="index"
|
|
|
class="wrapper-content-files"
|
|
|
>
|
|
|
<div> <SvgIcon
|
|
|
size="24"
|
|
|
style="margin-top:-6px"
|
|
|
name="excelicon"
|
|
|
|
|
|
/><span style="margin-left:10px;paddin-top:5px">{{ item.fileName }}</span></div>
|
|
|
<div>
|
|
|
<SvgIcon
|
|
|
size="16px"
|
|
|
style="display: block; margin-left: auto; cursor: pointer"
|
|
|
name="clear"
|
|
|
@click="removeHandler(item.uuid)"
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="bottomline"></div>
|
|
|
</div>
|
|
|
<template #footer>
|
|
|
<div class="footer">
|
|
|
<n-button type="info" style="width: 60px;
|
|
|
height: 36px;
|
|
|
background: #507afd;" @click="handleSumbit"> 确定 </n-button>
|
|
|
</div>
|
|
|
</template>
|
|
|
</n-card>
|
|
|
</n-modal>
|
|
|
</template>
|
|
|
|
|
|
<style lang="less" scoped>
|
|
|
.wrapper {
|
|
|
&-header {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
padding: 0px 0px 0 24px;
|
|
|
// padding: 10px;
|
|
|
}
|
|
|
|
|
|
&-left {
|
|
|
font-size: 16px;
|
|
|
font-family: PingFang SC, PingFang SC-Medium;
|
|
|
font-weight: bolder;
|
|
|
text-align: left;
|
|
|
color: #222222;
|
|
|
line-height: 24px;
|
|
|
}
|
|
|
|
|
|
&-right {
|
|
|
&-close {
|
|
|
width: 12px;
|
|
|
height: 12px;
|
|
|
cursor: pointer;
|
|
|
margin-right: 25px;
|
|
|
color: #999999;
|
|
|
}
|
|
|
|
|
|
&-icon {
|
|
|
background: #999999;
|
|
|
display: inline-block;
|
|
|
width: 16px;
|
|
|
height: 1px;
|
|
|
transform: rotate(45deg);
|
|
|
-webkit-transform: rotate(45deg);
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
&:after {
|
|
|
content: "";
|
|
|
display: block;
|
|
|
width: 16px;
|
|
|
height: 1px;
|
|
|
background: #999999;
|
|
|
transform: rotate(-90deg);
|
|
|
-webkit-transform: rotate(-90deg);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
&-content {
|
|
|
margin: 55px 24px 0 25px;
|
|
|
&-dragger {
|
|
|
height: 150px;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
border: 1px dashed #1980ff;
|
|
|
// width: 600px;
|
|
|
font-size: 14px;
|
|
|
font-weight: bold;
|
|
|
border-radius: 2px;
|
|
|
text-align: center;
|
|
|
background: rgba(202,210,221,0.10);
|
|
|
}
|
|
|
|
|
|
&-files {
|
|
|
margin-top: 17px;
|
|
|
font-size: 14px;
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.excel-upload-input {
|
|
|
display: none;
|
|
|
z-index: -9999;
|
|
|
}
|
|
|
|
|
|
.footer {
|
|
|
display: flex;
|
|
|
justify-content: flex-end;
|
|
|
padding-right: 30px;
|
|
|
}
|
|
|
|
|
|
::v-deep(.n-card.n-card--content-segmented > .n-card__content:not(:first-child)) {
|
|
|
border: 0px;
|
|
|
}
|
|
|
|
|
|
::v-deep(.n-card > .n-card-header) {
|
|
|
--n-padding-top: 0px;
|
|
|
--n-padding-bottom: 12px;
|
|
|
}
|
|
|
|
|
|
::v-deep(.n-divider:not(.n-divider--vertical)) {
|
|
|
margin-top: 0px;
|
|
|
margin-bottom: 0px;
|
|
|
}
|
|
|
::v-deep(.n-divider:not(.n-divider--dashed) .n-divider__line){
|
|
|
background: none;
|
|
|
}
|
|
|
::v-deep(.n-button){
|
|
|
width: 60px !important;
|
|
|
height: 36px !important;
|
|
|
background: #507afd !important;
|
|
|
border-radius: 3px;
|
|
|
}
|
|
|
.wrapper-tip1 {
|
|
|
font-size: 14px;
|
|
|
font-family: PingFang SC, PingFang SC-Regular;
|
|
|
font-weight: lighter;
|
|
|
color: #666666;
|
|
|
line-height: 24px;
|
|
|
}
|
|
|
.wrapper-tip2 {
|
|
|
font-size: 12px;
|
|
|
font-family: PingFang SC, PingFang SC-Regular;
|
|
|
font-weight: lighter;
|
|
|
text-align: left;
|
|
|
color: #999999;
|
|
|
line-height: 22px;
|
|
|
}
|
|
|
.topline{
|
|
|
background: rgba(0,0,0,0.09);
|
|
|
height: 1px;
|
|
|
width: 100%;
|
|
|
position: absolute;
|
|
|
top:56px
|
|
|
}
|
|
|
.bottomline{
|
|
|
background: rgba(0,0,0,0.09);
|
|
|
height: 1px;
|
|
|
width: 100%;
|
|
|
margin-top: 33px;
|
|
|
// position: absolute;
|
|
|
// bottom:68px
|
|
|
}
|
|
|
</style>
|