# board-customer 页面数据来源排查

> 排查日期：2026-03-18
> 页面路径：`pages/board-customer/board-customer`
> 引用文件：`board-customer.ts` / `.wxml` / `.wxss` / `.json`
> 外部依赖：`utils/ai-color-manager.ts`（`initPageAiColor`，已导入但未调用）

## 概览

| 分类 | 数量 | 说明 |
|------|------|------|
| A. Mock 数据 | 1 组（3 条记录，~30 个字段/条） | 页面内联 `MOCK_CUSTOMERS` 常量，未引用 `utils/mock-data.ts` |
| B. 硬编码数据 | 5 处 | 维度选项、项目选项、Tab 列表、列表标题、加载延迟 |
| C. 已对接 API | 0 | 无任何 `wx.request` / API 调用 |
| D. 前端计算/派生 | 3 处 | `dimType` 映射、滚动隐藏筛选栏、空/错误态判断 |
| E. 路由参数 | 0 | `onLoad` 无参数读取 |
| F. WXML 硬编码文案 | 12 处 | Tab 文案、列表标题、网格标签、状态提示等 |

---

## 一、Mock 数据

### 1.1 `MOCK_CUSTOMERS`（页面内联，第 72–178 行）

页面顶部定义的 `const MOCK_CUSTOMERS: CustomerItem[]`，包含 3 条客户记录（王先生 / 李女士 / 张先生），在 `loadData()` 中通过 `setTimeout(400ms)` 模拟异步加载。

**未引用 `utils/mock-data.ts`**：该文件中有 `mockCustomers: CustomerCard[]`，但 board-customer 页面使用了自己定义的 `CustomerItem` 接口（字段更丰富，支持 8 维度卡片）。

#### 字段清单（每条 CustomerItem）

| 字段 | 示例值（王先生） | 数据类型 | 联调时对应 API 字段 |
|------|------------------|----------|---------------------|
| `id` | `'u1'` | string | 客户 ID（`member_id`） |
| `name` | `'王先生'` | string | 客户姓名（`dim_member.nickname`，注意 DQ-6 断档） |
| `initial` | `'王'` | string | 前端从 `name` 截取首字 |
| `avatarCls` | `'avatar--amber'` | string | 前端根据规则分配头像色 |
| `idealDays` | `7` | number | 理想到店间隔天数（DWS 计算） |
| `elapsedDays` | `15` | number | 距上次到店已过天数（DWS 计算） |
| `overdueDays` | `8` | number | 超期天数 = `elapsedDays - idealDays` |
| `visits30d` | `3` | number | 近 30 天到店次数 |
| `balance` | `'¥2,680'` | string | 当前余额（储值卡） |
| `recallIndex` | `'9.2'` | string | 召回指数（DWS 综合评分） |
| `potentialTags` | `[{text:'高频',theme:'primary'}]` | array | 消费潜力标签（后端计算） |
| `spend30d` | `'¥4,200'` | string | 近 30 天消费金额 |
| `avgVisits` | `'6.2次'` | string | 月均到店次数 |
| `avgSpend` | `'¥680'` | string | 次均消费金额 |
| `lastVisit` | `'3天前'` | string | 最近到店（相对时间） |
| `monthlyConsume` | `'¥3,500'` | string | 月均消耗金额 |
| `availableMonths` | `'约0.8个月'` | string | 余额可用月数 = `balance / monthlyConsume` |
| `lastRecharge` | `'2月15日'` | string | 最后充值日期 |
| `rechargeAmount` | `'¥5,000'` | string | 最后充值金额 |
| `recharges60d` | `'2次'` | string | 近 60 天充值次数 |
| `currentBalance` | `'¥2,680'` | string | 当前余额（与 `balance` 重复） |
| `spend60d` | `'¥8,400'` | string | 近 60 天消费金额 |
| `visits60d` | `'12'` | string | 近 60 天到店次数 |
| `highSpendTag` | `true` | boolean | 是否高消费标签 |
| `avgInterval` | `'5.0天'` | string | 平均到店间隔 |
| `intimacy` | `'92'` | string | 专一度指数 |
| `topCoachName` | `'小燕'` | string | 最亲密助教姓名 |
| `topCoachHeart` | `9.2` | number | 最亲密助教关系指数 |
| `topCoachScore` | `'9.2'` | string | 同上（字符串版） |
| `coachDetails` | 2 条明细 | array | 助教服务明细表 |
| `weeklyVisits` | 8 周数据 | array | 8 周到店频率（迷你柱状图） |
| `coachName` | `'小燕'` | string | 主助教姓名 |
| `coachRatio` | `'78%'` | string | 主助教服务占比 |
| `visitFreq` | `'6.2次/月'` | string | 到店频率 |
| `daysAgo` | `3` | number | 距上次到店天数 |
| `assistants` | 2 条 | array | 助教列表（底部行） |

