FurBall/pages/mine/profile-edit.vue

346 lines
6.9 KiB
Vue

<template>
<view class="edit-page">
<!-- 头像区 -->
<view class="avatar-section">
<text class="section-label">头像</text>
<view class="avatar-row">
<button
class="avatar-btn"
:class="{ uploading }"
open-type="chooseAvatar"
:disabled="uploading"
@chooseavatar="onChooseAvatar"
>
<image
class="avatar-img"
:src="displayAvatarUrl || '/static/logo.jpg'"
mode="aspectFill"
/>
<view class="avatar-edit-icon">
<TnIcon name="edit-write-fill" :size="32" color="#FFFFFF" />
</view>
<view v-if="uploading" class="avatar-uploading-mask">
<TnIcon name="loading" :size="48" color="#FFFFFF" />
</view>
</button>
</view>
</view>
<!-- 表单区 -->
<view class="form-section">
<view class="form-item">
<text class="form-label">昵称</text>
<input
class="form-input"
type="nickname"
v-model="form.nickName"
placeholder="请输入昵称"
maxlength="20"
@blur="onNicknameBlur"
/>
</view>
<view class="form-item">
<text class="form-label">手机号</text>
<input
class="form-input"
type="number"
v-model="form.phone"
placeholder="选填,用于重要通知"
maxlength="11"
/>
</view>
</view>
<!-- 保存按钮 -->
<view class="btn-section">
<button
class="save-btn"
:disabled="saving"
:loading="saving"
@click="handleSave"
>
保存
</button>
</view>
</view>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from 'vue'
interface ProfileForm {
avatarUrl: string
nickName: string
phone: string
}
const saving = ref(false)
const uploading = ref(false)
const displayAvatarUrl = ref('')
const form = reactive<ProfileForm>({
avatarUrl: '',
nickName: '',
phone: '',
})
// 从本地缓存加载用户信息,如果是 cloud:// 则需要转换为可访问 URL
const cached = uni.getStorageSync('userInfo')
if (cached) {
form.avatarUrl = cached.avatarUrl || ''
form.nickName = cached.nickName || ''
form.phone = cached.phone || ''
}
onMounted(() => {
resolveAvatarUrl()
})
async function resolveAvatarUrl() {
const url = form.avatarUrl
if (!url) return
if (url.startsWith('cloud://')) {
try {
const res = await uniCloud.getTempFileURL({ fileList: [url] })
if (res.fileList && res.fileList[0]?.tempFileURL) {
displayAvatarUrl.value = res.fileList[0].tempFileURL
}
} catch (e) {
console.warn('获取头像临时URL失败', e)
}
} else {
displayAvatarUrl.value = url
}
}
// 微信小程序选择头像 → 上传到 uniCloud
async function onChooseAvatar(e: any) {
const tempPath = e.detail.avatarUrl
if (!tempPath) return
uploading.value = true
uni.showLoading({ title: '上传头像...', mask: true })
try {
const uploadRes = await uniCloud.uploadFile({
filePath: tempPath,
cloudPath: `avatars/${Date.now()}_${Math.random().toString(36).slice(2, 8)}.png`,
})
form.avatarUrl = uploadRes.fileID
// 转换为临时 HTTP URL 用于显示
const urlRes = await uniCloud.getTempFileURL({ fileList: [uploadRes.fileID] })
if (urlRes.fileList && urlRes.fileList[0]?.tempFileURL) {
displayAvatarUrl.value = urlRes.fileList[0].tempFileURL
}
} catch (err: any) {
console.error('头像上传失败:', err)
uni.showToast({ title: '头像上传失败', icon: 'none' })
} finally {
uni.hideLoading()
uploading.value = false
}
}
// 微信小程序昵称输入完成
function onNicknameBlur(e: any) {
if (e.detail?.value) {
form.nickName = e.detail.value
}
}
async function handleSave() {
saving.value = true
uni.showLoading({ title: '保存中...', mask: true })
try {
const uid = uni.getStorageSync('uid')
const res = await uniCloud.callFunction({
name: 'userManager',
data: {
action: 'updateProfile',
uid,
nickName: form.nickName,
avatarUrl: form.avatarUrl,
phone: form.phone,
},
})
if (res.result?.code !== 0) {
throw new Error(res.result?.message || '保存失败')
}
// 更新本地缓存
const info = uni.getStorageSync('userInfo') || {}
info.nickName = form.nickName
info.avatarUrl = form.avatarUrl
info.phone = form.phone
uni.setStorageSync('userInfo', info)
uni.hideLoading()
uni.showToast({ title: '保存成功', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 800)
} catch (err: any) {
console.error('保存用户信息失败:', err)
uni.hideLoading()
uni.showToast({ title: err.message || '保存失败', icon: 'none' })
} finally {
saving.value = false
}
}
</script>
<style lang="scss">
$primary: #F97316;
$primary-bg: #FFF7ED;
$text-dark: #431407;
$text-muted: #9CA3AF;
$card-bg: #FFFFFF;
$border-color: #FED7AA;
$radius-lg: 28rpx;
$shadow: 0 8rpx 24rpx rgba(249, 115, 22, 0.08);
.edit-page {
min-height: 100vh;
background-color: $primary-bg;
padding: 24rpx;
box-sizing: border-box;
}
// === 头像 ===
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
padding: 48rpx 0 32rpx;
}
.section-label {
font-size: 26rpx;
font-weight: 600;
color: $text-muted;
margin-bottom: 24rpx;
}
.avatar-row {
display: flex;
justify-content: center;
}
.avatar-btn {
position: relative;
width: 180rpx;
height: 180rpx;
padding: 0;
margin: 0;
border-radius: 50%;
border: 6rpx solid $border-color;
background: $card-bg;
overflow: hidden;
box-shadow: $shadow;
&::after {
border: none;
}
}
.avatar-img {
width: 100%;
height: 100%;
}
.avatar-edit-icon {
position: absolute;
bottom: 6rpx;
right: 6rpx;
width: 52rpx;
height: 52rpx;
border-radius: 50%;
background: linear-gradient(135deg, #FB923C, #F97316);
border: 3rpx solid #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 8rpx rgba(124, 45, 18, 0.2);
}
.avatar-uploading-mask {
position: absolute;
inset: 0;
border-radius: 50%;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
}
.avatar-btn.uploading::after {
border: none;
}
// === 表单 ===
.form-section {
background: $card-bg;
border-radius: $radius-lg;
border: 2rpx solid $border-color;
box-shadow: $shadow;
overflow: hidden;
}
.form-item {
display: flex;
align-items: center;
padding: 28rpx 28rpx;
border-bottom: 2rpx solid #FEF3C7;
&:last-child {
border-bottom: none;
}
}
.form-label {
font-size: 28rpx;
font-weight: 600;
color: $text-dark;
width: 120rpx;
flex-shrink: 0;
}
.form-input {
flex: 1;
font-size: 28rpx;
color: $text-dark;
text-align: right;
&::placeholder {
color: #D1D5DB;
}
}
// === 保存按钮 ===
.btn-section {
padding: 48rpx 24rpx;
}
.save-btn {
width: 100%;
height: 96rpx;
line-height: 96rpx;
background: linear-gradient(135deg, #F97316 0%, #FB923C 100%) !important;
border-radius: 48rpx;
font-size: 32rpx;
font-weight: 600;
color: #FFFFFF;
box-shadow: 0 12rpx 28rpx rgba(249, 115, 22, 0.3);
&::after {
border: none;
}
&[disabled] {
opacity: 0.6;
}
}
</style>