unis_crm/frontend/src/lib/auth.ts

1199 lines
31 KiB
TypeScript
Raw Normal View History

2026-03-20 08:39:07 +00:00
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;
2026-04-02 09:26:35 +00:00
roleCodes?: string[];
roles?: UserRole[];
}
export interface UserRole {
roleId?: number;
roleCode?: string;
roleName?: string;
2026-03-20 08:39:07 +00:00
}
export interface PlatformConfig {
projectName?: string;
logoUrl?: string;
iconUrl?: string;
loginBgUrl?: string;
icpInfo?: string;
copyrightInfo?: string;
systemDescription?: string;
}
export interface WecomExchangePayload {
ticket: string;
}
export interface WecomJsSdkConfig {
corpId: string;
timestamp: number;
nonceStr: string;
signature: string;
}
2026-03-20 08:39:07 +00:00
export interface DashboardStat {
name: string;
value?: number;
metricKey: string;
}
export interface DashboardTodo {
2026-03-26 09:29:55 +00:00
id: string;
2026-03-20 08:39:07 +00:00
title?: string;
bizType?: string;
2026-03-26 09:29:55 +00:00
bizId?: string;
2026-03-20 08:39:07 +00:00
priority?: string;
status?: string;
dueDate?: string;
createdAt?: string;
2026-03-26 09:29:55 +00:00
updatedAt?: string;
2026-03-20 08:39:07 +00:00
}
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;
2026-03-23 01:03:27 +00:00
photoUrls?: string[];
2026-03-26 09:29:55 +00:00
bizType?: "sales" | "channel" | "opportunity";
bizId?: number;
bizName?: string;
userName?: string;
deptName?: string;
2026-03-20 08:39:07 +00:00
}
export interface WorkDailyReport {
id: number;
date?: string;
submitTime?: string;
workContent?: string;
2026-03-26 09:29:55 +00:00
lineItems?: WorkReportLineItem[];
2026-03-20 08:39:07 +00:00
tomorrowPlan?: string;
2026-03-26 09:29:55 +00:00
planItems?: WorkTomorrowPlanItem[];
2026-03-20 08:39:07 +00:00
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;
2026-03-23 01:03:27 +00:00
photoUrls?: string[];
2026-03-20 08:39:07 +00:00
}
2026-04-08 09:41:17 +00:00
export interface WorkCheckInExportRow {
checkinDate?: string;
checkinTime?: string;
userName?: string;
deptName?: string;
bizType?: "sales" | "channel" | "opportunity" | string;
bizName?: string;
locationText?: string;
longitude?: number;
latitude?: number;
photoUrls?: string[];
remark?: string;
status?: string;
createdAt?: string;
updatedAt?: string;
}
export interface WorkDailyReportExportRow {
reportDate?: string;
submitTime?: string;
userName?: string;
deptName?: string;
sourceType?: string;
status?: string;
workContent?: string;
tomorrowPlan?: string;
score?: number;
comment?: string;
reviewerName?: string;
reviewedAt?: string;
createdAt?: string;
updatedAt?: string;
lineItems?: WorkReportLineItem[];
planItems?: WorkTomorrowPlanItem[];
}
2026-03-20 08:39:07 +00:00
export interface WorkOverview {
todayCheckIn?: WorkCheckIn;
todayReport?: WorkDailyReport;
suggestedWorkContent?: string;
history?: WorkHistoryItem[];
}
2026-03-26 09:29:55 +00:00
export interface WorkHistoryPage {
items?: WorkHistoryItem[];
hasMore?: boolean;
page?: number;
size?: number;
}
2026-04-08 09:41:17 +00:00
export interface WorkExportQuery {
startDate?: string;
endDate?: string;
keyword?: string;
deptName?: string;
bizType?: string;
status?: string;
}
2026-03-20 08:39:07 +00:00
export interface CreateWorkCheckInPayload {
locationText: string;
remark?: string;
longitude?: number;
latitude?: number;
2026-03-23 01:03:27 +00:00
photoUrls?: string[];
2026-03-26 09:29:55 +00:00
bizType?: "sales" | "channel" | "opportunity";
bizId?: number;
bizName?: string;
userName?: string;
deptName?: string;
}
export interface WorkReportLineItem {
workDate: string;
bizType: "sales" | "channel" | "opportunity";
bizId: number;
bizName?: string;
editorText?: string;
content: string;
visitStartTime?: string;
evaluationContent?: string;
nextPlan?: string;
latestProgress?: string;
communicationTime?: string;
communicationContent?: string;
}
export interface WorkTomorrowPlanItem {
content: string;
2026-03-20 08:39:07 +00:00
}
export interface CreateWorkDailyReportPayload {
workContent: string;
2026-03-26 09:29:55 +00:00
lineItems: WorkReportLineItem[];
planItems: WorkTomorrowPlanItem[];
2026-03-20 08:39:07 +00:00
tomorrowPlan: string;
sourceType?: string;
}
export interface OpportunityFollowUp {
id: number;
opportunityId?: number;
date?: string;
type?: string;
content?: string;
2026-03-26 09:29:55 +00:00
latestProgress?: string;
communicationTime?: string;
communicationContent?: string;
nextAction?: string;
2026-03-20 08:39:07 +00:00
user?: string;
}
export interface OpportunityItem {
id: number;
2026-04-02 09:26:35 +00:00
ownerUserId?: number;
2026-03-20 08:39:07 +00:00
code?: string;
name?: string;
client?: string;
owner?: string;
2026-04-15 08:21:52 +00:00
updatedAt?: string;
2026-03-26 09:29:55 +00:00
projectLocation?: string;
operatorCode?: string;
operatorName?: string;
2026-03-20 08:39:07 +00:00
amount?: number;
date?: string;
2026-04-01 09:24:06 +00:00
confidence?: string;
2026-03-26 09:29:55 +00:00
stageCode?: string;
2026-03-20 08:39:07 +00:00
stage?: string;
type?: string;
2026-03-27 09:05:41 +00:00
archived?: boolean;
2026-03-20 08:39:07 +00:00
pushedToOms?: boolean;
product?: string;
source?: string;
2026-03-26 09:29:55 +00:00
salesExpansionId?: number;
salesExpansionName?: string;
channelExpansionId?: number;
channelExpansionName?: string;
2026-04-01 09:24:06 +00:00
preSalesId?: number;
preSalesName?: string;
2026-03-26 09:29:55 +00:00
competitorName?: string;
latestProgress?: string;
nextPlan?: string;
2026-03-20 08:39:07 +00:00
notes?: string;
followUps?: OpportunityFollowUp[];
}
export interface OpportunityOverview {
items?: OpportunityItem[];
}
2026-03-26 09:29:55 +00:00
export interface OpportunityDictOption {
label?: string;
value?: string;
}
export interface OpportunityMeta {
stageOptions?: OpportunityDictOption[];
operatorOptions?: OpportunityDictOption[];
2026-04-01 09:24:06 +00:00
projectLocationOptions?: OpportunityDictOption[];
2026-04-02 02:07:21 +00:00
opportunityTypeOptions?: OpportunityDictOption[];
2026-04-15 08:21:52 +00:00
confidenceOptions?: OpportunityDictOption[];
2026-04-01 09:24:06 +00:00
}
export interface OmsPreSalesOption {
userId: number;
loginName?: string;
userName?: string;
2026-03-26 09:29:55 +00:00
}
2026-03-20 08:39:07 +00:00
export interface CreateOpportunityPayload {
opportunityName: string;
customerName: string;
2026-03-26 09:29:55 +00:00
projectLocation?: string;
operatorName?: string;
2026-03-20 08:39:07 +00:00
amount: number;
expectedCloseDate: string;
2026-04-01 09:24:06 +00:00
confidencePct: string;
2026-03-20 08:39:07 +00:00
stage?: string;
opportunityType?: string;
productType?: string;
source?: string;
2026-03-26 09:29:55 +00:00
salesExpansionId?: number;
channelExpansionId?: number;
competitorName?: string;
2026-03-20 08:39:07 +00:00
pushedToOms?: boolean;
description?: string;
}
2026-04-01 09:24:06 +00:00
export interface PushOpportunityToOmsPayload {
preSalesId?: number;
preSalesName?: string;
}
2026-03-20 08:39:07 +00:00
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;
2026-03-26 09:29:55 +00:00
visitStartTime?: string;
evaluationContent?: string;
nextPlan?: string;
2026-03-20 08:39:07 +00:00
}
export interface SalesExpansionItem {
id: number;
2026-04-02 09:26:35 +00:00
ownerUserId?: number;
2026-04-03 02:11:19 +00:00
owner?: string;
2026-03-20 08:39:07 +00:00
type: "sales";
2026-03-26 09:29:55 +00:00
employeeNo?: string;
2026-03-20 08:39:07 +00:00
name?: string;
2026-03-26 09:29:55 +00:00
officeCode?: string;
officeName?: string;
2026-03-20 08:39:07 +00:00
phone?: string;
email?: string;
2026-03-26 09:29:55 +00:00
targetDept?: string;
2026-03-20 08:39:07 +00:00
dept?: string;
2026-03-26 09:29:55 +00:00
industryCode?: string;
2026-03-20 08:39:07 +00:00
industry?: string;
title?: string;
intentLevel?: string;
intent?: string;
stageCode?: string;
stage?: string;
hasExp?: boolean;
inProgress?: boolean;
active?: boolean;
employmentStatus?: string;
expectedJoinDate?: string;
2026-04-15 08:21:52 +00:00
updatedAt?: string;
2026-03-20 08:39:07 +00:00
notes?: string;
2026-03-26 09:29:55 +00:00
relatedProjects?: RelatedProjectSummary[];
2026-03-20 08:39:07 +00:00
followUps?: ExpansionFollowUp[];
}
2026-03-26 09:29:55 +00:00
export interface RelatedProjectSummary {
opportunityId: number;
opportunityCode?: string;
opportunityName?: string;
amount?: number;
}
2026-03-20 08:39:07 +00:00
export interface ChannelExpansionItem {
id: number;
2026-04-02 09:26:35 +00:00
ownerUserId?: number;
2026-04-03 02:11:19 +00:00
owner?: string;
2026-03-20 08:39:07 +00:00
type: "channel";
2026-03-26 09:29:55 +00:00
channelCode?: string;
2026-03-20 08:39:07 +00:00
name?: string;
2026-04-01 09:24:06 +00:00
provinceCode?: string;
2026-03-20 08:39:07 +00:00
province?: string;
2026-04-01 09:24:06 +00:00
cityCode?: string;
city?: string;
2026-03-26 09:29:55 +00:00
officeAddress?: string;
2026-04-01 09:24:06 +00:00
channelIndustryCode?: string;
2026-03-26 09:29:55 +00:00
channelIndustry?: string;
2026-04-01 09:24:06 +00:00
certificationLevel?: string;
2026-03-20 08:39:07 +00:00
annualRevenue?: string;
revenue?: string;
size?: number;
2026-03-26 09:29:55 +00:00
primaryContactName?: string;
primaryContactTitle?: string;
primaryContactMobile?: string;
establishedDate?: string;
intentLevel?: string;
intent?: string;
hasDesktopExp?: boolean;
2026-03-27 09:05:41 +00:00
channelAttributeCode?: string;
2026-03-26 09:29:55 +00:00
channelAttribute?: string;
2026-03-27 09:05:41 +00:00
internalAttributeCode?: string;
2026-03-26 09:29:55 +00:00
internalAttribute?: string;
2026-03-20 08:39:07 +00:00
stageCode?: string;
stage?: string;
landed?: boolean;
expectedSignDate?: string;
2026-04-15 08:21:52 +00:00
updatedAt?: string;
2026-03-20 08:39:07 +00:00
notes?: string;
2026-03-26 09:29:55 +00:00
contacts?: ChannelExpansionContact[];
relatedProjects?: ChannelRelatedProjectSummary[];
2026-03-20 08:39:07 +00:00
followUps?: ExpansionFollowUp[];
}
2026-03-26 09:29:55 +00:00
export interface ChannelExpansionContact {
id?: number;
name?: string;
mobile?: string;
title?: string;
}
export interface ChannelRelatedProjectSummary {
opportunityId: number;
opportunityCode?: string;
opportunityName?: string;
amount?: number;
}
2026-03-20 08:39:07 +00:00
export interface ExpansionOverview {
salesItems?: SalesExpansionItem[];
channelItems?: ChannelExpansionItem[];
}
2026-03-26 09:29:55 +00:00
export interface ExpansionDictOption {
label?: string;
value?: string;
2026-03-20 08:39:07 +00:00
}
export interface ExpansionMeta {
2026-03-26 09:29:55 +00:00
officeOptions?: ExpansionDictOption[];
industryOptions?: ExpansionDictOption[];
2026-04-01 09:24:06 +00:00
provinceOptions?: ExpansionDictOption[];
certificationLevelOptions?: ExpansionDictOption[];
2026-03-26 09:29:55 +00:00
channelAttributeOptions?: ExpansionDictOption[];
internalAttributeOptions?: ExpansionDictOption[];
nextChannelCode?: string;
2026-03-20 08:39:07 +00:00
}
2026-04-02 05:19:22 +00:00
export interface ExpansionDuplicateCheck {
duplicated?: boolean;
message?: string;
}
2026-03-20 08:39:07 +00:00
export interface CreateSalesExpansionPayload {
2026-03-26 09:29:55 +00:00
employeeNo: string;
2026-03-20 08:39:07 +00:00
candidateName: string;
2026-03-26 09:29:55 +00:00
officeName?: string;
2026-03-20 08:39:07 +00:00
mobile?: string;
email?: string;
2026-03-26 09:29:55 +00:00
targetDept?: string;
2026-03-20 08:39:07 +00:00
industry?: string;
title?: string;
intentLevel?: string;
stage?: string;
hasDesktopExp?: boolean;
inProgress?: boolean;
employmentStatus?: string;
expectedJoinDate?: string;
remark?: string;
}
export interface CreateChannelExpansionPayload {
2026-03-26 09:29:55 +00:00
channelCode?: string;
officeAddress?: string;
2026-04-01 09:24:06 +00:00
channelIndustry?: string[];
2026-03-20 08:39:07 +00:00
channelName: string;
province?: string;
2026-04-01 09:24:06 +00:00
city?: string;
certificationLevel?: string;
2026-03-20 08:39:07 +00:00
annualRevenue?: number;
staffSize?: number;
2026-03-26 09:29:55 +00:00
contactEstablishedDate?: string;
intentLevel?: string;
hasDesktopExp?: boolean;
2026-03-27 09:05:41 +00:00
channelAttribute?: string[];
channelAttributeCustom?: string;
internalAttribute?: string[];
2026-03-20 08:39:07 +00:00
stage?: string;
remark?: string;
2026-03-26 09:29:55 +00:00
contacts?: ChannelExpansionContact[];
2026-03-20 08:39:07 +00:00
}
export interface UpdateSalesExpansionPayload extends CreateSalesExpansionPayload {}
export interface UpdateChannelExpansionPayload extends CreateChannelExpansionPayload {}
2026-03-27 09:05:41 +00:00
const EXPANSION_MULTI_VALUE_CUSTOM_PREFIX = "__custom__:";
function normalizeExpansionMultiValues(values?: string[]) {
return Array.from(new Set((values ?? []).map((value) => value?.trim()).filter((value): value is string => Boolean(value))));
}
export function encodeExpansionMultiValue(values?: string[], customText?: string) {
const normalizedValues = normalizeExpansionMultiValues(values);
const normalizedCustomText = customText?.trim();
if (normalizedCustomText) {
normalizedValues.push(`${EXPANSION_MULTI_VALUE_CUSTOM_PREFIX}${encodeURIComponent(normalizedCustomText)}`);
}
return normalizedValues.join(",");
}
export function decodeExpansionMultiValue(rawValue?: string) {
const result = {
values: [] as string[],
customText: "",
};
if (!rawValue?.trim()) {
return result;
}
rawValue
.split(",")
.map((item) => item.trim())
.filter(Boolean)
.forEach((item) => {
if (item.startsWith(EXPANSION_MULTI_VALUE_CUSTOM_PREFIX)) {
result.customText = decodeURIComponent(item.slice(EXPANSION_MULTI_VALUE_CUSTOM_PREFIX.length));
return;
}
result.values.push(item);
});
result.values = Array.from(new Set(result.values));
return result;
}
function serializeChannelExpansionPayload(payload: CreateChannelExpansionPayload) {
const { channelAttributeCustom, ...rest } = payload;
return {
...rest,
2026-04-01 09:24:06 +00:00
city: payload.city,
certificationLevel: payload.certificationLevel,
channelIndustry: encodeExpansionMultiValue(payload.channelIndustry),
2026-03-27 09:05:41 +00:00
channelAttribute: encodeExpansionMultiValue(payload.channelAttribute, channelAttributeCustom),
internalAttribute: encodeExpansionMultiValue(payload.internalAttribute),
};
}
2026-03-20 08:39:07 +00:00
export interface CreateExpansionFollowUpPayload {
followUpType: string;
content: string;
nextAction?: string;
followUpTime: string;
2026-03-26 09:29:55 +00:00
visitStartTime?: string;
evaluationContent?: string;
nextPlan?: string;
2026-03-20 08:39:07 +00:00
}
interface ApiEnvelope<T> {
2026-04-13 01:35:05 +00:00
code: string | number;
2026-03-20 08:39:07 +00:00
msg: string;
data: T;
}
interface ApiErrorBody {
2026-04-13 01:35:05 +00:00
code?: string | number;
2026-03-20 08:39:07 +00:00
msg?: string;
message?: string;
}
const LOGIN_PATH = "/login";
2026-03-26 09:29:55 +00:00
const USER_CONTEXT_CACHE_TTL_MS = 2 * 60 * 1000;
const CURRENT_USER_CACHE_KEY = "auth-cache:current-user";
const PROFILE_OVERVIEW_CACHE_KEY = "auth-cache:profile-overview";
const memoryRequestCache = new Map<string, { expiresAt: number; value: unknown }>();
const inFlightRequestCache = new Map<string, Promise<unknown>>();
2026-04-13 01:35:05 +00:00
function isSuccessCode(code: unknown) {
return code === "0" || code === "200" || code === 0 || code === 200;
}
2026-03-26 09:29:55 +00:00
function applyAuthHeaders(headers: Headers) {
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));
}
return headers;
}
function handleUnauthorizedResponse() {
clearAuth();
window.location.href = `${LOGIN_PATH}?timeout=1`;
}
2026-03-20 08:39:07 +00:00
2026-04-09 07:50:56 +00:00
function tryParseApiBody<T>(rawText: string, contentType?: string | null): (ApiEnvelope<T> & ApiErrorBody) | null {
const normalizedText = rawText.trim();
const normalizedContentType = (contentType || "").toLowerCase();
const looksLikeJson = normalizedContentType.includes("application/json")
|| normalizedContentType.includes("+json")
|| normalizedText.startsWith("{")
|| normalizedText.startsWith("[");
if (!normalizedText || !looksLikeJson) {
return null;
}
try {
return JSON.parse(normalizedText) as ApiEnvelope<T> & ApiErrorBody;
} catch {
return null;
}
}
function extractHtmlErrorMessage(rawText: string) {
const titleMatch = rawText.match(/<title[^>]*>([^<]+)<\/title>/i);
if (titleMatch?.[1]?.trim()) {
return titleMatch[1].trim();
}
const headingMatch = rawText.match(/<h[1-6][^>]*>([^<]+)<\/h[1-6]>/i);
if (headingMatch?.[1]?.trim()) {
return headingMatch[1].trim();
}
const textOnly = rawText
.replace(/<script[\s\S]*?<\/script>/gi, " ")
.replace(/<style[\s\S]*?<\/style>/gi, " ")
.replace(/<[^>]+>/g, " ")
.replace(/\s+/g, " ")
.trim();
return textOnly || null;
}
function buildApiErrorMessage(rawText: string, status: number, body?: ApiErrorBody | null) {
const directMessage = body?.msg?.trim() || body?.message?.trim();
if (directMessage) {
return directMessage;
}
const normalizedText = rawText.trim();
if (!normalizedText) {
return `请求失败(${status})`;
}
if (normalizedText.startsWith("<")) {
return extractHtmlErrorMessage(normalizedText) || `请求失败(${status})`;
}
return normalizedText.length > 300 ? normalizedText.slice(0, 300) : normalizedText;
}
2026-03-20 08:39:07 +00:00
async function request<T>(input: string, init?: RequestInit, withAuth = false): Promise<T> {
const headers = new Headers(init?.headers);
2026-03-23 01:03:27 +00:00
if (!headers.has("Content-Type") && init?.body && !(init.body instanceof FormData)) {
2026-03-20 08:39:07 +00:00
headers.set("Content-Type", "application/json");
}
if (withAuth) {
2026-03-26 09:29:55 +00:00
applyAuthHeaders(headers);
2026-03-20 08:39:07 +00:00
}
const response = await fetch(input, {
...init,
headers,
});
if (response.status === 401 || response.status === 403) {
2026-03-26 09:29:55 +00:00
handleUnauthorizedResponse();
2026-03-20 08:39:07 +00:00
throw new Error("登录已失效,请重新登录");
}
2026-04-09 07:50:56 +00:00
const rawText = await response.text();
const body = tryParseApiBody<T>(rawText, response.headers.get("content-type"));
2026-03-20 08:39:07 +00:00
if (!response.ok) {
2026-04-09 07:50:56 +00:00
throw new Error(buildApiErrorMessage(rawText, response.status, body));
2026-03-20 08:39:07 +00:00
}
if (!body) {
2026-04-09 07:50:56 +00:00
throw new Error(rawText.trim() ? buildApiErrorMessage(rawText, response.status, null) : "接口返回为空");
2026-03-20 08:39:07 +00:00
}
2026-04-13 01:35:05 +00:00
if (!isSuccessCode(body.code)) {
2026-04-09 07:50:56 +00:00
throw new Error(buildApiErrorMessage(rawText, response.status, body));
2026-03-20 08:39:07 +00:00
}
return body.data;
}
2026-03-26 09:29:55 +00:00
export async function fetchWithAuth(input: string, init?: RequestInit) {
const response = await fetch(input, {
...init,
headers: applyAuthHeaders(new Headers(init?.headers)),
});
if (response.status === 401 || response.status === 403) {
handleUnauthorizedResponse();
throw new Error("登录已失效,请重新登录");
}
return response;
}
2026-03-20 08:39:07 +00:00
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");
2026-03-26 09:29:55 +00:00
clearCachedAuthContext();
2026-03-20 08:39:07 +00:00
}
export function persistLogin(payload: TokenResponse, username: string) {
2026-03-26 09:29:55 +00:00
clearCachedAuthContext();
2026-03-20 08:39:07 +00:00
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<UserProfile>;
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;
}
2026-04-02 09:26:35 +00:00
export function getStoredCurrentUserId() {
return getStoredUserId();
}
2026-03-20 08:39:07 +00:00
export async function fetchCaptcha() {
return request<CaptchaResponse>("/api/sys/auth/captcha");
}
export async function login(payload: LoginPayload) {
return request<TokenResponse>("/api/sys/auth/login", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function exchangeWecomTicket(payload: WecomExchangePayload) {
return request<TokenResponse>("/api/wecom/sso/exchange", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function getWecomJsSdkConfig(url: string) {
const params = new URLSearchParams({ url });
return request<WecomJsSdkConfig>(`/api/wecom/sso/js-sdk-config?${params.toString()}`, undefined, true);
}
2026-03-20 08:39:07 +00:00
export async function getSystemParamValue(key: string, defaultValue?: string) {
const params = new URLSearchParams({ key });
if (defaultValue !== undefined) {
params.set("defaultValue", defaultValue);
}
return request<string>(`/api/sys/api/params/value?${params.toString()}`);
}
export async function getOpenPlatformConfig() {
return request<PlatformConfig>("/api/sys/api/open/platform/config");
}
export async function getCurrentUser() {
2026-03-26 09:29:55 +00:00
return getCachedAuthedRequest<UserProfile>(
CURRENT_USER_CACHE_KEY,
2026-04-02 09:26:35 +00:00
async () => {
const profile = await request<UserProfile>("/api/sys/api/users/me", undefined, true);
persistStoredUserProfile(profile);
return profile;
},
2026-03-26 09:29:55 +00:00
);
2026-03-20 08:39:07 +00:00
}
2026-04-02 09:26:35 +00:00
export async function refreshCurrentUser() {
clearCachedAuthContext();
const profile = await request<UserProfile>("/api/sys/api/users/me", undefined, true);
persistStoredUserProfile(profile);
return profile;
}
2026-03-20 08:39:07 +00:00
export async function getDashboardHome() {
return request<DashboardHome>("/api/dashboard/home", undefined, true);
}
2026-03-26 09:29:55 +00:00
export async function completeDashboardTodo(todoId: string) {
return request<void>(`/api/dashboard/todos/${todoId}/complete`, {
method: "POST",
}, true);
}
2026-03-20 08:39:07 +00:00
export async function getProfileOverview() {
2026-03-26 09:29:55 +00:00
return getCachedAuthedRequest<ProfileOverview>(
PROFILE_OVERVIEW_CACHE_KEY,
() => request<ProfileOverview>("/api/profile/overview", undefined, true),
);
2026-03-20 08:39:07 +00:00
}
export async function updateCurrentUserProfile(payload: UpdateCurrentUserProfilePayload) {
2026-03-26 09:29:55 +00:00
const result = await request<boolean>("/api/sys/api/users/profile", {
2026-03-20 08:39:07 +00:00
method: "PUT",
body: JSON.stringify(payload),
}, true);
2026-03-26 09:29:55 +00:00
clearCachedAuthContext();
return result;
2026-03-20 08:39:07 +00:00
}
export async function updateCurrentUserPassword(payload: UpdateCurrentUserPasswordPayload) {
return request<boolean>("/api/sys/api/users/password", {
method: "PUT",
body: JSON.stringify(payload),
}, true);
}
export async function updateUserProfileById(userId: number, payload: UpdateCurrentUserProfilePayload) {
return request<boolean>(`/api/sys/api/users/${userId}`, {
method: "PUT",
body: JSON.stringify(payload),
}, true);
}
export async function getWorkOverview() {
return request<WorkOverview>("/api/work/overview", undefined, true);
}
2026-03-26 09:29:55 +00:00
export async function getWorkHistory(type: "checkin" | "report", page = 1, size = 8) {
const params = new URLSearchParams({
type,
page: String(page),
size: String(size),
2026-03-23 07:21:09 +00:00
});
2026-03-26 09:29:55 +00:00
return request<WorkHistoryPage>(`/api/work/history?${params.toString()}`, undefined, true);
2026-03-23 07:21:09 +00:00
}
2026-04-08 09:41:17 +00:00
function buildWorkExportQuery(params?: WorkExportQuery) {
const searchParams = new URLSearchParams();
Object.entries(params ?? {}).forEach(([key, value]) => {
const normalized = value?.trim();
if (normalized) {
searchParams.set(key, normalized);
}
});
const query = searchParams.toString();
return query ? `?${query}` : "";
}
export async function getWorkCheckInExportData(params?: WorkExportQuery) {
return request<WorkCheckInExportRow[]>(`/api/work/checkins/export-data${buildWorkExportQuery(params)}`, undefined, true);
}
export async function getWorkDailyReportExportData(params?: WorkExportQuery) {
return request<WorkDailyReportExportRow[]>(`/api/work/daily-reports/export-data${buildWorkExportQuery(params)}`, undefined, true);
}
2026-03-26 09:29:55 +00:00
export async function reverseWorkGeocode(latitude: number, longitude: number) {
return request<string>(`/api/work/reverse-geocode?lat=${latitude}&lon=${longitude}`, undefined, true);
2026-03-20 08:39:07 +00:00
}
export async function saveWorkCheckIn(payload: CreateWorkCheckInPayload) {
return request<number>("/api/work/checkins", {
method: "POST",
body: JSON.stringify(payload),
}, true);
}
2026-03-23 01:03:27 +00:00
export async function uploadWorkCheckInPhoto(file: File) {
const formData = new FormData();
formData.append("file", file);
return request<string>("/api/work/checkin-photos", {
method: "POST",
body: formData,
}, true);
}
2026-03-20 08:39:07 +00:00
export async function saveWorkDailyReport(payload: CreateWorkDailyReportPayload) {
return request<number>("/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<OpportunityOverview>(`/api/opportunities/overview${query ? `?${query}` : ""}`, undefined, true);
}
2026-03-26 09:29:55 +00:00
export async function getOpportunityMeta() {
return request<OpportunityMeta>("/api/opportunities/meta", undefined, true);
}
2026-04-01 09:24:06 +00:00
export async function getOpportunityOmsPreSalesOptions() {
return request<OmsPreSalesOption[]>("/api/opportunities/oms/pre-sales", undefined, true);
}
2026-03-20 08:39:07 +00:00
export async function createOpportunity(payload: CreateOpportunityPayload) {
return request<number>("/api/opportunities", {
method: "POST",
body: JSON.stringify(payload),
}, true);
}
export async function updateOpportunity(opportunityId: number, payload: CreateOpportunityPayload) {
return request<number>(`/api/opportunities/${opportunityId}`, {
method: "PUT",
body: JSON.stringify(payload),
}, true);
}
2026-04-01 09:24:06 +00:00
export async function pushOpportunityToOms(opportunityId: number, payload?: PushOpportunityToOmsPayload) {
2026-03-26 09:29:55 +00:00
return request<number>(`/api/opportunities/${opportunityId}/push-oms`, {
method: "POST",
2026-04-01 09:24:06 +00:00
body: JSON.stringify(payload ?? {}),
2026-03-26 09:29:55 +00:00
}, true);
}
2026-03-20 08:39:07 +00:00
export async function createOpportunityFollowUp(opportunityId: number, payload: CreateOpportunityFollowUpPayload) {
return request<number>(`/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<ExpansionOverview>(`/api/expansion/overview${query ? `?${query}` : ""}`, undefined, true);
}
export async function getExpansionMeta() {
return request<ExpansionMeta>("/api/expansion/meta", undefined, true);
}
2026-04-01 09:24:06 +00:00
export async function getExpansionCityOptions(provinceName: string) {
const params = new URLSearchParams({ provinceName });
return request<ExpansionDictOption[]>(`/api/expansion/areas/cities?${params.toString()}`, undefined, true);
}
2026-04-02 05:19:22 +00:00
export async function checkSalesExpansionDuplicate(employeeNo: string, excludeId?: number) {
const params = new URLSearchParams({ employeeNo });
if (excludeId) {
params.set("excludeId", String(excludeId));
}
return request<ExpansionDuplicateCheck>(`/api/expansion/sales/duplicate-check?${params.toString()}`, undefined, true);
}
export async function checkChannelExpansionDuplicate(channelName: string, excludeId?: number) {
const params = new URLSearchParams({ channelName });
if (excludeId) {
params.set("excludeId", String(excludeId));
}
return request<ExpansionDuplicateCheck>(`/api/expansion/channel/duplicate-check?${params.toString()}`, undefined, true);
}
2026-03-20 08:39:07 +00:00
export async function createSalesExpansion(payload: CreateSalesExpansionPayload) {
return request<number>("/api/expansion/sales", {
method: "POST",
body: JSON.stringify(payload),
}, true);
}
export async function createChannelExpansion(payload: CreateChannelExpansionPayload) {
return request<number>("/api/expansion/channel", {
method: "POST",
2026-03-27 09:05:41 +00:00
body: JSON.stringify(serializeChannelExpansionPayload(payload)),
2026-03-20 08:39:07 +00:00
}, true);
}
export async function updateSalesExpansion(id: number, payload: UpdateSalesExpansionPayload) {
return request<void>(`/api/expansion/sales/${id}`, {
method: "PUT",
body: JSON.stringify(payload),
}, true);
}
export async function updateChannelExpansion(id: number, payload: UpdateChannelExpansionPayload) {
return request<void>(`/api/expansion/channel/${id}`, {
method: "PUT",
2026-03-27 09:05:41 +00:00
body: JSON.stringify(serializeChannelExpansionPayload(payload)),
2026-03-20 08:39:07 +00:00
}, true);
}
export async function createExpansionFollowUp(
bizType: "sales" | "channel",
bizId: number,
payload: CreateExpansionFollowUpPayload,
) {
return request<number>(`/api/expansion/${bizType}/${bizId}/followups`, {
method: "POST",
body: JSON.stringify(payload),
}, true);
}
2026-03-26 09:29:55 +00:00
function readCachedValue<T>(cacheKey: string) {
const memoryValue = memoryRequestCache.get(cacheKey);
if (memoryValue && memoryValue.expiresAt > Date.now()) {
return memoryValue.value as T;
}
memoryRequestCache.delete(cacheKey);
try {
const raw = sessionStorage.getItem(cacheKey);
if (!raw) {
return null;
}
const parsed = JSON.parse(raw) as { expiresAt?: number; value?: T };
if (!parsed.expiresAt || parsed.expiresAt <= Date.now()) {
sessionStorage.removeItem(cacheKey);
return null;
}
memoryRequestCache.set(cacheKey, { expiresAt: parsed.expiresAt, value: parsed.value });
return parsed.value ?? null;
} catch {
sessionStorage.removeItem(cacheKey);
return null;
}
}
function writeCachedValue<T>(cacheKey: string, value: T, ttlMs = USER_CONTEXT_CACHE_TTL_MS) {
const payload = {
expiresAt: Date.now() + ttlMs,
value,
};
memoryRequestCache.set(cacheKey, payload);
try {
sessionStorage.setItem(cacheKey, JSON.stringify(payload));
} catch {
// Ignore session cache failures and keep in-memory cache only.
}
}
function clearCachedAuthContext() {
[CURRENT_USER_CACHE_KEY, PROFILE_OVERVIEW_CACHE_KEY].forEach((cacheKey) => {
memoryRequestCache.delete(cacheKey);
inFlightRequestCache.delete(cacheKey);
try {
sessionStorage.removeItem(cacheKey);
} catch {
// Ignore session cache cleanup failures.
}
});
}
2026-04-02 09:26:35 +00:00
function persistStoredUserProfile(profile: UserProfile) {
try {
sessionStorage.setItem("userProfile", JSON.stringify(profile));
} catch {
// Ignore session storage failures and keep request cache only.
}
}
2026-03-26 09:29:55 +00:00
async function getCachedAuthedRequest<T>(
cacheKey: string,
fetcher: () => Promise<T>,
ttlMs = USER_CONTEXT_CACHE_TTL_MS,
) {
const cachedValue = readCachedValue<T>(cacheKey);
if (cachedValue) {
return cachedValue;
}
const inFlight = inFlightRequestCache.get(cacheKey);
if (inFlight) {
return inFlight as Promise<T>;
}
const requestPromise = fetcher()
.then((result) => {
writeCachedValue(cacheKey, result, ttlMs);
inFlightRequestCache.delete(cacheKey);
return result;
})
.catch((error) => {
inFlightRequestCache.delete(cacheKey);
throw error;
});
inFlightRequestCache.set(cacheKey, requestPromise);
return requestPromise;
}