Files
yunke/docs/superpowers/plans/2026-08-08-yunke-staff-modal-phase2.md
T

11 KiB
Raw Blame History

微信员工 stat modal 列对齐 + 客户微信→聊天(Phase B)Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax.

Goal: Phase A 的 5 数字 stat modal 与云客对齐——补 序号/员工信息/员工微信 列,modal 客户微信可点 → 带日期窗的 1:1 聊天;ChatTimeline 读 start/end query 过滤。

Architecture: 纯前端 2 文件改。StaffList 的 modal fetcher 在合成分页时给每行注入 _serial/customer_wx/staff_info/staff_wx 字段(不改 YkStaffDetailModal,其 cols 仍 prop 驱动 + 支持 onClick)→ 客户微信 onClick 跳 /chat/wechat/{owner}/{talker}?start=&end=。ChatTimeline 加 useRoute 读 query 透传 getMessages(已支持 start/end)。员工姓名→drawer、badge→最近聊天 不动。

Tech Stack: Vue3 + TS + Element Plus(yunkes/)。类型门禁 vue-tsc -b(无 Vue 单测)。无后端、无新表。

Spec: docs/superpowers/specs/2026-08-08-yunke-staff-modal-phase2-design.md(已过审 + 用户确认)。


关键事实(plan 核实,来自 codebase)

  • commModalColsFor(metric) 在 StaffList.vue:340,当前返回 [{客户微信}](+ effective 加发送/接收)。需改为 (metric, row) 并返回云客 5~7 列。
  • stat modal fetcher 在 openWechatCommModal(StaffList.vue:367-397):filtered.slice(startIdx, startIdx+page_size) 后 return {items, total, page, page_size}。注入字段就在这个 slice 的 .map 里做。
  • row.account_id = 员工 wxid(owner);StaffChatCustomer.talker_id = 客户 wxid。useRouter/router 已在 StaffList(line 3/60)。cellText(row, camelKey) helper 已存在。
  • YkStaffDetailModal Col 接口支持 onClick?: (row)=>void(渲染为 link button)与 prop 驱动默认 cell → 注入字段直接可显。不改该组件。
  • ChatTimeline defineProps<{channel,ownerId,talkerId?,roomId?}>(ChatTimeline.vue:20);queryFor(72-81)、loadInitial(88)、subtitle(56-60)、watch([props...])(166-171)、onMounted(loadInitial)(173)。getMessages 已支持 start/end(api/index.ts:65-66)。
  • /chat/:channel/:ownerId/:talkerId(name 'chat',router/index.ts:61,props:true)→ query 透传。
  • 云客真机(2026-08-08):modal 列 = 序号/客户微信(可点)/[有效沟通:发送·接收]/员工信息/员工微信,无操作列;客户微信→detail/message/{owner}/{talker}/1?start=&end=(带窗过滤);员工信息=员工姓名+手机号;员工微信=昵称+wxid。

实现决策(spec §4 已定)

  1. 客户微信显示 = friend_name + talker_id(注入为 customer_wx 字段,免 formatter),不脱敏。
  2. 员工信息 = 员工姓名(userName)+ 微信手机号(phone);员工微信 = 昵称(nickName)+ wxid(account_id)——匹配云客 modal(云客员工微信显示 wxid 非 alias)。缺失字段过滤掉,全缺显 --。
  3. ChatTimeline 仅带窗过滤(从 modal 进时);页内日期选择器留总 spec 第5步。watch 把 route.query.start/end 加入依赖(支持同路径改窗重载,边际稳健)。
  4. 同页 router.push(不 window.open)。

文件清单

  • 改 yunkes/src/views/StaffList.vue:commModalColsFor(metric, row) 列补齐 + openCustomerChat + fetcher 注入字段。
  • 改 yunkes/src/views/ChatTimeline.vue:useRoute + windowStart/End computed + queryFor 透传 + subtitle 显窗 + watch 加 query 依赖。
  • YkStaffDetailModal.vue 不改;无后端。

Task 1: StaffList — modal 列对齐云客 + 客户微信可点

