Files
QSY-lvjuxcx/pkgBooking/book_viewing/book_viewing.vue
liangjian 1111ac148b feat: 重构核心租住流程页面与交互体验
统一注册、看房、申请与入住等核心页面的视觉和流程衔接,补齐支付、签约与状态流转闭环,提升移动端操作连贯性。
2026-07-29 09:05:38 +08:00

1109 lines
29 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

<template>
<view class="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>