639 lines
14 KiB
Vue
639 lines
14 KiB
Vue
<template>
|
|
<view class="profile-page">
|
|
<!-- 自定义导航栏 -->
|
|
<nav-bar :title="$t('user.profile')"></nav-bar>
|
|
|
|
<view class="profile-content">
|
|
<!-- 头像区域 -->
|
|
<view class="avatar-section">
|
|
<image
|
|
:src="userInfo.avatar || defaultAvatar"
|
|
class="avatar"
|
|
@click="editAvatar"
|
|
/>
|
|
<text class="avatar-text" @click="editAvatar">
|
|
{{ userInfo.avatar ? $t('user.changeAvatar') : $t('user.setAvatar') }}
|
|
</text>
|
|
</view>
|
|
|
|
<!-- 信息列表 -->
|
|
<wd-cell-group border custom-class="info-list">
|
|
<wd-cell
|
|
v-for="field in fields"
|
|
title-width="100rpx"
|
|
:key="field.key"
|
|
:title="field.label"
|
|
clickable
|
|
@click="editField(field)"
|
|
>
|
|
<view class="value-wrapper">
|
|
<text v-if="userInfo[field.key] || userInfo[field.key] === 0" class="value">
|
|
{{ formatValue(field, userInfo[field.key]) }}
|
|
</text>
|
|
<text v-else class="placeholder">{{ $t('user.notSet') }}</text>
|
|
<wd-icon
|
|
v-if="userInfo[field.key]"
|
|
name="edit-1"
|
|
size="16px"
|
|
color="#54a966"
|
|
/>
|
|
</view>
|
|
</wd-cell>
|
|
</wd-cell-group>
|
|
</view>
|
|
|
|
<!-- 编辑弹窗 -->
|
|
<wd-popup v-model="showEditModal" position="bottom">
|
|
<view class="edit-modal">
|
|
<text class="modal-title">{{ editTitle }}</text>
|
|
|
|
<!-- 昵称/姓名/年龄编辑 -->
|
|
<view v-if="['nickname', 'name', 'age'].includes(currentField?.key || '')">
|
|
<wd-input
|
|
v-model="editValue"
|
|
:placeholder="getPlaceholder(currentField?.key)"
|
|
:type="currentField?.key === 'age' ? 'number' : 'text'"
|
|
/>
|
|
</view>
|
|
|
|
<!-- 邮箱编辑 -->
|
|
<view v-if="currentField?.key === 'email'">
|
|
<wd-input
|
|
v-model="editForm.email"
|
|
:placeholder="$t('user.email')"
|
|
type="email"
|
|
/>
|
|
<view class="code-input">
|
|
<wd-input
|
|
v-model="editForm.code"
|
|
:placeholder="$t('login.codePlaceholder')"
|
|
type="number"
|
|
class="flex-1"
|
|
/>
|
|
<wd-button
|
|
size="small"
|
|
:disabled="codeDisabled"
|
|
class="w-[100px]"
|
|
@click="sendCode"
|
|
>
|
|
{{ codeText }}
|
|
</wd-button>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 密码编辑 -->
|
|
<view v-if="currentField?.key === 'password'">
|
|
<wd-input
|
|
v-model="editForm.password"
|
|
:placeholder="$t('user.pleaseInputPassword')"
|
|
type="password"
|
|
@input="checkPasswordStrength"
|
|
/>
|
|
<view class="password-note">
|
|
<view v-if="passwordNote">{{ passwordNote }}</view>
|
|
<view v-html="passwordStrength"></view>
|
|
</view>
|
|
<wd-input
|
|
v-model="editForm.confirmPassword"
|
|
:placeholder="$t('user.pleaseInputPasswordAgain')"
|
|
type="password"
|
|
style="margin-top: 20rpx"
|
|
/>
|
|
</view>
|
|
|
|
<!-- 性别编辑 -->
|
|
<view v-if="currentField?.key === 'sex'">
|
|
<wd-radio-group v-model="editValue" shape="button" class="text-center">
|
|
<wd-radio v-for="item in sexOptions" :key="item.value" :value="item.value">{{ item.label }}</wd-radio>
|
|
</wd-radio-group>
|
|
</view>
|
|
|
|
<!-- 头像上传 -->
|
|
<view v-if="currentField?.key === 'avatar'">
|
|
<wd-upload
|
|
:file-list="avatarList"
|
|
:limit="1"
|
|
action="https://global.nuttyreading.com/oss/fileoss"
|
|
@change="handleChangeAvatar"
|
|
/>
|
|
</view>
|
|
|
|
<!-- 确认按钮 -->
|
|
<wd-button type="success" block :round="false" @click="handleSubmit" style="margin-top: 50rpx">
|
|
{{ $t('common.confirm') }}
|
|
</wd-button>
|
|
<view class="cancel-btn" @click="closeModal">
|
|
{{ $t('common.cancel') }}
|
|
</view>
|
|
</view>
|
|
</wd-popup>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
|
import { useUserStore } from '@/stores/user'
|
|
import {
|
|
getUserInfo,
|
|
updateUserInfo,
|
|
updateEmail,
|
|
updatePassword,
|
|
uploadImage,
|
|
sendEmailCode
|
|
} from '@/api/modules/user'
|
|
import { checkPasswordStrength as validatePassword } from '@/utils/validator'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
const { t } = useI18n()
|
|
const userStore = useUserStore()
|
|
|
|
// 默认头像
|
|
const defaultAvatar = '/static/home_icon.png'
|
|
|
|
// 字段列表
|
|
const fields = computed(() => [
|
|
{ key: 'nickname', label: t('user.nickname') },
|
|
{ key: 'email', label: t('user.email') },
|
|
{ key: 'password', label: t('login.password') },
|
|
{ key: 'age', label: t('user.age') },
|
|
{ key: 'sex', label: t('user.sex') },
|
|
{ key: 'name', label: t('user.name') }
|
|
])
|
|
|
|
// 性别选项
|
|
const sexOptions = [
|
|
{ label: t('user.male'), value: 1 },
|
|
{ label: t('user.female'), value: 0 },
|
|
{ label: t('user.secrecy'), value: 2 },
|
|
]
|
|
|
|
// 编辑相关
|
|
const showEditModal = ref(false)
|
|
const currentField = ref<any>(null)
|
|
const editTitle = ref('')
|
|
const editValue = ref<any>('')
|
|
const editForm = ref({
|
|
email: '',
|
|
code: '',
|
|
password: '',
|
|
confirmPassword: ''
|
|
})
|
|
|
|
// 验证码相关
|
|
const codeDisabled = ref(false)
|
|
const codeText = ref(t('login.getCode'))
|
|
let codeTimer: any = null
|
|
|
|
// 密码强度
|
|
const passwordNote = ref('')
|
|
const passwordStrength = ref('')
|
|
const passwordOk = ref(false)
|
|
|
|
// 头像上传
|
|
const avatarList = ref<any[]>([])
|
|
const avatarUrl = ref('')
|
|
|
|
|
|
/**
|
|
* 获取用户数据
|
|
*/
|
|
const userInfo = ref<any>({}) // 用户信息
|
|
const getData = async () => {
|
|
const res = await getUserInfo()
|
|
if (res.result) {
|
|
userStore.setUserInfo(res.result)
|
|
userInfo.value = res.result
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 格式化显示值
|
|
*/
|
|
const formatValue = (field: any, value: any) => {
|
|
if (field.key === 'sex') {
|
|
let result = null
|
|
switch(value) {
|
|
case 1:
|
|
result = t('user.male')
|
|
break
|
|
case 0:
|
|
result = t('user.female')
|
|
break
|
|
case 2:
|
|
result = t('user.secrecy')
|
|
break
|
|
default:
|
|
result = null
|
|
break
|
|
}
|
|
return result
|
|
}
|
|
if (field.key === 'password') {
|
|
return '******'
|
|
}
|
|
return value
|
|
}
|
|
|
|
/**
|
|
* 获取输入框占位符
|
|
*/
|
|
const getPlaceholder = (key?: string) => {
|
|
const placeholders: Record<string, string> = {
|
|
nickname: t('common.pleaseInput') + t('user.nickname'),
|
|
name: t('common.pleaseInput') + t('user.name'),
|
|
age: t('common.pleaseInput') + t('user.age')
|
|
}
|
|
return placeholders[key || ''] || ''
|
|
}
|
|
|
|
/**
|
|
* 编辑字段
|
|
*/
|
|
const editField = (field: any) => {
|
|
currentField.value = field
|
|
editTitle.value = (userInfo.value[field.key] ? t('common.edit') : t('user.clickToSet')) + field.label
|
|
|
|
// 重置表单
|
|
editValue.value = ''
|
|
editForm.value = {
|
|
email: '',
|
|
code: '',
|
|
password: '',
|
|
confirmPassword: ''
|
|
}
|
|
passwordNote.value = ''
|
|
passwordStrength.value = ''
|
|
|
|
// 设置初始值
|
|
if (field.key === 'sex' && userInfo.value.sex) {
|
|
editValue.value = userInfo.value.sex
|
|
}
|
|
|
|
showEditModal.value = true
|
|
}
|
|
|
|
/**
|
|
* 编辑头像
|
|
*/
|
|
const editAvatar = () => {
|
|
currentField.value = { key: 'avatar', label: t('user.avatar') }
|
|
editTitle.value = userInfo.value.avatar ? t('user.changeAvatar') : t('user.setAvatar')
|
|
avatarList.value = userInfo.value.avatar ? [{ url: userInfo.value.avatar }] : []
|
|
showEditModal.value = true
|
|
}
|
|
|
|
/**
|
|
* 上传前检查
|
|
*/
|
|
const beforeUpload = async () => {
|
|
// TODO: 检查相机和存储权限
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* 处理头像上传变化
|
|
*/
|
|
const handleChangeAvatar = async(filelist: any) => {
|
|
if (filelist.fileList.length > 0) {
|
|
avatarUrl.value = JSON.parse(filelist.fileList[0].response).url
|
|
} else {
|
|
avatarUrl.value = ''
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 删除头像
|
|
*/
|
|
const handleDeleteAvatar = () => {
|
|
avatarList.value = []
|
|
}
|
|
|
|
/**
|
|
* 发送验证码
|
|
*/
|
|
const sendCode = async () => {
|
|
if (!editForm.value.email) {
|
|
uni.showToast({
|
|
title: t('user.pleaseInput') + t('user.email'),
|
|
icon: 'none'
|
|
})
|
|
return
|
|
}
|
|
|
|
await sendEmailCode(editForm.value.email)
|
|
uni.showToast({
|
|
title: t('user.sendCodeSuccess'),
|
|
icon: 'none'
|
|
})
|
|
startCountdown()
|
|
}
|
|
|
|
/**
|
|
* 验证码倒计时
|
|
*/
|
|
const startCountdown = () => {
|
|
if (codeTimer) {
|
|
clearInterval(codeTimer)
|
|
}
|
|
|
|
codeDisabled.value = true
|
|
let countdown = 60
|
|
codeText.value = `${countdown}S`
|
|
|
|
codeTimer = setInterval(() => {
|
|
countdown--
|
|
codeText.value = `${countdown}S`
|
|
if (countdown <= 0) {
|
|
clearInterval(codeTimer)
|
|
codeText.value = t('login.getCode')
|
|
codeDisabled.value = false
|
|
}
|
|
}, 1000)
|
|
}
|
|
|
|
/**
|
|
* 检查密码强度
|
|
*/
|
|
const checkPasswordStrength = () => {
|
|
const password = editForm.value.password
|
|
if (!password) {
|
|
passwordNote.value = ''
|
|
passwordStrength.value = ''
|
|
passwordOk.value = false
|
|
return
|
|
}
|
|
|
|
const strength = validatePassword(password)
|
|
|
|
if (strength === 'strong') {
|
|
passwordStrength.value = `<span style="color:#18bc37">${t('forget.passwordStrengthStrong')}</span>`
|
|
passwordNote.value = ''
|
|
passwordOk.value = true
|
|
} else if (strength === 'medium') {
|
|
passwordStrength.value = `<span style="color:#2979ff">${t('forget.passwordStrengthMedium')}</span>`
|
|
passwordNote.value = t('forget.passwordStrengthWeak')
|
|
passwordOk.value = true
|
|
} else if (strength === 'weak') {
|
|
passwordStrength.value = `<span style="color:#f3a73f">弱</span>`
|
|
passwordNote.value = t('forget.passwordStrengthWeak')
|
|
passwordOk.value = false
|
|
} else {
|
|
passwordStrength.value = ''
|
|
passwordNote.value = t('forget.passwordStrengthWeak')
|
|
passwordOk.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 提交修改
|
|
*/
|
|
const handleSubmit = async () => {
|
|
const key = currentField.value?.key
|
|
|
|
// 构建更新数据对象
|
|
let updateData: any = Object.assign({}, userInfo.value)
|
|
|
|
switch (key) {
|
|
case 'email':
|
|
// 更新邮箱
|
|
if (!editForm.value.email || !editForm.value.code) {
|
|
uni.showToast({
|
|
title: t('user.pleaseInputCode'),
|
|
icon: 'none'
|
|
})
|
|
return
|
|
}
|
|
await updateEmail(userInfo.value.id, editForm.value.email, editForm.value.code)
|
|
break
|
|
|
|
case 'password':
|
|
// 更新密码
|
|
if (!passwordOk.value) {
|
|
uni.showToast({
|
|
title: passwordNote.value,
|
|
icon: 'none'
|
|
})
|
|
return
|
|
}
|
|
if (editForm.value.password !== editForm.value.confirmPassword) {
|
|
uni.showToast({
|
|
title: t('user.passwordNotMatch'),
|
|
icon: 'none'
|
|
})
|
|
return
|
|
}
|
|
await updatePassword(userInfo.value.id, editForm.value.password)
|
|
break
|
|
|
|
case 'avatar':
|
|
// 更新头像
|
|
console.log('avatarUrl.value:', avatarUrl.value)
|
|
if (!avatarUrl.value) {
|
|
uni.showToast({
|
|
title: t('common.pleaseSelect') + t('user.avatar'),
|
|
icon: 'none'
|
|
})
|
|
return
|
|
}
|
|
|
|
// 如果是新上传的图片,需要先上传
|
|
updateData.avatar = avatarUrl.value
|
|
await updateUserInfo(updateData)
|
|
break
|
|
|
|
case 'sex':
|
|
// 更新性别
|
|
updateData.sex = editValue.value
|
|
await updateUserInfo(updateData)
|
|
break
|
|
|
|
default:
|
|
// 更新其他字段
|
|
if (!editValue.value) {
|
|
uni.showToast({
|
|
title: getPlaceholder(key),
|
|
icon: 'none'
|
|
})
|
|
return
|
|
}
|
|
updateData[key] = editValue.value
|
|
await updateUserInfo(updateData)
|
|
break
|
|
}
|
|
|
|
uni.showToast({
|
|
title: t('user.updateSuccess'),
|
|
icon: 'success'
|
|
})
|
|
|
|
closeModal()
|
|
|
|
// 刷新数据
|
|
setTimeout(() => {
|
|
getData()
|
|
}, 500)
|
|
}
|
|
|
|
/**
|
|
* 关闭弹窗
|
|
*/
|
|
const closeModal = () => {
|
|
showEditModal.value = false
|
|
currentField.value = null
|
|
if (codeTimer) {
|
|
clearInterval(codeTimer)
|
|
codeDisabled.value = false
|
|
codeText.value = t('login.getCode')
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
getData()
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
$theme-color: #54a966;
|
|
|
|
.profile-page {
|
|
min-height: 100vh;
|
|
background-color: #f7faf9;
|
|
}
|
|
|
|
.custom-navbar {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 999;
|
|
background-color: #fff;
|
|
border-bottom: 1rpx solid #e5e5e5;
|
|
|
|
.navbar-content {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 44px;
|
|
position: relative;
|
|
|
|
.navbar-left {
|
|
position: absolute;
|
|
left: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 10rpx;
|
|
}
|
|
|
|
.navbar-title {
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
color: #333;
|
|
}
|
|
}
|
|
}
|
|
|
|
.profile-content {
|
|
padding: 20rpx;
|
|
}
|
|
|
|
.avatar-section {
|
|
background: #fff;
|
|
border-radius: 15rpx;
|
|
padding: 40rpx;
|
|
margin-bottom: 20rpx;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
|
|
|
.avatar {
|
|
width: 180rpx;
|
|
height: 180rpx;
|
|
border-radius: 180rpx;
|
|
background-color: #f5f5f5;
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.avatar-text {
|
|
font-size: 30rpx;
|
|
color: $theme-color;
|
|
font-weight: 600;
|
|
}
|
|
}
|
|
|
|
.info-list {
|
|
background: #fff;
|
|
border-radius: 15rpx;
|
|
overflow: hidden;
|
|
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.value-wrapper {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
|
|
.value {
|
|
color: #666;
|
|
margin-right: 10rpx;
|
|
}
|
|
|
|
.placeholder {
|
|
color: #999;
|
|
margin-right: 10rpx;
|
|
}
|
|
}
|
|
|
|
.edit-modal {
|
|
padding: 60rpx 50rpx 80rpx;
|
|
|
|
.modal-title {
|
|
font-size: 32rpx;
|
|
color: #555;
|
|
text-align: center;
|
|
font-weight: bold;
|
|
margin-bottom: 50rpx;
|
|
display: block;
|
|
}
|
|
|
|
.code-input {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 20rpx;
|
|
margin-top: 20rpx;
|
|
}
|
|
|
|
.password-note {
|
|
margin-top: 10rpx;
|
|
font-size: 28rpx;
|
|
color: #999;
|
|
line-height: 40rpx;
|
|
}
|
|
|
|
.radio-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 20rpx 0;
|
|
border-bottom: 1rpx solid #ededed;
|
|
|
|
&:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
text {
|
|
font-size: 30rpx;
|
|
color: #333;
|
|
}
|
|
}
|
|
|
|
.cancel-btn {
|
|
text-align: center;
|
|
font-size: 28rpx;
|
|
color: #888;
|
|
margin-top: 25rpx;
|
|
}
|
|
}
|
|
</style>
|