import axios from 'axios' import { Toast } from 'antd-mobile' // 声明飞书 JSAPI 类型 declare global { interface Window { tt?: any h5sdk?: { ready: (callback: () => void) => void } } } // API 响应类型 export interface ApiResponse { code: number message?: string data: T } // 分页响应类型 export interface PaginationResponse { list: T[] total: number page: number page_size: number } // 登录响应类型 export interface LoginResponse { token: string user: { id: number name: string email?: string phone?: string avatar?: string company_id: number status: number } } const API_BASE = '/api/v1' // 标记是否正在刷新 token let isRefreshing = false let failedQueue: Array<{ resolve: (value: any) => void reject: (reason?: any) => void }> = [] // 处理队列中的请求 const processQueue = (error: any, token: string | null = null) => { failedQueue.forEach(({ resolve, reject }) => { if (error) { reject(error) } else { resolve(token) } }) failedQueue = [] } // 飞书免登获取新 token const refreshFeishuToken = async (): Promise => { console.log('🔄 尝试飞书免登刷新 token...') // 检查是否在飞书环境中 const ua = navigator.userAgent const isInFeishu = ua.includes('Lark') || ua.includes('feishu') || ua.includes('lark') if (!isInFeishu) { throw new Error('NOT_IN_FEISHU') } // 等待 tt 对象 if (!window.tt) { // 如果有 h5sdk,等待 ready if (window.h5sdk && typeof window.h5sdk.ready === 'function') { await new Promise((resolve) => { ;(window.h5sdk as any).ready(resolve) }) } else { // 轮询等待 tt 对象 await new Promise((resolve, reject) => { let attempts = 0 const maxAttempts = 50 const checkInterval = setInterval(() => { attempts++ if (window.tt) { clearInterval(checkInterval) resolve() } else if (attempts >= maxAttempts) { clearInterval(checkInterval) reject(new Error('TT_OBJECT_TIMEOUT')) } }, 100) }) } } // 获取 company 参数(从当前 URL 或 localStorage) const urlParams = new URLSearchParams(window.location.search) let companyParam = urlParams.get('company') if (!companyParam) { // 尝试从用户信息中获取 company_id const userStorage = localStorage.getItem('user-storage') if (userStorage) { try { const user = JSON.parse(userStorage) companyParam = user.state?.user?.company_id?.toString() } catch (e) { console.error('解析用户信息失败:', e) } } } // 获取 appId let appId = urlParams.get('app_id') if (!appId && companyParam) { try { const response = await fetch(`/api/v1/auth/feishu-config?company=${companyParam}`) const result = await response.json() if (result.code === 0 && result.data.app_id) { appId = result.data.app_id } } catch (e) { console.error('获取 appId 失败:', e) } } if (!appId) { throw new Error('NO_APP_ID') } // 调用飞书免登 return new Promise((resolve, reject) => { const redirectUri = `${window.location.origin}/login/callback` window.tt.requestAuthCode({ appId: appId, redirectUri: redirectUri, success: async (res: any) => { try { // 用新授权码登录 const loginRes = await fetch('/api/v1/auth/feishu-login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ auth_code: res.code, company: companyParam || undefined, }), }) const loginResult = await loginRes.json() if (loginResult.code === 0 && loginResult.data.token) { const newToken = loginResult.data.token // 更新本地存储 localStorage.setItem('token', newToken) // 更新用户 store if (loginResult.data.user) { const { useUserStore } = await import('@/stores/user') const { login } = useUserStore.getState() login(loginResult.data.user, newToken) } console.log('✅ Token 刷新成功') resolve(newToken) } else { reject(new Error(loginResult.message || '登录失败')) } } catch (e) { reject(e) } }, fail: (error: any) => { reject(new Error(error.errMsg || '免登失败')) }, }) }) } // 标记是否正在处理登出(防止重复跳转) let isLoggingOut = false // 处理 401 未授权错误 const handleUnauthorized = async (): Promise => { // 如果正在刷新 token,将请求加入队列 if (isRefreshing) { return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }) }) } isRefreshing = true try { // 尝试飞书免登刷新 token const newToken = await refreshFeishuToken() isRefreshing = false processQueue(null, newToken) return } catch (error: any) { isRefreshing = false processQueue(error, null) // 飞书免登失败,执行原有跳转逻辑 console.error('飞书免登刷新失败:', error.message) // 如果是非飞书环境或免登失败,跳转到登录页 if (isLoggingOut) return isLoggingOut = true const currentUrl = window.location.pathname + window.location.search console.log('🚨 跳转到登录页, currentUrl:', currentUrl) sessionStorage.setItem('redirect_after_login', currentUrl) // 清除本地存储 localStorage.removeItem('token') localStorage.removeItem('user-storage') Toast.show({ content: '请先登录', duration: 1500, }) setTimeout(() => { isLoggingOut = false const redirectUrl = '/login?redirect=' + encodeURIComponent(currentUrl) window.location.href = redirectUrl }, 500) throw error } } // 创建 axios 实例 const api = axios.create({ baseURL: API_BASE, timeout: 30000, headers: { 'Content-Type': 'application/json', }, }) // 请求拦截器:添加 token 和语言 api.interceptors.request.use( (config) => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } // 添加语言头 const lang = localStorage.getItem('i18nextLng') || 'zh' config.headers['Accept-Language'] = lang return config }, (error) => { return Promise.reject(error) } ) // 响应拦截器:处理错误 api.interceptors.response.use( (response) => { const { data } = response if (data.code === 0) { return data.data } return Promise.reject(new Error(data.message || '请求失败')) }, async (error) => { const originalRequest = error.config // 处理 401 未授权错误 if (error.response?.status === 401 || error.response?.data?.code === 401) { // 防止死循环:如果已经重试过一次,不再重试 if (originalRequest._retry) { console.error('⚠️ 请求重试后仍然 401,可能需要重新登录') // 清除 token 并跳转登录页 localStorage.removeItem('token') window.location.href = '/login' return Promise.reject(new Error('登录已过期')) } try { // 尝试飞书免登刷新 token await handleUnauthorized() // token 刷新成功,重试原始请求 originalRequest._retry = true // 更新请求头中的 token originalRequest.headers.Authorization = `Bearer ${localStorage.getItem('token')}` console.log('🔄 使用新 token 重试请求:', originalRequest.url) return api.request(originalRequest) } catch (e) { // handleUnauthorized 内部已经处理了跳转到登录页 return Promise.reject(new Error('登录已过期')) } } // 处理其他错误 const message = error.response?.data?.message || error.message || '网络错误' return Promise.reject(new Error(message)) } ) // ========== 认证 API ========== export const authApi = { login: (username: string, password: string) => api.post('/auth/login', { username, password }), // 飞书免登(支持 app_id、tenant_key 或 company) feishuLogin: (authCode: string, company?: string) => api.post('/auth/feishu-login', { auth_code: authCode, company: company, }), // 生成二维码登录会话 qrcodeLogin: (company?: string) => api.post('/auth/qrcode-login', { company: company, }), // 轮询二维码登录状态 qrcodeLoginStatus: (sessionId: string) => api.get('/auth/qrcode-login-status', { params: { session_id: sessionId } }), } // ========== 用户 API ========== export const userApi = { getInfo: () => api.get('/user/info'), getMenu: () => api.get('/user/menu'), getCompanies: () => api.get('/user/companies'), switchCompany: (companyId: number) => api.post('/user/switch-company', { company_id: companyId }), // 获取用户列表 getUsers: (params: { keyword?: string; page?: number; page_size?: number }) => api.get>('/user/list', { params }), // 修改密码 changePassword: (data: { old_password: string; new_password: string; confirm_password: string }) => api.post('/user/change-password', data), } // ========== 出差申请 API ========== export const businessTripApi = { getList: (params: { type?: string; page?: number; page_size?: number }) => api.get>('/business-trip/list', { params }), getDetail: (id: number) => api.get(`/business-trip/detail/${id}`), // 获取默认审批人和抄送人 getDefaultApprovers: () => api.get; cc_users: Array<{ id: number; name: string; avatar?: string }> }>('/business-trip/default-approvers'), create: (data: any) => api.post('/business-trip/create', data), update: (id: number, data: any) => api.post(`/business-trip/update/${id}`, data), submit: (id: number, approvers: number[], ccUsers: number[] = []) => api.post(`/business-trip/submit/${id}`, { approvers, cc_users: ccUsers, }), cancel: (id: number) => api.post(`/business-trip/cancel/${id}`), deleteDraft: (id: number) => api.delete(`/business-trip/${id}`), approve: (id: number, action: 'approve' | 'reject', comment?: string) => api.post(`/business-trip/approve/${id}`, { action, comment }), } // ========== 审批 API ========== export const approvalApi = { getList: (params: { type?: string; page?: number; page_size?: number }) => { console.log('🚀 API Request approval/list:', params) return api.get>('/approval/list', { params }) .then((res: any) => { console.log('✅ API Response approval/list:', { requestParams: params, responseData: res, listCount: res?.list?.length, listIds: res?.list?.map((item: any) => ({ id: item.id, title: item.title, status: item.status })) }) return res }) .catch(err => { console.error('❌ API Error approval/list:', err) throw err }) }, getDetail: (id: number) => api.get(`/approval/detail/${id}`), approve: (id: number, comment?: string) => api.post(`/approval/approve/${id}`, { comment }), reject: (id: number, comment?: string) => api.post(`/approval/reject/${id}`, { comment }), cancel: (id: number) => api.post(`/approval/cancel/${id}`), } export default api