Files
xiaocai/client/src/pages/profile-edit/index.vue
wangxiaogang 21f4d81959 feat: 用户信息完善 + 一起记功能
- users 表添加 nickname、avatar_url 字段
- 用户信息 API(GET/PUT /me)+ 头像上传(multer)
- 头像通过 API 路由获取(公开,不经过 auth)
- 登录接口返回用户昵称和头像
- 用户信息 Store + 个人资料编辑页面
- 我的页面和首页显示真实用户信息
- 群组表(groups、group_members)+ transactions 添加 group_id
- 群组 API(创建/加入/退出/解散)
- 交易和统计 API 支持 group_id 视图切换
- 群组客户端 Store + 身份切换
- 群组管理页面
- App 初始化群组 Store
- UPLOAD_DIR 配置化
- Node.js 备份替代 mysqldump
- 统一前端 BASE_URL(config.ts)
- uploads 加入 .gitignore
- 修复 trust proxy 警告
2026-06-03 17:50:32 +08:00

251 lines
5.3 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="page">
<view class="header-fixed">
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
<view class="nav-bar" :style="{ paddingRight: capsuleRight + 'px' }">
<view class="nav-back" @tap="goBack">
<Icon name="arrowLeft" :size="40" color="#2D1B1B" />
</view>
<text class="nav-title">编辑资料</text>
<view class="nav-save" @tap="saveProfile">
<text class="save-text">保存</text>
</view>
</view>
</view>
<view class="avatar-section">
<view class="avatar-wrap" @tap="chooseAvatar">
<image v-if="previewUrl" :src="previewUrl" class="avatar-img" mode="aspectFill" />
<Icon v-else name="user" :size="64" color="#FFFFFF" />
<view class="avatar-badge">
<Icon name="edit" :size="24" color="#FFFFFF" />
</view>
</view>
<text class="avatar-hint">点击更换头像</text>
</view>
<view class="form-card">
<view class="form-item">
<text class="form-label">昵称</text>
<input class="form-input" v-model="formNickname" placeholder="请输入昵称" maxlength="50" />
</view>
</view>
<view class="tips-card">
<text class="tips-title">提示</text>
<text class="tips-text"> 昵称最长 50 个字符</text>
<text class="tips-text"> 头像支持 JPGPNG 格式最大 2MB</text>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
import { statusBarHeight, capsuleRight } from '@/utils/system'
import Icon from '@/components/Icon/Icon.vue'
const userStore = useUserStore()
const formNickname = ref('')
const previewUrl = ref('')
const uploading = ref(false)
const saving = ref(false)
onMounted(() => {
formNickname.value = userStore.nickname
previewUrl.value = userStore.avatarUrl
})
function goBack() {
uni.navigateBack()
}
function chooseAvatar() {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
const tempPath = res.tempFilePaths[0]
previewUrl.value = tempPath
uploading.value = true
try {
await userStore.uploadAvatar(tempPath)
uni.showToast({ title: '头像已更新', icon: 'success' })
} catch {
uni.showToast({ title: '头像上传失败', icon: 'none' })
previewUrl.value = userStore.avatarUrl
} finally {
uploading.value = false
}
}
})
}
async function saveProfile() {
if (saving.value || uploading.value) return
const nickname = formNickname.value.trim()
if (!nickname) {
uni.showToast({ title: '请输入昵称', icon: 'none' })
return
}
saving.value = true
try {
await userStore.updateProfile(nickname)
uni.showToast({ title: '保存成功', icon: 'success' })
setTimeout(() => uni.navigateBack(), 1500)
} catch {
uni.showToast({ title: '保存失败', icon: 'none' })
} finally {
saving.value = false
}
}
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: #FFF8F0;
padding: 0 0 180rpx;
}
.header-fixed {
position: sticky;
top: 0;
z-index: 100;
background: #FFF8F0;
}
.status-bar { background: #FFF8F0; }
.nav-bar {
display: flex;
align-items: center;
padding: 16rpx 40rpx 24rpx;
}
.nav-back {
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: 600;
color: #2D1B1B;
}
.nav-save {
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
}
.save-text {
font-size: 28rpx;
font-weight: 600;
color: #FF8C69;
}
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
padding: 48rpx 0 32rpx;
}
.avatar-wrap {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
border: 6rpx solid rgba(255, 255, 255, 0.5);
background: linear-gradient(135deg, #FF8C69, #FFB89A);
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.avatar-img { width: 100%; height: 100%; }
.avatar-badge {
position: absolute;
bottom: 0;
right: 0;
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
}
.avatar-hint {
font-size: 24rpx;
color: #BFB3B3;
margin-top: 16rpx;
}
.form-card {
margin: 32rpx 40rpx 0;
background: #FFFFFF;
border-radius: 40rpx;
border: 2rpx solid #F0E0D6;
box-shadow: 4rpx 4rpx 12rpx rgba(45, 27, 27, 0.08);
overflow: hidden;
}
.form-item {
display: flex;
align-items: center;
padding: 32rpx 40rpx;
}
.form-label {
width: 120rpx;
font-size: 28rpx;
font-weight: 500;
color: #2D1B1B;
flex-shrink: 0;
}
.form-input {
flex: 1;
font-size: 28rpx;
color: #2D1B1B;
}
.tips-card {
margin: 32rpx 40rpx 0;
padding: 32rpx 40rpx;
background: #FFFFFF;
border-radius: 40rpx;
border: 2rpx solid #F0E0D6;
box-shadow: 4rpx 4rpx 12rpx rgba(45, 27, 27, 0.08);
}
.tips-title {
font-size: 28rpx;
font-weight: 600;
color: #2D1B1B;
display: block;
margin-bottom: 16rpx;
}
.tips-text {
font-size: 24rpx;
color: #8B7E7E;
display: block;
margin-bottom: 8rpx;
}
</style>