diff --git a/package.json b/package.json index 1db893e..2517e31 100644 --- a/package.json +++ b/package.json @@ -152,6 +152,6 @@ "vite": "6.3.5", "vite-plugin-restart": "^0.4.2", "vitepress": "^1.5.0", - "vue-tsc": "^1.8.27" + "vue-tsc": "^2.2.10" } } 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 58faa82..7584ddd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -208,8 +208,8 @@ importers: specifier: ^1.5.0 version: 1.6.3(@algolia/client-search@5.25.0)(@types/node@20.17.9)(axios@1.7.9)(postcss@8.4.49)(sass@1.77.8)(search-insights@2.17.3)(terser@5.36.0)(typescript@5.7.2) 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: @@ -2488,14 +2488,14 @@ packages: vite: ^5.0.0 || ^6.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==} @@ -2549,6 +2549,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'} @@ -2565,8 +2568,8 @@ packages: '@vue/devtools-shared@7.7.6': resolution: {integrity: sha512-yFEgJZ/WblEsojQQceuyK6FzpFDx4kqrz2ohInxNj5/DnhoX023upTv4OD6lNPLAA5LLkbwPVb10o/7b+Y4FVA==} - '@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: @@ -2717,6 +2720,9 @@ packages: resolution: {integrity: sha512-n73BVorL4HIwKlfJKb4SEzAYkR3Buwfwbh+MYxg2mloFph2fFGV58E90QTzdbfzWrLn4HE5Czx/WTjI8fcHaMg==} engines: {node: '>= 14.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'} @@ -3050,9 +3056,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==} @@ -4424,8 +4427,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==} @@ -5077,11 +5080,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'} @@ -5652,6 +5650,9 @@ packages: postcss: 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'} @@ -5679,14 +5680,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==} @@ -9051,18 +9049,17 @@ snapshots: vite: 5.4.19(@types/node@20.17.9)(sass@1.77.8)(terser@5.36.0) 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': {} @@ -9184,6 +9181,11 @@ 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': {} @@ -9206,17 +9208,16 @@ snapshots: dependencies: rfdc: 1.4.1 - '@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 @@ -9345,6 +9346,8 @@ snapshots: '@algolia/requester-fetch': 5.25.0 '@algolia/requester-node-http': 5.25.0 + alien-signals@1.0.13: {} + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -9732,8 +9735,6 @@ snapshots: compare-versions@3.6.0: {} - computeds@0.0.1: {} - concat-map@0.0.1: {} confbox@0.1.8: {} @@ -11379,7 +11380,7 @@ snapshots: ms@2.1.3: {} - muggle-string@0.3.1: {} + muggle-string@0.4.1: {} nanoid@3.3.11: {} @@ -12035,8 +12036,6 @@ snapshots: semver@6.3.1: {} - semver@7.6.3: {} - semver@7.7.2: {} send@0.19.0: @@ -12692,6 +12691,8 @@ snapshots: - typescript - universal-cookie + 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) @@ -12713,16 +12714,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): diff --git a/src/api/login.ts b/src/api/login.ts new file mode 100644 index 0000000..effdd94 --- /dev/null +++ b/src/api/login.ts @@ -0,0 +1,83 @@ +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 = () => { + return http.get('/user/info') +} + +/** + * 退出登录 + */ +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)), + }) + }) +} + +/** + * 微信登录参数 + */ + +/** + * 微信登录 + * @param params 微信登录参数,包含code + * @returns Promise 包含登录结果 + */ +export const wxLogin = (data: { code: string }) => { + return http.post('/user/wxLogin', data) +} diff --git a/src/api/login.typings.ts b/src/api/login.typings.ts new file mode 100644 index 0000000..7b79431 --- /dev/null +++ b/src/api/login.typings.ts @@ -0,0 +1,57 @@ +/** + * 用户信息 + */ +export type IUserInfoVo = { + id: number + username: string + avatar: string + token: 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..5195f14 --- /dev/null +++ b/src/pages/login/index.vue @@ -0,0 +1,584 @@ + +{ + 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..c22773c --- /dev/null +++ b/src/pages/mine/index.vue @@ -0,0 +1,367 @@ + +{ + 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/static/images/default-avatar.png b/src/static/images/default-avatar.png new file mode 100644 index 0000000..4eb5879 Binary files /dev/null and b/src/static/images/default-avatar.png differ diff --git a/src/store/user.ts b/src/store/user.ts index 82bd873..a337e70 100644 --- a/src/store/user.ts +++ b/src/store/user.ts @@ -1,32 +1,100 @@ +import { + login as _login, + getUserInfo as _getUserInfo, + wxLogin as _wxLogin, + logout as _logout, + getWxCode, +} from '@/api/login' import { defineStore } from 'pinia' import { ref } from 'vue' +import { toast } from '@/utils/toast' +import { IUserInfoVo } from '@/api/login.typings' -const initState = { nickname: '', avatar: '' } +// 初始化状态 +const userInfoState: IUserInfoVo = { + id: 0, + username: '', + avatar: '/static/images/default-avatar.png', + token: '', +} 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 removeUserInfo = () => { + userInfo.value = { ...userInfoState } + uni.removeStorageSync('userInfo') + uni.removeStorageSync('token') + } + /** + * 用户登录 + * @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('登录成功') + getUserInfo() + return res + } + /** + * 获取用户信息 + */ + const getUserInfo = async () => { + const res = await _getUserInfo() + const userInfo = res.data + setUserInfo(userInfo) + uni.setStorageSync('userInfo', userInfo) + uni.setStorageSync('token', userInfo.token) + // TODO 这里可以增加获取用户路由的方法 根据用户的角色动态生成路由 + return res + } + /** + * 退出登录 并 删除用户信息 + */ + const logout = async () => { + _logout() + removeUserInfo() + } + /** + * 微信登录 + */ + const wxLogin = async () => { + // 获取微信小程序登录的code + const data = await getWxCode() + console.log('微信登录code', data) - const clearUserInfo = () => { - userInfo.value = { ...initState } + const res = await _wxLogin(data) + getUserInfo() + return res } - // 一般没有reset需求,不需要的可以删除 - const reset = () => { - userInfo.value = { ...initState } - } - 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/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..b415a08 --- /dev/null +++ b/src/utils/uploadFile.ts @@ -0,0 +1,336 @@ +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 + }, + // 确保文件名称合法 + 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('创建上传任务失败')) + } +}