chore(history): import initial web product snapshot

This commit is contained in:
wmq
2026-07-25 00:30:00 +08:00
commit cf0eed46c3
131 changed files with 15693 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
import { http } from './client'
import type { AiAnalysis, AiReplySuggestion, AiTextActionPayload, InterpretLine, TranslatePayload, TranslateResult } from '@/types'
/** POST /api/ai/translate */
export function translate(payload: TranslatePayload) {
return http.post<TranslateResult>('/api/ai/translate', payload)
}
/** POST /api/ai/analyze */
export function analyzeConversation(conversationId: string) {
return http.post<AiAnalysis>('/api/ai/analyze', { conversationId })
}
/** POST /api/ai/replies */
export function generateReplies(conversationId: string) {
return http.post<AiReplySuggestion[]>('/api/ai/replies', { conversationId })
}
/** POST /api/ai/summarize */
export function summarizeConversation(conversationId: string) {
return http.post<{ summary: string }>('/api/ai/summarize', { conversationId })
}
/** POST /api/ai/interpretation */
export function nextInterpretLine(index: number) {
return http.post<InterpretLine>('/api/ai/interpretation', { index })
}
/** POST /api/ai/text-action(润色/缩写/扩写/语气/翻译) */
export function textAction(payload: AiTextActionPayload) {
return http.post<{ result: string }>('/api/ai/text-action', payload)
}
+22
View File
@@ -0,0 +1,22 @@
import { http } from './client'
import type { AuthSession, LoginPayload, RegisterPayload } from '@/types'
/** POST /api/auth/login */
export function login(payload: LoginPayload & { role?: string }) {
return http.post<AuthSession>('/api/auth/login', payload)
}
/** POST /api/auth/register */
export function register(payload: RegisterPayload) {
return http.post<AuthSession>('/api/auth/register', payload)
}
/** GET /api/auth/me */
export function me() {
return http.get<{ user: AuthSession['user']; tenant: AuthSession['tenant'] }>('/api/auth/me')
}
/** 发送验证码(演示:固定可收到 6 位码) */
export function sendCode(account: string) {
return http.post<{ sent: boolean }>('/api/auth/send-code', { account }).catch(() => ({ sent: true }))
}
+38
View File
@@ -0,0 +1,38 @@
import { http } from './client'
import type { ChannelConnection, ChannelSyncLog, ChannelType } from '@/types'
/** GET /api/channels */
export function getChannels() {
return http.get<ChannelConnection[]>('/api/channels')
}
/** POST /api/channels/connect */
export function connectChannel(payload: {
type: ChannelType
account: string
syncRange?: string
defaultOwnerId?: string
autoReply?: boolean
}) {
return http.post<ChannelConnection>('/api/channels/connect', payload)
}
/** POST /api/channels/test */
export function testChannel(id: string) {
return http.post<{ success: boolean; latencyMs: number; message: string }>('/api/channels/test', { id })
}
/** PATCH /api/channels/:id */
export function updateChannel(id: string, payload: Partial<ChannelConnection>) {
return http.patch<ChannelConnection>(`/api/channels/${id}`, payload)
}
/** DELETE /api/channels/:id */
export function deleteChannel(id: string) {
return http.del<{ success: boolean }>(`/api/channels/${id}`)
}
/** GET /api/channels/:id/logs */
export function getChannelLogs(id: string) {
return http.get<ChannelSyncLog[]>(`/api/channels/${id}/logs`)
}
+75
View File
@@ -0,0 +1,75 @@
/**
* 统一 API 客户端
* - VITE_USE_MOCK=true :走内置 Mock 路由(本地模拟数据,含模拟网络延迟)
* - VITE_USE_MOCK=false :请求真实后端(VITE_API_BASE_URL),自动携带 Bearer Token
*/
import { env } from '@/config/env'
import { mockRequest } from '@/services/mock'
export class ApiError extends Error {
code: number
constructor(code: number, message: string) {
super(message)
this.code = code
}
}
export interface RequestOptions {
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'PUT'
body?: unknown
query?: Record<string, string | number | boolean | undefined | null>
}
/** 由 auth store 注入,避免循环依赖 */
let tokenGetter: () => string | null = () => null
export function setTokenGetter(fn: () => string | null) {
tokenGetter = fn
}
export async function api<T = unknown>(path: string, options: RequestOptions = {}): Promise<T> {
const method = options.method ?? 'GET'
if (env.useMock) {
return mockRequest<T>(method, path, options.query ?? {}, options.body)
}
const base = env.apiBaseUrl.replace(/\/$/, '')
const url = new URL(`${base}${path}`, window.location.origin)
Object.entries(options.query ?? {}).forEach(([k, v]) => {
if (v !== undefined && v !== null && v !== '') url.searchParams.set(k, String(v))
})
const token = tokenGetter()
const res = await fetch(url.toString(), {
method,
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: options.body === undefined ? undefined : JSON.stringify(options.body),
})
if (res.status === 401) {
throw new ApiError(401, '登录状态已失效,请重新登录')
}
if (!res.ok) {
const text = await res.text().catch(() => '')
throw new ApiError(res.status, text || `请求失败(${res.status})`)
}
const json = (await res.json().catch(() => null)) as { code?: number; message?: string; data?: T } | T | null
// 兼容 {code,message,data} 与直接返回数据两种后端风格
if (json && typeof json === 'object' && 'code' in json && 'data' in json) {
const wrapped = json as { code: number; message?: string; data: T }
if (wrapped.code !== 0 && wrapped.code !== 200) throw new ApiError(wrapped.code, wrapped.message ?? '请求失败')
return wrapped.data
}
return json as T
}
export const http = {
get: <T>(path: string, query?: RequestOptions['query']) => api<T>(path, { method: 'GET', query }),
post: <T>(path: string, body?: unknown) => api<T>(path, { method: 'POST', body }),
patch: <T>(path: string, body?: unknown) => api<T>(path, { method: 'PATCH', body }),
put: <T>(path: string, body?: unknown) => api<T>(path, { method: 'PUT', body }),
del: <T>(path: string) => api<T>(path, { method: 'DELETE' }),
}
+36
View File
@@ -0,0 +1,36 @@
import { http } from './client'
import type { Conversation, Customer, Message, SendMessagePayload } from '@/types'
export type ConversationListItem = Conversation & { customer: Customer }
export interface ConversationQuery {
filter?: string
channel?: string
keyword?: string
userId?: string
}
/** GET /api/conversations */
export function getConversations(query: ConversationQuery = {}) {
return http.get<{ list: ConversationListItem[]; total: number }>('/api/conversations', { ...query })
}
/** GET /api/conversations/:id */
export function getConversation(id: string) {
return http.get<{ conversation: Conversation; customer: Customer }>(`/api/conversations/${id}`)
}
/** GET /api/conversations/:id/messages */
export function getMessages(id: string) {
return http.get<Message[]>(`/api/conversations/${id}/messages`)
}
/** POST /api/conversations/:id/messages */
export function sendMessage(id: string, payload: SendMessagePayload & { translatedText?: string }) {
return http.post<Message>(`/api/conversations/${id}/messages`, payload)
}
/** PATCH /api/conversations/:id(标记已读/分配/状态) */
export function updateConversation(id: string, payload: { assigneeId?: string | null; markRead?: boolean; status?: string }) {
return http.patch<Conversation>(`/api/conversations/${id}`, payload)
}
+58
View File
@@ -0,0 +1,58 @@
import { http } from './client'
import type { FollowTask, Opportunity, RiskAlert } from '@/types'
/* ===== 商机 ===== */
/** GET /api/opportunities */
export function getOpportunities(query: { stage?: string; ownerId?: string; keyword?: string } = {}) {
return http.get<{ list: Opportunity[]; total: number }>('/api/opportunities', { ...query })
}
/** POST /api/opportunities */
export function createOpportunity(payload: Partial<Opportunity>) {
return http.post<Opportunity>('/api/opportunities', payload)
}
/** PATCH /api/opportunities/:id */
export function updateOpportunity(id: string, payload: Partial<Opportunity>) {
return http.patch<Opportunity>(`/api/opportunities/${id}`, payload)
}
/** DELETE /api/opportunities/:id */
export function deleteOpportunity(id: string) {
return http.del<{ success: boolean }>(`/api/opportunities/${id}`)
}
/* ===== 跟进任务 ===== */
/** GET /api/tasks */
export function getTasks(query: { filter?: string; ownerId?: string; customerId?: string } = {}) {
return http.get<{ list: FollowTask[]; total: number }>('/api/tasks', { ...query })
}
/** POST /api/tasks */
export function createTask(payload: Partial<FollowTask>) {
return http.post<FollowTask>('/api/tasks', payload)
}
/** PATCH /api/tasks/:id */
export function updateTask(id: string, payload: Partial<FollowTask>) {
return http.patch<FollowTask>(`/api/tasks/${id}`, payload)
}
/** DELETE /api/tasks/:id */
export function deleteTask(id: string) {
return http.del<{ success: boolean }>(`/api/tasks/${id}`)
}
/* ===== 风险预警 ===== */
/** GET /api/risks */
export function getRisks(query: { level?: string; type?: string; status?: string; keyword?: string } = {}) {
return http.get<{ list: RiskAlert[]; total: number }>('/api/risks', { ...query })
}
/** PATCH /api/risks/:id */
export function updateRisk(id: string, payload: { status?: string; ownerId?: string | null; note?: string }) {
return http.patch<RiskAlert>(`/api/risks/${id}`, payload)
}
+63
View File
@@ -0,0 +1,63 @@
import { http } from './client'
import type { Conversation, Customer, CustomerFilters, CustomerFile, ActivityLog, FollowTask, Opportunity, RiskAlert } from '@/types'
export interface CustomerDetail {
customer: Customer
conversations: (Conversation & { messageCount: number })[]
tasks: FollowTask[]
opportunities: Opportunity[]
risks: RiskAlert[]
files: CustomerFile[]
logs: ActivityLog[]
}
export interface DuplicatePair {
id: string
reason: string
customers: Customer[]
}
/** GET /api/customers */
export function getCustomers(filters: CustomerFilters = {}) {
return http.get<{ list: Customer[]; total: number }>('/api/customers', { ...filters })
}
/** GET /api/customers/:id */
export function getCustomer(id: string) {
return http.get<CustomerDetail>(`/api/customers/${id}`)
}
/** POST /api/customers */
export function createCustomer(payload: Partial<Customer>) {
return http.post<Customer>('/api/customers', payload)
}
/** PATCH /api/customers/:id */
export function updateCustomer(id: string, payload: Partial<Customer>) {
return http.patch<Customer>(`/api/customers/${id}`, payload)
}
/** DELETE /api/customers/:id */
export function deleteCustomer(id: string) {
return http.del<{ success: boolean }>(`/api/customers/${id}`)
}
/** POST /api/customers/merge */
export function mergeCustomers(keepId: string, mergeId: string) {
return http.post<Customer>('/api/customers/merge', { keepId, mergeId })
}
/** GET /api/customers/duplicates */
export function getDuplicateCustomers() {
return http.get<DuplicatePair[]>('/api/customers/duplicates')
}
/** POST /api/customers/import */
export function importCustomers(rows: Record<string, string>[]) {
return http.post<{ imported: number; skipped: number }>('/api/customers/import', { rows })
}
/** GET /api/customers/export */
export function exportCustomers() {
return http.get<{ csv: string }>('/api/customers/export')
}
+7
View File
@@ -0,0 +1,7 @@
import { http } from './client'
import type { DashboardOverview } from '@/types'
/** GET /api/dashboard/overview */
export function getDashboardOverview() {
return http.get<DashboardOverview>('/api/dashboard/overview')
}
+11
View File
@@ -0,0 +1,11 @@
export * from './client'
export * as authApi from './auth'
export * as dashboardApi from './dashboard'
export * as channelsApi from './channels'
export * as conversationsApi from './conversations'
export * as aiApi from './ai'
export * as customersApi from './customers'
export * as crmApi from './crm'
export * as knowledgeApi from './knowledge'
export * as orgApi from './org'
export * as searchApi from './search'
+42
View File
@@ -0,0 +1,42 @@
import { http } from './client'
import type { IndustryPack, KnowledgeItem, KnowledgeSearchResult, KnowledgeStats } from '@/types'
/** GET /api/knowledge */
export function getKnowledge(query: { category?: string; keyword?: string } = {}) {
return http.get<{ list: KnowledgeItem[]; total: number }>('/api/knowledge', { ...query })
}
/** POST /api/knowledge */
export function createKnowledge(payload: Partial<KnowledgeItem>) {
return http.post<KnowledgeItem>('/api/knowledge', payload)
}
/** PATCH /api/knowledge/:id */
export function updateKnowledge(id: string, payload: Partial<KnowledgeItem>) {
return http.patch<KnowledgeItem>(`/api/knowledge/${id}`, payload)
}
/** DELETE /api/knowledge/:id */
export function deleteKnowledge(id: string) {
return http.del<{ success: boolean }>(`/api/knowledge/${id}`)
}
/** POST /api/knowledge/upload */
export function uploadKnowledgeFiles(files: { name: string; type: string }[]) {
return http.post<{ items: KnowledgeItem[] }>('/api/knowledge/upload', { files })
}
/** POST /api/knowledge/search-test */
export function knowledgeSearchTest(question: string) {
return http.post<KnowledgeSearchResult>('/api/knowledge/search-test', { question })
}
/** GET /api/knowledge/stats */
export function getKnowledgeStats() {
return http.get<KnowledgeStats>('/api/knowledge/stats')
}
/** GET /api/knowledge/packs */
export function getIndustryPacks() {
return http.get<IndustryPack[]>('/api/knowledge/packs')
}
+79
View File
@@ -0,0 +1,79 @@
import { http } from './client'
import type { AnalyticsData } from '@/types'
import type { AppNotification, BillingInfo, BillingOrder, Invoice, LoginRecord, OperationLog, SystemSettings, TeamMember } from '@/types/org'
import type { RoleKey } from '@/config/roles'
/* ===== 数据分析 ===== */
/** GET /api/analytics */
export function getAnalytics(params: { period?: string; memberId?: string; channel?: string; country?: string; industry?: string } = {}) {
return http.get<AnalyticsData>('/api/analytics', { ...params })
}
/* ===== 团队 ===== */
/** GET /api/team */
export function getTeam() {
return http.get<TeamMember[]>('/api/team')
}
/** POST /api/team/invite */
export function inviteMember(payload: { name: string; email: string; phone?: string; department: string; role: RoleKey }) {
return http.post<TeamMember>('/api/team/invite', payload)
}
/** PATCH /api/team/:id */
export function updateMember(id: string, payload: Partial<TeamMember>) {
return http.patch<TeamMember>(`/api/team/${id}`, payload)
}
/** DELETE /api/team/:id */
export function deleteMember(id: string) {
return http.del<{ success: boolean }>(`/api/team/${id}`)
}
/** GET /api/team/operation-logs */
export function getOperationLogs() {
return http.get<OperationLog[]>('/api/team/operation-logs')
}
/** GET /api/team/login-records */
export function getLoginRecords() {
return http.get<LoginRecord[]>('/api/team/login-records')
}
/* ===== 账单 ===== */
/** GET /api/billing */
export function getBilling() {
return http.get<{ info: BillingInfo; orders: BillingOrder[]; invoices: Invoice[] }>('/api/billing')
}
/** POST /api/billing/upgrade */
export function upgradePlan(planKey: string) {
return http.post<BillingInfo>('/api/billing/upgrade', { planKey })
}
/* ===== 设置 ===== */
/** GET /api/settings */
export function getSettings() {
return http.get<SystemSettings>('/api/settings')
}
/** PATCH /api/settings */
export function updateSettings(payload: Partial<SystemSettings>) {
return http.patch<SystemSettings>('/api/settings', payload)
}
/* ===== 通知 ===== */
/** GET /api/notifications */
export function getNotifications() {
return http.get<AppNotification[]>('/api/notifications')
}
/** POST /api/notifications/read-all */
export function readAllNotifications() {
return http.post<{ success: boolean }>('/api/notifications/read-all')
}
+13
View File
@@ -0,0 +1,13 @@
import { http } from './client'
export interface GlobalSearchResult {
customers: { id: string; title: string; sub: string }[]
conversations: { id: string; title: string; sub: string }[]
knowledge: { id: string; title: string; sub: string }[]
tasks: { id: string; title: string; sub: string }[]
}
/** GET /api/search(全局搜索) */
export function globalSearch(q: string) {
return http.get<GlobalSearchResult>('/api/search', { q })
}