#### `coachDetails` 子字段

| 字段 | 示例值 | 说明 |
|------|--------|------|
| `name` | `'小燕'` | 助教姓名 |
| `cls` | `'assistant--assignee'` | 样式类（跟/弃/普通） |
| `heartScore` | `9.2` | 关系指数 |
| `badge` | `'跟'` | 跟/弃标签 |
| `avgDuration` | `'2.3h'` | 次均服务时长 |
| `serviceCount` | `'14'` | 服务次数 |
| `coachSpend` | `'¥4,200'` | 助教消费金额 |
| `relationIdx` | `9.2` | 关系指数（数值版） |

#### `assistants` 子字段

| 字段 | 示例值 | 说明 |
|------|--------|------|
| `name` | `'小燕'` | 助教姓名 |
| `cls` | `'assistant--assignee'` | 样式类 |
| `heartScore` | `9.2` | 关系指数 |
| `badge` | `'跟'` | 跟/弃标签（可选） |
| `badgeCls` | `'assistant-badge--follow'` | badge 样式类（可选） |


---

## 二、硬编码数据

### 2.1 `DIMENSION_OPTIONS`（维度下拉选项）

| 风险 | 字段 | 当前值 | 应从何处获取 |
|------|------|--------|-------------|
| 🟡 低 | `DIMENSION_OPTIONS` | 8 个维度选项（recall/potential/balance/recharge/recent/spend60/freq60/loyal） | 前端枚举，可保留硬编码；若后端支持动态维度则需 API |

```ts
const DIMENSION_OPTIONS = [
  { value: 'recall', text: '最应召回' },
  { value: 'potential', text: '最大消费潜力' },
  { value: 'balance', text: '最高余额' },
  { value: 'recharge', text: '最近充值' },
  { value: 'recent', text: '最近到店' },
  { value: 'spend60', text: '最高消费 近60天' },
  { value: 'freq60', text: '最频繁 近60天' },
  { value: 'loyal', text: '最专一 近60天' },
]
```

### 2.2 `PROJECT_OPTIONS`（项目筛选选项）

| 风险 | 字段 | 当前值 | 应从何处获取 |
|------|------|--------|-------------|
| 🟡 低 | `PROJECT_OPTIONS` | 5 个项目选项（all/chinese/snooker/mahjong/karaoke） | 前端枚举，可保留；若门店项目可配置则需 API |

```ts
const PROJECT_OPTIONS = [
  { value: 'all', text: '全部' },
  { value: 'chinese', text: '🎱 中式/追分' },
  { value: 'snooker', text: '斯诺克' },
  { value: 'mahjong', text: '🀄 麻将/棋牌' },
  { value: 'karaoke', text: '🎤 团建/K歌' },
]
```

### 2.3 `DIMENSION_TO_DIM` 映射表

| 风险 | 字段 | 说明 |
|------|------|------|
| 🟢 无 | `DIMENSION_TO_DIM` | 纯前端映射，维度 value → DimType，无需替换 |

### 2.4 `loadData()` 中的 `setTimeout(400ms)`

| 风险 | 位置 | 当前值 | 说明 |
|------|------|--------|------|
| 🔴 高 | `loadData()` | `setTimeout(() => {...}, 400)` | 模拟网络延迟，联调时必须替换为真实 `wx.request` |

### 2.5 `onPullDownRefresh()` 中的 `setTimeout(500ms)`

| 风险 | 位置 | 当前值 | 说明 |
|------|------|--------|------|
| 🟡 中 | `onPullDownRefresh()` | `setTimeout(() => wx.stopPullDownRefresh(), 500)` | 联调时应在 API 回调中调用 `wx.stopPullDownRefresh()` |

