1367 lines
46 KiB
TypeScript
Executable File
1367 lines
46 KiB
TypeScript
Executable File
// API 工具类 - 封装网络请求和认证逻辑
|
||
import {
|
||
IAppOption,
|
||
ILoginResponse,
|
||
IApiResponse,
|
||
IRecognitionResult,
|
||
ExtendedWordDetail,
|
||
IAuditHistoryResponse,
|
||
IUserInfo,
|
||
IDailySummaryResponse
|
||
} from '../types/app';
|
||
import { BASE_URL } from './config';
|
||
|
||
// 音频缓存映射
|
||
const audioCache: Map<string, string> = new Map()
|
||
// 图片缓存映射
|
||
const imageCache: Map<string, string> = new Map()
|
||
|
||
// 缓存大小统计(以字节为单位)
|
||
const cacheStats: Map<string, number> = new Map()
|
||
|
||
// 在类外部添加初始化缓存的方法
|
||
function initPersistentCache() {
|
||
try {
|
||
console.log('开始初始化持久化缓存')
|
||
const startTime = Date.now()
|
||
|
||
const fs = wx.getFileSystemManager()
|
||
const audioCacheDir = `${wx.env.USER_DATA_PATH}/audio_cache`
|
||
|
||
// 检查音频缓存目录是否存在
|
||
fs.accessSync(audioCacheDir)
|
||
|
||
// 递归遍历目录并重建缓存映射
|
||
traverseDirectory(audioCacheDir, '', fs)
|
||
|
||
const endTime = Date.now()
|
||
console.log('持久化缓存初始化完成,耗时:', endTime - startTime, 'ms,音频文件数量:', audioCache.size)
|
||
} catch (e) {
|
||
// 目录不存在,这是正常的,不需要处理
|
||
console.log('音频缓存目录不存在,无需初始化')
|
||
}
|
||
}
|
||
|
||
// 递归遍历目录
|
||
function traverseDirectory(baseDir: string, relativePath: string, fs: any) {
|
||
try {
|
||
const currentPath = relativePath ? `${baseDir}/${relativePath}` : baseDir
|
||
|
||
// 限制遍历深度,避免过深的目录结构
|
||
const pathDepth = relativePath.split('/').filter(p => p.length > 0).length
|
||
if (pathDepth > 5) {
|
||
console.warn('目录深度超过限制,跳过:', relativePath)
|
||
return
|
||
}
|
||
|
||
const items = fs.readdirSync(currentPath)
|
||
|
||
// 限制文件数量,避免过多文件导致性能问题
|
||
const maxItems = 100
|
||
if (items.length > maxItems) {
|
||
console.warn('目录文件数量超过限制,仅处理前', maxItems, '个文件')
|
||
items.splice(maxItems)
|
||
}
|
||
|
||
items.forEach((item: string) => {
|
||
const itemRelativePath = relativePath ? `${relativePath}/${item}` : item
|
||
const itemFullPath = `${baseDir}/${itemRelativePath}`
|
||
|
||
try {
|
||
const stat = fs.statSync(itemFullPath)
|
||
if (stat.isFile()) {
|
||
// 限制文件大小,避免处理过大的文件
|
||
const maxSize = 10 * 1024 * 1024 // 10MB
|
||
if (stat.size > maxSize) {
|
||
console.warn('文件过大,跳过:', itemRelativePath, '大小:', stat.size)
|
||
return
|
||
}
|
||
|
||
// 将文件添加到缓存映射中
|
||
audioCache.set(itemRelativePath, itemFullPath)
|
||
|
||
// 获取文件大小并更新统计
|
||
const fileSize = stat.size
|
||
cacheStats.set(`audio_${itemRelativePath}`, fileSize)
|
||
console.log(`初始化缓存文件: ${itemRelativePath}, 大小: ${fileSize} bytes`)
|
||
} else if (stat.isDirectory()) {
|
||
// 递归处理子目录
|
||
traverseDirectory(baseDir, itemRelativePath, fs)
|
||
}
|
||
} catch (err) {
|
||
// 单个文件处理出错不影响其他文件
|
||
console.warn(`处理项目 ${itemRelativePath} 时出错:`, err)
|
||
}
|
||
})
|
||
} catch (err) {
|
||
// 如果目录不存在或无法读取,记录日志但不中断
|
||
if (relativePath === '') {
|
||
console.log(`无法访问音频缓存目录:`, err)
|
||
} else {
|
||
console.warn(`遍历目录 ${relativePath} 时出错:`, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 异步初始化缓存,避免阻塞主线程
|
||
function initPersistentCacheAsync() {
|
||
// 使用 setTimeout 将初始化操作推迟到下一个事件循环
|
||
setTimeout(() => {
|
||
try {
|
||
initPersistentCache()
|
||
} catch (e) {
|
||
console.error('初始化持久化缓存时出错:', e)
|
||
}
|
||
}, 0)
|
||
}
|
||
|
||
// 在文件末尾调用异步初始化方法
|
||
initPersistentCacheAsync()
|
||
|
||
// 词典等级映射
|
||
const DICT_LEVEL_OPTIONS = {
|
||
LEVEL1: "初级",
|
||
LEVEL2: "中级",
|
||
LEVEL3: "高级"
|
||
}
|
||
|
||
class ApiManager {
|
||
private loginPromise: Promise<ILoginResponse | null> | null = null // 防止重复登录
|
||
private isAudioPlaying: boolean = false // 音频播放状态标志
|
||
|
||
// 获取请求头
|
||
private getHeaders(): Record<string, string> {
|
||
const app = getApp<IAppOption>()
|
||
const headers: Record<string, string> = {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
|
||
if (app.globalData.token) {
|
||
headers['Authorization'] = `Bearer ${app.globalData.token}`
|
||
}
|
||
|
||
return headers
|
||
}
|
||
|
||
// 专用于登录的请求方法(不处理401重试)
|
||
private loginRequest<T>(
|
||
url: string,
|
||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
||
data?: any,
|
||
showLoading: boolean = true
|
||
): Promise<IApiResponse<T>> {
|
||
return new Promise((resolve, reject) => {
|
||
if (showLoading) {
|
||
wx.showLoading({ title: '登录中...' })
|
||
}
|
||
|
||
console.log('发起登录请求:', {
|
||
url: `${BASE_URL}${url}`,
|
||
method,
|
||
hasData: !!data
|
||
})
|
||
|
||
wx.request({
|
||
url: `${BASE_URL}${url}`,
|
||
method,
|
||
data,
|
||
header: {
|
||
'Content-Type': 'application/json'
|
||
// 登录请求不需要Authorization头
|
||
},
|
||
success: (res) => {
|
||
if (showLoading) {
|
||
wx.hideLoading()
|
||
}
|
||
|
||
console.log('登录请求响应:', {
|
||
statusCode: res.statusCode,
|
||
hasData: !!res.data,
|
||
success: true
|
||
})
|
||
|
||
if (res.statusCode === 200) {
|
||
const response = res.data as IApiResponse<T>
|
||
if (response.code === 0 || response.code === 200) {
|
||
resolve(response)
|
||
} else {
|
||
const errorMsg = response.message || response.msg || '登录失败'
|
||
console.error('登录业务错误:', errorMsg, response)
|
||
wx.showToast({
|
||
title: errorMsg,
|
||
icon: 'none'
|
||
})
|
||
reject(new Error(errorMsg))
|
||
}
|
||
} else {
|
||
console.error('登录HTTP错误:', res.statusCode, res.data)
|
||
let errorMsg = '登录失败'
|
||
if (res.statusCode === 401) {
|
||
errorMsg = '登录凭据无效'
|
||
} else if (res.statusCode >= 500) {
|
||
errorMsg = '服务器错误,请稍后重试'
|
||
}
|
||
wx.showToast({
|
||
title: errorMsg,
|
||
icon: 'none'
|
||
})
|
||
reject(new Error(`HTTP ${res.statusCode}`))
|
||
}
|
||
},
|
||
fail: (error) => {
|
||
if (showLoading) {
|
||
wx.hideLoading()
|
||
}
|
||
|
||
console.error('登录请求失败:', error)
|
||
wx.showToast({
|
||
title: '网络连接失败',
|
||
icon: 'none'
|
||
})
|
||
reject(new Error('网络连接失败: ' + JSON.stringify(error)))
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
// 通用请求方法(支持401自动重试登录)
|
||
private async request<T>(
|
||
url: string,
|
||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
||
data?: any,
|
||
showLoading: boolean = true,
|
||
retryCount: number = 0
|
||
): Promise<IApiResponse<T>> {
|
||
const maxRetries = 1 // 最多重试1次(首次请求 + 1次重试)
|
||
|
||
return new Promise(async (resolve, reject) => {
|
||
if (showLoading && retryCount === 0) {
|
||
// wx.showLoading({ title: '加载中...' })
|
||
}
|
||
|
||
console.log('发起请求:', {
|
||
url: `${BASE_URL}${url}`,
|
||
method,
|
||
hasData: !!data,
|
||
hasHeaders: !!this.getHeaders(),
|
||
retryCount
|
||
})
|
||
|
||
wx.request({
|
||
url: `${BASE_URL}${url}`,
|
||
method,
|
||
data,
|
||
header: this.getHeaders(),
|
||
success: async (res) => {
|
||
if (showLoading) {
|
||
wx.hideLoading()
|
||
}
|
||
|
||
console.log('请求响应:', {
|
||
statusCode: res.statusCode,
|
||
hasData: !!res.data,
|
||
retryCount,
|
||
success: true
|
||
})
|
||
|
||
if (res.statusCode === 200) {
|
||
const response = res.data as IApiResponse<T>
|
||
if (response.code === 0 || response.code === 200) {
|
||
resolve(response)
|
||
} else {
|
||
// 业务错误
|
||
const errorMsg = response.message || response.msg || '请求失败'
|
||
console.error('业务错误:', errorMsg, response)
|
||
wx.showToast({
|
||
title: errorMsg,
|
||
icon: 'none'
|
||
})
|
||
reject(new Error(errorMsg))
|
||
}
|
||
} else if (res.statusCode === 401) {
|
||
console.warn('401认证失败,尝试自动登录', { retryCount, maxRetries })
|
||
|
||
// 如果还有重试机会,尝试自动登录并重试请求
|
||
if (retryCount < maxRetries) {
|
||
try {
|
||
console.log('开始401自动登录重试')
|
||
|
||
// 清理过期的认证数据
|
||
this.clearAuthData()
|
||
|
||
// 进行智能登录
|
||
const loginResult = await this.smartLogin(true) // 强制刷新token
|
||
|
||
if (loginResult) {
|
||
console.log('401自动登录成功,重试请求')
|
||
// 登录成功后重试原请求
|
||
try {
|
||
const retryResult = await this.request<T>(url, method, data, false, retryCount + 1)
|
||
resolve(retryResult)
|
||
} catch (retryError) {
|
||
reject(retryError)
|
||
}
|
||
} else {
|
||
console.error('401自动登录失败')
|
||
this.handleTokenExpired()
|
||
reject(new Error('登录失败,请重新登录'))
|
||
}
|
||
} catch (loginError) {
|
||
console.error('401自动登录发生错误:', loginError)
|
||
this.handleTokenExpired()
|
||
reject(new Error('登录失败,请重新登录'))
|
||
}
|
||
} else {
|
||
// 重试次数耗尽,直接处理token过期
|
||
console.error('401重试次数耗尽,跳转登录页')
|
||
this.handleTokenExpired()
|
||
reject(new Error('登录已过期'))
|
||
}
|
||
} else {
|
||
console.error('HTTP错误:', res.statusCode, res.data)
|
||
wx.showToast({
|
||
title: `网络请求失败 (${res.statusCode})`,
|
||
icon: 'none'
|
||
})
|
||
reject(new Error(`HTTP ${res.statusCode}`))
|
||
}
|
||
},
|
||
fail: (error) => {
|
||
if (showLoading) {
|
||
wx.hideLoading()
|
||
}
|
||
|
||
console.error('请求失败:', error)
|
||
wx.showToast({
|
||
title: '网络连接失败',
|
||
icon: 'none'
|
||
})
|
||
reject(new Error('网络连接失败: ' + JSON.stringify(error)))
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
// 处理 token 过期
|
||
private handleTokenExpired() {
|
||
console.log('处理token过期,清理状态并跳转登录页')
|
||
|
||
const app = getApp<IAppOption>()
|
||
// 清理全局状态
|
||
app.globalData.isLoggedIn = false
|
||
app.globalData.token = undefined
|
||
app.globalData.userInfo = undefined
|
||
app.globalData.dictLevel = undefined
|
||
|
||
// 清理本地存储
|
||
wx.removeStorageSync('token')
|
||
wx.removeStorageSync('userInfo')
|
||
wx.removeStorageSync('tokenExpiry')
|
||
wx.removeStorageSync('sessionUuid')
|
||
wx.removeStorageSync('dictLevel')
|
||
|
||
// 优化用户提示,不要太突充
|
||
console.log('登录状态已过期,即将跳转到登录页')
|
||
|
||
// 立即跳转到登录页,减少等待时间
|
||
wx.reLaunch({
|
||
url: '/pages/index/index'
|
||
})
|
||
}
|
||
|
||
// 智能登录(优先检查本地token)
|
||
async smartLogin(forceRefresh: boolean = false): Promise<ILoginResponse | null> {
|
||
// 如果已经在登录中,返回已有的Promise
|
||
if (this.loginPromise && !forceRefresh) {
|
||
console.log('登录正在进行中,等待现有登录完成')
|
||
return this.loginPromise
|
||
}
|
||
|
||
const app = getApp<IAppOption>()
|
||
console.log('开始智能登录,强制刷新:', forceRefresh)
|
||
|
||
// 如果不是强制刷新,检查本地存储中的token和过期时间
|
||
if (!forceRefresh) {
|
||
const authInfo = this.getStoredAuthInfo()
|
||
console.log('本地认证信息:', {
|
||
hasToken: !!authInfo?.token,
|
||
hasExpiry: !!authInfo?.tokenExpiry,
|
||
tokenLength: authInfo?.token?.length || 0
|
||
})
|
||
|
||
if (authInfo && authInfo.token && authInfo.tokenExpiry) {
|
||
// 检查token是否过期
|
||
const isExpired = this.isTokenExpired()
|
||
if (!isExpired) {
|
||
console.log('Token未过期,使用本地token')
|
||
|
||
// 更新全局状态
|
||
app.globalData.isLoggedIn = true
|
||
app.globalData.token = authInfo.token
|
||
app.globalData.dictLevel = authInfo.dictLevel || 'PRIMARY'
|
||
|
||
// 返回本地存储的登录信息
|
||
return {
|
||
access_token: authInfo.token,
|
||
access_token_expire_time: new Date(authInfo.tokenExpiry).toISOString(),
|
||
session_uuid: authInfo.sessionUuid || '',
|
||
dict_level: authInfo.dictLevel || 'PRIMARY'
|
||
}
|
||
} else {
|
||
console.log('Token已过期,需要重新登录')
|
||
// 清理过期的token
|
||
this.clearAuthData()
|
||
}
|
||
} else {
|
||
console.log('未找到本地token或过期时间,需要登录')
|
||
}
|
||
} else {
|
||
console.log('强制刷新token,重新登录')
|
||
// 清理现有数据
|
||
this.clearAuthData()
|
||
}
|
||
|
||
// Token过期或不存在,进行新的登录
|
||
console.log('发起新的登录请求')
|
||
|
||
// 创建新的登录Promise
|
||
this.loginPromise = this.login().then(
|
||
(result) => {
|
||
console.log('智能登录成功')
|
||
this.loginPromise = null // 清理Promise
|
||
return result
|
||
},
|
||
(error) => {
|
||
console.error('智能登录失败:', error)
|
||
this.loginPromise = null // 清理Promise
|
||
throw error
|
||
}
|
||
)
|
||
|
||
return this.loginPromise
|
||
}
|
||
|
||
// 清理认证数据
|
||
private clearAuthData() {
|
||
const app = getApp<IAppOption>()
|
||
|
||
// 清理全局状态
|
||
app.globalData.isLoggedIn = false
|
||
app.globalData.token = undefined
|
||
app.globalData.userInfo = undefined
|
||
app.globalData.dictLevel = undefined
|
||
|
||
// 清理本地存储
|
||
wx.removeStorageSync('token')
|
||
wx.removeStorageSync('userInfo')
|
||
wx.removeStorageSync('tokenExpiry')
|
||
wx.removeStorageSync('sessionUuid')
|
||
wx.removeStorageSync('dictLevel')
|
||
}
|
||
|
||
// 用户登录(带重试机制)
|
||
async login(retryCount: number = 0): Promise<ILoginResponse> {
|
||
const maxRetries = 2 // 减少重试次数,接口正常后不需要太多重试
|
||
|
||
return new Promise((resolve, reject) => {
|
||
if (retryCount > 0) {
|
||
console.log(`登录重试 ${retryCount}/${maxRetries + 1}`)
|
||
}
|
||
|
||
wx.login({
|
||
success: async (loginRes) => {
|
||
if (loginRes.code) {
|
||
try {
|
||
const response = await this.loginRequest<ILoginResponse>('/api/v1/wx/login', 'POST', {
|
||
code: loginRes.code
|
||
})
|
||
|
||
// 处理新的数据结构
|
||
const loginData = response.data
|
||
|
||
if (!loginData.access_token) {
|
||
throw new Error('服务器返回数据中缺少access_token')
|
||
}
|
||
|
||
// 存储 access_token
|
||
wx.setStorageSync('token', loginData.access_token)
|
||
|
||
// 处理过期时间,将字符串时间转换为时间戳
|
||
if (loginData.access_token_expire_time) {
|
||
const expireTime = new Date(loginData.access_token_expire_time).getTime()
|
||
if (isNaN(expireTime)) {
|
||
console.warn('过期时间格式错误,使用默认值')
|
||
const defaultExpiry = Date.now() + 30 * 24 * 60 * 60 * 1000
|
||
wx.setStorageSync('tokenExpiry', defaultExpiry)
|
||
} else {
|
||
wx.setStorageSync('tokenExpiry', expireTime)
|
||
}
|
||
} else {
|
||
const defaultExpiry = Date.now() + 30 * 24 * 60 * 60 * 1000
|
||
wx.setStorageSync('tokenExpiry', defaultExpiry)
|
||
}
|
||
|
||
// 存储 session_uuid
|
||
if (loginData.session_uuid) {
|
||
wx.setStorageSync('sessionUuid', loginData.session_uuid)
|
||
}
|
||
|
||
// 存储 dict_level
|
||
if (loginData.dict_level) {
|
||
wx.setStorageSync('dictLevel', loginData.dict_level)
|
||
// 更新全局状态
|
||
const app = getApp<IAppOption>()
|
||
app.globalData.dictLevel = loginData.dict_level
|
||
}
|
||
|
||
console.log('登录成功,数据存储完成')
|
||
resolve(loginData)
|
||
} catch (error: any) {
|
||
console.error(`登录请求失败:`, error.message)
|
||
|
||
// 如果还有重试次数,尝试重试
|
||
if (retryCount < maxRetries) {
|
||
setTimeout(async () => {
|
||
try {
|
||
const result = await this.login(retryCount + 1)
|
||
resolve(result)
|
||
} catch (retryError) {
|
||
reject(retryError)
|
||
}
|
||
}, 1500) // 减少重试间隔
|
||
} else {
|
||
reject(error)
|
||
}
|
||
}
|
||
} else {
|
||
const error = new Error('微信登录失败:未能获取到登录code')
|
||
|
||
if (retryCount < maxRetries) {
|
||
setTimeout(async () => {
|
||
try {
|
||
const result = await this.login(retryCount + 1)
|
||
resolve(result)
|
||
} catch (retryError) {
|
||
reject(retryError)
|
||
}
|
||
}, 1000)
|
||
} else {
|
||
reject(error)
|
||
}
|
||
}
|
||
},
|
||
fail: (error) => {
|
||
console.error('微信登录接口调用失败:', error)
|
||
|
||
if (retryCount < maxRetries) {
|
||
setTimeout(async () => {
|
||
try {
|
||
const result = await this.login(retryCount + 1)
|
||
resolve(result)
|
||
} catch (retryError) {
|
||
reject(retryError)
|
||
}
|
||
}, 1000)
|
||
} else {
|
||
reject(new Error('微信登录接口调用失败: ' + JSON.stringify(error)))
|
||
}
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
// 上传文件(第一步:上传文件获取ID)
|
||
async uploadFile(filePath: string, retryCount: number = 0): Promise<string> {
|
||
const maxRetries = 1 // 最多重试1次
|
||
|
||
return new Promise(async (resolve, reject) => {
|
||
console.log('上传文件请求:', { filePath, retryCount })
|
||
|
||
wx.uploadFile({
|
||
url: `${BASE_URL}/api/v1/file/upload`,
|
||
filePath,
|
||
name: 'file', // 根据API要求调整参数名
|
||
header: this.getHeaders(),
|
||
success: async (res) => {
|
||
console.log('上传文件响应:', {
|
||
statusCode: res.statusCode,
|
||
data: res.data,
|
||
retryCount
|
||
})
|
||
|
||
if (res.statusCode === 200) {
|
||
try {
|
||
const response = JSON.parse(res.data) as IApiResponse<{ id: string }>
|
||
if (response.code === 0 || response.code === 200) {
|
||
if (response.data?.id) {
|
||
console.log('文件上传成功,获得ID:', response.data.id)
|
||
resolve(response.data.id)
|
||
} else {
|
||
reject(new Error('服务器返回数据中缺少文件ID'))
|
||
}
|
||
} else {
|
||
const errorMsg = response.message || response.msg || '文件上传失败'
|
||
console.error('文件上传业务错误:', errorMsg, response)
|
||
wx.showToast({
|
||
title: errorMsg,
|
||
icon: 'none'
|
||
})
|
||
reject(new Error(errorMsg))
|
||
}
|
||
} catch (error) {
|
||
console.error('文件上传响应解析错误:', error)
|
||
wx.showToast({
|
||
title: '数据解析错误',
|
||
icon: 'none'
|
||
})
|
||
reject(error)
|
||
}
|
||
} else if (res.statusCode === 401) {
|
||
console.warn('文件上传401认证失败,尝试自动登录', { retryCount, maxRetries })
|
||
|
||
// 如果还有重试机会,尝试自动登录并重试请求
|
||
if (retryCount < maxRetries) {
|
||
try {
|
||
console.log('开始文件上传401自动登录重试')
|
||
|
||
// 清理过期的认证数据
|
||
this.clearAuthData()
|
||
|
||
// 进行智能登录
|
||
const loginResult = await this.smartLogin(true) // 强制刷新token
|
||
|
||
if (loginResult) {
|
||
console.log('文件上传401自动登录成功,重试请求')
|
||
// 登录成功后重试原请求
|
||
try {
|
||
const retryResult = await this.uploadFile(filePath, retryCount + 1)
|
||
resolve(retryResult)
|
||
} catch (retryError) {
|
||
reject(retryError)
|
||
}
|
||
} else {
|
||
console.error('文件上传401自动登录失败')
|
||
this.handleTokenExpired()
|
||
reject(new Error('登录已过期'))
|
||
}
|
||
} catch (loginError) {
|
||
console.error('文件上传401自动登录发生错误:', loginError)
|
||
this.handleTokenExpired()
|
||
reject(new Error('登录已过期'))
|
||
}
|
||
} else {
|
||
// 重试次数耗尽,直接处理token过期
|
||
console.error('文件上传401重试次数耗尽,跳转登录页')
|
||
this.handleTokenExpired()
|
||
reject(new Error('登录已过期'))
|
||
}
|
||
} else {
|
||
// Check for 413 Request Entity Too Large error
|
||
if (res.statusCode === 413) {
|
||
// Get file size for debugging
|
||
const fs = wx.getFileSystemManager();
|
||
fs.getFileInfo({
|
||
filePath: filePath,
|
||
success: (infoRes) => {
|
||
const fileSizeInMB = (infoRes.size / (1024 * 1024)).toFixed(4);
|
||
console.error('文件上传413错误 - 请求实体过大:', {
|
||
statusCode: res.statusCode,
|
||
fileSize: `${fileSizeInMB} MB`,
|
||
fileSizeInBytes: infoRes.size,
|
||
filePath: filePath,
|
||
data: res.data
|
||
});
|
||
},
|
||
fail: (infoError) => {
|
||
console.error('文件上传413错误 - 无法获取文件大小:', {
|
||
statusCode: res.statusCode,
|
||
filePath: filePath,
|
||
fileInfoError: infoError,
|
||
data: res.data
|
||
});
|
||
}
|
||
});
|
||
} else {
|
||
console.error('文件上传HTTP错误:', res.statusCode, res.data);
|
||
}
|
||
|
||
wx.showToast({
|
||
title: '文件上传失败',
|
||
icon: 'none'
|
||
});
|
||
reject(new Error(`HTTP ${res.statusCode}`));
|
||
}
|
||
},
|
||
fail: (error) => {
|
||
console.error('文件上传请求失败:', error)
|
||
wx.showToast({
|
||
title: '文件上传失败',
|
||
icon: 'none'
|
||
})
|
||
reject(error)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
// 图片识别(第二步:通过文件ID进行识别)
|
||
private async recognizeImage(fileId: string, type: string = 'word'): Promise<IRecognitionResult> {
|
||
console.log('开始图片识别请求:', { fileId, type })
|
||
|
||
// 获取当前的词典等级配置
|
||
const app = getApp<IAppOption>()
|
||
const dictLevel = app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'PRIMARY'
|
||
|
||
const response = await this.request<IRecognitionResult>('/api/v1/image/recognize', 'POST', {
|
||
file_id: fileId,
|
||
type: type,
|
||
dict_level: dictLevel // 添加词典等级参数
|
||
})
|
||
|
||
console.log('图片识别成功:', response.data)
|
||
return response.data
|
||
}
|
||
|
||
// 获取图片识别结果
|
||
async getRecognizeImage(fileId: string): Promise<IRecognitionResult & { file_id: string }> {
|
||
|
||
const app = getApp<IAppOption>()
|
||
const dictLevel = app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'PRIMARY'
|
||
|
||
const response = await this.request<IRecognitionResult>(`/api/v1/image/${fileId}`, 'GET', {
|
||
dict_level: dictLevel
|
||
})
|
||
|
||
console.log('图片识别结果获取成功:', response.data)
|
||
return response.data
|
||
}
|
||
|
||
// 上传图片并识别(对外接口,整合两个步骤)
|
||
async uploadImage(filePath: string, type: string = 'word'): Promise<IRecognitionResult> {
|
||
try {
|
||
// wx.showLoading({ title: '上传中...' })
|
||
|
||
console.log('开始图片上传和识别流程:', { filePath, type })
|
||
|
||
// 第一步:上传文件获取ID
|
||
const fileId = await this.uploadFile(filePath)
|
||
|
||
// 更新加载提示
|
||
// wx.showLoading({ title: '识别中...' })
|
||
|
||
// 第二步:通过文件ID进行图片识别
|
||
const recognitionResult = await this.recognizeImage(fileId, type)
|
||
|
||
// wx.hideLoading()
|
||
console.log('图片上传和识别流程完成')
|
||
|
||
return recognitionResult
|
||
|
||
} catch (error) {
|
||
// wx.hideLoading()
|
||
console.error('图片上传和识别流程失败:', error)
|
||
|
||
// 根据错误类型提供更具体的错误信息
|
||
if (error instanceof Error) {
|
||
if (error.message.includes('登录')) {
|
||
// 登录相关错误,不需要额外处理,已经在内部处理了
|
||
throw error
|
||
} else {
|
||
wx.showToast({
|
||
title: error.message || '处理失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
} else {
|
||
wx.showToast({
|
||
title: '处理失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
|
||
throw error
|
||
}
|
||
}
|
||
|
||
// 获取单词详情
|
||
async getWordDetail(word: string): Promise<ExtendedWordDetail> {
|
||
console.log('获取单词详情')
|
||
const response = await this.request<ExtendedWordDetail>(`/api/v1/dict/word/${encodeURIComponent(word)}`)
|
||
console.log('获取单词详情成功:', response)
|
||
return response.data
|
||
}
|
||
|
||
// 播放音频文件
|
||
async playAudio(fileName: string): Promise<void> {
|
||
// 检查是否有音频正在播放
|
||
if (this.isAudioPlaying) {
|
||
console.log('音频正在播放中,跳过本次播放请求')
|
||
return Promise.reject(new Error('音频正在播放中,请等待播放完成'))
|
||
}
|
||
|
||
return new Promise((resolve: () => void, reject: (reason: any) => void) => {
|
||
console.log('播放音频文件:', fileName)
|
||
|
||
// 设置音频播放状态为true
|
||
this.isAudioPlaying = true
|
||
|
||
// 首先检查缓存中是否已有该音频文件
|
||
if (audioCache.has(fileName)) {
|
||
const cachedFilePath = audioCache.get(fileName)!
|
||
console.log('使用缓存的音频文件:', cachedFilePath)
|
||
|
||
// 获取缓存文件大小并更新统计(如果还没有统计信息)
|
||
if (!cacheStats.has(`audio_${fileName}`)) {
|
||
const fs = wx.getFileSystemManager()
|
||
fs.getFileInfo({
|
||
filePath: cachedFilePath,
|
||
success: (infoRes) => {
|
||
const fileSize = infoRes.size
|
||
cacheStats.set(`audio_${fileName}`, fileSize)
|
||
console.log(`缓存音频文件大小: ${fileSize} bytes`)
|
||
},
|
||
fail: (err) => {
|
||
console.error('获取缓存音频文件大小失败:', err)
|
||
}
|
||
})
|
||
}
|
||
|
||
// 直接播放缓存的音频文件
|
||
this.playAudioFromFile(cachedFilePath)
|
||
.then(() => {
|
||
this.isAudioPlaying = false // 播放完成后重置状态
|
||
resolve()
|
||
})
|
||
.catch((error) => {
|
||
this.isAudioPlaying = false // 播放失败后重置状态
|
||
reject(error)
|
||
})
|
||
return
|
||
}
|
||
|
||
// 构建音频文件的完整URL
|
||
const audioUrl = `${BASE_URL}/api/v1/dict/audio/${encodeURIComponent(fileName)}`
|
||
|
||
// 获取认证头
|
||
const headers = this.getHeaders()
|
||
|
||
// 先下载音频文件到本地
|
||
wx.downloadFile({
|
||
url: audioUrl,
|
||
header: headers,
|
||
success: (res) => {
|
||
if (res.statusCode === 200) {
|
||
// 确保音频缓存目录存在
|
||
const fs = wx.getFileSystemManager()
|
||
const audioCacheDir = `${wx.env.USER_DATA_PATH}/audio_cache`
|
||
|
||
try {
|
||
// 检查目录是否存在,不存在则创建
|
||
fs.accessSync(audioCacheDir)
|
||
} catch (dirError) {
|
||
// 目录不存在,创建目录(包括父级目录)
|
||
try {
|
||
fs.mkdirSync(audioCacheDir, true)
|
||
console.log('音频缓存目录创建成功:', audioCacheDir)
|
||
} catch (mkdirError) {
|
||
console.error('创建音频缓存目录失败:', mkdirError)
|
||
// 如果目录创建失败,仍然使用临时文件
|
||
this.handleAudioFileSaveFailure(res.tempFilePath, fileName, resolve, reject)
|
||
return
|
||
}
|
||
}
|
||
|
||
// 构建目标文件路径
|
||
const persistentFilePath = `${audioCacheDir}/${fileName}`
|
||
|
||
// 确保目标文件的父目录也存在(处理嵌套目录的情况)
|
||
const lastSlashIndex = fileName.lastIndexOf('/')
|
||
if (lastSlashIndex > 0) {
|
||
const subDir = fileName.substring(0, lastSlashIndex)
|
||
const fullSubDir = `${audioCacheDir}/${subDir}`
|
||
|
||
try {
|
||
fs.accessSync(fullSubDir)
|
||
} catch (subDirError) {
|
||
// 子目录不存在,创建子目录
|
||
try {
|
||
fs.mkdirSync(fullSubDir, true)
|
||
console.log('音频缓存子目录创建成功:', fullSubDir)
|
||
} catch (subMkdirError) {
|
||
console.error('创建音频缓存子目录失败:', subMkdirError)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 将临时文件移动到持久化存储目录
|
||
fs.saveFile({
|
||
tempFilePath: res.tempFilePath,
|
||
filePath: persistentFilePath,
|
||
success: (saveRes: any) => {
|
||
// 将文件路径缓存起来
|
||
audioCache.set(fileName, persistentFilePath)
|
||
console.log('音频文件保存成功并已缓存:', persistentFilePath)
|
||
|
||
// 获取文件大小并更新统计
|
||
fs.getFileInfo({
|
||
filePath: persistentFilePath,
|
||
success: (infoRes) => {
|
||
const fileSize = infoRes.size
|
||
cacheStats.set(`audio_${fileName}`, fileSize)
|
||
console.log(`音频文件大小: ${fileSize} bytes`)
|
||
},
|
||
fail: (err) => {
|
||
console.error('获取音频文件大小失败:', err)
|
||
}
|
||
})
|
||
|
||
// 播放音频
|
||
this.playAudioFromFile(persistentFilePath)
|
||
.then(() => {
|
||
this.isAudioPlaying = false // 播放完成后重置状态
|
||
resolve()
|
||
})
|
||
.catch((error) => {
|
||
this.isAudioPlaying = false // 播放失败后重置状态
|
||
reject(error)
|
||
})
|
||
},
|
||
fail: (saveError) => {
|
||
console.error('保存音频文件失败:', saveError)
|
||
this.handleAudioFileSaveFailure(res.tempFilePath, fileName, resolve, reject)
|
||
}
|
||
})
|
||
} else {
|
||
console.error('下载音频文件失败,状态码:', res.statusCode)
|
||
this.isAudioPlaying = false // 下载失败后重置状态
|
||
reject(new Error(`下载音频文件失败,状态码: ${res.statusCode}`))
|
||
}
|
||
},
|
||
fail: (error) => {
|
||
console.error('下载音频文件失败:', error)
|
||
this.isAudioPlaying = false // 下载失败后重置状态
|
||
reject(error)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
// 从本地文件播放音频
|
||
private playAudioFromFile(filePath: string): Promise<void> {
|
||
return new Promise((resolve, reject) => {
|
||
// 使用微信的音频播放API
|
||
const innerAudioContext = wx.createInnerAudioContext()
|
||
|
||
// 设置音频源
|
||
innerAudioContext.src = filePath
|
||
|
||
// 监听播放事件
|
||
innerAudioContext.onPlay(() => {
|
||
console.log('开始播放音频')
|
||
resolve()
|
||
})
|
||
|
||
innerAudioContext.onError((res) => {
|
||
console.error('音频播放错误:', res.errMsg)
|
||
console.error('错误码:', res.errCode)
|
||
this.isAudioPlaying = false // 播放错误后重置状态
|
||
reject(new Error(res.errMsg))
|
||
})
|
||
|
||
// 播放完成后清理资源并重置状态
|
||
innerAudioContext.onEnded(() => {
|
||
this.isAudioPlaying = false // 播放完成后重置状态
|
||
innerAudioContext.destroy()
|
||
})
|
||
|
||
// 开始播放
|
||
innerAudioContext.play()
|
||
})
|
||
}
|
||
|
||
// 处理音频文件保存失败的情况
|
||
private handleAudioFileSaveFailure(tempFilePath: string, fileName: string, resolve: (value: void | PromiseLike<void>) => void, reject: (reason?: any) => void) {
|
||
console.log('使用临时文件播放音频')
|
||
|
||
// 将临时文件路径缓存起来
|
||
audioCache.set(fileName, tempFilePath)
|
||
|
||
// 获取文件大小并更新统计
|
||
const fs = wx.getFileSystemManager()
|
||
fs.getFileInfo({
|
||
filePath: tempFilePath,
|
||
success: (infoRes) => {
|
||
const fileSize = infoRes.size
|
||
cacheStats.set(`audio_${fileName}`, fileSize)
|
||
console.log(`临时音频文件大小: ${fileSize} bytes`)
|
||
},
|
||
fail: (err) => {
|
||
console.error('获取临时音频文件大小失败:', err)
|
||
}
|
||
})
|
||
|
||
// 播放音频
|
||
this.playAudioFromFile(tempFilePath)
|
||
.then(() => resolve())
|
||
.catch((error) => reject(error))
|
||
}
|
||
|
||
// 清除音频缓存
|
||
clearAudioCache(): void {
|
||
console.log('清除音频缓存,缓存数量:', audioCache.size)
|
||
|
||
// 删除持久化存储的音频文件
|
||
const fs = wx.getFileSystemManager()
|
||
const audioCacheDir = `${wx.env.USER_DATA_PATH}/audio_cache`
|
||
|
||
try {
|
||
// 检查目录是否存在
|
||
fs.accessSync(audioCacheDir)
|
||
// 删除整个音频缓存目录
|
||
fs.rmdirSync(audioCacheDir, true)
|
||
console.log('音频缓存目录已删除:', audioCacheDir)
|
||
} catch (e) {
|
||
// 如果目录不存在或删除失败,记录日志但不中断
|
||
console.log('音频缓存目录不存在或删除失败:', e)
|
||
}
|
||
|
||
// 清空内存中的缓存映射
|
||
audioCache.clear()
|
||
|
||
// 清除音频相关的缓存统计
|
||
const keysToDelete: string[] = []
|
||
cacheStats.forEach((value, key) => {
|
||
if (key.startsWith('audio_')) {
|
||
keysToDelete.push(key)
|
||
}
|
||
})
|
||
keysToDelete.forEach(key => cacheStats.delete(key))
|
||
}
|
||
|
||
// 获取缓存统计信息
|
||
getCacheStats(): { audioCount: number; imageCount: number; totalSize: number; sizeByType: Record<string, number> } {
|
||
console.log('开始获取缓存统计信息')
|
||
const startTime = Date.now()
|
||
|
||
let audioCount = 0
|
||
let imageCount = 0
|
||
let totalSize = 0
|
||
const sizeByType: Record<string, number> = { audio: 0, image: 0 }
|
||
|
||
// 遍历缓存统计信息
|
||
cacheStats.forEach((size, key) => {
|
||
totalSize += size
|
||
if (key.startsWith('audio_')) {
|
||
audioCount++
|
||
sizeByType.audio += size
|
||
} else if (key.startsWith('image_')) {
|
||
imageCount++
|
||
sizeByType.image += size
|
||
}
|
||
})
|
||
|
||
const endTime = Date.now()
|
||
console.log('缓存统计信息获取完成,耗时:', endTime - startTime, 'ms', {
|
||
audioCount,
|
||
imageCount,
|
||
totalSize,
|
||
sizeByType
|
||
})
|
||
|
||
return { audioCount, imageCount, totalSize, sizeByType }
|
||
}
|
||
|
||
// 格式化文件大小显示
|
||
formatFileSize(bytes: number): string {
|
||
if (bytes < 1024) {
|
||
return `${bytes}B`
|
||
} else if (bytes < 1024 * 1024) {
|
||
return `${(bytes / 1024).toFixed(1)}KB`
|
||
} else if (bytes < 1024 * 1024 * 1024) {
|
||
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`
|
||
} else {
|
||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`
|
||
}
|
||
}
|
||
|
||
// 获取用户信息
|
||
async getUserInfo(): Promise<IUserInfo> {
|
||
const response = await this.request<IUserInfo>('/api/v1/user/info')
|
||
return response.data
|
||
}
|
||
|
||
// 检查登录状态
|
||
async checkLoginStatus(): Promise<boolean> {
|
||
try {
|
||
const app = getApp<IAppOption>()
|
||
if (!app.globalData.token) {
|
||
return false
|
||
}
|
||
|
||
// 向服务器验证 token 是否有效
|
||
await this.request('/api/v1/auth/verify', 'GET', undefined, false)
|
||
return true
|
||
} catch (error) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 检查本地 token 是否过期
|
||
isTokenExpired(): boolean {
|
||
try {
|
||
const tokenExpiry = wx.getStorageSync('tokenExpiry')
|
||
if (!tokenExpiry) {
|
||
return true
|
||
}
|
||
|
||
const now = Date.now()
|
||
const isExpired = now >= tokenExpiry
|
||
|
||
console.log('Token 过期检查:', {
|
||
current: new Date(now).toLocaleString(),
|
||
expiry: new Date(tokenExpiry).toLocaleString(),
|
||
isExpired
|
||
})
|
||
|
||
return isExpired
|
||
} catch (error) {
|
||
console.error('检查 token 过期时间失败:', error)
|
||
return true
|
||
}
|
||
}
|
||
|
||
// 获取存储的认证信息
|
||
getStoredAuthInfo() {
|
||
try {
|
||
return {
|
||
token: wx.getStorageSync('token'),
|
||
tokenExpiry: wx.getStorageSync('tokenExpiry'),
|
||
sessionUuid: wx.getStorageSync('sessionUuid'),
|
||
userInfo: wx.getStorageSync('userInfo'),
|
||
dictLevel: wx.getStorageSync('dictLevel') // 新增词典等级
|
||
}
|
||
} catch (error) {
|
||
console.error('获取存储的认证信息失败:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
// 修改用户设置
|
||
// async updateUserSettings(settings: { dict_level: string }): Promise<boolean> {
|
||
// try {
|
||
// const response = await this.request<null>('/api/v1/wx/settings', 'PUT', settings)
|
||
|
||
// if (response.code === 0 || response.code === 200) {
|
||
// // 更新本地缓存和全局状态
|
||
// const app = getApp<IAppOption>()
|
||
// app.globalData.dictLevel = settings.dict_level
|
||
// wx.setStorageSync('dictLevel', settings.dict_level)
|
||
|
||
// console.log('用户设置更新成功:', settings)
|
||
// return true
|
||
// } else {
|
||
// throw new Error(response.message || response.msg || '更新失败')
|
||
// }
|
||
// } catch (error) {
|
||
// console.error('更新用户设置失败:', error)
|
||
// throw error
|
||
// }
|
||
// }
|
||
|
||
// 获取词典等级选项
|
||
getDictLevelOptions() {
|
||
return DICT_LEVEL_OPTIONS
|
||
}
|
||
|
||
// 测试401自动重试机制(仅用于开发调试)
|
||
async test401AutoRetry(): Promise<any> {
|
||
console.log('开始测试401自动重试机制')
|
||
try {
|
||
// 请求一个可能返回401的接口
|
||
const response = await this.request('/api/v1/auth/verify', 'GET', undefined, false)
|
||
console.log('401测试成功,响应:', response)
|
||
return response
|
||
} catch (error) {
|
||
console.log('401测试结果:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
// 获取审核统计数据
|
||
async getAuditStatistics(): Promise<{ total_count: number; today_count: number; image_count: number }> {
|
||
try {
|
||
console.log('开始获取审核统计数据')
|
||
const response = await this.request<{ total_count: number; today_count: number; image_count: number }>('/api/v1/audit/statistics')
|
||
console.log('获取审核统计数据成功:', response.data)
|
||
return response.data
|
||
} catch (error) {
|
||
console.error('获取审核统计数据失败:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
// 获取历史记录数据
|
||
async getAuditHistory(page: number = 1, size: number = 15): Promise<IAuditHistoryResponse> {
|
||
try {
|
||
console.log('开始获取审核历史记录数据', { page, size });
|
||
const response = await this.request<IAuditHistoryResponse>(`/api/v1/audit/history?page=${page}&size=${size}`);
|
||
console.log('获取审核历史记录数据成功:', response.data);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('获取审核历史记录数据失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async getDailySummary(page: number = 1, size: number = 15): Promise<IDailySummaryResponse> {
|
||
try {
|
||
console.log('开始获取历史记录数据', { page, size });
|
||
const response = await this.request<IDailySummaryResponse>(`/api/v1/audit/summary?page=${page}&size=${size}`);
|
||
console.log('获取历史记录数据成功:', response.data);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('获取历史记录数据失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async getTodaySummary(page: number = 1, size: number = 15): Promise<IDailySummaryResponse> {
|
||
try {
|
||
console.log('开始获取今日总结数据', { page, size });
|
||
const response = await this.request<IDailySummaryResponse>(`/api/v1/audit/today_summary?page=${page}&size=${size}`);
|
||
console.log('获取今日总结数据成功:', response.data);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('获取今日总结数据失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 获取口语评估结果
|
||
async getAssessmentResult(fileId: string, imageTextId: string): Promise<{
|
||
file_id: string
|
||
image_text_id: string
|
||
assessment_result: {
|
||
assessment: {
|
||
code: number
|
||
final: number
|
||
message: string
|
||
voice_id: string
|
||
result: {
|
||
RefTextId: number,
|
||
SentenceId: number,
|
||
PronAccuracy: number,
|
||
PronFluency: number,
|
||
PronCompletion: number,
|
||
SuggestedScore: number
|
||
Words: any
|
||
}
|
||
}
|
||
}
|
||
}> {
|
||
try {
|
||
console.log('开始获取口语评估结果', { fileId });
|
||
const response = await this.request<{
|
||
file_id: string
|
||
image_text_id: string
|
||
assessment_result: {
|
||
assessment: {
|
||
code: number
|
||
final: number
|
||
message: string
|
||
voice_id: string
|
||
result: {
|
||
RefTextId: number,
|
||
SentenceId: number,
|
||
PronAccuracy: number,
|
||
PronFluency: number,
|
||
PronCompletion: number,
|
||
SuggestedScore: number
|
||
Words: any
|
||
}
|
||
}
|
||
}
|
||
}>(`/api/v1/recording/assessment`, 'POST', {
|
||
file_id: fileId,
|
||
image_text_id: imageTextId
|
||
})
|
||
|
||
console.log('获取口语评估结果成功:', response.data);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('获取口语评估结果失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 获取图片文本和评分信息
|
||
async getImageTextInit(imageId: string): Promise<{
|
||
image_file_id: string,
|
||
assessments: Array<{
|
||
id: string
|
||
content: string,
|
||
details: {
|
||
assessment: {
|
||
code: number
|
||
final: number
|
||
message: string
|
||
voice_id: string
|
||
result: {
|
||
RefTextId: number,
|
||
SentenceId: number,
|
||
PronAccuracy: number,
|
||
PronFluency: number,
|
||
PronCompletion: number,
|
||
SuggestedScore: number,
|
||
Words: any
|
||
}
|
||
}
|
||
} | null
|
||
}>
|
||
}> {
|
||
try {
|
||
console.log('开始获取图片文本和评分信息', { imageId });
|
||
const app = getApp<IAppOption>()
|
||
const dictLevel = app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'PRIMARY'
|
||
const response = await this.request<{
|
||
image_file_id: string,
|
||
assessments: Array<{
|
||
id: string
|
||
content: string,
|
||
details: {
|
||
assessment: {
|
||
code: number
|
||
final: number
|
||
message: string
|
||
voice_id: string
|
||
result: {
|
||
RefTextId: number,
|
||
SentenceId: number,
|
||
PronAccuracy: number,
|
||
PronFluency: number,
|
||
PronCompletion: number,
|
||
SuggestedScore: number,
|
||
Words: any
|
||
}
|
||
}
|
||
} | null
|
||
}>
|
||
}>(`/api/v1/image_text/init`, 'POST', {
|
||
dict_level: dictLevel,
|
||
image_id: imageId
|
||
})
|
||
|
||
console.log('获取图片文本和评分信息成功:', response.data);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('获取图片文本和评分信息失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// 导出单例
|
||
const apiManager = new ApiManager()
|
||
export default apiManager
|
||
export { ApiManager } |