unis_crm/frontend/src/lib/auth.ts

641 lines
15 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;
}
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;
2026-03-23 01:03:27 +00:00
photoUrls?: string[];
2026-03-20 08:39:07 +00:00
}
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;
2026-03-23 01:03:27 +00:00
photoUrls?: string[];
2026-03-20 08:39:07 +00:00
}
export interface WorkOverview {
todayCheckIn?: WorkCheckIn;
todayReport?: WorkDailyReport;
suggestedWorkContent?: string;
history?: WorkHistoryItem[];
}
export interface CreateWorkCheckInPayload {
locationText: string;
remark?: string;
longitude?: number;
latitude?: number;
2026-03-23 01:03:27 +00:00
photoUrls?: string[];
2026-03-20 08:39:07 +00:00
}
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<T> {
code: string;
msg: string;
data: T;
}
interface ApiErrorBody {
code?: string;
msg?: string;
message?: string;
}
const LOGIN_PATH = "/login";
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) {
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<T> & ApiErrorBody) | null = null;
try {
body = (await response.json()) as ApiEnvelope<T> & 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<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;
}
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 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() {
return request<UserProfile>("/api/sys/api/users/me", undefined, true);
}
export async function getDashboardHome() {
return request<DashboardHome>("/api/dashboard/home", undefined, true);
}
export async function getProfileOverview() {
return request<ProfileOverview>("/api/profile/overview", undefined, true);
}
export async function updateCurrentUserProfile(payload: UpdateCurrentUserProfilePayload) {
return request<boolean>("/api/sys/api/users/profile", {
method: "PUT",
body: JSON.stringify(payload),
}, true);
}
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);
}
export async function reverseWorkGeocode(latitude: number, longitude: number) {
const params = new URLSearchParams({
lat: String(latitude),
lon: String(longitude),
});
return request<string>(`/api/work/reverse-geocode?${params.toString()}`, undefined, true);
}
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);
}
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);
}
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);
}
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",
body: JSON.stringify(payload),
}, 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",
body: JSON.stringify(payload),
}, 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);
}