feat: 包场预约网格改为1小时粒度+动态价格显示+锁场状态
- 时间线位置 /30 改为 /60 - 价格预览改为按格子累加 slot_prices - 格子内显示价格(¥xx) - 新增 locked(锁场)和 no_price(无价格)格子状态 - API映射层移除 unit_price,新增 slot_prices 和 source_type - 确认页半小时段改为时段
This commit is contained in:
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
VITE_ICE_API_BASE_URL=https://venue.test.chatgqt.top
|
VITE_ICE_API_BASE_URL=https://venue.test.chatgqt.top
|
||||||
VITE_COMPREHENSIVE_API_BASE_URL=https://venue2.test.chatgqt.top
|
VITE_COMPREHENSIVE_API_BASE_URL=https://8002.frp.chatgqt.top
|
||||||
@@ -46,10 +46,8 @@ const toPrivateBookingOptions = (backendData = {}) => ({
|
|||||||
court_type_label: backendData.court_type_label || '',
|
court_type_label: backendData.court_type_label || '',
|
||||||
open_time: backendData.open_time || '',
|
open_time: backendData.open_time || '',
|
||||||
close_time: backendData.close_time || '',
|
close_time: backendData.close_time || '',
|
||||||
unit_price: backendData.unit_price || '0.00',
|
duration_units: toNumber(backendData.duration_units, 1),
|
||||||
duration_units: toNumber(backendData.duration_units, 2),
|
|
||||||
duration_minutes: toNumber(backendData.duration_minutes, 60),
|
duration_minutes: toNumber(backendData.duration_minutes, 60),
|
||||||
amount_per_court: backendData.amount_per_court || '0.00',
|
|
||||||
total_court_count: toNumber(backendData.total_court_count),
|
total_court_count: toNumber(backendData.total_court_count),
|
||||||
time_options: toArray(backendData.time_options).map(toPrivateTimeOption)
|
time_options: toArray(backendData.time_options).map(toPrivateTimeOption)
|
||||||
})
|
})
|
||||||
@@ -67,7 +65,8 @@ const toGridSlot = (backendSlot = {}) => ({
|
|||||||
const toGridOccupation = (backendOcc = {}) => ({
|
const toGridOccupation = (backendOcc = {}) => ({
|
||||||
court_id: backendOcc.court_id ?? null,
|
court_id: backendOcc.court_id ?? null,
|
||||||
start_at: backendOcc.start_at || '',
|
start_at: backendOcc.start_at || '',
|
||||||
end_at: backendOcc.end_at || ''
|
end_at: backendOcc.end_at || '',
|
||||||
|
source_type: backendOcc.source_type || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const toPrivateBookingGrid = (backendData = {}) => ({
|
const toPrivateBookingGrid = (backendData = {}) => ({
|
||||||
@@ -76,10 +75,10 @@ const toPrivateBookingGrid = (backendData = {}) => ({
|
|||||||
court_type_label: backendData.court_type_label || '',
|
court_type_label: backendData.court_type_label || '',
|
||||||
open_time: backendData.open_time || '',
|
open_time: backendData.open_time || '',
|
||||||
close_time: backendData.close_time || '',
|
close_time: backendData.close_time || '',
|
||||||
unit_price: backendData.unit_price || '0.00',
|
|
||||||
courts: toArray(backendData.courts).map(toGridCourt),
|
courts: toArray(backendData.courts).map(toGridCourt),
|
||||||
time_slots: toArray(backendData.time_slots).map(toGridSlot),
|
time_slots: toArray(backendData.time_slots).map(toGridSlot),
|
||||||
occupations: toArray(backendData.occupations).map(toGridOccupation)
|
occupations: toArray(backendData.occupations).map(toGridOccupation),
|
||||||
|
slot_prices: backendData.slot_prices || {}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const privateBookingApi = {
|
export const privateBookingApi = {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<view class="card fee-card">
|
<view class="card fee-card">
|
||||||
<text class="card-title">费用明细</text>
|
<text class="card-title">费用明细</text>
|
||||||
<view class="summary-row">
|
<view class="summary-row">
|
||||||
<text>半小时段</text>
|
<text>时段</text>
|
||||||
<text>{{ quote.unit_count }} 段</text>
|
<text>{{ quote.unit_count }} 段</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="summary-row">
|
<view class="summary-row">
|
||||||
|
|||||||
@@ -61,7 +61,9 @@
|
|||||||
class="grid-cell"
|
class="grid-cell"
|
||||||
:class="getCellClass(court.id, si)"
|
:class="getCellClass(court.id, si)"
|
||||||
@tap="toggleCell(court.id, si)"
|
@tap="toggleCell(court.id, si)"
|
||||||
></view>
|
>
|
||||||
|
<text v-if="getCellPrice(court.id, si)" class="cell-price">¥{{ getCellPrice(court.id, si) }}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
v-if="currentTimeLineVisible && currentTimeTop >= 0"
|
v-if="currentTimeLineVisible && currentTimeTop >= 0"
|
||||||
@@ -146,6 +148,7 @@ const cellStatus = computed(() => {
|
|||||||
for (const court of grid.value.courts) {
|
for (const court of grid.value.courts) {
|
||||||
status[court.id] = {}
|
status[court.id] = {}
|
||||||
const occs = occupationMap.value[court.id] || []
|
const occs = occupationMap.value[court.id] || []
|
||||||
|
const courtPrices = grid.value?.slot_prices?.[court.id] || []
|
||||||
|
|
||||||
for (let si = 0; si < grid.value.time_slots.length; si++) {
|
for (let si = 0; si < grid.value.time_slots.length; si++) {
|
||||||
const slot = grid.value.time_slots[si]
|
const slot = grid.value.time_slots[si]
|
||||||
@@ -157,13 +160,23 @@ const cellStatus = computed(() => {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOccupied = occs.some((occ) => {
|
const overlappingOcc = occs.find((occ) => {
|
||||||
const occStart = dayjs(occ.start_at)
|
const occStart = dayjs(occ.start_at)
|
||||||
const occEnd = dayjs(occ.end_at)
|
const occEnd = dayjs(occ.end_at)
|
||||||
return occStart.isBefore(slotEnd) && occEnd.isAfter(slotStart)
|
return occStart.isBefore(slotEnd) && occEnd.isAfter(slotStart)
|
||||||
})
|
})
|
||||||
|
|
||||||
status[court.id][si] = isOccupied ? 'occupied' : 'available'
|
if (overlappingOcc) {
|
||||||
|
status[court.id][si] = overlappingOcc.source_type === 'lock' ? 'locked' : 'occupied'
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!courtPrices[si]) {
|
||||||
|
status[court.id][si] = 'no_price'
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
status[court.id][si] = 'available'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return status
|
return status
|
||||||
@@ -180,7 +193,7 @@ const currentTimeTop = computed(() => {
|
|||||||
const closeMin = parseTimeToMinutes(grid.value.close_time)
|
const closeMin = parseTimeToMinutes(grid.value.close_time)
|
||||||
const currentMin = now.value.hour() * 60 + now.value.minute()
|
const currentMin = now.value.hour() * 60 + now.value.minute()
|
||||||
if (currentMin < openMin || currentMin > closeMin) return -1
|
if (currentMin < openMin || currentMin > closeMin) return -1
|
||||||
return HEADER_HEIGHT + ((currentMin - openMin) / 30) * CELL_HEIGHT
|
return HEADER_HEIGHT + ((currentMin - openMin) / 60) * CELL_HEIGHT
|
||||||
})
|
})
|
||||||
|
|
||||||
const selectedCourtCount = computed(() => Object.keys(selectedCells.value).length)
|
const selectedCourtCount = computed(() => Object.keys(selectedCells.value).length)
|
||||||
@@ -203,8 +216,15 @@ const selectionTimeText = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const selectionAmount = computed(() => {
|
const selectionAmount = computed(() => {
|
||||||
const priceVal = parseFloat(grid.value?.unit_price || '0')
|
let total = 0
|
||||||
return (priceVal * selectedTotalSlots.value).toFixed(2)
|
for (const [courtId, slots] of Object.entries(selectedCells.value)) {
|
||||||
|
const prices = grid.value?.slot_prices?.[courtId] || []
|
||||||
|
for (const slotIndex of slots) {
|
||||||
|
const p = parseFloat(prices[slotIndex] || '0')
|
||||||
|
total += p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total.toFixed(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(loadGrid)
|
onMounted(loadGrid)
|
||||||
@@ -249,6 +269,11 @@ function getCellClass(courtId, slotIndex) {
|
|||||||
return { [`cell-${status}`]: true }
|
return { [`cell-${status}`]: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCellPrice(courtId, slotIndex) {
|
||||||
|
const prices = grid.value?.slot_prices?.[courtId] || []
|
||||||
|
return prices[slotIndex] || ''
|
||||||
|
}
|
||||||
|
|
||||||
function toggleCell(courtId, slotIndex) {
|
function toggleCell(courtId, slotIndex) {
|
||||||
const status = cellStatus.value[courtId]?.[slotIndex]
|
const status = cellStatus.value[courtId]?.[slotIndex]
|
||||||
if (status !== 'available') return
|
if (status !== 'available') return
|
||||||
@@ -530,6 +555,19 @@ defineExpose({
|
|||||||
height: 107rpx;
|
height: 107rpx;
|
||||||
border-right: 1rpx solid $bg-color-hover;
|
border-right: 1rpx solid $bg-color-hover;
|
||||||
border-bottom: 1rpx solid $bg-color-hover;
|
border-bottom: 1rpx solid $bg-color-hover;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-price {
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-color-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-selected .cell-price {
|
||||||
|
color: #FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell-available {
|
.cell-available {
|
||||||
@@ -544,6 +582,26 @@ defineExpose({
|
|||||||
background: $bg-color-hover;
|
background: $bg-color-hover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cell-locked {
|
||||||
|
background: $bg-color-hover;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-locked::after {
|
||||||
|
content: '锁';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-color-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-no_price {
|
||||||
|
background: $bg-color-hover;
|
||||||
|
}
|
||||||
|
|
||||||
.cell-selected {
|
.cell-selected {
|
||||||
background: $color-primary;
|
background: $color-primary;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user