feat: integrate luckysheet inline and fullscreen
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# 可能要忽略 deepseek AI 模块
|
||||
/wwwroot
|
||||
+1885
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "luckysheet-crdt-server",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "luckysheet-crdt-server",
|
||||
"scripts": {
|
||||
"serve": "tsc && node build/main.js",
|
||||
"build": "tsc && node scripts/build.js",
|
||||
"dev": "nodemon -w src/ -e ts --exec \"ts-node src/main.ts\"",
|
||||
"db": "tsc && node build/Sequelize/synchronization.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"dayjs": "^1.11.13",
|
||||
"express": "^4.21.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.11.5",
|
||||
"ollama": "^0.5.13",
|
||||
"pako": "^2.1.0",
|
||||
"sequelize": "^6.37.5",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/pako": "^2.0.3",
|
||||
"@types/ws": "^8.5.13",
|
||||
"nodemon": "^3.1.9",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
+1478
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* @author https://gitee.com/wfeng0/luckysheet-crdt
|
||||
* @description Luckysheet CRDT 协同全功能实现 - Server 服务端打包脚本
|
||||
* @license Apache 2.0
|
||||
* @copyright Copyright (c) 2025 https://gitee.com/wfeng0/luckysheet-crdt
|
||||
* @time 2025年04月25日
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const packageJson = require("../package.json");
|
||||
|
||||
const rootPath = path.resolve(__dirname, "../");
|
||||
|
||||
(async () => {
|
||||
console.log("1️⃣ Starting build wwwroot directory...");
|
||||
// 判断当前目录下是否存在 wwwroot 目录
|
||||
if (fs.existsSync(`${rootPath}/wwwroot`)) {
|
||||
// 如果存在的话,删除,再创建,确保文件夹内容是最新的
|
||||
fs.rmSync(`${rootPath}/wwwroot`, { recursive: true });
|
||||
}
|
||||
fs.mkdirSync(`${rootPath}/wwwroot`);
|
||||
|
||||
console.log("2️⃣ Starting build package.json...");
|
||||
// 进行后续操作
|
||||
// 1. 复制 package.json 文件到 wwwroot 目录下(不需要 xxx-lock 文件,服务器上都重新拉依赖)
|
||||
// 重修修订 scripts 命令
|
||||
delete packageJson.scripts.build;
|
||||
delete packageJson.scripts.dev;
|
||||
delete packageJson.scripts.serve;
|
||||
|
||||
// 重修修订 db 命令,取消 tsc 打包
|
||||
packageJson.scripts.db = "node build/Sequelize/synchronization.js";
|
||||
packageJson.scripts.start = "node build/main.js";
|
||||
|
||||
fs.writeFileSync(`${rootPath}/wwwroot/package.json`, JSON.stringify(JSON.parse(JSON.stringify(packageJson))));
|
||||
|
||||
// 2. 复制 build 文件夹到 wwwroot 目录下
|
||||
fs.cpSync(`${rootPath}/build`, `${rootPath}/wwwroot/build`, {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
console.log("3️⃣ Starting build public directory...");
|
||||
// 3. 复制 public/dist 文件夹到 wwwroot 目录下
|
||||
fs.cpSync(`${rootPath}/public/dist`, `${rootPath}/wwwroot/public/dist`, {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
console.log("4️⃣ Starting build license...");
|
||||
// 4. 创建打包版本信息文件
|
||||
const license = `
|
||||
/**
|
||||
* @author pushu
|
||||
* @description Luckysheet CRDT 协同全功能实现 - Server 服务端开源协议
|
||||
* @license Apache 2.0
|
||||
* @copyright Copyright (c) 2025 https://gitee.com/wfeng0/luckysheet-crdt
|
||||
* @buildTime ${new Date().toLocaleString()}
|
||||
* */
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.`;
|
||||
|
||||
// 创建 LICENSE 文件
|
||||
fs.writeFileSync(`${rootPath}/wwwroot/LICENSE`, license.toString());
|
||||
console.log("✅️ Successully builded.");
|
||||
})();
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Luckysheet Crdt Server Configuration - 统一配置对象
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
/**
|
||||
* HTTP 服务 与 websocket服务 共用一个端口
|
||||
*/
|
||||
const SERVER_PORT = 9000;
|
||||
|
||||
/**
|
||||
* 数据库配置对象 - sequelize
|
||||
* 数据库引擎 'mysql' | 'postgres' | 'sqlite' | 'mariadb' | 'mssql' | 'db2' | 'snowflake' | 'oracle'
|
||||
* # 选择以下之一(请注意:目前默认为 MySQL 如果使用其他引擎,请自行下载其他的驱动程序):
|
||||
* npm install --save pg pg-hstore # Postgres
|
||||
* npm install --save mysql2
|
||||
* npm install --save mariadb
|
||||
* npm install --save sqlite3
|
||||
* npm install --save tedious # Microsoft SQL Server
|
||||
* npm install --save oracledb # Oracle Database
|
||||
*/
|
||||
const SQL_CONFIG = {
|
||||
port: 3306, // 端口号 3306 3309
|
||||
host: '127.0.0.1', // localhost or 127.0.0.1
|
||||
database: 'luckysheet_crdt',
|
||||
user: 'root',
|
||||
password: 'root',
|
||||
logger: true, // 开启日志
|
||||
enable: true // 是否启用数据库服务
|
||||
};
|
||||
|
||||
/**
|
||||
* 日志配置对象
|
||||
*/
|
||||
const LOGGER_CONFIG = {
|
||||
filepath: path.resolve(__dirname, '../../logs'),
|
||||
filename: `luckysheet.${dayjs().format('YYYY-MM-DD')}.log`
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出演示用 Worker Books Info
|
||||
*/
|
||||
const WORKER_BOOK_INFO = {
|
||||
lang: 'zh',
|
||||
title: '测试工作簿',
|
||||
gridKey: 'gridkey_demo'
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出文件上传 Multer 配置对象
|
||||
*/
|
||||
const MULTER_CONFIG = {
|
||||
single: 'image', // 这个是前端 new FormData() 对象的 key 值,即上传文件的 key 值
|
||||
dest: path.resolve(__dirname, '../../public/uploads')
|
||||
};
|
||||
|
||||
// 静态资源地址 打包后 web 的入口文件
|
||||
const ENTRY_URL = path.join(__dirname, '../../public/dist/entry.html');
|
||||
|
||||
// 统一导出配置对象
|
||||
export {
|
||||
ENTRY_URL,
|
||||
SQL_CONFIG,
|
||||
SERVER_PORT,
|
||||
LOGGER_CONFIG,
|
||||
MULTER_CONFIG,
|
||||
WORKER_BOOK_INFO
|
||||
};
|
||||
@@ -0,0 +1,406 @@
|
||||
import {
|
||||
type ChartType,
|
||||
type MergeType,
|
||||
type ImagesType,
|
||||
type BorderInfoType,
|
||||
type CellDataItemType,
|
||||
type WorkerSheetItemType,
|
||||
CalcChainType,
|
||||
} from "../../Interface/luckysheet";
|
||||
import { DB } from "../../Sequelize";
|
||||
import { getURLQuery } from "../../Utils";
|
||||
import { logger } from "../../Utils/Logger";
|
||||
import { Request, Response } from "express";
|
||||
import { ImageService } from "../../Service/Image";
|
||||
import { MergeService } from "../../Service/Merge";
|
||||
import { ChartService } from "../../Service/Chart";
|
||||
import { CellDataService } from "../../Service/CellData";
|
||||
import { BorderInfoService } from "../../Service/Border";
|
||||
import { CalcChainService } from "../../Service/CalcChain";
|
||||
import { WorkerSheetService } from "../../Service/WorkerSheet";
|
||||
import { HiddenAndLenService } from "../../Service/HiddenAndLen";
|
||||
import { CellDataModelType } from "../../Sequelize/Models/CellData";
|
||||
import { WorkerSheetModelType } from "../../Sequelize/Models/WorkerSheet";
|
||||
|
||||
/**
|
||||
* loadSheetData loadUrl 加载数据
|
||||
* 协同第一步 : 解析数据库数据,生成 workerbook data 数据
|
||||
* @returns string
|
||||
*/
|
||||
export async function loadSheetData(req: Request, res: Response) {
|
||||
try {
|
||||
// 如果数据库没有连接,则直接返回空数组
|
||||
if (!DB.getConnectState()) {
|
||||
res.json(getEmptyData());
|
||||
return;
|
||||
}
|
||||
|
||||
const result: WorkerSheetItemType[] = [];
|
||||
|
||||
// 1. 解析用户 URL gridkey 参数 || WORKER_BOOK_INFO gridkey
|
||||
const gridKey = getURLQuery(req.url, "gridkey");
|
||||
|
||||
if (!gridKey) {
|
||||
res.json({ code: 400, msg: "gridKey 参数缺失" });
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 根据 gridKey 查询相关数据,拼接生成 luckysheet 初始数据,进行 luckysheet 初始化
|
||||
const sheets = await WorkerSheetService.findAllByGridKey(gridKey);
|
||||
if (!sheets || !sheets.length) {
|
||||
res.json({ code: 400, msg: "未查询到相关数据" });
|
||||
return;
|
||||
}
|
||||
|
||||
// 一个工作簿可能有多个工作表
|
||||
for (let i = 0; i < sheets!.length; i++) {
|
||||
const item = sheets![i].dataValues;
|
||||
const worker_sheet_id = item.worker_sheet_id;
|
||||
|
||||
// 初始化当前 sheet 页的基础数据
|
||||
const currentSheetData = getSheetDataTemp(item);
|
||||
|
||||
// 3. 查询关联当前 sheet 的 celldata 数据
|
||||
await parseCellData(worker_sheet_id, currentSheetData);
|
||||
|
||||
// 4. 查询关联当前 sheet 的 merge 数据 - 这里不仅要体现在 config 中,还要体现在 celldata.mc 中
|
||||
await parseMerge(worker_sheet_id, currentSheetData);
|
||||
|
||||
// 5. 查新关联当前 sheet 的 border 数据
|
||||
await parseConfigBorder(worker_sheet_id, currentSheetData);
|
||||
|
||||
// 6. 查询关联当前 sheet 的 hidden/rowlen/collen 数据
|
||||
await parseHiddenAndLen(worker_sheet_id, currentSheetData);
|
||||
|
||||
// 7. 查询 chart 数据
|
||||
await parseCharts(worker_sheet_id, currentSheetData);
|
||||
|
||||
// 8. 查询 image 数据
|
||||
await parseImages(worker_sheet_id, currentSheetData);
|
||||
|
||||
// 9. 解析公式链
|
||||
await parseCalcChain(worker_sheet_id, currentSheetData);
|
||||
|
||||
result.push(currentSheetData);
|
||||
}
|
||||
|
||||
// 9. 返回数据
|
||||
res.json(JSON.stringify(result));
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
res.json({ code: 500, msg: "服务异常" });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基础数据模板
|
||||
*/
|
||||
function getSheetDataTemp(item: WorkerSheetModelType) {
|
||||
const currentSheetData: WorkerSheetItemType = {
|
||||
name: item.name,
|
||||
index: <string>item.worker_sheet_id, // 注意此字段
|
||||
status: <number>item.status,
|
||||
order: <number>item.order,
|
||||
color: item.color,
|
||||
hide: Number(item.hide),
|
||||
celldata: [],
|
||||
config: {
|
||||
merge: {}, //合并单元格
|
||||
rowhidden: {}, //隐藏行
|
||||
colhidden: {}, //隐藏列
|
||||
borderInfo: [], //边框
|
||||
rowlen: {},
|
||||
columnlen: {},
|
||||
},
|
||||
images: {}, //图片
|
||||
chart: [], //图表配置
|
||||
calcChain: [], // 公式链
|
||||
};
|
||||
|
||||
return currentSheetData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库服务不可用,直接返回空模板数据
|
||||
*/
|
||||
function getEmptyData() {
|
||||
return JSON.stringify([
|
||||
{
|
||||
name: "Sheet1",
|
||||
index: "Sheet_Index_Demo",
|
||||
status: 1,
|
||||
order: 0,
|
||||
celldata: [
|
||||
{
|
||||
r: 0,
|
||||
c: 0,
|
||||
v: {
|
||||
v: "数据库服务不可用,但不影响协同功能",
|
||||
m: "数据库服务不可用,但不影响协同功能",
|
||||
bg: "#ff0000",
|
||||
fc: "#ffffff",
|
||||
fs: 12,
|
||||
ht: 0,
|
||||
vt: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* parseCellData 解析 cellData 数据
|
||||
*/
|
||||
async function parseCellData(worker_sheet_id: string, currentSheetData: WorkerSheetItemType) {
|
||||
try {
|
||||
const result = <CellDataItemType[]>[];
|
||||
|
||||
const cellDatas = await CellDataService.getCellData(worker_sheet_id);
|
||||
|
||||
cellDatas?.forEach((item) => {
|
||||
const data = <CellDataModelType>item.dataValues;
|
||||
|
||||
// 解析 cellData 生成 luckysheet 初始数据
|
||||
result.push({
|
||||
r: data.r,
|
||||
c: data.c,
|
||||
v: {
|
||||
ct: {
|
||||
fa: <string>data.ctfa,
|
||||
t: <string>data.ctt,
|
||||
s: JSON.parse(data.cts || "[]"),
|
||||
},
|
||||
v: data.v || "",
|
||||
m: data.m || "",
|
||||
bg: data.bg || "#FFFFFF",
|
||||
ff: data.ff || "",
|
||||
fc: data.fc || "#000000",
|
||||
bl: Boolean(data.bl),
|
||||
it: Boolean(data.it),
|
||||
fs: data.fs || 10,
|
||||
cl: Boolean(data.cl),
|
||||
ht: data.ht || 0,
|
||||
vt: data.vt || 0,
|
||||
/**
|
||||
* 此处的 '' 可能会引起 源码 formula.js xssDeal 解析值时,进入 f 判断
|
||||
* 源码已修改 => if (cell.f != null || (cell.f && cell.f != ""))
|
||||
*/
|
||||
f: data.f || null,
|
||||
un: Boolean(data.un),
|
||||
// 文本截断方式 tb textbeak 文本换行 0 截断、1溢出、2 自动换行
|
||||
tb: Number(data.tb) || 0,
|
||||
ps: data.ps ? { value: data.ps } : null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
currentSheetData.celldata = result;
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* parseMerge 解析合并单元格
|
||||
*/
|
||||
async function parseMerge(worker_sheet_id: string, currentSheetData: WorkerSheetItemType) {
|
||||
try {
|
||||
const result: MergeType = {};
|
||||
|
||||
const merges = await MergeService.findAllMerge(worker_sheet_id);
|
||||
|
||||
merges?.forEach((merge) => {
|
||||
// 拼接 r_c 格式
|
||||
const { r, c } = merge.dataValues;
|
||||
result[`${r}_${c}`] = merge.dataValues;
|
||||
|
||||
// 配置 celldata mc 属性
|
||||
const currentMergeCell = currentSheetData.celldata?.find((i) => i.r == r && i.c == c);
|
||||
|
||||
if (currentMergeCell) currentMergeCell.v.mc = merge.dataValues;
|
||||
});
|
||||
|
||||
currentSheetData.config.merge = result;
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* parseConfigBorder 解析边框
|
||||
*/
|
||||
async function parseConfigBorder(worker_sheet_id: string, currentSheetData: WorkerSheetItemType) {
|
||||
try {
|
||||
const result = <BorderInfoType[]>[];
|
||||
|
||||
const borders = await BorderInfoService.findAllBorder(worker_sheet_id);
|
||||
|
||||
borders?.forEach((border) => {
|
||||
const data = border.dataValues;
|
||||
// 根据当前数据,生成 config.borderInfo
|
||||
/**
|
||||
* 这里有两个类型哈 range cell 需要根据 rangeType 进行判断
|
||||
*/
|
||||
const { rangeType, borderType, style, color } = data;
|
||||
if (rangeType === "cell") {
|
||||
result.push({
|
||||
rangeType,
|
||||
value: {
|
||||
row_index: data.row_index,
|
||||
col_index: data.col_index,
|
||||
l: { style: data.l_style, color: data.l_color },
|
||||
r: { style: data.r_style, color: data.r_color },
|
||||
t: { style: data.t_style, color: data.t_color },
|
||||
b: { style: data.b_style, color: data.b_color },
|
||||
},
|
||||
});
|
||||
} else if (rangeType === "range") {
|
||||
const baseinfo = { rangeType, borderType, style, color };
|
||||
const row = <[number, number]>[data.row_start, data.row_end];
|
||||
const column = <[number, number]>[data.col_start, data.col_end];
|
||||
result.push({ ...baseinfo, range: [{ row, column }] });
|
||||
}
|
||||
});
|
||||
|
||||
currentSheetData.config.borderInfo = result;
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析隐藏行列和行高列宽
|
||||
*/
|
||||
async function parseHiddenAndLen(worker_sheet_id: string, currentSheetData: WorkerSheetItemType) {
|
||||
try {
|
||||
const dataArray = await HiddenAndLenService.findConfig(worker_sheet_id);
|
||||
dataArray?.forEach((item) => {
|
||||
const data = item.dataValues;
|
||||
const { config_type, config_index } = data;
|
||||
const value = Number(data.config_value);
|
||||
// 解析数据
|
||||
if (config_type === "rowhidden") {
|
||||
currentSheetData.config.rowhidden[config_index] = 0;
|
||||
} else if (config_type === "colhidden") {
|
||||
currentSheetData.config.colhidden[config_index] = 0;
|
||||
} else if (config_type === "rowlen") {
|
||||
currentSheetData.config.rowlen[config_index] = value;
|
||||
} else if (config_type === "columnlen") {
|
||||
currentSheetData.config.columnlen[config_index] = value;
|
||||
}
|
||||
});
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* parseImages 解析图片
|
||||
*/
|
||||
async function parseImages(worker_sheet_id: string, currentSheetData: WorkerSheetItemType) {
|
||||
try {
|
||||
const result = <{ [key: string]: ImagesType }>{};
|
||||
|
||||
const images = await ImageService.findAllImage(worker_sheet_id);
|
||||
|
||||
images?.forEach((image) => {
|
||||
const data = image.dataValues;
|
||||
result[data.image_key] = {
|
||||
type: data.image_type, //1移动并调整单元格大小 2移动并且不调整单元格的大小 3不要移动单元格并调整其大小
|
||||
src: data.image_src, //图片url
|
||||
|
||||
inCell: data.in_cell || "", // 所在单元格位置
|
||||
originWidth: data.image_originWidth,
|
||||
originHeight: data.image_originHeight,
|
||||
default: {
|
||||
width: data.image_default_width,
|
||||
height: data.image_default_height,
|
||||
left: data.image_default_left,
|
||||
top: data.image_default_top,
|
||||
},
|
||||
crop: {
|
||||
width: data.image_crop_width,
|
||||
height: data.image_crop_height,
|
||||
offsetLeft: data.image_crop_offsetLeft,
|
||||
offsetTop: data.image_crop_offsetTop,
|
||||
},
|
||||
isFixedPos: data.image_isFixedPos,
|
||||
fixedLeft: data.image_fixedLeft,
|
||||
fixedTop: data.image_fixedTop,
|
||||
border: {
|
||||
width: data.image_border_width,
|
||||
radius: data.image_border_radius,
|
||||
style: data.image_border_style,
|
||||
color: data.image_border_color,
|
||||
},
|
||||
};
|
||||
});
|
||||
currentSheetData.images = result;
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* parseCharts 解析图表数据
|
||||
*/
|
||||
|
||||
async function parseCharts(worker_sheet_id: string, currentSheetData: WorkerSheetItemType) {
|
||||
try {
|
||||
const result: ChartType[] = [];
|
||||
const charts = await ChartService.findAllChart(worker_sheet_id);
|
||||
charts?.forEach((chart) => {
|
||||
const data = chart.dataValues;
|
||||
result.push({
|
||||
chartType: data.chartType,
|
||||
chart_id: data.chart_id,
|
||||
width: data.width,
|
||||
height: data.height,
|
||||
left: data.left,
|
||||
top: data.top,
|
||||
sheetIndex: data.worker_sheet_id,
|
||||
needRangeShow: Boolean(data.needRangeShow),
|
||||
chartOptions: JSON.parse(data.chartOptions),
|
||||
});
|
||||
});
|
||||
|
||||
currentSheetData.chart = result;
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* parseCalcChain 解析公式连
|
||||
*/
|
||||
async function parseCalcChain(worker_sheet_id: string, currentSheetData: WorkerSheetItemType) {
|
||||
try {
|
||||
const result: CalcChainType[] = [];
|
||||
const charts = await CalcChainService.findAll(worker_sheet_id);
|
||||
charts?.forEach((chart) => {
|
||||
const data = chart.dataValues;
|
||||
result.push({
|
||||
r: data.r,
|
||||
c: data.c,
|
||||
index: data.worker_sheet_id,
|
||||
func: JSON.parse(data.func),
|
||||
color: "w",
|
||||
});
|
||||
});
|
||||
|
||||
currentSheetData.calcChain = result;
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { DB } from "../../Sequelize";
|
||||
import { Request, Response } from "express";
|
||||
import { WorkerBookService } from "../../Service/WorkerBook";
|
||||
|
||||
/**
|
||||
* 获取工作簿信息
|
||||
* @param { string } gridKey 工作簿key
|
||||
* @returns
|
||||
*/
|
||||
export async function getWorkerBook(req: Request, res: Response) {
|
||||
const gridKey = req.body.gridKey;
|
||||
|
||||
if (!gridKey) {
|
||||
res.status(400).json({ code: 400, msg: "gridKey 参数缺失" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!DB.getConnectState()) {
|
||||
res.json({
|
||||
code: 200,
|
||||
msg: "数据库未连接",
|
||||
data: { lang: "zh", title: "未命名工作簿" },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据 gridKey 请求 workerbooks 数据
|
||||
const book = await WorkerBookService.findOne(gridKey, {
|
||||
attributes: ["gridKey", "title", "lang"],
|
||||
});
|
||||
|
||||
res.json({ code: 200, msg: "ok", data: book });
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import fs from "fs";
|
||||
import multer from "multer";
|
||||
import { Request, Response } from "express";
|
||||
import { MULTER_CONFIG } from "../../Config";
|
||||
|
||||
// 配置 Multer
|
||||
const { dest, single } = MULTER_CONFIG;
|
||||
const upload = multer({ dest }).single(single);
|
||||
|
||||
/**
|
||||
* Luckysheet 自定义图片上传方法
|
||||
*/
|
||||
export async function uploadImage(req: Request, res: Response) {
|
||||
upload(req, res, async () => {
|
||||
const { file } = req;
|
||||
|
||||
// 如果没有解析到 file 对象,则直接返回 400
|
||||
if (!file) {
|
||||
res.status(400).json({ code: 400, msg: "请选择文件" });
|
||||
return;
|
||||
}
|
||||
|
||||
const { filename, originalname } = <Express.Multer.File>file;
|
||||
|
||||
/**
|
||||
* 处理文件路径:
|
||||
* 将原始路径 Snipaste_2024-10-10_09-46-01.png
|
||||
* 转换成 85d6d8c593b7239406ce2c13099c6110.png
|
||||
* 保留后缀,方便用户以静态资源访问
|
||||
*/
|
||||
const suffix = originalname.split(".").pop();
|
||||
const oldpath = `${MULTER_CONFIG.dest}/${filename}`;
|
||||
const newpath = `${MULTER_CONFIG.dest}/${filename}.${suffix}`;
|
||||
|
||||
// 重命名文件
|
||||
fs.renameSync(oldpath, newpath);
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
msg: "Success to upload.",
|
||||
url: `/uploads/${filename}.${suffix}`,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ENTRY_URL } from '../../Config';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
export function initPages(req: Request, res: Response) {
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8').sendFile(
|
||||
ENTRY_URL
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { initPages } from "./Page/index";
|
||||
import { uploadImage } from "./Luckysheet/uploadImage";
|
||||
import { loadSheetData } from "./Luckysheet/LoadSheetData";
|
||||
import { getWorkerBook } from "./Luckysheet/getWorkerBook";
|
||||
|
||||
// 统一导出控制层对象
|
||||
export const Controller = {
|
||||
initPages,
|
||||
uploadImage,
|
||||
loadSheetData,
|
||||
getWorkerBook,
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
import { BorderInfoType, CellDataItemType, BASE_CELL_DATA_TYPE } from "./luckysheet";
|
||||
import { WebSocket } from "ws";
|
||||
|
||||
interface CustomWebSocket extends WebSocket {
|
||||
clientInfo: {
|
||||
type: string; // 协同服务类型
|
||||
userid: string;
|
||||
username: string;
|
||||
gridkey: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 协同数据类型定义在这里 - 不同的操作类型会传递不同的 数据结构,这里都需要兼容
|
||||
type CRDTDataType<T> = {
|
||||
t: string; // 操作类型
|
||||
i: string; // 当前sheet的index值,而不是order
|
||||
r: number; // 行号
|
||||
c: number; // 列号
|
||||
v: T;
|
||||
|
||||
// RV
|
||||
range?: {
|
||||
row: [number, number];
|
||||
column: [number, number];
|
||||
};
|
||||
|
||||
// cg
|
||||
k?: "config" | "images" | "name" | "rowhidden" | "colhidden" | "rowlen" | "columnlen" | "borderInfo" | "color";
|
||||
|
||||
// Chart
|
||||
op?: "add" | "xy" | "wh" | "update" | "del";
|
||||
|
||||
// drc
|
||||
rc?: string;
|
||||
};
|
||||
|
||||
// 1. v
|
||||
type V = {
|
||||
v: string | number | null | undefined;
|
||||
m: string | number | null | undefined;
|
||||
} & BASE_CELL_DATA_TYPE;
|
||||
|
||||
// 2. rv
|
||||
type RV = BASE_CELL_DATA_TYPE[][];
|
||||
|
||||
// 3. cg
|
||||
type CG = {
|
||||
rangeType: string;
|
||||
borderType: string;
|
||||
style: string;
|
||||
color: string;
|
||||
range: [
|
||||
{
|
||||
row: [number, number];
|
||||
column: [number, number];
|
||||
}
|
||||
];
|
||||
}[];
|
||||
|
||||
type MERGE = {
|
||||
merge: {
|
||||
[key: string]: { r: number; c: number; rs: number; cs: number };
|
||||
};
|
||||
rowlen: {
|
||||
[key: string]: number;
|
||||
};
|
||||
};
|
||||
|
||||
type CHART = {
|
||||
chartType: string;
|
||||
chart_id: string;
|
||||
width: number | string;
|
||||
height: number | string;
|
||||
left: number | string;
|
||||
top: number | string;
|
||||
needRangeShow: boolean;
|
||||
chartOptions: string;
|
||||
};
|
||||
|
||||
type SHA = {
|
||||
name: string;
|
||||
status: string;
|
||||
order: number;
|
||||
index: string;
|
||||
row: number;
|
||||
column: number;
|
||||
celldata?: CellDataItemType[];
|
||||
copyindex?: string;
|
||||
config?: {
|
||||
merge: {
|
||||
[key: string]: { r: number; c: number; rs: number; cs: number };
|
||||
};
|
||||
rowlen: { [key: string]: number };
|
||||
columnlen: { [key: string]: number };
|
||||
rowhidden: { [key: string]: 0 };
|
||||
colhidden: { [key: string]: 0 };
|
||||
borderInfo: BorderInfoType[];
|
||||
};
|
||||
};
|
||||
|
||||
// 删除行列
|
||||
type DRC = {
|
||||
index: number;
|
||||
len: number;
|
||||
};
|
||||
|
||||
// 增加行列
|
||||
type ARC = {
|
||||
index: number;
|
||||
len: number;
|
||||
direction: string;
|
||||
data: BASE_CELL_DATA_TYPE[][];
|
||||
};
|
||||
|
||||
export { type V, type RV, type CG, type SHA, type DRC, type ARC, type CHART, type MERGE, CustomWebSocket, type CRDTDataType };
|
||||
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* 应该重新定义 luckysheet 前台/协同的数据类型,不要直接使用 uknown/any 等类型进行定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 1. worker book
|
||||
* 注意哈,这里的类型指的是可存储数据库的后台类型,并非指的是 loadUrl loadSheetUrl 等前台配置哈
|
||||
*/
|
||||
type WorkerBookType = {
|
||||
gridKey: string; // 唯一key
|
||||
title: string; // 工作簿名称
|
||||
lang: string; // 语言
|
||||
column?: number; // 空表格默认的列数量
|
||||
row?: number; // 空表格默认的行数量
|
||||
};
|
||||
|
||||
// 单个 sheet 页 配置
|
||||
type WorkerSheetItemType = {
|
||||
name: string; //工作表名称
|
||||
index: number | string; //工作表索引
|
||||
status: number; //激活状态
|
||||
order: number; //工作表的下标
|
||||
celldata: CellDataItemType[]; //初始化使用的单元格数据
|
||||
config: {
|
||||
merge: MergeType; //合并单元格
|
||||
rowlen: RowLenType; //表格行高
|
||||
columnlen: ColumnLenType; //表格列宽
|
||||
rowhidden: RowHiddenType; //隐藏行
|
||||
colhidden: ColHiddenType; //隐藏列
|
||||
borderInfo: BorderInfoType[]; //边框
|
||||
// authority: AuthorityType; //工作表保护
|
||||
};
|
||||
chart: ChartType[]; //图表配置
|
||||
images: { [key: string]: ImagesType }; //图片
|
||||
calcChain: CalcChainType[]; //公式链
|
||||
|
||||
/**
|
||||
* 下列属性非必选
|
||||
*/
|
||||
color?: string; //工作表颜色
|
||||
hide?: number; //是否隐藏
|
||||
row?: number; //行数
|
||||
column?: number; //列数
|
||||
defaultRowHeight?: number; //自定义行高
|
||||
defaultColWidth?: number; //自定义列宽
|
||||
scrollLeft?: number; //左右滚动条位置
|
||||
scrollTop?: number; //上下滚动条位置
|
||||
luckysheet_select_save?: []; //选中的区域
|
||||
isPivotTable?: false; //是否数据透视表
|
||||
filter?: []; //筛选配置
|
||||
luckysheet_alternateformat_save?: []; //交替颜色
|
||||
luckysheet_alternateformat_save_modelCustom?: []; //自定义交替颜色
|
||||
zoomRatio?: number; // 缩放比例
|
||||
showGridLines?: number; //是否显示网格线
|
||||
// dataVerification: {}; //数据验证配置
|
||||
// pivotTable: {}; //数据透视表设置
|
||||
// filter_select: {}; //筛选范围
|
||||
// luckysheet_conditionformat_save: {}; //条件格式
|
||||
// frozen: {}; //冻结行列配置
|
||||
};
|
||||
|
||||
// 单个单元格
|
||||
type CellDataItemType = {
|
||||
r: number;
|
||||
c: number;
|
||||
v: BASE_CELL_DATA_TYPE;
|
||||
};
|
||||
|
||||
// 基础的单元格数据类型
|
||||
type BASE_CELL_DATA_TYPE = {
|
||||
ct: {
|
||||
//单元格值格式
|
||||
fa: string; //格式名称为自动格式
|
||||
t: string; //格式类型为数字类型
|
||||
s?: {
|
||||
ff: string;
|
||||
fc: string;
|
||||
fs: number;
|
||||
cl: number;
|
||||
un: number;
|
||||
bl: number;
|
||||
it: number;
|
||||
v: string;
|
||||
}[];
|
||||
};
|
||||
v: string | number; //内容的原始值为 233
|
||||
m: string | number; //内容的显示值为 233
|
||||
bg?: string; //背景为 "#f6b26b"
|
||||
ff?: string; // 字体为 "Arial"
|
||||
fc?: string; //字体颜色为 "#990000"
|
||||
bl?: boolean; //字体加粗
|
||||
it?: boolean; //字体斜体
|
||||
fs?: number; //字体大小为 9px
|
||||
cl?: boolean; //启用删除线
|
||||
un?: boolean; // 启用下划线
|
||||
ht?: number; //水平居中
|
||||
vt?: number; //垂直居中
|
||||
tr?: number; //文字旋转 -45°
|
||||
tb?: number; //文本自动换行
|
||||
mc?: {
|
||||
// 合并单元格
|
||||
r: number; //主单元格的行号
|
||||
c: number; //主单元格的列号
|
||||
rs: number; //合并单元格占的行数
|
||||
cs: number; //合并单元格占的列数
|
||||
};
|
||||
ps?: {
|
||||
//批注
|
||||
left?: number; //批注框左边距
|
||||
top?: number; //批注框上边距
|
||||
width?: number; //批注框宽度
|
||||
height?: number; //批注框高度
|
||||
value: string; //批注内容
|
||||
isshow?: boolean; //批注框为显示状态
|
||||
} | null;
|
||||
f?: string | null; //单元格是一个求和公式 "=SUM(233)"
|
||||
};
|
||||
|
||||
// 合并单元格类型
|
||||
type MergeType = {
|
||||
[key: string]: {
|
||||
r: number;
|
||||
c: number;
|
||||
rs: number;
|
||||
cs: number;
|
||||
};
|
||||
};
|
||||
|
||||
// 行高类型
|
||||
type RowLenType = {
|
||||
[key: string]: number;
|
||||
};
|
||||
|
||||
// 列宽类型
|
||||
type ColumnLenType = {
|
||||
[key: string]: number;
|
||||
};
|
||||
|
||||
// 隐藏行类型
|
||||
type RowHiddenType = {
|
||||
[key: string]: 0;
|
||||
};
|
||||
|
||||
// 隐藏列类型
|
||||
type ColHiddenType = {
|
||||
[key: string]: 0;
|
||||
};
|
||||
|
||||
// 边框类型
|
||||
type BorderInfoType = {
|
||||
rangeType: string;
|
||||
borderType?: string;
|
||||
style?: string;
|
||||
color?: string;
|
||||
range?: [
|
||||
{
|
||||
row: [number, number];
|
||||
column: [number, number];
|
||||
}
|
||||
];
|
||||
|
||||
// 或者单个单元格 - https://dream-num.github.io/LuckysheetDocs/zh/guide/sheet.html#config-borderinfo
|
||||
value?: {
|
||||
row_index?: number;
|
||||
col_index?: number;
|
||||
l?: {
|
||||
style: number;
|
||||
color: string;
|
||||
};
|
||||
t?: {
|
||||
style: number;
|
||||
color: string;
|
||||
};
|
||||
r?: {
|
||||
style: number;
|
||||
color: string;
|
||||
};
|
||||
b?: {
|
||||
style: number;
|
||||
color: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// 图片类型
|
||||
type ImagesType = {
|
||||
type: string;
|
||||
src: string;
|
||||
originWidth: number;
|
||||
originHeight: number;
|
||||
inCell: string;
|
||||
default: {
|
||||
width: number;
|
||||
height: number;
|
||||
left: number;
|
||||
top: number;
|
||||
};
|
||||
crop: {
|
||||
width: number;
|
||||
height: number;
|
||||
offsetLeft: number;
|
||||
offsetTop: number;
|
||||
};
|
||||
isFixedPos: boolean;
|
||||
fixedLeft: number;
|
||||
fixedTop: number;
|
||||
border: {
|
||||
width: number;
|
||||
radius: number;
|
||||
style: string;
|
||||
color: string;
|
||||
};
|
||||
};
|
||||
|
||||
// 统计图表类型
|
||||
type ChartType = {
|
||||
chartType: string;
|
||||
chart_id: string;
|
||||
width: string | number;
|
||||
height: string | number;
|
||||
left: string | number;
|
||||
top: string | number;
|
||||
sheetIndex: string;
|
||||
needRangeShow: boolean;
|
||||
chartOptions: object;
|
||||
};
|
||||
|
||||
// 公式链操作
|
||||
type CalcChainType = {
|
||||
r: number; //行数
|
||||
c: number; //列数
|
||||
index: string; //工作表id
|
||||
func: [boolean, number | string, string]; //公式信息,包含公式计算结果和公式字符串
|
||||
color: "w"; //"w":采用深度优先算法 "b":普通计算
|
||||
};
|
||||
|
||||
export {
|
||||
type ChartType,
|
||||
type MergeType,
|
||||
type RowLenType,
|
||||
type ImagesType,
|
||||
type CalcChainType,
|
||||
type ColumnLenType,
|
||||
type RowHiddenType,
|
||||
type ColHiddenType,
|
||||
type BorderInfoType,
|
||||
type WorkerBookType,
|
||||
type CellDataItemType,
|
||||
type BASE_CELL_DATA_TYPE,
|
||||
type WorkerSheetItemType,
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import path from "path";
|
||||
import express from "express";
|
||||
|
||||
/**
|
||||
* 初始化静态资源
|
||||
*/
|
||||
function initStaticSource(app: express.Application) {
|
||||
app.use(express.static(path.resolve(__dirname, "../../public")));
|
||||
app.use(express.static(path.resolve(__dirname, "../../public/dist")));
|
||||
app.use(express.static(path.resolve(__dirname, "../../public/uploads")));
|
||||
app.use(express.static(path.resolve(__dirname, "../../public/dist/assets")));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理跨域
|
||||
*/
|
||||
function initCors(app: express.Application) {
|
||||
app.all("*", function (req, res, next) {
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
res.header("Access-Control-Allow-Headers", "Content-Type");
|
||||
res.header("Access-Control-Allow-Methods", "*");
|
||||
res.header("Content-Type", "application/json;charset=utf-8");
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 bodyparser
|
||||
*/
|
||||
function initBodyParser(app: express.Application) {
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(express.json());
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出注册中间件方法
|
||||
* @param app
|
||||
*/
|
||||
export const initMeddlewear = (app: express.Application) => {
|
||||
initStaticSource(app);
|
||||
initCors(app);
|
||||
initBodyParser(app);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import express from "express";
|
||||
import { Controller } from "../Controller";
|
||||
|
||||
const routes = express.Router();
|
||||
|
||||
// 用于处理 html 页面,部署使用
|
||||
routes.get("/", Controller.initPages);
|
||||
|
||||
// 用于上传图片 - 先定义 upload name 属性
|
||||
routes.post("/uploadImage", Controller.uploadImage);
|
||||
|
||||
// 请求 workerbook 数据
|
||||
routes.post("/getWorkerBook", Controller.getWorkerBook);
|
||||
|
||||
// 初始化 luckysheet 数据
|
||||
routes.post("/loadSheetData", Controller.loadSheetData);
|
||||
|
||||
// 模块化的路由,直接调用 routes.use() 即可
|
||||
export default routes;
|
||||
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* config border info model 边框数据模型
|
||||
*/
|
||||
|
||||
import { WorkerSheetModel } from "./WorkerSheet";
|
||||
import { DataTypes, InferAttributes, Model, Sequelize } from "sequelize";
|
||||
|
||||
export class BorderInfoModel extends Model {
|
||||
// 类型定义
|
||||
declare config_border_id?: string;
|
||||
declare worker_sheet_id: string;
|
||||
|
||||
declare rangeType: string; /** rangeType 合并单元格类型 range | cell */
|
||||
declare borderType?: string; /** borderType 边框类型 */
|
||||
declare style?: number; /** style 边框粗细 */
|
||||
declare color?: string; /** color 边框颜色 */
|
||||
|
||||
// 范围
|
||||
declare row_start?: number;
|
||||
declare row_end?: number;
|
||||
declare col_start?: number;
|
||||
declare col_end?: number;
|
||||
|
||||
// 单个单元格
|
||||
declare row_index?: number;
|
||||
declare col_index?: number;
|
||||
declare l_style?: number;
|
||||
declare l_color?: string;
|
||||
declare t_style?: number;
|
||||
declare t_color?: string;
|
||||
declare r_style?: number;
|
||||
declare r_color?: string;
|
||||
declare b_style?: number;
|
||||
declare b_color?: string;
|
||||
|
||||
// 注册模型
|
||||
static registerModule(sequelize: Sequelize) {
|
||||
BorderInfoModel.init(
|
||||
{
|
||||
config_border_id: {
|
||||
type: DataTypes.STRING, // 类型
|
||||
allowNull: false, // 非空
|
||||
comment: "config_border_id 边框配置表唯一ID", // 描述
|
||||
primaryKey: true,
|
||||
defaultValue: DataTypes.UUIDV4, // 默认使用 uuid 作为 主键ID
|
||||
},
|
||||
worker_sheet_id: {
|
||||
type: DataTypes.STRING, // 类型
|
||||
allowNull: false, // 非空
|
||||
comment: "外键:关联 workersheets 的 worker_sheet_id", // 描述
|
||||
references: {
|
||||
model: WorkerSheetModel,
|
||||
key: "worker_sheet_id",
|
||||
},
|
||||
},
|
||||
|
||||
rangeType: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: "rangeType 合并单元格类型 range | cell",
|
||||
},
|
||||
|
||||
// 因此,下列属性需要兼容 range cell 不同类型的合并属性
|
||||
borderType: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: "borderType 边框类型",
|
||||
},
|
||||
style: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: "style 边框粗细",
|
||||
},
|
||||
color: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: "color 边框颜色",
|
||||
},
|
||||
/**
|
||||
* range 行列信息数组
|
||||
* "range": [{
|
||||
* "row": [row_start, roe_end],
|
||||
* "column": [col_start, col_end]
|
||||
* }]
|
||||
*/
|
||||
row_start: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: "row_start 行号",
|
||||
},
|
||||
row_end: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: "row_end 行号",
|
||||
},
|
||||
col_start: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: "col_start 列号",
|
||||
},
|
||||
col_end: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: "col_end 列号",
|
||||
},
|
||||
/**
|
||||
* {
|
||||
* "row_index":7,
|
||||
* "col_index":6,
|
||||
* "l":{"style":1,"color":"#000000"},
|
||||
* "r":{"style":1,"color":"#000000"},
|
||||
* "t":{"style":1,"color":"#000000"},
|
||||
* "b":{"style":1,"color":"#000000"}
|
||||
* }
|
||||
*/
|
||||
row_index: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: "row_index 行号",
|
||||
},
|
||||
col_index: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: "col_index 列号",
|
||||
},
|
||||
l_style: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: "l_style 边框粗细",
|
||||
},
|
||||
l_color: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: "l_color 边框颜色",
|
||||
},
|
||||
t_style: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: "t_style 边框粗细",
|
||||
},
|
||||
t_color: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: "t_color 边框颜色",
|
||||
},
|
||||
r_style: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: "r_style 边框粗细",
|
||||
},
|
||||
r_color: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: "r_color 边框颜色",
|
||||
},
|
||||
b_style: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: "b_style 边框粗细",
|
||||
},
|
||||
b_color: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: "b_color 边框颜色",
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize, // 将模型与 Sequelize 实例关联
|
||||
tableName: "borders", // 直接式提供数据库表名
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出类型
|
||||
export type BorderInfoModelType = InferAttributes<BorderInfoModel>;
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 公式链单独存储
|
||||
*/
|
||||
/**
|
||||
* Config Merges 合并单元格数据模型
|
||||
*
|
||||
*/
|
||||
|
||||
import { WorkerSheetModel } from "./WorkerSheet";
|
||||
import { DataTypes, InferAttributes, Model, Sequelize } from "sequelize";
|
||||
|
||||
export class CalcChainModel extends Model {
|
||||
declare calcchain_id?: string;
|
||||
declare worker_sheet_id: string;
|
||||
declare r: number;
|
||||
declare c: number;
|
||||
declare func: string;
|
||||
declare pos?: number;
|
||||
|
||||
// 注册模型
|
||||
static registerModule(sequelize: Sequelize) {
|
||||
CalcChainModel.init(
|
||||
{
|
||||
calcchain_id: {
|
||||
type: DataTypes.STRING, // 类型
|
||||
allowNull: false, // 非空
|
||||
comment: "公式连表 id", // 描述
|
||||
primaryKey: true,
|
||||
defaultValue: DataTypes.UUIDV4, // 默认使用 uuid 作为 主键ID
|
||||
},
|
||||
worker_sheet_id: {
|
||||
type: DataTypes.STRING, // 类型
|
||||
allowNull: false, // 非空
|
||||
comment: "外键:关联 worker_sheet_id, sheet index", // 描述
|
||||
references: {
|
||||
model: WorkerSheetModel,
|
||||
key: "worker_sheet_id",
|
||||
},
|
||||
},
|
||||
r: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "行号",
|
||||
},
|
||||
c: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "列号",
|
||||
},
|
||||
func: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: "公式",
|
||||
},
|
||||
pos: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: "公式位置",
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize, // 将模型与 Sequelize 实例关联
|
||||
tableName: "calcchains", // 直接式提供数据库表名
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出类型
|
||||
export type CalcChainModelType = InferAttributes<CalcChainModel>;
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* CellData 数据模型
|
||||
*/
|
||||
|
||||
import { WorkerSheetModel } from "./WorkerSheet";
|
||||
import { DataTypes, InferAttributes, Model, Sequelize } from "sequelize";
|
||||
|
||||
/**
|
||||
* Sequelize 模型是 ES6 类. 你可以非常轻松地添加自定义实例或类级别的方法.
|
||||
*/
|
||||
export class CellDataModel extends Model {
|
||||
// 模型字段
|
||||
declare cell_data_id?: string;
|
||||
declare worker_sheet_id: string;
|
||||
declare r: number;
|
||||
declare c: number;
|
||||
declare v?: string;
|
||||
declare m?: string;
|
||||
|
||||
declare ctfa?: string;
|
||||
declare ctt?: string;
|
||||
declare cts?: string; // inline str
|
||||
declare bg?: string;
|
||||
declare ff?: string;
|
||||
declare fc?: string;
|
||||
declare fs?: number;
|
||||
declare bl?: boolean;
|
||||
declare it?: boolean;
|
||||
declare cl?: boolean;
|
||||
declare un?: boolean;
|
||||
declare vt?: number;
|
||||
declare ht?: number;
|
||||
declare f?: string | null;
|
||||
declare ps?: string | null;
|
||||
declare tb?: number; // 文本换行方式
|
||||
|
||||
// 都需要导出一个 register 方法,用于注册模型
|
||||
static registerModule(sequelize: Sequelize) {
|
||||
CellDataModel.init(
|
||||
{
|
||||
cell_data_id: {
|
||||
type: DataTypes.STRING, // 类型
|
||||
allowNull: false, // 非空
|
||||
comment: "cell data id", // 描述
|
||||
primaryKey: true,
|
||||
defaultValue: DataTypes.UUIDV4, // 默认使用 uuid 作为 主键ID
|
||||
},
|
||||
worker_sheet_id: {
|
||||
type: DataTypes.STRING, // 类型
|
||||
allowNull: false, // 非空
|
||||
comment: "外键:关联 worksheets 的 worker_sheet_id", // 描述
|
||||
references: {
|
||||
model: WorkerSheetModel,
|
||||
key: "worker_sheet_id",
|
||||
},
|
||||
},
|
||||
r: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "行号",
|
||||
},
|
||||
c: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "列号",
|
||||
},
|
||||
/** ctfa + ctt ==> ct:{fa,t} 这两个字段共同构成 celldata.ct 字段的值,决定了如何显示单元格内容 */
|
||||
ctfa: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: "ct:{fa} Format格式的定义串,'General'|'@'|'0'|'0.0'....",
|
||||
defaultValue: "General",
|
||||
},
|
||||
ctt: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: "ct:{t} Type类型,'g'|'s'|'n'....",
|
||||
defaultValue: "g",
|
||||
},
|
||||
cts: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: "ct:{s} inlineStr v 内容整体存储",
|
||||
defaultValue: "",
|
||||
},
|
||||
bg: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: "背景色",
|
||||
defaultValue: "#FFFFFF",
|
||||
},
|
||||
ff: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment:
|
||||
"0 Times New Roman、 1 Arial、2 Tahoma 、3 Verdana、4 微软雅黑、5 宋体(Song)、6 黑体(ST Heiti)、7 楷体(ST Kaiti)、 8 仿宋(ST FangSong)、9 新宋体(ST Song)、10 华文新魏、11 华文行楷、12 华文隶书",
|
||||
defaultValue: "5",
|
||||
},
|
||||
fc: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: "字体颜色",
|
||||
defaultValue: "#000000",
|
||||
},
|
||||
fs: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: "字体大小",
|
||||
defaultValue: 10,
|
||||
},
|
||||
bl: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: true,
|
||||
comment: "是否加粗",
|
||||
defaultValue: false,
|
||||
},
|
||||
it: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: true,
|
||||
comment: "是否斜体",
|
||||
defaultValue: false,
|
||||
},
|
||||
cl: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: true,
|
||||
comment: "是否删除线",
|
||||
defaultValue: false,
|
||||
},
|
||||
un: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: true,
|
||||
comment: "是否下划线",
|
||||
defaultValue: false,
|
||||
},
|
||||
vt: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: "垂直居中 0 中间、1 上、2下",
|
||||
defaultValue: 0,
|
||||
},
|
||||
ht: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: "水平居中 0 居中、1 左、2右",
|
||||
defaultValue: 0,
|
||||
},
|
||||
tb: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: "文本换行方式 0 自动换行、1 单行、2 多行",
|
||||
defaultValue: 0,
|
||||
},
|
||||
// tr rt 不想实现了~ 大家自己拓展其他的数据类型吧
|
||||
// v m 的取值逻辑由 ct:{fa t} 决定
|
||||
v: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: "原始值",
|
||||
},
|
||||
m: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: "显示值",
|
||||
},
|
||||
f: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: "公式",
|
||||
},
|
||||
ps: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: "批注",
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize, // 将模型与 Sequelize 实例关联
|
||||
tableName: "celldatas", // 直接式提供数据库表名
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type CellDataModelType = InferAttributes<CellDataModel>;
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 统计图
|
||||
*/
|
||||
|
||||
import { Model, Sequelize, DataTypes, InferAttributes } from "sequelize";
|
||||
import { WorkerSheetModel } from "./WorkerSheet";
|
||||
|
||||
export class ChartModel extends Model {
|
||||
declare chart_id: string;
|
||||
declare worker_sheet_id: string;
|
||||
declare chartType: string;
|
||||
declare width?: number | string;
|
||||
declare height?: number | string;
|
||||
declare left?: number | string;
|
||||
declare top?: number | string;
|
||||
declare needRangeShow?: boolean; // 目前不需要这个字段
|
||||
declare chartOptions?: string;
|
||||
|
||||
static registerModule(sequelize: Sequelize) {
|
||||
ChartModel.init(
|
||||
{
|
||||
chart_id: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
allowNull: false,
|
||||
comment: "表ID",
|
||||
},
|
||||
worker_sheet_id: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: "所属工作表ID",
|
||||
references: {
|
||||
model: WorkerSheetModel,
|
||||
key: "worker_sheet_id",
|
||||
},
|
||||
},
|
||||
chartType: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: "图表类型",
|
||||
defaultValue: "chartmix",
|
||||
},
|
||||
width: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: "width",
|
||||
},
|
||||
height: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: "height",
|
||||
},
|
||||
left: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: "left",
|
||||
},
|
||||
top: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: "top",
|
||||
},
|
||||
needRangeShow: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: true,
|
||||
defaultValue: false,
|
||||
comment: "needRangeShow",
|
||||
},
|
||||
chartOptions: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: "chartOptions",
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
tableName: "charts",
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出类型
|
||||
export type ChartModelType = InferAttributes<ChartModel>;
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* config row/col hiddens model 配置行列隐藏
|
||||
*/
|
||||
|
||||
import { WorkerSheetModel } from "./WorkerSheet";
|
||||
import { DataTypes, InferAttributes, Model, Sequelize } from "sequelize";
|
||||
|
||||
export class HiddenAndLenModel extends Model {
|
||||
// 类型定义
|
||||
declare config_id?: string;
|
||||
declare worker_sheet_id: string;
|
||||
declare config_type: string; /** 隐藏类型 row/col */
|
||||
declare config_index?: string; /** 隐藏的行号/列号 */
|
||||
declare config_value?: number; /** 定义的行高列宽 */
|
||||
|
||||
// 注册模型
|
||||
static registerModule(sequelize: Sequelize) {
|
||||
HiddenAndLenModel.init(
|
||||
{
|
||||
config_id: {
|
||||
type: DataTypes.STRING, // 类型
|
||||
allowNull: false, // 非空
|
||||
comment: "config_id", // 描述
|
||||
primaryKey: true,
|
||||
defaultValue: DataTypes.UUIDV4, // 默认使用 uuid 作为 主键ID
|
||||
},
|
||||
worker_sheet_id: {
|
||||
type: DataTypes.STRING, // 类型
|
||||
allowNull: false, // 非空
|
||||
comment: "外键:关联 workersheet 的 worker_sheet_id", // 描述
|
||||
references: {
|
||||
model: WorkerSheetModel,
|
||||
key: "worker_sheet_id",
|
||||
},
|
||||
},
|
||||
config_type: {
|
||||
type: DataTypes.STRING, // 类型
|
||||
allowNull: false, // 非空
|
||||
comment: "隐藏类型 rowhidden/colhidden/rowlen/collen", // 描述
|
||||
},
|
||||
/**
|
||||
* 根据下列字段,生成 下列配置对象
|
||||
* "rowhidden": {
|
||||
* "30": 0,
|
||||
* "31": 0
|
||||
* }
|
||||
*/
|
||||
config_index: {
|
||||
type: DataTypes.STRING, // 类型
|
||||
allowNull: false, // 非空
|
||||
comment: "隐藏的行号/列号", // 描述
|
||||
},
|
||||
config_value: {
|
||||
type: DataTypes.INTEGER, // 类型
|
||||
allowNull: false, // 非空
|
||||
comment: "隐藏的行号/列号的长度", // 描述
|
||||
defaultValue: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize, // 将模型与 Sequelize 实例关联
|
||||
tableName: "hiddenandlens", // 直接式提供数据库表名
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出类型
|
||||
export type HiddenAndLenModelType = InferAttributes<HiddenAndLenModel>;
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Image 图片
|
||||
*/
|
||||
|
||||
import { WorkerSheetModel } from "./WorkerSheet";
|
||||
import { Model, Sequelize, DataTypes, InferAttributes } from "sequelize";
|
||||
|
||||
export class ImageModel extends Model {
|
||||
declare image_id?: string;
|
||||
declare worker_sheet_id: string; // 外键
|
||||
declare image_type: string; // type 1移动并调整单元格大小 2移动并且不调整单元格的大小 3不要移动单元格并调整其大小
|
||||
declare image_src: string; // 图片地址
|
||||
|
||||
declare image_key: string; // 图片唯一标识
|
||||
|
||||
declare in_cell?: string; // 是否在单元格内
|
||||
|
||||
declare image_originWidth: number; // 原始宽度
|
||||
declare image_originHeight: number; // 原始高度
|
||||
|
||||
declare image_default_width: number; // 默认宽度
|
||||
declare image_default_height: number; // 默认高度
|
||||
declare image_default_left: number; // 默认左边距
|
||||
declare image_default_top: number; // 默认上边距
|
||||
|
||||
declare image_crop_width: number; // 裁剪宽度
|
||||
declare image_crop_height: number; // 裁剪高度
|
||||
declare image_crop_offsetLeft: number; // 裁剪左边距
|
||||
declare image_crop_offsetTop: number; // 裁剪上边距
|
||||
|
||||
declare image_isFixedPos: boolean; // 是否固定位置
|
||||
declare image_fixedLeft: number; // 固定左边距
|
||||
declare image_fixedTop: number; // 固定上边距
|
||||
|
||||
declare image_border_width: number; // 边框宽度
|
||||
declare image_border_radius: number; // 圆角
|
||||
declare image_border_style: string; // 边框样式
|
||||
declare image_border_color: string; // 边框颜色
|
||||
|
||||
static registerModule(sequelize: Sequelize) {
|
||||
ImageModel.init(
|
||||
{
|
||||
image_id: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
allowNull: false,
|
||||
comment: "图片ID",
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
},
|
||||
worker_sheet_id: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: "所属工作表ID",
|
||||
references: {
|
||||
model: WorkerSheetModel,
|
||||
key: "worker_sheet_id",
|
||||
},
|
||||
},
|
||||
image_type: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: "图片类型",
|
||||
},
|
||||
image_src: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: "图片地址",
|
||||
},
|
||||
in_cell: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: "单元格图片标识",
|
||||
},
|
||||
image_key: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: "图表标识",
|
||||
},
|
||||
image_originWidth: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "原始宽度",
|
||||
},
|
||||
image_originHeight: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "原始高度",
|
||||
},
|
||||
image_default_width: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "默认宽度",
|
||||
},
|
||||
image_default_height: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
image_default_left: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "默认左边距",
|
||||
},
|
||||
image_default_top: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "默认上边距",
|
||||
},
|
||||
image_crop_width: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "裁剪宽度",
|
||||
},
|
||||
image_crop_height: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "裁剪高度",
|
||||
},
|
||||
image_crop_offsetLeft: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "裁剪左边距",
|
||||
},
|
||||
image_crop_offsetTop: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "裁剪上边距",
|
||||
},
|
||||
image_isFixedPos: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
comment: "是否固定位置",
|
||||
},
|
||||
image_fixedLeft: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "固定左边距",
|
||||
},
|
||||
image_fixedTop: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
image_border_width: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "边框宽度",
|
||||
},
|
||||
image_border_radius: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "圆角",
|
||||
},
|
||||
image_border_style: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
image_border_color: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: "边框颜色",
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
tableName: "images",
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出类型
|
||||
export type ImageModelType = InferAttributes<ImageModel>;
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Config Merges 合并单元格数据模型
|
||||
*
|
||||
*/
|
||||
|
||||
import { WorkerSheetModel } from "./WorkerSheet";
|
||||
import { DataTypes, InferAttributes, Model, Sequelize } from "sequelize";
|
||||
|
||||
export class MergeModel extends Model {
|
||||
declare config_merge_id?: string;
|
||||
declare worker_sheet_id: string;
|
||||
declare r: number;
|
||||
declare c: number;
|
||||
declare rs: number;
|
||||
declare cs: number;
|
||||
|
||||
// 注册模型
|
||||
static registerModule(sequelize: Sequelize) {
|
||||
MergeModel.init(
|
||||
{
|
||||
config_merge_id: {
|
||||
type: DataTypes.STRING, // 类型
|
||||
allowNull: false, // 非空
|
||||
comment: "config merge id", // 描述
|
||||
primaryKey: true,
|
||||
defaultValue: DataTypes.UUIDV4, // 默认使用 uuid 作为 主键ID
|
||||
},
|
||||
worker_sheet_id: {
|
||||
type: DataTypes.STRING, // 类型
|
||||
allowNull: false, // 非空
|
||||
comment: "外键:关联 cell_datas 的 worker_sheet_id", // 描述
|
||||
references: {
|
||||
model: WorkerSheetModel,
|
||||
key: "worker_sheet_id",
|
||||
},
|
||||
},
|
||||
/**
|
||||
*
|
||||
* 设置一个合并单元格,需要处理两个地方,一是单元格对象中设置mc属性,二是在config中设置merge
|
||||
* "r": 0, //主单元格的行号
|
||||
* "c": 0, //主单元格的列号
|
||||
* "rs": 2, //合并单元格占的行数
|
||||
* "cs": 2 //合并单元格占的列数
|
||||
*
|
||||
* 业务上的处理逻辑是:先查询这个表,如果有字段值,则反馈生成 celldata mc 属性字段
|
||||
*/
|
||||
r: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "r 主单元格的行号",
|
||||
},
|
||||
c: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "c 主单元格的列号",
|
||||
},
|
||||
rs: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "rs 合并单元格占的行数",
|
||||
},
|
||||
cs: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "cs 合并单元格占的列数",
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize, // 将模型与 Sequelize 实例关联
|
||||
tableName: "merges", // 直接式提供数据库表名
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出类型
|
||||
export type MergeModelType = InferAttributes<MergeModel>;
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Worker Books 工作簿模型表
|
||||
*/
|
||||
|
||||
import { DataTypes, InferAttributes, Model, Sequelize } from "sequelize";
|
||||
|
||||
export class WorkerBookModel extends Model {
|
||||
declare gridKey: string; /** gridKey 是用户系统唯一文件ID 必须由用户传递 */
|
||||
declare title?: string;
|
||||
declare lang?: string;
|
||||
|
||||
static registerModule(sequelize: Sequelize) {
|
||||
WorkerBookModel.init(
|
||||
{
|
||||
gridKey: {
|
||||
type: DataTypes.STRING, // 类型
|
||||
allowNull: false, // 非空
|
||||
comment: "gridKey", // 描述
|
||||
primaryKey: true, // 主键
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING, // 类型
|
||||
allowNull: false, // 非空
|
||||
comment: "工作簿名称", // 描述
|
||||
defaultValue: "未命名工作簿", // 默认值
|
||||
},
|
||||
lang: {
|
||||
type: DataTypes.STRING(10), // 类型
|
||||
allowNull: false, // 非空
|
||||
comment: "语言", // 描述
|
||||
defaultValue: "zh", // zh en
|
||||
},
|
||||
// ... 更多字段,根据项目实际情况添加
|
||||
},
|
||||
{
|
||||
sequelize, // 将模型与 Sequelize 实例关联
|
||||
tableName: "workerbooks", // 直接式提供数据库表名
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出类型
|
||||
export type WorkerBookModelType = InferAttributes<WorkerBookModel>;
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Worker Books 工作簿模型表
|
||||
*/
|
||||
|
||||
import { WorkerBookModel } from "./WorkerBook";
|
||||
import { DataTypes, InferAttributes, Model, Sequelize } from "sequelize";
|
||||
|
||||
export class WorkerSheetModel extends Model {
|
||||
declare worker_sheet_id?: string; /** 有默认值,非必传 */
|
||||
declare gridKey: string;
|
||||
declare name: string;
|
||||
declare order?: number;
|
||||
declare status?: number;
|
||||
declare hide?: boolean;
|
||||
declare row?: number;
|
||||
declare column?: number;
|
||||
declare color?: string;
|
||||
declare defaultRowHeight?: number;
|
||||
declare defaultColWidth?: number;
|
||||
declare deleteFlag?: boolean; // 是否被删除
|
||||
|
||||
static registerModule(sequelize: Sequelize) {
|
||||
WorkerSheetModel.init(
|
||||
{
|
||||
worker_sheet_id: {
|
||||
type: DataTypes.STRING, // 类型
|
||||
allowNull: false, // 非空
|
||||
comment: "worker sheet id", // 描述
|
||||
primaryKey: true, // 主键
|
||||
defaultValue: DataTypes.UUIDV4, // 默认使用 uuid 作为 主键ID
|
||||
},
|
||||
gridKey: {
|
||||
type: DataTypes.STRING, // 类型
|
||||
allowNull: false, // 非空
|
||||
comment: "外键:关联 workerbooks 的 gridKey", // 描述
|
||||
// 外键
|
||||
references: {
|
||||
model: WorkerBookModel,
|
||||
key: "gridKey",
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: "工作表名称",
|
||||
},
|
||||
order: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "工作表下标序号",
|
||||
defaultValue: 0,
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "工作表激活状态,仅有一个激活状态的工作表,其他工作表为 0",
|
||||
defaultValue: 0,
|
||||
},
|
||||
hide: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
comment: "是否隐藏,0为不隐藏,1为隐藏",
|
||||
defaultValue: false,
|
||||
},
|
||||
row: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "行数", // 描述
|
||||
defaultValue: 30, // 默认值
|
||||
},
|
||||
column: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "列数", // 描述
|
||||
defaultValue: 24, // 默认值
|
||||
},
|
||||
color: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: "表格颜色", // 描述
|
||||
defaultValue: "", // 默认值
|
||||
},
|
||||
defaultRowHeight: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "默认行高",
|
||||
defaultValue: 20,
|
||||
},
|
||||
defaultColWidth: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "默认列宽",
|
||||
defaultValue: 80,
|
||||
},
|
||||
deleteFlag: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
comment: "是否被删除",
|
||||
defaultValue: false,
|
||||
},
|
||||
|
||||
// ... 更多字段,根据项目实际情况添加
|
||||
},
|
||||
{
|
||||
sequelize, // 将模型与 Sequelize 实例关联
|
||||
tableName: "workersheets", // 直接式提供数据库表名
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出类型
|
||||
export type WorkerSheetModelType = InferAttributes<WorkerSheetModel>;
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 使用 sequelzie 操作数据库
|
||||
* 导出唯一实例,需要提供判断数据库连接是否正常的方法
|
||||
*/
|
||||
|
||||
import { Sequelize } from "sequelize";
|
||||
import { logger } from "../Utils/Logger";
|
||||
import { ImageModel } from "./Models/Image";
|
||||
import { MergeModel } from "./Models/Merge";
|
||||
import { ChartModel } from "./Models/Chart";
|
||||
import { SQL_CONFIG } from "../Config/index";
|
||||
import { CellDataModel } from "./Models/CellData";
|
||||
import { BorderInfoModel } from "./Models/BorderInfo";
|
||||
import { WorkerBookModel } from "./Models/WorkerBook";
|
||||
import { WorkerSheetModel } from "./Models/WorkerSheet";
|
||||
import { HiddenAndLenModel } from "./Models/HiddenAndLen";
|
||||
import { CalcChainModel } from "./Models/CalcChain";
|
||||
|
||||
class DataBase {
|
||||
private _connected: boolean = false; // 连接状态
|
||||
private _sequelize: Sequelize | null = null; // 连接对象
|
||||
|
||||
constructor() {
|
||||
this._connected = false;
|
||||
this._sequelize = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据库
|
||||
*/
|
||||
public async connect() {
|
||||
const { port, host, database, user, password, enable } = SQL_CONFIG;
|
||||
|
||||
// 禁用数据库
|
||||
if (!enable) return logger.warn("🚫 数据库服务已禁用!");
|
||||
|
||||
// 创建连接
|
||||
this._sequelize = new Sequelize(database, user, password, {
|
||||
port,
|
||||
host,
|
||||
dialect: "mysql",
|
||||
logging: SQL_CONFIG.logger ? (sql: string) => logger.debug(sql) : false,
|
||||
logQueryParameters: true,
|
||||
});
|
||||
|
||||
// 测试连接
|
||||
try {
|
||||
await this._sequelize.authenticate();
|
||||
logger.info("✅️ Successfully connected to the database!");
|
||||
this._connected = true;
|
||||
|
||||
/** 连接成功后,进行模型注册 */
|
||||
this.registerModule();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (error) {
|
||||
logger.error("🚫 Failed to connect to the database!");
|
||||
this._connected = false;
|
||||
this._sequelize = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提供原始查询方法
|
||||
*/
|
||||
public async query(sql: string) {
|
||||
if (!this._sequelize || !this._connected) return;
|
||||
return await this._sequelize.query(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭数据库连接
|
||||
*/
|
||||
public close() {
|
||||
if (this._sequelize) this._sequelize.close();
|
||||
}
|
||||
/**
|
||||
* 获取连接状态
|
||||
*/
|
||||
public getConnectState(): boolean {
|
||||
return this._connected;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步表结构
|
||||
* 1. 请注意表的主键/外键关联关系,如果依赖外键,需要先注册主键表,不然会报错
|
||||
*/
|
||||
private registerModule() {
|
||||
if (!this._sequelize || !this._connected) return;
|
||||
// 初始化数据库表
|
||||
WorkerBookModel.registerModule(this._sequelize);
|
||||
WorkerSheetModel.registerModule(this._sequelize);
|
||||
CellDataModel.registerModule(this._sequelize);
|
||||
MergeModel.registerModule(this._sequelize);
|
||||
BorderInfoModel.registerModule(this._sequelize);
|
||||
HiddenAndLenModel.registerModule(this._sequelize);
|
||||
ImageModel.registerModule(this._sequelize);
|
||||
ChartModel.registerModule(this._sequelize);
|
||||
CalcChainModel.registerModule(this._sequelize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DataBase 数据库类 - 单例模式(简单实现)
|
||||
* 1. connect 连接数据库方法
|
||||
* 2. close 关闭数据库方法
|
||||
* 3. query(sql:string) 执行原生 SQL 查询
|
||||
* 4. getConnectState():boolean 获取连接状态
|
||||
*/
|
||||
export const DB = new DataBase();
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* database 重要文件 - 用于 package 初始化数据库表结构
|
||||
*/
|
||||
|
||||
import { Sequelize } from "sequelize";
|
||||
import { SQL_CONFIG } from "../Config";
|
||||
import { logger } from "../Utils/Logger";
|
||||
import { MergeModel } from "./Models/Merge";
|
||||
import { ImageModel } from "./Models/Image";
|
||||
import { ChartModel } from "./Models/Chart";
|
||||
import { CellDataModel } from "./Models/CellData";
|
||||
import { WorkerBookModel } from "./Models/WorkerBook";
|
||||
import { BorderInfoModel } from "./Models/BorderInfo";
|
||||
import { WorkerSheetModel } from "./Models/WorkerSheet";
|
||||
import { HiddenAndLenModel } from "./Models/HiddenAndLen";
|
||||
import { CalcChainModel } from "./Models/CalcChain";
|
||||
|
||||
(async () => {
|
||||
const { port, host, database, user, password } = SQL_CONFIG;
|
||||
const sequelize = new Sequelize(database, user, password, {
|
||||
port,
|
||||
host,
|
||||
dialect: "mysql",
|
||||
});
|
||||
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
logger.success("Connection has been established successfully.");
|
||||
|
||||
// 初始化数据库表
|
||||
WorkerBookModel.registerModule(sequelize);
|
||||
WorkerSheetModel.registerModule(sequelize);
|
||||
CellDataModel.registerModule(sequelize);
|
||||
MergeModel.registerModule(sequelize);
|
||||
BorderInfoModel.registerModule(sequelize);
|
||||
HiddenAndLenModel.registerModule(sequelize);
|
||||
ImageModel.registerModule(sequelize);
|
||||
ChartModel.registerModule(sequelize);
|
||||
CalcChainModel.registerModule(sequelize);
|
||||
|
||||
// 2. 同步模型 (非强制同步)
|
||||
await sequelize.sync({ alter: true });
|
||||
|
||||
sequelize.close();
|
||||
logger.success("✅️ 所有模型均已成功同步至最新状态.");
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
sequelize.close();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,66 @@
|
||||
import { BorderInfoModel, BorderInfoModelType } from "../Sequelize/Models/BorderInfo";
|
||||
import { logger } from "../Utils/Logger";
|
||||
|
||||
/**
|
||||
* 查找是否存在边框
|
||||
* @param info
|
||||
* @returns
|
||||
*/
|
||||
async function hasConfigBorder(info: BorderInfoModelType) {
|
||||
try {
|
||||
return await BorderInfoModel.findOne({
|
||||
where: info,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新的边框记录
|
||||
* @param info
|
||||
* @returns
|
||||
*/
|
||||
async function createConfigBorder(info: BorderInfoModelType) {
|
||||
try {
|
||||
return await BorderInfoModel.create(info);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新边框记录
|
||||
* @param info
|
||||
* @returns
|
||||
*/
|
||||
async function updateConfigBorder(info: BorderInfoModelType) {
|
||||
try {
|
||||
return await BorderInfoModel.update(info, {
|
||||
where: { config_border_id: info.config_border_id },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化查询全部 borderInfo
|
||||
* @param worker_sheet_id
|
||||
* @returns
|
||||
*/
|
||||
async function findAllBorder(worker_sheet_id: string) {
|
||||
try {
|
||||
// 边框属性需要根据创建记录升序排序,先创建的在第一条,返回时,才不会覆盖后面的记录
|
||||
return await BorderInfoModel.findAll({ where: { worker_sheet_id }, order: [["createdAt", "ASC"]] });
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export const BorderInfoService = {
|
||||
findAllBorder,
|
||||
hasConfigBorder,
|
||||
createConfigBorder,
|
||||
updateConfigBorder,
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import { CalcChainModel, CalcChainModelType } from "../Sequelize/Models/CalcChain";
|
||||
import { logger } from "../Utils/Logger";
|
||||
|
||||
/**
|
||||
* 查找是否已经存在记录
|
||||
*/
|
||||
async function findOne(worker_sheet_id: string, r: number, c: number) {
|
||||
try {
|
||||
return await CalcChainModel.findOne({
|
||||
where: { worker_sheet_id, r, c },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新
|
||||
async function update(calcchain_id: string, func: string) {
|
||||
try {
|
||||
return await CalcChainModel.update(
|
||||
{ func },
|
||||
{
|
||||
where: { calcchain_id },
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// ** 创建
|
||||
async function create(info: CalcChainModelType) {
|
||||
try {
|
||||
return await CalcChainModel.create(info);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
async function deleteCalcChain(worker_sheet_id: string, r: number, c: number) {
|
||||
try {
|
||||
return await CalcChainModel.destroy({
|
||||
where: { worker_sheet_id, r, c },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 查询全部
|
||||
async function findAll(worker_sheet_id: string) {
|
||||
try {
|
||||
return await CalcChainModel.findAll({ where: { worker_sheet_id } });
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export const CalcChainService = {
|
||||
findOne,
|
||||
update,
|
||||
create,
|
||||
findAll,
|
||||
deleteCalcChain,
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
import { logger } from "../Utils/Logger";
|
||||
import { CellDataModel, CellDataModelType } from "../Sequelize/Models/CellData";
|
||||
|
||||
/**
|
||||
* 通过 sheetid 查找当前数据表的单元格数据
|
||||
*/
|
||||
async function getCellData(worker_sheet_id: string) {
|
||||
try {
|
||||
return await CellDataModel.findAll({ where: { worker_sheet_id } });
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据传入的 sheetid rc 判断是否存在记录
|
||||
*/
|
||||
async function hasCellData(worker_sheet_id: string, r: number, c: number) {
|
||||
try {
|
||||
return await CellDataModel.findOne({
|
||||
where: { worker_sheet_id, r, c },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateCellData(info: CellDataModelType) {
|
||||
try {
|
||||
return await CellDataModel.update(info, {
|
||||
where: { cell_data_id: info.cell_data_id },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async function createCellData(info: CellDataModelType) {
|
||||
try {
|
||||
return await CellDataModel.create(info);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCellDataRC(worker_sheet_id: string, index: number, rc: "r" | "c") {
|
||||
try {
|
||||
return await CellDataModel.destroy({
|
||||
where: { worker_sheet_id, [rc]: index },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async function deleteCellData(worker_sheet_id: string, r: number, c: number) {
|
||||
try {
|
||||
return await CellDataModel.destroy({
|
||||
where: { worker_sheet_id, r, c },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除行列
|
||||
*/
|
||||
async function updateCellDataRC(payload: { worker_sheet_id: string; index: number; len: number; update_type: "r" | "c" }) {
|
||||
// {"t":"drc","i":"2b62e1f2-7f7f-4889-b34d-007fe7277364","v":{"index":0,"len":1},"rc":"r"}
|
||||
const { worker_sheet_id, index, len, update_type } = payload;
|
||||
|
||||
try {
|
||||
// 1. 查询所有的 celldata 的 数据
|
||||
const cellData = await CellDataModel.findAll({
|
||||
where: { worker_sheet_id },
|
||||
});
|
||||
|
||||
// 2. 更新 r = r + len
|
||||
if (cellData.length) {
|
||||
for (let i = 0; i < cellData.length; i++) {
|
||||
// 只有比 index 大的值,才进行处理
|
||||
if (cellData[i][update_type] > index) {
|
||||
cellData[i][update_type] = cellData[i][update_type] - len;
|
||||
await cellData[i].save();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加行列
|
||||
*/
|
||||
async function addCellDataRC(payload: { worker_sheet_id: string; index: number; len: number; update_type: "r" | "c" }) {
|
||||
const { worker_sheet_id, index, len, update_type } = payload;
|
||||
// 在上面加一列跟在下面加一列的区别:就是标记的这一行是否也跟着变化
|
||||
try {
|
||||
// 遍历查询
|
||||
const cellData = await CellDataModel.findAll({
|
||||
where: { worker_sheet_id },
|
||||
});
|
||||
|
||||
// {"t":"arc","i":"2b62e1f2...","v":{"index":8,"len":1,"direction":"lefttop","data":[]},"rc":"r"}
|
||||
|
||||
if (!cellData.length) return null;
|
||||
|
||||
// 不然执行增加操作
|
||||
// 如果rc的值是r新增行, 如果rc的值为c则新增列, 例如 rc=r,index=4,len=5,则代表从第4行开始增加5行
|
||||
// 然后把r大于4的单元格的整体的r值+5
|
||||
|
||||
for (let i = 0; i < cellData.length; i++) {
|
||||
if (cellData[i][update_type] >= index) {
|
||||
cellData[i][update_type] = cellData[i][update_type] + len;
|
||||
await cellData[i].save();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储函数链操作
|
||||
*/
|
||||
async function createFC(worker_sheet_id: string, r: number, c: number, calcChain: string) {
|
||||
try {
|
||||
await CellDataModel.update({ calcChain }, { where: { worker_sheet_id, r, c } });
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export const CellDataService = {
|
||||
getCellData,
|
||||
hasCellData,
|
||||
updateCellData,
|
||||
createCellData,
|
||||
deleteCellData,
|
||||
updateCellDataRC,
|
||||
deleteCellDataRC,
|
||||
addCellDataRC,
|
||||
createFC,
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { logger } from "../Utils/Logger";
|
||||
import { ChartModel, ChartModelType } from "../Sequelize/Models/Chart";
|
||||
|
||||
/**
|
||||
* 创建图表
|
||||
*/
|
||||
async function createChart(data: ChartModelType) {
|
||||
try {
|
||||
return await ChartModel.create(data);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新图表
|
||||
* @param data xy wh option
|
||||
* @returns
|
||||
*/
|
||||
async function updateChart(data: ChartModelType) {
|
||||
try {
|
||||
return await ChartModel.update(data, {
|
||||
where: { chart_id: data.chart_id },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除图表
|
||||
async function deleteChart(chart_id: string) {
|
||||
try {
|
||||
return await ChartModel.destroy({ where: { chart_id } });
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// findAllChart
|
||||
async function findAllChart(worker_sheet_id: string) {
|
||||
try {
|
||||
return await ChartModel.findAll({
|
||||
where: { worker_sheet_id },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export const ChartService = {
|
||||
createChart,
|
||||
updateChart,
|
||||
deleteChart,
|
||||
findAllChart,
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { HiddenAndLenModelType, HiddenAndLenModel } from "../Sequelize/Models/HiddenAndLen";
|
||||
import { logger } from "../Utils/Logger";
|
||||
|
||||
/**
|
||||
* 创建隐藏记录
|
||||
* HidenAndLen 表结构设定为 存储单条记录,因此,在创建之前,需要先查询是否已经存在 gridkey and config_type and config_index 的记录,
|
||||
* 如果存在的话,需要先进行删除
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
async function create(data: HiddenAndLenModelType) {
|
||||
try {
|
||||
// 查询是否存在记录
|
||||
const exist = await HiddenAndLenModel.findOne({
|
||||
where: {
|
||||
worker_sheet_id: data.worker_sheet_id,
|
||||
config_type: data.config_type,
|
||||
config_index: data.config_index,
|
||||
},
|
||||
});
|
||||
if (exist)
|
||||
await HiddenAndLenModel.destroy({
|
||||
where: { config_id: exist.config_id },
|
||||
});
|
||||
|
||||
return await HiddenAndLenModel.create(data);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 查询 worker_sheet_id 下所有隐藏记录 - 用于初始化时查询
|
||||
async function findConfig(worker_sheet_id: string) {
|
||||
try {
|
||||
return await HiddenAndLenModel.findAll({
|
||||
where: { worker_sheet_id },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除 hidden 记录
|
||||
async function deleteHidden(worker_sheet_id: string, config_type: string, config_index?: string) {
|
||||
try {
|
||||
return await HiddenAndLenModel.destroy({
|
||||
where: Object.assign({}, { worker_sheet_id, config_type }, config_index ? { config_index } : {}),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除列
|
||||
async function deleteRC(worker_sheet_id: string, config_index: string) {
|
||||
try {
|
||||
return await HiddenAndLenModel.destroy({
|
||||
where: { worker_sheet_id, config_index },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export const HiddenAndLenService = {
|
||||
create,
|
||||
findConfig,
|
||||
deleteHidden,
|
||||
deleteRC,
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { logger } from '../Utils/Logger';
|
||||
import { ImageModel, ImageModelType } from '../Sequelize/Models/Image';
|
||||
|
||||
/**
|
||||
* 先删除当前 sheet 的所有图片
|
||||
*/
|
||||
async function deleteImage(worker_sheet_id: string) {
|
||||
try {
|
||||
return await ImageModel.destroy({ where: { worker_sheet_id } });
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 image 记录
|
||||
*/
|
||||
async function createImage(data: ImageModelType) {
|
||||
try {
|
||||
return await ImageModel.create(data);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前 workersheetid 的图片
|
||||
*/
|
||||
async function findAllImage(worker_sheet_id: string) {
|
||||
try {
|
||||
return await ImageModel.findAll({ where: { worker_sheet_id } });
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
export const ImageService = { deleteImage, createImage, findAllImage };
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 表格合并单元格处理逻辑:
|
||||
* 设置一个合并单元格,需要处理两个地方,一是单元格对象中设置mc属性,二是在config中设置merge
|
||||
* "r": 0, //主单元格的行号
|
||||
* "c": 0, //主单元格的列号
|
||||
* "rs": 2, //合并单元格占的行数
|
||||
* "cs": 2 //合并单元格占的列数
|
||||
*
|
||||
* 业务上的处理逻辑是:先查询 merges 这个表,如果有字段值,则反馈生成 celldata mc 属性字段
|
||||
*
|
||||
* 如果是创建新的合并单元格,需要判断 r c 是否存在,存在则更新 rs cs,不存在则插入
|
||||
*/
|
||||
|
||||
import { logger } from "../Utils/Logger";
|
||||
import { MergeModel, MergeModelType } from "../Sequelize/Models/Merge";
|
||||
|
||||
async function deleteMerge(worker_sheet_id: string) {
|
||||
try {
|
||||
return await MergeModel.destroy({
|
||||
where: {
|
||||
worker_sheet_id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 新增记录
|
||||
async function createMerge(info: MergeModelType) {
|
||||
try {
|
||||
return await MergeModel.create(info);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询全部
|
||||
async function findAllMerge(worker_sheet_id: string) {
|
||||
try {
|
||||
return await MergeModel.findAll({
|
||||
where: { worker_sheet_id },
|
||||
order: [["createdAt", "ASC"]],
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const MergeService = {
|
||||
deleteMerge,
|
||||
createMerge,
|
||||
findAllMerge,
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Worker Books Service
|
||||
*/
|
||||
|
||||
import { logger } from "../Utils/Logger";
|
||||
import {
|
||||
WorkerBookModel,
|
||||
WorkerBookModelType,
|
||||
} from "../Sequelize/Models/WorkerBook";
|
||||
import {
|
||||
WorkerSheetModel,
|
||||
WorkerSheetModelType,
|
||||
} from "../Sequelize/Models/WorkerSheet";
|
||||
import { FindOptions } from "sequelize";
|
||||
|
||||
/**
|
||||
* 新增 workerBooks 记录
|
||||
* @param info WorkerBookModelType
|
||||
* @returns
|
||||
*/
|
||||
async function create(info: WorkerBookModelType) {
|
||||
try {
|
||||
// 1. 先查询用户传递的 gridKey 是否存在记录
|
||||
const exist = await findOne(info.gridKey);
|
||||
if (exist) return { code: 1, msg: "该 gridKey 已经存在" };
|
||||
|
||||
// 2. 创建一个 worker books
|
||||
const book = await WorkerBookModel.create(info);
|
||||
|
||||
// 3. 为当前 worker book 创建一个 worker sheet 因为一个 Ecxel 文档中至少需要一个 sheet
|
||||
const sheetInfo: WorkerSheetModelType = {
|
||||
gridKey: info.gridKey,
|
||||
name: "Sheet1",
|
||||
order: 0,
|
||||
status: 1,
|
||||
};
|
||||
const sheet = await WorkerSheetModel.create(sheetInfo);
|
||||
|
||||
return {
|
||||
code: 0,
|
||||
msg: "工作簿创建成功,已同步创建 worker sheet 记录.",
|
||||
book,
|
||||
sheet,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 workerBooks 记录
|
||||
*/
|
||||
async function update(info: WorkerBookModelType) {
|
||||
try {
|
||||
return await WorkerBookModel.update(info, {
|
||||
where: { gridKey: info.gridKey },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 workerBooks 记录 - 注意:该表有外键关联,如果删除记录可能会导致其他业务表有问题,因此,提供 stage 参数,标记记录是否可用
|
||||
*/
|
||||
async function del() {}
|
||||
|
||||
/**
|
||||
* 查询 workerBooks 记录
|
||||
*/
|
||||
async function findOne(
|
||||
gridKey: string,
|
||||
options?: FindOptions<WorkerBookModelType>
|
||||
) {
|
||||
try {
|
||||
return await WorkerBookModel.findOne({ where: { gridKey }, ...options });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 workerBooks 记录列表
|
||||
*/
|
||||
async function findAll() {
|
||||
try {
|
||||
return await WorkerBookModel.findAll();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export const WorkerBookService = {
|
||||
create,
|
||||
update,
|
||||
delete: del,
|
||||
findOne,
|
||||
findAll,
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { logger } from "../Utils/Logger";
|
||||
import {
|
||||
WorkerSheetModel,
|
||||
WorkerSheetModelType,
|
||||
} from "../Sequelize/Models/WorkerSheet";
|
||||
|
||||
/**
|
||||
* 更新相关配置
|
||||
*/
|
||||
async function update(data: WorkerSheetModelType) {
|
||||
try {
|
||||
return await WorkerSheetModel.update(data, {
|
||||
where: { worker_sheet_id: data.worker_sheet_id },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 gridkey 查找记录
|
||||
*/
|
||||
async function findAllByGridKey(gridKey: string) {
|
||||
try {
|
||||
return await WorkerSheetModel.findAll({
|
||||
// 要查找当前没有被删除的表
|
||||
where: { gridKey, deleteFlag: false },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建 sheet
|
||||
*/
|
||||
async function createSheet(data: WorkerSheetModelType) {
|
||||
try {
|
||||
return await WorkerSheetModel.create(data);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export const WorkerSheetService = {
|
||||
findAllByGridKey,
|
||||
update,
|
||||
createSheet,
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 手动实现 logger4js 日志记录功能
|
||||
*/
|
||||
import fs from "fs";
|
||||
import dayjs from "dayjs";
|
||||
import { LOGGER_CONFIG } from "../../Config";
|
||||
|
||||
type LogMessage = string | number | object | unknown;
|
||||
type LogType = "INFO" | "ERROR" | "WARN" | "DEBUG" | "SUCCESS";
|
||||
|
||||
class Logger {
|
||||
info(...argus: LogMessage[]) {
|
||||
const time = this.getTime();
|
||||
console.log("\x1b[94m %s \x1b[0m %s", `[${time}] [INFO] -`, ...argus);
|
||||
this.writeLogFile(time, "INFO", ...argus);
|
||||
}
|
||||
error(...argus: LogMessage[]) {
|
||||
const time = this.getTime();
|
||||
console.log("\x1b[31m %s \x1b[0m %s", `[${time}] [ERROR] -`, ...argus);
|
||||
this.writeLogFile(time, "ERROR", ...argus);
|
||||
}
|
||||
warn(...argus: LogMessage[]) {
|
||||
const time = this.getTime();
|
||||
console.log("\x1b[93m %s \x1b[0m %s", `[${time}] [WARN] -`, ...argus);
|
||||
this.writeLogFile(time, "WARN", ...argus);
|
||||
}
|
||||
debug(...argus: LogMessage[]) {
|
||||
const time = this.getTime();
|
||||
console.log("\x1b[90m %s \x1b[0m %s", `[${time}] [DEBUG] -`, ...argus);
|
||||
this.writeLogFile(time, "DEBUG", ...argus);
|
||||
}
|
||||
|
||||
success(...argus: LogMessage[]) {
|
||||
const time = this.getTime();
|
||||
console.log("\x1b[92m %s \x1b[0m %s", `[${time}] [SUCCESS] -`, ...argus);
|
||||
this.writeLogFile(time, "SUCCESS", ...argus);
|
||||
}
|
||||
|
||||
// 获取时间戳
|
||||
private getTime = () => dayjs().format("YYYY-MM-DD HH:MM:ss");
|
||||
|
||||
// 写入日志文件
|
||||
private writeLogFile = (
|
||||
time: string,
|
||||
type: LogType,
|
||||
...argus: LogMessage[]
|
||||
) => {
|
||||
const { filepath, filename } = LOGGER_CONFIG;
|
||||
const message = argus
|
||||
.map((i) => (typeof i === "object" ? JSON.stringify(i) : i))
|
||||
.join(" ");
|
||||
|
||||
// 保存日志文件的信息,还可以进行拓展,包括执行当前日志的用户信息等
|
||||
const data = `[${time}] [${type}] - ${message}\n`;
|
||||
|
||||
// 尝试查找目录、目录不存在,则自动创建目录
|
||||
if (!fs.existsSync(filepath)) fs.mkdirSync(filepath);
|
||||
|
||||
try {
|
||||
fs.appendFileSync(filepath + "/" + filename, data);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (error) {
|
||||
fs.writeFile(filepath + "/" + filename, data, (err) => {
|
||||
logger.error(err!.message.toString());
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const logger = new Logger();
|
||||
@@ -0,0 +1,54 @@
|
||||
import pako from "pako";
|
||||
|
||||
/**
|
||||
* Pako 数据解析
|
||||
*/
|
||||
|
||||
export function unzip(str: string): string {
|
||||
const chartData = str
|
||||
.toString()
|
||||
.split("")
|
||||
.map((i) => i.charCodeAt(0));
|
||||
|
||||
const binData = new Uint8Array(chartData);
|
||||
|
||||
const data = pako.inflate(binData);
|
||||
|
||||
// 使用 TextDecoder 替代 String.fromCharCode + Array.from 展开
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
return decodeURIComponent(decoder.decode(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 url 的某个 query 值
|
||||
* @param { string } url http://localhost:8089/?type=luckysheet&userid=1&username=userA&t=111&g=
|
||||
* @param { string } key type
|
||||
* @example type => luckysheet
|
||||
* @return { string } query 值
|
||||
*/
|
||||
export function getURLQuery(url: string | undefined, key: string) {
|
||||
if (!url) return "";
|
||||
// 通过 ? 分割
|
||||
const params = url.split("?")[1];
|
||||
if (!params || !params.includes("=")) return "";
|
||||
|
||||
/**
|
||||
* 此时,拿到的参数对象为 params ==> type=luckysheet&userid=1&username=userA&t=111&g=
|
||||
*/
|
||||
const queryArr = params.split("&");
|
||||
for (let i = 0; i < queryArr.length; i++) {
|
||||
const item = queryArr[i];
|
||||
const itemArr = item.split("=");
|
||||
if (itemArr[0] === key) {
|
||||
return itemArr[1];
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断传入的参数是否为空 null undefined ""
|
||||
*/
|
||||
export function isEmpty(val: unknown) {
|
||||
return val === null || val === undefined || val === "";
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { DB } from "../Sequelize";
|
||||
import { logger } from "../Utils/Logger";
|
||||
import { getURLQuery, unzip } from "../Utils";
|
||||
import { RawData, WebSocketServer } from "ws";
|
||||
import { IncomingMessage, Server } from "http";
|
||||
import { databaseHandler } from "./luckysheet/database";
|
||||
import { CustomWebSocket } from "../Interface/WebSocket";
|
||||
import { broadcastOtherClients } from "./luckysheet/broadcast";
|
||||
|
||||
/**
|
||||
* 创建 Web Socket 服务
|
||||
* 应该通过命名空间来处理 ai 及 luckysheet 协同消息的处理函数
|
||||
* @param port 服务端口
|
||||
*/
|
||||
export function createWebSocketServer(server: Server) {
|
||||
/** 创建WebSocketServer 实例,并监听 connection 事件 */
|
||||
const wss = new WebSocketServer({ server }).on("connection", connection);
|
||||
|
||||
/**
|
||||
* 监听 websocket 连接事件 - 主要做一些数据初始化、客户端连接对象事件注册等
|
||||
* @param client WebSocket 实例
|
||||
* @param req 请求对象
|
||||
*/
|
||||
function connection(client: CustomWebSocket, req: IncomingMessage) {
|
||||
/** 解析 request url 的参数,识别当前连接用户的 userid username gridkey type 属性 */
|
||||
const type = getURLQuery(req.url, "type");
|
||||
const userid = getURLQuery(req.url, "userid");
|
||||
const gridkey = getURLQuery(req.url, "gridkey");
|
||||
const username = getURLQuery(req.url, "username");
|
||||
|
||||
logger.info(`luckysheet 协同用户连接成功 [ID: ${userid} | NAME: ${username}].`);
|
||||
|
||||
/** 将 url 参数添加到 client 上,方便后续使用 */
|
||||
client.clientInfo = { userid, username, type, gridkey };
|
||||
|
||||
/** 监听消息 */
|
||||
client.on("message", (d) => onmessage(d, client));
|
||||
|
||||
/** 监听错误 */
|
||||
client.on("error", logger.error);
|
||||
|
||||
/** 监听关闭 code = 1000 正常关闭 */
|
||||
client.on("close", () => onclose(client));
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听 websocket 消息 - 协同消息处理函数
|
||||
* @param data
|
||||
* @param client
|
||||
*/
|
||||
function onmessage(data: RawData, client: CustomWebSocket) {
|
||||
// Luckysheet 协同消息处理函数
|
||||
try {
|
||||
// 1. 进行 pako 解压,将 buffer 转成 可识别 json 字符串
|
||||
const data_str = unzip(data.toString());
|
||||
|
||||
// 2. 用户每次编辑,都会触发 message 事件,因此,在这里实现协同数据存储
|
||||
if (DB.getConnectState()) databaseHandler(data_str, client.clientInfo.gridkey);
|
||||
|
||||
// 3. 广播给 wss.clients 其他客户端 - 所有的权限校验、文件ID 等验证,均在 broadcastOtherClients 函数中处理
|
||||
broadcastOtherClients(wss, client, data_str);
|
||||
} catch (error) {
|
||||
// 4. 捕获异常 判断是否为心跳包消息,心跳不处理,异常信息则记录日志
|
||||
if (data.toString() !== "rub") logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听 websocket 的 close 事件
|
||||
* @param client
|
||||
*/
|
||||
function onclose(client: CustomWebSocket) {
|
||||
broadcastOtherClients(wss, client, "exit");
|
||||
logger.warn("luckysheet 协同用户关闭连接");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 处理广播给其他客户端事件,客户端接收服务端要求数据结构:
|
||||
*
|
||||
* data: 修改的命令
|
||||
* id: "7a" websocket的id
|
||||
* username: 用户名(用于显示 xxx 正在编辑)
|
||||
* type:
|
||||
* message === '用户退出' 用户退出时,关闭协同编辑时其提示框
|
||||
* type == 1 send 成功或失败
|
||||
* type == 2 更新数据
|
||||
* type == 3 多人操作不同选区("t": "mv")(用不同颜色显示其他人所操作的选区)
|
||||
* type == 4 批量指令更新
|
||||
* type == 5 showloading
|
||||
* type == 6 hideloading
|
||||
*/
|
||||
|
||||
import WebSocket from "ws";
|
||||
import { CustomWebSocket } from "../../Interface/WebSocket";
|
||||
|
||||
/**
|
||||
* 广播给其他客户端
|
||||
* @param wss
|
||||
* @param currentClient
|
||||
* @param data
|
||||
*/
|
||||
export function broadcastOtherClients(wss: WebSocket.Server, currentClient: CustomWebSocket, data: string) {
|
||||
/** 根据传入的 data 进行协同回传消息类型处理 */
|
||||
const callback = getCallbackData(currentClient, data);
|
||||
|
||||
/**
|
||||
* 请注意,此处应该做校验:
|
||||
* 1. 协同类型是否为 luckysheet(同一个协同服务类型才协同)
|
||||
* 2. 当前用户的文件ID(使用 gridkey 替代 fileid) 是否一致(同一篇文档的用户才协同)
|
||||
* 3. 当前用户的 userid 是否一致(同一用户不发送消息)
|
||||
*/
|
||||
wss.clients.forEach((conn) => {
|
||||
const { type, gridkey, userid } = (<CustomWebSocket>conn).clientInfo;
|
||||
if (type !== "luckysheet") return;
|
||||
if (gridkey !== currentClient.clientInfo.gridkey) return;
|
||||
if (userid === currentClient.clientInfo.userid) return;
|
||||
// 校验通过,发送消息给客户端
|
||||
if (conn.readyState === WebSocket.OPEN) conn.send(callback);
|
||||
});
|
||||
}
|
||||
|
||||
function getCallbackData(currentClient: CustomWebSocket, data: string): string {
|
||||
const { userid, username } = currentClient.clientInfo;
|
||||
if (data === "exit") return JSON.stringify({ message: "用户退出", id: userid });
|
||||
|
||||
// 不然就正常封装,根据data的t识别不同的 type
|
||||
const { t } = JSON.parse(data);
|
||||
const info = { data, id: userid, username: decodeURIComponent(username), type: t === "mv" ? 3 : 2 };
|
||||
return JSON.stringify(info);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { CRDTDataType, MERGE } from "../../../Interface/WebSocket";
|
||||
import { HiddenAndLenModelType } from "../../../Sequelize/Models/HiddenAndLen";
|
||||
import { WorkerSheetModelType } from "../../../Sequelize/Models/WorkerSheet";
|
||||
import { HiddenAndLenService } from "../../../Service/HiddenAndLen";
|
||||
import { ImageService } from "../../../Service/Image";
|
||||
import { MergeService } from "../../../Service/Merge";
|
||||
import { WorkerSheetService } from "../../../Service/WorkerSheet";
|
||||
import { isEmpty } from "../../../Utils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
/**
|
||||
* 通用保存 冻结行列、修改工作表名、修改工作表颜色、合并单元格、筛选等操作,
|
||||
* 此方法仅处理 修改工作表名 合并单元格
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export async function all(data: string) {
|
||||
logger.info("[CRDT DATA]:", data);
|
||||
|
||||
const { t, i, v, k } = <CRDTDataType<MERGE>>JSON.parse(data);
|
||||
|
||||
if (t !== "all") return logger.error("t is not all.");
|
||||
if (isEmpty(i)) return logger.error("i is undefined.");
|
||||
|
||||
// 修改工作表名
|
||||
// {"t":"all","i":"12f8254d-3914-4f79-9886-9f9aec173048","v":"123","k":"name"}
|
||||
if (k === "name") {
|
||||
const info = <WorkerSheetModelType>{
|
||||
worker_sheet_id: i,
|
||||
name: <string>(<unknown>v),
|
||||
};
|
||||
await WorkerSheetService.update(info);
|
||||
} else if (k === "config") {
|
||||
// 合并单元格 - 又是一个先删除后新增的操作,由luckysheet 前台设计决定的
|
||||
// {"t":"all","i":"e73f971....","v":{"merge":{"1_0":{"r":1,"c":0,"rs":3,"cs":3}},},"k":"config"}
|
||||
// {"t":"all","i":"e73f971....","v":{"merge":{"1_0":{"r":1,"c":0,"rs":3,"cs":3},"9_1":{"r":9,"c":1,"rs":5,"cs":3}},},"k":"config"}
|
||||
// {"t":"all","i":"e73f971....","v":{"merge":{"9_1":{"r":9,"c":1,"rs":5,"cs":3}},},"k":"config"}
|
||||
|
||||
// 如果当前 sheet 没有 merge 属性,则表示最后一个合并单元格被取消,则删除当前 sheetindex 下的所有 merge 记录
|
||||
// 删除当前 sheet Index 下的所有merge 记录
|
||||
await MergeService.deleteMerge(i);
|
||||
// 如果有记录 再新增
|
||||
if (v && v.merge && Object.keys(v.merge).length) {
|
||||
// 再新增
|
||||
for (const key in v.merge) {
|
||||
if (Object.prototype.hasOwnProperty.call(v.merge, key)) {
|
||||
const { r, c, rs, cs } = v.merge[key];
|
||||
await MergeService.createMerge({
|
||||
worker_sheet_id: i,
|
||||
r,
|
||||
c,
|
||||
rs,
|
||||
cs,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 文本换行处理方式 - 自动换行是通过自定义行高实现的,这部分也需要存储哈
|
||||
// {"t":"all","i":"28a60885-46e3-4f59-9d25-442a30fdbba6","v":{"merge":{},"rowhidden":{},"colhidden":{},"borderInfo":[],"rowlen":{"10":63.5},"columnlen":{},"customHeight":{}},"k":"config"}
|
||||
if (v && v.rowlen && Object.keys(v.rowlen).length) {
|
||||
// "rowlen":{"10":63.5},
|
||||
for (const key in v.rowlen) {
|
||||
if (Object.prototype.hasOwnProperty.call(v.rowlen, key)) {
|
||||
const value = Number(v.rowlen[key]);
|
||||
// 此时 key 是行号,value 是行高
|
||||
const configInfo: HiddenAndLenModelType = {
|
||||
worker_sheet_id: i,
|
||||
config_index: key,
|
||||
config_type: "rowlen",
|
||||
config_value: value,
|
||||
};
|
||||
await HiddenAndLenService.create(configInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (k === "images") {
|
||||
// {"t":"all","i":"4735b996-d89d-4d7f-ad8e-1124bccc89b0","v":{
|
||||
// "img_3aaW3S653e10_1733991741264":{"type":"3","src":"/uploads/36399c90241d782399d05acd9dfb1d9d.png","originWidth":685,"originHeight":490,"default":{"width":400,"height":286,"left":18,"top":229},"crop":{"width":400,"height":286,"offsetLeft":0,"offsetTop":0},"isFixedPos":false,"fixedLeft":46,"fixedTop":90,"border":{"width":0,"radius":0,"style":"solid","color":"#000"}},
|
||||
// "img_eeeKop3oTl3a_1733991749199":{"type":"3","src":"/uploads/31ae8c6088f50c267a09b00b3555d787.png","originWidth":685,"originHeight":386,"default":{"width":400,"height":225,"left":0,"top":171},"crop":{"width":400,"height":225,"offsetLeft":0,"offsetTop":0},"isFixedPos":false,"fixedLeft":46,"fixedTop":90,"border":{"width":0,"radius":0,"style":"solid","color":"#000"}}
|
||||
// },"k":"images"}
|
||||
/* eslint-disable */
|
||||
// 又是一个先删除后新增的操作
|
||||
await ImageService.deleteImage(i);
|
||||
for (const key in v) {
|
||||
if (Object.prototype.hasOwnProperty.call(v, key)) {
|
||||
// @ts-ignore
|
||||
const value = v[key];
|
||||
// 解析 value 值
|
||||
await ImageService.createImage({
|
||||
image_key: key,
|
||||
worker_sheet_id: i,
|
||||
image_type: value.type, // type 1移动并调整单元格大小 2移动并且不调整单元格的大小 3不要移动单元格并调整其大小
|
||||
image_src: value.src, // 图片地址
|
||||
in_cell: value.inCell || "", // 所在单元格位置
|
||||
image_originWidth: value.originWidth, // 原始宽度
|
||||
image_originHeight: value.originHeight, // 原始高度
|
||||
image_default_width: value.default.width, // 默认宽度
|
||||
image_default_height: value.default.height, // 默认高度
|
||||
image_default_left: value.default.left, // 默认左边距
|
||||
image_default_top: value.default.top, // 默认上边距
|
||||
image_crop_width: value.crop.width, // 裁剪宽度
|
||||
image_crop_height: value.crop.height, // 裁剪高度
|
||||
image_crop_offsetLeft: value.crop.offsetLeft, // 裁剪左边距
|
||||
image_crop_offsetTop: value.crop.offsetTop, // 裁剪上边距
|
||||
image_isFixedPos: value.isFixedPos, // 是否固定位置
|
||||
image_fixedLeft: value.fixedLeft, // 固定左边距
|
||||
image_fixedTop: value.fixedTop, // 固定上边距
|
||||
image_border_width: value.border.width, // 边框宽度
|
||||
image_border_radius: value.border.radius, // 圆角
|
||||
image_border_style: value.border.style, // 边框样式
|
||||
image_border_color: value.border.color, // 边框颜色
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (k === "color") {
|
||||
// {"t":"all","i":"62e09f1d-b294-46b5-8924-a0f1a8e011a2","v":"#ffff00","k":"color"}
|
||||
// 更新 worker sheet color 字段即可
|
||||
const color_info = <WorkerSheetModelType>{
|
||||
worker_sheet_id: i,
|
||||
color: <string>(<unknown>v),
|
||||
};
|
||||
await WorkerSheetService.update(color_info);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// 增加行或列 - 会影响 celldata r c 的值,需要更新比新增行列大/小的 r c 值
|
||||
|
||||
import { CRDTDataType, ARC } from "../../../Interface/WebSocket";
|
||||
import { CellDataModelType } from "../../../Sequelize/Models/CellData";
|
||||
import { CellDataService } from "../../../Service/CellData";
|
||||
import { isEmpty } from "../../../Utils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
// 撤销删除行列时,也会触发该事件,并且携带 data 字段
|
||||
export async function arc(data: string) {
|
||||
logger.info("[CRDT DATA]:", data);
|
||||
|
||||
const { t, i, v, rc } = <CRDTDataType<ARC>>JSON.parse(data);
|
||||
|
||||
if (t !== "arc") return logger.error("t is not arc.");
|
||||
if (isEmpty(i)) return logger.error("i is undefined.");
|
||||
|
||||
// 如果rc的值是r新增行,
|
||||
// 如果rc的值为c则新增列,
|
||||
// 例如rc=r,index=4,len=5, 则代表从第4行开始增加5行,
|
||||
|
||||
// direction 标识添加行列的方向 lefttop 上方/左方添加 rightbottom 下方/右方添加
|
||||
|
||||
// 无数据示例(一般是新增空白行列)
|
||||
// {"t":"arc","i":"2b62e1f2-7f7f-4889-b34d-007fe7277364","v":{"index":0,"len":1,"direction":"rightbottom","data":[]},"rc":"r"}
|
||||
|
||||
// 有数据示例(一般是撤销删除行列时,会携带数据):
|
||||
// {"t":"arc","i":"2b62e1f2-7f7f-4889-b34d-007fe7277364","v":{"index":3,"len":1,"direction":"lefttop","data":[[null,
|
||||
// {"ct":{"fa":"General","t":"n"},"v":"333","m":"333","bg":"#FFFFFF","ff":"5","fc":"#000000","bl":false,"it":false,"fs":10,"cl":false,"ht":0,"vt":0,"f":null,"un":false},
|
||||
// ... 没值部分均为 null ...,null]]},"rc":"r"}
|
||||
// {"t":"arc","i":"2b62e1f2-7f7f-4889-b34d-007fe7277364",
|
||||
// "v":{"index":6,"len":3,"direction":"lefttop",
|
||||
// "data":[
|
||||
// [null,null,null,{"v":"1","ct":{"fa":"General","t":"n"},"m":"1"},null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
|
||||
// [null,null,null,{"v":"2","ct":{"fa":"General","t":"n"},"m":"2"},null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
|
||||
// [null,null,null,{"v":"3","ct":{"fa":"General","t":"n"},"m":"3"},null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]]},"rc":"r"}
|
||||
|
||||
// 1. 先新增行列 - 先处理 celldata r c 的关系
|
||||
await CellDataService.addCellDataRC({
|
||||
worker_sheet_id: i,
|
||||
index: v.index,
|
||||
len: v.len,
|
||||
update_type: <"r" | "c">rc,
|
||||
});
|
||||
|
||||
// 2. 后处理 data - 如果是撤销的话,需要新增 celldata 记录
|
||||
if (!v.data.length) return;
|
||||
|
||||
for (let k = 0; k < v.data.length; k++) {
|
||||
const item = v.data[k]; // 同时操作的可能有多列,因此这个item也是个数组,并且是 cellDataTypeItem []
|
||||
for (let j = 0; j < item.length; j++) {
|
||||
const cellItem = item[j];
|
||||
if (cellItem === null) continue;
|
||||
|
||||
let r = 0;
|
||||
let c = 0;
|
||||
// 注意撤销后的 r/c 取值
|
||||
if (rc === "r") {
|
||||
r = v.index + k;
|
||||
c = j;
|
||||
} else if (rc === "c") {
|
||||
r = k;
|
||||
c = v.index + j;
|
||||
}
|
||||
// 不然 执行celldata 的插入操作
|
||||
const celldata: CellDataModelType = {
|
||||
worker_sheet_id: i,
|
||||
...cellItem,
|
||||
r,
|
||||
c, // 注意撤销后的 r/c 取值
|
||||
m: <string>cellItem.m,
|
||||
v: <string>cellItem.v,
|
||||
ctfa: <string>cellItem?.ct?.fa,
|
||||
ctt: <string>cellItem?.ct?.t,
|
||||
ps: <string>cellItem?.ps?.value,
|
||||
};
|
||||
await CellDataService.createCellData(celldata);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { CRDTDataType, CHART } from "../../../Interface/WebSocket";
|
||||
import { ChartService } from "../../../Service/Chart";
|
||||
import { isEmpty } from "../../../Utils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
// 图表操作
|
||||
export async function c(data: string) {
|
||||
logger.info("[CRDT DATA]:", data);
|
||||
const { t, v, i, op } = <CRDTDataType<CHART>>JSON.parse(data);
|
||||
if (t !== "c") return logger.error("t is not c.");
|
||||
if (isEmpty(i)) return logger.error("i is undefined.");
|
||||
// 所有的图表ID均由前台传递
|
||||
// 创建图表
|
||||
const chartInfo = {
|
||||
chartType: v.chartType,
|
||||
worker_sheet_id: i,
|
||||
chart_id: v.chart_id,
|
||||
width: v.width,
|
||||
height: v.height,
|
||||
left: v.left,
|
||||
top: v.top,
|
||||
needRangeShow: v.needRangeShow,
|
||||
chartOptions: JSON.stringify(v.chartOptions),
|
||||
};
|
||||
if (op === "add") {
|
||||
await ChartService.createChart(chartInfo);
|
||||
}
|
||||
|
||||
// 更新图表
|
||||
// {"t":"c","i":"89357e56-c6bc-4de0-bfd1-0e00b3086da4","v":{"chart_id":"chart_01ieK40e4Kal_1734335434241","left":"172.3px","top":"158.3px","scrollTop":0,"scrollLeft":0},"cid":"chart_01ieK40e4Kal_1734335434241","op":"xy"}
|
||||
else if (op === "xy" || op === "wh") {
|
||||
await ChartService.updateChart({
|
||||
chartType: v.chartType,
|
||||
worker_sheet_id: i,
|
||||
chart_id: v.chart_id,
|
||||
left: v.left,
|
||||
top: v.top,
|
||||
width: v.width,
|
||||
height: v.height,
|
||||
});
|
||||
}
|
||||
|
||||
// 更新图表配置
|
||||
else if (op === "update") {
|
||||
await ChartService.updateChart({
|
||||
chartType: v.chartType,
|
||||
worker_sheet_id: i,
|
||||
chart_id: v.chart_id,
|
||||
chartOptions: JSON.stringify(v.chartOptions),
|
||||
});
|
||||
}
|
||||
|
||||
// 删除图表
|
||||
// {"t":"c","i":"89357e56-c6bc-4de0-bfd1-0e00b3086da4","v":{"cid":"chart_WW0t3io1towN_1734335743092"},"cid":"chart_WW0t3io1towN_1734335743092","op":"del"}
|
||||
else if (op === "del") {
|
||||
await ChartService.deleteChart(v.chart_id);
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// 图表更新单元格数据 update_data
|
||||
// {"t":"c","i":0,"v":{"r_st":1,"r_ed":1,"c_st":1,"c_ed":1,chart_id:"",chartOptions:""},"op":"update_data"}
|
||||
else if (op === "update_data") {
|
||||
// 更新 ChartModel options 配置项
|
||||
await ChartService.updateChart({
|
||||
chartType: v.chartType,
|
||||
worker_sheet_id: i,
|
||||
chart_id: v.chart_id,
|
||||
chartOptions: JSON.stringify(v.chartOptions),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { CRDTDataType, CG } from "../../../Interface/WebSocket";
|
||||
import { BorderInfoModelType } from "../../../Sequelize/Models/BorderInfo";
|
||||
import { HiddenAndLenModelType } from "../../../Sequelize/Models/HiddenAndLen";
|
||||
import { BorderInfoService } from "../../../Service/Border";
|
||||
import { HiddenAndLenService } from "../../../Service/HiddenAndLen";
|
||||
import { isEmpty } from "../../../Utils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
// config操作
|
||||
export async function cg(data: string) {
|
||||
logger.info("[CRDT DATA]:", data);
|
||||
const { t, i, v, k } = <CRDTDataType<CG>>JSON.parse(data);
|
||||
|
||||
if (t !== "cg") return logger.error("t is not cg.");
|
||||
if (isEmpty(i)) return logger.error("i is undefined.");
|
||||
|
||||
// k: 操作的key值,边框:'borderInfo' / :行隐藏:'rowhidden' / 列隐藏:'colhidden' / 行高:'rowlen' / 列宽:'columnlen'
|
||||
|
||||
// 行隐藏/列隐藏 统一处理
|
||||
// { "t": "cg", "i": "Sheet_0554kKiKl4M7_1597974810804", "v": { "5": 0, "6": 0, "13": 0, "14": 0 }, "k": "rowhidden"}
|
||||
|
||||
// 修改行高列宽
|
||||
// {"t":"cg","i":"e73f971d-606f-4b04-bcf1-98550940e8e3","v":{"4":100},"k":"rowlen"}
|
||||
|
||||
// 取消隐藏行
|
||||
// {"t":"cg","i":"Sheet_13073il53h5h_1757404832048","v":{},"k":"colhidden"}
|
||||
|
||||
if (k === "rowhidden" || k === "colhidden" || k === "rowlen" || k === "columnlen") {
|
||||
// 因为前台发送的全量数据,因此,每次执行,都需要先删除后添加
|
||||
// {"t":"cg","i":"e73f971d-606f-4b04-bcf1-98550940e8e3","v":{"7":0,"8":0,"9":0},"k":"rowhidden"}
|
||||
// {"t":"cg","i":"e73f971d-606f-4b04-bcf1-98550940e8e3","v":{},"k":"rowhidden"}
|
||||
await HiddenAndLenService.deleteHidden(i, k);
|
||||
for (const key in v) {
|
||||
if (Object.prototype.hasOwnProperty.call(v, key)) {
|
||||
const value = Number(v[key]);
|
||||
// 判断具体是 行还是列
|
||||
const configInfo: HiddenAndLenModelType = {
|
||||
worker_sheet_id: i,
|
||||
config_index: key,
|
||||
config_type: k,
|
||||
config_value: value,
|
||||
};
|
||||
|
||||
await HiddenAndLenService.create(configInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 场景三: k borderInfo 边框处理
|
||||
// {"t":"cg","i":"e73f971d606...","v":[{"rangeType":"range","borderType":"border-all","color":"#000","style":"1","range":[{"row":[0,0],"column":[0,0],"row_focus":0,"column_focus":0,"left":0,"width":73,"top":0,"height":19,"left_move":0,"width_move":73,"top_move":0,"height_move":19}]}],"k":"borderInfo"}
|
||||
// {"t":"cg","i":"e73f971d......","v":[{"rangeType":"range","borderType":"border-all","color":"#000","style":"1","range":[{"row":[2,7],"column":[1,2],"row_focus":2,"column_focus":1,"left":74,"width":73,"top":40,"height":19,"left_move":74,"width_move":147,"top_move":40,"height_move":119,}]}],"k":"borderInfo"}
|
||||
// {"t":"cg","i":"e73f971d......","v":[{"rangeType":"range","borderType":"border-bottom","color":"#000","style":"1","range":[{"left":148,"width":73,"top":260,"height":19,"left_move":148,"width_move":73,"top_move":260,"height_move":19,"row":[13,13],"column":[2,2],"row_focus":13,"column_focus":2}]}],"k":"borderInfo"}
|
||||
|
||||
// 20250827 新增修复BUG:边框[无] 时,刷新异常BUG 导致该BUG的原因: 传入的数据类型,并非顺序的 all none
|
||||
/**
|
||||
* { "rangeType": "range", "borderType": "border-all", "color": "#000", "range": [{ "row": [2, 14], "column": [3, 6] }] },
|
||||
* { "rangeType": "range", "borderType": "border-all", "color": "#000", "range": [{ "row": [8, 16], "column": [6, 8] }] },
|
||||
* { "rangeType": "range", "borderType": "border-none", "color": "#000", "range": [{ "row": [2, 14], "column": [3, 6] }] },
|
||||
* { "rangeType": "range", "borderType": "border-all", "color": "#000", "range": [{ "row": [2, 8], "column": [2, 4] }] },
|
||||
* { "rangeType": "range", "borderType": "border-none", "color": "#000", "range": [{ "row": [4, 13], "column": [5, 7] }] },
|
||||
* { "rangeType": "range", "borderType": "border-all", "color": "#000", "range": [{ "row": [4, 13], "column": [5, 7] }] },
|
||||
* { "rangeType": "range", "borderType": "border-none", "color": "#000", "range": [{ "row": [8, 16], "column": [6, 8] }] },
|
||||
* { "rangeType": "range", "borderType": "border-none", "color": "#000", "range": [{ "row": [2, 8], "column": [2, 4] }] },
|
||||
* { "rangeType": "range", "borderType": "border-none", "color": "#000", "range": [{ "row": [4, 13], "column": [5, 7] }] }
|
||||
*/
|
||||
else if (k === "borderInfo") {
|
||||
// 处理 rangeType
|
||||
for (let idx = 0; idx < v.length; idx++) {
|
||||
const border = v[idx];
|
||||
const { rangeType, borderType, color, style, range } = border;
|
||||
|
||||
// 用 i row column 及 borderType === 'border-none' 删除即可,需要添加 border-none 的记录
|
||||
// 如果是其他的 borderType,添加的时候,都需要先检查是否存在 border-none,如果存在,则不处理
|
||||
|
||||
const info: BorderInfoModelType = {
|
||||
worker_sheet_id: i,
|
||||
rangeType,
|
||||
borderType,
|
||||
row_start: range[0].row[0],
|
||||
row_end: range[0].row[1],
|
||||
col_start: range[0].column[0],
|
||||
col_end: range[0].column[1],
|
||||
};
|
||||
const exist = await BorderInfoService.hasConfigBorder(info);
|
||||
if (exist) {
|
||||
// 更新
|
||||
await BorderInfoService.updateConfigBorder({
|
||||
config_border_id: exist.config_border_id,
|
||||
...info,
|
||||
color,
|
||||
style: Number(style),
|
||||
});
|
||||
} else {
|
||||
// 创建新的边框记录
|
||||
await BorderInfoService.createConfigBorder({
|
||||
...info,
|
||||
style: Number(style),
|
||||
color,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// 删除的该行,可能会引起其他的一些变化,因此,也会触发 all 事件类型
|
||||
// {"t":"all","i":"2b62e1f2-7f7f-4889-b34d-007fe7277364","v":[],"k":"calcChain"}
|
||||
// {"t":"all","i":"2b62e1f2-7f7f-4889-b34d-007fe7277364","v":null,"k":"filter_select"}
|
||||
// {"t":"all","i":"2b62e1f2-7f7f-4889-b34d-007fe7277364","v":null,"k":"filter"}
|
||||
// {"t":"all","i":"2b62e1f2-7f7f-4889-b34d-007fe7277364","v":[],"k":"luckysheet_conditionformat_save"}
|
||||
// {"t":"all","i":"2b62e1f2-7f7f-4889-b34d-007fe7277364","v":[],"k":"luckysheet_alternateformat_save"}
|
||||
// {"t":"all","i":"2b62e1f2-7f7f-4889-b34d-007fe7277364","v":{},"k":"dataVerification"}
|
||||
// {"t":"all","i":"2b62e1f2-7f7f-4889-b34d-007fe7277364","v":{},"k":"hyperlink"}
|
||||
|
||||
import { CRDTDataType, DRC } from "../../../Interface/WebSocket";
|
||||
import { CellDataService } from "../../../Service/CellData";
|
||||
import { HiddenAndLenService } from "../../../Service/HiddenAndLen";
|
||||
import { isEmpty } from "../../../Utils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
// 删除行或列 - 会影响 celldata r c 的值,需要更新比新增行列大/小的 r c 值
|
||||
export async function drc(data: string) {
|
||||
logger.info("[CRDT DATA]:", data);
|
||||
|
||||
const { t, i, v, rc } = <CRDTDataType<DRC>>JSON.parse(data);
|
||||
|
||||
if (t !== "drc") return logger.error("t is not drc.");
|
||||
if (isEmpty(i)) return logger.error("i is undefined.");
|
||||
|
||||
// {"t":"drc","i":"2b62e1f2-7f7f-4889-b34d-007fe7277364","v":{"index":5,"len":1},"rc":"r"}
|
||||
// {"t":"drc","i":"2b62e1f2-7f7f-4889-b34d-007fe7277364","v":{"index":1,"len":5},"rc":"r"}
|
||||
// {"t":"drc","i":"2b62e1f2-7f7f-4889-b34d-007fe7277364","v":{"index":7,"len":3},"rc":"c"}
|
||||
|
||||
// TODO: 删除该列触发的 all 事件
|
||||
|
||||
// 删除行,则删除该行所有的列数据
|
||||
await CellDataService.deleteCellDataRC(i, v.index, <"r" | "c">rc);
|
||||
|
||||
// 通过 index len 来实现标记 从那里开始删除、删除多少行
|
||||
// 删除的索引 index 小的,不需要处理,只需要将 比 索引大的 记录 减小 len 即可
|
||||
await CellDataService.updateCellDataRC({
|
||||
worker_sheet_id: i,
|
||||
index: v.index,
|
||||
len: v.len,
|
||||
update_type: <"r" | "c">rc,
|
||||
});
|
||||
|
||||
// 删除该列的 hide and len
|
||||
await HiddenAndLenService.deleteRC(i, v.index.toString());
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { CRDTDataType } from "../../../Interface/WebSocket";
|
||||
import { CalcChainService } from "../../../Service/CalcChain";
|
||||
import { isEmpty } from "../../../Utils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
// 函数链操作 r c index func color parent children times
|
||||
export async function fc(data: string) {
|
||||
// console.log("==> fc", data);
|
||||
// https://dream-num.github.io/LuckysheetDocs/zh/guide/operate.html#%E5%87%BD%E6%95%B0%E9%93%BE%E6%93%8D%E4%BD%9C
|
||||
// fc {"t":"fc","i":"4298e271-548d-4207-a826-ecececceef6f","v":"{\"r\":0,\"c\":2,\"index\":\"4298e271-548d-4207-a826-ecececceef6f\"}","op":"add","pos":0}
|
||||
// {"t":"fc","i":"4298e271-548d-4207-a826-ecececceef6f","v":"{\"r\":6,\"c\":2,\"index\":\"4298e271-548d-4207-a826-ecececceef6f\",\"func\":[true,122,\"=A7+B7\"]}","op":"add","pos":0}
|
||||
logger.info("[CRDT DATA]:", data);
|
||||
const { t, i, v, op } = <CRDTDataType<string>>JSON.parse(data);
|
||||
if (t !== "fc") return logger.error("t is not fc.");
|
||||
if (isEmpty(i)) return logger.error("i is undefined.");
|
||||
|
||||
// 请注意,此处的 v 是字符串
|
||||
const item = JSON.parse(v);
|
||||
const { r, c, index, func } = item;
|
||||
|
||||
// 如果是 del 直接删除记录
|
||||
// {"t":"fc","i":"61f708fc-b159-4950-afab-176a81f4e1f6","v":null,"op":"del","pos":0}
|
||||
if (op === "del") {
|
||||
await CalcChainService.deleteCalcChain(index, r, c);
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断是否存在该记录
|
||||
const calcChain = await CalcChainService.findOne(index, r, c);
|
||||
// 存在则更新,不存在则创建
|
||||
if (calcChain) {
|
||||
await CalcChainService.update(calcChain.calcchain_id!, JSON.stringify(func));
|
||||
} else {
|
||||
await CalcChainService.create({
|
||||
worker_sheet_id: index,
|
||||
r,
|
||||
c,
|
||||
func: JSON.stringify(func),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { v } from "./v";
|
||||
import { rv } from "./rv";
|
||||
import { cg } from "./cg";
|
||||
import { all } from "./all";
|
||||
import { fc } from "./fc";
|
||||
import { drc } from "./drc";
|
||||
import { arc } from "./arc";
|
||||
import { shd } from "./shd";
|
||||
import { shre } from "./shre";
|
||||
import { shr } from "./shr";
|
||||
import { c } from "./c";
|
||||
import { sh } from "./sh";
|
||||
import { na } from "./na";
|
||||
import { sha } from "./sha";
|
||||
import { shc } from "./shc";
|
||||
|
||||
/**
|
||||
* 处理协同数据存储: 更多操作请参考: https://dream-num.github.io/LuckysheetDocs/zh/guide/operate.html
|
||||
* 1. 单个单元格刷新 "t": "v",
|
||||
* 2. 范围单元格刷新 "t": "rv"
|
||||
* 3. config操作 "t": "cg",
|
||||
* 4. 通用保存 "t": "all",
|
||||
* 5. 函数链操作 "t": "fc",
|
||||
* 6. 行列操作
|
||||
* - 删除行或列 "t": "drc",
|
||||
* - 增加行或列 "t": "arc",
|
||||
* 7. 筛选操作
|
||||
* - 清除筛选 "t": "fsc",
|
||||
* - 恢复筛选 "t": "fsr",
|
||||
* 8. sheet操作
|
||||
* - 新建sheet "t": "sha",
|
||||
* - 复制sheet "t": "shc",
|
||||
* - 删除sheet "t": "shd",
|
||||
* - 删除sheet后恢复操作 "t": "shre",
|
||||
* - 调整sheet位置 "t": "shr",
|
||||
* - 切换到指定sheet "t": "shs",
|
||||
* 9. sheet属性(隐藏或显示) "t": "sh", ==> "op": "show" / "hide"
|
||||
* 10. 表格信息更改
|
||||
* - 修改工作簿名称 "t": "na",
|
||||
* 11. 图表(TODO)
|
||||
*
|
||||
* 注意一点,对象中的i为当前sheet的index值,而不是order
|
||||
*/
|
||||
|
||||
/**
|
||||
* 协同消息映射的操作
|
||||
* @param data
|
||||
*/
|
||||
export function databaseHandler(data: string, gridKey: string) {
|
||||
const { t } = JSON.parse(data);
|
||||
if (t === "v") v(data);
|
||||
else if (t === "rv") rv(data);
|
||||
else if (t === "cg") cg(data);
|
||||
else if (t === "all") all(data);
|
||||
else if (t === "fc") fc(data);
|
||||
else if (t === "drc") drc(data);
|
||||
else if (t === "arc") arc(data);
|
||||
else if (t === "fsc") fsc(data);
|
||||
else if (t === "fsr") fsr(data);
|
||||
else if (t === "sha") sha(data, gridKey);
|
||||
else if (t === "shc") shc(data, gridKey);
|
||||
else if (t === "shd") shd(data);
|
||||
else if (t === "shre") shre(data);
|
||||
else if (t === "shr") shr(data);
|
||||
else if (t === "c") c(data);
|
||||
// else if (t === "shs") shs(data); // 切换到指定 sheet 是前台操作,可不存储数据库
|
||||
else if (t === "sh") sh(data);
|
||||
else if (t === "na") na(data, gridKey);
|
||||
}
|
||||
|
||||
// 清除筛选
|
||||
async function fsc(data: string) {
|
||||
console.log("==> fsc", data);
|
||||
}
|
||||
|
||||
// 恢复筛选
|
||||
async function fsr(data: string) {
|
||||
console.log("==> fsr", data);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { CRDTDataType } from "../../../Interface/WebSocket";
|
||||
import { WorkerBookService } from "../../../Service/WorkerBook";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
// 修改工作簿名称
|
||||
export async function na(data: string, gridKey: string) {
|
||||
// {"t":"na","i":null,"v":"Luckysheet Demo222"}
|
||||
logger.info("[CRDT DATA]:", data);
|
||||
const { t, v } = <CRDTDataType<string>>JSON.parse(data);
|
||||
if (t !== "na") return logger.error("t is not na.");
|
||||
|
||||
// 更新 workerBook name 属性即可 gridkey 在用户身上
|
||||
await WorkerBookService.update({ title: v, gridKey });
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { CRDTDataType, RV } from "../../../Interface/WebSocket";
|
||||
import { CellDataService } from "../../../Service/CellData";
|
||||
import { isEmpty } from "../../../Utils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
/**
|
||||
* 范围单元格刷新 - 与 单个单元格刷新的场景一致,也存在 复制粘贴 删除多个单元格 v m =null 的场景,都需要做区分
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export async function rv(data: string) {
|
||||
/**
|
||||
* 范围单元格刷新
|
||||
* 需要先取 range 范围行列数,v 的内容是根据 range 循环而来
|
||||
*/
|
||||
const { t, i, v, range } = <CRDTDataType<RV>>JSON.parse(data);
|
||||
if (t !== "rv") return logger.error("t is not rv.");
|
||||
if (isEmpty(i)) return logger.error("i is undefined.");
|
||||
if (isEmpty(range)) return logger.error("range is undefined.");
|
||||
if (isEmpty(v)) return logger.error("v is undefined.");
|
||||
|
||||
logger.info("[CRDT DATA]:", data);
|
||||
|
||||
// 范围添加背景颜色存储失败
|
||||
// {"t":"rv","i":"25a110fd-8e06-4318-a2d2-c42606b266ee","v":[[{"v":null,"bg":"#f19594"},{"v":null,"bg":"#f19594"}],[{"v":null,"bg":"#f19594"},{"v":null,"bg":"#f19594"}]],"range":{"row":[10,11],"column":[9,10]}}
|
||||
|
||||
// {"t":"rv","i":"2b62e1f2-7f7f-4889-b34d-007fe7277364",
|
||||
// "v":[
|
||||
// [{"ct":{"fa":"General","t":"g"},"v":"B","m":"B","bg":"#FFFFFF","ff":"5","fc":"#000000","bl":false,"it":false,"fs":10,"cl":false,"ht":0,"vt":0,"f":null,"un":false},
|
||||
// {"ct":{"fa":"General","t":"n"},"v":"111","m":"111","bg":"#FFFFFF","ff":"5","fc":"#000000","bl":false,"it":false,"fs":10,"cl":false,"ht":0,"vt":0,"f":null,"un":false}
|
||||
// ],
|
||||
// [{"ct":{"fa":"General","t":"g"},"v":"C","m":"C","bg":"#FFFFFF","ff":"5","fc":"#000000","bl":false,"it":false,"fs":10,"cl":false,"ht":0,"vt":0,"f":null,"un":false},
|
||||
// {"ct":{"fa":"General","t":"n"},"v":"111","m":"111","bg":"#FFFFFF","ff":"5","fc":"#000000","bl":false,"it":false,"fs":10,"cl":false,"ht":0,"vt":0,"f":null,"un":false}
|
||||
// ]
|
||||
// ],"range":{"row":[5,6],"column":[1,2]}}
|
||||
// {"t":"rv","i":"2b62e1f2-...",
|
||||
// "v":[
|
||||
// [{"ct":{"fa":"General","t":"g"},"v":"B","m":"B","bg":"#FFFFFF","ff":"5","fc":"#000000","bl":false,"it":false,"fs":10,"cl":false,"ht":0,"vt":0,"f":null,"un":false},
|
||||
// {"ct":{"fa":"General","t":"n"},"v":"111","m":"111","bg":"#FFFFFF","ff":"5","fc":"#000000","bl":false,"it":false,"fs":10,"cl":false,"ht":0,"vt":0,"f":null,"un":false}
|
||||
// ]
|
||||
// ],
|
||||
// "range":{"row":[4,4],"column":[0,1]}}
|
||||
// 循环列,取 v 的内容,然后创建记录
|
||||
for (let k = 0; k < v.length; k++) {
|
||||
// 这里面的每一项,都是一条记录
|
||||
for (let j = 0; j < v[k].length; j++) {
|
||||
// 解析内部的 r c 值
|
||||
const item = v[k][j];
|
||||
const r = range!.row[0] + k;
|
||||
const c = range!.column[0] + j;
|
||||
// 场景一:设置空单元格的样式数据 加粗、背景、颜色、字号等
|
||||
// {"t":"rv","i":"2b62e1f2-7f7f-4889-b34d-007fe7277364","v":[[{"v":null,"bl":1},{"v":null,"bl":1}],[{"v":null,"bl":1},{"v":null,"bl":1}]],"range":{"row":[6,7],"column":[3,4]}}
|
||||
|
||||
// 场景二:范围添加内容 v m 不为空
|
||||
// "v":[
|
||||
// [{"ct":{"fa":"General","t":"g"},"v":"B","m":"B","bg":"#FFFFFF","ff":"5","fc":"#000000","bl":false,"it":false,"fs":10,"cl":false,"ht":0,"vt":0,"f":null,"un":false},
|
||||
// {"ct":{"fa":"General","t":"n"},"v":"111","m":"111","bg":"#FFFFFF","ff":"5","fc":"#000000","bl":false,"it":false,"fs":10,"cl":false,"ht":0,"vt":0,"f":null,"un":false}
|
||||
// ]
|
||||
// ],
|
||||
// "range":{"row":[4,4],"column":[0,1]}}
|
||||
if ((item && item.v === null) || (item && item.v && item.m)) {
|
||||
// i r c 先判断是否存在记录,存在则更新,不存在则创建
|
||||
const exist = await CellDataService.hasCellData(i, r, c);
|
||||
|
||||
// 检查 item 是否为 null 或 undefined
|
||||
const cellInfo = {
|
||||
worker_sheet_id: i,
|
||||
r,
|
||||
c,
|
||||
f: item?.f || "",
|
||||
ctfa: item?.ct?.fa,
|
||||
ctt: item?.ct?.t,
|
||||
v: <string>item?.v || "",
|
||||
m: <string>item?.m || "",
|
||||
bg: item?.bg,
|
||||
bl: <boolean>item?.bl,
|
||||
cl: <boolean>item?.cl,
|
||||
fc: item?.fc,
|
||||
ff: <string>item?.ff,
|
||||
fs: <number>item?.fs,
|
||||
ht: item?.ht,
|
||||
it: <boolean>item?.it,
|
||||
un: <boolean>item?.un,
|
||||
vt: item?.vt,
|
||||
};
|
||||
|
||||
if (exist) {
|
||||
// 如果存在则更新 - 注意全量的样式数据
|
||||
await CellDataService.updateCellData({
|
||||
cell_data_id: exist.cell_data_id,
|
||||
...cellInfo,
|
||||
bg: cellInfo.bg,
|
||||
bl: <boolean>cellInfo.bl,
|
||||
cl: <boolean>cellInfo.cl,
|
||||
fc: cellInfo.fc,
|
||||
ff: <string>cellInfo.ff,
|
||||
});
|
||||
} else await CellDataService.createCellData(cellInfo);
|
||||
}
|
||||
|
||||
// 复制范围单元格,有样式与删除范围单元格触发的事件参数是一样的,目前未解决BUG
|
||||
// {"t":"rv","i":"Sheet_eiro660he3z5_1740971352584",
|
||||
// "v":[
|
||||
// [{"ct":{"fa":"General","t":"d"},"bg":"#FFFF00","ff":"5","fc":"#FF0000","bl":false,"it":false,"fs":10,"cl":false,"ht":0,"vt":0,"f":null,"un":false,"ps":null}],
|
||||
// [{"ct":{"fa":"General","t":"d"},"bg":"#FFFF00","ff":"5","fc":"#FF0000","bl":false,"it":false,"fs":10,"cl":false,"ht":0,"vt":0,"f":null,"un":false,"ps":null}]
|
||||
// ],
|
||||
// "range":{"row":[5,6],"column":[0,0]}
|
||||
// }
|
||||
// {"t":"rv","i":"Sheet_eiro660he3z5_1740971352584",
|
||||
// "v":[
|
||||
// [{"ct":{"fa":"General","t":"d"},"bg":"#FFFF00","ff":"5","fc":"#FF0000","bl":false,"it":false,"fs":10,"cl":false,"ht":0,"vt":0,"f":null,"un":false,"ps":null}],
|
||||
// [{"ct":{"fa":"General","t":"d"},"bg":"#FFFF00","ff":"5","fc":"#FF0000","bl":false,"it":false,"fs":10,"cl":false,"ht":0,"vt":0,"f":null,"un":false,"ps":null}]
|
||||
// ],
|
||||
// "range":{"row":[5,6],"column":[0,0]}}
|
||||
// 场景三:删除单元格内容
|
||||
else if (item && !item.v && !item.m) {
|
||||
await CellDataService.deleteCellData(i, r, c); // 删除记录
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { CRDTDataType } from "../../../Interface/WebSocket";
|
||||
import { WorkerSheetModelType } from "../../../Sequelize/Models/WorkerSheet";
|
||||
import { WorkerSheetService } from "../../../Service/WorkerSheet";
|
||||
import { isEmpty } from "../../../Utils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
// sheet属性(隐藏或显示)
|
||||
export async function sh(data: string) {
|
||||
// {"t":"sh","i":"Sheet_06ok13WM3kS3_1734398401711","v":1,"op":"hide","cur":"Sheet_ieo3iK3lo0m3_1734353939113"}
|
||||
logger.info("[CRDT DATA]:", data);
|
||||
const { t, v, i } = <CRDTDataType<number>>JSON.parse(data);
|
||||
if (t !== "sh") return logger.error("t is not sh.");
|
||||
if (isEmpty(i)) return logger.error("i is empty.");
|
||||
|
||||
// 更新 deleteFlag = false
|
||||
const info = <WorkerSheetModelType>{
|
||||
worker_sheet_id: i,
|
||||
hide: Boolean(v),
|
||||
};
|
||||
await WorkerSheetService.update(info);
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,82 @@
|
||||
import { CRDTDataType, SHA } from "../../../Interface/WebSocket";
|
||||
import { WorkerSheetModelType } from "../../../Sequelize/Models/WorkerSheet";
|
||||
import { BorderInfoService } from "../../../Service/Border";
|
||||
import { CellDataService } from "../../../Service/CellData";
|
||||
import { MergeService } from "../../../Service/Merge";
|
||||
import { WorkerSheetService } from "../../../Service/WorkerSheet";
|
||||
import { isEmpty } from "../../../Utils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
// 复制sheet - 复制了sheet 后,应该从被复制 sheet 的 index 直接复制数据即可,不需要saveParam
|
||||
export async function shc(data: string, gridKey: string) {
|
||||
// {"t":"shc","i":"Sheet_0NHdie3o0ba5_1734351308939","v":{"copyindex":"12f8254d-3914-4f79-9886-9f9aec173048","name":"Sheet(副本)"}}
|
||||
// 至于复制的sheet该放在什么位置上,会同步触发 shr 事件,调整sheet的位置
|
||||
logger.info("[CRDT DATA]:", data);
|
||||
const { t, i, v } = <CRDTDataType<SHA>>JSON.parse(data);
|
||||
if (t !== "shc") return logger.error("t is not shc.");
|
||||
if (isEmpty(i)) return logger.error("i is empty.");
|
||||
|
||||
const copy_sheet_info: WorkerSheetModelType = {
|
||||
worker_sheet_id: i,
|
||||
name: v.name,
|
||||
gridKey,
|
||||
};
|
||||
await WorkerSheetService.createSheet(copy_sheet_info);
|
||||
|
||||
// 真实实现复制 sheet 数据
|
||||
await copySheetData(v.copyindex!, i);
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具函数 - 复制 Sheet 时,需要同步被复制 sheet 的所有数据 borders、celldatas、charts、hidneandlens、images、merges
|
||||
* @param copyIndex 被复制的 sheet index
|
||||
* @param newSheetIndex 新 sheet index
|
||||
*/
|
||||
async function copySheetData(copyIndex: string, newSheetIndex: string) {
|
||||
// ==> copyIndex 25a110fd-8e06-4318-a2d2-c42606b266ee
|
||||
// ==> newSheetIndex Sheet_5330eTr735lz_1743664108612
|
||||
// 处理思路:先 查询当前被复制 sheet 的所有数据,然后批量插入到新 sheet 中
|
||||
// borders
|
||||
const copySheetBorder = await BorderInfoService.findAllBorder(copyIndex);
|
||||
if (copySheetBorder?.length) {
|
||||
for (let i = 0; i < copySheetBorder.length; i++) {
|
||||
const item = copySheetBorder[i].dataValues;
|
||||
delete item.config_border_id;
|
||||
// 批量插入到新 sheet 中
|
||||
await BorderInfoService.createConfigBorder({
|
||||
...item,
|
||||
worker_sheet_id: newSheetIndex,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// celldatas
|
||||
const copySheetCellData = await CellDataService.getCellData(copyIndex);
|
||||
if (copySheetCellData?.length) {
|
||||
for (let i = 0; i < copySheetCellData.length; i++) {
|
||||
const item = copySheetCellData[i].dataValues;
|
||||
delete item.cell_data_id;
|
||||
// 批量插入到新 sheet 中
|
||||
await CellDataService.createCellData({
|
||||
...item,
|
||||
worker_sheet_id: newSheetIndex,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// charts hidneandlens images 暂不处理 原理类似哈
|
||||
|
||||
// merges
|
||||
const copySheetMerge = await MergeService.findAllMerge(copyIndex);
|
||||
if (copySheetMerge?.length) {
|
||||
for (let i = 0; i < copySheetMerge.length; i++) {
|
||||
const item = copySheetMerge[i].dataValues;
|
||||
delete item.config_merge_id;
|
||||
// 批量插入到新 sheet 中
|
||||
await MergeService.createMerge({
|
||||
...item,
|
||||
worker_sheet_id: newSheetIndex,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 删除sheet - 不可以直接删除记录,因为还需要恢复,应该标记 deleteFlag 属性即可
|
||||
// celldata可能存在外键关联,请注意删除顺序
|
||||
|
||||
import { CRDTDataType } from "../../../Interface/WebSocket";
|
||||
import { WorkerSheetModelType } from "../../../Sequelize/Models/WorkerSheet";
|
||||
import { WorkerSheetService } from "../../../Service/WorkerSheet";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
// 请注意: 删除 Sheet 请真实删除 celldata 数据
|
||||
export async function shd(data: string) {
|
||||
logger.info("[CRDT DATA]:", data);
|
||||
const { t, v } = <CRDTDataType<{ deleIndex: string }>>JSON.parse(data);
|
||||
if (t !== "shd") return logger.error("t is not shd.");
|
||||
// {"t":"shd","i":null,"v":{"deleIndex":"Sheet_06ok13WM3kS3_1734398401711"}}
|
||||
// 更新 deleteFlag = true
|
||||
const info = <WorkerSheetModelType>{
|
||||
worker_sheet_id: v.deleIndex,
|
||||
deleteFlag: true,
|
||||
};
|
||||
await WorkerSheetService.update(info);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { CRDTDataType } from "../../../Interface/WebSocket";
|
||||
import { WorkerSheetModelType } from "../../../Sequelize/Models/WorkerSheet";
|
||||
import { WorkerSheetService } from "../../../Service/WorkerSheet";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
// 调整sheet位置
|
||||
export async function shr(data: string) {
|
||||
// {"t":"shr","i":null,"v":{"12f8254d-3914-4f79-9886-9f9aec173048":0,"Sheet_0NHdie3o0ba5_1734351308939":1,"Sheet_oi07n566135m_1734351229761":2}}
|
||||
logger.info("[CRDT DATA]:", data);
|
||||
const { t, v } = <CRDTDataType<{ [key: string]: number }>>JSON.parse(data);
|
||||
if (t !== "shr") return logger.error("t is not shr.");
|
||||
|
||||
// 循环调整位置
|
||||
for (const i in v) {
|
||||
if (Object.prototype.hasOwnProperty.call(v, i)) {
|
||||
const order = v[i];
|
||||
// 调整位置
|
||||
const info = <WorkerSheetModelType>{ worker_sheet_id: i, order };
|
||||
await WorkerSheetService.update(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { CRDTDataType } from "../../../Interface/WebSocket";
|
||||
import { WorkerSheetModelType } from "../../../Sequelize/Models/WorkerSheet";
|
||||
import { WorkerSheetService } from "../../../Service/WorkerSheet";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
// 删除sheet后恢复操作
|
||||
export async function shre(data: string) {
|
||||
// {"t":"shre","i":null,"v":{"reIndex":"Sheet_06ok13WM3kS3_1734398401711"}}
|
||||
logger.info("[CRDT DATA]:", data);
|
||||
const { t, v } = <CRDTDataType<{ reIndex: string }>>JSON.parse(data);
|
||||
if (t !== "shre") return logger.error("t is not shre.");
|
||||
// 更新 deleteFlag = false
|
||||
const info = <WorkerSheetModelType>{
|
||||
worker_sheet_id: v.reIndex,
|
||||
deleteFlag: false,
|
||||
};
|
||||
await WorkerSheetService.update(info);
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import { isEmpty } from "../../../Utils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { CellDataService } from "../../../Service/CellData";
|
||||
import { CRDTDataType, V } from "../../../Interface/WebSocket";
|
||||
import { CellDataModelType } from "../../../Sequelize/Models/CellData";
|
||||
|
||||
// 单个单元格刷新
|
||||
export async function v(data: string) {
|
||||
// 1. 解析 rc 单元格
|
||||
const { t, r, c, v, i } = <CRDTDataType<V>>JSON.parse(data);
|
||||
logger.info("[CRDT DATA]:", data);
|
||||
|
||||
// 纠错判断
|
||||
if (t !== "v") return logger.error("t is not v.");
|
||||
if (isEmpty(i)) return logger.error("i is undefined.");
|
||||
if (isEmpty(r) || isEmpty(c)) return logger.error("r or c is undefined.");
|
||||
|
||||
// 场景一 输入内容存在换行符,则内容是通过 s 传输
|
||||
// {"t":"v","i":"28a60885-46e3-4f59-9d25-442a30fdbba6","v":{"ct":{"fa":"General","t":"inlineStr","s":[{"ff":"Times New Roman","fc":"#000000","fs":10,"cl":0,"un":0,"bl":0,"it":0,"v":"123\r\nhhh"}]}},"r":7,"c":0}
|
||||
// {"t":"v","i":"28a60885-46e3-4f59-9d25-442a30fdbba6","v":{"ct":{"fa":"General","t":"inlineStr","s":[{"ff":"Times New Roman","fc":"#000000","fs":10,"cl":0,"un":0,"bl":0,"it":0,"v":"jasdjkasdh\r\n换行"}]}},"r":14,"c":1}
|
||||
if (v && !v.v && v.ct && v.ct.s && v.ct.s.length > 0) {
|
||||
// 取 v m
|
||||
const ctfa = v.ct.fa;
|
||||
const ctt = v.ct.t;
|
||||
const cts = JSON.stringify(v.ct.s);
|
||||
|
||||
// 判断表内是否存在当前记录
|
||||
const exist = await CellDataService.hasCellData(i, r, c);
|
||||
|
||||
const info: CellDataModelType = {
|
||||
worker_sheet_id: i,
|
||||
r,
|
||||
c,
|
||||
ctfa,
|
||||
ctt,
|
||||
cts,
|
||||
v: "",
|
||||
m: "",
|
||||
bg: v.bg,
|
||||
bl: <boolean>v.bl,
|
||||
cl: <boolean>v.cl,
|
||||
fc: v.fc,
|
||||
ff: <string>v.ff,
|
||||
fs: <number>v.fs,
|
||||
ht: v.ht,
|
||||
it: <boolean>v.it,
|
||||
un: <boolean>v.un,
|
||||
vt: v.vt,
|
||||
tb: <number>v.tb,
|
||||
ps: <string>v?.ps?.value,
|
||||
};
|
||||
|
||||
// 如果存在则更新
|
||||
if (exist) {
|
||||
await CellDataService.updateCellData({
|
||||
cell_data_id: exist.cell_data_id,
|
||||
...info,
|
||||
});
|
||||
} else {
|
||||
// 创建新的记录时,当前记录的 cell_data_id 由 sequelize 自动创建
|
||||
await CellDataService.createCellData(info);
|
||||
}
|
||||
}
|
||||
|
||||
// 场景一:单个单元格插入值(包括格式刷)
|
||||
// {"t":"v","i":"e73f971d-606f-4b04-bcf1-98550940e8e3","v":{"v":"123","ct":{"fa":"General","t":"n"},"m":"123"},"r":5,"c":0}
|
||||
else if (v && !isEmpty(v.v) && !isEmpty(v.m)) {
|
||||
// 取 v m
|
||||
const value = <string>v.v;
|
||||
const monitor = <string>v.m;
|
||||
|
||||
// 处理 ct 字段可能缺失的情况(格式刷时常见)
|
||||
const ctfa = v.ct?.fa || "General";
|
||||
const ctt = v.ct?.t || "n";
|
||||
|
||||
// 判断表内是否存在当前记录
|
||||
const exist = await CellDataService.hasCellData(i, r, c);
|
||||
|
||||
const info: CellDataModelType = {
|
||||
worker_sheet_id: i,
|
||||
r,
|
||||
c,
|
||||
f: v.f,
|
||||
v: value,
|
||||
m: monitor,
|
||||
ctfa,
|
||||
ctt,
|
||||
bg: v.bg,
|
||||
bl: <boolean>v.bl,
|
||||
cl: <boolean>v.cl,
|
||||
fc: v.fc,
|
||||
ff: <string>v.ff,
|
||||
fs: <number>v.fs,
|
||||
ht: v.ht,
|
||||
it: <boolean>v.it,
|
||||
un: <boolean>v.un,
|
||||
vt: v.vt,
|
||||
tb: <number>v.tb,
|
||||
ps: <string>v?.ps?.value,
|
||||
};
|
||||
|
||||
// 如果存在则更新
|
||||
if (exist) {
|
||||
await CellDataService.updateCellData({
|
||||
cell_data_id: exist.cell_data_id,
|
||||
...info,
|
||||
});
|
||||
} else {
|
||||
// 创建新的记录时,当前记录的 cell_data_id 由 sequelize 自动创建
|
||||
await CellDataService.createCellData(info);
|
||||
}
|
||||
}
|
||||
|
||||
// 场景二:设置空单元格的样式数据 加粗、背景、颜色、字号等
|
||||
// {"t":"v","i":"e73f971d-606f-4b04-bcf1-98550940e8e3","v":{"v":null,"bg":"#ff0000"},"r":3,"c":2}
|
||||
else if (v && v.v === null) {
|
||||
// 判断 i r c 是否存在
|
||||
const exist = await CellDataService.hasCellData(i, r, c);
|
||||
|
||||
const info: CellDataModelType = {
|
||||
worker_sheet_id: i,
|
||||
r,
|
||||
c,
|
||||
v: "",
|
||||
m: "",
|
||||
f: v.f || "",
|
||||
bg: v.bg,
|
||||
bl: <boolean>v.bl,
|
||||
cl: <boolean>v.cl,
|
||||
fc: v.fc,
|
||||
ff: <string>v.ff,
|
||||
fs: <number>v.fs,
|
||||
ht: v.ht,
|
||||
it: <boolean>v.it,
|
||||
un: <boolean>v.un,
|
||||
tb: <number>v.tb,
|
||||
vt: v.vt,
|
||||
};
|
||||
|
||||
if (exist) {
|
||||
// 如果存在则更新 - 注意全量的样式数据
|
||||
await CellDataService.updateCellData({
|
||||
cell_data_id: exist.cell_data_id,
|
||||
...info,
|
||||
});
|
||||
} else await CellDataService.createCellData(info);
|
||||
}
|
||||
|
||||
// 场景三:剪切/粘贴到某个单元格 - 会触发两次广播(一次是删除,一次是创建)
|
||||
// {"t":"v","i":"e73f971d-606f-4b04-bcf1-98550940e8e3","v":null,"r":9,"c":0}
|
||||
// {"t":"v","i":"e73f971d-606f-4b04-bcf1-98550940e8e3","v":{"ct":{"fa":"General","t":"n"},"v":"123","m":"123"},"r":13,"c":0}
|
||||
else if (v === null) {
|
||||
await CellDataService.deleteCellData(i, r, c);
|
||||
}
|
||||
|
||||
// 场景五:公式链操作时,会直接生成数值
|
||||
// {"t":"v","i":"61f708fc-b159-4950-afab-176a81f4e1f6","v":6,"r":0,"c":2}
|
||||
else if (v && typeof v === "number") {
|
||||
// console.log(" ==> 监听到了");
|
||||
// 更新 r c 的value 值
|
||||
const cell = await CellDataService.hasCellData(i, r, c);
|
||||
await CellDataService.updateCellData({
|
||||
cell_data_id: cell?.cell_data_id,
|
||||
worker_sheet_id: i,
|
||||
r,
|
||||
c,
|
||||
v: v,
|
||||
m: v,
|
||||
});
|
||||
}
|
||||
|
||||
// 场景六:单元格图片上设置
|
||||
// {"t":"v","i":"3fc7f0b8-8fda-4e12-8a51-f2bad072b9c5","v":{"f":"=DISPIMG('img_onb00NooTrk6_1757387493023',1)"},"r":5,"c":3}
|
||||
else if (v && v.f && v.f.startsWith("=DISPIMG(")) {
|
||||
// 更新 r c 的f 值
|
||||
|
||||
// 处理 ct 字段可能缺失的情况(格式刷时常见)
|
||||
const ctfa = v.ct?.fa || "General";
|
||||
const ctt = v.ct?.t || "n";
|
||||
|
||||
// 判断表内是否存在当前记录
|
||||
const exist = await CellDataService.hasCellData(i, r, c);
|
||||
|
||||
const info: CellDataModelType = {
|
||||
worker_sheet_id: i,
|
||||
r,
|
||||
c,
|
||||
f: v.f,
|
||||
v: "",
|
||||
m: "",
|
||||
ctfa,
|
||||
ctt,
|
||||
};
|
||||
|
||||
// 如果存在则更新
|
||||
if (exist) {
|
||||
await CellDataService.updateCellData({
|
||||
cell_data_id: exist.cell_data_id,
|
||||
...info,
|
||||
});
|
||||
} else {
|
||||
// 创建新的记录时,当前记录的 cell_data_id 由 sequelize 自动创建
|
||||
await CellDataService.createCellData(info);
|
||||
}
|
||||
}
|
||||
|
||||
// 场景四: 删除单元格内容
|
||||
// {"t":"v","i":"e73f971d-606f-4b04-bcf1-98550940e8e3","v":{"ct":{"fa":"General","t":"n"}},"r":5,"c":0}
|
||||
else if (v && typeof v === "object" && !v.v && !v.m) {
|
||||
await CellDataService.deleteCellData(i, r, c); // 删除记录
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import express from "express";
|
||||
import routes from "./Router";
|
||||
import { DB } from "./Sequelize/index"; // 导入 DB
|
||||
import { logger } from "./Utils/Logger";
|
||||
import { initMeddlewear } from "./Middleware";
|
||||
import { WorkerBookService } from "./Service/WorkerBook";
|
||||
import { SERVER_PORT, WORKER_BOOK_INFO } from "./Config";
|
||||
import { createWebSocketServer } from "./WebSocket/index"; // 导入 ws
|
||||
|
||||
/**
|
||||
* @author https://gitee.com/wfeng0/luckysheet-crdt
|
||||
* @description Luckysheet CRDT 协同全功能实现 - Server 服务端
|
||||
* @license Apache 2.0
|
||||
* @copyright Copyright (c) 2025 https://gitee.com/wfeng0/luckysheet-crdt
|
||||
* @time 2024年12月05日
|
||||
*/
|
||||
|
||||
(async () => {
|
||||
logger.info("✨ Server is starting, wait a moment please... ");
|
||||
|
||||
/** 创建 express 实例对象 */
|
||||
const app = express();
|
||||
|
||||
/** 注册中间件 */
|
||||
initMeddlewear(app);
|
||||
|
||||
/** 连接数据库 DB 请注意需要 await 不然后续的操作可能会受到影响 */
|
||||
try {
|
||||
await DB.connect();
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
/** 初始化路由 */
|
||||
app.use(routes);
|
||||
|
||||
/** 监听端口,获取 server 对象 */
|
||||
const server = app.listen(SERVER_PORT);
|
||||
logger.info(`✅️ Server is started at http://localhost:${SERVER_PORT} !`);
|
||||
|
||||
/** 初始化 WebSocket - 传入 server 对象 */
|
||||
createWebSocketServer(server);
|
||||
|
||||
/**
|
||||
* ⛔️ 注意:
|
||||
* 目前前台没有按钮创建工作簿,因此,需要提供一个默认的文件初始化, 也就是需要初始化一个 workerbooks 记录
|
||||
* 如果前台能通过 router 调用 WorkerBooksService.create 时,会直接返回一个默认文件,则应该注释下列代码
|
||||
* service 中已经做了兼容处理,不会重复添加 workerbook
|
||||
* **下列代码仅作演示使用**
|
||||
*/
|
||||
|
||||
try {
|
||||
if (!DB.getConnectState()) return;
|
||||
await WorkerBookService.create(WORKER_BOOK_INFO);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./build"
|
||||
},
|
||||
"exclude": ["scripts"]
|
||||
}
|
||||
Reference in New Issue
Block a user