| | |
| | | <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="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="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="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="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> |
| | | <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> |
| | | <view class="list-right"> |
| | | <text v-if="item.versionText" class="list-version-text">{{ item.versionText }}</text> |
| | | <u-icon name="arrow-right" color="#999" size="16"></u-icon> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <!-- <view class="app-version-text">海诊康道 版本{{ appVersionText }}</view> --> |
| | | <view class="icp-text">ICP备案号:琼ICP备2025059220号-3A</view> |
| | | </view> |
| | | <update-popup |
| | | :show="versionPopupVisible" |
| | | :title="versionPopupTitle" |
| | | :content="versionPopupContent" |
| | | :force-update="versionPopupForceUpdate" |
| | | :confirm-text="versionUpdateButtonText" |
| | | :submitting="versionUpdating" |
| | | @close="closeVersionPopup" |
| | | @submit="handleUpdateNow" |
| | | /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { computed, ref } from 'vue' |
| | | import { onLoad, onShow } from '@dcloudio/uni-app' |
| | | import UpdatePopup from '@/components/updatePopup.vue' |
| | | import { getNoticeList } from '../notice/service' |
| | | import { getUserCenter } from './service' |
| | | import { hasStoredSession } from '@/utils/auth-session.js' |
| | | import { checkAppVersion, normalizeVersionCheckData, normalizeVersionText, resolveRuntimeVersion, startVersionUpdate } from '@/utils/app-version.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 |
| | | 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 |
| | | 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 |
| | | title: string |
| | | icon: string |
| | | url: string |
| | | iconClass?: string |
| | | requiresLogin?: boolean |
| | | versionText?: string |
| | | } |
| | | |
| | | interface NoticeSummaryItem { |
| | | badge?: number | string | null |
| | | badge?: number | string | null |
| | | } |
| | | |
| | | const statusBarHeight = ref(0) |
| | | const unreadNoticeTotal = ref(0) |
| | | const isCheckingVersion = ref(false) |
| | | const manifestVersionName = String(manifestConfig.versionName || '') |
| | | const appVersionText = manifestVersionName |
| | | const appVersionText = ref(manifestVersionName) |
| | | const versionPopupVisible = ref(false) |
| | | const versionPopupTitle = ref('更新提示') |
| | | const versionPopupContent = ref('') |
| | | const versionPopupForceUpdate = ref(false) |
| | | const versionDownloadUrl = ref('') |
| | | const versionUpdating = ref(false) |
| | | const versionUpdateButtonText = ref('立即更新') |
| | | const userCenter = ref<UserCenterData>(createDefaultUserCenter()) |
| | | const userRole = computed<UserRole>(() => { |
| | | return isDoctorLikeUser(userCenter.value) ? 'doctor' : 'personal' |
| | | return isDoctorLikeUser(userCenter.value) ? 'doctor' : 'personal' |
| | | }) |
| | | const roleTagText = computed(() => { |
| | | const typeText = normalizeOptionalText(userCenter.value.typeText) |
| | | if (typeText.includes('店长')) { |
| | | return '店长' |
| | | } |
| | | return userRole.value === 'doctor' ? '医生' : '个人' |
| | | 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() ? '已登录用户' : '游客') |
| | | return normalizeOptionalText(userCenter.value.nickname) || normalizeOptionalText(userCenter.value.phone) || (hasStoredSession() ? '已登录用户' : '游客') |
| | | }) |
| | | const displayAvatar = computed(() => { |
| | | return normalizeOptionalText(userCenter.value.avatar) || '/static/image/mine/header.png' |
| | | 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 points = Number(userCenter.value.pointsBalance || 0) |
| | | return Number.isFinite(points) ? points : 0 |
| | | }) |
| | | const noticeBadgeText = computed(() => { |
| | | return unreadNoticeTotal.value > 99 ? '99+' : String(unreadNoticeTotal.value) |
| | | 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 }, |
| | | ] |
| | | 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, |
| | | ] |
| | | 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' }, |
| | | { title: '更新版本', icon: '/static/image/mine/setting.png', url: '', iconClass: 'icon-version', versionText: `V${appVersionText.value}` }, |
| | | ] |
| | | 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)}` |
| | | 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: '', |
| | | } |
| | | 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() |
| | | const systemInfo = uni.getSystemInfoSync() |
| | | statusBarHeight.value = systemInfo.statusBarHeight || 20 |
| | | syncAppVersion() |
| | | syncCachedUserInfo() |
| | | }) |
| | | |
| | | /** |
| | | * 页面回显时重新拉取个人中心聚合数据,保证头像和积分同步刷新。 |
| | | */ |
| | | onShow(() => { |
| | | syncCachedUserInfo() |
| | | loadUserCenter() |
| | | loadUnreadNoticeTotal() |
| | | 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 |
| | | } |
| | | 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) |
| | | 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。 |
| | | } |
| | | 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) |
| | | 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), |
| | | } |
| | | 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 |
| | | 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 |
| | | 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') |
| | | })) |
| | | 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) |
| | | 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) |
| | | if (!item.url) { |
| | | return |
| | | } |
| | | goPage(item) |
| | | } |
| | | |
| | | /** |
| | | * 处理底部普通菜单跳转。 |
| | | */ |
| | | async function handleBottomMenuClick(item: MenuItem) { |
| | | goPage(item) |
| | | if (item.title === '更新版本') { |
| | | await handleCheckAppVersion() |
| | | return |
| | | } |
| | | 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, |
| | | }) |
| | | 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) |
| | | 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() |
| | | if (typeof value !== 'string') { |
| | | return '' |
| | | } |
| | | return value.trim() |
| | | } |
| | | |
| | | /** |
| | | * 检查 APP 最新版本,按真实返回结构展示结果,暂不执行下载或安装。 |
| | | */ |
| | | async function handleCheckAppVersion() { |
| | | if (isCheckingVersion.value) { |
| | | return |
| | | } |
| | | isCheckingVersion.value = true |
| | | try { |
| | | const response = await checkAppVersion() |
| | | const versionInfo = normalizeVersionCheckData(response?.data) |
| | | if (!versionInfo.latestVersionNo) { |
| | | uni.showToast({ |
| | | title: '暂未获取到最新版本', |
| | | icon: 'none', |
| | | }) |
| | | return |
| | | } |
| | | |
| | | const currentVersion = normalizeVersionText(appVersionText.value) |
| | | const latestVersion = normalizeVersionText(versionInfo.latestVersionNo) |
| | | |
| | | if (!versionInfo.hasUpdate) { |
| | | uni.showToast({ |
| | | title: `当前已是最新版本 V${latestVersion}`, |
| | | icon: 'none', |
| | | }) |
| | | return |
| | | } |
| | | |
| | | const popupContent = versionInfo.updateNotice || `发现新版本 V${latestVersion},请及时更新` |
| | | openVersionPopup('更新提示', popupContent, versionInfo.forceUpdate, versionInfo.downloadUrl) |
| | | } catch (e) { |
| | | uni.showToast({ |
| | | title: '检查更新失败', |
| | | icon: 'none', |
| | | }) |
| | | } finally { |
| | | isCheckingVersion.value = false |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 优先读取运行时版本号,取不到时回退 manifest 配置版本。 |
| | | */ |
| | | function syncAppVersion() { |
| | | appVersionText.value = resolveRuntimeVersion(manifestVersionName || '1.0.0') |
| | | } |
| | | |
| | | /** |
| | | * 打开版本更新弹窗。 |
| | | */ |
| | | function openVersionPopup(title: string, content: string, forceUpdate: boolean, downloadUrl: string) { |
| | | versionPopupTitle.value = title || '更新提示' |
| | | versionPopupContent.value = content |
| | | versionPopupForceUpdate.value = forceUpdate |
| | | versionDownloadUrl.value = downloadUrl |
| | | versionUpdating.value = false |
| | | versionUpdateButtonText.value = '立即更新' |
| | | versionPopupVisible.value = true |
| | | } |
| | | |
| | | /** |
| | | * 关闭版本更新弹窗。 |
| | | */ |
| | | function closeVersionPopup() { |
| | | if (versionPopupForceUpdate.value) { |
| | | return |
| | | } |
| | | versionPopupVisible.value = false |
| | | } |
| | | |
| | | /** |
| | | * 点击立即更新后,Android 下载 APK 并拉起安装,iOS 继续跳转更新地址。 |
| | | */ |
| | | async function handleUpdateNow() { |
| | | if (versionUpdating.value) { |
| | | return |
| | | } |
| | | versionUpdating.value = true |
| | | try { |
| | | await startVersionUpdate(versionDownloadUrl.value, (progressInfo: { text?: string }) => { |
| | | versionUpdateButtonText.value = progressInfo?.text || '立即更新' |
| | | }) |
| | | versionPopupVisible.value = false |
| | | } catch (e) { |
| | | console.warn('[mine] 执行更新失败:', e) |
| | | } finally { |
| | | versionUpdating.value = false |
| | | versionUpdateButtonText.value = '立即更新' |
| | | } |
| | | } |
| | | |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | page { |
| | | background-color: #F8F6F4; |
| | | background-color: #F8F6F4; |
| | | } |
| | | |
| | | .container { |
| | | width: 750rpx; |
| | | min-height: 100vh; |
| | | position: relative; |
| | | background-color: #F8F6F4; |
| | | padding-bottom: env(safe-area-inset-bottom); |
| | | 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; |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | width: 750rpx; |
| | | z-index: 0; |
| | | |
| | | .bg-img { |
| | | width: 750rpx; |
| | | height: auto; |
| | | } |
| | | .bg-img { |
| | | width: 750rpx; |
| | | height: auto; |
| | | } |
| | | } |
| | | |
| | | .status-bar-placeholder { |
| | | width: 100%; |
| | | position: relative; |
| | | z-index: 10; |
| | | width: 100%; |
| | | position: relative; |
| | | z-index: 10; |
| | | } |
| | | |
| | | .content { |
| | | position: relative; |
| | | z-index: 10; |
| | | padding: 0 32rpx; |
| | | padding-bottom: 40rpx; |
| | | 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; |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | align-items: center; |
| | | padding-top: 80rpx; |
| | | gap: 32rpx; |
| | | |
| | | .notice-action { |
| | | position: relative; |
| | | width: 48rpx; |
| | | height: 48rpx; |
| | | } |
| | | .notice-action { |
| | | position: relative; |
| | | width: 48rpx; |
| | | height: 48rpx; |
| | | } |
| | | |
| | | .action-icon { |
| | | 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; |
| | | } |
| | | .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; |
| | | display: flex; |
| | | align-items: center; |
| | | margin-top: 24rpx; |
| | | |
| | | .avatar-wrap { |
| | | position: relative; |
| | | width: 120rpx; |
| | | height: 120rpx; |
| | | .avatar-wrap { |
| | | position: relative; |
| | | width: 120rpx; |
| | | height: 120rpx; |
| | | |
| | | .avatar { |
| | | width: 120rpx; |
| | | height: 120rpx; |
| | | border-radius: 50%; |
| | | background-color: #eee; |
| | | } |
| | | .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; |
| | | } |
| | | .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; |
| | | } |
| | | .doctor-tag { |
| | | background-color: #E64340; |
| | | } |
| | | |
| | | .personal-tag { |
| | | background-color: #FF5A5F; |
| | | } |
| | | } |
| | | .personal-tag { |
| | | background-color: #FF5A5F; |
| | | } |
| | | } |
| | | |
| | | .user-detail { |
| | | margin-left: 32rpx; |
| | | display: flex; |
| | | flex-direction: column; |
| | | .user-detail { |
| | | margin-left: 32rpx; |
| | | display: flex; |
| | | flex-direction: column; |
| | | |
| | | .user-name { |
| | | font-size: 40rpx; |
| | | color: #1A1A1A; |
| | | font-weight: 500; |
| | | } |
| | | .user-name { |
| | | font-size: 40rpx; |
| | | color: #1A1A1A; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .user-points { |
| | | font-size: 24rpx; |
| | | color: #666; |
| | | margin-top: 8rpx; |
| | | } |
| | | } |
| | | .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; |
| | | 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-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-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-left { |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: center; |
| | | |
| | | .vip-title { |
| | | font-size: 32rpx; |
| | | font-weight: bold; |
| | | color: #AA5200; |
| | | margin-bottom: 4rpx; |
| | | } |
| | | .vip-title { |
| | | font-size: 32rpx; |
| | | font-weight: bold; |
| | | color: #AA5200; |
| | | margin-bottom: 4rpx; |
| | | } |
| | | |
| | | .vip-desc { |
| | | font-size: 24rpx; |
| | | color: #AA5200; |
| | | opacity: 0.8; |
| | | } |
| | | } |
| | | .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; |
| | | } |
| | | } |
| | | .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; |
| | | background-color: #FFFFFF; |
| | | border-radius: 16rpx; |
| | | padding: 32rpx; |
| | | margin-bottom: 24rpx; |
| | | } |
| | | |
| | | .grid-section { |
| | | display: grid; |
| | | grid-template-columns: repeat(4, 1fr); |
| | | gap: 40rpx 0; |
| | | display: grid; |
| | | grid-template-columns: repeat(4, 1fr); |
| | | gap: 40rpx 0; |
| | | |
| | | .grid-item { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | .grid-item { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | |
| | | .grid-icon { |
| | | width: 56rpx; |
| | | height: 56rpx; |
| | | margin-bottom: 16rpx; |
| | | } |
| | | .grid-icon { |
| | | width: 56rpx; |
| | | height: 56rpx; |
| | | margin-bottom: 16rpx; |
| | | } |
| | | |
| | | .grid-title { |
| | | font-size: 26rpx; |
| | | color: #1A1A1A; |
| | | } |
| | | } |
| | | .grid-title { |
| | | font-size: 26rpx; |
| | | color: #1A1A1A; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .list-section { |
| | | padding: 0 32rpx; |
| | | padding: 0 32rpx; |
| | | |
| | | .list-item { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | height: 110rpx; |
| | | border-bottom: 1px solid #F2F3F5; |
| | | .list-item { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | height: 110rpx; |
| | | border-bottom: 1px solid #F2F3F5; |
| | | |
| | | &:last-child { |
| | | border-bottom: none; |
| | | } |
| | | &:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .list-left { |
| | | display: flex; |
| | | align-items: center; |
| | | .list-right { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .list-icon { |
| | | width: 40rpx; |
| | | height: 40rpx; |
| | | .list-left { |
| | | display: flex; |
| | | align-items: center; |
| | | |
| | | &.icon-help { |
| | | width: 34rpx; |
| | | height: 34rpx; |
| | | } |
| | | .list-icon { |
| | | width: 40rpx; |
| | | height: 40rpx; |
| | | |
| | | &.icon-feedback { |
| | | width: 34rpx; |
| | | height: 34rpx; |
| | | } |
| | | &.icon-help { |
| | | width: 34rpx; |
| | | height: 34rpx; |
| | | } |
| | | |
| | | &.icon-platform { |
| | | width: 34rpx; |
| | | height: 34rpx; |
| | | } |
| | | &.icon-feedback { |
| | | width: 34rpx; |
| | | height: 34rpx; |
| | | } |
| | | |
| | | &.icon-user-agree { |
| | | width: 30rpx; |
| | | height: 30rpx; |
| | | } |
| | | &.icon-platform { |
| | | width: 34rpx; |
| | | height: 34rpx; |
| | | } |
| | | |
| | | &.icon-privacy { |
| | | width: 27rpx; |
| | | height: 33rpx; |
| | | } |
| | | } |
| | | &.icon-user-agree { |
| | | width: 30rpx; |
| | | height: 30rpx; |
| | | } |
| | | |
| | | .list-title { |
| | | font-size: 32rpx; |
| | | color: #1A1A1A; |
| | | margin-left: 22rpx; |
| | | } |
| | | &.icon-privacy { |
| | | width: 27rpx; |
| | | height: 33rpx; |
| | | } |
| | | |
| | | } |
| | | } |
| | | &.icon-version { |
| | | width: 30rpx; |
| | | height: 30rpx; |
| | | } |
| | | } |
| | | |
| | | .list-title { |
| | | font-size: 32rpx; |
| | | color: #1A1A1A; |
| | | margin-left: 22rpx; |
| | | } |
| | | } |
| | | |
| | | .list-version-text { |
| | | margin-right: 16rpx; |
| | | font-size: 24rpx; |
| | | color: #86909C; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .app-version-text, |
| | | .icp-text { |
| | | font-size: 28rpx; |
| | | color: #86909C; |
| | | text-align: center; |
| | | font-size: 28rpx; |
| | | color: #86909C; |
| | | text-align: center; |
| | | } |
| | | |
| | | .app-version-text { |
| | | margin-top: 32rpx; |
| | | margin-top: 32rpx; |
| | | } |
| | | |
| | | .icp-text { |
| | | margin-top: 8rpx; |
| | | padding-bottom: 20rpx; |
| | | margin-top: 8rpx; |
| | | padding-bottom: 20rpx; |
| | | } |
| | | </style> |