552 lines
14 KiB
Vue
552 lines
14 KiB
Vue
<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>
|