From 4929b78434e2d1603536e90d86422a0275c13d2e Mon Sep 17 00:00:00 2001 From: ZhangAo Date: Thu, 30 Jul 2026 17:30:13 +0800 Subject: [PATCH] 20260730173012 --- .../field-name-sync-from-old-db_f94a2b08.md | 117 +++++ code/fms/fms-api/CheckEdicode.class | Bin 0 -> 5500 bytes .../migrations/003_create_login_log.sql | 18 +- .../migrations/022_add_otherdata_module.sql | 446 +++++++++++++++++ .../023_add_otherdata_bmfl_module_code.sql | 78 +++ .../fmsapi/service/LoginLogService.java | 4 +- .../fmsapi/service/LoginLogService.class | Bin 4072 -> 4072 bytes .../tools/migration/FieldNameSync.java | 264 ++++++++++ code/fms/fms-api/tools/migration/README.md | 112 +++++ .../fms-module-list/FmsModuleListPage.vue | 49 +- .../fms-module-list/FmsModuleTable.vue | 7 +- .../src/components/fms-table/FmsTable.vue | 24 +- .../utils/createEditableCellRenderer.js | 43 +- .../src/layouts/components/AppSidebar.vue | 28 ++ code/fms/fms-vue/src/router/routes.js | 35 +- .../fms-vue/src/views/base/contact/index.vue | 25 + .../src/views/base/othercompany/index.vue | 2 +- .../src/views/base/otherdata/index.vue | 456 ++++++++++++++++++ .../components/ModuleEditConfigPanel.vue | 15 +- .../components/ModuleI18nPanel.vue | 11 +- .../components/ModuleListConfigPanel.vue | 22 +- .../components/ModuleQueryConfigPanel.vue | 15 +- .../src/views/module-management/index.vue | 66 ++- .../src/views/system/login-log/index.vue | 2 +- .../fms-vue/src/views/system/user/index.vue | 25 + 25 files changed, 1796 insertions(+), 68 deletions(-) create mode 100644 code/fms/.codebuddy/plans/field-name-sync-from-old-db_f94a2b08.md create mode 100644 code/fms/fms-api/CheckEdicode.class create mode 100644 code/fms/fms-api/config/migrations/022_add_otherdata_module.sql create mode 100644 code/fms/fms-api/config/migrations/023_add_otherdata_bmfl_module_code.sql create mode 100644 code/fms/fms-api/tools/migration/FieldNameSync.java create mode 100644 code/fms/fms-api/tools/migration/README.md create mode 100644 code/fms/fms-vue/src/views/base/contact/index.vue create mode 100644 code/fms/fms-vue/src/views/base/otherdata/index.vue create mode 100644 code/fms/fms-vue/src/views/system/user/index.vue diff --git a/code/fms/.codebuddy/plans/field-name-sync-from-old-db_f94a2b08.md b/code/fms/.codebuddy/plans/field-name-sync-from-old-db_f94a2b08.md new file mode 100644 index 00000000..55ac19cc --- /dev/null +++ b/code/fms/.codebuddy/plans/field-name-sync-from-old-db_f94a2b08.md @@ -0,0 +1,117 @@ +--- +name: field-name-sync-from-old-db +overview: 创建可复用的字段名同步服务,从旧库 s_columnLib.col_Caption 同步到新库 s_module_field.b_name,按视图名和字段名匹配,首批执行 v_b_othercompany。 +todos: + - id: fix-old-db-ssl + content: 修复 g3hd-old.properties:移除 sslProtocol=TLSv1 参数,JDK 21 驱动自动协商 TLSv1.2 即可连通 + status: completed + - id: create-sync-service + content: 新建 FieldNameSyncService.java:实现跨库 UPDATE SQL,按 viewName 匹配 s_columnLib,将 col_Caption 写入 s_module_field.b_name,不匹配字段不改动,返回更新行数 + status: completed + dependencies: + - fix-old-db-ssl + - id: create-admin-controller + content: 新建 AdminController.java,添加 POST /api/admin/sync-field-names 端点,JWT 认证,viewName 参数白名单校验,调用 FieldNameSyncService 并返回 ApiResponse + status: completed + dependencies: + - create-sync-service + - id: add-sql-migration-ref + content: 新建 023_sync_field_names.sql 参考脚本,记录跨库同步 SQL 作为纯 SQL 手动执行备选方案 + status: completed +--- + +## 用户需求 + +构建一个可复用的工具,将旧库 `G3HY2025.dbo.s_columnLib` 中存储的字段中文名称(`col_Caption`)同步到新库 `fms.dbo.s_module_field.b_name`。 + +### 匹配规则 + +- `s_module.b_viewtable` = `s_columnLib.col_TableName`(模块视图名对应旧库表名) +- `s_module_field.b_field` = `s_columnLib.col_FieldName`(字段名精确匹配) +- 匹配不上的字段,`b_name` 保持不变 + +### 首批同步目标 + +视图 `v_b_othercompany` + +### 可复用要求 + +以后还有更多视图需要同步,工具须参数化视图名,方便批量复用。 + +## 技术方案 + +### 方案选择:跨库 SQL + 管理端点 + +**核心决策**:新旧库在同一 SQL Server 实例(`118.89.70.199:1433`),使用**跨数据库 SQL** 直接从新库连接执行 `UPDATE ... FROM G3HY2025.dbo.s_columnLib`,无需单独连接旧库。 + +```sql +UPDATE sf +SET sf.b_name = LTRIM(RTRIM(sc.col_Caption)) +FROM dbo.s_module_field sf +JOIN dbo.s_module m ON m.b_id = sf.b_module_id +JOIN G3HY2025.dbo.s_columnLib sc + ON sc.col_TableName = m.b_viewtable + AND sc.col_FieldName = sf.b_field +WHERE m.b_viewtable = ? + AND sc.col_Caption IS NOT NULL + AND LTRIM(RTRIM(sc.col_Caption)) != ''; +``` + +**为何需要新端点而非复用通用端点**: + +- 通用 `saveObjectApi` 只能操作当前 org 数据库的单表 insert/update/delete,无法表达跨库 JOIN 逻辑 +- 通用 `loadDataApi` 只能查询当前 org 的表,无法读取旧库 `s_columnLib` +- 此操作为管理类批量数据同步,不在业务 CRUD 范畴内 + +### 实现架构 + +```mermaid +flowchart LR + A[前端/管理员] -->|POST /api/admin/sync-field-names| B[AdminController] + B -->|校验 viewName 参数| C[FieldNameSyncService] + C -->|获取新库 Connection| D[OrgRoutingDataSource] + D -->|fms 连接| E[(SQL Server)] + E -->|跨库 JOIN| F[G3HY2025.dbo.s_columnLib] + E -->|UPDATE| G[fms.dbo.s_module_field] + C -->|返回同步数量| B + B -->|ApiResponse| A +``` + +### 模块划分 + +| 模块 | 文件 | 职责 | +| --- | --- | --- | +| 配置修复 | `config/dbconfigs/g3hd-old.properties` | 移除 `sslProtocol=TLSv1` | +| 同步服务 | `service/FieldNameSyncService.java` | 执行跨库 UPDATE,校验参数,返回影响行数 | +| 管理控制器 | `controller/AdminController.java` | `POST /api/admin/sync-field-names` 端点,JWT 保护 | +| 参考脚本 | `config/migrations/023_sync_field_names.sql` | 纯 SQL 版本供手动执行 | + + +### 数据流 + +1. 前端/管理员发送 `POST /api/admin/sync-field-names`,请求体 `{ "viewName": "v_b_othercompany" }` +2. `AdminController` 校验 `viewName` 为合法标识符(字母/下划线/数字,最长128) +3. `FieldNameSyncService` 通过 `@Resource DataSource` 获取新库连接 +4. 执行跨库 PreparedStatement UPDATE +5. 返回 `{ "updatedCount": 42, "message": "v_b_othercompany 同步完成,共更新 42 个字段名称" }` + +### 安全措施 + +- `viewName` 参数:白名单正则 `^[A-Za-z_][A-Za-z0-9_]{0,127},校验后作为 PreparedStatement 参数绑定(非字符串拼接) +- JWT 认证:`AdminController` 路径 `/api/admin/**` 经 `JwtAuthFilter` 保护,仅登录用户可调用 +- 失败不影响现有数据:UPDATE 在一条 SQL 内执行,单条匹配失败不阻断其他行 + +### 执行说明 + +- **热路径**:单条 UPDATE SQL 跨库 JOIN,性能取决于 `s_module_field` 行数和索引;对单个视图(~50 字段)毫秒级完成 +- **日志**:服务层记录同步开始/完成/失败日志,使用 `@Slf4j`,级别 INFO/ERROR +- **事务**:使用 Spring 声明式事务 `@Transactional`,确保整批同步原子提交或回滚 +- **向后兼容**:不修改现有 `DataController` 和通用端点,不影响业务数据流 + +## Agent Extensions + +### SubAgent + +- **code-explorer** +- 目的:探索旧库连接方式、现有 Service/Controller 模式、参数校验惯例 +- 预期结果:确认 `OrgDataSourceManager` 的 API 使用方式和现有异常处理/日志模式 \ No newline at end of file diff --git a/code/fms/fms-api/CheckEdicode.class b/code/fms/fms-api/CheckEdicode.class new file mode 100644 index 0000000000000000000000000000000000000000..827067a6a9ef862180d62589eb1e9a0eba7f71b6 GIT binary patch literal 5500 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 IeE~oJ50t9U!T tableOp = new LinkedHashMap<>(); - tableOp.put("table", "s_log_login"); + tableOp.put("table", "s_login_log"); tableOp.put("key_field", "b_id"); tableOp.put("inserts", List.of(row)); diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/LoginLogService.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/LoginLogService.class index f0513ed5937eff4b7ae79135561ad2ee65d6f549..3cdce4b95d5b476fe8c7ebcc29f964169182e5b0 100644 GIT binary patch delta 19 acmaDM|3ZGl8dkQ_f&A0$my$6^8 diff --git a/code/fms/fms-api/tools/migration/FieldNameSync.java b/code/fms/fms-api/tools/migration/FieldNameSync.java new file mode 100644 index 00000000..65ce7661 --- /dev/null +++ b/code/fms/fms-api/tools/migration/FieldNameSync.java @@ -0,0 +1,264 @@ +import java.sql.*; + +/** + * 从旧库 G3HY2025.dbo.s_columnLib 同步到新库 fms: + * 1. col_Caption → s_module_field.b_name(字段名称) + * 2. col_Visible → s_module_field_list.b_visible(列表显示,不存在则插入记录) + * 3. col_edit → s_module_field_edit.b_visible(编辑显示,不存在则插入记录) + * + * 用法: java -cp ".;mssql-jdbc.jar" FieldNameSync + */ +public class FieldNameSync { + + static final String URL = "jdbc:sqlserver://118.89.70.199:1433;databaseName=fms;encrypt=true;trustServerCertificate=true"; + static final String USER = "sa"; + static final String PASSWORD = "zhang520.."; + + // 同步字段名称 + static final String SYNC_NAME = """ + UPDATE sf + SET sf.b_name = LTRIM(RTRIM(sc.col_Caption)) + FROM dbo.s_module_field sf + JOIN dbo.s_module m ON m.b_id = sf.b_module_id + JOIN G3HY2025.dbo.s_columnLib sc + ON sc.col_TableName COLLATE Chinese_PRC_CI_AS = m.b_viewtable + AND sc.col_FieldName COLLATE Chinese_PRC_CI_AS = sf.b_field + WHERE m.b_viewtable = ? + AND sc.col_Caption IS NOT NULL + AND LTRIM(RTRIM(sc.col_Caption)) != '' + """; + + // 同步列表显示:更新已有记录 + static final String SYNC_LIST_UPDATE = """ + UPDATE sfl + SET sfl.b_visible = CASE WHEN sc.col_Visible = '1' THEN 1 ELSE 0 END + FROM dbo.s_module_field_list sfl + JOIN dbo.s_module_field sf ON sf.b_id = sfl.b_field_id + JOIN dbo.s_module m ON m.b_id = sf.b_module_id + JOIN G3HY2025.dbo.s_columnLib sc + ON sc.col_TableName COLLATE Chinese_PRC_CI_AS = m.b_viewtable + AND sc.col_FieldName COLLATE Chinese_PRC_CI_AS = sf.b_field + WHERE m.b_viewtable = ? + AND sc.col_Visible IS NOT NULL + AND LTRIM(RTRIM(sc.col_Visible)) != '' + """; + + // 查询需要插入列表配置的字段 + static final String LIST_INSERT_CANDIDATES = """ + SELECT sf.b_id AS field_id, m.b_id AS module_id, + CASE WHEN sc.col_Visible = '1' THEN 1 ELSE 0 END AS visible + FROM dbo.s_module_field sf + JOIN dbo.s_module m ON m.b_id = sf.b_module_id + JOIN G3HY2025.dbo.s_columnLib sc + ON sc.col_TableName COLLATE Chinese_PRC_CI_AS = m.b_viewtable + AND sc.col_FieldName COLLATE Chinese_PRC_CI_AS = sf.b_field + WHERE m.b_viewtable = ? + AND sc.col_Visible IS NOT NULL + AND LTRIM(RTRIM(sc.col_Visible)) != '' + AND NOT EXISTS ( + SELECT 1 FROM dbo.s_module_field_list sfl + WHERE sfl.b_field_id = sf.b_id + ) + """; + + static final String INSERT_LIST = """ + INSERT INTO dbo.s_module_field_list (b_id, b_module_id, b_field_id, b_visible, b_xh) + VALUES (?, ?, ?, ?, 0) + """; + + // 同步编辑显示:更新已有记录 + static final String SYNC_EDIT_UPDATE = """ + UPDATE sfe + SET sfe.b_visible = CASE WHEN sc.col_edit = '1' THEN 1 ELSE 0 END + FROM dbo.s_module_field_edit sfe + JOIN dbo.s_module_field sf ON sf.b_id = sfe.b_field_id + JOIN dbo.s_module m ON m.b_id = sf.b_module_id + JOIN G3HY2025.dbo.s_columnLib sc + ON sc.col_TableName COLLATE Chinese_PRC_CI_AS = m.b_viewtable + AND sc.col_FieldName COLLATE Chinese_PRC_CI_AS = sf.b_field + WHERE m.b_viewtable = ? + AND sc.col_edit IS NOT NULL + AND LTRIM(RTRIM(sc.col_edit)) != '' + """; + + static final String EDIT_INSERT_CANDIDATES = """ + SELECT sf.b_id AS field_id, m.b_id AS module_id, + CASE WHEN sc.col_edit = '1' THEN 1 ELSE 0 END AS visible + FROM dbo.s_module_field sf + JOIN dbo.s_module m ON m.b_id = sf.b_module_id + JOIN G3HY2025.dbo.s_columnLib sc + ON sc.col_TableName COLLATE Chinese_PRC_CI_AS = m.b_viewtable + AND sc.col_FieldName COLLATE Chinese_PRC_CI_AS = sf.b_field + WHERE m.b_viewtable = ? + AND sc.col_edit IS NOT NULL + AND LTRIM(RTRIM(sc.col_edit)) != '' + AND NOT EXISTS ( + SELECT 1 FROM dbo.s_module_field_edit sfe + WHERE sfe.b_field_id = sf.b_id + ) + """; + + static final String INSERT_EDIT = """ + INSERT INTO dbo.s_module_field_edit (b_id, b_module_id, b_field_id, b_visible, b_required, b_readonly, b_disabled, b_xh) + VALUES (?, ?, ?, ?, 0, 0, 0, 0) + """; + + // 查询模块 ID + static final String QUERY_MODULE_ID = "SELECT b_id FROM dbo.s_module WHERE b_viewtable = ?"; + + // 查询列表/编辑配置现状 + static final String QUERY_CONFIG = """ + SELECT sf.b_field, sf.b_name, + sfl.b_visible AS list_visible, + sfe.b_visible AS edit_visible + FROM dbo.s_module_field sf + JOIN dbo.s_module m ON m.b_id = sf.b_module_id + LEFT JOIN dbo.s_module_field_list sfl ON sfl.b_field_id = sf.b_id + LEFT JOIN dbo.s_module_field_edit sfe ON sfe.b_field_id = sf.b_id + WHERE m.b_viewtable = ? + ORDER BY sf.b_field + """; + + static long nextListId; + static long nextEditId; + + public static void main(String[] args) throws Exception { + if (args.length == 0) { + System.err.println("用法: java FieldNameSync "); + System.exit(1); + } + String viewName = args[0].trim(); + if (!viewName.matches("^[A-Za-z_][A-Za-z0-9_]{0,127}$")) { + System.err.println("非法视图名: " + viewName); + System.exit(1); + } + + System.out.println("连接到 fms 数据库..."); + try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD)) { + conn.setAutoCommit(false); + + // 查询模块是否存在 + Long moduleId = null; + try (PreparedStatement ps = conn.prepareStatement(QUERY_MODULE_ID)) { + ps.setString(1, viewName); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) moduleId = rs.getLong(1); + } + } + if (moduleId == null) { + System.err.println("模块 " + viewName + " 不存在!"); + System.exit(1); + } + + // 初始化 ID 生成器 + try (Statement st = conn.createStatement()) { + try (ResultSet rs = st.executeQuery("SELECT ISNULL(MAX(b_id),0) FROM dbo.s_module_field_list")) { + rs.next(); nextListId = rs.getLong(1) + 1; + } + try (ResultSet rs = st.executeQuery("SELECT ISNULL(MAX(b_id),0) FROM dbo.s_module_field_edit")) { + rs.next(); nextEditId = rs.getLong(1) + 1; + } + } + + // === 1. 同步前 === + System.out.println("\n=== 同步前 ==="); + printConfig(conn, viewName); + + // === 2. 字段名称 === + System.out.println("\n=== 1) 字段名称 (col_Caption → b_name) ==="); + try (PreparedStatement ps = conn.prepareStatement(SYNC_NAME)) { + ps.setString(1, viewName); + System.out.println(" 更新: " + ps.executeUpdate() + " 条"); + } + + // === 3. 列表显示 === + System.out.println("\n=== 2) 列表显示 (col_Visible → s_module_field_list.b_visible) ==="); + try (PreparedStatement ps = conn.prepareStatement(SYNC_LIST_UPDATE)) { + ps.setString(1, viewName); + System.out.println(" 更新已有: " + ps.executeUpdate() + " 条"); + } + java.util.ArrayList listCandidates = new java.util.ArrayList<>(); + try (PreparedStatement ps = conn.prepareStatement(LIST_INSERT_CANDIDATES)) { + ps.setString(1, viewName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + listCandidates.add(new long[]{rs.getLong("field_id"), rs.getLong("module_id"), rs.getLong("visible")}); + } + } + } + try (PreparedStatement ps = conn.prepareStatement(INSERT_LIST)) { + for (long[] row : listCandidates) { + ps.setLong(1, nextListId++); + ps.setLong(2, row[1]); + ps.setLong(3, row[0]); + ps.setInt(4, (int) row[2]); + ps.addBatch(); + } + if (!listCandidates.isEmpty()) { + ps.executeBatch(); + } + } + System.out.println(" 新增: " + listCandidates.size() + " 条"); + + // === 4. 编辑显示 === + System.out.println("\n=== 3) 编辑显示 (col_edit → s_module_field_edit.b_visible) ==="); + try (PreparedStatement ps = conn.prepareStatement(SYNC_EDIT_UPDATE)) { + ps.setString(1, viewName); + System.out.println(" 更新已有: " + ps.executeUpdate() + " 条"); + } + java.util.ArrayList editCandidates = new java.util.ArrayList<>(); + try (PreparedStatement ps = conn.prepareStatement(EDIT_INSERT_CANDIDATES)) { + ps.setString(1, viewName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + editCandidates.add(new long[]{rs.getLong("field_id"), rs.getLong("module_id"), rs.getLong("visible")}); + } + } + } + try (PreparedStatement ps = conn.prepareStatement(INSERT_EDIT)) { + for (long[] row : editCandidates) { + ps.setLong(1, nextEditId++); + ps.setLong(2, row[1]); + ps.setLong(3, row[0]); + ps.setInt(4, (int) row[2]); + ps.addBatch(); + } + if (!editCandidates.isEmpty()) { + ps.executeBatch(); + } + } + System.out.println(" 新增: " + editCandidates.size() + " 条"); + + conn.commit(); + System.out.println("\n=== 事务已提交 ==="); + + // === 5. 验证 === + System.out.println("\n=== 同步后 ==="); + printConfig(conn, viewName); + } + + System.out.println("\n完成."); + } + + static void printConfig(Connection conn, String viewName) throws SQLException { + System.out.printf(" %-30s %-16s %-8s %-8s%n", "b_field", "b_name", "list", "edit"); + System.out.println(" " + "-".repeat(66)); + try (PreparedStatement ps = conn.prepareStatement(QUERY_CONFIG)) { + ps.setString(1, viewName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String listVis = rs.getString("list_visible"); + String editVis = rs.getString("edit_visible"); + String listStr = listVis == null ? "--" : ("1".equals(listVis) ? "显示" : "隐藏"); + String editStr = editVis == null ? "--" : ("1".equals(editVis) ? "显示" : "隐藏"); + String name = rs.getString("b_name"); + System.out.printf(" %-30s %-16s %-8s %-8s%n", + rs.getString("b_field"), + name != null ? name : "(null)", + listStr, + editStr); + } + } + } + } +} diff --git a/code/fms/fms-api/tools/migration/README.md b/code/fms/fms-api/tools/migration/README.md new file mode 100644 index 00000000..cadb7028 --- /dev/null +++ b/code/fms/fms-api/tools/migration/README.md @@ -0,0 +1,112 @@ +# FieldNameSync — 模块字段配置迁移工具 + +从旧库 `G3HY2025.dbo.s_columnLib` 同步字段配置到新库 `fms`。 + +## 同步的三项内容 + +| 序号 | 旧库来源 | 新库目标 | 说明 | +| --- | --- | --- | --- | +| 1 | `col_Caption` | `s_module_field.b_name` | 字段中文名称 | +| 2 | `col_Visible` | `s_module_field_list.b_visible` | 列表是否显示(1=显示, 0=隐藏) | +| 3 | `col_edit` | `s_module_field_edit.b_visible` | 编辑表单是否显示(1=显示, 0=隐藏) | + +## 匹配规则 + +通过视图名和字段名关联新旧库: + +``` +旧库 s_columnLib.col_TableName = 新库 s_module.b_viewtable +旧库 s_columnLib.col_FieldName = 新库 s_module_field.b_field +``` + +- 旧库字段匹配不上的,对应配置**不做修改**(保留原值) +- 列表配置或编辑配置记录不存在时,**自动插入**新记录 + +## 前置条件 + +### 1. JDK 21 + +工具使用 Java 文本块语法(`"""`),需要 JDK 21+ 编译。 + +### 2. mssql-jdbc 驱动 + +确保本地 Maven 仓库存在驱动 jar: +``` +%USERPROFILE%\.m2\repository\com\microsoft\sqlserver\mssql-jdbc\13.4.0.jre11\mssql-jdbc-13.4.0.jre11.jar +``` + +如果不存在,从项目根目录 `fms-api/` 执行 `mvnw dependency:resolve` 下载。 + +### 3. 数据库连接 + +工具硬编码连接 `118.89.70.199:1433`,数据库 `fms`,直连新旧两库(同一实例)。 + +### 4. 新库已有 `s_module_field_edit.b_visible` 列 + +如果报 `Invalid column name 'b_visible'`,执行以下 SQL 添加列: +```sql +ALTER TABLE dbo.s_module_field_edit ADD b_visible int NOT NULL DEFAULT 1; +``` + +### 5. 模块已在 s_module 中注册 + +目标模块的 `b_viewtable` 必须已在 `s_module` 表中配置,且已有 `s_module_field` 记录。 + +## 编译和运行 + +```bash +# 进入迁移工具目录 +cd fms-api/tools/migration + +# 设置 JDK 21 +export JAVA_HOME=/path/to/jdk-21 # Linux/Mac +set JAVA_HOME=D:\devtool\jdk\jdk-21 # Windows + +# 编译 +javac -cp "%USERPROFILE%\.m2\repository\com\microsoft\sqlserver\mssql-jdbc\13.4.0.jre11\mssql-jdbc-13.4.0.jre11.jar" FieldNameSync.java + +# 运行(以 v_b_othercompany 为例) +java -cp ".;%USERPROFILE%\.m2\repository\com\microsoft\sqlserver\mssql-jdbc\13.4.0.jre11\mssql-jdbc-13.4.0.jre11.jar" FieldNameSync v_b_othercompany +``` + +## 输出示例 + +``` +=== 同步前 === + b_field b_name list edit + ------------------------------------------------------------------ + b_id 系统编号 显示 显示 + b_name 公司全称 显示 显示 + ... + +=== 1) 字段名称 (col_Caption → b_name) === + 更新: 47 条 + +=== 2) 列表显示 (col_Visible → s_module_field_list.b_visible) === + 更新已有: 50 条 + 新增: 0 条 + +=== 3) 编辑显示 (col_edit → s_module_field_edit.b_visible) === + 更新已有: 44 条 + 新增: 0 条 + +=== 事务已提交 === +``` + +## 用 AI 助手调用 + +如果通过 AI 助手(如 CodeBuddy)执行,告诉 AI 以下步骤即可: + +> 帮我用 FieldNameSync 工具同步视图 `` 的字段配置。 +> +> 1. 进入 `fms-api/tools/migration/` 目录 +> 2. 用 JDK 21 编译:`javac -cp "mssql-jdbc 驱动 jar 路径" FieldNameSync.java` +> 3. 运行:`java -cp ".;驱动 jar 路径" FieldNameSync ` +> 4. 运行完毕后删除 `FieldNameSync.class` 文件 + +## 已有同步记录 + +| 视图名 | 同步日期 | 字段数 | 备注 | +| --- | --- | --- | --- | +| `v_b_othercompany` | 2026-07-30 | 50 | 已同步 | +| `v_b_contact` | 2026-07-30 | 36 | 已同步 | diff --git a/code/fms/fms-vue/src/components/fms-module-list/FmsModuleListPage.vue b/code/fms/fms-vue/src/components/fms-module-list/FmsModuleListPage.vue index 907dc7fa..04d6005c 100644 --- a/code/fms/fms-vue/src/components/fms-module-list/FmsModuleListPage.vue +++ b/code/fms/fms-vue/src/components/fms-module-list/FmsModuleListPage.vue @@ -1,5 +1,15 @@ + + + + diff --git a/code/fms/fms-vue/src/views/base/othercompany/index.vue b/code/fms/fms-vue/src/views/base/othercompany/index.vue index 733005f7..22540d69 100644 --- a/code/fms/fms-vue/src/views/base/othercompany/index.vue +++ b/code/fms/fms-vue/src/views/base/othercompany/index.vue @@ -390,7 +390,7 @@ onMounted(loadCategories);
diff --git a/code/fms/fms-vue/src/views/base/otherdata/index.vue b/code/fms/fms-vue/src/views/base/otherdata/index.vue new file mode 100644 index 00000000..4d67155a --- /dev/null +++ b/code/fms/fms-vue/src/views/base/otherdata/index.vue @@ -0,0 +1,456 @@ + + + + + diff --git a/code/fms/fms-vue/src/views/module-management/components/ModuleEditConfigPanel.vue b/code/fms/fms-vue/src/views/module-management/components/ModuleEditConfigPanel.vue index 89b4df4c..3779bc95 100644 --- a/code/fms/fms-vue/src/views/module-management/components/ModuleEditConfigPanel.vue +++ b/code/fms/fms-vue/src/views/module-management/components/ModuleEditConfigPanel.vue @@ -47,12 +47,12 @@ function onRowOrderChange(rows) { emitUpdate(); } -const availableFields = computed(() => { - const options = []; +const fieldNameMap = computed(() => { + const map = new Map(); for (const field of props.fields || []) { - options.push({ label: field.b_name || field.b_field, value: String(field.b_id) }); + map.set(String(field.b_id), field.b_name || field.b_field); } - return options; + return map; }); const groupOptions = computed(() => { @@ -119,9 +119,10 @@ const columns = shallowRef([ dataIndex: "b_field_id", title: "字段", width: 160, - editor: { - type: "select", - options: () => availableFields.value, + customCell: ({ row }) => { + const id = row.b_field_id; + if (id == null) return ""; + return fieldNameMap.value.get(String(id)) || id; }, }, { diff --git a/code/fms/fms-vue/src/views/module-management/components/ModuleI18nPanel.vue b/code/fms/fms-vue/src/views/module-management/components/ModuleI18nPanel.vue index 63b1b740..1d22ecfc 100644 --- a/code/fms/fms-vue/src/views/module-management/components/ModuleI18nPanel.vue +++ b/code/fms/fms-vue/src/views/module-management/components/ModuleI18nPanel.vue @@ -302,6 +302,15 @@ function flatten(rows) { return flat; } +// 由父级「同步字段」调用:移除匹配指定 _managedId 的透视行(字段定义已删除时清理残留翻译)。 +function removeByManagedIds(ids) { + if (!ids || ids.size === 0) return; + const idSet = new Set(Array.from(ids).map(String)); + pivotRows.value = pivotRows.value.filter( + (row) => !(row._managedId && idSet.has(String(row._managedId))), + ); +} + // 由父级「同步字段」调用:把默认翻译条目({ key, locale, value, managedId })并入草稿行。 // 已存在同 b_key 的行只补全空值(不覆盖用户配置),避免新增重复键;逻辑见 utils/i18n.mergeI18nDefaults。 function mergeDefaults(entries) { @@ -374,7 +383,7 @@ function markSaved() { originalsFlat.value = cloneData(flatten(pivotRows.value)); } -defineExpose({ buildSaveRequest, markSaved, mergeDefaults, refreshKey, reconcileManagedRows }); +defineExpose({ buildSaveRequest, markSaved, mergeDefaults, refreshKey, reconcileManagedRows, removeByManagedIds }); const displayedData = computed(() => { const kw = searchKey.value.toLowerCase(); diff --git a/code/fms/fms-vue/src/views/module-management/components/ModuleListConfigPanel.vue b/code/fms/fms-vue/src/views/module-management/components/ModuleListConfigPanel.vue index c3ac0aab..a0a39b2b 100644 --- a/code/fms/fms-vue/src/views/module-management/components/ModuleListConfigPanel.vue +++ b/code/fms/fms-vue/src/views/module-management/components/ModuleListConfigPanel.vue @@ -47,12 +47,12 @@ function onRowOrderChange(rows) { emitUpdate(); } -const availableFields = computed(() => { - const options = []; +const fieldNameMap = computed(() => { + const map = new Map(); for (const field of props.fields || []) { - options.push({ label: field.b_name || field.b_field, value: String(field.b_id) }); + map.set(String(field.b_id), field.b_name || field.b_field); } - return options; + return map; }); const groupOptions = computed(() => { @@ -119,12 +119,18 @@ const columns = shallowRef([ dataIndex: "b_field_id", title: "字段", width: 160, - editor: { - type: "select", - options: () => availableFields.value, + customCell: ({ row }) => { + const id = row.b_field_id; + if (id == null) return ""; + return fieldNameMap.value.get(String(id)) || id; }, }, - { dataIndex: "b_width", title: "列宽", width: 80 }, + { + dataIndex: "b_width", + title: "列宽", + width: 80, + editor: { type: "input" }, + }, { dataIndex: "b_group_id", title: "分组", diff --git a/code/fms/fms-vue/src/views/module-management/components/ModuleQueryConfigPanel.vue b/code/fms/fms-vue/src/views/module-management/components/ModuleQueryConfigPanel.vue index ea85198c..4f9b60b0 100644 --- a/code/fms/fms-vue/src/views/module-management/components/ModuleQueryConfigPanel.vue +++ b/code/fms/fms-vue/src/views/module-management/components/ModuleQueryConfigPanel.vue @@ -56,12 +56,12 @@ function onRowOrderChange(rows) { emitUpdate(); } -const availableFields = computed(() => { - const options = []; +const fieldNameMap = computed(() => { + const map = new Map(); for (const field of props.fields || []) { - options.push({ label: field.b_name || field.b_field, value: String(field.b_id) }); + map.set(String(field.b_id), field.b_name || field.b_field); } - return options; + return map; }); function addRow() { @@ -84,9 +84,10 @@ const columns = shallowRef([ dataIndex: "b_field_id", title: "字段", width: 160, - editor: { - type: "select", - options: () => availableFields.value, + customCell: ({ row }) => { + const id = row.b_field_id; + if (id == null) return ""; + return fieldNameMap.value.get(String(id)) || id; }, }, { diff --git a/code/fms/fms-vue/src/views/module-management/index.vue b/code/fms/fms-vue/src/views/module-management/index.vue index f955e905..3e1a79c1 100644 --- a/code/fms/fms-vue/src/views/module-management/index.vue +++ b/code/fms/fms-vue/src/views/module-management/index.vue @@ -38,6 +38,7 @@ const treeLoading = ref(false); const expandedTreeKeys = ref([]); const selectedModuleId = ref(null); const skipLoad = ref(false); +const expandTimer = ref(null); const activeTab = ref("basic"); function buildTreeContextMenu(node) { const items = []; @@ -442,24 +443,54 @@ async function loadModuleConfig(moduleId) { } // ---- 树节点选择 ---- +function clearExpandTimer() { + if (expandTimer.value != null) { + clearTimeout(expandTimer.value); + expandTimer.value = null; + } +} + function onTreeSelect(keys, info) { if (busy.value) return; if (info?.node?.isFmsRoot) { selectedModuleId.value = null; + clearExpandTimer(); return; } const nodeKey = info?.node?.key ?? keys[0]; if (nodeKey == null) return; const key = String(nodeKey); - selectedModuleId.value = key; - const node = findNode(key); - if (!node?.children?.length) return; + const hasChildren = node?.children?.length; + + clearExpandTimer(); + + if (!hasChildren) { + // 叶子节点:立即加载数据 + selectedModuleId.value = key; + return; + } + + // 父节点:处理展开/收起 const expandedKeys = expandedTreeKeys.value.map(String); - expandedTreeKeys.value = expandedKeys.includes(key) - ? expandedKeys.filter((item) => item !== key) - : [...expandedKeys, key]; + const isCurrentlyExpanded = expandedKeys.includes(key); + + if (isCurrentlyExpanded) { + // 收起:立即生效,同时加载数据 + expandedTreeKeys.value = expandedKeys.filter((item) => item !== key); + selectedModuleId.value = key; + } else { + // 展开:立即高亮选中节点,动画完毕后再加载数据 + expandedTreeKeys.value = [...expandedKeys, key]; + skipLoad.value = true; + selectedModuleId.value = key; // 立即高亮,watch 中 skipLoad=true 跳过加载 + expandTimer.value = setTimeout(() => { + expandTimer.value = null; + loadedModuleId.value = null; + void loadModuleConfig(key); + }, 300); + } } watch(selectedModuleId, (id) => { @@ -1009,11 +1040,26 @@ async function syncFields() { } } - // 视图中不存在的已有字段标记为不可用 + // 视图中不存在的已有字段:从字段定义中移除,并清理其在列表/编辑/查询配置中的残留行。 + const removedFieldIds = new Set(); for (const existing of existingFields) { const fieldName = existing.b_field ? existing.b_field.toLowerCase() : ""; if (!viewColumnSet.has(fieldName)) { - mergedFields.push({ ...existing, b_canuse: 0 }); + removedFieldIds.add(String(existing.b_id)); + } + } + if (removedFieldIds.size > 0) { + for (const section of ["listConfig", "editConfig", "queryConfig"]) { + drafts.value[section] = (drafts.value[section] || []).filter( + (row) => !removedFieldIds.has(String(row.b_field_id)), + ); + } + // 多语言面板中移除旧字段对应的受管翻译行(_managedId 格式为 field:) + if (i18nPanelRef.value) { + const managedIds = new Set( + Array.from(removedFieldIds).map((id) => `field:${id}`), + ); + i18nPanelRef.value.removeByManagedIds(managedIds); } } @@ -1125,12 +1171,8 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa :selected-keys="selectedModuleId ? [selectedModuleId] : []" :context-menu="buildTreeContextMenu" :context-menu-disabled="busy" - draggable - :allow-drop="allowDrop" @select="onTreeSelect" @menu-click="onTreeMenuClick" - @drop="onTreeDrop" - @drag-start="onTreeDragStart" >