mi-assessment/uniapp/pages/mine/profile/index.vue
2026-02-20 14:57:43 +08:00

498 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="profile-page">
<!-- 页面内容 -->
<view class="page-content">
<!-- 头像区域 -->
<view class="profile-item avatar-item" @click="handleChangeAvatar">
<text class="item-label">头像</text>
<view class="item-value">
<image
class="avatar"
:src="userInfo.avatar || '/static/logo.png'"
mode="aspectFill"
/>
<text class="arrow"></text>
</view>
</view>
<!-- 昵称区域 -->
<view class="profile-item" @click="showNicknamePopup">
<text class="item-label">昵称</text>
<view class="item-value">
<text class="value-text">{{ userInfo.nickname || '未设置' }}</text>
<text class="arrow"></text>
</view>
</view>
<!-- UID区域不可修改 -->
<view class="profile-item uid-item">
<text class="item-label">UID</text>
<view class="item-value">
<text class="value-text uid-text">{{ userInfo.uid || '--' }}</text>
</view>
</view>
</view>
<!-- 修改昵称弹窗 -->
<view v-if="nicknamePopupVisible" class="popup-mask" @click="hideNicknamePopup">
<view class="popup-container nickname-popup" @click.stop>
<view class="popup-header">
<text class="popup-title">修改昵称</text>
</view>
<view class="popup-body">
<input
class="nickname-input"
type="text"
v-model="newNickname"
placeholder="请输入昵称"
maxlength="20"
:focus="nicknamePopupVisible"
/>
</view>
<view class="popup-footer">
<view class="popup-btn cancel" @click="hideNicknamePopup">
<text>取消</text>
</view>
<view class="popup-btn confirm" @click="handleUpdateNickname">
<text>确定</text>
</view>
</view>
</view>
</view>
<!-- 加载中 -->
<view v-if="loading" class="loading-mask">
<view class="loading-content">
<view class="loading-spinner"></view>
<text class="loading-text">{{ loadingText }}</text>
</view>
</view>
</view>
</template>
<script setup>
/**
* 个人资料页面
* 展示和修改用户头像、昵称
* UID 仅展示不可修改
*/
import { ref, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useUserStore } from '@/store/user.js'
import { getProfile, updateProfile, updateAvatar } from '@/api/user.js'
import config from '@/config/index.js'
const userStore = useUserStore()
// 状态
const loading = ref(false)
const loadingText = ref('加载中...')
const nicknamePopupVisible = ref(false)
const newNickname = ref('')
// 用户信息
const userInfo = computed(() => ({
userId: userStore.userId,
uid: userStore.uid,
nickname: userStore.nickname,
avatar: userStore.avatar
}))
/**
* 获取用户资料
*/
async function fetchProfile() {
try {
loading.value = true
loadingText.value = '加载中...'
const res = await getProfile()
if (res.code === 0 && res.data) {
userStore.updateUserInfo(res.data)
}
} catch (error) {
console.error('获取用户资料失败:', error)
uni.showToast({
title: '获取资料失败',
icon: 'none'
})
} finally {
loading.value = false
}
}
/**
* 选择并上传头像
*/
function handleChangeAvatar() {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
const tempFilePath = res.tempFilePaths[0]
await uploadAvatar(tempFilePath)
},
fail: (err) => {
if (err.errMsg && !err.errMsg.includes('cancel')) {
uni.showToast({
title: '选择图片失败',
icon: 'none'
})
}
}
})
}
/**
* 上传头像
* @param {string} filePath - 图片临时路径
*/
async function uploadAvatar(filePath) {
try {
loading.value = true
loadingText.value = '上传中...'
// 上传图片到服务器
const uploadRes = await new Promise((resolve, reject) => {
uni.uploadFile({
url: `${config.API_BASE_URL}/upload/image`,
filePath: filePath,
name: 'file',
header: {
'Authorization': `Bearer ${userStore.token}`
},
success: (res) => {
if (res.statusCode === 200) {
try {
const data = JSON.parse(res.data)
resolve(data)
} catch (e) {
reject(new Error('解析响应失败'))
}
} else {
reject(new Error('上传失败'))
}
},
fail: (err) => reject(err)
})
})
if (uploadRes.code === 0 && uploadRes.data) {
// 更新头像
const avatarUrl = uploadRes.data.url || uploadRes.data
const updateRes = await updateAvatar(avatarUrl)
if (updateRes.code === 0) {
userStore.updateUserInfo({ avatar: avatarUrl })
uni.showToast({
title: '头像更新成功',
icon: 'success'
})
} else {
throw new Error(updateRes.message || '更新头像失败')
}
} else {
throw new Error(uploadRes.message || '上传失败')
}
} catch (error) {
console.error('上传头像失败:', error)
uni.showToast({
title: error.message || '上传失败',
icon: 'none'
})
} finally {
loading.value = false
}
}
/**
* 显示修改昵称弹窗
*/
function showNicknamePopup() {
newNickname.value = userInfo.value.nickname || ''
nicknamePopupVisible.value = true
}
/**
* 隐藏修改昵称弹窗
*/
function hideNicknamePopup() {
nicknamePopupVisible.value = false
newNickname.value = ''
}
/**
* 更新昵称
*/
async function handleUpdateNickname() {
const nickname = newNickname.value.trim()
if (!nickname) {
uni.showToast({
title: '请输入昵称',
icon: 'none'
})
return
}
if (nickname === userInfo.value.nickname) {
hideNicknamePopup()
return
}
try {
loading.value = true
loadingText.value = '保存中...'
hideNicknamePopup()
const res = await updateProfile({ nickname })
if (res.code === 0) {
userStore.updateUserInfo({ nickname })
uni.showToast({
title: '昵称更新成功',
icon: 'success'
})
} else {
throw new Error(res.message || '更新失败')
}
} catch (error) {
console.error('更新昵称失败:', error)
uni.showToast({
title: error.message || '更新失败',
icon: 'none'
})
} finally {
loading.value = false
}
}
/**
* 页面显示时刷新数据
*/
onShow(() => {
userStore.restoreFromStorage()
})
/**
* 页面加载
*/
onMounted(() => {
userStore.restoreFromStorage()
fetchProfile()
})
</script>
<style lang="scss" scoped>
@import '@/styles/variables.scss';
.profile-page {
min-height: 100vh;
background-color: $bg-color;
}
// 页面内容
.page-content {
padding: $spacing-lg;
}
// 资料项
.profile-item {
display: flex;
align-items: center;
justify-content: space-between;
background-color: $bg-white;
padding: $spacing-lg;
margin-bottom: 2rpx;
&:first-child {
border-radius: $border-radius-lg $border-radius-lg 0 0;
}
&:last-child {
border-radius: 0 0 $border-radius-lg $border-radius-lg;
margin-bottom: 0;
}
&:active {
background-color: $bg-gray;
}
.item-label {
font-size: $font-size-md;
color: $text-color;
}
.item-value {
display: flex;
align-items: center;
.value-text {
font-size: $font-size-md;
color: $text-secondary;
margin-right: $spacing-sm;
}
.uid-text {
color: $text-placeholder;
}
.arrow {
font-size: 36rpx;
color: $text-placeholder;
}
}
// 头像项
&.avatar-item {
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-right: $spacing-sm;
background-color: $bg-gray;
}
}
// UID项不可点击
&.uid-item {
&:active {
background-color: $bg-white;
}
}
}
// 修改昵称弹窗
.popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: $bg-mask;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.popup-container {
width: 600rpx;
background-color: $bg-white;
border-radius: $border-radius-lg;
overflow: hidden;
}
.nickname-popup {
.popup-header {
padding: $spacing-lg;
text-align: center;
border-bottom: 1rpx solid $border-light;
.popup-title {
font-size: $font-size-lg;
font-weight: $font-weight-medium;
color: $text-color;
}
}
.popup-body {
padding: $spacing-lg;
.nickname-input {
width: 100%;
height: 80rpx;
padding: 0 $spacing-md;
font-size: $font-size-md;
color: $text-color;
background-color: $bg-gray;
border-radius: $border-radius-md;
box-sizing: border-box;
}
}
.popup-footer {
display: flex;
border-top: 1rpx solid $border-light;
.popup-btn {
flex: 1;
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
&:active {
background-color: $bg-gray;
}
text {
font-size: $font-size-lg;
}
&.cancel {
border-right: 1rpx solid $border-light;
text {
color: $text-secondary;
}
}
&.confirm {
text {
color: $primary-color;
}
}
}
}
}
// 加载中
.loading-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
z-index: 1001;
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
padding: $spacing-xl;
background-color: $bg-white;
border-radius: $border-radius-lg;
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid $border-color;
border-top-color: $primary-color;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.loading-text {
margin-top: $spacing-md;
font-size: $font-size-sm;
color: $text-secondary;
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>