Переглянути джерело

feat(个人中心): 添加个人中心页面及相关功能

- 新增个人中心页面,包含用户信息展示、头像上传、登录/退出功能
- 更新路由配置和底部导航栏以支持个人中心页面
- 修改登录跳转路径从'/pages/common/login/index'到'/pages/login/login'
- 在cSpell字典中添加'chooseavatar'单词
- 更新tsconfig.json包含package.json文件
feige996 8 місяців тому
батько
коміт
1747357e8b
7 змінених файлів з 255 додано та 14 видалено
  1. 1 0
      .vscode/settings.json
  2. 1 1
      src/http/alova.ts
  3. 13 0
      src/pages.json
  4. 219 0
      src/pages/me/me.vue
  5. 15 8
      src/tabbar/config.ts
  6. 5 5
      src/utils/index.ts
  7. 1 0
      tsconfig.json

+ 1 - 0
.vscode/settings.json

@@ -71,6 +71,7 @@
   "cSpell.words": [
     "alova",
     "Aplipay",
+    "chooseavatar",
     "climblee",
     "commitlint",
     "dcloudio",

+ 1 - 1
src/http/alova.ts

@@ -30,7 +30,7 @@ const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthenticati
       }
       catch (error) {
         // 切换到登录页
-        await uni.reLaunch({ url: '/pages/common/login/index' })
+        await uni.reLaunch({ url: '/pages/login/login' })
         throw error
       }
     },

+ 13 - 0
src/pages.json

@@ -36,6 +36,12 @@
         "selectedIconPath": "static/tabbar/exampleHL.png",
         "pagePath": "pages/about/about",
         "text": "关于"
+      },
+      {
+        "iconPath": "static/tabbar/personal.png",
+        "selectedIconPath": "static/tabbar/personalHL.png",
+        "pagePath": "pages/me/me",
+        "text": "个人"
       }
     ]
   },
@@ -88,6 +94,13 @@
       "style": {
         "navigationBarTitleText": "注册"
       }
