Files
QSY-lvjuxcx/pages/index/component/StayServiceTab.vue
liangjian 6e721ccc7e refactor: 国际化文本迁移至全局 locales/messages.js
将组件级 i18n 对象重构为统一由全局 locales/messages.js 管理,确保中英文语言切换时正确显示对应文本。

- 新增 utils/i18n.js 国际化工具函数
- 更新 store/$t.mixin.js 翻译混入逻辑
- 迁移 pkgService、pkgBooking、pkgAccount、pkgExplore、pages 等共 58 个组件的中英文文本至全局消息表
2026-07-24 13:43:41 +08:00

2241 lines
72 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="service-page tn-safe-area-inset-bottom">
<tn-nav-bar :isBack="false" :bottomShadow="false" :backgroundColor="navBarBackgroundColor" :customBackStyle="{ transition: 'all 0.3s ease' }">
<view class="tn-text-bold tn-text-xl tn-color-black">{{ $tr('我的入住') }}</view>
</tn-nav-bar>
<scroll-view class="service-scroll" scroll-y :style="{paddingTop: vuex_custom_bar_height + 'px'}" @scroll="handleScroll">
<view v-if="!hasCheckedIn" class="empty-state tn-padding animation-fade-in">
<!-- 无预订:去选房 -->
<view v-if="!currentBooking" class="no-booking-card">
<view class="no-booking-bg"></view>
<view class="no-booking-content tn-flex tn-flex-direction-column tn-flex-col-center">
<view class="icon-wrap tn-margin-bottom">
<text class="tn-icon-home-smile tn-color-white"></text>
</view>
<view class="tn-text-bold tn-color-white" style="font-size: 40rpx;">{{ $tr('您还没有在租房间') }}</view>
<view class="tn-color-white tn-margin-top-sm tn-text-center" style="opacity: 0.8; line-height: 1.6;">
{{ $tr('长租公寓为海外青年提供高端社区服务\n请先前往选房大厅挑选心仪套间') }}
</view>
<view class="custom-btn btn-glass tn-margin-top-xl" @click="goSelectRoom" style="width: 80%;">
<text class="tn-text-bold">{{ $tr('去选房') }}</text>
</view>
</view>
</view>
<!-- 有预订:审核中 -->
<view v-else class="pending-booking-card animation-fade-in" style="animation-delay: 0.1s;">
<!-- 顶部状态大图与标识 -->
<view class="pending-hero">
<view class="hero-bg-shape"></view>
<view class="hero-content tn-flex tn-flex-col-center tn-flex-row-center">
<view class="status-icon-ring tn-flex tn-flex-col-center tn-flex-row-center">
<text class="tn-icon-time-fill status-icon"></text>
</view>
</view>
</view>
<view class="pending-body tn-padding-xl">
<!-- 核心文案 -->
<view class="status-text-area tn-text-center">
<view class="tn-text-bold tn-text-xxl tn-color-black">{{ pendingStageTitle }}</view>
<view class="tn-color-gray tn-margin-top-sm tn-text-sm status-desc">
{{ pendingStageDesc }}
</view>
</view>
<!-- 预定信息卡片 (玻璃态拟物风格) -->
<view class="booking-ticket-card tn-margin-top-xl">
<view class="ticket-header tn-flex tn-flex-row-between tn-flex-col-center">
<view class="tn-flex tn-flex-col-center">
<view class="ticket-icon-wrap"><text class="tn-icon-order-fill"></text></view>
<text class="tn-text-bold tn-text-lg tn-color-black tn-margin-left-sm">{{ $tr('预定信息') }}</text>
</view>
<view class="ticket-status-badge">{{ bookingStatusText }}</view>
</view>
<view class="ticket-divider">
<view class="divider-line"></view>
</view>
<view class="ticket-content">
<view class="ticket-row">
<text class="ticket-label">{{ pendingInfoLabel }}</text>
<text class="ticket-value tn-text-bold">{{ bookingRoomText }}</text>
</view>
<view class="ticket-note tn-margin-top-sm">
{{ pendingStageNote }}
</view>
</view>
</view>
<!-- 流程节点 (微动效进度条) -->
<view class="process-track tn-margin-top-xl">
<view class="track-item active">
<view class="track-dot"><text class="tn-icon-success"></text></view>
<text class="track-text">{{ $tr('提交预定') }}</text>
</view>
<view class="track-line active-line"></view>
<view class="track-item" :class="isContractSigningStep ? 'active' : 'current'">
<view class="track-dot">
<view v-if="!isContractSigningStep" class="dot-pulse"></view>
<view v-if="!isContractSigningStep" class="dot-inner"></view>
<text v-if="isContractSigningStep" class="tn-icon-success"></text>
</view>
<text class="track-text">{{ $tr('管家审核') }}</text>
</view>
<view class="track-line" :class="{ 'active-line': isContractSigningStep }"></view>
<view class="track-item" :class="{ active: hasSignedContract, current: isContractSigningStep && !hasSignedContract }">
<view class="track-dot">
<view v-if="isContractSigningStep && !hasSignedContract" class="dot-pulse"></view>
<view v-if="isContractSigningStep && !hasSignedContract" class="dot-inner"></view>
<text v-if="hasSignedContract" class="tn-icon-success"></text>
</view>
<text class="track-text">{{ $tr('签订合同') }}</text>
</view>
<view class="track-line" :class="{ 'active-line': hasSignedContract }"></view>
<view class="track-item">
<view class="track-dot"></view>
<text class="track-text">{{ $tr('办理入住') }}</text>
</view>
</view>
<!-- 底部操作按钮 -->
<view class="action-btn-group action-btn-group--single tn-margin-top-xxl tn-flex tn-flex-row-between">
<view class="pending-action-btn pending-action-btn--primary" @click="handlePendingPrimaryAction">
<text class="tn-text-bold">{{ pendingPrimaryActionText }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 已入住状态下的服务大厅内容 -->
<view v-else class="animation-fade-in tn-padding-bottom">
<!-- 融合房卡与智能控制的新卡片 -->
<view class="integrated-room-card tn-margin-left tn-margin-right tn-margin-top">
<!-- 房卡背景 -->
<image v-if="roomCardCover" :src="roomCardCover" mode="aspectFill" class="irc-bg"></image>
<view v-else class="irc-placeholder">
<view class="irc-placeholder-glow"></view>
</view>
<view class="irc-mask"></view>
<view class="irc-content">
<!-- 顶部信息 -->
<view class="irc-header">
<view class="tn-flex tn-flex-row-between tn-flex-col-center">
<view class="irc-room-no">{{ roomDisplayText }}</view>
<view class="irc-status-badge">
<text class="tn-icon-vip-fill tn-margin-right-xs"></text>{{ $tr('使用中') }}
</view>
</view>
<view class="irc-room-title clamp-text-1 tn-margin-top-xs">{{ roomTitleText }}</view>
<view class="irc-lease tn-margin-top-sm">{{ $tr('租期') }}:{{ leaseTermText }}</view>
</view>
<!-- 环境与客控区 -->
<view class="irc-smart-area">
<view class="irc-env-row tn-flex tn-flex-row-between tn-flex-col-center">
<view class="irc-env-text">{{ smartSummaryText }}</view>
<view class="irc-smart-btn" @click="goSmartControl">
{{ $tr('全屋智能') }} <text class="tn-icon-right tn-margin-left-xs"></text>
</view>
</view>
<scroll-view scroll-x show-scrollbar="false" class="irc-quick-controls tn-margin-top">
<view v-if="smartQuickDeviceCards.length" class="irc-qc-list">
<view
v-for="item in smartQuickDeviceCards"
:key="item.key"
class="irc-qc-item"
:class="{'is-active': item.active}"
@click.stop="openSmartDevice(item)"
>
<view class="irc-qc-icon"><text :class="item.iconClass || 'tn-icon-my-computer'"></text></view>
<view class="irc-qc-name">{{ item.name }}</view>
</view>
</view>
<view v-else class="irc-qc-empty" @click.stop="goSmartControl">
<text class="tn-icon-add tn-margin-right-xs"></text>
{{ $tr('添加智能设备') }}
</view>
</scroll-view>
</view>
</view>
</view>
<view class="tn-margin-top tn-margin-left tn-margin-right">
<view class="tn-text-bold tn-text tn-color-black tn-margin-bottom">{{ $tr('住中申请') }}</view>
<view class="tn-flex tn-flex-wrap tn-flex-row-between">
<view class="service-item tn-margin-bottom-sm" v-for="item in stayActions" :key="item.key" @click="openServicePage(item)">
<view class="service-card-visual">
<image :src="item.image" mode="aspectFill" class="service-card-bg"></image>
<view class="service-card-mask"></view>
<view class="service-card-content tn-padding-sm tn-flex tn-flex-direction-column tn-flex-row-between" style="height: 100%;">
<view class="tn-flex tn-flex-row-between tn-flex-col-center">
<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>
<view class="status-tag tn-text-xs" :class="item.isBusy ? 'tag-busy' : 'tag-free'">
{{ pickItemText(item, 'statusZh', 'statusEn') }}
</view>
</view>
<view>
<view class="tn-text-bold tn-text tn-color-white">{{ pickItemText(item, 'label', 'labelEn') }}</view>
</view>
</view>
</view>
</view>
</view>
</view>
<view class="tn-margin-top-sm tn-margin-left tn-margin-right">
<view class="tn-flex tn-flex-row-between tn-flex-col-bottom tn-margin-bottom">
<view class="tn-text-bold tn-text tn-color-black">{{ $tr('公寓精选服务') }}</view>
<view class="tn-text-sm tn-color-gray">{{ $tr('全部') }} <text class="tn-icon-right section-arrow-icon"></text></view>
</view>
<view class="tn-flex tn-flex-wrap tn-flex-row-between">
<view class="service-item tn-margin-bottom-sm" v-for="item in services" :key="item.key" @click="openServicePage(item)">
<view class="service-card-visual">
<image :src="item.image" mode="aspectFill" class="service-card-bg"></image>
<view class="service-card-mask"></view>
<view class="service-card-content tn-padding-sm tn-flex tn-flex-direction-column tn-flex-row-between" style="height: 100%;">
<view class="tn-flex tn-flex-row-between tn-flex-col-center">
<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>
<view class="status-tag tn-text-xs" :class="item.isBusy ? 'tag-busy' : 'tag-free'">
{{ pickItemText(item, 'statusZh', 'statusEn') }}
</view>
</view>
<view>
<view class="tn-text-bold tn-text tn-color-white">{{ pickItemText(item, 'nameZh', 'nameEn') }}</view>
</view>
</view>
</view>
</view>
</view>
</view>
<view class="bill-section tn-margin-top tn-margin-left tn-margin-right tn-padding-lg">
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-bottom">
<view class="tn-text-bold tn-text tn-color-black">{{ $tr('本月账单') }}</view>
<view class="status-tag tn-text-xs" :class="unpaidBillCount ? 'tag-busy' : 'tag-free'">{{ unpaidBillCount ? $tr('待支付') : $tr('已结清') }}</view>
</view>
<view class="tn-flex tn-flex-row-between tn-flex-col-bottom tn-margin-bottom-sm">
<view class="tn-text-bold tn-color-black" style="font-size: 60rpx;">
<text class="tn-text-lg tn-margin-right-xs">¥</text>{{ formatMoney(unpaidBillTotal) }}
</view>
<view class="custom-btn btn-primary" style="padding: 10rpx 30rpx;" @click.stop="goUtilityRecharge">
<text class="tn-text-sm tn-text-bold">{{ unpaidBillCount ? $tr('去支付') : $tr('查看账单') }}</text>
</view>
</view>
<view class="tn-flex tn-flex-row-between tn-margin-top">
<view class="tn-flex tn-flex-direction-column">
<text class="tn-color-gray tn-text-xs">{{ $tr('账单类型') }}</text>
<text class="tn-text-bold tn-text-sm tn-margin-top-xs tn-color-black clamp-text-1">{{ unpaidBillPrimaryType }}</text>
</view>
<view class="tn-flex tn-flex-direction-column">
<text class="tn-color-gray tn-text-xs">{{ $tr('账单数量') }}</text>
<text class="tn-text-bold tn-text-sm tn-margin-top-xs tn-color-black">{{ unpaidBillCount }} {{ $tr('笔') }}</text>
</view>
<view class="tn-flex tn-flex-direction-column">
<text class="tn-color-gray tn-text-xs">{{ $tr('最近生成') }}</text>
<text class="tn-text-bold tn-text-sm tn-margin-top-xs tn-color-black">{{ latestBillTimeText }}</text>
</view>
</view>
</view>
<!-- 宿管与管理员列表 -->
<view class="butler-card-new tn-margin-top tn-margin-left tn-margin-right">
<view class="tn-text-bold tn-text tn-color-black tn-margin-bottom">{{ $tr('宿管与管理员') }}</view>
<view class="butler-card-new__inner">
<view v-if="managerContacts.length" class="butler-card-new__list">
<view
v-for="(contact, index) in managerContacts"
:key="contact.contactKey || index"
class="butler-member-new"
>
<view class="butler-member-new__identity">
<view class="butler-avatar-new">
<image v-if="getManagerAvatar(contact)" :src="getManagerAvatar(contact)" mode="aspectFill"></image>
<view v-else class="butler-avatar-new-fallback tn-flex tn-flex-col-center tn-flex-row-center">
<text class="butler-avatar-new-fallback-text">{{ getManagerInitial(contact) }}</text>
</view>
<view class="online-dot"></view>
</view>
<view class="butler-member-new__info">
<view class="butler-member-new__name">
{{ getManagerName(contact) }}
<text class="butler-member-new__tag butler-member-new__tag--type" :class="contact.contactType === 'counselor' ? 'tag-counselor' : 'tag-dormitory'">
{{ contact.contactType === 'counselor' ? $tr('管理员') : $tr('宿管') }}
</text>
</view>
<view class="butler-member-new__phone">{{ getManagerPhone(contact) || $tr('暂无联系电话') }}</view>
</view>
</view>
<view
class="butler-member-new__action"
:class="{ 'disabled': !getManagerPhone(contact) }"
@click.stop="handleAreaManagerCall(contact)"
>
<text class="tn-icon-phone-fill"></text>
</view>
</view>
</view>
<view v-else class="butler-card-new__list">
<view class="butler-member-new">
<view class="butler-member-new__identity">
<view class="butler-avatar-new">
<view class="butler-avatar-new-fallback tn-flex tn-flex-col-center tn-flex-row-center">
<text class="tn-icon-my-fill" style="font-size: 32rpx; color: #fff;"></text>
</view>
</view>
<view class="butler-member-new__info">
<view class="butler-member-new__name">{{ $tr('宿管与管理员') }}</view>
<view class="butler-member-new__phone">{{ isManagementContactsLoading ? $tr('正在同步管家信息...') : $tr('暂无管理联系人') }}</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
import { accountQueryBills, facilityManagers, myDormitoryManagers, myCounselor, gatewayRoomDeviceList, orderSearchedDetail } from '../../../api/index.js'
import { getSessionSnapshot, saveMyRoomData } from '../../../utils/app-state'
import { ACCOUNT_BILL_PAYMENT_STATUS, ACCOUNT_BILL_STATUS, fetchEnums, getAccountReferenceTypeLabel, getApplicationDetailStatusLabel } from '../../../utils/enum-store'
import { getBookingStatusText } from '../../../utils/flow'
import { normalizeBookingDetail, parseOrderDetailResponse } from '../../../utils/booking-helper'
import { normalizeSmartDeviceList } from '../../../utils/smart-device'
import { syncLoginStayState } from '../../../utils/session-sync'
import { compactEnglishText } from '../../../utils/select-room-normalizers'
import { addFilePrefix } from '../../../utils/common'
const DEFAULT_SMART_BRAND_ID = 'Tuya'
function normalizeBillList(response) {
const payload = response && response.data ? response.data : response
if (Array.isArray(payload)) {
return payload
}
if (payload && Array.isArray(payload.data)) {
return payload.data
}
return []
}
function looksLikeInternalCode(value) {
if (!value) {
return false
}
const text = String(value).trim()
if (!text) {
return false
}
return /^[A-Z0-9]{20,}$/i.test(text)
}
function pickReadableValue(...values) {
for (let i = 0; i < values.length; i += 1) {
const value = values[i]
if (value === undefined || value === null) {
continue
}
const text = String(value).trim()
if (!text || looksLikeInternalCode(text)) {
continue
}
return text
}
return ''
}
function pickFirstFilledValue(...values) {
for (let i = 0; i < values.length; i += 1) {
const value = values[i]
if (value === undefined || value === null) {
continue
}
const text = String(value).trim()
if (text) {
return text
}
}
return ''
}
function formatStayDate(value) {
if (!value) {
return ''
}
return String(value).replace('T', ' ').slice(0, 10)
}
function buildRoomDataFromOrder(order = {}) {
if (!order || typeof order !== 'object' || !Object.keys(order).length) {
return null
}
const stayStart = order.startDate || order.start || (order.checkInPeriod && order.checkInPeriod.start)
const stayEnd = order.endDate || order.end || (order.checkInPeriod && order.checkInPeriod.end)
const imageUrls = Array.isArray(order.imageUrls)
? order.imageUrls
: typeof order.imageUrls === 'string'
? order.imageUrls.split(',').map(item => item.trim()).filter(Boolean)
: []
return {
...order,
roomLabel: pickReadableValue(order.roomLabel, order.roomNo),
titleZh: pickReadableValue(order.titleZh, order.roomTypeName, order.roomName) || '当前入住房间',
titleEn: pickReadableValue(order.titleEn, order.roomTypeNameEn, order.roomTypeName) || 'Current Stay',
leaseText: order.leaseText || [stayStart, stayEnd].filter(Boolean).map(item => formatStayDate(item)).join(' - '),
image: order.image || order.imageUrl || imageUrls[0] || ''
}
}
export default {
name: 'StayServiceTab',
data() {
return {
hasCheckedIn: false, // 模拟未入住/已入住状态
myRoomData: null, // 从缓存获取的房间信息
currentBooking: null,
isPendingStatusRefreshing: false,
areaManager: null,
areaManagers: [],
isAreaManagerLoading: false,
// 宿管与管理员联系人
dormitoryManagers: [],
counselor: null,
isManagementContactsLoading: false,
// 导航栏滚动背景色
navBarBackgroundColor: 'transparent',
unpaidBillList: [],
smartDevices: [],
isSmartDevicesLoading: false,
smartDeviceError: '',
stayActions: [
{
key: 'change-room',
label: '换房申请',
labelEn: 'Change Room',
icon: 'tn-icon-home-smile-fill',
statusZh: '可提交',
statusEn: 'Available',
isBusy: false,
image: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_11.png',
title: '换房申请',
titleEn: 'Change Room Request',
titleEnShort: 'Room Change',
desc: '提交换房原因、期望房型和管家审核说明。',
descEn: 'Submit your reason, expected room type, and butler review notes.'
},
{
key: 'renew-stay',
label: '续住申请',
labelEn: 'Renew Stay',
icon: 'tn-icon-calendar-fill',
statusZh: '可提交',
statusEn: 'Available',
isBusy: false,
image: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_16.png',
title: '续住申请',
titleEn: 'Renew Stay Request',
titleEnShort: 'Renew Stay',
desc: '提交续住时长、续住原因和新的租期需求。',
descEn: 'Submit extension duration, reason, and new lease request.'
},
{
key: 'checkout',
label: '退房申请',
labelEn: 'Check Out',
icon: 'tn-icon-home-in-fill',
statusZh: '可提交',
statusEn: 'Available',
isBusy: false,
image: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_17.png',
title: '退房申请',
titleEn: 'Checkout Request',
titleEnShort: 'Check Out',
desc: '提交退房时间、离场说明和验房申请。',
descEn: 'Submit checkout time, leave notes, and inspection request.'
}
],
services: [
{
key: 'repair',
icon: 'tn-icon-set-fill',
nameZh: '报修',
nameEn: 'Repair',
statusZh: '24小时',
statusEn: '24 Hours',
isBusy: false,
image: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_19.png',
title: '报修申请',
titleEn: 'Repair Request',
titleEnShort: 'Repair',
desc: '提交报修位置、问题说明和期望上门时间。',
descEn: 'Submit the repair location, issue details, and preferred visit time.'
},
{
key: 'water-delivery',
icon: 'tn-icon-shop-fill',
nameZh: '送水服务',
nameEn: 'Water Delivery',
statusZh: '可提交',
statusEn: 'Available',
isBusy: false,
image: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_14.png',
title: '送水服务申请',
titleEn: 'Water Delivery Request',
titleEnShort: 'Water Delivery',
desc: '填写备注说明需要几桶水后提交申请。',
descEn: 'Submit the quantity of water needed and any notes.'
},
{
key: 'meal-delivery',
icon: 'tn-icon-cart-fill',
nameZh: '送餐服务',
nameEn: 'Food Delivery',
statusZh: '可下单',
statusEn: 'Order Now',
isBusy: false,
image: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_15.png',
title: '送餐服务',
titleEn: 'Food Delivery',
titleEnShort: 'Food',
desc: '一键跳转点餐小程序,挑选餐品并下单配送。',
descEn: 'Open the ordering mini-program to pick meals and get delivery.'
},
{
key: 'cleaning',
icon: 'tn-icon-clean',
nameZh: '清洁服务',
nameEn: 'Cleaning Service',
statusZh: '可预约',
statusEn: 'Book Now',
isBusy: false,
image: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_20.png',
title: '清洁服务申请',
titleEn: 'Cleaning Service Request',
titleEnShort: 'Cleaning',
desc: '选择上门清洁日期并提交服务申请。',
descEn: 'Choose a cleaning date and submit the service request.'
},
{
key: 'complaint',
icon: 'tn-icon-message-fill',
nameZh: '投诉建议',
nameEn: 'Feedback',
statusZh: '随时提交',
statusEn: 'Anytime',
isBusy: false,
image: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_8.png',
title: '投诉建议',
titleEn: 'Feedback',
titleEnShort: 'Feedback',
desc: '填写投诉内容或建议意见后提交反馈。',
descEn: 'Submit your complaint details or suggestions.'
},
{
key: 'lost-found',
icon: 'tn-icon-search',
nameZh: '失物拾物',
nameEn: 'Lost and Found',
statusZh: '登记中',
statusEn: 'Open',
isBusy: false,
image: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_13.png',
title: '失物拾物登记',
titleEn: 'Lost and Found',
titleEnShort: 'Lost & Found',
desc: '提交失物或拾获物品信息,方便社区协助查找。',
descEn: 'Submit lost or found item details so the community can help.'
}
],
}
},
computed: {
language() {
return this.$getLanguage()
},
bookingStatusText() {
if (!this.currentBooking) {
return ''
}
const statusCodeCandidates = [
this.currentBooking.statusCode,
this.currentBooking.orderStatus,
this.currentBooking.approvalStatus,
this.currentBooking.auditStatus,
this.currentBooking.status
]
const numericStatusCode = statusCodeCandidates
.map(value => Number(value))
.find(value => Number.isFinite(value))
return this.currentBooking.statusText
|| this.currentBooking.statusDesc
|| (numericStatusCode !== undefined ? getApplicationDetailStatusLabel(numericStatusCode) : '')
|| getBookingStatusText(this.currentBooking.status || this.currentBooking.statusKey, this.language)
},
pendingContracts() {
const storeContracts = this.$store && this.$store.state && this.$store.state.vuex_pending_contracts
if (Array.isArray(storeContracts) && storeContracts.length) {
return storeContracts
}
const session = getSessionSnapshot()
return Array.isArray(session.pendingContracts) ? session.pendingContracts : []
},
isContractSigningStep() {
if (!this.hasCheckedIn && this.pendingContracts.length > 0) {
return true
}
return false
},
hasSignedContract() {
if (!this.hasCheckedIn && this.currentBooking && this.currentBooking.contractStatus === 1) {
return true
}
return false
},
pendingStageTitle() {
if (this.hasSignedContract) {
return this.$tr('合同已签署,等待办理入住')
}
if (this.isContractSigningStep) {
return this.$tr('请尽快签订合同')
}
return this.$tr('您的预订正在审核中')
},
pendingStageDesc() {
if (this.hasSignedContract) {
return this.$tr('您的住宿合同已经完成签署,管家会尽快为您安排办理入住流程。')
}
if (this.isContractSigningStep) {
return this.$tr('系统已生成待签合同,当前流程已进入签订合同阶段,完成签约后即可继续后续入住流程。')
}
return this.$tr('定金已支付,预定单正在审批流中。审批通过后将分配管家并等待分房号。')
},
pendingInfoLabel() {
return this.hasSignedContract || this.isContractSigningStep ? this.$tr('合同房型') : this.$tr('预定房型')
},
pendingStageNote() {
if (this.hasSignedContract) {
return this.$tr('合同签署完成,请等待管家通知办理入住。')
}
if (this.isContractSigningStep) {
return this.$tr('检测到待签合同后,流程将停留在签订合同节点,请先完成签名。')
}
return this.$tr('订单已提交,管家审核通过后会同步更多入住信息。')
},
pendingPrimaryActionText() {
if (this.isPendingStatusRefreshing) {
return this.$tr('刷新中...')
}
if (this.isContractSigningStep && !this.hasSignedContract) {
return this.$tr('去签约')
}
return this.$tr('刷新状态')
},
bookingRoomText() {
if (!this.currentBooking) {
return '--'
}
return this.currentBooking.roomName
|| this.currentBooking.roomTypeName
|| this.currentBooking.roomTypeNameEn
|| this.$tr('待审核分配')
},
roomCardCover() {
return this.myRoomData && this.myRoomData.image ? this.myRoomData.image : ''
},
roomTitleText() {
if (!this.myRoomData) {
return this.$tr('当前入住房间')
}
return this.language === 'zh'
? (pickReadableValue(this.myRoomData.titleZh, this.myRoomData.roomTypeName, this.myRoomData.roomName, this.myRoomData.titleEn) || this.$tr('当前入住房间'))
: (pickReadableValue(this.myRoomData.titleEn, this.myRoomData.roomTypeNameEn, this.myRoomData.roomTypeName, this.myRoomData.titleZh) || this.$tr('当前入住房间'))
},
roomDisplayText() {
if (!this.myRoomData) {
return '--'
}
return pickReadableValue(
this.myRoomData.roomNo,
this.myRoomData.roomLabel,
this.myRoomData.roomName
) || '--'
},
leaseTermText() {
if (!this.myRoomData) {
return '--'
}
const start = this.myRoomData.startDate || this.myRoomData.start || (this.myRoomData.checkInPeriod && this.myRoomData.checkInPeriod.start)
const end = this.myRoomData.endDate || this.myRoomData.end || (this.myRoomData.checkInPeriod && this.myRoomData.checkInPeriod.end)
const leaseText = this.myRoomData.leaseText || [start, end].filter(Boolean).map(item => formatStayDate(item)).join(' - ')
return leaseText || '--'
},
unpaidBillCount() {
return this.unpaidBillList.length
},
unpaidBillTotal() {
return this.unpaidBillList.reduce((sum, item) => {
return sum + Number(item && item.remainingAmount ? item.remainingAmount : 0)
}, 0)
},
unpaidBillPrimaryType() {
if (!this.unpaidBillList.length) {
return this.$tr('暂无待支付账单')
}
const firstBill = this.unpaidBillList[0] || {}
return firstBill.referenceTypeDesc || getAccountReferenceTypeLabel(firstBill.referenceType, this.$tr('账单记录'))
},
latestBillTimeText() {
if (!this.unpaidBillList.length || !this.unpaidBillList[0].createTime) {
return '--'
}
return this.formatDateTime(this.unpaidBillList[0].createTime)
},
smartSummaryText() {
if (!this.hasCheckedIn) {
return this.$tr('入住后可使用智能设备')
}
if (this.isSmartDevicesLoading) {
return this.$tr('正在同步智能设备...')
}
if (this.smartDevices.length) {
return this.$tr('已接入 {count} 台智能设备').replace('{count}', this.smartDevices.length)
}
if (this.smartDeviceError) {
return this.$tr('智能设备同步失败')
}
return this.$tr('当前房间暂无智能设备')
},
smartQuickDeviceCards() {
return this.smartDevices.map((device, index) => ({
...device,
key: device.renderKey || device.deviceId || device.deviceNo || `smart-device-${index}`,
name: device.deviceName || this.$tr('设备 {index}').replace('{index}', index + 1),
stateText: device.stateText || this.$tr('可控制')
}))
},
stayLocationText() {
if (!this.myRoomData) {
return ''
}
const bedLabel = this.myRoomData.slot || this.myRoomData.slotCode
return [
this.myRoomData.areaName || this.myRoomData.campusName || '',
this.myRoomData.buildingName || '',
this.myRoomData.buildingSectionName || '',
this.myRoomData.floorName || this.myRoomData.floor || '',
bedLabel ? this.$tr('床位 {label}').replace('{label}', bedLabel) : ''
].filter(Boolean).join(' · ')
},
butlerAvatar() {
const manager = this.primaryAreaManager || {}
return addFilePrefix(
manager.avatar
|| manager.avatarUrl
|| manager.headImg
|| manager.photo
|| ''
)
},
primaryAreaManager() {
return this.areaManagers[0] || this.areaManager || null
},
butlerDisplayName() {
const manager = this.primaryAreaManager || {}
const managerName = manager.displayName || manager.name || manager.userName || manager.nickname || manager.userCode || ''
if (!managerName) {
return this.$tr('专属社区管家')
}
if (this.areaManagers.length > 1) {
return this.$tr('{name} 等{count}位管家')
.replace('{name}', managerName)
.replace('{count}', this.language === 'zh' ? this.areaManagers.length : this.areaManagers.length - 1)
}
return managerName
},
butlerSubtitle() {
if (this.isAreaManagerLoading) {
return this.$tr('正在同步管家信息...')
}
if (this.areaManagers.length > 1) {
return this.$tr('当前区域共有 {count} 位管家可联系').replace('{count}', this.areaManagers.length)
}
const manager = this.primaryAreaManager || {}
const phone = this.getManagerPhone(manager)
if (phone) {
return `${this.$tr('联系电话 ')}${phone}`
}
return this.$tr('当前在线 · 响应时间 < 5分钟')
},
butlerCountText() {
if (this.isAreaManagerLoading) {
return this.$tr('同步中')
}
if (this.areaManagers.length > 1) {
return this.$tr('{count}人值班').replace('{count}', this.areaManagers.length)
}
if (this.areaManagers.length === 1) {
return this.$tr('1人在线')
}
return this.$tr('待同步')
},
butlerPhoneNumber() {
const manager = this.primaryAreaManager || {}
return this.getManagerPhone(manager)
},
// 合并宿管列表与管理员(辅导员)为统一联系人列表
managerContacts() {
const contacts = Array.isArray(this.dormitoryManagers)
? this.dormitoryManagers.map((item, index) => ({
...this.normalizeManager(item),
contactType: 'dormitory-manager',
contactKey: String(item.code || item.userCode || item.phone || index)
}))
: []
const counselor = this.counselor
if (counselor && typeof counselor === 'object' && (counselor.name || counselor.phone || counselor.code)) {
contacts.push({
...this.normalizeManager(counselor),
contactType: 'counselor',
contactKey: String(counselor.code || counselor.userCode || counselor.phone || 'counselor')
})
}
return contacts
}
},
mounted() {
fetchEnums(false, this.language)
this.checkLocalStatus()
this.loadUnpaidBills()
},
methods: {
pickItemText(item = {}, zhField, enField, fallback = '') {
if (this.language === 'en') {
const shortField = `${enField}Short`
return item[shortField] || item[enField] || item[zhField] || fallback
}
return item[zhField] || item[enField] || fallback
},
normalizeManager(manager = {}) {
if (!manager || typeof manager !== 'object') {
return null
}
const phoneSource = manager.phone || manager.mobile || manager.tel || manager.managerPhone || ''
const phone = typeof phoneSource === 'string' ? phoneSource.trim() : phoneSource
const userCode = manager.userCode || manager.code || ''
const displayName = manager.name || manager.userName || manager.nickname || userCode || ''
return {
...manager,
phone,
userCode,
displayName
}
},
getStoreUser() {
return (this.$store && this.$store.state && this.$store.state.vuex_user) || {}
},
getStoreCurrentOrder() {
return (this.$store && this.$store.state && this.$store.state.vuex_current_order) || null
},
getStoreCheckinStatus() {
return !!(this.$store && this.$store.state && this.$store.state.vuex_checkin_status)
},
isCurrentLogin() {
const storeUser = this.getStoreUser()
if (storeUser && Object.keys(storeUser).length) {
return true
}
return !!getSessionSnapshot().isLogin
},
formatMoney(value) {
const amount = Number(value || 0)
return amount.toFixed(2)
},
formatDateTime(value) {
if (!value) {
return '--'
}
const text = String(value).replace('T', ' ')
return text.length > 16 ? text.slice(0, 16) : text
},
getCurrentOrderNo() {
const storeCurrentOrder = this.getStoreCurrentOrder()
if (storeCurrentOrder && storeCurrentOrder.orderNo) {
return String(storeCurrentOrder.orderNo).trim()
}
const roomData = this.myRoomData || {}
return String(roomData.orderNo || '').trim()
},
getCurrentAreaCode() {
const roomData = this.myRoomData || {}
return String(
roomData.areaCode
|| roomData.code
|| roomData.areaNo
|| ''
).trim()
},
getCurrentSmartBrandId() {
const roomData = this.myRoomData || {}
return pickFirstFilledValue(
roomData.brandId,
roomData.platform,
roomData.brand,
DEFAULT_SMART_BRAND_ID
) || DEFAULT_SMART_BRAND_ID
},
getCurrentSmartRoomCode() {
const roomData = this.myRoomData || {}
return pickFirstFilledValue(
roomData.roomCode,
roomData.facilityCode,
roomData.gatewayRoomCode,
roomData.iotRoomCode
)
},
getCurrentSmartRoomName() {
const roomData = this.myRoomData || {}
return pickReadableValue(
roomData.roomName,
roomData.roomLabel,
roomData.roomNo
)
},
mergeCurrentOrderDetail(orderDetail = {}) {
const detail = buildRoomDataFromOrder(orderDetail)
if (!detail) {
return
}
this.myRoomData = {
...(this.myRoomData || {}),
...detail
}
saveMyRoomData(this.myRoomData)
if (this.$store && this.$store.commit) {
const currentOrder = this.getStoreCurrentOrder()
this.$store.commit('$tStore', {
name: 'vuex_current_order',
value: {
...(currentOrder || {}),
...orderDetail
}
})
this.$store.commit('$tStore', {
name: 'vuex_user',
value: {
...this.getStoreUser(),
currentOrder: {
...((this.getStoreUser() && this.getStoreUser().currentOrder) || {}),
...orderDetail
}
}
})
}
this.loadSmartDevices()
},
resetSmartDevicesState() {
this.smartDevices = []
this.smartDeviceError = ''
},
loadSmartDevices() {
if (!this.hasCheckedIn) {
this.resetSmartDevicesState()
return Promise.resolve()
}
const brandId = this.getCurrentSmartBrandId()
const roomCode = this.getCurrentSmartRoomCode()
if (!brandId || !roomCode) {
this.resetSmartDevicesState()
this.smartDeviceError = this.$tr('房间智能设备未配置')
return Promise.resolve()
}
const requestKey = `${brandId}::${roomCode}`
if (this._loadSmartDevicesPromise && this._loadSmartDevicesKey === requestKey) {
return this._loadSmartDevicesPromise
}
this._loadSmartDevicesKey = requestKey
this.isSmartDevicesLoading = true
const requestPromise = gatewayRoomDeviceList(brandId, roomCode)
.then((response) => {
if (`${this.getCurrentSmartBrandId()}::${this.getCurrentSmartRoomCode()}` !== requestKey) {
return
}
const deviceList = normalizeSmartDeviceList(response)
this.smartDevices = deviceList
this.smartDeviceError = ''
})
.catch((error) => {
console.error('load stay smart devices failed', error)
this.smartDevices = []
this.smartDeviceError = (error && (error.message || error.msg)) || this.$tr('智能设备加载失败')
})
.finally(() => {
this.isSmartDevicesLoading = false
this._loadSmartDevicesPromise = null
})
this._loadSmartDevicesPromise = requestPromise
return requestPromise
},
loadAreaManager() {
if (!this.hasCheckedIn) {
this.areaManager = null
this.areaManagers = []
return Promise.resolve()
}
const areaCode = this.getCurrentAreaCode()
if (!areaCode) {
this.areaManager = null
this.areaManagers = []
return Promise.resolve()
}
if (this._loadAreaManagerPromise && this._loadAreaManagerAreaCode === areaCode) {
return this._loadAreaManagerPromise
}
this._loadAreaManagerAreaCode = areaCode
this.isAreaManagerLoading = true
this._loadAreaManagerPromise = facilityManagers({
FacilityType: 10,
FacilityCode: areaCode
}, this.language)
.then((res) => {
if (this.getCurrentAreaCode() !== areaCode) {
return
}
const rawData = res && res.data !== undefined ? res.data : res
const managerList = Array.isArray(rawData)
? rawData
: (
Array.isArray(rawData && rawData.data)
? rawData.data
: (Array.isArray(rawData && rawData.managers) ? rawData.managers : [])
)
const normalizedManagers = Array.isArray(managerList)
? managerList.map(item => this.normalizeManager(item)).filter(item => item && item.phone)
: []
this.areaManagers = normalizedManagers
this.areaManager = normalizedManagers[0] || null
})
.catch((error) => {
console.warn('load area manager failed', error)
this.areaManager = null
this.areaManagers = []
})
.finally(() => {
this.isAreaManagerLoading = false
this._loadAreaManagerPromise = null
})
return this._loadAreaManagerPromise
},
// 获取宿管列表与管理员(辅导员)联系人
fetchManagementContacts() {
if (!this.isCurrentLogin()) {
this.dormitoryManagers = []
this.counselor = null
return Promise.resolve()
}
if (this._fetchManagementContactsPromise) {
return this._fetchManagementContactsPromise
}
this.isManagementContactsLoading = true
this._fetchManagementContactsPromise = Promise.allSettled([
myDormitoryManagers(this.language),
myCounselor(this.language)
])
.then(([dormitoryManagersResult, counselorResult]) => {
this.dormitoryManagers = dormitoryManagersResult.status === 'fulfilled' && Array.isArray(dormitoryManagersResult.value && dormitoryManagersResult.value.data)
? dormitoryManagersResult.value.data
: []
this.counselor = counselorResult.status === 'fulfilled' && counselorResult.value && counselorResult.value.data && typeof counselorResult.value.data === 'object'
? counselorResult.value.data
: null
if (dormitoryManagersResult.status === 'rejected') {
console.warn('获取宿管联系人失败', dormitoryManagersResult.reason)
}
if (counselorResult.status === 'rejected') {
console.warn('获取管理员信息失败', counselorResult.reason)
}
})
.catch((error) => {
this.dormitoryManagers = []
this.counselor = null
console.warn('获取管理联系人失败', error)
})
.finally(() => {
this.isManagementContactsLoading = false
this._fetchManagementContactsPromise = null
})
return this._fetchManagementContactsPromise
},
// 滚动事件处理:顶部导航栏加背景色
handleScroll(e) {
const scrollTop = (e && e.detail && e.detail.scrollTop) || 0
if (scrollTop > 20) {
this.navBarBackgroundColor = '#F8FAFF'
} else {
this.navBarBackgroundColor = 'transparent'
}
},
fetchCurrentOrderDetail() {
if (!this.hasCheckedIn) {
return Promise.resolve()
}
const orderNo = this.getCurrentOrderNo()
if (!orderNo) {
return Promise.resolve()
}
if (this._fetchCurrentOrderDetailPromise && this._fetchCurrentOrderDetailOrderNo === orderNo) {
return this._fetchCurrentOrderDetailPromise
}
this._fetchCurrentOrderDetailOrderNo = orderNo
this._fetchCurrentOrderDetailPromise = orderSearchedDetail(orderNo)
.then((response) => {
if (this.getCurrentOrderNo() !== orderNo) {
return
}
const detail = normalizeBookingDetail(parseOrderDetailResponse(response))
if (detail) {
this.mergeCurrentOrderDetail(detail)
}
})
.catch((error) => {
console.error('fetch current order detail failed', error)
})
.finally(() => {
this._fetchCurrentOrderDetailPromise = null
})
return this._fetchCurrentOrderDetailPromise
},
checkLocalStatus() {
const session = getSessionSnapshot()
const storeUser = this.getStoreUser()
const storeCurrentOrder = this.getStoreCurrentOrder()
if (!this.isCurrentLogin()) {
this.hasCheckedIn = false
this.currentBooking = null
this.myRoomData = null
this.dormitoryManagers = []
this.counselor = null
return
}
this.currentBooking = storeUser.currentBooking || session.currentBooking || null
// 已登录即获取宿管与管理员联系人
this.fetchManagementContacts()
if (this.getStoreCheckinStatus() || session.hasCheckedIn) {
this.hasCheckedIn = true
this.myRoomData = buildRoomDataFromOrder(storeCurrentOrder) || buildRoomDataFromOrder(session.myRoomData) || session.myRoomData || null
this.fetchCurrentOrderDetail()
this.loadAreaManager()
this.loadSmartDevices()
} else {
this.hasCheckedIn = false
this.myRoomData = null
this.areaManager = null
this.areaManagers = []
this.resetSmartDevicesState()
}
},
loadUnpaidBills() {
if (!this.isCurrentLogin()) {
this.unpaidBillList = []
return Promise.resolve()
}
return accountQueryBills({
status: ACCOUNT_BILL_STATUS.ACTIVE,
paymentStatus: ACCOUNT_BILL_PAYMENT_STATUS.UNPAID
})
.then((response) => {
const list = normalizeBillList(response)
this.unpaidBillList = Array.isArray(list) ? list : []
})
.catch(() => {
this.unpaidBillList = []
})
},
handlePendingPrimaryAction() {
if (this.isContractSigningStep && !this.hasSignedContract) {
this.goSignContract()
return
}
this.refreshPendingStatus()
},
goSignContract() {
const contract = this.pendingContracts[0]
if (!contract) {
this.refreshPendingStatus()
return
}
uni.navigateTo({
url: `/pkgService/contract_sign/contract_sign?no=${encodeURIComponent(contract.no || contract.contractNo || '')}&orderNo=${encodeURIComponent(contract.orderNo || '')}`
})
},
refreshPendingStatus() {
if (this.isPendingStatusRefreshing) {
return
}
let refreshedStayState = {}
this.isPendingStatusRefreshing = true
syncLoginStayState({ force: true })
.then((result = {}) => {
refreshedStayState = result
this.checkLocalStatus()
return this.loadUnpaidBills()
})
.then(() => {
const pendingContracts = Array.isArray(refreshedStayState.pendingContracts)
? refreshedStayState.pendingContracts
: []
this.$emit('refresh-shell-state', {
pendingContracts
})
uni.showToast({
title: pendingContracts.length
? this.$tr('发现待签合同')
: this.$tr('状态已刷新'),
icon: 'success'
})
})
.catch((error) => {
console.error('refresh pending status failed', error)
uni.showToast({
title: this.$tr('刷新失败,请稍后重试'),
icon: 'none'
})
})
.finally(() => {
this.isPendingStatusRefreshing = false
})
},
goSelectRoom() {
uni.reLaunch({
url: '/pages/index/index?index=0&pageId=select-room'
});
},
goBookingDetail() {
const orderNo = this.currentBooking && this.currentBooking.orderNo
? encodeURIComponent(this.currentBooking.orderNo)
: ''
uni.navigateTo({
url: orderNo
? `/pkgBooking/booking_detail/booking_detail?orderNo=${orderNo}`
: '/pkgBooking/booking_detail/booking_detail'
});
},
buildServiceSubmitUrl(item = {}) {
const roomData = this.myRoomData || {}
const titleZh = item.title || item.titleEn || item.nameZh || item.nameEn || this.$tr('提交申请')
const titleEn = item.titleEnShort || item.titleEn || item.title || item.nameEn || item.nameZh || this.$tr('提交申请')
const descZh = item.desc || item.descEn || this.$tr('请完善表单信息后提交。')
const descEn = compactEnglishText(item.descEn || item.desc || '', 68) || item.descEn || item.desc || this.$tr('请完善表单信息后提交。')
const title = this.language === 'en'
? titleEn
: titleZh
const desc = this.language === 'en'
? descEn
: descZh
const payload = {
businessType: item.key || '',
sourceType: 'stay-service',
roomLabel: roomData.roomLabel || '',
currentRoomLabel: roomData.roomLabel || '',
areaCode: roomData.areaCode || '',
areaName: roomData.areaName || '',
roomTypeCode: item.key === 'change-room' ? (roomData.roomTypeCode || '') : '',
roomTypeName: item.key === 'change-room' ? (roomData.roomTypeName || roomData.titleZh || '') : '',
roomName: roomData.roomLabel || ''
}
return `/pkgService/submit_request/submit_request?title=${encodeURIComponent(title || this.$tr('提交申请'))}&titleZh=${encodeURIComponent(titleZh)}&titleEn=${encodeURIComponent(titleEn)}&desc=${encodeURIComponent(desc || this.$tr('请完善表单信息后提交。'))}&descZh=${encodeURIComponent(descZh)}&descEn=${encodeURIComponent(descEn)}&payload=${encodeURIComponent(JSON.stringify(payload))}`
},
buildTicketWorkbenchUrl(item = {}) {
const serviceTypeMap = {
repair: 'TicketRepairApply',
complaint: 'TicketSuggestionApply',
'change-room': 'TicketChangeApply',
checkout: 'TicketCheckOutApply',
'renew-stay': 'TicketRenewalApply',
'water-delivery': 'TicketWaterDeliveryApply',
cleaning: 'TicketCleaningApply'
}
const serviceType = serviceTypeMap[item.key]
if (!serviceType) {
return ''
}
return `/pkgService/ticket_workbench/ticket_workbench?serviceType=${encodeURIComponent(serviceType)}`
},
openServicePage(item) {
if (item.key === 'meal-delivery') {
this.openMealDeliveryMiniProgram()
return
}
if (item.key === 'utility') {
uni.navigateTo({
url: '/pkgService/utility_recharge/utility_recharge'
});
return
}
if (item.key === 'lost-found') {
uni.navigateTo({
url: '/pkgService/lost_found_publish/lost_found_publish'
});
return
}
const ticketUrl = this.buildTicketWorkbenchUrl(item)
if (ticketUrl) {
uni.navigateTo({
url: ticketUrl
});
return
}
uni.navigateTo({
url: this.buildServiceSubmitUrl(item)
});
},
// 送餐服务:跳转外部点餐小程序
openMealDeliveryMiniProgram() {
uni.navigateToMiniProgram({
appId: 'wxf221ee13f2abd417',
path: 'pages/scan/index?scene=1005&toMenuList=5&entityId=00684887&multiMenuId=006848879dd2e038019e017ba6e330d5',
success: () => {},
fail: (error) => {
console.error('跳转送餐小程序失败', error)
uni.showToast({
title: this.$tr('跳转送餐小程序失败'),
icon: 'none'
})
}
})
},
getManagerName(manager = {}) {
return manager.displayName || manager.name || manager.userName || manager.nickname || manager.userCode || manager.phone || ''
},
getManagerPhone(manager = {}) {
return manager.phone || manager.mobile || manager.tel || manager.managerPhone || ''
},
getManagerAvatar(manager = {}) {
return addFilePrefix(manager.avatar || manager.avatarUrl || manager.headImg || manager.photo || '')
},
getManagerInitial(manager = {}) {
const name = this.getManagerName(manager)
return String(name).trim().slice(0, 1) || this.$tr('管')
},
handleButlerWechat() {
const manager = this.primaryAreaManager || {}
const wechat = manager.wechat || manager.wechatNo || manager.wechatCode || ''
if (wechat) {
uni.setClipboardData({
data: String(wechat),
success: () => {
uni.showToast({
title: this.$tr('管家微信已复制'),
icon: 'success'
})
}
})
return
}
uni.showToast({
title: this.$tr('暂无管家微信'),
icon: 'none'
})
},
handleAreaManagerCall(manager = {}) {
const phone = this.getManagerPhone(manager)
if (!phone) {
uni.showToast({
title: this.$tr('暂无联系电话'),
icon: 'none'
})
return
}
uni.makePhoneCall({
phoneNumber: String(phone)
})
},
handleButlerCall() {
if (!this.butlerPhoneNumber) {
uni.showToast({
title: this.isAreaManagerLoading ? this.$tr('正在加载管家信息') : this.$tr('暂无联系电话'),
icon: 'none'
})
return
}
uni.makePhoneCall({
phoneNumber: String(this.butlerPhoneNumber)
})
},
goUtilityRecharge() {
uni.navigateTo({
url: '/pkgService/utility_recharge/utility_recharge'
});
},
goSmartControl() {
const roomCode = this.getCurrentSmartRoomCode()
const roomName = this.getCurrentSmartRoomName()
const brandId = this.getCurrentSmartBrandId()
uni.navigateTo({
url: `/pkgService/smart_control/smart_control?brandId=${encodeURIComponent(brandId)}&roomCode=${encodeURIComponent(roomCode || '')}&roomName=${encodeURIComponent(roomName || '')}`
})
},
openSmartDevice(device = {}) {
if (!device || !Object.keys(device).length) {
this.goSmartControl()
return
}
const roomCode = this.getCurrentSmartRoomCode()
const roomName = this.getCurrentSmartRoomName()
const brandId = this.getCurrentSmartBrandId()
const payload = encodeURIComponent(JSON.stringify(device))
uni.vibrateShort()
uni.navigateTo({
url: `/pkgService/device_control/device_control?brandId=${encodeURIComponent(brandId)}&roomCode=${encodeURIComponent(roomCode || '')}&roomName=${encodeURIComponent(roomName || '')}&device=${payload}`
})
}
}
}
</script>
<style lang="scss" scoped>
.service-page {
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
min-height: 100vh;
}
/* 导航栏背景色过渡 */
.service-page ::v-deep .tn-custom-nav-bar__bar {
transition: background-color 0.3s ease;
}
.service-scroll {
height: calc(100vh - 100rpx);
}
.empty-state {
min-height: calc(100vh - 100rpx);
}
/* 无预订卡片 */
.no-booking-card {
position: relative;
border-radius: 32rpx;
overflow: hidden;
box-shadow: 0 16rpx 40rpx rgba($brand-primary, 0.15);
margin-top: 20rpx;
}
.no-booking-bg {
position: absolute;
inset: 0;
background: linear-gradient(135deg, #1f4f8a 0%, #2a5caa 45%, #15345e 100%);
z-index: 1;
}
.no-booking-content {
position: relative;
z-index: 2;
padding: 80rpx 40rpx;
}
.icon-wrap {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
display: flex;
align-items: center;
justify-content: center;
}
.icon-wrap .tn-icon-home-smile {
font-size: 60rpx;
}
.btn-glass {
background: rgba(255, 255, 255, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.4);
backdrop-filter: blur(10px);
color: #ffffff;
}
/* 审核中卡片 - 重新设计 */
.pending-booking-card {
background: #ffffff;
border-radius: 32rpx;
box-shadow: 0 16rpx 40rpx rgba(43, 142, 255, 0.08);
overflow: hidden;
margin-top: 20rpx;
border: 1rpx solid rgba(255, 255, 255, 0.8);
position: relative;
}
.pending-hero {
height: 220rpx;
position: relative;
background: linear-gradient(135deg, rgba($brand-primary, 0.06) 0%, rgba($brand-primary, 0.12) 100%);
overflow: hidden;
}
.hero-bg-shape {
position: absolute;
top: -100rpx;
right: -50rpx;
width: 300rpx;
height: 300rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba($brand-primary, 0.1) 0%, rgba(255, 255, 255, 0) 70%);
}
.hero-content {
position: absolute;
inset: 0;
z-index: 2;
}
.status-icon-ring {
width: 140rpx;
height: 140rpx;
border-radius: 70rpx;
background: #ffffff;
box-shadow: 0 8rpx 24rpx rgba($brand-primary, 0.12);
border: 8rpx solid rgba(255, 255, 255, 0.6);
backdrop-filter: blur(8px);
}
.status-icon {
font-size: 70rpx;
color: #FFA500; /* 使用琥珀色表示审核中,与规范一致 */
}
.status-text-area {
margin-top: -10rpx;
}
.status-desc {
line-height: 1.6;
padding: 0 20rpx;
}
/* 预定信息卡片拟物化 */
.booking-ticket-card {
background: linear-gradient(180deg, #f8faff 0%, #ffffff 100%);
border-radius: 24rpx;
border: 1rpx solid rgba($brand-primary, 0.08);
box-shadow: inset 0 2rpx 0 rgba(255, 255, 255, 1), 0 8rpx 20rpx rgba(15, 23, 42, 0.03);
margin: 0 10rpx;
}
.ticket-header {
padding: 24rpx 32rpx;
}
.ticket-icon-wrap {
width: 48rpx;
height: 48rpx;
border-radius: 12rpx;
background: rgba($brand-primary, 0.1);
color: $brand-primary;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
}
.ticket-status-badge {
font-size: 22rpx;
color: #FFA500;
background: rgba(255, 165, 0, 0.1);
padding: 6rpx 16rpx;
border-radius: 12rpx;
font-weight: bold;
}
.ticket-divider {
padding: 0 32rpx;
}
.divider-line {
height: 2rpx;
background: repeating-linear-gradient(to right, rgba($brand-primary, 0.15) 0, rgba($brand-primary, 0.15) 8rpx, transparent 8rpx, transparent 16rpx);
}
.ticket-content {
padding: 32rpx;
}
.ticket-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.ticket-label {
color: #64748b;
font-size: 26rpx;
min-width: 120rpx;
}
.ticket-value {
color: #1e293b;
font-size: 28rpx;
flex: 1;
text-align: right;
margin-left: 20rpx;
word-break: break-all;
line-height: 1.4;
}
.ticket-note {
color: #64748b;
font-size: 24rpx;
line-height: 1.6;
}
/* 进度条样式升级 */
.process-track {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 0 10rpx;
}
.track-item {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 2;
flex: 1;
}
.track-dot {
width: 40rpx;
height: 40rpx;
border-radius: 20rpx;
background: #e2e8f0;
border: 4rpx solid #ffffff;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 20rpx;
position: relative;
}
.track-item.active .track-dot {
background: $brand-primary;
}
/* 呼吸灯效果 */
.track-item.current .track-dot {
background: transparent;
border: none;
box-shadow: none;
}
.dot-pulse {
position: absolute;
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background: rgba(#FFA500, 0.4);
animation: pulse 2s infinite;
}
.dot-inner {
position: relative;
width: 20rpx;
height: 20rpx;
border-radius: 50%;
background: #FFA500;
z-index: 2;
box-shadow: 0 0 10rpx rgba(#FFA500, 0.6);
}
@keyframes pulse {
0% { transform: scale(0.8); opacity: 1; }
100% { transform: scale(2); opacity: 0; }
}
.track-text {
font-size: 22rpx;
color: #94a3b8;
margin-top: 16rpx;
white-space: nowrap;
}
.track-item.active .track-text {
color: $brand-primary;
font-weight: bold;
}
.track-item.current .track-text {
color: #FFA500;
font-weight: bold;
}
.track-line {
flex: 1;
height: 4rpx;
background: #e2e8f0;
margin: 18rpx -30rpx 0;
z-index: 1;
border-radius: 2rpx;
}
.track-line.active-line {
background: linear-gradient(90deg, $brand-primary 0%, rgba($brand-primary, 0.3) 100%);
}
/* 操作按钮 */
.action-btn-group {
gap: 24rpx;
}
.action-btn-group--single {
justify-content: center;
}
.pending-action-btn {
flex: 1;
height: 92rpx;
border-radius: 24rpx;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 24rpx;
transition: all 0.3s ease;
font-size: 28rpx;
}
.action-btn-group--single .pending-action-btn {
flex: none;
width: 100%;
}
.pending-action-btn:active {
transform: scale(0.98);
}
.pending-action-btn--secondary {
background: #ffffff;
color: $brand-primary;
border: 2rpx solid rgba($brand-primary, 0.22);
box-shadow: 0 8rpx 18rpx rgba($brand-primary, 0.08);
}
.pending-action-btn--secondary:active {
box-shadow: 0 4rpx 10rpx rgba($brand-primary, 0.06);
}
.pending-action-btn--primary {
background: linear-gradient(135deg, rgba($brand-primary, 0.9) 0%, $brand-primary 100%);
color: #ffffff;
border: 2rpx solid transparent;
box-shadow: 0 10rpx 24rpx rgba($brand-primary, 0.22);
}
.pending-action-btn--primary:active {
box-shadow: 0 6rpx 14rpx rgba($brand-primary, 0.16);
}
.half-btn {
width: 48%;
height: 92rpx;
}
/* --- 融合版房卡 (Integrated Room Card) --- */
.integrated-room-card {
border-radius: 36rpx;
position: relative;
overflow: hidden;
background: linear-gradient(135deg, #122d52 0%, #1c4678 46%, #16355d 100%);
box-shadow: 0 20rpx 54rpx rgba(15, 36, 68, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.08);
}
.irc-bg {
width: 100%;
height: 100%;
position: absolute;
z-index: 1;
transform: scale(1.04);
}
.irc-placeholder {
position: absolute;
inset: 0;
z-index: 1;
background: linear-gradient(135deg, #143764 0%, #255694 46%, #102b4d 100%);
}
.irc-placeholder-glow {
position: absolute;
width: 360rpx;
height: 360rpx;
right: -100rpx;
top: -100rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.24) 0%, rgba(255, 255, 255, 0) 72%);
}
.irc-mask {
width: 100%;
height: 100%;
position: absolute;
z-index: 3;
background:
linear-gradient(135deg, rgba(5, 18, 38, 0.85) 0%, rgba(8, 27, 55, 0.6) 42%, rgba(6, 18, 36, 0.8) 100%),
linear-gradient(180deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0) 100%);
}
.irc-content {
position: relative;
z-index: 4;
padding: 36rpx;
}
.irc-header {
border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
padding-bottom: 30rpx;
}
.irc-room-no {
font-size: 64rpx;
font-weight: 700;
color: #ffffff;
letter-spacing: 2rpx;
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2);
}
.irc-status-badge {
height: 48rpx;
padding: 0 20rpx;
border-radius: 999rpx;
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, rgba(255, 219, 128, 0.9) 0%, rgba(255, 190, 92, 0.9) 100%);
color: #7a4c00;
font-size: 22rpx;
font-weight: 700;
}
.irc-room-title {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.9);
font-weight: 500;
}
.irc-lease {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.5);
}
.irc-smart-area {
padding-top: 30rpx;
}
.irc-env-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.7);
}
.irc-smart-btn {
font-size: 24rpx;
color: #4facfe;
background: rgba(79, 172, 254, 0.1);
padding: 8rpx 20rpx;
border-radius: 20rpx;
border: 1rpx solid rgba(79, 172, 254, 0.2);
}
.irc-smart-btn:active {
background: rgba(79, 172, 254, 0.2);
}
.irc-quick-controls {
width: 100%;
white-space: nowrap;
margin-top: 24rpx;
}
.irc-qc-list {
display: inline-flex;
gap: 20rpx;
min-width: 100%;
padding-bottom: 4rpx;
}
.irc-qc-item {
width: 144rpx;
height: 144rpx;
padding: 0 8rpx;
background: rgba(255, 255, 255, 0.08);
border: 1rpx solid rgba(255, 255, 255, 0.06);
border-radius: 24rpx;
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
box-sizing: border-box;
flex-shrink: 0;
}
.irc-qc-item:active {
transform: scale(0.95);
background: rgba(255, 255, 255, 0.12);
}
.irc-qc-item.is-active {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.1) 100%);
border-color: rgba(255, 255, 255, 0.3);
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.1);
}
.irc-qc-icon {
font-size: 46rpx;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 12rpx;
transition: all 0.3s;
}
.irc-qc-item.is-active .irc-qc-icon {
color: #ffffff;
text-shadow: 0 0 16rpx rgba(255, 255, 255, 0.4);
}
.irc-qc-name {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.7);
transition: all 0.3s;
width: 100%;
text-align: center;
font-weight: 500;
white-space: normal;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.2;
}
.irc-qc-item.is-active .irc-qc-name {
color: #ffffff;
}
.irc-qc-empty {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
height: 136rpx;
border-radius: 24rpx;
background: rgba(255, 255, 255, 0.05);
border: 1rpx dashed rgba(255, 255, 255, 0.15);
color: rgba(255, 255, 255, 0.5);
font-size: 26rpx;
letter-spacing: 2rpx;
}
/* --- 新版管家列表 (Butler Card New) --- */
.butler-card-new {
background-color: #ffffff;
border-radius: 24rpx;
padding: 30rpx;
box-shadow: 0 10rpx 30rpx rgba(0,0,0,0.03);
}
.butler-card-new__inner {
margin-top: 10rpx;
}
.butler-member-new {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.butler-member-new:last-child {
border-bottom: none;
padding-bottom: 0;
}
.butler-member-new__identity {
display: flex;
align-items: center;
}
.butler-avatar-new {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
position: relative;
}
.butler-avatar-new image {
width: 100%;
height: 100%;
border-radius: 40rpx;
}
.butler-avatar-new-fallback {
width: 100%;
height: 100%;
border-radius: 40rpx;
background: linear-gradient(135deg, rgba($brand-primary, 0.6) 0%, rgba($brand-primary, 0.8) 100%);
}
.butler-avatar-new-fallback-text {
font-size: 32rpx;
color: #fff;
font-weight: 700;
}
.butler-member-new__info {
margin-left: 20rpx;
}
.butler-member-new__name {
font-size: 30rpx;
font-weight: 600;
color: #333333;
display: flex;
align-items: center;
}
.butler-member-new__tag {
font-size: 20rpx;
background: rgba($brand-primary, 0.1);
color: $brand-primary;
padding: 2rpx 10rpx;
border-radius: 8rpx;
margin-left: 10rpx;
}
/* 宿管标签 - 绿色 */
.butler-member-new__tag--type.tag-dormitory {
background: rgba(16, 185, 129, 0.1);
color: #10B981;
}
/* 管理员标签 - 琥珀色 */
.butler-member-new__tag--type.tag-counselor {
background: rgba(245, 158, 11, 0.1);
color: #F59E0B;
}
.butler-member-new__phone {
font-size: 24rpx;
color: #888888;
margin-top: 6rpx;
}
.butler-member-new__action {
width: 64rpx;
height: 64rpx;
border-radius: 32rpx;
background: #f0f6ff;
color: $brand-primary;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
}
.butler-member-new__action:active {
background: #e6f0ff;
}
.butler-member-new__action.disabled {
background: #f5f5f5;
color: #cccccc;
}
.service-item {
width: 48%;
}
.service-card-visual {
height: 260rpx;
border-radius: 24rpx;
position: relative;
overflow: hidden;
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.03);
}
.service-card-bg {
width: 100%;
height: 100%;
position: absolute;
z-index: 1;
}
.service-card-mask {
width: 100%;
height: 100%;
position: absolute;
z-index: 2;
background: linear-gradient(to bottom, rgba(0,0,0,0.1) 0%, rgba(0,0,0,0.8) 100%);
}
.service-card-content {
position: relative;
z-index: 3;
}
.service-icon-box {
width: 60rpx;
height: 60rpx;
border-radius: 30rpx;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.92) 0%, rgba($brand-primary, 0.18) 100%);
box-shadow: inset 0 0 0 1rpx rgba(255, 255, 255, 0.28);
}
.service-icon-symbol {
font-size: 28rpx;
color: $icon-tone-strong;
line-height: 1;
}
.section-arrow-icon {
margin-left: 4rpx;
font-size: 24rpx;
line-height: 1;
color: $icon-tone-muted;
}
.status-tag {
padding: 4rpx 16rpx;
border-radius: 20rpx;
font-weight: bold;
}
.tag-free {
background-color: rgba(255,255,255,0.2);
color: #FFFFFF;
backdrop-filter: blur(4px);
}
.tag-busy {
background-color: rgba(255, 90, 95, 0.9);
color: #FFFFFF;
}
/* 自定义按钮样式 */
.custom-btn {
display: inline-flex;
justify-content: center;
align-items: center;
border-radius: 100rpx;
padding: 12rpx 36rpx;
transition: all 0.3s;
}
.custom-btn:active {
transform: scale(0.98);
}
.btn-primary {
background: linear-gradient(135deg, rgba($brand-primary, 0.9) 0%, $brand-primary 100%);
color: #ffffff;
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.25);
}
.btn-dark {
background: #ffffff;
color: $brand-primary;
border: 1rpx solid rgba($brand-primary, 0.3);
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
}
.animation-fade-in {
animation: fadeIn 0.4s ease-out forwards;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20rpx); }
to { opacity: 1; transform: translateY(0); }
}
.bill-section {
background-color: #FFFFFF;
border-radius: 24rpx;
box-shadow: 0 10rpx 30rpx rgba(0,0,0,0.03);
}
.event-card {
background-color: #FFFFFF;
border-radius: 24rpx;
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.02);
transition: all 0.3s;
}
.event-card:active {
transform: scale(0.98);
}
.event-img-box {
width: 160rpx;
height: 160rpx;
border-radius: 16rpx;
overflow: hidden;
flex-shrink: 0;
}
.event-img {
width: 100%;
height: 100%;
}
.avatar-stack {
display: flex;
align-items: center;
}
.stack-item {
width: 48rpx;
height: 48rpx;
border-radius: 24rpx;
border: 4rpx solid #FFFFFF;
margin-left: -16rpx;
}
.stack-item:first-child {
margin-left: 0;
}
.stack-more {
background-color: #F5F5F5;
color: #888888;
}
</style>