1109 lines
29 KiB
Vue
1109 lines
29 KiB
Vue
<template>
|
||
<view class="book-viewing-page tn-safe-area-inset-bottom">
|
||
<tn-nav-bar
|
||
:isBack="true"
|
||
:backTitle="$tr('返回')"
|
||
:backgroundColor="navBackgroundColor"
|
||
fontColor="#1A2233"
|
||
:bottomShadow="false"
|
||
:fixed="true"
|
||
>
|
||
<view class="nav-title">{{ $tr('预约现场带看') }}</view>
|
||
</tn-nav-bar>
|
||
|
||
<view class="content-wrapper" :style="{ paddingTop: vuex_custom_bar_height + 'px' }">
|
||
<!-- 顶部信息卡片 -->
|
||
<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-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
|
||
v-for="(item, index) in dateList"
|
||
:key="index"
|
||
class="date-item tn-flex tn-flex-direction-column tn-flex-col-center tn-flex-row-center"
|
||
:class="{ 'date-item--active': currentDateIndex === index }"
|
||
@click="selectDate(index)"
|
||
>
|
||
<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
|
||
v-for="(item, index) in timeSlots"
|
||
:key="index"
|
||
class="time-slot"
|
||
:class="{ 'time-slot--active': currentTimeIndex === index }"
|
||
@click="selectTimeSlot(index)"
|
||
>
|
||
<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 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">
|
||
<text class="field-dot field-dot--primary"></text>
|
||
<text>{{ $tr('意向房型') }}</text>
|
||
</view>
|
||
<room-type-selector
|
||
v-model="formData.intendedRoomTypeCode"
|
||
:initial-area-code="formData.intendedAreaCode"
|
||
@change="handleRoomTypeChange"
|
||
/>
|
||
</view>
|
||
|
||
<view class="field-item">
|
||
<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"
|
||
:key="index"
|
||
class="tag-item"
|
||
:class="{ 'tag-item--active': formData.duration === item }"
|
||
@click="selectDuration(item)"
|
||
>
|
||
{{ item }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="field-item">
|
||
<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-icon-wrap">
|
||
<text class="tn-icon-money input-icon"></text>
|
||
</view>
|
||
<input
|
||
class="custom-input"
|
||
v-model="formData.budget"
|
||
type="digit"
|
||
:placeholder="$tr('请输入您的心理价位')"
|
||
placeholder-style="color: #A1B0C4;"
|
||
/>
|
||
</view>
|
||
</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">
|
||
<view class="input-icon-wrap">
|
||
<text class="tn-icon-location input-icon"></text>
|
||
</view>
|
||
<input
|
||
class="custom-input"
|
||
v-model="formData.workplace"
|
||
type="text"
|
||
: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="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>
|
||
</template>
|
||
|
||
<script>
|
||
import { submitViewingApplicationForm } from '@/api/index.js'
|
||
import { resolveExistingBookingNavigationTarget } from '../../utils/booking-entry'
|
||
import { getSessionSnapshot } from '../../utils/app-state'
|
||
import { syncLoginStayState } from '../../utils/session-sync'
|
||
import { compactEnglishText, localizePayMethodText } from '../../utils/select-room-normalizers'
|
||
import RoomTypeSelector from './components/RoomTypeSelector.vue'
|
||
|
||
function buildViewingDateTime(date = '', time = '', isEnd = false) {
|
||
if (!date || !time) {
|
||
return null
|
||
}
|
||
const range = String(time).split('-')
|
||
const selected = isEnd ? range[1] : range[0]
|
||
if (!selected) {
|
||
return null
|
||
}
|
||
return `${date}T${selected}:00`
|
||
}
|
||
|
||
function formatDatePart(value) {
|
||
return String(value).padStart(2, '0')
|
||
}
|
||
|
||
function shiftDate(date = '', days = 0) {
|
||
if (!date) {
|
||
return ''
|
||
}
|
||
const next = new Date(`${date}T00:00:00`)
|
||
if (!Number.isFinite(next.getTime())) {
|
||
return ''
|
||
}
|
||
next.setDate(next.getDate() + days)
|
||
return `${next.getFullYear()}-${formatDatePart(next.getMonth() + 1)}-${formatDatePart(next.getDate())}`
|
||
}
|
||
|
||
function buildExpectedStayRange(date = '', durationIndex = 0) {
|
||
if (!date) {
|
||
return {
|
||
start: null,
|
||
end: null
|
||
}
|
||
}
|
||
|
||
const durationDays = [90, 180, 270, 365, 365]
|
||
const normalizedIndex = durationIndex >= 0 ? durationIndex : 0
|
||
const endDate = shiftDate(date, durationDays[normalizedIndex] || durationDays[0])
|
||
|
||
return {
|
||
start: `${date}T00:00:00`,
|
||
end: endDate ? `${endDate}T23:59:59` : null
|
||
}
|
||
}
|
||
|
||
export default {
|
||
components: {
|
||
RoomTypeSelector
|
||
},
|
||
computed: {
|
||
language() {
|
||
return this.$getLanguage()
|
||
},
|
||
navBackgroundColor() {
|
||
return this.scrollTop > 20 ? '#F8FAFF' : 'transparent'
|
||
},
|
||
displayInfoTitle() {
|
||
if (!this.sourceData) {
|
||
return ''
|
||
}
|
||
if (this.language === 'en') {
|
||
return this.sourceData.roomTypeNameEnDisplay
|
||
|| this.sourceData.roomNameEnDisplay
|
||
|| compactEnglishText(this.sourceData.roomTypeNameEn || this.sourceData.roomNameEn || this.sourceData.bannerTitleEn || this.sourceData.bannerTitle || this.sourceData.areaLabelEn || this.sourceData.areaLabel || this.sourceData.areaNameEn || this.sourceData.areaName || '', 24)
|
||
}
|
||
return this.sourceData.roomTypeName || this.sourceData.roomName || this.sourceData.bannerTitle || this.sourceData.areaLabel || this.sourceData.areaName || ''
|
||
},
|
||
displayInfoDesc() {
|
||
if (!this.sourceData) {
|
||
return this.$tr('专属管家为您提供一对一带看服务')
|
||
}
|
||
const bannerCopy = this.language === 'en'
|
||
? compactEnglishText(this.sourceData.bannerSubtitleEn || this.sourceData.bannerDescEn || this.sourceData.bannerSubtitle || this.sourceData.bannerDesc || '', 48)
|
||
: (this.sourceData.bannerSubtitle || this.sourceData.bannerDesc || this.sourceData.bannerSubtitleEn || this.sourceData.bannerDescEn || '')
|
||
const areaLabel = this.language === 'en'
|
||
? (this.sourceData.areaLabelEnDisplay || this.sourceData.areaNameEnDisplay || compactEnglishText(this.sourceData.areaLabelEn || this.sourceData.areaLabel || this.sourceData.areaNameEn || this.sourceData.areaName || '', 26))
|
||
: (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() {
|
||
return {
|
||
dateList: [],
|
||
timeSlots: [
|
||
'10:00-11:00', '11:00-12:00', '12:00-13:00',
|
||
'13:00-14:00', '14:00-15:00', '15:00-16:00',
|
||
'16:00-17:00', '17:00-18:00', '18:00-19:00',
|
||
'19:00-20:00'
|
||
],
|
||
durationOptions: [],
|
||
currentDateIndex: 0,
|
||
currentTimeIndex: -1,
|
||
formData: {
|
||
intendedAreaCode: '',
|
||
intendedRoomTypeCode: '',
|
||
budget: '',
|
||
duration: '',
|
||
workplace: ''
|
||
},
|
||
sourceData: null,
|
||
isSubmitting: false,
|
||
scrollTop: 0
|
||
}
|
||
},
|
||
onPageScroll(event) {
|
||
this.scrollTop = (event && event.scrollTop) || 0
|
||
},
|
||
onLoad(options) {
|
||
this.syncDurationOptions()
|
||
this.generateDates()
|
||
if (options.payload) {
|
||
try {
|
||
this.sourceData = JSON.parse(decodeURIComponent(options.payload))
|
||
this.formData.intendedAreaCode = this.sourceData && this.sourceData.areaCode ? this.sourceData.areaCode : ''
|
||
this.formData.intendedRoomTypeCode = this.sourceData && this.sourceData.roomTypeCode ? this.sourceData.roomTypeCode : ''
|
||
} catch (e) {
|
||
console.error('Failed to parse payload', 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 viewing booking entry failed', error)
|
||
return false
|
||
})
|
||
},
|
||
syncDurationOptions() {
|
||
this.durationOptions = [
|
||
this.$tr('1-3个月'),
|
||
this.$tr('4-6个月'),
|
||
this.$tr('7-9个月'),
|
||
this.$tr('10-12个月'),
|
||
this.$tr('1年以上')
|
||
]
|
||
},
|
||
generateDates() {
|
||
const dates = []
|
||
const today = new Date()
|
||
const weeks = [
|
||
this.$tr('周日'),
|
||
this.$tr('周一'),
|
||
this.$tr('周二'),
|
||
this.$tr('周三'),
|
||
this.$tr('周四'),
|
||
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('明天');
|
||
|
||
dates.push({
|
||
date: `${d.getFullYear()}-${month}-${day}`,
|
||
label: `${month}/${day}`,
|
||
week: weekStr
|
||
})
|
||
}
|
||
this.dateList = dates
|
||
},
|
||
selectDate(index) {
|
||
this.currentDateIndex = index
|
||
this.currentTimeIndex = -1
|
||
},
|
||
selectTimeSlot(index) {
|
||
this.currentTimeIndex = index
|
||
},
|
||
handleRoomTypeChange({ area, roomType }) {
|
||
this.formData.intendedAreaCode = area && area.code ? area.code : ''
|
||
this.formData.intendedRoomTypeCode = roomType && roomType.code ? roomType.code : ''
|
||
if (!this.sourceData) {
|
||
this.sourceData = {}
|
||
}
|
||
if (area) {
|
||
this.sourceData.areaCode = area.code || ''
|
||
this.sourceData.areaName = area.name || ''
|
||
this.sourceData.areaNameEn = area.nameEn || ''
|
||
}
|
||
if (roomType) {
|
||
this.sourceData.roomTypeCode = roomType.code || ''
|
||
this.sourceData.roomTypeName = roomType.name || ''
|
||
this.sourceData.roomTypeNameEn = roomType.nameEn || ''
|
||
}
|
||
},
|
||
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
|
||
}
|
||
if (this.currentTimeIndex === -1) {
|
||
uni.showToast({ title: this.$tr('请选择看房时间段'), icon: 'none' })
|
||
return
|
||
}
|
||
if (!this.formData.duration) {
|
||
uni.showToast({ title: this.$tr('请选择预计租住时长'), icon: 'none' })
|
||
return
|
||
}
|
||
|
||
const selectedDate = this.dateList[this.currentDateIndex].date
|
||
const selectedTime = this.timeSlots[this.currentTimeIndex]
|
||
const appointmentViewingTime = buildViewingDateTime(selectedDate, selectedTime, false)
|
||
const durationIndex = this.durationOptions.findIndex(item => item === this.formData.duration)
|
||
const expectedStayRange = buildExpectedStayRange(selectedDate, durationIndex)
|
||
const payload = {
|
||
intendedAreaCode: this.formData.intendedAreaCode || (this.sourceData && this.sourceData.areaCode ? this.sourceData.areaCode : null),
|
||
intendedRoomTypeCode: this.formData.intendedRoomTypeCode || (this.sourceData && this.sourceData.roomTypeCode ? this.sourceData.roomTypeCode : null),
|
||
roomSlotHeight: 1,
|
||
hobbiesAndInterestsTagKeys: null,
|
||
lifestyleHabitsTagKeys: null,
|
||
remark: null,
|
||
start: expectedStayRange.start,
|
||
end: expectedStayRange.end,
|
||
appointmentViewingTime
|
||
}
|
||
|
||
this.isSubmitting = true
|
||
uni.showLoading({ title: this.$tr('提交中...'), mask: true })
|
||
return submitViewingApplicationForm(payload)
|
||
.then(() => {
|
||
uni.hideLoading()
|
||
uni.showModal({
|
||
title: this.$tr('预约成功'),
|
||
content: this.$tr('您的预约看房申请已提交,请等待管家联系'),
|
||
showCancel: false,
|
||
confirmColor: '#01BEFF',
|
||
success: () => {
|
||
uni.reLaunch({
|
||
url: '/pages/index/index?pageId=stay-service'
|
||
})
|
||
}
|
||
})
|
||
})
|
||
.catch((error) => {
|
||
console.error('submit viewing application failed', error)
|
||
uni.hideLoading()
|
||
uni.showToast({
|
||
title: (error && (error.message || error.msg)) || this.$tr('预约提交失败'),
|
||
icon: 'none'
|
||
})
|
||
})
|
||
.finally(() => {
|
||
this.isSubmitting = false
|
||
})
|
||
}
|
||
}
|
||
}
|
||
</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, #EAF4FF 0%, $bg-page 30%, #FFFFFF 100%);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.nav-title {
|
||
font-size: 34rpx;
|
||
font-weight: 700;
|
||
color: $text-primary;
|
||
letter-spacing: 2rpx;
|
||
}
|
||
|
||
.content-wrapper {
|
||
flex: 1;
|
||
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
/* 顶部信息卡片(Hero) */
|
||
.hero-card {
|
||
position: relative;
|
||
margin: 24rpx 30rpx 30rpx;
|
||
border-radius: 36rpx;
|
||
background: linear-gradient(135deg, $hero-bg-start 0%, $hero-bg-end 100%);
|
||
box-shadow: 0 24rpx 60rpx rgba(10, 22, 40, 0.22);
|
||
overflow: hidden;
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
.hero-glow {
|
||
position: absolute;
|
||
top: -80rpx;
|
||
right: -60rpx;
|
||
width: 280rpx;
|
||
height: 280rpx;
|
||
border-radius: 50%;
|
||
background: radial-gradient(circle, rgba($brand-primary, 0.35) 0%, transparent 70%);
|
||
filter: blur(20rpx);
|
||
}
|
||
|
||
.hero-inner {
|
||
position: relative;
|
||
z-index: 2;
|
||
padding: 44rpx 36rpx;
|
||
}
|
||
|
||
.hero-content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.hero-tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 8rpx 18rpx;
|
||
background: rgba(255, 255, 255, 0.12);
|
||
border: 1rpx solid rgba(255, 255, 255, 0.18);
|
||
border-radius: 24rpx;
|
||
margin-bottom: 20rpx;
|
||
backdrop-filter: blur(8rpx);
|
||
}
|
||
|
||
.hero-tag-icon {
|
||
font-size: 22rpx;
|
||
color: $luxury-gold;
|
||
margin-right: 8rpx;
|
||
}
|
||
|
||
.hero-tag-text {
|
||
font-size: 22rpx;
|
||
font-weight: 600;
|
||
color: rgba(255, 255, 255, 0.95);
|
||
}
|
||
|
||
.hero-title {
|
||
font-size: 38rpx;
|
||
font-weight: 700;
|
||
line-height: 1.35;
|
||
margin-bottom: 16rpx;
|
||
letter-spacing: 1rpx;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.hero-desc {
|
||
font-size: 24rpx;
|
||
color: rgba(255, 255, 255, 0.72);
|
||
line-height: 1.55;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 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: 36rpx 32rpx;
|
||
border-radius: 32rpx;
|
||
background: $surface;
|
||
box-shadow: $shadow-card;
|
||
border: 1rpx solid $border-soft;
|
||
}
|
||
|
||
.card-header {
|
||
margin-bottom: 32rpx;
|
||
}
|
||
|
||
.header-icon {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
border-radius: 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 20rpx;
|
||
flex-shrink: 0;
|
||
|
||
&--primary {
|
||
background: rgba($brand-primary, 0.1);
|
||
color: $brand-primary;
|
||
}
|
||
|
||
&--luxury {
|
||
background: $luxury-gold-soft;
|
||
color: $luxury-gold;
|
||
}
|
||
|
||
text {
|
||
font-size: 32rpx;
|
||
}
|
||
}
|
||
|
||
.header-titles {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
color: $text-primary;
|
||
margin-bottom: 6rpx;
|
||
letter-spacing: 1rpx;
|
||
}
|
||
|
||
.card-subtitle {
|
||
font-size: 24rpx;
|
||
color: $text-secondary;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.date-flex {
|
||
display: inline-flex;
|
||
gap: 18rpx;
|
||
padding: 6rpx 4rpx 16rpx;
|
||
}
|
||
|
||
.date-item {
|
||
width: 120rpx;
|
||
height: 144rpx;
|
||
background: #F4F8FF;
|
||
border-radius: 24rpx;
|
||
flex-shrink: 0;
|
||
border: 2rpx solid transparent;
|
||
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||
position: relative;
|
||
}
|
||
|
||
.date-item--active {
|
||
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: $text-secondary;
|
||
margin-bottom: 10rpx;
|
||
transition: color 0.25s ease;
|
||
}
|
||
|
||
.date-item--active .date-week {
|
||
color: rgba(255, 255, 255, 0.9);
|
||
}
|
||
|
||
.date-day {
|
||
font-size: 30rpx;
|
||
color: $text-primary;
|
||
font-weight: 700;
|
||
transition: color 0.25s ease;
|
||
}
|
||
|
||
.date-item--active .date-day {
|
||
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: 18rpx;
|
||
padding: 6rpx 4rpx 16rpx;
|
||
}
|
||
|
||
.time-slot {
|
||
width: 200rpx;
|
||
height: 80rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #F4F8FF;
|
||
border-radius: 40rpx;
|
||
font-size: 26rpx;
|
||
color: $text-secondary;
|
||
border: 2rpx solid transparent;
|
||
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.time-slot--active {
|
||
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;
|
||
}
|
||
}
|
||
|
||
/* 需求字段 */
|
||
.field-item {
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.field-item--last {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.field-label {
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
color: $text-primary;
|
||
margin-bottom: 22rpx;
|
||
}
|
||
|
||
.field-dot {
|
||
width: 10rpx;
|
||
height: 10rpx;
|
||
border-radius: 50%;
|
||
margin-right: 12rpx;
|
||
|
||
&--primary {
|
||
background: $brand-primary;
|
||
box-shadow: 0 0 10rpx rgba($brand-primary, 0.45);
|
||
}
|
||
|
||
&--luxury {
|
||
background: $luxury-gold;
|
||
box-shadow: 0 0 10rpx rgba($luxury-gold, 0.45);
|
||
}
|
||
|
||
&--muted {
|
||
background: $text-tertiary;
|
||
}
|
||
}
|
||
|
||
/* 标签选择 */
|
||
.tag-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.tag-item {
|
||
padding: 0 34rpx;
|
||
height: 72rpx;
|
||
line-height: 72rpx;
|
||
background: #F4F8FF;
|
||
border-radius: 36rpx;
|
||
font-size: 26rpx;
|
||
color: $text-secondary;
|
||
border: 2rpx solid transparent;
|
||
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||
}
|
||
|
||
.tag-item--active {
|
||
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;
|
||
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: 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: $text-primary;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.footer-spacer {
|
||
height: 180rpx;
|
||
}
|
||
|
||
/* 底部按钮 */
|
||
.footer-bar {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
background: rgba(255, 255, 255, 0.96);
|
||
backdrop-filter: blur(20rpx);
|
||
padding: 24rpx 40rpx calc(24rpx + env(safe-area-inset-bottom));
|
||
box-shadow: 0 -8rpx 32rpx rgba(26, 34, 51, 0.06);
|
||
z-index: 100;
|
||
}
|
||
|
||
.footer-inner {
|
||
width: 100%;
|
||
}
|
||
|
||
.submit-btn {
|
||
width: 100%;
|
||
height: 96rpx;
|
||
border-radius: 48rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: linear-gradient(135deg, #A1B0C4 0%, #86909C 100%);
|
||
color: #FFFFFF;
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
letter-spacing: 2rpx;
|
||
box-shadow: 0 8rpx 24rpx rgba(134, 144, 156, 0.2);
|
||
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||
}
|
||
|
||
.submit-btn--active {
|
||
background: linear-gradient(135deg, $brand-primary 0%, #0099FF 100%);
|
||
box-shadow: 0 16rpx 36rpx rgba($brand-primary, 0.32);
|
||
}
|
||
|
||
.submit-btn--active:active {
|
||
transform: scale(0.98);
|
||
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.25);
|
||
}
|
||
|
||
.submit-btn-icon {
|
||
font-size: 32rpx;
|
||
margin-right: 12rpx;
|
||
}
|
||
|
||
.submit-btn-text {
|
||
line-height: 1;
|
||
}
|
||
|
||
.footer-hint {
|
||
text-align: center;
|
||
font-size: 22rpx;
|
||
color: $text-tertiary;
|
||
margin-top: 16rpx;
|
||
}
|
||
|
||
/* 导航栏背景色滚动过渡 */
|
||
.book-viewing-page ::v-deep .tn-custom-nav-bar__bar {
|
||
transition: background-color 0.3s ease, box-shadow 0.3s ease;
|
||
}
|
||
</style>
|