# 订单管理页面设计模式

## 布局结构

```
┌─────────────────────────────────────┐
│  📊 本周订单分析  (日期范围)          │
│  ┌─────┬─────┬─────┬─────┬─────┬───┐ │
│  │发货 │揽收 │运输 │派送 │签收 │无  │ │
│  │ N   │ N   │ N   │ N   │ N   │N   │ │
│  └─────┴─────┴─────┴─────┴─────┴───┘ │
│  ┌──────────────┐ ┌──────────────────┐│
│  │ 🏢 按部门     │ │ 📅 逐日发货趋势  ││
│  │ 拓展部 ███ 67 │ │ 周一 ▏ 周二 ▏...  ││
│  │ 大客户 ██  23 │ │                  ││
│  └──────────────┘ └──────────────────┘│
├─────────────────────────────────────┤
│  📦 订单明细  近90天 · N单  [查看全部→]│
│  ┌─筛选：日期┼部门┼下单人┼状态┼搜索─┐│
│  │ 📅近90天▼│📂全部▼│👤全部▼│📍全部▼││
│  └──────────────────────────────────┘│
│  ┌──────┬────┬──┬──┬──┬──────┬──┬──┐│
│  │编号  │客户│📝│👤│部│物流号│状│日││
│  │      │    │合│下│门│      │态│期││
│  │      │    │同│单│  │      │  │  ││
│  ├──────┼────┼──┼──┼──┼──────┼──┼──┤│
│  │ZH..  │ xx │xx│xx│xx│SF..  │🚚│07││ (点击展开↓)
│  │ 📦产品 货号 批号 规格 单位          ││
│  │ 🔢数量 💰单价 含税单价 金额 🧾合计  ││
│  │ 👤收件人 📞电话 🏠地址              ││
│  │ ○── 物流轨迹时间线 ──○             ││
│  └──────┴────┴──┴──┴──┴──────┴──┴──┘│
│          📋 在飞书查看全部 →            │
└─────────────────────────────────────┘
```

## 表格列（8列，2026-07-24版）

| # | 列名 | 宽度 | 说明 |
|---|------|------|------|
| 1 | 单据编号 | 130px | 粗体，点击展开物流 |
| 2 | 客户 | 自适应 | 超长截断+title提示 |
| 3 | 合同编号 | 110px | `contractNo`字段，截断+title |
| 4 | 下单人 | 60px | `creator`字段，MCP `创建者` name_cn |
| 5 | 部门 | 70px | `dept`字段 |
| 6 | 物流单号 | 110px | 等宽字体monospace |
| 7 | 物流状态 | 80px | 圆点+文字 |
| 8 | 日期 | 90px | `createDate` |

## 筛选器（5个，2026-07-24版）

| # | 类型 | orderState字段 | 说明 |
|---|------|---------------|------|
| 1 | select | `filterPeriod` | 📅 近90天/近30天/本周 |
| 2 | select | `filterDept` | 📂 全部部门（动态生成） |
| 3 | select | `filterCreator` | 👤 全部下单人（动态生成） |
| 4 | select | `filterStatus` | 📍 全部状态 |
| 5 | input | `filterText` | 🔍 搜索单号/客户 |

选择器回填用setTimeout：
```javascript
setTimeout(function() {
  var selects = document.querySelectorAll('.order-filter-bar select');
  if (selects[0]) selects[0].value = orderState.filterPeriod;
  if (selects[1]) selects[1].value = orderState.filterDept;
  if (selects[2]) selects[2].value = orderState.filterCreator;
  if (selects[3]) selects[3].value = orderState.filterStatus;
}, 10);
```

## 关键实现细节

### 本周范围计算
```javascript
function getWeekRange() {
  var now = new Date();
  var day = now.getDay(); // 0=Sun
  var monday = new Date(now);
  monday.setDate(now.getDate() - (day === 0 ? 6 : day - 1));
  monday.setHours(0,0,0,0);
  var sunday = new Date(monday);
  sunday.setDate(monday.getDate() + 6);
  sunday.setHours(23,59,59,999);
  return { start: monday, end: sunday };
}
```

