546 lines
13 KiB
Vue
546 lines
13 KiB
Vue
<template>
|
||
<view class="page">
|
||
<view class="profile-header" :style="headPad">
|
||
<view class="header-top">
|
||
<view class="back-platform" @tap="goBackPlatform">
|
||
<FaIcon name="chevron-left" color="#ffffff" :size="14" />
|
||
<text>回平台</text>
|
||
</view>
|
||
</view>
|
||
<view class="profile-info">
|
||
<view class="profile-avatar">
|
||
<text class="avatar-text">{{ avatarText }}</text>
|
||
</view>
|
||
<view class="profile-detail">
|
||
<text class="profile-name">{{ user?.nickname || user?.name || '温新云用户' }}</text>
|
||
<text class="profile-id">{{ user?.account ? `账号: ${user.account}` : `ID: ${userId}` }}</text>
|
||
</view>
|
||
<view class="profile-edit" @tap="onEdit">
|
||
<FaIcon name="pen-to-square" color="#ffffff" :size="16" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="profile-stats">
|
||
<view class="profile-stat" v-for="item in profileStats" :key="item.label">
|
||
<text class="stat-num">{{ item.value }}</text>
|
||
<text class="stat-label">{{ item.label }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="main-scroll" style="margin-top:10rpx">
|
||
<view class="menu-group" v-for="(group, gIndex) in menuGroups" :key="gIndex">
|
||
<u-cell-group :border="false">
|
||
<u-cell
|
||
v-for="item in group"
|
||
:key="item.title"
|
||
:title="item.title"
|
||
is-link
|
||
@click="onMenuTap(item)"
|
||
>
|
||
<template #icon>
|
||
<view class="menu-icon">
|
||
<FaIcon :name="item.icon" color="#2563eb" :size="18" />
|
||
</view>
|
||
</template>
|
||
<template #value>
|
||
<text v-if="item.badge" class="menu-badge">{{ item.badge }}</text>
|
||
</template>
|
||
</u-cell>
|
||
</u-cell-group>
|
||
</view>
|
||
|
||
<view class="logout-wrap">
|
||
<u-button
|
||
type="error"
|
||
plain
|
||
shape="circle"
|
||
text="退出登录"
|
||
@click="handleLogout"
|
||
/>
|
||
</view>
|
||
|
||
<view class="bottom-space" />
|
||
</view>
|
||
|
||
<AppTabbar current="profile" />
|
||
|
||
<!-- 编辑资料:姓名 / 邮箱可改,手机号由微信验证只读 -->
|
||
<view v-if="editVisible" class="edit-mask" @tap="closeEdit">
|
||
<view class="edit-sheet" @tap.stop>
|
||
<view class="edit-head">
|
||
<text class="edit-title">编辑资料</text>
|
||
<text class="edit-close" @tap="closeEdit">取消</text>
|
||
</view>
|
||
<view class="edit-field">
|
||
<text class="edit-label">姓名</text>
|
||
<input v-model="editForm.name" class="edit-input" placeholder="请输入姓名" :maxlength="30" />
|
||
</view>
|
||
<view class="edit-field">
|
||
<text class="edit-label">邮箱</text>
|
||
<input v-model="editForm.email" class="edit-input" placeholder="选填" />
|
||
</view>
|
||
<view class="edit-field">
|
||
<text class="edit-label">手机号</text>
|
||
<view class="edit-readonly">
|
||
<text>{{ phoneText }}</text>
|
||
<text class="readonly-tag">微信验证,不可修改</text>
|
||
</view>
|
||
</view>
|
||
<text v-if="editError" class="edit-error">{{ editError }}</text>
|
||
<button class="edit-submit" :disabled="saving" @tap="saveProfile">
|
||
{{ saving ? '保存中…' : '保存' }}
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, onMounted } from 'vue'
|
||
import { mpHeadPad } from '@/utils/mpHead.js'
|
||
|
||
const headPad = mpHeadPad()
|
||
import { getUser, setUser, logout, isLoggedIn, isSSOEnabled, getToken } from '@/utils/auth.js'
|
||
import { getCurrentUser, updateCurrentUserProfile, logoutApi, logoutUAC } from '@/api/auth.js'
|
||
import { requestSubscribeNotice } from '@/utils/subscribe.js'
|
||
import AppTabbar from '@/components/MainTabbar.vue'
|
||
|
||
const user = ref(null)
|
||
|
||
const avatarText = computed(() => {
|
||
const name = user.value?.nickname || user.value?.name || '云'
|
||
return name.charAt(0).toUpperCase()
|
||
})
|
||
|
||
const userId = computed(() => {
|
||
if (user.value?.phone) {
|
||
return user.value.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||
}
|
||
if (user.value?.account) return user.value.account
|
||
if (user.value?.id) return String(user.value.id)
|
||
return '-'
|
||
})
|
||
|
||
const profileStats = ref([
|
||
{ label: '关注', value: '128' },
|
||
{ label: '粉丝', value: '256' },
|
||
{ label: '获赞', value: '1.2k' }
|
||
])
|
||
|
||
const menuGroups = ref([
|
||
[
|
||
{ title: '我的订单', icon: 'clipboard-list' },
|
||
{ title: '我的收藏', icon: 'star' },
|
||
{ title: '浏览历史', icon: 'clock-rotate-left' }
|
||
],
|
||
[
|
||
{ title: '消息通知', icon: 'bell', badge: '3' },
|
||
{ title: '账号安全', icon: 'lock' },
|
||
{ title: '隐私设置', icon: 'eye' }
|
||
],
|
||
[
|
||
{ title: '帮助中心', icon: 'circle-question' },
|
||
{ title: '关于我们', icon: 'circle-info' },
|
||
{ title: '设置', icon: 'gear' }
|
||
]
|
||
])
|
||
|
||
async function loadUser() {
|
||
user.value = getUser()
|
||
try {
|
||
const data = await getCurrentUser()
|
||
if (data) {
|
||
const prev = getUser() || {}
|
||
const merged = {
|
||
...prev,
|
||
...data,
|
||
nickname: data.name || data.nickname || data.account || prev.nickname || '温新云用户'
|
||
}
|
||
setUser(merged)
|
||
user.value = merged
|
||
}
|
||
} catch {
|
||
// 401 时 request 会跳转登录
|
||
user.value = getUser()
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
if (!isLoggedIn()) {
|
||
// 新流程没有独立登录页:回平台点击模块即可重新授权手机号
|
||
uni.reLaunch({ url: '/pages/nav/nav' })
|
||
return
|
||
}
|
||
loadUser()
|
||
})
|
||
|
||
/* ==================== 编辑资料 ==================== */
|
||
const editVisible = ref(false)
|
||
const saving = ref(false)
|
||
const editError = ref('')
|
||
const editForm = reactive({ name: '', email: '' })
|
||
|
||
// 手机号来自微信验证,只读展示(掩码),不随表单提交
|
||
const phoneText = computed(() => {
|
||
const u = user.value || {}
|
||
const p = u.phone || (u.account && /^1\d{10}$/.test(String(u.account)) ? String(u.account) : '')
|
||
return p ? String(p).replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '微信已验证'
|
||
})
|
||
|
||
function onEdit() {
|
||
editForm.name = user.value?.nickname || user.value?.name || ''
|
||
editForm.email = user.value?.email || ''
|
||
editError.value = ''
|
||
editVisible.value = true
|
||
}
|
||
|
||
function closeEdit() {
|
||
if (!saving.value) editVisible.value = false
|
||
}
|
||
|
||
async function saveProfile() {
|
||
if (saving.value) return
|
||
const name = String(editForm.name || '').trim()
|
||
const email = String(editForm.email || '').trim()
|
||
if (!name) {
|
||
editError.value = '请填写姓名'
|
||
return
|
||
}
|
||
if (email && !email.includes('@')) {
|
||
editError.value = '邮箱格式不正确'
|
||
return
|
||
}
|
||
saving.value = true
|
||
editError.value = ''
|
||
try {
|
||
const res = await updateCurrentUserProfile({ name, email })
|
||
const next = {
|
||
...(getUser() || {}),
|
||
...(user.value || {}),
|
||
...(res || {}),
|
||
name,
|
||
nickname: name,
|
||
email
|
||
}
|
||
setUser(next)
|
||
user.value = next
|
||
editVisible.value = false
|
||
uni.showToast({ title: '资料已更新', icon: 'success' })
|
||
} catch (e) {
|
||
editError.value = (e && e.message) || '保存失败,请重试'
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
function goBackPlatform() {
|
||
uni.reLaunch({ url: '/pages/nav/nav' })
|
||
}
|
||
|
||
function onMenuTap(item) {
|
||
if (item.title === '账号安全') {
|
||
uni.navigateTo({ url: '/pages/login/forget' })
|
||
return
|
||
}
|
||
if (item.title === '消息通知') {
|
||
// 订阅消息授权:用户同意后,服务器才有一次主动推送的机会
|
||
requestSubscribeNotice()
|
||
return
|
||
}
|
||
uni.showToast({ title: item.title, icon: 'none' })
|
||
}
|
||
|
||
function handleLogout() {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '确定要退出登录吗?',
|
||
confirmText: '退出',
|
||
cancelText: '取消',
|
||
success(res) {
|
||
if (!res.confirm) return
|
||
// 统一认证:先吊销服务端会话与令牌(默认 1 号 1 机,不吊销会占着名额)
|
||
if (isSSOEnabled() && getToken()) {
|
||
logoutUAC().catch(() => {})
|
||
} else {
|
||
logoutApi().catch(() => {})
|
||
}
|
||
// 再清本地并跳转,避免接口超时/失败导致“退出无效”
|
||
logout()
|
||
uni.reLaunch({ url: '/pages/nav/nav' })
|
||
}
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@import '@/src/styles/page-common.scss';
|
||
|
||
.page {
|
||
@include page-shell;
|
||
}
|
||
|
||
.profile-header {
|
||
flex-shrink: 0;
|
||
padding: calc(var(--status-bar-height, 44px) + 24rpx) $page-padding-x 40rpx;
|
||
/* #ifdef MP-WEIXIN */
|
||
padding-top: calc(env(safe-area-inset-top, 25px) + 88rpx);
|
||
/* #endif */
|
||
background: $hero-gradient;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.profile-header::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: -50%;
|
||
right: -50%;
|
||
width: 400rpx;
|
||
height: 400rpx;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.profile-header::after {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: -30%;
|
||
left: -20%;
|
||
width: 350rpx;
|
||
height: 350rpx;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.profile-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 24rpx;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.profile-avatar {
|
||
width: 120rpx;
|
||
height: 120rpx;
|
||
border-radius: 50%;
|
||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.15) 100%);
|
||
border: 3rpx solid rgba(255, 255, 255, 0.4);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.avatar-text {
|
||
font-size: 44rpx;
|
||
font-weight: 600;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.profile-detail {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.profile-name {
|
||
display: block;
|
||
font-size: 36rpx;
|
||
font-weight: 600;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.profile-id {
|
||
display: block;
|
||
font-size: 24rpx;
|
||
color: rgba(255, 255, 255, 0.75);
|
||
margin-top: 6rpx;
|
||
}
|
||
|
||
.profile-edit {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.15);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
&:active {
|
||
background: rgba(255, 255, 255, 0.25);
|
||
}
|
||
}
|
||
|
||
.profile-stats {
|
||
display: flex;
|
||
justify-content: space-around;
|
||
margin-top: 40rpx;
|
||
padding-top: 32rpx;
|
||
border-top: 1rpx solid rgba(255, 255, 255, 0.2);
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.profile-stat {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.stat-num {
|
||
font-size: 36rpx;
|
||
font-weight: 600;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 24rpx;
|
||
color: rgba(255, 255, 255, 0.75);
|
||
margin-top: 6rpx;
|
||
}
|
||
|
||
.main-scroll {
|
||
@include scroll-body;
|
||
margin-top: -20rpx;
|
||
}
|
||
|
||
.menu-group {
|
||
@include card;
|
||
margin-bottom: 24rpx;
|
||
overflow: hidden;
|
||
border-radius: 16rpx;
|
||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
|
||
transition: box-shadow 0.3s ease;
|
||
}
|
||
|
||
.menu-group:active {
|
||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.12);
|
||
}
|
||
|
||
.menu-icon {
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
border-radius: 12rpx;
|
||
background: linear-gradient(135deg, rgba(57, 115, 255, 0.12) 0%, rgba(82, 168, 255, 0.14) 100%);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.menu-badge {
|
||
font-size: 20rpx;
|
||
font-weight: 600;
|
||
color: #ffffff;
|
||
background: linear-gradient(135deg, #2563eb 0%, #60a5fa 100%);
|
||
padding: 6rpx 14rpx;
|
||
border-radius: 12rpx;
|
||
min-width: 36rpx;
|
||
text-align: center;
|
||
line-height: 1.4;
|
||
box-shadow: 0 4rpx 12rpx rgba(37, 99, 235, 0.3);
|
||
}
|
||
|
||
.logout-wrap {
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.bottom-space {
|
||
height: 48rpx;
|
||
}
|
||
/* ============ 回平台 ============ */
|
||
.header-top {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 24rpx;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
.back-platform {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
padding: 10rpx 20rpx;
|
||
border-radius: $radius-full;
|
||
background: rgba(255, 255, 255, .18);
|
||
color: #ffffff;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
/* ============ 编辑资料弹层 ============ */
|
||
.edit-mask {
|
||
position: fixed;
|
||
left: 0;
|
||
top: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 1000;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
background: rgba(15, 23, 42, .48);
|
||
@include mask-responsive;
|
||
}
|
||
.edit-sheet {
|
||
width: 100%;
|
||
padding: 28rpx $page-padding-x 40rpx;
|
||
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||
box-sizing: border-box;
|
||
border-radius: 32rpx 32rpx 0 0;
|
||
background: #ffffff;
|
||
@include sheet-responsive;
|
||
}
|
||
.edit-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 30rpx;
|
||
}
|
||
.edit-title { font-size: 32rpx; font-weight: 700; color: $color-text; }
|
||
.edit-close { font-size: 26rpx; color: $color-text-muted; }
|
||
.edit-field { margin-bottom: 26rpx; }
|
||
.edit-label { display: block; margin-bottom: 12rpx; font-size: 25rpx; font-weight: 600; color: #334155; }
|
||
.edit-input {
|
||
height: 88rpx;
|
||
padding: 0 22rpx;
|
||
box-sizing: border-box;
|
||
border: 1rpx solid #dce7f7;
|
||
border-radius: $radius-md;
|
||
background: #fbfdff;
|
||
font-size: 28rpx;
|
||
color: $color-text;
|
||
}
|
||
.edit-readonly {
|
||
height: 88rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 22rpx;
|
||
box-sizing: border-box;
|
||
border: 1rpx solid #e2e8f0;
|
||
border-radius: $radius-md;
|
||
background: #f1f5f9;
|
||
font-size: 27rpx;
|
||
color: #475569;
|
||
}
|
||
.readonly-tag { font-size: 21rpx; color: #94a3b8; }
|
||
.edit-error { display: block; margin: 4rpx 0 16rpx; font-size: 24rpx; color: $color-danger; }
|
||
.edit-submit {
|
||
height: 92rpx;
|
||
line-height: 92rpx;
|
||
margin-top: 12rpx;
|
||
border: none;
|
||
border-radius: $radius-full;
|
||
background: linear-gradient(135deg, #60a5fa, #2563eb);
|
||
color: #ffffff;
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
|
||
&::after { border: none; }
|
||
&[disabled] { opacity: .6; }
|
||
}
|
||
</style>
|