# 工作台多选筛选器组件（2026-07-28）

## 概述

将工作台 HTML 页面中所有 `<select>` 单选筛选器改为多选复选框面板，支持全选/清除/标签展示。已应用于客诉（6个）、订单管理（3个）、客户（1个）共 10 个筛选器。

## 完整 JS 组件

```javascript
var _msInstances = {};
function multiSelect(id, label, options, selected, onChange) {
  _msInstances[id] = { options: options, selected: selected, onChange: onChange };
}
function renderMultiSelect(id, label, options, selected, labelMap) {
  labelMap = labelMap || {};
  var sel = selected || [];
  var hasSel = sel.length > 0;
  var display = label;
  if (hasSel && sel.length <= 3) {
    display = '<span class="ms-tags">' + sel.map(function(s){
      return '<span class="ms-tag">'+esc(labelMap[s]||s)+'</span>';
    }).join('') + '</span>';
  } else if (hasSel) {
    display = '<span class="ms-tags"><span class="ms-tag">'+sel.length+'项</span></span>';
  }
  return '<div class="ms-wrap" id="ms_'+id+'">'
    + '<div class="ms-btn'+(hasSel?' active':'')+'" onclick="toggleMs(\''+id+'\')">'+display+'</div>'
    + '<div class="ms-drop"><div style="padding:4px 12px">'
    + '<label style="font-size:10px;cursor:pointer" onclick="toggleMsAll(\''+id+'\',true)">全选</label> | '
    + '<label style="font-size:10px;cursor:pointer" onclick="toggleMsAll(\''+id+'\',false)">清除</label></div>'
    + options.map(function(o){
        var checked = sel.indexOf(o) >= 0 ? ' checked' : '';
        return '<div class="ms-item" onclick="toggleMsOpt(\''+id+'\',\''+esc(o)+'\')">'
          + '<input type="checkbox"'+checked+' onclick="event.stopPropagation()">'
          + esc(labelMap[o]||o)+'</div>';
      }).join('')
    + '</div></div>';
}
function toggleMs(id) {
  var dd = document.querySelector('#ms_'+id+' .ms-drop');
  if (!dd) return;
  document.querySelectorAll('.ms-drop.show').forEach(function(d){ if (d !== dd) d.classList.remove('show'); });
  dd.classList.toggle('show');
}
function toggleMsOpt(id, val) {
  var inst = _msInstances[id];
  if (!inst) return;
  var idx = inst.selected.indexOf(val);
  if (idx >= 0) inst.selected.splice(idx, 1);
  else inst.selected.push(val);
  inst.onChange(inst.selected);
}
function toggleMsAll(id, all) {
  var inst = _msInstances[id];
  if (!inst) return;
  if (all) inst.selected = inst.options.slice();
  else inst.selected = [];
  inst.onChange(inst.selected);
}
document.addEventListener('click', function(e) {
  if (!e.target.closest('.ms-wrap')) {
    document.querySelectorAll('.ms-drop.show').forEach(function(d){ d.classList.remove('show'); });
  }
});
```

## CSS

```css
.ms-wrap { position: relative; display: inline-block; }
.ms-btn { padding: 6px 10px; border: 1.5px solid var(--border); border-radius: 8px; font-size: 12px; background: #fff; cursor: pointer; white-space: nowrap; min-width: 70px; text-align: left; color: var(--text); }
.ms-btn:hover { border-color: var(--pri); }
.ms-btn.active { border-color: var(--pri); background: var(--pri-light); }
.ms-drop { display: none; position: absolute; top: 100%; left: 0; z-index: 100; background: #fff; border: 1.5px solid var(--border); border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,.12); min-width: 160px; max-height: 240px; overflow-y: auto; padding: 4px 0; }
.ms-drop.show { display: block; }
.ms-item { display: flex; align-items: center; gap: 6px; padding: 5px 12px; font-size: 12px; cursor: pointer; color: var(--text); }
.ms-item:hover { background: var(--pri-light); }
.ms-item input { margin: 0; accent-color: var(--pri); }
.ms-tags { display: inline-flex; gap: 3px; flex-wrap: wrap; max-width: 120px; }
.ms-tag { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 10px; background: var(--pri-light); color: var(--pri); font-weight: 600; }
```

## 筛选器变量改造

```javascript
// 旧：单选字符串
var filter = 'all';
if (filter !== 'all' && item.field !== filter) return false;

// 新：多选数组
var filter = [];
if (filter.length > 0 && filter.indexOf(item.field) < 0) return false;
```

## 使用四步法

### 1. 变量声明改为数组
```javascript
var complaintAreaFilter = [];  // 原 'all'
```

### 2. 在 render 函数中注册（渲染前）
```javascript
multiSelect('compArea', '📂 区域', areaList, complaintAreaFilter, function(v){complaintAreaFilter=v;render();});
```

### 3. 渲染多选组件
```javascript
renderMultiSelect('compArea', '📂 区域', areaList, complaintAreaFilter)
```

### 4. 更新筛选逻辑
```javascript
if (complaintAreaFilter.length > 0 && complaintAreaFilter.indexOf(c.area) < 0) return false;
```

### 带标签映射的状态筛选
```javascript
renderMultiSelect('ordStatus', '📍 状态', ['delivered','delivering'], orderState.filterStatus, {
  'delivered':'✅ 已签收','delivering':'🚚 派送中'
})
```

## 已应用的 Tab

| Tab | 筛选器 | 数量 |
|-----|--------|------|
| 客诉 | 区域/状态/优先级/类型/处理人（天数保留单选） | 5多选 |
| 订单管理 | 部门/下单人/状态（日期范围/搜索保留） | 3多选 |
| 客户 | 部门 | 1多选 |

## 注意事项

- 互斥型筛选器（天数范围、日期周期）保留单选 `<select>`
- 每次 render 需重新调用 `renderMultiSelect`（DOM 全量重建）
- `orderState` 对象中 filter 字段需同步改为数组（如 `filterDept: []` 替代 `filterDept: 'all'`）
