Files
yunzerwebsiteallinone/uniapp/pages/profile/profile.vue
T
2026-11-11 17:30:06 +08:00

546 lines
13 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="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>