feat(PageContent): 支持默认工具栏的导入

This commit is contained in:
cshaptx4869
2024-06-10 12:18:17 +08:00
parent 6c2542c78b
commit 5b66c33ef1
2 changed files with 257 additions and 51 deletions

View File

@@ -87,13 +87,23 @@
<!-- 导出 --> <!-- 导出 -->
<template v-else-if="item === 'exports'"> <template v-else-if="item === 'exports'">
<el-button <el-button
icon="FolderOpened" icon="download"
circle circle
title="导出" title="导出"
v-hasPerm="[`${contentConfig.pageName}:export`]" v-hasPerm="[`${contentConfig.pageName}:export`]"
@click="handleToolbar(item)" @click="handleToolbar(item)"
/> />
</template> </template>
<!-- 导入 -->
<template v-else-if="item === 'imports'">
<el-button
icon="upload"
circle
title="导入"
v-hasPerm="[`${contentConfig.pageName}:import`]"
@click="handleToolbar(item)"
/>
</template>
<!-- 搜索 --> <!-- 搜索 -->
<template v-else-if="item === 'search'"> <template v-else-if="item === 'search'">
<el-button <el-button
@@ -111,7 +121,7 @@
<el-button <el-button
:icon="item.icon" :icon="item.icon"
circle circle
:title="item.text" :title="item.title"
v-hasPerm="[`${contentConfig.pageName}:${item.auth}`]" v-hasPerm="[`${contentConfig.pageName}:${item.auth}`]"
@click="handleToolbar(item.name)" @click="handleToolbar(item.name)"
/> />
@@ -329,7 +339,9 @@
<!-- 导出弹窗 --> <!-- 导出弹窗 -->
<el-dialog <el-dialog
v-model="exportsModalVisible" v-model="exportsModalVisible"
:align-center="true"
title="导出数据" title="导出数据"
width="600px"
style="padding-right: 0" style="padding-right: 0"
@close="handleCloseExportsModal" @close="handleCloseExportsModal"
> >
@@ -390,6 +402,72 @@
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
<!-- 导入弹窗 -->
<el-dialog
v-model="importsModalVisible"
:align-center="true"
title="导入数据"
width="600px"
style="padding-right: 0"
@close="handleCloseImportsModal"
>
<!-- 滚动 -->
<el-scrollbar max-height="60vh">
<!-- 表单 -->
<el-form
ref="importsFormRef"
label-width="auto"
style="padding-right: var(--el-dialog-padding-primary)"
:model="importsFormData"
:rules="importsFormRules"
>
<el-form-item label="文件名" prop="files">
<el-upload
class="w-full"
ref="uploadRef"
v-model:file-list="importsFormData.files"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
:drag="true"
:limit="1"
:auto-upload="false"
:on-exceed="handleFileExceed"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
*.xlsx / *.xls
<el-link
v-if="contentConfig.importsTemplate"
type="primary"
icon="download"
:underline="false"
@click="handleDownloadTemplate"
>
下载模板
</el-link>
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
</el-scrollbar>
<!-- 弹窗底部操作按钮 -->
<template #footer>
<div style="padding-right: var(--el-dialog-padding-primary)">
<el-button
type="primary"
:disabled="importsFormData.files.length === 0"
@click="handleImportsSubmit"
>
</el-button>
<el-button @click="handleCloseImportsModal"> </el-button>
</div>
</template>
</el-dialog>
</el-card> </el-card>
</template> </template>
@@ -399,11 +477,15 @@ import { ref, reactive } from "vue";
import { useDateFormat, useThrottleFn } from "@vueuse/core"; import { useDateFormat, useThrottleFn } from "@vueuse/core";
import { hasAuth } from "@/plugins/permission"; import { hasAuth } from "@/plugins/permission";
import SvgIcon from "@/components/SvgIcon/index.vue"; import SvgIcon from "@/components/SvgIcon/index.vue";
import type { import {
TableProps, type TableProps,
PaginationProps, type PaginationProps,
FormInstance, type FormInstance,
FormRules, type FormRules,
type UploadInstance,
type UploadUserFile,
type UploadRawFile,
genFileId,
} from "element-plus"; } from "element-plus";
// 对象类型 // 对象类型
@@ -442,18 +524,22 @@ export interface IContentConfig<T = any> {
list: IObject[]; list: IObject[];
[key: string]: any; [key: string]: any;
}; };
// 删除的网络请求函数(需返回promise)
deleteAction?: (ids: string) => Promise<any>;
// 后端导出的网络请求函数(需返回promise)
exportAction?: (queryParams: T) => Promise<any>;
// 前端全量导出的网络请求函数(需返回promise)
exportsAction?: (queryParams: T) => Promise<IObject[]>;
// 修改属性的网络请求函数(需返回promise) // 修改属性的网络请求函数(需返回promise)
modifyAction?: (data: { modifyAction?: (data: {
[key: string]: any; [key: string]: any;
field: string; field: string;
value: boolean | string | number; value: boolean | string | number;
}) => Promise<any>; }) => Promise<any>;
// 删除的网络请求函数(需返回promise)
deleteAction?: (ids: string) => Promise<any>;
// 后端导出的网络请求函数(需返回promise)
exportAction?: (queryParams: T) => Promise<any>;
// 前端全量导出的网络请求函数(需返回promise)
exportsAction?: (queryParams: T) => Promise<IObject[]>;
// 前端导入模板
importsTemplate?: string | (() => Promise<any>);
// 前端导入的网络请求函数(需返回promise)
importsAction?: (data: IObject[]) => Promise<any>;
// 主键名(默认为id) // 主键名(默认为id)
pk?: string; pk?: string;
// 表格工具栏(默认支持add,delete,export,也可自定义) // 表格工具栏(默认支持add,delete,export,也可自定义)
@@ -472,6 +558,7 @@ export interface IContentConfig<T = any> {
defaultToolbar?: Array< defaultToolbar?: Array<
| "refresh" | "refresh"
| "filter" | "filter"
| "imports"
| "exports" | "exports"
| "search" | "search"
| { | {
@@ -561,8 +648,6 @@ const toolbar = props.contentConfig.toolbar ?? ["add", "delete"];
const defaultToolbar = props.contentConfig.defaultToolbar ?? [ const defaultToolbar = props.contentConfig.defaultToolbar ?? [
"refresh", "refresh",
"filter", "filter",
"exports",
"search",
]; ];
// 表格列 // 表格列
const cols = ref( const cols = ref(
@@ -716,36 +801,147 @@ function handleExports() {
if (props.contentConfig.exportsAction) { if (props.contentConfig.exportsAction) {
props.contentConfig.exportsAction(lastFormData).then((res) => { props.contentConfig.exportsAction(lastFormData).then((res) => {
worksheet.addRows(res); worksheet.addRows(res);
downloadXlsx(workbook, filename); workbook.xlsx
.writeBuffer()
.then((buffer) => {
saveXlsx(buffer, filename);
})
.catch((error) => console.log(error));
}); });
} else { } else {
ElMessage.error("未配置exportsAction"); ElMessage.error("未配置exportsAction");
} }
} else if (exportsFormData.origin === ExportsOriginEnum.SELECTED) {
worksheet.addRows(selectionData.value);
downloadXlsx(workbook, filename);
} else { } else {
worksheet.addRows(pageData.value); worksheet.addRows(
downloadXlsx(workbook, filename); exportsFormData.origin === ExportsOriginEnum.SELECTED
? selectionData.value
: pageData.value
);
workbook.xlsx
.writeBuffer()
.then((buffer) => {
saveXlsx(buffer, filename);
})
.catch((error) => console.log(error));
} }
} }
function downloadXlsx(workbook: ExcelJS.Workbook, filename: string) { // 导入表单
workbook.xlsx const uploadRef = ref<UploadInstance>();
.writeBuffer() const importsModalVisible = ref(false);
.then((buffer) => { const importsFormRef = ref<FormInstance>();
const blob = new Blob([buffer], { const importsFormData = reactive<{
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", files: UploadUserFile[];
}); }>({
const downloadUrl = window.URL.createObjectURL(blob); files: [],
const downloadLink = document.createElement("a"); });
downloadLink.href = downloadUrl; const importsFormRules: FormRules = {
downloadLink.download = filename; files: [{ required: true, message: "请选择文件" }],
document.body.appendChild(downloadLink); };
downloadLink.click(); // 打开导入弹窗
document.body.removeChild(downloadLink); function handleOpenImportsModal() {
window.URL.revokeObjectURL(downloadUrl); importsModalVisible.value = true;
}) }
.catch((error) => console.log("Error writing excel export", error)); // 覆盖前一个文件
function handleFileExceed(files: File[]) {
uploadRef.value!.clearFiles();
const file = files[0] as UploadRawFile;
file.uid = genFileId();
uploadRef.value!.handleStart(file);
}
// 下载导入模板
function handleDownloadTemplate() {
const importsTemplate = props.contentConfig.importsTemplate;
if (typeof importsTemplate === "string") {
window.open(importsTemplate);
} else if (typeof importsTemplate === "function") {
importsTemplate().then((response) => {
const fileData = response.data;
const fileName = decodeURI(
response.headers["content-disposition"].split(";")[1].split("=")[1]
);
saveXlsx(fileData, fileName);
});
} else {
ElMessage.error("未配置importsTemplate");
}
}
// 导入确认
const handleImportsSubmit = useThrottleFn(() => {
importsFormRef.value?.validate((valid: boolean) => {
valid && handleImports();
});
}, 3000);
// 关闭导入弹窗
function handleCloseImportsModal() {
importsModalVisible.value = false;
importsFormRef.value?.resetFields();
nextTick(() => {
importsFormRef.value?.clearValidate();
});
}
// 导入
function handleImports() {
const importsAction = props.contentConfig.importsAction;
if (importsAction === undefined) {
ElMessage.error("未配置importsAction");
return;
}
// 获取选择的文件
const file = importsFormData.files[0].raw as File;
// 创建Workbook实例
const workbook = new ExcelJS.Workbook();
// 使用FileReader对象来读取文件内容
const fileReader = new FileReader();
// 二进制字符串的形式加载文件
fileReader.readAsArrayBuffer(file);
fileReader.onload = (ev) => {
if (ev.target !== null && ev.target.result !== null) {
const result = ev.target.result as ArrayBuffer;
// 从 buffer中加载数据解析
workbook.xlsx
.load(result)
.then((workbook) => {
// 解析后的数据
const data = [];
// 获取第一个worksheet内容
const worksheet = workbook.getWorksheet(1);
if (worksheet) {
// 获取第一行的标题
const fields: any[] = [];
worksheet.getRow(1).eachCell((cell) => {
fields.push(cell.value);
});
// 遍历工作表的每一行(从第二行开始,因为第一行通常是标题行)
for (
let rowNumber = 2;
rowNumber <= worksheet.rowCount;
rowNumber++
) {
const rowData: IObject = {};
const row = worksheet.getRow(rowNumber);
// 遍历当前行的每个单元格
row.eachCell((cell, colNumber) => {
// 获取标题对应的键,并将当前单元格的值存储到相应的属性名中
rowData[fields[colNumber - 1]] = cell.value;
});
// 将当前行的数据对象添加到数组中
data.push(rowData);
}
}
if (data.length === 0) {
ElMessage.error("未解析到数据");
return;
}
importsAction(data).then(() => {
ElMessage.success("导入数据成功");
handleCloseImportsModal();
});
})
.catch((error) => console.log(error));
} else {
ElMessage.error("读取文件失败");
}
};
} }
// 操作栏 // 操作栏
function handleToolbar(name: string) { function handleToolbar(name: string) {
@@ -756,6 +952,9 @@ function handleToolbar(name: string) {
case "exports": case "exports":
handleOpenExportsModal(); handleOpenExportsModal();
break; break;
case "imports":
handleOpenImportsModal();
break;
case "search": case "search":
emit("searchClick"); emit("searchClick");
break; break;
@@ -878,23 +1077,30 @@ function exportPageData(formData: IObject = {}) {
const fileName = decodeURI( const fileName = decodeURI(
response.headers["content-disposition"].split(";")[1].split("=")[1] response.headers["content-disposition"].split(";")[1].split("=")[1]
); );
const fileType = saveXlsx(fileData, fileName);
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
}); });
} else { } else {
ElMessage.error("未配置exportAction"); ElMessage.error("未配置exportAction");
} }
} }
// 浏览器保存文件
function saveXlsx(fileData: BlobPart, fileName: string) {
const fileType =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
}
// 暴露的属性和方法 // 暴露的属性和方法
defineExpose({ fetchPageData, exportPageData, getFilterParams }); defineExpose({ fetchPageData, exportPageData, getFilterParams });

View File

@@ -48,7 +48,7 @@ function usePage() {
searchRef.value?.toggleVisible(); searchRef.value?.toggleVisible();
} }
// 涮选数据 // 涮选数据
function handleFilterChange(filterParams) { function handleFilterChange(filterParams: IObject) {
const queryParams = searchRef.value?.getQueryParams(); const queryParams = searchRef.value?.getQueryParams();
contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true); contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true);
} }