Bläddra i källkod

增加修改密码和修改个人信息页面

快乐的梦鱼 2 veckor sedan
förälder
incheckning
8ce03ce4e6

+ 39 - 1
src/api/CommonContent.ts

@@ -2,7 +2,7 @@ import { DataModel, transformArrayDataModel, type NewDataModel } from '@imengyu/
 import ApiCofig from '@/common/config/ApiCofig';
 import { AppServerRequestModule } from './RequestModules';
 import { transformSomeToArray } from './Utils';
-import type { QueryParams } from '@imengyu/imengyu-utils/dist/request';
+import { RequestApiConfig, RequestOptions, type QueryParams } from '@imengyu/imengyu-utils/dist/request';
 
 export class GetColumListParams extends DataModel<GetColumListParams> {
   
@@ -423,6 +423,44 @@ 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, '默认通用内容');

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

@@ -113,6 +113,31 @@ export class UserApi extends AppServerRequestModule<DataModel> {
       main_body_user_id,
     }, '获取用户信息', undefined, UserInfo)).data as UserInfo;
   }
+  async updatePassword(data: {
+    newpassword: string,
+    oldpassword: string,
+  }) {
+    return (await this.post('/content/main_body_user/changepwd', data, '更新密码'))
+  }
+  async updateUserInfo(data: {
+    nickname?: string,
+    avatar?: string,
+    intro?: string,
+    password?: string,
+  }) {
+    return (await this.post('/content/main_body_user/editMainBodyUser', data, '更新用户信息'))
+  }
+  async updateSystemUserInfo(data: {
+    nickname?: string,
+    avatar?: string,
+    bio?: string,
+  }) {
+    return (await this.post('/user/profile', {
+      nickname: data?.nickname,
+      avatar: data?.avatar,
+      bio: data?.bio,
+    }, '更新用户信息'))
+  }
   async refresh() {
     return (await this.post('/content/main_body_user/refreshUser', {
     }, '刷新用户', undefined, LoginResult)).data as LoginResult;

+ 14 - 0
src/pages.json

@@ -26,6 +26,20 @@
       }
     },
     {
+      "path": "pages/user/update/password",
+      "style": {
+        "navigationBarTitleText": "修改密码",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/user/update/profile",
+      "style": {
+        "navigationBarTitleText": "修改个人信息",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
       "path": "pages/article/editor/editor",
       "style": {
         "navigationBarTitleText": "编辑文章",

+ 1 - 1
src/pages/user/index.vue

@@ -1,6 +1,6 @@
 <template>
   <view class="main" style="padding-bottom:50rpx;">
-    <view v-if="userInfo" class="user-info">
+    <view v-if="userInfo" class="user-info" @click="navTo('/pages/user/update/profile')">
       <image :src="userInfo.avatar" mode="aspectFill" class="avatar"></image>
       <view class="info">
         <text class="nickname">{{ userInfo.nickname }}</text>

+ 213 - 0
src/pages/user/update/password.vue

@@ -0,0 +1,213 @@
+<template>
+  <view class="password-page">
+    <view class="content">
+      <uni-forms ref="formRef" :modelValue="formData" :rules="rules">
+        <uni-forms-item name="oldpassword" label="当前密码" required>
+          <uni-easyinput 
+            type="password" 
+            v-model="formData.oldpassword" 
+            placeholder="请输入当前密码"
+            maxlength="20"
+          />
+        </uni-forms-item>
+        
+        <uni-forms-item name="newpassword" label="新密码" required>
+          <uni-easyinput 
+            type="password" 
+            v-model="formData.newpassword" 
+            placeholder="请输入新密码"
+            maxlength="20"
+          />
+        </uni-forms-item>
+        
+        <uni-forms-item name="confirmPassword" label="确认新密码" required>
+          <uni-easyinput 
+            type="password" 
+            v-model="formData.confirmPassword" 
+            placeholder="请再次输入新密码"
+            maxlength="20"
+          />
+        </uni-forms-item>
+        
+        <view class="tips">
+          密码建议:8-20位,包含字母和数字,不要使用过于简单的密码
+        </view>
+        
+        <u-button type="primary" :loading="loading"  @click="submitForm" >
+          保存修改
+        </u-button>
+      </uni-forms>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue';
+import userApi from '@/api/auth/UserApi';
+
+const formRef = ref<any>(null);
+const loading = ref(false);
+
+// 表单数据
+const formData = reactive({
+  oldpassword: '',
+  newpassword: '',
+  confirmPassword: ''
+});
+
+// 表单验证规则
+const rules = {
+  oldpassword: {
+    rules: [
+      { required: true, errorMessage: '请输入当前密码' },
+      { minLength: 6, errorMessage: '密码长度至少6位' }
+    ] 
+  },
+  newpassword: {
+    rules: [
+      { required: true, errorMessage: '请输入新密码' },
+      { minLength: 6, errorMessage: '新密码长度至少6位' },
+      {
+        validator: (rule: any, value: string) => {
+          // 密码强度校验:至少包含字母和数字
+          const hasLetter = /[a-zA-Z]/.test(value);
+          const hasNumber = /[0-9]/.test(value);
+          return hasLetter && hasNumber;
+        },
+        errorMessage: '新密码必须包含字母和数字'
+      }
+    ] 
+  },
+  confirmPassword: {
+    rules: [
+      { required: true, errorMessage: '请确认新密码' },
+      {
+        validator: (rule: any, value: string) => {
+          return value === formData.newpassword;
+        },
+        errorMessage: '两次输入的密码不一致'
+      }
+    ] 
+  }
+};
+
+// 提交表单
+const submitForm = async () => {
+  console.log('submitForm:', formData);  
+
+  // 表单验证
+  try {
+    await formRef.value?.validate();
+  } catch (error) {
+    console.error('表单验证失败:', error);  
+    return;
+  }
+  
+  loading.value = true;
+  
+  try {
+    // 调用修改密码API
+    await userApi.updatePassword({
+      oldpassword: formData.oldpassword,
+      newpassword: formData.newpassword
+    });
+    
+    // 显示成功提示
+    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>
+.password-page {
+  min-height: 100vh;
+  background-color: #f5f5f5;
+  display: flex;
+  flex-direction: column;
+}
+
+.header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 16px;
+  height: 44px;
+  background-color: #ffffff;
+  position: sticky;
+  top: 0;
+  z-index: 10;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+.title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #333333;
+}
+
+.placeholder {
+  width: 24px;
+}
+
+.content {
+  flex: 1;
+  padding: 16px;
+}
+
+uni-forms {
+  background-color: #ffffff;
+  border-radius: 8px;
+  padding: 16px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+}
+
+uni-forms-item {
+  margin-bottom: 20px;
+}
+
+uni-forms-item:last-child {
+  margin-bottom: 0;
+}
+
+uni-easyinput {
+  border-bottom: 1px solid #f0f0f0;
+  padding: 8px 0;
+}
+
+uni-easyinput:focus {
+  border-bottom-color: #007aff;
+}
+
+.tips {
+  font-size: 12px;
+  color: #999999;
+  margin: 16px 0;
+  line-height: 1.5;
+}
+
+.submit-btn {
+  margin-top: 24px;
+  border-radius: 8px;
+  height: 44px;
+  font-size: 16px;
+}
+</style>

+ 313 - 0
src/pages/user/update/profile.vue

@@ -0,0 +1,313 @@
+<template>
+  <view class="profile-page">
+    <uni-forms ref="formRef" :model="formModel" :rules="rules" validate-trigger="submit">
+      <!-- 头像 -->
+      <view class="avatar-section">
+        <view class="avatar-container" @click="handleAvatarClick">
+          <image 
+            :src="formModel.avatar || '/static/images/default-avatar.png'" 
+            class="avatar-image" 
+            mode="aspectFill"
+          ></image>
+          <view class="avatar-edit-mask">
+            <uni-icons type="camera" size="24" color="#ffffff"></uni-icons>
+          </view>
+        </view>
+      </view>
+
+      <!-- 昵称 -->
+      <uni-forms-item name="nickname" label="昵称" required>
+        <uni-easyinput 
+          v-model="formModel.nickname" 
+          placeholder="请输入昵称"
+          maxlength="20"
+        />
+      </uni-forms-item>
+
+      <!-- 个人简介 -->
+      <uni-forms-item name="bio" label="个人简介">
+        <uni-easyinput 
+          v-model="formModel.bio" 
+          type="textarea"
+          placeholder="介绍一下自己吧"
+          maxlength="100"
+          :height="100"
+          show-word-limit
+        />
+      </uni-forms-item>
+
+      <!-- 提交按钮 -->    
+      <u-button type="primary" :loading="loading" @click="submitForm" >
+        保存修改
+      </u-button>
+      <view class="mt-3" /> 
+      <u-button type="primary" :plain="true" @click="navTo('/pages/user/update/password')">
+        修改密码
+      </u-button>
+    </uni-forms>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import userApi from '@/api/auth/UserApi';
+import CommonContent from '@/api/CommonContent';
+import { useAuthStore } from '@/store/auth';
+import { navTo } from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
+
+const authStore = useAuthStore();
+const formRef = ref<any>(null);
+const loading = ref(false);
+const uploading = ref(false);
+
+// 表单数据
+const formModel = ref({
+  avatar: '',
+  nickname: '',
+  bio: '',
+});
+
+// 表单验证规则
+const rules = {
+  nickname: {
+    rules: [
+      { required: true, errorMessage: '请输入昵称' },
+      { minLength: 2, errorMessage: '昵称长度至少2个字符' },
+      { maxLength: 20, errorMessage: '昵称长度最多20个字符' }
+    ]
+  },
+  bio: {
+    rules: [
+      { maxLength: 100, errorMessage: '个人简介最多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 {
+    // 调用修改头像API
+    await userApi.updateSystemUserInfo({
+      avatar: avatarUrl
+    });
+    
+    // 更新表单数据和store
+    formModel.value.avatar = avatarUrl;
+    if (authStore.userInfo) {
+      authStore.userInfo.avatar = avatarUrl;
+    }
+    
+    // 显示成功提示
+    uni.showToast({
+      title: '头像更新成功',
+      icon: 'success',
+      duration: 2000
+    });
+    
+  } catch (error: any) {
+    throw new Error(error?.message || '头像更新失败');
+  }
+};
+
+// 页面加载时获取用户信息
+onMounted(() => {
+  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 () => {
+  // 表单验证
+  const valid = await formRef.value?.validate();
+  if (!valid) return;
+  
+  loading.value = true;
+  
+  try {
+    // 调用修改个人信息API
+    await userApi.updateSystemUserInfo({
+      nickname: formModel.value.nickname,
+      bio: formModel.value.bio
+    });
+    
+    // 更新store中的用户信息
+    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>
+.profile-page {
+  min-height: 100vh;
+  background-color: #f5f5f5;
+  padding: 16px;
+}
+
+.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: inline-block;
+  margin-bottom: 8px;
+}
+
+.avatar-image {
+  width: 100px;
+  height: 100px;
+  border-radius: 50%;
+  border: 2px solid #e0e0e0;
+  background-color: #f5f5f5;
+}
+
+.avatar-edit-mask {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  border-radius: 50%;
+  background-color: rgba(0, 0, 0, 0.3);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  opacity: 0;
+  transition: opacity 0.3s ease;
+}
+
+.avatar-container:hover .avatar-edit-mask {
+  opacity: 1;
+}
+
+.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;
+}
+
+uni-forms {
+  background-color: #ffffff;
+  border-radius: 8px;
+  padding: 16px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+}
+
+uni-forms-item {
+  margin-bottom: 20px;
+}
+
+uni-forms-item:last-child {
+  margin-bottom: 0;
+}
+
+uni-easyinput {
+  border-bottom: 1px solid #f0f0f0;
+  padding: 8px 0;
+}
+
+uni-easyinput:focus {
+  border-bottom-color: #007aff;
+}
+
+.submit-btn {
+  margin-top: 32px;
+  border-radius: 8px;
+  height: 44px;
+  font-size: 16px;
+}
+</style>