Browse Source

支持批量下线,扫码线外人员资料

callm 3 days ago
parent
commit
d9f852a175

+ 103 - 0
UAS_MES_YDCY/.workbuddy/memory/2026-10-10.md

@@ -63,3 +63,106 @@ OperateResult.AppendText("<<" + emcode + " " + emname + " 在" + linecode + "上
 ## 待办
 - 需在 Visual Studio 中编译确认(本地无法编译)
 - DGV 刷新后勾选状态会因 `FillDgvWithDataTable` 重新绑定而清空,属预期
+
+---
+
+# 追加需求(同日):扫描格式「工号/人员姓名」兼容 + 名称改取 lm_emname
+
+## 用户原话
+> 同时兼容格式,数据不从 SOPEMPLOYEEDETAIL 表查询,扫描的数据格式个 工号/人员姓名,
+> 解析这个字段,也插入到 loadman 对应的字段里,上下线的时候兼容,表格里的人员名称显示取值 lm_emname
+
+## 改动文件
+`Query_LoadMan.cs`、`Query_LoadMan.Designer.cs`
+> **`CustomControl/TextBoxWithIcon/SnCollectionBox.cs` 最终未改动**(曾加过 `SkipInnerScan` 开关后被要求还原)。
+> 用户明确要求:**不要改动公共控件,只在当前功能界面做修改**。见本文件末尾「改用界面内拦截」一节。
+
+### 1. 解析方法 `SplitEmInfo`(Query_LoadMan.cs)
+```csharp
+private bool SplitEmInfo(string input, out string emcode, out string emname)
+```
+- 分隔符集合:`{ "/", "\\", "/", "|", "|", " ", " ", "、" }`(半角/全角斜杠、竖线、空格、中文顿号)
+- 用 `text.Split(sep, StringSplitOptions.RemoveEmptyEntries)` → `parts[0]` 工号、`parts[1]` 姓名
+- 返回 `false`(格式错)的判定:**原文里含分隔符,但解析后 `emcode == "" || emname == ""`**
+  → 覆盖 `"/张三"`、`"E001/"`、`"E001//张三"` 等
+- 纯工号(无分隔符)→ 正常返回,`emname` 为空串
+- 顺手解决末尾 `\r` 问题:**`\r` 不在分隔符集合里,靠第 2 段结尾的 `.Trim()` 清掉**
+- ⚠️ `Split(string[], StringSplitOptions)` 这个重载**不带 `StringSplitOptions.TrimEntries`**(那是 .NET Core 3.0+ 才有),只能手动 `.Trim()`
+
+### 3. 回车入口重构:`em_code_KeyDown` → `HandleEmCode` → `DoEmCode`
+原 `em_code_KeyDown` 里的业务代码整体挪进新方法 `DoEmCode()`,外面套一层防重入:
+
+```csharp
+private void em_code_KeyDown(object sender, KeyEventArgs e)
+{
+    if (e.KeyCode != Keys.Enter) return;
+    e.SuppressKeyPress = true;   // 阻止回车继续冒泡到 SnCollectionBox 的回车逻辑
+    e.Handled = true;
+    HandleEmCode();
+}
+
+private void Em_code_KeyPress(object sender, KeyPressEventArgs e)   // 需在 Designer 里 hook
+{
+    if (e.KeyChar == '\r' || e.KeyChar == '\n')
+    {
+        e.Handled = true;        // 吞掉字符级回车
+        HandleEmCode();
+    }
+}
+
+private bool _emBusy = false;    // 两处入口可能对同一次回车都触发,防重入
+private void HandleEmCode()
+{
+    if (_emBusy) return;
+    _emBusy = true;
+    try { DoEmCode(); }
+    finally { _emBusy = false; }
+}
+```
+- 业务分支本身未变:
+  - **上线**:`if (emname == "")` 时才去查 `SOPEMPLOYEEDETAIL`(扫描已带姓名就完全跳过查表)
+  - **下线**:`downName` 优先用解析出的姓名,否则 `dh.getFieldDataByCondition("loadman","max(lm_emname)","lm_emcode='xx' and lm_downtime is null")` —— 名称来源从 `SOPEMPLOYEEDETAIL` 改成了 `loadman` 自己的历史记录
+- **双保险机制**:`KeyPress` 拦字符级 `\r` 能挡住手工回车和以 `\r` 结尾的扫描枪;`KeyDown` 里 `SuppressKeyPress + Handled` 挡以 Tab 结尾(或 `\r` 已被 `KeyPress` 标记 `Handled`)的情况。两者都拦不住时靠 `_emBusy` 兜底不重复执行。
+
+### 4. `LoadGridData` 与 DGV 列
+- SQL 改成同时选两列:`lm.lm_emname,` 和 `nvl(lm.lm_emname,se.sed_emname) emname,`;join 保留
+- DGV 列由 `sed_emname` **改名为 `lm_emname`**(`Name`/`DataPropertyName`/字段声明三处),绑定 `lm_emname` → 优先显示本地值
+- `ConfirmDown_Click` 里 `row.Cells["sed_emname"]` → `row.Cells["lm_emname"]`
+- **保留 `SOPEMPLOYEEDETAIL` 的 left join + `nvl` 兜底**,是为了让**历史数据**(早期没写 `lm_emname` 的在线记录)仍能显示名称。用户说的是「不从 SOPEMPLOYEEDETAIL 查」,但加兜底不影响新数据走 `lm_emname`,且避免历史行姓名空白。若后续确认无需兜底,删掉 join 与 `nvl` 一段即可。
+
+---
+
+## ★ 改用界面内拦截(最终方案,用户要求不动公共控件)
+用户原话:**「不要改动SnCollectionBox,只在当前功能界面做修改」**
+
+- 已把 `SnCollectionBox.cs` 的 `SkipInnerScan` 属性与 `if (SkipInnerScan) return;` 全部删除,
+  Designer 里的 `this.em_code.SkipInnerScan = true;` 也删除 → **该文件经 git diff 确认零改动**。
+- 替代手段:**在 `Query_LoadMan` 内用字符级拦截**(见上面第 3 节的两处 hook + `_emBusy`)。
+- **核心原理**:`SnCollectionBox`/`EnterTextBox` 的回车处理全部挂在 **`KeyDown`** 上,
+  二者都**不注册 `KeyPress`**。而 `KeyPress` 在 `KeyDown` 之后、文本写入控件之前触发,
+  且 `KeyPressEventArgs.Handled = true` 能阻止该字符进入控件。
+  → 所以在 Form 的 `KeyPress` 回调里把 `\r` 吞掉,控件的 `KeyDown` 就收不到回车了,
+  `this.SelectAll()` 自然不会执行,**公共控件一行不用改**。
+- 查 git 用 `C:\Program Files\Git\cmd\git.exe`,且 `cmd /c` 的输出是 **GBK**,Python 里要 `decode('gbk')`,
+  否则 `UnicodeDecodeError`;直接用 `subprocess` 传 `cwd=` 比 `cmd /c cd /d` 稳。
+
+## 关键排查结论(值得复用)
+- **`RichTextAutoBottom` 从不注册任何 KeyDown**:它的 `InitializeComponent`(在 `.Designer.cs` 第 29-32 行)只有
+  `components = new System.ComponentModel.Container();`,`AppendText` 里的 `grep KeyDown` 无命中
+  → `OperateResult.AppendText(str, Color, ctl)` 的 `ctl.Text=""` 只清传入控件,不会重复触发扫描框的回车事件。
+- **事件 hook 必须派生类 + 基类一起看**:`SnCollectionBox.Designer.cs` 里 hook 了 `SnCollectionBox_KeyDown`,
+  `EnterTextBox` 构造函数里也 hook 了 `EnterTextBox_KeyDown`(只处理 Ctrl+A)。**两层都在 `KeyDown` 上。**
+- **`KeyPressEventArgs` 与 `KeyEventArgs` 的成员同名不同用途**:
+  两者都有 `Handled`,但 **只有 `KeyEventArgs` 有 `SuppressKeyPress`**。在 `KeyPress` 里只能设 `e.Handled`。
+- **`KeyEventArgs.Handled = true` 不会阻止本控件其他订阅者**,只标记「已处理」;要确保内置处理不执行,
+  `KeyDown` 里应设 `SuppressKeyPress = true`(同时会置 `Handled`),或改用上面的 `KeyPress` 字符级拦截。
+- 本机 `Grep` 工具对 `.Designer.cs` 里的中文注释行匹配不稳,要精确查看某段代码时改用 Python 逐行 dump 更可靠。
+
+## 自检
+- `git --no-pager diff --stat` 确认:**只有 `Query_LoadMan.cs`(+118/-23)和 `Query_LoadMan.Designer.cs`(21 行)有改动**,
+  `SnCollectionBox.cs` 未出现在 diff 中 → 已完全还原。
+- 括号平衡(Python 剥字面量后):`Query_LoadMan.cs` 40/40、120/120、27/27;
+  `Query_LoadMan.Designer.cs` 6/6、106/106、1/1 ✅
+- 仍需在 VS 编译确认。
+
+

+ 47 - 0
UAS_MES_YDCY/.workbuddy/memory/MEMORY.md

@@ -88,6 +88,49 @@
 - 提示输出用 **(string, Color) 两参数重载**,避开 (string,Color,Control) 重载内部 `ctl.Text=""` 误清输入框。
 - 刷新后勾选状态会因重新绑定被清空,属预期。
 
+## 扫码输入框(SnCollectionBox)的回车坑 —— 只在功能界面内解决
+- `SnCollectionBox_KeyDown`(在 `SnCollectionBox.Designer.cs` 里 hook)的 Enter 分支末尾有 **`this.SelectAll()`**。
+  因 Windows 消息顺序是 `KeyDown` → 字符输入,**在 `KeyDown` 里 SelectAll 会把刚扫进来的整串内容全选中,下一个字符直接覆盖** → 界面只收到最后几个字符。
+  → **界面自己处理回车时,绝对不要在 `KeyDown` 回调里调 `SelectAll()`。**
+- ⚠️ **约束:公共控件(`CustomControl/` 下)不许改,只能在功能界面内解决。** 用户明确要求过。
+- ✅ **推荐做法(零侵入)**:在功能界面对该控件**额外 hook `KeyPress`**,用字符级拦截吞掉 `\r`:
+  ```csharp
+  // Designer: this.em_code.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.Em_code_KeyPress);
+  private void Em_code_KeyPress(object sender, KeyPressEventArgs e)
+  {
+      if (e.KeyChar == '\r' || e.KeyChar == '\n')
+      {
+          e.Handled = true;      // 吞掉字符级回车
+          HandleEmCode();        // 自己的处理
+      }
+  }
+  ```
+  **原理**:`SnCollectionBox` / `EnterTextBox` 的回车处理**全部挂在 `KeyDown` 上,二者都不注册 `KeyPress`**;
+  而 `KeyPress` 在 `KeyDown` 之后、字符写入控件之前触发,`Handled = true` 能阻止该字符进入控件
+  → 控件的 `KeyDown` 就收不到回车,`SelectAll()` 自然不执行。
+- **建议同时 hook `KeyDown` 做双保险**(挡住以 Tab 结尾的扫描枪),并加防重入开关:
+  ```csharp
+  // KeyDown 里: e.SuppressKeyPress = true; e.Handled = true; HandleEmCode();
+  private bool _emBusy = false;
+  private void HandleEmCode() { if (_emBusy) return; _emBusy = true; try { DoEmCode(); } finally { _emBusy = false; } }
+  ```
+- **事件签名只能用 `(object, KeyEventArgs)` 或 `(object, KeyPressEventArgs)`**:
+  用 `KeyPressEventHandler` 却写 `KeyEventArgs` 参数会静默不触发。
+- ⚠️ 两个 args 类成员同名不同用途:**`SuppressKeyPress` 只在 `KeyEventArgs` 上**,`KeyPressEventArgs` 只有 `Handled`。
+  `KeyEventArgs.Handled = true` **不会**阻止本控件其他订阅者,只是标记「已处理」。
+- 不要让控件自己 hook `TextChanged` 去解析——**在 Form 的 `KeyPress`/`KeyDown` 里处理回车最可靠**,因为控件内部顺序不可控。
+- 查事件注册源头时**必须派生类 + 基类一起看**:`SnCollectionBox` 与其基类 `EnterTextBox` 都只在 `KeyDown` 上做文章;
+  而 `RichTextAutoBottom` 的 `InitializeComponent` 只做 `components = new Container()`,**不注册任何事件**,
+  所以 `AppendText(str, Color, ctl)` 的 `ctl.Text=""` 不会重复触发扫描框回车。
+
+## 扫描格式解析:工号/人员姓名
+- 扫描枪常输出 `工号/人员姓名` 两段。分隔符集合建议 `{ "/", "\\", "/", "|", "|", " ", " ", "、" }`(半角+全角斜杠/竖线、空格、中文顿号)。
+- 用 `text.Split(sep, StringSplitOptions.RemoveEmptyEntries)` → `parts[0]` 工号、`parts[1]` 姓名。
+  ⚠️ 这个重载**不带 `TrimEntries`**(.NET Core 3.0+ 才新增),只能手动 `.Trim()`。
+- 判「格式错」的条件:**原文含分隔符,但解析后任一段为空** → 覆盖 `"/张三"`、`"E001/"`、`"E001//张三"`。
+- 扫描内容常带末尾 `\r`:**`\r` 不要放进分隔符集合**,靠第 2 段结尾的 `.Trim()` 自然清掉即可。
+- 兼容原则:**纯工号(无分隔符)走原来的查表逻辑;带姓名(有分隔符)则完全跳过查表**。
+
 ## 数据访问
 - `dh.ExecuteSql(sql, "select")` 返回 `DataTable`;`dh.getFieldDataByCondition(表, 字段, 条件)` 返回单值。
   - **`字段` 参数可传聚合表达式**(如 `"max(sed_emname)"`),一次查询同时完成「校验存在」+「取名称」:
@@ -103,6 +146,10 @@
 - 改代码后需在 Visual Studio 中手动编译确认。
 - 自检手段:用 Python 剥离注释与字符串字面量后统计 `{}`/`()`/`[]` 平衡(脚本临时写到 `.workbuddy/`,用完删除)。
 - ⚠️ Edit 教训:`old_string` / `new_string` 的**末尾换行必须一致**,否则会把下一个方法签名和 `{` 挤到同一行(曾误伤 `UpLoadMan_CheckedChanged`)。
+- ⚠️ 本机 `Grep` 工具对 `.Designer.cs` 里的中文注释行匹配不稳,要精确查看某段代码时改用 Python 逐行 dump 更可靠。
+- ⚠️ `git` 不在 PATH 里,须用全路径 `C:\Program Files\Git\cmd\git.exe`(Python `subprocess` 传 `cwd=` 比 `cmd /c cd /d` 稳);
+  `cmd /c` 的输出是 **GBK**,Python 里要 `decode('gbk','replace')`,否则 `UnicodeDecodeError`。
+- 改动是否「零侵入」可以用 `git --no-pager diff --stat -- <path>` 快速自证:文件不出现在输出里 = 完全未改。
 
 ## 版本控制(git)
 - 仓库根在 **`C:\MES_NEW\mes-client`**(UAS_MES_YDCY 只是其中一个项目);远程 `mes-client` = `git@repos.ubtob.net:usoft/mes-client.git`(SSH,内网 10.1.81.211),分支 `master`,无 upstream 追踪。

+ 11 - 10
UAS_MES_YDCY/FunctionCode/Query/Query_LoadMan.Designer.cs

@@ -34,7 +34,7 @@ namespace UAS_MES_NEW.Query
             this.choose = new System.Windows.Forms.DataGridViewCheckBoxColumn();
             this.lm_id = new System.Windows.Forms.DataGridViewTextBoxColumn();
             this.lm_emcode = new System.Windows.Forms.DataGridViewTextBoxColumn();
-            this.sed_emname = new System.Windows.Forms.DataGridViewTextBoxColumn();
+            this.lm_emname = new System.Windows.Forms.DataGridViewTextBoxColumn();
             this.lm_linecode = new System.Windows.Forms.DataGridViewTextBoxColumn();
             this.lm_uptime = new System.Windows.Forms.DataGridViewTextBoxColumn();
             this.lm_inman = new System.Windows.Forms.DataGridViewTextBoxColumn();
@@ -102,6 +102,7 @@ namespace UAS_MES_NEW.Query
             this.em_code.Size = new System.Drawing.Size(220, 40);
             this.em_code.TabIndex = 2;
             this.em_code.KeyDown += new System.Windows.Forms.KeyEventHandler(this.em_code_KeyDown);
+            this.em_code.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.Em_code_KeyPress);
             //
             // UpLoadMan
             //
