feat: 新增工单中心、房间巡查、失物招领等业务模块
- 新增工单中心(ticket_center)与工单工作台(ticket_workbench)页面 - 新增房间巡查(room_inspection)与失物招领发布(lost_found_publish)页面 - 预约看样流程新增房型/楼栋/楼层选择器组件 - 新增工具模块: booking-entry、privacy、reservation-flow - api/index.js 补充楼层列表、看样提交等接口 - locales/messages.js 补充多语言文案 - manifest.json 开启微信小程序隐私检查 - 完善登录、注册、申请详情、账单等相关页面交互
This commit is contained in:
@@ -87,6 +87,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { resolveExistingBookingNavigationTarget } from '../../utils/booking-entry'
|
||||
import { getSessionSnapshot } from '../../utils/app-state'
|
||||
import { syncLoginStayState } from '../../utils/session-sync'
|
||||
import { compactEnglishText } from '../../utils/select-room-normalizers'
|
||||
|
||||
export default {
|
||||
@@ -194,11 +197,41 @@ export default {
|
||||
console.error('Failed to parse apply data', e);
|
||||
})
|
||||
}
|
||||
this.redirectIfBookingUnavailable()
|
||||
},
|
||||
methods: {
|
||||
t(key) {
|
||||
return this.$tm('pkgBooking.book.book', key)
|
||||
},
|
||||
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;
|
||||
},
|
||||
|
||||
@@ -41,17 +41,19 @@
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="time-grid">
|
||||
<view
|
||||
v-for="(item, index) in timeSlots"
|
||||
:key="index"
|
||||
class="time-slot"
|
||||
:class="{ 'time-slot--active': currentTimeIndex === index }"
|
||||
@click="selectTimeSlot(index)"
|
||||
>
|
||||
{{ item }}
|
||||
<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>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 租住需求卡片 -->
|
||||
@@ -63,6 +65,15 @@
|
||||
</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">
|
||||
@@ -119,8 +130,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { submitApplicationForm } from '@/api/index.js'
|
||||
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) {
|
||||
@@ -134,7 +149,44 @@ function buildViewingDateTime(date = '', time = '', isEnd = false) {
|
||||
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()
|
||||
@@ -177,6 +229,8 @@ export default {
|
||||
currentDateIndex: 0,
|
||||
currentTimeIndex: -1,
|
||||
formData: {
|
||||
intendedAreaCode: '',
|
||||
intendedRoomTypeCode: '',
|
||||
budget: '',
|
||||
duration: '',
|
||||
workplace: ''
|
||||
@@ -191,6 +245,7 @@ export default {
|
||||
selectViewingTimeDesc: '请选择您方便的时间段',
|
||||
rentalNeeds: '您的租住需求',
|
||||
rentalNeedsDesc: '帮助管家更好地为您推荐房源',
|
||||
roomTypeLabel: '意向房型',
|
||||
durationLabel: '预计租住时长',
|
||||
budgetLabel: '租金预算 (元/月)',
|
||||
budgetPlaceholder: '请输入您的心理价位',
|
||||
@@ -226,7 +281,7 @@ export default {
|
||||
selectDurationRequired: '请选择预计租住时长',
|
||||
submitting: '提交中...',
|
||||
submitSuccessTitle: '预约成功',
|
||||
submitSuccessContent: '您的预约看房申请已提交。\n系统会自动转发给对应区域有房间权限的宿管,宿管确认带看后可直接完结订单,或选定房型房号后转为预定单发送给租客支付。',
|
||||
submitSuccessContent: '您的预约看房申请已提交,请等待管家联系',
|
||||
submitFailed: '预约提交失败'
|
||||
},
|
||||
en: {
|
||||
@@ -236,6 +291,7 @@ export default {
|
||||
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',
|
||||
@@ -283,15 +339,47 @@ export default {
|
||||
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'),
|
||||
@@ -339,50 +427,26 @@ export default {
|
||||
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
|
||||
},
|
||||
buildRemark(selectedDate, selectedTime) {
|
||||
const lines = [
|
||||
this.t('remarkTitle'),
|
||||
this.t('remarkTime', { date: selectedDate, time: selectedTime }),
|
||||
this.t('remarkDuration', { duration: this.formData.duration })
|
||||
]
|
||||
|
||||
if (this.formData.budget) {
|
||||
lines.push(this.t('remarkBudget', { budget: this.formData.budget }))
|
||||
}
|
||||
if (this.formData.workplace) {
|
||||
lines.push(this.t('remarkWorkplace', { workplace: this.formData.workplace }))
|
||||
}
|
||||
if (this.sourceData && (this.sourceData.areaName || this.sourceData.areaNameEn)) {
|
||||
lines.push(this.t('remarkArea', {
|
||||
area: this.language === 'en'
|
||||
? (this.sourceData.areaNameEnDisplay || this.sourceData.areaLabelEnDisplay || this.sourceData.areaNameEn || this.sourceData.areaLabelEn || this.sourceData.areaName || this.sourceData.areaLabel || '')
|
||||
: (this.sourceData.areaName || this.sourceData.areaLabel || this.sourceData.areaNameEn || this.sourceData.areaLabelEn || '')
|
||||
}))
|
||||
}
|
||||
if (this.sourceData && (this.sourceData.roomTypeName || this.sourceData.roomTypeNameEn)) {
|
||||
lines.push(this.t('remarkRoomType', {
|
||||
roomType: this.language === 'en'
|
||||
? (this.sourceData.roomTypeNameEnDisplay || this.sourceData.roomTypeNameEn || this.sourceData.roomTypeName || '')
|
||||
: (this.sourceData.roomTypeName || this.sourceData.roomTypeNameEn || '')
|
||||
}))
|
||||
}
|
||||
if (this.sourceData && (this.sourceData.roomName || this.sourceData.roomNameEn)) {
|
||||
lines.push(this.t('remarkSource', {
|
||||
room: this.language === 'en'
|
||||
? (this.sourceData.roomNameEnDisplay || this.sourceData.roomNameEn || this.sourceData.roomName || '')
|
||||
: (this.sourceData.roomName || this.sourceData.roomNameEn || '')
|
||||
}))
|
||||
}
|
||||
if (this.sourceData && this.sourceData.payMethod) {
|
||||
lines.push(this.t('remarkPayMethod', { payMethod: localizePayMethodText(this.sourceData.payMethod, this.language) }))
|
||||
}
|
||||
|
||||
lines.push(this.t('remarkTail'))
|
||||
return lines.join('\n')
|
||||
},
|
||||
handleSubmit() {
|
||||
if (this.isSubmitting) {
|
||||
return
|
||||
@@ -398,20 +462,24 @@ export default {
|
||||
|
||||
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.sourceData && this.sourceData.areaCode ? this.sourceData.areaCode : null,
|
||||
intendedRoomTypeCode: this.sourceData && this.sourceData.roomTypeCode ? this.sourceData.roomTypeCode : null,
|
||||
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: this.buildRemark(selectedDate, selectedTime),
|
||||
start: buildViewingDateTime(selectedDate, selectedTime, false),
|
||||
end: buildViewingDateTime(selectedDate, selectedTime, true)
|
||||
};
|
||||
remark: null,
|
||||
start: expectedStayRange.start,
|
||||
end: expectedStayRange.end,
|
||||
appointmentViewingTime
|
||||
}
|
||||
|
||||
this.isSubmitting = true
|
||||
uni.showLoading({ title: this.t('submitting'), mask: true })
|
||||
return submitApplicationForm(payload)
|
||||
return submitViewingApplicationForm(payload)
|
||||
.then(() => {
|
||||
uni.hideLoading()
|
||||
uni.showModal({
|
||||
@@ -582,14 +650,18 @@ export default {
|
||||
}
|
||||
|
||||
/* 时间段选择 */
|
||||
.time-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.time-scroll {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.time-flex {
|
||||
display: inline-flex;
|
||||
gap: 20rpx;
|
||||
padding-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.time-slot {
|
||||
width: calc(33.33% - 14rpx);
|
||||
width: 220rpx;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
text-align: center;
|
||||
@@ -599,6 +671,7 @@ export default {
|
||||
color: #4E5969;
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.time-slot--active {
|
||||
|
||||
363
pkgBooking/book_viewing/components/RoomTypeSelector.vue
Normal file
363
pkgBooking/book_viewing/components/RoomTypeSelector.vue
Normal file
@@ -0,0 +1,363 @@
|
||||
<template>
|
||||
<view class="room-type-selector">
|
||||
<view class="selector-header tn-flex tn-flex-row-between tn-flex-col-center" @click="openSelector">
|
||||
<view class="selected-info">
|
||||
<view class="selected-name tn-text-ellipsis" :class="{ 'is-empty': !selectedRoomType }">
|
||||
{{ selectedRoomType ? getRoomTypeDisplayName(selectedRoomType) : t('selectRoomType') }}
|
||||
</view>
|
||||
<view class="selected-area tn-text-ellipsis" v-if="selectedArea">
|
||||
{{ getAreaDisplayName(selectedArea) }}
|
||||
</view>
|
||||
</view>
|
||||
<text class="tn-icon-right tn-color-gray"></text>
|
||||
</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">{{ t('popupTitle') }}</text>
|
||||
</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">{{ t('loadingRoomTypes') }}</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!areaList.length" class="empty-state tn-padding-xl tn-text-center">
|
||||
<text class="tn-color-gray">{{ t('emptyRoomTypes') }}</text>
|
||||
</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>
|
||||
<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
|
||||
class="room-type-item"
|
||||
v-for="room in area.roomTypes"
|
||||
:key="room.code"
|
||||
:class="{ 'is-active': selectedRoomType && selectedRoomType.code === room.code }"
|
||||
@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) }}{{ t('priceUnit') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</tn-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
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: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
initialAreaCode: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showPopup: false,
|
||||
isLoading: false,
|
||||
areaList: [],
|
||||
selectedArea: null,
|
||||
selectedRoomType: null,
|
||||
collapsedAreas: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
language() {
|
||||
return this.$getLanguage ? this.$getLanguage() : 'zh'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value() {
|
||||
this.syncSelection()
|
||||
},
|
||||
language() {
|
||||
this.loadData(true)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
t(key) {
|
||||
const lang = I18N[this.language] ? this.language : 'zh'
|
||||
return I18N[lang][key] || I18N.zh[key] || key
|
||||
},
|
||||
openSelector() {
|
||||
this.showPopup = true
|
||||
this.loadData()
|
||||
},
|
||||
loadData(force = false) {
|
||||
if (!force && this.areaList.length > 0) {
|
||||
this.syncSelection()
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
this.isLoading = true
|
||||
return areaPagination({ PageIndex: 1, PageSize: 100 }, this.language)
|
||||
.then((res) => {
|
||||
const payload = res && res.data ? res.data : res
|
||||
const rawList = Array.isArray(payload && payload.data) ? payload.data : (Array.isArray(payload) ? payload : [])
|
||||
this.areaList = rawList
|
||||
.map(item => normalizeAreaItem(item))
|
||||
.filter(area => Array.isArray(area.roomTypes) && area.roomTypes.length > 0)
|
||||
|
||||
const collapsed = {}
|
||||
this.areaList.forEach((_, index) => {
|
||||
collapsed[index] = false
|
||||
})
|
||||
this.collapsedAreas = collapsed
|
||||
this.syncSelection()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('load room types error', error)
|
||||
uni.showToast({ title: this.t('loadFailed'), icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
},
|
||||
syncSelection() {
|
||||
if (!this.areaList.length) {
|
||||
this.selectedArea = null
|
||||
this.selectedRoomType = null
|
||||
return
|
||||
}
|
||||
|
||||
let foundArea = null
|
||||
let foundRoom = null
|
||||
const previousAreaCode = this.selectedArea && this.selectedArea.code ? this.selectedArea.code : ''
|
||||
const previousRoomCode = this.selectedRoomType && this.selectedRoomType.code ? this.selectedRoomType.code : ''
|
||||
|
||||
if (this.value) {
|
||||
for (const area of this.areaList) {
|
||||
const room = Array.isArray(area.roomTypes) ? area.roomTypes.find(item => item.code === this.value) : null
|
||||
if (room) {
|
||||
foundArea = area
|
||||
foundRoom = room
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundRoom && this.initialAreaCode) {
|
||||
foundArea = this.areaList.find(area => area.code === this.initialAreaCode) || null
|
||||
}
|
||||
|
||||
if (!foundArea) {
|
||||
this.selectedArea = null
|
||||
this.selectedRoomType = null
|
||||
return
|
||||
}
|
||||
|
||||
this.selectedArea = foundArea
|
||||
this.selectedRoomType = foundRoom || null
|
||||
|
||||
if (foundRoom) {
|
||||
if (this.value !== foundRoom.code) {
|
||||
this.$emit('input', foundRoom.code)
|
||||
}
|
||||
if (previousAreaCode !== foundArea.code || previousRoomCode !== foundRoom.code) {
|
||||
this.$emit('change', { area: foundArea, roomType: foundRoom })
|
||||
}
|
||||
}
|
||||
},
|
||||
handleSelect(area, roomType) {
|
||||
this.selectedArea = area
|
||||
this.selectedRoomType = roomType
|
||||
this.$emit('input', roomType.code)
|
||||
this.$emit('change', { area, roomType })
|
||||
this.showPopup = false
|
||||
},
|
||||
getAreaDisplayName(area = {}) {
|
||||
if (this.language === 'en') {
|
||||
return area.nameEnDisplay || area.nameEn || area.name || ''
|
||||
}
|
||||
return area.name || area.nameEn || ''
|
||||
},
|
||||
getRoomTypeDisplayName(roomType = {}) {
|
||||
if (this.language === 'en') {
|
||||
return roomType.nameEnDisplay || roomType.nameEn || roomType.name || ''
|
||||
}
|
||||
return roomType.name || roomType.nameEn || ''
|
||||
},
|
||||
toggleArea(index) {
|
||||
this.$set(this.collapsedAreas, index, !this.collapsedAreas[index])
|
||||
},
|
||||
getPriceValue(priceText) {
|
||||
if (!priceText) {
|
||||
return '--'
|
||||
}
|
||||
const matched = String(priceText).match(/(\d+(\.\d+)?)/)
|
||||
return matched ? matched[1] : '--'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.room-type-selector {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selector-header {
|
||||
background: #FFFFFF;
|
||||
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;
|
||||
}
|
||||
|
||||
.selected-info {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.selected-name {
|
||||
font-size: 28rpx;
|
||||
color: #1A2233;
|
||||
font-weight: 600;
|
||||
|
||||
&.is-empty {
|
||||
color: #A0B0C0;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.selected-area {
|
||||
font-size: 22rpx;
|
||||
color: #6A7A99;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #F8FAFF;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
padding: 40rpx 30rpx 20rpx;
|
||||
text-align: center;
|
||||
border-bottom: 1px dashed rgba($brand-primary, 0.15);
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #1A2233;
|
||||
}
|
||||
|
||||
.area-list-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
padding: 20rpx 30rpx 60rpx;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.area-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1A2233;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.area-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: $brand-primary;
|
||||
margin-right: 12rpx;
|
||||
box-shadow: 0 0 8rpx rgba($brand-primary, 0.4);
|
||||
}
|
||||
|
||||
.room-type-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
&.is-active {
|
||||
background: #FFFFFF;
|
||||
border-color: $brand-primary;
|
||||
box-shadow: 0 4rpx 16rpx rgba($brand-primary, 0.1);
|
||||
|
||||
.room-name {
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.room-price {
|
||||
color: $brand-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.room-name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #1A2233;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.room-price {
|
||||
font-size: 22rpx;
|
||||
color: #6A7A99;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user