- 新增工单中心(ticket_center)与工单工作台(ticket_workbench)页面 - 新增房间巡查(room_inspection)与失物招领发布(lost_found_publish)页面 - 预约看样流程新增房型/楼栋/楼层选择器组件 - 新增工具模块: booking-entry、privacy、reservation-flow - api/index.js 补充楼层列表、看样提交等接口 - locales/messages.js 补充多语言文案 - manifest.json 开启微信小程序隐私检查 - 完善登录、注册、申请详情、账单等相关页面交互
779 lines
23 KiB
Vue
779 lines
23 KiB
Vue
<template>
|
|
<view class="book-viewing-page tn-safe-area-inset-bottom">
|
|
<tn-nav-bar :isBack="true" :backTitle="t('backTitle')" backgroundColor="#F0F6FF" :bottomShadow="false" :fixed="true">
|
|
<view class="nav-title">{{ t('navTitle') }}</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>
|
|
</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">{{ t('selectViewingTime') }}</text>
|
|
<text class="card-subtitle">{{ t('selectViewingTimeDesc') }}</text>
|
|
</view>
|
|
</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>
|
|
</view>
|
|
</scroll-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)"
|
|
>
|
|
{{ item }}
|
|
</view>
|
|
</view>
|
|
</scroll-view>
|
|
</view>
|
|
|
|
<!-- 租住需求卡片 -->
|
|
<view class="form-card">
|
|
<view class="card-header">
|
|
<view class="header-left tn-flex tn-flex-col-bottom">
|
|
<text class="card-title">{{ t('rentalNeeds') }}</text>
|
|
<text class="card-subtitle">{{ t('rentalNeedsDesc') }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="field-item">
|
|
<view class="field-label">{{ t('roomTypeLabel') }}</view>
|
|
<room-type-selector
|
|
v-model="formData.intendedRoomTypeCode"
|
|
:initial-area-code="formData.intendedAreaCode"
|
|
@change="handleRoomTypeChange"
|
|
/>
|
|
</view>
|
|
|
|
<view class="field-item">
|
|
<view class="field-label">{{ t('durationLabel') }}</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">{{ t('budgetLabel') }}</view>
|
|
<view class="input-box tn-flex tn-flex-col-center">
|
|
<text class="tn-icon-money input-icon"></text>
|
|
<input
|
|
class="custom-input"
|
|
v-model="formData.budget"
|
|
type="digit"
|
|
:placeholder="t('budgetPlaceholder')"
|
|
placeholder-style="color: #A1B0C4;"
|
|
/>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="field-item">
|
|
<view class="field-label">{{ t('workplaceLabel') }}</view>
|
|
<view class="input-box tn-flex tn-flex-col-center">
|
|
<text class="tn-icon-location input-icon"></text>
|
|
<input
|
|
class="custom-input"
|
|
v-model="formData.workplace"
|
|
type="text"
|
|
:placeholder="t('workplacePlaceholder')"
|
|
placeholder-style="color: #A1B0C4;"
|
|
/>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
</view>
|
|
|
|
<!-- 底部操作区 -->
|
|
<view class="footer-bar tn-safe-area-inset-bottom">
|
|
<view class="submit-btn" @click="handleSubmit">
|
|
<text>{{ t('submitBooking') }}</text>
|
|
</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()
|
|
},
|
|
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.t('defaultInfoDesc')
|
|
}
|
|
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.t('defaultInfoDesc')
|
|
}
|
|
},
|
|
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,
|
|
i18n: {
|
|
zh: {
|
|
backTitle: '返回',
|
|
navTitle: '预约现场带看',
|
|
selectViewingTime: '选择看房时间',
|
|
selectViewingTimeDesc: '请选择您方便的时间段',
|
|
rentalNeeds: '您的租住需求',
|
|
rentalNeedsDesc: '帮助管家更好地为您推荐房源',
|
|
roomTypeLabel: '意向房型',
|
|
durationLabel: '预计租住时长',
|
|
budgetLabel: '租金预算 (元/月)',
|
|
budgetPlaceholder: '请输入您的心理价位',
|
|
workplaceLabel: '工作/通勤地点',
|
|
workplacePlaceholder: '请输入主要通勤地点',
|
|
submitBooking: '提交预约',
|
|
defaultInfoDesc: '专属管家为您提供一对一带看服务',
|
|
duration1: '1-3个月',
|
|
duration2: '4-6个月',
|
|
duration3: '7-9个月',
|
|
duration4: '10-12个月',
|
|
duration5: '1年以上',
|
|
week0: '周日',
|
|
week1: '周一',
|
|
week2: '周二',
|
|
week3: '周三',
|
|
week4: '周四',
|
|
week5: '周五',
|
|
week6: '周六',
|
|
today: '今天',
|
|
tomorrow: '明天',
|
|
remarkTitle: '预约看房申请',
|
|
remarkTime: '看房时间:{date} {time}',
|
|
remarkDuration: '预计租住时长:{duration}',
|
|
remarkBudget: '租金预算:{budget} 元/月',
|
|
remarkWorkplace: '工作/通勤地点:{workplace}',
|
|
remarkArea: '意向区域:{area}',
|
|
remarkRoomType: '意向房型:{roomType}',
|
|
remarkSource: '当前来源:{room}',
|
|
remarkPayMethod: '支付方式:{payMethod}',
|
|
remarkTail: '备注:请宿管确认带看安排;若带看完成并锁定房号,可转为预定单通知租客支付。',
|
|
selectTimeRequired: '请选择看房时间段',
|
|
selectDurationRequired: '请选择预计租住时长',
|
|
submitting: '提交中...',
|
|
submitSuccessTitle: '预约成功',
|
|
submitSuccessContent: '您的预约看房申请已提交,请等待管家联系',
|
|
submitFailed: '预约提交失败'
|
|
},
|
|
en: {
|
|
backTitle: 'Back',
|
|
navTitle: 'On-site Viewing',
|
|
selectViewingTime: 'Select Viewing Time',
|
|
selectViewingTimeDesc: 'Choose a time slot that works for you',
|
|
rentalNeeds: 'Rental Needs',
|
|
rentalNeedsDesc: 'Help the manager recommend a suitable room',
|
|
roomTypeLabel: 'Preferred Room Type',
|
|
durationLabel: 'Expected Stay Duration',
|
|
budgetLabel: 'Budget (CNY/month)',
|
|
budgetPlaceholder: 'Enter your expected budget',
|
|
workplaceLabel: 'Work / Commute Location',
|
|
workplacePlaceholder: 'Enter your main commute location',
|
|
submitBooking: 'Submit Request',
|
|
defaultInfoDesc: 'A dedicated manager will arrange a one-on-one viewing for you',
|
|
duration1: '1-3 months',
|
|
duration2: '4-6 months',
|
|
duration3: '7-9 months',
|
|
duration4: '10-12 months',
|
|
duration5: 'Over 1 year',
|
|
week0: 'Sun',
|
|
week1: 'Mon',
|
|
week2: 'Tue',
|
|
week3: 'Wed',
|
|
week4: 'Thu',
|
|
week5: 'Fri',
|
|
week6: 'Sat',
|
|
today: 'Today',
|
|
tomorrow: 'Tomorrow',
|
|
remarkTitle: 'Viewing Request',
|
|
remarkTime: 'Viewing Time: {date} {time}',
|
|
remarkDuration: 'Expected Stay: {duration}',
|
|
remarkBudget: 'Budget: {budget} CNY/month',
|
|
remarkWorkplace: 'Work / Commute Location: {workplace}',
|
|
remarkArea: 'Preferred Area: {area}',
|
|
remarkRoomType: 'Preferred Room Type: {roomType}',
|
|
remarkSource: 'Current Source: {room}',
|
|
remarkPayMethod: 'Payment Method: {payMethod}',
|
|
remarkTail: 'Note: Please confirm the viewing schedule with the manager. After viewing and locking a room number, the order can be converted into a booking for payment.',
|
|
selectTimeRequired: 'Please select a viewing time slot',
|
|
selectDurationRequired: 'Please select the expected stay duration',
|
|
submitting: 'Submitting...',
|
|
submitSuccessTitle: 'Booking Submitted',
|
|
submitSuccessContent: 'Your viewing request has been submitted.\nThe system will forward it to the authorized manager of the selected area. After the manager confirms the viewing, the order can be completed directly or converted into a booking for payment once a room is selected.',
|
|
submitFailed: 'Failed to submit the booking'
|
|
}
|
|
}
|
|
}
|
|
},
|
|
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: {
|
|
t(key, params) {
|
|
return this.$tm('pkgBooking.book_viewing.book_viewing', key, params)
|
|
},
|
|
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.t('duration1'),
|
|
this.t('duration2'),
|
|
this.t('duration3'),
|
|
this.t('duration4'),
|
|
this.t('duration5')
|
|
]
|
|
},
|
|
generateDates() {
|
|
const dates = []
|
|
const today = new Date()
|
|
const weeks = [
|
|
this.t('week0'),
|
|
this.t('week1'),
|
|
this.t('week2'),
|
|
this.t('week3'),
|
|
this.t('week4'),
|
|
this.t('week5'),
|
|
this.t('week6')
|
|
]
|
|
|
|
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.t('today');
|
|
else if (i === 1) weekStr = this.t('tomorrow');
|
|
|
|
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
|
|
},
|
|
handleSubmit() {
|
|
if (this.isSubmitting) {
|
|
return
|
|
}
|
|
if (this.currentTimeIndex === -1) {
|
|
uni.showToast({ title: this.t('selectTimeRequired'), icon: 'none' })
|
|
return
|
|
}
|
|
if (!this.formData.duration) {
|
|
uni.showToast({ title: this.t('selectDurationRequired'), 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.t('submitting'), mask: true })
|
|
return submitViewingApplicationForm(payload)
|
|
.then(() => {
|
|
uni.hideLoading()
|
|
uni.showModal({
|
|
title: this.t('submitSuccessTitle'),
|
|
content: this.t('submitSuccessContent'),
|
|
showCancel: false,
|
|
confirmColor: '#01BEFF',
|
|
success: () => {
|
|
uni.redirectTo({
|
|
url: '/pkgService/my_application/my_application'
|
|
})
|
|
}
|
|
})
|
|
})
|
|
.catch((error) => {
|
|
console.error('submit viewing application failed', error)
|
|
uni.hideLoading()
|
|
uni.showToast({
|
|
title: (error && (error.message || error.msg)) || this.t('submitFailed'),
|
|
icon: 'none'
|
|
})
|
|
})
|
|
.finally(() => {
|
|
this.isSubmitting = false
|
|
})
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.book-viewing-page {
|
|
min-height: 100vh;
|
|
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.nav-title {
|
|
font-size: 34rpx;
|
|
font-weight: bold;
|
|
color: #1D2129;
|
|
}
|
|
|
|
.content-wrapper {
|
|
flex: 1;
|
|
padding-bottom: calc(140rpx + 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;
|
|
}
|
|
|
|
.info-left {
|
|
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;
|
|
}
|
|
|
|
.info-desc {
|
|
font-size: 24rpx;
|
|
opacity: 0.9;
|
|
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;
|
|
}
|
|
|
|
/* 通用表单卡片 */
|
|
.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);
|
|
}
|
|
|
|
.card-header {
|
|
margin-bottom: 30rpx;
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 34rpx;
|
|
font-weight: bold;
|
|
color: #1D2129;
|
|
margin-right: 16rpx;
|
|
}
|
|
|
|
.card-subtitle {
|
|
font-size: 24rpx;
|
|
color: #86909C;
|
|
}
|
|
|
|
/* 日期选择 */
|
|
.date-scroll {
|
|
width: 100%;
|
|
white-space: nowrap;
|
|
margin-bottom: 30rpx;
|
|
}
|
|
|
|
.date-flex {
|
|
display: inline-flex;
|
|
gap: 20rpx;
|
|
padding-bottom: 10rpx;
|
|
}
|
|
|
|
.date-item {
|
|
width: 110rpx;
|
|
height: 130rpx;
|
|
background: #F4F8FF;
|
|
border-radius: 16rpx;
|
|
flex-shrink: 0;
|
|
border: 2rpx solid transparent;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.date-item--active {
|
|
background: #EAF0FF;
|
|
border-color: #01BEFF;
|
|
}
|
|
|
|
.date-week {
|
|
font-size: 24rpx;
|
|
color: #86909C;
|
|
margin-bottom: 8rpx;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.date-item--active .date-week {
|
|
color: #01BEFF;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.date-day {
|
|
font-size: 28rpx;
|
|
color: #1D2129;
|
|
font-weight: 500;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.date-item--active .date-day {
|
|
color: #01BEFF;
|
|
}
|
|
|
|
/* 时间段选择 */
|
|
.time-scroll {
|
|
width: 100%;
|
|
}
|
|
|
|
.time-flex {
|
|
display: inline-flex;
|
|
gap: 20rpx;
|
|
padding-bottom: 8rpx;
|
|
}
|
|
|
|
.time-slot {
|
|
width: 220rpx;
|
|
height: 72rpx;
|
|
line-height: 72rpx;
|
|
text-align: center;
|
|
background: #F4F8FF;
|
|
border-radius: 12rpx;
|
|
font-size: 26rpx;
|
|
color: #4E5969;
|
|
border: 2rpx solid transparent;
|
|
transition: all 0.3s ease;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.time-slot--active {
|
|
background: #01BEFF;
|
|
color: #ffffff;
|
|
font-weight: bold;
|
|
box-shadow: 0 8rpx 16rpx rgba(1, 190, 255, 0.2);
|
|
}
|
|
|
|
/* 需求字段 */
|
|
.field-item {
|
|
margin-bottom: 40rpx;
|
|
}
|
|
|
|
.field-item:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.field-label {
|
|
font-size: 28rpx;
|
|
color: #1D2129;
|
|
font-weight: bold;
|
|
margin-bottom: 24rpx;
|
|
}
|
|
|
|
/* 标签选择 */
|
|
.tag-grid {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 20rpx;
|
|
}
|
|
|
|
.tag-item {
|
|
padding: 0 32rpx;
|
|
height: 68rpx;
|
|
line-height: 68rpx;
|
|
background: #F4F8FF;
|
|
border-radius: 34rpx;
|
|
font-size: 26rpx;
|
|
color: #4E5969;
|
|
border: 2rpx solid transparent;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.tag-item--active {
|
|
background: #EAF0FF;
|
|
border-color: #01BEFF;
|
|
color: #01BEFF;
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* 输入框 */
|
|
.input-box {
|
|
background: #F4F8FF;
|
|
border-radius: 16rpx;
|
|
height: 88rpx;
|
|
padding: 0 30rpx;
|
|
}
|
|
|
|
.input-icon {
|
|
font-size: 36rpx;
|
|
color: #A1B0C4;
|
|
margin-right: 16rpx;
|
|
}
|
|
|
|
.custom-input {
|
|
flex: 1;
|
|
font-size: 28rpx;
|
|
color: #1D2129;
|
|
}
|
|
|
|
/* 底部按钮 */
|
|
.footer-bar {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
background: #ffffff;
|
|
padding: 24rpx 40rpx;
|
|
box-shadow: 0 -4rpx 24rpx rgba(0, 0, 0, 0.04);
|
|
z-index: 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;
|
|
font-size: 32rpx;
|
|
font-weight: bold;
|
|
letter-spacing: 2rpx;
|
|
box-shadow: 0 12rpx 24rpx rgba(1, 190, 255, 0.25);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.submit-btn:active {
|
|
transform: scale(0.98);
|
|
opacity: 0.9;
|
|
}
|
|
</style>
|