|
|
@@ -1,6 +1,6 @@
|
|
|
<template>
|
|
|
<div>
|
|
|
- <!-- <input style="font-size: 16px" type="file" @change="uploadExcel" /> -->
|
|
|
+ <!-- UniverSheet -->
|
|
|
<el-row type="flex" class="row-bg" justify="space-between">
|
|
|
<el-col>
|
|
|
<el-button
|
|
|
@@ -18,187 +18,309 @@
|
|
|
</el-button>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
-
|
|
|
- <div id="luckysheet" style="width: 100%; height: 88vh"></div>
|
|
|
- <!-- 显示获取的数据 -->
|
|
|
+ <!-- Univer 表格容器 -->
|
|
|
+ <div id="univer-sheet" style="width: 100%; height: 88vh"></div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import luckysheet from "luckysheet";
|
|
|
+import { createUniver, LocaleType, mergeLocales } from "@univerjs/presets";
|
|
|
+import { UniverSheetsCorePreset } from "@univerjs/preset-sheets-core";
|
|
|
+import zhCN from "@univerjs/preset-sheets-core/locales/zh-CN";
|
|
|
+import "@univerjs/preset-sheets-core/lib/index.css";
|
|
|
+
|
|
|
import {
|
|
|
getDataFromIndexedDB,
|
|
|
storeSetData,
|
|
|
initDatabase,
|
|
|
} from "@/utils/indexedDb";
|
|
|
import { format } from "date-fns";
|
|
|
+
|
|
|
export default {
|
|
|
- name: "LuckySheetDemo",
|
|
|
+ name: "UniverSheetDemo",
|
|
|
data() {
|
|
|
return {
|
|
|
- sheetData: null, // 用于存储表格数据
|
|
|
+ univer: null,
|
|
|
+ univerAPI: null,
|
|
|
+ workbook: null,
|
|
|
sheetName: "",
|
|
|
};
|
|
|
},
|
|
|
- mounted() {
|
|
|
- this.initSheetData();
|
|
|
+ async mounted() {
|
|
|
+ await this.initSheetData();
|
|
|
},
|
|
|
+ beforeDestroy() {
|
|
|
+ if (this.univer) {
|
|
|
+ this.univer.dispose();
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
methods: {
|
|
|
+ /** 初始化表格数据 */
|
|
|
async initSheetData() {
|
|
|
const jsonData = await getDataFromIndexedDB();
|
|
|
-
|
|
|
- if (jsonData && jsonData.length > 0) {
|
|
|
- this.sheetName = [...jsonData].filter(
|
|
|
- (item) => item.fileId == this.$route.query.id
|
|
|
- )[0]?.fileOldName;
|
|
|
-
|
|
|
- // 1. 动态提取表头
|
|
|
- const headers = Object.keys(
|
|
|
- [...jsonData].filter((item) => item.fileId == this.$route.query.id)[0]
|
|
|
- .fileData[0]
|
|
|
- );
|
|
|
- // 2. 将 JSON 数据转换为二维数组格式
|
|
|
- const formattedData = [...jsonData]
|
|
|
- .filter((item) => item.fileId == this.$route.query.id)[0]
|
|
|
- .fileData.map((item) => headers.map((header) => item[header]));
|
|
|
- // 3. 将表头插入到数据的第一行
|
|
|
- formattedData.unshift(headers);
|
|
|
- //4. 将 JSON 数据转换为 Luckysheet 格式
|
|
|
- this.initLuckySheet(formattedData);
|
|
|
- } else {
|
|
|
+ if (!jsonData || jsonData.length === 0) {
|
|
|
this.$message.warning("暂无数据,请先进行数据导入");
|
|
|
this.$router.push("/home/performance/customAnalysis");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const targetFile = jsonData.find(
|
|
|
+ (item) => item.fileId == this.$route.query.id
|
|
|
+ );
|
|
|
+ if (!targetFile) {
|
|
|
+ this.$message.warning("未找到对应的数据文件");
|
|
|
+ this.$router.push("/home/performance/customAnalysis");
|
|
|
+ return;
|
|
|
}
|
|
|
+
|
|
|
+ this.sheetName = await this.truncateString(targetFile.fileOldName);
|
|
|
+
|
|
|
+ const headers = Object.keys(targetFile.fileData[0]);
|
|
|
+ const formattedData = targetFile.fileData.map((row) =>
|
|
|
+ headers.map((h) => row[h])
|
|
|
+ );
|
|
|
+ formattedData.unshift(headers);
|
|
|
+
|
|
|
+ this.initUniverSheet(formattedData);
|
|
|
},
|
|
|
- async initLuckySheet(formattedData) {
|
|
|
- const options = {
|
|
|
- container: "luckysheet",
|
|
|
- showReturnIcon: false, // 假设有这个选项
|
|
|
- data: [
|
|
|
- {
|
|
|
- name: await this.truncateString(this.sheetName), //工作表名称
|
|
|
- index: 0, //工作表索引
|
|
|
- status: 1, //激活状态
|
|
|
- order: 0, //工作表的下标
|
|
|
- hide: 0, //是否隐藏
|
|
|
- row: 36, //行数
|
|
|
- column: 18, //列数
|
|
|
- defaultRowHeight: 19, //自定义行高
|
|
|
- defaultColWidth: 73, //自定义列宽
|
|
|
- celldata: formattedData
|
|
|
- .map((row, rowIndex) =>
|
|
|
- row.map((cell, colIndex) => ({
|
|
|
- r: rowIndex,
|
|
|
- c: colIndex,
|
|
|
- v: {
|
|
|
- m: cell, // 显示的内容
|
|
|
- ct: {
|
|
|
- fa: "General", // 格式(通用)
|
|
|
- t: "g", // 类型(general)
|
|
|
- },
|
|
|
- v: cell, // 实际值
|
|
|
- },
|
|
|
- }))
|
|
|
- )
|
|
|
- .flat(),
|
|
|
- //初始化使用的单元格数据
|
|
|
- config: {
|
|
|
- merge: {}, //合并单元格
|
|
|
- rowlen: {}, //表格行高
|
|
|
- columnlen: {}, //表格列宽
|
|
|
- rowhidden: {}, //隐藏行
|
|
|
- colhidden: {}, //隐藏列
|
|
|
- borderInfo: {}, //边框
|
|
|
- authority: {}, //工作表保护
|
|
|
- },
|
|
|
- scrollLeft: 0, //左右滚动条位置
|
|
|
- scrollTop: 315, //上下滚动条位置
|
|
|
- luckysheet_select_save: [], //选中的区域
|
|
|
- calcChain: [], //公式链
|
|
|
- isPivotTable: false, //是否数据透视表
|
|
|
- pivotTable: {}, //数据透视表设置
|
|
|
- filter_select: {}, //筛选范围
|
|
|
- filter: null, //筛选配置
|
|
|
- luckysheet_alternateformat_save: [], //交替颜色
|
|
|
- luckysheet_alternateformat_save_modelCustom: [], //自定义交替颜色
|
|
|
- luckysheet_conditionformat_save: {}, //条件格式
|
|
|
- frozen: {}, //冻结行列配置
|
|
|
- // chart: [], //图表配置
|
|
|
- zoomRatio: 1, // 缩放比例
|
|
|
- image: [], //图片
|
|
|
- showGridLines: 1, //是否显示网格线
|
|
|
- dataVerification: {}, //数据验证配置
|
|
|
- },
|
|
|
- ],
|
|
|
- title: "LuckySheet示例", // 表格标题
|
|
|
- lang: "zh", // 语言设置
|
|
|
- showtoolbar: true, // 是否显示工具栏
|
|
|
- chart: {
|
|
|
- //
|
|
|
- // 配置图表
|
|
|
- enable: false, // 启用图表功能
|
|
|
- },
|
|
|
- showtoolbarConfig: {
|
|
|
- chart: false, // 隐藏图表工具按钮
|
|
|
+
|
|
|
+ /** 初始化 UniverSheet */
|
|
|
+ initUniverSheet(formattedData) {
|
|
|
+ // 1. 创建 Univer 实例(返回 univer + univerAPI)
|
|
|
+ const { univer, univerAPI } = createUniver({
|
|
|
+ locale: LocaleType.ZH_CN,
|
|
|
+ locales: {
|
|
|
+ [LocaleType.ZH_CN]: mergeLocales(zhCN),
|
|
|
},
|
|
|
- };
|
|
|
- luckysheet.create(options);
|
|
|
+ presets: [
|
|
|
+ UniverSheetsCorePreset({
|
|
|
+ container: "univer-sheet",
|
|
|
+ }),
|
|
|
+ ],
|
|
|
+ });
|
|
|
+
|
|
|
+ this.univer = univer;
|
|
|
+ this.univerAPI = univerAPI;
|
|
|
+
|
|
|
+ // 2. 创建 Workbook(新版 API)
|
|
|
+ const fWorkbook = univerAPI.createWorkbook({
|
|
|
+ name: this.sheetName || "数据表",
|
|
|
+ });
|
|
|
+ this.workbook = fWorkbook;
|
|
|
+
|
|
|
+ // 3. 获取当前工作表
|
|
|
+ const fSheet = fWorkbook.getActiveSheet();
|
|
|
+
|
|
|
+ // 4. 写入数据
|
|
|
+ const rows = formattedData.length;
|
|
|
+ const cols = formattedData[0]?.length || 0;
|
|
|
+ if (rows && cols) {
|
|
|
+ fSheet.getRange(0, 0, rows, cols).setValues(formattedData);
|
|
|
+ }
|
|
|
+ // 直接设置名字(Facade API 提供的方法)
|
|
|
+ if (fSheet && typeof fSheet.setName === "function") {
|
|
|
+ fSheet.setName(this.sheetName);
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log("✅ Univer 初始化完成,数据已填充");
|
|
|
+ },
|
|
|
+ // 剪掉底部和右侧全空的行/列
|
|
|
+ trimEmptyRowsCols(values) {
|
|
|
+ if (!Array.isArray(values) || values.length === 0) return [];
|
|
|
+
|
|
|
+ // 去掉底部全空行
|
|
|
+ let lastRow = values.length - 1;
|
|
|
+ while (lastRow >= 0) {
|
|
|
+ const row = values[lastRow] || [];
|
|
|
+ if (row.some((v) => v !== "" && v != null)) break;
|
|
|
+ lastRow--;
|
|
|
+ }
|
|
|
+ if (lastRow < 0) return [];
|
|
|
+
|
|
|
+ values = values.slice(0, lastRow + 1);
|
|
|
+
|
|
|
+ // 找到最后一列索引
|
|
|
+ let lastCol = 0;
|
|
|
+ for (let r = 0; r < values.length; r++) {
|
|
|
+ const row = values[r] || [];
|
|
|
+ for (let c = row.length - 1; c >= 0; c--) {
|
|
|
+ if (row[c] !== "" && row[c] != null) {
|
|
|
+ if (c > lastCol) lastCol = c;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 切每行到 lastCol
|
|
|
+ const trimmed = values.map((row) => (row || []).slice(0, lastCol + 1));
|
|
|
+ return trimmed;
|
|
|
},
|
|
|
+
|
|
|
async saveData() {
|
|
|
- // 获取所有工作表
|
|
|
- const sheets = luckysheet.getAllSheets();
|
|
|
+ // 检查 workbook(你之前通过 univerAPI.createWorkbook 得到的 fWorkbook)
|
|
|
+ if (!this.workbook) {
|
|
|
+ this.$message.error("表格未初始化");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
const allFileData = [];
|
|
|
- sheets.forEach((sheet, ind) => {
|
|
|
- const data = sheet.data; // 获取当前工作表的数据
|
|
|
- const formattedData = [];
|
|
|
- const headers = data[0].map((cell) => (cell && cell.v ? cell.v : "")); // 表头内容
|
|
|
-
|
|
|
- // 遍历数据行,从第二行开始
|
|
|
- for (let i = 1; i < data.length; i++) {
|
|
|
- const row = data[i];
|
|
|
- const rowData = {};
|
|
|
- // 遍历每一列,并根据表头动态生成键值对
|
|
|
- for (let j = 0; j < headers.length; j++) {
|
|
|
- if (row[j] && (row[j].v || row[j].v === 0)) {
|
|
|
- rowData[headers[j]] = row[j].v || 0;
|
|
|
+
|
|
|
+ // 获取 Facade sheets 列表(多数版本支持 getSheets())
|
|
|
+ const fSheets =
|
|
|
+ typeof this.workbook.getSheets === "function"
|
|
|
+ ? this.workbook.getSheets()
|
|
|
+ : Array.isArray(this.workbook.sheets)
|
|
|
+ ? this.workbook.sheets
|
|
|
+ : [];
|
|
|
+
|
|
|
+ for (let index = 0; index < fSheets.length; index++) {
|
|
|
+ const fSheet = fSheets[index];
|
|
|
+ // 1) 获取 sheet 名(多种回退)
|
|
|
+ let name = "";
|
|
|
+ try {
|
|
|
+ if (fSheet && typeof fSheet.getName === "function") {
|
|
|
+ name = fSheet.getName();
|
|
|
+ } else if (fSheet && typeof fSheet.getSheet === "function") {
|
|
|
+ const coreSheet = fSheet.getSheet();
|
|
|
+ if (coreSheet) {
|
|
|
+ if (typeof coreSheet.getName === "function") {
|
|
|
+ name = coreSheet.getName();
|
|
|
+ } else if (typeof coreSheet.getConfig === "function") {
|
|
|
+ const cfg = coreSheet.getConfig() || {};
|
|
|
+ name = cfg.name || cfg.sheetName || "";
|
|
|
+ } else if (coreSheet.name) {
|
|
|
+ name = coreSheet.name;
|
|
|
+ }
|
|
|
}
|
|
|
+ } else if (fSheet && fSheet.getConfig) {
|
|
|
+ const cfg = fSheet.getConfig() || {};
|
|
|
+ name = cfg.name || "";
|
|
|
+ } else if (fSheet && fSheet.name) {
|
|
|
+ name = fSheet.name;
|
|
|
}
|
|
|
- // 如果行数据不为空,加入到结果数组中
|
|
|
- if (Object.keys(rowData).length) {
|
|
|
- formattedData.push(rowData);
|
|
|
+ } catch (e) {
|
|
|
+ console.warn("取 sheet 名出错,使用默认名", e);
|
|
|
+ }
|
|
|
+ if (!name) name = `Sheet${index}`;
|
|
|
+
|
|
|
+ // 2) 获取行列数(优先使用 core snapshot)
|
|
|
+ let rowCount = 0;
|
|
|
+ let colCount = 0;
|
|
|
+ try {
|
|
|
+ const coreSheet =
|
|
|
+ fSheet && typeof fSheet.getSheet === "function"
|
|
|
+ ? fSheet.getSheet()
|
|
|
+ : null;
|
|
|
+ if (coreSheet && typeof coreSheet.getSnapshot === "function") {
|
|
|
+ const snap = coreSheet.getSnapshot() || {};
|
|
|
+ rowCount = snap.rowCount || 0;
|
|
|
+ colCount = snap.columnCount || 0;
|
|
|
+ } else if (
|
|
|
+ fSheet &&
|
|
|
+ typeof fSheet.getRange === "function" &&
|
|
|
+ typeof fSheet.getRange().getValues === "function"
|
|
|
+ ) {
|
|
|
+ // 无 snapshot 的情况下无法精确知道行列,后面会尝试读取默认区域
|
|
|
}
|
|
|
+ } catch (e) {
|
|
|
+ console.warn("取 snapshot 失败,稍后使用回退读取。", e);
|
|
|
}
|
|
|
- // 生成唯一的 fileData
|
|
|
+
|
|
|
+ // 3) 读取值(优先用精确 rowCount/colCount,否则回退到安全的区域)
|
|
|
+ let values = [];
|
|
|
+ try {
|
|
|
+ if (
|
|
|
+ rowCount > 0 &&
|
|
|
+ colCount > 0 &&
|
|
|
+ typeof fSheet.getRange === "function"
|
|
|
+ ) {
|
|
|
+ values = fSheet.getRange(0, 0, rowCount, colCount).getValues();
|
|
|
+ } else if (typeof fSheet.getRange === "function") {
|
|
|
+ // 回退:读取一个合理的上限区域(可根据数据量调整)
|
|
|
+ const tryRows = 1000; // 你可以根据实际场景调大或调小
|
|
|
+ const tryCols = 200;
|
|
|
+ values = fSheet.getRange(0, 0, tryRows, tryCols).getValues();
|
|
|
+ } else {
|
|
|
+ // 如果 fSheet 没有 getRange,则尝试从 coreSheet 读取(某些版本差别)
|
|
|
+ const coreSheet =
|
|
|
+ fSheet && typeof fSheet.getSheet === "function"
|
|
|
+ ? fSheet.getSheet()
|
|
|
+ : null;
|
|
|
+ if (coreSheet && typeof coreSheet.getSnapshot === "function") {
|
|
|
+ const snap = coreSheet.getSnapshot();
|
|
|
+ rowCount = rowCount || snap.rowCount || 0;
|
|
|
+ colCount = colCount || snap.columnCount || 0;
|
|
|
+ if (
|
|
|
+ rowCount > 0 &&
|
|
|
+ colCount > 0 &&
|
|
|
+ typeof fSheet.getRange === "function"
|
|
|
+ ) {
|
|
|
+ values = fSheet.getRange(0, 0, rowCount, colCount).getValues();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.warn("读取表格值失败,返回空数组作为回退", e);
|
|
|
+ values = [];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4) 裁剪空行空列并按你的格式转成对象数组
|
|
|
+ const trimmed = this.trimEmptyRowsCols(values);
|
|
|
+ if (!trimmed || trimmed.length <= 1) {
|
|
|
+ // 没有数据或只有表头
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const headers = trimmed[0];
|
|
|
+ const formatted = trimmed
|
|
|
+ .slice(1)
|
|
|
+ .filter((row) => row && row.some((v) => v !== "" && v != null))
|
|
|
+ .map((row) => {
|
|
|
+ const obj = {};
|
|
|
+ headers.forEach((h, i) => {
|
|
|
+ if (h !== undefined && h !== null && h !== "") {
|
|
|
+ obj[h] = row[i] ?? "";
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return obj;
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!formatted.length) continue;
|
|
|
+
|
|
|
const fileData = {
|
|
|
filename:
|
|
|
- format(new Date(), "yyyyMMdd-HH:mm:ss") + "_" + ind + sheet.name,
|
|
|
- fileData: formattedData,
|
|
|
- fileOldName: sheet.name + "_" + ind,
|
|
|
- fileId: new Date().getTime() + "_" + ind,
|
|
|
+ format(new Date(), "yyyyMMdd-HH:mm:ss") + "_" + index + name,
|
|
|
+ fileData: formatted,
|
|
|
+ fileOldName: name + "_" + index,
|
|
|
+ fileId: new Date().getTime() + "_" + index,
|
|
|
};
|
|
|
- allFileData.push(fileData); // 将当前工作表的文件数据加入到数组中
|
|
|
- });
|
|
|
|
|
|
- // 批量存储到数据库
|
|
|
- await initDatabase()
|
|
|
- .then((database) => {
|
|
|
- allFileData.forEach((fileData) => {
|
|
|
- if (fileData && fileData.fileData.length > 0) {
|
|
|
- storeSetData(database, "files", "fileDataArray", fileData, () => {
|
|
|
- console.log("数据存储成功:", fileData.filename);
|
|
|
- });
|
|
|
- }
|
|
|
- });
|
|
|
- // 跳转到分析页面
|
|
|
- this.$router.push("/home/performance/customAnalysis");
|
|
|
- })
|
|
|
- .catch((error) => {
|
|
|
- console.error("数据库初始化失败,无法继续存储数据。", error);
|
|
|
+ allFileData.push(fileData);
|
|
|
+ } // for sheets
|
|
|
+
|
|
|
+ // 存 DB
|
|
|
+ try {
|
|
|
+ const db = await initDatabase();
|
|
|
+ allFileData.forEach((fileData) => {
|
|
|
+ if (fileData && fileData.fileData && fileData.fileData.length > 0) {
|
|
|
+ storeSetData(db, "files", "fileDataArray", fileData, () => {
|
|
|
+ console.log("数据存储成功:", fileData.filename);
|
|
|
+ });
|
|
|
+ }
|
|
|
});
|
|
|
+ } catch (e) {
|
|
|
+ console.error("保存到 IndexedDB 失败", e);
|
|
|
+ this.$message.error("保存失败,请检查控制台错误");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.$router.push("/home/performance/customAnalysis");
|
|
|
},
|
|
|
+ // /** 保存数据 */
|
|
|
+
|
|
|
+ // /** 截断 Sheet 名称 */
|
|
|
truncateString(str) {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- // 如果字符串长度超过30
|
|
|
+ return new Promise((resolve) => {
|
|
|
if (str.length > 30) {
|
|
|
this.$confirm(
|
|
|
"sheet页名长度已超过31个字符,可能会影响后续对函数的使用,是否需要进行自动截取?",
|
|
|
@@ -212,14 +334,10 @@ export default {
|
|
|
.then(() => {
|
|
|
const suffixes = [".csv", ".xlsx", ".xls"];
|
|
|
let truncatedFilename = str;
|
|
|
-
|
|
|
- // 查找文件名中的后缀位置并进行截取
|
|
|
+ console.log(str, "str");
|
|
|
for (let suffix of suffixes) {
|
|
|
const suffixIndex = str.lastIndexOf(suffix);
|
|
|
-
|
|
|
- // 如果找到了后缀,进行截取
|
|
|
if (suffixIndex !== -1) {
|
|
|
- // 截取文件名,确保不超过31个字符
|
|
|
truncatedFilename =
|
|
|
str.slice(0, 31 - suffix.length) + str.slice(suffixIndex);
|
|
|
break;
|
|
|
@@ -228,15 +346,13 @@ export default {
|
|
|
resolve(truncatedFilename);
|
|
|
})
|
|
|
.catch(() => {
|
|
|
- // 用户取消时返回原始的str
|
|
|
this.$message({
|
|
|
type: "info",
|
|
|
- message: "已取消删除",
|
|
|
+ message: "已取消自动截取",
|
|
|
});
|
|
|
resolve(str);
|
|
|
});
|
|
|
} else {
|
|
|
- // 字符串长度不超过30,直接返回原字符串
|
|
|
resolve(str);
|
|
|
}
|
|
|
});
|
|
|
@@ -246,7 +362,7 @@ export default {
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
-/* 可以添加一些自定义样式 */
|
|
|
+/* 隐藏返回按钮 */
|
|
|
::v-deep .luckysheet_info_detail div.luckysheet_info_detail_back {
|
|
|
display: none;
|
|
|
}
|