2026-02-25 15:22:12 +08:00
|
|
|
|
import axios from 'axios'
|
2026-02-26 15:19:37 +08:00
|
|
|
|
import { Toast } from 'antd-mobile'
|
2026-02-24 17:57:25 +08:00
|
|
|
|
|
2026-02-28 11:12:26 +08:00
|
|
|
|
// 声明飞书 JSAPI 类型
|
|
|
|
|
|
declare global {
|
|
|
|
|
|
interface Window {
|
|
|
|
|
|
tt?: any
|
|
|
|
|
|
h5sdk?: {
|
|
|
|
|
|
ready: (callback: () => void) => void
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 14:58:25 +08:00
|
|
|
|
// API 响应类型
|
|
|
|
|
|
export interface ApiResponse<T = any> {
|
|
|
|
|
|
code: number
|
|
|
|
|
|
message?: string
|
|
|
|
|
|
data: T
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 分页响应类型
|
|
|
|
|
|
export interface PaginationResponse<T> {
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 15:22:12 +08:00
|
|
|
|
const API_BASE = '/api/v1'
|
2026-02-24 17:57:25 +08:00
|
|
|
|
|
2026-02-28 11:12:26 +08:00
|
|
|
|
// 标记是否正在刷新 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<string> => {
|
|
|
|
|
|
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
|
2026-02-28 14:58:25 +08:00
|
|
|
|
if (window.h5sdk && typeof window.h5sdk.ready === 'function') {
|
2026-02-28 11:12:26 +08:00
|
|
|
|
await new Promise<void>((resolve) => {
|
2026-02-28 14:58:25 +08:00
|
|
|
|
;(window.h5sdk as any).ready(resolve)
|
2026-02-28 11:12:26 +08:00
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 轮询等待 tt 对象
|
|
|
|
|
|
await new Promise<void>((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 || '免登失败'))
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 15:19:37 +08:00
|
|
|
|
// 标记是否正在处理登出(防止重复跳转)
|
|
|
|
|
|
let isLoggingOut = false
|
|
|
|
|
|
|
|
|
|
|
|
// 处理 401 未授权错误
|
2026-02-28 11:12:26 +08:00
|
|
|
|
const handleUnauthorized = async (): Promise<void> => {
|
|
|
|
|
|
// 如果正在刷新 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)
|
2026-02-26 15:19:37 +08:00
|
|
|
|
|
2026-02-28 11:12:26 +08:00
|
|
|
|
// 飞书免登失败,执行原有跳转逻辑
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-02-26 15:19:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 17:57:25 +08:00
|
|
|
|
// 创建 axios 实例
|
2026-02-25 15:22:12 +08:00
|
|
|
|
const api = axios.create({
|
|
|
|
|
|
baseURL: API_BASE,
|
|
|
|
|
|
timeout: 30000,
|
2026-02-24 17:57:25 +08:00
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-25 18:21:05 +08:00
|
|
|
|
// 请求拦截器:添加 token 和语言
|
2026-02-25 15:22:12 +08:00
|
|
|
|
api.interceptors.request.use(
|
|
|
|
|
|
(config) => {
|
|
|
|
|
|
const token = localStorage.getItem('token')
|
|
|
|
|
|
if (token) {
|
2026-02-24 17:57:25 +08:00
|
|
|
|
config.headers.Authorization = `Bearer ${token}`
|
|
|
|
|
|
}
|
2026-02-25 18:21:05 +08:00
|
|
|
|
// 添加语言头
|
|
|
|
|
|
const lang = localStorage.getItem('i18nextLng') || 'zh'
|
|
|
|
|
|
config.headers['Accept-Language'] = lang
|
2026-02-24 17:57:25 +08:00
|
|
|
|
return config
|
|
|
|
|
|
},
|
2026-02-25 15:22:12 +08:00
|
|
|
|
(error) => {
|
2026-02-24 17:57:25 +08:00
|
|
|
|
return Promise.reject(error)
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-25 15:22:12 +08:00
|
|
|
|
// 响应拦截器:处理错误
|
|
|
|
|
|
api.interceptors.response.use(
|
|
|
|
|
|
(response) => {
|
2026-02-24 17:57:25 +08:00
|
|
|
|
const { data } = response
|
|
|
|
|
|
if (data.code === 0) {
|
2026-02-25 15:22:12 +08:00
|
|
|
|
return data.data
|
2026-02-24 17:57:25 +08:00
|
|
|
|
}
|
2026-02-25 15:22:12 +08:00
|
|
|
|
return Promise.reject(new Error(data.message || '请求失败'))
|
2026-02-24 17:57:25 +08:00
|
|
|
|
},
|
2026-02-28 11:12:26 +08:00
|
|
|
|
async (error) => {
|
|
|
|
|
|
const originalRequest = error.config
|
|
|
|
|
|
|
2026-02-26 15:19:37 +08:00
|
|
|
|
// 处理 401 未授权错误
|
|
|
|
|
|
if (error.response?.status === 401 || error.response?.data?.code === 401) {
|
2026-02-28 11:12:26 +08:00
|
|
|
|
// 防止死循环:如果已经重试过一次,不再重试
|
|
|
|
|
|
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('登录已过期'))
|
|
|
|
|
|
}
|
2026-02-26 15:19:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理其他错误
|
2026-02-25 15:22:12 +08:00
|
|
|
|
const message = error.response?.data?.message || error.message || '网络错误'
|
|
|
|
|
|
return Promise.reject(new Error(message))
|
2026-02-24 17:57:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-25 15:22:12 +08:00
|
|
|
|
// ========== 认证 API ==========
|
|
|
|
|
|
export const authApi = {
|
|
|
|
|
|
login: (username: string, password: string) =>
|
2026-02-28 14:58:25 +08:00
|
|
|
|
api.post<any, LoginResponse>('/auth/login', { username, password }),
|
2026-02-24 17:57:25 +08:00
|
|
|
|
|
2026-02-27 17:57:49 +08:00
|
|
|
|
// 飞书免登(支持 app_id、tenant_key 或 company)
|
|
|
|
|
|
feishuLogin: (authCode: string, company?: string) =>
|
2026-02-28 14:58:25 +08:00
|
|
|
|
api.post<any, LoginResponse>('/auth/feishu-login', {
|
2026-02-26 15:19:37 +08:00
|
|
|
|
auth_code: authCode,
|
2026-02-27 17:57:49 +08:00
|
|
|
|
company: company,
|
2026-02-26 15:19:37 +08:00
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
|
|
// 生成二维码登录会话
|
2026-02-27 17:57:49 +08:00
|
|
|
|
qrcodeLogin: (company?: string) =>
|
2026-02-28 14:58:25 +08:00
|
|
|
|
api.post<any, { session_id: string; qrcode_url: string }>('/auth/qrcode-login', {
|
2026-02-27 17:57:49 +08:00
|
|
|
|
company: company,
|
2026-02-26 15:19:37 +08:00
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
|
|
// 轮询二维码登录状态
|
|
|
|
|
|
qrcodeLoginStatus: (sessionId: string) =>
|
2026-02-28 14:58:25 +08:00
|
|
|
|
api.get<any, { status: string; token?: string; user?: any }>('/auth/qrcode-login-status', { params: { session_id: sessionId } }),
|
2026-02-25 15:22:12 +08:00
|
|
|
|
}
|
2026-02-24 17:57:25 +08:00
|
|
|
|
|
2026-02-25 15:22:12 +08:00
|
|
|
|
// ========== 用户 API ==========
|
|
|
|
|
|
export const userApi = {
|
2026-02-28 14:58:25 +08:00
|
|
|
|
getInfo: () => api.get<any, any>('/user/info'),
|
2026-02-25 15:22:12 +08:00
|
|
|
|
getMenu: () => api.get('/user/menu'),
|
|
|
|
|
|
getCompanies: () => api.get('/user/companies'),
|
|
|
|
|
|
switchCompany: (companyId: number) =>
|
|
|
|
|
|
api.post('/user/switch-company', { company_id: companyId }),
|
2026-02-24 17:57:25 +08:00
|
|
|
|
|
2026-02-25 15:22:12 +08:00
|
|
|
|
// 获取用户列表
|
|
|
|
|
|
getUsers: (params: { keyword?: string; page?: number; page_size?: number }) =>
|
2026-02-28 14:58:25 +08:00
|
|
|
|
api.get<any, PaginationResponse<any>>('/user/list', { params }),
|
2026-02-26 15:19:37 +08:00
|
|
|
|
|
|
|
|
|
|
// 修改密码
|
|
|
|
|
|
changePassword: (data: { old_password: string; new_password: string; confirm_password: string }) =>
|
|
|
|
|
|
api.post('/user/change-password', data),
|
2026-02-25 15:22:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 出差申请 API ==========
|
|
|
|
|
|
export const businessTripApi = {
|
|
|
|
|
|
getList: (params: { type?: string; page?: number; page_size?: number }) =>
|
2026-02-28 14:58:25 +08:00
|
|
|
|
api.get<any, PaginationResponse<any>>('/business-trip/list', { params }),
|
2026-02-25 15:22:12 +08:00
|
|
|
|
|
2026-02-28 14:58:25 +08:00
|
|
|
|
getDetail: (id: number) => api.get<any, any>(`/business-trip/detail/${id}`),
|
2026-02-25 15:22:12 +08:00
|
|
|
|
|
2026-02-25 18:54:38 +08:00
|
|
|
|
// 获取默认审批人和抄送人
|
2026-02-28 14:58:25 +08:00
|
|
|
|
getDefaultApprovers: () => api.get<any, { approvers: Array<{ id: number; name: string; avatar?: string }>; cc_users: Array<{ id: number; name: string; avatar?: string }> }>('/business-trip/default-approvers'),
|
2026-02-25 18:54:38 +08:00
|
|
|
|
|
2026-02-28 14:58:25 +08:00
|
|
|
|
create: (data: any) => api.post<any, { id: number }>('/business-trip/create', data),
|
2026-02-25 15:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
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}`),
|
2026-02-25 18:09:04 +08:00
|
|
|
|
|
|
|
|
|
|
deleteDraft: (id: number) => api.delete(`/business-trip/${id}`),
|
|
|
|
|
|
|
|
|
|
|
|
approve: (id: number, action: 'approve' | 'reject', comment?: string) =>
|
|
|
|
|
|
api.post(`/business-trip/approve/${id}`, { action, comment }),
|
2026-02-25 15:22:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 审批 API ==========
|
|
|
|
|
|
export const approvalApi = {
|
2026-02-26 09:12:21 +08:00
|
|
|
|
getList: (params: { type?: string; page?: number; page_size?: number }) => {
|
|
|
|
|
|
console.log('🚀 API Request approval/list:', params)
|
2026-02-28 14:58:25 +08:00
|
|
|
|
return api.get<any, PaginationResponse<any>>('/approval/list', { params })
|
|
|
|
|
|
.then((res: any) => {
|
2026-02-26 09:12:21 +08:00
|
|
|
|
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
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
2026-02-25 15:22:12 +08:00
|
|
|
|
|
2026-02-28 14:58:25 +08:00
|
|
|
|
getDetail: (id: number) => api.get<any, any>(`/approval/detail/${id}`),
|
2026-02-25 15:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
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}`),
|
2026-02-24 17:57:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 15:22:12 +08:00
|
|
|
|
export default api
|