@@ -155,7 +156,7 @@ namespace UAS_MES_NEW.Query
             this.choose,
             this.lm_id,
             this.lm_emcode,
-            this.sed_emname,
+            this.lm_emname,
             this.lm_linecode,
             this.lm_uptime,
             this.lm_inman});
@@ -192,14 +193,14 @@ namespace UAS_MES_NEW.Query
             this.lm_emcode.ReadOnly = true;
             this.lm_emcode.Width = 180;
             //
-            // sed_emname
+            // lm_emname
             //
-            this.sed_emname.DataPropertyName = "sed_emname";
-            this.sed_emname.HeaderText = "人员名称";
-            this.sed_emname.MinimumWidth = 10;
-            this.sed_emname.Name = "sed_emname";
-            this.sed_emname.ReadOnly = true;
-            this.sed_emname.Width = 120;
+            this.lm_emname.DataPropertyName = "lm_emname";
+            this.lm_emname.HeaderText = "人员名称";
+            this.lm_emname.MinimumWidth = 10;
+            this.lm_emname.Name = "lm_emname";
+            this.lm_emname.ReadOnly = true;
+            this.lm_emname.Width = 120;
             //
             // lm_linecode
             //
@@ -275,7 +276,7 @@ namespace UAS_MES_NEW.Query
         private System.Windows.Forms.DataGridViewCheckBoxColumn choose;
         private System.Windows.Forms.DataGridViewTextBoxColumn lm_id;
         private System.Windows.Forms.DataGridViewTextBoxColumn lm_emcode;
