Files
QSY-lvjuxcx/pkgService/submit_request/submit_request.vue
liangjian 9fee2c3a74 feat: 优化首页服务入口与租住业务流程
补齐首页待办跳转、押金支付引导和多类租住服务页面能力,统一租客端关键业务流程体验。
2026-08-25 13:53:12 +08:00

2286 lines
77 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="submit-request-page tn-safe-area-inset-bottom">
<tn-nav-bar
:isBack="true"
:backTitle="$tr('返回')"
:backgroundColor="navBackgroundColor"
fontColor="#1A2233"
:bottomShadow="false"
:fixed="true"
>
<view class="nav-title" :style="{ opacity: navTitleOpacity }">{{ pageTitle }}</view>
</tn-nav-bar>
<view class="content-wrapper" :style="{ paddingTop: vuex_custom_bar_height + 'px' }">
<!-- 顶部信息卡片 -->
<view class="hero-card">
<view class="hero-glow"></view>
<view class="hero-inner tn-flex tn-flex-row-between tn-flex-col-center">
<view class="hero-content">
<view class="hero-tag">
<text class="tn-icon-home hero-tag-icon"></text>
<text class="hero-tag-text">{{ $tr('入住申请') }}</text>
</view>
<view class="hero-title">{{ pageTitle }}</view>
<view class="hero-desc">{{ pageDesc }}</view>
</view>
<view class="hero-figure">
<view class="figure-ring figure-ring--outer"></view>
<view class="figure-ring figure-ring--inner"></view>
<text class="tn-icon-home figure-icon"></text>
</view>
</view>
<view class="hero-accent"></view>
</view>
<!-- 申请信息表单卡片 -->
<view class="form-card">
<view class="card-header tn-flex tn-flex-col-bottom">
<view class="header-icon header-icon--primary">
<text class="tn-icon-edit"></text>
</view>
<view class="header-titles">
<view class="card-title">{{ formBlockTitle }}</view>
<view class="card-subtitle">{{ formBlockDesc }}</view>
</view>
</view>
<view v-if="showRoomTypeSelector" class="field-item">
<view class="field-label">
<text class="field-dot field-dot--primary"></text>
<text>{{ roomTypeLabel }}</text>
</view>
<room-type-selector
v-model="form.intendedRoomTypeCode"
:initial-area-code="form.intendedAreaCode"
@change="handleRoomTypeChange"
/>
</view>
<view v-if="showFloorSelector" class="field-item">
<view class="field-label">
<text class="field-dot field-dot--luxury"></text>
<text>{{ floorLabel }}</text>
</view>
<floor-selector
v-model="form.intendedFloorLevel"
@change="handleFloorChange"
/>
</view>
<view v-if="showPreferenceSelectors" class="field-item">
<view class="field-label">
<text class="field-dot field-dot--primary"></text>
<text>{{ $tr('兴趣爱好') }}</text>
</view>
<tag-selector
v-model="form.hobbiesAndInterestsTagKeys"
:options="hobbyOptions"
:loading="dictLoading"
:emptyText="$tr('暂无兴趣爱好标签')"
/>
</view>
<view v-if="showPreferenceSelectors" class="field-item">
<view class="field-label">
<text class="field-dot field-dot--primary"></text>
<text>{{ $tr('生活习惯') }}</text>
</view>
<tag-selector
v-model="form.lifestyleHabitsTagKeys"
:options="lifestyleOptions"
:loading="dictLoading"
:emptyText="$tr('暂无生活习惯标签')"
/>
</view>
<view v-if="showDateRange" class="field-item">
<view class="field-label">
<text class="field-dot field-dot--muted"></text>
<text>{{ dateRangeLabel }}</text>
</view>
<date-range-selector
:start.sync="form.start"
:end.sync="form.end"
:start-label="dateRangeStartLabel"
:end-label="dateRangeEndLabel"
:start-placeholder="dateRangeStartPlaceholder"
:end-placeholder="dateRangeEndPlaceholder"
/>
</view>
<view class="field-item field-item--last">
<view class="field-label">
<text class="field-dot field-dot--muted"></text>
<text>{{ $tr('补充说明') }}</text>
</view>
<textarea
class="form-textarea"
:placeholder="remarkPlaceholder"
placeholder-style="color: #A1B0C4;"
v-model="form.remark"
/>
</view>
</view>
<view v-if="!showFloatingSubmitBtn" class="footer-spacer"></view>
<view v-else class="footer-spacer footer-spacer--floating"></view>
</view>
<!-- 底部操作区 -->
<view v-if="showFloatingSubmitBtn || !showFloatingSubmitBtn" class="footer-bar tn-safe-area-inset-bottom">
<view class="footer-inner">
<view class="submit-btn" :class="{ 'submit-btn--active': canSubmit }" @click="submitRequest">
<text class="submit-btn-icon tn-icon-send"></text>
<text class="submit-btn-text">{{ isSubmitting ? $tr('提交中...') : primaryActionText }}</text>
</view>
<view class="footer-hint">{{ $tr('提交后专属管家将尽快与您联系') }}</view>
</view>
</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-title">{{ $tr('租房定金须知') }}</view>
<view v-if="pendingDepositPaymentContext && pendingDepositPaymentContext.paymentNo" class="popup-subtitle">
{{ $tr('支付号') }}:{{ pendingDepositPaymentContext.paymentNo }}
</view>
</view>
<scroll-view scroll-y class="popup-body" @scrolltolower="onNoticeScrollToBottom">
<view class="notice-content">
<view class="notice-item">
<view class="notice-item-zh">1、请在 5 分钟内支付定金或者款项,否则系统将自动取消这个订单,释放锁定的房源。</view>
<view class="notice-item-en">Please pay the deposit or full payment within 5 minutes. Otherwise, the system will automatically cancel this order and release the reserved housing unit.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">2、缴纳线上定金可锁定房源并预约专属看房,平台收取定金后该房源临时下架,不再接待其他客户看房。</view>
<view class="notice-item-en">Payment of the online deposit reserves the housing unit and arranges exclusive on-site viewing for you. After receiving the deposit, we will temporarily remove the property from the rental list and stop arranging viewings for other clients.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">3、请于约定时间完成实地看房,并在看房结束后 3 个工作日内签署租赁合同;若逾期未看房或未完成签约,视作您主动放弃房源,定金不予退还。</view>
<view class="notice-item-en">You shall complete the on-site viewing at the agreed time and sign the rental contract within 3 working days after the viewing. Failure to complete the viewing or sign the contract within the time limit shall be deemed your voluntary waiver of the property, and the deposit will not be refunded.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">4、如因租客个人原因取消看房、看房后不愿承租房屋或拒绝签署租赁合同,定金不予退还。</view>
<view class="notice-item-en">The deposit is non-refundable if you cancel the viewing, refuse to rent the property or decline to sign the contract due to personal reasons.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">5、若因我方房源信息虚假、房屋无法正常交付等我方过错,造成无法看房或签约,定金将全额按原支付路径退还。</view>
<view class="notice-item-en">If the viewing or contract signing cannot be conducted due to our liabilities such as false property information or failure to deliver the housing normally, the full deposit will be refunded via your original payment channel.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">6、待双方正式签署租赁合同后,该定金可直接抵扣首期租金。</view>
<view class="notice-item-en">After both parties formally sign the rental contract, this deposit may be directly deducted from your initial rent.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">7、您支付定金,即代表已完整知晓房源全部信息,并自愿接受本须知全部条款约束。</view>
<view class="notice-item-en">By making the deposit payment, you confirm that you have fully reviewed all property information and voluntarily accept all terms and conditions set forth in this notice.</view>
</view>
</view>
</scroll-view>
<view class="popup-footer">
<view
class="notice-action-btn"
:class="{ 'notice-action-btn--disabled': !hasReadNotice }"
@click="confirmAndPayDeposit"
>
<text class="tn-text-bold">{{ $tr('同意并继续') }}</text>
<text v-if="!hasReadNotice" class="tn-margin-left-xs tn-text-sm">({{ $tr('请滑动阅读至底部') }})</text>
</view>
</view>
</view>
</tn-popup>
<tn-popup v-model="showPaymentMethodPopup" mode="bottom" length="72%" :borderRadius="32" :closeBtn="true" @close="closePaymentMethodPopup">
<view class="payment-method-popup">
<view class="payment-method-popup__header">
<view class="payment-method-popup__eyebrow">{{ $tr('支付方式') }}</view>
<view class="payment-method-popup__title">{{ $tr('选择定金支付方式') }}</view>
<view class="payment-method-popup__desc">
{{ language === 'en'
? 'Choose the channel you want to use for the room-type application deposit.'
: '请选择房型申请定金的支付渠道,提交后可在“我的入住”继续查看进度。' }}
</view>
</view>
<view class="payment-method-popup__summary">
<view class="payment-method-popup__summary-label">{{ $tr('支付号') }}</view>
<view class="payment-method-popup__summary-value">
{{ (pendingPaymentMethodContext && pendingPaymentMethodContext.paymentNo) || $tr('提交后生成') }}
</view>
</view>
<view class="payment-method-card payment-method-card--wechat" @click="handlePaymentMethodSelect('wechat')">
<view class="payment-method-card__main">
<view class="payment-method-card__icon payment-method-card__icon--wechat">
<text class="tn-icon-wechat"></text>
</view>
<view class="payment-method-card__copy">
<view class="payment-method-card__title-row">
<view class="payment-method-card__title">{{ $tr('微信支付') }}</view>
<view class="payment-method-card__tag">{{ $tr('推荐') }}</view>
</view>
<view class="payment-method-card__desc">
{{ language === 'en'
? 'Continue with the standard online payment flow.'
: '继续拉起线上支付,适合直接完成定金支付。' }}
</view>
</view>
</view>
<view class="payment-method-card__arrow">
<text class="tn-icon-right"></text>
</view>
</view>
<view class="payment-method-card payment-method-card--offline" @click="handlePaymentMethodSelect('offline')">
<view class="payment-method-card__main">
<view class="payment-method-card__icon payment-method-card__icon--offline">
<text class="tn-icon-bank-card"></text>
</view>
<view class="payment-method-card__copy">
<view class="payment-method-card__title-row">
<view class="payment-method-card__title">{{ $tr('其他支付方式') }}</view>
</view>
<view class="payment-method-card__desc">
{{ language === 'en'
? 'View the configured phone, QR code, and offline payment instructions.'
: '查看后台配置的联系电话、二维码与线下支付说明。' }}
</view>
</view>
</view>
<view class="payment-method-card__arrow">
<text class="tn-icon-right"></text>
</view>
</view>
</view>
</tn-popup>
<deposit-other-payment-popup
v-model="showOfflinePaymentNotice"
:config="depositOtherPaymentConfig"
:loading="depositOtherPaymentConfigLoading"
@input="handleOfflinePaymentPopupInput"
/>
</view>
</template>
<script>
import { accountQueryBill, accountQueryBills, applicationFormDetail, paymentBillPay, paymentPayingBills, submitApplicationForm, sysdictPage } from '@/api/index.js'
import DepositOtherPaymentPopup from '@/components/DepositOtherPaymentPopup.vue'
import { saveActiveApplicationCode, saveHomePageTabId } from '@/utils/app-state'
import { createDefaultDepositOtherPaymentConfig, loadDepositOtherPaymentConfig } from '@/utils/deposit-other-payment'
import { compactEnglishText, localizePayMethodText } from '../../utils/select-room-normalizers'
import { getStoredUser } from '../../utils/flow'
import TagSelector from './components/TagSelector.vue'
import DateRangeSelector from './components/DateRangeSelector.vue'
import RoomTypeSelector from './components/RoomTypeSelector.vue'
import FloorSelector from './components/FloorSelector.vue'
function createDefaultForm() {
const now = new Date()
const pad = n => String(n).padStart(2, '0')
const defaultStart = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`
return {
intendedAreaCode: '',
intendedRoomTypeCode: '',
intendedFloorLevel: '',
roomSlotHeight: 1,
hobbiesAndInterestsTagKeys: [],
lifestyleHabitsTagKeys: [],
remark: '',
start: defaultStart,
end: ''
}
}
const DICT_TEXT_FALLBACK_MAP = {
hobbies: {
'打篮球': 'Play Basketball',
'打羽毛球': 'Play Badminton',
'唱歌': 'Sing',
'电竞': 'Esports Games',
'电竞游戏': 'Esports Games',
'旅游': 'Travel'
},
lifestyle: {
'晚睡': 'Stay Up Late',
'早睡': 'Sleep Early',
'早起': 'Wake Up Early',
'安静': 'Quiet',
'整洁': 'Tidy',
'爱干净': 'Neat'
}
}
function getOptionTextCandidates(item = {}) {
return [
item.value,
item.label,
item.description,
item.name,
item.cname,
item.nameZh,
item.labelZh,
item.valueZh,
item.descriptionZh,
item.ename,
item.nameEn,
item.labelEn,
item.valueEn,
item.descriptionEn,
item.code
].map(value => String(value || '').trim()).filter(Boolean)
}
function getDictFallbackText(item = {}, language = 'zh', type = '') {
if (language !== 'en') {
return ''
}
const fallbackMap = DICT_TEXT_FALLBACK_MAP[type] || {}
const baseText = [
item.description,
item.value,
item.label,
item.name,
item.cname,
item.nameZh,
item.labelZh,
item.valueZh,
item.descriptionZh
].map(value => String(value || '').trim()).find(Boolean)
return baseText ? (fallbackMap[baseText] || '') : ''
}
function getLocalizedOptionText(item = {}, language = 'zh', type = '') {
const isEn = language === 'en'
const preferred = isEn
? [item.descriptionEn, item.ename, item.nameEn, item.labelEn, item.valueEn, item.description, item.value, item.label, item.name, item.cname, item.code]
: [item.description, item.cname, item.nameZh, item.labelZh, item.valueZh, item.value, item.label, item.name, item.code]
const matched = preferred.map(value => String(value || '').trim()).find(Boolean)
if (!matched) {
return getDictFallbackText(item, language, type)
}
if (isEn) {
const fallbackMap = DICT_TEXT_FALLBACK_MAP[type] || {}
if (fallbackMap[matched]) {
return fallbackMap[matched]
}
const zhText = [item.label, item.name, item.cname, item.nameZh, item.labelZh, item.description, item.value, item.valueZh, item.descriptionZh]
.map(value => String(value || '').trim())
.find(Boolean)
if (zhText && fallbackMap[zhText]) {
return fallbackMap[zhText]
}
if (zhText && matched === zhText) {
return getDictFallbackText(item, language, type) || matched
}
}
return matched
}
function normalizeDictList(response, language = 'zh', type = '') {
const raw = response && response.data ? (response.data.data || response.data) : response
const list = Array.isArray(raw) ? raw : []
return list
.map(item => ({
key: item && (item.key || item.code || item.value || ''),
value: getLocalizedOptionText(item, language, type) || getOptionTextCandidates(item)[0] || ''
}))
.filter(item => item.key && item.value)
}
function normalizeDateTime(date, endOfDay = false) {
if (!date) {
return ''
}
return `${date}T${endOfDay ? '23:59:59' : '00:00:00'}`
}
function buildRoomLabel(payload = {}) {
return payload.roomLabel || payload.currentRoomLabel || payload.roomName || ''
}
function isPlainObject(value) {
return !!value && typeof value === 'object' && !Array.isArray(value)
}
function normalizeCodeValue(value) {
if (value === undefined || value === null) {
return ''
}
if (typeof value === 'string' || typeof value === 'number') {
return String(value).trim()
}
return ''
}
function extractSubmitApplicationCode(response) {
const payload = response && response.data !== undefined ? response.data : response
const directValues = [
payload,
payload && payload.data,
payload && payload.result,
payload && payload.record,
payload && payload.application
]
for (let i = 0; i < directValues.length; i += 1) {
const directCode = normalizeCodeValue(directValues[i])
if (directCode) {
return directCode
}
}
const sources = directValues.filter(isPlainObject)
const codeFields = ['detailCode', 'code', 'applicationCode', 'formCode', 'referenceTypeNo', 'no']
for (let i = 0; i < sources.length; i += 1) {
const current = sources[i]
for (let j = 0; j < codeFields.length; j += 1) {
const nextCode = normalizeCodeValue(current[codeFields[j]])
if (nextCode) {
return nextCode
}
}
}
return ''
}
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 parseResponseObject(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 {}
}
function parseResponseList(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 []
}
function parseExpireTimeToTimestamp(value) {
if (value === null || 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
}
const BUSINESS_TYPE_CONFIG = {
'change-room': {
title: '换房申请',
titleEn: 'Change Room Request',
titleEnShort: 'Room Change',
desc: '请填写期望房型、计划换房时间和补充说明,提交后由管家继续跟进。',
descEn: 'Fill in the expected room type, move date, and notes. Your butler will follow up after submission.',
formBlockTitle: '换房申请信息',
formBlockTitleEn: 'Change Room Details',
formBlockDesc: '将通过真实申请接口提交换房意向。',
formBlockDescEn: 'Your change room request will be submitted through the live application API.',
primaryActionText: '提交换房申请',
primaryActionTextEn: 'Submit Change Room Request',
remarkPlaceholder: '请输入换房原因、房间偏好和其他补充说明',
remarkPlaceholderEn: 'Enter the reason, room preference, and additional notes',
roomTypeLabel: '期望房型',
roomTypeLabelEn: 'Expected Room Type',
dateRangeLabel: '时间安排',
dateRangeLabelEn: 'Schedule',
dateRangeStartLabel: '期望换房',
dateRangeStartLabelEn: 'Move Date',
dateRangeEndLabel: '预计入住',
dateRangeEndLabelEn: 'Expected Check-in',
dateRangeStartPlaceholder: '请选择换房日期',
dateRangeStartPlaceholderEn: 'Select move date',
dateRangeEndPlaceholder: '请选择入住日期',
dateRangeEndPlaceholderEn: 'Select check-in date',
showRoomTypeSelector: true,
showPreferenceSelectors: false,
showDateRange: true,
buildDefaultRemark(payload = {}) {
const roomLabel = buildRoomLabel(payload)
const language = payload.language || 'zh'
if (language === 'en') {
return roomLabel
? `I am currently staying in "${roomLabel}" and would like to request a room change. Please help confirm available room types and processing time.`
: 'I would like to request a room change. Please help confirm available room types and processing time.'
}
return roomLabel
? `我当前居住房间为“${roomLabel}”,现申请换房,请协助确认可调整房型和办理时间。`
: '我想申请换房,请协助确认可调整房型和办理时间。'
}
},
'renew-stay': {
title: '续住申请',
titleEn: 'Renew Stay Request',
titleEnShort: 'Renew Stay',
desc: '请填写计划续住时间和补充说明,提交后由管家继续跟进。',
descEn: 'Fill in the renewal period and notes. Your butler will follow up after submission.',
formBlockTitle: '续住申请信息',
formBlockTitleEn: 'Renewal Details',
formBlockDesc: '将通过真实申请接口提交续住意向。',
formBlockDescEn: 'Your renewal request will be submitted through the live application API.',
primaryActionText: '提交续住申请',
primaryActionTextEn: 'Submit Renewal Request',
remarkPlaceholder: '请输入续住原因、计划续住时长和其他说明',
remarkPlaceholderEn: 'Enter the renewal reason, duration, and notes',
dateRangeLabel: '续住时间',
dateRangeLabelEn: 'Renewal Period',
dateRangeStartLabel: '当前到期',
dateRangeStartLabelEn: 'Current End Date',
dateRangeEndLabel: '计划续住至',
dateRangeEndLabelEn: 'Renew Until',
dateRangeStartPlaceholder: '请选择当前到期日',
dateRangeStartPlaceholderEn: 'Select current end date',
dateRangeEndPlaceholder: '请选择续住到期日',
dateRangeEndPlaceholderEn: 'Select renewed end date',
showRoomTypeSelector: false,
showPreferenceSelectors: false,
showDateRange: true,
buildDefaultRemark(payload = {}) {
const roomLabel = buildRoomLabel(payload)
const language = payload.language || 'zh'
if (language === 'en') {
return roomLabel
? `I am currently staying in "${roomLabel}" and would like to renew my stay. Please help confirm the renewal process.`
: 'I would like to renew my stay. Please help confirm the renewal process.'
}
return roomLabel
? `我当前居住房间为“${roomLabel}”,现申请续住,请协助确认续住办理流程。`
: '我想申请续住,请协助确认续住办理流程。'
}
},
checkout: {
title: '退房申请',
titleEn: 'Checkout Request',
titleEnShort: 'Check Out',
desc: '请填写计划退房时间和离场说明,提交后由管家安排后续验房流程。',
descEn: 'Fill in the checkout schedule and departure notes. Your butler will arrange the inspection process.',
formBlockTitle: '退房申请信息',
formBlockTitleEn: 'Checkout Details',
formBlockDesc: '将通过真实申请接口提交退房意向。',
formBlockDescEn: 'Your checkout request will be submitted through the live application API.',
primaryActionText: '提交退房申请',
primaryActionTextEn: 'Submit Checkout Request',
remarkPlaceholder: '请输入退房原因、物品处理说明和其他补充内容',
remarkPlaceholderEn: 'Enter the checkout reason, belongings arrangement, and notes',
dateRangeLabel: '退房安排',
dateRangeLabelEn: 'Checkout Schedule',
dateRangeStartLabel: '计划退房',
dateRangeStartLabelEn: 'Planned Checkout',
dateRangeEndLabel: '最晚离场',
dateRangeEndLabelEn: 'Latest Departure',
dateRangeStartPlaceholder: '请选择退房日期',
dateRangeStartPlaceholderEn: 'Select checkout date',
dateRangeEndPlaceholder: '请选择离场日期',
dateRangeEndPlaceholderEn: 'Select departure date',
showRoomTypeSelector: false,
showPreferenceSelectors: false,
showDateRange: true,
buildDefaultRemark(payload = {}) {
const roomLabel = buildRoomLabel(payload)
const language = payload.language || 'zh'
if (language === 'en') {
return roomLabel
? `I am currently staying in "${roomLabel}" and would like to check out. Please help arrange the inspection and departure process.`
: 'I would like to check out. Please help arrange the inspection and departure process.'
}
return roomLabel
? `我当前居住房间为“${roomLabel}”,现申请退房,请协助安排后续验房和离场流程。`
: '我想申请退房,请协助安排后续验房和离场流程。'
}
},
repair: {
title: '设备报修',
titleEn: 'Repair Request',
titleEnShort: 'Repair',
desc: '请填写故障位置、问题描述和期望上门时间,提交后由管家安排处理。',
descEn: 'Fill in the issue location, problem details, and preferred visit time. Your butler will arrange service.',
formBlockTitle: '报修信息',
formBlockTitleEn: 'Repair Details',
formBlockDesc: '将通过真实申请接口提交报修需求。',
formBlockDescEn: 'Your repair request will be submitted through the live application API.',
primaryActionText: '提交报修申请',
primaryActionTextEn: 'Submit Repair Request',
remarkPlaceholder: '请输入故障位置、问题现象和期望处理时间',
remarkPlaceholderEn: 'Enter the issue location, symptoms, and preferred handling time',
dateRangeLabel: '上门时间',
dateRangeLabelEn: 'Visit Time',
dateRangeStartLabel: '期望上门',
dateRangeStartLabelEn: 'Preferred Visit',
dateRangeEndLabel: '可接受至',
dateRangeEndLabelEn: 'Available Until',
dateRangeStartPlaceholder: '请选择期望时间',
dateRangeStartPlaceholderEn: 'Select preferred time',
dateRangeEndPlaceholder: '请选择最晚时间',
dateRangeEndPlaceholderEn: 'Select latest time',
showRoomTypeSelector: false,
showPreferenceSelectors: false,
showDateRange: true,
buildDefaultRemark(payload = {}) {
const roomLabel = buildRoomLabel(payload)
const language = payload.language || 'zh'
if (language === 'en') {
return roomLabel
? `I am currently staying in "${roomLabel}" and need help with a device repair. Please arrange service support.`
: 'I need help with a device repair. Please arrange service support.'
}
return roomLabel
? `我当前居住房间为“${roomLabel}”,有设备需要报修,请协助安排人员处理。`
: '我有设备需要报修,请协助安排人员处理。'
}
},
laundry: {
title: '洗衣房预约',
titleEn: 'Laundry Room Booking',
titleEnShort: 'Laundry',
desc: '请填写预约时间和补充说明,提交后由管家确认场地安排。',
descEn: 'Fill in the booking time and notes. Your butler will confirm the venue arrangement.',
formBlockTitle: '预约信息',
formBlockTitleEn: 'Booking Details',
formBlockDesc: '将通过真实申请接口提交洗衣房预约需求。',
formBlockDescEn: 'Your laundry room booking will be submitted through the live application API.',
primaryActionText: '提交洗衣房预约',
primaryActionTextEn: 'Submit Laundry Booking',
remarkPlaceholder: '请输入预约需求、设备偏好和其他说明',
remarkPlaceholderEn: 'Enter the booking request, equipment preference, and notes',
dateRangeLabel: '预约时间',
dateRangeLabelEn: 'Booking Time',
dateRangeStartLabel: '预约开始',
dateRangeStartLabelEn: 'Start Time',
dateRangeEndLabel: '预约结束',
dateRangeEndLabelEn: 'End Time',
dateRangeStartPlaceholder: '请选择开始时间',
dateRangeStartPlaceholderEn: 'Select start time',
dateRangeEndPlaceholder: '请选择结束时间',
dateRangeEndPlaceholderEn: 'Select end time',
showRoomTypeSelector: false,
showPreferenceSelectors: false,
showDateRange: true,
buildDefaultRemark(payload = {}) {
if ((payload.language || 'zh') === 'en') {
return 'I would like to book the laundry room. Please help confirm available time slots and equipment arrangements.'
}
return '我想预约洗衣房,请协助确认可用时段和设备安排。'
}
},
gym: {
title: '健身房预约',
titleEn: 'Gym Booking',
titleEnShort: 'Gym',
desc: '请填写预约时间和补充说明,提交后由管家确认场地安排。',
descEn: 'Fill in the booking time and notes. Your butler will confirm the venue arrangement.',
formBlockTitle: '预约信息',
formBlockTitleEn: 'Booking Details',
formBlockDesc: '将通过真实申请接口提交健身房预约需求。',
formBlockDescEn: 'Your gym booking will be submitted through the live application API.',
primaryActionText: '提交健身房预约',
primaryActionTextEn: 'Submit Gym Booking',
remarkPlaceholder: '请输入预约需求、人数和其他说明',
remarkPlaceholderEn: 'Enter the booking request, attendee count, and notes',
dateRangeLabel: '预约时间',
dateRangeLabelEn: 'Booking Time',
dateRangeStartLabel: '预约开始',
dateRangeStartLabelEn: 'Start Time',
dateRangeEndLabel: '预约结束',
dateRangeEndLabelEn: 'End Time',
dateRangeStartPlaceholder: '请选择开始时间',
dateRangeStartPlaceholderEn: 'Select start time',
dateRangeEndPlaceholder: '请选择结束时间',
dateRangeEndPlaceholderEn: 'Select end time',
showRoomTypeSelector: false,
showPreferenceSelectors: false,
showDateRange: true,
buildDefaultRemark(payload = {}) {
if ((payload.language || 'zh') === 'en') {
return 'I would like to book the gym. Please help confirm available time slots.'
}
return '我想预约健身房,请协助确认可用时段。'
}
},
kitchen: {
title: '公共厨房预约',
titleEn: 'Shared Kitchen Booking',
titleEnShort: 'Kitchen',
desc: '请填写预约时间和补充说明,提交后由管家确认场地安排。',
descEn: 'Fill in the booking time and notes. Your butler will confirm the venue arrangement.',
formBlockTitle: '预约信息',
formBlockTitleEn: 'Booking Details',
formBlockDesc: '将通过真实申请接口提交公共厨房预约需求。',
formBlockDescEn: 'Your shared kitchen booking will be submitted through the live application API.',
primaryActionText: '提交公共厨房预约',
primaryActionTextEn: 'Submit Kitchen Booking',
remarkPlaceholder: '请输入预约需求、人数和其他说明',
remarkPlaceholderEn: 'Enter the booking request, attendee count, and notes',
dateRangeLabel: '预约时间',
dateRangeLabelEn: 'Booking Time',
dateRangeStartLabel: '预约开始',
dateRangeStartLabelEn: 'Start Time',
dateRangeEndLabel: '预约结束',
dateRangeEndLabelEn: 'End Time',
dateRangeStartPlaceholder: '请选择开始时间',
dateRangeStartPlaceholderEn: 'Select start time',
dateRangeEndPlaceholder: '请选择结束时间',
dateRangeEndPlaceholderEn: 'Select end time',
showRoomTypeSelector: false,
showPreferenceSelectors: false,
showDateRange: true,
buildDefaultRemark(payload = {}) {
if ((payload.language || 'zh') === 'en') {
return 'I would like to book the shared kitchen. Please help confirm available time slots.'
}
return '我想预约公共厨房,请协助确认可用时段。'
}
},
'butler-contact': {
title: '联系管家',
titleEn: 'Contact Butler',
titleEnShort: 'Butler',
desc: '请填写需要协助的事项和补充说明,提交后由管家跟进处理。',
descEn: 'Fill in the request details and notes. Your butler will follow up after submission.',
formBlockTitle: '沟通信息',
formBlockTitleEn: 'Request Details',
formBlockDesc: '将通过真实申请接口提交沟通需求。',
formBlockDescEn: 'Your request will be submitted through the live application API.',
primaryActionText: '提交联系事项',
primaryActionTextEn: 'Submit Request',
remarkPlaceholder: '请输入希望联系管家的事项内容',
remarkPlaceholderEn: 'Enter what you would like help from your butler with',
showRoomTypeSelector: false,
showPreferenceSelectors: false,
showDateRange: false,
buildDefaultRemark(payload = {}) {
if ((payload.language || 'zh') === 'en') {
return 'I need to contact my butler for an in-stay request. Please assist as soon as possible.'
}
return '我需要联系管家处理住中事项,请尽快协助。'
}
}
}
export default {
components: {
DepositOtherPaymentPopup,
TagSelector,
DateRangeSelector,
RoomTypeSelector,
FloorSelector
},
data() {
return {
sourceTitleZh: '',
sourceTitleEn: '',
sourceDescZh: '',
sourceDescEn: '',
rawSourceTitle: '',
rawSourceDesc: '',
sourcePayload: {},
isSubmitting: false,
dictLoading: false,
hobbyOptions: [],
lifestyleOptions: [],
form: createDefaultForm(),
scrollTop: 0,
lastAutoRemark: '',
showDepositNotice: false,
showPaymentMethodPopup: false,
showOfflinePaymentNotice: false,
hasReadNotice: false,
isClosingDepositNoticeForPayment: false,
isSwitchingPaymentMethodPopup: false,
pendingDepositPaymentContext: null,
pendingPaymentMethodContext: null,
depositOtherPaymentConfig: createDefaultDepositOtherPaymentConfig(),
depositOtherPaymentConfigLoading: false
}
},
computed: {
language() {
return this.$getLanguage()
},
navBackgroundColor() {
return this.scrollTop > 20 ? '#F8FAFF' : 'transparent'
},
navTitleOpacity() {
return Math.min(Math.max((this.scrollTop - 40) / 50, 0), 1)
},
businessType() {
return this.sourcePayload && this.sourcePayload.businessType ? this.sourcePayload.businessType : ''
},
businessConfig() {
return BUSINESS_TYPE_CONFIG[this.businessType] || null
},
isRoomTypeApply() {
return this.sourcePayload && this.sourcePayload.sourceType === 'area-room-type'
},
pageTitle() {
if (this.isRoomTypeApply) {
return this.language === 'en'
? compactEnglishText(this.$tr('房型申请'), 22)
: this.$tr('房型申请')
}
return this.getBusinessText('titleShort')
|| this.getBusinessText('title')
|| this.getLocalizedSourceText('title')
|| this.$tr('入住申请')
},
pageDesc() {
if (this.isRoomTypeApply) {
return this.language === 'en'
? compactEnglishText(this.$tr('请确认房型、区域、偏好标签和租期信息后提交申请。'), 96)
: this.$tr('请确认房型、区域、偏好标签和租期信息后提交申请。')
}
const text = this.getBusinessText('desc') || this.getLocalizedSourceText('desc') || this.$tr('请填写您的入住意向、生活偏好与预计租期后提交申请单。')
return this.language === 'en' ? compactEnglishText(text, 96) : text
},
formBlockTitle() {
if (this.isRoomTypeApply) {
return this.$tr('房型申请信息')
}
return this.getBusinessText('formBlockTitle') || this.$tr('申请信息')
},
formBlockDesc() {
if (this.isRoomTypeApply) {
return this.$tr('已接入真实申请单接口,支持带出房型与区域编码后直接提交。')
}
return this.getBusinessText('formBlockDesc') || this.$tr('请填写您的入住意向、生活偏好与预计租期后提交申请单。')
},
remarkPlaceholder() {
if (this.isRoomTypeApply) {
return this.$tr('请输入您的申请补充说明')
}
return this.getBusinessText('remarkPlaceholder') || this.$tr('请输入补充说明')
},
primaryActionText() {
if (this.isRoomTypeApply) {
return this.language === 'en'
? compactEnglishText(this.$tr('提交房型申请'), 24)
: this.$tr('提交房型申请')
}
const text = this.getBusinessText('primaryActionText') || this.$tr('提交申请')
return this.language === 'en' ? compactEnglishText(text, 24) : text
},
roomTypeLabel() {
return this.getBusinessText('roomTypeLabel') || this.$tr('申请房型')
},
showFloatingSubmitBtn() {
return this.isRoomTypeApply
},
floorLabel() {
return this.getBusinessText('floorLabel') || this.$tr('楼层')
},
showRoomTypeSelector() {
if (this.isRoomTypeApply) {
return true
}
return this.businessConfig ? this.businessConfig.showRoomTypeSelector !== false : true
},
showFloorSelector() {
if (this.isRoomTypeApply) {
return true
}
return this.businessConfig ? this.businessConfig.showFloorSelector !== false : true
},
showPreferenceSelectors() {
if (this.isRoomTypeApply) {
return true
}
return this.businessConfig ? !!this.businessConfig.showPreferenceSelectors : true
},
showDateRange() {
if (this.isRoomTypeApply) {
return true
}
return this.businessConfig ? this.businessConfig.showDateRange !== false : true
},
dateRangeLabel() {
return this.getBusinessText('dateRangeLabel') || this.$tr('租期时间')
},
dateRangeStartLabel() {
return this.getBusinessText('dateRangeStartLabel') || this.$tr('预计入住')
},
dateRangeEndLabel() {
return this.getBusinessText('dateRangeEndLabel') || this.$tr('预计离开')
},
dateRangeStartPlaceholder() {
return this.getBusinessText('dateRangeStartPlaceholder') || this.$tr('请选择入住日期')
},
dateRangeEndPlaceholder() {
return this.getBusinessText('dateRangeEndPlaceholder') || this.$tr('请选择离开日期')
},
canSubmit() {
if (this.showDateRange) {
if (!this.form.start || !this.form.end) {
return false
}
if (new Date(this.form.end).getTime() < new Date(this.form.start).getTime()) {
return false
}
}
return !!this.form.remark.trim()
}
},
watch: {
language() {
this.syncLocalizedDraftContent()
this.loadDictOptions()
if (this.showOfflinePaymentNotice) {
this.loadDepositOtherPaymentConfig()
}
}
},
onPageScroll(event) {
this.scrollTop = (event && event.scrollTop) || 0
},
onLoad(options) {
this.sourceTitleZh = this.decodeRouteText(options && (options.titleZh || options.title), '')
this.sourceTitleEn = this.decodeRouteText(options && options.titleEn, '')
this.sourceDescZh = this.decodeRouteText(options && (options.descZh || options.desc), '')
this.sourceDescEn = this.decodeRouteText(options && options.descEn, '')
this.rawSourceTitle = this.decodeRouteText(options && options.title, '')
this.rawSourceDesc = this.decodeRouteText(options && options.desc, '')
if (options.payload) {
Promise.resolve(options.payload)
.then(decodeURIComponent)
.then(JSON.parse)
.then((payload) => {
this.sourcePayload = payload && typeof payload === 'object' ? payload : {}
})
.catch((error) => {
console.error('parse submit payload error', error)
this.sourcePayload = {}
})
.finally(() => {
this.initFormFromSource()
this.loadDictOptions()
})
return
}
this.initFormFromSource()
this.loadDictOptions()
},
methods: {
decodeRouteText(value, fallback = '') {
if (value === undefined || value === null || value === '') {
return fallback
}
try {
return decodeURIComponent(String(value))
} catch (error) {
return String(value)
}
},
getLocalizedSourceText(type) {
if (type === 'title') {
if (this.language === 'en') {
return this.sourceTitleEn || this.sourceTitleZh || this.rawSourceTitle || ''
}
return this.sourceTitleZh || this.sourceTitleEn || this.rawSourceTitle || ''
}
if (this.language === 'en') {
return this.sourceDescEn || this.sourceDescZh || this.rawSourceDesc || ''
}
return this.sourceDescZh || this.sourceDescEn || this.rawSourceDesc || ''
},
getBusinessText(field) {
if (!this.businessConfig) {
return ''
}
const fieldKey = this.language === 'en'
? `${field}En${field.endsWith('Short') ? '' : ''}`
: field
if (this.language === 'en' && field === 'titleShort') {
return this.businessConfig.titleEnShort || this.businessConfig.titleEn || this.businessConfig.title || ''
}
return this.businessConfig[fieldKey] || this.businessConfig[field] || ''
},
syncLocalizedDraftContent() {
// 备注不再预填默认内容,保持用户手动输入
this.lastAutoRemark = ''
},
initFormFromSource() {
const defaultForm = createDefaultForm()
this.form = {
...defaultForm,
intendedAreaCode: this.sourcePayload.areaCode || '',
intendedRoomTypeCode: this.sourcePayload.roomTypeCode || '',
intendedFloorLevel: this.sourcePayload.intendedFloorLevel || this.sourcePayload.IntendedFloorLevel || '',
remark: '',
start: this.sourcePayload.start || defaultForm.start,
end: this.sourcePayload.end || ''
}
if (this.sourcePayload.areaCode && !this.sourcePayload.roomTypeCode) {
this.form.intendedAreaCode = this.sourcePayload.areaCode
}
this.form.remark = ''
this.lastAutoRemark = ''
},
handleRoomTypeChange({ area, roomType }) {
if (area && area.code) {
this.form.intendedAreaCode = area.code
}
if (roomType) {
if (!this.sourcePayload) this.sourcePayload = {}
this.sourcePayload.roomTypeCode = roomType.code
this.sourcePayload.roomTypeName = roomType.name
this.sourcePayload.roomTypeNameEn = roomType.nameEn || ''
this.sourcePayload.selectedRoomTypeBuildingCode = roomType.buildingCode || ''
this.sourcePayload.selectedRoomTypeBuildingSectionCode = roomType.buildingSectionCode || ''
this.sourcePayload.selectedRoomTypeBuildingName = roomType.buildingName || ''
this.sourcePayload.selectedRoomTypeBuildingSectionName = roomType.buildingSectionName || ''
if (area) {
this.sourcePayload.areaCode = area.code
this.sourcePayload.areaName = area.name
this.sourcePayload.areaNameEn = area.nameEn || ''
}
if (roomType.price) {
this.sourcePayload.price = roomType.price
}
}
},
handleFloorChange({ floor }) {
if (!this.sourcePayload) {
this.sourcePayload = {}
}
this.sourcePayload.intendedFloorLevel = floor && floor.value ? floor.value : ''
this.sourcePayload.intendedFloorName = floor
? (this.language === 'en' ? (floor.nameEn || floor.name || '') : (floor.name || floor.nameEn || ''))
: ''
},
loadDictOptions() {
this.dictLoading = true
return Promise.all([
sysdictPage({ Type: 'UserHobbiesAndInterestsTag', PageIndex: 1, PageSize: 100 }, this.language),
sysdictPage({ Type: 'UserLifestyleHabitsTag', PageIndex: 1, PageSize: 100 }, this.language)
])
.then(([hobbyRes, lifestyleRes]) => {
this.hobbyOptions = normalizeDictList(hobbyRes, this.language, 'hobbies')
this.lifestyleOptions = normalizeDictList(lifestyleRes, this.language, 'lifestyle')
this.initFormTagsFromUser()
})
.catch((error) => {
console.error('load application dict options failed', error)
uni.showToast({
title: this.$tr('偏好标签加载失败'),
icon: 'none'
})
})
.finally(() => {
this.dictLoading = false
})
},
initFormTagsFromUser() {
const user = getStoredUser()
const authProfile = user && user.authProfile ? user.authProfile : {}
if (!this.form.hobbiesAndInterestsTagKeys.length) {
this.form.hobbiesAndInterestsTagKeys = this.matchTagKeys(authProfile.hobbyTags, this.hobbyOptions)
}
if (!this.form.lifestyleHabitsTagKeys.length) {
this.form.lifestyleHabitsTagKeys = this.matchTagKeys(authProfile.dietaryHabitTags, this.lifestyleOptions)
}
},
matchTagKeys(tagList = [], options = []) {
if (!Array.isArray(tagList) || !Array.isArray(options) || !options.length) {
return []
}
return tagList
.map(item => {
const rawKey = item && (item.key || item.code || item.value || '')
const matched = options.find(option => {
return String(option.key) === String(rawKey) || option.value === rawKey
})
return matched ? matched.key : ''
})
.filter(Boolean)
},
validateForm() {
if (this.showDateRange) {
if (!this.form.start) {
return this.$tr('请选择{label}', { label: this.dateRangeStartLabel })
}
if (!this.form.end) {
return this.$tr('请选择{label}', { label: this.dateRangeEndLabel })
}
if (new Date(this.form.end).getTime() < new Date(this.form.start).getTime()) {
return this.$tr('{endLabel}不能早于{startLabel}', {
endLabel: this.dateRangeEndLabel,
startLabel: this.dateRangeStartLabel
})
}
}
if (!this.form.remark.trim()) {
return this.$tr('请输入补充说明')
}
return ''
},
buildSubmitPayload() {
return {
intendedAreaCode: this.form.intendedAreaCode || null,
intendedRoomTypeCode: this.form.intendedRoomTypeCode || null,
IntendedFloorLevel: this.form.intendedFloorLevel || null,
roomSlotHeight: Number(this.form.roomSlotHeight),
hobbiesAndInterestsTagKeys: this.showPreferenceSelectors && this.form.hobbiesAndInterestsTagKeys.length ? this.form.hobbiesAndInterestsTagKeys : null,
lifestyleHabitsTagKeys: this.showPreferenceSelectors && this.form.lifestyleHabitsTagKeys.length ? this.form.lifestyleHabitsTagKeys : null,
remark: this.form.remark.trim() || null,
start: this.showDateRange ? normalizeDateTime(this.form.start, false) : null,
end: this.showDateRange ? normalizeDateTime(this.form.end, true) : null
}
},
normalizeApplicationDetail(response) {
return parseResponseObject(response)
},
normalizeBillDetail(response) {
return parseResponseObject(response)
},
normalizeBillList(response) {
return parseResponseList(response)
},
normalizePayingBills(response) {
return parseResponseList(response)
},
getBillNo(bill = {}) {
return String(bill.no || bill.billNo || '').trim()
},
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 => !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()
},
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
})
})
},
goStayServiceFlow() {
saveHomePageTabId('stay-service')
uni.reLaunch({
url: '/pages/index/index?pageId=stay-service'
})
},
async getPendingPaymentOrder(billNo = '') {
const normalizedBillNo = String(billNo || '').trim()
if (!normalizedBillNo) {
return null
}
const response = await paymentPayingBills({
BillNo: normalizedBillNo
}, this.language)
const orderList = this.normalizePayingBills(response)
return orderList.length ? orderList[0] : 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(),
paymentNo: String(detail.paymentNo || detail.payNo || detail.payRequestNo || '').trim(),
detail
}
},
async resolvePaymentPreview(referenceTypeNo = '') {
const context = await this.resolveApplicationDetailPaymentContext(referenceTypeNo).catch(() => null)
const normalizedReferenceTypeNo = context && context.referenceTypeNo
? context.referenceTypeNo
: String(referenceTypeNo || '').trim()
let billNo = context && context.billNo ? context.billNo : ''
let pendingPaymentOrder = billNo ? await this.getPendingPaymentOrder(billNo).catch(() => null) : null
let requiresDepositPayment = !!billNo
if (normalizedReferenceTypeNo) {
const billRes = await accountQueryBills({
referenceTypeNo: normalizedReferenceTypeNo,
status: 10,
paymentStatus: 0,
ProviderType: 'WechatMiniProgram'
}, this.language)
const billList = this.normalizeBillList(billRes)
const matchedBill = Array.isArray(billList) && billList.length
? ((billNo
? billList.find(item => String(this.getBillNo(item) || '').trim() === String(billNo || '').trim())
: null) || billList[0])
: null
if (matchedBill) {
billNo = this.getBillNo(matchedBill)
requiresDepositPayment = !!billNo
if (!pendingPaymentOrder && billNo) {
pendingPaymentOrder = await this.getPendingPaymentOrder(billNo).catch(() => null)
}
if (!pendingPaymentOrder) {
pendingPaymentOrder = this.buildPendingPaymentOrderFromBillDetail(matchedBill)
}
} else {
billNo = ''
pendingPaymentOrder = null
requiresDepositPayment = false
}
}
return {
referenceTypeNo: normalizedReferenceTypeNo,
billNo,
paymentNo: requiresDepositPayment
? (this.getPendingPaymentOrderNo(pendingPaymentOrder) || (context && context.paymentNo ? context.paymentNo : ''))
: '',
pendingPaymentOrder,
requiresDepositPayment
}
},
confirmSubmitRequest() {
return new Promise((resolve) => {
uni.showModal({
title: this.$tr('提交前确认'),
content: this.$tr('确认提交当前申请单吗?'),
// WeChat Mini Program limits modal button text length.
// Keep English actions short so the modal does not fail silently.
confirmText: this.language === 'en' ? 'OK' : this.$tr('确认提交'),
cancelText: this.language === 'en' ? 'Back' : this.$tr('再想想'),
success: (res) => resolve(!!(res && res.confirm)),
fail: () => resolve(false)
})
})
},
openDepositNotice(paymentContext = {}) {
this.pendingDepositPaymentContext = {
referenceTypeNo: String(paymentContext.referenceTypeNo || '').trim(),
billNo: String(paymentContext.billNo || '').trim(),
paymentNo: String(paymentContext.paymentNo || '').trim(),
pendingPaymentOrder: paymentContext.pendingPaymentOrder || null
}
this.pendingPaymentMethodContext = null
this.hasReadNotice = false
this.showDepositNotice = true
},
closeDepositNotice() {
if (this.isClosingDepositNoticeForPayment) {
this.isClosingDepositNoticeForPayment = false
return
}
this.showDepositNotice = false
this.hasReadNotice = false
this.pendingDepositPaymentContext = null
this.pendingPaymentMethodContext = null
this.goStayServiceFlow()
},
onNoticeScrollToBottom() {
this.hasReadNotice = true
},
openPaymentMethodPopup(paymentContext = {}) {
this.pendingPaymentMethodContext = {
referenceTypeNo: String(paymentContext.referenceTypeNo || '').trim(),
billNo: String(paymentContext.billNo || '').trim(),
paymentNo: String(paymentContext.paymentNo || '').trim(),
pendingPaymentOrder: paymentContext.pendingPaymentOrder || null
}
this.showPaymentMethodPopup = true
},
closePaymentMethodPopup() {
if (this.isSwitchingPaymentMethodPopup) {
this.isSwitchingPaymentMethodPopup = false
return
}
this.showPaymentMethodPopup = false
this.pendingPaymentMethodContext = null
this.goStayServiceFlow()
},
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
})
},
async handlePaymentMethodSelect(type = '') {
const paymentContext = this.pendingPaymentMethodContext || {}
this.isSwitchingPaymentMethodPopup = true
this.showPaymentMethodPopup = false
if (type === 'offline') {
setTimeout(() => {
this.showOfflinePaymentNotice = true
this.loadDepositOtherPaymentConfig()
}, 40)
return
}
try {
await this.payByPaymentContext({
referenceTypeNo: paymentContext.referenceTypeNo,
billNo: paymentContext.billNo,
pendingPaymentOrder: paymentContext.pendingPaymentOrder,
description: this.$tr('支付定金')
})
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()
this.pendingPaymentMethodContext = null
setTimeout(() => {
this.goStayServiceFlow()
}, 300)
}
},
handleOfflinePaymentPopupInput(value) {
this.showOfflinePaymentNotice = !!value
if (value) {
return
}
this.pendingPaymentMethodContext = null
setTimeout(() => {
this.goStayServiceFlow()
}, 300)
},
async payByPaymentContext(context = {}) {
let referenceTypeNo = String(context.referenceTypeNo || '').trim()
let billNo = String(context.billNo || '').trim()
const description = String(context.description || '').trim() || this.$tr('支付定金')
if (!referenceTypeNo && !billNo) {
throw new Error(this.$tr('申请单编号缺失'))
}
if (referenceTypeNo) {
saveActiveApplicationCode(referenceTypeNo)
}
uni.showLoading({ title: this.$tr('正在获取账单...') })
let bill = null
let pendingPaymentOrder = context.pendingPaymentOrder || null
if (referenceTypeNo && !billNo) {
const detailPaymentContext = await this.resolveApplicationDetailPaymentContext(referenceTypeNo).catch(() => null)
if (detailPaymentContext) {
referenceTypeNo = detailPaymentContext.referenceTypeNo || referenceTypeNo
billNo = detailPaymentContext.billNo || billNo
}
}
if (!pendingPaymentOrder && billNo) {
pendingPaymentOrder = await this.getPendingPaymentOrder(billNo).catch(() => null)
}
if (billNo) {
const billDetail = this.normalizeBillDetail(await accountQueryBill({ billNo }, this.language))
if (billDetail && Object.keys(billDetail).length) {
bill = billDetail
billNo = this.getBillNo(bill) || billNo
referenceTypeNo = referenceTypeNo || String(bill.referenceTypeNo || '').trim()
if (!pendingPaymentOrder) {
pendingPaymentOrder = this.buildPendingPaymentOrderFromBillDetail(bill)
}
}
}
if (referenceTypeNo) {
const billRes = await accountQueryBills({
referenceTypeNo,
status: 10,
paymentStatus: 0,
ProviderType: 'WechatMiniProgram'
}, this.language)
const billList = this.normalizeBillList(billRes)
if (Array.isArray(billList) && billList.length) {
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) {
throw new Error(this.$tr('暂无待支付定金账单'))
}
uni.showLoading({ title: this.$tr('正在拉起支付...') })
const expireTimestamp = parseExpireTimeToTimestamp(
pendingPaymentOrder && (pendingPaymentOrder.expireTime || (pendingPaymentOrder.cashier && pendingPaymentOrder.cashier.expireTime))
)
const reusablePendingPaymentOrder = pendingPaymentOrder && (!expireTimestamp || expireTimestamp > Date.now())
? pendingPaymentOrder
: null
if (reusablePendingPaymentOrder && reusablePendingPaymentOrder.cashier && reusablePendingPaymentOrder.cashier.paymentInfo) {
await this.requestWechatPayment(reusablePendingPaymentOrder.cashier.paymentInfo)
return
}
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
}, this.language)
const payload = payRes && payRes.data ? payRes.data : payRes
const cashier = payload && payload.cashier ? payload.cashier : {}
await this.requestWechatPayment(cashier.paymentInfo)
},
async confirmAndPayDeposit() {
if (!this.hasReadNotice) {
return
}
const paymentContext = this.pendingDepositPaymentContext || {}
this.isClosingDepositNoticeForPayment = true
this.showDepositNotice = false
this.hasReadNotice = false
this.pendingDepositPaymentContext = null
setTimeout(() => {
this.openPaymentMethodPopup(paymentContext)
}, 40)
},
async executeSubmitRequest() {
this.isSubmitting = true
uni.showLoading({
title: this.$tr('提交中...')
})
try {
const payload = this.buildSubmitPayload()
const response = await submitApplicationForm(payload, this.language)
const applicationCode = extractSubmitApplicationCode(response)
if (this.isRoomTypeApply && !applicationCode) {
throw new Error(this.$tr('申请单编号缺失'))
}
if (applicationCode) {
saveActiveApplicationCode(applicationCode)
}
uni.hideLoading()
if (this.isRoomTypeApply) {
const paymentContext = await this.resolvePaymentPreview(applicationCode).catch(() => null)
if (paymentContext && paymentContext.requiresDepositPayment === false) {
uni.showToast({
title: this.$tr('申请单提交成功'),
icon: 'success'
})
setTimeout(() => {
this.goStayServiceFlow()
}, 800)
return
}
this.openDepositNotice({
...(paymentContext || {}),
referenceTypeNo: (paymentContext && paymentContext.referenceTypeNo) || applicationCode
})
return
}
uni.showToast({
title: this.$tr('申请单提交成功'),
icon: 'success'
})
setTimeout(() => {
saveHomePageTabId('stay-service')
uni.reLaunch({
url: '/pages/index/index?pageId=stay-service'
})
}, 800)
} catch (error) {
console.error('submit application form failed', error)
uni.hideLoading()
throw error
} finally {
this.isSubmitting = false
}
},
submitRequest() {
if (this.isSubmitting) {
return
}
const errorMessage = this.validateForm()
if (errorMessage) {
uni.showToast({
title: errorMessage,
icon: 'none'
})
return
}
return this.confirmSubmitRequest()
.then((confirmed) => {
if (!confirmed) {
return null
}
return this.executeSubmitRequest()
})
.catch((error) => {
if (!error) {
return
}
uni.showToast({
title: (error && (error.message || error.msg)) || this.$tr('申请单提交失败'),
icon: 'none'
})
})
}
}
}
</script>
<style scoped lang="scss">
/* 设计令牌 */
$hero-bg-start: #0A1628;
$hero-bg-end: #1E3A5F;
$luxury-gold: #D8B56A;
$luxury-gold-soft: rgba(216, 181, 106, 0.12);
$surface: #FFFFFF;
$bg-page: #F5F9FF;
$text-primary: #1A2233;
$text-secondary: #6A7A99;
$text-tertiary: #A1B0C4;
$border-soft: rgba($brand-primary, 0.08);
$shadow-card: 0 16rpx 48rpx rgba(26, 34, 51, 0.06);
$shadow-active: 0 8rpx 24rpx rgba($brand-primary, 0.22);
.submit-request-page {
min-height: 100vh;
background: linear-gradient(180deg, #EAF4FF 0%, $bg-page 30%, #FFFFFF 100%);
display: flex;
flex-direction: column;
}
.nav-title {
font-size: 34rpx;
font-weight: 700;
color: $text-primary;
letter-spacing: 2rpx;
}
.content-wrapper {
flex: 1;
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
}
/* 顶部信息卡片(Hero) */
.hero-card {
position: relative;
margin: 24rpx 30rpx 30rpx;
border-radius: 36rpx;
background: linear-gradient(135deg, $hero-bg-start 0%, $hero-bg-end 100%);
box-shadow: 0 24rpx 60rpx rgba(10, 22, 40, 0.22);
overflow: hidden;
color: #FFFFFF;
}
.hero-glow {
position: absolute;
top: -80rpx;
right: -60rpx;
width: 280rpx;
height: 280rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba($brand-primary, 0.35) 0%, transparent 70%);
filter: blur(20rpx);
}
.hero-inner {
position: relative;
z-index: 2;
padding: 44rpx 36rpx;
}
.hero-content {
flex: 1;
min-width: 0;
}
.hero-tag {
display: inline-flex;
align-items: center;
padding: 8rpx 18rpx;
background: rgba(255, 255, 255, 0.12);
border: 1rpx solid rgba(255, 255, 255, 0.18);
border-radius: 24rpx;
margin-bottom: 20rpx;
backdrop-filter: blur(8rpx);
}
.hero-tag-icon {
font-size: 22rpx;
color: $luxury-gold;
margin-right: 8rpx;
}
.hero-tag-text {
font-size: 22rpx;
font-weight: 600;
color: rgba(255, 255, 255, 0.95);
}
.hero-title {
font-size: 38rpx;
font-weight: 700;
line-height: 1.35;
margin-bottom: 16rpx;
letter-spacing: 1rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.hero-desc {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.72);
line-height: 1.55;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.hero-figure {
position: relative;
width: 136rpx;
height: 136rpx;
margin-left: 24rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.figure-ring {
position: absolute;
border-radius: 50%;
border: 2rpx solid rgba(255, 255, 255, 0.1);
&--outer {
width: 136rpx;
height: 136rpx;
}
&--inner {
width: 96rpx;
height: 96rpx;
border-color: rgba($luxury-gold, 0.35);
}
}
.figure-icon {
position: relative;
z-index: 2;
font-size: 56rpx;
color: rgba(255, 255, 255, 0.9);
}
.hero-accent {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 4rpx;
background: linear-gradient(90deg, transparent 0%, $luxury-gold 50%, transparent 100%);
opacity: 0.7;
}
/* 通用表单卡片 */
.form-card {
margin: 0 30rpx 30rpx;
padding: 36rpx 32rpx;
border-radius: 32rpx;
background: $surface;
box-shadow: $shadow-card;
border: 1rpx solid $border-soft;
}
.card-header {
margin-bottom: 32rpx;
}
.header-icon {
width: 64rpx;
height: 64rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
flex-shrink: 0;
&--primary {
background: rgba($brand-primary, 0.1);
color: $brand-primary;
}
&--luxury {
background: $luxury-gold-soft;
color: $luxury-gold;
}
text {
font-size: 32rpx;
}
}
.header-titles {
flex: 1;
min-width: 0;
}
.card-title {
font-size: 32rpx;
font-weight: 700;
color: $text-primary;
margin-bottom: 6rpx;
letter-spacing: 1rpx;
}
.card-subtitle {
font-size: 24rpx;
color: $text-secondary;
}
.field-item {
margin-bottom: 40rpx;
}
.field-item--last {
margin-bottom: 0;
}
.field-label {
display: flex;
align-items: center;
font-size: 28rpx;
font-weight: 700;
color: $text-primary;
margin-bottom: 22rpx;
}
.field-dot {
width: 10rpx;
height: 10rpx;
border-radius: 50%;
margin-right: 12rpx;
&--primary {
background: $brand-primary;
box-shadow: 0 0 10rpx rgba($brand-primary, 0.45);
}
&--luxury {
background: $luxury-gold;
box-shadow: 0 0 10rpx rgba($luxury-gold, 0.45);
}
&--muted {
background: $text-tertiary;
}
}
.form-textarea {
width: 100%;
box-sizing: border-box;
background: #F4F8FF;
border-radius: 20rpx;
padding: 26rpx 32rpx;
border: 2rpx solid transparent;
font-size: 28rpx;
color: $text-primary;
height: 200rpx;
line-height: 1.5;
transition: all 0.25s ease;
}
.form-textarea:focus {
background: #FFFFFF;
border-color: rgba($brand-primary, 0.25);
box-shadow: 0 6rpx 20rpx rgba($brand-primary, 0.08);
}
.footer-spacer {
height: 180rpx;
&--floating {
height: 200rpx;
}
}
/* 底部按钮 */
.footer-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(20rpx);
padding: 24rpx 40rpx calc(24rpx + env(safe-area-inset-bottom));
box-shadow: 0 -8rpx 32rpx rgba(26, 34, 51, 0.06);
z-index: 100;
}
.footer-inner {
width: 100%;
}
.submit-btn {
width: 100%;
height: 96rpx;
border-radius: 48rpx;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #A1B0C4 0%, #86909C 100%);
color: #FFFFFF;
font-size: 32rpx;
font-weight: 700;
letter-spacing: 2rpx;
box-shadow: 0 8rpx 24rpx rgba(134, 144, 156, 0.2);
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.submit-btn--active {
background: linear-gradient(135deg, $brand-primary 0%, #0099FF 100%);
box-shadow: 0 16rpx 36rpx rgba($brand-primary, 0.32);
}
.submit-btn--active:active {
transform: scale(0.98);
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.25);
}
.submit-btn-icon {
font-size: 32rpx;
margin-right: 12rpx;
}
.submit-btn-text {
line-height: 1;
}
.footer-hint {
text-align: center;
font-size: 22rpx;
color: $text-tertiary;
margin-top: 16rpx;
}
.payment-method-popup {
padding: 30rpx 30rpx calc(28rpx + env(safe-area-inset-bottom));
background:
radial-gradient(circle at top right, rgba($brand-primary, 0.16) 0%, rgba($brand-primary, 0) 36%),
linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
border-radius: 32rpx 32rpx 0 0;
}
.payment-method-popup__header {
padding: 8rpx 4rpx 20rpx;
}
.payment-method-popup__eyebrow {
display: inline-flex;
align-items: center;
padding: 8rpx 18rpx;
border-radius: 999rpx;
background: rgba($brand-primary, 0.1);
color: $brand-primary;
font-size: 22rpx;
font-weight: 700;
}
.payment-method-popup__title {
margin-top: 18rpx;
font-size: 40rpx;
line-height: 1.25;
color: $text-primary;
font-weight: 700;
}
.payment-method-popup__desc {
margin-top: 12rpx;
font-size: 24rpx;
line-height: 1.7;
color: $text-secondary;
}
.payment-method-popup__summary {
margin-bottom: 22rpx;
padding: 22rpx 26rpx;
border-radius: 24rpx;
background: rgba(255, 255, 255, 0.92);
border: 1rpx solid rgba($brand-primary, 0.08);
box-shadow: 0 12rpx 28rpx rgba($brand-primary, 0.08);
}
.payment-method-popup__summary-label {
font-size: 22rpx;
color: $text-tertiary;
}
.payment-method-popup__summary-value {
margin-top: 8rpx;
font-size: 28rpx;
line-height: 1.5;
color: $text-primary;
font-weight: 700;
word-break: break-all;
}
.payment-method-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
margin-top: 18rpx;
padding: 26rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.92);
border: 1rpx solid rgba($brand-primary, 0.08);
box-shadow: 0 14rpx 30rpx rgba($brand-primary, 0.08);
}
.payment-method-card:active {
transform: scale(0.985);
}
.payment-method-card--wechat {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(239, 253, 244, 0.96) 100%);
border-color: rgba(34, 197, 94, 0.14);
}
.payment-method-card--offline {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(247, 250, 255, 0.96) 100%);
}
.payment-method-card__main {
display: flex;
align-items: center;
gap: 18rpx;
flex: 1;
min-width: 0;
}
.payment-method-card__icon {
width: 88rpx;
height: 88rpx;
border-radius: 26rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 42rpx;
flex-shrink: 0;
}
.payment-method-card__icon--wechat {
background: rgba(34, 197, 94, 0.12);
color: #16A34A;
}
.payment-method-card__icon--offline {
background: rgba($brand-primary, 0.1);
color: $brand-primary;
}
.payment-method-card__copy {
flex: 1;
min-width: 0;
}
.payment-method-card__title-row {
display: flex;
align-items: center;
gap: 12rpx;
flex-wrap: wrap;
}
.payment-method-card__title {
font-size: 30rpx;
line-height: 1.35;
color: $text-primary;
font-weight: 700;
}
.payment-method-card__tag {
padding: 6rpx 14rpx;
border-radius: 999rpx;
background: rgba(34, 197, 94, 0.12);
color: #15803D;
font-size: 20rpx;
font-weight: 700;
}
.payment-method-card__desc {
margin-top: 10rpx;
font-size: 22rpx;
line-height: 1.7;
color: $text-secondary;
}
.payment-method-card__arrow {
width: 56rpx;
height: 56rpx;
border-radius: 999rpx;
background: rgba(148, 163, 184, 0.12);
color: #64748B;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
flex-shrink: 0;
}
/* 导航栏背景色滚动过渡 */
.submit-request-page ::v-deep .tn-custom-nav-bar__bar {
transition: background-color 0.3s ease, box-shadow 0.3s ease;
}
.deposit-notice-popup {
background: #FFFFFF;
border-radius: 32rpx 32rpx 0 0;
display: flex;
flex-direction: column;
height: 80vh;
}
.popup-header {
padding: 32rpx;
text-align: center;
border-bottom: 1px solid #F1F5F9;
}
.popup-title {
font-size: 36rpx;
font-weight: 700;
color: $text-primary;
}
.popup-subtitle {
margin-top: 12rpx;
font-size: 24rpx;
color: $text-secondary;
}
.popup-body {
flex: 1;
padding: 32rpx;
overflow-y: auto;
}
.notice-content {
padding-bottom: 40rpx;
}
.notice-item {
margin-bottom: 32rpx;
}
.notice-item-zh {
font-size: 28rpx;
color: #334155;
line-height: 1.6;
margin-bottom: 8rpx;
font-weight: 500;
}
.notice-item-en {
font-size: 24rpx;
color: #64748B;
line-height: 1.5;
}
.popup-footer {
padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
border-top: 1px solid #F1F5F9;
background: #FFFFFF;
display: flex;
justify-content: center;
}
.notice-action-btn {
min-width: 420rpx;
max-width: 100%;
height: 92rpx;
padding: 0 40rpx;
border-radius: 999rpx;
display: inline-flex;
align-items: center;
justify-content: center;
text-align: center;
background: linear-gradient(135deg, $brand-primary 0%, #0099CC 100%);
color: #FFFFFF;
box-shadow: 0 16rpx 34rpx rgba($brand-primary, 0.24);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.notice-action-btn:active {
transform: scale(0.98);
}
.notice-action-btn--disabled {
opacity: 0.6;
background: #94A3B8;
box-shadow: none;
}
</style>