更新:增加“我的”相关功能
This commit is contained in:
645
pages/user/profile/index.vue
Normal file
645
pages/user/profile/index.vue
Normal file
@@ -0,0 +1,645 @@
|
||||
<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]" 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: 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 () => {
|
||||
uni.showLoading()
|
||||
try {
|
||||
const res = await getUserInfo()
|
||||
uni.hideLoading()
|
||||
if (res.result) {
|
||||
userStore.setUserInfo(res.result)
|
||||
userInfo.value = res.result
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化显示值
|
||||
*/
|
||||
const formatValue = (field: any, value: any) => {
|
||||
if (field.key === 'sex') {
|
||||
return value === 1 ? t('user.male') : t('user.female')
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
try {
|
||||
await sendEmailCode(editForm.value.email)
|
||||
uni.showToast({
|
||||
title: t('user.sendCodeSuccess'),
|
||||
icon: 'none'
|
||||
})
|
||||
startCountdown()
|
||||
} catch (error) {
|
||||
console.error('发送验证码失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码倒计时
|
||||
*/
|
||||
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
|
||||
|
||||
try {
|
||||
uni.showLoading()
|
||||
|
||||
// 构建更新数据对象
|
||||
let updateData: any = Object.assign({}, userInfo.value)
|
||||
|
||||
switch (key) {
|
||||
case 'email':
|
||||
// 更新邮箱
|
||||
if (!editForm.value.email || !editForm.value.code) {
|
||||
uni.hideLoading()
|
||||
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.hideLoading()
|
||||
uni.showToast({
|
||||
title: passwordNote.value,
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (editForm.value.password !== editForm.value.confirmPassword) {
|
||||
uni.hideLoading()
|
||||
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.hideLoading()
|
||||
uni.showToast({
|
||||
title: t('common.pleaseSelect') + t('user.avatar'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是新上传的图片,需要先上传
|
||||
updateData.avatar = avatarUrl.value
|
||||
await updateUserInfo(updateData)
|
||||
break
|
||||
|
||||
case 'sex':
|
||||
// 更新性别
|
||||
const sexValue = editValue.value === 2 ? 0 : editValue.value
|
||||
updateData.sex = sexValue
|
||||
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.hideLoading()
|
||||
uni.showToast({
|
||||
title: t('user.updateSuccess'),
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
closeModal()
|
||||
|
||||
// 刷新数据
|
||||
setTimeout(() => {
|
||||
getData()
|
||||
}, 500)
|
||||
} catch (error) {
|
||||
console.error('更新失败:', error)
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗
|
||||
*/
|
||||
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>
|
||||
Reference in New Issue
Block a user