Files
QSY-lvjuxcx/pages/index/component/ProfileCenterTab.vue
liangjian 9fee2c3a74 feat: 优化首页服务入口与租住业务流程
补齐首页待办跳转、押金支付引导和多类租住服务页面能力,统一租客端关键业务流程体验。
2026-08-25 13:53:12 +08:00

932 lines
30 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="profile-page tn-safe-area-inset-bottom">
<!-- 头部背景与用户信息 (悬浮质感设计) -->
<view class="profile-header-bg">
<view class="bg-shape-1"></view>
<view class="bg-shape-2"></view>
</view>
<view class="profile-header tn-padding" :style="{ paddingTop: headerPaddingTop + 'px' }">
<view class="user-profile-card tn-padding-lg">
<view class="card-decor"></view>
<!-- 用户身份信息 -->
<view class="user-info-row tn-flex tn-flex-row-between tn-flex-col-center">
<view class="tn-flex tn-flex-col-center" v-if="isLogin" style="flex: 1; overflow: hidden;">
<view class="avatar-wrapper" style="position: relative;">
<view class="avatar-box avatar-text">
<image v-if="displayAvatar" :src="displayAvatar" class="avatar-image" mode="aspectFill"></image>
<template v-else>{{ displayUserInitial }}</template>
</view>
<view
v-if="userInfo.gender"
class="gender-badge tn-flex tn-flex-col-center tn-flex-row-center"
:class="userInfo.gender === 1 || userInfo.gender === 'M' || userInfo.gender === '男' ? 'gender-male' : 'gender-female'"
>
<text :class="userInfo.gender === 1 || userInfo.gender === 'M' || userInfo.gender === '男' ? 'tn-icon-male' : 'tn-icon-female'" class="gender-badge-icon"></text>
</view>
</view>
<view class="tn-margin-left-md" style="flex: 1; overflow: hidden;">
<view class="user-name clamp-text-1">
{{ displayUserName }}
</view>
<view class="user-id-wrap tn-margin-top-xs clamp-text-1">
<text class="user-id">ID: {{ displayUserId }}</text>
</view>
</view>
</view>
<view class="tn-flex tn-flex-col-center" v-else @click="handleGuestEntry" style="flex: 1;">
<view class="avatar-box guest-avatar-box">
<text class="tn-icon-my-fill guest-avatar-icon"></text>
</view>
<view class="tn-margin-left-md">
<view class="user-name clamp-text-1">
{{ $tr('游客') }}
</view>
<view class="tn-color-gray tn-text-sm tn-margin-top-xs">{{ guestHeaderDesc }}</view>
</view>
</view>
<view v-if="isLogin">
<view class="edit-btn">
<text class="tn-icon-edit edit-btn-icon"></text>
</view>
</view>
</view>
<view class="card-divider"></view>
<!-- 数据统计面板 -->
<view class="stats-row tn-flex tn-flex-row-between">
<view class="stat-item">
<view class="stat-value">{{ getStayDaysText() }}</view>
<view class="stat-label">{{ $tr('入住天数') }}</view>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<view class="stat-value">{{ getActivityCountText() }}</view>
<view class="stat-label">{{ $tr('参与活动') }}</view>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<view class="stat-value">{{ getPointsText() }}</view>
<view class="stat-label">{{ $tr('积分') }}</view>
</view>
</view>
</view>
</view>
<!-- 下方内容区 -->
<view class="profile-content tn-padding">
<view class="guest-registration-card tn-padding-lg tn-margin-bottom-lg" v-if="!isLogin">
<view class="tn-flex tn-flex-row-between tn-flex-col-center">
<view>
<view class="tn-text-bold tn-text-lg tn-color-black">{{ $tr('注册记录') }}</view>
<view class="tn-margin-top-sm tn-text-sm tn-color-gray">
{{ registrationApplication ? $tr('当前微信账号已提交注册申请,可查看历史记录与审批状态。') : $tr('未登录也可查看当前微信账号提交的注册申请记录。') }}
</view>
</view>
<view class="custom-btn btn-primary guest-registration-btn" @click="goToRegistrationRecords">
<text class="tn-text-sm tn-text-bold">{{ $tr('立即查看') }}</text>
</view>
</view>
</view>
<view class="application-status-card tn-padding-lg tn-margin-bottom-lg" v-if="isLogin && pendingContractCount">
<view class="tn-flex tn-flex-row-between tn-flex-col-center">
<view class="tn-text-bold tn-text-lg tn-color-black">{{ $tr('合同状态') }}</view>
<view class="status-tag status-pending">
{{ $tr('待签约') + ' ' + pendingContractCount }}
</view>
</view>
<view class="tn-margin-top-sm tn-color-gray tn-text-sm">
{{ $tr('您当前有待签署合同,请尽快查看并完成签约。') }}
</view>
<view class="tn-margin-top tn-flex tn-flex-row-between tn-flex-col-center">
<view class="tn-color-gray tn-text-xs">
{{ $tr('待签合同') + ':' + pendingContractCount }}
</view>
<view class="custom-btn btn-primary" style="padding: 8rpx 28rpx;" @click="goToContractList">
<text class="tn-text-sm tn-text-bold">{{ $tr('查看详情') }}</text>
</view>
</view>
</view>
<!-- 核心服务 (宫格) -->
<view class="core-services-card tn-padding-lg tn-margin-bottom-lg" v-if="isLogin">
<view class="tn-text-bold tn-text-lg tn-color-black tn-margin-bottom">{{ $tr('我的服务') }}</view>
<view class="tn-flex tn-flex-row-between">
<view
class="core-service-item tn-flex tn-flex-direction-column tn-flex-col-center"
v-for="item in coreServices"
:key="item.key"
@click="handleCoreService(item.key)"
>
<view class="service-icon-box tn-flex tn-flex-col-center tn-flex-row-center">
<text :class="item.icon" class="service-icon-symbol"></text>
</view>
<text class="tn-margin-top-sm tn-text-sm tn-color-black">{{ $tr(item.i18nKey) }}</text>
</view>
</view>
</view>
<!-- 快捷菜单 -->
<view class="menu-group" v-if="isLogin">
<view
class="menu-item tn-flex tn-flex-row-between tn-flex-col-center"
v-for="item in businessMenus"
:key="item.key"
@click="openBusinessPage(item)"
>
<view class="tn-flex tn-flex-col-center">
<view class="menu-icon"><text :class="item.icon" class="menu-icon-symbol"></text></view>
<text class="tn-margin-left-sm tn-text-md tn-color-black tn-text-bold">{{ getMenuLabel(item) }}</text>
</view>
<view class="tn-flex tn-flex-col-center">
<view v-if="item.available === false" class="menu-coming-soon">{{ $tr('建设中') }}</view>
<text class="tn-icon-right menu-arrow"></text>
</view>
</view>
<view class="menu-item tn-flex tn-flex-row-between tn-flex-col-center" @click="handleLanguageToggle">
<view class="tn-flex tn-flex-col-center">
<view class="menu-icon"><text class="tn-icon-set menu-icon-symbol"></text></view>
<text class="tn-margin-left-sm tn-text-md tn-color-black tn-text-bold">{{ $tr('语言切换') }}</text>
</view>
<text class="tn-text-sm tn-color-gray">{{ $tr('EN') }}</text>
</view>
</view>
<!-- 退出登录 -->
<view class="tn-margin-top-xl tn-text-center" v-if="isLogin">
<view class="custom-btn btn-danger" style="width: 80%; height: 90rpx;" @click="handleLogout">
<text class="tn-text-bold">{{ $tr('退出登录') }}</text>
</view>
</view>
<!-- 底部占位避开 Tabbar -->
<view style="height: 150rpx;"></view>
</view>
</view>
</template>
<script>
import { clearLoginSession, getSessionSnapshot } from '../../../utils/app-state'
import { registrationMyorder } from '../../../api/index.js'
import { fetchEnums, getApprovalStatusLabel } from '../../../utils/enum-store'
import { getBookingStatusText, getBookingTagType, getStoredUser } from '../../../utils/flow'
import { compactEnglishText } from '../../../utils/select-room-normalizers'
import { addFilePrefix } from '../../../utils/common'
export default {
name: 'ProfileCenterTab',
computed: {
language() {
return this.$getLanguage()
},
headerPaddingTop() {
return typeof this.vuex_custom_bar_height === 'number' ? this.vuex_custom_bar_height : 0
},
displayUserName() {
return this.userInfo.name || this.userInfo.account || this.userInfo.username || this.userInfo.mobile || this.$tr('用户')
},
displayUserInitial() {
const firstChar = this.userInfo.firstChar
if (firstChar) return String(firstChar).charAt(0).toUpperCase()
return this.displayUserName.charAt(0).toUpperCase()
},
displayUserId() {
return this.userInfo.id || this.userInfo.account || this.userInfo.username || '--'
},
displayAvatar() {
return addFilePrefix(this.userInfo.avatar || this.userInfo.base64Face || '')
},
registrationStatus() {
if (!this.registrationApplication) {
return 'unsubmitted'
}
const status = this.registrationApplication.approvalStatus
if (status === 1) return 'approved'
if (status === -1) return 'rejected'
if (status === 0) return 'pending'
return 'unsubmitted'
},
guestHeaderDesc() {
return this.registrationApplication
? `${this.$tr('注册申请进度')}:${this.getRegistrationStatusText(this.registrationStatus)}`
: this.$tr('点击登录')
},
registrationStatusDesc() {
if (!this.registrationApplication) return this.$tr('点击登录')
if (this.registrationStatus === 'pending') {
return this.$tr('注册申请审核中,审批通过后才可登录')
}
if (this.registrationStatus === 'approved') {
return this.$tr('注册申请已通过,可前往登录')
}
if (this.registrationStatus === 'rejected') {
return this.registrationApplication.approveReason || this.$tr('注册申请已驳回,请查看原因后重新提交')
}
return this.$tr('暂未提交注册申请')
},
currentBookingCard() {
if (!this.isLogin) {
return null
}
const booking = this.userInfo.currentBooking || null
if (!booking) {
return null
}
return {
orderNo: booking.orderNo || '',
status: booking.status || booking.statusKey || this.userInfo.bookingStatus || 'none',
rejectReason: booking.rejectReasonText || booking.rejectReason || this.userInfo.bookingRejectReason || ''
}
}
},
watch: {
language() {
fetchEnums(false, this.language)
if (!this.isLogin) {
this.fetchActiveRegistration()
}
}
},
data() {
return {
isLogin: false,
hasCheckedIn: false,
userInfo: {},
registrationApplication: null,
pendingContractCount: 0,
businessMenus: [
{ key: 'lock', label: '门锁授权', labelEn: 'Smart Lock', icon: 'tn-icon-lock', title: '门锁授权', titleEn: 'Smart Lock', desc: '管理门锁授权对象、时段和权限状态。', descEn: 'Manage smart lock authorization, schedule, and permission status.', available: true },
{ key: 'serviceOrder', label: '服务订单', labelEn: 'Service Orders', labelEnShort: 'Orders', icon: 'tn-icon-calendar', title: '服务订单', titleEn: 'Service Orders', desc: '查看报修、投诉、续住、退房等服务订单。', descEn: 'View repair, complaint, renewal, and checkout service orders.', available: true },
{ key: 'registrationRecord', label: '注册记录', labelEn: 'Registration Records', labelEnShort: 'Records', icon: 'tn-icon-form', title: '注册记录', titleEn: 'Registration Records', desc: '查看当前微信账号提交的人员注册申请记录与审批状态。', descEn: 'Review registration records and approval status for this Wechat account.', available: true },
{ key: 'points', label: '积分', labelEn: 'Points', icon: 'tn-icon-star-fill', title: '积分', titleEn: 'Points', desc: '查看积分余额、获取记录和使用说明。', descEn: 'Check point balance, earning records, and usage instructions.', available: true },
{ key: 'code', label: '一码通', labelEn: 'Pass Code', icon: 'tn-icon-qr-code', title: '一码通', titleEn: 'Pass Code', desc: '查看门禁码、访客码和社区通行二维码。', descEn: 'View access codes, visitor codes, and community QR passes.', available: true },
{ key: 'butler', label: '联系管家', labelEn: 'Contact Butler', icon: 'tn-icon-service-fill', title: '联系管家', titleEn: 'Contact Butler', desc: '联系专属管家,沟通入住和住中服务事项。', descEn: 'Contact your butler about move-in and in-stay services.', available: true }
],
coreServices: [
{ key: 'application', i18nKey: '入住申请', icon: 'tn-icon-edit-form' },
{ key: 'bookingList', i18nKey: '预订记录', icon: 'tn-icon-order-fill' },
{ key: 'contract', i18nKey: '我的合同', icon: 'tn-icon-copy' },
{ key: 'bill', i18nKey: '账单', icon: 'tn-icon-money' }
],
}
},
async mounted() {
await fetchEnums(false, this.language)
this.checkLoginStatus();
},
methods: {
handleCoreService(key) {
if (key === 'application') this.handleApplicationEntry();
else if (key === 'bookingList') this.goToBookingList();
else if (key === 'contract') this.goToContractList();
else if (key === 'bill') this.goToUtilityRecharge();
},
getMenuLabel(item = {}) {
return this.language === 'en'
? (item.labelEnShort || item.labelEn || item.label || '')
: (item.label || item.labelEn || '')
},
getStoreUser() {
return (this.$store && this.$store.state && this.$store.state.vuex_user) || {}
},
getCurrentOrder() {
const storeState = (this.$store && this.$store.state) || {}
const session = getSessionSnapshot()
return storeState.vuex_current_order || session.currentOrder || null
},
getStayDaysText() {
if (!this.isLogin || !this.hasCheckedIn) {
return '-'
}
const currentOrder = this.getCurrentOrder() || {}
const startText = currentOrder.contractStartTime || currentOrder.startDate || currentOrder.start || currentOrder.checkinDate || ''
if (!startText) {
return '--'
}
const startDate = new Date(String(startText).replace(/-/g, '/'))
if (Number.isNaN(startDate.getTime())) {
return '--'
}
const diffDays = Math.floor((Date.now() - startDate.getTime()) / (24 * 60 * 60 * 1000)) + 1
return String(Math.max(diffDays, 1))
},
getActivityCountText() {
return this.isLogin ? '--' : '-'
},
getPointsText() {
return this.isLogin ? '--' : '-'
},
getWechatCode() {
return new Promise((resolve) => {
// #ifdef MP-WEIXIN
uni.login({
provider: 'weixin',
success: (res) => {
resolve(res.code || '')
},
fail: (err) => {
console.error('getWechatCode failed', err)
resolve('')
}
})
// #endif
// #ifndef MP-WEIXIN
resolve('')
// #endif
})
},
async fetchActiveRegistration() {
try {
const providerValue = await this.getWechatCode()
if (!providerValue) {
this.registrationApplication = null
return
}
const response = await registrationMyorder({
providerValue
}, this.language)
const rawTarget = response && response.data && typeof response.data === 'object' ? response.data : response
const target = rawTarget.data !== undefined ? rawTarget.data : rawTarget
if (target && typeof target === 'object' && Object.keys(target).length > 0) {
this.registrationApplication = target
} else {
this.registrationApplication = null
}
} catch (e) {
console.error('fetch active registration failed', e)
this.registrationApplication = null
}
},
checkLoginStatus() {
const session = getSessionSnapshot()
const storeState = (this.$store && this.$store.state) || {}
this.isLogin = session.isLogin;
this.hasCheckedIn = !!storeState.vuex_checkin_status || session.hasCheckedIn;
this.pendingContractCount = Number(storeState.vuex_pending_contract_count || session.pendingContractCount || 0)
if (this.isLogin) {
this.userInfo = getStoredUser();
} else {
this.userInfo = {};
this.pendingContractCount = 0
this.fetchActiveRegistration()
}
},
goToLogin() {
uni.navigateTo({
url: '/pkgAccount/login/login'
});
},
handleGuestEntry() {
if (this.registrationApplication) {
this.goToAuthResult()
return
}
this.goToLogin()
},
handleLanguageToggle() {
this.$toggleLocale()
},
handleLogout() {
uni.showModal({
title: this.$tr('提示'),
content: this.$tr('确定要退出登录吗?'),
success: (res) => {
if (res.confirm) {
clearLoginSession()
this.checkLoginStatus();
this.$parent.changeTabbar && this.$parent.changeTabbar(0, 'select-room');
uni.showToast({ title: this.$tr('已退出'), icon: 'success' });
}
}
});
},
getBookingStatusTextLocal(status) {
return getBookingStatusText(status);
},
getBookingTag(status) {
return getBookingTagType(status);
},
getRegistrationStatusText(status) {
if (this.registrationApplication && typeof this.registrationApplication.approvalStatus === 'number') {
return getApprovalStatusLabel(this.registrationApplication.approvalStatus, this.$tr('暂未提交注册申请'))
}
const map = {
unsubmitted: this.$tr('暂未提交注册申请'),
pending: this.$tr('审核中'),
approved: this.$tr('已通过'),
rejected: this.$tr('已拒绝')
}
return map[status] || map.unsubmitted
},
goToAuthResult() {
uni.navigateTo({
url: '/pkgAccount/auth_result/auth_result'
});
},
goToRegistrationRecords() {
uni.navigateTo({
url: '/pkgAccount/registration_records/registration_records'
})
},
goToBookingDetail() {
const booking = this.currentBookingCard
uni.navigateTo({
url: booking && booking.orderNo
? `/pkgBooking/booking_detail/booking_detail?orderNo=${encodeURIComponent(booking.orderNo)}`
: '/pkgBooking/booking_detail/booking_detail'
});
},
goToBookingList() {
uni.navigateTo({
url: '/pkgBooking/booking_list/booking_list'
});
},
goToContractList() {
uni.navigateTo({
url: '/pkgService/my_contract/my_contract'
});
},
goToUtilityRecharge() {
uni.navigateTo({
url: '/pkgService/utility_recharge/utility_recharge'
});
},
goToTicketCenter() {
uni.navigateTo({
url: '/pkgService/ticket_center/ticket_center'
})
},
goToSubmitRequest(title, desc, payload = {}, titleZh = '', titleEn = '', descZh = '', descEn = '') {
uni.navigateTo({
url: `/pkgService/submit_request/submit_request?title=${encodeURIComponent(title || this.$tr('提交申请'))}&titleZh=${encodeURIComponent(titleZh || title || this.$tr('提交申请'))}&titleEn=${encodeURIComponent(titleEn || title || this.$tr('提交申请'))}&desc=${encodeURIComponent(desc || this.$tr('请完善表单信息后提交。'))}&descZh=${encodeURIComponent(descZh || desc || this.$tr('请完善表单信息后提交。'))}&descEn=${encodeURIComponent(descEn || desc || this.$tr('请完善表单信息后提交。'))}&payload=${encodeURIComponent(JSON.stringify(payload || {}))}`
})
},
handleApplicationEntry() {
uni.navigateTo({
url: '/pkgService/my_application/my_application'
});
// if (this.userInfo.currentBooking) {
// this.goToBookingDetail();
// return;
// }
},
openBusinessPage(item) {
const itemTitleZh = item.title || item.titleEn || item.label || ''
const itemTitleEn = item.labelEnShort || item.titleEn || item.title || item.label || ''
const itemDescZh = item.desc || item.descEn || ''
const itemDescEn = compactEnglishText(item.descEn || item.desc || '', 72) || item.descEn || item.desc || ''
const itemTitle = this.language === 'en'
? itemTitleEn
: itemTitleZh
const itemDesc = this.language === 'en'
? itemDescEn
: itemDescZh
if (item.key === 'contract') {
this.goToContractList()
return
}
if (item.key === 'bill') {
this.goToUtilityRecharge()
return
}
if (item.key === 'serviceOrder') {
this.goToTicketCenter()
return
}
if (item.key === 'registrationRecord') {
this.goToRegistrationRecords()
return
}
if (item.key === 'lock') {
uni.navigateTo({
url: '/pkgService/lock_control/lock_control'
})
return
}
if (item.key === 'butler') {
this.goToSubmitRequest(itemTitle, itemDesc, {
businessType: 'butler-contact',
sourceType: 'profile-center'
}, itemTitleZh, itemTitleEn, itemDescZh, itemDescEn)
return
}
const comingSoonDesc = this.$tr('{title} 功能正在建设中,后续开放后会在这里提供正式入口和数据。', { title: itemTitle })
const comingSoonDescZh = this.$tr('{title} 功能正在建设中,后续开放后会在这里提供正式入口和数据。', { title: itemTitleZh || itemTitle })
const comingSoonDescEn = this.$tr('{title} 功能正在建设中,后续开放后会在这里提供正式入口和数据。', { title: itemTitleEn || itemTitle })
uni.navigateTo({
url: `/pkgService/service_stub/service_stub?title=${encodeURIComponent(itemTitle)}&titleZh=${encodeURIComponent(itemTitleZh || itemTitle)}&titleEn=${encodeURIComponent(itemTitleEn || itemTitle)}&desc=${encodeURIComponent(comingSoonDesc)}&descZh=${encodeURIComponent(comingSoonDescZh)}&descEn=${encodeURIComponent(comingSoonDescEn)}&key=${item.key}`
});
}
}
}
</script>
<style scoped lang="scss">
.profile-page {
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
min-height: 100vh;
position: relative;
}
/* 顶部炫酷背景 */
.profile-header-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 520rpx;
background: linear-gradient(135deg, #E8F2FF 0%, #F8FAFF 100%);
z-index: 0;
overflow: hidden;
border-bottom-left-radius: 60rpx;
border-bottom-right-radius: 60rpx;
}
.bg-shape-1 {
position: absolute;
top: -100rpx;
right: -50rpx;
width: 500rpx;
height: 500rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba($brand-primary, 0.12) 0%, rgba(255, 255, 255, 0) 70%);
}
.bg-shape-2 {
position: absolute;
bottom: -50rpx;
left: -50rpx;
width: 400rpx;
height: 400rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba($brand-primary, 0.08) 0%, rgba(255, 255, 255, 0) 70%);
}
.profile-header {
position: relative;
z-index: 1;
}
/* 用户卡片重构 */
.user-profile-card {
margin-top: 20rpx;
background: #ffffff;
border-radius: 32rpx;
box-shadow: 0 16rpx 40rpx rgba(43, 142, 255, 0.08);
position: relative;
overflow: hidden;
border: 1rpx solid rgba(255, 255, 255, 0.8);
}
.card-decor {
position: absolute;
top: -60rpx;
right: -40rpx;
width: 200rpx;
height: 200rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba(43, 142, 255, 0.06) 0%, rgba(255, 255, 255, 0) 70%);
z-index: 0;
}
.user-info-row {
position: relative;
z-index: 1;
}
.user-name {
font-size: 44rpx;
font-weight: 800;
color: #1e293b;
letter-spacing: 2rpx;
}
.user-id-wrap {
display: inline-flex;
}
.user-id {
padding: 4rpx 16rpx;
background: rgba(43, 142, 255, 0.08);
color: #2b8eff;
border-radius: 12rpx;
font-size: 24rpx;
font-family: monospace;
}
.edit-btn {
width: 64rpx;
height: 64rpx;
border-radius: 32rpx;
background: rgba(43, 142, 255, 0.05);
color: #2b8eff;
display: flex;
justify-content: center;
align-items: center;
transition: all 0.2s;
}
.edit-btn:active {
background: rgba(43, 142, 255, 0.15);
transform: scale(0.95);
}
.card-divider {
height: 2rpx;
background: linear-gradient(90deg, rgba(226, 232, 240, 0.2) 0%, rgba(226, 232, 240, 1) 50%, rgba(226, 232, 240, 0.2) 100%);
margin: 32rpx 0;
}
.stats-row {
padding: 0 10rpx;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.stat-value {
font-size: 40rpx;
font-weight: 800;
color: #0f172a;
font-family: 'Helvetica Neue', sans-serif;
}
.stat-label {
font-size: 24rpx;
color: #64748b;
margin-top: 8rpx;
}
.stat-divider {
width: 2rpx;
height: 40rpx;
background: #e2e8f0;
margin-top: 16rpx;
}
.avatar-box {
width: 130rpx;
height: 130rpx;
border-radius: 65rpx;
border: 6rpx solid #FFFFFF;
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.15);
position: relative;
overflow: hidden;
}
.avatar-image {
width: 100%;
height: 100%;
display: block;
}
.avatar-text {
background: linear-gradient(135deg, rgba($brand-primary, 0.9) 0%, $brand-primary 100%);
color: #FFFFFF;
font-size: 56rpx;
font-weight: bold;
display: flex;
justify-content: center;
align-items: center;
}
.guest-avatar-box {
background-color: #FFFFFF;
display: flex;
justify-content: center;
align-items: center;
}
.guest-avatar-icon {
color: $brand-primary;
font-size: 48rpx;
}
.gender-badge {
position: absolute;
bottom: 0;
right: -8rpx;
width: 40rpx;
height: 40rpx;
border-radius: 20rpx;
border: 4rpx solid #FFFFFF;
}
.gender-male {
background-color: #2b8eff;
}
.gender-female {
background-color: #ff5a92;
}
.gender-badge-icon {
color: #FFFFFF;
font-size: 24rpx;
}
.edit-btn {
width: 60rpx;
height: 60rpx;
border-radius: 30rpx;
background-color: #FFFFFF;
color: $brand-primary;
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.1);
}
.edit-btn-icon {
font-size: 28rpx;
color: $brand-primary;
}
/* 数据统计面板 */
.stats-panel {
background-color: #FFFFFF;
border-radius: 24rpx;
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
border: 1rpx solid rgba($brand-primary, 0.05);
}
.stats-divider {
width: 2rpx;
height: 60rpx;
background-color: rgba($brand-primary, 0.1);
}
/* 专属管家横幅 */
.butler-banner {
background: linear-gradient(135deg, #F0F6FF 0%, #E8F2FF 100%);
border-radius: 24rpx;
border: 1rpx solid rgba($brand-primary, 0.15);
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.05);
}
.butler-icon {
width: 70rpx;
height: 70rpx;
border-radius: 20rpx;
background: #ffffff;
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.1);
}
.butler-icon-symbol {
color: $brand-primary;
font-size: 36rpx;
}
/* 申请状态卡片 */
.application-status-card {
background-color: #FFFFFF;
border-radius: 24rpx;
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
border: 1rpx solid rgba($brand-primary, 0.05);
}
.guest-registration-card {
background-color: #FFFFFF;
border-radius: 24rpx;
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
border: 1rpx solid rgba($brand-primary, 0.05);
}
.guest-registration-btn {
flex-shrink: 0;
min-width: 152rpx;
padding-left: 24rpx;
padding-right: 24rpx;
}
/* 核心服务卡片 */
.core-services-card {
background-color: #FFFFFF;
border-radius: 24rpx;
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
border: 1rpx solid rgba($brand-primary, 0.05);
}
.core-service-item {
width: 25%;
}
.service-icon-box {
width: 90rpx;
height: 90rpx;
border-radius: 24rpx;
background: linear-gradient(135deg, rgba($brand-primary, 0.05) 0%, rgba($brand-primary, 0.15) 100%);
transition: all 0.2s;
}
.core-service-item:active .service-icon-box {
transform: scale(0.9);
background: linear-gradient(135deg, rgba($brand-primary, 0.1) 0%, rgba($brand-primary, 0.2) 100%);
}
.service-icon-symbol {
font-size: 48rpx;
color: $brand-primary;
}
.status-tag {
padding: 6rpx 20rpx;
border-radius: 12rpx;
font-size: 24rpx;
font-weight: bold;
}
.status-pending {
background-color: rgba(255, 165, 0, 0.1);
color: #FFA500;
}
.status-unsubmitted {
background-color: rgba(189, 189, 189, 0.16);
color: #6d6d6d;
}
.status-approved {
background-color: rgba(43, 142, 60, 0.1);
color: #2B8E3C;
}
.status-rejected {
background-color: rgba(255, 90, 95, 0.1);
color: #FF5A5F;
}
.menu-group {
background-color: #FFFFFF;
border-radius: 24rpx;
padding: 10rpx 40rpx;
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
border: 1rpx solid rgba($brand-primary, 0.05);
}
.menu-item {
padding: 40rpx 0;
border-bottom: 2rpx solid #F5F5F5;
min-width: 0;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-icon {
width: 60rpx;
height: 60rpx;
border-radius: 16rpx;
background: linear-gradient(135deg, rgba($brand-primary, 0.1) 0%, rgba($brand-primary, 0.2) 100%);
color: $brand-primary;
display: flex;
justify-content: center;
align-items: center;
}
.menu-icon-symbol {
font-size: 32rpx;
line-height: 1;
color: $brand-primary;
}
.menu-arrow {
color: $icon-tone-muted;
font-size: 28rpx;
line-height: 1;
}
.menu-item .tn-flex:first-child {
flex: 1;
min-width: 0;
}
.menu-item .tn-flex:first-child text:last-child {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.menu-coming-soon {
margin-right: 12rpx;
padding: 6rpx 16rpx;
border-radius: 999rpx;
background: rgba($brand-primary, 0.1);
color: $brand-primary;
font-size: 20rpx;
font-weight: 600;
}
.status-card-icon {
color: $icon-tone-brand-deep;
font-size: 32rpx;
}
/* 自定义按钮样式 */
.custom-btn {
display: inline-flex;
justify-content: center;
align-items: center;
border-radius: 100rpx;
padding: 12rpx 36rpx;
transition: all 0.2s;
border: 1rpx solid transparent;
}
.custom-btn:active {
transform: scale(0.95);
}
.btn-primary {
background: linear-gradient(135deg, rgba($brand-primary, 0.9) 0%, $brand-primary 100%);
color: #ffffff;
box-shadow: 0 6rpx 16rpx rgba($brand-primary, 0.2);
}
.btn-dark {
background-color: #222222;
color: $brand-primary;
}
.btn-danger {
background-color: #ffffff;
color: #FF5A5F;
border: 1rpx solid rgba(255, 90, 95, 0.3);
box-shadow: 0 4rpx 12rpx rgba(255, 90, 95, 0.05);
}
</style>