You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
794 lines
110 KiB
794 lines
110 KiB
|
1 week ago
|
"use strict";
|
||
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||
|
|
if (k2 === undefined) k2 = k;
|
||
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||
|
|
}
|
||
|
|
Object.defineProperty(o, k2, desc);
|
||
|
|
}) : (function(o, m, k, k2) {
|
||
|
|
if (k2 === undefined) k2 = k;
|
||
|
|
o[k2] = m[k];
|
||
|
|
}));
|
||
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||
|
|
}) : function(o, v) {
|
||
|
|
o["default"] = v;
|
||
|
|
});
|
||
|
|
var __importStar = (this && this.__importStar) || (function () {
|
||
|
|
var ownKeys = function(o) {
|
||
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||
|
|
var ar = [];
|
||
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||
|
|
return ar;
|
||
|
|
};
|
||
|
|
return ownKeys(o);
|
||
|
|
};
|
||
|
|
return function (mod) {
|
||
|
|
if (mod && mod.__esModule) return mod;
|
||
|
|
var result = {};
|
||
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||
|
|
__setModuleDefault(result, mod);
|
||
|
|
return result;
|
||
|
|
};
|
||
|
|
})();
|
||
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||
|
|
exports.excelToJsonPlugin = void 0;
|
||
|
|
// 由于Cocos Creator插件环境特殊,使用动态require方式
|
||
|
|
const fs = __importStar(require("fs"));
|
||
|
|
const path = __importStar(require("path"));
|
||
|
|
const os = __importStar(require("os"));
|
||
|
|
const XLSX = __importStar(require("xlsx"));
|
||
|
|
class ExcelToJsonPlugin {
|
||
|
|
constructor() {
|
||
|
|
this.config = {
|
||
|
|
excelPath: './配置表/excelList',
|
||
|
|
jsonExportPath: './assets/bundles/first/config',
|
||
|
|
tsExportPath: './assets/script/interface',
|
||
|
|
autoExport: true,
|
||
|
|
watcher: null
|
||
|
|
};
|
||
|
|
this.fileWatcher = null;
|
||
|
|
// 消息处理
|
||
|
|
this.messages = {
|
||
|
|
'excel-to-json-ts:export-manual': () => {
|
||
|
|
this.exportManual();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
async load() {
|
||
|
|
// 先执行一次导出
|
||
|
|
await this.exportAllExcel();
|
||
|
|
// 设置文件监听
|
||
|
|
this.setupFileWatcher();
|
||
|
|
// // 注册消息处理
|
||
|
|
// Editor.Message.addBroadcastListener('excel-to-json-ts:profile-updated', () => {
|
||
|
|
// this.loadConfig();
|
||
|
|
// // 如果配置了自动导出,重新设置监听
|
||
|
|
// if (this.config.autoExport) {
|
||
|
|
// this.stopFileWatcher();
|
||
|
|
// this.setupFileWatcher();
|
||
|
|
// this.exportAllExcel(); // 配置保存后立即导出一次
|
||
|
|
// } else {
|
||
|
|
// this.stopFileWatcher();
|
||
|
|
// }
|
||
|
|
// });
|
||
|
|
}
|
||
|
|
unload() {
|
||
|
|
this.log('[Excel插件] 插件已卸载');
|
||
|
|
this.stopFileWatcher();
|
||
|
|
}
|
||
|
|
// 加载配置
|
||
|
|
async loadConfig() {
|
||
|
|
try {
|
||
|
|
let conf = await Editor.Profile.getProject('excel-to-json-ts');
|
||
|
|
if (conf)
|
||
|
|
this.config = conf;
|
||
|
|
else {
|
||
|
|
Editor.Profile.setProject('excel-to-json-ts', "excelPath", this.config.excelPath);
|
||
|
|
Editor.Profile.setProject('excel-to-json-ts', "jsonExportPath", this.config.jsonExportPath);
|
||
|
|
Editor.Profile.setProject('excel-to-json-ts', "tsExportPath", this.config.tsExportPath);
|
||
|
|
this.warn(`[Excel插件] 路径配置不正确`);
|
||
|
|
}
|
||
|
|
this.log('[Excel插件] 配置已加载');
|
||
|
|
this.log('[Excel插件] excelPath' + this.config.excelPath);
|
||
|
|
this.log('[Excel插件] jsonPath' + this.config.jsonExportPath);
|
||
|
|
this.log('[Excel插件] tsPath' + this.config.tsExportPath);
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
this.error(`[Excel插件] 加载配置失败: ${err}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// 手动导出
|
||
|
|
exportManual() {
|
||
|
|
this.exportAllExcel();
|
||
|
|
}
|
||
|
|
// 导出所有Excel(增强健壮性)
|
||
|
|
async exportAllExcel() {
|
||
|
|
await this.loadConfig();
|
||
|
|
if (!this.config.excelPath) {
|
||
|
|
this.error('[Excel插件] Excel目录路径未配置');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!fs.existsSync(this.config.excelPath)) {
|
||
|
|
this.error(`[Excel插件] Excel目录不存在: ${this.config.excelPath}`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
// 确保导出目录存在
|
||
|
|
if (this.config.jsonExportPath && !fs.existsSync(this.config.jsonExportPath)) {
|
||
|
|
fs.mkdirSync(this.config.jsonExportPath, { recursive: true });
|
||
|
|
}
|
||
|
|
if (this.config.tsExportPath && !fs.existsSync(this.config.tsExportPath)) {
|
||
|
|
fs.mkdirSync(this.config.tsExportPath, { recursive: true });
|
||
|
|
}
|
||
|
|
// 获取目录中所有Excel文件
|
||
|
|
const excelFiles = this.getExcelFilesInDirectory(this.config.excelPath);
|
||
|
|
if (excelFiles.length === 0) {
|
||
|
|
this.warn(`[Excel插件] 目录中没有找到Excel文件: ${this.config.excelPath}`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.log(`[Excel插件] 找到 ${excelFiles.length} 个Excel文件`);
|
||
|
|
let successCount = 0;
|
||
|
|
let errorCount = 0;
|
||
|
|
// 处理每个Excel文件
|
||
|
|
for (const excelFile of excelFiles) {
|
||
|
|
try {
|
||
|
|
this.processExcelFile(excelFile);
|
||
|
|
successCount++;
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
this.error(`[Excel插件] 处理文件失败 ${path.basename(excelFile)}: ${err}`);
|
||
|
|
errorCount++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (successCount > 0) {
|
||
|
|
this.refreshAssetDatabase();
|
||
|
|
this.success(`[Excel插件] 导出完成,成功处理 ${successCount} 个文件,失败 ${errorCount} 个`);
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
this.error(`[Excel插件] 导出失败,没有成功处理任何文件`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
this.error(`[Excel插件] 导出失败: ${err}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// 获取目录中的所有Excel文件
|
||
|
|
getExcelFilesInDirectory(dirPath) {
|
||
|
|
return fs.readdirSync(dirPath)
|
||
|
|
.filter(file => {
|
||
|
|
// 过滤隐藏文件(以点开头的文件)
|
||
|
|
if (file.startsWith('.') || file.startsWith('~')) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const ext = path.extname(file).toLowerCase();
|
||
|
|
return (ext === '.xlsx' || ext === '.xls') &&
|
||
|
|
fs.statSync(path.join(dirPath, file)).isFile();
|
||
|
|
})
|
||
|
|
.map(file => path.join(dirPath, file));
|
||
|
|
}
|
||
|
|
// 处理单个Excel文件
|
||
|
|
processExcelFile(filePath) {
|
||
|
|
try {
|
||
|
|
// 安全读取打开状态的Excel文件
|
||
|
|
const workbook = this.readExcelSafely(filePath);
|
||
|
|
const sheetNames = workbook.SheetNames;
|
||
|
|
// 导出每个工作表
|
||
|
|
for (const sheetName of sheetNames) {
|
||
|
|
if (sheetName.match("Sheet") || !sheetName.match("|")) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
this.exportSheet(workbook, sheetName, filePath);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
throw new Error(`处理文件 ${path.basename(filePath)} 失败: ${err}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// 安全读取打开状态的Excel文件(终极版)
|
||
|
|
readExcelSafely(filePath) {
|
||
|
|
const tempPath = path.join(os.tmpdir(), `excel-temp-${Date.now()}-${path.basename(filePath)}`);
|
||
|
|
try {
|
||
|
|
// 详细记录文件状态
|
||
|
|
const stats = fs.statSync(filePath);
|
||
|
|
// this.log(`[Excel插件] 尝试读取文件 - 大小: ${stats.size} bytes, 最后修改: ${new Date(stats.mtimeMs).toLocaleTimeString()}`);
|
||
|
|
// 尝试复制文件,带详细错误处理
|
||
|
|
try {
|
||
|
|
fs.copyFileSync(filePath, tempPath);
|
||
|
|
}
|
||
|
|
catch (copyErr) {
|
||
|
|
this.warn(`[Excel插件] 第一次复制失败: ${copyErr}`);
|
||
|
|
// 再次尝试,有时第一次会失败
|
||
|
|
try {
|
||
|
|
fs.copyFileSync(filePath, tempPath);
|
||
|
|
}
|
||
|
|
catch (finalErr) {
|
||
|
|
this.error(`[Excel插件] 两次复制尝试均失败: ${finalErr}`);
|
||
|
|
throw finalErr;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// 验证临时文件
|
||
|
|
const tempStats = fs.statSync(tempPath);
|
||
|
|
if (tempStats.size === 0) {
|
||
|
|
throw new Error("临时文件大小为0,复制可能未完成");
|
||
|
|
}
|
||
|
|
return XLSX.readFile(tempPath);
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
throw new Error(`读取Excel失败: ${err.message || err}`);
|
||
|
|
}
|
||
|
|
finally {
|
||
|
|
// 确保清理临时文件
|
||
|
|
try {
|
||
|
|
if (fs.existsSync(tempPath)) {
|
||
|
|
fs.unlinkSync(tempPath);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch (e) {
|
||
|
|
this.warn(`[Excel插件] 无法删除临时文件: ${tempPath} - ${e}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// 导出单个工作表
|
||
|
|
exportSheet(workbook, sheetName, filePath) {
|
||
|
|
try {
|
||
|
|
const worksheet = workbook.Sheets[sheetName];
|
||
|
|
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
||
|
|
// 检查是否有足够的表头行(至少3行:字段名、类型、说明)
|
||
|
|
if (jsonData.length < 3) {
|
||
|
|
this.warn(`[Excel插件] 工作表 "${sheetName}" 数据不足,跳过导出 (文件: ${path.basename(filePath)})的${sheetName}`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!sheetName.match("|") || sheetName.split("|").length < 2) {
|
||
|
|
this.warn(`[Excel插件] 工作表 "${sheetName}" 名称格式错误,跳过导出 (文件: ${path.basename(filePath)})的${sheetName}`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const sName = sheetName.split("|")[1];
|
||
|
|
// 提取表头信息
|
||
|
|
const headers = {
|
||
|
|
fields: jsonData[0],
|
||
|
|
types: jsonData[1],
|
||
|
|
descriptions: jsonData[2]
|
||
|
|
};
|
||
|
|
// 从第4行开始是数据
|
||
|
|
const dataRows = jsonData.slice(3);
|
||
|
|
// 生成JSON数据
|
||
|
|
const jsonResult = this.generateJsonData(dataRows, headers);
|
||
|
|
// 生成TS接口
|
||
|
|
const tsInterface = this.generateTSInterface(sName, headers);
|
||
|
|
// 生成文件名
|
||
|
|
const safeSheetName = this.makeSafeFileName(sName);
|
||
|
|
const jsonFileName = `${safeSheetName}.json`;
|
||
|
|
//const tsFileName = `ITable${safeSheetName}.ts`;
|
||
|
|
const tsFileName = `AutoTable.ts`;
|
||
|
|
// 写入JSON文件
|
||
|
|
if (this.config.jsonExportPath) {
|
||
|
|
const jsonPath = path.join(this.config.jsonExportPath, jsonFileName);
|
||
|
|
fs.writeFileSync(jsonPath, JSON.stringify(jsonResult, null, 4), 'utf-8');
|
||
|
|
}
|
||
|
|
// 写入TS文件
|
||
|
|
if (this.config.tsExportPath) {
|
||
|
|
const tsPath = path.join(this.config.tsExportPath, tsFileName);
|
||
|
|
let tsContent = "";
|
||
|
|
try {
|
||
|
|
if (fs.existsSync(tsPath)) {
|
||
|
|
tsContent = fs.readFileSync(tsPath, 'utf-8');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
this.warn(`[Excel插件] 读取TS文件失败: ${err}`);
|
||
|
|
}
|
||
|
|
if (tsContent == "") {
|
||
|
|
tsContent = "declare global {\n\n}\nexport { };";
|
||
|
|
}
|
||
|
|
if (this.extractFullInterfaceBlock(tsContent, "ITable" + sName)) {
|
||
|
|
//console.log("替换接口:" + sName + ",内容:" + tsInterface);
|
||
|
|
tsContent = this.replaceInterfaceBlock(tsContent, "ITable" + sName, tsInterface);
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
//console.log("加到末尾:" + tsInterface);
|
||
|
|
tsContent = tsContent.replace(/}\nexport { };$/, tsInterface + "}\nexport { };");
|
||
|
|
}
|
||
|
|
fs.writeFileSync(tsPath, tsContent, 'utf-8');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
throw new Error(`工作表 "${sheetName}" 导出失败 (文件: ${path.basename(filePath)}): ${err}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* 转义正则特殊字符
|
||
|
|
*/
|
||
|
|
escapeRegExp(str) {
|
||
|
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* 精准查找接口声明行(全字匹配)
|
||
|
|
*/
|
||
|
|
findInterfaceDeclarationLine(fileContent, interfaceName) {
|
||
|
|
// 使用单词边界 \b 确保全字匹配
|
||
|
|
const regex = new RegExp(`\\binterface\\s+${this.escapeRegExp(interfaceName)}\\b(?:\\s*<[^>]*>)?\\s*[\\{\\n]`, 'g');
|
||
|
|
let match;
|
||
|
|
while ((match = regex.exec(fileContent)) !== null) {
|
||
|
|
// 找到匹配位置,确定完整行
|
||
|
|
const matchStart = match.index;
|
||
|
|
// 向前找到行开始
|
||
|
|
const lineStart = fileContent.lastIndexOf('\n', matchStart - 1) + 1;
|
||
|
|
// 向后找到行结束
|
||
|
|
const lineEnd = fileContent.indexOf('\n', matchStart);
|
||
|
|
const actualLineEnd = lineEnd === -1 ? fileContent.length : lineEnd;
|
||
|
|
const lineContent = fileContent.substring(lineStart, actualLineEnd);
|
||
|
|
// 验证:确保这一行确实是我们的目标接口
|
||
|
|
if (lineContent.trim().startsWith(`interface ${interfaceName}`)) {
|
||
|
|
return {
|
||
|
|
lineStart,
|
||
|
|
lineEnd: actualLineEnd,
|
||
|
|
lineContent
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* 提取完整的接口块(包括上方注释)
|
||
|
|
*/
|
||
|
|
extractFullInterfaceBlock(fileContent, interfaceName) {
|
||
|
|
const interfaceLine = this.findInterfaceDeclarationLine(fileContent, interfaceName);
|
||
|
|
if (!interfaceLine) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
// === 向前查找连续的注释块 ===
|
||
|
|
let blockStart = interfaceLine.lineStart;
|
||
|
|
let currentPos = interfaceLine.lineStart;
|
||
|
|
while (currentPos > 0) {
|
||
|
|
const prevLineEnd = currentPos - 1;
|
||
|
|
const prevLineStart = fileContent.lastIndexOf('\n', prevLineEnd - 1) + 1;
|
||
|
|
const prevLine = fileContent.substring(prevLineStart, prevLineEnd).trim();
|
||
|
|
// 检查是否为注释行或空行
|
||
|
|
if (prevLine === '' ||
|
||
|
|
prevLine.startsWith('//') ||
|
||
|
|
prevLine.startsWith('/*') ||
|
||
|
|
prevLine.startsWith('*')) {
|
||
|
|
currentPos = prevLineStart;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
blockStart = currentPos;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (currentPos === 0) {
|
||
|
|
blockStart = 0;
|
||
|
|
}
|
||
|
|
// === 向后查找接口体结束 ===
|
||
|
|
// 找到 interface 行中的开括号位置
|
||
|
|
const openBraceIndex = interfaceLine.lineContent.indexOf('{');
|
||
|
|
let startPos = interfaceLine.lineStart;
|
||
|
|
let braceCount = 0;
|
||
|
|
if (openBraceIndex !== -1) {
|
||
|
|
// 开括号在同一行
|
||
|
|
startPos = interfaceLine.lineStart + openBraceIndex;
|
||
|
|
braceCount = 1;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
// 开括号在下一行,找到第一个 {
|
||
|
|
const nextOpenBrace = fileContent.indexOf('{', interfaceLine.lineEnd);
|
||
|
|
if (nextOpenBrace === -1)
|
||
|
|
return null;
|
||
|
|
startPos = nextOpenBrace;
|
||
|
|
braceCount = 1;
|
||
|
|
}
|
||
|
|
let pos = startPos + 1; // 跳过 {
|
||
|
|
while (pos < fileContent.length && braceCount > 0) {
|
||
|
|
if (fileContent[pos] === '{') {
|
||
|
|
braceCount++;
|
||
|
|
}
|
||
|
|
else if (fileContent[pos] === '}') {
|
||
|
|
braceCount--;
|
||
|
|
}
|
||
|
|
pos++;
|
||
|
|
}
|
||
|
|
if (braceCount !== 0) {
|
||
|
|
return null; // 括号不匹配
|
||
|
|
}
|
||
|
|
const blockEnd = pos;
|
||
|
|
const fullContent = fileContent.substring(blockStart, blockEnd);
|
||
|
|
return {
|
||
|
|
start: blockStart,
|
||
|
|
end: blockEnd,
|
||
|
|
content: fullContent
|
||
|
|
};
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* 完全替换接口块(全字匹配,精准替换)
|
||
|
|
*/
|
||
|
|
replaceInterfaceBlock(fileContent, interfaceName, newInterfaceBlock) {
|
||
|
|
const blockInfo = this.extractFullInterfaceBlock(fileContent, interfaceName);
|
||
|
|
if (!blockInfo) {
|
||
|
|
throw new Error(`❌ 接口 "${interfaceName}" 未找到!请检查名称是否正确。`);
|
||
|
|
}
|
||
|
|
// 验证:确保提取的块确实只包含目标接口
|
||
|
|
const interfaceLines = blockInfo.content.split('\n');
|
||
|
|
const interfaceDeclLines = interfaceLines.filter(line => line.trim().startsWith('interface '));
|
||
|
|
if (interfaceDeclLines.length !== 1) {
|
||
|
|
throw new Error(`❌ 找到多个 interface 声明,可能匹配错误!`);
|
||
|
|
}
|
||
|
|
if (!interfaceDeclLines[0].includes(`interface ${interfaceName}`)) {
|
||
|
|
throw new Error(`❌ 匹配到错误的接口名称!`);
|
||
|
|
}
|
||
|
|
//console.log(`✅ 精准匹配到接口 "${interfaceName}"`);
|
||
|
|
return (fileContent.substring(0, blockInfo.start) +
|
||
|
|
newInterfaceBlock +
|
||
|
|
fileContent.substring(blockInfo.end));
|
||
|
|
}
|
||
|
|
// 设置文件监听(终极版)
|
||
|
|
setupFileWatcher() {
|
||
|
|
this.stopFileWatcher();
|
||
|
|
if (!this.config.excelPath)
|
||
|
|
return;
|
||
|
|
if (!fs.existsSync(this.config.excelPath)) {
|
||
|
|
this.error(`[Excel插件] 监听目录不存在: ${this.config.excelPath}`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
// 使用更可靠的监听方式
|
||
|
|
this.fileWatcher = fs.watch(this.config.excelPath, {
|
||
|
|
recursive: false,
|
||
|
|
encoding: 'utf8'
|
||
|
|
}, async (eventType, filename) => {
|
||
|
|
if (!filename)
|
||
|
|
return;
|
||
|
|
this.loadConfig();
|
||
|
|
if (this.config.autoExport == false)
|
||
|
|
return;
|
||
|
|
// 确保是字符串
|
||
|
|
const fileNameStr = typeof filename === 'string' ? filename : filename + "";
|
||
|
|
const ext = path.extname(fileNameStr).toLowerCase();
|
||
|
|
if (ext === '.xlsx' || ext === '.xls') {
|
||
|
|
const filePath = path.join(this.config.excelPath, fileNameStr);
|
||
|
|
// 额外验证:确保是文件且存在
|
||
|
|
try {
|
||
|
|
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
||
|
|
this.log(`[Excel插件] 检测到文件变更: ${fileNameStr} (${eventType})`);
|
||
|
|
// 使用终极重试机制
|
||
|
|
// 重试次数增加到8,基础延迟1秒
|
||
|
|
this.processExcelFileWithRetry(filePath, 8, 1000);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
this.warn(`[Excel插件] 验证文件状态时出错: ${err}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
// 处理监听错误
|
||
|
|
if (this.fileWatcher && typeof this.fileWatcher.on === 'function') {
|
||
|
|
this.fileWatcher.on('error', (error) => {
|
||
|
|
this.error(`[Excel插件] 文件监听错误: ${error}`);
|
||
|
|
// 尝试重新设置监听
|
||
|
|
setTimeout(() => {
|
||
|
|
this.stopFileWatcher();
|
||
|
|
this.setupFileWatcher();
|
||
|
|
}, 15000);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
this.log('[Excel插件] 已启动Excel目录监听(终极版)');
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
this.error(`[Excel插件] 目录监听失败: ${err}`);
|
||
|
|
// 尝试重新设置监听
|
||
|
|
setTimeout(() => {
|
||
|
|
this.stopFileWatcher();
|
||
|
|
this.setupFileWatcher();
|
||
|
|
}, 20000);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// 带终极重试机制处理Excel文件
|
||
|
|
processExcelFileWithRetry(filePath, maxRetries = 8, baseDelay = 1000) {
|
||
|
|
let attempt = 0;
|
||
|
|
const startTime = Date.now();
|
||
|
|
const tryProcess = () => {
|
||
|
|
attempt++;
|
||
|
|
const elapsed = Date.now() - startTime;
|
||
|
|
const delay = baseDelay * Math.min(1.5 ** (attempt - 1), 4); // 指数退避,但上限为4倍基础延迟
|
||
|
|
this.log(`[Excel插件] 处理文件 "${path.basename(filePath)}" - 尝试 ${attempt}/${maxRetries} (延迟 ${Math.round(delay)}ms, 已耗时 ${Math.round(elapsed / 1000)}s)`);
|
||
|
|
setTimeout(() => {
|
||
|
|
try {
|
||
|
|
// 额外检查文件是否可读
|
||
|
|
const stats = fs.statSync(filePath);
|
||
|
|
if (stats.size === 0) {
|
||
|
|
throw new Error("文件大小为0,可能仍在写入");
|
||
|
|
}
|
||
|
|
// 检查文件最后修改时间,确保不是刚被修改
|
||
|
|
const now = Date.now();
|
||
|
|
const fileAge = now - stats.mtimeMs;
|
||
|
|
if (fileAge < 2000) { // 文件修改时间少于2秒,可能还在写入
|
||
|
|
throw new Error(`文件刚被修改 (${Math.round(fileAge)}ms前),可能仍在写入`);
|
||
|
|
}
|
||
|
|
this.processExcelFile(filePath);
|
||
|
|
const totalTime = Date.now() - startTime;
|
||
|
|
this.refreshAssetDatabase();
|
||
|
|
this.success(`[Excel插件] 文件 "${path.basename(filePath)}" 自动导出成功 (总耗时 ${Math.round(totalTime / 1000)}s, 尝试 ${attempt}/${maxRetries})`);
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
// 详细记录错误
|
||
|
|
this.warn(`[Excel插件] 尝试 ${attempt} 失败: ${err.message || err}`);
|
||
|
|
// 检查是否是文件锁定或权限错误
|
||
|
|
const isFileLockError = err.message && (err.message.includes('EPERM') ||
|
||
|
|
err.message.includes('EBUSY') ||
|
||
|
|
err.message.includes('permission') ||
|
||
|
|
err.message.includes('locked') ||
|
||
|
|
err.message.includes('EACCES') ||
|
||
|
|
err.message.includes('file size 0') ||
|
||
|
|
err.message.includes('刚被修改'));
|
||
|
|
if (isFileLockError && attempt < maxRetries) {
|
||
|
|
// 计算下一次尝试的延迟(指数退避)
|
||
|
|
const nextDelay = baseDelay * Math.min(1.5 ** attempt, 4);
|
||
|
|
this.warn(`[Excel插件] 文件 "${path.basename(filePath)}" 可能仍在保存中,将在 ${Math.round(nextDelay)}ms 后重试 (尝试 ${attempt + 1}/${maxRetries})`);
|
||
|
|
setTimeout(tryProcess, nextDelay);
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
const totalTime = Date.now() - startTime;
|
||
|
|
this.error(`[Excel插件] 文件 "${path.basename(filePath)}" 自动导出失败 (总耗时 ${Math.round(totalTime / 1000)}s): ${err}`);
|
||
|
|
// 如果是文件锁定错误但已达到最大重试次数,提供具体建议
|
||
|
|
if (isFileLockError) {
|
||
|
|
this.error(`[Excel插件] 建议: 确保Excel完全保存后再进行修改,或增加重试次数和延迟`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}, delay);
|
||
|
|
};
|
||
|
|
// 开始第一次尝试
|
||
|
|
tryProcess();
|
||
|
|
}
|
||
|
|
// 停止文件监听(更新)
|
||
|
|
stopFileWatcher() {
|
||
|
|
if (this.fileWatcher) {
|
||
|
|
try {
|
||
|
|
// 使用类型断言确保调用close方法
|
||
|
|
this.fileWatcher.close();
|
||
|
|
this.fileWatcher = null;
|
||
|
|
}
|
||
|
|
catch (e) {
|
||
|
|
this.warn(`[Excel插件] 停止监听时出错: ${e}`);
|
||
|
|
}
|
||
|
|
if (this.config.watcher) {
|
||
|
|
clearTimeout(this.config.watcher);
|
||
|
|
this.config.watcher = null;
|
||
|
|
}
|
||
|
|
this.log('[Excel插件] 已停止Excel文件监听');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// 生成JSON数据
|
||
|
|
generateJsonData(dataRows, headers) {
|
||
|
|
const result = [];
|
||
|
|
for (const row of dataRows) {
|
||
|
|
const obj = {};
|
||
|
|
for (let i = 0; i < headers.fields.length; i++) {
|
||
|
|
const field = headers.fields[i];
|
||
|
|
const type = headers.types[i];
|
||
|
|
if (field) {
|
||
|
|
// 处理空单元格,使用对应类型的默认值
|
||
|
|
const value = row[i] !== undefined ? row[i] : this.getDefaultValue(type);
|
||
|
|
obj[field] = this.convertValue(value, type);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
result.push(obj);
|
||
|
|
}
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
// 根据类型获取默认值
|
||
|
|
getDefaultValue(type) {
|
||
|
|
if (!type)
|
||
|
|
return null;
|
||
|
|
const cleanType = type.toLowerCase().trim();
|
||
|
|
switch (cleanType) {
|
||
|
|
case 'int':
|
||
|
|
case 'float':
|
||
|
|
return 0;
|
||
|
|
case 'boolean':
|
||
|
|
return false;
|
||
|
|
case 'int[]':
|
||
|
|
case 'float[]':
|
||
|
|
case 'string[]':
|
||
|
|
case 'any[]':
|
||
|
|
return [];
|
||
|
|
case 'string':
|
||
|
|
return '';
|
||
|
|
default:
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// 类型转换
|
||
|
|
convertValue(value, type) {
|
||
|
|
if (value === null || value === undefined)
|
||
|
|
return null;
|
||
|
|
const cleanType = type.toLowerCase().trim();
|
||
|
|
try {
|
||
|
|
switch (cleanType) {
|
||
|
|
case 'int':
|
||
|
|
return parseInt(value);
|
||
|
|
case 'float':
|
||
|
|
return parseFloat(value);
|
||
|
|
case 'json':
|
||
|
|
return JSON.parse(value);
|
||
|
|
case 'boolean':
|
||
|
|
return value.toString().toLowerCase() === 'true' || value === 1;
|
||
|
|
case 'int[]':
|
||
|
|
return this.parseArray(value, 'int');
|
||
|
|
case 'float[]':
|
||
|
|
return this.parseArray(value, 'float');
|
||
|
|
case 'string[]':
|
||
|
|
return this.parseArray(value, 'string');
|
||
|
|
case 'any[]':
|
||
|
|
return this.parseArray(value, 'any');
|
||
|
|
default:
|
||
|
|
return value;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch (e) {
|
||
|
|
this.warn(`[Excel插件] 类型转换失败 [${type}]: ${value} - ${e}`);
|
||
|
|
return value;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// 解析数组
|
||
|
|
parseArray(value, targetType) {
|
||
|
|
if (Array.isArray(value))
|
||
|
|
return value;
|
||
|
|
// 尝试分割字符串
|
||
|
|
let items = [];
|
||
|
|
if (typeof value === 'string') {
|
||
|
|
items = value.split(/[,;#\s]\s*/).filter(v => v);
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
items = [value];
|
||
|
|
}
|
||
|
|
return items.map(item => {
|
||
|
|
if (targetType === 'any') {
|
||
|
|
// 自动识别类型
|
||
|
|
if (!isNaN(item)) {
|
||
|
|
return !isNaN(parseFloat(item)) ? parseFloat(item) : parseInt(item);
|
||
|
|
}
|
||
|
|
if (item.toLowerCase() === 'true' || item.toLowerCase() === 'false') {
|
||
|
|
return item.toLowerCase() === 'true';
|
||
|
|
}
|
||
|
|
return item;
|
||
|
|
}
|
||
|
|
// 指定类型转换
|
||
|
|
switch (targetType) {
|
||
|
|
case 'int': return parseInt(item);
|
||
|
|
case 'float': return parseFloat(item);
|
||
|
|
case 'string': return String(item);
|
||
|
|
default: return item;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
generateTSInterfaceStart(sheetName) {
|
||
|
|
const interfaceName = `ITable${sheetName}`;
|
||
|
|
const safeSheetName = `${this.makeSafeFileName(sheetName)}`;
|
||
|
|
let tsContent = ` /**\n * ${safeSheetName} 配置数据\n */\n`;
|
||
|
|
tsContent += ` interface ${interfaceName} {\n`;
|
||
|
|
return tsContent;
|
||
|
|
}
|
||
|
|
// 生成TS接口
|
||
|
|
generateTSInterface(sheetName, headers) {
|
||
|
|
const interfaceName = `ITable${sheetName}`;
|
||
|
|
const safeSheetName = `${this.makeSafeFileName(sheetName)}`;
|
||
|
|
let tsContent = ` /**\n * ${safeSheetName} 配置数据\n */\n`;
|
||
|
|
tsContent += ` interface ${interfaceName} {\n`;
|
||
|
|
for (let i = 0; i < headers.fields.length; i++) {
|
||
|
|
const field = headers.fields[i];
|
||
|
|
const type = headers.types[i];
|
||
|
|
const description = headers.descriptions[i] || '字段说明';
|
||
|
|
if (field) {
|
||
|
|
tsContent += ` /** ${description} */\n`;
|
||
|
|
tsContent += ` readonly ${field}: ${this.convertToTSType(type)};\n`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
tsContent += ` }\n`;
|
||
|
|
return tsContent;
|
||
|
|
}
|
||
|
|
// 转换为TS类型
|
||
|
|
convertToTSType(excelType) {
|
||
|
|
if (!excelType)
|
||
|
|
return 'any';
|
||
|
|
const type = excelType.toLowerCase().trim();
|
||
|
|
switch (type) {
|
||
|
|
case 'string':
|
||
|
|
return 'string';
|
||
|
|
case 'int':
|
||
|
|
case 'float':
|
||
|
|
return 'number';
|
||
|
|
case 'boolean':
|
||
|
|
return 'boolean';
|
||
|
|
case 'int[]':
|
||
|
|
return 'number[]';
|
||
|
|
case 'float[]':
|
||
|
|
return 'number[]';
|
||
|
|
case 'string[]':
|
||
|
|
return 'string[]';
|
||
|
|
case 'any[]':
|
||
|
|
return '(number | string | boolean)[]';
|
||
|
|
default:
|
||
|
|
return 'any';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// 转换为PascalCase
|
||
|
|
toPascalCase(str) {
|
||
|
|
return str.replace(/(\w)(\w*)/g, (_, g1, g2) => g1.toUpperCase() + g2.toLowerCase()).replace(/[^a-zA-Z0-9]/g, '');
|
||
|
|
}
|
||
|
|
// 创建安全的文件名
|
||
|
|
makeSafeFileName(name) {
|
||
|
|
return name.replace(/[^a-zA-Z0-9_]/g, '_');
|
||
|
|
}
|
||
|
|
// 日志输出
|
||
|
|
log(message) {
|
||
|
|
console.log(message);
|
||
|
|
}
|
||
|
|
error(message) {
|
||
|
|
console.error(message);
|
||
|
|
}
|
||
|
|
warn(message) {
|
||
|
|
console.warn(message);
|
||
|
|
}
|
||
|
|
success(message) {
|
||
|
|
console.log(message);
|
||
|
|
}
|
||
|
|
// 刷新资源管理器
|
||
|
|
async refreshAssetDatabase() {
|
||
|
|
try {
|
||
|
|
// 确保路径是项目内的路径(db://assets/开头)
|
||
|
|
const jsonDbPath = this.convertToDbPath(this.config.jsonExportPath);
|
||
|
|
const tsDbPath = this.convertToDbPath(this.config.tsExportPath);
|
||
|
|
// 刷新JSON导出目录
|
||
|
|
if (jsonDbPath && jsonDbPath.startsWith('db://')) {
|
||
|
|
await Editor.Message.request('asset-db', 'refresh-asset', jsonDbPath);
|
||
|
|
}
|
||
|
|
// 刷新TS导出目录
|
||
|
|
if (tsDbPath && tsDbPath.startsWith('db://')) {
|
||
|
|
await Editor.Message.request('asset-db', 'refresh-asset', tsDbPath);
|
||
|
|
}
|
||
|
|
this.success('[Excel插件] 资源刷新请求已发送');
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
this.error(`[Excel插件] 资源刷新失败: ${err}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// 将系统路径转换为Cocos的db路径
|
||
|
|
convertToDbPath(systemPath) {
|
||
|
|
if (!systemPath)
|
||
|
|
return '';
|
||
|
|
try {
|
||
|
|
// 获取项目路径
|
||
|
|
const projectPath = Editor.Project.path;
|
||
|
|
// 规范化路径(统一使用正斜杠)
|
||
|
|
systemPath = systemPath.replace(/\\/g, '/');
|
||
|
|
const normalizedProjectPath = projectPath.replace(/\\/g, '/');
|
||
|
|
// 确保路径以/结尾
|
||
|
|
const projectPathWithSlash = normalizedProjectPath.endsWith('/')
|
||
|
|
? normalizedProjectPath
|
||
|
|
: normalizedProjectPath + '/';
|
||
|
|
// 检查是否在项目目录内
|
||
|
|
if (systemPath.startsWith(projectPathWithSlash)) {
|
||
|
|
// 计算相对路径
|
||
|
|
const relativePath = systemPath.substring(projectPathWithSlash.length);
|
||
|
|
return `db://${relativePath}`;
|
||
|
|
}
|
||
|
|
// 如果路径包含assets目录,尝试直接转换
|
||
|
|
if (systemPath.includes('/assets/') || systemPath.includes('\\assets\\')) {
|
||
|
|
const assetsIndex = systemPath.toLowerCase().indexOf('/assets/');
|
||
|
|
if (assetsIndex !== -1) {
|
||
|
|
const relativeToAssets = systemPath.substring(assetsIndex + 1);
|
||
|
|
return `db://${relativeToAssets}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
this.warn(`[Excel插件] 路径不在项目内,可能无法正确刷新: ${systemPath}`);
|
||
|
|
return systemPath;
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
this.error(`[Excel插件] 路径转换失败: ${err}`);
|
||
|
|
return systemPath;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
exports.excelToJsonPlugin = new ExcelToJsonPlugin();
|
||
|
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXhjZWwyanNvbkFuZFRzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc291cmNlL2V4Y2VsMmpzb25BbmRUcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFBQSxzQ0FBc0M7QUFDdEMsdUNBQXlCO0FBQ3pCLDJDQUE2QjtBQUM3Qix1Q0FBeUI7QUFDekIsMkNBQTZCO0FBYTdCLE1BQU0saUJBQWlCO0lBQXZCO1FBQ1ksV0FBTSxHQUFpQjtZQUMzQixTQUFTLEVBQUUsaUJBQWlCO1lBQzVCLGNBQWMsRUFBRSwrQkFBK0I7WUFDL0MsWUFBWSxFQUFFLDJCQUEyQjtZQUN6QyxVQUFVLEVBQUUsSUFBSTtZQUNoQixPQUFPLEVBQUUsSUFBSTtTQUNoQixDQUFDO1FBRU0sZ0JBQVcsR0FBd0IsSUFBSSxDQUFDO1FBNnhCaEQsT0FBTztRQUNQLGFBQVEsR0FBRztZQUNQLGdDQUFnQyxFQUFFLEdBQUcsRUFBRTtnQkFDbkMsSUFBSSxDQUFDLFlBQVksRUFBRSxDQUFDO1lBQ3hCLENBQUM7U0FDSixDQUFBO0lBaUVMLENBQUM7SUFqMkJHLEtBQUssQ0FBQyxJQUFJO1FBRU4sVUFBVTtRQUNWLE1BQU0sSUFBSSxDQUFDLGNBQWMsRUFBRSxDQUFDO1FBRTVCLFNBQVM7UUFDVCxJQUFJLENBQUMsZ0JBQWdCLEVBQUUsQ0FBQztRQUd4QixZQUFZO1FBQ1osa0ZBQWtGO1FBQ2xGLHlCQUF5QjtRQUV6QiwwQkFBMEI7UUFDMUIsb0NBQW9DO1FBQ3BDLGtDQUFrQztRQUNsQyxtQ0FBbUM7UUFDbkMsZ0RBQWdEO1FBQ2hELGVBQWU7UUFDZixrQ0FBa0M7UUFDbEMsUUFBUTtRQUNSLE1BQU07SUFDVixDQUFDO0lBRUQsTUFBTTtRQUNGLElBQUksQ0FBQyxHQUFHLENBQUMsaUJBQWlCLENBQUMsQ0FBQztRQUM1QixJQUFJLENBQUMsZUFBZSxFQUFFLENBQUM7SUFDM0IsQ0FBQztJQUVELE9BQU87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
|