diff --git a/docs/excel-import-template-feature-design.md b/docs/excel-import-template-feature-design.md new file mode 100644 index 0000000..ccfcde6 --- /dev/null +++ b/docs/excel-import-template-feature-design.md @@ -0,0 +1,541 @@ +# Excel 导入模板功能设计文档:单条模板 / 批量模板 + +## 1. 设计目标 + +当前项目已经具备“模板管理”能力,但需要进一步明确模板类型,避免把单条变量模板和从 Excel 导入的一整套变量混在一起。 + +新的设计目标是把模板管理拆成两类: + +```text +单条模板管理:每次新增变量时,复制/套用一条变量模板。 +批量模板管理:导入 Excel,读取 MES_基本变量原始数据,保存为一整套批量模板。 +``` + +其中: + +- `单条模板` 保留现在的使用方式,用于新增或编辑单条变量时快速填充字段。 +- `批量模板` 用于把外部 Excel 中的 `MES_基本变量原始数据` 整批复制到当前设备模块。 +- 批量模板只读取导入 Excel 的 `MES_基本变量原始数据` Sheet。 +- 批量模板不读取、不解析、不使用 `设备类型对应表`。 +- 批量模板不会根据外部 Excel 修改当前网页中的设备模块、工位、设备 IP、DB。 + +## 2. 模板管理分类 + +模板管理分为两个 Tab 或两个区域: + +```text +单条模板管理 | 批量模板管理 +``` + +| 类型 | 数据来源 | 使用方式 | 结果 | +| --- | --- | --- | --- | +| 单条模板管理 | 用户手动维护的一条变量字段模板 | 新增变量时选择并套用 | 新增 1 条变量 | +| 批量模板管理 | 外部 Excel 的 `MES_基本变量原始数据` | 选择批量模板并使用 | 一次新增多条变量 | + +第一版不做设备模板、不做工位模板、不从 Excel 反向生成设备配置。 + +## 3. 单条模板管理 + +单条模板管理保留当前功能。 + +### 3.1 功能定位 + +单条模板用于减少单条变量录入时的重复填写。 + +例如用户新增变量时,可以选择: + +- `Bool 信号模板` +- `Int 数值模板` +- `Real 浮点模板` +- `String 字符串模板` + +然后自动填充: + +- `TagTypeID` +- `TagLong` +- `变量类型` +- `是否启用` +- `是否监控` +- 其它常用字段 + +### 3.2 单条模板使用规则 + +新增变量时: + +1. 用户点击 `新增变量`。 +2. 系统打开变量编辑弹窗。 +3. 用户选择一个单条模板。 +4. 点击 `使用模板`。 +5. 模板字段填入当前变量表单。 +6. 用户继续补充 `TagID / TagName / 变量地址 / 测量项目` 等字段。 +7. 保存后只新增一条共享变量。 + +### 3.3 单条模板保存内容 + +单条模板保存的是一条变量的部分字段,不需要保存完整 38 字段。 + +建议结构: + +```json +{ + "id": "tpl-bool-signal", + "templateName": "Bool 信号模板", + "description": "常用 Bool 开关量信号", + "fields": { + "TagTypeID": "11", + "TagLong": "1", + "变量类型": "Bool", + "是否启用": "1", + "是否监控": "1" + } +} +``` + +## 4. 批量模板管理 + +批量模板管理是本次新增的重点。 + +### 4.1 功能定位 + +批量模板用于把外部 Excel 中的一整套基本变量原始数据保存成可复用模板。 + +用户后续可以选择这个批量模板,并一次性追加到当前设备模块的共享变量数据中。 + +效果类似之前版本: + +```text +直接复制 Excel 中的 MES_基本变量原始数据 +``` + +### 4.2 批量模板来源 + +批量模板只来自外部 Excel 的: + +```text +MES_基本变量原始数据 +``` + +即使导入 Excel 中存在: + +```text +设备类型对应表 +``` + +系统也不读取、不解析、不使用。 + +### 4.3 批量模板使用结果 + +用户使用批量模板后: + +- 批量模板中的所有变量追加到当前选中的设备模块。 +- 当前设备模块右侧 `MES_基本变量原始数据展开预览` 立即刷新。 +- 如果当前设备模块有多个配置行,右侧按多个工位展开显示。 +- 导出 Excel 时,新追加的变量会出现在 `MES_基本变量原始数据` 中。 + +## 5. Excel 导入规则 + +### 5.1 Sheet 读取规则 + +导入 Excel 时只查找并读取: + +```text +MES_基本变量原始数据 +``` + +如果没有该 Sheet,提示: + +```text +未找到 MES_基本变量原始数据 Sheet,无法导入批量模板。 +``` + +不会读取: + +```text +设备类型对应表 +``` + +也不会读取其它 Sheet。 + +### 5.2 表头规则 + +第一行作为表头。 + +字段以当前系统中的 `MES_HEADERS` 为准。 + +处理规则: + +| 情况 | 处理方式 | +| --- | --- | +| Excel 存在系统支持字段 | 读取该字段 | +| Excel 存在未知字段 | 忽略 | +| Excel 缺少系统字段 | 补为空 | +| Excel 空行 | 忽略 | + +至少应存在: + +- `TagID` +- `TagName` + +建议必须填写: + +- `TagTypeID` +- `TagLong` + +### 5.3 字段保留规则 + +批量模板保存导入 Excel 中识别出的多条基本变量原始数据。 + +保存时字段仍按 38 列标准字段管理: + +```text +TagID +TagName +工位号 +数据来源 +IP +TagTypeID +TagLong +DbName +... +IsWebSocket +``` + +但导出和右侧预览时,以下字段仍以当前网页设备模块配置为准: + +- `工位号` +- `数据来源` +- `IP` +- `DbName` + +也就是说,外部 Excel 中这些字段可以被导入保存,但不会覆盖当前网页的设备模块配置。 + +### 5.4 TagID 规则 + +外部 Excel 中可能存在两种格式: + +```text +TAG_0000001 +ST04_TAG_0000001 +``` + +批量模板保存时建议统一保存为基础格式: + +```text +TAG_0000001 +``` + +规则: + +```text +如果 TagID 中包含 TAG_,则从 TAG_ 开始截取。 +``` + +示例: + +| 导入值 | 模板保存值 | +| --- | --- | +| `TAG_0000001` | `TAG_0000001` | +| `ST04_TAG_0000001` | `TAG_0000001` | +| `ST11_TAG_0000023` | `TAG_0000023` | + +使用批量模板追加到设备模块后,页面预览和 Excel 导出再按当前工位展开: + +```text +ST04_TAG_0000001 +ST05_TAG_0000001 +``` + +## 6. 批量模板使用规则 + +### 6.1 使用入口 + +在模板管理中进入: + +```text +批量模板管理 +``` + +选择一个批量模板后点击: + +```text +使用批量模板 +``` + +### 6.2 使用前提 + +必须先选择左侧设备模块。 + +如果未选择设备模块,提示: + +```text +请先选择设备模块,再使用批量模板。 +``` + +### 6.3 追加方式 + +使用批量模板时,不覆盖当前设备模块已有变量,而是追加。 + +示例: + +```text +当前设备模块已有变量:3 条 +批量模板变量:23 条 +使用后当前设备模块变量:26 条 +``` + +### 6.4 TagID 冲突处理 + +如果批量模板中的 `TagID` 与当前设备模块已有变量重复,推荐第一版自动重排。 + +示例: + +当前已有: + +```text +TAG_0000001 +TAG_0000002 +``` + +批量模板中有: + +```text +TAG_0000001 +TAG_0000002 +TAG_0000003 +``` + +追加后生成: + +```text +TAG_0000003 +TAG_0000004 +TAG_0000005 +``` + +这样可以避免同一设备模块中基础 `TagID` 重复。 + +### 6.5 展开预览 + +批量模板追加后,右侧表格按当前设备模块配置行展开。 + +例如: + +```text +当前设备模块配置行:ST04、ST05 +批量模板变量:23 条 +右侧展开显示:46 行 +``` + +## 7. 数据结构建议 + +### 7.1 总配置结构 + +建议 OPFS/localStorage 中保存: + +```json +{ + "version": 3, + "updatedAt": "2026-06-02T10:00:00.000Z", + "devices": [], + "variableTemplates": [], + "batchVariableTemplates": [] +} +``` + +### 7.2 单条模板结构 + +```json +{ + "id": "tpl-bool-signal", + "templateName": "Bool 信号模板", + "description": "常用 Bool 开关量信号", + "fields": { + "TagTypeID": "11", + "TagLong": "1", + "变量类型": "Bool" + } +} +``` + +### 7.3 批量模板结构 + +```json +{ + "id": "batch-tpl-001", + "templateName": "放油标准批量模板", + "description": "从外部 Excel 导入", + "sourceFileName": "SignalTable_CZ.xlsx", + "createdAt": "2026-06-02T10:00:00.000Z", + "variables": [ + { + "TagID": "TAG_0000001", + "TagName": "PLC_PC_工件到位", + "TagTypeID": "11", + "TagLong": "1", + "变量类型": "Bool", + "变量地址": "0", + "变量其它": ".0" + } + ] +} +``` + +## 8. 页面交互设计 + +### 8.1 模板管理弹窗 + +模板管理弹窗分为两个 Tab: + +```text +单条模板管理 | 批量模板管理 +``` + +### 8.2 单条模板管理 + +保留当前能力: + +- 新增单条模板 +- 编辑单条模板 +- 删除单条模板 +- 在新增变量弹窗中使用单条模板 +- 从当前变量保存为单条模板 + +### 8.3 批量模板管理 + +新增能力: + +- `导入 Excel` +- `预览批量模板` +- `使用批量模板` +- `删除批量模板` + +批量模板列表展示: + +- 模板名称 +- 来源文件 +- 变量数量 +- 创建时间 + +### 8.4 导入 Excel 确认弹窗 + +导入 Excel 后先展示确认信息: + +```text +来源文件:SignalTable_CZ.xlsx +读取 Sheet:MES_基本变量原始数据 +有效变量数:23 +忽略空行:0 +忽略未知列:2 + +模板名称:[放油标准批量模板] +模板说明:[从外部 Excel 导入] + +[取消] [保存为批量模板] +``` + +## 9. 测试方案 + +### 9.1 单条模板测试 + +1. 打开新增变量弹窗。 +2. 选择一个单条模板。 +3. 点击 `使用模板`。 +4. 确认只填充当前变量表单。 +5. 保存后只新增一条变量。 + +### 9.2 批量模板导入测试 + +1. 打开模板管理。 +2. 进入 `批量模板管理`。 +3. 点击 `导入 Excel`。 +4. 选择包含 `MES_基本变量原始数据` 的 Excel。 +5. 确认系统只读取该 Sheet。 +6. 保存后生成一个批量模板。 + +### 9.3 非目标 Sheet 测试 + +1. 导入包含 `设备类型对应表` 的 Excel。 +2. 确认系统不读取、不解析、不使用该 Sheet。 +3. 当前设备模块配置不发生变化。 + +### 9.4 批量模板使用测试 + +1. 选择左侧一个设备模块。 +2. 在批量模板管理中选择一个模板。 +3. 点击 `使用批量模板`。 +4. 当前设备模块一次性新增多条变量。 +5. 右侧 `MES_基本变量原始数据展开预览` 立即刷新。 + +### 9.5 Excel 导出测试 + +1. 使用批量模板批量新增变量。 +2. 点击 `生成 Excel`。 +3. 导出的 `MES_基本变量原始数据` 包含新增变量。 +4. 导出的 `工位号 / 数据来源 / IP / DbName` 使用当前网页设备模块配置。 +5. 导出的 Excel 仍只包含两个 Sheet。 + +### 9.6 异常测试 + +需要验证: + +- Excel 没有 `MES_基本变量原始数据` 时提示错误。 +- Excel 表头缺少 `TagID` 或 `TagName` 时提示错误。 +- Excel 空行被忽略。 +- Excel 未知列被忽略。 +- 批量模板保存后刷新页面仍存在。 + +## 10. 页面草图 + +### 10.1 模板管理 + +```text +┌──────────────────────────────────────────────────────────────────────────────┐ +│ 模板管理 │ +├──────────────────────────────────────────────────────────────────────────────┤ +│ [单条模板管理] [批量模板管理] │ +├──────────────────────────────┬───────────────────────────────────────────────┤ +│ 批量模板列表 │ 批量模板预览 │ +│ [导入 Excel] │ 模板名称:放油标准批量模板 │ +│ ┌──────────────────────────┐ │ 来源文件:SignalTable_CZ.xlsx │ +│ │ 放油标准批量模板 23 条 │ │ 变量数量:23 │ +│ │ 称重批量模板 12 条 │ │ │ +│ └──────────────────────────┘ │ ┌───────────────────────────────────────────┐ │ +│ │ │ TagID TagName TagTypeID │ │ +│ │ │ TAG_0000001 PLC_PC_工件到位 11 │ │ +│ │ │ TAG_0000002 PLC_PC_读取完成 11 │ │ +│ │ └───────────────────────────────────────────┘ │ +├──────────────────────────────┴───────────────────────────────────────────────┤ +│ [删除模板] [使用批量模板] [关闭] │ +└──────────────────────────────────────────────────────────────────────────────┘ +``` + +### 10.2 导入确认 + +```text +┌──────────────────────────────────────────────────────┐ +│ 导入 Excel 为批量模板 │ +├──────────────────────────────────────────────────────┤ +│ 来源文件:SignalTable_CZ.xlsx │ +│ 读取 Sheet:MES_基本变量原始数据 │ +│ 有效变量数:23 │ +│ 忽略空行:0 │ +│ 忽略未知列:2 │ +│ │ +│ 模板名称:[放油标准批量模板 ] │ +│ 模板说明:[从外部 Excel 导入 ] │ +├──────────────────────────────────────────────────────┤ +│ [取消] [保存为批量模板] │ +└──────────────────────────────────────────────────────┘ +``` + +## 11. 后续迭代方向 + +后续可以继续增强: + +- 支持导入 Excel 后直接追加到当前设备模块,不先保存为批量模板。 +- 支持批量模板内变量字段编辑。 +- 支持批量模板导出为 JSON。 +- 支持从当前设备模块变量保存为批量模板。 +- 支持批量模板使用时选择 TagID 策略:自动重排、保留原值、覆盖重复。 +- 支持导入前筛选变量,例如按 `变量类型`、`TagName`、`是否启用` 筛选。 + diff --git a/src/utils/excelTemplateImporter.js b/src/utils/excelTemplateImporter.js new file mode 100644 index 0000000..9bd67be --- /dev/null +++ b/src/utils/excelTemplateImporter.js @@ -0,0 +1,74 @@ +import XLSX from 'xlsx' +import { MES_HEADERS } from './signalTableBuilder' +import { normalizeBatchTemplateVariables, validateUniqueTagIds } from './templateBuilder' + +const MES_SHEET_NAME = 'MES_基本变量原始数据' +const REQUIRED_HEADERS = ['TagID', 'TagName'] + +function isEmptyRow(row) { + return !row.some(value => value !== undefined && value !== null && String(value).trim() !== '') +} + +export async function importBatchTemplateFromExcel(file) { + const content = await file.arrayBuffer() + const workbook = XLSX.read(content, { type: 'array' }) + const worksheet = workbook.Sheets[MES_SHEET_NAME] + + if (!worksheet) { + throw new Error(`未找到 ${MES_SHEET_NAME} Sheet,无法导入批量模板。`) + } + + const rows = XLSX.utils.sheet_to_json(worksheet, { + header: 1, + defval: '', + raw: false + }) + + if (!rows.length) { + throw new Error(`${MES_SHEET_NAME} Sheet 为空,无法导入批量模板。`) + } + + const headers = rows[0].map(value => String(value || '').trim()) + const missingHeaders = REQUIRED_HEADERS.filter(header => !headers.includes(header)) + if (missingHeaders.length) { + throw new Error(`缺少必需表头:${missingHeaders.join('、')}。`) + } + + const ignoredHeaders = headers.filter(header => header && !MES_HEADERS.includes(header)) + const variables = [] + let ignoredEmptyRows = 0 + + rows.slice(1).forEach(sourceRow => { + if (isEmptyRow(sourceRow)) { + ignoredEmptyRows += 1 + return + } + + const variable = {} + MES_HEADERS.forEach(header => { + const index = headers.indexOf(header) + variable[header] = index === -1 ? '' : sourceRow[index] + }) + variables.push(variable) + }) + + if (!variables.length) { + throw new Error(`${MES_SHEET_NAME} Sheet 中没有可导入的变量数据。`) + } + + const normalizedVariables = normalizeBatchTemplateVariables(variables) + const duplicateTagIds = validateUniqueTagIds(normalizedVariables) + if (duplicateTagIds.length) { + throw new Error(`导入数据存在重复基础 TagID:${duplicateTagIds.join('、')}。`) + } + + return { + sourceFileName: file.name, + sheetName: MES_SHEET_NAME, + variables: normalizedVariables, + variableCount: normalizedVariables.length, + ignoredEmptyRows, + ignoredUnknownColumns: ignoredHeaders.length + } +} + diff --git a/src/utils/excelWriter.js b/src/utils/excelWriter.js index c756394..6f96d21 100644 --- a/src/utils/excelWriter.js +++ b/src/utils/excelWriter.js @@ -14,7 +14,7 @@ export function createSignalTableWorkbook(data) { return workbook } -export async function exportSignalTable(data, filename = 'SignalTable_CZ.xlsx') { +export async function exportSignalTable(data, filename = 'SignalTable.xlsx') { const workbook = createSignalTableWorkbook(data) const content = XLSX.write(workbook, { bookType: 'xlsx', diff --git a/src/utils/filePicker.js b/src/utils/filePicker.js index e86381e..fbb51d3 100644 --- a/src/utils/filePicker.js +++ b/src/utils/filePicker.js @@ -55,3 +55,23 @@ export async function openJsonFile() { if (!handles.length) return null return handles[0].getFile() } + +export async function openExcelFile() { + if (!isOpenPickerSupported()) return null + + const handles = await window.showOpenFilePicker({ + multiple: false, + types: [ + { + description: 'Excel 工作簿', + accept: { + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], + 'application/vnd.ms-excel': ['.xls'] + } + } + ] + }) + + if (!handles.length) return null + return handles[0].getFile() +} diff --git a/src/utils/opfsStore.js b/src/utils/opfsStore.js index 9db0241..b871c89 100644 --- a/src/utils/opfsStore.js +++ b/src/utils/opfsStore.js @@ -9,11 +9,12 @@ export function isOpfsSupported() { function wrapConfig(data) { return { - version: 2, + version: 3, updatedAt: new Date().toISOString(), devices: Array.isArray(data.devices) ? data.devices : [], variables: Array.isArray(data.variables) ? data.variables : [], - variableTemplates: Array.isArray(data.variableTemplates) ? data.variableTemplates : [] + variableTemplates: Array.isArray(data.variableTemplates) ? data.variableTemplates : [], + batchVariableTemplates: Array.isArray(data.batchVariableTemplates) ? data.batchVariableTemplates : [] } } diff --git a/src/utils/signalTableBuilder.js b/src/utils/signalTableBuilder.js index cefefc6..c88698a 100644 --- a/src/utils/signalTableBuilder.js +++ b/src/utils/signalTableBuilder.js @@ -220,7 +220,8 @@ export function normalizeData(data) { return { devices, variables: templateVariables.length ? templateVariables : fallbackVariables.map(normalizeVariable), - variableTemplates: Array.isArray(source.variableTemplates) ? cloneData(source.variableTemplates) : [] + variableTemplates: Array.isArray(source.variableTemplates) ? cloneData(source.variableTemplates) : [], + batchVariableTemplates: Array.isArray(source.batchVariableTemplates) ? cloneData(source.batchVariableTemplates) : [] } } @@ -340,45 +341,26 @@ export function buildMesRows(data) { ;(device.stations || []).forEach(station => { ;(device.variables || []).filter(hasMeaningfulVariableData).forEach(variable => { const row = applyStationToVariable(variable, device, station) - rows.push(MES_HEADERS.map(header => row[header])) + rows.push(MES_HEADERS.map(header => row[header] == null ? '' : row[header])) }) }) }) - return rows + return normalizeRowLength(rows, MES_HEADERS.length) } -function removeEmptyColumns(rows, cols = []) { - if (!rows.length) return { rows, cols } - const dataRows = rows.slice(1) - if (!dataRows.length) return { rows, cols } - - const keepIndexes = rows[0] - .map((header, index) => ({ header, index })) - .filter(({ index }) => { - return dataRows.some(row => { - const value = row[index] - return value !== undefined && value !== null && String(value).trim() !== '' - }) - }) - .map(item => item.index) - - return { - rows: rows.map(row => keepIndexes.map(index => row[index])), - cols: keepIndexes.map(index => cols[index]).filter(width => width !== undefined) - } +function normalizeRowLength(rows, length) { + return (rows || []).map(row => { + const next = Array.from({ length }, (_, index) => row[index] == null ? '' : row[index]) + return next + }) } export function buildWorkbookSheets(data) { - const mesSheet = removeEmptyColumns( - buildMesRows(data), - [18, 28, 16, 16, 12, 12, 9, 12, 12, 10, 10, 8, 8, 12, 24, 10, 14, 58, 8, 8, 10, 8, 10, 20, 10, 14, 12, 12, 12, 10, 12, 10, 10, 10, 10, 12, 12, 12] - ) - return [ { name: 'MES_基本变量原始数据', - rows: mesSheet.rows, - cols: mesSheet.cols + rows: buildMesRows(data), + cols: [18, 28, 16, 16, 12, 12, 9, 12, 12, 10, 10, 8, 8, 12, 24, 10, 14, 58, 8, 8, 10, 8, 10, 20, 10, 14, 12, 12, 12, 10, 12, 10, 10, 10, 10, 12, 12, 12] }, { name: '设备类型对应表', diff --git a/src/utils/templateBuilder.js b/src/utils/templateBuilder.js index e1f0dcc..ca3d29f 100644 --- a/src/utils/templateBuilder.js +++ b/src/utils/templateBuilder.js @@ -3,6 +3,12 @@ import { cloneData, MES_HEADERS } from './signalTableBuilder' const SOURCE_FIELDS = new Set(['工位号', '数据来源', 'IP', 'DbName']) const NON_TEMPLATE_FIELDS = new Set(['TagID', 'TagName', ...SOURCE_FIELDS]) +export function normalizeBaseTagId(tagId) { + const text = String(tagId || '').trim() + const tagIndex = text.indexOf('TAG_') + return tagIndex === -1 ? text : text.slice(tagIndex) +} + export function normalizeVariableTemplates(templates, fallbackTemplates = []) { const source = Array.isArray(templates) && templates.length ? templates : fallbackTemplates @@ -100,3 +106,82 @@ export function sanitizeTemplateForSave(template) { return next } +export function normalizeBatchTemplateVariables(variables) { + return (variables || []).map(variable => { + const row = {} + MES_HEADERS.forEach(header => { + row[header] = Object.prototype.hasOwnProperty.call(variable || {}, header) ? variable[header] : '' + }) + row.TagID = normalizeBaseTagId(row.TagID) + row['工位号'] = 'OPNAME' + row['数据来源'] = 'OPNAME' + row.IP = 'IP' + row.DbName = 'DB' + return row + }) +} + +export function normalizeBatchVariableTemplates(templates) { + return (templates || []).map((template, index) => { + return { + id: template.id || `batch-tpl-${Date.now()}-${index}`, + templateName: template.templateName || `批量模板${index + 1}`, + description: template.description || '', + sourceFileName: template.sourceFileName || '', + createdAt: template.createdAt || new Date().toISOString(), + variables: normalizeBatchTemplateVariables(template.variables) + } + }) +} + +export function createBatchVariableTemplate(candidate, templateName, description = '') { + return { + id: `batch-tpl-${Date.now()}`, + templateName: String(templateName || '').trim(), + description: String(description || '').trim(), + sourceFileName: candidate.sourceFileName || '', + createdAt: new Date().toISOString(), + variables: normalizeBatchTemplateVariables(candidate.variables) + } +} + +export function validateUniqueTagIds(variables) { + const counts = {} + ;(variables || []).forEach(variable => { + const tagId = normalizeBaseTagId(variable.TagID) + if (!tagId) return + counts[tagId] = (counts[tagId] || 0) + 1 + }) + return Object.keys(counts).filter(tagId => counts[tagId] > 1) +} + +export function findBatchTemplateTagIdConflicts(currentVariables, templateVariables) { + const existing = new Set((currentVariables || []).map(variable => normalizeBaseTagId(variable.TagID)).filter(Boolean)) + return Array.from(new Set( + (templateVariables || []) + .map(variable => normalizeBaseTagId(variable.TagID)) + .filter(tagId => tagId && existing.has(tagId)) + )) +} + +export function batchTemplateNameExists(templates, name, ignoreId = '') { + const target = String(name || '').trim().toLowerCase() + return (templates || []).some(template => { + return template.id !== ignoreId && String(template.templateName || '').trim().toLowerCase() === target + }) +} + +export function getCopyTemplateName(existingTemplates, baseName) { + const rawName = String(baseName || '模板').trim() || '模板' + const sourceName = rawName.replace(/COPY\d+$/i, '') || rawName + const names = new Set((existingTemplates || []).map(template => String(template.templateName || '').trim().toLowerCase())) + let index = 1 + let nextName = `${sourceName}COPY${index}` + + while (names.has(nextName.toLowerCase())) { + index += 1 + nextName = `${sourceName}COPY${index}` + } + + return nextName +} diff --git a/src/views/SignalTableGenerator.vue b/src/views/SignalTableGenerator.vue index f145a9f..928c5d5 100644 --- a/src/views/SignalTableGenerator.vue +++ b/src/views/SignalTableGenerator.vue @@ -15,6 +15,7 @@ 导入配置 导出配置 + @@ -124,14 +125,15 @@ :visible.sync="deviceDialogVisible" width="1040px" > - -
- 设备模块配置 - 点击左上角 + 新增一条完整配置;表格中每一行都会导出为设备类型对应表的一行。 +
+ 设备模块配置 + 点击新增模块配置,表格会新增一行;每一行都会导出为设备类型对应表的一行。 +
+ + 新增模块配置 +
使用模板 - 保存为模板
@@ -239,49 +240,133 @@ width="1040px" class="template-dialog" > -
- + + +
+ -
- - - - - - - - +
+ + + + + + + + -
模板字段
-
- +
模板字段
+
+ +
+
- -
+ + + +
+ + +
+ +
请选择或导入一个批量模板
+
+
+
+ - 删除 - 取消 - 保存模板 + + + + + + +
+ 来源文件:{{ batchImportCandidate.sourceFileName }} + 读取 Sheet:{{ batchImportCandidate.sheetName }} + 有效变量数:{{ batchImportCandidate.variableCount }} + 忽略空行:{{ batchImportCandidate.ignoredEmptyRows }} + 忽略未知列:{{ batchImportCandidate.ignoredUnknownColumns }} +
+ + + + + + + + + + 取消 + 保存为批量模板
@@ -290,6 +375,7 @@