Spec: align all list/detail pages to 云客 (columns + interactions); covers interaction taxonomy, backend extensions (customer add_time/filters, /api/sessions), phase-0 capture. Plan 1: StaffList interaction closure + customer backend + YkStaffDetailModal. Co-Authored-By: Claude <noreply@anthropic.com>
30 KiB
StaffList 交互补全 + customer 后端基础 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 for tracking.
Goal: 把 StaffList(员工列表)的交互对齐云客——统计数字点击弹明细 modal、操作按钮跳转按员工筛选的列表、昵称跳聊天——并补齐所需后端能力(customer add_time 列 + 筛选参数),抽通用明细 modal 组件供后续页面复用。
Architecture: 后端先 TDD 扩展(Customer.add_time 列 + Alembic 迁移 + scraper 映射 + 回填脚本 + router 三个筛选参数),再前端:通用 YkStaffDetailModal 组件 + StaffList 三类交互接入 + CustomerList/GroupList 读 query 预筛。
Tech Stack: FastAPI + SQLAlchemy 2.0 async + Alembic(后端);Vue3 + TS + Element Plus + Pinia(前端)。
Spec: docs/superpowers/specs/2026-08-05-yunke-ui-alignment-design.md
已采集事实(WhatsApp 员工表交互,Playwright 验证):
- 添加人数/沟通人数(数字)→ modal(明细表)
- 操作「好友列表」→
/waBusiness/friendlist/0/{userId};「群聊」→friendlist/1/{userId} - WA 昵称 → 新 tab 聊天记录(
wschatroom,会话列表+消息流,只读)
运行命令(验证用):
- 后端:
cd yunke-server && DB_URL="sqlite+aiosqlite:///./smoke.db" .venv/Scripts/python.exe -m uvicorn app.api.main:app --port 8000 --log-level warning - 前端:
cd yunkes && npm run dev - 后端测试:
cd yunke-server && .venv/Scripts/python.exe -m pytest tests/ -x -q - 前端类型:
cd yunkes && npx vue-tsc -b
File Map
后端(yunke-server/):
- Modify
app/db/models.py—Customer加add_time列 - Modify
app/api/schemas.py—CustomerOut加add_time - Create
migrations/versions/<rev>_customer_add_time.py— Alembic 迁移 - Modify
app/scraper/wechat.py— customer 映射加add_time - Modify
app/scraper/whatsapp.py— customer 映射加add_time - Create
scripts/backfill_customer_add_time.py— 解析 raw_json 回填历史 - Modify
app/api/routers/customer.py— 加user_id/add_time_since/last_chat_since - Modify
tests/test_api_customer.py— 新筛选参数测试 - Modify
tests/test_scrape_wechat_friends.py/test_scrape_whatsapp_friends.py— add_time 映射测试
前端(yunkes/src/):
- Modify
api/types.ts—Customer加add_time - Modify
api/index.ts—CustomerQuery+getCustomers加三参数 - Create
components/YkStaffDetailModal.vue— 通用明细 modal - Modify
views/StaffList.vue— 数字→modal、操作→跳转、昵称→聊天 - Modify
views/CustomerList.vue/views/GroupList.vue— 读route.queryowner/user 预筛
Task 0: 探测微信员工表交互(产出目标映射)
WhatsApp 员工表交互已采集(见上)。微信员工表(
/cms/wechat/wechatlist)列结构已采,但数字是否可点击未探测。本 task 用同一探测脚本补齐。
Files: 无代码改动,产出记录写入本 plan 末尾「探测结果」。
-
Step 1: 浏览器登录云客 + 打开微信员工列表(Playwright;若已登录复用会话) 导航到
https://crm2.yunkecn.com/cms/wechat/wechatlist。 -
Step 2: 跑探测脚本(参照 spec「交互探测方法」):对第一行每个可点元素(button/a)点击,记录 URL 变化 / modal title / popup,每次点前回列表页、关残留 modal/tab。
-
Step 3: 把结果填到本 plan 末尾「微信员工表探测结果」,据此调整 Task 7/8 的数字→维度映射(微信表的"好友数/消息数/沟通数/及时回复"哪些可点、点出什么 modal)。
-
Step 4: Commit 探测结论(更新本 plan)。
git add docs/superpowers/plans/2026-08-05-yunke-stafflist-interaction.md git commit -m "docs: record wechat staff-list interaction probe results"
Task 1: Customer.add_time 列 + schema + Alembic 迁移
Files:
-
Modify:
yunke-server/app/db/models.py:235(Customer,last_chat_time下方加add_time) -
Modify:
yunke-server/app/api/schemas.py(CustomerOut) -
Create:
yunke-server/migrations/versions/<rev>_customer_add_time.py -
Step 1: 加模型列(
app/db/models.py,Customer类内,紧跟last_chat_time):last_chat_time: Mapped[int | None] = mapped_column(BigInteger, nullable=True) add_time: Mapped[int | None] = mapped_column(BigInteger, nullable=True) # ms epoch; from raw_json.addTime -
Step 2: 加 schema 字段(
app/api/schemas.py,CustomerOut,紧跟last_chat_time):last_chat_time: int | None = None add_time: int | None = None -
Step 3: 生成 Alembic 迁移
cd yunke-server && DB_URL="sqlite+aiosqlite:///./_autogen.db" .venv/Scripts/python.exe -m alembic revision --autogenerate -m "customer add_time"预期:
migrations/versions/<hash>_customer_add_time.py生成,内含op.add_column('customer', sa.Column('add_time', sa.BigInteger(), nullable=True))。 -
Step 4: 应用迁移到 smoke.db 验证
cd yunke-server && DB_URL="sqlite+aiosqlite:///./smoke.db" .venv/Scripts/python.exe -m alembic upgrade head预期:
Running upgrade ... -> <hash>, customer add_time。验证:sqlite3 smoke.db "PRAGMA table_info(customer)"含add_time。 -
Step 5: 跑模型测试确认不破坏
cd yunke-server && .venv/Scripts/python.exe -m pytest tests/test_models.py tests/test_api_customer.py -x -q预期:全 PASS。
-
Step 6: Commit
git add yunke-server/app/db/models.py yunke-server/app/api/schemas.py yunke-server/migrations/versions/ git commit -m "feat(db): add Customer.add_time column + migration"
Task 2: scraper 映射 add_time(wechat + whatsapp)
addTime实测为字符串"YYYY-MM-DD HH:mm:ss"(见 smoke.db);whatsapplastUpdate已用parse_datetime_to_ms。add_time要支持字符串与 epoch 两种输入。复用parse_datetime_to_ms(app/scraper/whatsapp.py已定义;若 wechat.py 未 import,提到app/scraper/_datetime.py共享或直接 import)。
Files:
-
Modify:
yunke-server/app/scraper/wechat.py(customer 映射函数,~line 164-192) -
Modify:
yunke-server/app/scraper/whatsapp.py(customer 映射函数,~line 252-264) -
Test:
yunke-server/tests/test_scrape_wechat_friends.py,test_scrape_whatsapp_friends.py -
Step 1: 写失败测试(
tests/test_scrape_wechat_friends.py,加一个 case)。 函数名是_normalize(app/scraper/wechat.py:157,不是_normalize_friend):from app.scraper.wechat import _normalize def test_normalize_maps_add_time_string_to_epoch(): record = {"friendWxId": "x", "addTime": "2026-04-28 16:24:39", "lastChatTime": 1700000000000} out = _normalize(record) # parse_datetime_to_ms 按 naive UTC 解析;断言是合理的近期 epoch,不写死值(避免 tz/算术陷阱)。 # 2026 年任意时刻 > 1.76e12。 assert isinstance(out["add_time"], int) and out["add_time"] > 1_760_000_000_000 -
Step 2: 跑测试确认 FAIL
cd yunke-server && .venv/Scripts/python.exe -m pytest tests/test_scrape_wechat_friends.py::test_normalize_friend_maps_add_time_string_to_epoch -x -q预期:KeyError 或 None(add_time 未映射)。
-
Step 3: wechat 映射加 add_time(
app/scraper/wechat.py,customer 映射 return dict 内,紧跟last_chat_time):"last_chat_time": record.get("lastChatTime"), "add_time": _parse_add_time(record.get("addTime")),在文件 helper 区加(处理字符串+epoch)。
parse_datetime_to_ms实际定义在app/scraper/parsers.py(whatsapp.py仅 re-import),按 naive UTC 解析:from app.scraper.parsers import parse_datetime_to_ms _ADD_TIME_FORMATS = ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M") def _parse_add_time(v): """addTime: ms-epoch int 或 'YYYY-MM-DD HH:mm[:ss]' 字符串 → ms epoch (naive UTC)。""" if v is None or v == "": return None if isinstance(v, (int, float)): return int(v) return parse_datetime_to_ms(v, _ADD_TIME_FORMATS)先读
app/scraper/parsers.py的parse_datetime_to_ms确认它接受 format tuple 且返回 ms epoch(naive UTC)。若签名不同,改为内联:int(dt.datetime.strptime(v, fmt).replace(tzinfo=dt.UTC).timestamp() * 1000)(遍历_ADD_TIME_FORMATS容错)。 -
Step 4: 跑 wechat 测试确认 PASS。然后处理 whatsapp(
app/scraper/whatsapp.py的_normalize_whatsapp_friend,~line 252)。先 dump 确认 WA customer 有无 customer-side add-time 字段:
cd yunke-server && .venv/Scripts/python.exe -c "import sqlite3,json; c=sqlite3.connect('smoke.db'); r=c.execute(\"SELECT raw_json FROM customer WHERE channel='whatsapp' LIMIT 1\").fetchone(); print(list(json.loads(r[0]).keys()) if r else 'none')"⚠️ 关键:whatsapp
raw_json里的createTime是 owner-staff 字段(见_normalize_whatsapp_frienddocstring line 246-251),不是 customer 的添加时间。若 dump 确认 WA customer 无独立的 add-time 字段,则 WA 的 add_time 留 None,不要强行用 createTime(会语义错误)。仅 wechat add_time 可靠填充;WA"添加人数"modal 届时按 wechat 路径或降级处理。 -
Step 5: 跑全部 scraper 测试
cd yunke-server && .venv/Scripts/python.exe -m pytest tests/test_scrape_wechat_friends.py tests/test_scrape_whatsapp_friends.py -x -q预期:PASS。
-
Step 6: Commit
git add yunke-server/app/scraper/ yunke-server/tests/test_scrape_wechat_friends.py yunke-server/tests/test_scrape_whatsapp_friends.py git commit -m "feat(scraper): map customer add_time from raw_json"
Task 3: add_time 回填脚本(历史数据)
解析现有 customer 行的
raw_json.addTime→add_time列。不重抓(区别于backfill_customers.py)。参照其 async sessionmaker + batch commit 模式。
Files:
-
Create:
yunke-server/scripts/backfill_customer_add_time.py -
Step 1: 写脚本(参照
scripts/backfill_customers.py的 engine/sessionmaker 设置):"""Backfill Customer.add_time from raw_json.addTime (no re-scrape). Iterates existing customer rows, parses raw_json.addTime (str or epoch) → add_time ms-epoch, updates in batches. Idempotent: skips rows already set. """ import asyncio import json from sqlalchemy import select, update from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from app.db import models # noqa: F401 from app.db.models import Customer from app.scraper.wechat import _parse_add_time # reuse the parser BATCH = 200 async def main(db_url: str = "sqlite+aiosqlite:///./smoke.db") -> None: engine = create_async_engine(db_url) sm = async_sessionmaker(engine, expire_on_commit=False) done = 0 async with sm() as s: rows = (await s.execute(select(Customer.friend_id, Customer.raw_json, Customer.add_time))).all() for i, (fid, raw, existing) in enumerate(rows): if existing or not raw: continue val = _parse_add_time(raw.get("addTime")) if val is None: continue await s.execute(update(Customer).where(Customer.friend_id == fid).values(add_time=val)) done += 1 if done % BATCH == 0: await s.commit() print(f"[{time.strftime('%H:%M:%S')}] {done} updated...") await s.commit() print(f"DONE — {done} customers got add_time") await engine.dispose() if __name__ == "__main__": import time asyncio.run(main())补
import time到顶部(plan 内为可读性放底部,实际放顶部)。 -
Step 2: 跑回填(smoke.db)
cd yunke-server && .venv/Scripts/python.exe -m scripts.backfill_customer_add_time预期:
DONE — N customers got add_time。 -
Step 3: 验证 —— query smoke.db 确认 add_time 非空:
cd yunke-server && .venv/Scripts/python.exe -c "import sqlite3;c=sqlite3.connect('smoke.db');print('with add_time:', c.execute('SELECT COUNT(*) FROM customer WHERE add_time IS NOT NULL').fetchone()[0]); print('total:', c.execute('SELECT COUNT(*) FROM customer').fetchone()[0])" -
Step 4: Commit
git add yunke-server/scripts/backfill_customer_add_time.py git commit -m "feat(scripts): backfill Customer.add_time from raw_json"
Task 4: customer router 加 user_id / add_time_since / last_chat_since(TDD)
Files:
-
Modify:
yunke-server/app/api/routers/customer.py(list_customers签名 + conditions) -
Test:
yunke-server/tests/test_api_customer.py -
Step 1: 写失败测试(
tests/test_api_customer.py)。 ⚠️ 不要扩展共享clientfixture——它内联 seed 3 行,且 4 个现有测试断言total == 3(test_list_default_page_returns_all等),加行会破坏它们。新建独立 fixture(参照tests/test_api_customer.py:47-93的导入与 app/client 构造方式,对齐conftest.py的async_engine):@pytest_asyncio.fixture async def filter_client(async_engine): async with async_engine.connect() as conn: await conn.run_sync(Base.metadata.create_all) Session = async_sessionmaker(async_engine, expire_on_commit=False) async with Session() as s: s.add_all([ Customer(friend_id="u1_a", channel="wechat", user_id="U1", add_time=1000, last_chat_time=500), Customer(friend_id="u1_b", channel="wechat", user_id="U1", add_time=2000, last_chat_time=2000), Customer(friend_id="u2_a", channel="wechat", user_id="U2", add_time=3000, last_chat_time=3000), ]) await s.commit() async with AsyncClient(app=app, base_url="http://test") as c: yield c async def test_filter_by_user_id(filter_client): r = await filter_client.get("/api/customers", params={"user_id": "U1"}) assert {c["friend_id"] for c in r.json()["items"]} == {"u1_a", "u1_b"} async def test_filter_by_add_time_since(filter_client): r = await filter_client.get("/api/customers", params={"add_time_since": 1500}) ids = [c["friend_id"] for c in r.json()["items"]] assert "u1_a" not in ids and "u1_b" in ids and "u2_a" in ids async def test_filter_by_last_chat_since(filter_client): r = await filter_client.get("/api/customers", params={"last_chat_since": 1500}) assert "u1_a" not in [c["friend_id"] for c in r.json()["items"]] -
Step 2: 跑测试确认 FAIL
cd yunke-server && .venv/Scripts/python.exe -m pytest tests/test_api_customer.py -k "user_id or add_time_since or last_chat_since" -x -q预期:FAIL(参数未识别 / 无筛选)。
-
Step 3: 实现筛选(
app/api/routers/customer.py): 签名加:user_id: Annotated[str | None, Query()] = None, add_time_since: Annotated[int | None, Query(ge=0)] = None, last_chat_since: Annotated[int | None, Query(ge=0)] = None,conditions 加:
if user_id is not None: conditions.append(Customer.user_id == user_id) if add_time_since is not None: conditions.append(Customer.add_time >= add_time_since) if last_chat_since is not None: conditions.append(Customer.last_chat_time >= last_chat_since) -
Step 4: 跑测试确认 PASS
cd yunke-server && .venv/Scripts/python.exe -m pytest tests/test_api_customer.py -x -q -
Step 5: 全量回归
cd yunke-server && .venv/Scripts/python.exe -m pytest tests/ -x -q预期:全 PASS(≥323)。
-
Step 6: Commit
git add yunke-server/app/api/routers/customer.py yunke-server/tests/test_api_customer.py git commit -m "feat(api): customer filters user_id / add_time_since / last_chat_since"
Task 5: 前端 api/types 加 customer 参数与字段
Files:
-
Modify:
yunkes/src/api/types.ts(Customer) -
Modify:
yunkes/src/api/index.ts(CustomerQuery+getCustomers) -
Step 1:
types.tsCustomer加字段(紧跟last_chat_time):last_chat_time?: number | null /** ms epoch of friend add time (from raw_json.addTime). */ add_time?: number | null -
Step 2:
api/index.tsCustomerQuery加三参数 +getCustomers透传:export interface CustomerQuery { channel?: Channel | 'all' page?: number page_size?: number owner_account_id?: string user_id?: string department_id?: string add_time_since?: number last_chat_since?: number } export function getCustomers(query: CustomerQuery = {}): Promise<Page<Customer>> { return getJson<Page<Customer>>( '/customers', clean({ page: query.page ?? 1, page_size: query.page_size ?? 20, channel: query.channel === 'all' ? undefined : query.channel, owner_account_id: query.owner_account_id, user_id: query.user_id, department_id: query.department_id, add_time_since: query.add_time_since, last_chat_since: query.last_chat_since, }), ) } -
Step 3: 类型检查
cd yunkes && npx vue-tsc -b预期:exit 0。
-
Step 4: Commit
git add yunkes/src/api/types.ts yunkes/src/api/index.ts git commit -m "feat(api): customer query params user_id/add_time_since/last_chat_since"
Task 6: YkStaffDetailModal 通用组件
云客"统计数字→明细 modal"的通用复用件。Props:标题、API 函数、查询参数、列定义。内部
el-dialog+el-table+el-pagination。
Files:
-
Create:
yunkes/src/components/YkStaffDetailModal.vue -
Step 1: 写组件:
<script setup lang="ts"> import { ref, watch } from 'vue' import type { Page } from '@/api/types' /** * 通用明细 modal —— 复刻云客"统计数字点击弹明细表"。 * 调用方传 fetcher(返回 Page<T>)+ 列定义;组件负责 dialog/table/pagination。 */ interface Col { prop: string label: string width?: number | string minWidth?: number | string align?: 'left' | 'center' | 'right' } const props = defineProps<{ modelValue: boolean title: string cols: Col[] /** fetcher(page, page_size) → Page<T>。宽松 Page<any>:Customer 无 index signature, * 不可直接赋 Page<Record<string,unknown>>,故用 any 接纳 Page<Customer> 等。 */ fetcher: (page: number, page_size: number) => Promise<Page<any>> pageSizes?: number[] }>() const emit = defineEmits<{ 'update:modelValue': [boolean] }>() const loading = ref(false) const items = ref<any[]>([]) const total = ref(0) const page = ref(1) const pageSize = ref(props.pageSizes?.[0] ?? 10) async function load() { loading.value = true try { const res = await props.fetcher(page.value, pageSize.value) items.value = res.items total.value = res.total } finally { loading.value = false } } watch( () => props.modelValue, (open) => { if (open) { page.value = 1 load() } }, ) </script> <template> <el-dialog :model-value="modelValue" :title="title" width="900px" @update:model-value="emit('update:modelValue', $event)" > <el-table v-loading="loading" :data="items" border style="width: 100%" size="small"> <el-table-column v-for="c in cols" :key="c.prop" :prop="c.prop" :label="c.label" :width="c.width" :min-width="c.minWidth" :align="c.align" show-overflow-tooltip /> </el-table> <div style="margin-top: 10px; display: flex; justify-content: flex-end"> <el-pagination :current-page="page" :page-size="pageSize" :page-sizes="pageSizes ?? [10, 20, 50]" :total="total" layout="total, sizes, prev, pager, next" background size="small" @current-change="(p: number) => { page = p; load() }" @size-change="(s: number) => { pageSize = s; page = 1; load() }" /> </div> </el-dialog> </template> -
Step 2: 类型检查
cd yunkes && npx vue-tsc -b预期:exit 0。
-
Step 3: Commit
git add yunkes/src/components/YkStaffDetailModal.vue git commit -m "feat(ui): YkStaffDetailModal shared detail-modal component"
Task 7: StaffList WhatsApp 数字→modal(添加人数 / 沟通人数)
云客 WhatsApp 员工表:点「添加人数」→ modal 列
序号/员工信息/员工WhatsApp/客户WhatsApp/添加时间/最后沟通时间;点「沟通人数」→ 类似(沟通客户)。我们用 customer 端点 +user_id+add_time_since/last_chat_since筛选近似。
Files:
-
Modify:
yunkes/src/views/StaffList.vue -
Step 1: import 组件 + getCustomers StaffList
<script setup>顶部加:import { getCustomers } from '@/api' import YkStaffDetailModal from '@/components/YkStaffDetailModal.vue'加 modal 状态 + fetcher(script 内):
// ---- detail modal (stat-number click → 明细) ---- const detailOpen = ref(false) const detailTitle = ref('') const detailFetcher = ref<(p: number, s: number) => Promise<any>>(async () => ({ items: [], total: 0 })) const detailCols = ref<any[]>([]) const baseCustomerCols = [ { prop: 'friend_nickname', label: '客户', minWidth: 140 }, { prop: 'phone', label: '客户号码', width: 130 }, { prop: 'add_time', label: '添加时间', width: 150 }, { prop: 'last_chat_time', label: '最后沟通', width: 150 }, ] function openAddedModal(row: Account) { // 添加人数 = 该员工最近新增好友(add_time 近 N 天;N 待 Task 0 确认,默认 7) const since = Date.now() - 7 * 86400000 detailTitle.value = `添加人数 · ${row.user_name ?? row.account_id}` detailCols.value = baseCustomerCols detailFetcher.value = (p, s) => getCustomers({ user_id: row.user_id!, channel: 'whatsapp', add_time_since: since, page: p, page_size: s }) detailOpen.value = true } function openTalkedModal(row: Account) { const since = Date.now() - 7 * 86400000 detailTitle.value = `沟通人数 · ${row.user_name ?? row.account_id}` detailCols.value = baseCustomerCols detailFetcher.value = (p, s) => getCustomers({ user_id: row.user_id!, channel: 'whatsapp', last_chat_since: since, page: p, page_size: s }) detailOpen.value = true }Date.now()在 StaffList 浏览器运行时可用(非 workflow 脚本)。 -
Step 2: 模板——把 WhatsApp 的「添加人数」「沟通人数」列从纯数字改为可点 link 找到 WhatsApp tab 的:
<el-table-column label="添加人数" width="100" align="right"> <template #default="{ row }">{{ cellNum(row, 'friendAddNum') }}</template> </el-table-column>改为:
<el-table-column label="添加人数" width="100" align="right"> <template #default="{ row }"> <el-button link type="primary" size="small" @click.stop="openAddedModal(row)"> {{ cellNum(row, 'friendAddNum') }} </el-button> </template> </el-table-column>「沟通人数」同理用
openTalkedModal。 -
Step 3: 模板末尾挂 modal(在
</el-tabs>后、yk-staff__hint前):<YkStaffDetailModal v-model="detailOpen" :title="detailTitle" :cols="detailCols" :fetcher="detailFetcher" /> -
Step 4: 类型检查 + 浏览器验证
cd yunkes && npx vue-tsc -b浏览器打开
http://localhost:5173/staff→ WhatsApp tab → 点「添加人数」数字 → 确认弹 modal 且有数据。 -
Step 5: Commit
git add yunkes/src/views/StaffList.vue git commit -m "feat(staff): WhatsApp stat-number click → detail modal"
Task 8: StaffList 微信数字→modal
基于 Task 0 探测结果。微信员工表的「好友数/消息数/沟通数/及时回复」哪些可点、点出什么,按探测结论接入(复用
openAddedModal/openTalkedModal模式 + 调整维度)。
Files: Modify yunkes/src/views/StaffList.vue
-
Step 1: 据探测结果(本 plan 末尾「微信员工表探测结果」),为微信 tab 的可点数字加
@click.stop="openXxxModal(row)"。 -
Step 2: 浏览器验证微信 tab 各数字点击 → modal 正确。
-
Step 3: 类型检查 + Commit
cd yunkes && npx vue-tsc -b git add yunkes/src/views/StaffList.vue git commit -m "feat(staff): wechat stat-number click → detail modal"
Task 9: 操作按钮跳转 + CustomerList/GroupList 读 query(含后端 groups user_id TDD)
⚠️ 本 task 含一个后端 TDD 子任务(Step 1:groups router 加
user_id,独立于前端改动,等同 Task 4 的规模,不要跳过测试)。
云客「好友列表」→ 该员工客户列表;「群聊」→ 该员工群列表。我们
router.push('/customers') + setChannel + ?user_id=;目标页读 query 预筛。
Files:
- Modify:
yunkes/src/views/StaffList.vue(已有viewCustomers/viewGroups,改为带 query + 按 row) - Modify:
yunkes/src/views/CustomerList.vue(读route.query.user_id) - Modify:
yunkes/src/views/GroupList.vue(读route.query.user_id,调/api/groups待加user_id——见下注)
注:groups router 加
user_id不在本 plan(spec 列了但属通用基础,可在此顺带加,参照 Task 4 模式:ChatGroup.owner_user_id == user_id)。本 task 顺带加。
-
Step 1: groups router 加 user_id(
app/api/routers/groups.py,参照 customer.py): 签名加user_id: Annotated[str | None, Query()] = None,conditions 加if user_id is not None: conditions.append(ChatGroup.owner_user_id == user_id)。加测试tests/test_api_groups.py::test_filter_by_user_id。跑测试 PASS。 -
Step 2: 前端 GroupQuery + getGroups 加 user_id(
api/index.ts)。 -
Step 3: StaffList
viewCustomers/viewGroups改为按 row + 带 query:function viewCustomers(row: Account) { channelStore.setChannel('whatsapp') router.push({ path: '/customers', query: row.user_id ? { user_id: row.user_id } : {} }) } function viewGroups(row: Account) { channelStore.setChannel('whatsapp') router.push({ path: '/groups', query: row.user_id ? { user_id: row.user_id } : {} }) }模板操作列改为
@click.stop="viewCustomers(row)"/viewGroups(row)"。 -
Step 4: CustomerList 读 user_id query(参照它已读
department_id的模式,~line 33):const userId = route.query.user_id ? String(route.query.user_id) : nullgetCustomers调用加user_id: userId;watch 响应userId变化;顶部加 chip + 清除(清除时router.replace({ query: {} }))。 -
Step 5: GroupList 同理读 user_id query。
-
Step 6: 浏览器验证 —— StaffList WhatsApp → 点「好友列表」→ 跳客户列表,只显示该员工客户 + 顶部 chip;点「群聊」同理。
-
Step 7: 类型检查 + 全后端回归 + Commit
cd yunkes && npx vue-tsc -b cd ../yunke-server && .venv/Scripts/python.exe -m pytest tests/ -x -q git add ... git commit -m "feat(staff): action buttons → filtered customer/group list"
Task 10: 昵称→聊天(ChatTimeline)
云客点 WA 昵称开聊天记录页(会话列表+消息流)。我们 ChatTimeline 当前是消息流,需具体 talkerId。点员工昵称跳"该账号聊天"——暂降级为跳 ChatTimeline 用该账号最近一条会话(spec 风险章节已记)。
Files: Modify yunkes/src/views/StaffList.vue
-
Step 1: 加
viewChat函数(取该 owner 最近一条 message 的 talker_id,跳 ChatTimeline):import { getMessages } from '@/api' async function viewChat(row: Account) { // 最近一条会话的 talker const res = await getMessages({ channel: row.channel, owner_account_id: row.account_id, page: 1, page_size: 1 }) const m = res.items[0] if (!m) { ElMessage.info('该账号暂无聊天记录'); return } const path = m.room_id ? `/group/${row.channel}/${row.account_id}/${m.room_id}` : `/chat/${row.channel}/${row.account_id}/${m.talker_id}` router.push(path) } -
Step 2: 模板——WhatsApp「员工WhatsApp」列昵称可点: 找到
<span class="yk-staffinfo__nick" :title=...">{{ cellText(row, 'nickName') }}</span>,包一层@click.stop="viewChat(row)"+class="yk-link",加.yk-link { color: var(--el-color-primary); cursor: pointer; }样式。 -
Step 3: 浏览器验证 —— 点 WhatsApp 昵称 → 跳 ChatTimeline 该账号聊天。
-
Step 4: 类型检查 + Commit
cd yunkes && npx vue-tsc -b git add yunkes/src/views/StaffList.vue git commit -m "feat(staff): WhatsApp nickname → chat timeline"
收尾验证
- 全后端测试绿:
cd yunke-server && .venv/Scripts/python.exe -m pytest tests/ -q(≥323) - 前端类型:
cd yunkes && npx vue-tsc -bexit 0 - 浏览器
http://localhost:5173/staff:微信+WhatsApp 两 tab,数字点击、操作跳转、昵称跳聊天全部对照云客验证,截图留档。 /api/customers?user_id=X&add_time_since=Y返回正确筛选项(curl 验证)。
微信员工表探测结果(Task 0 填写)
待 Task 0 执行后填写:每个可点数字 → modal 标题 + 列 + 对应后端筛选维度。
- 好友数(1229):____ → modal? 列? 筛选维度?
- 消息数:____
- 沟通数:____
- 及时回复:____
- 员工信息列内元素:____
后续 Plan(不在本 plan 范围)
- Plan 2: CustomerList 列+交互对齐(phase 0 采集后写)
- Plan 3: GroupList / CallList / SmsList
- Plan 4: ChatTimeline 会话列表 +
/api/sessions聚合端点 - Plan 5: SearchView + AccountList 删除