feat: 包场预约网格改为1小时粒度+动态价格显示+锁场状态

- 时间线位置 /30 改为 /60
- 价格预览改为按格子累加 slot_prices
- 格子内显示价格(¥xx)
- 新增 locked(锁场)和 no_price(无价格)格子状态
- API映射层移除 unit_price,新增 slot_prices 和 source_type
- 确认页半小时段改为时段
This commit is contained in:
gqt
2026-07-26 21:42:46 +08:00
parent 37eb77aede
commit 30cad4e765
4 changed files with 71 additions and 14 deletions
@@ -21,7 +21,7 @@
<view class="card fee-card">
<text class="card-title">费用明细</text>
<view class="summary-row">
<text>半小时段</text>
<text>时段</text>
<text>{{ quote.unit_count }} </text>
</view>
<view class="summary-row">
@@ -61,7 +61,9 @@
class="grid-cell"
:class="getCellClass(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
v-if="currentTimeLineVisible && currentTimeTop >= 0"
@@ -146,6 +148,7 @@ const cellStatus = computed(() => {
for (const court of grid.value.courts) {
status[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++) {
const slot = grid.value.time_slots[si]
@@ -157,13 +160,23 @@ const cellStatus = computed(() => {
continue
}
const isOccupied = occs.some((occ) => {
const overlappingOcc = occs.find((occ) => {
const occStart = dayjs(occ.start_at)
const occEnd = dayjs(occ.end_at)
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
@@ -180,7 +193,7 @@ const currentTimeTop = computed(() => {
const closeMin = parseTimeToMinutes(grid.value.close_time)
const currentMin = now.value.hour() * 60 + now.value.minute()
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)
@@ -203,8 +216,15 @@ const selectionTimeText = computed(() => {
})
const selectionAmount = computed(() => {
const priceVal = parseFloat(grid.value?.unit_price || '0')
return (priceVal * selectedTotalSlots.value).toFixed(2)
let total = 0
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)
@@ -249,6 +269,11 @@ function getCellClass(courtId, slotIndex) {
return { [`cell-${status}`]: true }
}
function getCellPrice(courtId, slotIndex) {
const prices = grid.value?.slot_prices?.[courtId] || []
return prices[slotIndex] || ''
}
function toggleCell(courtId, slotIndex) {
const status = cellStatus.value[courtId]?.[slotIndex]
if (status !== 'available') return
@@ -530,6 +555,19 @@ defineExpose({
height: 107rpx;
border-right: 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 {
@@ -544,6 +582,26 @@ defineExpose({
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 {
background: $color-primary;
}