This commit is contained in:
oneao committed 2026-03-26 17:25:23 +08:00
1 parent c7240ea7c9
commit 189a085353
26 files changed
+2650 -201

No files matched your search

@@ -47,7 +47,6 @@ export function updateViewTableColumns(tableRef) {
return;
}
const newColumns = columns
.filter((item) => item.type === undefined && item.params && item.params.b_id)
.map((item, index) => ({
@@ -119,3 +118,35 @@ export function formatValueByFormat(value, format) {
return value;
}
/**
* 根据 format 格式化值
* @param {any} value 原始值
* @param {string} format 格式描述,如 money(2)、percent(1)
*/
export function formatValueByFullFormat(value, format) {
if (value === null || value === undefined || value === "") return "";
if (!format || typeof format !== "string") return value;
const fmt = format.trim().toLowerCase();
// money(n)
if (fmt.startsWith("money")) {
const match = fmt.match(/^money\((\d+)\)$/);
const decimals = match ? Number(match[1]) : 2;
const num = Number.parseFloat(value);
if (Number.isNaN(num)) return value;
return num.toFixed(decimals);
} else if (fmt === "date") {
return formatToDate(value);
} else if (fmt === "datetime") {
return formatToDateTime(value);
} else if (fmt === "number") {
return Number(value) || 0;
}
return value;
}
@@ -0,0 +1,361 @@
import ExcelJS from "exceljs";
import { formatValueByFullFormat, getAlignByFormat } from "@/utils/columns";
import { ElLoading, ElMessage } from "element-plus";
import { getAliyunFileBlob } from "./file";
import { saveAs } from "file-saver";
export async function ExportVxeTableExcel(tableRef) {
const $table = unref(tableRef);
if (!$table) {
return;
}
const loading = ElLoading.service({
lock: true,
text: "正在下载Excel文件中,请稍候...",
});
// columns
const fullColumns = $table.getFullColumns();
const exportColumns = buildColumns(fullColumns) || [];
const exportDatas =
$table.getCheckboxRecords().length > 0 ? $table.getCheckboxRecords() : $table.getFullData();
// const footerDatas = updateTableFooterData(exportColumns, exportDatas)
const footerDatas = [];
const options = {
columns: exportColumns,
data: exportDatas,
};
try {
await exportExcel(options);
} catch (e) {
} finally {
loading.close();
}
}
async function urlToBase64(url) {
return new Promise(async (resolve, reject) => {
try {
const blob = await getAliyunFileBlob(url);
// Create an object URL from the blob
const objectURL = URL.createObjectURL(blob);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
img.crossOrigin = "Anonymous";
img.onload = function () {
canvas.height = img.height;
canvas.width = img.width;
ctx.drawImage(img, 0, 0, img.width, img.height);
var dataURL = canvas.toDataURL("image/png");
resolve(dataURL);
// Optional: Revoke the object URL once done to free memory
URL.revokeObjectURL(objectURL);
};
img.src = objectURL;
} catch (error) {
reject(error);
}
});
}
function buildColumns(arr) {
return arr
.map((item) => {
// 先处理 children(关键!)
let children = [];
if (item.children && item.children.length > 0) {
children = buildColumns(item.children);
}
// 判断当前节点是否需要保留
const valid = item.type === undefined && item.field !== undefined;
// 如果当前不合法 + 没有子节点,直接丢
if (!valid && children.length === 0) {
return null;
}
// 构建 column
const column = {
label: item.title,
width: item.params?.col_Width || item.renderWidth,
field: item.field,
type: item.params?.col_DisplayFormat,
};
// 有子节点就挂上
if (children.length > 0) {
column.children = children;
}
return column;
})
.filter(Boolean);
}
/**
* 导出Excel文件(支持多级表头、表尾、图片)
* @param {Object} options 导出配置
* @param {String} fileName 文件名(默认:export.xlsx)
*/
export async function exportExcel(options, fileName = "export.xlsx") {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Sheet1");
// 1. 处理多级表头
const { headerRows, leafColumns, mergeCells } = buildMultiLevelHeader(options.columns);
// 写入表头行
headerRows.forEach((row, rowIndex) => {
const headerRow = worksheet.addRow(row);
// 设置表头样式(突出显示)
headerRow.eachCell((cell) => {
cell.style = {
font: { bold: true, size: 12, color: { argb: "FFFFFFFF" } },
fill: {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FF4F81BD" }, // 蓝色背景
},
border: {
top: { style: "thin", color: { argb: "FF000000" } },
left: { style: "thin", color: { argb: "FF000000" } },
bottom: { style: "thin", color: { argb: "FF000000" } },
right: { style: "thin", color: { argb: "FF000000" } },
},
alignment: { vertical: "middle", horizontal: "center" },
};
});
});
// 合并表头单元格
mergeCells.forEach((merge) => {
worksheet.mergeCells(merge.start.row, merge.start.col, merge.end.row, merge.end.col);
});
// 2. 写入数据行
const dataStartRow = headerRows.length + 1;
for (const [rowIndex, item] of options.data.entries()) {
const dataRow = worksheet.addRow([]);
for (const [colIndex, col] of leafColumns.entries()) {
const cell = dataRow.getCell(colIndex + 1);
const value = getNestedValue(item, col.field);
// 处理图片类型
if (col.type === "image" && value) {
try {
const base64 = await urlToBase64(value)
// 添加图片到工作簿
const imageId = workbook.addImage({
base64,
extension: "png",
});
// 计算图片位置(跳过表头行)
const imageRow = dataStartRow + rowIndex;
worksheet.addImage(imageId, {
tl: { col: colIndex, row: imageRow - 1 },
ext: { width: col.imageWidth || 80, height: col.imageHeight || 80 },
});
// 调整行高以适应图片
dataRow.height = Math.max(dataRow.height || 20, (col.imageHeight || 80) * 0.75);
} catch (error) {
console.warn(`图片导出失败: ${error.message}`);
cell.value = "[图片]";
}
} else {
cell.value = value;
}
// 设置数据行样式
cell.style = {
alignment: { vertical: "middle", horizontal: "left" },
};
}
}
// 3. 写入表尾数据(突出显示)
if (options.footer && options.footer.length > 0) {
const footerStartRow = dataStartRow + options.data.length;
options.footer.forEach((footerItem, rowIndex) => {
const footerRow = worksheet.addRow([]);
leafColumns.forEach((col, colIndex) => {
const cell = footerRow.getCell(colIndex + 1);
const value = getNestedValue(footerItem, col.field);
cell.value = value;
// 设置表尾样式(突出显示)
cell.style = {
font: { bold: true },
// fill: {
// type: "pattern",
// pattern: "solid",
// // fgColor: { argb: "FFC00000" }, // 红色背景
// },
// border: {
// top: { style: "medium", color: { argb: "FF000000" } },
// left: { style: "medium", color: { argb: "FF000000" } },
// bottom: { style: "medium", color: { argb: "FF000000" } },
// right: { style: "medium", color: { argb: "FF000000" } },
// },
alignment: { vertical: "middle", horizontal: "center" },
};
});
});
}
// 4. 设置列宽
leafColumns.forEach((col, index) => {
const column = worksheet.getColumn(index + 1);
column.width = col.width ? col.width / 7 : 20; // 转换像素为Excel单位
});
// 5. 生成并保存文件
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
saveAs(blob, fileName);
}
/**
* 构建多级表头
* @param {Array} columns 表头配置
* @returns {Object} { headerRows, leafColumns, mergeCells }
*/
function buildMultiLevelHeader(columns) {
// 获取表头最大深度
const maxDepth = getMaxDepth(columns);
// 获取所有叶子节点列
const leafColumns = getLeafColumns(columns);
// 初始化表头二维数组
const headerRows = Array.from({ length: maxDepth }, () => Array(leafColumns.length).fill(""));
const mergeCells = [];
// 递归填充表头
function fillHeader(nodes, startCol = 0, depth = 0) {
let currentCol = startCol;
nodes.forEach((node) => {
// 计算当前节点跨越的列数
const colSpan = node.children ? getLeafCount(node.children) : 1;
// 填充当前节点的表头
headerRows[depth][currentCol] = node.label || "";
// 如果有子节点,递归处理
if (node.children && node.children.length > 0) {
fillHeader(node.children, currentCol, depth + 1);
// 如果跨多列,需要合并单元格
if (colSpan > 1) {
mergeCells.push({
start: { row: depth + 1, col: currentCol + 1 },
end: { row: depth + 1, col: currentCol + colSpan },
});
}
}
// 如果当前节点有父节点且不是叶子节点,需要纵向合并
if (depth < maxDepth - 1 && !node.children) {
mergeCells.push({
start: { row: depth + 1, col: currentCol + 1 },
end: { row: maxDepth, col: currentCol + 1 },
});
}
currentCol += colSpan;
});
}
fillHeader(columns);
return { headerRows, leafColumns, mergeCells };
}
/**
* 获取树的最大深度
*/
function getMaxDepth(nodes, currentDepth = 1) {
if (!nodes || nodes.length === 0) return currentDepth;
let maxDepth = currentDepth;
nodes.forEach((node) => {
if (node.children && node.children.length > 0) {
const depth = getMaxDepth(node.children, currentDepth + 1);
maxDepth = Math.max(maxDepth, depth);
}
});
return maxDepth;
}
/**
* 获取所有叶子节点列
*/
function getLeafColumns(nodes, result = []) {
nodes.forEach((node) => {
if (node.children && node.children.length > 0) {
getLeafColumns(node.children, result);
} else {
result.push({
field: node.field,
label: node.label,
width: node.width,
type: node.type,
imageWidth: node.imageWidth,
imageHeight: node.imageHeight,
});
}
});
return result;
}
/**
* 获取叶子节点数量
*/
function getLeafCount(nodes) {
if (!nodes || nodes.length === 0) return 0;
return nodes.reduce((count, node) => {
if (node.children && node.children.length > 0) {
return count + getLeafCount(node.children);
}
return count + 1;
}, 0);
}
/**
* 获取嵌套对象的值
*/
function getNestedValue(obj, path) {
if (!path) return "";
return (
path.split(".").reduce((prev, curr) => {
return prev ? prev[curr] : "";
}, obj) || ""
);
}
+135 -71
View File
@@ -203,14 +203,113 @@ export function getFileExtension(fileName) {
return fileExt;
}
// 上传到阿里云
// blob fileName fileSize xh moduleid father cateid
export function downloadZip(files, fileName = "文件合集", callback) {
const zip = new JSZip();
const addedFiles = new Set();
const tasks = files.map((item) => {
return getAliyunFileBlob(item.mx_mapfilename)
.then((res) => {
const uniqueFileName = getUniqueFileName(item.mx_filename, addedFiles);
zip.file(uniqueFileName, res, { binary: true });
addedFiles.add(uniqueFileName);
})
.catch((err) => {
// 捕获单个文件下载失败的错误,不影响其他文件下载
console.error(`Failed to download file: ${item.mx_filename}`, err);
});
});
// 关键:使用 Promise.all 确保所有任务完成
return Promise.all(tasks)
.then(() => zip.generateAsync({ type: "blob" }))
.then((res) => {
saveAs(res, `${fileName}.zip`);
callback && callback();
})
.catch((err) => {
callback && callback();
throw err;
});
}
// 处理文件名重复的函数
function getUniqueFileName(fileName, addedFiles) {
let uniqueFileName = fileName;
let counter = 1;
// 如果文件名已经存在,则在文件名后加上 (1), (2), ...
while (addedFiles.has(uniqueFileName)) {
const fileNameWithoutExtension = uniqueFileName.replace(/\.[^/.]+$/, "");
const extension = uniqueFileName.slice(uniqueFileName.lastIndexOf("."));
uniqueFileName = `${fileNameWithoutExtension} (${counter})${extension}`;
counter++;
}
return uniqueFileName;
}
// ============================================ 阿里云上传和下载 ============================================
let cachedCredentials = null; // 用来缓存凭证
let isFetchingCredentials = false; // 控制是否正在请求凭证
// 获取阿里云文件的上传凭证
export function getAliyunUploadCredentials() {
return new Promise((resolve, reject) => {
// 如果缓存中有有效的凭证,则直接使用
if (cachedCredentials && !isCredentialsExpired(cachedCredentials)) {
resolve(cachedCredentials); // 凭证未过期,直接返回缓存的凭证
} else {
// 如果正在请求凭证,则等待
if (isFetchingCredentials) {
const interval = setInterval(() => {
// 等待直到凭证被请求完毕
if (!isFetchingCredentials) {
clearInterval(interval); // 请求完成后停止轮询
resolve(cachedCredentials); // 返回缓存的凭证
}
}, 100); // 每100ms检查一次
} else {
// 如果凭证过期,重新请求新的凭证
isFetchingCredentials = true; // 设置为正在请求凭证状态
getAliyunKeyApi()
.then((res) => {
if (res.success) {
const { accessid, policy, signature, expire, dir, host } = res.data;
// 缓存凭证,并记录过期时间
cachedCredentials = {
accessid,
policy,
signature,
expire: parseInt(expire, 10),
dir,
host,
};
isFetchingCredentials = false; // 请求完成,解除锁定
resolve(cachedCredentials); // 返回新凭证
} else {
isFetchingCredentials = false; // 请求失败,解除锁定
reject(new Error("获取凭证失败"));
}
})
.catch((err) => {
isFetchingCredentials = false; // 请求失败,解除锁定
reject(new Error("请求凭证失败:" + err.message));
});
}
}
});
}
// 上传文件到阿里云
export function uploadFile(blob, fileName, fileExt, fileSize, xh, moduleid, father, cateid) {
return new Promise((resolve, reject) => {
getAliyunKeyApi()
.then(async (res) => {
getAliyunUploadCredentials()
.then(async (aliyunKey) => {
try {
const aliyunKey = res.data;
const formData = new FormData();
let uniqueName = `${Date.now()}_${Math.random().toString(36).slice(2)}`;
@@ -219,12 +318,17 @@ export function uploadFile(blob, fileName, fileExt, fileSize, xh, moduleid, fath
uniqueName += `.${fileExt}`;
}
// 构造上传的文件路径
formData.append("key", `${aliyunKey.dir}${uniqueName}`);
formData.append("OSSAccessKeyId", aliyunKey.accessid);
formData.append("policy", aliyunKey.policy);
formData.append("Signature", aliyunKey.signature);
formData.append("file", blob, uniqueName);
console.log(formData);
console.log(aliyunKey.host);
console.log(aliyunKey.dir);
const xhr = new XMLHttpRequest();
xhr.open("POST", aliyunKey.host, true);
@@ -232,6 +336,7 @@ export function uploadFile(blob, fileName, fileExt, fileSize, xh, moduleid, fath
if (xhr.status === 200 || xhr.status === 204) {
const allFilePath = `${aliyunKey.host}/${aliyunKey.dir}${uniqueName}`;
// 上传后处理(插入文件记录等)
const insertRes = await insertFileApi({
father,
mx_moduleid: moduleid,
@@ -278,76 +383,35 @@ export function uploadFile(blob, fileName, fileExt, fileSize, xh, moduleid, fath
});
}
// 检查凭证是否过期
function isCredentialsExpired(credentials) {
const currentTime = Math.floor(Date.now() / 1000);
return currentTime > credentials.expire;
}
// 获取阿里云文件的 Blob 数据(用于下载)
export function getAliyunFileBlob(url) {
return new Promise((resolve, reject) => {
// 创建一个独立的异步函数来处理 fetch 请求
async function fetchFile() {
try {
const response = await fetch(url, { mode: "cors" });
getAliyunUploadCredentials()
.then((aliyunKey) => {
const downloadUrl = `${url}?OSSAccessKeyId=${aliyunKey.accessid}&policy=${aliyunKey.policy}&Signature=${aliyunKey.signature}`;
// 检查响应状态
if (!response.ok) {
reject(new Error(`下载失败:${response.status}`));
return;
}
// 获取文件内容
const blob = await response.blob();
// 返回blob
resolve(blob);
} catch (error) {
reject(error);
}
}
// 调用异步函数
fetchFile();
});
}
export function downloadZip(files, fileName = "文件合集", callback) {
const zip = new JSZip();
const addedFiles = new Set();
const tasks = files.map((item) => {
return getAliyunFileBlob(item.mx_mapfilename)
.then((res) => {
const uniqueFileName = getUniqueFileName(item.mx_filename, addedFiles);
zip.file(uniqueFileName, res, { binary: true });
addedFiles.add(uniqueFileName);
fetch(downloadUrl, { mode: "cors" })
.then((response) => {
if (!response.ok) {
throw new Error(`下载失败,状态码:${response.status}`);
}
return response.blob();
})
.then((blob) => {
resolve(blob);
})
.catch((error) => {
reject(error);
});
})
.catch((err) => {
// 捕获单个文件下载失败的错误,不影响其他文件下载
console.error(`Failed to download file: ${item.mx_filename}`, err);
});
.catch(reject);
});
// 关键:使用 Promise.all 确保所有任务完成
return Promise.all(tasks)
.then(() => zip.generateAsync({ type: "blob" }))
.then((res) => {
saveAs(res, `${fileName}.zip`);
callback && callback();
})
.catch((err) => {
callback && callback();
throw err;
});
}
// 处理文件名重复的函数
function getUniqueFileName(fileName, addedFiles) {
let uniqueFileName = fileName;
let counter = 1;
// 如果文件名已经存在,则在文件名后加上 (1), (2), ...
while (addedFiles.has(uniqueFileName)) {
const fileNameWithoutExtension = uniqueFileName.replace(/\.[^/.]+$/, "");
const extension = uniqueFileName.slice(uniqueFileName.lastIndexOf("."));
uniqueFileName = `${fileNameWithoutExtension} (${counter})${extension}`;
counter++;
}
return uniqueFileName;
}
// ============================================ 阿里云上传和下载 ============================================