feat: 重构核心租住流程页面与交互体验
统一注册、看房、申请与入住等核心页面的视觉和流程衔接,补齐支付、签约与状态流转闭环,提升移动端操作连贯性。
This commit is contained in:
@@ -79,24 +79,189 @@
|
||||
<view class="tn-text-bold tn-text-xl" style="color: $brand-primary;">$500.00</view>
|
||||
</view>
|
||||
<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>
|
||||
|
||||
<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、缴纳线上定金可锁定房源并预约专属看房,平台收取定金后该房源临时下架,不再接待其他客户看房。</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">2、请于约定时间完成实地看房,并在看房结束后 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">3、如因租客个人原因取消看房、看房后不愿承租房屋或拒绝签署租赁合同,定金不予退还。</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">4、若因我方房源信息虚假、房屋无法正常交付等我方过错,造成无法看房或签约,定金将全额按原支付路径退还。</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">5、待双方正式签署租赁合同后,该定金可直接抵扣首期租金。</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">6、您支付定金,即代表已完整知晓房源全部信息,并自愿接受本须知全部条款约束。</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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { resolveExistingBookingNavigationTarget } from '../../utils/booking-entry'
|
||||
import { getSessionSnapshot } from '../../utils/app-state'
|
||||
import { syncLoginStayState } from '../../utils/session-sync'
|
||||
import { accountQueryBill, accountQueryBills, applicationFormDetail, paymentBillPay, paymentPayingBills, submitApplicationForm } from '../../api/index.js'
|
||||
import { saveActiveApplicationCode, saveHomePageTabId } from '../../utils/app-state'
|
||||
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 {
|
||||
data() {
|
||||
return {
|
||||
isSubmitting: false,
|
||||
applyData: null,
|
||||
showDepositNotice: false,
|
||||
hasReadNotice: false,
|
||||
isClosingDepositNoticeForPayment: false,
|
||||
depositNoticeReadyAt: 0,
|
||||
pendingDepositPaymentContext: null,
|
||||
form: {
|
||||
startDate: '',
|
||||
endDate: ''
|
||||
@@ -163,44 +328,339 @@ export default {
|
||||
console.error('Failed to parse apply data', e);
|
||||
})
|
||||
}
|
||||
this.redirectIfBookingUnavailable()
|
||||
},
|
||||
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) {
|
||||
this.form.startDate = e.detail.value;
|
||||
},
|
||||
onEndDateChange(e) {
|
||||
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() {
|
||||
if (this.isSubmitting) return;
|
||||
if (!this.form.startDate || !this.form.endDate) {
|
||||
@@ -213,24 +673,39 @@ export default {
|
||||
}
|
||||
|
||||
this.isSubmitting = true;
|
||||
const orderData = {
|
||||
...this.applyData,
|
||||
startDate: this.form.startDate,
|
||||
endDate: this.form.endDate,
|
||||
stayNights: this.stayNights,
|
||||
sourceType: 'booking-intent'
|
||||
};
|
||||
|
||||
let roomDataStr = encodeURIComponent(JSON.stringify(orderData));
|
||||
|
||||
uni.showLoading({ title: this.language === 'zh' ? '正在生成订单...' : 'Creating Order...' });
|
||||
setTimeout(() => {
|
||||
uni.hideLoading();
|
||||
this.isSubmitting = false;
|
||||
uni.redirectTo({
|
||||
url: `/pkgBooking/pay/pay?roomData=${roomDataStr}`
|
||||
uni.showLoading({ title: this.language === 'zh' ? '提交中...' : 'Submitting...' });
|
||||
|
||||
const payload = this.buildSubmitPayload()
|
||||
return submitApplicationForm(payload, this.language)
|
||||
.then(async (response) => {
|
||||
const applicationCode = extractSubmitApplicationCode(response)
|
||||
if (!applicationCode) {
|
||||
throw new Error(this.$tr('申请单编号缺失'))
|
||||
}
|
||||
saveActiveApplicationCode(applicationCode)
|
||||
const paymentContext = await this.resolvePaymentPreview(applicationCode).catch(() => ({
|
||||
referenceTypeNo: applicationCode,
|
||||
billNo: '',
|
||||
paymentNo: '',
|
||||
pendingPaymentOrder: null
|
||||
}))
|
||||
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 +822,92 @@ export default {
|
||||
opacity: 0.6;
|
||||
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>
|
||||
|
||||
@@ -1,31 +1,51 @@
|
||||
<template>
|
||||
<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>
|
||||
</tn-nav-bar>
|
||||
|
||||
<view class="content-wrapper" :style="{ paddingTop: vuex_custom_bar_height + 'px' }">
|
||||
|
||||
<!-- 房型/区域信息 -->
|
||||
<view class="info-card tn-flex tn-flex-row-between tn-flex-col-center" v-if="displayInfoTitle">
|
||||
<view class="info-left">
|
||||
<view class="info-title">{{ displayInfoTitle }}</view>
|
||||
<view class="info-desc">{{ displayInfoDesc }}</view>
|
||||
</view>
|
||||
<view class="info-icon">
|
||||
<text class="tn-icon-company"></text>
|
||||
<!-- 顶部信息卡片 -->
|
||||
<view class="hero-card" v-if="displayInfoTitle">
|
||||
<view class="hero-glow"></view>
|
||||
<view class="hero-inner tn-flex tn-flex-row-between tn-flex-col-center">
|
||||
<view class="hero-content">
|
||||
<view class="hero-tag">
|
||||
<text class="tn-icon-company hero-tag-icon"></text>
|
||||
<text class="hero-tag-text">{{ $tr('专属管家') }}</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 class="hero-accent"></view>
|
||||
</view>
|
||||
|
||||
<!-- 看房时间卡片 -->
|
||||
<view class="form-card">
|
||||
<view class="card-header tn-flex tn-flex-row-between tn-flex-col-bottom">
|
||||
<view class="header-left tn-flex tn-flex-col-bottom">
|
||||
<text class="card-title">{{ $tr('选择看房时间') }}</text>
|
||||
<text class="card-subtitle">{{ $tr('请选择您方便的时间段') }}</text>
|
||||
<view class="card-header tn-flex tn-flex-col-bottom">
|
||||
<view class="header-icon header-icon--primary">
|
||||
<text class="tn-icon-time"></text>
|
||||
</view>
|
||||
<view class="header-titles">
|
||||
<view class="card-title">{{ $tr('选择看房时间') }}</view>
|
||||
<view class="card-subtitle">{{ $tr('请选择您方便的时间段') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<view class="section-label">{{ $tr('预约日期') }}</view>
|
||||
<scroll-view class="date-scroll" scroll-x :show-scrollbar="false">
|
||||
<view class="date-flex">
|
||||
<view
|
||||
@@ -37,10 +57,12 @@
|
||||
>
|
||||
<text class="date-week">{{ item.week }}</text>
|
||||
<text class="date-day">{{ item.label }}</text>
|
||||
<view class="date-indicator"></view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="section-label section-label--time">{{ $tr('可约时段') }}</view>
|
||||
<scroll-view class="time-scroll" scroll-x :show-scrollbar="false">
|
||||
<view class="time-flex">
|
||||
<view
|
||||
@@ -50,23 +72,43 @@
|
||||
:class="{ 'time-slot--active': currentTimeIndex === index }"
|
||||
@click="selectTimeSlot(index)"
|
||||
>
|
||||
{{ item }}
|
||||
<text class="time-slot-text">{{ item }}</text>
|
||||
</view>
|
||||
</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 class="form-card">
|
||||
<view class="card-header">
|
||||
<view class="header-left tn-flex tn-flex-col-bottom">
|
||||
<text class="card-title">{{ $tr('您的租住需求') }}</text>
|
||||
<text class="card-subtitle">{{ $tr('帮助管家更好地为您推荐房源') }}</text>
|
||||
<view class="card-header tn-flex tn-flex-col-bottom">
|
||||
<view class="header-icon header-icon--luxury">
|
||||
<text class="tn-icon-like"></text>
|
||||
</view>
|
||||
<view class="header-titles">
|
||||
<view class="card-title">{{ $tr('您的租住需求') }}</view>
|
||||
<view class="card-subtitle">{{ $tr('帮助管家更好地为您推荐房源') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<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
|
||||
v-model="formData.intendedRoomTypeCode"
|
||||
:initial-area-code="formData.intendedAreaCode"
|
||||
@@ -75,7 +117,10 @@
|
||||
</view>
|
||||
|
||||
<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
|
||||
v-for="(item, index) in durationOptions"
|
||||
@@ -90,9 +135,14 @@
|
||||
</view>
|
||||
|
||||
<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">
|
||||
<text class="tn-icon-money input-icon"></text>
|
||||
<view class="input-icon-wrap">
|
||||
<text class="tn-icon-money input-icon"></text>
|
||||
</view>
|
||||
<input
|
||||
class="custom-input"
|
||||
v-model="formData.budget"
|
||||
@@ -103,10 +153,15 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="field-item">
|
||||
<view class="field-label">{{ $tr('工作/通勤地点') }}</view>
|
||||
<view class="field-item field-item--last">
|
||||
<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">
|
||||
<text class="tn-icon-location input-icon"></text>
|
||||
<view class="input-icon-wrap">
|
||||
<text class="tn-icon-location input-icon"></text>
|
||||
</view>
|
||||
<input
|
||||
class="custom-input"
|
||||
v-model="formData.workplace"
|
||||
@@ -114,16 +169,26 @@
|
||||
:placeholder="$tr('请输入主要通勤地点')"
|
||||
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>
|
||||
|
||||
<!-- 底部占位,避免被 fixed 按钮遮挡 -->
|
||||
<view class="footer-spacer"></view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作区 -->
|
||||
<view class="footer-bar tn-safe-area-inset-bottom">
|
||||
<view class="submit-btn" @click="handleSubmit">
|
||||
<text>{{ $tr('提交预约') }}</text>
|
||||
<view class="footer-inner">
|
||||
<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>
|
||||
@@ -191,6 +256,9 @@ export default {
|
||||
language() {
|
||||
return this.$getLanguage()
|
||||
},
|
||||
navBackgroundColor() {
|
||||
return this.scrollTop > 20 ? '#F8FAFF' : 'transparent'
|
||||
},
|
||||
displayInfoTitle() {
|
||||
if (!this.sourceData) {
|
||||
return ''
|
||||
@@ -214,6 +282,20 @@ export default {
|
||||
: (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(' · ')
|
||||
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() {
|
||||
@@ -236,9 +318,13 @@ export default {
|
||||
workplace: ''
|
||||
},
|
||||
sourceData: null,
|
||||
isSubmitting: false
|
||||
isSubmitting: false,
|
||||
scrollTop: 0
|
||||
}
|
||||
},
|
||||
onPageScroll(event) {
|
||||
this.scrollTop = (event && event.scrollTop) || 0
|
||||
},
|
||||
onLoad(options) {
|
||||
this.syncDurationOptions()
|
||||
this.generateDates()
|
||||
@@ -304,13 +390,13 @@ export default {
|
||||
this.$tr('周五'),
|
||||
this.$tr('周六')
|
||||
]
|
||||
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const d = new Date(today)
|
||||
d.setDate(today.getDate() + i)
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
|
||||
|
||||
let weekStr = weeks[d.getDay()]
|
||||
if (i === 0) weekStr = this.$tr('今天');
|
||||
else if (i === 1) weekStr = this.$tr('明天');
|
||||
@@ -350,6 +436,26 @@ export default {
|
||||
selectDuration(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() {
|
||||
if (this.isSubmitting) {
|
||||
return
|
||||
@@ -391,8 +497,8 @@ export default {
|
||||
showCancel: false,
|
||||
confirmColor: '#01BEFF',
|
||||
success: () => {
|
||||
uni.redirectTo({
|
||||
url: '/pkgService/my_application/my_application'
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index?pageId=stay-service'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -414,174 +520,387 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 设计令牌 */
|
||||
$hero-bg-start: #0A1628;
|
||||
$hero-bg-end: #1E3A5F;
|
||||
$luxury-gold: #D8B56A;
|
||||
$luxury-gold-soft: rgba(216, 181, 106, 0.12);
|
||||
$surface: #FFFFFF;
|
||||
$bg-page: #F5F9FF;
|
||||
$text-primary: #1A2233;
|
||||
$text-secondary: #6A7A99;
|
||||
$text-tertiary: #A1B0C4;
|
||||
$border-soft: rgba($brand-primary, 0.08);
|
||||
$shadow-card: 0 16rpx 48rpx rgba(26, 34, 51, 0.06);
|
||||
$shadow-active: 0 8rpx 24rpx rgba($brand-primary, 0.22);
|
||||
|
||||
.book-viewing-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
|
||||
background: linear-gradient(180deg, #EAF4FF 0%, $bg-page 30%, #FFFFFF 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #1D2129;
|
||||
font-weight: 700;
|
||||
color: $text-primary;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
|
||||
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* 顶部信息卡片 */
|
||||
.info-card {
|
||||
margin: 30rpx;
|
||||
padding: 40rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(135deg, #01BEFF 0%, #0099FF 100%);
|
||||
box-shadow: 0 16rpx 32rpx rgba(1, 190, 255, 0.2);
|
||||
color: #ffffff;
|
||||
/* 顶部信息卡片(Hero) */
|
||||
.hero-card {
|
||||
position: relative;
|
||||
margin: 24rpx 30rpx 30rpx;
|
||||
border-radius: 36rpx;
|
||||
background: linear-gradient(135deg, $hero-bg-start 0%, $hero-bg-end 100%);
|
||||
box-shadow: 0 24rpx 60rpx rgba(10, 22, 40, 0.22);
|
||||
overflow: hidden;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.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;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.info-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 12rpx;
|
||||
letter-spacing: 2rpx;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
.hero-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8rpx 18rpx;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
backdrop-filter: blur(8rpx);
|
||||
}
|
||||
|
||||
.info-desc {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.9;
|
||||
.hero-tag-icon {
|
||||
font-size: 22rpx;
|
||||
color: $luxury-gold;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.hero-tag-text {
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 38rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
margin-bottom: 16rpx;
|
||||
letter-spacing: 1rpx;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
font-size: 80rpx;
|
||||
opacity: 0.2;
|
||||
margin-left: 20rpx;
|
||||
.hero-desc {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
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 {
|
||||
margin: 0 30rpx 30rpx;
|
||||
padding: 40rpx 30rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.03);
|
||||
padding: 36rpx 32rpx;
|
||||
border-radius: 32rpx;
|
||||
background: $surface;
|
||||
box-shadow: $shadow-card;
|
||||
border: 1rpx solid $border-soft;
|
||||
}
|
||||
|
||||
.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 {
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #1D2129;
|
||||
margin-right: 16rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: $text-primary;
|
||||
margin-bottom: 6rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
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 {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.date-flex {
|
||||
display: inline-flex;
|
||||
gap: 20rpx;
|
||||
padding-bottom: 10rpx;
|
||||
gap: 18rpx;
|
||||
padding: 6rpx 4rpx 16rpx;
|
||||
}
|
||||
|
||||
.date-item {
|
||||
width: 110rpx;
|
||||
height: 130rpx;
|
||||
width: 120rpx;
|
||||
height: 144rpx;
|
||||
background: #F4F8FF;
|
||||
border-radius: 16rpx;
|
||||
border-radius: 24rpx;
|
||||
flex-shrink: 0;
|
||||
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 {
|
||||
background: #EAF0FF;
|
||||
border-color: #01BEFF;
|
||||
background: linear-gradient(135deg, $brand-primary 0%, #0099FF 100%);
|
||||
border-color: rgba($brand-primary, 0.35);
|
||||
box-shadow: $shadow-active;
|
||||
transform: translateY(-4rpx);
|
||||
}
|
||||
|
||||
.date-week {
|
||||
font-size: 24rpx;
|
||||
color: #86909C;
|
||||
margin-bottom: 8rpx;
|
||||
transition: all 0.3s ease;
|
||||
color: $text-secondary;
|
||||
margin-bottom: 10rpx;
|
||||
transition: color 0.25s ease;
|
||||
}
|
||||
|
||||
.date-item--active .date-week {
|
||||
color: #01BEFF;
|
||||
font-weight: bold;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.date-day {
|
||||
font-size: 28rpx;
|
||||
color: #1D2129;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 30rpx;
|
||||
color: $text-primary;
|
||||
font-weight: 700;
|
||||
transition: color 0.25s ease;
|
||||
}
|
||||
|
||||
.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 {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.time-flex {
|
||||
display: inline-flex;
|
||||
gap: 20rpx;
|
||||
padding-bottom: 8rpx;
|
||||
gap: 18rpx;
|
||||
padding: 6rpx 4rpx 16rpx;
|
||||
}
|
||||
|
||||
.time-slot {
|
||||
width: 220rpx;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
text-align: center;
|
||||
width: 200rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #F4F8FF;
|
||||
border-radius: 12rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 26rpx;
|
||||
color: #4E5969;
|
||||
color: $text-secondary;
|
||||
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;
|
||||
}
|
||||
|
||||
.time-slot--active {
|
||||
background: #01BEFF;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 8rpx 16rpx rgba(1, 190, 255, 0.2);
|
||||
background: linear-gradient(135deg, $brand-primary 0%, #0099FF 100%);
|
||||
color: #FFFFFF;
|
||||
font-weight: 600;
|
||||
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;
|
||||
}
|
||||
|
||||
.field-item:last-child {
|
||||
.field-item--last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 28rpx;
|
||||
color: #1D2129;
|
||||
font-weight: bold;
|
||||
margin-bottom: 24rpx;
|
||||
font-weight: 700;
|
||||
color: $text-primary;
|
||||
margin-bottom: 22rpx;
|
||||
}
|
||||
|
||||
.field-dot {
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
border-radius: 50%;
|
||||
margin-right: 12rpx;
|
||||
|
||||
&--primary {
|
||||
background: $brand-primary;
|
||||
box-shadow: 0 0 10rpx rgba($brand-primary, 0.45);
|
||||
}
|
||||
|
||||
&--luxury {
|
||||
background: $luxury-gold;
|
||||
box-shadow: 0 0 10rpx rgba($luxury-gold, 0.45);
|
||||
}
|
||||
|
||||
&--muted {
|
||||
background: $text-tertiary;
|
||||
}
|
||||
}
|
||||
|
||||
/* 标签选择 */
|
||||
@@ -608,42 +950,96 @@ export default {
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
padding: 0 32rpx;
|
||||
height: 68rpx;
|
||||
line-height: 68rpx;
|
||||
padding: 0 34rpx;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
background: #F4F8FF;
|
||||
border-radius: 34rpx;
|
||||
border-radius: 36rpx;
|
||||
font-size: 26rpx;
|
||||
color: #4E5969;
|
||||
color: $text-secondary;
|
||||
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 {
|
||||
background: #EAF0FF;
|
||||
border-color: #01BEFF;
|
||||
color: #01BEFF;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(135deg, rgba($brand-primary, 0.1) 0%, rgba($brand-primary, 0.06) 100%);
|
||||
border-color: rgba($brand-primary, 0.35);
|
||||
color: $brand-primary;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 6rpx 16rpx rgba($brand-primary, 0.12);
|
||||
}
|
||||
|
||||
/* 输入框 */
|
||||
.input-box {
|
||||
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;
|
||||
height: 88rpx;
|
||||
padding: 0 30rpx;
|
||||
background: #FFFFFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 18rpx;
|
||||
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
font-size: 36rpx;
|
||||
color: #A1B0C4;
|
||||
margin-right: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.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 {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #1D2129;
|
||||
color: $text-primary;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.footer-spacer {
|
||||
height: 180rpx;
|
||||
}
|
||||
|
||||
/* 底部按钮 */
|
||||
@@ -652,30 +1048,61 @@ export default {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
padding: 24rpx 40rpx;
|
||||
box-shadow: 0 -4rpx 24rpx rgba(0, 0, 0, 0.04);
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
backdrop-filter: blur(20rpx);
|
||||
padding: 24rpx 40rpx calc(24rpx + env(safe-area-inset-bottom));
|
||||
box-shadow: 0 -8rpx 32rpx rgba(26, 34, 51, 0.06);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.footer-inner {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
background: linear-gradient(135deg, #01BEFF 0%, #0099FF 100%);
|
||||
border-radius: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, #A1B0C4 0%, #86909C 100%);
|
||||
color: #FFFFFF;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2rpx;
|
||||
box-shadow: 0 12rpx 24rpx rgba(1, 190, 255, 0.25);
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 8rpx 24rpx rgba(134, 144, 156, 0.2);
|
||||
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.submit-btn:active {
|
||||
.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);
|
||||
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>
|
||||
|
||||
@@ -1,40 +1,53 @@
|
||||
<template>
|
||||
<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-label">{{ $tr('意向房型') }}</view>
|
||||
<view class="selected-name tn-text-ellipsis" :class="{ 'is-empty': !selectedRoomType }">
|
||||
{{ selectedRoomType ? getRoomTypeDisplayName(selectedRoomType) : $tr('请选择房型') }}
|
||||
{{ selectedRoomType ? getRoomTypeDisplayName(selectedRoomType) : $tr('请选择意向房型') }}
|
||||
</view>
|
||||
<view class="selected-area tn-text-ellipsis" v-if="selectedArea">
|
||||
<text class="tn-icon-location area-icon"></text>
|
||||
{{ getAreaDisplayName(selectedArea) }}
|
||||
</view>
|
||||
</view>
|
||||
<text class="tn-icon-right tn-color-gray"></text>
|
||||
<view class="selector-arrow">
|
||||
<text class="tn-icon-right"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<tn-popup v-model="showPopup" mode="bottom" length="80%" :borderRadius="32" :closeBtn="true">
|
||||
<view class="popup-content">
|
||||
<view class="popup-header">
|
||||
<text class="popup-title">{{ $tr('选择意向房型') }}</text>
|
||||
<view class="popup-title">{{ $tr('选择意向房型') }}</view>
|
||||
<view class="popup-subtitle">{{ $tr('选择您心仪的房型与区域') }}</view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="area-list-scroll">
|
||||
<view v-if="isLoading" class="loading-state tn-padding-xl tn-text-center">
|
||||
<tn-loading mode="circle"></tn-loading>
|
||||
<view class="tn-margin-top-sm tn-color-gray">{{ $tr('房型加载中...') }}</view>
|
||||
<tn-loading mode="circle" :size="48"></tn-loading>
|
||||
<view class="tn-margin-top tn-color-gray">{{ $tr('房型加载中...') }}</view>
|
||||
</view>
|
||||
|
||||
<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 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="tn-flex tn-flex-col-center">
|
||||
<text class="area-dot"></text>
|
||||
<text>{{ getAreaDisplayName(area) }}</text>
|
||||
<view class="area-title-left tn-flex tn-flex-col-center">
|
||||
<view class="area-icon-wrap">
|
||||
<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>
|
||||
<text :class="collapsedAreas[index] ? 'tn-icon-down' : 'tn-icon-up'" class="tn-color-gray"></text>
|
||||
</view>
|
||||
<view class="room-type-grid" v-show="!collapsedAreas[index]">
|
||||
<view
|
||||
@@ -45,7 +58,15 @@
|
||||
@click="handleSelect(area, room)"
|
||||
>
|
||||
<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>
|
||||
@@ -59,25 +80,6 @@
|
||||
import { areaPagination } from '@/api/index.js'
|
||||
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 {
|
||||
name: 'BookViewingRoomTypeSelector',
|
||||
props: {
|
||||
@@ -230,17 +232,30 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$luxury-gold: #D8B56A;
|
||||
$text-primary: #1A2233;
|
||||
$text-secondary: #6A7A99;
|
||||
$text-tertiary: #A1B0C4;
|
||||
$surface: #FFFFFF;
|
||||
$bg-popup: #F8FAFF;
|
||||
|
||||
.room-type-selector {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selector-header {
|
||||
background: #FFFFFF;
|
||||
.selector-card {
|
||||
background: $surface;
|
||||
border-radius: 20rpx;
|
||||
padding: 26rpx 32rpx;
|
||||
border: 1rpx solid rgba($brand-primary, 0.15);
|
||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
||||
min-height: 100rpx;
|
||||
padding: 26rpx 30rpx;
|
||||
border: 2rpx solid rgba($brand-primary, 0.12);
|
||||
box-shadow: 0 6rpx 20rpx rgba($brand-primary, 0.05);
|
||||
min-height: 120rpx;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.selector-card:active {
|
||||
background: #FAFCFF;
|
||||
border-color: rgba($brand-primary, 0.25);
|
||||
}
|
||||
|
||||
.selected-info {
|
||||
@@ -248,72 +263,175 @@ export default {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.selected-label {
|
||||
font-size: 22rpx;
|
||||
color: $text-tertiary;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.selected-name {
|
||||
font-size: 28rpx;
|
||||
color: #1A2233;
|
||||
font-weight: 600;
|
||||
font-size: 30rpx;
|
||||
color: $text-primary;
|
||||
font-weight: 700;
|
||||
|
||||
&.is-empty {
|
||||
color: #A0B0C0;
|
||||
color: $text-tertiary;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.selected-area {
|
||||
font-size: 24rpx;
|
||||
color: $text-secondary;
|
||||
margin-top: 10rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.area-icon {
|
||||
font-size: 22rpx;
|
||||
color: #6A7A99;
|
||||
margin-top: 6rpx;
|
||||
margin-right: 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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #F8FAFF;
|
||||
background: $bg-popup;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
padding: 40rpx 30rpx 20rpx;
|
||||
padding: 40rpx 30rpx 28rpx;
|
||||
text-align: center;
|
||||
border-bottom: 1px dashed rgba($brand-primary, 0.15);
|
||||
background: #FFFFFF;
|
||||
background: $surface;
|
||||
border-bottom: 1rpx solid rgba($brand-primary, 0.08);
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 32rpx;
|
||||
font-size: 34rpx;
|
||||
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 {
|
||||
flex: 1;
|
||||
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 {
|
||||
margin-bottom: 30rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.04);
|
||||
border: 1rpx solid rgba($brand-primary, 0.05);
|
||||
margin-bottom: 28rpx;
|
||||
background: $surface;
|
||||
border-radius: 24rpx;
|
||||
padding: 28rpx;
|
||||
box-shadow: 0 8rpx 28rpx rgba(26, 34, 51, 0.04);
|
||||
border: 1rpx solid rgba($brand-primary, 0.06);
|
||||
}
|
||||
|
||||
.area-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1A2233;
|
||||
margin-bottom: 20rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.area-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
.area-title-left {
|
||||
flex: 1;
|
||||
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%;
|
||||
background: $brand-primary;
|
||||
margin-right: 12rpx;
|
||||
box-shadow: 0 0 8rpx rgba($brand-primary, 0.4);
|
||||
background: #F4F8FF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
text {
|
||||
font-size: 24rpx;
|
||||
color: $text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.room-type-grid {
|
||||
@@ -323,17 +441,18 @@ export default {
|
||||
}
|
||||
|
||||
.room-type-item {
|
||||
padding: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #FFFFFF;
|
||||
border: 1rpx solid rgba($brand-primary, 0.15);
|
||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
padding: 24rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #FAFCFF;
|
||||
border: 2rpx solid rgba($brand-primary, 0.1);
|
||||
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
overflow: hidden;
|
||||
|
||||
&.is-active {
|
||||
background: #FFFFFF;
|
||||
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 {
|
||||
color: $brand-primary;
|
||||
@@ -342,18 +461,66 @@ export default {
|
||||
.room-price {
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.active-mark {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.room-name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #1A2233;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: $text-primary;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.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;
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user