From ad22d9f95fc0c0ac28dc053a773ebeb30f75b932 Mon Sep 17 00:00:00 2001 From: feige996 <1020102647@qq.com> Date: Tue, 27 May 2025 23:19:09 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(=E7=94=A8=E6=88=B7=E4=B8=AD=E5=BF=83):?= =?UTF-8?q?=20=E6=96=B0=E5=A2=9E=E7=94=A8=E6=88=B7=E4=B8=AD=E5=BF=83?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现用户中心完整功能,包括: 1. 新增登录页面及登录逻辑 2. 添加个人资料、修改密码、关于我们等子页面 3. 实现头像上传功能 4. 添加js-cookie依赖处理token存储 5. 完善用户信息类型定义和API接口 6. 新增tabbar"我的"入口及相关路由配置 新增工具函数: 1. 添加auth.ts处理认证相关逻辑 2. 实现toast.ts统一消息提示 3. 添加uploadFile.ts处理文件上传 4. 新增isTableBar判断页面是否为tabbar页 --- package.json | 1 + pages.config.ts | 6 + pnpm-lock.yaml | 9 + src/api/login.ts | 86 +++++ src/api/login.typings.ts | 63 ++++ src/interceptors/route.ts | 2 +- src/pages.json | 42 +++ src/pages/login/index.vue | 590 ++++++++++++++++++++++++++++++ src/pages/mine/about/index.vue | 173 +++++++++ src/pages/mine/index.vue | 346 ++++++++++++++++++ src/pages/mine/info/index.vue | 190 ++++++++++ src/pages/mine/password/index.vue | 203 ++++++++++ src/static/images/avatar.jpg | Bin 0 -> 58805 bytes src/store/user.ts | 97 ++++- src/types/components.d.ts | 2 - src/types/uni-pages.d.ts | 9 +- src/typings.ts | 9 + src/utils/auth.ts | 81 ++++ src/utils/index.ts | 20 + src/utils/toast.ts | 65 ++++ src/utils/uploadFile.ts | 338 +++++++++++++++++ 21 files changed, 2312 insertions(+), 20 deletions(-) create mode 100644 src/api/login.ts create mode 100644 src/api/login.typings.ts create mode 100644 src/pages/login/index.vue create mode 100644 src/pages/mine/about/index.vue create mode 100644 src/pages/mine/index.vue create mode 100644 src/pages/mine/info/index.vue create mode 100644 src/pages/mine/password/index.vue create mode 100644 src/static/images/avatar.jpg create mode 100644 src/utils/auth.ts create mode 100644 src/utils/toast.ts create mode 100644 src/utils/uploadFile.ts 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 0000000000000000000000000000000000000000..2010a704fa8a3bd674eb7837fe527c6a9020009d GIT binary patch literal 58805 zcmbTd2UJsS*DV^lbb+9NAXOwFNC~}23ndUB3B3t|NP_gDpeRK^BE1Qrm(WAeglYk) zq7evHnsfvNML=xm*}U)f|Np&bj5Ef$HyO{)PS|AZXJE86Qyq*G7T~kL#=YpJ}sezV>s8TkC6xI&LkEHR*X(BJteN++|7#`{UV`sEhvVW<;+<=et`>;7o{N5 zP-PWWHFXUGLzt1Vi76ayZDVU^@8Ia+dEM)VHwGIV5*l{vc6dbWgShyFhl#k1%&hF3 z$GLgFMd{nNH?KLwgGtbX@cdq6&=MdPpYs zKprv0=u=?*^wQ>TW^pCd7v5_@qbz46pi39_PNw$H%>M5t7W4mWX8%30|C!eeh>ea0 z`0?ntKvzJ!Z>-MaVJc~fw{WVs6r8SUx>=v0k_CSW6AB3f$$3*$a?)7s7euozlylRp zv=MPVdvZFjKEN|g{KESt%U;~M`>6Y%`U=VTNSZwL`>y&?^;5?IkCM(~!k<3jnX8{{ zG^9r_wg2}!Y3T>9f2e;!*HU*bf0IrI)7-^g`Gk%y|I>Hl542o|L{oZP{->+_m+^75 zb@*ZVUJ1MK&Uu~1%D*7*{UaD^LkLswbEMT&B16pBu563n{p(cOX6w1qtB=Xywh!+G zy5?5|vqVD7dum9h`an3hP2Kubpc0F$$RGuL*wkEExr1}m&<-y=PL{;FlNR;thM#L5~HrW z=~ek(h40tNI{tj8mm^n{J?eP7_Ux-+ezZed4k5T=?nM@prGfQgXUv*`(eWLxO8O!t zt5Lg)3#9q&&xjw-Yl5)wvq8T`+aouhjp`=svPCKY7pnH`*`WsUM>(I-{a4*&7oslC zNl}d0rJzywB0mm4R~4rwrNZ?Bf1D?IMvO^S(k0fmqQfx-Qy@qESp*e|j()ho~T(sAkiPTjVvyAxQN5@$lA_Wcg2{i$DN zb?Ye@hDYav{(?e~8<&q@dUz#W`{LoNySRY1t9mxUy3kjB!=%e4gtN@YGgbHiC`;c< zZEdOj$J9En0toP*5kFCnqBm{!b#=y1wOn*Ws?|nO<4Zh0Yh=DAo;#bg66M32jF}eh zoQ!dJk))C$?S>`Y53$}xc@22w@p}dktXh%(f(_@86dQ(8MCm+*Lo~x48Hw&vzlYr$UD|jHZi%45OR;?HEH57jIMTDjP1)bkKXp|p)@x9Svboi$Rup-v( zb2Vw8QbTFXW4$misAba4B` zkPtD1HW}4jB|?4SA8FHl&mUs$Uamd!YwHTnAKp3XvLxwfi-8ub`kPg?>@P3MUVfdh zT)zJ*$C3@pearIgUr=1whc5z3sC7F?k1@dqZx-I1w*8Tr_~0ZiCQ` z=?OE62J52D2<@G5m+Whl=Wp$}5|WSTHc{*CPNJ>t(3H9ghkwExkGLBX4l%&I-8c|j3i?=PaGB&| z6UJH-i4^N#ibRN^^Jts(oodEB&vpWvwCt-a`4Yp~xN~vFZOAqhm*k$&9�#uFE+G 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 z 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 { - 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('创建上传任务失败')) + } +} From dd177f81bb302417fc37899de63885be7c919923 Mon Sep 17 00:00:00 2001 From: feige996 <1020102647@qq.com> Date: Tue, 27 May 2025 23:31:38 +0800 Subject: [PATCH 2/4] =?UTF-8?q?build:=20=E5=8D=87=E7=BA=A7=20vue-tsc=20?= =?UTF-8?q?=E5=88=B0=20v2.2.10=20=E5=8F=8A=E7=9B=B8=E5=85=B3=E4=BE=9D?= =?UTF-8?q?=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 更新 vue-tsc 主版本至 2.x,同步升级 @volar 系列依赖包至兼容版本 --- package.json | 2 +- pnpm-lock.yaml | 101 +++++++++++++++++++++++-------------------------- 2 files changed, 49 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index 5de1cb9..3caf3d8 100644 --- a/package.json +++ b/package.json @@ -147,6 +147,6 @@ "unplugin-auto-import": "^0.17.8", "vite": "6.3.5", "vite-plugin-restart": "^0.4.2", - "vue-tsc": "^1.8.27" + "vue-tsc": "^2.2.10" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 19026dd..e9d7daa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -202,8 +202,8 @@ importers: specifier: ^0.4.2 version: 0.4.2(vite@6.3.5(@types/node@20.17.9)(jiti@2.4.2)(sass@1.77.8)(terser@5.36.0)(yaml@2.6.1)) vue-tsc: - specifier: ^1.8.27 - version: 1.8.27(typescript@5.7.2) + specifier: ^2.2.10 + version: 2.2.10(typescript@5.7.2) packages: @@ -2191,14 +2191,14 @@ packages: vite: ^5.0.0 vue: ^3.2.25 - '@volar/language-core@1.11.1': - resolution: {integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==} + '@volar/language-core@2.4.14': + resolution: {integrity: sha512-X6beusV0DvuVseaOEy7GoagS4rYHgDHnTrdOj5jeUb49fW5ceQyP9Ej5rBhqgz2wJggl+2fDbbojq1XKaxDi6w==} - '@volar/source-map@1.11.1': - resolution: {integrity: sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==} + '@volar/source-map@2.4.14': + resolution: {integrity: sha512-5TeKKMh7Sfxo8021cJfmBzcjfY1SsXsPMMjMvjY7ivesdnybqqS+GxGAoXHAOUawQTwtdUxgP65Im+dEmvWtYQ==} - '@volar/typescript@1.11.1': - resolution: {integrity: sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==} + '@volar/typescript@2.4.14': + resolution: {integrity: sha512-p8Z6f/bZM3/HyCdRNFZOEEzts51uV8WHeN8Tnfnm2EBv6FDB2TQLzfVx7aJvnl8ofKAOnS64B2O8bImBFaauRw==} '@vue/babel-helper-vue-transform-on@1.2.5': resolution: {integrity: sha512-lOz4t39ZdmU4DJAa2hwPYmKc8EsuGa2U0L9KaZaOJUt0UwQNjNA3AZTq6uEivhOKhhG1Wvy96SvYBoFmCg3uuw==} @@ -2252,6 +2252,9 @@ packages: '@vue/compiler-ssr@3.5.15': resolution: {integrity: sha512-gShn8zRREZbrXqTtmLSCffgZXDWv8nHc/GhsW+mbwBfNZL5pI96e7IWcIq8XGQe1TLtVbu7EV9gFIVSmfyarPg==} + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + '@vue/consolidate@1.0.0': resolution: {integrity: sha512-oTyUE+QHIzLw2PpV14GD/c7EohDyP64xCniWTcqcEmTd699eFqTIwOmtDYjcO1j3QgdXoJEoWv1/cCdLrRoOfg==} engines: {node: '>= 0.12.0'} @@ -2259,8 +2262,8 @@ packages: '@vue/devtools-api@6.6.4': resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} - '@vue/language-core@1.8.27': - resolution: {integrity: sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==} + '@vue/language-core@2.2.10': + resolution: {integrity: sha512-+yNoYx6XIKuAO8Mqh1vGytu8jkFEOH5C8iOv3i8Z/65A7x9iAOXA97Q+PqZ3nlm2lxf5rOJuIGI/wDtx/riNYw==} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -2357,6 +2360,9 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} + alien-signals@1.0.13: + resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -2675,9 +2681,6 @@ packages: compare-versions@3.6.0: resolution: {integrity: sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==} - computeds@0.0.1: - resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} - concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -3985,8 +3988,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - muggle-string@0.3.1: - resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} @@ -4617,11 +4620,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} - engines: {node: '>=10'} - hasBin: true - semver@7.7.2: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} @@ -5105,6 +5103,9 @@ packages: yaml: optional: true + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} engines: {node: '>=12'} @@ -5126,14 +5127,11 @@ packages: peerDependencies: vue: ^3.2.0 - vue-template-compiler@2.7.16: - resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} - - vue-tsc@1.8.27: - resolution: {integrity: sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==} + vue-tsc@2.2.10: + resolution: {integrity: sha512-jWZ1xSaNbabEV3whpIDMbjVSVawjAyW+x1n3JeGQo7S0uv2n9F/JMgWW90tGWNFRKya4YwKMZgCtr0vRAM7DeQ==} hasBin: true peerDependencies: - typescript: '*' + typescript: '>=5.0.0' vue@3.5.15: resolution: {integrity: sha512-aD9zK4rB43JAMK/5BmS4LdPiEp8Fdh8P1Ve/XNuMF5YRf78fCyPE6FUbQwcaWQ5oZ1R2CD9NKE0FFOVpMR7gEQ==} @@ -8225,18 +8223,17 @@ snapshots: vite: 6.3.5(@types/node@20.17.9)(jiti@2.4.2)(sass@1.77.8)(terser@5.36.0)(yaml@2.6.1) vue: 3.5.15(typescript@5.7.2) - '@volar/language-core@1.11.1': + '@volar/language-core@2.4.14': dependencies: - '@volar/source-map': 1.11.1 + '@volar/source-map': 2.4.14 - '@volar/source-map@1.11.1': - dependencies: - muggle-string: 0.3.1 + '@volar/source-map@2.4.14': {} - '@volar/typescript@1.11.1': + '@volar/typescript@2.4.14': dependencies: - '@volar/language-core': 1.11.1 + '@volar/language-core': 2.4.14 path-browserify: 1.0.1 + vscode-uri: 3.1.0 '@vue/babel-helper-vue-transform-on@1.2.5': {} @@ -8358,21 +8355,25 @@ snapshots: '@vue/compiler-dom': 3.5.15 '@vue/shared': 3.5.15 + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + '@vue/consolidate@1.0.0': {} '@vue/devtools-api@6.6.4': {} - '@vue/language-core@1.8.27(typescript@5.7.2)': + '@vue/language-core@2.2.10(typescript@5.7.2)': dependencies: - '@volar/language-core': 1.11.1 - '@volar/source-map': 1.11.1 + '@volar/language-core': 2.4.14 '@vue/compiler-dom': 3.5.15 + '@vue/compiler-vue2': 2.7.16 '@vue/shared': 3.5.15 - computeds: 0.0.1 + alien-signals: 1.0.13 minimatch: 9.0.5 - muggle-string: 0.3.1 + muggle-string: 0.4.1 path-browserify: 1.0.1 - vue-template-compiler: 2.7.16 optionalDependencies: typescript: 5.7.2 @@ -8457,6 +8458,8 @@ snapshots: transitivePeerDependencies: - supports-color + alien-signals@1.0.13: {} + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -8834,8 +8837,6 @@ snapshots: compare-versions@3.6.0: {} - computeds@0.0.1: {} - concat-map@0.0.1: {} confbox@0.1.8: {} @@ -10380,7 +10381,7 @@ snapshots: ms@2.1.3: {} - muggle-string@0.3.1: {} + muggle-string@0.4.1: {} nanoid@3.3.11: {} @@ -11014,8 +11015,6 @@ snapshots: semver@6.3.1: {} - semver@7.6.3: {} - semver@7.7.2: {} send@0.19.0: @@ -11544,6 +11543,8 @@ snapshots: terser: 5.36.0 yaml: 2.6.1 + vscode-uri@3.1.0: {} + vue-demi@0.14.10(vue@3.5.15(typescript@5.7.2)): dependencies: vue: 3.5.15(typescript@5.7.2) @@ -11557,16 +11558,10 @@ snapshots: '@vue/devtools-api': 6.6.4 vue: 3.5.15(typescript@5.7.2) - vue-template-compiler@2.7.16: + vue-tsc@2.2.10(typescript@5.7.2): dependencies: - de-indent: 1.0.2 - he: 1.2.0 - - vue-tsc@1.8.27(typescript@5.7.2): - dependencies: - '@volar/typescript': 1.11.1 - '@vue/language-core': 1.8.27(typescript@5.7.2) - semver: 7.6.3 + '@volar/typescript': 2.4.14 + '@vue/language-core': 2.2.10(typescript@5.7.2) typescript: 5.7.2 vue@3.5.15(typescript@5.7.2): From b4316befdd27909ce965e16c44057f4b1ecfe98b Mon Sep 17 00:00:00 2001 From: feige996 <1020102647@qq.com> Date: Wed, 28 May 2025 00:16:33 +0800 Subject: [PATCH 3/4] =?UTF-8?q?refactor(auth):=20=E7=A7=BB=E9=99=A4token?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E9=80=BB=E8=BE=91=E5=B9=B6=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=99=BB=E5=BD=95=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除auth.ts及相关token管理函数 - 修改登录接口和用户信息获取接口,不再依赖token - 使用uni-app存储替代cookie存储用户信息 - 重构微信登录流程,简化参数传递 - 更新用户头像默认路径为新增的default-avatar.png - 在个人中心页面增加登录状态判断和登录按钮 ``` 这个提交消息遵循了以下原则: 1. 使用refactor类型,因为这是对现有代码结构的重构 2. 添加了scope(auth)来明确这是认证相关的重构 3. 描述简明扼要地说明了主要变更 4. 在body中列出了主要变更点,没有重复描述 5. 使用中文并保持简洁,每个变更点用短句说明 6. 使用动词开头并保持一致的格式 --- src/api/login.ts | 11 ++-- src/pages/login/index.vue | 6 +- src/pages/mine/index.vue | 30 ++++++++-- src/static/images/default-avatar.png | Bin 0 -> 560 bytes src/store/user.ts | 26 ++++++--- src/utils/auth.ts | 81 --------------------------- src/utils/uploadFile.ts | 2 - 7 files changed, 49 insertions(+), 107 deletions(-) create mode 100644 src/static/images/default-avatar.png delete mode 100644 src/utils/auth.ts diff --git a/src/api/login.ts b/src/api/login.ts index 9a53a35..effdd94 100644 --- a/src/api/login.ts +++ b/src/api/login.ts @@ -30,8 +30,8 @@ export const login = (loginForm: ILoginForm) => { /** * 获取用户信息 */ -export const getUserInfo = (token: string) => { - return http.get('/user/info', { token }) +export const getUserInfo = () => { + return http.get('/user/info') } /** @@ -72,15 +72,12 @@ export const getWxCode = () => { /** * 微信登录参数 */ -export interface IWxLoginParams { - code: string -} /** * 微信登录 * @param params 微信登录参数,包含code * @returns Promise 包含登录结果 */ -export const wxLogin = (params: IWxLoginParams) => { - return http.post('/app/wx/login', {}, params) +export const wxLogin = (data: { code: string }) => { + return http.post('/user/wxLogin', data) } diff --git a/src/pages/login/index.vue b/src/pages/login/index.vue index 2f6436e..7610f20 100644 --- a/src/pages/login/index.vue +++ b/src/pages/login/index.vue @@ -127,7 +127,7 @@ import { ref } from 'vue' import { useUserStore } from '@/store/user' import { isMpWeixin } from '@/utils/platform' -import { getCode, getWxCode, ILoginForm } from '@/api/login' +import { getCode, ILoginForm } from '@/api/login' import { toast } from '@/utils/toast' import { isTableBar } from '@/utils/index' import { ICaptcha } from '@/api/login.typings' @@ -210,10 +210,8 @@ const handleWechatLogin = async () => { toast.error('请先阅读并同意用户协议和隐私政策') return } - // 获取微信小程序登录的code - const { code } = await getWxCode() // 微信登录 - await userStore.wxLogin({ code }) + await userStore.wxLogin() // 获取用户信息 await userStore.getUserInfo() // 跳转到首页或重定向页面 diff --git a/src/pages/mine/index.vue b/src/pages/mine/index.vue index f3e8084..881cb10 100644 --- a/src/pages/mine/index.vue +++ b/src/pages/mine/index.vue @@ -77,7 +77,8 @@ - 退出登录 + 退出登录 + 登录 @@ -90,10 +91,16 @@ import { uploadFileUrl, useUpload } from '@/utils/uploadFile' import { storeToRefs } from 'pinia' import { IUploadSuccessInfo } from '@/api/login.typings' +const userStore = useUserStore() + const toast = useToast() +const hasLogin = ref(false) + onShow((options) => { + hasLogin.value = !!uni.getStorageSync('token') console.log('个人中心onShow', options) - useUserStore().getUserInfo() + + hasLogin.value && useUserStore().getUserInfo() }) // #ifndef MP-WEIXIN // 上传头像 @@ -106,7 +113,21 @@ const { run } = useUpload( ) // #endif +// 微信小程序下登录 +const handleLogin = async () => { + // #ifdef MP-WEIXIN + + // 微信登录 + await userStore.wxLogin() + hasLogin.value = true + // #endif + // #ifndef MP-WEIXIN + uni.navigateTo({ url: '/pages/login/index' }) + // #endif +} + // #ifdef MP-WEIXIN + // 微信小程序下选择头像事件 const onChooseAvatar = (e: any) => { console.log('选择头像', e.detail) @@ -215,15 +236,16 @@ const handleLogout = () => { if (res.confirm) { // 清空用户信息 useUserStore().logout() + hasLogin.value = false // 执行退出登录逻辑 toast.success('退出登录成功') // #ifdef MP-WEIXIN // 微信小程序,去首页 - uni.reLaunch({ url: '/pages/index/index' }) + // uni.reLaunch({ url: '/pages/index/index' }) // #endif // #ifndef MP-WEIXIN // 非微信小程序,去登录页 - uni.reLaunch({ url: '/pages/login/index' }) + // uni.reLaunch({ url: '/pages/login/index' }) // #endif } }, diff --git a/src/static/images/default-avatar.png b/src/static/images/default-avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..4eb587954368fc74189a34a26a96e356f0433429 GIT binary patch literal 560 zcmV-00?+-4P)*wd_wY9afva;LT-0tr1&CSm1>+QO_ zy5Qj9x3{=Gp>E`egj zB-!E$SHTP*L>1CoxM-Ugz(ZHGBg6Lz6ZRUkH!@e-47=O$R{}jTF1=$8Z(!84=r!WO zVJ<%8@h;nPuB*Ue=7Sc?zaYNO#qj0$K{FROdAxy*@svU9g8Neqf5{h5K|J>S(v1{l z1di3rJ>Z^cOuW_2(i7|r@S;oqOnXoD-lJx3)@Ucpo~xT{*J>x)-BSHLx${)-6XoVL z?L@Ofyl<~-CsIFJjoZAR^(5(KsntL85|d)y80Yk8Z}_jiN$Jw7s#Odx;{PDlDdLnX yP?aK1xlO1_`;F)-*O)H|f*=TjAP9mW?9K;~E^CkQH+8iD0000 { userInfo.value = { ...userInfoState } - removeToken() + uni.removeStorageSync('userInfo') + uni.removeStorageSync('token') } /** * 用户登录 @@ -59,14 +60,16 @@ export const useUserStore = defineStore( const res = await _login(credentials) console.log('登录信息', res) toast.success('登录成功') - setToken(res.data.token) + const userInfo = res.data + uni.setStorageSync('userInfo', userInfo) + uni.setStorageSync('token', userInfo.token) return res } /** * 获取用户信息 */ const getUserInfo = async () => { - const res = await _getUserInfo(getToken()) + const res = await _getUserInfo() setUserInfo(res.data) // TODO 这里可以增加获取用户路由的方法 根据用户的角色动态生成路由 return res @@ -80,11 +83,16 @@ export const useUserStore = defineStore( } /** * 微信登录 - * @param credentials 微信登录Code */ - const wxLogin = async (credentials: { code: string }) => { - const res = await _wxLogin(credentials) - setToken(res.data.token) + const wxLogin = async () => { + // 获取微信小程序登录的code + const data = await getWxCode() + console.log('微信登录code', data) + + const res = await _wxLogin(data) + const userInfo = res.data + uni.setStorageSync('userInfo', userInfo) + uni.setStorageSync('token', userInfo.token) return res } diff --git a/src/utils/auth.ts b/src/utils/auth.ts deleted file mode 100644 index 3d071e4..0000000 --- a/src/utils/auth.ts +++ /dev/null @@ -1,81 +0,0 @@ -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/uploadFile.ts b/src/utils/uploadFile.ts index 594f24c..b415a08 100644 --- a/src/utils/uploadFile.ts +++ b/src/utils/uploadFile.ts @@ -1,4 +1,3 @@ -import { getToken, getTokenKey } from './auth' import { toast } from './toast' /** @@ -286,7 +285,6 @@ function uploadFile({ // #ifndef H5 'Content-Type': 'multipart/form-data', // #endif - [getTokenKey()]: getToken(), // 添加认证token }, // 确保文件名称合法 success: (uploadFileRes) => { From cc56472da6715fd89fef81747a8fbd48b084bb0a Mon Sep 17 00:00:00 2001 From: feige996 <1020102647@qq.com> Date: Wed, 28 May 2025 00:33:41 +0800 Subject: [PATCH 4/4] =?UTF-8?q?feat(=E7=99=BB=E5=BD=95):=20=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/login.typings.ts | 8 +------- src/pages/login/index.vue | 4 ---- src/pages/mine/index.vue | 15 +++++++-------- src/store/user.ts | 23 ++++++++--------------- 4 files changed, 16 insertions(+), 34 deletions(-) diff --git a/src/api/login.typings.ts b/src/api/login.typings.ts index d344f05..7b79431 100644 --- a/src/api/login.typings.ts +++ b/src/api/login.typings.ts @@ -4,14 +4,8 @@ export type IUserInfoVo = { id: number username: string - name: string - sex: string - email: string - phone: string avatar: string - createTime: string - roles: string[] - permissions: string[] + token: string } /** diff --git a/src/pages/login/index.vue b/src/pages/login/index.vue index 7610f20..5195f14 100644 --- a/src/pages/login/index.vue +++ b/src/pages/login/index.vue @@ -187,8 +187,6 @@ const handleAccountLogin = async () => { } // 执行登录 await userStore.login(loginForm.value) - // 获取用户信息 - await userStore.getUserInfo() // 跳转到首页或重定向页面 const targetUrl = redirectRoute.value || '/pages/index/index' if (isTableBar(targetUrl)) { @@ -212,8 +210,6 @@ const handleWechatLogin = async () => { } // 微信登录 await userStore.wxLogin() - // 获取用户信息 - await userStore.getUserInfo() // 跳转到首页或重定向页面 const targetUrl = redirectRoute.value || '/pages/index/index' if (isTableBar(targetUrl)) { diff --git a/src/pages/mine/index.vue b/src/pages/mine/index.vue index 881cb10..c22773c 100644 --- a/src/pages/mine/index.vue +++ b/src/pages/mine/index.vue @@ -8,16 +8,17 @@