Files: Modify yunkes/src/views/StaffList.vue(commModalColsFor / openWechatCommModal / 新增 openCustomerChat、joinFields)。

  • Step 1: 加两个 helper(放在 commModalColsFor 之前)
/** 拼接非空字段(过滤 '--'),全缺则 '--'。用于 modal 里员工信息/员工微信 单行显示。 */
function joinFields(...vals: (string | undefined | null)[]): string {
  const parts = vals.map((v) => (v == null ? '' : String(v).trim())).filter((v) => v && v !== '--')
  return parts.length ? parts.join(' ') : '--'
}

/** modal 客户微信 → 该员工×客户聊天(带 StaffList 当前日期窗)。缺失 owner/talker 静默 no-op。 */
function openCustomerChat(row: Account, customer: StaffChatCustomer): void {
  if (!row.account_id || !customer.talker_id) return
  router.push({
    name: 'chat',
    params: { channel: 'wechat', ownerId: row.account_id, talkerId: customer.talker_id },
    query: { start: String(statsStart.value), end: String(statsEnd.value) },
  })
}

StaffChatCustomer 已在 Phase A import;router/statsStart/statsEnd/cellText 均已存在。

  • Step 2: 改 commModalColsFor 签名为 (metric, row) + 返回云客列序

把现有 commModalColsFor(metric) 整体替换为(注意 row 参数 + 客户微信 onClick + 末尾员工信息/员工微信):

// 对齐云客(2026-08-08 真机):序号 / 客户微信(可点→聊天) / [仅有效沟通:发送·接收] / 员工信息 / 员工微信。
// 序号/员工信息/员工微信 靠 fetcher 注入字段实现,不改 YkStaffDetailModal(其 cols 仍 prop 驱动)。
function commModalColsFor(metric: ChatMetric, row: Account): any[] {
  const cols: any[] = [
    { prop: '_serial', label: '序号', width: 70, align: 'center' },
    {
      prop: 'customer_wx',
      label: '客户微信',
      minWidth: 180,
      onClick: (c: any) => openCustomerChat(row, c),
    },
  ]
  if (metric === 'effective') {
    cols.push(
      { prop: 'send_count', label: '发送消息数', width: 110, align: 'center' },
      { prop: 'receive_count', label: '接收消息数', width: 110, align: 'center' },
    )
  }
  cols.push(
    { prop: 'staff_info', label: '员工信息', minWidth: 140 },
    { prop: 'staff_wx', label: '员工微信', minWidth: 160 },
  )
  return cols
}
  • Step 3: openWechatCommModal 传 row 给 cols + fetcher 注入字段

detailCols.value = commModalColsFor(metric) → commModalColsFor(metric, row)。

fetcher 里把

    return {
      items: filtered.slice(startIdx, startIdx + page_size),
      total: filtered.length,
      page,
      page_size,
    }

替换为(注入 _serial(跨页连续)/customer_wx/staff_info/staff_wx):

    const staffInfo = joinFields(cellText(row, 'userName'), cellText(row, 'phone'))
    const staffWx = joinFields(cellText(row, 'nickName'), row.account_id)
    const items = filtered.slice(startIdx, startIdx + page_size).map((c, i) => ({
      ...c,
      _serial: startIdx + i + 1,
      customer_wx: joinFields(c.friend_name, c.talker_id),
      staff_info: staffInfo,
      staff_wx: staffWx,
    }))
    return { items, total: filtered.length, page, page_size }

注:staffInfo/staffWx 对整页常量,提到 map 外只算一次。

  • Step 4: 类型门禁
cd yunkes && npx vue-tsc -b

Expected: exit 0。常见坑:commModalColsFor 旧的无参 metric 调用点遗漏(只有 openWechatCommModal 一处);customer_wx/_serial 等注入字段是 any 不报类型。

  • Step 5: Commit
git add yunkes/src/views/StaffList.vue
git commit -m "feat(staff): stat modal 列对齐云客(序号/员工信息/员工微信) + 客户微信可点带窗跳聊天"

End with blank line + Co-Authored-By: Claude <noreply@anthropic.com>。


