Parcourir la source

🎨 修改我的页面细节问题

快乐的梦鱼 il y a 2 mois
Parent
commit
14f160447d

+ 41 - 1
src/api/CommonContent.ts

@@ -2,7 +2,7 @@ import { DataModel, transformArrayDataModel, type NewDataModel } from '@imengyu/
 import { AppServerRequestModule } from './RequestModules';
 import ApiCofig from '@/common/config/ApiCofig';
 import { transformSomeToArray } from './Utils';
-import type { QueryParams } from '@imengyu/imengyu-utils';
+import { RequestApiConfig, RequestOptions, type QueryParams } from '@imengyu/imengyu-utils';
 
 export class GetColumListParams extends DataModel<GetColumListParams> {
   
@@ -472,6 +472,46 @@ export class CommonContentApi extends AppServerRequestModule<DataModel> {
       .then(res => res.data as T)
       .catch(e => { throw e });
   }
+
+  
+  /**
+   * 上传文件到服务器
+   */
+  async uploadFile(file: string, fileType?: "image" | "video" | "audio" | undefined, name = 'file', data?: any) {
+    return new Promise<{
+      fullurl: string,
+      url: string
+    }>((resolve, reject) => {
+      let url = RequestApiConfig.getConfig().BaseUrl + '/common/upload';
+      let req : RequestOptions = {
+        method: 'POST',
+        data: data,
+        header: {},
+      }
+      if (this.config.requestInceptor) {
+        const { newReq, newUrl } = this.config.requestInceptor(url, req);
+        url = newUrl;
+        data = newReq;
+      }
+      uni.uploadFile({
+        url: url,
+        name,
+        header: req.header,
+        filePath: file,
+        formData: data,
+        fileType,
+        success: (result) => {
+          let data = JSON.parse(result.data);
+          if (data.code !== 1)
+            throw new Error(data.msg ?? 'code: ' + data.code);
+          resolve(data.data);
+        },
+        fail(result) {
+          reject(result);
+        },
+      })
+    })
+  }
 }
 
 export default new CommonContentApi(undefined, 0, '默认通用内容');

+ 8 - 0
src/api/auth/UserApi.ts

@@ -99,6 +99,14 @@ export class UserApi extends AppServerRequestModule<DataModel> {
       main_body_user_id,
     }, '获取用户信息', undefined, UserInfo)).data as UserInfo;
   }