-        private System.Windows.Forms.DataGridViewTextBoxColumn sed_emname;
+        private System.Windows.Forms.DataGridViewTextBoxColumn lm_emname;
         private System.Windows.Forms.DataGridViewTextBoxColumn lm_linecode;
         private System.Windows.Forms.DataGridViewTextBoxColumn lm_uptime;
         private System.Windows.Forms.DataGridViewTextBoxColumn lm_inman;

+ 105 - 13
UAS_MES_YDCY/FunctionCode/Query/Query_LoadMan.cs

@@ -54,14 +54,66 @@ namespace UAS_MES_NEW.Query
 
         /// <summary>
         /// 人员编号输入框,回车后按选中的模式执行上线/下线
+        /// 兼容两种输入格式:①只输入工号(名称从 SOPEMPLOYEEDETAIL 查) ②扫描/输入「工号/人员姓名」(直接解析,不再查表)
         /// </summary>
         private void em_code_KeyDown(object sender, KeyEventArgs e)
         {
             if (e.KeyCode != Keys.Enter)
                 return;
+            //阻止回车继续冒泡到 SnCollectionBox / EnterTextBox 的回车逻辑
+            //(SnCollectionBox 的 KeyDown 里有一句 this.SelectAll(),会受消息顺序影响;
+            // 另配 Em_code_KeyPress 拦掉字符级回车,双保险,不让内置逻辑有机会执行)
+            e.SuppressKeyPress = true;
+            e.Handled = true;
+            HandleEmCode();
+        }
+
+        /// <summary>
+        /// 字符级拦截:SnCollectionBox/EnterTextBox 内部的回车处理挂在 KeyDown 上,不监听 KeyPress,
+        /// 而扫描枪以 Tab 结尾或手工回车都会先产生字符级 \r。
+        /// 这里直接把 \r 吞掉并调用统一处理,SnCollectionBox 的回车逻辑(含 SelectAll)就完全不会触发,
+        /// 从而不必改动该公共控件。
+        /// </summary>
+        private void Em_code_KeyPress(object sender, KeyPressEventArgs e)
+        {
+            if (e.KeyChar == '\r' || e.KeyChar == '\n')
+            {
+                e.Handled = true;
+                HandleEmCode();
+            }
+        }
+
+        /// <summary>
+        /// 上线/下线的统一处理入口(由回车、字符级 \r 两处触发,故用 _emBusy 防止同一次回车重复执行)
+        /// </summary>
+        private bool _emBusy = false;
+
+        private void HandleEmCode()
+        {
+            if (_emBusy)
+                return;
+            _emBusy = true;
+            try
+            {
+                DoEmCode();
+            }
+            finally
+            {
+                _emBusy = false;
+            }
+        }
+
+        private void DoEmCode()
+        {
             string linecode = li_code.Text.Trim();
-            string emcode = em_code.Text.Trim();
-            //限制:人员编号不能为空
+            string emcode = "";
+            string emname = "";
+            //解析输入内容:扫描枪输出「工号/人员姓名」两段格式,手工输入则只有工号
+            if (!SplitEmInfo(em_code.Text, out emcode, out emname))
+            {
+                OperateResult.AppendText("<<扫描内容格式有误,应为「工号/人员姓名」\n", Color.Red, em_code);
+                return;
+            }
             if (emcode == "")
             {
                 OperateResult.AppendText("<<人员编号不能为空\n", Color.Red, em_code);
@@ -75,14 +127,18 @@ namespace UAS_MES_NEW.Query
                     OperateResult.AppendText("<<请先选择线别\n", Color.Red, li_code);
                     return;
                 }
-                //上线:先校验人员编号存在于 SOPEMPLOYEEDETAIL,并取出人员名称(同一 sed_emcode 可能多条,取最大值聚合)
-                string emname = dh.getFieldDataByCondition("SOPEMPLOYEEDETAIL", "max(sed_emname)", "sed_emcode='" + emcode + "'").ToString().Trim();
+                //上线:扫描格式已带姓名则直接用;只输入工号时才去 SOPEMPLOYEEDETAIL 取名称
                 if (emname == "")
                 {
-                    OperateResult.AppendText("<<" + emcode + "人员编号不存在\n", Color.Red, em_code);
-                    em_code.Clear();
-                    em_code.Focus();    
-                    return;
+                    //同一 sed_emcode 可能多条,取最大值聚合
+                    emname = dh.getFieldDataByCondition("SOPEMPLOYEEDETAIL", "max(sed_emname)", "sed_emcode='" + emcode + "'").ToString().Trim();
+                    if (emname == "")
+                    {
+                        OperateResult.AppendText("<<" + emcode + "人员编号不存在\n", Color.Red, em_code);
+                        em_code.Clear();
+                        em_code.Focus();
+                        return;
+                    }
                 }
                 //一个人只能同时一条线上线:若已在别的线体上线,先把原线体自动下线,再在新线体上线
                 DataTable dtOn = (DataTable)dh.ExecuteSql("select lm_linecode from loadman where lm_emcode='" + emcode + "' and lm_downtime is null", "select");
@@ -117,7 +173,11 @@ namespace UAS_MES_NEW.Query
                     return;
                 }
                 dh.ExecuteSql("update loadman set lm_downtime=sysdate where lm_emcode='" + emcode + "' and lm_downtime is null", "update");
