4957 lines
158 KiB
Vue
4957 lines
158 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" :class="navBarTextColor" style="transition: color 0.3s ease">{{ $tr('我的入住') }}</view>
|
||
</tn-nav-bar>
|
||
|
||
<scroll-view class="service-scroll" scroll-y @scroll="handleScroll">
|
||
<view v-if="!hasCheckedIn" class="empty-state animation-fade-in" :class="{'has-booking': currentBooking}">
|
||
<!-- 无预订:去选房 -->
|
||
<view v-if="!currentBooking" class="no-booking-card animation-fade-in" :style="{marginTop: vuex_custom_bar_height + 20 + 'px', marginInline: '30rpx'}">
|
||
<view class="no-booking-glow no-booking-glow--one"></view>
|
||
<view class="no-booking-glow no-booking-glow--two"></view>
|
||
<view class="no-booking-content tn-flex tn-flex-direction-column tn-flex-col-center">
|
||
<view class="icon-wrap">
|
||
<text class="tn-icon-home-smile"></text>
|
||
</view>
|
||
<view class="no-booking-kicker">{{ $tr('入住服务') }}</view>
|
||
<view class="no-booking-title">{{ $tr('您还没有在租房间') }}</view>
|
||
<view class="no-booking-desc">
|
||
{{ $tr('长租公寓为海外青年提供高端社区服务,请先前往选房大厅挑选心仪套间') }}
|
||
</view>
|
||
<view class="no-booking-action" @click="goSelectRoom">
|
||
<text class="tn-text-bold">{{ $tr('去选房') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 有预订:流程作业页面 -->
|
||
<view v-else class="flow-page animation-fade-in" style="animation-delay: 0.1s;">
|
||
<!-- 缩小的顶部Banner -->
|
||
<view class="flow-banner">
|
||
<swiper
|
||
v-if="applicationHeroImages.length"
|
||
class="flow-banner-swiper"
|
||
:autoplay="applicationHeroImages.length > 1"
|
||
:circular="applicationHeroImages.length > 1"
|
||
interval="4200"
|
||
duration="480"
|
||
@change="handleApplicationHeroChange"
|
||
>
|
||
<swiper-item v-for="(image, index) in applicationHeroImages" :key="`${image}-${index}`">
|
||
<image :src="image" mode="aspectFill" class="flow-banner-image"></image>
|
||
</swiper-item>
|
||
</swiper>
|
||
<view v-else class="flow-banner-placeholder">
|
||
<view class="flow-banner-placeholder-glow"></view>
|
||
</view>
|
||
<view class="flow-banner-overlay"></view>
|
||
|
||
<view class="flow-banner-content" :style="{paddingTop: vuex_custom_bar_height + 'px'}">
|
||
<view class="flow-banner-topline">
|
||
<view class="flow-banner-chip">{{ applicationTypeText }}</view>
|
||
</view>
|
||
<view class="flow-banner-title clamp-text-1">{{ applicationHeroTitle }}</view>
|
||
<view class="flow-banner-subtitle clamp-text-1">{{ applicationHeroArea }}</view>
|
||
</view>
|
||
<view v-if="applicationHeroImages.length > 1" class="flow-banner-pagination">
|
||
<view
|
||
v-for="(image, index) in applicationHeroImages"
|
||
:key="`dot-${index}`"
|
||
class="flow-banner-pagination-dot"
|
||
:class="{ active: index === applicationHeroIndex }"
|
||
></view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="flow-body">
|
||
<view class="flow-unified-panel">
|
||
<view class="flow-unified-panel-glow flow-unified-panel-glow--one"></view>
|
||
<view class="flow-unified-panel-glow flow-unified-panel-glow--two"></view>
|
||
|
||
<!-- 房型信息卡片 -->
|
||
<view v-if="applicationHeroMeta.length" class="flow-meta-card">
|
||
<view class="flow-meta-header tn-text-bold tn-text-lg tn-color-black">{{ $tr('房型信息') }}</view>
|
||
<view class="flow-meta-grid">
|
||
<view v-for="item in applicationHeroMeta" :key="item.label" class="flow-meta-grid-item">
|
||
<view class="flow-meta-grid-label">{{ item.label }}</view>
|
||
<view class="flow-meta-grid-value clamp-text-2">{{ item.value }}</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="applicationHeroMeta.length" class="flow-panel-divider"></view>
|
||
|
||
<!-- 竖向流程轨道 + 当前阶段卡片 -->
|
||
<view class="flow-timeline-card">
|
||
<view class="flow-timeline-head">
|
||
<view class="flow-timeline-title">{{ $tr('流程进度') }}</view>
|
||
<view class="flow-progress-pill">{{ flowProgressText }}</view>
|
||
</view>
|
||
|
||
<view class="flow-step-rail">
|
||
<view
|
||
v-for="node in flowNodes"
|
||
:key="node.step"
|
||
class="flow-step-item"
|
||
:class="{ completed: node.completed, current: node.current }"
|
||
>
|
||
<view class="flow-step-axis">
|
||
<view class="flow-step-orb">
|
||
<text v-if="node.completed" class="tn-icon-success"></text>
|
||
<text v-else class="flow-step-index">{{ node.step }}</text>
|
||
</view>
|
||
<view v-if="node.step !== flowNodes.length" class="flow-step-line"></view>
|
||
</view>
|
||
<view class="flow-step-main">
|
||
<view class="flow-step-label-row">
|
||
<view class="flow-step-label">{{ node.label }}</view>
|
||
<view v-if="node.current" class="flow-stage-status">{{ bookingStatusText }}</view>
|
||
</view>
|
||
<view v-if="node.current" class="flow-stage-content">
|
||
<view class="flow-stage-copy">
|
||
<view class="flow-stage-kicker">{{ $tr('当前阶段') }}</view>
|
||
<view class="flow-stage-title">{{ pendingStageTitle }}</view>
|
||
</view>
|
||
|
||
<view v-if="currentStageMetaText" class="flow-stage-meta-line">
|
||
<text class="flow-stage-meta-label">{{ currentStageMetaLabel }}</text>
|
||
<text class="flow-stage-meta-value">{{ currentStageMetaText }}</text>
|
||
</view>
|
||
|
||
<view class="flow-stage-desc">{{ pendingStageDesc }}</view>
|
||
|
||
<view v-if="nextFlowLabel" class="flow-stage-next">{{ $tr('下一步') }} · {{ nextFlowLabel }}</view>
|
||
|
||
<view v-if="workflowActionText || canCancelCurrentApplication" class="flow-stage-actions">
|
||
<view v-if="workflowActionText" class="action-btn primary" @click="handlePendingPrimaryAction">
|
||
<text class="tn-text-bold">{{ workflowActionText }}</text>
|
||
</view>
|
||
<view v-if="canCancelCurrentApplication" class="action-btn danger" @click="handleCancelApplication">
|
||
<text class="tn-text-bold">{{ cancelApplicationActionText }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 已入住状态下的服务大厅内容 -->
|
||
<view v-else class="animation-fade-in tn-padding-bottom" :style="{paddingTop: vuex_custom_bar_height + 'px'}">
|
||
<!-- 融合房卡与智能控制的新卡片 -->
|
||
<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-decor irc-decor--aurora"></view>
|
||
<view class="irc-decor irc-decor--ring"></view>
|
||
<view class="irc-decor irc-decor--grid"></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>
|
||
|
||
<view class="irc-lock-entry" @click="goLockControl">
|
||
<view class="irc-lock-entry__icon">
|
||
<text class="tn-icon-lock"></text>
|
||
</view>
|
||
<view class="irc-lock-entry__content">
|
||
<view class="irc-lock-entry__title">{{ $tr('门锁') }}</view>
|
||
<view class="irc-lock-entry__desc">{{ $tr('我的钥匙 · 门锁密码 · 录入指纹') }}</view>
|
||
</view>
|
||
<view class="irc-lock-entry__arrow">
|
||
<text class="tn-icon-right"></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-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="getFeaturedServiceImage(item)" 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 v-if="stayApplicationsEnabled && stayActions.length" 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-row-between">
|
||
<view
|
||
v-for="item in stayActions"
|
||
:key="item.key"
|
||
class="stay-action-btn tn-flex tn-flex-col-center tn-flex-row-center"
|
||
@click="openServicePage(item)"
|
||
>
|
||
<view class="stay-action-icon-box tn-flex tn-flex-col-center tn-flex-row-center">
|
||
<text :class="item.icon"></text>
|
||
</view>
|
||
<text>{{ pickItemText(item, 'label', 'labelEn') }}</text>
|
||
</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 bill-status-tag"
|
||
:class="unpaidBillCount ? 'bill-status-tag--active' : 'bill-status-tag--settled'"
|
||
>{{ 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>
|
||
|
||
<!-- 支付确认弹窗 -->
|
||
<tn-popup v-model="showDepositNotice" mode="bottom" length="80%" :borderRadius="32" :closeBtn="true" @close="closeDepositNotice">
|
||
<view class="deposit-notice-popup">
|
||
<view class="popup-header">
|
||
<view class="popup-handle"></view>
|
||
<view class="popup-title">{{ getPaymentNoticeTitle() }}</view>
|
||
</view>
|
||
<view class="popup-body">
|
||
<view class="notice-content">
|
||
<view class="payment-sheet">
|
||
<view class="payment-hero">
|
||
<view class="payment-hero-label">{{ $tr('本次支付') }}</view>
|
||
<view class="payment-hero-amount">{{ getPaymentNoticeBillAmountDisplay() }}</view>
|
||
<view class="payment-hero-hint">{{ $tr('确认后将发起微信支付') }}</view>
|
||
</view>
|
||
<view v-if="paymentNoticeSourceLoading || paymentNoticeBillLoading" class="payment-sync-group">
|
||
<view v-if="paymentNoticeSourceLoading" class="payment-sync-chip">
|
||
{{ $tr('正在同步申请详情,请稍候...') }}
|
||
</view>
|
||
<view v-if="paymentNoticeBillLoading" class="payment-sync-chip">
|
||
{{ $tr('正在同步支付预览,请稍候...') }}
|
||
</view>
|
||
</view>
|
||
<view v-if="getPaymentNoticeDetailRows().length" class="payment-detail-panel">
|
||
<view class="payment-detail-header">
|
||
<view class="payment-detail-title">{{ $tr('入住申请信息') }}</view>
|
||
</view>
|
||
<view class="payment-detail-list">
|
||
<view
|
||
v-for="item in getPaymentNoticeDetailRows()"
|
||
:key="item.label"
|
||
class="payment-detail-row"
|
||
>
|
||
<text class="payment-detail-label">{{ item.label }}</text>
|
||
<text class="payment-detail-value">{{ item.value }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="popup-footer">
|
||
<view class="notice-action-btn" @click="confirmAndPayDeposit">
|
||
<text class="tn-text-bold">{{ getPaymentNoticeConfirmText() }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</tn-popup>
|
||
|
||
<tn-action-sheet
|
||
v-model="showPaymentMethodSheet"
|
||
:list="getPaymentMethodSheetOptions()"
|
||
:tips="{ text: getPaymentMethodSheetTipText() }"
|
||
:borderRadius="32"
|
||
:safeAreaInsetBottom="true"
|
||
@click="handlePaymentMethodSheetClick"
|
||
></tn-action-sheet>
|
||
|
||
<deposit-other-payment-popup
|
||
v-model="showOfflinePaymentNotice"
|
||
:config="depositOtherPaymentConfig"
|
||
:loading="depositOtherPaymentConfigLoading"
|
||
/>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import DepositOtherPaymentPopup from '../../../components/DepositOtherPaymentPopup.vue'
|
||
import { accountQueryBill, accountQueryBillPreview, accountQueryBills, applicationFormDetail, contractDetail, facilityManagers, gatewayRoomDeviceList, orderSearchedDetail, paymentBillPay, paymentPayingBills, roomInspectionDetail, sysdictPage, userDetailCancel } from '../../../api/index.js'
|
||
import { getSessionSnapshot, saveActiveApplicationCode, saveMyRoomData } from '../../../utils/app-state'
|
||
import { createDefaultDepositOtherPaymentConfig, loadDepositOtherPaymentConfig } from '../../../utils/deposit-other-payment'
|
||
import { ACCOUNT_BILL_PAYMENT_STATUS, ACCOUNT_BILL_STATUS, fetchEnums, getAccountReferenceTypeLabel, getApplicationDetailStatusLabel } from '../../../utils/enum-store'
|
||
import { getBookingStatusText } from '../../../utils/flow'
|
||
import { getOrderStatusCode, normalizeBookingDetail, parseOrderDetailResponse } from '../../../utils/booking-helper'
|
||
import { normalizeSmartDeviceList } from '../../../utils/smart-device'
|
||
import { compactEnglishText, localizeApartmentName, localizePayMethodText } from '../../../utils/select-room-normalizers'
|
||
import { addFilePrefix } from '../../../utils/common'
|
||
import { getReservationOrderNo } from '../../../utils/reservation-flow'
|
||
|
||
const DEFAULT_SMART_BRAND_ID = 'Tuya'
|
||
const MOBILE_STAY_APPLICATION_CONFIG_TYPE = 'MobileStayApplicationConfig'
|
||
const MOBILE_FEATURED_SERVICE_CONFIG_TYPE = 'MobileFeaturedServiceConfig'
|
||
const VIEWING_APPLICATION_PATTERN = /预约看房申请|看房时间[::]|意向房型[::]|意向区域[::]|viewing request|viewing time[::]|preferred room type[::]|preferred area[::]/i
|
||
|
||
const DEFAULT_STAY_ACTIONS = [
|
||
{
|
||
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.',
|
||
entryType: 'ticket',
|
||
serviceType: 'TicketChangeApply',
|
||
businessType: 'change-room'
|
||
},
|
||
{
|
||
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.',
|
||
entryType: 'ticket',
|
||
serviceType: 'TicketRenewalApply',
|
||
businessType: 'renew-stay'
|
||
},
|
||
{
|
||
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.',
|
||
entryType: 'ticket',
|
||
serviceType: 'TicketCheckOutApply',
|
||
businessType: 'checkout'
|
||
}
|
||
]
|
||
|
||
const DEFAULT_STAY_ACTION_MAP = DEFAULT_STAY_ACTIONS.reduce((result, item) => {
|
||
result[item.key] = item
|
||
return result
|
||
}, {})
|
||
|
||
const DEFAULT_FEATURED_SERVICES = [
|
||
{
|
||
key: 'repair',
|
||
icon: 'tn-icon-set-fill',
|
||
nameZh: '报修',
|
||
nameEn: 'Repair',
|
||
statusZh: '24小时',
|
||
statusEn: '24 Hours',
|
||
isBusy: false,
|
||
image: '',
|
||
entryType: 'ticket',
|
||
serviceType: 'TicketRepairApply',
|
||
businessType: 'repair'
|
||
},
|
||
{
|
||
key: 'water-delivery',
|
||
icon: 'tn-icon-shop-fill',
|
||
nameZh: '送水服务',
|
||
nameEn: 'Water Delivery',
|
||
statusZh: '可提交',
|
||
statusEn: 'Available',
|
||
isBusy: false,
|
||
image: '',
|
||
entryType: 'ticket',
|
||
serviceType: 'TicketWaterDeliveryApply',
|
||
businessType: 'water-delivery'
|
||
},
|
||
{
|
||
key: 'meal-delivery',
|
||
icon: 'tn-icon-cart-fill',
|
||
nameZh: '送餐服务',
|
||
nameEn: 'Food Delivery',
|
||
statusZh: '可下单',
|
||
statusEn: 'Order Now',
|
||
isBusy: false,
|
||
image: '',
|
||
businessType: 'meal-delivery'
|
||
},
|
||
{
|
||
key: 'cleaning',
|
||
icon: 'tn-icon-clean',
|
||
nameZh: '清洁服务',
|
||
nameEn: 'Cleaning Service',
|
||
statusZh: '可预约',
|
||
statusEn: 'Book Now',
|
||
isBusy: false,
|
||
image: '',
|
||
entryType: 'ticket',
|
||
serviceType: 'TicketCleaningApply',
|
||
businessType: 'cleaning'
|
||
},
|
||
{
|
||
key: 'complaint',
|
||
icon: 'tn-icon-message-fill',
|
||
nameZh: '投诉建议',
|
||
nameEn: 'Feedback',
|
||
statusZh: '随时提交',
|
||
statusEn: 'Anytime',
|
||
isBusy: false,
|
||
image: '',
|
||
entryType: 'ticket',
|
||
serviceType: 'TicketSuggestionApply',
|
||
businessType: 'complaint'
|
||
},
|
||
{
|
||
key: 'lost-found',
|
||
icon: 'tn-icon-search',
|
||
nameZh: '失物拾物',
|
||
nameEn: 'Lost and Found',
|
||
statusZh: '登记中',
|
||
statusEn: 'Open',
|
||
isBusy: false,
|
||
image: '',
|
||
businessType: 'lost-found'
|
||
}
|
||
]
|
||
|
||
const DEFAULT_FEATURED_SERVICE_MAP = DEFAULT_FEATURED_SERVICES.reduce((result, item) => {
|
||
result[item.key] = item
|
||
return result
|
||
}, {})
|
||
|
||
function parseJsonStringMaybe(value) {
|
||
if (typeof value !== 'string') {
|
||
return value
|
||
}
|
||
const text = value.trim()
|
||
if (!text) {
|
||
return value
|
||
}
|
||
if (!((text.startsWith('{') && text.endsWith('}')) || (text.startsWith('[') && text.endsWith(']')))) {
|
||
return value
|
||
}
|
||
try {
|
||
return JSON.parse(text)
|
||
} catch (error) {
|
||
return value
|
||
}
|
||
}
|
||
|
||
function parseExpireTimeToTimestamp(value) {
|
||
if (value === null || typeof value === 'undefined' || value === '') {
|
||
return 0
|
||
}
|
||
if (typeof value === 'number') {
|
||
return Number.isFinite(value) ? value : 0
|
||
}
|
||
const text = String(value).trim()
|
||
if (!text) {
|
||
return 0
|
||
}
|
||
if (/^\d{13,}$/.test(text)) {
|
||
return Number(text)
|
||
}
|
||
if (/^\d{10}$/.test(text)) {
|
||
return Number(text) * 1000
|
||
}
|
||
const timestamp = new Date(text).getTime()
|
||
return Number.isFinite(timestamp) ? timestamp : 0
|
||
}
|
||
|
||
function padCountdownUnit(value) {
|
||
return String(Math.max(0, Number(value) || 0)).padStart(2, '0')
|
||
}
|
||
|
||
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 isViewingApplicationRecord(item = {}) {
|
||
if (!item || typeof item !== 'object') {
|
||
return false
|
||
}
|
||
|
||
const typeText = [
|
||
item.businessType,
|
||
item.applicationType,
|
||
item.applicationTypeDesc,
|
||
item.formType,
|
||
item.bizType,
|
||
item.type,
|
||
item.typeDesc
|
||
]
|
||
.filter(value => value !== null && typeof value !== 'undefined' && String(value).trim())
|
||
.join(' ')
|
||
|
||
if (/viewing|看房|带看/i.test(typeText)) {
|
||
return true
|
||
}
|
||
|
||
if ([item.appointmentViewingTime, item.viewingTime, item.bookViewingTime].some(value => String(value || '').trim())) {
|
||
return true
|
||
}
|
||
|
||
const remark = item && item.remark ? String(item.remark) : ''
|
||
return VIEWING_APPLICATION_PATTERN.test(remark)
|
||
}
|
||
|
||
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 normalizeStayDictRows(response) {
|
||
const payload = response && response.data !== undefined ? response.data : response
|
||
if (Array.isArray(payload)) {
|
||
return payload
|
||
}
|
||
if (payload && Array.isArray(payload.data)) {
|
||
return payload.data
|
||
}
|
||
if (payload && payload.data && Array.isArray(payload.data.data)) {
|
||
return payload.data.data
|
||
}
|
||
return []
|
||
}
|
||
|
||
function normalizeStayActionConfigRows(rows = []) {
|
||
return rows
|
||
.map((row) => {
|
||
const extraConfig = parseJsonStringMaybe(row && (row.extraInfo || row.extraConfig || row.ExtraConfig))
|
||
const extra = extraConfig && typeof extraConfig === 'object' && !Array.isArray(extraConfig)
|
||
? extraConfig
|
||
: {}
|
||
const key = String(extra.key || row.key || '').trim()
|
||
if (!key) {
|
||
return null
|
||
}
|
||
|
||
const baseConfig = DEFAULT_STAY_ACTION_MAP[key] || {}
|
||
const enabled = extra.enabled === undefined ? true : extra.enabled !== false
|
||
if (!enabled) {
|
||
return null
|
||
}
|
||
|
||
return {
|
||
...baseConfig,
|
||
key,
|
||
label: String(extra.label || row.value || baseConfig.label || '').trim(),
|
||
labelEn: String(extra.labelEn || row.descriptionEn || baseConfig.labelEn || row.value || '').trim(),
|
||
icon: String(extra.icon || baseConfig.icon || 'tn-icon-home-smile-fill').trim(),
|
||
statusZh: String(extra.statusZh || baseConfig.statusZh || '可提交').trim(),
|
||
statusEn: String(extra.statusEn || baseConfig.statusEn || 'Available').trim(),
|
||
isBusy: extra.isBusy === true,
|
||
image: String(extra.image || '').trim(),
|
||
title: String(extra.title || row.value || baseConfig.title || '').trim(),
|
||
titleEn: String(extra.titleEn || row.descriptionEn || baseConfig.titleEn || row.value || '').trim(),
|
||
titleEnShort: String(extra.titleEnShort || extra.titleEn || baseConfig.titleEnShort || baseConfig.titleEn || row.descriptionEn || '').trim(),
|
||
desc: String(extra.desc || row.description || baseConfig.desc || '').trim(),
|
||
descEn: String(extra.descEn || baseConfig.descEn || row.descriptionEn || '').trim(),
|
||
entryType: String(extra.entryType || baseConfig.entryType || (extra.serviceType ? 'ticket' : 'submit')).trim().toLowerCase(),
|
||
serviceType: String(extra.serviceType || baseConfig.serviceType || '').trim(),
|
||
businessType: String(extra.businessType || baseConfig.businessType || key).trim(),
|
||
path: String(extra.path || '').trim(),
|
||
sort: Number(extra.priority ?? row.sort ?? 0) || 0
|
||
}
|
||
})
|
||
.filter(Boolean)
|
||
.sort((left, right) => left.sort - right.sort)
|
||
}
|
||
|
||
function normalizeFeaturedServiceConfigRows(rows = []) {
|
||
return rows
|
||
.map((row) => {
|
||
const extraConfig = parseJsonStringMaybe(row && (row.extraInfo || row.extraConfig || row.ExtraConfig))
|
||
const extra = extraConfig && typeof extraConfig === 'object' && !Array.isArray(extraConfig)
|
||
? extraConfig
|
||
: {}
|
||
const key = String(extra.key || row.key || '').trim()
|
||
if (!key) {
|
||
return null
|
||
}
|
||
|
||
const baseConfig = DEFAULT_FEATURED_SERVICE_MAP[key] || {}
|
||
const enabled = extra.enabled === undefined ? true : extra.enabled !== false
|
||
if (!enabled) {
|
||
return null
|
||
}
|
||
|
||
return {
|
||
...baseConfig,
|
||
key,
|
||
icon: String(extra.icon || baseConfig.icon || '').trim(),
|
||
nameZh: String(extra.nameZh || row.value || baseConfig.nameZh || '').trim(),
|
||
nameEn: String(extra.nameEn || row.descriptionEn || baseConfig.nameEn || row.value || '').trim(),
|
||
statusZh: String(extra.statusZh || row.description || baseConfig.statusZh || '').trim(),
|
||
statusEn: String(extra.statusEn || baseConfig.statusEn || '').trim(),
|
||
isBusy: extra.isBusy === true,
|
||
image: String(extra.image || baseConfig.image || '').trim(),
|
||
entryType: String(extra.entryType || baseConfig.entryType || '').trim().toLowerCase(),
|
||
serviceType: String(extra.serviceType || baseConfig.serviceType || '').trim(),
|
||
businessType: String(extra.businessType || baseConfig.businessType || key).trim(),
|
||
path: String(extra.path || baseConfig.path || '').trim(),
|
||
sort: Number(extra.priority ?? row.sort ?? 0) || 0
|
||
}
|
||
})
|
||
.filter(Boolean)
|
||
.sort((left, right) => left.sort - right.sort)
|
||
}
|
||
|
||
function appendImageCandidates(result = [], value) {
|
||
if (value === undefined || value === null || value === '') {
|
||
return result
|
||
}
|
||
|
||
const normalizedValue = parseJsonStringMaybe(value)
|
||
if (Array.isArray(normalizedValue)) {
|
||
normalizedValue.forEach(item => appendImageCandidates(result, item))
|
||
return result
|
||
}
|
||
|
||
if (normalizedValue && typeof normalizedValue === 'object') {
|
||
const keys = [
|
||
'image',
|
||
'imageUrl',
|
||
'url',
|
||
'src',
|
||
'cover',
|
||
'coverUrl',
|
||
'poster',
|
||
'posterUrl',
|
||
'photo',
|
||
'photoUrl',
|
||
'thumb',
|
||
'thumbUrl',
|
||
'fileUrl',
|
||
'path'
|
||
]
|
||
keys.forEach((key) => {
|
||
if (Object.prototype.hasOwnProperty.call(normalizedValue, key)) {
|
||
appendImageCandidates(result, normalizedValue[key])
|
||
}
|
||
})
|
||
return result
|
||
}
|
||
|
||
const text = String(normalizedValue).trim()
|
||
if (!text) {
|
||
return result
|
||
}
|
||
|
||
const values = text.includes(',') ? text.split(',') : [text]
|
||
values
|
||
.map(item => addFilePrefix(String(item || '').trim()))
|
||
.filter(Boolean)
|
||
.forEach((item) => {
|
||
if (!result.includes(item)) {
|
||
result.push(item)
|
||
}
|
||
})
|
||
|
||
return result
|
||
}
|
||
|
||
function extractGalleryImages(...sources) {
|
||
const result = []
|
||
const candidateKeys = [
|
||
'image',
|
||
'imageUrl',
|
||
'cover',
|
||
'coverUrl',
|
||
'poster',
|
||
'posterUrl',
|
||
'banner',
|
||
'bannerUrl',
|
||
'bannerImages',
|
||
'carouselImages',
|
||
'images',
|
||
'imageUrls',
|
||
'imageList',
|
||
'photos',
|
||
'photoList',
|
||
'pictures',
|
||
'album',
|
||
'albums',
|
||
'roomImages',
|
||
'roomTypeImages'
|
||
]
|
||
|
||
sources.forEach((source) => {
|
||
if (!source || typeof source !== 'object') {
|
||
return
|
||
}
|
||
candidateKeys.forEach((key) => {
|
||
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
||
appendImageCandidates(result, source[key])
|
||
}
|
||
})
|
||
})
|
||
|
||
return result
|
||
}
|
||
|
||
function buildRoomDataFromOrder(order = {}) {
|
||
if (!order || typeof order !== 'object' || !Object.keys(order).length) {
|
||
return null
|
||
}
|
||
|
||
const stayStart = order.contractStartTime || order.startDate || order.start || (order.checkInPeriod && order.checkInPeriod.start)
|
||
const stayEnd = order.contractEndTime || 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] || ''
|
||
}
|
||
}
|
||
|
||
function buildFlowLocationTitle(source = {}) {
|
||
if (!source || typeof source !== 'object') {
|
||
return ''
|
||
}
|
||
|
||
const roomName = pickReadableValue(
|
||
source.roomNo,
|
||
source.roomCode,
|
||
source.roomLabel,
|
||
source.roomName,
|
||
source.roomNoEn,
|
||
source.roomNameEn
|
||
)
|
||
|
||
return roomName
|
||
}
|
||
|
||
export default {
|
||
name: 'StayServiceTab',
|
||
components: {
|
||
DepositOtherPaymentPopup
|
||
},
|
||
data() {
|
||
return {
|
||
hasCheckedIn: false, // 模拟未入住/已入住状态
|
||
myRoomData: null, // 从缓存获取的房间信息
|
||
currentBooking: null,
|
||
currentFlow: null,
|
||
currentReservationStep: null,
|
||
currentReservationRefNo: '',
|
||
currentApplicationDetail: null,
|
||
reservationContractDetail: null,
|
||
reservationInspectionDetail: null,
|
||
applicationHeroIndex: 0,
|
||
isPendingStatusRefreshing: false,
|
||
isCancelingApplication: false,
|
||
areaManager: null,
|
||
areaManagers: [],
|
||
isAreaManagerLoading: false,
|
||
// 宿管与管理员联系人
|
||
dormitoryManagers: [],
|
||
counselor: null,
|
||
isManagementContactsLoading: false,
|
||
stayApplicationsEnabled: false,
|
||
// 导航栏滚动背景色
|
||
navBarBackgroundColor: 'transparent',
|
||
// 支付定金弹窗
|
||
showDepositNotice: false,
|
||
showPaymentMethodSheet: false,
|
||
showOfflinePaymentNotice: false,
|
||
isPaymentActionRunning: false,
|
||
pendingDepositReferenceTypeNo: '',
|
||
pendingPaymentMethodContext: null,
|
||
pendingDepositPaymentContext: null,
|
||
paymentNoticeSourceItem: null,
|
||
paymentNoticeSourceLoading: false,
|
||
paymentNoticeBillLoading: false,
|
||
paymentNoticeBillDetail: null,
|
||
depositOtherPaymentConfig: createDefaultDepositOtherPaymentConfig(),
|
||
depositOtherPaymentConfigLoading: false,
|
||
reservationBillDetailMap: {},
|
||
unpaidBillList: [],
|
||
smartDevices: [],
|
||
isSmartDevicesLoading: false,
|
||
smartDeviceError: '',
|
||
stayActions: DEFAULT_STAY_ACTIONS.map(item => ({ ...item })),
|
||
services: DEFAULT_FEATURED_SERVICES.map(item => ({ ...item })),
|
||
}
|
||
},
|
||
watch: {
|
||
language() {
|
||
this.loadStayApplicationConfig()
|
||
this.loadFeaturedServiceConfig()
|
||
this.loadDepositOtherPaymentConfig()
|
||
}
|
||
},
|
||
computed: {
|
||
navBarTextColor() {
|
||
// 在流程作业页面(有预订且未入住)未滚动时,标题使用白色,否则黑色
|
||
if (!this.hasCheckedIn && this.currentBooking && this.navBarBackgroundColor === 'transparent') {
|
||
return 'tn-color-white'
|
||
}
|
||
return 'tn-color-black'
|
||
},
|
||
language() {
|
||
return this.$getLanguage()
|
||
},
|
||
bookingStatusText() {
|
||
if (this.isCurrentStepPendingDispatch) {
|
||
return this.$tr('待下发')
|
||
}
|
||
if (this.isCurrentContractAwaitingReview) {
|
||
return this.$tr('待审核')
|
||
}
|
||
if (this.isApplicationStep) {
|
||
return this.$tr('申请中')
|
||
}
|
||
if (this.isInspectionStep) {
|
||
return this.$tr('验房中')
|
||
}
|
||
if (this.isBillStep) {
|
||
return this.$tr('待支付')
|
||
}
|
||
if (this.isContractSigningStep) {
|
||
return this.$tr('待签约')
|
||
}
|
||
if (Number(this.currentReservationStep || 0) === 5) {
|
||
return this.$tr('待入住')
|
||
}
|
||
|
||
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 : []
|
||
},
|
||
currentReservationRawStep() {
|
||
return Number(this.currentReservationStep || 0)
|
||
},
|
||
currentReservationDisplayStep() {
|
||
return this.currentReservationRawStep
|
||
},
|
||
isCurrentStepPendingDispatch() {
|
||
return this.currentReservationRawStep >= 2 && this.currentReservationRawStep <= 5 && !this.currentReservationRefNo
|
||
},
|
||
pendingDispatchTargetLabel() {
|
||
const targetMap = {
|
||
2: this.$tr('验房单'),
|
||
3: this.$tr('合同'),
|
||
4: this.$tr('账单'),
|
||
5: this.$tr('入住安排')
|
||
}
|
||
return targetMap[this.currentReservationRawStep] || this.$tr('下一步流程')
|
||
},
|
||
isApplicationStep() {
|
||
return this.currentReservationDisplayStep === 1
|
||
},
|
||
currentApplicationPayStatus() {
|
||
const detail = this.currentApplicationDetail || {}
|
||
const numericStatus = Number(
|
||
detail.payStatus !== undefined && detail.payStatus !== null
|
||
? detail.payStatus
|
||
: detail.paymentStatus
|
||
)
|
||
return Number.isFinite(numericStatus) ? numericStatus : null
|
||
},
|
||
isCurrentApplicationPaid() {
|
||
if (!this.isApplicationStep || this.isViewingApplicationStep) {
|
||
return false
|
||
}
|
||
const payStatus = this.currentApplicationPayStatus
|
||
if (payStatus === 10) {
|
||
return true
|
||
}
|
||
const detail = this.currentApplicationDetail || {}
|
||
const payStatusText = String(detail.payStatusDesc || detail.paymentStatusDesc || '').trim()
|
||
return /已支付|paid/i.test(payStatusText)
|
||
},
|
||
isApplicationPendingPayment() {
|
||
if (!this.isApplicationStep) {
|
||
return false
|
||
}
|
||
if (this.isViewingApplicationStep) {
|
||
return false
|
||
}
|
||
if (this.isCurrentApplicationPaid) {
|
||
return false
|
||
}
|
||
if (this.isCurrentApplicationWaitingRoomAssignment) {
|
||
return false
|
||
}
|
||
return this.hasCurrentApplicationPendingDepositBill
|
||
},
|
||
hasCurrentApplicationPendingDepositBill() {
|
||
const detail = this.currentApplicationDetail || {}
|
||
if (detail.pendingDepositBillQueried) {
|
||
return !!String(this.getBillNo(detail.pendingDepositBill || {}) || '').trim()
|
||
}
|
||
const flow = this.currentFlow || {}
|
||
if (this.currentReservationRefNo) {
|
||
return true
|
||
}
|
||
return !!(flow.pendingPaymentOrder || flow.pendingDepositBill)
|
||
},
|
||
isCurrentApplicationWaitingRoomAssignment() {
|
||
const detail = this.currentApplicationDetail || {}
|
||
return this.isApplicationStep
|
||
&& !this.isViewingApplicationStep
|
||
&& Number(detail.status) === 0
|
||
&& Number(detail.payStatus) !== 10
|
||
&& !!String(detail.detailCode || '').trim()
|
||
&& !!detail.pendingDepositBillQueried
|
||
&& !this.hasCurrentApplicationPendingDepositBill
|
||
},
|
||
isViewingApplicationStep() {
|
||
return this.isApplicationStep && !!(this.currentApplicationDetail && this.currentApplicationDetail.isViewingApplication)
|
||
},
|
||
canCancelCurrentApplication() {
|
||
if (!this.currentReservationRefNo || !this.isApplicationStep) {
|
||
return false
|
||
}
|
||
return this.isViewingApplicationStep || this.isApplicationPendingPayment
|
||
},
|
||
isInspectionStep() {
|
||
return this.currentReservationDisplayStep === 2
|
||
},
|
||
isReservationInspectionPendingReview() {
|
||
if (!this.isInspectionStep) {
|
||
return false
|
||
}
|
||
const detail = this.reservationInspectionDetail || {}
|
||
return Number(detail.status) === 2
|
||
},
|
||
isBillStep() {
|
||
return this.currentReservationDisplayStep === 4
|
||
},
|
||
isContractSigningStep() {
|
||
if (!this.hasCheckedIn && this.pendingContracts.length > 0) {
|
||
return true
|
||
}
|
||
return this.currentReservationDisplayStep === 3
|
||
},
|
||
isCurrentContractAwaitingReview() {
|
||
if (!this.isContractSigningStep) {
|
||
return false
|
||
}
|
||
const detail = this.reservationContractDetail || {}
|
||
const queryStatus = Number(detail.queryStatus)
|
||
const approvalStatus = Number(detail.approvalStatus)
|
||
const hasSigned = !!String(detail.signTime || detail.signImageUrl || '').trim()
|
||
return queryStatus === 1 || (hasSigned && approvalStatus === 0)
|
||
},
|
||
hasSignedContract() {
|
||
if (!this.hasCheckedIn && this.currentBooking && this.currentBooking.contractStatus === 1) {
|
||
return true
|
||
}
|
||
return false
|
||
},
|
||
currentStepIcon() {
|
||
const step = Number(this.currentReservationDisplayStep || 0)
|
||
const iconMap = {
|
||
1: 'tn-icon-safe',
|
||
2: 'tn-icon-search',
|
||
3: 'tn-icon-order-fill',
|
||
4: 'tn-icon-order-fill',
|
||
5: 'tn-icon-home-smile'
|
||
}
|
||
return iconMap[step] || 'tn-icon-home-smile'
|
||
},
|
||
currentStepLabel() {
|
||
const currentNode = this.flowNodes.find(item => item.current)
|
||
return currentNode ? currentNode.label : this.$tr('流程进度')
|
||
},
|
||
applicationTypeText() {
|
||
const detail = this.currentApplicationDetail || {}
|
||
return pickReadableValue(
|
||
detail.typeDesc,
|
||
detail.applicationTypeDesc,
|
||
detail.businessTypeDesc,
|
||
this.isViewingApplicationStep ? this.$tr('预约看房') : this.$tr('入住申请')
|
||
) || this.$tr('入住申请')
|
||
},
|
||
applicationHeroImages() {
|
||
return extractGalleryImages(
|
||
this.currentApplicationDetail,
|
||
this.currentBooking,
|
||
this.currentFlow,
|
||
this.myRoomData
|
||
)
|
||
},
|
||
currentFlowOrderDetail() {
|
||
const roomData = this.myRoomData || {}
|
||
const currentOrderNo = String(this.getCurrentOrderNo() || '').trim()
|
||
const roomOrderNo = String(roomData.orderNo || '').trim()
|
||
if (!currentOrderNo || !roomOrderNo || currentOrderNo !== roomOrderNo) {
|
||
return null
|
||
}
|
||
return roomData
|
||
},
|
||
applicationHeroTitle() {
|
||
const orderDetail = this.currentFlowOrderDetail || {}
|
||
const orderLocationTitle = buildFlowLocationTitle(orderDetail)
|
||
if (orderLocationTitle) {
|
||
return orderLocationTitle
|
||
}
|
||
const detail = this.currentApplicationDetail || {}
|
||
const booking = this.currentBooking || {}
|
||
if (this.language === 'en') {
|
||
return pickReadableValue(
|
||
detail.intendedRoomTypeNameEn,
|
||
detail.roomTypeNameEn,
|
||
booking.roomTypeNameEn,
|
||
booking.roomNameEn,
|
||
booking.titleEn,
|
||
detail.intendedRoomTypeName,
|
||
detail.roomTypeName,
|
||
booking.roomTypeName,
|
||
booking.roomName,
|
||
booking.titleZh
|
||
) || this.$tr('申请房型待确认')
|
||
}
|
||
return pickReadableValue(
|
||
detail.intendedRoomTypeName,
|
||
detail.roomTypeName,
|
||
booking.roomTypeName,
|
||
booking.roomName,
|
||
booking.titleZh,
|
||
booking.titleEn
|
||
) || this.$tr('申请房型待确认')
|
||
},
|
||
applicationHeroArea() {
|
||
const orderDetail = this.currentFlowOrderDetail || {}
|
||
const orderArea = this.language === 'en'
|
||
? pickReadableValue(
|
||
orderDetail.areaNameEn,
|
||
orderDetail.campusNameEn,
|
||
this.localizeStayDisplayText(orderDetail.areaName, { apartment: true }),
|
||
this.localizeStayDisplayText(orderDetail.campusName, { apartment: true })
|
||
)
|
||
: pickReadableValue(
|
||
orderDetail.areaName,
|
||
orderDetail.campusName,
|
||
orderDetail.areaNameEn,
|
||
orderDetail.campusNameEn
|
||
)
|
||
const orderRoomTypeName = this.language === 'en'
|
||
? pickReadableValue(
|
||
orderDetail.roomTypeNameEn,
|
||
orderDetail.titleEn,
|
||
orderDetail.roomTypeName,
|
||
orderDetail.roomTypeText,
|
||
orderDetail.titleZh
|
||
)
|
||
: pickReadableValue(
|
||
orderDetail.roomTypeName,
|
||
orderDetail.roomTypeText,
|
||
orderDetail.titleZh,
|
||
orderDetail.roomTypeNameEn,
|
||
orderDetail.titleEn
|
||
)
|
||
const orderAreaSummary = [orderArea, orderRoomTypeName].filter(Boolean).join(' · ')
|
||
if (orderAreaSummary) {
|
||
return orderAreaSummary
|
||
}
|
||
const detail = this.currentApplicationDetail || {}
|
||
const booking = this.currentBooking || {}
|
||
if (this.language === 'en') {
|
||
return pickReadableValue(
|
||
detail.intendedAreaNameEn,
|
||
detail.areaNameEn,
|
||
booking.areaNameEn,
|
||
booking.campusNameEn,
|
||
this.myRoomData && this.myRoomData.areaNameEn,
|
||
this.localizeStayDisplayText(detail.intendedAreaName, { apartment: true }),
|
||
this.localizeStayDisplayText(detail.areaName, { apartment: true }),
|
||
this.localizeStayDisplayText(booking.areaName, { apartment: true }),
|
||
this.localizeStayDisplayText(booking.campusName, { apartment: true }),
|
||
this.localizeStayDisplayText(this.myRoomData && this.myRoomData.areaName, { apartment: true })
|
||
) || this.$tr('公寓待确认')
|
||
}
|
||
return pickReadableValue(
|
||
detail.intendedAreaName,
|
||
detail.areaName,
|
||
booking.areaName,
|
||
booking.campusName,
|
||
this.myRoomData && this.myRoomData.areaName
|
||
) || this.$tr('公寓待确认')
|
||
},
|
||
applicationHeroTimelineLabel() {
|
||
return this.isViewingApplicationStep ? this.$tr('预约时间') : this.$tr('预计入住')
|
||
},
|
||
applicationHeroMeta() {
|
||
const detail = this.currentApplicationDetail || {}
|
||
const timelineValue = this.isViewingApplicationStep
|
||
? (detail.appointmentViewingTime ? this.formatDateTime(detail.appointmentViewingTime) : '')
|
||
: ([formatStayDate(detail.start), formatStayDate(detail.end)].filter(Boolean).join(' - ') || '')
|
||
const floorValue = pickReadableValue(detail.intendedFloorLevelDesc, detail.floorLevelDesc, '')
|
||
|
||
return [
|
||
{
|
||
label: this.applicationHeroTimelineLabel,
|
||
value: this.isViewingApplicationStep ? '' : timelineValue
|
||
},
|
||
{
|
||
label: this.$tr('意向楼层'),
|
||
value: floorValue
|
||
}
|
||
].filter(item => item.value && item.value !== '--' && item.value !== this.$tr('未知'))
|
||
},
|
||
currentStageMetaLabel() {
|
||
return this.isViewingApplicationStep ? this.$tr('预约时间') : this.$tr('预计入住')
|
||
},
|
||
currentStageMetaText() {
|
||
const detail = this.currentApplicationDetail || {}
|
||
if (this.isViewingApplicationStep) {
|
||
return detail.appointmentViewingTime ? this.formatDateTime(detail.appointmentViewingTime) : ''
|
||
}
|
||
return ''
|
||
},
|
||
canViewBookingDetail() {
|
||
return !!(this.currentBooking && this.currentBooking.orderNo)
|
||
},
|
||
workflowActionText() {
|
||
if (this.isPendingStatusRefreshing) {
|
||
return this.$tr('刷新中...')
|
||
}
|
||
if (this.isReservationInspectionPendingReview) {
|
||
return ''
|
||
}
|
||
if (this.isCurrentStepPendingDispatch) {
|
||
return this.$tr('刷新状态')
|
||
}
|
||
if (this.isContractSigningStep && !this.hasSignedContract && !this.isCurrentContractAwaitingReview) {
|
||
return this.$tr('去签约')
|
||
}
|
||
if (this.isBillStep) {
|
||
return this.$tr('去支付')
|
||
}
|
||
if (this.isInspectionStep) {
|
||
return this.$tr('去验房')
|
||
}
|
||
if (this.isApplicationPendingPayment) {
|
||
return this.$tr('去支付')
|
||
}
|
||
if (this.isViewingApplicationStep) {
|
||
return this.$tr('刷新状态')
|
||
}
|
||
return this.$tr('刷新状态')
|
||
},
|
||
cancelApplicationActionText() {
|
||
return this.isCancelingApplication
|
||
? this.$tr('取消中...')
|
||
: this.$tr('取消申请')
|
||
},
|
||
flowNodes() {
|
||
const currentStep = Number(this.currentReservationDisplayStep || 0)
|
||
const labels = [
|
||
{ step: 1, label: this.$tr('申请单') },
|
||
{ step: 2, label: this.$tr('验房') },
|
||
{ step: 3, label: this.$tr('合同') },
|
||
{ step: 4, label: this.$tr('账单') },
|
||
{ step: 5, label: this.$tr('入住') }
|
||
]
|
||
return labels.map(item => ({
|
||
...item,
|
||
completed: currentStep > item.step,
|
||
current: currentStep === item.step
|
||
}))
|
||
},
|
||
currentFlowNode() {
|
||
return this.flowNodes.find(item => item.current)
|
||
|| this.flowNodes.find(item => !item.completed)
|
||
|| this.flowNodes[this.flowNodes.length - 1]
|
||
|| {
|
||
step: 0,
|
||
label: this.$tr('流程进度')
|
||
}
|
||
},
|
||
flowProgressText() {
|
||
const total = this.flowNodes.length
|
||
const currentStep = Number(this.currentFlowNode && this.currentFlowNode.step ? this.currentFlowNode.step : 0)
|
||
if (!total || !currentStep) {
|
||
return '00/00'
|
||
}
|
||
return `${String(currentStep).padStart(2, '0')}/${String(total).padStart(2, '0')}`
|
||
},
|
||
nextFlowLabel() {
|
||
const currentStep = Number(this.currentFlowNode && this.currentFlowNode.step ? this.currentFlowNode.step : 0)
|
||
if (!currentStep) {
|
||
return ''
|
||
}
|
||
const nextNode = this.flowNodes.find(item => item.step === currentStep + 1)
|
||
return nextNode ? nextNode.label : ''
|
||
},
|
||
pendingStageTitle() {
|
||
if (this.currentReservationRawStep === 5 && !this.isCurrentStepPendingDispatch) {
|
||
return this.$tr('合同已签署,等待办理入住')
|
||
}
|
||
if (this.isCurrentStepPendingDispatch) {
|
||
return `${this.$tr('等待管家发出')}${this.pendingDispatchTargetLabel}`
|
||
}
|
||
if (this.isReservationInspectionPendingReview) {
|
||
return this.$tr('验房已提交,等待管家审核')
|
||
}
|
||
if (this.isCurrentContractAwaitingReview) {
|
||
return this.$tr('合同已签署,等待管家审核')
|
||
}
|
||
if (this.isContractSigningStep) {
|
||
return this.$tr('请尽快签订合同')
|
||
}
|
||
if (this.isBillStep) {
|
||
return this.$tr('账单已生成,请尽快支付')
|
||
}
|
||
if (this.isInspectionStep) {
|
||
return this.$tr('请预约并完成验房')
|
||
}
|
||
if (this.isApplicationStep) {
|
||
if (this.isViewingApplicationStep) {
|
||
return this.$tr('预约看房已提交,等待确认')
|
||
}
|
||
if (this.isCurrentApplicationWaitingRoomAssignment) {
|
||
return this.$tr('等待管家排房')
|
||
}
|
||
if (this.isApplicationPendingPayment) {
|
||
return this.$tr('请支付定金')
|
||
}
|
||
if (this.isCurrentApplicationPaid) {
|
||
return this.$tr('定金已支付,等待管家处理')
|
||
}
|
||
return this.$tr('申请单已提交,等待审核')
|
||
}
|
||
return this.$tr('您的预订正在审核中')
|
||
},
|
||
pendingStageDesc() {
|
||
if (this.currentReservationRawStep === 5 && !this.isCurrentStepPendingDispatch) {
|
||
return this.$tr('您的住宿合同已经完成签署,管家会尽快为您安排办理入住流程。')
|
||
}
|
||
if (this.isCurrentStepPendingDispatch) {
|
||
return `${this.$tr('当前流程正在等待管家发出')}${this.pendingDispatchTargetLabel}${this.$tr(',下发后即可继续下一步操作。')}`
|
||
}
|
||
if (this.isReservationInspectionPendingReview) {
|
||
const detail = this.reservationInspectionDetail || {}
|
||
return String(detail.statusDesc || detail.statusDes || '').trim()
|
||
|| this.$tr('您的验房单已提交成功,当前正在等待管家审核,审核通过后将进入合同签署环节。')
|
||
}
|
||
if (this.isCurrentContractAwaitingReview) {
|
||
const detail = this.reservationContractDetail || {}
|
||
return String(detail.queryStatusDesc || detail.approvalStatusDesc || detail.statusDesc || '').trim()
|
||
|| this.$tr('您的合同已经签署完成,当前正在等待管家审核,审核通过后将进入账单支付环节。')
|
||
}
|
||
if (this.isContractSigningStep) {
|
||
return this.$tr('系统已生成待签合同,当前流程已进入签订合同阶段,完成签约后即可进入账单支付环节。')
|
||
}
|
||
if (this.isBillStep) {
|
||
return this.$tr('当前流程已进入账单支付阶段,完成支付后即可进入入住办理环节。')
|
||
}
|
||
if (this.isInspectionStep) {
|
||
return this.$tr('当前流程已进入验房阶段,请按预约时间完成房间验收。')
|
||
}
|
||
if (this.isApplicationStep) {
|
||
if (this.isViewingApplicationStep) {
|
||
return this.$tr('您的预约看房申请已提交,请等待管家与您确认看房安排。')
|
||
}
|
||
if (this.isCurrentApplicationWaitingRoomAssignment) {
|
||
return this.$tr('当前暂无待支付定金账单,管家正在为您排房,请耐心等待。')
|
||
}
|
||
if (this.isApplicationPendingPayment) {
|
||
return this.$tr('您的入住申请已提交,请先支付定金以确认预订。')
|
||
}
|
||
if (this.isCurrentApplicationPaid) {
|
||
return this.$tr('您的定金已支付成功,当前申请正在等待管家审核和后续处理。')
|
||
}
|
||
return this.$tr('您的申请单已提交,管家审核通过后将进入验房环节。')
|
||
}
|
||
return 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.contractStartTime || this.myRoomData.startDate || this.myRoomData.start || (this.myRoomData.checkInPeriod && this.myRoomData.checkInPeriod.start)
|
||
const end = this.myRoomData.contractEndTime || 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.loadStayApplicationConfig()
|
||
this.loadFeaturedServiceConfig()
|
||
this.loadDepositOtherPaymentConfig()
|
||
this.checkLocalStatus()
|
||
this.loadUnpaidBills()
|
||
},
|
||
methods: {
|
||
createDefaultStayActions() {
|
||
return DEFAULT_STAY_ACTIONS.map(item => ({ ...item }))
|
||
},
|
||
createDefaultFeaturedServices() {
|
||
return DEFAULT_FEATURED_SERVICES.map(item => ({ ...item }))
|
||
},
|
||
applyStayActionConfig(actionList = [], useFallback = true) {
|
||
const hasConfigRows = Array.isArray(actionList) && actionList.length
|
||
const nextList = hasConfigRows || !useFallback
|
||
? (Array.isArray(actionList) ? actionList : [])
|
||
: this.createDefaultStayActions()
|
||
this.stayActions = nextList
|
||
this.stayApplicationsEnabled = nextList.length > 0
|
||
},
|
||
applyFeaturedServiceConfig(serviceList = [], useFallback = true) {
|
||
const hasConfigRows = Array.isArray(serviceList) && serviceList.length
|
||
this.services = hasConfigRows || !useFallback
|
||
? (Array.isArray(serviceList) ? serviceList : [])
|
||
: this.createDefaultFeaturedServices()
|
||
},
|
||
loadStayApplicationConfig() {
|
||
return sysdictPage({
|
||
Type: MOBILE_STAY_APPLICATION_CONFIG_TYPE,
|
||
PageIndex: 1,
|
||
PageSize: 100
|
||
}, this.language)
|
||
.then((response) => {
|
||
const rows = normalizeStayDictRows(response)
|
||
const actions = normalizeStayActionConfigRows(rows)
|
||
this.applyStayActionConfig(actions, rows.length === 0)
|
||
})
|
||
.catch((error) => {
|
||
console.error('load stay application config failed', error)
|
||
this.applyStayActionConfig()
|
||
})
|
||
},
|
||
loadFeaturedServiceConfig() {
|
||
return sysdictPage({
|
||
Type: MOBILE_FEATURED_SERVICE_CONFIG_TYPE,
|
||
PageIndex: 1,
|
||
PageSize: 100
|
||
}, this.language)
|
||
.then((response) => {
|
||
const rows = normalizeStayDictRows(response)
|
||
const services = normalizeFeaturedServiceConfigRows(rows)
|
||
this.applyFeaturedServiceConfig(services, rows.length === 0)
|
||
})
|
||
.catch((error) => {
|
||
console.error('load featured service config failed', error)
|
||
this.applyFeaturedServiceConfig()
|
||
})
|
||
},
|
||
loadDepositOtherPaymentConfig() {
|
||
this.depositOtherPaymentConfigLoading = true
|
||
return loadDepositOtherPaymentConfig(this.language)
|
||
.then((config) => {
|
||
this.depositOtherPaymentConfig = config || createDefaultDepositOtherPaymentConfig()
|
||
})
|
||
.catch((error) => {
|
||
console.error('load deposit other payment config failed', error)
|
||
this.depositOtherPaymentConfig = createDefaultDepositOtherPaymentConfig()
|
||
})
|
||
.finally(() => {
|
||
this.depositOtherPaymentConfigLoading = false
|
||
})
|
||
},
|
||
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
|
||
},
|
||
getFeaturedServiceImage(item = {}) {
|
||
return addFilePrefix(item.image || '')
|
||
},
|
||
localizeStayDisplayText(value, options = {}) {
|
||
const text = String(value || '').trim()
|
||
if (!text) {
|
||
return ''
|
||
}
|
||
if (text === '未知') {
|
||
return this.$tr('未知')
|
||
}
|
||
if (options.apartment) {
|
||
return localizeApartmentName(text, this.language)
|
||
}
|
||
return text
|
||
},
|
||
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)
|
||
},
|
||
getStorePendingContracts() {
|
||
const list = this.$store && this.$store.state && this.$store.state.vuex_pending_contracts
|
||
return Array.isArray(list) ? list : []
|
||
},
|
||
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
|
||
},
|
||
handleApplicationHeroChange(event) {
|
||
const detail = event && event.detail ? event.detail : {}
|
||
this.applicationHeroIndex = Number(detail.current || 0)
|
||
},
|
||
getCurrentOrderNo() {
|
||
const storeCurrentOrder = this.getStoreCurrentOrder()
|
||
if (storeCurrentOrder && storeCurrentOrder.orderNo) {
|
||
return String(storeCurrentOrder.orderNo).trim()
|
||
}
|
||
if (Number(this.currentReservationRawStep || 0) === 5) {
|
||
const reservationOrderNo = String(this.currentReservationRefNo || '').trim()
|
||
if (reservationOrderNo) {
|
||
return reservationOrderNo
|
||
}
|
||
}
|
||
const bookingOrderNo = String((this.currentBooking && this.currentBooking.orderNo) || '').trim()
|
||
if (bookingOrderNo) {
|
||
return bookingOrderNo
|
||
}
|
||
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
|
||
)
|
||
},
|
||
getCurrentLockSuiteCode() {
|
||
const roomData = this.myRoomData || {}
|
||
return pickFirstFilledValue(
|
||
roomData.suiteCode,
|
||
roomData.suiteNo,
|
||
roomData.setCode,
|
||
roomData.unitCode,
|
||
roomData.parentRoomCode
|
||
)
|
||
},
|
||
getCurrentLockSuiteName() {
|
||
const roomData = this.myRoomData || {}
|
||
return pickReadableValue(
|
||
roomData.suiteName,
|
||
roomData.unitName,
|
||
roomData.suiteNo
|
||
)
|
||
},
|
||
getCurrentUserCode() {
|
||
const storeUser = this.getStoreUser()
|
||
const session = getSessionSnapshot()
|
||
return pickFirstFilledValue(
|
||
storeUser.userCode,
|
||
storeUser.code,
|
||
session && session.resolvedUser && session.resolvedUser.userCode,
|
||
session && session.resolvedUser && session.resolvedUser.code,
|
||
session && session.currentUser && session.currentUser.userCode,
|
||
session && session.currentUser && session.currentUser.code
|
||
)
|
||
},
|
||
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.loadAreaManager()
|
||
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
|
||
},
|
||
// 滚动事件处理:顶部导航栏加背景色
|
||
handleScroll(e) {
|
||
const scrollTop = (e && e.detail && e.detail.scrollTop) || 0
|
||
if (scrollTop > 20) {
|
||
this.navBarBackgroundColor = '#F8FAFF'
|
||
} else {
|
||
this.navBarBackgroundColor = 'transparent'
|
||
}
|
||
},
|
||
fetchCurrentOrderDetail() {
|
||
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.reservationContractDetail = null
|
||
this.reservationInspectionDetail = null
|
||
this.myRoomData = null
|
||
this.dormitoryManagers = []
|
||
this.counselor = null
|
||
return
|
||
}
|
||
|
||
this.currentBooking = storeUser.currentBooking || session.currentBooking || null
|
||
this.currentFlow = storeUser.currentFlow || session.currentFlow || null
|
||
this.currentReservationStep = storeUser.currentReservationStep || session.currentReservationStep || (this.currentBooking && this.currentBooking.step) || null
|
||
this.currentReservationRefNo = storeUser.currentReservationRefNo || session.currentReservationRefNo || (this.currentBooking && this.currentBooking.refNo) || ''
|
||
this.syncCurrentApplicationDetail()
|
||
this.ensureCurrentContractDetail()
|
||
this.ensureCurrentInspectionDetail()
|
||
const currentFlowOrderNo = String(
|
||
(this.currentBooking && this.currentBooking.orderNo)
|
||
|| (this.currentFlow && this.currentFlow.orderNo)
|
||
|| (Number(this.currentReservationRawStep || 0) === 5 ? this.currentReservationRefNo : '')
|
||
|| ''
|
||
).trim()
|
||
const currentOrderStatus = Number(getOrderStatusCode(storeCurrentOrder || {}))
|
||
const shouldTreatAsCheckedIn = currentOrderStatus === 10
|
||
if (shouldTreatAsCheckedIn || currentFlowOrderNo) {
|
||
this.hasCheckedIn = shouldTreatAsCheckedIn
|
||
this.myRoomData = buildRoomDataFromOrder(storeCurrentOrder) || buildRoomDataFromOrder(session.myRoomData) || session.myRoomData || null
|
||
this.fetchCurrentOrderDetail()
|
||
if (shouldTreatAsCheckedIn) {
|
||
this.loadAreaManager()
|
||
this.loadSmartDevices()
|
||
} else {
|
||
this.areaManager = null
|
||
this.areaManagers = []
|
||
this.resetSmartDevicesState()
|
||
}
|
||
} else {
|
||
this.hasCheckedIn = false
|
||
this.myRoomData = null
|
||
this.areaManager = null
|
||
this.areaManagers = []
|
||
this.resetSmartDevicesState()
|
||
}
|
||
},
|
||
canAutoRefreshReservationFlow() {
|
||
if (this.hasCheckedIn || !this.currentBooking) {
|
||
return false
|
||
}
|
||
if (this.isPendingStatusRefreshing || this.isCancelingApplication) {
|
||
return false
|
||
}
|
||
if (this.showDepositNotice || this.isPaymentActionRunning) {
|
||
return false
|
||
}
|
||
if (this.paymentNoticeSourceLoading || this.paymentNoticeBillLoading) {
|
||
return false
|
||
}
|
||
return true
|
||
},
|
||
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.isCurrentStepPendingDispatch) {
|
||
this.refreshPendingStatus()
|
||
return
|
||
}
|
||
if (this.isContractSigningStep && !this.hasSignedContract) {
|
||
this.goSignContract()
|
||
return
|
||
}
|
||
if (this.isBillStep) {
|
||
this.goPayBill()
|
||
return
|
||
}
|
||
if (this.isInspectionStep) {
|
||
this.goRoomInspection()
|
||
return
|
||
}
|
||
if (this.isApplicationPendingPayment) {
|
||
this.goPayDeposit()
|
||
return
|
||
}
|
||
this.refreshPendingStatus()
|
||
},
|
||
getCancelApplicationContent() {
|
||
return this.isViewingApplicationStep
|
||
? this.$tr('取消后将关闭当前预约看房申请,是否继续?')
|
||
: this.$tr('取消后将关闭当前申请单,是否继续?')
|
||
},
|
||
confirmCancelApplication() {
|
||
return new Promise((resolve) => {
|
||
uni.showModal({
|
||
title: this.$tr('取消申请'),
|
||
content: this.getCancelApplicationContent(),
|
||
confirmColor: '#ef4444',
|
||
success: (res) => resolve(!!(res && res.confirm)),
|
||
fail: () => resolve(false)
|
||
})
|
||
})
|
||
},
|
||
handleCancelApplication() {
|
||
if (!this.canCancelCurrentApplication || this.isCancelingApplication) {
|
||
return
|
||
}
|
||
this.confirmCancelApplication()
|
||
.then((confirmed) => {
|
||
if (!confirmed) {
|
||
return
|
||
}
|
||
this.isCancelingApplication = true
|
||
return userDetailCancel({
|
||
detailCode: this.currentReservationRefNo
|
||
}, this.language)
|
||
.then(() => {
|
||
if (this.currentApplicationDetail && typeof this.currentApplicationDetail === 'object') {
|
||
this.currentApplicationDetail = {
|
||
...this.currentApplicationDetail,
|
||
status: 99,
|
||
statusDesc: this.$tr('已结束')
|
||
}
|
||
}
|
||
uni.showToast({
|
||
title: this.$tr('申请已取消'),
|
||
icon: 'success'
|
||
})
|
||
this.$emit('refresh-shell-state')
|
||
})
|
||
})
|
||
.catch((error) => {
|
||
uni.showToast({
|
||
title: (error && (error.message || error.msg)) || this.$tr('取消申请失败'),
|
||
icon: 'none'
|
||
})
|
||
})
|
||
.finally(() => {
|
||
this.isCancelingApplication = false
|
||
})
|
||
},
|
||
goPayDeposit() {
|
||
const refNo = this.currentReservationRefNo
|
||
if (!refNo) {
|
||
uni.showToast({
|
||
title: this.$tr('申请单编号缺失'),
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
this.openPaymentMethodSelector({
|
||
referenceTypeNo: refNo,
|
||
description: this.$tr('支付定金'),
|
||
mode: 'deposit',
|
||
sourceItem: this.currentBooking || this.currentApplicationDetail || this.currentFlow || {}
|
||
})
|
||
},
|
||
getPaymentMethodSheetOptions() {
|
||
return [
|
||
{
|
||
text: this.language === 'en' ? 'WeChat Pay' : '微信支付',
|
||
subText: this.language === 'en'
|
||
? 'The system will continue by opening WeChat for payment.'
|
||
: '系统将继续拉起微信完成支付'
|
||
},
|
||
{
|
||
text: this.language === 'en' ? 'Other Payment Methods' : '其他支付方式',
|
||
subText: this.language === 'en'
|
||
? 'View the corporate bank account details when WeChat Pay is unavailable.'
|
||
: '无法使用微信支付时查看对公账户信息'
|
||
}
|
||
]
|
||
},
|
||
getPaymentMethodSheetTipText() {
|
||
return this.language === 'en' ? 'Please select a payment method' : '请选择支付方式'
|
||
},
|
||
openPaymentMethodSelector(context = {}) {
|
||
this.pendingPaymentMethodContext = context || null
|
||
this.showPaymentMethodSheet = true
|
||
},
|
||
handlePaymentMethodSheetClick(index) {
|
||
const context = this.pendingPaymentMethodContext || {}
|
||
this.pendingPaymentMethodContext = null
|
||
if (Number(index) === 0) {
|
||
this.openDepositNoticeForPayment(context)
|
||
return
|
||
}
|
||
if (Number(index) === 1) {
|
||
this.showOfflinePaymentNotice = true
|
||
this.loadDepositOtherPaymentConfig()
|
||
}
|
||
},
|
||
closeOfflinePaymentNotice() {
|
||
this.showOfflinePaymentNotice = false
|
||
},
|
||
getPaymentContextMode(context = {}) {
|
||
const explicitMode = String(context.mode || '').trim()
|
||
if (explicitMode) {
|
||
return explicitMode
|
||
}
|
||
return context.billNo ? 'bill' : 'deposit'
|
||
},
|
||
isBillPaymentNotice() {
|
||
return this.getPaymentContextMode(this.pendingDepositPaymentContext || {}) === 'bill'
|
||
},
|
||
getPaymentNoticeTitle() {
|
||
return this.$tr('确认支付信息')
|
||
},
|
||
getPaymentNoticeConfirmText() {
|
||
return this.$tr('确认并支付')
|
||
},
|
||
getPaymentNoticeBillNo() {
|
||
const context = this.pendingDepositPaymentContext || {}
|
||
const detail = this.paymentNoticeBillDetail || {}
|
||
return String(this.getBillNo(detail) || context.billNo || '').trim()
|
||
},
|
||
getPaymentNoticeBillTitle() {
|
||
const detail = this.paymentNoticeBillDetail || {}
|
||
return String(
|
||
detail.description
|
||
|| detail.paymentFeeType
|
||
|| detail.remark
|
||
|| detail.createTypeDesc
|
||
|| detail.referenceTypeDesc
|
||
|| detail.billName
|
||
|| detail.name
|
||
|| (this.pendingDepositPaymentContext && this.pendingDepositPaymentContext.description)
|
||
|| this.$tr('账单支付')
|
||
).trim()
|
||
},
|
||
getPaymentNoticeBillAmount() {
|
||
const detail = this.paymentNoticeBillDetail || {}
|
||
const amount = detail.remainingAmount
|
||
if (amount !== '' && amount !== null && amount !== undefined) {
|
||
return amount
|
||
}
|
||
const source = this.paymentNoticeSourceItem || {}
|
||
const sourceAmount = source.remainingAmount
|
||
if (sourceAmount !== '' && sourceAmount !== null && sourceAmount !== undefined) {
|
||
return sourceAmount
|
||
}
|
||
const detailTotal = detail.totalCharge !== undefined && detail.totalCharge !== null && detail.totalCharge !== ''
|
||
? detail.totalCharge
|
||
: (detail.totalAmount !== undefined && detail.totalAmount !== null && detail.totalAmount !== '' ? detail.totalAmount : detail.amount)
|
||
if (detailTotal !== '' && detailTotal !== null && detailTotal !== undefined) {
|
||
return detailTotal
|
||
}
|
||
const sourceTotal = source.totalCharge !== undefined && source.totalCharge !== null && source.totalCharge !== ''
|
||
? source.totalCharge
|
||
: (source.totalAmount !== undefined && source.totalAmount !== null && source.totalAmount !== '' ? source.totalAmount : source.amount)
|
||
if (sourceTotal !== '' && sourceTotal !== null && sourceTotal !== undefined) {
|
||
return sourceTotal
|
||
}
|
||
return ''
|
||
},
|
||
getPaymentNoticeBillAmountDisplay() {
|
||
const amount = this.getPaymentNoticeBillAmount()
|
||
if (amount === '' || amount === null || amount === undefined) {
|
||
return '--'
|
||
}
|
||
return `¥${this.formatMoney(amount)}`
|
||
},
|
||
buildPaymentNoticePayMethod(source = {}) {
|
||
const pricePlanDetail = source && source.pricePlanDetail && typeof source.pricePlanDetail === 'object'
|
||
? source.pricePlanDetail
|
||
: {}
|
||
const payMethodText = String(
|
||
source.depositPrepayDesc
|
||
|| source.depositAndPrepayDesc
|
||
|| source.depositPrepayText
|
||
|| source.payMethod
|
||
|| source.payMethodDesc
|
||
|| source.paymentMethod
|
||
|| source.paymentMethodDesc
|
||
|| pricePlanDetail.payMethod
|
||
|| ''
|
||
).trim()
|
||
if (payMethodText) {
|
||
return localizePayMethodText(payMethodText, this.language)
|
||
}
|
||
const deposit = pricePlanDetail.deposit
|
||
const prepay = pricePlanDetail.prepay
|
||
if (deposit !== '' && deposit !== null && deposit !== undefined && prepay !== '' && prepay !== null && prepay !== undefined) {
|
||
return localizePayMethodText(`押${deposit}付${prepay}`, this.language)
|
||
}
|
||
return ''
|
||
},
|
||
getPaymentNoticeDetailRows() {
|
||
const source = this.paymentNoticeSourceItem || {}
|
||
const formatShortDate = (value) => {
|
||
if (!value) {
|
||
return ''
|
||
}
|
||
return String(value).replace('T', ' ').slice(0, 10)
|
||
}
|
||
const residentName = String(
|
||
source.userName
|
||
|| source.name
|
||
|| ''
|
||
).trim()
|
||
const areaName = this.language === 'en'
|
||
? pickReadableValue(
|
||
source.areaTextEn,
|
||
source.intendedAreaNameEn,
|
||
source.areaNameEn,
|
||
source.campusNameEn,
|
||
this.currentApplicationDetail && this.currentApplicationDetail.areaNameEn,
|
||
this.localizeStayDisplayText(source.areaText, { apartment: true }),
|
||
this.localizeStayDisplayText(source.intendedAreaName, { apartment: true }),
|
||
this.localizeStayDisplayText(source.areaName, { apartment: true }),
|
||
this.localizeStayDisplayText(source.campusName, { apartment: true }),
|
||
this.localizeStayDisplayText(this.currentApplicationDetail && this.currentApplicationDetail.areaName, { apartment: true })
|
||
)
|
||
: String(
|
||
source.areaText
|
||
|| source.intendedAreaName
|
||
|| source.areaName
|
||
|| source.campusName
|
||
|| (this.currentApplicationDetail && this.currentApplicationDetail.areaName)
|
||
|| ''
|
||
).trim()
|
||
const roomTypeName = this.language === 'en'
|
||
? pickReadableValue(
|
||
source.roomTypeTextEn,
|
||
source.roomTypeNameEn,
|
||
source.intendedRoomTypeNameEn,
|
||
source.roomNameEn,
|
||
source.titleEn,
|
||
this.currentApplicationDetail && this.currentApplicationDetail.intendedRoomTypeNameEn,
|
||
source.roomTypeText,
|
||
source.roomTypeName,
|
||
source.intendedRoomTypeName,
|
||
source.roomTypeCode,
|
||
source.roomName,
|
||
source.titleZh,
|
||
this.currentApplicationDetail && this.currentApplicationDetail.intendedRoomTypeName
|
||
)
|
||
: String(
|
||
source.roomTypeText
|
||
|| source.roomTypeName
|
||
|| source.intendedRoomTypeName
|
||
|| source.roomTypeCode
|
||
|| source.roomName
|
||
|| source.titleZh
|
||
|| source.titleEn
|
||
|| (this.currentApplicationDetail && this.currentApplicationDetail.intendedRoomTypeName)
|
||
|| ''
|
||
).trim()
|
||
const roomNo = String(
|
||
source.roomNoText
|
||
|| source.roomNo
|
||
|| source.roomLabel
|
||
|| ''
|
||
).trim()
|
||
const moveInDate = formatShortDate(
|
||
source.contractStartTime
|
||
|| source.startDateText
|
||
|| source.expectedCheckInTime
|
||
|| source.startDate
|
||
|| source.start
|
||
|| source.checkinDate
|
||
|| (this.currentApplicationDetail && this.currentApplicationDetail.start)
|
||
|| ''
|
||
)
|
||
const leaseStart = formatShortDate(
|
||
source.contractStartTime
|
||
|| source.startDateText
|
||
|| source.expectedCheckInTime
|
||
|| source.startDate
|
||
|| source.start
|
||
|| source.checkinDate
|
||
|| ''
|
||
)
|
||
const leaseEnd = formatShortDate(
|
||
source.contractEndTime
|
||
|| source.endDateText
|
||
|| source.expectedCheckOutTime
|
||
|| source.endDate
|
||
|| source.end
|
||
|| source.checkoutDate
|
||
|| ''
|
||
)
|
||
const leasePeriod = leaseStart && leaseEnd
|
||
? `${leaseStart}${this.$tr(' 至 ')}${leaseEnd}`
|
||
: ''
|
||
const floorLevel = this.localizeStayDisplayText(
|
||
source.intendedFloorLevelDesc
|
||
|| source.floorLevelDesc
|
||
|| ''
|
||
)
|
||
const payMethod = this.buildPaymentNoticePayMethod(source)
|
||
return [
|
||
residentName ? { label: this.$tr('入住人'), value: residentName } : null,
|
||
areaName ? { label: this.$tr('所属公寓'), value: areaName } : null,
|
||
roomTypeName ? { label: this.$tr('房型'), value: roomTypeName } : null,
|
||
roomNo ? { label: this.$tr('房间号'), value: roomNo } : null,
|
||
floorLevel ? { label: this.$tr('意向楼层'), value: floorLevel } : null,
|
||
moveInDate ? { label: this.$tr('预计入住'), value: moveInDate } : null,
|
||
leasePeriod ? { label: this.$tr('租期'), value: leasePeriod } : null,
|
||
payMethod ? { label: this.$tr('押付方式'), value: payMethod } : null,
|
||
].filter(Boolean)
|
||
},
|
||
normalizePaymentNoticePreview(response, orderNo = '') {
|
||
const payload = parseJsonStringMaybe(response && response.data ? response.data : response)
|
||
const normalizeList = (list = []) => list.filter(item => item && typeof item === 'object')
|
||
const normalizedOrderNo = String(orderNo || '').trim()
|
||
let candidates = []
|
||
|
||
if (Array.isArray(payload)) {
|
||
candidates = normalizeList(payload)
|
||
} else if (payload && Array.isArray(payload.data)) {
|
||
candidates = normalizeList(payload.data)
|
||
} else if (payload && payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
|
||
candidates = [payload.data]
|
||
} else if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
||
candidates = [payload]
|
||
}
|
||
|
||
if (!candidates.length) {
|
||
return null
|
||
}
|
||
|
||
return candidates.find(item => String(item.orderNo || '').trim() === normalizedOrderNo) || candidates[0]
|
||
},
|
||
resolvePaymentNoticeOrderNo(context = {}, sourceItem = null) {
|
||
return String(
|
||
context.orderNo
|
||
|| (sourceItem && (sourceItem.orderNo || sourceItem.reservationOrderNo || getReservationOrderNo(sourceItem)))
|
||
|| (this.currentBooking && this.currentBooking.orderNo)
|
||
|| (this.currentFlow && this.currentFlow.orderNo)
|
||
|| ''
|
||
).trim()
|
||
},
|
||
loadPaymentNoticeSourceDetail(orderNo = '', fallbackSource = null) {
|
||
const normalizedOrderNo = String(orderNo || this.resolvePaymentNoticeOrderNo({}, fallbackSource) || '').trim()
|
||
if (!normalizedOrderNo) {
|
||
this.paymentNoticeSourceLoading = false
|
||
this.paymentNoticeSourceItem = fallbackSource || this.paymentNoticeSourceItem || null
|
||
return Promise.resolve(this.paymentNoticeSourceItem)
|
||
}
|
||
|
||
this.paymentNoticeSourceLoading = true
|
||
return orderSearchedDetail(normalizedOrderNo, this.language)
|
||
.then((response) => {
|
||
const detail = normalizeBookingDetail(parseOrderDetailResponse(response), this.language)
|
||
this.paymentNoticeSourceItem = detail
|
||
? {
|
||
...(fallbackSource || {}),
|
||
...detail
|
||
}
|
||
: (fallbackSource || this.paymentNoticeSourceItem || null)
|
||
return this.paymentNoticeSourceItem
|
||
})
|
||
.catch(() => {
|
||
this.paymentNoticeSourceItem = fallbackSource || this.paymentNoticeSourceItem || null
|
||
return this.paymentNoticeSourceItem
|
||
})
|
||
.finally(() => {
|
||
this.paymentNoticeSourceLoading = false
|
||
})
|
||
},
|
||
loadPaymentNoticeBillDetail(orderNo = '') {
|
||
const normalizedOrderNo = String(orderNo || '').trim()
|
||
if (!normalizedOrderNo) {
|
||
this.paymentNoticeBillLoading = false
|
||
this.paymentNoticeBillDetail = null
|
||
return Promise.resolve(null)
|
||
}
|
||
|
||
const cacheKey = `preview:${normalizedOrderNo}`
|
||
const cached = this.reservationBillDetailMap[cacheKey] || null
|
||
if (cached) {
|
||
this.paymentNoticeBillLoading = false
|
||
this.paymentNoticeBillDetail = cached
|
||
return Promise.resolve(cached)
|
||
}
|
||
|
||
this.paymentNoticeBillLoading = true
|
||
this.paymentNoticeBillDetail = null
|
||
return accountQueryBillPreview({
|
||
OrderNo: normalizedOrderNo
|
||
}, this.language)
|
||
.then((response) => {
|
||
const detail = this.normalizePaymentNoticePreview(response, normalizedOrderNo)
|
||
const normalizedDetail = detail && Object.keys(detail).length ? detail : null
|
||
if (normalizedDetail) {
|
||
this.$set(this.reservationBillDetailMap, cacheKey, normalizedDetail)
|
||
}
|
||
this.paymentNoticeBillDetail = normalizedDetail
|
||
return normalizedDetail
|
||
})
|
||
.catch(() => {
|
||
this.paymentNoticeBillDetail = null
|
||
return null
|
||
})
|
||
.finally(() => {
|
||
this.paymentNoticeBillLoading = false
|
||
})
|
||
},
|
||
openDepositNoticeForPayment(context = {}) {
|
||
const paymentSourceItem = context.sourceItem || this.currentBooking || this.currentApplicationDetail || this.currentFlow || null
|
||
const paymentReferenceTypeNo = String(
|
||
context.referenceTypeNo
|
||
|| this.getApplicationDetailCodeFromFlow()
|
||
|| ''
|
||
).trim()
|
||
const paymentOrderNo = this.resolvePaymentNoticeOrderNo(context, paymentSourceItem)
|
||
this.pendingDepositPaymentContext = {
|
||
referenceTypeNo: paymentReferenceTypeNo,
|
||
orderNo: paymentOrderNo,
|
||
billNo: String(context.billNo || '').trim(),
|
||
paymentNo: String(context.paymentNo || '').trim(),
|
||
description: String(context.description || '').trim(),
|
||
mode: this.getPaymentContextMode(context)
|
||
}
|
||
this.paymentNoticeSourceItem = paymentSourceItem
|
||
this.pendingDepositReferenceTypeNo = this.pendingDepositPaymentContext.referenceTypeNo
|
||
this.loadPaymentNoticeBillDetail(paymentOrderNo)
|
||
.then((preview) => {
|
||
const mergedSource = preview
|
||
? {
|
||
...(paymentSourceItem || {}),
|
||
...preview
|
||
}
|
||
: paymentSourceItem
|
||
this.paymentNoticeSourceItem = mergedSource
|
||
const nextReferenceTypeNo = String(
|
||
(preview && (preview.applicationDetailCode || preview.detailCode))
|
||
|| this.pendingDepositPaymentContext.referenceTypeNo
|
||
|| ''
|
||
).trim()
|
||
this.pendingDepositPaymentContext.referenceTypeNo = nextReferenceTypeNo
|
||
this.pendingDepositReferenceTypeNo = nextReferenceTypeNo
|
||
return this.loadPaymentNoticeSourceDetail(paymentOrderNo, mergedSource)
|
||
})
|
||
.catch(() => {
|
||
this.loadPaymentNoticeSourceDetail(paymentOrderNo, this.paymentNoticeSourceItem)
|
||
})
|
||
this.showDepositNotice = true
|
||
},
|
||
closeDepositNotice() {
|
||
this.showDepositNotice = false
|
||
this.pendingDepositReferenceTypeNo = ''
|
||
this.pendingPaymentMethodContext = null
|
||
this.pendingDepositPaymentContext = null
|
||
this.paymentNoticeSourceItem = null
|
||
this.paymentNoticeSourceLoading = false
|
||
this.paymentNoticeBillLoading = false
|
||
this.paymentNoticeBillDetail = null
|
||
},
|
||
async confirmAndPayDeposit() {
|
||
const paymentContext = this.pendingDepositPaymentContext || {
|
||
referenceTypeNo: this.pendingDepositReferenceTypeNo
|
||
}
|
||
this.showDepositNotice = false
|
||
this.pendingDepositReferenceTypeNo = ''
|
||
this.pendingDepositPaymentContext = null
|
||
return this.payByPaymentContext(paymentContext)
|
||
},
|
||
normalizePayingBills(response) {
|
||
const payload = parseJsonStringMaybe(response && response.data ? response.data : response)
|
||
if (Array.isArray(payload)) {
|
||
return payload.filter(item => item && typeof item === 'object')
|
||
}
|
||
if (payload && Array.isArray(payload.data)) {
|
||
return payload.data.filter(item => item && typeof item === 'object')
|
||
}
|
||
return []
|
||
},
|
||
async getPendingPaymentOrder(billNo = '') {
|
||
const normalizedBillNo = String(billNo || '').trim()
|
||
if (!normalizedBillNo) {
|
||
return null
|
||
}
|
||
const response = await paymentPayingBills({
|
||
BillNo: normalizedBillNo
|
||
})
|
||
const orderList = this.normalizePayingBills(response)
|
||
return orderList.length ? orderList[0] : null
|
||
},
|
||
normalizeBillList(response) {
|
||
const payload = response && response.data ? response.data : response
|
||
if (Array.isArray(payload)) {
|
||
return payload.filter(item => item && typeof item === 'object')
|
||
}
|
||
if (payload && Array.isArray(payload.data)) {
|
||
return payload.data.filter(item => item && typeof item === 'object')
|
||
}
|
||
return []
|
||
},
|
||
normalizeBillDetail(response) {
|
||
const payload = parseJsonStringMaybe(response && response.data ? response.data : response)
|
||
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
||
return payload
|
||
}
|
||
if (payload && payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
|
||
return payload.data
|
||
}
|
||
return {}
|
||
},
|
||
normalizeApplicationDetail(response) {
|
||
const payload = parseJsonStringMaybe(response && response.data ? response.data : response)
|
||
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
||
return payload
|
||
}
|
||
if (payload && payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
|
||
return payload.data
|
||
}
|
||
return {}
|
||
},
|
||
normalizeContractDetail(response) {
|
||
const payload = parseJsonStringMaybe(response && response.data ? response.data : response)
|
||
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
||
return payload
|
||
}
|
||
if (payload && payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
|
||
return payload.data
|
||
}
|
||
return {}
|
||
},
|
||
normalizeInspectionDetail(response) {
|
||
const payload = parseJsonStringMaybe(response && response.data ? response.data : response)
|
||
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
||
return payload
|
||
}
|
||
if (payload && payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
|
||
return payload.data
|
||
}
|
||
return {}
|
||
},
|
||
ensureCurrentContractDetail() {
|
||
const contractNo = String(this.currentReservationRefNo || '').trim()
|
||
if (!this.isContractSigningStep || !contractNo) {
|
||
this.reservationContractDetail = null
|
||
return Promise.resolve(null)
|
||
}
|
||
|
||
return contractDetail({
|
||
contractNo
|
||
}, this.language)
|
||
.then((response) => {
|
||
if (!this.isContractSigningStep || String(this.currentReservationRefNo || '').trim() !== contractNo) {
|
||
return this.reservationContractDetail
|
||
}
|
||
const detail = this.normalizeContractDetail(response)
|
||
this.reservationContractDetail = detail && Object.keys(detail).length ? detail : null
|
||
return this.reservationContractDetail
|
||
})
|
||
.catch(() => {
|
||
if (String(this.currentReservationRefNo || '').trim() === contractNo) {
|
||
this.reservationContractDetail = null
|
||
}
|
||
return null
|
||
})
|
||
},
|
||
ensureCurrentInspectionDetail() {
|
||
const inspectionNo = String(this.currentReservationRefNo || '').trim()
|
||
if (!this.isInspectionStep || !inspectionNo) {
|
||
this.reservationInspectionDetail = null
|
||
return Promise.resolve(null)
|
||
}
|
||
|
||
return roomInspectionDetail({
|
||
inspectionNo
|
||
}, this.language)
|
||
.then((response) => {
|
||
if (!this.isInspectionStep || String(this.currentReservationRefNo || '').trim() !== inspectionNo) {
|
||
return this.reservationInspectionDetail
|
||
}
|
||
const detail = this.normalizeInspectionDetail(response)
|
||
this.reservationInspectionDetail = detail && Object.keys(detail).length ? detail : null
|
||
return this.reservationInspectionDetail
|
||
})
|
||
.catch(() => {
|
||
if (String(this.currentReservationRefNo || '').trim() === inspectionNo) {
|
||
this.reservationInspectionDetail = null
|
||
}
|
||
return null
|
||
})
|
||
},
|
||
async resolveApplicationDetailPaymentContext(referenceTypeNo = '') {
|
||
const normalizedReferenceTypeNo = String(referenceTypeNo || '').trim()
|
||
if (!normalizedReferenceTypeNo) {
|
||
return null
|
||
}
|
||
|
||
const detail = this.normalizeApplicationDetail(await applicationFormDetail(normalizedReferenceTypeNo, this.language))
|
||
if (!detail || !Object.keys(detail).length) {
|
||
return null
|
||
}
|
||
|
||
return {
|
||
referenceTypeNo: String(detail.detailCode || normalizedReferenceTypeNo).trim(),
|
||
billNo: String(detail.billNo || detail.BillNo || '').trim(),
|
||
isViewingApplication: isViewingApplicationRecord(detail),
|
||
detail: {
|
||
...detail,
|
||
isViewingApplication: isViewingApplicationRecord(detail)
|
||
}
|
||
}
|
||
},
|
||
getApplicationDetailCodeFromFlow() {
|
||
const flow = this.currentFlow || {}
|
||
const flowNodes = Array.isArray(flow.flowNodes) ? flow.flowNodes : []
|
||
const applicationNode = [...flowNodes]
|
||
.filter(item => item && typeof item === 'object' && Number(item.step || 0) === 1)
|
||
.sort((prev, next) => Number(prev.sort || 0) - Number(next.sort || 0))[0] || null
|
||
|
||
return String(
|
||
(applicationNode && (applicationNode.detailCode || applicationNode.code || applicationNode.refNo))
|
||
|| (this.isApplicationStep ? this.currentReservationRefNo : '')
|
||
|| ''
|
||
).trim()
|
||
},
|
||
syncCurrentApplicationDetail() {
|
||
const applicationDetailCode = this.getApplicationDetailCodeFromFlow()
|
||
if (!applicationDetailCode) {
|
||
this.currentApplicationDetail = null
|
||
return Promise.resolve(null)
|
||
}
|
||
|
||
return applicationFormDetail(applicationDetailCode, this.language)
|
||
.then((response) => {
|
||
if (applicationDetailCode !== this.getApplicationDetailCodeFromFlow()) {
|
||
return this.currentApplicationDetail
|
||
}
|
||
const detail = this.normalizeApplicationDetail(response)
|
||
const nextDetail = detail && Object.keys(detail).length
|
||
? {
|
||
...detail,
|
||
isViewingApplication: isViewingApplicationRecord(detail)
|
||
}
|
||
: null
|
||
this.currentApplicationDetail = nextDetail
|
||
this.applicationHeroIndex = 0
|
||
if (!nextDetail || nextDetail.isViewingApplication || Number(nextDetail.status) !== 0 || Number(nextDetail.payStatus) === 10) {
|
||
return this.currentApplicationDetail
|
||
}
|
||
return accountQueryBills({
|
||
referenceTypeNo: applicationDetailCode,
|
||
status: 10,
|
||
paymentStatus: 0,
|
||
ProviderType: 'WechatMiniProgram'
|
||
}, this.language)
|
||
.then((billResponse) => {
|
||
if (applicationDetailCode !== this.getApplicationDetailCodeFromFlow()) {
|
||
return this.currentApplicationDetail
|
||
}
|
||
const billList = this.normalizeBillList(billResponse)
|
||
this.currentApplicationDetail = {
|
||
...this.currentApplicationDetail,
|
||
pendingDepositBill: Array.isArray(billList) && billList.length ? billList[0] : null,
|
||
pendingDepositBillQueried: true
|
||
}
|
||
return this.currentApplicationDetail
|
||
})
|
||
.catch(() => this.currentApplicationDetail)
|
||
})
|
||
.catch(() => {
|
||
if (applicationDetailCode === this.getApplicationDetailCodeFromFlow()) {
|
||
this.currentApplicationDetail = null
|
||
this.applicationHeroIndex = 0
|
||
}
|
||
return null
|
||
})
|
||
},
|
||
getBillPayRequests(bill = {}) {
|
||
return (Array.isArray(bill.payRequests) ? bill.payRequests : [])
|
||
.filter(item => item && typeof item === 'object')
|
||
.sort((prev, next) => new Date(next.createTime || 0).getTime() - new Date(prev.createTime || 0).getTime())
|
||
},
|
||
buildPendingPaymentOrderFromBillDetail(bill = {}) {
|
||
const payRequest = this.getBillPayRequests(bill).find((item) => {
|
||
return !item.paidTime
|
||
})
|
||
if (!payRequest) {
|
||
return null
|
||
}
|
||
return {
|
||
amount: Number(payRequest.amount || bill.remainingAmount || bill.totalCharge || 0),
|
||
expireTime: payRequest.expireTime || '',
|
||
payRequestNo: payRequest.no || '',
|
||
paymentNo: payRequest.no || '',
|
||
cashier: {
|
||
paymentInfo: payRequest.paymentInfo || '',
|
||
expireTime: payRequest.expireTime || ''
|
||
}
|
||
}
|
||
},
|
||
getPendingPaymentOrderNo(order = {}) {
|
||
const cashier = order && order.cashier ? order.cashier : {}
|
||
return String(
|
||
order.payRequestNo
|
||
|| order.payNo
|
||
|| order.paymentNo
|
||
|| cashier.payRequestNo
|
||
|| cashier.payNo
|
||
|| cashier.paymentNo
|
||
|| ''
|
||
).trim()
|
||
},
|
||
getBillNo(bill = {}) {
|
||
return bill.no || bill.billNo || ''
|
||
},
|
||
parsePaymentInfo(paymentInfo) {
|
||
if (!paymentInfo) {
|
||
return {}
|
||
}
|
||
if (typeof paymentInfo === 'object') {
|
||
return paymentInfo
|
||
}
|
||
const text = String(paymentInfo).trim()
|
||
if (!text) {
|
||
return {}
|
||
}
|
||
try {
|
||
return JSON.parse(text)
|
||
} catch (error) {
|
||
return {}
|
||
}
|
||
},
|
||
normalizeWechatPayOptions(paymentInfo) {
|
||
const source = this.parsePaymentInfo(paymentInfo)
|
||
return {
|
||
provider: 'wxpay',
|
||
timeStamp: String(source.timeStamp || source.timestamp || source.time_stamp || ''),
|
||
nonceStr: source.nonceStr || source.noncestr || source.nonce_str || '',
|
||
package: source.package || source.packageValue || source.package_value || '',
|
||
signType: source.signType || source.sign_type || 'MD5',
|
||
paySign: source.paySign || source.paysign || source.pay_sign || ''
|
||
}
|
||
},
|
||
requestWechatPayment(paymentInfo) {
|
||
const payOptions = this.normalizeWechatPayOptions(paymentInfo)
|
||
if (!payOptions.timeStamp || !payOptions.nonceStr || !payOptions.package || !payOptions.paySign) {
|
||
return Promise.reject(new Error(this.$tr('获取支付参数失败')))
|
||
}
|
||
return new Promise((resolve, reject) => {
|
||
uni.requestPayment({
|
||
...payOptions,
|
||
success: resolve,
|
||
fail: reject
|
||
})
|
||
})
|
||
},
|
||
getWechatLoginCode() {
|
||
return new Promise((resolve, reject) => {
|
||
uni.login({
|
||
provider: 'weixin',
|
||
success: (res) => {
|
||
if (!res || !res.code) {
|
||
reject(new Error(this.$tr('获取微信登录凭证失败')))
|
||
return
|
||
}
|
||
resolve(res.code)
|
||
},
|
||
fail: reject
|
||
})
|
||
})
|
||
},
|
||
async payByPaymentContext(context = {}) {
|
||
const paymentMode = this.getPaymentContextMode(context)
|
||
let referenceTypeNo = String(context.referenceTypeNo || '').trim()
|
||
let billNo = String(context.billNo || '').trim()
|
||
const paymentNo = String(context.paymentNo || '').trim()
|
||
const description = String(context.description || '').trim() || (paymentMode === 'bill' ? this.$tr('账单支付') : this.$tr('支付定金'))
|
||
const emptyBillMessage = paymentMode === 'bill'
|
||
? this.$tr('暂无待支付账单')
|
||
: this.$tr('暂无待支付定金账单')
|
||
const missingContextMessage = paymentMode === 'bill'
|
||
? this.$tr('账单编号缺失')
|
||
: this.$tr('申请单编号缺失')
|
||
|
||
if (!referenceTypeNo && !billNo && !paymentNo) {
|
||
uni.showToast({
|
||
title: missingContextMessage,
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
if (referenceTypeNo) {
|
||
saveActiveApplicationCode(referenceTypeNo)
|
||
}
|
||
|
||
let shouldRefreshAfterPayment = false
|
||
this.isPaymentActionRunning = true
|
||
uni.showLoading({ title: this.$tr('正在获取账单...') })
|
||
try {
|
||
let bill = null
|
||
if (referenceTypeNo && !billNo) {
|
||
const detailPaymentContext = await this.resolveApplicationDetailPaymentContext(referenceTypeNo).catch(() => null)
|
||
if (detailPaymentContext) {
|
||
if (detailPaymentContext.isViewingApplication) {
|
||
uni.showToast({
|
||
title: this.$tr('预约看房申请无需支付定金'),
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
referenceTypeNo = detailPaymentContext.referenceTypeNo || referenceTypeNo
|
||
billNo = detailPaymentContext.billNo || billNo
|
||
}
|
||
}
|
||
let pendingPaymentOrder = billNo ? await this.getPendingPaymentOrder(billNo).catch(() => null) : null
|
||
|
||
if (referenceTypeNo && !billNo) {
|
||
const detailPaymentContext = await this.resolveApplicationDetailPaymentContext(referenceTypeNo).catch(() => null)
|
||
if (detailPaymentContext) {
|
||
if (detailPaymentContext.isViewingApplication) {
|
||
uni.showToast({
|
||
title: this.$tr('预约看房申请无需支付定金'),
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
referenceTypeNo = detailPaymentContext.referenceTypeNo || referenceTypeNo
|
||
billNo = detailPaymentContext.billNo || billNo
|
||
}
|
||
if (!pendingPaymentOrder && billNo) {
|
||
pendingPaymentOrder = await this.getPendingPaymentOrder(billNo).catch(() => null)
|
||
}
|
||
}
|
||
|
||
if (billNo && !bill) {
|
||
const billDetail = this.normalizeBillDetail(await accountQueryBill({
|
||
billNo
|
||
}, this.language))
|
||
if (billDetail && Object.keys(billDetail).length) {
|
||
bill = billDetail
|
||
billNo = this.getBillNo(bill) || billNo
|
||
this.$set(this.reservationBillDetailMap, billNo, billDetail)
|
||
referenceTypeNo = referenceTypeNo || String(bill.referenceTypeNo || '').trim()
|
||
if (!pendingPaymentOrder) {
|
||
pendingPaymentOrder = this.buildPendingPaymentOrderFromBillDetail(bill)
|
||
}
|
||
}
|
||
}
|
||
|
||
const shouldQueryBillListByReference = !!referenceTypeNo && !(paymentMode === 'bill' && billNo)
|
||
if (shouldQueryBillListByReference) {
|
||
const billRes = await accountQueryBills({
|
||
referenceTypeNo,
|
||
status: 10,
|
||
paymentStatus: 0,
|
||
ProviderType: 'WechatMiniProgram'
|
||
})
|
||
const billList = this.normalizeBillList(billRes)
|
||
if (!Array.isArray(billList) || !billList.length) {
|
||
if (pendingPaymentOrder && billNo) {
|
||
bill = {
|
||
billNo,
|
||
no: billNo,
|
||
referenceTypeNo,
|
||
remainingAmount: Number(pendingPaymentOrder.amount || 0),
|
||
amount: Number(pendingPaymentOrder.amount || 0),
|
||
billName: description
|
||
}
|
||
} else {
|
||
uni.showToast({ title: emptyBillMessage, icon: 'none' })
|
||
return
|
||
}
|
||
} else {
|
||
bill = billNo
|
||
? (billList.find(item => String(this.getBillNo(item) || '').trim() === billNo) || null)
|
||
: null
|
||
if (!bill) {
|
||
bill = billList[0]
|
||
}
|
||
billNo = this.getBillNo(bill)
|
||
referenceTypeNo = referenceTypeNo || String(bill.referenceTypeNo || '').trim()
|
||
if (!pendingPaymentOrder && billNo) {
|
||
pendingPaymentOrder = await this.getPendingPaymentOrder(billNo).catch(() => null)
|
||
}
|
||
if (!pendingPaymentOrder) {
|
||
pendingPaymentOrder = this.buildPendingPaymentOrderFromBillDetail(bill)
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!billNo) {
|
||
uni.showToast({ title: emptyBillMessage, icon: 'none' })
|
||
return
|
||
}
|
||
|
||
uni.showLoading({ title: this.$tr('正在拉起支付...') })
|
||
|
||
const isPendingPaymentOrderExpired = (order = {}) => {
|
||
const expireTimestamp = parseExpireTimeToTimestamp(order.expireTime || (order.cashier && order.cashier.expireTime) || '')
|
||
if (!expireTimestamp) {
|
||
return false
|
||
}
|
||
return expireTimestamp <= Date.now()
|
||
}
|
||
|
||
const reusablePendingPaymentOrder = pendingPaymentOrder && !isPendingPaymentOrderExpired(pendingPaymentOrder)
|
||
? pendingPaymentOrder
|
||
: null
|
||
|
||
shouldRefreshAfterPayment = true
|
||
if (reusablePendingPaymentOrder && reusablePendingPaymentOrder.cashier && reusablePendingPaymentOrder.cashier.paymentInfo) {
|
||
await this.requestWechatPayment(reusablePendingPaymentOrder.cashier.paymentInfo)
|
||
} else {
|
||
if (!referenceTypeNo || !bill) {
|
||
throw new Error(this.$tr('获取支付参数失败'))
|
||
}
|
||
const wechatCode = await this.getWechatLoginCode()
|
||
const payRes = await paymentBillPay({
|
||
referenceTypeNo,
|
||
billNo,
|
||
payMethod: 'Wechat',
|
||
amount: Number((bill && (bill.remainingAmount || bill.amount)) || 0),
|
||
description: (bill && (bill.billName || bill.name)) || description,
|
||
ExtraInfo: wechatCode
|
||
})
|
||
|
||
const payload = payRes && payRes.data ? payRes.data : payRes
|
||
const cashier = payload && payload.cashier ? payload.cashier : {}
|
||
await this.requestWechatPayment(cashier.paymentInfo)
|
||
}
|
||
|
||
uni.showToast({ title: this.$tr('支付成功'), icon: 'success' })
|
||
} catch (error) {
|
||
const errMsg = error && (error.errMsg || error.message || error.msg) ? (error.errMsg || error.message || error.msg) : this.$tr('支付失败')
|
||
if (/cancel/i.test(errMsg)) {
|
||
uni.showToast({ title: this.$tr('已取消支付'), icon: 'none' })
|
||
} else {
|
||
uni.showToast({ title: errMsg, icon: 'none' })
|
||
}
|
||
} finally {
|
||
uni.hideLoading()
|
||
if (shouldRefreshAfterPayment) {
|
||
setTimeout(() => {
|
||
this.isPaymentActionRunning = false
|
||
this.refreshPendingStatus()
|
||
}, 1500)
|
||
} else {
|
||
this.isPaymentActionRunning = false
|
||
}
|
||
}
|
||
},
|
||
goPayBill() {
|
||
const billNo = String(this.currentReservationRefNo || '').trim()
|
||
if (billNo) {
|
||
this.openPaymentMethodSelector({
|
||
referenceTypeNo: this.getApplicationDetailCodeFromFlow(),
|
||
billNo,
|
||
description: this.$tr('账单支付'),
|
||
mode: 'bill',
|
||
sourceItem: this.currentBooking || this.currentApplicationDetail || {}
|
||
})
|
||
return
|
||
}
|
||
const orderNo = this.currentBooking && this.currentBooking.orderNo
|
||
if (orderNo) {
|
||
uni.navigateTo({
|
||
url: `/pkgBooking/pay/pay?orderNo=${encodeURIComponent(orderNo)}`
|
||
})
|
||
return
|
||
}
|
||
this.goBookingDetail()
|
||
},
|
||
goRoomInspection() {
|
||
const orderNo = this.currentBooking && this.currentBooking.orderNo
|
||
uni.navigateTo({
|
||
url: orderNo
|
||
? `/pkgService/room_inspection/room_inspection?orderNo=${encodeURIComponent(orderNo)}`
|
||
: '/pkgService/room_inspection/room_inspection'
|
||
})
|
||
},
|
||
goSignContract() {
|
||
const contractNo = String(this.currentReservationRefNo || '').trim()
|
||
const orderNo = String(this.currentBooking && this.currentBooking.orderNo || '').trim()
|
||
if (!contractNo) {
|
||
uni.showToast({
|
||
title: this.$tr('合同编号缺失'),
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
if (this.isCurrentContractAwaitingReview) {
|
||
uni.showToast({
|
||
title: this.$tr('合同已签,等待管家审核'),
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
uni.navigateTo({
|
||
url: `/pkgService/contract_sign/contract_sign?no=${encodeURIComponent(contractNo)}&orderNo=${encodeURIComponent(orderNo)}`
|
||
})
|
||
},
|
||
refreshPendingStatus() {
|
||
if (this.isPendingStatusRefreshing) {
|
||
return
|
||
}
|
||
this.isPendingStatusRefreshing = true
|
||
// 通知父页只重新获取流程
|
||
this.$emit('refresh-shell-state')
|
||
},
|
||
onFlowRefreshed(result = {}) {
|
||
this.checkLocalStatus()
|
||
this.loadUnpaidBills()
|
||
this.isPendingStatusRefreshing = false
|
||
if (result.silent) {
|
||
return
|
||
}
|
||
if (result.success === false) {
|
||
uni.showToast({
|
||
title: this.$tr('刷新失败,请稍后重试'),
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
const pendingContracts = this.getStorePendingContracts()
|
||
uni.showToast({
|
||
title: pendingContracts.length
|
||
? this.$tr('发现待签合同')
|
||
: this.$tr('状态已刷新'),
|
||
icon: 'success'
|
||
})
|
||
},
|
||
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.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 = item.serviceType || serviceTypeMap[item.key]
|
||
if (!serviceType) {
|
||
return ''
|
||
}
|
||
return `/pkgService/ticket_workbench/ticket_workbench?serviceType=${encodeURIComponent(serviceType)}`
|
||
},
|
||
openServicePage(item) {
|
||
if (!this.stayApplicationsEnabled && ['change-room', 'renew-stay', 'checkout'].includes(item.key)) {
|
||
uni.showToast({
|
||
title: this.$tr('该功能暂未开放'),
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
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
|
||
}
|
||
if (item.path) {
|
||
uni.navigateTo({
|
||
url: item.path
|
||
});
|
||
return
|
||
}
|
||
if (String(item.entryType || '').trim().toLowerCase() === 'submit') {
|
||
uni.navigateTo({
|
||
url: this.buildServiceSubmitUrl(item)
|
||
});
|
||
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 || '')}`
|
||
})
|
||
},
|
||
goLockControl() {
|
||
const roomCode = this.getCurrentSmartRoomCode()
|
||
const roomName = this.getCurrentSmartRoomName()
|
||
const suiteCode = this.getCurrentLockSuiteCode()
|
||
const suiteName = this.getCurrentLockSuiteName()
|
||
const userCode = this.getCurrentUserCode()
|
||
uni.navigateTo({
|
||
url: `/pkgService/lock_control/lock_control?roomCode=${encodeURIComponent(roomCode || '')}&roomName=${encodeURIComponent(roomName || '')}&suiteCode=${encodeURIComponent(suiteCode || '')}&suiteName=${encodeURIComponent(suiteName || '')}&userCode=${encodeURIComponent(userCode || '')}`
|
||
})
|
||
},
|
||
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:
|
||
radial-gradient(circle at top, rgba(56, 189, 248, 0.14) 0%, rgba(56, 189, 248, 0) 26%),
|
||
linear-gradient(180deg, #EAF3FF 0%, #F5F8FF 28%, #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;
|
||
overflow: hidden;
|
||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.92) 0%, rgba(246, 250, 255, 0.98) 100%);
|
||
border-radius: 38rpx;
|
||
box-shadow: 0 24rpx 60rpx rgba(15, 23, 42, 0.1);
|
||
border: 1rpx solid rgba(255, 255, 255, 0.88);
|
||
backdrop-filter: blur(20rpx);
|
||
margin-top: 20rpx;
|
||
}
|
||
|
||
.no-booking-glow {
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.no-booking-glow--one {
|
||
top: -70rpx;
|
||
right: -30rpx;
|
||
width: 220rpx;
|
||
height: 220rpx;
|
||
background: rgba(56, 189, 248, 0.12);
|
||
}
|
||
|
||
.no-booking-glow--two {
|
||
left: -60rpx;
|
||
bottom: -50rpx;
|
||
width: 180rpx;
|
||
height: 180rpx;
|
||
background: rgba(15, 23, 42, 0.04);
|
||
}
|
||
|
||
.no-booking-bg {
|
||
display: none;
|
||
}
|
||
|
||
.no-booking-content {
|
||
position: relative;
|
||
z-index: 1;
|
||
padding: 96rpx 48rpx 88rpx;
|
||
}
|
||
|
||
.no-booking-card .icon-wrap {
|
||
width: 132rpx;
|
||
height: 132rpx;
|
||
border-radius: 40rpx;
|
||
background: linear-gradient(180deg, rgba(56, 189, 248, 0.12) 0%, rgba(56, 189, 248, 0.06) 100%);
|
||
border: 1rpx solid rgba(56, 189, 248, 0.08);
|
||
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.75);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.no-booking-card .icon-wrap .tn-icon-home-smile {
|
||
font-size: 64rpx;
|
||
color: $brand-primary;
|
||
}
|
||
|
||
.no-booking-kicker {
|
||
margin-top: 28rpx;
|
||
font-size: 20rpx;
|
||
letter-spacing: 4rpx;
|
||
color: #94A3B8;
|
||
}
|
||
|
||
.no-booking-title {
|
||
font-size: 42rpx;
|
||
font-weight: 700;
|
||
color: #0F172A;
|
||
margin-top: 22rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.no-booking-desc {
|
||
max-width: 520rpx;
|
||
font-size: 26rpx;
|
||
color: #64748B;
|
||
text-align: center;
|
||
line-height: 1.8;
|
||
margin-top: 18rpx;
|
||
}
|
||
|
||
.no-booking-action {
|
||
min-width: 220rpx;
|
||
height: 72rpx;
|
||
padding: 0 34rpx;
|
||
margin-top: 40rpx;
|
||
border-radius: 999rpx;
|
||
background: linear-gradient(135deg, #0F172A 0%, #163E63 100%);
|
||
color: #FFFFFF;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 28rpx;
|
||
box-shadow: 0 10rpx 22rpx rgba(15, 23, 42, 0.18);
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.no-booking-action:active {
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
/* 流程作业页面重构 */
|
||
.flow-page {
|
||
padding-bottom: 60rpx;
|
||
}
|
||
|
||
.flow-banner {
|
||
position: relative;
|
||
height: 456rpx;
|
||
overflow: hidden;
|
||
background:
|
||
radial-gradient(circle at 18% 22%, rgba(125, 211, 252, 0.22) 0%, rgba(125, 211, 252, 0) 28%),
|
||
radial-gradient(circle at 82% 18%, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0) 24%),
|
||
linear-gradient(145deg, #0C2741 0%, #133A62 42%, #0F5F86 100%);
|
||
}
|
||
|
||
.flow-banner-swiper,
|
||
.flow-banner-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.flow-banner-placeholder {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: relative;
|
||
overflow: hidden;
|
||
background:
|
||
radial-gradient(circle at 20% 20%, rgba(126, 214, 255, 0.26) 0%, rgba(126, 214, 255, 0) 42%),
|
||
linear-gradient(135deg, #15385F 0%, #1A5784 56%, #1697C9 100%);
|
||
}
|
||
|
||
.flow-banner-placeholder-glow {
|
||
position: absolute;
|
||
top: 40rpx;
|
||
right: 40rpx;
|
||
width: 140rpx;
|
||
height: 140rpx;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.12);
|
||
filter: blur(12rpx);
|
||
}
|
||
|
||
.flow-banner-overlay {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: linear-gradient(180deg, rgba(15, 23, 42, 0.06) 0%, rgba(15, 23, 42, 0.18) 38%, rgba(15, 23, 42, 0.72) 100%);
|
||
}
|
||
|
||
.flow-banner-content {
|
||
position: absolute;
|
||
left: 32rpx;
|
||
right: 32rpx;
|
||
bottom: 118rpx;
|
||
z-index: 3;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.flow-banner-topline {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 18rpx;
|
||
}
|
||
|
||
.flow-banner-chip {
|
||
align-self: flex-start;
|
||
padding: 8rpx 20rpx;
|
||
border-radius: 999rpx;
|
||
font-size: 22rpx;
|
||
font-weight: 600;
|
||
color: #FFFFFF;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
backdrop-filter: blur(12rpx);
|
||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
.flow-banner-title {
|
||
font-size: 44rpx;
|
||
font-weight: 700;
|
||
color: #FFFFFF;
|
||
line-height: 1.3;
|
||
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.flow-banner-subtitle {
|
||
margin-top: 8rpx;
|
||
font-size: 26rpx;
|
||
color: rgba(255, 255, 255, 0.85);
|
||
}
|
||
|
||
.flow-banner-pagination {
|
||
position: absolute;
|
||
right: 32rpx;
|
||
bottom: 34rpx;
|
||
z-index: 3;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10rpx;
|
||
}
|
||
|
||
.flow-banner-pagination-dot {
|
||
width: 10rpx;
|
||
height: 10rpx;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.4);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.flow-banner-pagination-dot.active {
|
||
width: 24rpx;
|
||
border-radius: 999rpx;
|
||
background: #FFFFFF;
|
||
}
|
||
|
||
/* 主体内收,卡片式布局 */
|
||
.flow-body {
|
||
padding: 0 24rpx;
|
||
margin-top: -98rpx;
|
||
position: relative;
|
||
z-index: 10;
|
||
}
|
||
|
||
.flow-unified-panel {
|
||
position: relative;
|
||
overflow: hidden;
|
||
border-radius: 38rpx;
|
||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.92) 0%, rgba(246, 250, 255, 0.98) 100%);
|
||
box-shadow: 0 24rpx 60rpx rgba(15, 23, 42, 0.1);
|
||
border: 1rpx solid rgba(255, 255, 255, 0.88);
|
||
}
|
||
|
||
.flow-unified-panel-glow {
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.flow-unified-panel-glow--one {
|
||
top: -80rpx;
|
||
right: -30rpx;
|
||
width: 220rpx;
|
||
height: 220rpx;
|
||
background: rgba(56, 189, 248, 0.12);
|
||
}
|
||
|
||
.flow-unified-panel-glow--two {
|
||
left: -50rpx;
|
||
top: 180rpx;
|
||
width: 180rpx;
|
||
height: 180rpx;
|
||
background: rgba(15, 23, 42, 0.04);
|
||
}
|
||
|
||
.flow-meta-card {
|
||
position: relative;
|
||
z-index: 1;
|
||
padding: 34rpx 28rpx 26rpx;
|
||
}
|
||
|
||
.flow-meta-header {
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.flow-meta-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.flow-meta-grid-item {
|
||
padding: 22rpx 20rpx;
|
||
border-radius: 22rpx;
|
||
background: rgba(255, 255, 255, 0.62);
|
||
border: 1rpx solid rgba(15, 23, 42, 0.04);
|
||
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.78);
|
||
}
|
||
|
||
.flow-meta-grid-label {
|
||
font-size: 22rpx;
|
||
color: #64748B;
|
||
}
|
||
|
||
.flow-meta-grid-value {
|
||
margin-top: 8rpx;
|
||
font-size: 26rpx;
|
||
font-weight: 600;
|
||
color: #0F172A;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.flow-panel-divider {
|
||
height: 1rpx;
|
||
margin: 0 28rpx;
|
||
background: linear-gradient(90deg, rgba(15, 23, 42, 0) 0%, rgba(15, 23, 42, 0.08) 18%, rgba(15, 23, 42, 0.08) 82%, rgba(15, 23, 42, 0) 100%);
|
||
}
|
||
|
||
.flow-timeline-card {
|
||
position: relative;
|
||
z-index: 1;
|
||
padding: 30rpx 28rpx 28rpx;
|
||
}
|
||
|
||
.flow-timeline-head {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.flow-timeline-title {
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
color: #0F172A;
|
||
letter-spacing: 1rpx;
|
||
}
|
||
|
||
.flow-progress-pill {
|
||
flex-shrink: 0;
|
||
min-width: 108rpx;
|
||
height: 54rpx;
|
||
padding: 0 20rpx;
|
||
border-radius: 999rpx;
|
||
background: rgba(15, 23, 42, 0.05);
|
||
color: #0F172A;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 22rpx;
|
||
font-weight: 700;
|
||
letter-spacing: 1rpx;
|
||
}
|
||
|
||
.flow-step-rail {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
margin-top: 30rpx;
|
||
padding-top: 6rpx;
|
||
}
|
||
|
||
.flow-step-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 18rpx;
|
||
}
|
||
|
||
.flow-step-axis {
|
||
width: 60rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.flow-step-line {
|
||
width: 2rpx;
|
||
min-height: 54rpx;
|
||
margin: 10rpx 0 0;
|
||
background: linear-gradient(180deg, rgba($brand-primary, 0.2) 0%, rgba(15, 23, 42, 0.08) 100%);
|
||
}
|
||
|
||
.flow-step-orb {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
border-radius: 22rpx;
|
||
background: rgba(255, 255, 255, 0.8);
|
||
border: 1rpx solid rgba(15, 23, 42, 0.08);
|
||
box-shadow: 0 10rpx 24rpx rgba(15, 23, 42, 0.06);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #94A3B8;
|
||
font-size: 24rpx;
|
||
transition: all 0.25s ease;
|
||
}
|
||
|
||
.flow-step-index {
|
||
font-size: 22rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.flow-step-item.completed .flow-step-orb {
|
||
background: rgba($brand-primary, 0.12);
|
||
border-color: rgba($brand-primary, 0.14);
|
||
color: $brand-primary;
|
||
}
|
||
|
||
.flow-step-item.current .flow-step-orb {
|
||
background: linear-gradient(135deg, #0F172A 0%, #163E63 100%);
|
||
border-color: transparent;
|
||
color: #FFFFFF;
|
||
box-shadow: 0 14rpx 30rpx rgba(15, 23, 42, 0.22);
|
||
}
|
||
|
||
.flow-step-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
padding-bottom: 12rpx;
|
||
}
|
||
|
||
.flow-step-label-row {
|
||
min-height: 60rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 14rpx;
|
||
}
|
||
|
||
.flow-step-label {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #475569;
|
||
}
|
||
|
||
.flow-step-item.current .flow-step-label,
|
||
.flow-step-item.completed .flow-step-label {
|
||
color: #0F172A;
|
||
}
|
||
|
||
.flow-stage-content {
|
||
margin-top: 8rpx;
|
||
padding: 10rpx 0 18rpx 2rpx;
|
||
}
|
||
|
||
.flow-stage-copy {
|
||
flex: 1;
|
||
}
|
||
|
||
.flow-stage-kicker {
|
||
font-size: 20rpx;
|
||
letter-spacing: 4rpx;
|
||
color: #94A3B8;
|
||
}
|
||
|
||
.flow-stage-title {
|
||
margin-top: 10rpx;
|
||
font-size: 32rpx;
|
||
line-height: 1.35;
|
||
color: #0F172A;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.flow-stage-meta-line {
|
||
margin-top: 14rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.flow-stage-meta-label {
|
||
font-size: 22rpx;
|
||
color: #94A3B8;
|
||
}
|
||
|
||
.flow-stage-meta-value {
|
||
font-size: 24rpx;
|
||
color: #0F172A;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.flow-stage-status {
|
||
flex-shrink: 0;
|
||
min-width: 104rpx;
|
||
height: 52rpx;
|
||
padding: 0 18rpx;
|
||
border-radius: 999rpx;
|
||
background: rgba($brand-primary, 0.1);
|
||
color: $brand-primary;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 22rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.flow-stage-desc {
|
||
margin-top: 14rpx;
|
||
font-size: 24rpx;
|
||
color: #475569;
|
||
line-height: 1.75;
|
||
}
|
||
|
||
.flow-stage-next {
|
||
margin-top: 12rpx;
|
||
font-size: 22rpx;
|
||
color: #64748B;
|
||
}
|
||
|
||
.flow-stage-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 14rpx;
|
||
margin-top: 18rpx;
|
||
}
|
||
|
||
.action-btn {
|
||
min-width: 164rpx;
|
||
height: 64rpx;
|
||
padding: 0 24rpx;
|
||
border-radius: 999rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 24rpx;
|
||
transition: all 0.2s ease;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.action-btn:active {
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.action-btn.primary {
|
||
background: linear-gradient(135deg, #0F172A 0%, #163E63 100%);
|
||
color: #FFFFFF;
|
||
box-shadow: 0 10rpx 22rpx rgba(15, 23, 42, 0.18);
|
||
}
|
||
|
||
.action-btn.secondary {
|
||
background: rgba(255, 255, 255, 0.88);
|
||
color: #0F172A;
|
||
border: 1rpx solid rgba(15, 23, 42, 0.08);
|
||
}
|
||
|
||
.action-btn.danger {
|
||
background: rgba(239, 68, 68, 0.06);
|
||
color: #DC2626;
|
||
border: 1rpx solid rgba(239, 68, 68, 0.12);
|
||
}
|
||
|
||
/* 操作按钮 */
|
||
.flow-task-footer {
|
||
padding: 0 32rpx 36rpx;
|
||
display: flex;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.flow-btn {
|
||
flex: 1;
|
||
height: 92rpx;
|
||
border-radius: 28rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 30rpx;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.flow-btn:active {
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.flow-btn--primary {
|
||
background: linear-gradient(135deg, $brand-primary 0%, #0099CC 100%);
|
||
color: #FFFFFF;
|
||
box-shadow: 0 12rpx 28rpx rgba($brand-primary, 0.25);
|
||
}
|
||
|
||
.flow-btn--primary:active {
|
||
box-shadow: 0 6rpx 16rpx rgba($brand-primary, 0.18);
|
||
}
|
||
|
||
.flow-btn--secondary {
|
||
background: #FFFFFF;
|
||
color: $brand-primary;
|
||
border: 2rpx solid rgba($brand-primary, 0.2);
|
||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.06);
|
||
}
|
||
|
||
.flow-btn--secondary:active {
|
||
background: rgba($brand-primary, 0.04);
|
||
}
|
||
|
||
/* --- 融合版房卡 (Integrated Room Card) --- */
|
||
.integrated-room-card {
|
||
border-radius: 40rpx;
|
||
position: relative;
|
||
overflow: hidden;
|
||
background:
|
||
radial-gradient(circle at top left, rgba(96, 165, 250, 0.2) 0%, rgba(96, 165, 250, 0) 28%),
|
||
linear-gradient(135deg, #081120 0%, #0f1b31 38%, #16284a 100%);
|
||
box-shadow: 0 26rpx 56rpx rgba(8, 17, 32, 0.2);
|
||
border: 1rpx solid rgba(255, 255, 255, 0.06);
|
||
}
|
||
|
||
.irc-bg {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: absolute;
|
||
z-index: 1;
|
||
transform: scale(1.04);
|
||
filter: saturate(0.8) brightness(0.66);
|
||
}
|
||
|
||
.irc-placeholder {
|
||
position: absolute;
|
||
inset: 0;
|
||
z-index: 1;
|
||
background:
|
||
radial-gradient(circle at top left, rgba(96, 165, 250, 0.18) 0%, rgba(96, 165, 250, 0) 28%),
|
||
linear-gradient(135deg, #081120 0%, #0f1b31 38%, #16284a 100%);
|
||
}
|
||
|
||
.irc-placeholder-glow {
|
||
position: absolute;
|
||
width: 460rpx;
|
||
height: 460rpx;
|
||
right: -160rpx;
|
||
top: -160rpx;
|
||
border-radius: 50%;
|
||
background: radial-gradient(circle, rgba(56, 189, 248, 0.22) 0%, rgba(56, 189, 248, 0) 70%);
|
||
}
|
||
|
||
.irc-decor {
|
||
position: absolute;
|
||
pointer-events: none;
|
||
z-index: 2;
|
||
}
|
||
|
||
.irc-decor--aurora {
|
||
left: -80rpx;
|
||
top: -90rpx;
|
||
width: 340rpx;
|
||
height: 220rpx;
|
||
border-radius: 50%;
|
||
background: radial-gradient(circle at center, rgba(125, 211, 252, 0.22) 0%, rgba(125, 211, 252, 0) 72%);
|
||
transform: rotate(-12deg);
|
||
}
|
||
|
||
.irc-decor--ring {
|
||
right: -70rpx;
|
||
top: 72rpx;
|
||
width: 260rpx;
|
||
height: 260rpx;
|
||
border-radius: 50%;
|
||
border: 1rpx solid rgba(255, 255, 255, 0.12);
|
||
box-shadow:
|
||
0 0 0 26rpx rgba(255, 255, 255, 0.03),
|
||
0 0 80rpx rgba(59, 130, 246, 0.08);
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.irc-decor--grid {
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
height: 180rpx;
|
||
background-image:
|
||
linear-gradient(rgba(255, 255, 255, 0.05) 1rpx, transparent 1rpx),
|
||
linear-gradient(90deg, rgba(255, 255, 255, 0.05) 1rpx, transparent 1rpx);
|
||
background-size: 36rpx 36rpx;
|
||
mask-image: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, 0.7) 28%, #000 100%);
|
||
opacity: 0.35;
|
||
}
|
||
|
||
.irc-mask {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: absolute;
|
||
z-index: 3;
|
||
background:
|
||
linear-gradient(135deg, rgba(5, 11, 23, 0.72) 0%, rgba(9, 19, 36, 0.44) 42%, rgba(5, 11, 23, 0.78) 100%),
|
||
linear-gradient(180deg, rgba(255, 255, 255, 0.06) 0%, rgba(255, 255, 255, 0) 24%, rgba(4, 10, 20, 0.16) 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: rgba(255, 255, 255, 0.9);
|
||
background: rgba(255, 255, 255, 0.1);
|
||
padding: 8rpx 24rpx;
|
||
border-radius: 30rpx;
|
||
border: 1rpx solid rgba(255, 255, 255, 0.15);
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
|
||
.irc-smart-btn:active {
|
||
background: rgba(255, 255, 255, 0.15);
|
||
}
|
||
|
||
.irc-lock-entry {
|
||
margin-top: 22rpx;
|
||
padding: 24rpx 26rpx;
|
||
border-radius: 26rpx;
|
||
background: rgba(255, 255, 255, 0.04);
|
||
backdrop-filter: blur(16px);
|
||
border: 1rpx solid rgba(255, 255, 255, 0.06);
|
||
display: flex;
|
||
align-items: center;
|
||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.irc-lock-entry:active {
|
||
transform: scale(0.98);
|
||
background: rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.irc-lock-entry__icon {
|
||
width: 84rpx;
|
||
height: 84rpx;
|
||
border-radius: 24rpx;
|
||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.02) 100%);
|
||
color: #ffffff;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 44rpx;
|
||
flex-shrink: 0;
|
||
box-shadow: inset 0 2rpx 4rpx rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.irc-lock-entry__content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
margin-left: 20rpx;
|
||
}
|
||
|
||
.irc-lock-entry__title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #ffffff;
|
||
letter-spacing: 1rpx;
|
||
}
|
||
|
||
.irc-lock-entry__desc {
|
||
margin-top: 8rpx;
|
||
font-size: 22rpx;
|
||
color: rgba(255, 255, 255, 0.6);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.irc-lock-entry__arrow {
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
margin-left: 16rpx;
|
||
border-radius: 28rpx;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
color: rgba(255, 255, 255, 0.8);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 24rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.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.04);
|
||
backdrop-filter: blur(12px);
|
||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||
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: 28rpx;
|
||
padding: 30rpx;
|
||
border: 1rpx solid rgba($brand-primary, 0.08);
|
||
box-shadow: 0 10rpx 28rpx rgba($brand-primary, 0.06);
|
||
}
|
||
|
||
.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 rgba($brand-primary, 0.08);
|
||
}
|
||
|
||
.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: #1A2233;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.butler-member-new__tag {
|
||
font-size: 20rpx;
|
||
background: rgba($brand-primary, 0.1);
|
||
color: $brand-primary;
|
||
border: 1rpx solid rgba($brand-primary, 0.12);
|
||
padding: 2rpx 10rpx;
|
||
border-radius: 8rpx;
|
||
margin-left: 10rpx;
|
||
}
|
||
|
||
.butler-member-new__tag--type.tag-dormitory {
|
||
background: rgba($brand-primary, 0.08);
|
||
color: $brand-primary;
|
||
}
|
||
|
||
.butler-member-new__tag--type.tag-counselor {
|
||
background: rgba($brand-primary, 0.16);
|
||
color: $brand-primary;
|
||
}
|
||
|
||
.butler-member-new__phone {
|
||
font-size: 24rpx;
|
||
color: #6B7280;
|
||
margin-top: 6rpx;
|
||
}
|
||
|
||
.butler-member-new__action {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
border-radius: 32rpx;
|
||
background: rgba($brand-primary, 0.12);
|
||
border: 1rpx solid rgba($brand-primary, 0.12);
|
||
color: $brand-primary;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 32rpx;
|
||
}
|
||
|
||
.butler-member-new__action:active {
|
||
background: rgba($brand-primary, 0.18);
|
||
}
|
||
|
||
.butler-member-new__action.disabled {
|
||
background: #f5f5f5;
|
||
color: #cccccc;
|
||
}
|
||
|
||
.stay-action-btn {
|
||
flex: 1;
|
||
height: 96rpx;
|
||
margin-right: 20rpx;
|
||
background: linear-gradient(180deg, #ffffff 0%, rgba($brand-primary, 0.04) 100%);
|
||
border-radius: 20rpx;
|
||
font-size: 26rpx;
|
||
color: #1A2233;
|
||
box-shadow: 0 8rpx 24rpx rgba($brand-primary, 0.06);
|
||
border: 1rpx solid rgba($brand-primary, 0.12);
|
||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||
font-weight: 600;
|
||
}
|
||
.stay-action-btn:last-child {
|
||
margin-right: 0;
|
||
}
|
||
.stay-action-btn:active {
|
||
transform: scale(0.96);
|
||
background: rgba($brand-primary, 0.08);
|
||
border-color: rgba($brand-primary, 0.22);
|
||
color: $brand-primary;
|
||
}
|
||
.stay-action-icon-box {
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
border-radius: 12rpx;
|
||
background: rgba($brand-primary, 0.12);
|
||
color: $brand-primary;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 14rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.service-item {
|
||
width: 48%;
|
||
}
|
||
|
||
.service-card-visual {
|
||
height: 240rpx;
|
||
border-radius: 28rpx;
|
||
position: relative;
|
||
overflow: hidden;
|
||
box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.service-card-bg {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: absolute;
|
||
z-index: 1;
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.service-item:active .service-card-bg {
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.service-card-mask {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: absolute;
|
||
z-index: 2;
|
||
background: linear-gradient(to bottom, rgba(15, 23, 42, 0) 0%, rgba(15, 23, 42, 0.75) 100%);
|
||
}
|
||
|
||
.service-card-content {
|
||
position: relative;
|
||
z-index: 3;
|
||
}
|
||
|
||
.service-icon-box {
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
border-radius: 20rpx;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
backdrop-filter: blur(8px);
|
||
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
.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: linear-gradient(180deg, #FFFFFF 0%, rgba($brand-primary, 0.04) 100%);
|
||
border-radius: 28rpx;
|
||
border: 1rpx solid rgba($brand-primary, 0.08);
|
||
box-shadow: 0 10rpx 28rpx rgba($brand-primary, 0.06);
|
||
}
|
||
|
||
.bill-status-tag {
|
||
backdrop-filter: none;
|
||
border: 1rpx solid transparent;
|
||
}
|
||
|
||
.bill-status-tag--active {
|
||
background: rgba($brand-primary, 0.14);
|
||
border-color: rgba($brand-primary, 0.16);
|
||
color: $brand-primary;
|
||
}
|
||
|
||
.bill-status-tag--settled {
|
||
background: rgba($brand-primary, 0.08);
|
||
border-color: rgba($brand-primary, 0.1);
|
||
color: rgba($brand-primary, 0.9);
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* 支付定金须知弹窗 */
|
||
.deposit-notice-popup {
|
||
background: #FFFFFF;
|
||
border-radius: 32rpx 32rpx 0 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 80vh;
|
||
}
|
||
|
||
.popup-header {
|
||
padding: 18rpx 88rpx 20rpx;
|
||
text-align: center;
|
||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(251, 253, 255, 0.96) 100%);
|
||
}
|
||
|
||
.popup-handle {
|
||
width: 88rpx;
|
||
height: 8rpx;
|
||
margin: 0 auto 18rpx;
|
||
border-radius: 999rpx;
|
||
background: rgba(148, 163, 184, 0.35);
|
||
}
|
||
|
||
.popup-title {
|
||
font-size: 34rpx;
|
||
font-weight: 600;
|
||
line-height: 1.2;
|
||
letter-spacing: 1rpx;
|
||
color: #1A2233;
|
||
}
|
||
|
||
.popup-body {
|
||
flex: 1;
|
||
padding: 28rpx 28rpx 24rpx;
|
||
overflow-y: auto;
|
||
background: linear-gradient(180deg, #F7FAFF 0%, #F2F5FC 100%);
|
||
}
|
||
|
||
.notice-content {
|
||
padding-bottom: 24rpx;
|
||
}
|
||
|
||
.payment-sheet {
|
||
position: relative;
|
||
padding: 28rpx;
|
||
border-radius: 32rpx;
|
||
background:
|
||
radial-gradient(circle at top right, rgba(43, 142, 255, 0.16) 0, rgba(43, 142, 255, 0) 38%),
|
||
linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(246, 250, 255, 0.96) 100%);
|
||
border: 1rpx solid rgba(43, 142, 255, 0.1);
|
||
box-shadow: 0 24rpx 60rpx rgba(43, 142, 255, 0.1);
|
||
}
|
||
|
||
.payment-hero {
|
||
padding: 18rpx 6rpx 14rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.payment-hero-label {
|
||
font-size: 24rpx;
|
||
color: #7C8CA5;
|
||
letter-spacing: 2rpx;
|
||
}
|
||
|
||
.payment-hero-amount {
|
||
margin-top: 14rpx;
|
||
font-size: 68rpx;
|
||
line-height: 1;
|
||
font-weight: 700;
|
||
color: #183B73;
|
||
}
|
||
|
||
.payment-hero-hint {
|
||
margin-top: 16rpx;
|
||
font-size: 24rpx;
|
||
color: #6F809C;
|
||
}
|
||
|
||
.payment-sync-group {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 14rpx;
|
||
margin-top: 18rpx;
|
||
}
|
||
|
||
.payment-sync-chip {
|
||
padding: 10rpx 18rpx;
|
||
border-radius: 999rpx;
|
||
background: rgba(43, 142, 255, 0.08);
|
||
color: #4C6A95;
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.payment-detail-panel {
|
||
margin-top: 26rpx;
|
||
padding: 24rpx;
|
||
border-radius: 26rpx;
|
||
background: rgba(255, 255, 255, 0.82);
|
||
border: 1rpx solid rgba(43, 142, 255, 0.08);
|
||
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.8);
|
||
}
|
||
|
||
.payment-detail-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.payment-detail-title {
|
||
font-size: 28rpx;
|
||
line-height: 1.2;
|
||
color: #22324D;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.payment-detail-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.payment-detail-row {
|
||
padding: 22rpx 4rpx;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 28rpx;
|
||
}
|
||
|
||
.payment-detail-row + .payment-detail-row {
|
||
border-top: 1rpx solid rgba(43, 142, 255, 0.08);
|
||
}
|
||
|
||
.payment-detail-label {
|
||
flex: 0 0 148rpx;
|
||
font-size: 22rpx;
|
||
color: #7C8CA5;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.payment-detail-value {
|
||
flex: 1;
|
||
font-size: 26rpx;
|
||
line-height: 1.4;
|
||
color: #22324D;
|
||
word-break: break-all;
|
||
font-weight: 600;
|
||
text-align: right;
|
||
}
|
||
|
||
.popup-footer {
|
||
padding: 18rpx 28rpx calc(20rpx + env(safe-area-inset-bottom));
|
||
background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.92) 26%, #FFFFFF 100%);
|
||
display: flex;
|
||
justify-content: center;
|
||
backdrop-filter: blur(8rpx);
|
||
}
|
||
|
||
.notice-action-btn {
|
||
width: 100%;
|
||
height: 88rpx;
|
||
padding: 0 40rpx;
|
||
border-radius: 999rpx;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
background: linear-gradient(135deg, #2B8EFF 0%, #1F6FE5 100%);
|
||
color: #ffffff;
|
||
box-shadow: 0 14rpx 28rpx rgba(43, 142, 255, 0.22);
|
||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||
}
|
||
|
||
.notice-action-btn:active {
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.offline-payment-sheet {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24rpx;
|
||
}
|
||
|
||
.offline-payment-intro {
|
||
padding: 24rpx 28rpx;
|
||
border-radius: 28rpx;
|
||
background: linear-gradient(180deg, rgba(43, 142, 255, 0.08) 0%, rgba(31, 111, 229, 0.04) 100%);
|
||
border: 1rpx solid rgba(43, 142, 255, 0.08);
|
||
}
|
||
|
||
.offline-payment-intro__zh {
|
||
font-size: 28rpx;
|
||
line-height: 1.6;
|
||
color: #22324D;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.offline-payment-intro__en {
|
||
margin-top: 12rpx;
|
||
font-size: 22rpx;
|
||
line-height: 1.7;
|
||
color: #5B6B86;
|
||
}
|
||
|
||
.offline-payment-card {
|
||
padding: 24rpx 28rpx;
|
||
border-radius: 28rpx;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
border: 1rpx solid rgba(43, 142, 255, 0.08);
|
||
box-shadow: 0 16rpx 36rpx rgba(43, 142, 255, 0.08);
|
||
}
|
||
|
||
.offline-payment-row {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12rpx;
|
||
padding: 22rpx 0;
|
||
}
|
||
|
||
.offline-payment-row + .offline-payment-row {
|
||
border-top: 1rpx solid rgba(43, 142, 255, 0.08);
|
||
}
|
||
|
||
.offline-payment-label-group,
|
||
.offline-payment-value-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.offline-payment-label {
|
||
font-size: 24rpx;
|
||
line-height: 1.5;
|
||
color: #6B7A92;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.offline-payment-label-en {
|
||
font-size: 20rpx;
|
||
line-height: 1.6;
|
||
color: #8B97AA;
|
||
}
|
||
|
||
.offline-payment-value {
|
||
font-size: 28rpx;
|
||
line-height: 1.6;
|
||
color: #22324D;
|
||
font-weight: 600;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.offline-payment-value-en {
|
||
font-size: 22rpx;
|
||
line-height: 1.7;
|
||
color: #4F5F79;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.offline-payment-note-title {
|
||
font-size: 28rpx;
|
||
line-height: 1.4;
|
||
color: #22324D;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.offline-payment-note-title-en {
|
||
margin-top: 8rpx;
|
||
font-size: 22rpx;
|
||
line-height: 1.6;
|
||
color: #6B7A92;
|
||
}
|
||
|
||
.offline-payment-note {
|
||
margin-top: 22rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.offline-payment-note__zh {
|
||
font-size: 25rpx;
|
||
line-height: 1.7;
|
||
color: #22324D;
|
||
}
|
||
|
||
.offline-payment-note__en {
|
||
font-size: 21rpx;
|
||
line-height: 1.7;
|
||
color: #5B6B86;
|
||
}
|
||
|
||
</style>
|