Files
QSY-lvjuxcx/pages/index/component/ProfileCenterTab.vue
liangjian 445bd850f4 feat: 新增工单中心、房间巡查、失物招领等业务模块
- 新增工单中心(ticket_center)与工单工作台(ticket_workbench)页面
- 新增房间巡查(room_inspection)与失物招领发布(lost_found_publish)页面
- 预约看样流程新增房型/楼栋/楼层选择器组件
- 新增工具模块: booking-entry、privacy、reservation-flow
- api/index.js 补充楼层列表、看样提交等接口
- locales/messages.js 补充多语言文案
- manifest.json 开启微信小程序隐私检查
- 完善登录、注册、申请详情、账单等相关页面交互
2026-07-24 09:35:46 +08:00

1037 lines
35 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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">
{{ 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>