import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { createMembershipSet, membershipAdd, membershipClear, membershipFromArray, membershipToArray, } from './MembershipSet'; /** 内置 xlsx 库,无需 npm install */ // eslint-disable-next-line @typescript-eslint/no-require-imports const XLSX = require(path.join(__dirname, '../lib/xlsx.js')); interface PluginConfigFile { excelPath: string; jsonExportPath: string; /** 语言表 lang_* 导出根目录,实际路径为 `{langJsonExportPath}/lang_xx/config/` */ langJsonExportPath: string; tsExportPath: string; autoExport: boolean; } interface PluginConfig extends PluginConfigFile { watcher: ReturnType | null; } const DEFAULT_CONFIG: PluginConfigFile = { excelPath: '配置表/excelList', jsonExportPath: 'assets/bundles/first/config', langJsonExportPath: 'assets/bundles/lang', tsExportPath: 'assets/script/interface', autoExport: true, }; class ExcelToJsonPlugin { private config: PluginConfig = { ...DEFAULT_CONFIG, watcher: null }; private fileWatcher: fs.FSWatcher | null = null; private isExporting = false; private exportQueue: (() => void)[] = []; /** 本次导出写入过的 JSON 目录,用于刷新 asset-db */ private jsonRefreshPaths = createMembershipSet(); async load() { await this.exportAllExcel(); this.setupFileWatcher(); } /** 插件根目录(config.json 所在目录) */ private getPluginRoot(): string { return path.join(__dirname, '..'); } /** 将 config.json 中的相对路径解析为绝对路径(相对项目根目录) */ private resolveProjectPath(relativePath: string): string { const projectRoot = Editor.Project.path; return path.normalize(path.join(projectRoot, relativePath.replace(/\\/g, '/').replace(/^\.\//, ''))); } /** 判断工作表是否需要导出:格式为 "描述|json文件名",且非默认 Sheet */ private isExportableSheet(sheetName: string): boolean { if (!sheetName || sheetName.startsWith('Sheet')) { return false; } const parts = sheetName.split('|'); return parts.length >= 2 && parts[1].trim().length > 0; } /** 获取工作表导出的 json 文件名(| 后第一段) */ private getSheetExportName(sheetName: string): string { return sheetName.split('|')[1].trim(); } /** 语言表:lang_cn / lang_en … */ private isLangExportName(exportName: string): boolean { return /^lang_[a-z]{2}$/i.test(exportName); } /** 解析 JSON 导出目录(语言表进对应语言包 bundle) */ private resolveJsonExportDir(exportName: string): string { if (this.isLangExportName(exportName)) { return path.join(this.config.langJsonExportPath, exportName, 'config'); } return this.config.jsonExportPath; } /** 清理 first 中遗留的语言表 JSON(历史路径) */ private removeStaleLangJsonInFirst(jsonFileName: string): void { const stalePath = path.join(this.config.jsonExportPath, jsonFileName); if (fs.existsSync(stalePath)) { fs.unlinkSync(stalePath); this.log(`[Excel插件] 已删除 first 中遗留语言表: ${jsonFileName}`); } const staleMeta = `${stalePath}.meta`; if (fs.existsSync(staleMeta)) { fs.unlinkSync(staleMeta); } } unload() { this.log('[Excel插件] 插件已卸载'); this.stopFileWatcher(); } /** 从插件根目录 config.json 加载配置,路径相对项目根目录 */ loadConfig() { const configPath = path.join(this.getPluginRoot(), 'config.json'); try { if (!fs.existsSync(configPath)) { fs.writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 4), 'utf-8'); this.warn(`[Excel插件] 已创建默认配置文件: ${configPath}`); } const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as Partial; const merged = { ...DEFAULT_CONFIG, ...raw }; this.config.excelPath = this.resolveProjectPath(merged.excelPath); this.config.jsonExportPath = this.resolveProjectPath(merged.jsonExportPath); this.config.langJsonExportPath = this.resolveProjectPath( merged.langJsonExportPath ?? DEFAULT_CONFIG.langJsonExportPath, ); this.config.tsExportPath = this.resolveProjectPath(merged.tsExportPath); this.config.autoExport = merged.autoExport; this.log('[Excel插件] 配置已加载 (extensions/excel-to-json-ts/config.json)'); this.log(`[Excel插件] excelPath: ${this.config.excelPath}`); this.log(`[Excel插件] jsonPath: ${this.config.jsonExportPath}`); this.log(`[Excel插件] langJsonPath: ${this.config.langJsonExportPath}`); this.log(`[Excel插件] tsPath: ${this.config.tsExportPath}`); this.log(`[Excel插件] autoExport: ${this.config.autoExport}`); } catch (err) { this.error(`[Excel插件] 加载配置失败 (${configPath}): ${err}`); } } // 手动导出 exportManual() { this.exportAllExcel(); } // 导出所有Excel(增强健壮性) async exportAllExcel() { 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 { membershipClear(this.jsonRefreshPaths); // 确保导出目录存在 if (this.config.jsonExportPath && !fs.existsSync(this.config.jsonExportPath)) { fs.mkdirSync(this.config.jsonExportPath, { recursive: true }); } if (this.config.langJsonExportPath && !fs.existsSync(this.config.langJsonExportPath)) { fs.mkdirSync(this.config.langJsonExportPath, { 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: string): string[] { 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: string) { try { // 安全读取打开状态的Excel文件 const workbook = this.readExcelSafely(filePath); const sheetNames = workbook.SheetNames; // 导出每个工作表 for (const sheetName of sheetNames) { if (!this.isExportableSheet(sheetName)) { continue; } this.exportSheet(workbook, sheetName, filePath); } } catch (err) { throw new Error(`处理文件 ${path.basename(filePath)} 失败: ${err}`); } } /** 从 buffer 解析 Excel,避免 readFile 在 Windows 中文路径下失败 */ private parseExcelBuffer(buffer: Buffer): any { return XLSX.read(buffer, { type: 'buffer' }); } /** 读取 Excel:优先直接读,失败时复制到 ASCII 临时文件再读 */ readExcelSafely(filePath: string): any { // 1. 直接读取(文件未占用时最快) try { const buffer = fs.readFileSync(filePath); if (buffer.length > 0) { return this.parseExcelBuffer(buffer); } } catch (directErr) { this.warn(`[Excel插件] 直接读取失败,尝试复制后读取: ${path.basename(filePath)} (${directErr})`); } // 2. 复制到纯 ASCII 临时路径(避免中文文件名导致 xlsx 库无法访问) const tempPath = path.join(os.tmpdir(), `excel-temp-${Date.now()}-${process.pid}.xlsx`); try { fs.copyFileSync(filePath, tempPath); const tempStats = fs.statSync(tempPath); if (tempStats.size === 0) { throw new Error('临时文件大小为0,复制可能未完成'); } const buffer = fs.readFileSync(tempPath); return this.parseExcelBuffer(buffer); } catch (err: any) { 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: any, sheetName: string, filePath: string) { 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 (!this.isExportableSheet(sheetName)) { this.warn(`[Excel插件] 工作表 "${sheetName}" 名称格式错误,跳过导出 (文件: ${path.basename(filePath)})`); return; } const sName = this.getSheetExportName(sheetName); // 提取表头信息 const headers = { fields: jsonData[0], types: jsonData[1], descriptions: jsonData[2] }; // 从第4行开始是数据 const dataRows = jsonData.slice(3) as any[][]; // 生成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`; const jsonExportDir = this.resolveJsonExportDir(sName); if (!fs.existsSync(jsonExportDir)) { fs.mkdirSync(jsonExportDir, { recursive: true }); } // 写入JSON文件 if (jsonExportDir) { const jsonPath = path.join(jsonExportDir, jsonFileName); fs.writeFileSync(jsonPath, JSON.stringify(jsonResult, null, 4), 'utf-8'); membershipAdd(this.jsonRefreshPaths, jsonExportDir); if (this.isLangExportName(sName)) { this.removeStaleLangJsonInFirst(jsonFileName); } } // 写入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)) { tsContent = this.replaceInterfaceBlock(tsContent, "ITable" + sName, tsInterface); } else { tsContent = tsContent.replace(/\}\r?\nexport \{ \};$/, tsInterface + "}\nexport { };"); } tsContent = this.normalizeAutoTableBlankLines(tsContent); fs.writeFileSync(tsPath, tsContent, 'utf-8'); } } catch (err) { throw new Error(`工作表 "${sheetName}" 导出失败 (文件: ${path.basename(filePath)}): ${err}`); } } /** * 转义正则特殊字符 */ escapeRegExp(str: string): string { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** * 精准查找接口声明行(全字匹配) */ findInterfaceDeclarationLine(fileContent: string, interfaceName: string): { lineStart: number; lineEnd: number; lineContent: string; } | null { // 使用单词边界 \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: string, interfaceName: string): { start: number; end: number; content: string; } | null { 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; // 括号不匹配 } // 吞掉 } 后紧跟的换行,避免替换时 newBlock 再带 \n 导致空行累加 let blockEnd = pos; if (fileContent[blockEnd] === '\r') blockEnd++; if (fileContent[blockEnd] === '\n') blockEnd++; const fullContent = fileContent.substring(blockStart, blockEnd); return { start: blockStart, end: blockEnd, content: fullContent }; } /** * 去掉接口之间 / declare global 末尾多余空行(生成时易累加) */ normalizeAutoTableBlankLines(content: string): string { let out = content.replace(/\r\n/g, '\n'); // 若误带入未解决冲突(通常是空行 vs 无空行),丢掉冲突标记 out = out.replace(/^<<<<<<< .+\n(?:.*\n)*?=======\n(?:.*\n)*?>>>>>>> .+\n/gm, ''); // 接口结束 } 后只保留一个换行,再接下一接口注释或 global 结束 } out = out.replace(/(\n {4}\})\n(?:[ \t]*\n)+(?=[ \t]*(?:\/\*\*|\}))/g, '$1\n'); // 文件末尾 export 前也不留空行 out = out.replace(/\n(?:[ \t]*\n)+(\}\nexport \{ \};)\s*$/g, '\n$1\n'); return out; } /** * 完全替换接口块(全字匹配,精准替换) */ replaceInterfaceBlock( fileContent: string, interfaceName: string, newInterfaceBlock: string ): string { 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(); this.loadConfig(); if (!this.config.autoExport) { this.log('[Excel插件] 自动导出已关闭,跳过文件监听'); return; } 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) 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: 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: string, maxRetries: number = 8, baseDelay: number = 1000) { this.enqueueExport(() => this.processExcelFileWithRetryInternal(filePath, maxRetries, baseDelay)); } /** 串行执行导出,避免并发写 AutoTable.ts */ private enqueueExport(task: () => void) { this.exportQueue.push(task); if (!this.isExporting) { this.runNextExport(); } } private runNextExport() { const task = this.exportQueue.shift(); if (!task) { this.isExporting = false; return; } this.isExporting = true; task(); } private processExcelFileWithRetryInternal(filePath: string, maxRetries: number = 8, baseDelay: number = 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})`); this.runNextExport(); } catch (err: any) { // 详细记录错误 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完全保存后再进行修改,或增加重试次数和延迟`); } this.runNextExport(); } } }, delay); }; tryProcess(); } // 停止文件监听(更新) stopFileWatcher() { if (this.fileWatcher) { try { // 使用类型断言确保调用close方法 (this.fileWatcher as any).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: any[][], headers: any): any[] { const result = []; for (const row of dataRows) { if (!this.isDataRow(row, headers.fields)) { continue; } const obj: any = {}; 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; } /** 跳过全空行 */ private isDataRow(row: any[], fields: string[]): boolean { if (!row || row.length === 0) { return false; } for (let i = 0; i < fields.length; i++) { const field = fields[i]; if (!field) continue; const value = row[i]; if (value !== null && value !== undefined && String(value).trim() !== '') { return true; } } return false; } /** 判断单元格是否为空 */ private isEmptyValue(value: any): boolean { return value === null || value === undefined || (typeof value === 'string' && value.trim() === ''); } // 根据类型获取默认值 getDefaultValue(type: string): any { 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: any, type: string): any { if (this.isEmptyValue(value)) { return this.getDefaultValue(type); } const cleanType = type.toLowerCase().trim(); try { switch (cleanType) { case 'int': { const n = parseInt(value, 10); return isNaN(n) ? 0 : n; } case 'float': { const n = parseFloat(value); return isNaN(n) ? 0 : n; } 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: any, targetType: string): any[] { 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: string): string { const interfaceName = `ITable${sheetName}`; const safeSheetName = `${this.makeSafeFileName(sheetName)}`; let tsContent = ` /**\n * ${safeSheetName} 配置数据\n */\n`; tsContent += ` interface ${interfaceName} {\n`; return tsContent; } // 生成TS接口 generateTSInterface(sheetName: string, headers: any): string { 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: string): string { 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: string): string { return str.replace(/(\w)(\w*)/g, (_, g1, g2) => g1.toUpperCase() + g2.toLowerCase() ).replace(/[^a-zA-Z0-9]/g, ''); } // 创建安全的文件名 makeSafeFileName(name: string): string { return name.replace(/[^a-zA-Z0-9_]/g, '_'); } // 日志输出 log(message: string) { console.log(message); } error(message: string) { console.error(message); } warn(message: string) { console.warn(message); } success(message: string) { console.log(message); } // 消息处理 messages = { 'excel-to-json-ts:export-manual': () => { this.exportManual(); } } // 刷新资源管理器 async refreshAssetDatabase() { try { const paths = membershipFromArray([ ...membershipToArray(this.jsonRefreshPaths), this.config.jsonExportPath, this.config.langJsonExportPath, ]); for (const systemPath of membershipToArray(paths)) { const dbPath = this.convertToDbPath(systemPath); if (dbPath && dbPath.startsWith('db://')) { await Editor.Message.request('asset-db', 'refresh-asset', dbPath); } } const tsDbPath = this.convertToDbPath(this.config.tsExportPath); 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: string): string { 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; } } } export const excelToJsonPlugin = new ExcelToJsonPlugin();