---

## 三、已对接 API

**无。** 页面中没有任何 `wx.request`、`wx.cloud.callFunction` 或封装的 API 调用。

`loadData()` 当前实现：
```ts
loadData() {
  this.setData({ pageState: 'loading' })
  setTimeout(() => {
    const data = MOCK_CUSTOMERS  // ← 直接引用内联 mock
    if (!data || data.length === 0) {
      this.setData({ pageState: 'empty' })
      return
    }
    this.setData({
      allCustomers: data,
      customers: data,
      totalCount: data.length,
      pageState: 'normal',
    })
  }, 400)
}
```

---

## 四、前端计算/派生数据

| 字段/逻辑 | 位置 | 说明 |
|-----------|------|------|
| `dimType` | `onDimensionChange()` | 由 `DIMENSION_TO_DIM[selectedDimension]` 映射得出，控制 WXML 卡片模板切换 |
| `filterBarVisible` | `onPageScroll()` | 滚动方向 + 累积距离判断，纯 UI 交互逻辑 |
| `pageState` | `loadData()` | 根据数据加载结果设置 `'loading'` / `'empty'` / `'normal'` / `'error'` |
| `totalCount` | `loadData()` | `data.length`，由返回数据长度计算 |
| `overdueDays > 7` | WXML | 控制超期标签颜色（danger / warn），纯前端判断 |
| `item.highSpendTag` | WXML | 控制是否显示"高消费"标签，值来自 mock 数据 |
| 柱状图 `opacity` | WXML | `0.2 + wIdx * 0.057`，渐变透明度，纯 UI 计算 |
| `cd.relationIdx >= 8` | WXML | 控制关系指数颜色（primary / gray），纯前端判断 |

---

## 五、路由参数

**无。** `onLoad()` 不接收任何参数，直接调用 `loadData()`。

页面作为导航目标时的入口：
- `board-finance.ts` → `wx.navigateTo({ url: '/pages/board-customer/board-customer' })`
- `board-coach.ts` → `wx.navigateTo({ url: '/pages/board-customer/board-customer' })`
- `dev-tools.ts` → 开发工具页面列表

---

## 六、WXML 中的硬编码文案

| 位置 | 硬编码文案 | 风险 | 说明 |
|------|-----------|------|------|
| 加载态 | `加载中...` | 🟢 无 | 通用 UI 文案 |
| 空状态 | `暂无客户数据` | 🟢 无 | 通用 UI 文案 |
| 错误态 | `加载失败` | 🟢 无 | 通用 UI 文案 |
| 错误态按钮 | `点击重试` | 🟢 无 | 通用 UI 文案 |
| Tab 栏 | `财务` / `客户` / `助教` | 🟢 无 | 固定导航文案 |
| 列表标题 | `客户列表` | 🟢 无 | 固定标题 |
| 列表副标题 | `· 前100名` | 🟡 低 | 若后端支持分页/动态 TopN，需改为动态值 |
| 列表计数 | `共{{totalCount}}名客户` | 🟢 无 | 动态绑定 |
| 召回维度标签 | `理想` / `已过` / `超期` / `天` | 🟢 无 | 固定维度标签 |
| 网格标签 | `30天消费` / `月均到店` / `余额` / `次均消费` / `月均消耗` / `可用` / `最后充值` / `充值` / `60天充值` / `当前余额` / `近60天消费` / `到店次数` | 🟢 无 | 固定维度标签 |
| 频率维度 | `60天到店` / `次` / `8周前` / `本周` | 🟢 无 | 固定维度标签 |
| 专一维度表头 | `助教` / `次均时长` / `服务次数` / `助教消费` / `关系指数` | 🟢 无 | 固定表头 |
| 助教标签 | `助教:` / `跟` / `弃` | 🟢 无 | 固定 UI 文案 |
| 最近到店 | `天前到店` / `理想间隔` / `60天到店` / `次均消费` | 🟢 无 | 固定维度标签 |
| 频率中间行 | `平均间隔` / `60天消费` | 🟢 无 | 固定维度标签 |
| 召回中间行 | `30天到店` / `余额` / `召回指数` | 🟢 无 | 固定维度标签 |
| 最近到店右上角 | `天前到店` | 🟢 无 | 固定维度标签 |

