feat: update Excel template management and export handling
This commit is contained in:
541
docs/excel-import-template-feature-design.md
Normal file
541
docs/excel-import-template-feature-design.md
Normal file
@@ -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`、`是否启用` 筛选。
|
||||
|
||||
74
src/utils/excelTemplateImporter.js
Normal file
74
src/utils/excelTemplateImporter.js
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 : []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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: '设备类型对应表',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<el-button icon="el-icon-upload2" @click="triggerImport">导入配置</el-button>
|
||||
<el-button icon="el-icon-document" @click="handleExportConfig">导出配置</el-button>
|
||||
<input ref="configFileInput" class="hidden-input" type="file" accept="application/json,.json" @change="handleImportConfig">
|
||||
<input ref="batchExcelFileInput" class="hidden-input" type="file" accept=".xlsx,.xls" @change="handleBatchExcelInput">
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -124,14 +125,15 @@
|
||||
:visible.sync="deviceDialogVisible"
|
||||
width="1040px"
|
||||
>
|
||||
<button class="dialog-plus" type="button" title="新增完整工位" @click="addStationToDeviceForm()">
|
||||
<i class="el-icon-plus"></i>
|
||||
</button>
|
||||
|
||||
<section class="dialog-station-section">
|
||||
<div class="dialog-station-head">
|
||||
<span>设备模块配置</span>
|
||||
<small>点击左上角 + 新增一条完整配置;表格中每一行都会导出为设备类型对应表的一行。</small>
|
||||
<div class="dialog-station-title">
|
||||
<span>设备模块配置</span>
|
||||
<small>点击新增模块配置,表格会新增一行;每一行都会导出为设备类型对应表的一行。</small>
|
||||
</div>
|
||||
<el-button size="mini" type="primary" icon="el-icon-plus" @click="addStationToDeviceForm()">
|
||||
新增模块配置
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
ref="deviceStationTable"
|
||||
@@ -214,7 +216,6 @@
|
||||
/>
|
||||
</el-select>
|
||||
<el-button size="small" :disabled="!variableTemplateSelectId" @click="applySelectedTemplateToVariable">使用模板</el-button>
|
||||
<el-button size="small" @click="saveVariableFormAsTemplate">保存为模板</el-button>
|
||||
</section>
|
||||
|
||||
<el-form ref="variableForm" :model="variableForm" :rules="variableRules" label-width="126px">
|
||||
@@ -239,49 +240,133 @@
|
||||
width="1040px"
|
||||
class="template-dialog"
|
||||
>
|
||||
<div class="template-manager">
|
||||
<aside class="template-list">
|
||||
<div class="template-list-head">
|
||||
<span>模板列表</span>
|
||||
<el-button size="mini" type="primary" icon="el-icon-plus" @click="createTemplateInManager">新增模板</el-button>
|
||||
</div>
|
||||
<button
|
||||
v-for="template in variableTemplates"
|
||||
:key="template.id"
|
||||
type="button"
|
||||
class="template-row"
|
||||
:class="{ active: selectedTemplateId === template.id }"
|
||||
@click="selectTemplateForEdit(template.id)"
|
||||
>
|
||||
<strong>{{ template.templateName }}</strong>
|
||||
<small>{{ template.description || '暂无说明' }}</small>
|
||||
</button>
|
||||
<div v-if="!variableTemplates.length" class="template-empty">暂无模板</div>
|
||||
</aside>
|
||||
<el-tabs v-model="templateManagerTab">
|
||||
<el-tab-pane label="单条模板管理" name="single">
|
||||
<div class="template-manager">
|
||||
<aside class="template-list">
|
||||
<div class="template-list-head">
|
||||
<span>单条模板列表</span>
|
||||
<el-button size="mini" type="primary" icon="el-icon-plus" @click="createTemplateInManager">新增模板</el-button>
|
||||
</div>
|
||||
<button
|
||||
v-for="template in variableTemplates"
|
||||
:key="template.id"
|
||||
type="button"
|
||||
class="template-row"
|
||||
:class="{ active: selectedTemplateId === template.id }"
|
||||
@click="selectTemplateForEdit(template.id)"
|
||||
>
|
||||
<strong>{{ template.templateName }}</strong>
|
||||
<small>{{ template.description || '暂无说明' }}</small>
|
||||
</button>
|
||||
<div v-if="!variableTemplates.length" class="template-empty">暂无模板</div>
|
||||
</aside>
|
||||
|
||||
<section class="template-editor">
|
||||
<el-form label-width="104px">
|
||||
<el-form-item label="模板名称" required>
|
||||
<el-input v-model="templateForm.templateName" placeholder="请输入模板名称"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="模板说明">
|
||||
<el-input v-model="templateForm.description" placeholder="请输入模板说明"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<section class="template-editor">
|
||||
<el-form label-width="104px">
|
||||
<el-form-item label="模板名称" required>
|
||||
<el-input v-model="templateForm.templateName" placeholder="请输入模板名称"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="模板说明">
|
||||
<el-input v-model="templateForm.description" placeholder="请输入模板说明"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="template-field-title">模板字段</div>
|
||||
<div class="template-field-grid">
|
||||
<label v-for="field in templateEditableFields" :key="field" class="template-field">
|
||||
<span>{{ field }}</span>
|
||||
<el-input v-model="templateForm.fields[field]" size="mini"/>
|
||||
</label>
|
||||
<div class="template-field-title">模板字段</div>
|
||||
<div class="template-field-grid">
|
||||
<label v-for="field in templateEditableFields" :key="field" class="template-field">
|
||||
<span>{{ field }}</span>
|
||||
<el-input v-model="templateForm.fields[field]" size="mini"/>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="批量模板管理" name="batch">
|
||||
<div class="template-manager batch-template-manager">
|
||||
<aside class="template-list">
|
||||
<div class="template-list-head">
|
||||
<span>批量模板列表</span>
|
||||
<el-button size="mini" type="primary" icon="el-icon-upload2" @click="triggerBatchExcelImport">导入 Excel</el-button>
|
||||
</div>
|
||||
<button
|
||||
v-for="template in batchVariableTemplates"
|
||||
:key="template.id"
|
||||
type="button"
|
||||
class="template-row"
|
||||
:class="{ active: selectedBatchTemplateId === template.id }"
|
||||
@click="selectedBatchTemplateId = template.id"
|
||||
>
|
||||
<strong>{{ template.templateName }}</strong>
|
||||
<small>{{ template.sourceFileName || '外部 Excel' }} / {{ template.variables.length }} 条</small>
|
||||
</button>
|
||||
<div v-if="!batchVariableTemplates.length" class="template-empty">暂无批量模板</div>
|
||||
</aside>
|
||||
|
||||
<section class="template-editor batch-template-preview">
|
||||
<template v-if="selectedBatchTemplate">
|
||||
<div class="batch-template-summary">
|
||||
<strong>{{ selectedBatchTemplate.templateName }}</strong>
|
||||
<span>来源文件:{{ selectedBatchTemplate.sourceFileName || '未知' }}</span>
|
||||
<span>创建时间:{{ formatTime(selectedBatchTemplate.createdAt) }}</span>
|
||||
<span>变量数量:{{ selectedBatchTemplate.variables.length }}</span>
|
||||
<span v-if="selectedBatchTemplate.description">模板说明:{{ selectedBatchTemplate.description }}</span>
|
||||
</div>
|
||||
<el-table :data="selectedBatchTemplate.variables" border size="mini" height="420">
|
||||
<el-table-column prop="TagID" label="TagID" min-width="150" show-overflow-tooltip/>
|
||||
<el-table-column prop="TagName" label="TagName" min-width="210" show-overflow-tooltip/>
|
||||
<el-table-column prop="TagTypeID" label="TagTypeID" min-width="90"/>
|
||||
<el-table-column prop="TagLong" label="TagLong" min-width="80"/>
|
||||
<el-table-column prop="变量类型" label="变量类型" min-width="90"/>
|
||||
<el-table-column prop="变量地址" label="变量地址" min-width="90"/>
|
||||
<el-table-column prop="变量其它" label="变量其它" min-width="90"/>
|
||||
</el-table>
|
||||
</template>
|
||||
<div v-else class="batch-preview-empty">请选择或导入一个批量模板</div>
|
||||
</section>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<span slot="footer">
|
||||
<el-button type="danger" plain :disabled="!selectedTemplateId" @click="deleteTemplateInManager">删除</el-button>
|
||||
<el-button @click="templateDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveTemplateInManager">保存模板</el-button>
|
||||
<template v-if="templateManagerTab === 'single'">
|
||||
<el-button type="danger" plain :disabled="!selectedTemplateId" @click="deleteTemplateInManager">删除</el-button>
|
||||
<el-button :disabled="!selectedTemplateId" @click="cloneSelectedTemplate">克隆模板</el-button>
|
||||
<el-button @click="templateDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveTemplateInManager">保存模板</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button type="danger" plain :disabled="!selectedBatchTemplate" @click="deleteSelectedBatchTemplate">删除模板</el-button>
|
||||
<el-button :disabled="!selectedBatchTemplate" @click="cloneSelectedBatchTemplate">克隆模板</el-button>
|
||||
<el-button @click="templateDialogVisible = false">关闭</el-button>
|
||||
<el-button type="primary" :disabled="!selectedBatchTemplate" @click="applySelectedBatchTemplate">使用批量模板</el-button>
|
||||
</template>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
title="导入 Excel 为批量模板"
|
||||
:visible.sync="batchImportDialogVisible"
|
||||
width="620px"
|
||||
>
|
||||
<div v-if="batchImportCandidate" class="batch-import-summary">
|
||||
<span>来源文件:{{ batchImportCandidate.sourceFileName }}</span>
|
||||
<span>读取 Sheet:{{ batchImportCandidate.sheetName }}</span>
|
||||
<span>有效变量数:{{ batchImportCandidate.variableCount }}</span>
|
||||
<span>忽略空行:{{ batchImportCandidate.ignoredEmptyRows }}</span>
|
||||
<span>忽略未知列:{{ batchImportCandidate.ignoredUnknownColumns }}</span>
|
||||
</div>
|
||||
<el-form label-width="96px">
|
||||
<el-form-item label="模板名称" required>
|
||||
<el-input v-model="batchImportForm.templateName" placeholder="请输入批量模板名称"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="模板说明">
|
||||
<el-input v-model="batchImportForm.description" placeholder="请输入模板说明"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer">
|
||||
<el-button @click="batchImportDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveImportedBatchTemplate">保存为批量模板</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -290,6 +375,7 @@
|
||||
<script>
|
||||
import defaultData from '@/data/default-signal-table.json'
|
||||
import defaultVariableTemplates from '@/data/default-variable-templates.json'
|
||||
import { importBatchTemplateFromExcel } from '@/utils/excelTemplateImporter'
|
||||
import { exportSignalTable } from '@/utils/excelWriter'
|
||||
import {
|
||||
MES_HEADERS,
|
||||
@@ -306,8 +392,13 @@ import {
|
||||
} from '@/utils/signalTableBuilder'
|
||||
import {
|
||||
applyTemplateToVariableForm,
|
||||
batchTemplateNameExists,
|
||||
createEmptyTemplate,
|
||||
createTemplateFromVariable,
|
||||
createBatchVariableTemplate,
|
||||
findBatchTemplateTagIdConflicts,
|
||||
getCopyTemplateName,
|
||||
normalizeBatchTemplateVariables,
|
||||
normalizeBatchVariableTemplates,
|
||||
normalizeVariableTemplates,
|
||||
sanitizeTemplateForSave,
|
||||
templateHasFields,
|
||||
@@ -320,7 +411,7 @@ import {
|
||||
loadConfigFromOpfs,
|
||||
saveConfigToOpfs
|
||||
} from '@/utils/opfsStore'
|
||||
import { isAbortError, openJsonFile } from '@/utils/filePicker'
|
||||
import { isAbortError, openExcelFile, openJsonFile } from '@/utils/filePicker'
|
||||
|
||||
function createEmptyDevice() {
|
||||
return {
|
||||
@@ -359,6 +450,7 @@ function createTemplateEditorForm(template) {
|
||||
function normalizeConfigData(data) {
|
||||
const normalized = normalizeData(data)
|
||||
normalized.variableTemplates = normalizeVariableTemplates(normalized.variableTemplates, defaultVariableTemplates)
|
||||
normalized.batchVariableTemplates = normalizeBatchVariableTemplates(normalized.batchVariableTemplates)
|
||||
return normalized
|
||||
}
|
||||
|
||||
@@ -391,9 +483,17 @@ export default {
|
||||
variableColumns: MES_HEADERS,
|
||||
variableGroups: VARIABLE_FIELD_GROUPS,
|
||||
templateDialogVisible: false,
|
||||
templateManagerTab: 'single',
|
||||
selectedTemplateId: normalized.variableTemplates[0] ? normalized.variableTemplates[0].id : '',
|
||||
templateForm: createTemplateEditorForm(normalized.variableTemplates[0]),
|
||||
templateEditableFields: TEMPLATE_EDITABLE_FIELDS,
|
||||
selectedBatchTemplateId: normalized.batchVariableTemplates[0] ? normalized.batchVariableTemplates[0].id : '',
|
||||
batchImportDialogVisible: false,
|
||||
batchImportCandidate: null,
|
||||
batchImportForm: {
|
||||
templateName: '',
|
||||
description: ''
|
||||
},
|
||||
variableRules: {
|
||||
TagID: [{ required: true, message: '请输入TagID', trigger: 'blur' }],
|
||||
TagName: [{ required: true, message: '请输入TagName', trigger: 'blur' }],
|
||||
@@ -424,6 +524,12 @@ export default {
|
||||
variableTemplates() {
|
||||
return Array.isArray(this.tableData.variableTemplates) ? this.tableData.variableTemplates : []
|
||||
},
|
||||
batchVariableTemplates() {
|
||||
return Array.isArray(this.tableData.batchVariableTemplates) ? this.tableData.batchVariableTemplates : []
|
||||
},
|
||||
selectedBatchTemplate() {
|
||||
return this.batchVariableTemplates.find(template => template.id === this.selectedBatchTemplateId) || null
|
||||
},
|
||||
filteredDevices() {
|
||||
const key = this.keyword.trim().toLowerCase()
|
||||
if (!key) return this.devices
|
||||
@@ -692,30 +798,6 @@ export default {
|
||||
})
|
||||
this.$message.success('已使用模板填充空字段')
|
||||
},
|
||||
saveVariableFormAsTemplate() {
|
||||
this.$prompt('请输入模板名称', '保存为模板', {
|
||||
confirmButtonText: '保存',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /.+/,
|
||||
inputErrorMessage: '模板名称不能为空'
|
||||
}).then(async ({ value }) => {
|
||||
const name = String(value || '').trim()
|
||||
if (templateNameExists(this.variableTemplates, name)) {
|
||||
this.$message.warning('模板名称已存在')
|
||||
return
|
||||
}
|
||||
|
||||
const template = createTemplateFromVariable(this.variableForm, name)
|
||||
if (!templateHasFields(template)) {
|
||||
this.$message.warning('当前变量没有可保存的模板字段')
|
||||
return
|
||||
}
|
||||
|
||||
this.tableData.variableTemplates.push(template)
|
||||
this.variableTemplateSelectId = template.id
|
||||
await this.persistConfigSafely(true)
|
||||
}).catch(() => {})
|
||||
},
|
||||
openTemplateManager() {
|
||||
if (!this.variableTemplates.length) {
|
||||
this.selectedTemplateId = ''
|
||||
@@ -726,6 +808,9 @@ export default {
|
||||
} else {
|
||||
this.templateForm = createTemplateEditorForm(this.getTemplateById(this.selectedTemplateId))
|
||||
}
|
||||
if (this.batchVariableTemplates.length && !this.selectedBatchTemplate) {
|
||||
this.selectedBatchTemplateId = this.batchVariableTemplates[0].id
|
||||
}
|
||||
this.templateDialogVisible = true
|
||||
},
|
||||
selectTemplateForEdit(id) {
|
||||
@@ -780,6 +865,166 @@ export default {
|
||||
this.persistConfigSafely(true)
|
||||
}).catch(() => {})
|
||||
},
|
||||
async cloneSelectedTemplate() {
|
||||
const template = this.getTemplateById(this.selectedTemplateId)
|
||||
if (!template) return
|
||||
|
||||
const copy = {
|
||||
id: `tpl-${Date.now()}`,
|
||||
templateName: getCopyTemplateName(this.variableTemplates, template.templateName),
|
||||
description: template.description || '',
|
||||
fields: cloneData(template.fields || {})
|
||||
}
|
||||
|
||||
this.tableData.variableTemplates.push(copy)
|
||||
this.selectedTemplateId = copy.id
|
||||
this.templateForm = createTemplateEditorForm(copy)
|
||||
await this.persistConfigSafely(true)
|
||||
},
|
||||
async triggerBatchExcelImport() {
|
||||
try {
|
||||
const file = await openExcelFile()
|
||||
if (file) {
|
||||
await this.importBatchExcelFile(file)
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
if (isAbortError(error)) return
|
||||
this.$alert(error && error.message ? error.message : String(error), '打开 Excel 失败', {
|
||||
type: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.$refs.batchExcelFileInput.click()
|
||||
},
|
||||
async handleBatchExcelInput(event) {
|
||||
const file = event.target.files && event.target.files[0]
|
||||
event.target.value = ''
|
||||
if (!file) return
|
||||
|
||||
await this.importBatchExcelFile(file)
|
||||
},
|
||||
async importBatchExcelFile(file) {
|
||||
try {
|
||||
const candidate = await importBatchTemplateFromExcel(file)
|
||||
this.batchImportCandidate = candidate
|
||||
this.batchImportForm = {
|
||||
templateName: String(file.name || '批量模板').replace(/\.(xlsx|xls)$/i, ''),
|
||||
description: '从外部 Excel 导入'
|
||||
}
|
||||
this.batchImportDialogVisible = true
|
||||
} catch (error) {
|
||||
this.$alert(error && error.message ? error.message : String(error), '导入 Excel 失败', {
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
},
|
||||
async saveImportedBatchTemplate() {
|
||||
if (!this.batchImportCandidate) return
|
||||
const name = String(this.batchImportForm.templateName || '').trim()
|
||||
if (!name) {
|
||||
this.$message.warning('批量模板名称不能为空')
|
||||
return
|
||||
}
|
||||
if (batchTemplateNameExists(this.batchVariableTemplates, name)) {
|
||||
this.$message.warning('批量模板名称不能重复')
|
||||
return
|
||||
}
|
||||
|
||||
const variables = normalizeBatchTemplateVariables(this.batchImportCandidate.variables)
|
||||
const duplicateTagIds = this.validateBatchTemplateUniqueTagIds(variables)
|
||||
if (duplicateTagIds.length) {
|
||||
this.$alert(`批量模板中存在重复基础 TagID:${duplicateTagIds.join('、')}`, '保存失败', {
|
||||
type: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const template = createBatchVariableTemplate({
|
||||
...this.batchImportCandidate,
|
||||
variables
|
||||
}, name, this.batchImportForm.description)
|
||||
|
||||
this.tableData.batchVariableTemplates.push(template)
|
||||
this.selectedBatchTemplateId = template.id
|
||||
this.batchImportDialogVisible = false
|
||||
this.batchImportCandidate = null
|
||||
this.templateManagerTab = 'batch'
|
||||
await this.persistConfigSafely(true)
|
||||
},
|
||||
validateBatchTemplateUniqueTagIds(variables) {
|
||||
const counts = {}
|
||||
;(variables || []).forEach(variable => {
|
||||
const tagId = String(variable.TagID || '').trim()
|
||||
if (!tagId) return
|
||||
counts[tagId] = (counts[tagId] || 0) + 1
|
||||
})
|
||||
return Object.keys(counts).filter(tagId => counts[tagId] > 1)
|
||||
},
|
||||
deleteSelectedBatchTemplate() {
|
||||
if (!this.selectedBatchTemplate) return
|
||||
this.$confirm(`确认删除批量模板「${this.selectedBatchTemplate.templateName}」?`, '提示', {
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const index = this.batchVariableTemplates.findIndex(template => template.id === this.selectedBatchTemplateId)
|
||||
if (index !== -1) this.tableData.batchVariableTemplates.splice(index, 1)
|
||||
const next = this.batchVariableTemplates[0]
|
||||
this.selectedBatchTemplateId = next ? next.id : ''
|
||||
this.persistConfigSafely(true)
|
||||
}).catch(() => {})
|
||||
},
|
||||
async cloneSelectedBatchTemplate() {
|
||||
if (!this.selectedBatchTemplate) return
|
||||
|
||||
const copy = {
|
||||
id: `batch-tpl-${Date.now()}`,
|
||||
templateName: getCopyTemplateName(this.batchVariableTemplates, this.selectedBatchTemplate.templateName),
|
||||
description: this.selectedBatchTemplate.description || '',
|
||||
sourceFileName: this.selectedBatchTemplate.sourceFileName || '',
|
||||
createdAt: new Date().toISOString(),
|
||||
variables: normalizeBatchTemplateVariables(cloneData(this.selectedBatchTemplate.variables || []))
|
||||
}
|
||||
|
||||
this.tableData.batchVariableTemplates.push(copy)
|
||||
this.selectedBatchTemplateId = copy.id
|
||||
await this.persistConfigSafely(true)
|
||||
},
|
||||
async applySelectedBatchTemplate() {
|
||||
if (!this.selectedDevice) {
|
||||
this.$message.warning('请先选择设备模块,再使用批量模板。')
|
||||
return
|
||||
}
|
||||
if (!this.selectedBatchTemplate) return
|
||||
|
||||
const deviceIndex = this.devices.findIndex(device => device.id === this.selectedDeviceId)
|
||||
if (deviceIndex === -1) return
|
||||
const variables = this.devices[deviceIndex].variables || []
|
||||
const templateVariables = normalizeBatchTemplateVariables(this.selectedBatchTemplate.variables)
|
||||
const duplicateTagIds = this.validateBatchTemplateUniqueTagIds(templateVariables)
|
||||
if (duplicateTagIds.length) {
|
||||
this.$alert(`批量模板中存在重复基础 TagID:${duplicateTagIds.join('、')}`, '无法使用批量模板', {
|
||||
type: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const conflicts = findBatchTemplateTagIdConflicts(variables, templateVariables)
|
||||
const startIndex = conflicts.length ? 0 : variables.length
|
||||
const importedVariables = templateVariables.map((variable, index) => ({
|
||||
...variable,
|
||||
'变量排序': String(startIndex + index)
|
||||
}))
|
||||
const nextVariables = conflicts.length ? importedVariables : variables.concat(importedVariables)
|
||||
|
||||
this.$set(this.tableData.devices[deviceIndex], 'variables', nextVariables)
|
||||
this.selectedVariableIndex = startIndex
|
||||
this.selectedDisplayVariable = null
|
||||
this.$message.success(conflicts.length
|
||||
? `检测到 TagID 冲突,已清空当前设备模块原变量并导入 ${templateVariables.length} 条变量`
|
||||
: `已追加 ${templateVariables.length} 条变量`)
|
||||
await this.persistConfigSafely(true)
|
||||
},
|
||||
applyChangedVariableFields(baseVariable, originalForm, currentForm) {
|
||||
const next = this.stripSharedVariablePreviewFields(baseVariable)
|
||||
|
||||
@@ -886,7 +1131,7 @@ export default {
|
||||
try {
|
||||
const exportData = buildSingleDeviceExportData(this.tableData, this.selectedDevice.id)
|
||||
const result = await exportSignalTable(exportData)
|
||||
this.$message.success(result.method === 'picker' ? '已保存 SignalTable_CZ.xlsx' : '已下载 SignalTable_CZ.xlsx')
|
||||
this.$message.success(result.method === 'picker' ? '已保存 SignalTable.xlsx' : '已下载 SignalTable.xlsx')
|
||||
} catch (error) {
|
||||
if (isAbortError(error)) return
|
||||
this.$alert(error && error.message ? error.message : String(error), '导出失败', {
|
||||
@@ -1295,6 +1540,37 @@ export default {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.batch-template-manager {
|
||||
min-height: 560px;
|
||||
}
|
||||
|
||||
.batch-template-preview {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.batch-template-summary,
|
||||
.batch-import-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
margin-bottom: 12px;
|
||||
color: #526273;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.batch-template-summary strong {
|
||||
color: #1f2d3d;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.batch-preview-empty {
|
||||
height: 420px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8b98a8;
|
||||
}
|
||||
|
||||
.template-field-title {
|
||||
margin: 8px 0 10px;
|
||||
font-weight: 650;
|
||||
@@ -1334,37 +1610,26 @@ export default {
|
||||
column-gap: 12px;
|
||||
}
|
||||
|
||||
.dialog-plus {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
margin: -6px 0 12px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #f04b52;
|
||||
font-size: 28px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dialog-plus:hover {
|
||||
color: #d9363e;
|
||||
}
|
||||
|
||||
.dialog-station-section {
|
||||
margin-top: 4px;
|
||||
border-top: 1px solid #e3e8ef;
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
.dialog-station-head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
margin-bottom: 10px;
|
||||
color: #1f2d3d;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.dialog-station-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.dialog-station-head small {
|
||||
color: #8b98a8;
|
||||
font-weight: 400;
|
||||
|
||||
Reference in New Issue
Block a user