309 lines
14 KiB
TypeScript
309 lines
14 KiB
TypeScript
import { get, post, put, del, getList, postForm } from './request'
|
|
|
|
export interface LoginParams { username: string; password: string }
|
|
export interface LoginResult { token: string; user_id: number; tenant_id: number; nickname: string; role: string }
|
|
|
|
export interface Session {
|
|
id: number; tenant_id: number; channel_id: number; customer_id: number; agent_id: number | null
|
|
status: string; priority: string; unread_count: number; satisfaction_score: number | null
|
|
last_message?: string; last_message_at?: string | null
|
|
satisfaction_text?: string
|
|
end_reason?: string
|
|
visitor_ip?: string
|
|
visitor_region?: string
|
|
user_agent?: string
|
|
created_at: string; ended_at: string | null
|
|
/** 列表接口补全字段 */
|
|
message_count?: number
|
|
customer_name?: string
|
|
agent_name?: string
|
|
channel_name?: string
|
|
channel_type?: string
|
|
}
|
|
|
|
export interface Message {
|
|
id: number; session_id: number; sender_type: 'visitor' | 'agent'; sender_id: number | null
|
|
content: string; type: 'text' | 'image'; seq: number; sent_at: string
|
|
}
|
|
|
|
export interface SessionEvent {
|
|
id: number; session_id: number; operator_id: number; action: string; detail: string; created_at: string
|
|
}
|
|
|
|
export interface AvailableAgent { id: number; nickname: string; status: string }
|
|
|
|
export interface Customer {
|
|
id: number; tenant_id: number; name: string; phone: string; email: string; tags: string
|
|
source: string; status: string; conversation_count: number; last_contact_at: string
|
|
}
|
|
|
|
export interface KnowledgeCategory {
|
|
id: number; tenant_id: number; parent_id: number | null; name: string
|
|
/** 本分类直属条目数 */
|
|
entry_count?: number
|
|
/** 含子分类汇总 */
|
|
total_count?: number
|
|
}
|
|
|
|
export interface KnowledgeEntry {
|
|
id: number; title: string; content: string; status: string; usage_count: number
|
|
category_id: number; updated_at: string
|
|
category_name?: string
|
|
}
|
|
|
|
export interface Channel {
|
|
id: number; tenant_id: number; type: string; name: string; status: string
|
|
config: string; script_code: string; channel_key: string
|
|
}
|
|
|
|
export interface Tenant {
|
|
id: number; name: string; plan_id: number | null; seat_count: number; expire_at: string; status: string
|
|
contact_name: string; contact_phone: string; contact_email: string
|
|
created_at?: string
|
|
}
|
|
|
|
export interface Plan {
|
|
id: number; name: string; price_monthly: number; seats: number; storage_days: number
|
|
kb_limit: number; features: string; status: string
|
|
}
|
|
|
|
export interface OperationLog {
|
|
id: number; operator_id: number; action: string; detail: string
|
|
target_type?: string; target_id?: number; ip?: string; created_at: string
|
|
}
|
|
|
|
export interface Announcement {
|
|
id: number; title: string; content: string; status: string
|
|
created_at: string; updated_at?: string
|
|
}
|
|
|
|
export interface AdminStats {
|
|
tenant_total: number
|
|
active_tenant: number
|
|
suspended_tenant?: number
|
|
expiring_tenant?: number
|
|
new_tenants_month?: number
|
|
monthly_income: number
|
|
system_uptime: string
|
|
plan_distribution?: { plan_id: number; name: string; count: number }[]
|
|
tenant_growth?: { month: string; month_key: string; new_count: number; total_count: number }[]
|
|
recent_logs?: OperationLog[]
|
|
}
|
|
|
|
export interface StatisticsKpis {
|
|
total_sessions: number
|
|
avg_response_time: number
|
|
satisfaction_avg: number
|
|
first_resolve_rate: number
|
|
total_messages: number
|
|
}
|
|
|
|
// Auth
|
|
export const login = (params: LoginParams) => post<LoginResult>('/login', params)
|
|
|
|
// Sessions
|
|
export const getSessions = (params?: {
|
|
status?: string
|
|
priority?: string
|
|
agent_id?: number | string
|
|
channel_id?: number | string
|
|
search?: string
|
|
from?: string
|
|
to?: string
|
|
page?: number
|
|
pageSize?: number
|
|
}) => {
|
|
const search = new URLSearchParams()
|
|
if (params?.status) search.set('status', params.status)
|
|
if (params?.priority) search.set('priority', params.priority)
|
|
if (params?.agent_id != null && params.agent_id !== '') search.set('agent_id', String(params.agent_id))
|
|
if (params?.channel_id != null && params.channel_id !== '') search.set('channel_id', String(params.channel_id))
|
|
if (params?.search) search.set('search', params.search)
|
|
if (params?.from) search.set('from', params.from)
|
|
if (params?.to) search.set('to', params.to)
|
|
if (params?.page) search.set('page', String(params.page))
|
|
if (params?.pageSize) search.set('pageSize', String(params.pageSize))
|
|
return getList<Session>(`/sessions?${search}`)
|
|
}
|
|
export const getSession = (id: number) => get<{ session: Session; messages: Message[]; events: SessionEvent[]; pending_count: number }>(`/sessions/${id}`)
|
|
export const assignSession = (id: number, agentId: number) => post(`/sessions/${id}/assign`, { agent_id: agentId })
|
|
export const claimSession = (id: number) => post(`/sessions/${id}/assign`, {})
|
|
export const transferSession = (id: number, agentId: number) => post(`/sessions/${id}/transfer`, { agent_id: agentId })
|
|
export const endSession = (id: number, reason: string) => post(`/sessions/${id}/end?reason=${reason}`, {})
|
|
export const archiveSession = (id: number) => post<{ message: string; session: Session }>(`/sessions/${id}/archive`, {})
|
|
export const batchArchiveSessions = (ids: number[]) => post<{ message: string; count: number }>('/sessions/batch-archive', { ids })
|
|
export const updateSessionPriority = (id: number, priority: 'normal' | 'urgent') => put(`/sessions/${id}/priority?priority=${priority}`, {})
|
|
export const markSessionRead = (id: number) => post(`/sessions/${id}/read`, {})
|
|
export const addSessionNote = (id: number, content: string) => post<SessionEvent>(`/sessions/${id}/notes`, { content })
|
|
export const sendSessionMessage = (id: number, content: string, type: 'text' | 'image' = 'text') => post<Message>(`/sessions/${id}/messages`, { content, type })
|
|
/** onlineOnly 默认 true;传 false 返回全部坐席(对话记录筛选) */
|
|
export const getAvailableAgents = (onlineOnly = true) =>
|
|
get<AvailableAgent[]>(`/agents/available${onlineOnly ? '' : '?all=1'}`)
|
|
|
|
export interface UploadImageResult {
|
|
url: string
|
|
thumb_url: string
|
|
content_type: string
|
|
width: number
|
|
height: number
|
|
size: number
|
|
}
|
|
|
|
/** 客服端上传图片 → 返回对象存储 URL */
|
|
export const uploadImage = (file: File) => {
|
|
const form = new FormData()
|
|
form.append('file', file)
|
|
return postForm<UploadImageResult>('/uploads', form)
|
|
}
|
|
|
|
/** 访客端上传图片 */
|
|
export const uploadWidgetImage = async (file: File, sessionId: number, visitorToken: string) => {
|
|
const form = new FormData()
|
|
form.append('file', file)
|
|
form.append('session_id', String(sessionId))
|
|
form.append('visitor_token', visitorToken)
|
|
const res = await fetch('/api/widget/upload', {
|
|
method: 'POST',
|
|
headers: { 'X-Visitor-Token': visitorToken },
|
|
body: form,
|
|
})
|
|
const json = await res.json()
|
|
if (json.code !== 0) throw new Error(json.message || '上传失败')
|
|
return json as { code: number; data: UploadImageResult }
|
|
}
|
|
|
|
// Customers
|
|
export const getCustomers = (params?: { search?: string; status?: string; source?: string; page?: number; pageSize?: number }) => {
|
|
const search = new URLSearchParams()
|
|
if (params?.search) search.set('search', params.search)
|
|
if (params?.status) search.set('status', params.status)
|
|
if (params?.source) search.set('source', params.source)
|
|
if (params?.page) search.set('page', String(params.page || 1))
|
|
if (params?.pageSize) search.set('pageSize', String(params.pageSize))
|
|
return getList<Customer>(`/customers?${search}`)
|
|
}
|
|
export const getCustomer = (id: number) => get<{ customer: Customer; sessions: Session[] }>(`/customers/${id}`)
|
|
export const createCustomer = (data: Partial<Customer>) => post<Customer>('/customers', data)
|
|
export const updateCustomer = (id: number, data: Partial<Customer>) => put<Customer>(`/customers/${id}`, data)
|
|
export const deleteCustomer = (id: number) => del(`/customers/${id}`)
|
|
|
|
// Knowledge
|
|
export const getKnowledgeCategories = () =>
|
|
get<{ list: KnowledgeCategory[]; total_entries: number }>('/knowledge/categories')
|
|
export const createKnowledgeCategory = (data: { name: string; parent_id?: number | null }) =>
|
|
post<KnowledgeCategory>('/knowledge/categories', data)
|
|
export const updateKnowledgeCategory = (id: number, data: { name: string; parent_id?: number | null }) =>
|
|
put<KnowledgeCategory>(`/knowledge/categories/${id}`, data)
|
|
export const deleteKnowledgeCategory = (id: number) => del(`/knowledge/categories/${id}`)
|
|
export const getKnowledgeEntries = (params?: {
|
|
category_id?: string
|
|
search?: string
|
|
status?: string
|
|
sort?: 'usage' | 'updated' | 'title'
|
|
page?: number
|
|
pageSize?: number
|
|
}) => {
|
|
const search = new URLSearchParams()
|
|
if (params?.category_id) search.set('category_id', params.category_id)
|
|
if (params?.search) search.set('search', params.search)
|
|
if (params?.status) search.set('status', params.status)
|
|
if (params?.sort) search.set('sort', params.sort)
|
|
if (params?.page) search.set('page', String(params.page || 1))
|
|
if (params?.pageSize) search.set('pageSize', String(params.pageSize || 10))
|
|
return getList<KnowledgeEntry>(`/knowledge/entries?${search}`)
|
|
}
|
|
export const createKnowledgeEntry = (data: { title: string; content: string; category_id: number; status?: string }) =>
|
|
post<KnowledgeEntry>('/knowledge/entries', data)
|
|
export const updateKnowledgeEntry = (id: number, data: Partial<KnowledgeEntry>) =>
|
|
put<KnowledgeEntry>(`/knowledge/entries/${id}`, data)
|
|
export const deleteKnowledgeEntry = (id: number) => del(`/knowledge/entries/${id}`)
|
|
|
|
// Channels
|
|
export const getChannels = () => get<Channel[]>('/channels')
|
|
export const createChannel = (data: { type: string; name?: string }) => post<Channel>('/channels', data)
|
|
export const updateChannel = (id: number, data: { name?: string; status?: string }) => put<Channel>(`/channels/${id}`, data)
|
|
|
|
// 坐席账号
|
|
export interface StaffUser {
|
|
id: number
|
|
username: string
|
|
nickname: string
|
|
role: string
|
|
status: string
|
|
created_at: string
|
|
last_online_at?: string
|
|
}
|
|
export interface StaffListResult {
|
|
list: StaffUser[]
|
|
seat_limit: number
|
|
seat_used: number
|
|
}
|
|
export const getStaff = () => get<StaffListResult>('/staff')
|
|
export const createStaff = (data: { username: string; password: string; nickname?: string; role?: string }) =>
|
|
post<StaffUser>('/staff', data)
|
|
export const updateStaff = (id: number, data: { nickname?: string; role?: string; status?: string; password?: string }) =>
|
|
put<StaffUser>(`/staff/${id}`, data)
|
|
export const deleteStaff = (id: number) => del(`/staff/${id}`)
|
|
|
|
// Tenant settings
|
|
export type WorkHours = Record<string, string>
|
|
export interface TenantSettings {
|
|
tenant_id: number
|
|
display_name: string
|
|
agent_nickname: string
|
|
timezone: string
|
|
welcome_message: string
|
|
offline_prompt: string
|
|
work_hours: WorkHours
|
|
worktime_prompt: string
|
|
notify_new_session: boolean
|
|
notify_offline_leave: boolean
|
|
notify_daily_report: boolean
|
|
}
|
|
export const getTenantSettings = () => get<TenantSettings>('/settings')
|
|
export const updateTenantSettings = (data: Partial<TenantSettings> & { work_hours?: WorkHours }) =>
|
|
put<TenantSettings>('/settings', data)
|
|
|
|
// Statistics
|
|
export const getKPIs = () => get<StatisticsKpis>('/statistics/kpi')
|
|
export const getSessionTrend = (period: 'today' | 'day' | 'month' = 'day') => get<{ date: string; count: number }[]>(`/statistics/trend?period=${period}`)
|
|
export const getResponseDistribution = () => get<{ range: string; count: number }[]>('/statistics/response-distribution')
|
|
export const getChannelDistribution = () => get<{ type: string; value: number }[]>('/statistics/channels')
|
|
export const getAgentPerformance = () => get<{ name: string; conversations: number; avg_response: number; satisfaction: number }[]>('/statistics/performance')
|
|
|
|
// Admin
|
|
export const getTenants = (params?: { search?: string; status?: string; page?: number; pageSize?: number }) => {
|
|
const search = new URLSearchParams()
|
|
if (params?.search) search.set('search', params.search)
|
|
if (params?.status) search.set('status', params.status)
|
|
if (params?.page) search.set('page', String(params.page || 1))
|
|
if (params?.pageSize) search.set('pageSize', String(params.pageSize))
|
|
return getList<Tenant>(`/admin/tenants?${search}`)
|
|
}
|
|
export const getAdminStats = () => get<AdminStats>('/admin/stats')
|
|
export const createTenant = (data: {
|
|
name: string; contact_name: string; contact_phone: string; contact_email?: string
|
|
plan_id?: number; seat_count?: number; duration_months?: number
|
|
admin_username?: string; admin_password?: string
|
|
}) => post<{ tenant: Tenant; admin_username: string; admin_password: string }>('/admin/tenants', data)
|
|
export const updateTenant = (id: number, data: Partial<Tenant>) => put<Tenant>(`/admin/tenants/${id}`, data)
|
|
export const suspendTenant = (id: number) => post(`/admin/tenants/${id}/suspend`, {})
|
|
export const resumeTenant = (id: number) => post(`/admin/tenants/${id}/resume`, {})
|
|
|
|
export const getPlans = () => get<Plan[]>('/admin/plans')
|
|
export const createPlan = (data: Partial<Plan>) => post<Plan>('/admin/plans', data)
|
|
export const updatePlan = (id: number, data: Partial<Plan>) => put<Plan>(`/admin/plans/${id}`, data)
|
|
|
|
export const getAdminLogs = (params?: { page?: number; pageSize?: number }) => {
|
|
const search = new URLSearchParams()
|
|
if (params?.page) search.set('page', String(params.page || 1))
|
|
if (params?.pageSize) search.set('pageSize', String(params.pageSize || 10))
|
|
return getList<OperationLog>(`/admin/logs?${search}`)
|
|
}
|
|
export const getAnnouncements = () => get<Announcement[]>('/admin/announcements')
|
|
export const createAnnouncement = (data: { title: string; content: string; status?: string }) =>
|
|
post<Announcement>('/admin/announcements', data)
|
|
export const updateAnnouncement = (id: number, data: Partial<Announcement>) =>
|
|
put<Announcement>(`/admin/announcements/${id}`, data)
|
|
export const deleteAnnouncement = (id: number) => del(`/admin/announcements/${id}`)
|