Files
sionrui/frontend/api/axios/client.js
sion123 36195ea55a feat: 重构 IdentifyFace.vue 为 Hooks 架构
- 新增 hooks/ 目录,包含三个专用 Hook:
  * useVoiceGeneration - 语音生成和校验逻辑
  * useDigitalHumanGeneration - 数字人视频生成逻辑
  * useIdentifyFaceController - 协调两个子 Hook 的控制器

- 新增 types/identify-face.ts 完整类型定义

- 重构 IdentifyFace.vue 使用 hooks 架构:
  * 视图层与业务逻辑分离
  * 状态管理清晰化
  * 模块解耦,逻辑清晰

- 遵循单一职责原则,每个 Hook 只负责一个领域
- 提升代码可测试性和可维护性
- 支持两种视频素材来源:素材库选择和直接上传
- 实现语音生成优先校验的业务规则

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-28 00:19:17 +08:00

243 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Mono 级别的 C 端 Axios 实例
* 供 monorepo 中所有应用使用的统一 HTTP 客户端
*/
import axios from 'axios'
// 直接使用实例(最简单、最可靠)
import tokenManager from '@gold/utils/token-manager'
/**
* 不需要 token 的接口白名单
*/
const WHITE_LIST = [
'/auth/login',
'/auth/send-sms-code',
'/auth/sms-login',
'/auth/validate-sms-code',
'/auth/register',
'/auth/reset-password',
'/auth/refresh-token',
]
/**
* 检查 URL 是否在白名单中
*/
function isInWhiteList(url) {
if (!url) return false
return WHITE_LIST.some((path) => url.includes(path))
}
/**
* 自动刷新 token 的锁机制和队列
*/
let isRefreshing = false
let refreshSubscribers = []
/**
* 订阅 token 刷新完成
* @param {Function} callback - 回调函数
*/
function subscribeTokenRefresh(callback) {
if (isRefreshing) {
refreshSubscribers.push(callback)
} else {
callback()
}
}
/**
* 执行所有订阅回调
*/
function onRefreshed() {
refreshSubscribers.forEach(callback => callback())
refreshSubscribers = []
}
/**
* 处理 401 未授权错误
* 注意:只做清理工作,不处理重定向(重定向由上层回调处理)
*/
let isHandling401 = false
function handle401Error(error) {
if (isHandling401) return
isHandling401 = true
try {
// 清空token
tokenManager.clearTokens()
console.warn('Token已清空因401错误')
} catch (e) {
console.error('清空 token 失败:', e)
}
// 延迟重置标志
setTimeout(() => {
isHandling401 = false
}, 2000)
}
/**
* 创建 C 端 Axios 实例
* @param {Object} options - 配置选项
* @param {string} options.baseURL - 基础 URL
* @param {number} options.timeout - 超时时间(毫秒)
* @param {Function} options.on401 - 401 错误处理函数
* @param {Function} options.on403 - 403 错误处理函数
* @param {Function} options.refreshTokenFn - Token 刷新函数(可选)
* @returns {AxiosInstance} Axios 实例
*/
export function createClientAxios(options = {}) {
const {
baseURL = '/',
timeout = 180000,
on401 = handle401Error,
on403 = null,
refreshTokenFn = null,
} = options
const client = axios.create({
baseURL,
timeout,
})
// 请求拦截器
client.interceptors.request.use((config) => {
// 添加 tenant-id
const tenantId =
(typeof import.meta !== 'undefined' && import.meta.env?.VITE_TENANT_ID) ||
(typeof process !== 'undefined' && process.env?.VITE_TENANT_ID) ||
'1'
if (tenantId) {
config.headers['tenant-id'] = tenantId
}
// 检查是否需要认证
const needToken = config.headers?.isToken !== false && !isInWhiteList(config.url || '')
if (needToken) {
// 检查 token 是否即将过期30秒缓冲
const BUFFER_TIME = 30 * 1000
const currentToken = tokenManager.getAccessToken()
if (!currentToken) {
console.warn('[Token] 没有可用的 accessToken')
return config
}
const isTokenExpired = tokenManager.isExpired(BUFFER_TIME)
if (isTokenExpired) {
console.info('[Token] Token刷新')
// 如果不在刷新过程中,启动刷新
if (!isRefreshing) {
isRefreshing = true
// 执行刷新(使用上层传入的刷新函数)
if (refreshTokenFn && typeof refreshTokenFn === 'function') {
refreshTokenFn()
.then(() => {
console.info('[Token] 刷新成功')
isRefreshing = false
onRefreshed()
})
.catch((error) => {
isRefreshing = false
onRefreshed()
console.error('[Token] 刷新失败:', error.message)
})
} else {
console.warn('[Token] 未提供刷新函数,跳过刷新')
isRefreshing = false
onRefreshed()
}
}
// 等待刷新完成
return new Promise((resolve) => {
subscribeTokenRefresh(() => {
// 刷新完成后,重新获取 token 并添加到请求头
const authHeader = tokenManager.getAuthHeader()
if (authHeader) {
config.headers.Authorization = authHeader
}
resolve(config)
})
})
} else {
// Token 未过期,直接添加 Authorization 头
const authHeader = tokenManager.getAuthHeader()
if (authHeader) {
config.headers.Authorization = authHeader
}
}
}
return config
})
// 响应拦截器
client.interceptors.response.use(
(response) => {
const data = response.data
// 检查业务状态码
if (data && typeof data.code === 'number') {
if (data.code === 0 || data.code === 200) {
return data
}
// 创建业务错误对象
const error = new Error(data?.message || data?.msg || '请求失败')
error.code = data?.code
error.data = data
// 业务码 401/403 只在响应拦截器处理,避免重复处理
if (data.code === 401 && typeof on401 === 'function') {
on401(error)
}
if (data.code === 403 && typeof on403 === 'function') {
// 使用通用的权限不足错误,不使用后端返回的 message
const forbiddenError = new Error('权限不足,无法访问该资源')
forbiddenError.code = 403
on403(forbiddenError)
}
// 抛出错误,业务代码可以捕获
return Promise.reject(error)
}
return data
},
(error) => {
// HTTP 状态码 401/403 只在错误拦截器处理
if (error.response?.status === 401 && typeof on401 === 'function') {
on401(error)
}
if (error.response?.status === 403 && typeof on403 === 'function') {
const forbiddenError = new Error('权限不足,无法访问该资源')
forbiddenError.code = 403
on403(forbiddenError)
}
return Promise.reject(error)
}
)
return client
}
/**
* 默认导出的 C 端 Axios 实例
* 可在应用层覆盖配置
*/
export const clientAxios = createClientAxios()
export default clientAxios