WXL
5 天以前 2140c5aae2032673cef7055df9738b9bacf37816
pagesSub/my/profile.vue
@@ -1,114 +1,137 @@
<template>
  <view class="profile-container">
    <!-- 头像区域 -->
    <view class="avatar-section">
      <view class="avatar-wrapper" @tap="changeAvatar">
        <image :src="userInfo.avatar" mode="aspectFill" class="avatar" />
    <view class="avatar-section" @tap="changeAvatar">
      <view class="avatar-wrapper">
        <image :src="avatarUrl" mode="aspectFill" class="avatar" />
        <view class="edit-icon">
          <text class="iconfont icon-camera"></text>
          <text class="plus">+</text>
        </view>
      </view>
      <text class="tip">点击更换头像</text>
    </view>
    
    <!-- 基本信息 -->
    <!-- 个人信息 -->
    <view class="info-card">
      <view class="info-item" @tap="editNickname">
        <text class="label">昵称</text>
        <view class="value">
          <text>{{ userInfo.nickname }}</text>
          <text class="iconfont icon-arrow-right"></text>
      <view class="card-header">
        <text class="title">个人信息</text>
      </view>
      <view class="info-list">
        <view class="info-item">
          <text class="label">用户名称</text>
          <text class="value">{{ user.userName }}</text>
        </view>
        <view class="info-item">
          <text class="label">手机号码</text>
          <text class="value">{{ user.phonenumber }}</text>
        </view>
        <view class="info-item">
          <text class="label">用户邮箱</text>
          <text class="value">{{ user.email }}</text>
        </view>
        <view class="info-item" v-if="user.dept">
          <text class="label">所属部门</text>
          <text class="value">{{ user.dept.deptName }} / {{ postGroup }}</text>
        </view>
        <view class="info-item">
          <text class="label">所属角色</text>
          <text class="value">{{ roleGroup }}</text>
        </view>
        <view class="info-item">
          <text class="label">创建日期</text>
          <text class="value">{{ user.createTime }}</text>
        </view>
        </view>
      </view>
      
      <view class="info-item" @tap="editGender">
    <!-- 基本资料 -->
    <view class="info-card">
      <view class="card-header">
        <text class="title">基本资料</text>
      </view>
      <view class="form-list">
        <view class="form-item">
          <text class="label required">用户昵称</text>
          <input
            v-model="form.nickName"
            placeholder="请输入用户昵称"
            maxlength="30"
          />
        </view>
        <view class="form-item">
          <text class="label required">手机号码</text>
          <input
            v-model="form.phonenumber"
            type="number"
            placeholder="请输入手机号码"
            maxlength="11"
          />
        </view>
        <view class="form-item">
          <text class="label required">邮箱</text>
          <input
            v-model="form.email"
            placeholder="请输入邮箱"
            maxlength="50"
          />
        </view>
        <view class="form-item">
        <text class="label">性别</text>
        <view class="value">
          <text>{{ userInfo.gender === 1 ? '男' : '女' }}</text>
          <text class="iconfont icon-arrow-right"></text>
          <view class="gender-types">
            <view
              class="gender-item"
              :class="{ active: form.sex === '0' }"
              @tap="form.sex = '0'"
            >
              <text class="radio"></text>
              <text>男</text>
        </view>
            <view
              class="gender-item"
              :class="{ active: form.sex === '1' }"
              @tap="form.sex = '1'"
            >
              <text class="radio"></text>
              <text>女</text>
            </view>
          </view>
        </view>
      </view>
      <button class="save-btn" @tap="saveProfile">保存</button>
      </view>
      
      <view class="info-item" @tap="editBirthday">
        <text class="label">生日</text>
        <view class="value">
          <text>{{ userInfo.birthday || '请选择' }}</text>
          <text class="iconfont icon-arrow-right"></text>
        </view>
      </view>
      <view class="info-item" @tap="editPhone">
        <text class="label">手机号</text>
        <view class="value">
          <text>{{ formatPhone(userInfo.phone) }}</text>
          <text class="iconfont icon-arrow-right"></text>
        </view>
      </view>
      <view class="info-item" @tap="editEmail">
        <text class="label">邮箱</text>
        <view class="value">
          <text>{{ userInfo.email || '未绑定' }}</text>
          <text class="iconfont icon-arrow-right"></text>
        </view>
      </view>
    </view>
    <!-- 实名认证 -->
    <view class="verify-card" @tap="goVerify">
      <view class="verify-info">
        <text class="title">实名认证</text>
        <text class="status">{{ userInfo.verified ? '已认证' : '未认证' }}</text>
      </view>
      <text class="iconfont icon-arrow-right"></text>
    </view>
    <!-- 隐私设置 -->
    <view class="privacy-card">
    <!-- 修改密码 -->
    <view class="info-card">
      <view class="card-header">
        <text class="title">隐私设置</text>
        <text class="title">修改密码</text>
      </view>
      <view class="setting-list">
        <view class="setting-item">
          <text class="label">公开个人主页</text>
          <switch
            :checked="privacy.publicProfile"
            @change="e => togglePrivacy('publicProfile', e.detail.value)"
            color="#0f95b0"
      <view class="form-list">
        <view class="form-item">
          <text class="label required">旧密码</text>
          <input
            v-model="pwdForm.oldPassword"
            :password="true"
            placeholder="请输入旧密码"
          />
        </view>
        <view class="setting-item">
          <text class="label">允许陌生人私信</text>
          <switch
            :checked="privacy.allowMessage"
            @change="e => togglePrivacy('allowMessage', e.detail.value)"
            color="#0f95b0"
        <view class="form-item">
          <text class="label required">新密码</text>
          <input
            v-model="pwdForm.newPassword"
            :password="true"
            placeholder="请输入新密码(8-20位)"
          />
        </view>
        <view class="form-item">
          <text class="label required">确认密码</text>
          <input
            v-model="pwdForm.confirmPassword"
            :password="true"
            placeholder="请确认新密码"
          />
        </view>
      </view>
    </view>
    <!-- 账号安全 -->
    <view class="security-card">
      <view class="card-header">
        <text class="title">账号安全</text>
      </view>
      <view class="security-list">
        <view class="security-item" @tap="changePassword">
          <view class="info">
            <text class="label">修改密码</text>
            <text class="desc">建议定期更换密码,确保账号安全</text>
          </view>
          <text class="iconfont icon-arrow-right"></text>
        </view>
        <view class="security-item" @tap="bindPhone">
          <view class="info">
            <text class="label">手机绑定</text>
            <text class="desc">已绑定手机号:{{ formatPhone(userInfo.phone) }}</text>
          </view>
          <text class="iconfont icon-arrow-right"></text>
        </view>
      </view>
      <button class="save-btn" @tap="savePassword">保存</button>
    </view>
    
    <!-- 退出登录 -->
