- add category/tag caching and optimistic updates - add stats dashboard aggregation and batched tracking - add undo delete snackbar and group read-only view mode - improve app-ready timeout handling and refresh guards
265 lines
5.9 KiB
Vue
265 lines
5.9 KiB
Vue
<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 class="form-divider"></view>
|
||
<view class="form-item">
|
||
<text class="form-label">签名</text>
|
||
<input class="form-input" v-model="formSlogan" placeholder="写一句个性签名吧" maxlength="100" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="tips-card">
|
||
<text class="tips-title">提示</text>
|
||
<text class="tips-text">• 昵称最长 50 个字符</text>
|
||
<text class="tips-text">• 签名最长 100 个字符</text>
|
||
<text class="tips-text">• 头像支持 JPG、PNG、WebP 格式,最大 2MB</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted } from 'vue'
|
||
import { useUserStore } from '@/stores/user'
|
||
import { waitForReady } from '@/utils/app-ready'
|
||
import { statusBarHeight, capsuleRight } from '@/utils/system'
|
||
import Icon from '@/components/Icon/Icon.vue'
|
||
|
||
const userStore = useUserStore()
|
||
const formNickname = ref('')
|
||
const formSlogan = ref('')
|
||
const previewUrl = ref('')
|
||
const uploading = ref(false)
|
||
const saving = ref(false)
|
||
|
||
onMounted(async () => {
|
||
try { await waitForReady() } catch { /* 超时,继续加载 */ }
|
||
formNickname.value = userStore.nickname
|
||
formSlogan.value = userStore.slogan
|
||
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,
|
||
slogan: formSlogan.value.trim()
|
||
})
|
||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||
setTimeout(() => uni.navigateBack(), 1500)
|
||
} catch {
|
||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@import '@/styles/mixins.scss';
|
||
|
||
.page {
|
||
@include page-base;
|
||
}
|
||
|
||
.header-fixed {
|
||
@include sticky-header;
|
||
}
|
||
|
||
.status-bar { @include status-bar; }
|
||
|
||
.nav-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: $space-sm $space-xl $space-md;
|
||
}
|
||
|
||
.nav-back {
|
||
@include nav-back;
|
||
}
|
||
|
||
.nav-title {
|
||
flex: 1;
|
||
text-align: center;
|
||
font-size: $font-2xl;
|
||
font-weight: 600;
|
||
color: $text;
|
||
}
|
||
|
||
.nav-save {
|
||
width: 88rpx;
|
||
height: 88rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
&:active { opacity: 0.6; }
|
||
}
|
||
|
||
.save-text {
|
||
font-size: $font-lg;
|
||
font-weight: 600;
|
||
color: $primary;
|
||
}
|
||
|
||
.avatar-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: $space-2xl 0 $space-lg;
|
||
}
|
||
|
||
.avatar-wrap {
|
||
width: 160rpx;
|
||
height: 160rpx;
|
||
border-radius: 50%;
|
||
border: 6rpx solid rgba(255, 255, 255, 0.5);
|
||
background: linear-gradient(135deg, $primary, #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: 64rpx;
|
||
height: 64rpx;
|
||
border-radius: 50%;
|
||
background: rgba(0, 0, 0, 0.4);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.avatar-hint {
|
||
font-size: $font-md;
|
||
color: $text-muted;
|
||
margin-top: $space-sm;
|
||
}
|
||
|
||
.form-card {
|
||
margin: $space-lg $space-xl 0;
|
||
background: $surface;
|
||
border-radius: $radius-2xl;
|
||
border: 2rpx solid $border;
|
||
box-shadow: $shadow-md;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.form-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: $space-lg $space-xl;
|
||
}
|
||
|
||
.form-divider {
|
||
height: 2rpx;
|
||
background: $border;
|
||
margin: 0 $space-xl;
|
||
}
|
||
|
||
.form-label {
|
||
width: 120rpx;
|
||
font-size: $font-lg;
|
||
font-weight: 500;
|
||
color: $text;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.form-input {
|
||
flex: 1;
|
||
font-size: $font-lg;
|
||
color: $text;
|
||
}
|
||
|
||
.tips-card {
|
||
margin: $space-lg $space-xl 0;
|
||
padding: $space-lg $space-xl;
|
||
background: $surface;
|
||
border-radius: $radius-2xl;
|
||
border: 2rpx solid $border;
|
||
box-shadow: $shadow-md;
|
||
}
|
||
|
||
.tips-title {
|
||
font-size: $font-lg;
|
||
font-weight: 600;
|
||
color: $text;
|
||
display: block;
|
||
margin-bottom: $space-sm;
|
||
}
|
||
|
||
.tips-text {
|
||
font-size: $font-md;
|
||
color: $text-sec;
|
||
display: block;
|
||
margin-bottom: $space-xs;
|
||
}
|
||
</style>
|