feat: full-text search (messages + customers)

Backend (yunke-server):
- GET /api/search?q=&type=messages|customers|all&channel=&start=&end=&page=&page_size=
  LIKE '%q%' scan over message.text + customer friend_nickname/real_name/
  phone/wechat_phone. type=all returns both sections; each is its own Page[T].
- type=messages orders by timestamp desc; type=customers orders by
  last_chat_time desc (nulls last).
- q required: missing/empty/whitespace -> HTTP 400.
- 14 new tests in tests/test_api_search.py (TDD red -> green).

Frontend (yunkes):
- New /search route + 搜索 nav item with Search icon.
- SearchView.vue: search bar, two stacked sections (消息结果 / 客户结果),
  counts + independent pagination per section, keyword highlighting (escaped),
  click message -> /chat/:channel/:ownerId/:talkerId,
  click customer -> all-fields drawer (CustomerList pattern reuse).
- search() wrapper in src/api/index.ts with SearchQuery/SearchPayload types.
- Empty state: "输入关键词搜索消息和客户".

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
浅念
2026-08-05 11:46:21 +08:00
parent 8fde1d8597
commit a92fa2927c
7 changed files with 1303 additions and 0 deletions
+3
View File
@@ -16,6 +16,7 @@ Includes:
* ``GET /api/calls`` (see :mod:`app.api.routers.calls`).
* ``GET /api/sms`` (see :mod:`app.api.routers.sms`).
* ``GET /api/sync/status`` (see :mod:`app.api.routers.sync`).
* ``GET /api/search`` (see :mod:`app.api.routers.search`).
CORS is configured permissively (wildcard origin, no credentials) so the local
frontend can call the API during development.
@@ -41,6 +42,7 @@ from app.api.routers import (
media,
messages,
organization,
search,
sms,
sync,
)
@@ -62,6 +64,7 @@ app.include_router(groups.router)
app.include_router(calls.router)
app.include_router(sms.router)
app.include_router(sync.router)
app.include_router(search.router)
@app.get("/api/health")