diff --git a/package.json b/package.json index e7434c3..5de1cb9 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "@tanstack/vue-query": "^5.62.16", "abortcontroller-polyfill": "^1.7.8", "dayjs": "1.11.10", + "js-cookie": "^3.0.5", "pinia": "2.0.36", "pinia-plugin-persistedstate": "3.2.1", "qs": "6.5.3", diff --git a/pages.config.ts b/pages.config.ts index b8cb24c..6eb5f9a 100644 --- a/pages.config.ts +++ b/pages.config.ts @@ -40,6 +40,12 @@ export default defineUniPages({ pagePath: 'pages/about/about', text: '关于', }, + { + iconPath: 'static/tabbar/personal.png', + selectedIconPath: 'static/tabbar/personalHL.png', + pagePath: 'pages/mine/index', + text: '我的', + }, ], }, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40f225d..19026dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: dayjs: specifier: 1.11.10 version: 1.11.10 + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 pinia: specifier: 2.0.36 version: 2.0.36(typescript@5.7.2)(vue@3.5.15(typescript@5.7.2)) @@ -3656,6 +3659,10 @@ packages: jpeg-js@0.3.7: resolution: {integrity: sha512-9IXdWudL61npZjvLuVe/ktHiA41iE8qFyLB+4VDTblEsWBzeg8WQTlktdUK4CdncUqtUgUg0bbOmTE2bKBKaBQ==} + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -10051,6 +10058,8 @@ snapshots: jpeg-js@0.3.7: {} + js-cookie@3.0.5: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} diff --git a/src/api/login.ts b/src/api/login.ts new file mode 100644 index 0000000..9a53a35 --- /dev/null +++ b/src/api/login.ts @@ -0,0 +1,86 @@ +import { ICaptcha, IUpdateInfo, IUpdatePassword, IUserInfoVo, IUserLogin } from './login.typings' +import { http } from '@/utils/http' + +/** + * 登录表单 + */ +export interface ILoginForm { + username: string + password: string + code: string + uuid: string +} + +/** + * 获取验证码 + * @returns ICaptcha 验证码 + */ +export const getCode = () => { + return http.get('/user/getCode') +} + +/** + * 用户登录 + * @param loginForm 登录表单 + */ +export const login = (loginForm: ILoginForm) => { + return http.post('/user/login', loginForm) +} + +/** + * 获取用户信息 + */ +export const getUserInfo = (token: string) => { + return http.get('/user/info', { token }) +} + +/** + * 退出登录 + */ +export const logout = () => { + return http.get('/user/logout') +} + +/** + * 修改用户信息 + */ +export const updateInfo = (data: IUpdateInfo) => { + return http.post('/user/updateInfo', data) +} + +/** + * 修改用户密码 + */ +export const updateUserPassword = (data: IUpdatePassword) => { + return http.post('/user/updatePassword', data) +} + +/** + * 获取微信登录凭证 + * @returns Promise 包含微信登录凭证(code) + */ +export const getWxCode = () => { + return new Promise((resolve, reject) => { + uni.login({ + provider: 'weixin', + success: (res) => resolve(res), + fail: (err) => reject(new Error(err)), + }) + }) +} + +/** + * 微信登录参数 + */ +export interface IWxLoginParams { + code: string +} + +/** + * 微信登录 + * @param params 微信登录参数,包含code + * @returns Promise 包含登录结果 + */ +export const wxLogin = (params: IWxLoginParams) => { + return http.post('/app/wx/login', {}, params) +} diff --git a/src/api/login.typings.ts b/src/api/login.typings.ts new file mode 100644 index 0000000..d344f05 --- /dev/null +++ b/src/api/login.typings.ts @@ -0,0 +1,63 @@ +/** + * 用户信息 + */ +export type IUserInfoVo = { + id: number + username: string + name: string + sex: string + email: string + phone: string + avatar: string + createTime: string + roles: string[] + permissions: string[] +} + +/** + * 登录返回的信息 + */ +export type IUserLogin = { + id: string + username: string + token: string +} + +/** + * 获取验证码 + */ +export type ICaptcha = { + captchaEnabled: boolean + uuid: string + image: string +} +/** + * 上传成功的信息 + */ +export type IUploadSuccessInfo = { + fileId: number + originalName: string + fileName: string + storagePath: string + fileHash: string + fileType: string + fileBusinessType: string + fileSize: number +} +/** + * 更新用户信息 + */ +export type IUpdateInfo = { + id: number + name: string + sex: string +} +/** + * 更新用户信息 + */ +export type IUpdatePassword = { + id: number + oldPassword: string + newPassword: string + confirmPassword: string +} diff --git a/src/interceptors/route.ts b/src/interceptors/route.ts index 6e71763..060c6f3 100644 --- a/src/interceptors/route.ts +++ b/src/interceptors/route.ts @@ -12,7 +12,7 @@ const loginRoute = '/pages/login/index' const isLogined = () => { const userStore = useUserStore() - return userStore.isLogined + return !!userStore.userInfo.username } const isDev = import.meta.env.DEV diff --git a/src/pages.json b/src/pages.json index e3b0765..d8c43f9 100644 --- a/src/pages.json +++ b/src/pages.json @@ -35,6 +35,12 @@ "selectedIconPath": "static/tabbar/exampleHL.png", "pagePath": "pages/about/about", "text": "关于" + }, + { + "iconPath": "static/tabbar/personal.png", + "selectedIconPath": "static/tabbar/personalHL.png", + "pagePath": "pages/mine/index", + "text": "我的" } ] }, @@ -54,6 +60,42 @@ "navigationBarTitleText": "关于", "navigationStyle": "custom" } + }, + { + "path": "pages/login/index", + "type": "page", + "style": { + "navigationBarTitleText": "登录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mine/index", + "type": "page", + "style": { + "navigationBarTitleText": "我的" + } + }, + { + "path": "pages/mine/about/index", + "type": "page", + "style": { + "navigationBarTitleText": "关于我们" + } + }, + { + "path": "pages/mine/info/index", + "type": "page", + "style": { + "navigationBarTitleText": "个人资料" + } + }, + { + "path": "pages/mine/password/index", + "type": "page", + "style": { + "navigationBarTitleText": "修改密码" + } } ], "subPackages": [] diff --git a/src/pages/login/index.vue b/src/pages/login/index.vue new file mode 100644 index 0000000..2f6436e --- /dev/null +++ b/src/pages/login/index.vue @@ -0,0 +1,590 @@ + +{ + style: { + navigationBarTitleText: '登录', + navigationStyle: 'custom', + }, +} + + + + + + diff --git a/src/pages/mine/about/index.vue b/src/pages/mine/about/index.vue new file mode 100644 index 0000000..d7e152a --- /dev/null +++ b/src/pages/mine/about/index.vue @@ -0,0 +1,173 @@ + +{ + style: { + navigationBarTitleText: '关于我们', + }, +} + + + + + + + diff --git a/src/pages/mine/index.vue b/src/pages/mine/index.vue new file mode 100644 index 0000000..f3e8084 --- /dev/null +++ b/src/pages/mine/index.vue @@ -0,0 +1,346 @@ + +{ + style: { + navigationBarTitleText: '我的', + }, +} + + + + + + + diff --git a/src/pages/mine/info/index.vue b/src/pages/mine/info/index.vue new file mode 100644 index 0000000..2feb64d --- /dev/null +++ b/src/pages/mine/info/index.vue @@ -0,0 +1,190 @@ + +{ + style: { + navigationBarTitleText: '个人资料', + }, +} + + + + + + + diff --git a/src/pages/mine/password/index.vue b/src/pages/mine/password/index.vue new file mode 100644 index 0000000..a92d2d5 --- /dev/null +++ b/src/pages/mine/password/index.vue @@ -0,0 +1,203 @@ + +{ + style: { + navigationBarTitleText: '修改密码', + }, +} + + + + + + + diff --git a/src/static/images/avatar.jpg b/src/static/images/avatar.jpg new file mode 100644 index 0000000..2010a70 Binary files /dev/null and b/src/static/images/avatar.jpg differ diff --git a/src/store/user.ts b/src/store/user.ts index 82bd873..a0c606c 100644 --- a/src/store/user.ts +++ b/src/store/user.ts @@ -1,32 +1,99 @@ +import { + login as _login, + getUserInfo as _getUserInfo, + wxLogin as _wxLogin, + logout as _logout, +} from '@/api/login' +import { getToken, getTokenKey, removeToken, setToken } from '@/utils/auth' import { defineStore } from 'pinia' import { ref } from 'vue' +import { toast } from '@/utils/toast' +import { IUserInfoVo, IUserLogin } from '@/api/login.typings' -const initState = { nickname: '', avatar: '' } +// 初始化状态 +const userInfoState: IUserInfoVo = { + id: 0, + username: '', + name: '', + sex: '', + email: '', + phone: '', + avatar: '/static/images/avatar.jpg', + createTime: '', + roles: [], + permissions: [], +} export const useUserStore = defineStore( 'user', () => { - const userInfo = ref({ ...initState }) - - const setUserInfo = (val: IUserInfo) => { + // 定义用户信息 + const userInfo = ref({ ...userInfoState }) + // 设置用户信息 + const setUserInfo = (val: IUserInfoVo) => { + console.log('设置用户信息', val) + // 若头像为空 则使用默认头像 + if (!val.avatar) { + val.avatar = userInfoState.avatar + } else { + val.avatar = 'https://oss.laf.run/ukw0y1-site/avatar.jpg?feige' + } userInfo.value = val } - - const clearUserInfo = () => { - userInfo.value = { ...initState } + // 删除用户信息 + const removeUserInfo = () => { + userInfo.value = { ...userInfoState } + removeToken() } - // 一般没有reset需求,不需要的可以删除 - const reset = () => { - userInfo.value = { ...initState } + /** + * 用户登录 + * @param credentials 登录参数 + * @returns R + */ + const login = async (credentials: { + username: string + password: string + code: string + uuid: string + }) => { + const res = await _login(credentials) + console.log('登录信息', res) + toast.success('登录成功') + setToken(res.data.token) + return res + } + /** + * 获取用户信息 + */ + const getUserInfo = async () => { + const res = await _getUserInfo(getToken()) + setUserInfo(res.data) + // TODO 这里可以增加获取用户路由的方法 根据用户的角色动态生成路由 + return res + } + /** + * 退出登录 并 删除用户信息 + */ + const logout = async () => { + _logout() + removeUserInfo() + } + /** + * 微信登录 + * @param credentials 微信登录Code + */ + const wxLogin = async (credentials: { code: string }) => { + const res = await _wxLogin(credentials) + setToken(res.data.token) + return res } - const isLogined = computed(() => !!userInfo.value.token) return { userInfo, - setUserInfo, - clearUserInfo, - isLogined, - reset, + login, + wxLogin, + getUserInfo, + logout, } }, { diff --git a/src/types/components.d.ts b/src/types/components.d.ts index 9fec748..842ef29 100644 --- a/src/types/components.d.ts +++ b/src/types/components.d.ts @@ -8,8 +8,6 @@ export {} declare module 'vue' { export interface GlobalComponents { FgNavbar: typeof import('./../components/fg-navbar/fg-navbar.vue')['default'] - FgTabbar: typeof import('./../components/fg-tabbar/fg-tabbar.vue')['default'] PrivacyPopup: typeof import('./../components/privacy-popup/privacy-popup.vue')['default'] - Tabbar: typeof import('./../components/tabbar/tabbar.vue')['default'] } } diff --git a/src/types/uni-pages.d.ts b/src/types/uni-pages.d.ts index 1644db7..47d231e 100644 --- a/src/types/uni-pages.d.ts +++ b/src/types/uni-pages.d.ts @@ -5,12 +5,17 @@ interface NavigateToOptions { url: "/pages/index/index" | - "/pages/about/about"; + "/pages/about/about" | + "/pages/login/index" | + "/pages/mine/index" | + "/pages/mine/about/index" | + "/pages/mine/info/index" | + "/pages/mine/password/index"; } interface RedirectToOptions extends NavigateToOptions {} interface SwitchTabOptions { - url: "/pages/index/index" | "/pages/about/about" + url: "/pages/index/index" | "/pages/about/about" | "/pages/mine/index" } type ReLaunchOptions = NavigateToOptions | SwitchTabOptions; diff --git a/src/typings.ts b/src/typings.ts index 016e462..cadb468 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -4,3 +4,12 @@ export enum TestEnum { A = '1', B = '2', } + +// uni.uploadFile文件上传参数 +export type IUniUploadFileOptions = { + file?: File + files?: UniApp.UploadFileOptionFiles[] + filePath?: string + name?: string + formData?: any +} diff --git a/src/utils/auth.ts b/src/utils/auth.ts new file mode 100644 index 0000000..3d071e4 --- /dev/null +++ b/src/utils/auth.ts @@ -0,0 +1,81 @@ +import Cookie from 'js-cookie' +import { isMpWeixin } from './platform' +/** + * TokeKey的名字 + */ +const TokenKey: string = 'token' + +/** + * 获取tokenKeyName + * @returns tokenKeyName + */ +export const getTokenKey = (): string => { + return TokenKey +} + +/** + * 是否登录,即是否有token,不检查Token是否过期和是否有效 + * @returns 是否登录 + */ +export const isLogin = () => { + return !!getToken() +} + +/** + * 获取Token + * @returns 令牌 + */ +export const getToken = () => { + return getCookieMap(getTokenKey()) +} + +/** + * 设置Token + * @param token 令牌 + */ +export const setToken = (token: string) => { + setCookieMap(getTokenKey(), token) +} +/** + * 删除Token + */ +export const removeToken = () => { + removeCookieMap(getTokenKey()) +} + +/** + * 设置Cookie + * @param key Cookie的key + * @param value Cookie的value + */ +export const setCookieMap = (key: string, value: any) => { + if (isMpWeixin) { + uni.setStorageSync(key, value) + return + } + Cookie.set(key, value) +} + +/** + * 获取Cookie + * @param key Cookie的key + * @returns Cookie的value + */ +export const getCookieMap = (key: string) => { + if (isMpWeixin) { + return uni.getStorageSync(key) as T + } + return Cookie.get(key) as T +} + +/** + * 删除Cookie + * @param key Cookie的key + */ +export const removeCookieMap = (key: string) => { + if (isMpWeixin) { + uni.removeStorageSync(key) + return + } + Cookie.remove(key) +} diff --git a/src/utils/index.ts b/src/utils/index.ts index c2e9ef5..ddc905d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -23,6 +23,26 @@ export const getIsTabbar = () => { } } +/** + * 判断指定页面是否是 tabbar 页 + * @param path 页面路径 + * @returns true: 是 tabbar 页 false: 不是 tabbar 页 + */ +export const isTableBar = (path: string) => { + if (!tabBar) { + return false + } + if (!tabBar.list.length) { + // 通常有 tabBar 的话,list 不能有空,且至少有2个元素,这里其实不用处理 + return false + } + // 这里需要处理一下 path,因为 tabBar 中的 pagePath 是不带 /pages 前缀的 + if (path.startsWith('/')) { + path = path.substring(1) + } + return !!tabBar.list.find((e) => e.pagePath === path) +} + /** * 获取当前页面路由的 path 路径和 redirectPath 路径 * path 如 '/pages/login/index' diff --git a/src/utils/toast.ts b/src/utils/toast.ts new file mode 100644 index 0000000..30f6522 --- /dev/null +++ b/src/utils/toast.ts @@ -0,0 +1,65 @@ +/** + * toast 弹窗组件 + * 支持 success/error/warning/info 四种状态 + * 可配置 duration, position 等参数 + */ + +type ToastType = 'success' | 'error' | 'warning' | 'info' + +interface ToastOptions { + type?: ToastType + duration?: number + position?: 'top' | 'middle' | 'bottom' + icon?: 'success' | 'error' | 'none' | 'loading' | 'fail' | 'exception' + message: string +} + +export function showToast(options: ToastOptions | string) { + const defaultOptions: ToastOptions = { + type: 'info', + duration: 2000, + position: 'middle', + message: '', + } + const mergedOptions = + typeof options === 'string' + ? { ...defaultOptions, message: options } + : { ...defaultOptions, ...options } + // 映射position到uniapp支持的格式 + const positionMap: Record = { + top: 'top', + middle: 'center', + bottom: 'bottom', + } + + // 映射图标类型 + const iconMap: Record< + ToastType, + 'success' | 'error' | 'none' | 'loading' | 'fail' | 'exception' + > = { + success: 'success', + error: 'error', + warning: 'fail', + info: 'none', + } + + // 调用uni.showToast显示提示 + uni.showToast({ + title: mergedOptions.message, + duration: mergedOptions.duration, + position: positionMap[mergedOptions.position], + icon: mergedOptions.icon || iconMap[mergedOptions.type], + mask: true, + }) +} + +export const toast = { + success: (message: string, options?: Omit) => + showToast({ ...options, type: 'success', message }), + error: (message: string, options?: Omit) => + showToast({ ...options, type: 'error', message }), + warning: (message: string, options?: Omit) => + showToast({ ...options, type: 'warning', message }), + info: (message: string, options?: Omit) => + showToast({ ...options, type: 'info', message }), +} diff --git a/src/utils/uploadFile.ts b/src/utils/uploadFile.ts new file mode 100644 index 0000000..594f24c --- /dev/null +++ b/src/utils/uploadFile.ts @@ -0,0 +1,338 @@ +import { getToken, getTokenKey } from './auth' +import { toast } from './toast' + +/** + * 文件上传钩子函数使用示例 + * @example + * const { loading, error, data, progress, run } = useUpload( + * uploadUrl, + * {}, + * { + * maxSize: 5, // 最大5MB + * sourceType: ['album'], // 仅支持从相册选择 + * onProgress: (p) => console.log(`上传进度:${p}%`), + * onSuccess: (res) => console.log('上传成功', res), + * onError: (err) => console.error('上传失败', err), + * }, + * ) + */ + +/** + * 上传文件的URL配置 + */ +export const uploadFileUrl = { + /** 用户头像上传地址 */ + USER_AVATAR: import.meta.env.VITE_SERVER_BASEURL + '/user/avatar', +} + +/** + * 通用文件上传函数(支持直接传入文件路径) + * @param url 上传地址 + * @param filePath 本地文件路径 + * @param formData 额外表单数据 + * @param options 上传选项 + */ +export const useFileUpload = ( + url: string, + filePath: string, + formData: Record = {}, + options: Omit = {}, +) => { + return useUpload( + url, + formData, + { + ...options, + sourceType: ['album'], + sizeType: ['original'], + }, + filePath, + ) +} + +export interface UploadOptions { + /** 最大可选择的图片数量,默认为1 */ + count?: number + /** 所选的图片的尺寸,original-原图,compressed-压缩图 */ + sizeType?: Array<'original' | 'compressed'> + /** 选择图片的来源,album-相册,camera-相机 */ + sourceType?: Array<'album' | 'camera'> + /** 文件大小限制,单位:MB */ + maxSize?: number // + /** 上传进度回调函数 */ + onProgress?: (progress: number) => void + /** 上传成功回调函数 */ + onSuccess?: (res: UniApp.UploadFileSuccessCallbackResult) => void + /** 上传失败回调函数 */ + onError?: (err: Error | UniApp.GeneralCallbackResult) => void + /** 上传完成回调函数(无论成功失败) */ + onComplete?: () => void +} + +/** + * 文件上传钩子函数 + * @template T 上传成功后返回的数据类型 + * @param url 上传地址 + * @param formData 额外的表单数据 + * @param options 上传选项 + * @returns 上传状态和控制对象 + */ +export const useUpload = ( + url: string, + formData: Record = {}, + options: UploadOptions = {}, + /** 直接传入文件路径,跳过选择器 */ + directFilePath?: string, +) => { + /** 上传中状态 */ + const loading = ref(false) + /** 上传错误状态 */ + const error = ref(false) + /** 上传成功后的响应数据 */ + const data = ref() + /** 上传进度(0-100) */ + const progress = ref(0) + + /** 解构上传选项,设置默认值 */ + const { + /** 最大可选择的图片数量 */ + count = 1, + /** 所选的图片的尺寸 */ + sizeType = ['original', 'compressed'], + /** 选择图片的来源 */ + sourceType = ['album', 'camera'], + /** 文件大小限制(MB) */ + maxSize = 10, + /** 进度回调 */ + onProgress, + /** 成功回调 */ + onSuccess, + /** 失败回调 */ + onError, + /** 完成回调 */ + onComplete, + } = options + + /** + * 检查文件大小是否超过限制 + * @param size 文件大小(字节) + * @returns 是否通过检查 + */ + const checkFileSize = (size: number) => { + const sizeInMB = size / 1024 / 1024 + if (sizeInMB > maxSize) { + toast.warning(`文件大小不能超过${maxSize}MB`) + return false + } + return true + } + /** + * 触发文件选择和上传 + * 根据平台使用不同的选择器: + * - 微信小程序使用 chooseMedia + * - 其他平台使用 chooseImage + */ + const run = () => { + if (directFilePath) { + // 直接使用传入的文件路径 + loading.value = true + progress.value = 0 + uploadFile({ + url, + tempFilePath: directFilePath, + formData, + data, + error, + loading, + progress, + onProgress, + onSuccess, + onError, + onComplete, + }) + return + } + + // #ifdef MP-WEIXIN + // 微信小程序环境下使用 chooseMedia API + uni.chooseMedia({ + count, + mediaType: ['image'], // 仅支持图片类型 + sourceType, + success: (res) => { + const file = res.tempFiles[0] + // 检查文件大小是否符合限制 + if (!checkFileSize(file.size)) return + + // 开始上传 + loading.value = true + progress.value = 0 + uploadFile({ + url, + tempFilePath: file.tempFilePath, + formData, + data, + error, + loading, + progress, + onProgress, + onSuccess, + onError, + onComplete, + }) + }, + fail: (err) => { + console.error('选择媒体文件失败:', err) + error.value = true + onError?.(err) + }, + }) + // #endif + + // #ifndef MP-WEIXIN + // 非微信小程序环境下使用 chooseImage API + uni.chooseImage({ + count, + sizeType, + sourceType, + success: (res) => { + console.log('选择图片成功:', res) + + // 开始上传 + loading.value = true + progress.value = 0 + uploadFile({ + url, + tempFilePath: res.tempFilePaths[0], + formData, + data, + error, + loading, + progress, + onProgress, + onSuccess, + onError, + onComplete, + }) + }, + fail: (err) => { + console.error('选择图片失败:', err) + error.value = true + onError?.(err) + }, + }) + // #endif + } + + return { loading, error, data, progress, run } +} + +/** + * 文件上传选项接口 + * @template T 上传成功后返回的数据类型 + */ +interface UploadFileOptions { + /** 上传地址 */ + url: string + /** 临时文件路径 */ + tempFilePath: string + /** 额外的表单数据 */ + formData: Record + /** 上传成功后的响应数据 */ + data: Ref + /** 上传错误状态 */ + error: Ref + /** 上传中状态 */ + loading: Ref + /** 上传进度(0-100) */ + progress: Ref + /** 上传进度回调 */ + onProgress?: (progress: number) => void + /** 上传成功回调 */ + onSuccess?: (res: UniApp.UploadFileSuccessCallbackResult) => void + /** 上传失败回调 */ + onError?: (err: Error | UniApp.GeneralCallbackResult) => void + /** 上传完成回调 */ + onComplete?: () => void +} + +/** + * 执行文件上传 + * @template T 上传成功后返回的数据类型 + * @param options 上传选项 + */ +function uploadFile({ + url, + tempFilePath, + formData, + data, + error, + loading, + progress, + onProgress, + onSuccess, + onError, + onComplete, +}: UploadFileOptions) { + try { + // 创建上传任务 + const uploadTask = uni.uploadFile({ + url, + filePath: tempFilePath, + name: 'file', // 文件对应的 key + formData, + header: { + // H5环境下不需要手动设置Content-Type,让浏览器自动处理multipart格式 + // #ifndef H5 + 'Content-Type': 'multipart/form-data', + // #endif + [getTokenKey()]: getToken(), // 添加认证token + }, + // 确保文件名称合法 + success: (uploadFileRes) => { + try { + // 解析响应数据 + const result = JSON.parse(uploadFileRes.data) + if (result.code === 1) { + // 上传成功 + data.value = result.data as T + onSuccess?.(uploadFileRes) + } else { + // 业务错误 + const err = new Error(result.message || '上传失败') + error.value = true + onError?.(err) + } + } catch (err) { + // 响应解析错误 + console.error('解析上传响应失败:', err) + error.value = true + onError?.(new Error('上传响应解析失败')) + } + }, + fail: (err) => { + // 上传请求失败 + console.error('上传文件失败:', err) + error.value = true + onError?.(err) + }, + complete: () => { + // 无论成功失败都执行 + loading.value = false + onComplete?.() + }, + }) + + // 监听上传进度 + uploadTask.onProgressUpdate((res) => { + progress.value = res.progress + onProgress?.(res.progress) + }) + } catch (err) { + // 创建上传任务失败 + console.error('创建上传任务失败:', err) + error.value = true + loading.value = false + onError?.(new Error('创建上传任务失败')) + } +}