<template>
|
<view class="container">
|
<view class="top-bg">
|
<image src="/static/image/home/bg.png" mode="widthFix" class="bg-img"></image>
|
</view>
|
<view class="status-bar-placeholder" :style="{ height: statusBarHeight + 'px' }"></view>
|
|
<view class="content">
|
<view class="top-actions">
|
<view class="notice-action" @tap="goPage('/pages/notice/notice')">
|
<image class="action-icon" src="/static/image/mine/notice.png" mode="aspectFit"></image>
|
<view v-if="unreadNoticeTotal > 0" class="notice-badge">{{ noticeBadgeText }}</view>
|
</view>
|
<image class="action-icon" src="/static/image/mine/setting.png" mode="aspectFit" @tap="goPage('/pages/settings/settings')"></image>
|
</view>
|
|
<view class="user-info">
|
<view class="avatar-wrap">
|
<image class="avatar" :src="displayAvatar" mode="aspectFill"></image>
|
<view class="role-tag" :class="userRole === 'doctor' ? 'doctor-tag' : 'personal-tag'">
|
{{ roleTagText }}
|
</view>
|
</view>
|
<view class="user-detail">
|
<view class="user-name">{{ displayName }}</view>
|
<view v-if="userRole === 'personal'" class="user-points">积分:{{ displayPoints }}</view>
|
</view>
|
</view>
|
|
|
<view class="grid-section card-box">
|
<view class="grid-item" v-for="(item, index) in gridMenuList" :key="`${item.title}-${index}`" @tap="handleGridClick(item)">
|
<image class="grid-icon" :src="item.icon" mode="aspectFit"></image>
|
<text class="grid-title">{{ item.title }}</text>
|
</view>
|
</view>
|
|
<view class="list-section card-box">
|
<view class="list-item" v-for="(item, index) in bottomMenuList" :key="`${item.title}-${index}`" @tap="handleBottomMenuClick(item)">
|
<view class="list-left">
|
<image class="list-icon" :class="item.iconClass" :src="item.icon" mode="aspectFit"></image>
|
<text class="list-title">{{ item.title }}</text>
|
</view>
|
<u-icon name="arrow-right" color="#999" size="16"></u-icon>
|
</view>
|
</view>
|
<view class="app-version-text">海诊康道 版本{{ appVersionText }}</view>
|
<view class="icp-text">ICP备案号:琼ICP备2025059220号-3A</view>
|
</view>
|
</view>
|
</template>
|
|
<script setup lang="ts">
|
import { computed, ref } from 'vue'
|
import { onLoad, onShow } from '@dcloudio/uni-app'
|
import { getNoticeList } from '../notice/service'
|
import { getUserCenter } from './service'
|
import { hasStoredSession } from '@/utils/auth-session.js'
|
import manifestConfig from '../../manifest.json'
|
|
type UserRole = 'personal' | 'doctor'
|
|
interface UserCenterData {
|
id?: number | null
|
nickname?: string
|
avatar?: string
|
phone?: string
|
type?: number | null
|
typeText?: string
|
roleCode?: string
|
roleCodes?: string[]
|
memberExpireAt?: string
|
pointsBalance?: number | null
|
clinicId?: number | null
|
clinicName?: string
|
}
|
|
interface AppUserCache {
|
id?: number | null
|
nickname?: string
|
avatar?: string
|
phone?: string
|
type?: number | null
|
typeText?: string
|
roleCode?: string
|
roleCodes?: string[]
|
memberExpireAt?: string
|
clinicId?: number | null
|
}
|
|
interface MenuItem {
|
title: string
|
icon: string
|
url: string
|
iconClass?: string
|
requiresLogin?: boolean
|
}
|
|
interface NoticeSummaryItem {
|
badge?: number | string | null
|
}
|
|
const statusBarHeight = ref(0)
|
const unreadNoticeTotal = ref(0)
|
const manifestVersionName = String(manifestConfig.versionName || '')
|
const appVersionText = manifestVersionName
|
const userCenter = ref<UserCenterData>(createDefaultUserCenter())
|
const userRole = computed<UserRole>(() => {
|
return isDoctorLikeUser(userCenter.value) ? 'doctor' : 'personal'
|
})
|
const roleTagText = computed(() => {
|
const typeText = normalizeOptionalText(userCenter.value.typeText)
|
if (typeText.includes('店长')) {
|
return '店长'
|
}
|
return userRole.value === 'doctor' ? '医生' : '个人'
|
})
|
const displayName = computed(() => {
|
return normalizeOptionalText(userCenter.value.nickname) || normalizeOptionalText(userCenter.value.phone) || (hasStoredSession() ? '已登录用户' : '游客')
|
})
|
const displayAvatar = computed(() => {
|
return normalizeOptionalText(userCenter.value.avatar) || '/static/image/mine/header.png'
|
})
|
const displayPoints = computed(() => {
|
const points = Number(userCenter.value.pointsBalance || 0)
|
return Number.isFinite(points) ? points : 0
|
})
|
const noticeBadgeText = computed(() => {
|
return unreadNoticeTotal.value > 99 ? '99+' : String(unreadNoticeTotal.value)
|
})
|
const gridMenuList = computed<MenuItem[]>(() => {
|
if (userRole.value === 'doctor') {
|
return [
|
{ title: '购课记录', icon: '/static/image/mine/Book@2x.png', url: '/pages/courseRecord/courseRecord', requiresLogin: true },
|
{ title: '收藏列表', icon: '/static/image/mine/list.png', url: '/pages/favorites/favorites', requiresLogin: true },
|
{ title: '我的下载', icon: '/static/image/mine/downLoad.png', url: '/pages/downloads/downloads', requiresLogin: true },
|
{ title: '我的患者', icon: '/static/image/mine/person.png', url: '/pages/myPatients/myPatients', requiresLogin: true },
|
{ title: '诊所信息', icon: '/static/image/mine/hosptial.png', url: '/pages/clinicInfo/clinicInfo', requiresLogin: true },
|
...(Number(userCenter.value.type) === 1 ? [
|
{ title: '银行卡设置', icon: '/static/image/mine/yinghangka.png', url: '/pages/bankCard/bankCard', requiresLogin: true },
|
] : []),
|
{ title: '二维码', icon: '/static/image/mine/code.png', url: '/pages/inviteUser/inviteUser', requiresLogin: true },
|
]
|
}
|
return [
|
{ title: '购课记录', icon: '/static/image/mine/Book@2x.png', url: '/pages/courseRecord/courseRecord', requiresLogin: true },
|
{ title: '兑换订单', icon: '/static/image/mine/Shopping@2x.png', url: '/pages/exchangeRecord/exchangeRecord', requiresLogin: true },
|
{ title: '收藏列表', icon: '/static/image/mine/list.png', url: '/pages/favorites/favorites', requiresLogin: true },
|
{ title: '我的下载', icon: '/static/image/mine/downLoad.png', url: '/pages/downloads/downloads', requiresLogin: true },
|
{ title: '我的积分', icon: '/static/image/mine/point.png', url: '/pages/point/point', requiresLogin: true },
|
]
|
})
|
|
const WEIZAN_LIVE_STATS_URL = 'https://wzzhibo.haizhentong.com/live/m/agent/index?zbid=1247381477&v=1787212616638'
|
const WEIZAN_LIVE_ORDER_URL = 'https://wzzhibo.haizhentong.com/live/m/order/myorder?zbid=1247381477&tpid=0&state=0&v=1787212292181'
|
|
const bottomMenuList = computed<MenuItem[]>(() => {
|
const baseList = [
|
{ title: '我的课程', icon: '/static/image/mine/myClass.png', url: '/pages/myCourses/myCourses', iconClass: 'icon-myclass', requiresLogin: true },
|
{ title: '帮助中心', icon: '/static/image/mine/help.png', url: '/pages/helpCenter/helpCenter', iconClass: 'icon-help' },
|
{ title: '意见反馈', icon: '/static/image/mine/message.png', url: '/pages/feedback/feedback', iconClass: 'icon-feedback' },
|
]
|
const liveOrderItem = { title: '直播订单', icon: '/static/image/mine/dingdan.png', url: buildWebviewUrl(WEIZAN_LIVE_ORDER_URL, '直播订单'), iconClass: 'icon-live-order', requiresLogin: true }
|
const liveStatsList = Number(userCenter.value.type) === 1
|
? [{ title: '直播统计', icon: '/static/image/mine/list.png', url: buildWebviewUrl(WEIZAN_LIVE_STATS_URL, '直播统计'), iconClass: 'icon-live-stats', requiresLogin: true }]
|
: []
|
const agreementList = [
|
{ title: '用户协议', icon: '/static/image/mine/coustom.png', url: '/pages/protocol/protocol?type=user', iconClass: 'icon-user-agree' },
|
{ title: '隐私协议', icon: '/static/image/mine/agree.png', url: '/pages/protocol/protocol?type=privacy', iconClass: 'icon-privacy' },
|
]
|
if (userRole.value === 'doctor') {
|
return [
|
...liveStatsList,
|
liveOrderItem,
|
...baseList,
|
{ title: '平台资质', icon: '/static/image/mine/chimaping.png', url: '/pages/qualification/qualification', iconClass: 'icon-platform' },
|
...agreementList,
|
]
|
}
|
return [
|
liveOrderItem,
|
...baseList,
|
...agreementList,
|
]
|
})
|
|
/**
|
* 生成微赞 H5 内嵌页地址。
|
*/
|
function buildWebviewUrl(url: string, title: string) {
|
return `/pages/webview/webview?url=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}`
|
}
|
|
/**
|
* 创建个人中心默认数据,便于首屏兜底。
|
*/
|
function createDefaultUserCenter(): UserCenterData {
|
return {
|
id: null,
|
nickname: '',
|
avatar: '',
|
phone: '',
|
type: 0,
|
typeText: '个人',
|
memberExpireAt: '',
|
pointsBalance: 0,
|
clinicId: null,
|
clinicName: '',
|
}
|
}
|
|
/**
|
* 页面加载时初始化状态栏高度并回显本地缓存。
|
*/
|
onLoad(() => {
|
const systemInfo = uni.getSystemInfoSync()
|
statusBarHeight.value = systemInfo.statusBarHeight || 20
|
syncCachedUserInfo()
|
})
|
|
/**
|
* 页面回显时重新拉取个人中心聚合数据,保证头像和积分同步刷新。
|
*/
|
onShow(() => {
|
syncCachedUserInfo()
|
loadUserCenter()
|
loadUnreadNoticeTotal()
|
})
|
|
/**
|
* 拉取当前登录用户的通知未读总数。
|
*/
|
async function loadUnreadNoticeTotal() {
|
try {
|
const response = await getNoticeList()
|
if (response?.code !== 0 || !Array.isArray(response.data)) {
|
unreadNoticeTotal.value = 0
|
return
|
}
|
unreadNoticeTotal.value = response.data.reduce((total: number, item: NoticeSummaryItem) => {
|
return total + normalizeNoticeBadge(item?.badge)
|
}, 0)
|
} catch {
|
// 通知角标失败时静默降级,避免影响我的页主体信息展示。
|
unreadNoticeTotal.value = 0
|
}
|
}
|
|
/**
|
* 将通知分类未读数量归一化为非负整数。
|
*/
|
function normalizeNoticeBadge(value: unknown) {
|
const parsedValue = Number(value)
|
if (!Number.isFinite(parsedValue) || parsedValue <= 0) {
|
return 0
|
}
|
return Math.floor(parsedValue)
|
}
|
|
/**
|
* 拉取当前登录用户的个人中心聚合数据。
|
*/
|
async function loadUserCenter() {
|
try {
|
const response = await getUserCenter()
|
if (response?.code !== 0 || !response.data) {
|
return
|
}
|
userCenter.value = normalizeUserCenter(response.data)
|
cacheUserCenter(userCenter.value)
|
} catch {
|
// 未登录或网络异常时由统一请求层处理跳转,这里不重复 toast。
|
}
|
}
|
|
/**
|
* 使用本地登录缓存兜底首屏展示,减少页面闪烁。
|
*/
|
function syncCachedUserInfo() {
|
const cachedUser = uni.getStorageSync('appUserInfo')
|
if (!cachedUser || typeof cachedUser !== 'object') {
|
userCenter.value = createDefaultUserCenter()
|
return
|
}
|
userCenter.value = normalizeUserCenter(cachedUser)
|
}
|
|
/**
|
* 将接口或缓存中的用户信息归一化为页面可直接消费的结构。
|
*/
|
function normalizeUserCenter(value: unknown): UserCenterData {
|
const source = (value && typeof value === 'object' ? value : {}) as Partial<UserCenterData> & Record<string, unknown>
|
const type = resolveUserType(source)
|
return {
|
id: Number.isFinite(Number(source.id)) ? Number(source.id) : null,
|
nickname: normalizeOptionalText(source.nickname) || normalizeOptionalText(source.nickName) || normalizeOptionalText(source.name),
|
avatar: normalizeOptionalText(source.avatar) || normalizeOptionalText(source.headImgUrl),
|
phone: normalizeOptionalText(source.phone) || normalizeOptionalText(source.mobile),
|
type,
|
typeText: normalizeOptionalText(source.typeText),
|
roleCode: normalizeOptionalText(source.roleCode),
|
roleCodes: Array.isArray(source.roleCodes) ? source.roleCodes.map((item) => normalizeOptionalText(item)).filter(Boolean) : [],
|
memberExpireAt: normalizeOptionalText(source.memberExpireAt),
|
pointsBalance: Number.isFinite(Number(source.pointsBalance)) ? Number(source.pointsBalance) : 0,
|
clinicId: resolveClinicId(source),
|
clinicName: normalizeOptionalText(source.clinicName),
|
}
|
}
|
|
/**
|
* 兼容登录缓存、个人中心接口不同字段;绑定诊所/店长文案都视为医生端用户。
|
*/
|
function resolveUserType(source: Partial<UserCenterData> & Record<string, unknown>) {
|
const explicitType = Number(source.type ?? source.userType ?? source.user_type)
|
if (Number.isFinite(explicitType)) {
|
return explicitType === 1 ? 1 : 0
|
}
|
const rawType = normalizeOptionalText(source.type || source.userType || source.user_type).toLowerCase()
|
if (['doctor', 'operator', 'clinic_manager', 'manager', 'store_manager'].includes(rawType)) {
|
return 1
|
}
|
const clinicId = Number(resolveClinicId(source) || 0)
|
if (Number.isFinite(clinicId) && clinicId > 0) {
|
return 1
|
}
|
const roleCodes = Array.isArray(source.roleCodes) ? source.roleCodes.join(',') : ''
|
const typeText = normalizeOptionalText(source.typeText || source.roleName || source.roleText || source.roleCode || roleCodes).toLowerCase()
|
if (typeText.includes('医生') || typeText.includes('店长') || typeText.includes('诊所')
|
|| typeText.includes('doctor') || typeText.includes('clinic') || typeText.includes('manager')) {
|
return 1
|
}
|
return 0
|
}
|
|
function resolveClinicId(source: Record<string, unknown>) {
|
const clinicId = Number(source.clinicId ?? source.clinic_id ?? source.clinicID ?? source.storeId ?? source.store_id ?? 0)
|
return Number.isFinite(clinicId) && clinicId > 0 ? clinicId : null
|
}
|
|
function isDoctorLikeUser(value: UserCenterData) {
|
return Number(value.type) === 1
|
|| (Number.isFinite(Number(value.clinicId)) && Number(value.clinicId) > 0)
|
|| normalizeOptionalText(value.typeText).includes('医生')
|
|| normalizeOptionalText(value.typeText).includes('店长')
|
|| normalizeOptionalText(value.roleCode).toLowerCase().includes('doctor')
|
|| normalizeOptionalText(value.roleCode).toLowerCase().includes('manager')
|
|| (Array.isArray(value.roleCodes) && value.roleCodes.some((item) => {
|
const code = normalizeOptionalText(item).toLowerCase()
|
return code.includes('doctor') || code.includes('manager') || code.includes('clinic')
|
}))
|
}
|
|
/**
|
* 将个人中心接口返回的关键用户字段同步回本地登录缓存。
|
*/
|
function cacheUserCenter(centerData: UserCenterData) {
|
const cachedUser = uni.getStorageSync('appUserInfo')
|
const nextCache: AppUserCache = {
|
...(cachedUser && typeof cachedUser === 'object' ? cachedUser : {}),
|
id: centerData.id,
|
nickname: centerData.nickname,
|
avatar: centerData.avatar,
|
phone: centerData.phone,
|
type: centerData.type,
|
typeText: centerData.typeText,
|
roleCode: centerData.roleCode,
|
roleCodes: centerData.roleCodes,
|
memberExpireAt: centerData.memberExpireAt,
|
clinicId: centerData.clinicId,
|
}
|
uni.setStorageSync('appUserInfo', nextCache)
|
}
|
|
/**
|
* 点击宫格菜单时跳转目标页面。
|
*/
|
function handleGridClick(item: MenuItem) {
|
if (!item.url) {
|
return
|
}
|
goPage(item)
|
}
|
|
/**
|
* 处理底部普通菜单跳转。
|
*/
|
async function handleBottomMenuClick(item: MenuItem) {
|
goPage(item)
|
}
|
|
/**
|
* 统一跳转个人中心相关页面。
|
*/
|
function goPage(target: string | MenuItem) {
|
const url = typeof target === 'string' ? target : target.url
|
if (!url) {
|
return
|
}
|
if (typeof target !== 'string' && target.requiresLogin && !hasStoredSession()) {
|
redirectToLogin()
|
return
|
}
|
uni.navigateTo({
|
url,
|
})
|
}
|
|
/**
|
* 未登录访问个人资产类页面时,先进入登录页。
|
*/
|
function redirectToLogin() {
|
uni.showToast({
|
title: '请先登录',
|
icon: 'none',
|
})
|
setTimeout(() => {
|
uni.navigateTo({
|
url: '/pages/login/login',
|
})
|
}, 200)
|
}
|
|
/**
|
* 将可选文本归一化为空字符串。
|
*/
|
function normalizeOptionalText(value: unknown) {
|
if (typeof value !== 'string') {
|
return ''
|
}
|
return value.trim()
|
}
|
</script>
|
|
<style lang="scss" scoped>
|
page {
|
background-color: #F8F6F4;
|
}
|
|
.container {
|
width: 750rpx;
|
min-height: 100vh;
|
position: relative;
|
background-color: #F8F6F4;
|
padding-bottom: env(safe-area-inset-bottom);
|
}
|
|
.top-bg {
|
position: absolute;
|
top: 0;
|
left: 0;
|
width: 750rpx;
|
z-index: 0;
|
|
.bg-img {
|
width: 750rpx;
|
height: auto;
|
}
|
}
|
|
.status-bar-placeholder {
|
width: 100%;
|
position: relative;
|
z-index: 10;
|
}
|
|
.content {
|
position: relative;
|
z-index: 10;
|
padding: 0 32rpx;
|
padding-bottom: 40rpx;
|
}
|
|
.top-actions {
|
display: flex;
|
justify-content: flex-end;
|
align-items: center;
|
padding-top: 80rpx;
|
gap: 32rpx;
|
|
.notice-action {
|
position: relative;
|
width: 48rpx;
|
height: 48rpx;
|
}
|
|
.action-icon {
|
width: 48rpx;
|
height: 48rpx;
|
}
|
|
.notice-badge {
|
position: absolute;
|
top: -8rpx;
|
right: -12rpx;
|
min-width: 28rpx;
|
height: 28rpx;
|
padding: 0 8rpx;
|
border-radius: 16rpx;
|
background-color: #E64340;
|
color: #FFFFFF;
|
font-size: 20rpx;
|
font-weight: 500;
|
line-height: 28rpx;
|
text-align: center;
|
box-sizing: border-box;
|
}
|
}
|
|
.user-info {
|
display: flex;
|
align-items: center;
|
margin-top: 24rpx;
|
|
.avatar-wrap {
|
position: relative;
|
width: 120rpx;
|
height: 120rpx;
|
|
.avatar {
|
width: 120rpx;
|
height: 120rpx;
|
border-radius: 50%;
|
background-color: #eee;
|
}
|
|
.role-tag {
|
position: absolute;
|
bottom: -10rpx;
|
left: 50%;
|
transform: translateX(-50%);
|
color: #fff;
|
font-size: 20rpx;
|
padding: 2rpx 12rpx;
|
border-radius: 10rpx;
|
white-space: nowrap;
|
z-index: 1;
|
}
|
|
.doctor-tag {
|
background-color: #E64340;
|
}
|
|
.personal-tag {
|
background-color: #FF5A5F;
|
}
|
}
|
|
.user-detail {
|
margin-left: 32rpx;
|
display: flex;
|
flex-direction: column;
|
|
.user-name {
|
font-size: 40rpx;
|
color: #1A1A1A;
|
font-weight: 500;
|
}
|
|
.user-points {
|
font-size: 24rpx;
|
color: #666;
|
margin-top: 8rpx;
|
}
|
}
|
}
|
|
.vip-banner {
|
position: relative;
|
width: 100%;
|
height: 140rpx;
|
border-radius: 16rpx;
|
overflow: hidden;
|
margin-top: 48rpx;
|
margin-bottom: 32rpx;
|
|
.vip-bg {
|
position: absolute;
|
width: 100%;
|
height: 100%;
|
top: 0;
|
left: 0;
|
}
|
|
.vip-content {
|
position: relative;
|
width: 100%;
|
height: 100%;
|
box-sizing: border-box;
|
padding: 24rpx 32rpx 24rpx 140rpx;
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
|
.vip-left {
|
display: flex;
|
flex-direction: column;
|
justify-content: center;
|
|
.vip-title {
|
font-size: 32rpx;
|
font-weight: bold;
|
color: #AA5200;
|
margin-bottom: 4rpx;
|
}
|
|
.vip-desc {
|
font-size: 24rpx;
|
color: #AA5200;
|
opacity: 0.8;
|
}
|
}
|
|
.vip-btn {
|
background: linear-gradient(180deg, #FFD99B 0%, #FFB13C 100%);
|
color: #AA5200;
|
font-size: 24rpx;
|
font-weight: bold;
|
padding: 12rpx 24rpx;
|
border-radius: 30rpx;
|
}
|
}
|
}
|
|
.card-box {
|
background-color: #FFFFFF;
|
border-radius: 16rpx;
|
padding: 32rpx;
|
margin-bottom: 24rpx;
|
}
|
|
.grid-section {
|
display: grid;
|
grid-template-columns: repeat(4, 1fr);
|
gap: 40rpx 0;
|
|
.grid-item {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
|
.grid-icon {
|
width: 56rpx;
|
height: 56rpx;
|
margin-bottom: 16rpx;
|
}
|
|
.grid-title {
|
font-size: 26rpx;
|
color: #1A1A1A;
|
}
|
}
|
}
|
|
.list-section {
|
padding: 0 32rpx;
|
|
.list-item {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
height: 110rpx;
|
border-bottom: 1px solid #F2F3F5;
|
|
&:last-child {
|
border-bottom: none;
|
}
|
|
.list-left {
|
display: flex;
|
align-items: center;
|
|
.list-icon {
|
width: 40rpx;
|
height: 40rpx;
|
|
&.icon-help {
|
width: 34rpx;
|
height: 34rpx;
|
}
|
|
&.icon-feedback {
|
width: 34rpx;
|
height: 34rpx;
|
}
|
|
&.icon-platform {
|
width: 34rpx;
|
height: 34rpx;
|
}
|
|
&.icon-user-agree {
|
width: 30rpx;
|
height: 30rpx;
|
}
|
|
&.icon-privacy {
|
width: 27rpx;
|
height: 33rpx;
|
}
|
}
|
|
.list-title {
|
font-size: 32rpx;
|
color: #1A1A1A;
|
margin-left: 22rpx;
|
}
|
|
}
|
}
|
}
|
|
.app-version-text,
|
.icp-text {
|
font-size: 28rpx;
|
color: #86909C;
|
text-align: center;
|
}
|
|
.app-version-text {
|
margin-top: 32rpx;
|
}
|
|
.icp-text {
|
margin-top: 8rpx;
|
padding-bottom: 20rpx;
|
}
|
</style>
|