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,
|
||||
}
|
||||
|
||||
@@ -50,6 +50,101 @@ export interface Staff {
|
||||
is_on_job?: boolean | null
|
||||
}
|
||||
|
||||
/** Chat group summary row (mirrors backend GroupOut). */
|
||||
export interface Group {
|
||||
room_id: string
|
||||
channel: Channel
|
||||
room_name?: string | null
|
||||
owner_account_id?: string | null
|
||||
owner_account_nickname?: string | null
|
||||
member_count?: number | null
|
||||
message_count?: number | null
|
||||
/** ms-epoch of most recent message */
|
||||
last_chat_time?: number | null
|
||||
}
|
||||
|
||||
/** Member of a chat group (mirrors backend GroupMemberOut). */
|
||||
export interface GroupMember {
|
||||
channel: Channel
|
||||
room_id: string
|
||||
member_id: string
|
||||
nickname?: string | null
|
||||
display_name?: string | null
|
||||
avatar?: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Phone / voice call record (mirrors backend PhoneCallOut). `channel` here is
|
||||
* the call-type in {phone_call, voice_call}, not a messaging channel.
|
||||
*/
|
||||
export type CallChannel = 'phone_call' | 'voice_call'
|
||||
|
||||
export interface PhoneCall {
|
||||
call_id: string
|
||||
channel: CallChannel
|
||||
/** ms-epoch */
|
||||
start_call_time?: number | null
|
||||
/** seconds */
|
||||
call_duration?: number | null
|
||||
call_status?: number | null
|
||||
/** 1 = incoming, 0 = outgoing (per upstream schema). */
|
||||
incoming_call?: number | null
|
||||
user_id?: string | null
|
||||
user_id_name?: string | null
|
||||
call_number?: string | null
|
||||
sim_phone?: string | null
|
||||
/** OSS URL of the recording — present only when a recording exists. */
|
||||
record_file?: string | null
|
||||
}
|
||||
|
||||
/** SMS log row (mirrors backend SmsOut). */
|
||||
export interface Sms {
|
||||
sms_id: string
|
||||
customer_phone?: string | null
|
||||
content_preview?: string | null
|
||||
/** Full SMS body (shown in the detail dialog). */
|
||||
content?: string | null
|
||||
sender?: string | null
|
||||
/** 1 = sent (outbound), 0 = received (inbound). */
|
||||
direction?: number | null
|
||||
user_id?: string | null
|
||||
receive_name?: string | null
|
||||
/** ms-epoch */
|
||||
create_time?: number | null
|
||||
sim_phone?: string | null
|
||||
}
|
||||
|
||||
/** Resumable sync cursor (mirrors backend SyncStateOut). */
|
||||
export interface SyncState {
|
||||
state_id: number
|
||||
entity_type: string
|
||||
channel?: string | null
|
||||
scope_id?: string | null
|
||||
last_version?: string | null
|
||||
last_timestamp?: number | null
|
||||
last_synced_at?: string | null
|
||||
}
|
||||
|
||||
/** Audit row for a single sync execution (mirrors backend SyncRunOut). */
|
||||
export interface SyncRun {
|
||||
run_id: number
|
||||
entity_type: string
|
||||
channel?: string | null
|
||||
scope_id?: string | null
|
||||
status: string
|
||||
started_at: string
|
||||
finished_at?: string | null
|
||||
records_fetched: number
|
||||
records_upserted: number
|
||||
error?: string | null
|
||||
}
|
||||
|
||||
/** Aggregate monitor payload returned by /api/sync/status. */
|
||||
export interface SyncStatus {
|
||||
states: SyncState[]
|
||||
recent_runs: SyncRun[]
|
||||
}
|
||||
|
||||
/** Normalized message type label (text/image/video/voice/divider/system/unknown). */
|
||||
export type MessageType =
|
||||
| 'text'
|
||||
|
||||
Reference in New Issue
Block a user