// 数据分析板块 API(2026-09-04)。client.ts 的 http 已带 Bearer + baseURL=/api, // getJson/requestJson 负责解包与泛型。Page/Channel 沿用 types.ts 现成定义。 import { getJson, requestJson } from './client' import type { Channel, Page } from './types' export type { Channel } export type Scope = 'today' | 'week' | 'month' // 看板 v2(2026-09-07):overview 渠道参数支持 'all'=全平台(后端译成 // channel=None);列表页(churn/unreplied)仍是单渠道,沿用 types.ts 的 Channel export type DashboardChannel = 'all' | Channel export interface OverviewCards { msg_total: number active_customers: number active_staff: number unmapped_accounts: number call_cnt: number first_contact: number prev_msg_total?: number msg_total_delta?: number | null } export interface TrendPoint { day: number; msg: number; active_customers: number; call_cnt: number } export interface AnomalyStaff { user_id: string; yesterday: number; prev7_mean: number; new?: boolean } export interface Anomaly { total_delta_pct: number; yesterday_total: number; prev7_mean: number; staff: AnomalyStaff[] } // 员工活跃排行(2026-09-07 管理者反馈补强):active_*=窗内 distinct peer, // msg_delta 仅周/月且当前/前窗均非 0 才有值 export interface StaffRankingRow { user_id: string; name: string; unmapped: boolean msg_total: number; active_friends: number; active_rooms: number call_cnt: number; msg_delta: number | null } export interface ActionCounts { unreplied_2h: number; churn_real_30d: number; sensitive_pending: number } // by_channel/trend_by_channel 仅 channel='all' 时后端才返回(可选字段); // 渠道趋势序列 call_cnt 恒 0——通话量无渠道语义不拆,总量走 trend export interface PerChannelCards { msg_total: number; active_customers: number; first_contact: number } export interface OverviewResp { scope: Scope channel: Channel | 'all' days: number[] cards: OverviewCards trend: TrendPoint[] anomaly: Anomaly | null staff_ranking: StaffRankingRow[] actions: ActionCounts by_channel?: { wechat: PerChannelCards; whatsapp: PerChannelCards } trend_by_channel?: { wechat: TrendPoint[]; whatsapp: TrendPoint[] } } export interface HealthResp { data_as_of: number | null; entities: { table: string; last_seen: string | null }[] } export interface ChurnRow { friend_id: string; channel: string; nickname: string; owner_account_id: string | null user_id: string | null; last_chat_time: number | null; hist_msgs: number; active_days: number } export interface UnrepliedRow { peer_id: string; channel: string; owner_account_id: string; last_ts: number wait_ms: number; preview: string; msg_id: string } export interface SensitiveWordRow { id: number; word: string; category: string | null; enabled: boolean min_len: number; exclude_words: string[] } export interface SensitiveHitRow { id: number; word_id: number; word: string; category: string | null; msg_id: string channel: string; owner_account_id: string | null; talker_id: string | null room_id: string | null; ts: number | null; snippet: string | null; disposition: string } export const getOverview = (channel: DashboardChannel, scope: Scope) => getJson('/analytics/overview', { channel, scope }) export const getAnalyticsHealth = () => getJson('/analytics/health') export const getChurn = (params: Record) => getJson>('/analytics/churn', params) export const getUnreplied = (params: Record) => getJson>('/analytics/unreplied', params) export const getWords = () => getJson('/analytics/sensitive/words') export const previewWord = (data: { word: string; exclude_words?: string[] }) => requestJson<{ count: number; warn: boolean }>('post', '/analytics/sensitive/words/preview', data) export const createWord = (data: { word: string; category?: string; exclude_words?: string[] }) => requestJson<{ id: number; word: string; enabled: boolean }>('post', '/analytics/sensitive/words', data) export const patchWord = (id: number, data: Record) => requestJson>('patch', `/analytics/sensitive/words/${id}`, data) export const deleteWord = (id: number) => requestJson<{ ok: boolean }>('delete', `/analytics/sensitive/words/${id}`) export const getHits = (params: Record) => getJson>('/analytics/sensitive/hits', params) export const disposeHits = (ids: number[], disposition: string) => requestJson<{ updated: number }>('patch', '/analytics/sensitive/hits', { ids, disposition })