peixue-dev/peidu/uniapp/user-package/pages/user/profile.vue

552 lines
14 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">
<scroll-view scroll-y class="content-scroll">
<!-- 头像 -->
<view class="avatar-section">
<view class="avatar-wrapper" @click="changeAvatar">
<image v-if="userInfo.avatar" :src="userInfo.avatar" class="avatar-image" mode="aspectFill"></image>
<text v-else class="avatar-emoji">👤</text>
<view class="avatar-edit">
<text>📷</text>
</view>
</view>
</view>
<!-- 基本信息 -->
<view class="form-section">
<view class="form-item" @click="editNickname">
<text class="label">昵称</text>
<view class="value-wrapper">
<text class="value">{{ userInfo.nickname || '未设置' }}</text>
<text class="arrow"></text>
</view>
</view>
<view class="form-item">
<text class="label">手机号</text>
<view class="value-wrapper">
<text class="value">{{ userInfo.phone || '未绑定' }}</text>
<text class="arrow"></text>
</view>
</view>
<view class="form-item" @click="selectGender">
<text class="label">性别</text>
<view class="value-wrapper">
<text class="value">{{ getGenderText() }}</text>
<text class="arrow"></text>
</view>
</view>
<view class="form-item" @click="selectBirthday">
<text class="label">生日</text>
<view class="value-wrapper">
<text class="value">{{ userInfo.birthday || '未设置' }}</text>
<text class="arrow"></text>
</view>
</view>
<view class="form-item" @click="selectRegion">
<text class="label">所在地区</text>
<view class="value-wrapper">
<text class="value">{{ userInfo.region || '未设置' }}</text>
<text class="arrow"></text>
</view>
</view>
</view>
<!-- 账号信息 -->
<view class="form-section">
<view class="section-title">账号信息</view>
<view class="form-item">
<text class="label">用户ID</text>
<view class="value-wrapper">
<text class="value">{{ userInfo.id || '-' }}</text>
</view>
</view>
<view class="form-item">
<text class="label">注册时间</text>
<view class="value-wrapper">
<text class="value">{{ userInfo.registerTime || '-' }}</text>
</view>
</view>
</view>
<!-- 实名认证 -->
<view class="form-section">
<view class="section-title">实名认证</view>
<view class="form-item" @click="goRealName">
<text class="label">实名状态</text>
<view class="value-wrapper">
<text class="value" :class="{ verified: userInfo.realNameVerified }">
{{ userInfo.realNameVerified ? '已认证' : '未认证' }}
</text>
<text class="arrow"></text>
</view>
</view>
</view>
<!-- 保存按钮 -->
<view class="save-section">
<button class="btn-save" @click="saveProfile">保存修改</button>
</view>
</scroll-view>
</view>
</template>
<script>
import { authApi } from '@/api/index.js'
import request from '@/utils/request'
export default {
data() {
return {
userInfo: {
id: null,
nickname: '',
phone: '',
gender: 0,
birthday: '',
region: '',
avatar: '',
registerTime: '',
realNameVerified: false
}
}
},
onLoad() {
this.loadUserInfo()
},
methods: {
async loadUserInfo() {
try {
uni.showLoading({ title: '加载中...' })
const res = await request.get('/api/user/profile')
if (res && res.code === 200 && res.data) {
this.userInfo = {
id: res.data.id,
nickname: res.data.nickname || '',
phone: res.data.phone || '',
gender: res.data.gender || 0,
birthday: res.data.birthday || '',
region: res.data.region || '',
avatar: res.data.avatar || '',
registerTime: res.data.createTime || '',
realNameVerified: res.data.realNameVerified || false
}
}
} catch (e) {
console.error('加载用户信息失败', e)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
uni.hideLoading()
}
},
changeAvatar() {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePath = res.tempFilePaths[0]
this.uploadAvatar(tempFilePath)
}
})
},
async uploadAvatar(filePath) {
console.log('=== 开始上传头像 ===')
console.log('文件路径:', filePath)
uni.showLoading({ title: '上传中...' })
try {
// 获取token
const token = uni.getStorageSync('token')
if (!token) {
throw new Error('请先登录')
}
console.log('Token获取成功')
// 上传文件
const uploadRes = await new Promise((resolve, reject) => {
uni.uploadFile({
url: 'http://localhost:8089/api/user/upload-avatar',
filePath: filePath,
name: 'file',
header: {
'Authorization': 'Bearer ' + token
},
success: (res) => {
console.log('上传响应:', res)
console.log('响应状态码:', res.statusCode)
console.log('响应数据:', res.data)
console.log('响应数据类型:', typeof res.data)
resolve(res)
},
fail: (err) => {
console.error('上传失败:', err)
reject(err)
}
})
})
console.log('uploadRes:', uploadRes)
if (!uploadRes || !uploadRes.data) {
throw new Error('服务器响应异常')
}
// 解析响应数据
let data
try {
// 第一次解析
const firstParse = JSON.parse(uploadRes.data)
console.log('第一次解析结果:', firstParse)
// 检查是否需要第二次解析(双重编码的情况)
if (typeof firstParse.data === 'string') {
console.log('检测到双重编码,进行第二次解析')
data = JSON.parse(firstParse.data)
} else {
data = firstParse
}
} catch (e) {
console.error('JSON解析失败:', e)
throw new Error('响应数据格式错误')
}
console.log('最终解析后的数据:', data)
if (data.code === 200 && data.data) {
// 更新头像URL
this.userInfo.avatar = data.data
console.log('头像URL已更新:', this.userInfo.avatar)
// 更新本地存储
const localUserInfo = uni.getStorageSync('userInfo') || {}
localUserInfo.avatar = data.data
uni.setStorageSync('userInfo', localUserInfo)
console.log('本地存储已更新')
// 强制更新视图
this.$forceUpdate()
uni.showToast({
title: '头像上传成功',
icon: 'success'
})
// 重新加载用户信息以确保数据同步
setTimeout(() => {
this.loadUserInfo()
}, 500)
} else {
throw new Error(data.message || '上传失败')
}
} catch (error) {
console.error('头像上传失败:', error)
uni.showToast({
title: error.message || '头像上传失败',
icon: 'none'
})
} finally {
uni.hideLoading()
}
},
editNickname() {
uni.showModal({
title: '修改昵称',
editable: true,
placeholderText: '请输入昵称',
content: this.userInfo.nickname,
success: (res) => {
if (res.confirm && res.content) {
this.userInfo.nickname = res.content
}
}
})
},
selectGender() {
uni.showActionSheet({
itemList: ['男', '女'],
success: (res) => {
this.userInfo.gender = res.tapIndex + 1
}
})
},
selectBirthday() {
// 使用uni-app的日期选择器
const currentDate = this.userInfo.birthday || this.formatDate(new Date())
uni.showModal({
title: '选择生日',
content: '当前生日:' + currentDate,
showCancel: true,
confirmText: '选择日期',
success: (res) => {
if (res.confirm) {
// 触发日期选择
this.showDatePicker()
}
}
})
},
showDatePicker() {
// 简化实现:直接使用输入框
uni.showModal({
title: '输入生日',
editable: true,
placeholderText: 'YYYY-MM-DD',
content: this.userInfo.birthday || '',
success: (res) => {
if (res.confirm && res.content) {
// 验证日期格式
if (/^\d{4}-\d{2}-\d{2}$/.test(res.content)) {
this.userInfo.birthday = res.content
uni.showToast({
title: '生日已设置',
icon: 'success'
})
} else {
uni.showToast({
title: '日期格式错误请使用YYYY-MM-DD格式',
icon: 'none',
duration: 2000
})
}
}
}
})
},
formatDate(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
selectRegion() {
// TODO: 实现地区选择器
uni.showToast({
title: '地区选择功能开发中',
icon: 'none'
})
},
getGenderText() {
const genderMap = {
0: '未知',
1: '男',
2: '女'
}
return genderMap[this.userInfo.gender] || '未设置'
},
goRealName() {
if (this.userInfo.realNameVerified) {
uni.showToast({
title: '已完成实名认证',
icon: 'none'
})
} else {
uni.showToast({
title: '实名认证功能开发中',
icon: 'none'
})
}
},
async saveProfile() {
try {
uni.showLoading({ title: '保存中...' })
// 使用正确的API路径
const res = await request({
url: '/api/user/profile',
method: 'PUT',
data: {
nickname: this.userInfo.nickname,
gender: this.userInfo.gender,
birthday: this.userInfo.birthday,
avatar: this.userInfo.avatar
}
})
if (res && res.code === 200) {
uni.showToast({
title: '保存成功',
icon: 'success'
})
// 更新本地存储
const localUserInfo = uni.getStorageSync('userInfo') || {}
Object.assign(localUserInfo, {
nickname: this.userInfo.nickname,
gender: this.userInfo.gender,
birthday: this.userInfo.birthday,
avatar: this.userInfo.avatar
})
uni.setStorageSync('userInfo', localUserInfo)
// 1.5秒后返回上一页
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else {
throw new Error(res.message || '保存失败')
}
} catch (e) {
console.error('保存失败', e)
uni.showToast({
title: e.message || '保存失败',
icon: 'none'
})
} finally {
uni.hideLoading()
}
}
}
}
</script>
<style lang="scss" scoped>
$primary-green: #5fc9ba;
$white: #fff;
$black: #333;
$gray: #999;
$light-gray: #f5f5f5;
.profile-page {
min-height: 100vh;
background: $light-gray;
}
.content-scroll {
padding-bottom: 40rpx;
}
.avatar-section {
background: $white;
padding: 60rpx 30rpx;
display: flex;
justify-content: center;
margin-bottom: 20rpx;
.avatar-wrapper {
position: relative;
width: 160rpx;
height: 160rpx;
background: linear-gradient(135deg, #5fc9ba 0%, #7dd9ca 100%);
border-radius: 80rpx;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
.avatar-image {
width: 100%;
height: 100%;
border-radius: 80rpx;
}
.avatar-emoji {
font-size: 80rpx;
}
.avatar-edit {
position: absolute;
right: 0;
bottom: 0;
width: 48rpx;
height: 48rpx;
background: $white;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
text {
font-size: 24rpx;
}
}
}
}
.form-section {
background: $white;
margin-bottom: 20rpx;
.section-title {
padding: 24rpx 30rpx 12rpx;
font-size: 26rpx;
color: $gray;
}
.form-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 28rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.label {
font-size: 30rpx;
color: $black;
}
.value-wrapper {
display: flex;
align-items: center;
gap: 12rpx;
.value {
font-size: 28rpx;
color: $gray;
&.verified {
color: $primary-green;
}
}
.arrow {
font-size: 32rpx;
color: #ddd;
}
}
}
}
.save-section {
padding: 40rpx 30rpx;
.btn-save {
width: 100%;
height: 88rpx;
background: $primary-green;
color: $white;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: bold;
}
}
</style>