-                OperateResult.AppendText("<<" + emcode + "已从" + dtOn.Rows[0]["lm_linecode"].ToString() + "下线\n", Color.Green, em_code);
+                //姓名优先取扫描解析出的,否则用 loadman 里记录的名称
+                string downName = emname == "" ? dh.getFieldDataByCondition("loadman", "max(lm_emname)",
+                    "lm_emcode='" + emcode + "' and lm_downtime is null").ToString().Trim() : emname;
+                OperateResult.AppendText("<<" + emcode + (downName == "" ? "" : " " + downName) + " 已从"
+                    + dtOn.Rows[0]["lm_linecode"].ToString() + "下线\n", Color.Green, em_code);
             }
             //数据变更后刷新
             LoadGridData();
@@ -125,6 +185,34 @@ namespace UAS_MES_NEW.Query
             em_code.Focus();
         }
 
+        /// <summary>
+        /// 解析人员输入内容,兼容两种格式:
+        /// ①「工号/人员姓名」(扫描枪常用格式,分隔符支持 / \ 全角/ 竖线 | ,也兼容空格和中文顿号) → 工号与姓名都带出
+        /// ②只有工号(手工输入) → 姓名返回空串,由调用方决定是否去 SOPEMPLOYEEDETAIL 查
+        /// 分隔符在首尾或出现多次时视为格式错误,返回 false
+        /// </summary>
+        private bool SplitEmInfo(string input, out string emcode, out string emname)
+        {
+            emcode = "";
+            emname = "";
+            if (input == null)
+                return true;
+            string text = input.Trim();
+            if (text == "")
+                return true;
+            string[] sep = new string[] { "/", "\\", "/", "|", "|", " ", " ", "、" };
+            string[] parts = text.Split(sep, StringSplitOptions.RemoveEmptyEntries);
+            if (parts.Length == 0)
+                return true;
+            emcode = parts[0].Trim();
+            if (parts.Length > 1)
+                emname = parts[1].Trim();
+            //带分隔符却没解析出两段,说明格式不对(如以分隔符开头或多余分隔符)
+            if (text.IndexOfAny(new char[] { '/', '\\', '/', '|', '|', ' ', ' ', '、' }) >= 0 && (emcode == "" || emname == ""))
+                return false;
+            return true;
+        }
+
         /// <summary>
         /// 判断DGV中是否有勾选的行
         /// </summary>
