Files
taimed-international-app/pages/user/profile/index.vue

638 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"
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/logo.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()
userStore.setUserInfo(res.result)
userInfo.value = res.result
userInfo.value.avatar = res.result.avatar || defaultAvatar
}
/**
* 格式化显示值
*/
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>