- 新增工单中心(ticket_center)与工单工作台(ticket_workbench)页面 - 新增房间巡查(room_inspection)与失物招领发布(lost_found_publish)页面 - 预约看样流程新增房型/楼栋/楼层选择器组件 - 新增工具模块: booking-entry、privacy、reservation-flow - api/index.js 补充楼层列表、看样提交等接口 - locales/messages.js 补充多语言文案 - manifest.json 开启微信小程序隐私检查 - 完善登录、注册、申请详情、账单等相关页面交互
1037 lines
35 KiB
Vue
1037 lines
35 KiB
Vue
<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">
|
||
{{ t('guest') }}
|
||
</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">{{ t('daysIn') }}</view>
|
||
</view>
|
||
<view class="stat-divider"></view>
|
||
<view class="stat-item">
|
||
<view class="stat-value">{{ getActivityCountText() }}</view>
|
||
<view class="stat-label">{{ t('activities') }}</view>
|
||
</view>
|
||
<view class="stat-divider"></view>
|
||
<view class="stat-item">
|
||
<view class="stat-value">{{ getPointsText() }}</view>
|
||
<view class="stat-label">{{ t('points') }}</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">{{ t('registrationRecordTitle') }}</view>
|
||
<view class="tn-margin-top-sm tn-text-sm tn-color-gray">
|
||
{{ registrationApplication ? t('registrationRecordSubmittedDesc') : t('registrationRecordGuestDesc') }}
|
||
</view>
|
||
</view>
|
||
<view class="custom-btn btn-primary guest-registration-btn" @click="goToRegistrationRecords">
|
||
<text class="tn-text-sm tn-text-bold">{{ t('viewNow') }}</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">{{ t('contractStatusTitle') }}</view>
|
||
<view class="status-tag status-pending">
|
||
{{ t('contractPendingTag') + ' ' + pendingContractCount }}
|
||
</view>
|
||
</view>
|
||
<view class="tn-margin-top-sm tn-color-gray tn-text-sm">
|
||
{{ t('contractPendingDesc') }}
|
||
</view>
|
||
<view class="tn-margin-top tn-flex tn-flex-row-between tn-flex-col-center">
|
||
<view class="tn-color-gray tn-text-xs">
|
||
{{ t('pendingCountText') + ':' + pendingContractCount }}
|
||
</view>
|
||
<view class="custom-btn btn-primary" style="padding: 8rpx 28rpx;" @click="goToContractList">
|
||
<text class="tn-text-sm tn-text-bold">{{ t('viewDetail') }}</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">{{ t('coreServices') }}</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">{{ t(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">{{ t('comingSoon') }}</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">{{ t('languageSwitch') }}</text>
|
||
</view>
|
||
<text class="tn-text-sm tn-color-gray">{{ t('languageToggleLabel') }}</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">{{ t('logout') }}</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.t('userFallback')
|
||
},
|
||
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.t('registrationStatusTitle')}:${this.getRegistrationStatusText(this.registrationStatus)}`
|
||
: this.t('clickToLogin')
|
||
},
|
||
registrationStatusDesc() {
|
||
if (!this.registrationApplication) return this.t('clickToLogin')
|
||
if (this.registrationStatus === 'pending') {
|
||
return this.t('registrationPendingDesc')
|
||
}
|
||
if (this.registrationStatus === 'approved') {
|
||
return this.t('registrationApprovedDesc')
|
||
}
|
||
if (this.registrationStatus === 'rejected') {
|
||
return this.registrationApplication.approveReason || this.t('registrationRejectedDesc')
|
||
}
|
||
return this.t('registrationEmptyDesc')
|
||
},
|
||
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: 'application', icon: 'tn-icon-edit-form' },
|
||
{ key: 'bookingList', i18nKey: 'bookingList', icon: 'tn-icon-order-fill' },
|
||
{ key: 'contract', i18nKey: 'contract', icon: 'tn-icon-copy' },
|
||
{ key: 'bill', i18nKey: 'bill', icon: 'tn-icon-money' }
|
||
],
|
||
i18n: {
|
||
zh: {
|
||
edit: '编辑资料',
|
||
guest: '游客',
|
||
clickToLogin: '点击登录',
|
||
daysIn: '入住天数',
|
||
activities: '参与活动',
|
||
points: '积分',
|
||
applicationStatus: '申请进度',
|
||
registrationStatusTitle: '注册申请进度',
|
||
notSubmittedYet: '暂未提交注册申请',
|
||
registrationPendingDesc: '注册申请审核中,审批通过后才可登录',
|
||
registrationApprovedDesc: '注册申请已通过,可前往登录',
|
||
registrationRejectedDesc: '注册申请已驳回,请查看原因后重新提交',
|
||
registrationEmptyDesc: '暂未提交注册申请',
|
||
registrationRecordTitle: '注册记录',
|
||
registrationRecordSubmittedDesc: '当前微信账号已提交注册申请,可查看历史记录与审批状态。',
|
||
registrationRecordGuestDesc: '未登录也可查看当前微信账号提交的注册申请记录。',
|
||
viewNow: '立即查看',
|
||
statusPending: '审核中',
|
||
statusApproved: '已通过',
|
||
statusRejected: '已拒绝',
|
||
remind: '催一下',
|
||
done: '已完成',
|
||
viewDetail: '查看详情',
|
||
exclusiveButler: '专属管家',
|
||
onlineService: '24小时在线服务',
|
||
contact: '联系',
|
||
application: '入住申请',
|
||
bookingList: '预订记录',
|
||
bookingStatusTitle: '预订审核进度',
|
||
contractStatusTitle: '合同状态',
|
||
bookingNo: '预定单号',
|
||
bookingPlaceholder: '支付后生成的预定单会显示在这里',
|
||
bookingProgressDesc: '支付完成后会进入审核,审核通过后分配管家并等待分房号。',
|
||
contractPendingTag: '待签约',
|
||
contractPendingDesc: '您当前有待签署合同,请尽快查看并完成签约。',
|
||
pendingCountText: '待签合同',
|
||
contract: '我的合同',
|
||
bill: '账单',
|
||
coreServices: '我的服务',
|
||
comingSoon: '建设中',
|
||
settings: '偏好设置',
|
||
languageSwitch: '语言切换',
|
||
languageToggleLabel: 'EN',
|
||
logout: '退出登录',
|
||
logoutConfirmTitle: '提示',
|
||
logoutConfirmContent: '确定要退出登录吗?',
|
||
logoutSuccess: '已退出',
|
||
userFallback: '用户',
|
||
submitRequest: '提交申请',
|
||
completeFormBeforeSubmit: '请完善表单信息后提交。',
|
||
comingSoonDesc: '{title} 功能正在建设中,后续开放后会在这里提供正式入口和数据。'
|
||
},
|
||
en: {
|
||
edit: 'Edit',
|
||
guest: 'Guest',
|
||
clickToLogin: 'Click to Sign In',
|
||
daysIn: 'Days Stayed',
|
||
activities: 'Activities',
|
||
points: 'Points',
|
||
applicationStatus: 'Application Status',
|
||
registrationStatusTitle: 'Registration Status',
|
||
notSubmittedYet: 'No registration submitted',
|
||
registrationPendingDesc: 'Registration is under review',
|
||
registrationApprovedDesc: 'Registration approved, ready to sign in',
|
||
registrationRejectedDesc: 'Registration rejected, please review the reason',
|
||
registrationEmptyDesc: 'No registration submitted',
|
||
registrationRecordTitle: 'Registration Records',
|
||
registrationRecordSubmittedDesc: 'A registration has been submitted with this Wechat account. You can review its history and approval status.',
|
||
registrationRecordGuestDesc: 'You can still review registration records submitted with this Wechat account before signing in.',
|
||
viewNow: 'View Now',
|
||
statusPending: 'Under Review',
|
||
statusApproved: 'Approved',
|
||
statusRejected: 'Rejected',
|
||
remind: 'Remind',
|
||
done: 'Done',
|
||
viewDetail: 'Details',
|
||
exclusiveButler: 'Exclusive Butler',
|
||
onlineService: '24/7 Online Service',
|
||
contact: 'Contact',
|
||
application: 'Applications',
|
||
bookingList: 'Bookings',
|
||
bookingStatusTitle: 'Booking Progress',
|
||
contractStatusTitle: 'Contract Status',
|
||
bookingNo: 'Booking No.',
|
||
bookingPlaceholder: 'Your booking will appear here after payment',
|
||
bookingProgressDesc: 'Booking will be reviewed after payment. Butler will be assigned upon approval.',
|
||
contractPendingTag: 'Pending',
|
||
contractPendingDesc: 'You have contracts waiting for signature. Please review them soon.',
|
||
pendingCountText: 'Pending Contracts',
|
||
contract: 'My Contract',
|
||
bill: 'Billing',
|
||
coreServices: 'My Services',
|
||
comingSoon: 'Soon',
|
||
settings: 'Preferences',
|
||
languageSwitch: 'Language',
|
||
languageToggleLabel: '中文',
|
||
logout: 'Log Out',
|
||
logoutConfirmTitle: 'Notice',
|
||
logoutConfirmContent: 'Are you sure to log out?',
|
||
logoutSuccess: 'Logged out',
|
||
userFallback: 'User',
|
||
submitRequest: 'Submit Request',
|
||
completeFormBeforeSubmit: 'Please complete the form before submitting.',
|
||
comingSoonDesc: '{title} is under development. The formal entry and data will be available here later.'
|
||
}
|
||
}
|
||
}
|
||
},
|
||
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();
|
||
},
|
||
t(key, params) {
|
||
return this.$tm('pages.index.component.ProfileCenterTab', key, params)
|
||
},
|
||
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.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.t('logoutConfirmTitle'),
|
||
content: this.t('logoutConfirmContent'),
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
clearLoginSession()
|
||
this.checkLoginStatus();
|
||
this.$parent.changeTabbar && this.$parent.changeTabbar(0, 'select-room');
|
||
uni.showToast({ title: this.t('logoutSuccess'), 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.t('notSubmittedYet'))
|
||
}
|
||
const map = {
|
||
unsubmitted: this.t('notSubmittedYet'),
|
||
pending: this.t('statusPending'),
|
||
approved: this.t('statusApproved'),
|
||
rejected: this.t('statusRejected')
|
||
}
|
||
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.t('submitRequest'))}&titleZh=${encodeURIComponent(titleZh || title || this.t('submitRequest'))}&titleEn=${encodeURIComponent(titleEn || title || this.t('submitRequest'))}&desc=${encodeURIComponent(desc || this.t('completeFormBeforeSubmit'))}&descZh=${encodeURIComponent(descZh || desc || this.t('completeFormBeforeSubmit'))}&descEn=${encodeURIComponent(descEn || desc || this.t('completeFormBeforeSubmit'))}&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 === 'butler') {
|
||
this.goToSubmitRequest(itemTitle, itemDesc, {
|
||
businessType: 'butler-contact',
|
||
sourceType: 'profile-center'
|
||
}, itemTitleZh, itemTitleEn, itemDescZh, itemDescEn)
|
||
return
|
||
}
|
||
const comingSoonDesc = this.t('comingSoonDesc', { title: itemTitle })
|
||
const comingSoonDescZh = this.$tm('pages.index.component.ProfileCenterTab', 'comingSoonDesc', { title: itemTitleZh || itemTitle })
|
||
const comingSoonDescEn = this.$tm('pages.index.component.ProfileCenterTab', 'comingSoonDesc', { 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>
|