+    },
+    {
+      "path": "pages/me/me",
+      "type": "page",
+      "style": {
+        "navigationBarTitleText": "我的"
+      }
     }
   ],
   "subPackages": [

+ 219 - 0
src/pages/me/me.vue

@@ -0,0 +1,219 @@
+<route lang="json5">
+{
+  style: {
+    navigationBarTitleText: '我的',
+  },
+}
+</route>
+
+<script lang="ts" setup>
+import type { IUploadSuccessInfo } from '@/api/types/login'
+import { storeToRefs } from 'pinia'
+import { useUserStore } from '@/store'
+import { useUpload } from '@/utils/uploadFile'
+
+const userStore = useUserStore()
+// 使用storeToRefs解构userInfo
+const { userInfo } = storeToRefs(userStore)
+const hasLogin = ref(false)
+
+onShow((options) => {
+  hasLogin.value = !!uni.getStorageSync('token')
+  console.log('个人中心onShow', hasLogin.value, options)
+
+  hasLogin.value && useUserStore().getUserInfo()
+})
+// #ifndef MP-WEIXIN
+// 上传头像
+const { run: uploadAvatar } = useUpload<IUploadSuccessInfo>(
+  import.meta.env.VITE_UPLOAD_BASEURL,
+  {},
+  {
+    onSuccess: (res) => {
+      console.log('h5头像上传成功', res)
+      useUserStore().setUserAvatar(res.url)
+    },
+  },
+)
+// #endif
+
+// 微信小程序下登录
+async function handleLogin() {
+  // #ifdef MP-WEIXIN
+
+  // 微信登录
+  await userStore.wxLogin()
+  hasLogin.value = true
+  // #endif
+  // #ifndef MP-WEIXIN
+  uni.navigateTo({ url: '/pages/login/login' })
+  // #endif
+}
+
+// #ifdef MP-WEIXIN
+
+// 微信小程序下选择头像事件
+function onChooseAvatar(e: any) {
+  console.log('选择头像', e.detail)
+  const { avatarUrl } = e.detail
+  const { run } = useUpload<IUploadSuccessInfo>(
+    import.meta.env.VITE_UPLOAD_BASEURL,
+    {},
+    {
+      onSuccess: (res) => {
+        console.log('wx头像上传成功', res)
+        useUserStore().setUserAvatar(res.url)
+      },
+    },
+    avatarUrl,
+  )
+  run()
+}
+// #endif
+// #ifdef MP-WEIXIN
+// 微信小程序下设置用户名
+function getUserInfo(e: any) {
+  console.log(e.detail)
+}
+// #endif
+
+// 退出登录
+function handleLogout() {
+  uni.showModal({
+    title: '提示',
+    content: '确定要退出登录吗?',
+    success: (res) => {
+      if (res.confirm) {
+        // 清空用户信息
+        useUserStore().logout()
+        hasLogin.value = false
+        // 执行退出登录逻辑
+        uni.showToast({
+          title: '退出登录成功',
+          icon: 'success',
+        })
+        // #ifdef MP-WEIXIN
+        // 微信小程序,去首页
+        // uni.reLaunch({ url: '/pages/index/index' })
+        // #endif
+        // #ifndef MP-WEIXIN
+        // 非微信小程序,去登录页
+        // uni.navigateTo({ url: '/pages/login/login' })
+        // #endif
+      }
+    },
+  })
+}
+</script>
+
+<template>
+  <view class="profile-container">
+    <!-- 用户信息区域 -->
+    <view class="user-info-section">
+      <!-- #ifdef MP-WEIXIN -->
+      <button class="avatar-button" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
+        <image :src="userInfo.avatar" mode="scaleToFill" class="h-full w-full" />
+      </button>
+      <!-- #endif -->
+      <!-- #ifndef MP-WEIXIN -->
+      <view class="avatar-wrapper" @click="uploadAvatar">
+        <image :src="userInfo.avatar" mode="scaleToFill" class="h-full w-full" />
+      </view>
+      <!-- #endif -->
+      <view class="user-details">
+        <!-- #ifdef MP-WEIXIN -->
+        <input
+          v-model="userInfo.username"
+          type="nickname"
+          class="weui-input"
+          placeholder="请输入昵称"
+        >
+        <!-- #endif -->
+        <!-- #ifndef MP-WEIXIN -->
+        <view class="username">
+          {{ userInfo.username }}
+        </view>
+        <!-- #endif -->
+        <view class="user-id">
+          ID: {{ userInfo.id }}
+        </view>
+      </view>
+    </view>
+
+    <view class="mt-3 break-all px-3">
+      {{ JSON.stringify(userInfo, null, 2) }}
+    </view>
+
+    <view class="mt-20 px-3">
+      <view class="m-auto w-160px text-center">
+        <button v-if="hasLogin" type="warn" class="w-full" @click="handleLogout">
+          退出登录
+        </button>
+        <button v-else type="primary" class="w-full" @click="handleLogin">
+          登录
+        </button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<style lang="scss" scoped>
+/* 基础样式 */
+.profile-container {
+  overflow: hidden;
+  font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif;
+  // background-color: #f7f8fa;
+}
+/* 用户信息区域 */
+.user-info-section {
+  display: flex;
+  align-items: center;
+  padding: 40rpx;
+  margin: 30rpx 30rpx 20rpx;
+  background-color: #fff;
+  border-radius: 24rpx;
+  box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
+  transition: all 0.3s ease;
+}
+
+.avatar-wrapper {
+  width: 160rpx;
+  height: 160rpx;
+  margin-right: 40rpx;
+  overflow: hidden;
+  border: 4rpx solid #f5f5f5;
+  border-radius: 50%;
+  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
+}
+.avatar-button {
+  height: 160rpx;
+  padding: 0;
+  margin-right: 40rpx;
+  overflow: hidden;
+  border: 4rpx solid #f5f5f5;
+  border-radius: 50%;
+  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
+}
+.user-details {
+  flex: 1;
+}
+
+.username {
+  margin-bottom: 12rpx;
+  font-size: 38rpx;
+  font-weight: 600;
+  color: #333;
+  letter-spacing: 0.5rpx;
+}
+
+.user-id {
+  font-size: 28rpx;
+  color: #666;
+}
+
+.user-created {
+  margin-top: 8rpx;
+  font-size: 24rpx;
+  color: #999;
+}
+</style>

+ 15 - 8
src/tabbar/config.ts

@@ -38,6 +38,12 @@ export const nativeTabbarList: NativeTabBarItem[] = [
     pagePath: 'pages/about/about',
     text: '关于',
   },
+  {
+    iconPath: 'static/tabbar/personal.png',
+    selectedIconPath: 'static/tabbar/personalHL.png',
+    pagePath: 'pages/me/me',
+    text: '个人',
+  },
 ]
 
 export interface CustomTabBarItem {
@@ -71,17 +77,18 @@ export const customTabbarList: CustomTabBarItem[] = [
     icon: 'i-carbon-code',
     // badge: 10,
   },
-
-  // {
-  //   pagePath: 'pages/mine/index',
-  //   text: '我的',
-  //   // 注意 iconfont 图标需要额外加上 'iconfont',如下
-  //   iconType: 'iconfont',
-  //   icon: 'iconfont icon-my',
-  // },
+  {
+    pagePath: 'pages/me/me',
+    text: '我的',
+    iconType: 'uniUi',
+    icon: 'contact',
+  },
   // {
   //   pagePath: 'pages/index/index',
   //   text: '首页',
+  // 注意 iconfont 图标需要额外加上 'iconfont',如下
+  // iconType: 'iconfont',
+  // icon: 'iconfont icon-my',
   //   // 使用 ‘image’时,需要配置 icon + iconActive 2张图片(不推荐)
   //   // 既然已经用了自定义tabbar了,就不建议用图片了,所以不推荐
   //   iconType: 'image',

+ 5 - 5
src/utils/index.ts

@@ -12,7 +12,7 @@ export function getLastPage() {
 
 /**
  * 获取当前页面路由的 path 路径和 redirectPath 路径
- * path 如 '/pages/login/index'
+ * path 如 '/pages/login/login'
  * redirectPath 如 '/pages/demo/base/route-interceptor'
  */
 export function currRoute() {
@@ -25,8 +25,8 @@ export function currRoute() {
   // 经过多端测试,只有 fullPath 靠谱,其他都不靠谱
   const { fullPath } = currRoute as { fullPath: string }
   // console.log(fullPath)
-  // eg: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor (小程序)
-  // eg: /pages/login/index?redirect=%2Fpages%2Froute-interceptor%2Findex%3Fname%3Dfeige%26age%3D30(h5)
+  // eg: /pages/login/login?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor (小程序)
+  // eg: /pages/login/login?redirect=%2Fpages%2Froute-interceptor%2Findex%3Fname%3Dfeige%26age%3D30(h5)
   return getUrlObj(fullPath)
 }
 
@@ -38,8 +38,8 @@ export function ensureDecodeURIComponent(url: string) {
 }
 /**
  * 解析 url 得到 path 和 query
- * 比如输入url: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor
- * 输出: {path: /pages/login/index, query: {redirect: /pages/demo/base/route-interceptor}}
+ * 比如输入url: /pages/login/login?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor
+ * 输出: {path: /pages/login/login, query: {redirect: /pages/demo/base/route-interceptor}}
  */
 export function getUrlObj(url: string) {
   const [path, queryStr] = url.split('?')

+ 1 - 0
tsconfig.json

@@ -29,6 +29,7 @@
     "plugins": ["@uni-helper/uni-types/volar-plugin"]
   },
   "include": [
+    "package.json",
     "src/**/*.ts",
     "src/**/*.js",
     "src/**/*.d.ts",