@@ -168,7 +256,7 @@ namespace UAS_MES_NEW.Query
                     continue;
                 lmId = row.Cells["lm_id"].Value.ToString();
                 emcode = row.Cells["lm_emcode"].Value == null ? "" : row.Cells["lm_emcode"].Value.ToString();
-                emname = row.Cells["sed_emname"].Value == null ? "" : row.Cells["sed_emname"].Value.ToString();
+                emname = row.Cells["lm_emname"].Value == null ? "" : row.Cells["lm_emname"].Value.ToString();
                 linecode = row.Cells["lm_linecode"].Value == null ? "" : row.Cells["lm_linecode"].Value.ToString();
                 try
                 {
@@ -207,12 +295,16 @@ namespace UAS_MES_NEW.Query
         }
 
         /// <summary>
-        /// 刷新DGV:只取未下线的人员,已选线别时只显示当前线别;按 lm_emcode 关联 SOPEMPLOYEEDETAIL 取人员名称
-        /// (SOPEMPLOYEEDETAIL 同一 sed_emcode 可能有多条,先按其聚合去重再关联;上线时间 to_char 到秒)
+        /// 刷新DGV:只取未下线的人员,已选线别时只显示当前线别
+        /// 人员名称直接取 loadman.lm_emname(上线时已写入),不再关联 SOPEMPLOYEEDETAIL;
+        /// 兼容历史数据:lm_emname 为空时才回退到 SOPEMPLOYEEDETAIL 取名称
+        /// (SOPEMPLOYEEDETAIL 同一 sed_emcode 可能有多条,先按其聚合去重;上线时间 to_char 到秒)
         /// </summary>
         private void LoadGridData()
         {
-            string sql = "select lm.lm_id,lm.lm_linecode,lm.lm_emcode,se.sed_emname,to_char(lm.lm_uptime,'yyyy-mm-dd hh24:mi:ss') lm_uptime,lm.lm_inman from loadman lm"
+            string sql = "select lm.lm_id,lm.lm_linecode,lm.lm_emcode,lm.lm_emname,"
+                + "nvl(lm.lm_emname,se.sed_emname) emname,"
+                + "to_char(lm.lm_uptime,'yyyy-mm-dd hh24:mi:ss') lm_uptime,lm.lm_inman from loadman lm"
                 + " left join (select sed_emcode,max(sed_emname) sed_emname from SOPEMPLOYEEDETAIL group by sed_emcode) se on lm.lm_emcode=se.sed_emcode"
                 + " where lm.lm_downtime is null";
             if (li_code.Text != "")