|
|
@@ -1,195 +1,280 @@
|
|
|
<template>
|
|
|
- <view class="edit-profile-container">
|
|
|
- <view class="edit-header">
|
|
|
- <text class="header-title">编辑资料</text>
|
|
|
- </view>
|
|
|
-
|
|
|
- <view class="edit-form">
|
|
|
- <!-- 头像 -->
|
|
|
- <view class="form-item">
|
|
|
- <text class="item-label">头像</text>
|
|
|
- <button class="avatar-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
|
|
|
- <image class="avatar-preview" :src="avatarUrl" mode="aspectFill"></image>
|
|
|
- <text class="change-text">点击更换</text>
|
|
|
- </button>
|
|
|
+ <view class="page-container">
|
|
|
+ <!-- 顶部导航栏 -->
|
|
|
+ <view class="custom-navbar">
|
|
|
+ <view class="navbar-back" @click="handleBack">
|
|
|
+ <text class="back-icon">←</text>
|
|
|
</view>
|
|
|
-
|
|
|
- <!-- 昵称 -->
|
|
|
- <view class="form-item">
|
|
|
- <text class="item-label">昵称</text>
|
|
|
- <input
|
|
|
- class="item-input"
|
|
|
- type="nickname"
|
|
|
- v-model="nickname"
|
|
|
- placeholder="请输入昵称"
|
|
|
- maxlength="20"
|
|
|
- />
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 手机号(只读) -->
|
|
|
- <view class="form-item" v-if="phone">
|
|
|
- <text class="item-label">手机号</text>
|
|
|
- <text class="item-value">{{ phone }}</text>
|
|
|
+ <view class="navbar-title">
|
|
|
+ <text class="title-text">编辑资料</text>
|
|
|
</view>
|
|
|
+ <view class="navbar-placeholder"></view>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 保存按钮 -->
|
|
|
- <view class="action-area">
|
|
|
- <button class="save-btn" @click="handleSave">保存</button>
|
|
|
- </view>
|
|
|
+ <scroll-view class="scroll-view" scroll-y>
|
|
|
+ <view class="content-wrapper">
|
|
|
+ <!-- 表单卡片 -->
|
|
|
+ <view class="form-card">
|
|
|
+ <!-- 头像 -->
|
|
|
+ <view class="form-item">
|
|
|
+ <text class="item-label">头像</text>
|
|
|
+ <view class="avatar-section">
|
|
|
+ <button class="avatar-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
|
|
|
+ <image class="avatar-preview" :src="avatarUrl" mode="aspectFill"></image>
|
|
|
+ </button>
|
|
|
+ <text class="change-tip">点击更换</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 昵称 -->
|
|
|
+ <view class="form-item">
|
|
|
+ <text class="item-label">昵称</text>
|
|
|
+ <input
|
|
|
+ class="item-input"
|
|
|
+ type="nickname"
|
|
|
+ v-model="nickname"
|
|
|
+ placeholder="请输入昵称"
|
|
|
+ maxlength="20"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 手机号(只读) -->
|
|
|
+ <view class="form-item" v-if="phone">
|
|
|
+ <text class="item-label">手机号</text>
|
|
|
+ <text class="item-value">{{ formatPhone(phone) }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 保存按钮 -->
|
|
|
+ <view class="action-section">
|
|
|
+ <button class="save-btn" @click="handleSave" :disabled="saving">
|
|
|
+ {{ saving ? '保存中...' : '保存' }}
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="bottom-safe-area"></view>
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
-<script>
|
|
|
-import { getUserInfo, setUserInfo, isLoggedIn as checkLoginStatus } from '@/utils/auth.js'
|
|
|
+<script setup>
|
|
|
+import { ref, onMounted } from 'vue'
|
|
|
+import { onShow } from '@dcloudio/uni-app'
|
|
|
+import { getUserInfo, setUserInfo, refreshUserInfo } from '@/utils/auth.js'
|
|
|
import { updateUserProfile } from '@/utils/api.js'
|
|
|
|
|
|
-export default {
|
|
|
- data() {
|
|
|
- return {
|
|
|
- avatarUrl: '/static/images/head.png',
|
|
|
- nickname: '',
|
|
|
- phone: '',
|
|
|
- originalAvatar: '',
|
|
|
- originalNickname: ''
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- onLoad() {
|
|
|
- const loginStatus = checkLoginStatus()
|
|
|
- console.log('[编辑资料] 登录状态:', loginStatus)
|
|
|
- this.loadUserInfo()
|
|
|
- },
|
|
|
-
|
|
|
- onShow() {
|
|
|
- // 设置导航栏标题
|
|
|
- uni.setNavigationBarTitle({ title: '量化交易大师' })
|
|
|
- },
|
|
|
-
|
|
|
- methods: {
|
|
|
- /**
|
|
|
- * 加载用户信息
|
|
|
- */
|
|
|
- loadUserInfo() {
|
|
|
- const userInfo = getUserInfo()
|
|
|
- console.log('[编辑资料] 加载用户信息:', userInfo)
|
|
|
- if (userInfo) {
|
|
|
- this.avatarUrl = userInfo.avatar || '/static/images/head.png'
|
|
|
- this.nickname = userInfo.nickname || ''
|
|
|
- this.phone = userInfo.phone || ''
|
|
|
- this.originalAvatar = this.avatarUrl
|
|
|
- this.originalNickname = this.nickname
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 选择头像
|
|
|
- */
|
|
|
- onChooseAvatar(e) {
|
|
|
- const { avatarUrl } = e.detail
|
|
|
- this.avatarUrl = avatarUrl
|
|
|
- console.log('选择头像:', avatarUrl)
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 保存资料
|
|
|
- */
|
|
|
- async handleSave() {
|
|
|
- if (!this.nickname || this.nickname.trim() === '') {
|
|
|
- uni.showToast({
|
|
|
- title: '请输入昵称',
|
|
|
- icon: 'none'
|
|
|
- })
|
|
|
- return
|
|
|
- }
|
|
|
+const avatarUrl = ref('/static/images/head.png')
|
|
|
+const nickname = ref('')
|
|
|
+const phone = ref('')
|
|
|
+const originalAvatar = ref('')
|
|
|
+const originalNickname = ref('')
|
|
|
+const saving = ref(false)
|
|
|
|
|
|
- // 检查是否有修改
|
|
|
- if (this.avatarUrl === this.originalAvatar && this.nickname === this.originalNickname) {
|
|
|
- uni.showToast({
|
|
|
- title: '没有修改',
|
|
|
- icon: 'none'
|
|
|
- })
|
|
|
- return
|
|
|
- }
|
|
|
+const handleBack = () => {
|
|
|
+ const pages = getCurrentPages()
|
|
|
+ pages.length > 1 ? uni.navigateBack() : uni.switchTab({ url: '/pages/mine/mine' })
|
|
|
+}
|
|
|
|
|
|
- try {
|
|
|
- uni.showLoading({ title: '保存中...' })
|
|
|
+onMounted(() => {
|
|
|
+ loadUserInfo()
|
|
|
+})
|
|
|
+
|
|
|
+onShow(() => {
|
|
|
+ loadUserInfo()
|
|
|
+})
|
|
|
+
|
|
|
+const loadUserInfo = async () => {
|
|
|
+ // 先显示本地缓存
|
|
|
+ const userInfo = getUserInfo()
|
|
|
+ if (userInfo) {
|
|
|
+ avatarUrl.value = userInfo.avatar || '/static/images/head.png'
|
|
|
+ nickname.value = userInfo.nickname || ''
|
|
|
+ phone.value = userInfo.phone || ''
|
|
|
+ originalAvatar.value = avatarUrl.value
|
|
|
+ originalNickname.value = nickname.value
|
|
|
+ }
|
|
|
+
|
|
|
+ // 刷新最新状态
|
|
|
+ const latestInfo = await refreshUserInfo()
|
|
|
+ if (latestInfo) {
|
|
|
+ avatarUrl.value = latestInfo.avatar || '/static/images/head.png'
|
|
|
+ nickname.value = latestInfo.nickname || ''
|
|
|
+ phone.value = latestInfo.phone || ''
|
|
|
+ originalAvatar.value = avatarUrl.value
|
|
|
+ originalNickname.value = nickname.value
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const formatPhone = (p) => {
|
|
|
+ if (!p || p.length !== 11) return p
|
|
|
+ return p.substring(0, 3) + '****' + p.substring(7)
|
|
|
+}
|
|
|
+
|
|
|
+const onChooseAvatar = (e) => {
|
|
|
+ avatarUrl.value = e.detail.avatarUrl
|
|
|
+}
|
|
|
|
|
|
- // 上传头像(如果更换了头像且不是默认头像)
|
|
|
- let uploadedAvatarUrl = this.avatarUrl
|
|
|
- if (this.avatarUrl !== this.originalAvatar && !this.avatarUrl.startsWith('/static/')) {
|
|
|
- // 暂时直接使用微信临时路径,后续可扩展上传到服务器
|
|
|
- uploadedAvatarUrl = this.avatarUrl
|
|
|
+const uploadAvatar = async (tempPath) => {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ const token = uni.getStorageSync('user_token')
|
|
|
+ uni.uploadFile({
|
|
|
+ url: 'http://localhost:8081/v1/file/upload',
|
|
|
+ filePath: tempPath,
|
|
|
+ name: 'file',
|
|
|
+ header: {
|
|
|
+ 'Authorization': `Bearer ${token}`
|
|
|
+ },
|
|
|
+ success: (res) => {
|
|
|
+ if (res.statusCode === 200) {
|
|
|
+ const data = JSON.parse(res.data)
|
|
|
+ if (data.code === 200 && data.data?.url) {
|
|
|
+ resolve(data.data.url)
|
|
|
+ } else {
|
|
|
+ reject(new Error(data.message || '上传失败'))
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ reject(new Error('上传失败'))
|
|
|
}
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ reject(new Error('网络错误'))
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const handleSave = async () => {
|
|
|
+ if (!nickname.value || nickname.value.trim() === '') {
|
|
|
+ uni.showToast({ title: '请输入昵称', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (avatarUrl.value === originalAvatar.value && nickname.value === originalNickname.value) {
|
|
|
+ uni.showToast({ title: '没有修改', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- // 调用后端接口更新用户信息
|
|
|
- const result = await updateUserProfile({
|
|
|
- nickname: this.nickname,
|
|
|
- avatar: uploadedAvatarUrl
|
|
|
- })
|
|
|
-
|
|
|
- console.log('更新成功:', result)
|
|
|
-
|
|
|
- // 更新本地存储的用户信息
|
|
|
- const userInfo = getUserInfo()
|
|
|
- userInfo.nickname = this.nickname
|
|
|
- userInfo.avatar = uploadedAvatarUrl
|
|
|
- setUserInfo(userInfo)
|
|
|
-
|
|
|
- uni.hideLoading()
|
|
|
- uni.showToast({
|
|
|
- title: '保存成功',
|
|
|
- icon: 'success'
|
|
|
- })
|
|
|
-
|
|
|
- // 延迟返回
|
|
|
- setTimeout(() => {
|
|
|
- uni.navigateBack()
|
|
|
- }, 1500)
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('保存失败:', error)
|
|
|
- uni.hideLoading()
|
|
|
- uni.showToast({
|
|
|
- title: error.message || '保存失败',
|
|
|
- icon: 'none'
|
|
|
- })
|
|
|
+ saving.value = true
|
|
|
+ uni.showLoading({ title: '保存中...' })
|
|
|
+
|
|
|
+ try {
|
|
|
+ let uploadedAvatarUrl = avatarUrl.value
|
|
|
+
|
|
|
+ // 如果头像是临时文件路径,需要上传到OSS
|
|
|
+ if (avatarUrl.value !== originalAvatar.value &&
|
|
|
+ !avatarUrl.value.startsWith('/static/') &&
|
|
|
+ !avatarUrl.value.startsWith('http')) {
|
|
|
+ try {
|
|
|
+ uploadedAvatarUrl = await uploadAvatar(avatarUrl.value)
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('头像上传失败,使用默认头像:', e.message)
|
|
|
+ uploadedAvatarUrl = '/static/images/head.png'
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ // 调用后端接口更新用户信息
|
|
|
+ await updateUserProfile({
|
|
|
+ nickname: nickname.value.trim(),
|
|
|
+ avatar: uploadedAvatarUrl
|
|
|
+ })
|
|
|
+
|
|
|
+ // 更新本地存储
|
|
|
+ const userInfo = getUserInfo()
|
|
|
+ userInfo.nickname = nickname.value.trim()
|
|
|
+ userInfo.avatar = uploadedAvatarUrl
|
|
|
+ setUserInfo(userInfo)
|
|
|
+
|
|
|
+ uni.hideLoading()
|
|
|
+ uni.showToast({ title: '保存成功', icon: 'success' })
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ uni.navigateBack()
|
|
|
+ }, 1500)
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('保存失败:', error)
|
|
|
+ uni.hideLoading()
|
|
|
+ uni.showToast({ title: error.message || '保存失败', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ saving.value = false
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-.edit-profile-container {
|
|
|
+.page-container {
|
|
|
min-height: 100vh;
|
|
|
background: #f5f6fb;
|
|
|
- padding: 30rpx;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+/* 导航栏 */
|
|
|
+.custom-navbar {
|
|
|
+ background: #ffffff;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 80rpx 32rpx 30rpx;
|
|
|
+ box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
|
|
|
+ position: relative;
|
|
|
}
|
|
|
|
|
|
-.edit-header {
|
|
|
- text-align: center;
|
|
|
- padding: 40rpx 0;
|
|
|
+.navbar-back {
|
|
|
+ width: 80rpx;
|
|
|
+ height: 60rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
}
|
|
|
|
|
|
-.header-title {
|
|
|
+.back-icon {
|
|
|
font-size: 40rpx;
|
|
|
+ color: #222222;
|
|
|
font-weight: bold;
|
|
|
- color: #222;
|
|
|
}
|
|
|
|
|
|
-.edit-form {
|
|
|
- background: #fff;
|
|
|
- border-radius: 20rpx;
|
|
|
- padding: 40rpx;
|
|
|
+.navbar-title {
|
|
|
+ position: absolute;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+}
|
|
|
+
|
|
|
+.title-text {
|
|
|
+ font-size: 36rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #222222;
|
|
|
+}
|
|
|
+
|
|
|
+.navbar-placeholder {
|
|
|
+ width: 80rpx;
|
|
|
+}
|
|
|
+
|
|
|
+/* 内容区 */
|
|
|
+.scroll-view {
|
|
|
+ flex: 1;
|
|
|
+ height: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.content-wrapper {
|
|
|
+ padding: 32rpx;
|
|
|
+}
|
|
|
+
|
|
|
+/* 表单卡片 */
|
|
|
+.form-card {
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 24rpx;
|
|
|
+ padding: 16rpx 32rpx;
|
|
|
+ box-shadow: 0 8rpx 24rpx rgba(37, 52, 94, 0.08);
|
|
|
margin-bottom: 40rpx;
|
|
|
}
|
|
|
|
|
|
.form-item {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- padding: 30rpx 0;
|
|
|
- border-bottom: 1rpx solid #f0f0f0;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 32rpx 0;
|
|
|
+ border-bottom: 1rpx solid #f5f6fb;
|
|
|
}
|
|
|
|
|
|
.form-item:last-child {
|
|
|
@@ -197,17 +282,18 @@ export default {
|
|
|
}
|
|
|
|
|
|
.item-label {
|
|
|
- width: 150rpx;
|
|
|
- font-size: 32rpx;
|
|
|
- color: #333;
|
|
|
- font-weight: 500;
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #222222;
|
|
|
+ min-width: 120rpx;
|
|
|
}
|
|
|
|
|
|
-.avatar-btn {
|
|
|
- flex: 1;
|
|
|
+.avatar-section {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
+}
|
|
|
+
|
|
|
+.avatar-btn {
|
|
|
background: transparent;
|
|
|
border: none;
|
|
|
padding: 0;
|
|
|
@@ -223,46 +309,55 @@ export default {
|
|
|
width: 120rpx;
|
|
|
height: 120rpx;
|
|
|
border-radius: 50%;
|
|
|
- background: #f0f0f0;
|
|
|
+ background: #f5f6fb;
|
|
|
}
|
|
|
|
|
|
-.change-text {
|
|
|
- font-size: 28rpx;
|
|
|
- color: #5d55e8;
|
|
|
+.change-tip {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #5B5AEA;
|
|
|
+ margin-left: 20rpx;
|
|
|
}
|
|
|
|
|
|
.item-input {
|
|
|
flex: 1;
|
|
|
- height: 80rpx;
|
|
|
- font-size: 32rpx;
|
|
|
- color: #333;
|
|
|
+ height: 60rpx;
|
|
|
+ font-size: 30rpx;
|
|
|
+ color: #222222;
|
|
|
text-align: right;
|
|
|
}
|
|
|
|
|
|
.item-value {
|
|
|
- flex: 1;
|
|
|
- font-size: 32rpx;
|
|
|
- color: #999;
|
|
|
- text-align: right;
|
|
|
+ font-size: 30rpx;
|
|
|
+ color: #9ca2b5;
|
|
|
}
|
|
|
|
|
|
-.action-area {
|
|
|
- padding: 0 40rpx;
|
|
|
+/* 保存按钮 */
|
|
|
+.action-section {
|
|
|
+ padding: 0 20rpx;
|
|
|
}
|
|
|
|
|
|
.save-btn {
|
|
|
width: 100%;
|
|
|
- height: 90rpx;
|
|
|
- background: linear-gradient(135deg, #5d55e8, #7568ff);
|
|
|
- color: #fff;
|
|
|
+ height: 96rpx;
|
|
|
+ background: #5B5AEA;
|
|
|
+ color: #ffffff;
|
|
|
font-size: 32rpx;
|
|
|
- border-radius: 45rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ border-radius: 48rpx;
|
|
|
border: none;
|
|
|
- box-shadow: 0 12rpx 24rpx rgba(93, 85, 232, 0.4);
|
|
|
- line-height: 90rpx;
|
|
|
+ box-shadow: 0 12rpx 24rpx rgba(91, 90, 234, 0.3);
|
|
|
+ line-height: 96rpx;
|
|
|
}
|
|
|
|
|
|
.save-btn:active {
|
|
|
opacity: 0.9;
|
|
|
}
|
|
|
+
|
|
|
+.save-btn[disabled] {
|
|
|
+ opacity: 0.6;
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-safe-area {
|
|
|
+ height: 80rpx;
|
|
|
+}
|
|
|
</style>
|