Compare commits
2 Commits
da6bdc6a02
...
9fee2c3a74
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fee2c3a74 | ||
|
|
1111ac148b |
115
api/index.js
115
api/index.js
@@ -30,6 +30,10 @@ function buildIotGatewayUrl(path = '') {
|
|||||||
return `${iotBaseUrl}${normalizedPath}`
|
return `${iotBaseUrl}${normalizedPath}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldIgnoreGatewayRoom404(error = {}) {
|
||||||
|
return Number(error && error.statusCode) === 404
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeUploadResponse(data) {
|
function normalizeUploadResponse(data) {
|
||||||
if (typeof data !== 'string') {
|
if (typeof data !== 'string') {
|
||||||
return Promise.resolve(data)
|
return Promise.resolve(data)
|
||||||
@@ -237,6 +241,21 @@ export function applicationFormMePage(params = {}, language) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function applicationFormDetail(detailCode = '', language) {
|
||||||
|
const normalizedDetailCode = String(detailCode || '').trim()
|
||||||
|
if (!normalizedDetailCode) {
|
||||||
|
return Promise.reject(new Error('申请明细编码不能为空'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return request({
|
||||||
|
url: `/applicationform/detail/${encodeURIComponent(normalizedDetailCode)}`,
|
||||||
|
method: 'GET',
|
||||||
|
header: {
|
||||||
|
'X-Requested-Language': resolveLanguageHeader(language)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// --- 工单 ---
|
// --- 工单 ---
|
||||||
export function ticketOrderPage(params = {}, language) {
|
export function ticketOrderPage(params = {}, language) {
|
||||||
return request({
|
return request({
|
||||||
@@ -430,14 +449,15 @@ export function orderSearchedReservationFlow(data = {}, language) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function roomInspectionMyPage(params = {}, language) {
|
export function roomInspectionMyPage(params = {}, language, options = {}) {
|
||||||
return request({
|
return request({
|
||||||
url: '/roominspection/my/page',
|
url: '/roominspection/my/page',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params: params || {},
|
params: params || {},
|
||||||
header: {
|
header: {
|
||||||
'X-Requested-Language': resolveLanguageHeader(language)
|
'X-Requested-Language': resolveLanguageHeader(language)
|
||||||
}
|
},
|
||||||
|
...options
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,6 +546,24 @@ export function accountQueryBill(params = {}, language) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function accountQueryBillPreview(params = {}, language) {
|
||||||
|
const query = params || {}
|
||||||
|
const orderNo = String(query.orderNo || query.OrderNo || '').trim()
|
||||||
|
if (!orderNo) {
|
||||||
|
return Promise.reject(new Error('订单号不能为空'))
|
||||||
|
}
|
||||||
|
return request({
|
||||||
|
url: '/accountquery/bill-preview',
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
OrderNo: orderNo
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
'X-Requested-Language': resolveLanguageHeader(language)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function paymentBillPay(data = {}, language) {
|
export function paymentBillPay(data = {}, language) {
|
||||||
const payload = data || {}
|
const payload = data || {}
|
||||||
return request({
|
return request({
|
||||||
@@ -576,9 +614,13 @@ export function paymentStatusByBillNo(params = {}, language) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function userDetailCancel(data = {}, language) {
|
export function userDetailCancel(data = {}, language) {
|
||||||
const payload = data || {}
|
const source = data || {}
|
||||||
|
const payload = {
|
||||||
|
detailCode: String(source.detailCode || source.code || source.referenceTypeNo || '').trim(),
|
||||||
|
remark: source.remark == null ? '' : String(source.remark)
|
||||||
|
}
|
||||||
return request({
|
return request({
|
||||||
url: '/ApplicationForm/user/detail/cancel',
|
url: '/applicationform/user/detail/cancel',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: payload,
|
data: payload,
|
||||||
header: {
|
header: {
|
||||||
@@ -696,6 +738,18 @@ export function notificationRead(code, language) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMyTodoList(params = {}, language, options = {}) {
|
||||||
|
return request({
|
||||||
|
url: '/todo/my/list',
|
||||||
|
method: 'GET',
|
||||||
|
params: params || {},
|
||||||
|
header: {
|
||||||
|
'X-Requested-Language': resolveLanguageHeader(language)
|
||||||
|
},
|
||||||
|
...options
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// --- 房源与区域 ---
|
// --- 房源与区域 ---
|
||||||
export function areaPagination(params = {}, language) {
|
export function areaPagination(params = {}, language) {
|
||||||
return request({
|
return request({
|
||||||
@@ -709,6 +763,18 @@ export function areaPagination(params = {}, language) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function areaDetailPagination(params = {}, language) {
|
||||||
|
return request({
|
||||||
|
url: '/area/pagination',
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
header: {
|
||||||
|
'X-Requested-Language': resolveLanguageHeader(language)
|
||||||
|
},
|
||||||
|
withAuth: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function facilityManagers(params = {}, language) {
|
export function facilityManagers(params = {}, language) {
|
||||||
return request({
|
return request({
|
||||||
url: '/facility/managers',
|
url: '/facility/managers',
|
||||||
@@ -818,8 +884,15 @@ export function gatewayRoomDeviceList(brandId = '', roomCode = '', language) {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
header: {
|
header: {
|
||||||
'X-Requested-Language': resolveLanguageHeader(language)
|
'X-Requested-Language': resolveLanguageHeader(language)
|
||||||
}
|
},
|
||||||
|
hideErrorToast: true
|
||||||
})
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (shouldIgnoreGatewayRoom404(error)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringifyGatewayExecuteValue(value = '') {
|
function stringifyGatewayExecuteValue(value = '') {
|
||||||
@@ -922,6 +995,38 @@ export function gatewaySyncDeviceInfo(brandId = '', deviceNo = '', language) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 门锁 ---
|
||||||
|
export function deviceGetDevicesByRoomAsync(params = {}, language) {
|
||||||
|
const roomCode = String((params && params.roomCode) || '').trim()
|
||||||
|
if (!roomCode) {
|
||||||
|
return Promise.reject(new Error('roomCode 不能为空'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return request({
|
||||||
|
url: '/device/getdevicesbyroomasync',
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
roomCode
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
'X-Requested-Language': resolveLanguageHeader(language)
|
||||||
|
},
|
||||||
|
hideErrorToast: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lockHxjOpenLock(data = {}, language) {
|
||||||
|
return createJsonPatchRequest('/LockHxj/OpenLock', data, language)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lockHxjGetKeysByUserCode(data = {}, language) {
|
||||||
|
return createJsonPatchRequest('/LockHxj/GetKeysByUserCode', data, language)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lockHxjAddFingerKey(data = {}, language) {
|
||||||
|
return createJsonPatchRequest('/LockHxj/AddFingerKey', data, language)
|
||||||
|
}
|
||||||
|
|
||||||
// --- 内容 ---
|
// --- 内容 ---
|
||||||
export function bannerAll(language) {
|
export function bannerAll(language) {
|
||||||
return request({
|
return request({
|
||||||
|
|||||||
376
components/DepositOtherPaymentPopup.vue
Normal file
376
components/DepositOtherPaymentPopup.vue
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
<template>
|
||||||
|
<tn-popup :value="value" mode="bottom" length="84%" :borderRadius="32" :closeBtn="true" @close="close" @input="handleInput">
|
||||||
|
<view class="deposit-other-payment-popup">
|
||||||
|
<view class="popup-header">
|
||||||
|
<view class="popup-handle"></view>
|
||||||
|
<view class="popup-title">{{ getLocalizedText('其他支付方式', 'Other Payment Methods') }}</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="popup-body" :show-scrollbar="false">
|
||||||
|
<view class="popup-content">
|
||||||
|
<view v-if="hasConfiguredContent" class="hero-card">
|
||||||
|
<view class="hero-badge">{{ getLocalizedText('线下支付说明', 'Alternative Payment') }}</view>
|
||||||
|
<view class="hero-title">{{ getLocalizedText('定金支付备用方案', 'Deposit payment backup plan') }}</view>
|
||||||
|
<view class="hero-desc">
|
||||||
|
{{ getLocalizedText(
|
||||||
|
'当线上支付渠道暂不可用时,可通过下方联系电话、二维码或转账说明继续完成定金支付。',
|
||||||
|
'Use the configured contact, QR code, or transfer instructions below when the online payment channel is unavailable.'
|
||||||
|
) }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading" class="loading-card">
|
||||||
|
<view class="loading-chip">{{ getLocalizedText('正在同步其他支付配置,请稍候...', 'Syncing alternative payment details...') }}</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<view v-if="phone" class="contact-card">
|
||||||
|
<view class="contact-main">
|
||||||
|
<view class="contact-label">{{ getLocalizedText('联系电话', 'Contact Phone') }}</view>
|
||||||
|
<view class="contact-value">{{ phone }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="contact-action" @click="handleCallPhone">
|
||||||
|
<text class="tn-text-bold">{{ getLocalizedText('拨打电话', 'Call') }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="qrCodeUrl" class="qr-card">
|
||||||
|
<view class="section-title">{{ getLocalizedText('支付二维码', 'Payment QR Code') }}</view>
|
||||||
|
<image class="qr-image" :src="qrCodeUrl" mode="widthFix" @click="previewQrCode"></image>
|
||||||
|
<view class="qr-hint">
|
||||||
|
{{ getLocalizedText('点击二维码图片可查看大图。', 'Tap the image to preview the code in full size.') }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="richTextNodes" class="rich-text-card">
|
||||||
|
<view class="section-title">{{ getLocalizedText('支付说明', 'Payment Instructions') }}</view>
|
||||||
|
<rich-text class="rich-text-content" :nodes="richTextNodes"></rich-text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="!phone && !qrCodeUrl && !richTextNodes" class="empty-card">
|
||||||
|
<view class="empty-title">{{ getLocalizedText('暂未配置展示内容', 'Configuration Pending') }}</view>
|
||||||
|
<view class="empty-desc">
|
||||||
|
{{ getLocalizedText(
|
||||||
|
'当前暂未同步到其他支付配置,请联系公寓工作人员处理。',
|
||||||
|
'Alternative payment details are not available yet. Please contact the property staff for manual payment assistance.'
|
||||||
|
) }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view class="popup-footer">
|
||||||
|
<view class="notice-action-btn" @click="close">
|
||||||
|
<text class="tn-text-bold">{{ getLocalizedText('我知道了', 'Got It') }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</tn-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { addFilePrefix } from '@/utils/common'
|
||||||
|
import { normalizeRichTextImages } from '@/utils/rich-text'
|
||||||
|
|
||||||
|
function escapeHtml(value = '') {
|
||||||
|
return String(value)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeContentToRichText(value = '') {
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
if (!text) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (/<[a-z][\s\S]*>/i.test(text)) {
|
||||||
|
return normalizeRichTextImages(
|
||||||
|
text,
|
||||||
|
'display:block;width:100%;max-width:100%;height:auto;box-sizing:border-box;border-radius:20rpx;margin:18rpx 0;vertical-align:top;'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return `<div style="white-space:pre-wrap;line-height:1.9;color:#334155;font-size:28rpx;">${escapeHtml(text)}</div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DepositOtherPaymentPopup',
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
language() {
|
||||||
|
return this.$getLanguage()
|
||||||
|
},
|
||||||
|
phone() {
|
||||||
|
return String((this.config && this.config.phone) || '').trim()
|
||||||
|
},
|
||||||
|
qrCodeUrl() {
|
||||||
|
return addFilePrefix(String((this.config && this.config.qrCode) || '').trim())
|
||||||
|
},
|
||||||
|
localizedContent() {
|
||||||
|
if (this.language === 'en') {
|
||||||
|
return this.config.contentEn || this.config.contentZh || ''
|
||||||
|
}
|
||||||
|
return this.config.contentZh || this.config.contentEn || ''
|
||||||
|
},
|
||||||
|
hasConfiguredContent() {
|
||||||
|
return !!(this.phone || this.qrCodeUrl || this.richTextNodes)
|
||||||
|
},
|
||||||
|
richTextNodes() {
|
||||||
|
return normalizeContentToRichText(this.localizedContent)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getLocalizedText(zh, en) {
|
||||||
|
return this.language === 'en' ? en : zh
|
||||||
|
},
|
||||||
|
handleInput(value) {
|
||||||
|
this.$emit('input', value)
|
||||||
|
this.$emit('update:modelValue', value)
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.handleInput(false)
|
||||||
|
},
|
||||||
|
handleCallPhone() {
|
||||||
|
if (!this.phone) {
|
||||||
|
uni.showToast({
|
||||||
|
title: this.getLocalizedText('暂无联系电话', 'No contact phone available'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.makePhoneCall({
|
||||||
|
phoneNumber: this.phone
|
||||||
|
})
|
||||||
|
},
|
||||||
|
previewQrCode() {
|
||||||
|
if (!this.qrCodeUrl) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.previewImage({
|
||||||
|
urls: [this.qrCodeUrl]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.deposit-other-payment-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;
|
||||||
|
height: 0;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 28rpx 28rpx 24rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: linear-gradient(180deg, #f7faff 0%, #f2f5fc 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24rpx;
|
||||||
|
padding-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(43, 142, 255, 0.1);
|
||||||
|
color: #2b6fd6;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
font-size: 36rpx;
|
||||||
|
line-height: 1.25;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #183b73;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-desc {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 25rpx;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #607089;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-card,
|
||||||
|
.contact-card,
|
||||||
|
.qr-card,
|
||||||
|
.rich-text-card,
|
||||||
|
.empty-card {
|
||||||
|
padding: 24rpx 28rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.88);
|
||||||
|
border: 1rpx solid rgba(43, 142, 255, 0.08);
|
||||||
|
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 12rpx 20rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(43, 142, 255, 0.08);
|
||||||
|
color: #4c6a95;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-label,
|
||||||
|
.section-title {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #7c8ca5;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-value {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #22324d;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-action {
|
||||||
|
padding: 0 28rpx;
|
||||||
|
height: 76rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #2b8eff 0%, #1f6fe5 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 14rpx 28rpx rgba(43, 142, 255, 0.18);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-card {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-image {
|
||||||
|
width: 360rpx;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 18rpx auto 0;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 18rpx 40rpx rgba(15, 23, 42, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-hint {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #7c8ca5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-content {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #22324d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-desc {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #7c8ca5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name" : "测试",
|
"name" : "绿居公寓手机端",
|
||||||
"appid" : "__UNI__056D753",
|
"appid" : "__UNI__056D753",
|
||||||
"description" : "",
|
"description" : "",
|
||||||
"versionName" : "1.0.0",
|
"versionName" : "1.0.0",
|
||||||
|
|||||||
@@ -156,6 +156,12 @@
|
|||||||
"navigationBarTitleText": "Utility Recharge"
|
"navigationBarTitleText": "Utility Recharge"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "lock_control/lock_control",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "Smart Lock"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "smart_control/smart_control",
|
"path": "smart_control/smart_control",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="profile-page tn-safe-area-inset-bottom">
|
<view class="profile-page tn-safe-area-inset-bottom">
|
||||||
|
|
||||||
<!-- 头部背景与用户信息 (悬浮质感设计) -->
|
<!-- 头部背景与用户信息 (悬浮质感设计) -->
|
||||||
@@ -308,7 +308,7 @@ export default {
|
|||||||
return '-'
|
return '-'
|
||||||
}
|
}
|
||||||
const currentOrder = this.getCurrentOrder() || {}
|
const currentOrder = this.getCurrentOrder() || {}
|
||||||
const startText = currentOrder.startDate || currentOrder.start || currentOrder.checkinDate || ''
|
const startText = currentOrder.contractStartTime || currentOrder.startDate || currentOrder.start || currentOrder.checkinDate || ''
|
||||||
if (!startText) {
|
if (!startText) {
|
||||||
return '--'
|
return '--'
|
||||||
}
|
}
|
||||||
@@ -509,6 +509,12 @@ export default {
|
|||||||
this.goToRegistrationRecords()
|
this.goToRegistrationRecords()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (item.key === 'lock') {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pkgService/lock_control/lock_control'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
if (item.key === 'butler') {
|
if (item.key === 'butler') {
|
||||||
this.goToSubmitRequest(itemTitle, itemDesc, {
|
this.goToSubmitRequest(itemTitle, itemDesc, {
|
||||||
businessType: 'butler-contact',
|
businessType: 'butler-contact',
|
||||||
|
|||||||
@@ -252,6 +252,10 @@ export default {
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
},
|
},
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
language: {
|
language: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'zh'
|
default: 'zh'
|
||||||
@@ -348,7 +352,14 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
handleBannerChange(e) {
|
handleBannerChange(e) {
|
||||||
this.$emit('banner-change', e.detail.current)
|
if (!this.isActive) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const nextIndex = Number(e && e.detail && e.detail.current)
|
||||||
|
if (!Number.isFinite(nextIndex)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$emit('banner-change', nextIndex)
|
||||||
},
|
},
|
||||||
openStoreDetail(area) {
|
openStoreDetail(area) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
|
|||||||
@@ -91,6 +91,10 @@ export default {
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
},
|
},
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
language: {
|
language: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'zh'
|
default: 'zh'
|
||||||
@@ -144,9 +148,19 @@ export default {
|
|||||||
.trim()
|
.trim()
|
||||||
},
|
},
|
||||||
handleBannerChange(e) {
|
handleBannerChange(e) {
|
||||||
this.$emit('banner-change', e.detail.current)
|
if (!this.isActive) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const nextIndex = Number(e && e.detail && e.detail.current)
|
||||||
|
if (!Number.isFinite(nextIndex)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$emit('banner-change', nextIndex)
|
||||||
},
|
},
|
||||||
switchBanner(index) {
|
switchBanner(index) {
|
||||||
|
if (!this.isActive) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$emit('banner-switch', index)
|
this.$emit('banner-switch', index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
>
|
>
|
||||||
<swiper-item>
|
<swiper-item>
|
||||||
<SelectRoomHero
|
<SelectRoomHero
|
||||||
|
:is-active="currentScreen === 0"
|
||||||
:language="currentLang"
|
:language="currentLang"
|
||||||
:banner-list="bannerList"
|
:banner-list="bannerList"
|
||||||
:current-banner="currentBanner"
|
:current-banner="currentBanner"
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
:show-scrollbar="false"
|
:show-scrollbar="false"
|
||||||
>
|
>
|
||||||
<SelectRoomAreaPanel
|
<SelectRoomAreaPanel
|
||||||
|
:is-active="currentScreen === 1"
|
||||||
:language="currentLang"
|
:language="currentLang"
|
||||||
:banner-list="bannerList"
|
:banner-list="bannerList"
|
||||||
:current-banner-index="currentBannerIndex"
|
:current-banner-index="currentBannerIndex"
|
||||||
@@ -196,12 +198,12 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
currentLang() {
|
currentLang() {
|
||||||
this.loadBannerList()
|
this.loadBannerList()
|
||||||
this.loadAreaList()
|
this.refreshSelectRoomState()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.checkLoginStatus()
|
this.checkLoginStatus()
|
||||||
await Promise.all([this.loadBannerList(), this.loadAreaList()])
|
await this.loadBannerList()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getLocalizedBannerText(banner = {}, field = '') {
|
getLocalizedBannerText(banner = {}, field = '') {
|
||||||
@@ -236,6 +238,29 @@ export default {
|
|||||||
this.isLogin = session.isLogin
|
this.isLogin = session.isLogin
|
||||||
this.userInfo = this.isLogin ? getStoredUser() : null
|
this.userInfo = this.isLogin ? getStoredUser() : null
|
||||||
},
|
},
|
||||||
|
hasActiveReservationFlow() {
|
||||||
|
const storeUser = (this.$store && this.$store.state && this.$store.state.vuex_user) || {}
|
||||||
|
if (storeUser.hasActiveReservationFlow === true) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (storeUser.hasActiveReservationFlow === false) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const session = getSessionSnapshot()
|
||||||
|
const user = session.currentUser || session.registeredUser || session.resolvedUser || {}
|
||||||
|
if (user.hasActiveReservationFlow === true) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (user.hasActiveReservationFlow === false) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
refreshSelectRoomState() {
|
||||||
|
if (!this.isLogin || !this.hasActiveReservationFlow()) {
|
||||||
|
this.loadAreaList()
|
||||||
|
}
|
||||||
|
},
|
||||||
loadBannerList() {
|
loadBannerList() {
|
||||||
this.isBannerLoading = true
|
this.isBannerLoading = true
|
||||||
this.bannerLoadError = ''
|
this.bannerLoadError = ''
|
||||||
@@ -408,10 +433,18 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
handleBannerChange(index) {
|
handleBannerChange(index) {
|
||||||
this.currentBannerIndex = index
|
const nextIndex = Number(index)
|
||||||
|
if (!Number.isFinite(nextIndex) || nextIndex === this.currentBannerIndex) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.currentBannerIndex = nextIndex
|
||||||
},
|
},
|
||||||
switchBanner(index) {
|
switchBanner(index) {
|
||||||
this.currentBannerIndex = index
|
const nextIndex = Number(index)
|
||||||
|
if (!Number.isFinite(nextIndex) || nextIndex === this.currentBannerIndex) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.currentBannerIndex = nextIndex
|
||||||
},
|
},
|
||||||
collapseToTopBanner() {
|
collapseToTopBanner() {
|
||||||
this.currentScreen = 1
|
this.currentScreen = 1
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -75,7 +75,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getRegisteredUser, saveRegisteredUser, startAuthorizedLoginSession } from '../../utils/app-state'
|
import { getRegisteredUser, saveRegisteredUser, startAuthorizedLoginSession } from '../../utils/app-state'
|
||||||
import { mergeBackendUserInfo, normalizeUser, resolveRegistrationApplicationCode } from '../../utils/flow'
|
import { mergeBackendUserInfo, normalizeUser } from '../../utils/flow'
|
||||||
import {
|
import {
|
||||||
extractLoginSession,
|
extractLoginSession,
|
||||||
matchRegisteredUser,
|
matchRegisteredUser,
|
||||||
@@ -83,8 +83,8 @@ import {
|
|||||||
resolveLoginPreparation
|
resolveLoginPreparation
|
||||||
} from '../../utils/login-helper'
|
} from '../../utils/login-helper'
|
||||||
import { resolveBookingRedirectAfterLogin } from '../../utils/booking-entry'
|
import { resolveBookingRedirectAfterLogin } from '../../utils/booking-entry'
|
||||||
import { syncLoginStayState } from '../../utils/session-sync'
|
import { parseReservationFlowResponse, getReservationCurrentFlow, getReservationCurrentNode, hasEffectiveReservationFlow } from '../../utils/reservation-flow'
|
||||||
import { registrationMyorder, userMe, usersLogin } from '@/api/index.js'
|
import { registrationMyorder, orderSearchedReservationFlow, userMe, usersLogin } from '@/api/index.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
@@ -235,14 +235,39 @@ export default {
|
|||||||
|
|
||||||
return extractLoginSession(response, this.$getLanguage())
|
return extractLoginSession(response, this.$getLanguage())
|
||||||
},
|
},
|
||||||
fetchUserInfoAfterLogin() {
|
fetchReservationFlowAfterLogin(userCode = '') {
|
||||||
return userMe({}, this.$getLanguage())
|
const resolvedUserCode = String(userCode || '').trim()
|
||||||
.then(userInfoRes => {
|
if (!resolvedUserCode) {
|
||||||
return userInfoRes && userInfoRes.data ? userInfoRes.data : userInfoRes
|
return Promise.resolve({
|
||||||
|
flowList: [],
|
||||||
|
currentFlow: null,
|
||||||
|
currentNode: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return orderSearchedReservationFlow({
|
||||||
|
userCode: resolvedUserCode
|
||||||
|
}, this.$getLanguage())
|
||||||
|
.then(response => {
|
||||||
|
const flowList = parseReservationFlowResponse(response)
|
||||||
|
const currentFlow = getReservationCurrentFlow(flowList) || flowList[0] || null
|
||||||
|
const currentNode = currentFlow ? getReservationCurrentNode(currentFlow) : null
|
||||||
|
const hasActiveReservationFlow = flowList.some(item => hasEffectiveReservationFlow(item))
|
||||||
|
return {
|
||||||
|
flowList,
|
||||||
|
currentFlow,
|
||||||
|
currentNode,
|
||||||
|
hasActiveReservationFlow
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('fetch userinfo failed', error)
|
console.error('fetch reservation flow after login failed', error)
|
||||||
return null
|
return {
|
||||||
|
flowList: [],
|
||||||
|
currentFlow: null,
|
||||||
|
currentNode: null,
|
||||||
|
hasActiveReservationFlow: false
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
rejectHandledLogin(options = {}) {
|
rejectHandledLogin(options = {}) {
|
||||||
@@ -274,14 +299,15 @@ export default {
|
|||||||
async resolveLoginState() {
|
async resolveLoginState() {
|
||||||
const localRegisteredUser = normalizeUser(getRegisteredUser())
|
const localRegisteredUser = normalizeUser(getRegisteredUser())
|
||||||
const matchedLocalUser = matchRegisteredUser(localRegisteredUser, this.form.account)
|
const matchedLocalUser = matchRegisteredUser(localRegisteredUser, this.form.account)
|
||||||
const { token, expires } = await this.loginByApi()
|
const { token, expires, userCode } = await this.loginByApi()
|
||||||
const latestRegistration = await this.fetchLatestRegistration()
|
const latestRegistration = await this.fetchLatestRegistration()
|
||||||
const loginPreparation = resolveLoginPreparation({
|
const loginPreparation = resolveLoginPreparation({
|
||||||
language: this.$getLanguage(),
|
language: this.$getLanguage(),
|
||||||
matchedLocalUser,
|
matchedLocalUser,
|
||||||
latestRegistration,
|
latestRegistration,
|
||||||
account: this.form.account,
|
account: this.form.account,
|
||||||
password: this.form.password
|
password: this.form.password,
|
||||||
|
userCode
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!loginPreparation.ok) {
|
if (!loginPreparation.ok) {
|
||||||
@@ -301,21 +327,47 @@ export default {
|
|||||||
} = loginState
|
} = loginState
|
||||||
|
|
||||||
startAuthorizedLoginSession(sessionUser, token, expires)
|
startAuthorizedLoginSession(sessionUser, token, expires)
|
||||||
const backendUserInfo = await this.fetchUserInfoAfterLogin()
|
|
||||||
const finalUser = mergeBackendUserInfo(sessionUser, backendUserInfo)
|
// 先获取用户信息(含 userCode),再获取流程
|
||||||
|
let finalSessionUser = sessionUser
|
||||||
|
try {
|
||||||
|
const userInfoRes = await userMe({}, this.$getLanguage())
|
||||||
|
finalSessionUser = mergeBackendUserInfo(sessionUser, userInfoRes)
|
||||||
|
startAuthorizedLoginSession(finalSessionUser, token, expires)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('fetch user info after login failed', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userCode = finalSessionUser.userCode || finalSessionUser.code || ''
|
||||||
|
const reservationFlow = await this.fetchReservationFlowAfterLogin(userCode)
|
||||||
|
const currentStep = reservationFlow.currentNode ? reservationFlow.currentNode.step : null
|
||||||
|
|
||||||
|
const finalUser = {
|
||||||
|
...finalSessionUser,
|
||||||
|
reservationFlowList: reservationFlow.flowList,
|
||||||
|
currentReservationStep: currentStep,
|
||||||
|
hasActiveReservationFlow: reservationFlow.hasActiveReservationFlow
|
||||||
|
}
|
||||||
|
|
||||||
saveRegisteredUser(finalUser)
|
saveRegisteredUser(finalUser)
|
||||||
startAuthorizedLoginSession(finalUser, token, expires)
|
startAuthorizedLoginSession(finalUser, token, expires)
|
||||||
let nextRedirectUrl = this.redirectUrl
|
let nextRedirectUrl = this.redirectUrl
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stayState = await syncLoginStayState({
|
const stayState = {
|
||||||
language: this.$getLanguage(),
|
hasCheckedIn: currentStep === 5,
|
||||||
force: true
|
currentOrder: null,
|
||||||
})
|
currentBooking: reservationFlow.hasActiveReservationFlow && reservationFlow.currentFlow
|
||||||
|
? {
|
||||||
|
orderNo: reservationFlow.currentFlow.orderNo || '',
|
||||||
|
refNo: reservationFlow.currentNode ? String(reservationFlow.currentNode.refNo || '').trim() : '',
|
||||||
|
step: currentStep
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
nextRedirectUrl = resolveBookingRedirectAfterLogin(this.redirectUrl, stayState)
|
nextRedirectUrl = resolveBookingRedirectAfterLogin(this.redirectUrl, stayState)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('sync stay state after login failed', error)
|
console.error('resolve booking redirect after login failed', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -79,24 +79,193 @@
|
|||||||
<view class="tn-text-bold tn-text-xl" style="color: $brand-primary;">$500.00</view>
|
<view class="tn-text-bold tn-text-xl" style="color: $brand-primary;">$500.00</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="custom-btn btn-dark" style="width: 60%; height: 100rpx;" @click="submitBooking" :class="{ 'btn-disabled': isSubmitting }">
|
<view class="custom-btn btn-dark" style="width: 60%; height: 100rpx;" @click="submitBooking" :class="{ 'btn-disabled': isSubmitting }">
|
||||||
<text class="tn-text-bold tn-text-lg">{{ isSubmitting ? (language === 'zh' ? '处理中...' : 'Processing...') : $tr('进入支付页') }}</text>
|
<text class="tn-text-bold tn-text-lg">{{ isSubmitting ? (language === 'zh' ? '处理中...' : 'Processing...') : $tr('提交并支付') }}</text>
|
||||||
</view>
|
</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>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { resolveExistingBookingNavigationTarget } from '../../utils/booking-entry'
|
import { accountQueryBill, accountQueryBills, applicationFormDetail, paymentBillPay, paymentPayingBills, submitApplicationForm } from '../../api/index.js'
|
||||||
import { getSessionSnapshot } from '../../utils/app-state'
|
import { saveActiveApplicationCode, saveHomePageTabId } from '../../utils/app-state'
|
||||||
import { syncLoginStayState } from '../../utils/session-sync'
|
|
||||||
import { compactEnglishText } from '../../utils/select-room-normalizers'
|
import { compactEnglishText } from '../../utils/select-room-normalizers'
|
||||||
|
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(date = '', endOfDay = false) {
|
||||||
|
if (!date) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return `${date}T${endOfDay ? '23:59:59' : '00:00:00'}`
|
||||||
|
}
|
||||||
|
|
||||||
|
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 []
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
applyData: null,
|
applyData: null,
|
||||||
|
showDepositNotice: false,
|
||||||
|
hasReadNotice: false,
|
||||||
|
isClosingDepositNoticeForPayment: false,
|
||||||
|
depositNoticeReadyAt: 0,
|
||||||
|
pendingDepositPaymentContext: null,
|
||||||
form: {
|
form: {
|
||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: ''
|
endDate: ''
|
||||||
@@ -163,44 +332,339 @@ export default {
|
|||||||
console.error('Failed to parse apply data', e);
|
console.error('Failed to parse apply data', e);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.redirectIfBookingUnavailable()
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
redirectIfBookingUnavailable() {
|
|
||||||
const session = getSessionSnapshot()
|
|
||||||
if (!session.isLogin) {
|
|
||||||
return Promise.resolve(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return syncLoginStayState({
|
|
||||||
language: this.$getLanguage(),
|
|
||||||
force: true
|
|
||||||
})
|
|
||||||
.then((stayState) => {
|
|
||||||
const targetUrl = resolveExistingBookingNavigationTarget(stayState)
|
|
||||||
if (!targetUrl) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/^\/pages\/index\/index(?:\?|$)/.test(targetUrl)) {
|
|
||||||
uni.reLaunch({ url: targetUrl })
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
uni.redirectTo({ url: targetUrl })
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('check booking entry failed', error)
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onStartDateChange(e) {
|
onStartDateChange(e) {
|
||||||
this.form.startDate = e.detail.value;
|
this.form.startDate = e.detail.value;
|
||||||
},
|
},
|
||||||
onEndDateChange(e) {
|
onEndDateChange(e) {
|
||||||
this.form.endDate = e.detail.value;
|
this.form.endDate = e.detail.value;
|
||||||
},
|
},
|
||||||
|
goStayServiceFlow() {
|
||||||
|
saveHomePageTabId('stay-service')
|
||||||
|
uni.reLaunch({
|
||||||
|
url: '/pages/index/index?pageId=stay-service'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
buildSubmitPayload() {
|
||||||
|
return {
|
||||||
|
intendedAreaCode: this.applyData && this.applyData.areaCode ? this.applyData.areaCode : null,
|
||||||
|
intendedRoomTypeCode: this.applyData && this.applyData.roomTypeCode ? this.applyData.roomTypeCode : null,
|
||||||
|
roomSlotHeight: 1,
|
||||||
|
hobbiesAndInterestsTagKeys: null,
|
||||||
|
lifestyleHabitsTagKeys: null,
|
||||||
|
remark: null,
|
||||||
|
start: formatDateTime(this.form.startDate, false),
|
||||||
|
end: formatDateTime(this.form.endDate, true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
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 billNo = context && context.billNo ? context.billNo : ''
|
||||||
|
const pendingPaymentOrder = billNo ? await this.getPendingPaymentOrder(billNo).catch(() => null) : null
|
||||||
|
return {
|
||||||
|
referenceTypeNo: context && context.referenceTypeNo ? context.referenceTypeNo : String(referenceTypeNo || '').trim(),
|
||||||
|
billNo,
|
||||||
|
paymentNo: this.getPendingPaymentOrderNo(pendingPaymentOrder) || (context && context.paymentNo ? context.paymentNo : ''),
|
||||||
|
pendingPaymentOrder
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
openDepositNotice(paymentContext = {}) {
|
||||||
|
this.pendingDepositPaymentContext = {
|
||||||
|
referenceTypeNo: String(paymentContext.referenceTypeNo || '').trim(),
|
||||||
|
billNo: String(paymentContext.billNo || '').trim(),
|
||||||
|
paymentNo: String(paymentContext.paymentNo || '').trim(),
|
||||||
|
pendingPaymentOrder: paymentContext.pendingPaymentOrder || null
|
||||||
|
}
|
||||||
|
this.hasReadNotice = false
|
||||||
|
this.depositNoticeReadyAt = Date.now() + 600
|
||||||
|
setTimeout(() => {
|
||||||
|
this.showDepositNotice = true
|
||||||
|
}, 80)
|
||||||
|
},
|
||||||
|
closeDepositNotice() {
|
||||||
|
if (this.isClosingDepositNoticeForPayment) {
|
||||||
|
this.isClosingDepositNoticeForPayment = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.showDepositNotice = false
|
||||||
|
this.hasReadNotice = false
|
||||||
|
this.depositNoticeReadyAt = 0
|
||||||
|
this.pendingDepositPaymentContext = null
|
||||||
|
},
|
||||||
|
onNoticeScrollToBottom() {
|
||||||
|
this.hasReadNotice = true
|
||||||
|
},
|
||||||
|
async confirmAndPayDeposit() {
|
||||||
|
if (!this.hasReadNotice) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Date.now() < this.depositNoticeReadyAt) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const paymentContext = this.pendingDepositPaymentContext || {}
|
||||||
|
this.isClosingDepositNoticeForPayment = true
|
||||||
|
this.showDepositNotice = false
|
||||||
|
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.hasReadNotice = false
|
||||||
|
this.isClosingDepositNoticeForPayment = false
|
||||||
|
this.depositNoticeReadyAt = 0
|
||||||
|
this.pendingDepositPaymentContext = null
|
||||||
|
setTimeout(() => {
|
||||||
|
this.goStayServiceFlow()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
},
|
||||||
submitBooking() {
|
submitBooking() {
|
||||||
if (this.isSubmitting) return;
|
if (this.isSubmitting) return;
|
||||||
if (!this.form.startDate || !this.form.endDate) {
|
if (!this.form.startDate || !this.form.endDate) {
|
||||||
@@ -213,24 +677,39 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.isSubmitting = true;
|
this.isSubmitting = true;
|
||||||
const orderData = {
|
uni.showLoading({ title: this.language === 'zh' ? '提交中...' : 'Submitting...' });
|
||||||
...this.applyData,
|
|
||||||
startDate: this.form.startDate,
|
const payload = this.buildSubmitPayload()
|
||||||
endDate: this.form.endDate,
|
return submitApplicationForm(payload, this.language)
|
||||||
stayNights: this.stayNights,
|
.then(async (response) => {
|
||||||
sourceType: 'booking-intent'
|
const applicationCode = extractSubmitApplicationCode(response)
|
||||||
};
|
if (!applicationCode) {
|
||||||
|
throw new Error(this.$tr('申请单编号缺失'))
|
||||||
let roomDataStr = encodeURIComponent(JSON.stringify(orderData));
|
}
|
||||||
|
saveActiveApplicationCode(applicationCode)
|
||||||
uni.showLoading({ title: this.language === 'zh' ? '正在生成订单...' : 'Creating Order...' });
|
const paymentContext = await this.resolvePaymentPreview(applicationCode).catch(() => ({
|
||||||
setTimeout(() => {
|
referenceTypeNo: applicationCode,
|
||||||
uni.hideLoading();
|
billNo: '',
|
||||||
this.isSubmitting = false;
|
paymentNo: '',
|
||||||
uni.redirectTo({
|
pendingPaymentOrder: null
|
||||||
url: `/pkgBooking/pay/pay?roomData=${roomDataStr}`
|
}))
|
||||||
|
uni.hideLoading()
|
||||||
|
this.openDepositNotice({
|
||||||
|
...paymentContext,
|
||||||
|
referenceTypeNo: paymentContext.referenceTypeNo || applicationCode
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('submit booking application failed', error)
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({
|
||||||
|
title: (error && (error.message || error.msg)) || this.$tr('申请提交失败'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.isSubmitting = false;
|
||||||
});
|
});
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -347,4 +826,92 @@ export default {
|
|||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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: bold;
|
||||||
|
color: #1A2233;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-subtitle {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #64748B;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
</style>
|
||||||
|
|||||||
@@ -1,31 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="book-viewing-page tn-safe-area-inset-bottom">
|
<view class="book-viewing-page tn-safe-area-inset-bottom">
|
||||||
<tn-nav-bar :isBack="true" :backTitle="$tr('返回')" backgroundColor="#F0F6FF" :bottomShadow="false" :fixed="true">
|
<tn-nav-bar
|
||||||
|
:isBack="true"
|
||||||
|
:backTitle="$tr('返回')"
|
||||||
|
:backgroundColor="navBackgroundColor"
|
||||||
|
fontColor="#1A2233"
|
||||||
|
:bottomShadow="false"
|
||||||
|
:fixed="true"
|
||||||
|
>
|
||||||
<view class="nav-title">{{ $tr('预约现场带看') }}</view>
|
<view class="nav-title">{{ $tr('预约现场带看') }}</view>
|
||||||
</tn-nav-bar>
|
</tn-nav-bar>
|
||||||
|
|
||||||
<view class="content-wrapper" :style="{ paddingTop: vuex_custom_bar_height + 'px' }">
|
<view class="content-wrapper" :style="{ paddingTop: vuex_custom_bar_height + 'px' }">
|
||||||
|
<!-- 顶部信息卡片 -->
|
||||||
<!-- 房型/区域信息 -->
|
<view class="hero-card" v-if="displayInfoTitle">
|
||||||
<view class="info-card tn-flex tn-flex-row-between tn-flex-col-center" v-if="displayInfoTitle">
|
<view class="hero-glow"></view>
|
||||||
<view class="info-left">
|
<view class="hero-inner tn-flex tn-flex-row-between tn-flex-col-center">
|
||||||
<view class="info-title">{{ displayInfoTitle }}</view>
|
<view class="hero-content">
|
||||||
<view class="info-desc">{{ displayInfoDesc }}</view>
|
<view class="hero-tag">
|
||||||
</view>
|
<text class="tn-icon-company hero-tag-icon"></text>
|
||||||
<view class="info-icon">
|
<text class="hero-tag-text">{{ $tr('专属管家') }}</text>
|
||||||
<text class="tn-icon-company"></text>
|
</view>
|
||||||
|
<view class="hero-title">{{ displayInfoTitle }}</view>
|
||||||
|
<view class="hero-desc">{{ displayInfoDesc }}</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-company figure-icon"></text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="hero-accent"></view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 看房时间卡片 -->
|
<!-- 看房时间卡片 -->
|
||||||
<view class="form-card">
|
<view class="form-card">
|
||||||
<view class="card-header tn-flex tn-flex-row-between tn-flex-col-bottom">
|
<view class="card-header tn-flex tn-flex-col-bottom">
|
||||||
<view class="header-left tn-flex tn-flex-col-bottom">
|
<view class="header-icon header-icon--primary">
|
||||||
<text class="card-title">{{ $tr('选择看房时间') }}</text>
|
<text class="tn-icon-time"></text>
|
||||||
<text class="card-subtitle">{{ $tr('请选择您方便的时间段') }}</text>
|
</view>
|
||||||
|
<view class="header-titles">
|
||||||
|
<view class="card-title">{{ $tr('选择看房时间') }}</view>
|
||||||
|
<view class="card-subtitle">{{ $tr('请选择您方便的时间段') }}</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view class="section-label">{{ $tr('预约日期') }}</view>
|
||||||
<scroll-view class="date-scroll" scroll-x :show-scrollbar="false">
|
<scroll-view class="date-scroll" scroll-x :show-scrollbar="false">
|
||||||
<view class="date-flex">
|
<view class="date-flex">
|
||||||
<view
|
<view
|
||||||
@@ -37,10 +57,12 @@
|
|||||||
>
|
>
|
||||||
<text class="date-week">{{ item.week }}</text>
|
<text class="date-week">{{ item.week }}</text>
|
||||||
<text class="date-day">{{ item.label }}</text>
|
<text class="date-day">{{ item.label }}</text>
|
||||||
|
<view class="date-indicator"></view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
|
<view class="section-label section-label--time">{{ $tr('可约时段') }}</view>
|
||||||
<scroll-view class="time-scroll" scroll-x :show-scrollbar="false">
|
<scroll-view class="time-scroll" scroll-x :show-scrollbar="false">
|
||||||
<view class="time-flex">
|
<view class="time-flex">
|
||||||
<view
|
<view
|
||||||
@@ -50,23 +72,43 @@
|
|||||||
:class="{ 'time-slot--active': currentTimeIndex === index }"
|
:class="{ 'time-slot--active': currentTimeIndex === index }"
|
||||||
@click="selectTimeSlot(index)"
|
@click="selectTimeSlot(index)"
|
||||||
>
|
>
|
||||||
{{ item }}
|
<text class="time-slot-text">{{ item }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 已选时间摘要 -->
|
||||||
|
<view v-if="currentTimeIndex !== -1" class="selected-summary tn-flex tn-flex-col-center">
|
||||||
|
<view class="summary-line"></view>
|
||||||
|
<view class="summary-body tn-flex-1 tn-flex tn-flex-row-between tn-flex-col-center">
|
||||||
|
<view class="summary-info">
|
||||||
|
<text class="summary-label">{{ $tr('已选时间') }}</text>
|
||||||
|
<text class="summary-value">{{ selectedDateTimeText }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="summary-badge">
|
||||||
|
<text class="tn-icon-check"></text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 租住需求卡片 -->
|
<!-- 租住需求卡片 -->
|
||||||
<view class="form-card">
|
<view class="form-card">
|
||||||
<view class="card-header">
|
<view class="card-header tn-flex tn-flex-col-bottom">
|
||||||
<view class="header-left tn-flex tn-flex-col-bottom">
|
<view class="header-icon header-icon--luxury">
|
||||||
<text class="card-title">{{ $tr('您的租住需求') }}</text>
|
<text class="tn-icon-like"></text>
|
||||||
<text class="card-subtitle">{{ $tr('帮助管家更好地为您推荐房源') }}</text>
|
</view>
|
||||||
|
<view class="header-titles">
|
||||||
|
<view class="card-title">{{ $tr('您的租住需求') }}</view>
|
||||||
|
<view class="card-subtitle">{{ $tr('帮助管家更好地为您推荐房源') }}</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="field-item">
|
<view class="field-item">
|
||||||
<view class="field-label">{{ $tr('意向房型') }}</view>
|
<view class="field-label">
|
||||||
|
<text class="field-dot field-dot--primary"></text>
|
||||||
|
<text>{{ $tr('意向房型') }}</text>
|
||||||
|
</view>
|
||||||
<room-type-selector
|
<room-type-selector
|
||||||
v-model="formData.intendedRoomTypeCode"
|
v-model="formData.intendedRoomTypeCode"
|
||||||
:initial-area-code="formData.intendedAreaCode"
|
:initial-area-code="formData.intendedAreaCode"
|
||||||
@@ -75,7 +117,10 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="field-item">
|
<view class="field-item">
|
||||||
<view class="field-label">{{ $tr('预计租住时长') }}</view>
|
<view class="field-label">
|
||||||
|
<text class="field-dot field-dot--luxury"></text>
|
||||||
|
<text>{{ $tr('预计租住时长') }}</text>
|
||||||
|
</view>
|
||||||
<view class="tag-grid">
|
<view class="tag-grid">
|
||||||
<view
|
<view
|
||||||
v-for="(item, index) in durationOptions"
|
v-for="(item, index) in durationOptions"
|
||||||
@@ -90,9 +135,14 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="field-item">
|
<view class="field-item">
|
||||||
<view class="field-label">{{ $tr('租金预算 (元/月)') }}</view>
|
<view class="field-label">
|
||||||
|
<text class="field-dot field-dot--primary"></text>
|
||||||
|
<text>{{ $tr('租金预算 (元/月)') }}</text>
|
||||||
|
</view>
|
||||||
<view class="input-box tn-flex tn-flex-col-center">
|
<view class="input-box tn-flex tn-flex-col-center">
|
||||||
<text class="tn-icon-money input-icon"></text>
|
<view class="input-icon-wrap">
|
||||||
|
<text class="tn-icon-money input-icon"></text>
|
||||||
|
</view>
|
||||||
<input
|
<input
|
||||||
class="custom-input"
|
class="custom-input"
|
||||||
v-model="formData.budget"
|
v-model="formData.budget"
|
||||||
@@ -103,10 +153,15 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="field-item">
|
<view class="field-item field-item--last">
|
||||||
<view class="field-label">{{ $tr('工作/通勤地点') }}</view>
|
<view class="field-label">
|
||||||
|
<text class="field-dot field-dot--muted"></text>
|
||||||
|
<text>{{ $tr('工作/通勤地点') }}</text>
|
||||||
|
</view>
|
||||||
<view class="input-box tn-flex tn-flex-col-center">
|
<view class="input-box tn-flex tn-flex-col-center">
|
||||||
<text class="tn-icon-location input-icon"></text>
|
<view class="input-icon-wrap">
|
||||||
|
<text class="tn-icon-location input-icon"></text>
|
||||||
|
</view>
|
||||||
<input
|
<input
|
||||||
class="custom-input"
|
class="custom-input"
|
||||||
v-model="formData.workplace"
|
v-model="formData.workplace"
|
||||||
@@ -114,16 +169,26 @@
|
|||||||
:placeholder="$tr('请输入主要通勤地点')"
|
:placeholder="$tr('请输入主要通勤地点')"
|
||||||
placeholder-style="color: #A1B0C4;"
|
placeholder-style="color: #A1B0C4;"
|
||||||
/>
|
/>
|
||||||
|
<view class="input-divider"></view>
|
||||||
|
<view class="map-btn" @click="chooseWorkplaceLocation">
|
||||||
|
<text class="tn-icon-map map-btn-icon"></text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部占位,避免被 fixed 按钮遮挡 -->
|
||||||
|
<view class="footer-spacer"></view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 底部操作区 -->
|
<!-- 底部操作区 -->
|
||||||
<view class="footer-bar tn-safe-area-inset-bottom">
|
<view class="footer-bar tn-safe-area-inset-bottom">
|
||||||
<view class="submit-btn" @click="handleSubmit">
|
<view class="footer-inner">
|
||||||
<text>{{ $tr('提交预约') }}</text>
|
<view class="submit-btn" :class="{ 'submit-btn--active': canSubmit }" @click="handleSubmit">
|
||||||
|
<text class="submit-btn-icon tn-icon-send"></text>
|
||||||
|
<text class="submit-btn-text">{{ $tr('提交预约') }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="footer-hint">{{ $tr('提交后专属管家将尽快与您联系') }}</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -191,6 +256,9 @@ export default {
|
|||||||
language() {
|
language() {
|
||||||
return this.$getLanguage()
|
return this.$getLanguage()
|
||||||
},
|
},
|
||||||
|
navBackgroundColor() {
|
||||||
|
return this.scrollTop > 20 ? '#F8FAFF' : 'transparent'
|
||||||
|
},
|
||||||
displayInfoTitle() {
|
displayInfoTitle() {
|
||||||
if (!this.sourceData) {
|
if (!this.sourceData) {
|
||||||
return ''
|
return ''
|
||||||
@@ -214,6 +282,20 @@ export default {
|
|||||||
: (this.sourceData.areaLabel || this.sourceData.areaName || this.sourceData.areaLabelEn || this.sourceData.areaNameEn || '')
|
: (this.sourceData.areaLabel || this.sourceData.areaName || this.sourceData.areaLabelEn || this.sourceData.areaNameEn || '')
|
||||||
const meta = [bannerCopy, areaLabel, localizePayMethodText(this.sourceData.payMethod, this.language)].filter(Boolean).join(' · ')
|
const meta = [bannerCopy, areaLabel, localizePayMethodText(this.sourceData.payMethod, this.language)].filter(Boolean).join(' · ')
|
||||||
return meta || this.$tr('专属管家为您提供一对一带看服务')
|
return meta || this.$tr('专属管家为您提供一对一带看服务')
|
||||||
|
},
|
||||||
|
selectedDateTimeText() {
|
||||||
|
if (this.currentDateIndex === -1 || this.currentTimeIndex === -1) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const dateItem = this.dateList[this.currentDateIndex]
|
||||||
|
const timeItem = this.timeSlots[this.currentTimeIndex]
|
||||||
|
if (!dateItem || !timeItem) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return `${dateItem.week} ${dateItem.label} ${timeItem}`
|
||||||
|
},
|
||||||
|
canSubmit() {
|
||||||
|
return this.currentTimeIndex !== -1 && this.formData.duration
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -236,9 +318,13 @@ export default {
|
|||||||
workplace: ''
|
workplace: ''
|
||||||
},
|
},
|
||||||
sourceData: null,
|
sourceData: null,
|
||||||
isSubmitting: false
|
isSubmitting: false,
|
||||||
|
scrollTop: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onPageScroll(event) {
|
||||||
|
this.scrollTop = (event && event.scrollTop) || 0
|
||||||
|
},
|
||||||
onLoad(options) {
|
onLoad(options) {
|
||||||
this.syncDurationOptions()
|
this.syncDurationOptions()
|
||||||
this.generateDates()
|
this.generateDates()
|
||||||
@@ -304,13 +390,13 @@ export default {
|
|||||||
this.$tr('周五'),
|
this.$tr('周五'),
|
||||||
this.$tr('周六')
|
this.$tr('周六')
|
||||||
]
|
]
|
||||||
|
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
const d = new Date(today)
|
const d = new Date(today)
|
||||||
d.setDate(today.getDate() + i)
|
d.setDate(today.getDate() + i)
|
||||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
const day = String(d.getDate()).padStart(2, '0')
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
|
|
||||||
let weekStr = weeks[d.getDay()]
|
let weekStr = weeks[d.getDay()]
|
||||||
if (i === 0) weekStr = this.$tr('今天');
|
if (i === 0) weekStr = this.$tr('今天');
|
||||||
else if (i === 1) weekStr = this.$tr('明天');
|
else if (i === 1) weekStr = this.$tr('明天');
|
||||||
@@ -350,6 +436,26 @@ export default {
|
|||||||
selectDuration(item) {
|
selectDuration(item) {
|
||||||
this.formData.duration = item
|
this.formData.duration = item
|
||||||
},
|
},
|
||||||
|
chooseWorkplaceLocation() {
|
||||||
|
uni.chooseLocation({
|
||||||
|
success: (res) => {
|
||||||
|
const locationText = res.address || res.name || ''
|
||||||
|
if (locationText) {
|
||||||
|
this.formData.workplace = locationText
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
const errMsg = (error && error.errMsg) || ''
|
||||||
|
if (errMsg.includes('cancel') || errMsg.includes('Cancel')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showToast({
|
||||||
|
title: this.$tr('地图选择失败,请手动输入'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
handleSubmit() {
|
handleSubmit() {
|
||||||
if (this.isSubmitting) {
|
if (this.isSubmitting) {
|
||||||
return
|
return
|
||||||
@@ -391,8 +497,8 @@ export default {
|
|||||||
showCancel: false,
|
showCancel: false,
|
||||||
confirmColor: '#01BEFF',
|
confirmColor: '#01BEFF',
|
||||||
success: () => {
|
success: () => {
|
||||||
uni.redirectTo({
|
uni.reLaunch({
|
||||||
url: '/pkgService/my_application/my_application'
|
url: '/pages/index/index?pageId=stay-service'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -414,174 +520,387 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<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);
|
||||||
|
|
||||||
.book-viewing-page {
|
.book-viewing-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
|
background: linear-gradient(180deg, #EAF4FF 0%, $bg-page 30%, #FFFFFF 100%);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-title {
|
.nav-title {
|
||||||
font-size: 34rpx;
|
font-size: 34rpx;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
color: #1D2129;
|
color: $text-primary;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-wrapper {
|
.content-wrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
|
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 顶部信息卡片 */
|
/* 顶部信息卡片(Hero) */
|
||||||
.info-card {
|
.hero-card {
|
||||||
margin: 30rpx;
|
position: relative;
|
||||||
padding: 40rpx;
|
margin: 24rpx 30rpx 30rpx;
|
||||||
border-radius: 24rpx;
|
border-radius: 36rpx;
|
||||||
background: linear-gradient(135deg, #01BEFF 0%, #0099FF 100%);
|
background: linear-gradient(135deg, $hero-bg-start 0%, $hero-bg-end 100%);
|
||||||
box-shadow: 0 16rpx 32rpx rgba(1, 190, 255, 0.2);
|
box-shadow: 0 24rpx 60rpx rgba(10, 22, 40, 0.22);
|
||||||
color: #ffffff;
|
overflow: hidden;
|
||||||
|
color: #FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-left {
|
.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;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-title {
|
.hero-tag {
|
||||||
font-size: 36rpx;
|
display: inline-flex;
|
||||||
font-weight: bold;
|
align-items: center;
|
||||||
margin-bottom: 12rpx;
|
padding: 8rpx 18rpx;
|
||||||
letter-spacing: 2rpx;
|
background: rgba(255, 255, 255, 0.12);
|
||||||
line-height: 1.3;
|
border: 1rpx solid rgba(255, 255, 255, 0.18);
|
||||||
display: -webkit-box;
|
border-radius: 24rpx;
|
||||||
-webkit-line-clamp: 2;
|
margin-bottom: 20rpx;
|
||||||
-webkit-box-orient: vertical;
|
backdrop-filter: blur(8rpx);
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-desc {
|
.hero-tag-icon {
|
||||||
font-size: 24rpx;
|
font-size: 22rpx;
|
||||||
opacity: 0.9;
|
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;
|
letter-spacing: 1rpx;
|
||||||
line-height: 1.5;
|
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-icon {
|
.hero-desc {
|
||||||
font-size: 80rpx;
|
font-size: 24rpx;
|
||||||
opacity: 0.2;
|
color: rgba(255, 255, 255, 0.72);
|
||||||
margin-left: 20rpx;
|
line-height: 1.55;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-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 {
|
.form-card {
|
||||||
margin: 0 30rpx 30rpx;
|
margin: 0 30rpx 30rpx;
|
||||||
padding: 40rpx 30rpx;
|
padding: 36rpx 32rpx;
|
||||||
border-radius: 24rpx;
|
border-radius: 32rpx;
|
||||||
background: #ffffff;
|
background: $surface;
|
||||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.03);
|
box-shadow: $shadow-card;
|
||||||
|
border: 1rpx solid $border-soft;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
margin-bottom: 30rpx;
|
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 {
|
.card-title {
|
||||||
font-size: 34rpx;
|
font-size: 32rpx;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
color: #1D2129;
|
color: $text-primary;
|
||||||
margin-right: 16rpx;
|
margin-bottom: 6rpx;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-subtitle {
|
.card-subtitle {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #86909C;
|
color: $text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-label {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-primary;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
|
||||||
|
&--time {
|
||||||
|
margin-top: 36rpx;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 日期选择 */
|
/* 日期选择 */
|
||||||
.date-scroll {
|
.date-scroll {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
margin-bottom: 30rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-flex {
|
.date-flex {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: 20rpx;
|
gap: 18rpx;
|
||||||
padding-bottom: 10rpx;
|
padding: 6rpx 4rpx 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-item {
|
.date-item {
|
||||||
width: 110rpx;
|
width: 120rpx;
|
||||||
height: 130rpx;
|
height: 144rpx;
|
||||||
background: #F4F8FF;
|
background: #F4F8FF;
|
||||||
border-radius: 16rpx;
|
border-radius: 24rpx;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border: 2rpx solid transparent;
|
border: 2rpx solid transparent;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-item--active {
|
.date-item--active {
|
||||||
background: #EAF0FF;
|
background: linear-gradient(135deg, $brand-primary 0%, #0099FF 100%);
|
||||||
border-color: #01BEFF;
|
border-color: rgba($brand-primary, 0.35);
|
||||||
|
box-shadow: $shadow-active;
|
||||||
|
transform: translateY(-4rpx);
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-week {
|
.date-week {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #86909C;
|
color: $text-secondary;
|
||||||
margin-bottom: 8rpx;
|
margin-bottom: 10rpx;
|
||||||
transition: all 0.3s ease;
|
transition: color 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-item--active .date-week {
|
.date-item--active .date-week {
|
||||||
color: #01BEFF;
|
color: rgba(255, 255, 255, 0.9);
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-day {
|
.date-day {
|
||||||
font-size: 28rpx;
|
font-size: 30rpx;
|
||||||
color: #1D2129;
|
color: $text-primary;
|
||||||
font-weight: 500;
|
font-weight: 700;
|
||||||
transition: all 0.3s ease;
|
transition: color 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-item--active .date-day {
|
.date-item--active .date-day {
|
||||||
color: #01BEFF;
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 14rpx;
|
||||||
|
width: 8rpx;
|
||||||
|
height: 8rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $brand-primary;
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0);
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-item--active .date-indicator {
|
||||||
|
background: #FFFFFF;
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 时间段选择 */
|
/* 时间段选择 */
|
||||||
.time-scroll {
|
.time-scroll {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-flex {
|
.time-flex {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: 20rpx;
|
gap: 18rpx;
|
||||||
padding-bottom: 8rpx;
|
padding: 6rpx 4rpx 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-slot {
|
.time-slot {
|
||||||
width: 220rpx;
|
width: 200rpx;
|
||||||
height: 72rpx;
|
height: 80rpx;
|
||||||
line-height: 72rpx;
|
display: flex;
|
||||||
text-align: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
background: #F4F8FF;
|
background: #F4F8FF;
|
||||||
border-radius: 12rpx;
|
border-radius: 40rpx;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: #4E5969;
|
color: $text-secondary;
|
||||||
border: 2rpx solid transparent;
|
border: 2rpx solid transparent;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-slot--active {
|
.time-slot--active {
|
||||||
background: #01BEFF;
|
background: linear-gradient(135deg, $brand-primary 0%, #0099FF 100%);
|
||||||
color: #ffffff;
|
color: #FFFFFF;
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
box-shadow: 0 8rpx 16rpx rgba(1, 190, 255, 0.2);
|
border-color: rgba($brand-primary, 0.3);
|
||||||
|
box-shadow: $shadow-active;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-slot-text {
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 已选时间摘要 */
|
||||||
|
.selected-summary {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
background: linear-gradient(135deg, rgba($brand-primary, 0.06) 0%, rgba($brand-primary, 0.02) 100%);
|
||||||
|
border-radius: 20rpx;
|
||||||
|
border: 1rpx solid rgba($brand-primary, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-line {
|
||||||
|
width: 4rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border-radius: 2rpx;
|
||||||
|
background: $brand-primary;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-label {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $text-secondary;
|
||||||
|
margin-bottom: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-badge {
|
||||||
|
width: 44rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $brand-primary;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.25);
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 需求字段 */
|
/* 需求字段 */
|
||||||
@@ -589,15 +908,38 @@ export default {
|
|||||||
margin-bottom: 40rpx;
|
margin-bottom: 40rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-item:last-child {
|
.field-item--last {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-label {
|
.field-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
color: #1D2129;
|
font-weight: 700;
|
||||||
font-weight: bold;
|
color: $text-primary;
|
||||||
margin-bottom: 24rpx;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 标签选择 */
|
/* 标签选择 */
|
||||||
@@ -608,42 +950,96 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tag-item {
|
.tag-item {
|
||||||
padding: 0 32rpx;
|
padding: 0 34rpx;
|
||||||
height: 68rpx;
|
height: 72rpx;
|
||||||
line-height: 68rpx;
|
line-height: 72rpx;
|
||||||
background: #F4F8FF;
|
background: #F4F8FF;
|
||||||
border-radius: 34rpx;
|
border-radius: 36rpx;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: #4E5969;
|
color: $text-secondary;
|
||||||
border: 2rpx solid transparent;
|
border: 2rpx solid transparent;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-item--active {
|
.tag-item--active {
|
||||||
background: #EAF0FF;
|
background: linear-gradient(135deg, rgba($brand-primary, 0.1) 0%, rgba($brand-primary, 0.06) 100%);
|
||||||
border-color: #01BEFF;
|
border-color: rgba($brand-primary, 0.35);
|
||||||
color: #01BEFF;
|
color: $brand-primary;
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
|
box-shadow: 0 6rpx 16rpx rgba($brand-primary, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 输入框 */
|
/* 输入框 */
|
||||||
.input-box {
|
.input-box {
|
||||||
background: #F4F8FF;
|
background: #F4F8FF;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
padding: 0 26rpx;
|
||||||
|
border: 2rpx solid transparent;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box:focus-within {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-color: rgba($brand-primary, 0.25);
|
||||||
|
box-shadow: 0 6rpx 20rpx rgba($brand-primary, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-icon-wrap {
|
||||||
|
width: 52rpx;
|
||||||
|
height: 52rpx;
|
||||||
border-radius: 16rpx;
|
border-radius: 16rpx;
|
||||||
height: 88rpx;
|
background: #FFFFFF;
|
||||||
padding: 0 30rpx;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 18rpx;
|
||||||
|
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-icon {
|
.input-icon {
|
||||||
font-size: 36rpx;
|
font-size: 28rpx;
|
||||||
color: #A1B0C4;
|
color: $brand-primary;
|
||||||
margin-right: 16rpx;
|
}
|
||||||
|
|
||||||
|
.input-divider {
|
||||||
|
width: 2rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
background: rgba($text-tertiary, 0.25);
|
||||||
|
margin: 0 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-btn {
|
||||||
|
width: 52rpx;
|
||||||
|
height: 52rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background: rgba($brand-primary, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-btn:active {
|
||||||
|
background: rgba($brand-primary, 0.2);
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-btn-icon {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: $brand-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-input {
|
.custom-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
color: #1D2129;
|
color: $text-primary;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-spacer {
|
||||||
|
height: 180rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 底部按钮 */
|
/* 底部按钮 */
|
||||||
@@ -652,30 +1048,61 @@ export default {
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: #ffffff;
|
background: rgba(255, 255, 255, 0.96);
|
||||||
padding: 24rpx 40rpx;
|
backdrop-filter: blur(20rpx);
|
||||||
box-shadow: 0 -4rpx 24rpx rgba(0, 0, 0, 0.04);
|
padding: 24rpx 40rpx calc(24rpx + env(safe-area-inset-bottom));
|
||||||
|
box-shadow: 0 -8rpx 32rpx rgba(26, 34, 51, 0.06);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer-inner {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.submit-btn {
|
.submit-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 96rpx;
|
height: 96rpx;
|
||||||
background: linear-gradient(135deg, #01BEFF 0%, #0099FF 100%);
|
|
||||||
border-radius: 48rpx;
|
border-radius: 48rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #ffffff;
|
background: linear-gradient(135deg, #A1B0C4 0%, #86909C 100%);
|
||||||
|
color: #FFFFFF;
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
letter-spacing: 2rpx;
|
letter-spacing: 2rpx;
|
||||||
box-shadow: 0 12rpx 24rpx rgba(1, 190, 255, 0.25);
|
box-shadow: 0 8rpx 24rpx rgba(134, 144, 156, 0.2);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-btn:active {
|
.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);
|
transform: scale(0.98);
|
||||||
opacity: 0.9;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 导航栏背景色滚动过渡 */
|
||||||
|
.book-viewing-page ::v-deep .tn-custom-nav-bar__bar {
|
||||||
|
transition: background-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,40 +1,53 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="room-type-selector">
|
<view class="room-type-selector">
|
||||||
<view class="selector-header tn-flex tn-flex-row-between tn-flex-col-center" @click="openSelector">
|
<view class="selector-card tn-flex tn-flex-row-between tn-flex-col-center" @click="openSelector">
|
||||||
<view class="selected-info">
|
<view class="selected-info">
|
||||||
|
<view class="selected-label">{{ $tr('意向房型') }}</view>
|
||||||
<view class="selected-name tn-text-ellipsis" :class="{ 'is-empty': !selectedRoomType }">
|
<view class="selected-name tn-text-ellipsis" :class="{ 'is-empty': !selectedRoomType }">
|
||||||
{{ selectedRoomType ? getRoomTypeDisplayName(selectedRoomType) : $tr('请选择房型') }}
|
{{ selectedRoomType ? getRoomTypeDisplayName(selectedRoomType) : $tr('请选择意向房型') }}
|
||||||
</view>
|
</view>
|
||||||
<view class="selected-area tn-text-ellipsis" v-if="selectedArea">
|
<view class="selected-area tn-text-ellipsis" v-if="selectedArea">
|
||||||
|
<text class="tn-icon-location area-icon"></text>
|
||||||
{{ getAreaDisplayName(selectedArea) }}
|
{{ getAreaDisplayName(selectedArea) }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="tn-icon-right tn-color-gray"></text>
|
<view class="selector-arrow">
|
||||||
|
<text class="tn-icon-right"></text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<tn-popup v-model="showPopup" mode="bottom" length="80%" :borderRadius="32" :closeBtn="true">
|
<tn-popup v-model="showPopup" mode="bottom" length="80%" :borderRadius="32" :closeBtn="true">
|
||||||
<view class="popup-content">
|
<view class="popup-content">
|
||||||
<view class="popup-header">
|
<view class="popup-header">
|
||||||
<text class="popup-title">{{ $tr('选择意向房型') }}</text>
|
<view class="popup-title">{{ $tr('选择意向房型') }}</view>
|
||||||
|
<view class="popup-subtitle">{{ $tr('选择您心仪的房型与区域') }}</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<scroll-view scroll-y class="area-list-scroll">
|
<scroll-view scroll-y class="area-list-scroll">
|
||||||
<view v-if="isLoading" class="loading-state tn-padding-xl tn-text-center">
|
<view v-if="isLoading" class="loading-state tn-padding-xl tn-text-center">
|
||||||
<tn-loading mode="circle"></tn-loading>
|
<tn-loading mode="circle" :size="48"></tn-loading>
|
||||||
<view class="tn-margin-top-sm tn-color-gray">{{ $tr('房型加载中...') }}</view>
|
<view class="tn-margin-top tn-color-gray">{{ $tr('房型加载中...') }}</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else-if="!areaList.length" class="empty-state tn-padding-xl tn-text-center">
|
<view v-else-if="!areaList.length" class="empty-state tn-padding-xl tn-text-center">
|
||||||
<text class="tn-color-gray">{{ $tr('暂无可选房型') }}</text>
|
<view class="empty-icon">
|
||||||
|
<text class="tn-icon-empty-data"></text>
|
||||||
|
</view>
|
||||||
|
<view class="empty-title">{{ $tr('暂无可选房型') }}</view>
|
||||||
|
<view class="empty-desc">{{ $tr('当前暂无可用房型,请稍后再试') }}</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else class="area-group" v-for="(area, index) in areaList" :key="area.code">
|
<view v-else class="area-group" v-for="(area, index) in areaList" :key="area.code">
|
||||||
<view class="area-title tn-flex tn-flex-row-between tn-flex-col-center" @click="toggleArea(index)">
|
<view class="area-title tn-flex tn-flex-row-between tn-flex-col-center" @click="toggleArea(index)">
|
||||||
<view class="tn-flex tn-flex-col-center">
|
<view class="area-title-left tn-flex tn-flex-col-center">
|
||||||
<text class="area-dot"></text>
|
<view class="area-icon-wrap">
|
||||||
<text>{{ getAreaDisplayName(area) }}</text>
|
<text class="tn-icon-home"></text>
|
||||||
|
</view>
|
||||||
|
<text class="area-title-text">{{ getAreaDisplayName(area) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="area-toggle">
|
||||||
|
<text :class="collapsedAreas[index] ? 'tn-icon-down' : 'tn-icon-up'"></text>
|
||||||
</view>
|
</view>
|
||||||
<text :class="collapsedAreas[index] ? 'tn-icon-down' : 'tn-icon-up'" class="tn-color-gray"></text>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="room-type-grid" v-show="!collapsedAreas[index]">
|
<view class="room-type-grid" v-show="!collapsedAreas[index]">
|
||||||
<view
|
<view
|
||||||
@@ -45,7 +58,15 @@
|
|||||||
@click="handleSelect(area, room)"
|
@click="handleSelect(area, room)"
|
||||||
>
|
>
|
||||||
<view class="room-name tn-text-ellipsis">{{ getRoomTypeDisplayName(room) }}</view>
|
<view class="room-name tn-text-ellipsis">{{ getRoomTypeDisplayName(room) }}</view>
|
||||||
<view class="room-price" v-if="room.price">¥{{ getPriceValue(room.price) }}{{ $tr('/月') }}</view>
|
<view class="room-price" v-if="room.price">
|
||||||
|
<text class="price-symbol">¥</text>
|
||||||
|
<text class="price-value">{{ getPriceValue(room.price) }}</text>
|
||||||
|
<text class="price-unit">{{ $tr('/月') }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="room-price room-price--empty">{{ $tr('价格面议') }}</view>
|
||||||
|
<view class="active-mark">
|
||||||
|
<text class="tn-icon-check"></text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -59,25 +80,6 @@
|
|||||||
import { areaPagination } from '@/api/index.js'
|
import { areaPagination } from '@/api/index.js'
|
||||||
import { normalizeAreaItem } from '../../../utils/select-room-normalizers'
|
import { normalizeAreaItem } from '../../../utils/select-room-normalizers'
|
||||||
|
|
||||||
const I18N = {
|
|
||||||
zh: {
|
|
||||||
emptyRoomTypes: '暂无可选房型',
|
|
||||||
loadFailed: '房型加载失败',
|
|
||||||
loadingRoomTypes: '房型加载中...',
|
|
||||||
popupTitle: '选择意向房型',
|
|
||||||
priceUnit: '/月',
|
|
||||||
selectRoomType: '请选择房型'
|
|
||||||
},
|
|
||||||
en: {
|
|
||||||
emptyRoomTypes: 'No room types available',
|
|
||||||
loadFailed: 'Failed to load room types',
|
|
||||||
loadingRoomTypes: 'Loading room types...',
|
|
||||||
popupTitle: 'Select Room Type',
|
|
||||||
priceUnit: '/month',
|
|
||||||
selectRoomType: 'Select room type'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'BookViewingRoomTypeSelector',
|
name: 'BookViewingRoomTypeSelector',
|
||||||
props: {
|
props: {
|
||||||
@@ -230,17 +232,30 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
$luxury-gold: #D8B56A;
|
||||||
|
$text-primary: #1A2233;
|
||||||
|
$text-secondary: #6A7A99;
|
||||||
|
$text-tertiary: #A1B0C4;
|
||||||
|
$surface: #FFFFFF;
|
||||||
|
$bg-popup: #F8FAFF;
|
||||||
|
|
||||||
.room-type-selector {
|
.room-type-selector {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-header {
|
.selector-card {
|
||||||
background: #FFFFFF;
|
background: $surface;
|
||||||
border-radius: 20rpx;
|
border-radius: 20rpx;
|
||||||
padding: 26rpx 32rpx;
|
padding: 26rpx 30rpx;
|
||||||
border: 1rpx solid rgba($brand-primary, 0.15);
|
border: 2rpx solid rgba($brand-primary, 0.12);
|
||||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
box-shadow: 0 6rpx 20rpx rgba($brand-primary, 0.05);
|
||||||
min-height: 100rpx;
|
min-height: 120rpx;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-card:active {
|
||||||
|
background: #FAFCFF;
|
||||||
|
border-color: rgba($brand-primary, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected-info {
|
.selected-info {
|
||||||
@@ -248,72 +263,175 @@ export default {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selected-label {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $text-tertiary;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.selected-name {
|
.selected-name {
|
||||||
font-size: 28rpx;
|
font-size: 30rpx;
|
||||||
color: #1A2233;
|
color: $text-primary;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
|
|
||||||
&.is-empty {
|
&.is-empty {
|
||||||
color: #A0B0C0;
|
color: $text-tertiary;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected-area {
|
.selected-area {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-secondary;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.area-icon {
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: #6A7A99;
|
margin-right: 6rpx;
|
||||||
margin-top: 6rpx;
|
color: $brand-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-arrow {
|
||||||
|
width: 52rpx;
|
||||||
|
height: 52rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #F4F8FF;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: $text-secondary;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-content {
|
.popup-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: #F8FAFF;
|
background: $bg-popup;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-header {
|
.popup-header {
|
||||||
padding: 40rpx 30rpx 20rpx;
|
padding: 40rpx 30rpx 28rpx;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-bottom: 1px dashed rgba($brand-primary, 0.15);
|
background: $surface;
|
||||||
background: #FFFFFF;
|
border-bottom: 1rpx solid rgba($brand-primary, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-title {
|
.popup-title {
|
||||||
font-size: 32rpx;
|
font-size: 34rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1A2233;
|
color: $text-primary;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-subtitle {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.area-list-scroll {
|
.area-list-scroll {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 0;
|
height: 0;
|
||||||
padding: 20rpx 30rpx 60rpx;
|
padding: 24rpx 30rpx 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state,
|
||||||
|
.empty-state {
|
||||||
|
padding-top: 120rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba($brand-primary, 0.08);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto 24rpx;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 56rpx;
|
||||||
|
color: $brand-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-primary;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-desc {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.area-group {
|
.area-group {
|
||||||
margin-bottom: 30rpx;
|
margin-bottom: 28rpx;
|
||||||
background: #FFFFFF;
|
background: $surface;
|
||||||
border-radius: 20rpx;
|
border-radius: 24rpx;
|
||||||
padding: 24rpx;
|
padding: 28rpx;
|
||||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.04);
|
box-shadow: 0 8rpx 28rpx rgba(26, 34, 51, 0.04);
|
||||||
border: 1rpx solid rgba($brand-primary, 0.05);
|
border: 1rpx solid rgba($brand-primary, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.area-title {
|
.area-title {
|
||||||
font-size: 28rpx;
|
margin-bottom: 24rpx;
|
||||||
font-weight: 600;
|
|
||||||
color: #1A2233;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.area-dot {
|
.area-title-left {
|
||||||
width: 12rpx;
|
flex: 1;
|
||||||
height: 12rpx;
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.area-icon-wrap {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
border-radius: 14rpx;
|
||||||
|
background: rgba($brand-primary, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $brand-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.area-title-text {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.area-toggle {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: $brand-primary;
|
background: #F4F8FF;
|
||||||
margin-right: 12rpx;
|
display: flex;
|
||||||
box-shadow: 0 0 8rpx rgba($brand-primary, 0.4);
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-secondary;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.room-type-grid {
|
.room-type-grid {
|
||||||
@@ -323,17 +441,18 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.room-type-item {
|
.room-type-item {
|
||||||
padding: 20rpx;
|
position: relative;
|
||||||
border-radius: 16rpx;
|
padding: 24rpx 20rpx;
|
||||||
background: #FFFFFF;
|
border-radius: 20rpx;
|
||||||
border: 1rpx solid rgba($brand-primary, 0.15);
|
background: #FAFCFF;
|
||||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
border: 2rpx solid rgba($brand-primary, 0.1);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
&.is-active {
|
&.is-active {
|
||||||
background: #FFFFFF;
|
background: #FFFFFF;
|
||||||
border-color: $brand-primary;
|
border-color: $brand-primary;
|
||||||
box-shadow: 0 4rpx 16rpx rgba($brand-primary, 0.1);
|
box-shadow: 0 8rpx 24rpx rgba($brand-primary, 0.14);
|
||||||
|
|
||||||
.room-name {
|
.room-name {
|
||||||
color: $brand-primary;
|
color: $brand-primary;
|
||||||
@@ -342,18 +461,66 @@ export default {
|
|||||||
.room-price {
|
.room-price {
|
||||||
color: $brand-primary;
|
color: $brand-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.active-mark {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.room-name {
|
.room-name {
|
||||||
font-size: 26rpx;
|
font-size: 28rpx;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: #1A2233;
|
color: $text-primary;
|
||||||
margin-bottom: 8rpx;
|
margin-bottom: 10rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.room-price {
|
.room-price {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-secondary;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
&--empty {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $text-tertiary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-symbol {
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: #6A7A99;
|
margin-right: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-right: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-unit {
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-mark {
|
||||||
|
position: absolute;
|
||||||
|
top: 12rpx;
|
||||||
|
right: 12rpx;
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $brand-primary;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transform: scale(0);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -192,9 +192,10 @@ export default {
|
|||||||
`${this.$tr('支付方式')}: ${this.payMethod}`,
|
`${this.$tr('支付方式')}: ${this.payMethod}`,
|
||||||
`${this.$tr('支付金额')}: $500.00`,
|
`${this.$tr('支付金额')}: $500.00`,
|
||||||
'',
|
'',
|
||||||
`1. ${this.$tr('请确认支付方式和金额无误后再继续操作。')}`,
|
`1. ${this.$tr('请在 5 分钟内支付定金或者款项,否则系统将自动取消这个订单,释放锁定的房源。')}`,
|
||||||
`2. ${this.$tr('当前页面为预支付引导页,真实支付接口暂未接入。')}`,
|
`2. ${this.$tr('请确认支付方式和金额无误后再继续操作。')}`,
|
||||||
`3. ${this.$tr('确认后将进入后续提交流程,由工作人员继续跟进。')}`
|
`3. ${this.$tr('当前页面为预支付引导页,真实支付接口暂未接入。')}`,
|
||||||
|
`4. ${this.$tr('确认后将进入后续提交流程,由工作人员继续跟进。')}`
|
||||||
].join('\n')
|
].join('\n')
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<text class="asset-name tn-text-ellipsis">{{ item.assetName }}</text>
|
<text class="asset-name tn-text-ellipsis">{{ getAssetDisplayName(item) }}</text>
|
||||||
<text v-if="item.brand" class="asset-brand tn-text-ellipsis">{{ item.brand }}</text>
|
<text v-if="item.brand" class="asset-brand tn-text-ellipsis">{{ item.brand }}</text>
|
||||||
<text v-else class="asset-brand-placeholder"></text>
|
<text v-else class="asset-brand-placeholder"></text>
|
||||||
</view>
|
</view>
|
||||||
@@ -53,7 +53,14 @@ export default {
|
|||||||
default: 'zh'
|
default: 'zh'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {}
|
methods: {
|
||||||
|
getAssetDisplayName(item = {}) {
|
||||||
|
if (this.language === 'en') {
|
||||||
|
return item.assetNameEn || item.assetName || ''
|
||||||
|
}
|
||||||
|
return item.assetName || item.assetNameEn || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -446,6 +446,7 @@ function normalizeRoomAssets(list = []) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const assetName = normalizeText(item.assetName || item.name || item.label)
|
const assetName = normalizeText(item.assetName || item.name || item.label)
|
||||||
|
const assetNameEn = normalizeText(item.assetNameEn || item.nameEn || item.labelEn)
|
||||||
if (!assetName) {
|
if (!assetName) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -456,6 +457,7 @@ function normalizeRoomAssets(list = []) {
|
|||||||
key: `${item.roomTypeCode || 'asset'}-${item.assetName || item.name || index}`,
|
key: `${item.roomTypeCode || 'asset'}-${item.assetName || item.name || index}`,
|
||||||
roomTypeCode: item.roomTypeCode || '',
|
roomTypeCode: item.roomTypeCode || '',
|
||||||
assetName,
|
assetName,
|
||||||
|
assetNameEn,
|
||||||
brand: normalizeText(item.brand),
|
brand: normalizeText(item.brand),
|
||||||
icon: iconClass ? '' : addFilePrefix(rawIcon),
|
icon: iconClass ? '' : addFilePrefix(rawIcon),
|
||||||
iconClass,
|
iconClass,
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="section-panel" v-if="facilityList && facilityList.length">
|
<view class="section-panel" v-if="facilityList && facilityList.length">
|
||||||
<view class="section-header">
|
<view class="section-header">
|
||||||
|
<view class="title-bar"></view>
|
||||||
<text class="section-title">{{ $tr('配套设施') }}</text>
|
<text class="section-title">{{ $tr('配套设施') }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="facility-grid tn-flex tn-flex-wrap">
|
<view class="facility-grid tn-flex tn-flex-wrap">
|
||||||
<view class="facility-item tn-flex tn-flex-direction-column tn-flex-col-center" v-for="(item, index) in facilityList" :key="index">
|
<view class="facility-item tn-flex tn-flex-direction-column tn-flex-col-center" v-for="(item, index) in facilityList" :key="index">
|
||||||
<text :class="item.icon" class="facility-icon"></text>
|
<view class="icon-wrap tn-flex tn-flex-row-center tn-flex-col-center">
|
||||||
|
<text :class="item.icon" class="facility-icon"></text>
|
||||||
|
</view>
|
||||||
<text class="facility-name">{{ item.name }}</text>
|
<text class="facility-name">{{ item.name }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -29,25 +32,39 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
$navy-deep: #1E3A5F;
|
||||||
|
$gold: #C9A96E;
|
||||||
|
$gold-light: #D4BC8C;
|
||||||
|
$text-primary: #0F172A;
|
||||||
|
$text-secondary: #64748B;
|
||||||
|
|
||||||
.section-panel {
|
.section-panel {
|
||||||
background: rgba(255, 255, 255, 0.08);
|
background: #FFFFFF;
|
||||||
backdrop-filter: blur(20px);
|
border: 1rpx solid rgba($navy-deep, 0.08);
|
||||||
-webkit-backdrop-filter: blur(20px);
|
box-shadow: 0 8rpx 28rpx rgba(30, 58, 95, 0.06);
|
||||||
border: 1rpx solid rgba(255, 255, 255, 0.15);
|
|
||||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.05);
|
|
||||||
margin: 24rpx 32rpx 0;
|
margin: 24rpx 32rpx 0;
|
||||||
padding: 32rpx;
|
padding: 32rpx;
|
||||||
border-radius: 24rpx;
|
border-radius: 28rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
margin-bottom: 24rpx;
|
margin-bottom: 28rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar {
|
||||||
|
width: 6rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
background: linear-gradient(180deg, $gold 0%, $gold-light 100%);
|
||||||
|
border-radius: 999rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
font-weight: bold;
|
font-weight: 800;
|
||||||
color: #fff;
|
color: $navy-deep;
|
||||||
}
|
}
|
||||||
|
|
||||||
.facility-grid {
|
.facility-grid {
|
||||||
@@ -58,14 +75,23 @@ export default {
|
|||||||
width: 25%;
|
width: 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-wrap {
|
||||||
|
width: 88rpx;
|
||||||
|
height: 88rpx;
|
||||||
|
background: linear-gradient(135deg, rgba($navy-deep, 0.08) 0%, rgba($navy-deep, 0.03) 100%);
|
||||||
|
border: 1rpx solid rgba($navy-deep, 0.08);
|
||||||
|
border-radius: 24rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.facility-icon {
|
.facility-icon {
|
||||||
font-size: 52rpx;
|
font-size: 44rpx;
|
||||||
color: rgba(255, 255, 255, 0.9);
|
color: $navy-deep;
|
||||||
margin-bottom: 12rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.facility-name {
|
.facility-name {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: $text-secondary;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="section-panel room-section">
|
<view class="section-panel room-section">
|
||||||
<view class="section-header tn-flex tn-flex-row-between tn-flex-col-center">
|
<view class="section-header tn-flex tn-flex-row-between tn-flex-col-center">
|
||||||
<text class="section-title">{{ $tr('在租房型({count})', { count: roomTypes.length }) }}</text>
|
<view class="title-wrap tn-flex tn-flex-col-center">
|
||||||
|
<view class="title-bar"></view>
|
||||||
|
<text class="section-title">{{ $tr('在租房型({count})', { count: roomTypes.length }) }}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="room-list-vertical">
|
<view class="room-list-vertical">
|
||||||
<view
|
<view
|
||||||
@@ -99,25 +102,43 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
$navy-deep: #1E3A5F;
|
||||||
|
$navy-light: #2E4A73;
|
||||||
|
$gold: #C9A96E;
|
||||||
|
$gold-light: #D4BC8C;
|
||||||
|
$text-primary: #0F172A;
|
||||||
|
$text-secondary: #64748B;
|
||||||
|
$text-tertiary: #94A3B8;
|
||||||
|
|
||||||
.section-panel {
|
.section-panel {
|
||||||
background: rgba(255, 255, 255, 0.08);
|
background: #FFFFFF;
|
||||||
backdrop-filter: blur(20px);
|
border: 1rpx solid rgba($navy-deep, 0.08);
|
||||||
-webkit-backdrop-filter: blur(20px);
|
box-shadow: 0 8rpx 28rpx rgba(30, 58, 95, 0.06);
|
||||||
border: 1rpx solid rgba(255, 255, 255, 0.15);
|
|
||||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.05);
|
|
||||||
margin: 24rpx 32rpx 0;
|
margin: 24rpx 32rpx 0;
|
||||||
padding: 32rpx;
|
padding: 32rpx;
|
||||||
border-radius: 24rpx;
|
border-radius: 28rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
margin-bottom: 24rpx;
|
margin-bottom: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-wrap {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar {
|
||||||
|
width: 6rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
background: linear-gradient(180deg, $gold 0%, $gold-light 100%);
|
||||||
|
border-radius: 999rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
font-weight: bold;
|
font-weight: 800;
|
||||||
color: #fff;
|
color: $navy-deep;
|
||||||
}
|
}
|
||||||
|
|
||||||
.room-list-vertical {
|
.room-list-vertical {
|
||||||
@@ -129,11 +150,17 @@ export default {
|
|||||||
.room-card-v {
|
.room-card-v {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
border-radius: 16rpx;
|
border-radius: 24rpx;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: #FFFFFF;
|
||||||
border: 1rpx solid rgba(255, 255, 255, 0.1);
|
border: 1rpx solid rgba($navy-deep, 0.08);
|
||||||
box-shadow: none;
|
box-shadow: 0 8rpx 24rpx rgba(30, 58, 95, 0.05);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-card-v:active {
|
||||||
|
transform: translateY(-4rpx);
|
||||||
|
box-shadow: 0 16rpx 40rpx rgba(30, 58, 95, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.room-img-v {
|
.room-img-v {
|
||||||
@@ -146,38 +173,38 @@ export default {
|
|||||||
.img {
|
.img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: linear-gradient(135deg, #E2E8F0 0%, #CBD5E1 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pay-method-tag {
|
.pay-method-tag {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 12rpx;
|
top: 12rpx;
|
||||||
left: 12rpx;
|
left: 12rpx;
|
||||||
background: rgba(0, 0, 0, 0.6);
|
background: rgba($navy-deep, 0.85);
|
||||||
backdrop-filter: blur(8rpx);
|
backdrop-filter: blur(8rpx);
|
||||||
color: #fff;
|
color: $gold-light;
|
||||||
font-size: 18rpx;
|
font-size: 18rpx;
|
||||||
padding: 4rpx 10rpx;
|
padding: 4rpx 12rpx;
|
||||||
border-radius: 6rpx;
|
border-radius: 8rpx;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.room-info-v {
|
.room-info-v {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 16rpx 20rpx;
|
padding: 20rpx 24rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.room-header-v {
|
.room-header-v {
|
||||||
margin-bottom: 8rpx;
|
margin-bottom: 10rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.room-name-v {
|
.room-name-v {
|
||||||
font-size: 30rpx;
|
font-size: 30rpx;
|
||||||
font-weight: bold;
|
font-weight: 800;
|
||||||
color: #fff;
|
color: $navy-deep;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,16 +215,18 @@ export default {
|
|||||||
|
|
||||||
.tag-v {
|
.tag-v {
|
||||||
font-size: 20rpx;
|
font-size: 20rpx;
|
||||||
color: rgba(255, 255, 255, 0.85);
|
color: $navy-deep;
|
||||||
background: rgba(255, 255, 255, 0.15);
|
background: rgba($navy-deep, 0.06);
|
||||||
padding: 4rpx 12rpx;
|
padding: 4rpx 12rpx;
|
||||||
border-radius: 6rpx;
|
border-radius: 8rpx;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.room-desc-v {
|
.room-desc-v {
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: rgba(255, 255, 255, 0.6);
|
color: $text-tertiary;
|
||||||
margin-bottom: auto; /* 将底部价格推下去 */
|
margin-bottom: auto;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.room-bottom-v {
|
.room-bottom-v {
|
||||||
@@ -205,41 +234,45 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.room-price-v {
|
.room-price-v {
|
||||||
color: #ffb8b8;
|
color: $navy-deep;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-symbol {
|
.price-symbol {
|
||||||
font-size: 20rpx;
|
font-size: 20rpx;
|
||||||
font-weight: bold;
|
font-weight: 800;
|
||||||
margin-right: 2rpx;
|
margin-right: 2rpx;
|
||||||
|
color: $gold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-val {
|
.price-val {
|
||||||
font-size: 36rpx;
|
font-size: 36rpx;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
color: $navy-deep;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-unit {
|
.price-unit {
|
||||||
font-size: 20rpx;
|
font-size: 20rpx;
|
||||||
color: rgba(255, 255, 255, 0.6);
|
color: $text-tertiary;
|
||||||
margin-left: 4rpx;
|
margin-left: 4rpx;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.go-btn-v {
|
.go-btn-v {
|
||||||
background: rgba(255, 255, 255, 0.15);
|
background: linear-gradient(135deg, $navy-light 0%, $navy-deep 100%);
|
||||||
color: #fff;
|
color: #FFFFFF;
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
padding: 8rpx 20rpx;
|
padding: 10rpx 22rpx;
|
||||||
border-radius: 30rpx;
|
border-radius: 30rpx;
|
||||||
border: 1rpx solid rgba(255, 255, 255, 0.2);
|
font-weight: 600;
|
||||||
|
box-shadow: 0 6rpx 16rpx rgba($navy-deep, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.go-btn-v .tn-icon-right {
|
.go-btn-v .tn-icon-right {
|
||||||
font-size: 20rpx;
|
font-size: 20rpx;
|
||||||
margin-left: 4rpx;
|
margin-left: 4rpx;
|
||||||
|
color: $gold-light;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="store-info-panel">
|
<view class="store-info-panel">
|
||||||
<!-- 核心信息卡片 -->
|
<!-- 核心信息卡片:地址 -->
|
||||||
<view class="info-card tn-flex tn-flex-row-between tn-flex-col-center" @click="openMap">
|
<view class="info-card tn-flex tn-flex-row-between tn-flex-col-center" @click="openMap">
|
||||||
<view class="address-left tn-flex tn-flex-col-center">
|
<view class="address-left tn-flex tn-flex-col-center">
|
||||||
<view class="icon-wrap tn-flex tn-flex-row-center tn-flex-col-center">
|
<view class="icon-wrap tn-flex tn-flex-row-center tn-flex-col-center">
|
||||||
<text class="tn-icon-location-fill location-icon"></text>
|
<text class="tn-icon-location-fill location-icon"></text>
|
||||||
</view>
|
</view>
|
||||||
<text class="address-title tn-text-ellipsis">{{ shortAddress }}</text>
|
<view class="address-content">
|
||||||
|
<text class="address-label">{{ $tr('公寓地址') }}</text>
|
||||||
|
<text class="address-title tn-text-ellipsis">{{ shortAddress }}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="address-right tn-flex tn-flex-col-center">
|
<view class="address-right tn-flex tn-flex-col-center">
|
||||||
<text class="nav-text">{{ $tr('导航') }}</text>
|
<text class="nav-text">{{ $tr('导航') }}</text>
|
||||||
@@ -17,6 +20,7 @@
|
|||||||
<!-- 小区简介 -->
|
<!-- 小区简介 -->
|
||||||
<view class="section-panel" v-if="displayDescription">
|
<view class="section-panel" v-if="displayDescription">
|
||||||
<view class="section-header">
|
<view class="section-header">
|
||||||
|
<view class="title-bar"></view>
|
||||||
<text class="section-title">{{ $tr('小区简介') }}</text>
|
<text class="section-title">{{ $tr('小区简介') }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
@@ -53,14 +57,14 @@ function normalizeRichDescription(value = '') {
|
|||||||
html,
|
html,
|
||||||
'display:block;width:100%;max-width:100%;height:auto;box-sizing:border-box;border-radius:16rpx;margin:16rpx 0;vertical-align:top;'
|
'display:block;width:100%;max-width:100%;height:auto;box-sizing:border-box;border-radius:16rpx;margin:16rpx 0;vertical-align:top;'
|
||||||
)
|
)
|
||||||
html = html.replace(/<p\b([^>]*)>/gi, '<p$1 style="line-height:1.9;color:rgba(255,255,255,0.78);margin:0 0 20rpx;word-break:break-word;">')
|
html = html.replace(/<p\b([^>]*)>/gi, '<p$1 style="line-height:1.9;color:#475569;margin:0 0 20rpx;word-break:break-word;">')
|
||||||
html = html.replace(/<div\b([^>]*)>/gi, '<div$1 style="line-height:1.9;color:rgba(255,255,255,0.78);margin:0 0 20rpx;word-break:break-word;">')
|
html = html.replace(/<div\b([^>]*)>/gi, '<div$1 style="line-height:1.9;color:#475569;margin:0 0 20rpx;word-break:break-word;">')
|
||||||
html = html.replace(/<ul\b([^>]*)>/gi, '<ul$1 style="padding-left:28rpx;margin:0 0 20rpx;color:rgba(255,255,255,0.78);line-height:1.9;">')
|
html = html.replace(/<ul\b([^>]*)>/gi, '<ul$1 style="padding-left:28rpx;margin:0 0 20rpx;color:#475569;line-height:1.9;">')
|
||||||
html = html.replace(/<ol\b([^>]*)>/gi, '<ol$1 style="padding-left:32rpx;margin:0 0 20rpx;color:rgba(255,255,255,0.78);line-height:1.9;">')
|
html = html.replace(/<ol\b([^>]*)>/gi, '<ol$1 style="padding-left:32rpx;margin:0 0 20rpx;color:#475569;line-height:1.9;">')
|
||||||
html = html.replace(/<li\b([^>]*)>/gi, '<li$1 style="margin-bottom:12rpx;">')
|
html = html.replace(/<li\b([^>]*)>/gi, '<li$1 style="margin-bottom:12rpx;">')
|
||||||
html = html.replace(/<h([1-6])\b([^>]*)>/gi, '<h$1$2 style="color:#FFFFFF;line-height:1.5;margin:0 0 20rpx;font-weight:600;">')
|
html = html.replace(/<h([1-6])\b([^>]*)>/gi, '<h$1$2 style="color:#1E3A5F;line-height:1.5;margin:0 0 20rpx;font-weight:600;">')
|
||||||
html = html.replace(/<strong\b([^>]*)>/gi, '<strong$1 style="color:#FFFFFF;font-weight:600;">')
|
html = html.replace(/<strong\b([^>]*)>/gi, '<strong$1 style="color:#1E3A5F;font-weight:600;">')
|
||||||
html = html.replace(/<a\b([^>]*)>/gi, '<a$1 style="color:#9EC5FF;text-decoration:underline;">')
|
html = html.replace(/<a\b([^>]*)>/gi, '<a$1 style="color:#2E4A73;text-decoration:underline;">')
|
||||||
return html
|
return html
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +129,14 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
$navy-deep: #1E3A5F;
|
||||||
|
$navy-light: #2E4A73;
|
||||||
|
$gold: #C9A96E;
|
||||||
|
$gold-light: #D4BC8C;
|
||||||
|
$text-primary: #0F172A;
|
||||||
|
$text-secondary: #64748B;
|
||||||
|
$text-tertiary: #94A3B8;
|
||||||
|
|
||||||
.store-info-panel {
|
.store-info-panel {
|
||||||
padding: 0 32rpx;
|
padding: 0 32rpx;
|
||||||
margin-top: 24rpx;
|
margin-top: 24rpx;
|
||||||
@@ -133,18 +145,16 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.info-card {
|
.info-card {
|
||||||
background: rgba(255, 255, 255, 0.08);
|
background: #FFFFFF;
|
||||||
backdrop-filter: blur(20px);
|
border: 1rpx solid rgba($navy-deep, 0.08);
|
||||||
-webkit-backdrop-filter: blur(20px);
|
box-shadow: 0 8rpx 28rpx rgba(30, 58, 95, 0.06);
|
||||||
border: 1rpx solid rgba(255, 255, 255, 0.15);
|
border-radius: 28rpx;
|
||||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.05);
|
|
||||||
border-radius: 24rpx;
|
|
||||||
padding: 32rpx;
|
padding: 32rpx;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background: rgba(255, 255, 255, 0.12);
|
|
||||||
transform: scale(0.98);
|
transform: scale(0.98);
|
||||||
|
box-shadow: 0 12rpx 32rpx rgba(30, 58, 95, 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,69 +165,91 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon-wrap {
|
.icon-wrap {
|
||||||
width: 56rpx;
|
width: 72rpx;
|
||||||
height: 56rpx;
|
height: 72rpx;
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: linear-gradient(135deg, rgba($navy-deep, 0.08) 0%, rgba($navy-deep, 0.04) 100%);
|
||||||
border-radius: 16rpx;
|
border-radius: 20rpx;
|
||||||
margin-right: 16rpx;
|
margin-right: 20rpx;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
border: 1rpx solid rgba($navy-deep, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.location-icon {
|
.location-icon {
|
||||||
color: #fff;
|
color: $navy-deep;
|
||||||
font-size: 32rpx;
|
font-size: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-tertiary;
|
||||||
|
margin-bottom: 6rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.address-title {
|
.address-title {
|
||||||
font-size: 32rpx;
|
font-size: 30rpx;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
color: #fff;
|
color: $text-primary;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.address-right {
|
.address-right {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding-left: 20rpx;
|
padding-left: 24rpx;
|
||||||
border-left: 1rpx solid rgba(255, 255, 255, 0.2);
|
border-left: 1rpx solid rgba($navy-deep, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-text {
|
.nav-text {
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: #fff;
|
color: $navy-deep;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-arrow {
|
.nav-arrow {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: rgba(255, 255, 255, 0.8);
|
color: $gold;
|
||||||
margin-left: 6rpx;
|
margin-left: 6rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-panel {
|
.section-panel {
|
||||||
background: rgba(255, 255, 255, 0.08);
|
background: #FFFFFF;
|
||||||
backdrop-filter: blur(20px);
|
border: 1rpx solid rgba($navy-deep, 0.08);
|
||||||
-webkit-backdrop-filter: blur(20px);
|
box-shadow: 0 8rpx 28rpx rgba(30, 58, 95, 0.06);
|
||||||
border: 1rpx solid rgba(255, 255, 255, 0.15);
|
|
||||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.05);
|
|
||||||
margin-top: 24rpx;
|
margin-top: 24rpx;
|
||||||
padding: 32rpx;
|
padding: 32rpx;
|
||||||
border-radius: 24rpx;
|
border-radius: 28rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar {
|
||||||
|
width: 6rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
background: linear-gradient(180deg, $gold 0%, $gold-light 100%);
|
||||||
|
border-radius: 999rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
font-weight: bold;
|
font-weight: 800;
|
||||||
color: #fff;
|
color: $navy-deep;
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-desc {
|
.store-desc {
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: $text-secondary;
|
||||||
line-height: 1.6;
|
line-height: 1.7;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,14 +265,16 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.expand-btn {
|
.expand-btn {
|
||||||
margin-top: 16rpx;
|
margin-top: 20rpx;
|
||||||
font-size: 24rpx;
|
font-size: 26rpx;
|
||||||
color: #fff;
|
color: $navy-deep;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expand-icon {
|
.expand-icon {
|
||||||
margin-left: 8rpx;
|
margin-left: 8rpx;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
color: $gold;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,71 +1,66 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="store-detail-page">
|
<view class="store-detail-page">
|
||||||
<!-- 底层:全屏轮播背景 -->
|
<!-- 顶部导航栏:滚动前透明/白色元素,滚动后白底深海蓝标题 -->
|
||||||
<view class="fullscreen-bg">
|
|
||||||
<swiper
|
|
||||||
v-if="storeGalleryList.length"
|
|
||||||
class="bg-swiper"
|
|
||||||
circular
|
|
||||||
autoplay
|
|
||||||
:interval="5000"
|
|
||||||
:duration="1000"
|
|
||||||
@change="onHeroSwiperChange"
|
|
||||||
>
|
|
||||||
<swiper-item v-for="(image, index) in storeGalleryList" :key="`${image}-${index}`">
|
|
||||||
<image class="bg-image" :src="image" mode="aspectFill"></image>
|
|
||||||
</swiper-item>
|
|
||||||
</swiper>
|
|
||||||
<view v-else class="bg-empty tn-flex tn-flex-col-center tn-flex-row-center">{{ $tr('暂无图片') }}</view>
|
|
||||||
<!-- 渐变暗化遮罩,确保上层文字卡片清晰 -->
|
|
||||||
<view class="bg-mask"></view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 顶部导航栏 -->
|
|
||||||
<tn-nav-bar
|
<tn-nav-bar
|
||||||
:isBack="true"
|
:isBack="true"
|
||||||
:bottomShadow="false"
|
:bottomShadow="false"
|
||||||
backgroundColor="transparent"
|
:backgroundColor="navBarBackgroundColor"
|
||||||
:customBackStyle="navBarBackStyle"
|
:customBackStyle="navBarBackStyle"
|
||||||
backIconColor="#FFFFFF"
|
:backIconColor="navBarIconColor"
|
||||||
:fixed="true"
|
:fixed="true"
|
||||||
class="transparent-nav-bar"
|
class="glass-nav-bar"
|
||||||
>
|
>
|
||||||
<view class="nav-title" :style="{ opacity: navTitleOpacity }">{{ displayStoreName }}</view>
|
<view class="nav-title" :style="{ opacity: navTitleOpacity, color: navTitleColor }">{{ displayStoreName }}</view>
|
||||||
</tn-nav-bar>
|
</tn-nav-bar>
|
||||||
|
|
||||||
<!-- 上层:透明可滚动内容区 -->
|
<!-- 可滚动内容区 -->
|
||||||
<scroll-view
|
<scroll-view
|
||||||
class="content-scroll"
|
class="content-scroll"
|
||||||
scroll-y
|
scroll-y
|
||||||
:show-scrollbar="false"
|
:show-scrollbar="false"
|
||||||
@scroll="handleScroll"
|
@scroll="handleScroll"
|
||||||
enable-back-to-top
|
enable-back-to-top
|
||||||
>
|
>
|
||||||
<view class="scroll-inner">
|
<view class="scroll-inner">
|
||||||
<!-- 留出足够的顶部空间,让背景图展示出来 -->
|
<!-- Hero 大图区 -->
|
||||||
<view class="hero-space tn-flex tn-flex-direction-column tn-flex-col-bottom">
|
<view class="hero-panel">
|
||||||
<!-- 照片计数器移到店名上方,避免与导航栏重叠 -->
|
<swiper
|
||||||
<view v-if="storeGalleryList.length > 1" class="hero-counter tn-flex tn-flex-col-center">
|
v-if="storeGalleryList.length"
|
||||||
<text class="tn-icon-image counter-icon"></text>
|
class="hero-swiper"
|
||||||
<text>{{ currentImageIndex + 1 }} / {{ storeGalleryList.length }}</text>
|
circular
|
||||||
</view>
|
autoplay
|
||||||
|
:interval="5000"
|
||||||
<view class="store-title-wrap">
|
:duration="1000"
|
||||||
<view class="store-title">{{ displayStoreName }}</view>
|
@change="onHeroSwiperChange"
|
||||||
<view v-if="storeTags.length" class="store-tags tn-flex tn-flex-wrap">
|
>
|
||||||
<view class="tag-item" v-for="(tag, index) in storeTags" :key="index">{{ tag }}</view>
|
<swiper-item v-for="(image, index) in storeGalleryList" :key="`${image}-${index}`">
|
||||||
</view>
|
<image class="hero-image" :src="image" mode="aspectFill"></image>
|
||||||
</view>
|
</swiper-item>
|
||||||
|
</swiper>
|
||||||
|
<view v-else class="hero-empty tn-flex tn-flex-col-center tn-flex-row-center">{{ $tr('暂无图片') }}</view>
|
||||||
|
<view v-if="storeGalleryList.length > 1" class="hero-counter">
|
||||||
|
<text class="tn-icon-image counter-icon"></text>
|
||||||
|
<text>{{ currentImageIndex + 1 }} / {{ storeGalleryList.length }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 悬浮毛玻璃信息流 -->
|
<!-- 店名主卡片 -->
|
||||||
<view class="floating-content-wrap">
|
<view class="store-title-card">
|
||||||
|
<view class="store-title">{{ displayStoreName }}</view>
|
||||||
|
<view class="title-divider"></view>
|
||||||
|
<view v-if="storeTags.length" class="store-tags tn-flex tn-flex-wrap">
|
||||||
|
<view class="tag-item" v-for="(tag, index) in storeTags" :key="index">{{ tag }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 信息流 -->
|
||||||
|
<view class="content-body">
|
||||||
<store-info
|
<store-info
|
||||||
:storeData="storeData"
|
:storeData="storeData"
|
||||||
:language="language"
|
:language="language"
|
||||||
@open-map="openMap"
|
@open-map="openMap"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<facility-list
|
<facility-list
|
||||||
:facilityList="facilityList"
|
:facilityList="facilityList"
|
||||||
:language="language"
|
:language="language"
|
||||||
@@ -77,20 +72,20 @@
|
|||||||
:language="language"
|
:language="language"
|
||||||
@go-detail="goToRoomDetail"
|
@go-detail="goToRoomDetail"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<view class="bottom-spacer"></view>
|
<view class="bottom-spacer"></view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
<!-- 悬浮操作栏 (固定在底部,透明风格) -->
|
<!-- 底部操作栏 -->
|
||||||
<view class="action-bar-wrap tn-safe-area-inset-bottom">
|
<view class="action-bar-wrap tn-safe-area-inset-bottom">
|
||||||
<view class="action-bar">
|
<view class="action-bar">
|
||||||
<button class="action-btn action-btn--glass" open-type="share">
|
<button class="action-btn action-btn--secondary" open-type="share">
|
||||||
<text class="tn-icon-share action-btn__icon"></text>
|
<text class="tn-icon-share action-btn__icon"></text>
|
||||||
<text>{{ $tr('分享') }}</text>
|
<text>{{ $tr('分享') }}</text>
|
||||||
</button>
|
</button>
|
||||||
<button class="action-btn action-btn--primary-glass" @click="handleCall">
|
<button class="action-btn action-btn--primary" @click="handleCall">
|
||||||
<text class="tn-icon-phone action-btn__icon"></text>
|
<text class="tn-icon-phone action-btn__icon"></text>
|
||||||
<text>{{ contactButtonText }}</text>
|
<text>{{ contactButtonText }}</text>
|
||||||
</button>
|
</button>
|
||||||
@@ -103,8 +98,8 @@
|
|||||||
import StoreInfo from './components/StoreInfo.vue';
|
import StoreInfo from './components/StoreInfo.vue';
|
||||||
import FacilityList from './components/FacilityList.vue';
|
import FacilityList from './components/FacilityList.vue';
|
||||||
import RoomList from './components/RoomList.vue';
|
import RoomList from './components/RoomList.vue';
|
||||||
import { facilityManagers } from '@/api/index.js'
|
import { areaDetailPagination, facilityManagers } from '@/api/index.js'
|
||||||
import { localizeFacilityName, localizeHousingTag } from '../../utils/select-room-normalizers'
|
import { localizeFacilityName, localizeHousingTag, normalizeAreaItem } from '../../utils/select-room-normalizers'
|
||||||
|
|
||||||
function parseCoordinate(...values) {
|
function parseCoordinate(...values) {
|
||||||
for (let i = 0; i < values.length; i += 1) {
|
for (let i = 0; i < values.length; i += 1) {
|
||||||
@@ -145,18 +140,12 @@ export default {
|
|||||||
if (Array.isArray(this.storeData.images) && this.storeData.images.length) {
|
if (Array.isArray(this.storeData.images) && this.storeData.images.length) {
|
||||||
return this.storeData.images.filter(Boolean)
|
return this.storeData.images.filter(Boolean)
|
||||||
}
|
}
|
||||||
if (this.heroImage) {
|
|
||||||
return [this.heroImage]
|
|
||||||
}
|
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
heroImage() {
|
heroImage() {
|
||||||
if (this.storeData.images && this.storeData.images[0]) {
|
if (this.storeData.images && this.storeData.images[0]) {
|
||||||
return this.storeData.images[0]
|
return this.storeData.images[0]
|
||||||
}
|
}
|
||||||
if (this.roomTypes.length > 0 && this.roomTypes[0].images && this.roomTypes[0].images[0]) {
|
|
||||||
return this.roomTypes[0].images[0]
|
|
||||||
}
|
|
||||||
return ''
|
return ''
|
||||||
},
|
},
|
||||||
storeTags() {
|
storeTags() {
|
||||||
@@ -224,24 +213,37 @@ export default {
|
|||||||
}
|
}
|
||||||
return this.$tr('电话咨询')
|
return this.$tr('电话咨询')
|
||||||
},
|
},
|
||||||
|
navBarBackgroundColor() {
|
||||||
|
const opacity = Math.min(Math.max((this.scrollTop - 20) / 80, 0), 1);
|
||||||
|
return `rgba(255, 255, 255, ${opacity})`;
|
||||||
|
},
|
||||||
navBarBackStyle() {
|
navBarBackStyle() {
|
||||||
const isDarkBg = this.scrollTop < 100;
|
const isTop = this.scrollTop < 60;
|
||||||
return {
|
return {
|
||||||
transition: 'all 0.3s',
|
transition: 'all 0.3s',
|
||||||
background: isDarkBg ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.15)',
|
background: isTop ? 'rgba(255, 255, 255, 0.92)' : '#FFFFFF',
|
||||||
backdropFilter: 'blur(10px)',
|
backdropFilter: 'blur(12rpx)',
|
||||||
|
WebkitBackdropFilter: 'blur(12rpx)',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
width: '64rpx',
|
width: '68rpx',
|
||||||
height: '64rpx',
|
height: '68rpx',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginLeft: '12rpx',
|
marginLeft: '12rpx',
|
||||||
border: '1rpx solid rgba(255, 255, 255, 0.25)'
|
boxShadow: isTop
|
||||||
|
? '0 8rpx 24rpx rgba(30, 58, 95, 0.12), inset 0 1rpx 0 rgba(255, 255, 255, 0.6)'
|
||||||
|
: '0 8rpx 20rpx rgba(15, 23, 42, 0.08)'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
navBarIconColor() {
|
||||||
|
return '#1E3A5F';
|
||||||
|
},
|
||||||
|
navTitleColor() {
|
||||||
|
return '#1E3A5F';
|
||||||
|
},
|
||||||
navTitleOpacity() {
|
navTitleOpacity() {
|
||||||
return Math.min(Math.max((this.scrollTop - 80) / 100, 0), 1);
|
return Math.min(Math.max((this.scrollTop - 60) / 80, 0), 1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLoad(options) {
|
onLoad(options) {
|
||||||
@@ -276,7 +278,74 @@ export default {
|
|||||||
applyStoreData(data) {
|
applyStoreData(data) {
|
||||||
this.storeData = data && typeof data === 'object' ? data : {}
|
this.storeData = data && typeof data === 'object' ? data : {}
|
||||||
this.roomTypes = this.storeData.roomTypes || []
|
this.roomTypes = this.storeData.roomTypes || []
|
||||||
this.loadAreaManager()
|
this.currentImageIndex = 0
|
||||||
|
return this.loadAreaDetail()
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn('查询区域详情失败', error)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loadAreaManager()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
pickAreaDetail(rawList = []) {
|
||||||
|
if (!Array.isArray(rawList) || !rawList.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const areaCode = String(this.storeData.code || this.storeData.areaCode || '').trim()
|
||||||
|
const areaName = String(this.storeData.name || '').trim()
|
||||||
|
const areaNameEn = String(this.storeData.nameEn || '').trim()
|
||||||
|
|
||||||
|
if (areaCode) {
|
||||||
|
const exactCodeItem = rawList.find((item) => String(item && item.code || '').trim() === areaCode)
|
||||||
|
if (exactCodeItem) {
|
||||||
|
return exactCodeItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const exactNameItem = rawList.find((item) => {
|
||||||
|
const name = String(item && item.name || '').trim()
|
||||||
|
const nameEn = String(item && item.nameEn || '').trim()
|
||||||
|
return (areaName && name === areaName) || (areaNameEn && nameEn === areaNameEn)
|
||||||
|
})
|
||||||
|
if (exactNameItem) {
|
||||||
|
return exactNameItem
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawList[0] || null
|
||||||
|
},
|
||||||
|
loadAreaDetail() {
|
||||||
|
const areaCode = String(this.storeData.code || this.storeData.areaCode || '').trim()
|
||||||
|
const searchKeyword = areaCode || String(this.storeData.name || '').trim()
|
||||||
|
if (!searchKeyword) {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
return areaDetailPagination({
|
||||||
|
PageIndex: 1,
|
||||||
|
PageSize: areaCode ? 20 : 50,
|
||||||
|
Search: searchKeyword
|
||||||
|
}, this.language)
|
||||||
|
.then((res) => {
|
||||||
|
const rawList = Array.isArray(res && res.data && res.data.data)
|
||||||
|
? res.data.data
|
||||||
|
: Array.isArray(res && res.data)
|
||||||
|
? res.data
|
||||||
|
: []
|
||||||
|
const matchedArea = this.pickAreaDetail(rawList)
|
||||||
|
if (!matchedArea) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedArea = normalizeAreaItem(matchedArea)
|
||||||
|
this.storeData = {
|
||||||
|
...this.storeData,
|
||||||
|
...normalizedArea,
|
||||||
|
roomTypes: (this.storeData.roomTypes || []).length ? (this.storeData.roomTypes || []) : normalizedArea.roomTypes,
|
||||||
|
tags: (this.storeData.tags || []).length ? this.storeData.tags : normalizedArea.tags,
|
||||||
|
facilities: (this.storeData.facilities || []).length ? this.storeData.facilities : normalizedArea.facilities
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
loadAreaManager() {
|
loadAreaManager() {
|
||||||
const areaCode = this.storeData.code || this.storeData.areaCode || ''
|
const areaCode = this.storeData.code || this.storeData.areaCode || ''
|
||||||
@@ -412,9 +481,20 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
/* 品牌色变量本地化(深海蓝 + 香槟金) */
|
||||||
|
$navy-deep: #1E3A5F;
|
||||||
|
$navy-light: #2E4A73;
|
||||||
|
$gold: #C9A96E;
|
||||||
|
$gold-light: #D4BC8C;
|
||||||
|
$bg-blue: #F0F5FF;
|
||||||
|
$bg-page: #F8FAFF;
|
||||||
|
$text-primary: #0F172A;
|
||||||
|
$text-secondary: #64748B;
|
||||||
|
$text-tertiary: #94A3B8;
|
||||||
|
|
||||||
.store-detail-page {
|
.store-detail-page {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background-color: #000; /* 全屏黑色底色,防止滑动露白 */
|
background: linear-gradient(180deg, $bg-blue 0%, $bg-page 25%, #FFFFFF 100%);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -424,7 +504,6 @@ export default {
|
|||||||
.nav-title {
|
.nav-title {
|
||||||
font-size: 34rpx;
|
font-size: 34rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #fff;
|
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
max-width: 420rpx;
|
max-width: 420rpx;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -432,40 +511,14 @@ export default {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transparent-nav-bar ::v-deep .tn-nav-bar {
|
.glass-nav-bar ::v-deep .tn-nav-bar {
|
||||||
background: transparent !important;
|
backdrop-filter: blur(20rpx);
|
||||||
|
-webkit-backdrop-filter: blur(20rpx);
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 底层背景 */
|
/* 可滚动内容区 */
|
||||||
.fullscreen-bg {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-swiper, .bg-image, .bg-empty {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-empty {
|
|
||||||
background: #111;
|
|
||||||
color: rgba(255, 255, 255, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-mask {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background: linear-gradient(180deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.4) 60%, rgba(0,0,0,0.85) 100%);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 上层滚动内容区 */
|
|
||||||
.content-scroll {
|
.content-scroll {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
@@ -482,28 +535,48 @@ export default {
|
|||||||
|
|
||||||
.scroll-inner {
|
.scroll-inner {
|
||||||
min-height: 101%;
|
min-height: 101%;
|
||||||
|
padding-top: calc(100rpx + env(safe-area-inset-top));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 留出背景展示区 */
|
/* Hero 大图区 */
|
||||||
.hero-space {
|
.hero-panel {
|
||||||
|
position: relative;
|
||||||
|
height: 460rpx;
|
||||||
|
margin: 0 32rpx;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 20rpx 60rpx rgba(30, 58, 95, 0.16);
|
||||||
|
background: linear-gradient(135deg, #E2E8F0 0%, #CBD5E1 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-swiper,
|
||||||
|
.hero-image,
|
||||||
|
.hero-empty {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 35vh; /* 进一步降低留白高度,确保底部房型列表至少透出一条数据 */
|
height: 100%;
|
||||||
padding: 0 32rpx 40rpx;
|
}
|
||||||
justify-content: flex-end; /* 内容沉底 */
|
|
||||||
|
.hero-empty {
|
||||||
|
color: $text-tertiary;
|
||||||
|
font-size: 28rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-counter {
|
.hero-counter {
|
||||||
|
position: absolute;
|
||||||
|
right: 24rpx;
|
||||||
|
bottom: 24rpx;
|
||||||
height: 52rpx;
|
height: 52rpx;
|
||||||
padding: 0 20rpx;
|
padding: 0 20rpx;
|
||||||
border-radius: 999rpx;
|
border-radius: 999rpx;
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba($navy-deep, 0.85);
|
||||||
backdrop-filter: blur(14px);
|
backdrop-filter: blur(14px);
|
||||||
color: #ffffff;
|
color: $gold-light;
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
border: 1rpx solid rgba(255, 255, 255, 0.2);
|
display: inline-flex;
|
||||||
margin-bottom: 24rpx;
|
align-items: center;
|
||||||
align-self: flex-end; /* 靠右对齐 */
|
justify-content: center;
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba(30, 58, 95, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.counter-icon {
|
.counter-icon {
|
||||||
@@ -511,59 +584,72 @@ export default {
|
|||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-title-wrap {
|
/* 店名主卡片 */
|
||||||
width: 100%;
|
.store-title-card {
|
||||||
color: #fff;
|
position: relative;
|
||||||
|
margin: -60rpx 32rpx 0;
|
||||||
|
padding: 40rpx;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
box-shadow: 0 12rpx 40rpx rgba(30, 58, 95, 0.08);
|
||||||
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-title {
|
.store-title {
|
||||||
font-size: 64rpx;
|
font-size: 48rpx;
|
||||||
font-weight: bold;
|
font-weight: 800;
|
||||||
margin-bottom: 24rpx;
|
color: $navy-deep;
|
||||||
text-shadow: 0 4rpx 16rpx rgba(0,0,0,0.6);
|
line-height: 1.3;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title-divider {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 4rpx;
|
||||||
|
margin: 24rpx 0;
|
||||||
|
background: linear-gradient(90deg, $gold 0%, $gold-light 100%);
|
||||||
|
border-radius: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.store-tags {
|
.store-tags {
|
||||||
gap: 16rpx;
|
gap: 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-item {
|
.tag-item {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
padding: 8rpx 20rpx;
|
padding: 8rpx 18rpx;
|
||||||
border-radius: 8rpx;
|
border-radius: 999rpx;
|
||||||
background: rgba(255, 255, 255, 0.15);
|
background: rgba($navy-deep, 0.06);
|
||||||
backdrop-filter: blur(10px);
|
border: 1rpx solid rgba($navy-deep, 0.12);
|
||||||
border: 1rpx solid rgba(255, 255, 255, 0.2);
|
color: $navy-deep;
|
||||||
color: #fff;
|
font-weight: 500;
|
||||||
text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 悬浮毛玻璃信息流 */
|
/* 信息流 */
|
||||||
.floating-content-wrap {
|
.content-body {
|
||||||
position: relative;
|
padding: 24rpx 0 0;
|
||||||
padding-bottom: 180rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 底部操作栏 - 毛玻璃悬浮 */
|
/* 底部操作栏 */
|
||||||
.action-bar-wrap {
|
.action-bar-wrap {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
padding: 18rpx 32rpx calc(18rpx + env(safe-area-inset-bottom));
|
padding: 18rpx 32rpx calc(18rpx + env(safe-area-inset-bottom));
|
||||||
background: linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0) 100%);
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
backdrop-filter: blur(24rpx);
|
||||||
|
-webkit-backdrop-filter: blur(24rpx);
|
||||||
|
box-shadow: 0 -8rpx 30rpx rgba(30, 58, 95, 0.06);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
pointer-events: none; /* 让渐变层不阻挡点击 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-bar {
|
.action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 24rpx;
|
gap: 24rpx;
|
||||||
pointer-events: auto; /* 恢复按钮区域点击 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
@@ -573,35 +659,41 @@ export default {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 30rpx;
|
font-size: 30rpx;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
backdrop-filter: blur(16px);
|
transition: all 0.2s;
|
||||||
-webkit-backdrop-filter: blur(16px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button.action-btn::after {
|
button.action-btn::after {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-btn:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
.action-btn__icon {
|
.action-btn__icon {
|
||||||
margin-right: 12rpx;
|
margin-right: 12rpx;
|
||||||
font-size: 34rpx;
|
font-size: 34rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn--glass {
|
.action-btn--secondary {
|
||||||
flex: 0 0 200rpx;
|
flex: 0 0 200rpx;
|
||||||
background: rgba(255, 255, 255, 0.15);
|
background: rgba($navy-deep, 0.06);
|
||||||
color: #fff;
|
color: $navy-deep;
|
||||||
|
border: 1rpx solid rgba($navy-deep, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn--primary-glass {
|
.action-btn--primary {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: rgba($brand-primary, 0.85);
|
background: linear-gradient(135deg, $navy-light 0%, $navy-deep 100%);
|
||||||
color: #fff;
|
color: #FFFFFF;
|
||||||
border-color: rgba($brand-primary, 0.5);
|
box-shadow: 0 12rpx 32rpx rgba($navy-deep, 0.25);
|
||||||
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.3);
|
}
|
||||||
|
|
||||||
|
.action-btn--primary .action-btn__icon {
|
||||||
|
color: $gold-light;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-spacer {
|
.bottom-spacer {
|
||||||
|
|||||||
@@ -23,16 +23,28 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
v-if="showPaymentStatusPanel"
|
||||||
|
class="payment-status-panel tn-margin-bottom-lg"
|
||||||
|
:class="`payment-status-panel--${paymentStatusType}`"
|
||||||
|
>
|
||||||
|
<view class="payment-status-panel__label">{{ $tr('支付状态') }}</view>
|
||||||
|
<view class="payment-status-panel__main">
|
||||||
|
<view class="payment-status-panel__value">{{ paymentStatusText }}</view>
|
||||||
|
<view v-if="canViewBill" class="payment-status-panel__hint">{{ $tr('请尽快完成定金支付') }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view
|
<view
|
||||||
v-if="statusReminderText"
|
v-if="statusReminderText"
|
||||||
class="status-reminder-panel tn-margin-bottom-lg"
|
class="status-reminder-panel tn-margin-bottom-lg"
|
||||||
:class="{
|
:class="{
|
||||||
'status-reminder-panel--warning': shouldPayDeposit(detail),
|
'status-reminder-panel--warning': shouldPayDeposit(detail) && !isWaitingButlerRoomAssignment(detail),
|
||||||
'status-reminder-panel--info': isWaitingRoomAssignment(detail)
|
'status-reminder-panel--info': isWaitingRoomAssignment(detail)
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<view class="status-reminder-panel__icon">
|
<view class="status-reminder-panel__icon">
|
||||||
<text :class="shouldPayDeposit(detail) ? 'tn-icon-warning-fill' : 'tn-icon-time-fill'"></text>
|
<text :class="shouldPayDeposit(detail) && !isWaitingButlerRoomAssignment(detail) ? 'tn-icon-warning-fill' : 'tn-icon-time-fill'"></text>
|
||||||
</view>
|
</view>
|
||||||
<view class="status-reminder-panel__content">
|
<view class="status-reminder-panel__content">
|
||||||
<view class="status-reminder-panel__title">{{ $tr('当前进度提醒') }}</view>
|
<view class="status-reminder-panel__title">{{ $tr('当前进度提醒') }}</view>
|
||||||
@@ -116,7 +128,7 @@
|
|||||||
:class="{ 'danger-btn--loading': canceling }"
|
:class="{ 'danger-btn--loading': canceling }"
|
||||||
@click.stop="handleCancelOrder"
|
@click.stop="handleCancelOrder"
|
||||||
>
|
>
|
||||||
<text class="tn-text-bold">{{ canceling ? $tr('取消中...') : $tr('取消订单') }}</text>
|
<text class="tn-text-bold">{{ canceling ? $tr('取消中...') : $tr('取消申请') }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
v-if="currentStepActionText"
|
v-if="currentStepActionText"
|
||||||
@@ -145,7 +157,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { contractDetail, orderSearchedReservationFlow, roomInspectionDetail, userDetailCancel } from '@/api/index.js'
|
import { accountQueryBills, contractDetail, orderSearchedReservationFlow, roomInspectionDetail, userDetailCancel } from '@/api/index.js'
|
||||||
import { getMyApplicationList, saveActiveApplicationCode, saveMyApplicationList } from '@/utils/app-state'
|
import { getMyApplicationList, saveActiveApplicationCode, saveMyApplicationList } from '@/utils/app-state'
|
||||||
import {
|
import {
|
||||||
RESERVATION_FLOW_STEP,
|
RESERVATION_FLOW_STEP,
|
||||||
@@ -202,6 +214,43 @@ function normalizeApplicationList(list = []) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveApplicationPaymentStatusInfo(item = {}) {
|
||||||
|
if (!item || item.isViewingApplication) {
|
||||||
|
return {
|
||||||
|
key: 'exempt',
|
||||||
|
desc: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const desc = String(item.payStatusDesc || item.paymentStatusDesc || '').trim()
|
||||||
|
const status = Number(
|
||||||
|
item.payStatus !== undefined && item.payStatus !== null
|
||||||
|
? item.payStatus
|
||||||
|
: item.paymentStatus
|
||||||
|
)
|
||||||
|
|
||||||
|
if (status === 10 || /已支付|paid/i.test(desc)) {
|
||||||
|
return { key: 'paid', desc }
|
||||||
|
}
|
||||||
|
if (/退款|refunded?/i.test(desc)) {
|
||||||
|
return { key: 'refunded', desc }
|
||||||
|
}
|
||||||
|
if (/失败|failed?/i.test(desc)) {
|
||||||
|
return { key: 'failed', desc }
|
||||||
|
}
|
||||||
|
if (/处理中|processing/i.test(desc)) {
|
||||||
|
return { key: 'processing', desc }
|
||||||
|
}
|
||||||
|
if (status === 0 || /待支付|未支付|unpaid|pending/i.test(desc)) {
|
||||||
|
return { key: 'pending', desc }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: desc ? 'processing' : 'pending',
|
||||||
|
desc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function parseJsonStringMaybe(value) {
|
function parseJsonStringMaybe(value) {
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
return value
|
return value
|
||||||
@@ -233,6 +282,7 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
language() {
|
language() {
|
||||||
this.loadReservationFlow()
|
this.loadReservationFlow()
|
||||||
|
this.loadPendingDepositBillState()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -278,6 +328,42 @@ export default {
|
|||||||
heroBadgeClass() {
|
heroBadgeClass() {
|
||||||
return this.isViewingApplication ? 'badge-viewing' : 'badge-stay'
|
return this.isViewingApplication ? 'badge-viewing' : 'badge-stay'
|
||||||
},
|
},
|
||||||
|
showPaymentStatusPanel() {
|
||||||
|
return !this.isViewingApplication
|
||||||
|
},
|
||||||
|
paymentStatusText() {
|
||||||
|
if (this.isWaitingButlerRoomAssignment(this.detail)) {
|
||||||
|
return this.$tr('等待管家排房')
|
||||||
|
}
|
||||||
|
const statusInfo = resolveApplicationPaymentStatusInfo(this.detail)
|
||||||
|
if (this.$getLanguage() !== 'en' && statusInfo.desc) {
|
||||||
|
return statusInfo.desc
|
||||||
|
}
|
||||||
|
const textMap = {
|
||||||
|
exempt: this.$tr('无需支付'),
|
||||||
|
paid: this.$tr('已支付'),
|
||||||
|
refunded: this.$tr('已退款'),
|
||||||
|
failed: this.$tr('支付失败'),
|
||||||
|
processing: this.$tr('处理中'),
|
||||||
|
pending: this.$tr('待支付')
|
||||||
|
}
|
||||||
|
return textMap[statusInfo.key] || statusInfo.desc || this.$tr('待支付')
|
||||||
|
},
|
||||||
|
paymentStatusType() {
|
||||||
|
if (this.isWaitingButlerRoomAssignment(this.detail)) {
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
const statusInfo = resolveApplicationPaymentStatusInfo(this.detail)
|
||||||
|
const typeMap = {
|
||||||
|
exempt: 'neutral',
|
||||||
|
paid: 'success',
|
||||||
|
refunded: 'neutral',
|
||||||
|
failed: 'danger',
|
||||||
|
processing: 'info',
|
||||||
|
pending: 'warning'
|
||||||
|
}
|
||||||
|
return typeMap[statusInfo.key] || 'warning'
|
||||||
|
},
|
||||||
displayDateRange() {
|
displayDateRange() {
|
||||||
if (!this.detail.start && !this.detail.end) {
|
if (!this.detail.start && !this.detail.end) {
|
||||||
return '--'
|
return '--'
|
||||||
@@ -329,27 +415,39 @@ export default {
|
|||||||
if (reservationStepText) {
|
if (reservationStepText) {
|
||||||
return reservationStepText
|
return reservationStepText
|
||||||
}
|
}
|
||||||
|
if (this.isWaitingButlerRoomAssignment(this.detail)) {
|
||||||
|
return this.$tr('等待管家排房')
|
||||||
|
}
|
||||||
if (this.shouldPayDeposit(this.detail)) {
|
if (this.shouldPayDeposit(this.detail)) {
|
||||||
return this.$tr('请支付定金')
|
return this.$tr('请支付定金')
|
||||||
}
|
}
|
||||||
if (this.isWaitingRoomAssignment(this.detail)) {
|
if (this.isWaitingRoomAssignment(this.detail)) {
|
||||||
return this.$tr('等待预订中心分配房间')
|
return this.$tr('等待管家排房')
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
},
|
},
|
||||||
canViewBill() {
|
canViewBill() {
|
||||||
return this.shouldPayDeposit(this.detail)
|
return this.shouldPayDeposit(this.detail)
|
||||||
|
&& this.hasPendingDepositBill(this.detail)
|
||||||
&& !this.isViewingApplication
|
&& !this.isViewingApplication
|
||||||
&& this.detail
|
&& this.detail
|
||||||
&& this.detail.detailCode
|
&& this.detail.detailCode
|
||||||
&& this.statusType === 'pending'
|
&& this.statusType === 'pending'
|
||||||
},
|
},
|
||||||
|
canCancelApplication() {
|
||||||
|
if (!(this.detail && this.detail.detailCode && Number(this.detail.status) === 0)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (this.isViewingApplication) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (Number(this.detail.payStatus) === 10) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return this.hasPendingDepositBill(this.detail)
|
||||||
|
},
|
||||||
canCancelOrder() {
|
canCancelOrder() {
|
||||||
return this.shouldPayDeposit(this.detail)
|
return this.canCancelApplication && !this.canceling
|
||||||
&& !this.isViewingApplication
|
|
||||||
&& this.detail
|
|
||||||
&& this.detail.detailCode
|
|
||||||
&& !this.canceling
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLoad(options = {}) {
|
onLoad(options = {}) {
|
||||||
@@ -358,11 +456,39 @@ export default {
|
|||||||
},
|
},
|
||||||
onShow() {
|
onShow() {
|
||||||
this.loadReservationFlow()
|
this.loadReservationFlow()
|
||||||
|
this.loadPendingDepositBillState()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getBillNo(item = {}) {
|
||||||
|
return item.no || item.billNo || ''
|
||||||
|
},
|
||||||
|
normalizeBillList(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 []
|
||||||
|
},
|
||||||
isPaidApplication(item = {}) {
|
isPaidApplication(item = {}) {
|
||||||
return !item.isViewingApplication && Number(item.payStatus) === 10
|
return !item.isViewingApplication && Number(item.payStatus) === 10
|
||||||
},
|
},
|
||||||
|
shouldQueryPendingDepositBill(item = {}) {
|
||||||
|
return !item.isViewingApplication
|
||||||
|
&& Number(item.status) === 0
|
||||||
|
&& Number(item.payStatus) !== 10
|
||||||
|
&& !!String(item.detailCode || this.detailCode || '').trim()
|
||||||
|
},
|
||||||
|
hasPendingDepositBill(item = {}) {
|
||||||
|
return !!String(this.getBillNo(item && item.pendingDepositBill ? item.pendingDepositBill : {}) || '').trim()
|
||||||
|
},
|
||||||
|
isWaitingButlerRoomAssignment(item = {}) {
|
||||||
|
return this.shouldQueryPendingDepositBill(item)
|
||||||
|
&& !!(item && item.pendingDepositBillQueried)
|
||||||
|
&& !this.hasPendingDepositBill(item)
|
||||||
|
},
|
||||||
shouldPayDeposit(item = {}) {
|
shouldPayDeposit(item = {}) {
|
||||||
return !item.isViewingApplication
|
return !item.isViewingApplication
|
||||||
&& Number(item.status) === 0
|
&& Number(item.status) === 0
|
||||||
@@ -371,7 +497,7 @@ export default {
|
|||||||
isWaitingRoomAssignment(item = {}) {
|
isWaitingRoomAssignment(item = {}) {
|
||||||
return !item.isViewingApplication
|
return !item.isViewingApplication
|
||||||
&& Number(item.status) === 0
|
&& Number(item.status) === 0
|
||||||
&& Number(item.payStatus) === 10
|
&& (Number(item.payStatus) === 10 || this.isWaitingButlerRoomAssignment(item))
|
||||||
},
|
},
|
||||||
getReservationCurrentStepValue() {
|
getReservationCurrentStepValue() {
|
||||||
return Number(this.detail && this.detail.reservationCurrentNode ? this.detail.reservationCurrentNode.step : 0)
|
return Number(this.detail && this.detail.reservationCurrentNode ? this.detail.reservationCurrentNode.step : 0)
|
||||||
@@ -491,6 +617,50 @@ export default {
|
|||||||
isViewingApplication: false
|
isViewingApplication: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
syncDetailToCache(detail = this.detail) {
|
||||||
|
const list = normalizeApplicationList(getMyApplicationList())
|
||||||
|
if (!list.length || !detail || !detail.detailCode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saveMyApplicationList(list.map((item) => {
|
||||||
|
if (String(item.detailCode || '') !== String(detail.detailCode || '')) {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
...detail
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
async loadPendingDepositBillState() {
|
||||||
|
const currentDetail = this.detail && typeof this.detail === 'object' ? this.detail : {}
|
||||||
|
const referenceTypeNo = String(currentDetail.detailCode || this.detailCode || '').trim()
|
||||||
|
if (!this.shouldQueryPendingDepositBill(currentDetail) || !referenceTypeNo) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await accountQueryBills({
|
||||||
|
referenceTypeNo,
|
||||||
|
status: 10,
|
||||||
|
paymentStatus: 0,
|
||||||
|
ProviderType: 'WechatMiniProgram'
|
||||||
|
}, this.language)
|
||||||
|
if (referenceTypeNo !== String(this.detail && this.detail.detailCode || this.detailCode || '').trim()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const billList = this.normalizeBillList(response)
|
||||||
|
const nextDetail = {
|
||||||
|
...this.detail,
|
||||||
|
pendingDepositBill: Array.isArray(billList) && billList.length ? billList[0] : null,
|
||||||
|
pendingDepositBillQueried: true
|
||||||
|
}
|
||||||
|
this.detail = nextDetail
|
||||||
|
this.syncDetailToCache(nextDetail)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('load pending deposit bill failed', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
async loadReservationFlow() {
|
async loadReservationFlow() {
|
||||||
if (!this.detail || this.detail.isViewingApplication) {
|
if (!this.detail || this.detail.isViewingApplication) {
|
||||||
return
|
return
|
||||||
@@ -601,11 +771,16 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
getCancelOrderContent() {
|
||||||
|
return this.isViewingApplication
|
||||||
|
? this.$tr('取消后将关闭当前预约看房申请,是否继续?')
|
||||||
|
: this.$tr('取消后将关闭当前申请单,是否继续?')
|
||||||
|
},
|
||||||
confirmCancelOrder() {
|
confirmCancelOrder() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: this.$tr('取消订单'),
|
title: this.$tr('取消申请'),
|
||||||
content: this.$tr('取消后将关闭当前未支付的定金订单,是否继续?'),
|
content: this.getCancelOrderContent(),
|
||||||
confirmColor: '#ef4444',
|
confirmColor: '#ef4444',
|
||||||
success: (res) => resolve(!!(res && res.confirm)),
|
success: (res) => resolve(!!(res && res.confirm)),
|
||||||
fail: () => resolve(false)
|
fail: () => resolve(false)
|
||||||
@@ -640,9 +815,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.canceling = true
|
this.canceling = true
|
||||||
return userDetailCancel({
|
return userDetailCancel({
|
||||||
detailCode: this.detail.detailCode,
|
detailCode: this.detail.detailCode
|
||||||
code: this.detail.detailCode,
|
|
||||||
referenceTypeNo: this.detail.detailCode
|
|
||||||
}, this.language)
|
}, this.language)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.detail = {
|
this.detail = {
|
||||||
@@ -652,14 +825,14 @@ export default {
|
|||||||
}
|
}
|
||||||
this.syncCanceledDetailToCache()
|
this.syncCanceledDetailToCache()
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: this.$tr('订单已取消'),
|
title: this.$tr('申请已取消'),
|
||||||
icon: 'success'
|
icon: 'success'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: (error && (error.message || error.msg)) || this.$tr('取消订单失败'),
|
title: (error && (error.message || error.msg)) || this.$tr('取消申请失败'),
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -777,6 +950,85 @@ export default {
|
|||||||
border: 1px solid rgba(59, 130, 246, 0.14);
|
border: 1px solid rgba(59, 130, 246, 0.14);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.payment-status-panel {
|
||||||
|
padding: 24rpx 28rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24rpx;
|
||||||
|
border: 1rpx solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-panel__label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #64748B;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-panel__main {
|
||||||
|
min-width: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-panel__value {
|
||||||
|
font-size: 34rpx;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-panel__hint {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #9A3412;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-panel--warning {
|
||||||
|
background: linear-gradient(180deg, rgba(255, 247, 237, 0.96) 0%, rgba(255, 251, 245, 0.98) 100%);
|
||||||
|
border-color: rgba(249, 115, 22, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-panel--warning .payment-status-panel__value {
|
||||||
|
color: #C2410C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-panel--success {
|
||||||
|
background: linear-gradient(180deg, rgba(236, 253, 245, 0.96) 0%, rgba(240, 253, 250, 0.98) 100%);
|
||||||
|
border-color: rgba(16, 185, 129, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-panel--success .payment-status-panel__value {
|
||||||
|
color: #047857;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-panel--info {
|
||||||
|
background: linear-gradient(180deg, rgba(239, 246, 255, 0.96) 0%, rgba(248, 250, 252, 0.98) 100%);
|
||||||
|
border-color: rgba(59, 130, 246, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-panel--info .payment-status-panel__value {
|
||||||
|
color: #1D4ED8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-panel--danger {
|
||||||
|
background: linear-gradient(180deg, rgba(254, 242, 242, 0.96) 0%, rgba(255, 245, 245, 0.98) 100%);
|
||||||
|
border-color: rgba(239, 68, 68, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-panel--danger .payment-status-panel__value {
|
||||||
|
color: #B91C1C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-panel--neutral {
|
||||||
|
background: linear-gradient(180deg, rgba(241, 245, 249, 0.96) 0%, rgba(248, 250, 252, 0.98) 100%);
|
||||||
|
border-color: rgba(148, 163, 184, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-panel--neutral .payment-status-panel__value {
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
.status-reminder-panel__icon {
|
.status-reminder-panel__icon {
|
||||||
width: 68rpx;
|
width: 68rpx;
|
||||||
height: 68rpx;
|
height: 68rpx;
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ import {
|
|||||||
getAccountBillRefundStatusLabel,
|
getAccountBillRefundStatusLabel,
|
||||||
getAccountReferenceTypeLabel
|
getAccountReferenceTypeLabel
|
||||||
} from '@/utils/enum-store.js'
|
} from '@/utils/enum-store.js'
|
||||||
import { getActiveApplicationCode, getMyApplicationList, saveActiveApplicationCode, saveMyApplicationList } from '@/utils/app-state'
|
import { getActiveApplicationCode, getMyApplicationList, getSessionSnapshot, saveActiveApplicationCode, saveMyApplicationList } from '@/utils/app-state'
|
||||||
|
|
||||||
const VIEWING_APPLICATION_PATTERN = /预约看房申请|看房时间:|意向房型:|意向区域:|viewing request|viewing time|preferred room type|preferred area/i
|
const VIEWING_APPLICATION_PATTERN = /预约看房申请|看房时间:|意向房型:|意向区域:|viewing request|viewing time|preferred room type|preferred area/i
|
||||||
|
|
||||||
@@ -730,7 +730,7 @@ export default {
|
|||||||
getCancelButtonText(item = {}) {
|
getCancelButtonText(item = {}) {
|
||||||
return this.cancelingBillNo === this.getBillNo(item)
|
return this.cancelingBillNo === this.getBillNo(item)
|
||||||
? this.$tr('取消中...')
|
? this.$tr('取消中...')
|
||||||
: this.$tr('取消订单')
|
: this.$tr('取消申请')
|
||||||
},
|
},
|
||||||
resolveReferenceTypeNo(item = {}) {
|
resolveReferenceTypeNo(item = {}) {
|
||||||
return item.code || item.detailCode || ''
|
return item.code || item.detailCode || ''
|
||||||
@@ -755,6 +755,19 @@ export default {
|
|||||||
return Promise.resolve('')
|
return Promise.resolve('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const session = getSessionSnapshot()
|
||||||
|
const sessionUser = session && session.currentUser ? session.currentUser : {}
|
||||||
|
const currentFlowRefNo = String(
|
||||||
|
sessionUser.currentReservationRefNo
|
||||||
|
|| (session && session.resolvedUser && session.resolvedUser.currentReservationRefNo)
|
||||||
|
|| ''
|
||||||
|
).trim()
|
||||||
|
if (currentFlowRefNo) {
|
||||||
|
this.currentReferenceTypeNo = currentFlowRefNo
|
||||||
|
saveActiveApplicationCode(currentFlowRefNo)
|
||||||
|
return Promise.resolve(this.currentReferenceTypeNo)
|
||||||
|
}
|
||||||
|
|
||||||
const cachedActiveCode = getActiveApplicationCode()
|
const cachedActiveCode = getActiveApplicationCode()
|
||||||
if (cachedActiveCode) {
|
if (cachedActiveCode) {
|
||||||
this.currentReferenceTypeNo = cachedActiveCode
|
this.currentReferenceTypeNo = cachedActiveCode
|
||||||
@@ -1011,8 +1024,8 @@ export default {
|
|||||||
confirmCancelOrder() {
|
confirmCancelOrder() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: this.$tr('取消订单'),
|
title: this.$tr('取消申请'),
|
||||||
content: this.$tr('取消后将关闭当前未支付订单,是否继续?'),
|
content: this.$tr('取消后将关闭当前申请单,是否继续?'),
|
||||||
confirmColor: '#ef4444',
|
confirmColor: '#ef4444',
|
||||||
success: (res) => resolve(!!(res && res.confirm)),
|
success: (res) => resolve(!!(res && res.confirm)),
|
||||||
fail: () => resolve(false)
|
fail: () => resolve(false)
|
||||||
@@ -1020,18 +1033,8 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
buildCancelOrderPayload(item = {}) {
|
buildCancelOrderPayload(item = {}) {
|
||||||
const pendingOrder = this.getPendingPaymentOrder(item) || {}
|
|
||||||
const billNo = this.getBillNo(item)
|
|
||||||
const paymentNo = pendingOrder.paymentNo || pendingOrder.payNo || pendingOrder.payRequestNo || ''
|
|
||||||
return {
|
return {
|
||||||
detailCode: this.currentReferenceTypeNo,
|
detailCode: this.currentReferenceTypeNo
|
||||||
code: this.currentReferenceTypeNo,
|
|
||||||
referenceTypeNo: this.currentReferenceTypeNo,
|
|
||||||
billNo,
|
|
||||||
BillNo: billNo,
|
|
||||||
paymentNo,
|
|
||||||
payNo: paymentNo,
|
|
||||||
payRequestNo: pendingOrder.payRequestNo || ''
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createPaymentSession(item = {}) {
|
createPaymentSession(item = {}) {
|
||||||
@@ -1274,7 +1277,7 @@ export default {
|
|||||||
return userDetailCancel(this.buildCancelOrderPayload(item), this.language)
|
return userDetailCancel(this.buildCancelOrderPayload(item), this.language)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: this.$tr('订单已取消'),
|
title: this.$tr('申请已取消'),
|
||||||
icon: 'success'
|
icon: 'success'
|
||||||
})
|
})
|
||||||
return this.refreshBillList()
|
return this.refreshBillList()
|
||||||
@@ -1282,7 +1285,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: (error && (error.message || error.msg)) || this.$tr('取消订单失败'),
|
title: (error && (error.message || error.msg)) || this.$tr('取消申请失败'),
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<view class="overview-panel">
|
<view class="overview-panel">
|
||||||
<view class="overview-header">
|
<view class="overview-header">
|
||||||
<view>
|
<view>
|
||||||
<view class="overview-title">{{ $tr('合同影像') }}</view>
|
<view class="overview-title">{{ contractDisplayName || $tr('合同影像') }}</view>
|
||||||
<view class="overview-subtitle">{{ $tr('合同编号:') }}{{ contractNo || $tr('未获取') }}</view>
|
<view class="overview-subtitle">{{ $tr('合同编号:') }}{{ contractNo || $tr('未获取') }}</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="overview-badge">
|
<view class="overview-badge">
|
||||||
@@ -83,6 +83,7 @@
|
|||||||
import { addFilePrefix } from '@/utils/common.js'
|
import { addFilePrefix } from '@/utils/common.js'
|
||||||
import { contractPreview } from '@/api/index.js'
|
import { contractPreview } from '@/api/index.js'
|
||||||
import { normalizeRichTextImages } from '../../utils/rich-text'
|
import { normalizeRichTextImages } from '../../utils/rich-text'
|
||||||
|
import { fetchContractDisplayInfo } from '../../utils/contract-display'
|
||||||
|
|
||||||
function resolvePreviewPayload(response) {
|
function resolvePreviewPayload(response) {
|
||||||
const payload = response && response.data ? response.data : response
|
const payload = response && response.data ? response.data : response
|
||||||
@@ -103,6 +104,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
contractNo: '',
|
contractNo: '',
|
||||||
|
contractDisplayName: '',
|
||||||
orderNo: '',
|
orderNo: '',
|
||||||
loading: false,
|
loading: false,
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
@@ -114,9 +116,24 @@ export default {
|
|||||||
this.contractNo = decodeURIComponent(options.no || '')
|
this.contractNo = decodeURIComponent(options.no || '')
|
||||||
this.orderNo = decodeURIComponent(options.orderNo || '')
|
this.orderNo = decodeURIComponent(options.orderNo || '')
|
||||||
this.imageUrl = addFilePrefix(decodeURIComponent(options.contentImageUrl || ''))
|
this.imageUrl = addFilePrefix(decodeURIComponent(options.contentImageUrl || ''))
|
||||||
|
this.fetchContractDisplayName()
|
||||||
this.fetchContractDetail()
|
this.fetchContractDetail()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
fetchContractDisplayName() {
|
||||||
|
if (!this.contractNo) {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchContractDisplayInfo(this.contractNo, this.$getLanguage())
|
||||||
|
.then(({ contractDisplayName }) => {
|
||||||
|
this.contractDisplayName = contractDisplayName || ''
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('fetch contract display name failed', error)
|
||||||
|
this.contractDisplayName = ''
|
||||||
|
})
|
||||||
|
},
|
||||||
fetchContractDetail() {
|
fetchContractDetail() {
|
||||||
if (!this.contractNo || (this.imageUrl && this.contractContent)) {
|
if (!this.contractNo || (this.imageUrl && this.contractContent)) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<view class="overview-panel">
|
<view class="overview-panel">
|
||||||
<view class="overview-header">
|
<view class="overview-header">
|
||||||
<view>
|
<view>
|
||||||
<view class="overview-title">{{ $tr('签约前确认') }}</view>
|
<view class="overview-title">{{ contractDisplayName || $tr('签约前确认') }}</view>
|
||||||
<view class="overview-subtitle">{{ $tr('合同编号:') }}{{ contractNo || $tr('未获取') }}</view>
|
<view class="overview-subtitle">{{ $tr('合同编号:') }}{{ contractNo || $tr('未获取') }}</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="overview-badge">
|
<view class="overview-badge">
|
||||||
@@ -106,6 +106,7 @@ import { contractPreview, contractSign, uploadCommonFile } from '@/api/index.js'
|
|||||||
import { syncLoginStayState } from '../../utils/session-sync'
|
import { syncLoginStayState } from '../../utils/session-sync'
|
||||||
import ContractSignDialog from '../../components/ContractSignDialog.vue'
|
import ContractSignDialog from '../../components/ContractSignDialog.vue'
|
||||||
import { normalizeRichTextImages } from '../../utils/rich-text'
|
import { normalizeRichTextImages } from '../../utils/rich-text'
|
||||||
|
import { fetchContractDisplayInfo } from '../../utils/contract-display'
|
||||||
|
|
||||||
function normalizeContractContent(content = '') {
|
function normalizeContractContent(content = '') {
|
||||||
return normalizeRichTextImages(
|
return normalizeRichTextImages(
|
||||||
@@ -144,6 +145,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
contractNo: '',
|
contractNo: '',
|
||||||
|
contractDisplayName: '',
|
||||||
orderNo: '',
|
orderNo: '',
|
||||||
contractContent: '',
|
contractContent: '',
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -158,9 +160,24 @@ export default {
|
|||||||
this.contractNo = decodeURIComponent(options.no || '')
|
this.contractNo = decodeURIComponent(options.no || '')
|
||||||
this.orderNo = decodeURIComponent(options.orderNo || '')
|
this.orderNo = decodeURIComponent(options.orderNo || '')
|
||||||
this.signerName = this.getSignerName()
|
this.signerName = this.getSignerName()
|
||||||
|
this.fetchContractDisplayName()
|
||||||
this.fetchContractContent()
|
this.fetchContractContent()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
fetchContractDisplayName() {
|
||||||
|
if (!this.contractNo) {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchContractDisplayInfo(this.contractNo, this.$getLanguage())
|
||||||
|
.then(({ contractDisplayName }) => {
|
||||||
|
this.contractDisplayName = contractDisplayName || ''
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('fetch contract display name failed', error)
|
||||||
|
this.contractDisplayName = ''
|
||||||
|
})
|
||||||
|
},
|
||||||
fetchContractContent() {
|
fetchContractContent() {
|
||||||
if (!this.contractNo) {
|
if (!this.contractNo) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
|
|||||||
1156
pkgService/lock_control/lock_control.vue
Normal file
1156
pkgService/lock_control/lock_control.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -93,7 +93,8 @@
|
|||||||
:formData="uploadFormData"
|
:formData="uploadFormData"
|
||||||
:max-count="6"
|
:max-count="6"
|
||||||
:max-size="5 * 1024 * 1024"
|
:max-size="5 * 1024 * 1024"
|
||||||
uploadText=""
|
:uploadText="''"
|
||||||
|
:customBtn="true"
|
||||||
@on-success="onUploadSuccess"
|
@on-success="onUploadSuccess"
|
||||||
@on-list-change="onPhotoListChange"
|
@on-list-change="onPhotoListChange"
|
||||||
@on-remove="onRemoveImage"
|
@on-remove="onRemoveImage"
|
||||||
|
|||||||
@@ -52,6 +52,15 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
v-if="shouldShowApplicationPaymentStatus(item)"
|
||||||
|
class="payment-status-banner tn-margin-top"
|
||||||
|
:class="`payment-status-banner--${getApplicationPaymentStatusType(item)}`"
|
||||||
|
>
|
||||||
|
<text class="payment-status-banner__label">{{ $tr('支付状态') }}</text>
|
||||||
|
<text class="payment-status-banner__value">{{ getApplicationPaymentStatusText(item) }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="card-body tn-margin-top">
|
<view class="card-body tn-margin-top">
|
||||||
<view class="info-row tn-flex tn-flex-row-between">
|
<view class="info-row tn-flex tn-flex-row-between">
|
||||||
<text class="info-label">{{ $tr('所属公寓') }}</text>
|
<text class="info-label">{{ $tr('所属公寓') }}</text>
|
||||||
@@ -154,6 +163,43 @@ function isViewingApplicationRecord(item = {}) {
|
|||||||
return VIEWING_APPLICATION_PATTERN.test(remark)
|
return VIEWING_APPLICATION_PATTERN.test(remark)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveApplicationPaymentStatusInfo(item = {}) {
|
||||||
|
if (!item || item.isViewingApplication) {
|
||||||
|
return {
|
||||||
|
key: 'exempt',
|
||||||
|
desc: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const desc = String(item.payStatusDesc || item.paymentStatusDesc || '').trim()
|
||||||
|
const status = Number(
|
||||||
|
item.payStatus !== undefined && item.payStatus !== null
|
||||||
|
? item.payStatus
|
||||||
|
: item.paymentStatus
|
||||||
|
)
|
||||||
|
|
||||||
|
if (status === 10 || /已支付|paid/i.test(desc)) {
|
||||||
|
return { key: 'paid', desc }
|
||||||
|
}
|
||||||
|
if (/退款|refunded?/i.test(desc)) {
|
||||||
|
return { key: 'refunded', desc }
|
||||||
|
}
|
||||||
|
if (/失败|failed?/i.test(desc)) {
|
||||||
|
return { key: 'failed', desc }
|
||||||
|
}
|
||||||
|
if (/处理中|processing/i.test(desc)) {
|
||||||
|
return { key: 'processing', desc }
|
||||||
|
}
|
||||||
|
if (status === 0 || /待支付|未支付|unpaid|pending/i.test(desc)) {
|
||||||
|
return { key: 'pending', desc }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: desc ? 'processing' : 'pending',
|
||||||
|
desc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeApplicationList(list = []) {
|
function normalizeApplicationList(list = []) {
|
||||||
if (!Array.isArray(list)) {
|
if (!Array.isArray(list)) {
|
||||||
return []
|
return []
|
||||||
@@ -362,6 +408,36 @@ export default {
|
|||||||
}
|
}
|
||||||
return item.intendedRoomTypeName || this.$tr('入住申请')
|
return item.intendedRoomTypeName || this.$tr('入住申请')
|
||||||
},
|
},
|
||||||
|
shouldShowApplicationPaymentStatus(item = {}) {
|
||||||
|
return !item.isViewingApplication
|
||||||
|
},
|
||||||
|
getApplicationPaymentStatusText(item = {}) {
|
||||||
|
const statusInfo = resolveApplicationPaymentStatusInfo(item)
|
||||||
|
if (this.$getLanguage() !== 'en' && statusInfo.desc) {
|
||||||
|
return statusInfo.desc
|
||||||
|
}
|
||||||
|
const textMap = {
|
||||||
|
exempt: this.$tr('无需支付'),
|
||||||
|
paid: this.$tr('已支付'),
|
||||||
|
refunded: this.$tr('已退款'),
|
||||||
|
failed: this.$tr('支付失败'),
|
||||||
|
processing: this.$tr('处理中'),
|
||||||
|
pending: this.$tr('待支付')
|
||||||
|
}
|
||||||
|
return textMap[statusInfo.key] || statusInfo.desc || this.$tr('待支付')
|
||||||
|
},
|
||||||
|
getApplicationPaymentStatusType(item = {}) {
|
||||||
|
const statusInfo = resolveApplicationPaymentStatusInfo(item)
|
||||||
|
const typeMap = {
|
||||||
|
exempt: 'neutral',
|
||||||
|
paid: 'success',
|
||||||
|
refunded: 'neutral',
|
||||||
|
failed: 'danger',
|
||||||
|
processing: 'info',
|
||||||
|
pending: 'warning'
|
||||||
|
}
|
||||||
|
return typeMap[statusInfo.key] || 'warning'
|
||||||
|
},
|
||||||
resolveReferenceTypeNo(item = {}) {
|
resolveReferenceTypeNo(item = {}) {
|
||||||
return item.detailCode || ''
|
return item.detailCode || ''
|
||||||
},
|
},
|
||||||
@@ -555,6 +631,72 @@ export default {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.payment-status-banner {
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
border: 1rpx solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-banner__label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #64748B;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-banner__value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-banner--warning {
|
||||||
|
background: linear-gradient(180deg, rgba(255, 247, 237, 0.96) 0%, rgba(255, 251, 245, 0.98) 100%);
|
||||||
|
border-color: rgba(249, 115, 22, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-banner--warning .payment-status-banner__value {
|
||||||
|
color: #C2410C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-banner--success {
|
||||||
|
background: linear-gradient(180deg, rgba(236, 253, 245, 0.96) 0%, rgba(240, 253, 250, 0.98) 100%);
|
||||||
|
border-color: rgba(16, 185, 129, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-banner--success .payment-status-banner__value {
|
||||||
|
color: #047857;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-banner--info {
|
||||||
|
background: linear-gradient(180deg, rgba(239, 246, 255, 0.96) 0%, rgba(248, 250, 252, 0.98) 100%);
|
||||||
|
border-color: rgba(59, 130, 246, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-banner--info .payment-status-banner__value {
|
||||||
|
color: #1D4ED8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-banner--danger {
|
||||||
|
background: linear-gradient(180deg, rgba(254, 242, 242, 0.96) 0%, rgba(255, 245, 245, 0.98) 100%);
|
||||||
|
border-color: rgba(239, 68, 68, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-banner--danger .payment-status-banner__value {
|
||||||
|
color: #B91C1C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-banner--neutral {
|
||||||
|
background: linear-gradient(180deg, rgba(241, 245, 249, 0.96) 0%, rgba(248, 250, 252, 0.98) 100%);
|
||||||
|
border-color: rgba(148, 163, 184, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-status-banner--neutral .payment-status-banner__value {
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
.status-pending {
|
.status-pending {
|
||||||
background: rgba(245, 158, 11, 0.12);
|
background: rgba(245, 158, 11, 0.12);
|
||||||
color: #B45309;
|
color: #B45309;
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
<view v-else-if="displayList.length" class="list-wrapper">
|
<view v-else-if="displayList.length" class="list-wrapper">
|
||||||
<view
|
<view
|
||||||
v-for="(item, index) in displayList"
|
v-for="(item, index) in displayList"
|
||||||
:key="item.no || item.contractNo || item.orderNo || index"
|
:key="getContractKey(item, index)"
|
||||||
class="contract-card"
|
class="contract-card"
|
||||||
:style="{ animationDelay: `${index * 0.08}s` }"
|
:style="{ animationDelay: `${index * 0.08}s` }"
|
||||||
>
|
>
|
||||||
@@ -58,8 +58,8 @@
|
|||||||
<text class="tn-icon-edit-write-fill"></text>
|
<text class="tn-icon-edit-write-fill"></text>
|
||||||
</view>
|
</view>
|
||||||
<view class="card-title-group">
|
<view class="card-title-group">
|
||||||
<view class="card-title">{{ $tr('住宿合同') }}</view>
|
<view class="card-title">{{ item.contractDisplayName || $tr('住宿合同') }}</view>
|
||||||
<view class="card-code">{{ item.no || item.contractNo || '--' }}</view>
|
<view class="card-code">{{ $tr('合同编号:') }}{{ item.no || item.contractNo || '--' }}</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view :class="['status-badge', `status-${resolveStatusType(item.approvalStatus)}`]">
|
<view :class="['status-badge', `status-${resolveStatusType(item.approvalStatus)}`]">
|
||||||
@@ -88,10 +88,10 @@
|
|||||||
<text class="info-label">{{ $tr('记录说明') }}</text>
|
<text class="info-label">{{ $tr('记录说明') }}</text>
|
||||||
<text class="info-value">{{ getStatusTip(item) }}</text>
|
<text class="info-value">{{ getStatusTip(item) }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="item.signImageUrl" class="sign-row">
|
<view v-if="item.contentImageUrl" class="sign-row">
|
||||||
<text class="info-label">{{ $tr('我的签名') }}</text>
|
<text class="info-label">{{ $tr('合同图片') }}</text>
|
||||||
<view class="sign-preview">
|
<view class="sign-preview">
|
||||||
<image :src="item.signImageUrl" mode="aspectFit"></image>
|
<image :src="item.contentImageUrl" mode="aspectFit" @click="previewContractImage(index)"></image>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
<view
|
<view
|
||||||
class="action-btn"
|
class="action-btn"
|
||||||
:class="{ 'action-btn--secondary': !shouldShowSignAction(item) }"
|
:class="{ 'action-btn--secondary': !shouldShowSignAction(item) }"
|
||||||
@click="handleContractAction(item)"
|
@click="handleContractAction(index)"
|
||||||
>
|
>
|
||||||
{{ resolveActionText(item) }}
|
{{ resolveActionText(item) }}
|
||||||
</view>
|
</view>
|
||||||
@@ -121,8 +121,42 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { addFilePrefix } from '@/utils/common.js'
|
import { addFilePrefix } from '@/utils/common.js'
|
||||||
|
import { getMyEffectiveContracts, getMyPendingSignContracts } from '@/api/index.js'
|
||||||
import { getSessionSnapshot } from '../../utils/app-state'
|
import { getSessionSnapshot } from '../../utils/app-state'
|
||||||
import { syncLoginStayState } from '../../utils/session-sync'
|
import { syncLoginStayState } from '../../utils/session-sync'
|
||||||
|
import { enrichContractDisplayNames } from '../../utils/contract-display'
|
||||||
|
|
||||||
|
function pickFirstValue(...values) {
|
||||||
|
for (let index = 0; index < values.length; index += 1) {
|
||||||
|
const value = values[index]
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const text = String(value).trim()
|
||||||
|
if (text) {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseContractList(response) {
|
||||||
|
const payload = response && response.data ? 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 normalizeContract(item = {}) {
|
function normalizeContract(item = {}) {
|
||||||
return {
|
return {
|
||||||
@@ -190,6 +224,67 @@ export default {
|
|||||||
const list = this.$store && this.$store.state && this.$store.state.vuex_effective_contracts
|
const list = this.$store && this.$store.state && this.$store.state.vuex_effective_contracts
|
||||||
return Array.isArray(list) ? list : []
|
return Array.isArray(list) ? list : []
|
||||||
},
|
},
|
||||||
|
resolveContractItem(source) {
|
||||||
|
if (typeof source === 'number') {
|
||||||
|
return this.displayList[source] || {}
|
||||||
|
}
|
||||||
|
if (source && typeof source === 'object') {
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
resolveCurrentOrderNo(currentOrder = null) {
|
||||||
|
const storeCurrentOrder = this.$store && this.$store.state && this.$store.state.vuex_current_order
|
||||||
|
const session = getSessionSnapshot()
|
||||||
|
const sessionCurrentOrder = session && session.resolvedUser && session.resolvedUser.currentOrder
|
||||||
|
return pickFirstValue(
|
||||||
|
currentOrder && (currentOrder.orderNo || currentOrder.no || currentOrder.code || currentOrder.OrderNo),
|
||||||
|
storeCurrentOrder && (storeCurrentOrder.orderNo || storeCurrentOrder.no || storeCurrentOrder.code || storeCurrentOrder.OrderNo),
|
||||||
|
sessionCurrentOrder && (sessionCurrentOrder.orderNo || sessionCurrentOrder.no || sessionCurrentOrder.code || sessionCurrentOrder.OrderNo)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
fetchLatestContractsByOrderNo(orderNo = '') {
|
||||||
|
const normalizedOrderNo = String(orderNo || '').trim()
|
||||||
|
if (!normalizedOrderNo) {
|
||||||
|
return Promise.resolve(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
OrderNo: normalizedOrderNo,
|
||||||
|
ProviderType: 'WechatMiniProgram'
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
getMyPendingSignContracts(query, this.language).catch(() => []),
|
||||||
|
getMyEffectiveContracts(query, this.language).catch(() => [])
|
||||||
|
]).then(([pendingContractsResponse, effectiveContractsResponse]) => {
|
||||||
|
return {
|
||||||
|
pendingContracts: parseContractList(pendingContractsResponse),
|
||||||
|
effectiveContracts: parseContractList(effectiveContractsResponse)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateContractImageUrl(targetNo = '', imageUrl = '') {
|
||||||
|
const normalizedNo = String(targetNo || '').trim()
|
||||||
|
const normalizedImageUrl = addFilePrefix(String(imageUrl || '').trim())
|
||||||
|
if (!normalizedNo || !normalizedImageUrl) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const patchList = (list = []) => list.map((contract) => {
|
||||||
|
const contractNo = String((contract && (contract.no || contract.contractNo)) || '').trim()
|
||||||
|
if (contractNo !== normalizedNo) {
|
||||||
|
return contract
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...contract,
|
||||||
|
contentImageUrl: normalizedImageUrl
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.pendingContracts = patchList(this.pendingContracts)
|
||||||
|
this.effectiveContracts = patchList(this.effectiveContracts)
|
||||||
|
},
|
||||||
isCurrentLogin() {
|
isCurrentLogin() {
|
||||||
const storeUser = this.getStoreUser()
|
const storeUser = this.getStoreUser()
|
||||||
if (storeUser && Object.keys(storeUser).length) {
|
if (storeUser && Object.keys(storeUser).length) {
|
||||||
@@ -201,12 +296,23 @@ export default {
|
|||||||
pendingContracts = [],
|
pendingContracts = [],
|
||||||
effectiveContracts = []
|
effectiveContracts = []
|
||||||
} = {}) {
|
} = {}) {
|
||||||
this.pendingContracts = Array.isArray(pendingContracts)
|
const normalizedPendingContracts = Array.isArray(pendingContracts)
|
||||||
? pendingContracts.map(normalizeContract)
|
? pendingContracts.map(normalizeContract)
|
||||||
: []
|
: []
|
||||||
this.effectiveContracts = Array.isArray(effectiveContracts)
|
const normalizedEffectiveContracts = Array.isArray(effectiveContracts)
|
||||||
? effectiveContracts.map(normalizeContract)
|
? effectiveContracts.map(normalizeContract)
|
||||||
: []
|
: []
|
||||||
|
|
||||||
|
this.pendingContracts = normalizedPendingContracts
|
||||||
|
this.effectiveContracts = normalizedEffectiveContracts
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
enrichContractDisplayNames(normalizedPendingContracts, this.language),
|
||||||
|
enrichContractDisplayNames(normalizedEffectiveContracts, this.language)
|
||||||
|
]).then(([nextPendingContracts, nextEffectiveContracts]) => {
|
||||||
|
this.pendingContracts = nextPendingContracts
|
||||||
|
this.effectiveContracts = nextEffectiveContracts
|
||||||
|
})
|
||||||
},
|
},
|
||||||
refreshContracts() {
|
refreshContracts() {
|
||||||
if (!this.isCurrentLogin()) {
|
if (!this.isCurrentLogin()) {
|
||||||
@@ -218,20 +324,32 @@ export default {
|
|||||||
this.loading = true
|
this.loading = true
|
||||||
return syncLoginStayState({ force: true })
|
return syncLoginStayState({ force: true })
|
||||||
.then((result = {}) => {
|
.then((result = {}) => {
|
||||||
this.applyContractState({
|
const fallbackState = {
|
||||||
pendingContracts: result.pendingContracts,
|
pendingContracts: result.pendingContracts,
|
||||||
effectiveContracts: result.effectiveContracts
|
effectiveContracts: result.effectiveContracts
|
||||||
})
|
}
|
||||||
|
const currentOrderNo = this.resolveCurrentOrderNo(result.currentOrder)
|
||||||
|
|
||||||
|
return this.fetchLatestContractsByOrderNo(currentOrderNo)
|
||||||
|
.then((latestState) => {
|
||||||
|
this.applyContractState(latestState || fallbackState)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('refresh contracts failed', error)
|
console.error('refresh contracts failed', error)
|
||||||
const storePendingContracts = this.getStorePendingContracts()
|
const storePendingContracts = this.getStorePendingContracts()
|
||||||
const storeEffectiveContracts = this.getStoreEffectiveContracts()
|
const storeEffectiveContracts = this.getStoreEffectiveContracts()
|
||||||
const session = getSessionSnapshot()
|
const session = getSessionSnapshot()
|
||||||
this.applyContractState({
|
const fallbackState = {
|
||||||
pendingContracts: storePendingContracts.length ? storePendingContracts : session.pendingContracts,
|
pendingContracts: storePendingContracts.length ? storePendingContracts : session.pendingContracts,
|
||||||
effectiveContracts: storeEffectiveContracts.length ? storeEffectiveContracts : session.effectiveContracts
|
effectiveContracts: storeEffectiveContracts.length ? storeEffectiveContracts : session.effectiveContracts
|
||||||
})
|
}
|
||||||
|
const currentOrderNo = this.resolveCurrentOrderNo()
|
||||||
|
|
||||||
|
return this.fetchLatestContractsByOrderNo(currentOrderNo)
|
||||||
|
.then((latestState) => {
|
||||||
|
this.applyContractState(latestState || fallbackState)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
@@ -265,6 +383,9 @@ export default {
|
|||||||
formatDate(value) {
|
formatDate(value) {
|
||||||
return value ? String(value).slice(0, 10) : '--'
|
return value ? String(value).slice(0, 10) : '--'
|
||||||
},
|
},
|
||||||
|
getContractKey(item = {}, index = 0) {
|
||||||
|
return item.no || item.contractNo || item.orderNo || `contract-${index}`
|
||||||
|
},
|
||||||
formatDateRange(start, end) {
|
formatDateRange(start, end) {
|
||||||
if (!start && !end) {
|
if (!start && !end) {
|
||||||
return '--'
|
return '--'
|
||||||
@@ -290,15 +411,84 @@ export default {
|
|||||||
if (this.shouldShowSignAction(item)) {
|
if (this.shouldShowSignAction(item)) {
|
||||||
return this.$tr('去签约')
|
return this.$tr('去签约')
|
||||||
}
|
}
|
||||||
return this.$tr('查看详情')
|
return this.$tr('查看合同')
|
||||||
},
|
},
|
||||||
shouldShowSignAction(item = {}) {
|
shouldShowSignAction(item = {}) {
|
||||||
return Number(item.approvalStatus) === 0 && !item.signImageUrl
|
return Number(item.approvalStatus) === 0 && !item.signImageUrl
|
||||||
},
|
},
|
||||||
handleContractAction(item = {}) {
|
previewContractImage(source = {}) {
|
||||||
|
const item = this.resolveContractItem(source)
|
||||||
|
const directImageUrl = addFilePrefix(String(item.contentImageUrl || '').trim())
|
||||||
|
if (directImageUrl) {
|
||||||
|
uni.previewImage({
|
||||||
|
urls: [directImageUrl],
|
||||||
|
current: directImageUrl
|
||||||
|
})
|
||||||
|
return Promise.resolve(directImageUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderNo = String(item.orderNo || '').trim()
|
||||||
|
const contractNo = String(item.no || item.contractNo || '').trim()
|
||||||
|
if (!orderNo) {
|
||||||
|
uni.showToast({
|
||||||
|
title: this.$tr('暂无合同图片'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return Promise.resolve('')
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showLoading({
|
||||||
|
title: this.$tr('加载中')
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.fetchLatestContractsByOrderNo(orderNo)
|
||||||
|
.then((latestState) => {
|
||||||
|
const effectiveContracts = latestState && Array.isArray(latestState.effectiveContracts)
|
||||||
|
? latestState.effectiveContracts
|
||||||
|
: []
|
||||||
|
const matchedContract = effectiveContracts.find((contract) => {
|
||||||
|
const currentNo = String((contract && (contract.no || contract.contractNo)) || '').trim()
|
||||||
|
const currentOrderNo = String((contract && contract.orderNo) || '').trim()
|
||||||
|
if (contractNo && currentNo) {
|
||||||
|
return currentNo === contractNo
|
||||||
|
}
|
||||||
|
return currentOrderNo === orderNo
|
||||||
|
}) || effectiveContracts[0] || null
|
||||||
|
const imageUrl = addFilePrefix(String(matchedContract && matchedContract.contentImageUrl || '').trim())
|
||||||
|
|
||||||
|
if (!imageUrl) {
|
||||||
|
uni.showToast({
|
||||||
|
title: this.$tr('暂无合同图片'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contractNo) {
|
||||||
|
this.updateContractImageUrl(contractNo, imageUrl)
|
||||||
|
}
|
||||||
|
uni.previewImage({
|
||||||
|
urls: [imageUrl],
|
||||||
|
current: imageUrl
|
||||||
|
})
|
||||||
|
return imageUrl
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('preview contract image failed', error)
|
||||||
|
uni.showToast({
|
||||||
|
title: this.$tr('合同图片加载失败'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
uni.hideLoading()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleContractAction(source = {}) {
|
||||||
|
const item = this.resolveContractItem(source)
|
||||||
const contractNo = encodeURIComponent(item.no || item.contractNo || '')
|
const contractNo = encodeURIComponent(item.no || item.contractNo || '')
|
||||||
const orderNo = encodeURIComponent(item.orderNo || '')
|
const orderNo = encodeURIComponent(item.orderNo || '')
|
||||||
const contentImageUrl = encodeURIComponent(item.contentImageUrl || '')
|
|
||||||
|
|
||||||
if (Number(item.approvalStatus) === 0 && !item.signImageUrl) {
|
if (Number(item.approvalStatus) === 0 && !item.signImageUrl) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
@@ -307,9 +497,7 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
uni.navigateTo({
|
this.previewContractImage(item)
|
||||||
url: `/pkgService/contract_detail/contract_detail?no=${contractNo}&orderNo=${orderNo}&contentImageUrl=${contentImageUrl}`
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -13,13 +13,13 @@
|
|||||||
<text class="tn-icon-lock tn-color-gray" style="font-size: 80rpx;"></text>
|
<text class="tn-icon-lock tn-color-gray" style="font-size: 80rpx;"></text>
|
||||||
</view>
|
</view>
|
||||||
<view class="hero-title tn-text-bold">{{ $tr('暂无门锁权限') }}</view>
|
<view class="hero-title tn-text-bold">{{ $tr('暂无门锁权限') }}</view>
|
||||||
<view class="hero-desc tn-margin-top-sm tn-text-center">{{ $tr('您当前暂未分配智能门锁,或门锁暂未联网。\n入住后即可使用一键开门与临时密码下发功能。') }}</view>
|
<view class="hero-desc tn-margin-top-sm tn-text-center">{{ $tr('您当前暂未分配智能门锁,或门锁暂未联网。\n入住后即可使用门锁密码、指纹录入与临时密码下发功能。') }}</view>
|
||||||
<view class="tn-flex tn-margin-top-xl tn-width-full tn-flex-row-between">
|
<view class="tn-flex tn-margin-top-xl tn-width-full tn-flex-row-between">
|
||||||
<view class="btn-light tn-flex-1 tn-margin-right-sm tn-flex tn-flex-col-center tn-flex-row-center tn-padding-sm" style="border-radius: 12rpx; opacity: 0.6;">
|
<view class="btn-light tn-flex-1 tn-margin-right-sm tn-flex tn-flex-col-center tn-flex-row-center tn-padding-sm" style="border-radius: 12rpx; opacity: 0.6;">
|
||||||
<text class="tn-icon-key tn-margin-right-xs"></text> {{ $tr('临时密码') }}
|
<text class="tn-icon-key tn-margin-right-xs"></text> {{ $tr('临时密码') }}
|
||||||
</view>
|
</view>
|
||||||
<view class="btn-primary tn-flex-1 tn-margin-left-sm tn-flex tn-flex-col-center tn-flex-row-center tn-padding-sm" style="border-radius: 12rpx; opacity: 0.6;">
|
<view class="btn-primary tn-flex-1 tn-margin-left-sm tn-flex tn-flex-col-center tn-flex-row-center tn-padding-sm" style="border-radius: 12rpx; opacity: 0.6;">
|
||||||
<text class="tn-icon-unlock tn-margin-right-xs"></text> {{ $tr('一键开锁') }}
|
<text class="tn-icon-fingerprint tn-margin-right-xs"></text> {{ $tr('录入指纹') }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -1,32 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="building-selector">
|
<view class="building-selector">
|
||||||
<view
|
<view
|
||||||
class="selector-header tn-flex tn-flex-row-between tn-flex-col-center"
|
class="selector-card tn-flex tn-flex-row-between tn-flex-col-center"
|
||||||
:class="{ 'selector-header--disabled': !areaCode }"
|
:class="{ 'selector-card--disabled': !areaCode }"
|
||||||
@click="handleOpen"
|
@click="handleOpen"
|
||||||
>
|
>
|
||||||
<view class="selected-info">
|
<view class="selected-info">
|
||||||
|
<view class="selected-label">{{ $tr('楼栋') }}</view>
|
||||||
<view class="selected-name tn-text-ellipsis" :class="{ 'is-empty': !selectedBuilding }">
|
<view class="selected-name tn-text-ellipsis" :class="{ 'is-empty': !selectedBuilding }">
|
||||||
{{ selectedBuilding ? getBuildingDisplayName(selectedBuilding) : placeholderText }}
|
{{ selectedBuilding ? getBuildingDisplayName(selectedBuilding) : placeholderText }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="tn-icon-right" :class="areaCode ? 'tn-color-gray' : 'tn-color-disabled'"></text>
|
<view class="selector-arrow">
|
||||||
|
<text class="tn-icon-right"></text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<tn-popup v-model="showPopup" mode="bottom" length="72%" :borderRadius="32" :closeBtn="true">
|
<tn-popup v-model="showPopup" mode="bottom" length="72%" :borderRadius="32" :closeBtn="true">
|
||||||
<view class="popup-content">
|
<view class="popup-content">
|
||||||
<view class="popup-header">
|
<view class="popup-header">
|
||||||
<text class="popup-title">{{ $tr('选择意向楼栋') }}</text>
|
<view class="popup-title">{{ $tr('选择意向楼栋') }}</view>
|
||||||
|
<view class="popup-subtitle">{{ $tr('选择您期望入住的楼栋') }}</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<scroll-view scroll-y class="building-list-scroll">
|
<scroll-view scroll-y class="building-list-scroll">
|
||||||
<view v-if="isLoading" class="loading-state tn-padding-xl tn-text-center">
|
<view v-if="isLoading" class="loading-state tn-padding-xl tn-text-center">
|
||||||
<tn-loading mode="circle"></tn-loading>
|
<tn-loading mode="circle" :size="48"></tn-loading>
|
||||||
<view class="tn-margin-top-sm tn-color-gray">{{ $tr('楼栋加载中...') }}</view>
|
<view class="tn-margin-top tn-color-gray">{{ $tr('楼栋加载中...') }}</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else-if="!buildingList.length" class="empty-state tn-padding-xl tn-text-center">
|
<view v-else-if="!buildingList.length" class="empty-state tn-padding-xl tn-text-center">
|
||||||
<text class="tn-color-gray">{{ $tr('暂无可选楼栋') }}</text>
|
<view class="empty-icon">
|
||||||
|
<text class="tn-icon-empty-data"></text>
|
||||||
|
</view>
|
||||||
|
<view class="empty-title">{{ $tr('暂无可选楼栋') }}</view>
|
||||||
|
<view class="empty-desc">{{ $tr('当前暂无可用楼栋,请稍后再试') }}</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else class="building-list">
|
<view v-else class="building-list">
|
||||||
@@ -37,8 +45,13 @@
|
|||||||
:class="{ 'is-active': selectedBuilding && selectedBuilding.code === item.code }"
|
:class="{ 'is-active': selectedBuilding && selectedBuilding.code === item.code }"
|
||||||
@click="handleSelect(item)"
|
@click="handleSelect(item)"
|
||||||
>
|
>
|
||||||
<text class="building-name">{{ getBuildingDisplayName(item) }}</text>
|
<view class="building-name-left tn-flex tn-flex-col-center">
|
||||||
<text v-if="selectedBuilding && selectedBuilding.code === item.code" class="tn-icon-success-circle-fill building-check"></text>
|
<view class="building-icon-wrap">
|
||||||
|
<text class="tn-icon-building"></text>
|
||||||
|
</view>
|
||||||
|
<text class="building-name">{{ getBuildingDisplayName(item) }}</text>
|
||||||
|
</view>
|
||||||
|
<text v-if="selectedBuilding && selectedBuilding.code === item.code" class="tn-icon-check building-check"></text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
@@ -186,7 +199,7 @@ export default {
|
|||||||
this.buildingList = []
|
this.buildingList = []
|
||||||
this.selectedBuilding = null
|
this.selectedBuilding = null
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: this.$tr('楼栋列表加载失败'),
|
title: this.$tr('楼栋加载失败'),
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -218,20 +231,32 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
$text-primary: #1A2233;
|
||||||
|
$text-secondary: #6A7A99;
|
||||||
|
$text-tertiary: #A1B0C4;
|
||||||
|
$surface: #FFFFFF;
|
||||||
|
$bg-popup: #F8FAFF;
|
||||||
|
|
||||||
.building-selector {
|
.building-selector {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-header {
|
.selector-card {
|
||||||
background: #FFFFFF;
|
background: $surface;
|
||||||
border-radius: 20rpx;
|
border-radius: 20rpx;
|
||||||
padding: 26rpx 32rpx;
|
padding: 26rpx 30rpx;
|
||||||
border: 1rpx solid rgba($brand-primary, 0.15);
|
border: 2rpx solid rgba($brand-primary, 0.12);
|
||||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
box-shadow: 0 6rpx 20rpx rgba($brand-primary, 0.05);
|
||||||
min-height: 100rpx;
|
min-height: 120rpx;
|
||||||
|
transition: all 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-header--disabled {
|
.selector-card:active {
|
||||||
|
background: #FAFCFF;
|
||||||
|
border-color: rgba($brand-primary, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-card--disabled {
|
||||||
opacity: 0.72;
|
opacity: 0.72;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,41 +265,104 @@ export default {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selected-label {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $text-tertiary;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.selected-name {
|
.selected-name {
|
||||||
font-size: 28rpx;
|
font-size: 30rpx;
|
||||||
color: #1A2233;
|
color: $text-primary;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
|
|
||||||
&.is-empty {
|
&.is-empty {
|
||||||
color: #A0B0C0;
|
color: $text-tertiary;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selector-arrow {
|
||||||
|
width: 52rpx;
|
||||||
|
height: 52rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #F4F8FF;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: $text-secondary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.popup-content {
|
.popup-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: #F8FAFF;
|
background: $bg-popup;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-header {
|
.popup-header {
|
||||||
padding: 40rpx 30rpx 20rpx;
|
padding: 40rpx 30rpx 28rpx;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-bottom: 1px dashed rgba($brand-primary, 0.15);
|
background: $surface;
|
||||||
background: #FFFFFF;
|
border-bottom: 1rpx solid rgba($brand-primary, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-title {
|
.popup-title {
|
||||||
font-size: 32rpx;
|
font-size: 34rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1A2233;
|
color: $text-primary;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-subtitle {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.building-list-scroll {
|
.building-list-scroll {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 0;
|
height: 0;
|
||||||
padding: 20rpx 30rpx 60rpx;
|
padding: 24rpx 30rpx 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state,
|
||||||
|
.empty-state {
|
||||||
|
padding-top: 120rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba($brand-primary, 0.08);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto 24rpx;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 56rpx;
|
||||||
|
color: $brand-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-primary;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-desc {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.building-list {
|
.building-list {
|
||||||
@@ -289,23 +377,55 @@ export default {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background: #FFFFFF;
|
background: #FFFFFF;
|
||||||
border-radius: 20rpx;
|
border-radius: 20rpx;
|
||||||
padding: 28rpx 30rpx;
|
padding: 24rpx 28rpx;
|
||||||
border: 1rpx solid rgba($brand-primary, 0.12);
|
border: 2rpx solid rgba($brand-primary, 0.1);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
|
||||||
&.is-active {
|
&.is-active {
|
||||||
border-color: rgba($brand-primary, 0.5);
|
background: #FFFFFF;
|
||||||
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.08);
|
border-color: $brand-primary;
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba($brand-primary, 0.14);
|
||||||
|
|
||||||
|
.building-name {
|
||||||
|
color: $brand-primary;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.building-icon-wrap {
|
||||||
|
background: rgba($brand-primary, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.building-name-left {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.building-icon-wrap {
|
||||||
|
width: 44rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
background: rgba($brand-primary, 0.08);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $brand-primary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.building-name {
|
.building-name {
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
color: #1A2233;
|
color: $text-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.building-check {
|
.building-check {
|
||||||
color: $brand-primary;
|
color: $brand-primary;
|
||||||
font-size: 30rpx;
|
font-size: 32rpx;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,22 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="date-range-selector">
|
<view class="date-range-selector tn-flex tn-flex-col-center">
|
||||||
<view class="date-item">
|
<picker mode="date" @change="onStartChange" class="date-picker date-picker--start">
|
||||||
<view class="date-label">{{ resolvedStartLabel }}</view>
|
<view class="date-card" :class="{ 'is-empty': !start, 'is-active': start }">
|
||||||
<picker mode="date" @change="onStartChange">
|
<view class="date-card-header tn-flex tn-flex-row-between tn-flex-col-center">
|
||||||
<view class="date-input" :class="{ 'is-empty': !start }">
|
<text class="date-label">{{ resolvedStartLabel }}</text>
|
||||||
{{ start || resolvedStartPlaceholder }}
|
<view class="date-icon-wrap">
|
||||||
|
<text class="tn-icon-calendar"></text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</picker>
|
<view class="date-value">{{ start || resolvedStartPlaceholder }}</view>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
|
||||||
|
<view class="range-connector">
|
||||||
|
<view class="connector-line"></view>
|
||||||
|
<text class="connector-text">{{ $tr('至') }}</text>
|
||||||
|
<view class="connector-line"></view>
|
||||||
</view>
|
</view>
|
||||||
<view class="date-divider">{{ $tr('至') }}</view>
|
|
||||||
<view class="date-item">
|
<picker mode="date" @change="onEndChange" class="date-picker date-picker--end">
|
||||||
<view class="date-label">{{ resolvedEndLabel }}</view>
|
<view class="date-card" :class="{ 'is-empty': !end, 'is-active': end }">
|
||||||
<picker mode="date" @change="onEndChange">
|
<view class="date-card-header tn-flex tn-flex-row-between tn-flex-col-center">
|
||||||
<view class="date-input" :class="{ 'is-empty': !end }">
|
<text class="date-label">{{ resolvedEndLabel }}</text>
|
||||||
{{ end || resolvedEndPlaceholder }}
|
<view class="date-icon-wrap">
|
||||||
|
<text class="tn-icon-calendar"></text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</picker>
|
<view class="date-value">{{ end || resolvedEndPlaceholder }}</view>
|
||||||
</view>
|
</view>
|
||||||
|
</picker>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -75,44 +87,92 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
$text-primary: #1A2233;
|
||||||
|
$text-secondary: #6A7A99;
|
||||||
|
$text-tertiary: #A1B0C4;
|
||||||
|
|
||||||
.date-range-selector {
|
.date-range-selector {
|
||||||
display: flex;
|
width: 100%;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
background: rgba(255, 255, 255, 0.6);
|
|
||||||
border-radius: 24rpx;
|
|
||||||
padding: 24rpx 32rpx;
|
|
||||||
border: 2rpx solid rgba(226, 232, 240, 0.8);
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.02);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-item {
|
.date-picker {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: center;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-divider {
|
.date-card {
|
||||||
margin: 0 24rpx;
|
background: #F4F8FF;
|
||||||
color: #94A3B8;
|
border-radius: 20rpx;
|
||||||
font-size: 24rpx;
|
padding: 24rpx;
|
||||||
font-weight: 500;
|
border: 2rpx solid transparent;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-color: rgba($brand-primary, 0.25);
|
||||||
|
box-shadow: 0 6rpx 20rpx rgba($brand-primary, 0.08);
|
||||||
|
|
||||||
|
.date-value {
|
||||||
|
color: $text-primary;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-empty .date-value {
|
||||||
|
color: $text-tertiary;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-card-header {
|
||||||
|
margin-bottom: 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-label {
|
.date-label {
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: #64748B;
|
color: $text-secondary;
|
||||||
margin-bottom: 12rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-input {
|
.date-icon-wrap {
|
||||||
font-size: 28rpx;
|
width: 40rpx;
|
||||||
color: #1A2B4C;
|
height: 40rpx;
|
||||||
font-weight: 600;
|
border-radius: 12rpx;
|
||||||
|
background: rgba($brand-primary, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
&.is-empty {
|
text {
|
||||||
color: #CBD5E1;
|
font-size: 22rpx;
|
||||||
font-weight: normal;
|
color: $brand-primary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: $text-primary;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-connector {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 18rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connector-line {
|
||||||
|
width: 2rpx;
|
||||||
|
height: 24rpx;
|
||||||
|
background: rgba($brand-primary, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connector-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $brand-primary;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 8rpx 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,31 +1,39 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="floor-selector">
|
<view class="floor-selector">
|
||||||
<view
|
<view
|
||||||
class="selector-header tn-flex tn-flex-row-between tn-flex-col-center"
|
class="selector-card tn-flex tn-flex-row-between tn-flex-col-center"
|
||||||
@click="handleOpen"
|
@click="handleOpen"
|
||||||
>
|
>
|
||||||
<view class="selected-info">
|
<view class="selected-info">
|
||||||
|
<view class="selected-label">{{ $tr('楼层') }}</view>
|
||||||
<view class="selected-name tn-text-ellipsis" :class="{ 'is-empty': !selectedFloor }">
|
<view class="selected-name tn-text-ellipsis" :class="{ 'is-empty': !selectedFloor }">
|
||||||
{{ selectedFloor ? getFloorDisplayName(selectedFloor) : placeholderText }}
|
{{ selectedFloor ? getFloorDisplayName(selectedFloor) : placeholderText }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="tn-icon-right tn-color-gray"></text>
|
<view class="selector-arrow">
|
||||||
|
<text class="tn-icon-right"></text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<tn-popup v-model="showPopup" mode="bottom" length="72%" :borderRadius="32" :closeBtn="true">
|
<tn-popup v-model="showPopup" mode="bottom" length="72%" :borderRadius="32" :closeBtn="true">
|
||||||
<view class="popup-content">
|
<view class="popup-content">
|
||||||
<view class="popup-header">
|
<view class="popup-header">
|
||||||
<text class="popup-title">{{ $tr('选择意向楼层') }}</text>
|
<view class="popup-title">{{ $tr('选择意向楼层') }}</view>
|
||||||
|
<view class="popup-subtitle">{{ $tr('选择您期望入住的楼层') }}</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<scroll-view scroll-y class="floor-list-scroll">
|
<scroll-view scroll-y class="floor-list-scroll">
|
||||||
<view v-if="isLoading" class="loading-state tn-padding-xl tn-text-center">
|
<view v-if="isLoading" class="loading-state tn-padding-xl tn-text-center">
|
||||||
<tn-loading mode="circle"></tn-loading>
|
<tn-loading mode="circle" :size="48"></tn-loading>
|
||||||
<view class="tn-margin-top-sm tn-color-gray">{{ $tr('楼层加载中...') }}</view>
|
<view class="tn-margin-top tn-color-gray">{{ $tr('楼层加载中...') }}</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else-if="!floorList.length" class="empty-state tn-padding-xl tn-text-center">
|
<view v-else-if="!floorList.length" class="empty-state tn-padding-xl tn-text-center">
|
||||||
<text class="tn-color-gray">{{ $tr('暂无可选楼层') }}</text>
|
<view class="empty-icon">
|
||||||
|
<text class="tn-icon-empty-data"></text>
|
||||||
|
</view>
|
||||||
|
<view class="empty-title">{{ $tr('暂无可选楼层') }}</view>
|
||||||
|
<view class="empty-desc">{{ $tr('当前暂无可用楼层,请稍后再试') }}</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else class="floor-list">
|
<view v-else class="floor-list">
|
||||||
@@ -34,8 +42,13 @@
|
|||||||
:class="{ 'is-active': !selectedFloor }"
|
:class="{ 'is-active': !selectedFloor }"
|
||||||
@click="handleClear"
|
@click="handleClear"
|
||||||
>
|
>
|
||||||
<text class="floor-name">{{ $tr('不选择') }}</text>
|
<view class="floor-name-left tn-flex tn-flex-col-center">
|
||||||
<text v-if="!selectedFloor" class="tn-icon-success-circle-fill floor-check"></text>
|
<view class="floor-icon-wrap">
|
||||||
|
<text class="tn-icon-close" style="font-size: 22rpx;"></text>
|
||||||
|
</view>
|
||||||
|
<text class="floor-name">{{ $tr('不选择') }}</text>
|
||||||
|
</view>
|
||||||
|
<text v-if="!selectedFloor" class="tn-icon-check floor-check"></text>
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
v-for="item in floorList"
|
v-for="item in floorList"
|
||||||
@@ -44,8 +57,13 @@
|
|||||||
:class="{ 'is-active': selectedFloor && selectedFloor.value === item.value }"
|
:class="{ 'is-active': selectedFloor && selectedFloor.value === item.value }"
|
||||||
@click="handleSelect(item)"
|
@click="handleSelect(item)"
|
||||||
>
|
>
|
||||||
<text class="floor-name">{{ getFloorDisplayName(item) }}</text>
|
<view class="floor-name-left tn-flex tn-flex-col-center">
|
||||||
<text v-if="selectedFloor && selectedFloor.value === item.value" class="tn-icon-success-circle-fill floor-check"></text>
|
<view class="floor-icon-wrap">
|
||||||
|
<text class="tn-icon-building"></text>
|
||||||
|
</view>
|
||||||
|
<text class="floor-name">{{ getFloorDisplayName(item) }}</text>
|
||||||
|
</view>
|
||||||
|
<text v-if="selectedFloor && selectedFloor.value === item.value" class="tn-icon-check floor-check"></text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
@@ -173,7 +191,7 @@ export default {
|
|||||||
this.floorList = []
|
this.floorList = []
|
||||||
this.selectedFloor = null
|
this.selectedFloor = null
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: this.$tr('楼层列表加载失败'),
|
title: this.$tr('楼层加载失败'),
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -212,17 +230,29 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
$text-primary: #1A2233;
|
||||||
|
$text-secondary: #6A7A99;
|
||||||
|
$text-tertiary: #A1B0C4;
|
||||||
|
$surface: #FFFFFF;
|
||||||
|
$bg-popup: #F8FAFF;
|
||||||
|
|
||||||
.floor-selector {
|
.floor-selector {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-header {
|
.selector-card {
|
||||||
background: #FFFFFF;
|
background: $surface;
|
||||||
border-radius: 20rpx;
|
border-radius: 20rpx;
|
||||||
padding: 26rpx 32rpx;
|
padding: 26rpx 30rpx;
|
||||||
border: 1rpx solid rgba($brand-primary, 0.15);
|
border: 2rpx solid rgba($brand-primary, 0.12);
|
||||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
box-shadow: 0 6rpx 20rpx rgba($brand-primary, 0.05);
|
||||||
min-height: 100rpx;
|
min-height: 120rpx;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-card:active {
|
||||||
|
background: #FAFCFF;
|
||||||
|
border-color: rgba($brand-primary, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected-info {
|
.selected-info {
|
||||||
@@ -230,41 +260,104 @@ export default {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selected-label {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $text-tertiary;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.selected-name {
|
.selected-name {
|
||||||
font-size: 28rpx;
|
font-size: 30rpx;
|
||||||
color: #1A2233;
|
color: $text-primary;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
|
|
||||||
&.is-empty {
|
&.is-empty {
|
||||||
color: #A0B0C0;
|
color: $text-tertiary;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selector-arrow {
|
||||||
|
width: 52rpx;
|
||||||
|
height: 52rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #F4F8FF;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: $text-secondary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.popup-content {
|
.popup-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: #F8FAFF;
|
background: $bg-popup;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-header {
|
.popup-header {
|
||||||
padding: 40rpx 30rpx 20rpx;
|
padding: 40rpx 30rpx 28rpx;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-bottom: 1px dashed rgba($brand-primary, 0.15);
|
background: $surface;
|
||||||
background: #FFFFFF;
|
border-bottom: 1rpx solid rgba($brand-primary, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-title {
|
.popup-title {
|
||||||
font-size: 32rpx;
|
font-size: 34rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1A2233;
|
color: $text-primary;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-subtitle {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.floor-list-scroll {
|
.floor-list-scroll {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 0;
|
height: 0;
|
||||||
padding: 20rpx 30rpx 60rpx;
|
padding: 24rpx 30rpx 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state,
|
||||||
|
.empty-state {
|
||||||
|
padding-top: 120rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba($brand-primary, 0.08);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto 24rpx;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 56rpx;
|
||||||
|
color: $brand-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-primary;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-desc {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.floor-list {
|
.floor-list {
|
||||||
@@ -279,13 +372,23 @@ export default {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background: #FFFFFF;
|
background: #FFFFFF;
|
||||||
border-radius: 20rpx;
|
border-radius: 20rpx;
|
||||||
padding: 28rpx 30rpx;
|
padding: 24rpx 28rpx;
|
||||||
border: 1rpx solid rgba($brand-primary, 0.12);
|
border: 2rpx solid rgba($brand-primary, 0.1);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
|
||||||
&.is-active {
|
&.is-active {
|
||||||
border-color: rgba($brand-primary, 0.5);
|
background: #FFFFFF;
|
||||||
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.08);
|
border-color: $brand-primary;
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba($brand-primary, 0.14);
|
||||||
|
|
||||||
|
.floor-name {
|
||||||
|
color: $brand-primary;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-icon-wrap {
|
||||||
|
background: rgba($brand-primary, 0.15);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,13 +396,35 @@ export default {
|
|||||||
border-style: dashed;
|
border-style: dashed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.floor-name-left {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-icon-wrap {
|
||||||
|
width: 44rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
background: rgba($brand-primary, 0.08);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $brand-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.floor-name {
|
.floor-name {
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
color: #1A2233;
|
color: $text-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.floor-check {
|
.floor-check {
|
||||||
color: $brand-primary;
|
color: $brand-primary;
|
||||||
font-size: 30rpx;
|
font-size: 32rpx;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,51 +1,72 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="room-type-selector">
|
<view class="room-type-selector">
|
||||||
<view class="selector-header tn-flex tn-flex-row-between tn-flex-col-center" @click="openSelector">
|
<view class="selector-card tn-flex tn-flex-row-between tn-flex-col-center" @click="openSelector">
|
||||||
<view class="selected-info">
|
<view class="selected-info">
|
||||||
|
<view class="selected-label">{{ $tr('意向房型') }}</view>
|
||||||
<view class="selected-name tn-text-ellipsis" :class="{ 'is-empty': !selectedRoomType }">
|
<view class="selected-name tn-text-ellipsis" :class="{ 'is-empty': !selectedRoomType }">
|
||||||
{{ selectedRoomType ? getRoomTypeDisplayName(selectedRoomType) : $tr('请选择意向房型') }}
|
{{ selectedRoomType ? getRoomTypeDisplayName(selectedRoomType) : $tr('请选择意向房型') }}
|
||||||
</view>
|
</view>
|
||||||
<view class="selected-area tn-text-ellipsis" v-if="selectedArea">
|
<view class="selected-area tn-text-ellipsis" v-if="selectedArea">
|
||||||
|
<text class="tn-icon-location area-icon"></text>
|
||||||
{{ getAreaDisplayName(selectedArea) }}
|
{{ getAreaDisplayName(selectedArea) }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="tn-icon-right tn-color-gray"></text>
|
<view class="selector-arrow">
|
||||||
|
<text class="tn-icon-right"></text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<tn-popup v-model="showPopup" mode="bottom" length="80%" :borderRadius="32" :closeBtn="true">
|
<tn-popup v-model="showPopup" mode="bottom" length="80%" :borderRadius="32" :closeBtn="true">
|
||||||
<view class="popup-content">
|
<view class="popup-content">
|
||||||
<view class="popup-header">
|
<view class="popup-header">
|
||||||
<text class="popup-title">{{ $tr('选择意向房型') }}</text>
|
<view class="popup-title">{{ $tr('选择意向房型') }}</view>
|
||||||
|
<view class="popup-subtitle">{{ $tr('选择您心仪的房型与区域') }}</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<scroll-view scroll-y class="area-list-scroll">
|
<scroll-view scroll-y class="area-list-scroll">
|
||||||
<view v-if="isLoading" class="loading-state tn-padding-xl tn-text-center">
|
<view v-if="isLoading" class="loading-state tn-padding-xl tn-text-center">
|
||||||
<tn-loading mode="circle"></tn-loading>
|
<tn-loading mode="circle" :size="48"></tn-loading>
|
||||||
<view class="tn-margin-top-sm tn-color-gray">{{ $tr('房型加载中...') }}</view>
|
<view class="tn-margin-top tn-color-gray">{{ $tr('房型加载中...') }}</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else-if="!displayAreaList.length" class="empty-state tn-padding-xl tn-text-center">
|
<view v-else-if="!displayAreaList.length" class="empty-state tn-padding-xl tn-text-center">
|
||||||
<text class="tn-color-gray">{{ $tr('暂无可选房型') }}</text>
|
<view class="empty-icon">
|
||||||
|
<text class="tn-icon-empty-data"></text>
|
||||||
|
</view>
|
||||||
|
<view class="empty-title">{{ $tr('暂无可选房型') }}</view>
|
||||||
|
<view class="empty-desc">{{ $tr('当前暂无可用房型,请稍后再试') }}</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else class="area-group" v-for="(area, index) in displayAreaList" :key="area.code">
|
<view v-else class="area-group" v-for="(area, index) in displayAreaList" :key="area.code">
|
||||||
<view class="area-title tn-flex tn-flex-row-between tn-flex-col-center" @click="toggleArea(index)">
|
<view class="area-title tn-flex tn-flex-row-between tn-flex-col-center" @click="toggleArea(index)">
|
||||||
<view class="tn-flex tn-flex-col-center">
|
<view class="area-title-left tn-flex tn-flex-col-center">
|
||||||
<text class="area-dot"></text>
|
<view class="area-icon-wrap">
|
||||||
<text>{{ getAreaDisplayName(area) }}</text>
|
<text class="tn-icon-home"></text>
|
||||||
|
</view>
|
||||||
|
<text class="area-title-text">{{ getAreaDisplayName(area) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="area-toggle">
|
||||||
|
<text :class="collapsedAreas[index] ? 'tn-icon-down' : 'tn-icon-up'"></text>
|
||||||
</view>
|
</view>
|
||||||
<text :class="collapsedAreas[index] ? 'tn-icon-down' : 'tn-icon-up'" class="tn-color-gray"></text>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="room-type-grid" v-show="!collapsedAreas[index]">
|
<view class="room-type-grid" v-show="!collapsedAreas[index]">
|
||||||
<view
|
<view
|
||||||
class="room-type-item"
|
class="room-type-item"
|
||||||
v-for="room in area.roomTypes"
|
v-for="room in area.roomTypes"
|
||||||
:key="room.code"
|
:key="room.code"
|
||||||
:class="{ 'is-active': selectedRoomType && selectedRoomType.code === room.code }"
|
:class="{ 'is-active': selectedRoomType && selectedRoomType.code === room.code }"
|
||||||
@click="handleSelect(area, room)"
|
@click="handleSelect(area, room)"
|
||||||
>
|
>
|
||||||
<view class="room-name tn-text-ellipsis">{{ getRoomTypeDisplayName(room) }}</view>
|
<view class="room-name tn-text-ellipsis">{{ getRoomTypeDisplayName(room) }}</view>
|
||||||
<view class="room-price" v-if="room.price">¥{{ getPriceValue(room.price) }}{{ $tr('/月') }}</view>
|
<view class="room-price" v-if="room.price">
|
||||||
|
<text class="price-symbol">¥</text>
|
||||||
|
<text class="price-value">{{ getPriceValue(room.price) }}</text>
|
||||||
|
<text class="price-unit">{{ $tr('/月') }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="room-price room-price--empty">{{ $tr('价格面议') }}</view>
|
||||||
|
<view class="active-mark">
|
||||||
|
<text class="tn-icon-check"></text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -124,28 +145,28 @@ export default {
|
|||||||
},
|
},
|
||||||
loadData(force = false) {
|
loadData(force = false) {
|
||||||
if (!force && this.areaList.length > 0) return
|
if (!force && this.areaList.length > 0) return
|
||||||
|
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
return areaPagination({ PageIndex: 1, PageSize: 100 }, this.language)
|
return areaPagination({ PageIndex: 1, PageSize: 100 }, this.language)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const rawList = Array.isArray(res?.data?.data) ? res.data.data : (Array.isArray(res?.data) ? res.data : [])
|
const rawList = Array.isArray(res?.data?.data) ? res.data.data : (Array.isArray(res?.data) ? res.data : [])
|
||||||
|
|
||||||
this.areaList = rawList
|
this.areaList = rawList
|
||||||
.map(item => normalizeAreaItem(item))
|
.map(item => normalizeAreaItem(item))
|
||||||
.filter(area => area.roomTypes && area.roomTypes.length > 0)
|
.filter(area => area.roomTypes && area.roomTypes.length > 0)
|
||||||
|
|
||||||
// 初始化全部不折叠
|
// 初始化全部不折叠
|
||||||
const collapsed = {}
|
const collapsed = {}
|
||||||
this.areaList.forEach((_, index) => {
|
this.areaList.forEach((_, index) => {
|
||||||
collapsed[index] = false
|
collapsed[index] = false
|
||||||
})
|
})
|
||||||
this.collapsedAreas = collapsed
|
this.collapsedAreas = collapsed
|
||||||
|
|
||||||
this.syncSelection()
|
this.syncSelection()
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error('load room types error', e)
|
console.error('load room types error', e)
|
||||||
uni.showToast({ title: this.$tr('房型列表加载失败'), icon: 'none' })
|
uni.showToast({ title: this.$tr('房型加载失败'), icon: 'none' })
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
@@ -153,12 +174,12 @@ export default {
|
|||||||
},
|
},
|
||||||
syncSelection() {
|
syncSelection() {
|
||||||
if (!this.displayAreaList.length) return
|
if (!this.displayAreaList.length) return
|
||||||
|
|
||||||
let foundArea = null
|
let foundArea = null
|
||||||
let foundRoom = null
|
let foundRoom = null
|
||||||
const previousAreaCode = this.selectedArea && this.selectedArea.code ? this.selectedArea.code : ''
|
const previousAreaCode = this.selectedArea && this.selectedArea.code ? this.selectedArea.code : ''
|
||||||
const previousRoomCode = this.selectedRoomType && this.selectedRoomType.code ? this.selectedRoomType.code : ''
|
const previousRoomCode = this.selectedRoomType && this.selectedRoomType.code ? this.selectedRoomType.code : ''
|
||||||
|
|
||||||
// Try to find by room type code
|
// Try to find by room type code
|
||||||
if (this.value) {
|
if (this.value) {
|
||||||
for (const area of this.displayAreaList) {
|
for (const area of this.displayAreaList) {
|
||||||
@@ -170,18 +191,18 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not found by room, try to find by initial area code
|
// If not found by room, try to find by initial area code
|
||||||
if (!foundRoom && this.initialAreaCode) {
|
if (!foundRoom && this.initialAreaCode) {
|
||||||
foundArea = this.displayAreaList.find(a => a.code === this.initialAreaCode)
|
foundArea = this.displayAreaList.find(a => a.code === this.initialAreaCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (foundArea) {
|
if (foundArea) {
|
||||||
this.selectedArea = foundArea
|
this.selectedArea = foundArea
|
||||||
if (foundRoom) {
|
if (foundRoom) {
|
||||||
this.selectedRoomType = foundRoom
|
this.selectedRoomType = foundRoom
|
||||||
}
|
}
|
||||||
|
|
||||||
if (foundRoom) {
|
if (foundRoom) {
|
||||||
if (this.value !== foundRoom.code) {
|
if (this.value !== foundRoom.code) {
|
||||||
this.$emit('input', foundRoom.code)
|
this.$emit('input', foundRoom.code)
|
||||||
@@ -227,17 +248,30 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
$luxury-gold: #D8B56A;
|
||||||
|
$text-primary: #1A2233;
|
||||||
|
$text-secondary: #6A7A99;
|
||||||
|
$text-tertiary: #A1B0C4;
|
||||||
|
$surface: #FFFFFF;
|
||||||
|
$bg-popup: #F8FAFF;
|
||||||
|
|
||||||
.room-type-selector {
|
.room-type-selector {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-header {
|
.selector-card {
|
||||||
background: #FFFFFF;
|
background: $surface;
|
||||||
border-radius: 20rpx;
|
border-radius: 20rpx;
|
||||||
padding: 26rpx 32rpx;
|
padding: 26rpx 30rpx;
|
||||||
border: 1rpx solid rgba($brand-primary, 0.15);
|
border: 2rpx solid rgba($brand-primary, 0.12);
|
||||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
box-shadow: 0 6rpx 20rpx rgba($brand-primary, 0.05);
|
||||||
min-height: 100rpx;
|
min-height: 120rpx;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-card:active {
|
||||||
|
background: #FAFCFF;
|
||||||
|
border-color: rgba($brand-primary, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected-info {
|
.selected-info {
|
||||||
@@ -245,72 +279,175 @@ export default {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selected-label {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $text-tertiary;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.selected-name {
|
.selected-name {
|
||||||
font-size: 28rpx;
|
font-size: 30rpx;
|
||||||
color: #1A2233;
|
color: $text-primary;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
|
|
||||||
&.is-empty {
|
&.is-empty {
|
||||||
color: #A0B0C0;
|
color: $text-tertiary;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected-area {
|
.selected-area {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-secondary;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.area-icon {
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: #6A7A99;
|
margin-right: 6rpx;
|
||||||
margin-top: 6rpx;
|
color: $brand-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-arrow {
|
||||||
|
width: 52rpx;
|
||||||
|
height: 52rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #F4F8FF;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: $text-secondary;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-content {
|
.popup-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: #F8FAFF;
|
background: $bg-popup;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-header {
|
.popup-header {
|
||||||
padding: 40rpx 30rpx 20rpx;
|
padding: 40rpx 30rpx 28rpx;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-bottom: 1px dashed rgba($brand-primary, 0.15);
|
background: $surface;
|
||||||
background: #FFFFFF;
|
border-bottom: 1rpx solid rgba($brand-primary, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-title {
|
.popup-title {
|
||||||
font-size: 32rpx;
|
font-size: 34rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1A2233;
|
color: $text-primary;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-subtitle {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.area-list-scroll {
|
.area-list-scroll {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 0;
|
height: 0;
|
||||||
padding: 20rpx 30rpx 60rpx;
|
padding: 24rpx 30rpx 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state,
|
||||||
|
.empty-state {
|
||||||
|
padding-top: 120rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba($brand-primary, 0.08);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto 24rpx;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 56rpx;
|
||||||
|
color: $brand-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-primary;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-desc {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.area-group {
|
.area-group {
|
||||||
margin-bottom: 30rpx;
|
margin-bottom: 28rpx;
|
||||||
background: #FFFFFF;
|
background: $surface;
|
||||||
border-radius: 20rpx;
|
border-radius: 24rpx;
|
||||||
padding: 24rpx;
|
padding: 28rpx;
|
||||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.04);
|
box-shadow: 0 8rpx 28rpx rgba(26, 34, 51, 0.04);
|
||||||
border: 1rpx solid rgba($brand-primary, 0.05);
|
border: 1rpx solid rgba($brand-primary, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.area-title {
|
.area-title {
|
||||||
font-size: 28rpx;
|
margin-bottom: 24rpx;
|
||||||
font-weight: 600;
|
|
||||||
color: #1A2233;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.area-dot {
|
.area-title-left {
|
||||||
width: 12rpx;
|
flex: 1;
|
||||||
height: 12rpx;
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.area-icon-wrap {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
border-radius: 14rpx;
|
||||||
|
background: rgba($brand-primary, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $brand-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.area-title-text {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.area-toggle {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: $brand-primary;
|
background: #F4F8FF;
|
||||||
margin-right: 12rpx;
|
display: flex;
|
||||||
box-shadow: 0 0 8rpx rgba($brand-primary, 0.4);
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-secondary;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.room-type-grid {
|
.room-type-grid {
|
||||||
@@ -320,36 +457,86 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.room-type-item {
|
.room-type-item {
|
||||||
padding: 20rpx;
|
position: relative;
|
||||||
border-radius: 16rpx;
|
padding: 24rpx 20rpx;
|
||||||
background: #FFFFFF;
|
border-radius: 20rpx;
|
||||||
border: 1rpx solid rgba($brand-primary, 0.15);
|
background: #FAFCFF;
|
||||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
border: 2rpx solid rgba($brand-primary, 0.1);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
&.is-active {
|
&.is-active {
|
||||||
background: #FFFFFF;
|
background: #FFFFFF;
|
||||||
border-color: $brand-primary;
|
border-color: $brand-primary;
|
||||||
box-shadow: 0 4rpx 16rpx rgba($brand-primary, 0.1);
|
box-shadow: 0 8rpx 24rpx rgba($brand-primary, 0.14);
|
||||||
|
|
||||||
.room-name {
|
.room-name {
|
||||||
color: $brand-primary;
|
color: $brand-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.room-price {
|
.room-price {
|
||||||
color: $brand-primary;
|
color: $brand-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.active-mark {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.room-name {
|
.room-name {
|
||||||
font-size: 26rpx;
|
font-size: 28rpx;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: #1A2233;
|
color: $text-primary;
|
||||||
margin-bottom: 8rpx;
|
margin-bottom: 10rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.room-price {
|
.room-price {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-secondary;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
&--empty {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $text-tertiary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-symbol {
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: #6A7A99;
|
margin-right: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-right: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-unit {
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-mark {
|
||||||
|
position: absolute;
|
||||||
|
top: 12rpx;
|
||||||
|
right: 12rpx;
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $brand-primary;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transform: scale(0);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -57,35 +57,38 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
$text-primary: #1A2233;
|
||||||
|
$text-secondary: #6A7A99;
|
||||||
|
$text-tertiary: #A1B0C4;
|
||||||
|
|
||||||
.option-row {
|
.option-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 16rpx;
|
gap: 20rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-chip {
|
.option-chip {
|
||||||
padding: 16rpx 28rpx;
|
padding: 0 34rpx;
|
||||||
border-radius: 999rpx;
|
height: 72rpx;
|
||||||
background: rgba(255, 255, 255, 0.7);
|
line-height: 72rpx;
|
||||||
color: #64748B;
|
background: #F4F8FF;
|
||||||
font-size: 24rpx;
|
border-radius: 36rpx;
|
||||||
line-height: 1;
|
font-size: 26rpx;
|
||||||
border: 2rpx solid rgba(226, 232, 240, 0.8);
|
color: $text-secondary;
|
||||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.02);
|
border: 2rpx solid transparent;
|
||||||
backdrop-filter: blur(8px);
|
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-chip--active {
|
.option-chip--active {
|
||||||
background: #1A2B4C;
|
background: linear-gradient(135deg, rgba($brand-primary, 0.1) 0%, rgba($brand-primary, 0.06) 100%);
|
||||||
border-color: #1A2B4C;
|
border-color: rgba($brand-primary, 0.35);
|
||||||
color: #FFFFFF;
|
color: $brand-primary;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
box-shadow: 0 8rpx 24rpx rgba(26, 43, 76, 0.2);
|
box-shadow: 0 6rpx 16rpx rgba($brand-primary, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-tips {
|
.empty-tips {
|
||||||
color: #94A3B8;
|
color: $text-tertiary;
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -181,7 +181,8 @@
|
|||||||
:formData="uploadFormData"
|
:formData="uploadFormData"
|
||||||
:max-count="6"
|
:max-count="6"
|
||||||
:max-size="5 * 1024 * 1024"
|
:max-size="5 * 1024 * 1024"
|
||||||
uploadText=""
|
:uploadText="''"
|
||||||
|
:customBtn="true"
|
||||||
@on-success="onUploadSuccess"
|
@on-success="onUploadSuccess"
|
||||||
@on-list-change="onPhotoListChange"
|
@on-list-change="onPhotoListChange"
|
||||||
@on-remove="onRemoveImage"
|
@on-remove="onRemoveImage"
|
||||||
@@ -426,6 +427,25 @@ function pickReadableValue(...values) {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isOpaqueCodeValue(value = '') {
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
if (!text) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return /^[A-Z0-9-]{12,}$/i.test(text) && !/[\u4e00-\u9fa5\s]/.test(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickReadableLabel(...values) {
|
||||||
|
for (let index = 0; index < values.length; index += 1) {
|
||||||
|
const text = String(values[index] || '').trim()
|
||||||
|
if (!text || isOpaqueCodeValue(text)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
function safeJsonParse(value, fallback = null) {
|
function safeJsonParse(value, fallback = null) {
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
return value || fallback
|
return value || fallback
|
||||||
@@ -532,18 +552,20 @@ export default {
|
|||||||
return 'rgba(255, 255, 255, 0.96)'
|
return 'rgba(255, 255, 255, 0.96)'
|
||||||
},
|
},
|
||||||
navBarBackStyle() {
|
navBarBackStyle() {
|
||||||
const isDarkBg = this.scrollTop < 40
|
|
||||||
return {
|
return {
|
||||||
transition: 'all 0.2s ease',
|
transition: 'all 0.2s ease',
|
||||||
background: isDarkBg ? 'rgba(0, 0, 0, 0.2)' : 'transparent',
|
background: this.scrollTop < 40 ? 'rgba(255, 255, 255, 0.82)' : 'rgba(255, 255, 255, 0.96)',
|
||||||
borderRadius: '50%',
|
border: '1rpx solid rgba(43, 142, 255, 0.08)',
|
||||||
width: '64rpx',
|
boxShadow: '0 10rpx 28rpx rgba(31, 76, 145, 0.10)',
|
||||||
height: '64rpx',
|
backdropFilter: 'blur(18rpx)',
|
||||||
|
borderRadius: '20rpx',
|
||||||
|
width: '72rpx',
|
||||||
|
height: '72rpx',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginLeft: '12rpx',
|
marginLeft: '12rpx',
|
||||||
color: isDarkBg ? '#FFFFFF' : '#000000'
|
color: '#1A2233'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
navTitleOpacity() {
|
navTitleOpacity() {
|
||||||
@@ -573,7 +595,12 @@ export default {
|
|||||||
) || '--'
|
) || '--'
|
||||||
},
|
},
|
||||||
currentAreaText() {
|
currentAreaText() {
|
||||||
return pickReadableValue(this.currentOrderDetail.areaName, this.currentOrderDetail.campusName, this.currentOrderDetail.areaCode)
|
return pickReadableLabel(
|
||||||
|
this.currentOrderDetail.areaText,
|
||||||
|
this.currentOrderDetail.areaName,
|
||||||
|
this.currentOrderDetail.areaLabel,
|
||||||
|
this.currentOrderDetail.campusName
|
||||||
|
)
|
||||||
},
|
},
|
||||||
currentLeaseText() {
|
currentLeaseText() {
|
||||||
const start = String(this.currentOrderDetail.startDate || this.currentOrderDetail.start || '').slice(0, 10)
|
const start = String(this.currentOrderDetail.startDate || this.currentOrderDetail.start || '').slice(0, 10)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<current-application-bill-panel
|
<current-application-bill-panel
|
||||||
ref="billPanel"
|
ref="billPanel"
|
||||||
:reference-type-no="referenceTypeNo"
|
:reference-type-no="referenceTypeNo"
|
||||||
|
:auto-resolve-reference="!pendingBillNo || !!referenceTypeNo"
|
||||||
@reference-resolved="handleReferenceResolved"
|
@reference-resolved="handleReferenceResolved"
|
||||||
@all-paid="handleAllPaid"
|
@all-paid="handleAllPaid"
|
||||||
/>
|
/>
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { accountQueryBill } from '@/api/index.js'
|
||||||
import { fetchEnums } from '@/utils/enum-store.js'
|
import { fetchEnums } from '@/utils/enum-store.js'
|
||||||
import { saveActiveApplicationCode, saveHomePageTabId } from '@/utils/app-state'
|
import { saveActiveApplicationCode, saveHomePageTabId } from '@/utils/app-state'
|
||||||
import CurrentApplicationBillPanel from '../components/CurrentApplicationBillPanel.vue'
|
import CurrentApplicationBillPanel from '../components/CurrentApplicationBillPanel.vue'
|
||||||
@@ -26,17 +28,54 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
referenceTypeNo: ''
|
referenceTypeNo: '',
|
||||||
|
pendingBillNo: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async onLoad(options) {
|
async onLoad(options) {
|
||||||
await fetchEnums(false, this.$getLanguage())
|
await fetchEnums(false, this.$getLanguage())
|
||||||
this.referenceTypeNo = options && options.referenceTypeNo ? decodeURIComponent(options.referenceTypeNo) : ''
|
this.referenceTypeNo = options && options.referenceTypeNo ? decodeURIComponent(options.referenceTypeNo) : ''
|
||||||
|
this.pendingBillNo = options && options.billNo ? decodeURIComponent(options.billNo) : ''
|
||||||
|
if (!this.referenceTypeNo && this.pendingBillNo) {
|
||||||
|
await this.resolveReferenceTypeNoByBillNo(this.pendingBillNo)
|
||||||
|
}
|
||||||
if (this.referenceTypeNo) {
|
if (this.referenceTypeNo) {
|
||||||
saveActiveApplicationCode(this.referenceTypeNo)
|
saveActiveApplicationCode(this.referenceTypeNo)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
extractBillDetail(response) {
|
||||||
|
const payload = response && response.data ? response.data : response
|
||||||
|
if (payload && payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
|
||||||
|
return payload.data
|
||||||
|
}
|
||||||
|
return payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {}
|
||||||
|
},
|
||||||
|
async resolveReferenceTypeNoByBillNo(billNo = '') {
|
||||||
|
const currentBillNo = String(billNo || '').trim()
|
||||||
|
if (!currentBillNo) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await accountQueryBill({ billNo: currentBillNo }, this.$getLanguage())
|
||||||
|
const detail = this.extractBillDetail(response)
|
||||||
|
const referenceTypeNo = String(
|
||||||
|
detail.referenceTypeNo
|
||||||
|
|| detail.ReferenceTypeNo
|
||||||
|
|| detail.detailCode
|
||||||
|
|| detail.applicationCode
|
||||||
|
|| ''
|
||||||
|
).trim()
|
||||||
|
if (referenceTypeNo) {
|
||||||
|
this.referenceTypeNo = referenceTypeNo
|
||||||
|
saveActiveApplicationCode(referenceTypeNo)
|
||||||
|
}
|
||||||
|
return referenceTypeNo
|
||||||
|
} catch (error) {
|
||||||
|
console.error('根据账单号解析申请单号失败', error)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
handleReferenceResolved(referenceTypeNo) {
|
handleReferenceResolved(referenceTypeNo) {
|
||||||
this.referenceTypeNo = referenceTypeNo || ''
|
this.referenceTypeNo = referenceTypeNo || ''
|
||||||
if (this.referenceTypeNo) {
|
if (this.referenceTypeNo) {
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ function logRequestError(name, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveOrderNo(order = {}) {
|
function resolveOrderNo(order = {}) {
|
||||||
const candidates = [order.orderNo, order.no, order.code, order.OrderNo]
|
const safeOrder = order || {}
|
||||||
|
const candidates = [safeOrder.orderNo, safeOrder.no, safeOrder.code, safeOrder.OrderNo]
|
||||||
for (let index = 0; index < candidates.length; index += 1) {
|
for (let index = 0; index < candidates.length; index += 1) {
|
||||||
const value = candidates[index]
|
const value = candidates[index]
|
||||||
if (value === undefined || value === null) {
|
if (value === undefined || value === null) {
|
||||||
@@ -177,7 +178,7 @@ const store = new Vuex.Store({
|
|||||||
? getMyPendingSignContracts({ OrderNo: currentOrderNo, ProviderType: 'WechatMiniProgram' }, language).catch((error) => logRequestError('getMyPendingSignContracts', error))
|
? getMyPendingSignContracts({ OrderNo: currentOrderNo, ProviderType: 'WechatMiniProgram' }, language).catch((error) => logRequestError('getMyPendingSignContracts', error))
|
||||||
: Promise.resolve([]),
|
: Promise.resolve([]),
|
||||||
currentOrderNo
|
currentOrderNo
|
||||||
? getMyEffectiveContracts({ OrderNo: currentOrderNo }, language).catch((error) => logRequestError('getMyEffectiveContracts', error))
|
? getMyEffectiveContracts({ OrderNo: currentOrderNo, ProviderType: 'WechatMiniProgram' }, language).catch((error) => logRequestError('getMyEffectiveContracts', error))
|
||||||
: Promise.resolve([])
|
: Promise.resolve([])
|
||||||
])
|
])
|
||||||
}).then(([validOrders, currentOrder, pendingContractsResponse, effectiveContractsResponse]) => {
|
}).then(([validOrders, currentOrder, pendingContractsResponse, effectiveContractsResponse]) => {
|
||||||
|
|||||||
@@ -682,7 +682,7 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
&--custom {
|
&--custom {
|
||||||
width: 100%;
|
width: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ function buildRoomDataFromOrder(order = {}) {
|
|||||||
order.roomTypeName || order.roomType || ''
|
order.roomTypeName || order.roomType || ''
|
||||||
].filter(Boolean).join(' · ')
|
].filter(Boolean).join(' · ')
|
||||||
|
|
||||||
const start = String(order.startDate || order.start || order.checkinDate || '').slice(0, 10)
|
const start = String(order.contractStartTime || order.startDate || order.start || order.checkinDate || '').slice(0, 10)
|
||||||
const end = String(order.endDate || order.end || order.checkoutDate || '').slice(0, 10)
|
const end = String(order.contractEndTime || order.endDate || order.end || order.checkoutDate || '').slice(0, 10)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...order,
|
...order,
|
||||||
|
|||||||
@@ -11,12 +11,8 @@ export function resolveExistingBookingNavigationTarget(stayState = {}) {
|
|||||||
const currentBooking = source.currentBooking || null
|
const currentBooking = source.currentBooking || null
|
||||||
const hasCheckedIn = source.hasCheckedIn === true
|
const hasCheckedIn = source.hasCheckedIn === true
|
||||||
|
|
||||||
if (currentBooking && currentBooking.orderNo) {
|
|
||||||
return `/pkgBooking/booking_detail/booking_detail?orderNo=${encodeURIComponent(currentBooking.orderNo)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentBooking) {
|
if (currentBooking) {
|
||||||
return '/pkgBooking/booking_detail/booking_detail'
|
return '/pages/index/index?pageId=stay-service'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasCheckedIn) {
|
if (hasCheckedIn) {
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ function getGuardText(language = 'zh') {
|
|||||||
return {
|
return {
|
||||||
loginTitle: zh ? '提示' : 'Notice',
|
loginTitle: zh ? '提示' : 'Notice',
|
||||||
loginContent: zh ? '您还未登录,请先登录或注册账号。' : 'Please sign in or register an account first.',
|
loginContent: zh ? '您还未登录,请先登录或注册账号。' : 'Please sign in or register an account first.',
|
||||||
|
loginCancelText: zh ? '取消' : 'Cancel',
|
||||||
loginConfirmText: zh ? '去登录' : 'Sign In',
|
loginConfirmText: zh ? '去登录' : 'Sign In',
|
||||||
existingBookingTitle: zh ? '已有进行中的预订' : 'Existing Booking',
|
existingBookingTitle: zh ? '已有进行中的预订' : 'Existing Booking',
|
||||||
existingBookingContent: zh
|
existingBookingContent: zh
|
||||||
? '您当前状态为“{status}”,请先查看预订详情。'
|
? '您当前状态为“{status}”,请先查看预订详情。'
|
||||||
: 'Your current booking status is "{status}". Please review the booking detail first.',
|
: 'Your current booking status is "{status}". Please review the booking detail first.',
|
||||||
|
existingBookingCancelText: zh ? '取消' : 'Cancel',
|
||||||
viewBooking: zh ? '查看预订' : 'View Booking'
|
viewBooking: zh ? '查看预订' : 'View Booking'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,6 +71,7 @@ export function ensureBookingAccess(options = {}) {
|
|||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: options.loginTitle || guardText.loginTitle,
|
title: options.loginTitle || guardText.loginTitle,
|
||||||
content: options.loginContent || guardText.loginContent,
|
content: options.loginContent || guardText.loginContent,
|
||||||
|
cancelText: options.loginCancelText || guardText.loginCancelText,
|
||||||
confirmText: options.loginConfirmText || guardText.loginConfirmText,
|
confirmText: options.loginConfirmText || guardText.loginConfirmText,
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
@@ -89,6 +92,7 @@ export function ensureBookingAccess(options = {}) {
|
|||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: guardText.existingBookingTitle,
|
title: guardText.existingBookingTitle,
|
||||||
content: guardText.existingBookingContent.replace('{status}', statusText),
|
content: guardText.existingBookingContent.replace('{status}', statusText),
|
||||||
|
cancelText: guardText.existingBookingCancelText,
|
||||||
confirmText: guardText.viewBooking,
|
confirmText: guardText.viewBooking,
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
|
|||||||
@@ -144,22 +144,23 @@ export function formatOrderDate(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeStatusKey(order = {}) {
|
function normalizeStatusKey(order = {}) {
|
||||||
const rawStatus = String(order.status || order.orderStatus || '').trim()
|
const safeOrder = order || {}
|
||||||
|
const rawStatus = String(safeOrder.status || safeOrder.orderStatus || '').trim()
|
||||||
if (['pending_review', 'approved_wait_assign', 'assigned_wait_room', 'rejected', 'contract_signed'].includes(rawStatus)) {
|
if (['pending_review', 'approved_wait_assign', 'assigned_wait_room', 'rejected', 'contract_signed'].includes(rawStatus)) {
|
||||||
return rawStatus
|
return rawStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
const contractStatus = Number(order.contractStatus)
|
const contractStatus = Number(safeOrder.contractStatus)
|
||||||
if (contractStatus === 1) {
|
if (contractStatus === 1) {
|
||||||
return 'contract_signed'
|
return 'contract_signed'
|
||||||
}
|
}
|
||||||
|
|
||||||
const approvalStatus = Number(
|
const approvalStatus = Number(
|
||||||
order.approvalStatus !== undefined
|
safeOrder.approvalStatus !== undefined
|
||||||
? order.approvalStatus
|
? safeOrder.approvalStatus
|
||||||
: order.auditStatus !== undefined
|
: safeOrder.auditStatus !== undefined
|
||||||
? order.auditStatus
|
? safeOrder.auditStatus
|
||||||
: order.statusCode
|
: safeOrder.statusCode
|
||||||
)
|
)
|
||||||
|
|
||||||
if (approvalStatus === -1 || /reject|refuse|驳回/i.test(rawStatus)) {
|
if (approvalStatus === -1 || /reject|refuse|驳回/i.test(rawStatus)) {
|
||||||
@@ -169,12 +170,12 @@ function normalizeStatusKey(order = {}) {
|
|||||||
return 'pending_review'
|
return 'pending_review'
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
order.roomNo ||
|
safeOrder.roomNo ||
|
||||||
order.roomCode ||
|
safeOrder.roomCode ||
|
||||||
order.roomName ||
|
safeOrder.roomName ||
|
||||||
order.checkinDate ||
|
safeOrder.checkinDate ||
|
||||||
order.startDate ||
|
safeOrder.startDate ||
|
||||||
order.start
|
safeOrder.start
|
||||||
) {
|
) {
|
||||||
return 'assigned_wait_room'
|
return 'assigned_wait_room'
|
||||||
}
|
}
|
||||||
@@ -182,17 +183,18 @@ function normalizeStatusKey(order = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveStatusText(order = {}, statusKey, language = getCurrentLanguage()) {
|
function resolveStatusText(order = {}, statusKey, language = getCurrentLanguage()) {
|
||||||
|
const safeOrder = order || {}
|
||||||
|
|
||||||
const contractStatus = Number(order.contractStatus)
|
const contractStatus = Number(safeOrder.contractStatus)
|
||||||
if (contractStatus === 1 && order.contractStatusDesc && language !== 'en') {
|
if (contractStatus === 1 && safeOrder.contractStatusDesc && language !== 'en') {
|
||||||
return order.contractStatusDesc
|
return safeOrder.contractStatusDesc
|
||||||
}
|
}
|
||||||
|
|
||||||
if (language !== 'en' && order.approvalStatusDesc) {
|
if (language !== 'en' && safeOrder.approvalStatusDesc) {
|
||||||
return order.approvalStatusDesc
|
return safeOrder.approvalStatusDesc
|
||||||
}
|
}
|
||||||
if (language !== 'en' && order.statusDesc) {
|
if (language !== 'en' && safeOrder.statusDesc) {
|
||||||
return order.statusDesc
|
return safeOrder.statusDesc
|
||||||
}
|
}
|
||||||
const map = {
|
const map = {
|
||||||
pending_review: getBookingText('pendingReview', language),
|
pending_review: getBookingText('pendingReview', language),
|
||||||
@@ -218,35 +220,39 @@ function resolveTagType(statusKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getBuildingFloorText(order = {}, language = getCurrentLanguage()) {
|
function getBuildingFloorText(order = {}, language = getCurrentLanguage()) {
|
||||||
|
const safeOrder = order || {}
|
||||||
const isEn = language === 'en'
|
const isEn = language === 'en'
|
||||||
const building = [
|
const building = [
|
||||||
isEn
|
isEn
|
||||||
? (order.buildingNameEn || order.buildingEn || order.buildingSectionNameEn || order.buildingName || order.building || order.buildingSectionName || '')
|
? (safeOrder.buildingNameEn || safeOrder.buildingEn || safeOrder.buildingSectionNameEn || safeOrder.buildingName || safeOrder.building || safeOrder.buildingSectionName || '')
|
||||||
: (order.buildingName || order.building || order.buildingSectionName || ''),
|
: (safeOrder.buildingName || safeOrder.building || safeOrder.buildingSectionName || ''),
|
||||||
isEn
|
isEn
|
||||||
? (order.floorNameEn || order.floorEn || order.floorName || order.floor || '')
|
? (safeOrder.floorNameEn || safeOrder.floorEn || safeOrder.floorName || safeOrder.floor || '')
|
||||||
: (order.floorName || order.floor || '')
|
: (safeOrder.floorName || safeOrder.floor || '')
|
||||||
].filter(Boolean).join(' ')
|
].filter(Boolean).join(' ')
|
||||||
return building || '--'
|
return building || '--'
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRoomTypeText(order = {}, language = getCurrentLanguage()) {
|
function getRoomTypeText(order = {}, language = getCurrentLanguage()) {
|
||||||
|
const safeOrder = order || {}
|
||||||
const isEn = language === 'en'
|
const isEn = language === 'en'
|
||||||
if (isEn) {
|
if (isEn) {
|
||||||
return order.roomTypeNameEn || order.styleNameEn || order.roomNameEn || order.roomTypeName || order.styleNameZh || order.roomType || order.roomName || '--'
|
return safeOrder.roomTypeNameEn || safeOrder.styleNameEn || safeOrder.roomNameEn || safeOrder.roomTypeName || safeOrder.styleNameZh || safeOrder.roomType || safeOrder.roomName || '--'
|
||||||
}
|
}
|
||||||
return order.roomTypeName || order.styleNameZh || order.roomType || order.roomName || '--'
|
return safeOrder.roomTypeName || safeOrder.styleNameZh || safeOrder.roomType || safeOrder.roomName || '--'
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOrderNo(order = {}) {
|
function getOrderNo(order = {}) {
|
||||||
return order.orderNo || order.no || order.code || '--'
|
const safeOrder = order || {}
|
||||||
|
return safeOrder.orderNo || safeOrder.no || safeOrder.code || '--'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOrderStatusCode(order = {}) {
|
export function getOrderStatusCode(order = {}) {
|
||||||
|
const safeOrder = order || {}
|
||||||
const candidates = [
|
const candidates = [
|
||||||
order.orderStatus,
|
safeOrder.orderStatus,
|
||||||
order.statusCode,
|
safeOrder.statusCode,
|
||||||
order.status
|
safeOrder.status
|
||||||
]
|
]
|
||||||
|
|
||||||
for (let index = 0; index < candidates.length; index += 1) {
|
for (let index = 0; index < candidates.length; index += 1) {
|
||||||
@@ -264,14 +270,15 @@ export function getOrderStatusCode(order = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getOrderTime(order = {}) {
|
export function getOrderTime(order = {}) {
|
||||||
const checkInPeriod = order.checkInPeriod || {}
|
const safeOrder = order || {}
|
||||||
|
const checkInPeriod = safeOrder.checkInPeriod || {}
|
||||||
const candidates = [
|
const candidates = [
|
||||||
order.updateTime,
|
safeOrder.updateTime,
|
||||||
order.createTime,
|
safeOrder.createTime,
|
||||||
order.createdAt,
|
safeOrder.createdAt,
|
||||||
order.startDate,
|
safeOrder.startDate,
|
||||||
order.start,
|
safeOrder.start,
|
||||||
order.checkinDate,
|
safeOrder.checkinDate,
|
||||||
checkInPeriod.start
|
checkInPeriod.start
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -287,13 +294,14 @@ export function sortOrdersByLatest(orderList = []) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hasStayFields(order = {}) {
|
function hasStayFields(order = {}) {
|
||||||
|
const safeOrder = order || {}
|
||||||
return !!(
|
return !!(
|
||||||
order.roomNo
|
safeOrder.roomNo
|
||||||
|| order.roomCode
|
|| safeOrder.roomCode
|
||||||
|| order.slotCode
|
|| safeOrder.slotCode
|
||||||
|| order.contractNo
|
|| safeOrder.contractNo
|
||||||
|| order.actualCheckInTime
|
|| safeOrder.actualCheckInTime
|
||||||
|| order.checkinTime
|
|| safeOrder.checkinTime
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,32 +344,33 @@ export function isCurrentBookingOrder(order = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeBookingOrder(order = {}, language = getCurrentLanguage()) {
|
export function normalizeBookingOrder(order = {}, language = getCurrentLanguage()) {
|
||||||
const statusKey = normalizeStatusKey(order)
|
const safeOrder = order || {}
|
||||||
const statusCode = getOrderStatusCode(order)
|
const statusKey = normalizeStatusKey(safeOrder)
|
||||||
const checkInPeriod = order.checkInPeriod || {}
|
const statusCode = getOrderStatusCode(safeOrder)
|
||||||
const startDate = order.startDate || order.start || order.checkinDate || checkInPeriod.start
|
const checkInPeriod = safeOrder.checkInPeriod || {}
|
||||||
const endDate = order.endDate || order.end || order.checkoutDate || checkInPeriod.end
|
const startDate = safeOrder.contractStartTime || safeOrder.startDate || safeOrder.start || safeOrder.checkinDate || checkInPeriod.start
|
||||||
|
const endDate = safeOrder.contractEndTime || safeOrder.endDate || safeOrder.end || safeOrder.checkoutDate || checkInPeriod.end
|
||||||
return {
|
return {
|
||||||
...order,
|
...safeOrder,
|
||||||
orderNo: getOrderNo(order),
|
orderNo: getOrderNo(safeOrder),
|
||||||
roomTypeText: getRoomTypeText(order, language),
|
roomTypeText: getRoomTypeText(safeOrder, language),
|
||||||
buildingFloorText: getBuildingFloorText(order, language),
|
buildingFloorText: getBuildingFloorText(safeOrder, language),
|
||||||
startDateText: formatOrderDate(startDate),
|
startDateText: formatOrderDate(startDate),
|
||||||
endDateText: formatOrderDate(endDate),
|
endDateText: formatOrderDate(endDate),
|
||||||
createdAtText: formatOrderDateTime(order.createTime || order.updateTime || order.createdAt),
|
createdAtText: formatOrderDateTime(safeOrder.createTime || safeOrder.updateTime || safeOrder.createdAt),
|
||||||
roomNoText: language === 'en'
|
roomNoText: language === 'en'
|
||||||
? (order.roomNoEn || order.roomCodeEn || order.roomNameEn || order.roomNo || order.roomCode || order.roomName || '--')
|
? (safeOrder.roomNoEn || safeOrder.roomCodeEn || safeOrder.roomNameEn || safeOrder.roomNo || safeOrder.roomCode || safeOrder.roomName || '--')
|
||||||
: (order.roomNo || order.roomCode || order.roomName || order.roomNoEn || order.roomCodeEn || order.roomNameEn || '--'),
|
: (safeOrder.roomNo || safeOrder.roomCode || safeOrder.roomName || safeOrder.roomNoEn || safeOrder.roomCodeEn || safeOrder.roomNameEn || '--'),
|
||||||
areaText: language === 'en'
|
areaText: language === 'en'
|
||||||
? (order.areaNameEn || order.campusNameEn || order.areaName || order.campusName || '--')
|
? (safeOrder.areaNameEn || safeOrder.campusNameEn || safeOrder.areaName || safeOrder.campusName || '--')
|
||||||
: (order.areaName || order.campusName || '--'),
|
: (safeOrder.areaName || safeOrder.campusName || '--'),
|
||||||
rejectReasonText: order.rejectReason || order.approveReason || order.remark || '',
|
rejectReasonText: safeOrder.rejectReason || safeOrder.approveReason || safeOrder.remark || '',
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
status: statusKey,
|
status: statusKey,
|
||||||
statusCode,
|
statusCode,
|
||||||
statusKey,
|
statusKey,
|
||||||
statusText: resolveStatusText(order, statusKey, language),
|
statusText: resolveStatusText(safeOrder, statusKey, language),
|
||||||
tagType: resolveTagType(statusKey)
|
tagType: resolveTagType(statusKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
112
utils/contract-display.js
Normal file
112
utils/contract-display.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { contractDetail } from '@/api/index.js'
|
||||||
|
|
||||||
|
const contractNameCache = Object.create(null)
|
||||||
|
const contractDetailCache = Object.create(null)
|
||||||
|
|
||||||
|
function pickFirstText(...values) {
|
||||||
|
for (let index = 0; index < values.length; index += 1) {
|
||||||
|
const value = values[index]
|
||||||
|
if (value === undefined || value === null) continue
|
||||||
|
const text = String(value).trim()
|
||||||
|
if (text) return text
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseContractDetailResponse(response) {
|
||||||
|
const payload = response && response.data ? response.data : response
|
||||||
|
if (payload && payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
|
||||||
|
return payload.data
|
||||||
|
}
|
||||||
|
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveContractNameFromSource(source = {}) {
|
||||||
|
return pickFirstText(
|
||||||
|
source.contractDisplayName,
|
||||||
|
source.contractName,
|
||||||
|
source.contractTemplateName,
|
||||||
|
source.contractTypeName,
|
||||||
|
source.contractType,
|
||||||
|
source.type,
|
||||||
|
source.typeName,
|
||||||
|
source.templateName,
|
||||||
|
source.title
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveContractNo(source = {}) {
|
||||||
|
return pickFirstText(source.no, source.contractNo)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getContractDisplayName(source = {}, fallbackText = '') {
|
||||||
|
const directName = resolveContractNameFromSource(source)
|
||||||
|
if (directName) return directName
|
||||||
|
|
||||||
|
const contractNo = resolveContractNo(source)
|
||||||
|
if (contractNo && contractNameCache[contractNo]) return contractNameCache[contractNo]
|
||||||
|
|
||||||
|
return fallbackText || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchContractDisplayInfo(contractNo = '', language) {
|
||||||
|
const normalizedContractNo = String(contractNo || '').trim()
|
||||||
|
if (!normalizedContractNo) {
|
||||||
|
return {
|
||||||
|
contractDisplayName: '',
|
||||||
|
detail: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contractDetailCache[normalizedContractNo]) {
|
||||||
|
const cachedDetail = contractDetailCache[normalizedContractNo]
|
||||||
|
return {
|
||||||
|
contractDisplayName: resolveContractNameFromSource(cachedDetail) || contractNameCache[normalizedContractNo] || '',
|
||||||
|
detail: cachedDetail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await contractDetail({ contractNo: normalizedContractNo }, language)
|
||||||
|
const detail = parseContractDetailResponse(response)
|
||||||
|
const contractDisplayName = resolveContractNameFromSource(detail)
|
||||||
|
|
||||||
|
contractDetailCache[normalizedContractNo] = detail
|
||||||
|
contractNameCache[normalizedContractNo] = contractDisplayName
|
||||||
|
|
||||||
|
return {
|
||||||
|
contractDisplayName,
|
||||||
|
detail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function enrichContractDisplayNames(list = [], language) {
|
||||||
|
const sourceList = Array.isArray(list) ? list : []
|
||||||
|
const pendingNos = [...new Set(
|
||||||
|
sourceList
|
||||||
|
.filter(item => !resolveContractNameFromSource(item))
|
||||||
|
.map(item => resolveContractNo(item))
|
||||||
|
.filter(Boolean)
|
||||||
|
)]
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
pendingNos.map((contractNo) =>
|
||||||
|
fetchContractDisplayInfo(contractNo, language).catch(() => ({
|
||||||
|
contractDisplayName: '',
|
||||||
|
detail: {}
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return sourceList.map((item) => {
|
||||||
|
const contractNo = resolveContractNo(item)
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
contractDisplayName: getContractDisplayName(item, '')
|
||||||
|
|| (contractNo ? contractNameCache[contractNo] : '')
|
||||||
|
|| ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
90
utils/deposit-other-payment.js
Normal file
90
utils/deposit-other-payment.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { sysdictPage } from '@/api/index.js'
|
||||||
|
|
||||||
|
export const MOBILE_DEPOSIT_OTHER_PAYMENT_CONFIG_TYPE = 'MobileDepositOtherPaymentConfig'
|
||||||
|
export const MOBILE_PROVIDER_TYPE = 'WechatMiniProgram'
|
||||||
|
export const DEPOSIT_OTHER_PAYMENT_CONFIG_KEY = 'depositOtherPayment'
|
||||||
|
|
||||||
|
function normalizeText(value) {
|
||||||
|
return String(value || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJsonStringMaybe(value) {
|
||||||
|
const text = normalizeText(value)
|
||||||
|
if (!text) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(text)
|
||||||
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
|
||||||
|
} catch (error) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRows(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 getProviderType(row = {}) {
|
||||||
|
return normalizeText(row.providerType || row.ProviderType)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDefaultDepositOtherPaymentConfig() {
|
||||||
|
return {
|
||||||
|
code: '',
|
||||||
|
key: DEPOSIT_OTHER_PAYMENT_CONFIG_KEY,
|
||||||
|
phone: '',
|
||||||
|
qrCode: '',
|
||||||
|
contentZh: '',
|
||||||
|
contentEn: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeDepositOtherPaymentConfig(row = {}) {
|
||||||
|
const defaults = createDefaultDepositOtherPaymentConfig()
|
||||||
|
const extra = parseJsonStringMaybe(row.extraInfo || row.extraConfig || row.ExtraConfig)
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: normalizeText(row.code),
|
||||||
|
key: normalizeText(row.key) || defaults.key,
|
||||||
|
phone: normalizeText(extra.phone || row.description),
|
||||||
|
qrCode: normalizeText(extra.qrCode || extra.qrCodeImage || extra.qrImage),
|
||||||
|
contentZh: normalizeText(extra.contentZh || extra.content || defaults.contentZh),
|
||||||
|
contentEn: normalizeText(extra.contentEn || defaults.contentEn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findMatchedRow(rows = []) {
|
||||||
|
const providerRows = rows.filter(item => getProviderType(item) === MOBILE_PROVIDER_TYPE)
|
||||||
|
const legacyRows = rows.filter(item => !getProviderType(item))
|
||||||
|
return (
|
||||||
|
providerRows.find(item => normalizeText(item && item.key) === DEPOSIT_OTHER_PAYMENT_CONFIG_KEY)
|
||||||
|
|| providerRows[0]
|
||||||
|
|| legacyRows.find(item => normalizeText(item && item.key) === DEPOSIT_OTHER_PAYMENT_CONFIG_KEY)
|
||||||
|
|| legacyRows[0]
|
||||||
|
|| null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadDepositOtherPaymentConfig(language) {
|
||||||
|
return sysdictPage({
|
||||||
|
Type: MOBILE_DEPOSIT_OTHER_PAYMENT_CONFIG_TYPE,
|
||||||
|
ProviderType: MOBILE_PROVIDER_TYPE,
|
||||||
|
PageIndex: 1,
|
||||||
|
PageSize: 100
|
||||||
|
}, language).then((response) => {
|
||||||
|
const rows = normalizeRows(response)
|
||||||
|
const matchedRow = findMatchedRow(rows)
|
||||||
|
return normalizeDepositOtherPaymentConfig(matchedRow || {})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -77,9 +77,18 @@ export function extractLoginSession(response = {}, language = 'zh') {
|
|||||||
throw new Error(getText(language, '登录成功但未返回 accessToken', 'Login succeeded but accessToken is missing'))
|
throw new Error(getText(language, '登录成功但未返回 accessToken', 'Login succeeded but accessToken is missing'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userCode = responseData.userCode
|
||||||
|
|| responseData.code
|
||||||
|
|| nestedData.userCode
|
||||||
|
|| nestedData.code
|
||||||
|
|| response.userCode
|
||||||
|
|| response.code
|
||||||
|
|| ''
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token,
|
token,
|
||||||
expires
|
expires,
|
||||||
|
userCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,11 +107,13 @@ export function buildLoginSessionUser(options = {}) {
|
|||||||
const account = source.account || ''
|
const account = source.account || ''
|
||||||
const password = source.password || ''
|
const password = source.password || ''
|
||||||
const language = source.language || 'zh'
|
const language = source.language || 'zh'
|
||||||
|
const userCode = source.userCode || ''
|
||||||
|
|
||||||
if (latestRegistration) {
|
if (latestRegistration) {
|
||||||
return buildUserFromRegistrationApplication(latestRegistration, {
|
return buildUserFromRegistrationApplication(latestRegistration, {
|
||||||
password,
|
password,
|
||||||
language
|
language,
|
||||||
|
userCode
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +124,8 @@ export function buildLoginSessionUser(options = {}) {
|
|||||||
return normalizeUser({
|
return normalizeUser({
|
||||||
account,
|
account,
|
||||||
username: account,
|
username: account,
|
||||||
password
|
password,
|
||||||
|
userCode
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +136,7 @@ export function resolveLoginPreparation(options = {}) {
|
|||||||
const latestRegistration = source.latestRegistration || null
|
const latestRegistration = source.latestRegistration || null
|
||||||
const account = source.account || ''
|
const account = source.account || ''
|
||||||
const password = source.password || ''
|
const password = source.password || ''
|
||||||
|
const userCode = source.userCode || ''
|
||||||
|
|
||||||
if (!latestRegistration) {
|
if (!latestRegistration) {
|
||||||
return {
|
return {
|
||||||
@@ -132,7 +145,8 @@ export function resolveLoginPreparation(options = {}) {
|
|||||||
matchedLocalUser,
|
matchedLocalUser,
|
||||||
account,
|
account,
|
||||||
password,
|
password,
|
||||||
language
|
language,
|
||||||
|
userCode
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,8 +177,9 @@ export function resolveLoginPreparation(options = {}) {
|
|||||||
latestRegistration,
|
latestRegistration,
|
||||||
matchedLocalUser,
|
matchedLocalUser,
|
||||||
account,
|
account,
|
||||||
password,
|
password,
|
||||||
language
|
language,
|
||||||
|
userCode
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
const RESERVATION_FLOW_STEP = {
|
const RESERVATION_FLOW_STEP = {
|
||||||
INSPECTION: 10,
|
APPLICATION: 1,
|
||||||
BILL: 20,
|
INSPECTION: 2,
|
||||||
CONTRACT: 30,
|
CONTRACT: 3,
|
||||||
CHECKIN: 40
|
BILL: 4,
|
||||||
|
CHECKIN: 5
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLanguage(language) {
|
function getLanguage(language) {
|
||||||
@@ -89,7 +90,87 @@ export function getReservationCurrentNode(flow = {}) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return nodes.find(item => item.isCurrent) || nodes[nodes.length - 1]
|
const currentNodes = nodes.filter(item => item.isCurrent)
|
||||||
|
if (currentNodes.length) {
|
||||||
|
const currentCheckinNode = currentNodes.find(item => (
|
||||||
|
Number(item.step || 0) === RESERVATION_FLOW_STEP.CHECKIN
|
||||||
|
&& String(item.refNo || '').trim()
|
||||||
|
))
|
||||||
|
if (currentCheckinNode) {
|
||||||
|
return currentCheckinNode
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后端偶发会把前序节点也标记成 isCurrent,优先取排序最靠后的当前节点
|
||||||
|
return currentNodes[currentNodes.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkinNode = nodes.find(item => (
|
||||||
|
Number(item.step || 0) === RESERVATION_FLOW_STEP.CHECKIN
|
||||||
|
&& String(item.refNo || '').trim()
|
||||||
|
))
|
||||||
|
if (checkinNode) {
|
||||||
|
return checkinNode
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes[nodes.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getReservationCurrentFlow(flowList = []) {
|
||||||
|
const flows = normalizeArray(flowList)
|
||||||
|
.filter(item => item && typeof item === 'object')
|
||||||
|
.map(item => ({
|
||||||
|
flow: item,
|
||||||
|
currentNode: getReservationCurrentNode(item)
|
||||||
|
}))
|
||||||
|
.filter(item => item.currentNode)
|
||||||
|
|
||||||
|
if (!flows.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const compareNode = (target) => {
|
||||||
|
const node = target.currentNode || {}
|
||||||
|
const step = Number(node.step || 0)
|
||||||
|
const sort = Number(node.sort || 0)
|
||||||
|
const refNo = String(node.refNo || '').trim()
|
||||||
|
const isCurrent = !!node.isCurrent
|
||||||
|
const isCheckinWithRefNo = step === RESERVATION_FLOW_STEP.CHECKIN && !!refNo
|
||||||
|
return [
|
||||||
|
isCheckinWithRefNo ? 1 : 0,
|
||||||
|
isCurrent ? 1 : 0,
|
||||||
|
step,
|
||||||
|
sort
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return flows
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aScore = compareNode(a)
|
||||||
|
const bScore = compareNode(b)
|
||||||
|
for (let index = 0; index < aScore.length; index += 1) {
|
||||||
|
if (aScore[index] !== bScore[index]) {
|
||||||
|
return bScore[index] - aScore[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})[0].flow
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasEffectiveReservationFlow(flow = {}) {
|
||||||
|
const currentNode = getReservationCurrentNode(flow)
|
||||||
|
if (!currentNode) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const step = Number(currentNode.step || 0)
|
||||||
|
const refNo = String(currentNode.refNo || '').trim()
|
||||||
|
|
||||||
|
// 当前步骤为申请单(1)且没有 refNo 时,视为尚未提交预约单
|
||||||
|
if (step === 1 && !refNo) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return step > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildReservationFlowMap(flowList = []) {
|
export function buildReservationFlowMap(flowList = []) {
|
||||||
@@ -131,7 +212,7 @@ export function getReservationStepMeta(step, language = 'zh') {
|
|||||||
},
|
},
|
||||||
[RESERVATION_FLOW_STEP.BILL]: {
|
[RESERVATION_FLOW_STEP.BILL]: {
|
||||||
label: currentLanguage === 'en' ? 'Bill' : '账单',
|
label: currentLanguage === 'en' ? 'Bill' : '账单',
|
||||||
actionText: currentLanguage === 'en' ? 'Go to Bill' : '去缴费'
|
actionText: currentLanguage === 'en' ? 'Go to Payment' : '去支付'
|
||||||
},
|
},
|
||||||
[RESERVATION_FLOW_STEP.CONTRACT]: {
|
[RESERVATION_FLOW_STEP.CONTRACT]: {
|
||||||
label: currentLanguage === 'en' ? 'Contract' : '合同',
|
label: currentLanguage === 'en' ? 'Contract' : '合同',
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ const BANNER_TEXT_FALLBACK_MAP = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const VALUE_TEXT_FALLBACK_MAP = {
|
const VALUE_TEXT_FALLBACK_MAP = {
|
||||||
|
apartment: {
|
||||||
|
'绿居·和山国际公寓': 'Luju Heshan Intl. Apartments',
|
||||||
|
'和山国际公寓': 'Heshan Intl. Apartments',
|
||||||
|
'乾晖里': 'Qianhui Residence'
|
||||||
|
},
|
||||||
tags: {
|
tags: {
|
||||||
'押一付一': 'Monthly Deposit',
|
'押一付一': 'Monthly Deposit',
|
||||||
'大型超市': 'Large Supermarket',
|
'大型超市': 'Large Supermarket',
|
||||||
@@ -148,6 +153,10 @@ export function localizeHousingTag(tag = '', language = getCurrentLanguage()) {
|
|||||||
return localizeKnownValue('tags', tag, language)
|
return localizeKnownValue('tags', tag, language)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function localizeApartmentName(name = '', language = getCurrentLanguage()) {
|
||||||
|
return localizeKnownValue('apartment', name, language)
|
||||||
|
}
|
||||||
|
|
||||||
export function localizePayMethodText(payMethod = '', language = getCurrentLanguage()) {
|
export function localizePayMethodText(payMethod = '', language = getCurrentLanguage()) {
|
||||||
return localizeKnownValue('payMethod', payMethod, language)
|
return localizeKnownValue('payMethod', payMethod, language)
|
||||||
}
|
}
|
||||||
@@ -330,7 +339,19 @@ export function normalizeBannerItem(item = {}, index = 0) {
|
|||||||
|
|
||||||
export function normalizeImageList(item = {}) {
|
export function normalizeImageList(item = {}) {
|
||||||
const candidates = []
|
const candidates = []
|
||||||
const listFields = [item.images, item.imageUrls, item.pictures, item.pictureList]
|
const listFields = [
|
||||||
|
item.images,
|
||||||
|
item.imageUrls,
|
||||||
|
item.pictures,
|
||||||
|
item.pictureList,
|
||||||
|
item.bannerImages,
|
||||||
|
item.carouselImages,
|
||||||
|
item.imageList,
|
||||||
|
item.photos,
|
||||||
|
item.photoList,
|
||||||
|
item.album,
|
||||||
|
item.albums
|
||||||
|
]
|
||||||
|
|
||||||
listFields.forEach((field) => {
|
listFields.forEach((field) => {
|
||||||
if (Array.isArray(field)) {
|
if (Array.isArray(field)) {
|
||||||
@@ -343,7 +364,19 @@ export function normalizeImageList(item = {}) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
;[item.imageUrl, item.image, item.cover, item.picture].forEach((image) => {
|
;[
|
||||||
|
item.imageUrl,
|
||||||
|
item.image,
|
||||||
|
item.cover,
|
||||||
|
item.coverUrl,
|
||||||
|
item.picture,
|
||||||
|
item.poster,
|
||||||
|
item.posterUrl,
|
||||||
|
item.banner,
|
||||||
|
item.bannerUrl,
|
||||||
|
item.areaImage,
|
||||||
|
item.areaImageUrl
|
||||||
|
].forEach((image) => {
|
||||||
const normalized = normalizeBannerImage(image)
|
const normalized = normalizeBannerImage(image)
|
||||||
if (normalized) {
|
if (normalized) {
|
||||||
candidates.push(normalized)
|
candidates.push(normalized)
|
||||||
|
|||||||
@@ -113,7 +113,8 @@ function pickFirstValue(...values) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildPendingContractQuery(order = {}) {
|
function buildPendingContractQuery(order = {}) {
|
||||||
const orderNo = pickFirstValue(order.orderNo, order.no, order.code, order.OrderNo)
|
const safeOrder = order || {}
|
||||||
|
const orderNo = pickFirstValue(safeOrder.orderNo, safeOrder.no, safeOrder.code, safeOrder.OrderNo)
|
||||||
if (!orderNo) {
|
if (!orderNo) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -124,11 +125,13 @@ function buildPendingContractQuery(order = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildEffectiveContractQuery(order = {}) {
|
function buildEffectiveContractQuery(order = {}) {
|
||||||
const orderNo = pickFirstValue(order.orderNo, order.no, order.code, order.OrderNo)
|
const safeOrder = order || {}
|
||||||
|
const orderNo = pickFirstValue(safeOrder.orderNo, safeOrder.no, safeOrder.code, safeOrder.OrderNo)
|
||||||
if (!orderNo) {
|
if (!orderNo) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
ProviderType: 'WechatMiniProgram',
|
||||||
OrderNo: orderNo
|
OrderNo: orderNo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
utils/todo-route.js
Normal file
64
utils/todo-route.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
function ensureArray(value) {
|
||||||
|
return Array.isArray(value) ? value : []
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickFirstText() {
|
||||||
|
for (let index = 0; index < arguments.length; index += 1) {
|
||||||
|
const value = arguments[index]
|
||||||
|
if (value === null || value === undefined) continue
|
||||||
|
const text = String(value).trim()
|
||||||
|
if (text) return text
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeNumber(value, fallback = 0) {
|
||||||
|
const num = Number(value)
|
||||||
|
return Number.isFinite(num) ? num : fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTodoEntry(raw) {
|
||||||
|
if (raw && typeof raw === 'object') {
|
||||||
|
return {
|
||||||
|
...raw,
|
||||||
|
_todoCode: pickFirstText(raw.code, raw.no, raw.contractNo, raw.billNo, raw.orderNo, raw.referenceTypeNo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const text = pickFirstText(raw)
|
||||||
|
return text ? { code: text, _todoCode: text } : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTodoEntries(item) {
|
||||||
|
const sourceList = [item && item.items, item && item.codes, item && item.list, item && item.data]
|
||||||
|
for (let index = 0; index < sourceList.length; index += 1) {
|
||||||
|
const current = sourceList[index]
|
||||||
|
if (!Array.isArray(current)) continue
|
||||||
|
return current.map(entry => normalizeTodoEntry(entry)).filter(Boolean)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractTodoRows(response) {
|
||||||
|
const payload = response && response.data !== undefined ? response.data : response
|
||||||
|
if (Array.isArray(payload)) return payload
|
||||||
|
if (!payload || typeof payload !== 'object') return []
|
||||||
|
if (Array.isArray(payload.data)) return payload.data
|
||||||
|
if (payload.data && Array.isArray(payload.data.data)) return payload.data.data
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeTodoList(rows = []) {
|
||||||
|
return ensureArray(rows).map((item) => {
|
||||||
|
const entries = extractTodoEntries(item)
|
||||||
|
const count = Math.max(normalizeNumber(item && item.count, entries.length), entries.length)
|
||||||
|
return {
|
||||||
|
todoType: String(item && item.todoType !== undefined ? item.todoType : ''),
|
||||||
|
todoTypeDesc: pickFirstText(item && item.todoTypeDesc),
|
||||||
|
count,
|
||||||
|
entries,
|
||||||
|
codes: entries
|
||||||
|
.map(entry => ({ code: pickFirstText(entry && entry._todoCode) }))
|
||||||
|
.filter(entry => entry.code)
|
||||||
|
}
|
||||||
|
}).filter(item => item.count > 0 || item.entries.length > 0 || item.todoTypeDesc)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user