将组件级 i18n 对象重构为统一由全局 locales/messages.js 管理,确保中英文语言切换时正确显示对应文本。 - 新增 utils/i18n.js 国际化工具函数 - 更新 store/$t.mixin.js 翻译混入逻辑 - 迁移 pkgService、pkgBooking、pkgAccount、pkgExplore、pages 等共 58 个组件的中英文文本至全局消息表
2241 lines
72 KiB
Vue
2241 lines
72 KiB
Vue
<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>
|