---

## 七、引用组件清单

| 组件 | 路径 | 数据来源 | 说明 |
|------|------|----------|------|
| `filter-dropdown` | `/components/filter-dropdown/` | 父组件传入 `options` / `value` | 纯展示组件，无自有数据 |
| `heart-icon` | `/components/heart-icon/` | 父组件传入 `score` | 纯展示组件，根据分数渲染心形图标 |
| `ai-float-button` | `/components/ai-float-button/` | 自有逻辑 | AI 悬浮按钮，独立组件 |
| `board-tab-bar` | `/custom-tab-bar/index` | `active="board"` | 底部导航栏，当前 mock 3 按钮 |
| `dev-fab` | `/components/dev-fab/` | — | 开发调试按钮，`wx:if="{{false}}"` 已隐藏 |
| `t-loading` / `t-empty` / `t-icon` / `t-tag` | TDesign | — | 第三方 UI 组件 |

---

## 八、联调 TODO

### 🔴 P0 — 必须替换

| # | 任务 | 当前状态 | 目标 |
|---|------|----------|------|
| 1 | 替换 `MOCK_CUSTOMERS` 为 API 调用 | 页面内联 3 条 mock 数据 | 调用后端客户看板 API，传入 `dimension` + `project` 参数 |
| 2 | 实现 `loadData()` 真实请求 | `setTimeout(400ms)` 模拟 | `wx.request` → 后端 API，处理 loading/error/empty 三态 |
| 3 | 下拉刷新对接 | `setTimeout(500ms)` 后停止 | API 回调中 `wx.stopPullDownRefresh()` |
| 4 | 维度切换重新请求 | `onDimensionChange` 仅切换 `dimType` | 切换维度后重新调用 API（不同维度返回不同排序/字段） |
| 5 | 项目筛选重新请求 | `onProjectChange` 仅更新 `selectedProject` | 切换项目后重新调用 API（按项目类型过滤） |

### 🟡 P1 — 建议优化

| # | 任务 | 说明 |
|---|------|------|
| 6 | 分页/滚动加载 | 当前一次性加载全部，"前 100 名"硬编码；若数据量大需分页 |
| 7 | `initPageAiColor` 调用 | 已导入但未在 `onLoad` 中调用，需补充 |
| 8 | 金额字段格式化 | Mock 中金额为预格式化字符串（`'¥2,680'`），联调后需前端格式化 |
| 9 | 相对时间计算 | `lastVisit: '3天前'`、`daysAgo: 3` 等，联调后需从时间戳计算 |
| 10 | `balance` 与 `currentBalance` 去重 | Mock 中两字段值相同，API 设计时应统一 |

### 🟢 P2 — 可保留

| # | 项目 | 说明 |
|---|------|------|
| 11 | `DIMENSION_OPTIONS` | 前端枚举，维度列表固定，可保留硬编码 |
| 12 | `PROJECT_OPTIONS` | 前端枚举，若门店项目固定可保留 |
| 13 | `DIMENSION_TO_DIM` 映射 | 纯前端逻辑，保留 |
| 14 | WXML 固定文案 | 维度标签、表头等，保留 |

---

## 九、待确认 API 设计

联调前需与后端确认以下接口设计：

```
GET /api/v1/board/customers
  Query:
    dimension: string    — 排序维度（recall/potential/balance/...）
    project: string      — 项目筛选（all/chinese/snooker/...）
    limit: number        — 返回条数（默认 100）
    site_id: string      — 门店 ID（从登录态获取）
  Response:
    total: number
    items: CustomerItem[]
```

**关键字段口径注意**：
- 金额字段（`balance`、`spend30d`、`spend60d` 等）：后端返回 `numeric(2)` 原始数值，前端负责 `¥` 前缀 + 千分位格式化
- 会员姓名：通过 `member_id` JOIN `dim_member.nickname`（DQ-6 断档，禁止直接用 `member_name`）
- 储值卡余额：DWS 层 `balance_pay`（总额）= `recharge_card_pay` + `gift_card_pay`
- 消费金额：使用 `items_sum` 口径，禁止直接使用 `consume_money`（三种历史口径混合）