@@ -117,124 +140,248 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { useUserStore } from "@/stores/user";
// 用户信息
const userInfo = ref({
  avatar: '/static/avatar/default.jpg',
  nickname: '张三',
  gender: 1,
  birthday: '1990-01-01',
  phone: '13800138000',
  email: 'example@email.com',
  verified: true
})
const userStore = useUserStore();
// 隐私设置
const privacy = ref({
  publicProfile: true,
  allowMessage: false
})
// 用户信息(来自 /system/user/profile)
const user = ref({});
const roleGroup = ref("");
const postGroup = ref("");
// 格式化手机号
const formatPhone = (phone) => {
  if (!phone) return '未绑定'
  return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
// 基本资料表单
const form = ref({
  nickName: "",
  phonenumber: "",
  email: "",
  sex: "0",
});
// 修改密码表单
const pwdForm = ref({
  oldPassword: "",
  newPassword: "",
  confirmPassword: "",
});
// 头像地址(相对路径拼接服务器地址)
const avatarUrl = computed(() => {
  const avatar = user.value.avatar;
  if (!avatar) return "/static/avatar/default.png";
  if (/^https?:\/\//.test(avatar)) return avatar;
  // #ifdef H5
  return `/api${avatar}`;
  // #endif
  // #ifndef H5
  return `${userStore.baseUrlHt}${avatar}`;
  // #endif
});
onLoad(() => {
  loadProfile();
});
// 加载个人信息
const loadProfile = async () => {
  // 先用 store 中的用户信息兜底显示
  if (userStore.userInfo) {
    user.value = { ...userStore.userInfo };
    fillForm();
}
  try {
    const res = await uni.$uapi.get("/system/user/profile");
    user.value = res.data || {};
    roleGroup.value = res.roleGroup || "";
    postGroup.value = res.postGroup || "";
    fillForm();
  } catch (e) {
    // 拦截器已统一提示
  }
};
// 用用户信息回填基本资料表单
const fillForm = () => {
  form.value.nickName = user.value.nickName || "";
  form.value.phonenumber = user.value.phonenumber || "";
  form.value.email = user.value.email || "";
  form.value.sex = String(user.value.sex ?? "0");
};
// 更换头像
const changeAvatar = () => {
  uni.chooseImage({
    count: 1,
    sizeType: ["compressed"],
    sourceType: ["album", "camera"],
    success: (res) => {
      // 这里处理头像上传逻辑
      userInfo.value.avatar = res.tempFilePaths[0]
    }
  })
}
      uploadAvatar(res.tempFilePaths[0]);
    },
  });
};
// 编辑昵称
const editNickname = () => {
  uni.navigateTo({
    url: '/pages/my/edit-nickname'
  })
}
// 上传头像
const uploadAvatar = (filePath) => {
  uni.showLoading({ title: "上传中...", mask: true });
  const token = uni.getStorageSync("token");
  // #ifdef H5
  const uploadUrl = "/api/system/user/profile/avatar";
  // #endif
  // #ifndef H5
  const uploadUrl = `${userStore.baseUrlHt}/system/user/profile/avatar`;
  // #endif
// 编辑性别
const editGender = () => {
  uni.showActionSheet({
    itemList: ['男', '女'],
  uni.uploadFile({
    url: uploadUrl,
    filePath,
    name: "avatarfile",
    header: { Authorization: `Bearer ${token}` },
    success: (res) => {
      userInfo.value.gender = res.tapIndex + 1
      uni.hideLoading();
      if (res.statusCode === 200) {
        try {
          const data = JSON.parse(res.data);
          if (data.code === 200) {
            user.value.avatar = data.imgUrl;
            userStore.refreshUserInfo();
            uni.showToast({ title: "头像修改成功", icon: "success" });
          } else {
            uni.showToast({ title: data.msg || "上传失败", icon: "none" });
    }
  })
        } catch (e) {
          uni.showToast({ title: "响应解析失败", icon: "none" });
        }
      } else {
        uni.showToast({
          title: `上传失败,状态码: ${res.statusCode}`,
          icon: "none",
        });
      }
    },
    fail: () => {
      uni.hideLoading();
      uni.showToast({ title: "上传失败", icon: "none" });
    },
  });
};
// 保存基本资料
const saveProfile = async () => {
  if (!form.value.nickName) {
    uni.showToast({ title: "用户昵称不能为空", icon: "none" });
    return;
  }
  if (!form.value.phonenumber) {
    uni.showToast({ title: "手机号码不能为空", icon: "none" });
    return;
  }
  if (!/^1[3|4|5|6|7|8|9][0-9]\d{8}$/.test(form.value.phonenumber)) {
    uni.showToast({ title: "请输入正确的手机号码", icon: "none" });
    return;
  }
  if (!form.value.email) {
    uni.showToast({ title: "邮箱地址不能为空", icon: "none" });
    return;
  }
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.value.email)) {
    uni.showToast({ title: "请输入正确的邮箱地址", icon: "none" });
    return;
}
// 编辑生日
const editBirthday = () => {
  uni.showDatePicker({
    current: userInfo.value.birthday,
    success: (res) => {
      userInfo.value.birthday = res.date
  uni.showLoading({ title: "保存中...", mask: true });
  try {
    await uni.$uapi.post("/system/user/profile/updateProfile", {
      nickName: form.value.nickName,
      phonenumber: form.value.phonenumber,
      email: form.value.email,
      sex: form.value.sex,
    });
    uni.hideLoading();
    uni.showToast({ title: "修改成功", icon: "success" });
    // 同步本地显示与 store
    user.value.nickName = form.value.nickName;
    user.value.phonenumber = form.value.phonenumber;
    user.value.email = form.value.email;
    user.value.sex = form.value.sex;
    userStore.refreshUserInfo();
  } catch (e) {
    uni.hideLoading();
    }
  })
};
// 密码复杂度校验:需同时包含大写、小写、数字和特殊字符其中三项
const checkPwdLegal = (pwd) => {
  if (!pwd || pwd.length < 8 || pwd.length > 20) return false;
  let count = 0;
  if (/[a-z]/.test(pwd)) count++;
  if (/[A-Z]/.test(pwd)) count++;
  if (/[0-9]/.test(pwd)) count++;
  if (/[\W_]/.test(pwd)) count++;
  return count >= 3;
};
// 保存密码
const savePassword = async () => {
  const { oldPassword, newPassword, confirmPassword } = pwdForm.value;
  if (!oldPassword) {
    uni.showToast({ title: "旧密码不能为空", icon: "none" });
    return;
  }
  if (!newPassword) {
    uni.showToast({ title: "新密码不能为空", icon: "none" });
    return;
  }
  if (newPassword.length < 8 || newPassword.length > 20) {
    uni.showToast({ title: "密码长度在 8 到 20 个字符", icon: "none" });
    return;
  }
  if (!checkPwdLegal(newPassword)) {
    uni.showToast({
      title: "密码必须同时包含大写、小写、数字和特殊字符其中三项",
      icon: "none",
    });
    return;
  }
  if (!confirmPassword) {
    uni.showToast({ title: "确认密码不能为空", icon: "none" });
    return;
  }
  if (confirmPassword !== newPassword) {
    uni.showToast({ title: "两次输入的密码不一致", icon: "none" });
    return;
}
// 编辑手机号
const editPhone = () => {
  uni.navigateTo({
    url: '/pages/my/edit-phone'
  })
  uni.showLoading({ title: "保存中...", mask: true });
  try {
    await uni.$uapi.post("/system/user/profile/updatePwd", {
      oldPassword,
      newPassword,
    });
    uni.hideLoading();
    uni.showToast({ title: "密码修改成功", icon: "success" });
    pwdForm.value = { oldPassword: "", newPassword: "", confirmPassword: "" };
  } catch (e) {
    uni.hideLoading();
}
// 编辑邮箱
const editEmail = () => {
  uni.navigateTo({
    url: '/pages/my/edit-email'
  })
}
// 前往实名认证
const goVerify = () => {
  uni.navigateTo({
    url: '/pages/my/verify'
  })
}
// 切换隐私设置
const togglePrivacy = (key, value) => {
  privacy.value[key] = value
}
// 修改密码
const changePassword = () => {
  uni.navigateTo({
    url: '/pages/my/change-password'
  })
}
// 绑定手机
const bindPhone = () => {
  uni.navigateTo({
    url: '/pages/my/bind-phone'
  })
}
};
// 退出登录
const logout = () => {
  uni.showModal({
    title: '提示',
    content: '确定要退出登录吗?',
    title: "提示",
    content: "确定要退出登录吗?",
    success: (res) => {
      if (res.confirm) {
        // 这里处理退出登录逻辑
        uni.clearStorageSync();
        uni.reLaunch({
          url: '/pages/login/Login'
        })
          url: "/pages/login/Login",
        });
      }
    }
  })
}
    },
  });
};
</script>
<style lang="scss">
@@ -247,17 +394,18 @@
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 60rpx 0;
    background: #fff;
    padding: 60rpx 0 40rpx;
    background: $primary-gradient;
    
    .avatar-wrapper {
      position: relative;
      margin-bottom: 20rpx;
      margin-bottom: 16rpx;
      
      .avatar {
        width: 160rpx;
        height: 160rpx;
        border-radius: 50%;
        border: 4rpx solid rgba(255, 255, 255, 0.4);
      }
      
      .edit-icon {
@@ -266,107 +414,37 @@
        bottom: 0;
        width: 48rpx;
        height: 48rpx;
        background: $primary-color;
        background: #fff;
        border-radius: 50%;
        display: flex;
        align-items: center;
        justify-content: center;
        
        .iconfont {
          font-size: 28rpx;
          color: #fff;
        .plus {
          font-size: 40rpx;
          font-weight: bold;
          color: $primary-color;
          line-height: 1;
        }
      }
    }
    
    .tip {
      font-size: 26rpx;
      color: $text-secondary;
      font-size: 24rpx;
      color: rgba(255, 255, 255, 0.9);
    }
  }
  
  .info-card {
    background: #fff;
    margin: 20rpx 0;
    .info-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 30rpx;
      border-bottom: 1rpx solid $border-color;
      &:last-child {
        border-bottom: none;
      }
      .label {
        font-size: 30rpx;
        color: $text-primary;
      }
      .value {
        display: flex;
        align-items: center;
        text {
          font-size: 30rpx;
          color: $text-regular;
          &.iconfont {
            font-size: 32rpx;
            color: $text-secondary;
            margin-left: 12rpx;
          }
        }
      }
      &:active {
        background: $bg-color;
      }
    }
  }
  .verify-card {
    display: flex;
    justify-content: space-between;
    align-items: center;
    background: #fff;
    padding: 30rpx;
    margin: 20rpx 0;
    .verify-info {
      .title {
        font-size: 30rpx;
        color: $text-primary;
        margin-bottom: 8rpx;
        display: block;
      }
      .status {
        font-size: 26rpx;
        color: $success;
      }
    }
    .iconfont {
      font-size: 32rpx;
      color: $text-secondary;
    }
    &:active {
      background: $bg-color;
    }
  }
  .privacy-card,
  .security-card {
    background: #fff;
    margin: 20rpx 0;
    margin: 20rpx 20rpx 0;
    border-radius: $radius-lg;
    overflow: hidden;
    box-shadow: $shadow-sm;
    
    .card-header {
      padding: 30rpx;
      border-bottom: 1rpx solid $border-color;
      border-bottom: 1rpx solid #eee;
      
      .title {
        font-size: 32rpx;
@@ -375,59 +453,141 @@
      }
    }
    
    .setting-list {
      .setting-item {
    .info-list {
      .info-item {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 30rpx;
        border-bottom: 1rpx solid $border-color;
        padding: 28rpx 30rpx;
        border-bottom: 1rpx solid #f5f5f5;
        
        &:last-child {
          border-bottom: none;
        }
        
        .label {
          font-size: 30rpx;
          font-size: 28rpx;
          color: $text-regular;
        }
        .value {
          font-size: 28rpx;
          color: $text-primary;
          text-align: right;
          flex: 1;
          margin-left: 20rpx;
        }
      }
    }
    
    .security-list {
      .security-item {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 30rpx;
        border-bottom: 1rpx solid $border-color;
    .form-list {
      padding: 0 30rpx;
      .form-item {
        margin-top: 30rpx;
        
        &:last-child {
          border-bottom: none;
          margin-bottom: 30rpx;
        }
        
        .info {
          .label {
            font-size: 30rpx;
          font-size: 28rpx;
            color: $text-primary;
            margin-bottom: 8rpx;
          margin-bottom: 16rpx;
            display: block;
          }
          
          .desc {
            font-size: 26rpx;
            color: $text-secondary;
          &.required::before {
            content: "*";
            color: $danger;
            margin-right: 4rpx;
          }
        }
        
        .iconfont {
          font-size: 32rpx;
          color: $text-secondary;
        input {
          width: 100%;
          height: 88rpx;
          background: $bg-color;
          border-radius: $radius-lg;
          padding: 0 30rpx;
          font-size: 28rpx;
          color: $text-primary;
          box-sizing: border-box;
        }
        .gender-types {
          display: flex;
          gap: 30rpx;
          .gender-item {
            flex: 1;
            height: 88rpx;
            background: $bg-color;
            border-radius: $radius-lg;
            display: flex;
            align-items: center;
            padding: 0 30rpx;
            .radio {
              width: 32rpx;
              height: 32rpx;
              border: 2rpx solid $text-secondary;
              border-radius: 50%;
              margin-right: 16rpx;
              position: relative;
              &::after {
                content: "";
                position: absolute;
                left: 50%;
                top: 50%;
                transform: translate(-50%, -50%);
                width: 16rpx;
                height: 16rpx;
                background: $primary-color;
                border-radius: 50%;
                opacity: 0;
                transition: all 0.3s;
              }
            }
            text {
              font-size: 28rpx;
              color: $text-regular;
            }
            &.active {
              background: $primary-light;
              .radio {
                border-color: $primary-color;
                &::after {
                  opacity: 1;
                }
              }
              text {
                color: $primary-color;
              }
            }
          }
        }
      }
    }
    .save-btn {
      width: calc(100% - 60rpx);
      height: 80rpx;
      line-height: 80rpx;
      margin: 0 30rpx 30rpx;
      font-size: 30rpx;
      color: #fff;
      background: $primary-gradient;
      border-radius: $radius-xl;
      box-shadow: 0 4rpx 12rpx rgba($primary-color, 0.3);
        
        &:active {
          background: $bg-color;
        }
        opacity: 0.85;
      }
    }
  }
@@ -436,9 +596,9 @@
    width: calc(100% - 60rpx);
    height: 88rpx;
    line-height: 88rpx;
    margin: 40rpx auto;
    margin: 40rpx auto 0;
    font-size: 32rpx;
    color: $warning;
    color: $danger;
    background: #fff;
    border-radius: $radius-lg;