export interface CaptchaResponse { captchaId: string; imageBase64: string; } export interface TenantInfo { tenantId: number; tenantCode: string; tenantName: string; } export interface TokenResponse { accessToken: string; refreshToken: string; accessExpiresInMinutes: number; refreshExpiresInDays: number; availableTenants?: TenantInfo[]; } export interface LoginPayload { username: string; password: string; tenantCode?: string; captchaId?: string; captchaCode?: string; } export interface UserProfile { userId: number; tenantId: number; username: string; displayName: string; email?: string; phone?: string; status?: number; isAdmin?: boolean; isPlatformAdmin?: boolean; pwdResetRequired?: number; } export interface PlatformConfig { projectName?: string; logoUrl?: string; iconUrl?: string; loginBgUrl?: string; icpInfo?: string; copyrightInfo?: string; systemDescription?: string; } export interface DashboardStat { name: string; value?: number; metricKey: string; } export interface DashboardTodo { id: number; title?: string; bizType?: string; bizId?: number; priority?: string; status?: string; dueDate?: string; createdAt?: string; } export interface DashboardActivity { id: number; bizType?: string; bizId?: number; actionType?: string; title?: string; content?: string; operatorUserId?: number; operatorName?: string; createdAt?: string; timeText?: string; } export interface DashboardHome { userId?: number; realName?: string; jobTitle?: string; deptName?: string; onboardingDays?: number; stats?: DashboardStat[]; todos?: DashboardTodo[]; activities?: DashboardActivity[]; } export interface ProfileOverview { userId?: number; monthlyOpportunityCount?: number; monthlyExpansionCount?: number; averageScore?: number; onboardingDays?: number; realName?: string; jobTitle?: string; deptName?: string; accountStatus?: string; } export interface UpdateCurrentUserProfilePayload { userId?: number; username?: string; displayName?: string; email?: string; phone?: string; pwdResetRequired?: number; isPlatformAdmin?: boolean; orgId?: number; } export interface UpdateCurrentUserPasswordPayload { oldPassword: string; newPassword: string; } export interface WorkCheckIn { id: number; date?: string; time?: string; locationText?: string; remark?: string; status?: string; longitude?: number; latitude?: number; photoUrls?: string[]; } export interface WorkDailyReport { id: number; date?: string; submitTime?: string; workContent?: string; tomorrowPlan?: string; sourceType?: string; status?: string; score?: number; comment?: string; } export interface WorkHistoryItem { id: number; type?: string; date?: string; time?: string; content?: string; status?: string; score?: number; comment?: string; photoUrls?: string[]; } export interface WorkOverview { todayCheckIn?: WorkCheckIn; todayReport?: WorkDailyReport; suggestedWorkContent?: string; history?: WorkHistoryItem[]; } export interface CreateWorkCheckInPayload { locationText: string; remark?: string; longitude?: number; latitude?: number; photoUrls?: string[]; } export interface CreateWorkDailyReportPayload { workContent: string; tomorrowPlan: string; sourceType?: string; } export interface OpportunityFollowUp { id: number; opportunityId?: number; date?: string; type?: string; content?: string; user?: string; } export interface OpportunityItem { id: number; code?: string; name?: string; client?: string; owner?: string; amount?: number; date?: string; confidence?: number; stage?: string; type?: string; pushedToOms?: boolean; product?: string; source?: string; notes?: string; followUps?: OpportunityFollowUp[]; } export interface OpportunityOverview { items?: OpportunityItem[]; } export interface CreateOpportunityPayload { opportunityName: string; customerName: string; amount: number; expectedCloseDate: string; confidencePct: number; stage?: string; opportunityType?: string; productType?: string; source?: string; pushedToOms?: boolean; description?: string; } export interface CreateOpportunityFollowUpPayload { followUpType: string; content: string; nextAction?: string; followUpTime: string; } export interface ExpansionFollowUp { id: number; bizId?: number; bizType?: string; date?: string; type?: string; content?: string; user?: string; } export interface SalesExpansionItem { id: number; type: "sales"; name?: string; phone?: string; email?: string; targetDeptId?: number; dept?: string; industry?: string; title?: string; intentLevel?: string; intent?: string; stageCode?: string; stage?: string; hasExp?: boolean; inProgress?: boolean; active?: boolean; employmentStatus?: string; expectedJoinDate?: string; notes?: string; followUps?: ExpansionFollowUp[]; } export interface ChannelExpansionItem { id: number; type: "channel"; name?: string; province?: string; industry?: string; annualRevenue?: string; revenue?: string; size?: number; contact?: string; contactTitle?: string; phone?: string; stageCode?: string; stage?: string; landed?: boolean; expectedSignDate?: string; notes?: string; followUps?: ExpansionFollowUp[]; } export interface ExpansionOverview { salesItems?: SalesExpansionItem[]; channelItems?: ChannelExpansionItem[]; } export interface DepartmentOption { id: number; name?: string; } export interface ExpansionMeta { departments?: DepartmentOption[]; } export interface CreateSalesExpansionPayload { candidateName: string; mobile?: string; email?: string; targetDeptId?: number; industry?: string; title?: string; intentLevel?: string; stage?: string; hasDesktopExp?: boolean; inProgress?: boolean; employmentStatus?: string; expectedJoinDate?: string; remark?: string; } export interface CreateChannelExpansionPayload { channelName: string; province?: string; industry?: string; annualRevenue?: number; staffSize?: number; contactName?: string; contactTitle?: string; contactMobile?: string; stage?: string; landedFlag?: boolean; expectedSignDate?: string; remark?: string; } export interface UpdateSalesExpansionPayload extends CreateSalesExpansionPayload {} export interface UpdateChannelExpansionPayload extends CreateChannelExpansionPayload {} export interface CreateExpansionFollowUpPayload { followUpType: string; content: string; nextAction?: string; followUpTime: string; } interface ApiEnvelope { code: string; msg: string; data: T; } interface ApiErrorBody { code?: string; msg?: string; message?: string; } const LOGIN_PATH = "/login"; async function request(input: string, init?: RequestInit, withAuth = false): Promise { const headers = new Headers(init?.headers); if (!headers.has("Content-Type") && init?.body && !(init.body instanceof FormData)) { headers.set("Content-Type", "application/json"); } if (withAuth) { const token = localStorage.getItem("accessToken"); if (token) { headers.set("Authorization", `Bearer ${token}`); } const userId = getStoredUserId(); if (userId !== undefined) { headers.set("X-User-Id", String(userId)); } } const response = await fetch(input, { ...init, headers, }); if (response.status === 401 || response.status === 403) { clearAuth(); window.location.href = `${LOGIN_PATH}?timeout=1`; throw new Error("登录已失效,请重新登录"); } let body: (ApiEnvelope & ApiErrorBody) | null = null; try { body = (await response.json()) as ApiEnvelope & ApiErrorBody; } catch { if (!response.ok) { throw new Error(`请求失败(${response.status})`); } } if (!response.ok) { throw new Error(body?.msg || body?.message || `请求失败(${response.status})`); } if (!body) { throw new Error("接口返回为空"); } if (body.code !== "0") { throw new Error(body.msg || "请求失败"); } return body.data; } export function isAuthed() { return Boolean(localStorage.getItem("accessToken")); } export function clearAuth() { localStorage.removeItem("accessToken"); localStorage.removeItem("refreshToken"); localStorage.removeItem("username"); localStorage.removeItem("availableTenants"); localStorage.removeItem("activeTenantId"); sessionStorage.removeItem("userProfile"); } export function persistLogin(payload: TokenResponse, username: string) { localStorage.setItem("accessToken", payload.accessToken); localStorage.setItem("refreshToken", payload.refreshToken); localStorage.setItem("username", username); if (payload.availableTenants) { localStorage.setItem("availableTenants", JSON.stringify(payload.availableTenants)); try { const tokenPayload = JSON.parse(atob(payload.accessToken.split(".")[1])); if (tokenPayload?.tenantId !== undefined) { localStorage.setItem("activeTenantId", String(tokenPayload.tenantId)); } } catch { localStorage.removeItem("activeTenantId"); } } } function getStoredUserId() { try { const rawProfile = sessionStorage.getItem("userProfile"); if (rawProfile) { const profile = JSON.parse(rawProfile) as Partial; if (typeof profile.userId === "number" && Number.isFinite(profile.userId)) { return profile.userId; } } } catch { // Ignore invalid session cache and fall back to token parsing. } try { const token = localStorage.getItem("accessToken"); if (!token) { return undefined; } const tokenPayload = JSON.parse(atob(token.split(".")[1])) as { userId?: number }; if (typeof tokenPayload.userId === "number" && Number.isFinite(tokenPayload.userId)) { return tokenPayload.userId; } } catch { return undefined; } return undefined; } export async function fetchCaptcha() { return request("/api/sys/auth/captcha"); } export async function login(payload: LoginPayload) { return request("/api/sys/auth/login", { method: "POST", body: JSON.stringify(payload), }); } export async function getSystemParamValue(key: string, defaultValue?: string) { const params = new URLSearchParams({ key }); if (defaultValue !== undefined) { params.set("defaultValue", defaultValue); } return request(`/api/sys/api/params/value?${params.toString()}`); } export async function getOpenPlatformConfig() { return request("/api/sys/api/open/platform/config"); } export async function getCurrentUser() { return request("/api/sys/api/users/me", undefined, true); } export async function getDashboardHome() { return request("/api/dashboard/home", undefined, true); } export async function getProfileOverview() { return request("/api/profile/overview", undefined, true); } export async function updateCurrentUserProfile(payload: UpdateCurrentUserProfilePayload) { return request("/api/sys/api/users/profile", { method: "PUT", body: JSON.stringify(payload), }, true); } export async function updateCurrentUserPassword(payload: UpdateCurrentUserPasswordPayload) { return request("/api/sys/api/users/password", { method: "PUT", body: JSON.stringify(payload), }, true); } export async function updateUserProfileById(userId: number, payload: UpdateCurrentUserProfilePayload) { return request(`/api/sys/api/users/${userId}`, { method: "PUT", body: JSON.stringify(payload), }, true); } export async function getWorkOverview() { return request("/api/work/overview", undefined, true); } export async function reverseWorkGeocode(latitude: number, longitude: number) { const params = new URLSearchParams({ lat: String(latitude), lon: String(longitude), }); return request(`/api/work/reverse-geocode?${params.toString()}`, undefined, true); } export async function saveWorkCheckIn(payload: CreateWorkCheckInPayload) { return request("/api/work/checkins", { method: "POST", body: JSON.stringify(payload), }, true); } export async function uploadWorkCheckInPhoto(file: File) { const formData = new FormData(); formData.append("file", file); return request("/api/work/checkin-photos", { method: "POST", body: formData, }, true); } export async function saveWorkDailyReport(payload: CreateWorkDailyReportPayload) { return request("/api/work/daily-reports", { method: "POST", body: JSON.stringify(payload), }, true); } export async function getOpportunityOverview(keyword?: string, stage?: string) { const params = new URLSearchParams(); if (keyword && keyword.trim()) { params.set("keyword", keyword.trim()); } if (stage && stage.trim() && stage !== "全部") { params.set("stage", stage.trim()); } const query = params.toString(); return request(`/api/opportunities/overview${query ? `?${query}` : ""}`, undefined, true); } export async function createOpportunity(payload: CreateOpportunityPayload) { return request("/api/opportunities", { method: "POST", body: JSON.stringify(payload), }, true); } export async function updateOpportunity(opportunityId: number, payload: CreateOpportunityPayload) { return request(`/api/opportunities/${opportunityId}`, { method: "PUT", body: JSON.stringify(payload), }, true); } export async function createOpportunityFollowUp(opportunityId: number, payload: CreateOpportunityFollowUpPayload) { return request(`/api/opportunities/${opportunityId}/followups`, { method: "POST", body: JSON.stringify(payload), }, true); } export async function getExpansionOverview(keyword?: string) { const params = new URLSearchParams(); if (keyword && keyword.trim()) { params.set("keyword", keyword.trim()); } const query = params.toString(); return request(`/api/expansion/overview${query ? `?${query}` : ""}`, undefined, true); } export async function getExpansionMeta() { return request("/api/expansion/meta", undefined, true); } export async function createSalesExpansion(payload: CreateSalesExpansionPayload) { return request("/api/expansion/sales", { method: "POST", body: JSON.stringify(payload), }, true); } export async function createChannelExpansion(payload: CreateChannelExpansionPayload) { return request("/api/expansion/channel", { method: "POST", body: JSON.stringify(payload), }, true); } export async function updateSalesExpansion(id: number, payload: UpdateSalesExpansionPayload) { return request(`/api/expansion/sales/${id}`, { method: "PUT", body: JSON.stringify(payload), }, true); } export async function updateChannelExpansion(id: number, payload: UpdateChannelExpansionPayload) { return request(`/api/expansion/channel/${id}`, { method: "PUT", body: JSON.stringify(payload), }, true); } export async function createExpansionFollowUp( bizType: "sales" | "channel", bizId: number, payload: CreateExpansionFollowUpPayload, ) { return request(`/api/expansion/${bizType}/${bizId}/followups`, { method: "POST", body: JSON.stringify(payload), }, true); }