+  async updateUserInfo(data: {
+    nickname?: string,
+    avatar?: string,
+    intro?: string,
+    password?: string,
+  }) {
+    return (await this.post('/content/main_body_user/editMainBodyUser', data, '更新用户信息'))
+  }
   async refresh() {
     return (await this.post('/content/main_body_user/refreshUser', {
     }, '刷新用户', undefined, LoginResult)).data as LoginResult;

+ 2 - 0
src/components/basic/Image.vue

@@ -2,6 +2,7 @@
   <view 
     class="nana-image-wrapper"
     :style="style"
+    :class="innerClass"
     @click="handleClick"
   >
     <image 
@@ -98,6 +99,7 @@ export interface ImageProps {
    * 内部样式
    */
   innerStyle?: object;
+  innerClass?: string,
 }
 
 defineOptions({

+ 7 - 0
src/pages.json

@@ -247,6 +247,13 @@
       }
     },
     {
+      "path": "pages/user/profile/index",
+      "style": {
+        "navigationBarTitleText": "个人信息",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
       "path": "pages/user/contribute/list",
       "style": {
         "navigationBarTitleText": "我的投稿",

+ 50 - 73
src/pages/user/index.vue

@@ -1,32 +1,47 @@
 <template>
   <view class="home-container h-100vh d-flex flex-col bg-base page-user-index">
-    <image 
-      class="w-100 position-absolute"
+    <Image 
+      :innerStyle="{ position:'absolute' }"
+      width="100%"
       src="https://mncdn.wenlvti.net/app_static/minnan/images/mine/Banner.png"
       mode="widthFix"
     />
-    <image 
-      class="position-absolute title"
+    <Image 
+      :innerStyle="{ position:'absolute', }"
+      innerClass="title"
+      width="100rpx"
       src="https://mncdn.wenlvti.net/app_static/minnan/images/mine/Title.png"
       mode="widthFix"
     />
     <view class="content h-100 d-flex flex-col wing-l">
-      <view v-if="userInfo" class="user-info">
-        <image :src="userInfo.avatar" mode="aspectFill" class="avatar"></image>
-        <view class="info">
-          <text class="nickname">{{ userInfo.nickname }}</text>
-          <text class="extra"><text class="label">守护编号</text><text>{{ userInfo.id }}</text><text class="label point-label">积分</text><text>{{ userInfo.totalCheckins }}</text></text>
-        </view>
-        <text class="iconfont icon-arrow-right"></text>
-      </view>
-      <view v-else class="user-info" @click="navTo('login')">
-        <image :src="UserHead" mode="aspectFill" class="avatar"></image>
-        <view class="info">
-          <text class="nickname">点击登录</text>
-          <text class="extra"> 登录后您将获得更多权益</text>
-        </view>
-        <text class="iconfont icon-arrow-right"></text>
-      </view>
+      <Touchable 
+        direction="row"
+        justify="space-between" 
+        align="center"  
+        touchable 
+        :gap="25"
+        :margin="[36,0]"
+        @click="goUserProfile"
+      >
+        <FlexRow>
+          <Image 
+            :src="userInfo?.avatar"
+            :defaultImage="UserHead"
+            :failedImage="UserHead"
+            mode="aspectFill" 
+            class="avatar" 
+            width="100rpx"
+            height="100rpx"
+            round
+          />
+          <Width :size="20" />
+          <FlexCol>
+            <H4 color="white">{{ userInfo?.nickname || '欢迎登录' }}</H4>
+            <Text color="white" v-if="userInfo">守护编号 {{ userInfo.id }} 积分 {{ userInfo.totalCheckins }}</Text>
+          </FlexCol>
+        </FlexRow>
+        <Icon icon="arrow-right-bold" color="white" />
+      </Touchable>
 
       <CellGroup round>
         <Cell icon="https://mncdn.wenlvti.net/uploads/20250313/07f750b4cf4959654c40171fdae91c3a.png" title="去投稿" showArrow touchable @click="goContribute" />
@@ -63,6 +78,14 @@ import { computed } from 'vue';
 import { useReqireLogin } from '@/common/composeabe/RequireLogin';
 import CellGroup from '@/components/basic/CellGroup.vue';
 import Cell from '@/components/basic/Cell.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import Icon from '@/components/basic/Icon.vue';
+import Touchable from '@/components/feedback/Touchable.vue';
+import H4 from '@/components/typography/H4.vue';
+import Image from '@/components/basic/Image.vue';
+import Width from '@/components/layout/space/Width.vue';
+import Text from '@/components/basic/Text.vue';
 
 const UserHead = 'https://mncdn.wenlvti.net/app_static/minnan/images/home/UserHead.png';
 
@@ -84,6 +107,12 @@ function goContributeList() {
 function goContribute() {
   requireLogin(() => navTo('contribute/submit'), '登录后才能投稿哦!');
 }
+function goUserProfile() {
+  if (authStore.isLogged)
+    navTo('profile/index');
+  else
+    navTo('login');
+}
 function showService() {
   uni.showModal({
     title: '联系客服',
@@ -96,60 +125,8 @@ function showService() {
 
 <style lang="scss" scoped>
 .page-user-index {
-  > .content {
+  .content {
     margin-top: 10vh;
   }
-  > .title {
-    width: 100rpx;
-  }
-}
-.user-info{
-  display: flex;
-  align-items: center;
-  padding: 24rpx 8rpx 60rpx 8rpx;
-  image.avatar{
-    width: 127rpx;
-    height: 127rpx;
-    border-radius: 50%;
-    margin-right: 24rpx;
-  }
-  .info{
-    color:#111111;
-    flex:1;
-    .nickname{
-      font-weight: bold;
-      display: block;
-      font-size: 36rpx;
-      color: #333333;
-      margin-bottom: 20rpx;
-   }
-    .extra{
-      font-size: 24rpx;
-      text{
-        color: #333333;
-        font-weight: 600;
-      }
-      text.label{
-        display: inline-block;
-        margin-right: 10rpx;
-        color:#666666;
-        &.point-label{
-          margin-left: 24rpx;
-        }
-      }
-    }
-  }
-}
-.btn{
-  width: 148rpx;
-  height: 54rpx;
-  background: linear-gradient(0deg, #299365, rgba(41, 147, 101, 0.8));
-  border-radius: 27rpx;
-  font-weight: 400;
-  font-size: 24rpx;
-  color: #FFFFFF;
-  line-height: 54rpx;
-  text-align: center;
-  margin-right: 27rpx;
 }
 </style>

+ 233 - 0
src/pages/user/profile/index.vue

@@ -0,0 +1,233 @@
+<template>
+  <FlexCol height="100vh" :padding="30" backgroundColor="#f6f2e7">
+    <Form 
+      ref="formRef"
+      :model="formModel"
+      :rules="rules" 
+      validateTrigger="submit"
+      labelAlign="right"
+      :labelWidth="140"
+    >
+      <!-- 头像 -->
+      <view class="avatar-section">
+        <view class="avatar-container" @click="handleAvatarClick">
+          <image 
+            :src="formModel.avatar || DefaultAvatar" 
+            class="avatar-image" 
+            mode="aspectFill"
+          ></image>
+          <text class="avatar-hint">点击可修改头像</text>
+        </view>
+      </view>
+
+      <Field name="nickname" label="昵称" placeholder="请输入昵称" required />
+      <Field name="bio" multiline label="个人简介" placeholder="输入个人简介" :inputStyle="{width: '240px'}" />
+    </Form>
+
+    <Height :height="40" />
+
+    <Button type="primary" :loading="loading" @click="submitForm" >
+      保存修改
+    </Button>
+    <Height :height="20" />
+    <Button type="primary" scheme="plain" @click="back()">
+      返回
+    </Button>
+  </FlexCol>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import { useAuthStore } from '@/store/auth';
+import UserApi from '@/api/auth/UserApi';
+import CommonContent from '@/api/CommonContent';
+import Form from '@/components/form/Form.vue';
+import Field from '@/components/form/Field.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import Button from '@/components/basic/Button.vue';
+import Height from '@/components/layout/space/Height.vue';
+import type { Rules } from 'async-validator';
+import { back } from '@/components/utils/PageAction';
+
+const DefaultAvatar = 'https://mncdn.wenlvti.net/app_static/minnan/images/home/UserHead.png';
+const authStore = useAuthStore();
+const formRef = ref<any>(null);
+const loading = ref(false);
+const uploading = ref(false);
+
+const formModel = ref({
+  avatar: '',
+  nickname: '',
+  bio: '',
+});
+const rules : Rules = {
+  nickname: [
+    { required: true, message: '请输入昵称' },
+    { min: 2, message: '昵称长度至少2个字符' },
+    { max: 20, message: '昵称长度最多20个字符' }
+  ],
+  bio: [
+    { max: 100, message: '个人简介最多100个字符' }
+  ],
+};
+
+// 处理头像点击事件
+const handleAvatarClick = async () => {
+  try {
+    // 选择图片
+    const chooseResult = await uni.chooseImage({
+      count: 1,
+      sizeType: ['compressed'],
+      sourceType: ['album', 'camera'],
+    });
+    
+    const tempFilePath = chooseResult.tempFilePaths[0];
+    
+    // 上传图片
+    uploading.value = true;
+    const uploadResult = await CommonContent.uploadFile(tempFilePath, 'image');
+    
+    // 更新头像并保存到服务器
+    await updateAvatar(uploadResult.fullurl);
+    
+  } catch (error: any) {
+    if (error.errMsg !== 'chooseImage:fail cancel') {
+      uni.showToast({
+        title: '头像更换失败',
+        icon: 'none',
+        duration: 2000
+      });
+    }
+  } finally {
+    uploading.value = false;
+  }
+};
+// 更新头像到服务器
+const updateAvatar = async (avatarUrl: string) => {
+  try {
+    await UserApi.updateUserInfo({
+      avatar: avatarUrl
+    });
+    formModel.value.avatar = avatarUrl;
+    if (authStore.userInfo) {
+      authStore.userInfo.avatar = avatarUrl;
+      authStore.saveLoginState();
+    }
+    uni.showToast({
+      title: '头像更新成功',
+      icon: 'success',
+      duration: 2000
+    });
+    
+  } catch (error: any) {
+    throw new Error(error?.message || '头像更新失败');
+  }
+};
+
+onMounted(() => {
+  console.log(authStore.userInfo);
+  if (authStore.userInfo) {
+    formModel.value.avatar = authStore.userInfo.avatar || '';
+    formModel.value.nickname = authStore.userInfo.nickname || '';
+    formModel.value.bio = (authStore.userInfo.intro || authStore.userInfo.bio || '') as string;
+  }
+});
+
+// 提交表单
+const submitForm = async () => {
+  try {
+    await formRef.value?.validate();
+  } catch {
+    return;
+  }
+  
+  loading.value = true;
+  
+  try {
+    await UserApi.updateUserInfo({
+      nickname: formModel.value.nickname,
+      intro: formModel.value.bio
+    });
+    if (authStore.userInfo) {
+      authStore.userInfo.nickname = formModel.value.nickname;
+      authStore.userInfo.avatar = formModel.value.avatar;
+      authStore.userInfo.intro = formModel.value.bio;
+    }
+    uni.showToast({
+      title: '个人信息更新成功',
+      icon: 'success',
+      duration: 2000
+    });
+    setTimeout(() => {
+      uni.navigateBack();
+    }, 2000);
+    
+  } catch (error: any) {
+    uni.showToast({
+      title: error?.message || '更新失败,请稍后重试',
+      icon: 'none',
+      duration: 2000
+    });
+  } finally {
+    loading.value = false;
+  }
+};
+</script>
+
+<style scoped>
+.avatar-section {
+  text-align: center;
+}
+
+.avatar-label {
+  font-size: 14px;
+  color: #333333;
+  margin-bottom: 12px;
+  font-weight: 500;
+  text-align: left;
+}
+
+.avatar-container {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin-bottom: 18px;
+}
+
+.avatar-image {
+  width: 100px;
+  height: 100px;
+  border-radius: 50%;
+  border: 2px solid #e0e0e0;
+  background-color: #f5f5f5;
+}
+.avatar-hint {
+  font-size: 12px;
+  color: #007aff;
+  margin-top: 4px;
+}
+
+/* 上传中遮罩 */
+.uploading-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.5);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  z-index: 9999;
+}
+.uploading-content {
+  background-color: #ffffff;
+  padding: 20px;
+  border-radius: 8px;
+  text-align: center;
+}
+.uploading-content uni-loading {
+  margin-bottom: 10px;
+}
+</style>

+ 3 - 3
src/store/auth.ts

@@ -60,9 +60,9 @@ export const useAuthStore = defineStore('auth', {
       this.userId = loginResult.mainBodyUserInfo.id;
       this.userInfo = loginResult.mainBodyUserInfo;
       this.expireAt = loginResult.auth.expiresIn + Date.now();
-
-      console.log('loginResultHandle');
-      
+      this.saveLoginState();
+    },
+    saveLoginState() {
       uni.setStorage({ 
         key: STORAGE_KEY, 
         data: JSON.stringify({