Task 2: ChatTimeline — 读 start/end query 过滤

Files: Modify yunkes/src/views/ChatTimeline.vue。

  • Step 1: import useRoute + 实例化

line 3:import { useRouter } from 'vue-router' → import { useRoute, useRouter } from 'vue-router'。 在 const router = useRouter()(line 27)后加:

const route = useRoute()
  • Step 2: 加日期窗 computed(放在 queryFor 之前)
/** 从路由 query 读日期窗(从 StaffList stat modal 带窗跳来时有值);缺失=全量(向后兼容)。 */
const windowStart = computed(() => {
  const s = Array.isArray(route.query.start) ? route.query.start[0] : route.query.start
  return s != null && /^\d+$/.test(String(s)) ? Number(s) : undefined
})
const windowEnd = computed(() => {
  const e = Array.isArray(route.query.end) ? route.query.end[0] : route.query.end
  return e != null && /^\d+$/.test(String(e)) ? Number(e) : undefined
})
  • Step 3: queryFor 透传 start/end
function queryFor(pageNum: number) {
  return {
    channel: channelTyped.value,
    owner_account_id: props.ownerId,
    talker_id: isGroup.value ? undefined : props.talkerId,
    room_id: isGroup.value ? props.roomId : undefined,
    start: windowStart.value,
    end: windowEnd.value,
    page: pageNum,
    page_size: PAGE_SIZE,
  }
}

getMessages→clean() 会 drop undefined,故无 query 时不过滤(现行行为不变)。

  • Step 4: subtitle 有窗时显时段

把 subtitle computed 改为:

const subtitle = computed(() => {
  let s =
    `渠道:${channelLabel(channelTyped.value)} · 归属账号:${props.ownerId}` +
    (total.value > 0 ? ` · 共 ${total.value} 条` : '')
  if (windowStart.value && windowEnd.value) {
    s += ` · 时段:${formatTime(windowStart.value)} ~ ${formatTime(windowEnd.value)}`
  }
  return s
})
  • Step 5: watch 加 query 依赖(支持同路径改窗重载)
watch(
  () =>
    [props.channel, props.ownerId, props.talkerId, props.roomId, windowStart.value, windowEnd.value] as const,
  () => {
    loadInitial()
  },
)
  • Step 6: 类型门禁
cd yunkes && npx vue-tsc -b

Expected: exit 0。

  • Step 7: Commit
git add yunkes/src/views/ChatTimeline.vue
git commit -m "feat(chat): ChatTimeline 支持 start/end query 日期窗过滤(从 stat modal 带窗跳入)"

End with blank line + Co-Authored-By: Claude <noreply@anthropic.com>。


验收(spec §10)

  • cd yunkes && npx vue-tsc -b exit 0,无 console 报错。
  • 浏览器实测(本地后端 smoke.db + 前端,绝不碰云客):
    • StaffList 微信员工行点 5 数字 → modal 列对齐云客:序号 / 客户微信 / [有效沟通多发送·接收] / 员工信息 / 员工微信;无操作列;序号跨页连续。
    • 点 modal 客户微信 → 跳 /chat/wechat/{owner}/{talker}?start=&end=,URL 带窗;ChatTimeline 仅显示窗内消息,副标题显时段;改 StaffList 日期窗 → modal 数字与跳转窗同步。
    • 从其它入口进 ChatTimeline(CustomerList「查看聊天记录」、StaffList badge viewChat)无 query → 全量(向后兼容)。
    • 员工姓名→drawer、badge→最近聊天 不变;WA tab 不变。
    • network 仅 localhost(无云客)。

风险

  • ChatTimeline 过滤+分页:loadInitial 先取 page1 算 total 再跳末页——带窗时 total 是窗内总数,末页为窗内最新,逻辑不变;loadOlder 往前翻亦在窗内。浏览器验。
  • 字段缺失:员工信息/员工微信 缺字段→joinFields 过滤掉,全缺显 --(不阻塞)。
  • 同路径改窗:watch 已加 query 依赖(Step 5)→ URL 改 start/end 会重载;主流程(modal 重新点击)走 onMounted。