### 部门水平进度条
```javascript
var barW = Math.round(d.count / maxDeptCount * 100);
'<div style="height:8px;background:#f3f4f6;border-radius:4px">' +
'<div style="height:100%;width:' + barW + '%;background:linear-gradient(90deg,var(--pri),var(--pri-dark))"></div></div>';
```

### 时间切换 + 三栏分析（2026-07-24 终版）

上半部分改用 `seg-control` 分段按钮切换 `orderState.analysisPeriod`。

```javascript
var orderState = { analysisPeriod: 'week' };
if (orderState.analysisPeriod === 'month') {
  range.start = new Date(now.getFullYear(), now.getMonth(), 1); range.label = '本月';
} else if (orderState.analysisPeriod === 'quarter') {
  var q = Math.floor(now.getMonth() / 3);
  range.start = new Date(now.getFullYear(), q * 3, 1); range.label = '本季';
}
```

**六维卡片**：发货数 + 价税合计（`ps.taxTotal`，优先 `o.taxTotal` fallback `o.amount`）+ 运输中 + 已签收 + 物流异常 + 暂无路由。

**三栏分析**（`grid 1fr 1fr 1fr`）：客户Top5（按价税合计排名）+ 订单类型占比（彩色进度条）+ 物流异常明细（最多10条，超出折叠）。

### ⚠️ 物流异常定义（关键修正）
```javascript
// 旧版本把185运输中误算入 → 247条异常；修正后仅62条（真正异常）
var anomalyOrders = periodOrders.filter(function(o) {
  var s = (o.shipment || {}).status || '';
  return s !== 'delivered' && s !== 'in_transit' && s !== 'no_tracking';
});
```

62条全是暂无路由，原因为顺丰手机号校验不匹配 / 新单未记录。185条运输中单独卡片展示，不归入异常。

### 订单详情展开
两行信息：
- 上行：产品/货号/批号/规格/单位
- 下行（灰底高亮 `#f8fafc`）：数量/单价/含税单价/金额/价税合计（按字段逐项判断显示）
- 收货人：姓名/电话/地址
- 物流时间线（竖线+圆点）

### ⚠️ 金额字段 falsy 陷阱
```javascript
// ❌ 错误：0、"". 都是 falsy，整行不显示
var hasMoney = o.quantity || o.unitPrice || o.amount || o.taxTotal;

// ✅ 正确：逐字段判断
var moneyParts = [];
if (o.quantity != null && o.quantity !== '') 
  moneyParts.push('<span>🔢 数量: <b>' + esc(String(o.quantity)) + '</b></span>');
if (o.unitPrice != null && o.unitPrice !== '' && o.unitPrice !== 0)
  moneyParts.push('<span>💰 单价: <b>' + esc(String(o.unitPrice)) + '</b></span>');
// ... 每个字段独立判断
var moneyRow = moneyParts.length ? '<div>...' + moneyParts.join('') + '...</div>' : '';
```

### 权限过滤
```javascript
var USER_DEPTS = {
  'zhaoyunhao': ['诊断原料大客户销售部'],
  'zhangliya': ['诊断原料大客户销售部'],
  'director': null,  // 看全部
  'gm': null
};
```

### 数据源
`state.data.shipments.orders[]` 从 `../数据/shipment_status.json` 加载，在 ENDPOINTS 中注册。

### 飞书链接
```html
<a href="https://project.feishu.cn/xsguanli/workObjectView/xsdd/fX8uonjSg?scope=workspaces&node=21377459" target="_blank">
📋 查看全部订单 →
</a>
```

### 到货弹窗
检查 `localStorage('bior_delivery_notified')` 中未通知的已签收订单，弹模态窗口提示。

### ⚠️ HTML缓存陷阱
```python
# 服务器 end_headers() 中必须给 .html 加 no-cache
if path.endswith('.json') or path.startswith('/api/') or path.endswith('.html'):
    self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate')
```

### MCP字段映射（下单人）
```python
# user_value 类型字段取 name_cn 而非 name
uv = (x.get("value") or {}).get("user_value", {})
owner = uv.get("name_cn", "") or uv.get("name", "")
```
