feat: remaining frontend views (groups/calls/sms/sync) + messageout media fields
Backend (Part A): - MessageOut: add file_th_url, file_hd_url, duration, transcription_text, md5 (room_id + sender_head_url already existed) so the chat timeline can render image thumbnails, voice duration/transcription, and avatars from a single response. - PhoneCallOut: add record_file so the Calls view can offer recording playback. - SmsOut: add content (full body) so the SMS detail dialog shows the whole text. - Tests: red->green for each new field (media fields default None + populated, record_file round-trip, content round-trip). Suite 267 -> 271. Frontend (Part B): - api/index.ts: wrappers getGroups/getGroupMembers/getCalls/getSms/getSyncStatus + Group/GroupMember/PhoneCall/Sms/SyncState/SyncRun/SyncStatus types in types.ts (mirror the *Out pydantic schemas). - views/GroupList.vue (/groups): table + channel filter + pagination; row click opens the group timeline. - views/CallList.vue (/calls): table + call-type filter (phone_call/voice_call/ all); "播放录音" action opens an el-dialog with <audio controls>. - views/SmsList.vue (/sms): table; row click opens a detail dialog with the full content. - views/SyncStatus.vue (/sync): two tables (states + recent_runs) with a manual refresh; run status color-coded. - ChatTimeline.vue: reused for both 1:1 DM (talkerId) and group (roomId) by branching the query; group title + per-member sender already handled by MessageBubble. - router: add /groups /calls /sms /sync + /group/:channel/:ownerId/:roomId. - layouts/MainLayout.vue: replace the single 客户列表 button with an el-menu (客户 / 群 / 通话 / 短信 / 同步); org tree kept on the left. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+118
-1
@@ -5,12 +5,20 @@
|
||||
// (the backend treats missing as "no filter").
|
||||
import { getJson } from './client'
|
||||
import type {
|
||||
CallChannel,
|
||||
Channel,
|
||||
Customer,
|
||||
Department,
|
||||
Group,
|
||||
GroupMember,
|
||||
Message,
|
||||
Page,
|
||||
PhoneCall,
|
||||
Sms,
|
||||
Staff,
|
||||
SyncRun,
|
||||
SyncState,
|
||||
SyncStatus,
|
||||
} from './types'
|
||||
|
||||
/** Drop entries whose value is `undefined`, `null`, or `''`. */
|
||||
@@ -94,4 +102,113 @@ export function getStaff(query: StaffQuery = {}): Promise<Page<Staff>> {
|
||||
)
|
||||
}
|
||||
|
||||
export type { Channel, Customer, Department, Message, Page, Staff }
|
||||
// ---------------------------------------------------------------------------
|
||||
// Chat groups
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface GroupQuery {
|
||||
channel?: Channel | 'all'
|
||||
owner_account_id?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
export function getGroups(query: GroupQuery = {}): Promise<Page<Group>> {
|
||||
return getJson<Page<Group>>(
|
||||
'/groups',
|
||||
clean({
|
||||
channel: query.channel === 'all' ? undefined : query.channel,
|
||||
owner_account_id: query.owner_account_id,
|
||||
page: query.page ?? 1,
|
||||
page_size: query.page_size ?? 20,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function getGroupMembers(roomId: string): Promise<GroupMember[]> {
|
||||
// room_id can contain '@' — axios encodes params, but path segments need
|
||||
// explicit encoding so the FastAPI `{room_id}` path param matches.
|
||||
return getJson<GroupMember[]>(
|
||||
`/groups/${encodeURIComponent(roomId)}/members`,
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phone / voice calls
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface CallQuery {
|
||||
/** call-type filter; `'all'` (default) drops the param. */
|
||||
channel?: CallChannel | 'all'
|
||||
user_id?: string
|
||||
/** ms-epoch inclusive window compared against start_call_time. */
|
||||
start?: number
|
||||
end?: number
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
export function getCalls(query: CallQuery = {}): Promise<Page<PhoneCall>> {
|
||||
return getJson<Page<PhoneCall>>(
|
||||
'/calls',
|
||||
clean({
|
||||
channel: query.channel === 'all' ? undefined : query.channel,
|
||||
user_id: query.user_id,
|
||||
start: query.start,
|
||||
end: query.end,
|
||||
page: query.page ?? 1,
|
||||
page_size: query.page_size ?? 20,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SMS
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface SmsQuery {
|
||||
user_id?: string
|
||||
/** ms-epoch inclusive window compared against create_time. */
|
||||
start?: number
|
||||
end?: number
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
export function getSms(query: SmsQuery = {}): Promise<Page<Sms>> {
|
||||
return getJson<Page<Sms>>(
|
||||
'/sms',
|
||||
clean({
|
||||
user_id: query.user_id,
|
||||
start: query.start,
|
||||
end: query.end,
|
||||
page: query.page ?? 1,
|
||||
page_size: query.page_size ?? 20,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sync monitor
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function getSyncStatus(): Promise<SyncStatus> {
|
||||
return getJson<SyncStatus>('/sync/status')
|
||||
}
|
||||
|
||||
export type {
|
||||
CallChannel,
|
||||
Channel,
|
||||
Customer,
|
||||
Department,
|
||||
Group,
|
||||
GroupMember,
|
||||
Message,
|
||||
Page,
|
||||
PhoneCall,
|
||||
Sms,
|
||||
Staff,
|
||||
SyncRun,
|
||||
SyncState,
|
||||
SyncStatus,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user