# 搜索框打断中文输入法：组合输入期间禁止重渲染（2026-09-11）

**症状**（用户原话）：「销售工作台，订单管理的搜索框，输入文字的时候会强制刷新，打断输入法」——输入拼音打到一半，候选词窗口消失、文字被吃掉、光标/焦点丢失。

**根因**：`oninput` → 防抖 → 整页 `render()` → `main.innerHTML = ...` 把 `<input>` 元素本身销毁重建。输入法的组合态（composition session）绑定在具体 DOM 节点上，节点被换掉就中断。

## 修复：三段式，缺一不可

### ① 组合期不重渲染
```javascript
function orderSearchInput(inp, ev) {
  orderState.filterText = inp.value;
  if (orderState.composing) return;                 // 正在拼字：交给 compositionend
  if (ev && ev.isComposing) return;                 // 双保险：部分输入法不派发 compositionstart
  _orderSearchDebounced();
}
var _orderSearchDebounced = debounce(function() {
  if (orderState.composing) return;
  refreshOrderTable();
}, 300);
```

### ② 组合事件必须用文档级事件委托
inline `oncompositionstart="fn()"` **在部分内核不生效**：属性写进了 DOM，但 property 未编译 → 事件派发了却不执行（判据：`el.getAttribute('oncompositionstart')` 有值，而 `typeof el.oncompositionstart === 'undefined'`）。`oninput` 则正常编译，所以只用 inline 会「看起来装上了、实际没反应」。

```javascript
document.addEventListener('compositionstart', function(e) {
  var t = e.target;
  if (!t || !t.id) return;
  if (t.id === 'orderSearchInput') orderState.composing = true;
  else if (t.id === 'custSearch') window._custComposing = true;
}, true);
document.addEventListener('compositionend', function(e) {
  var t = e.target;
  if (!t || !t.id) return;
  if (t.id === 'orderSearchInput') { orderState.composing = false; orderState.filterText = t.value; refreshOrderTable(); }
  else if (t.id === 'custSearch') { window._custComposing = false; customerSearch = t.value; render(); }
}, true);
```
委托写在脚本顶层 → 跨 `render()` 存活，重新渲染后无需重新绑定。

### ③ 只局部重绘，不重建 input
把表格抽成独立函数，搜索时只换容器内容：
```javascript
var ORDER_TABLE_WRAP_OPEN = '<div class="card" id="orderTableWrap"><div class="card-body" style="padding:0;overflow-x:auto">';
function orderTableInner(filtered) { return '<table ...>...</table>'; }
function refreshOrderTable() {
  var wrap = document.getElementById('orderTableWrap');
  if (!wrap) { render(); return; }        // 兜底
  var wk = getWeekRange(), d90 = new Date(wk.start); d90.setDate(d90.getDate() - 90);
  var orders90d = getShipmentOrders().orders.filter(function(o){ var d = parseDate(o.createDate); return d && d >= d90; });
  wrap.innerHTML = '<div class="card-body" style="padding:0;overflow-x:auto">' + orderTableInner(orderFilterList(orders90d)) + '</div>';
}
```
**只做 ①+② 不够**：组合结束仍走整页 `render()` → input 重建 → 下一个字又要重新点搜索框。必须让 input 节点在打字全程存活。

## 验证（headless 无真输入法，用事件模拟）
断言**节点 identity** 和**整页 render 次数**，不要断言文本或焦点：
```javascript
var el = document.getElementById('orderSearchInput'); window._el = el;
window._wrap = document.getElementById('orderTableWrap');
window._n = 0; var _r = window.render; window.render = function(){ window._n++; return _r.apply(this, arguments); };
el.dispatchEvent(new CompositionEvent('compositionstart', {bubbles:true}));
el.value = '叶'; el.dispatchEvent(new InputEvent('input', {bubbles:true, data:'叶', isComposing:true}));
el.value = '叶信'; el.dispatchEvent(new InputEvent('input', {bubbles:true, data:'信', isComposing:true}));
// 等 > 防抖时长(>400ms) 后：
//   整页 render 次数 == 0；document.getElementById('orderSearchInput') === window._el；
//   表格行数不变（组合中不筛）
el.value = '叶信福'; el.dispatchEvent(new CompositionEvent('compositionend', {bubbles:true, data:'叶信福'}));
//   表格行数应变少；input/wrap identity 仍不变
```
⚠️ headless 下 `document.hasFocus()` 恒为 false、`document.activeElement` 是 body，**焦点断言不可信**，别据此判定失败。

## 同类排查
任何 `oninput → 整页 render()` 的搜索框都同病：本次修了 `orderSearchInput`（订单管理）+ `debouncedSearch`（客户档案 custSearch）。`qvSearchCust` 只更新外侧下拉容器 `#qvCustResults`，input 不被重建 → 天然安全。

## 部署
```bash
cd ~/Desktop/Hermes输出-工作类
tcb hosting deploy 工具/index.html /workbench/index.html -e bier-sales-d0gatbvlx288724e9
# 验证 Origin（CDN 有 5-10 分钟缓存延迟）
tcb hosting download /workbench/index.html /tmp/verify_wb.html -e bier-sales-d0gatbvlx288724e9
```
`工具/app.html`（本地测试页）不在 cb_sync.sh 的部署清单里，改了要单独同步。飞书内嵌 WebView 有额外缓存层，需**完全退出飞书**才见新版。
