fix: 老师代约只允许已发布课表,去掉临时加开时段

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
richarjiang
2026-09-07 17:21:19 +08:00
parent f5c7b7eaac
commit 86ad9ee64f
5 changed files with 25 additions and 335 deletions

View File

@@ -47,7 +47,7 @@
<view v-else-if="filteredSlots.length === 0" class="empty-slots">
<text class="empty-title">这天还没有可安排的课表</text>
<text class="empty-sub">可以去排课管理发布或在下方加开一个时段</text>
<text class="empty-sub">请先去排课管理发布时段</text>
<view class="ghost-link" @tap="goSchedule">
<text class="ghost-link-text">前往排课管理</text>
</view>
@@ -56,7 +56,7 @@
<view v-else class="slot-list">
<view
v-for="slot in filteredSlots"
:key="slot.startTime + slot.endTime + (slot.id || 'draft')"
:key="slot.id"
class="slot-row"
:class="{ 'slot-row--disabled': !canPickSlot(slot) }"
@tap="onPickSlot(slot)"
@@ -73,25 +73,6 @@
</view>
</view>
<view class="custom-block">
<text class="custom-title">加开此时段</text>
<view class="custom-row">
<picker mode="time" :value="customStart" @change="onCustomStartChange">
<view class="custom-picker">
<text class="custom-picker-text">{{ customStart }} {{ customEnd }}</text>
<text class="custom-picker-arrow">选择开始时间</text>
</view>
</picker>
<view
class="custom-btn"
:class="{ 'custom-btn--disabled': !canArrange }"
@tap="onCustomArrange"
>
<text class="custom-btn-text">加开并安排</text>
</view>
</view>
</view>
<view v-if="confirmVisible && pendingSlot" class="mask" @tap="confirmVisible = false">
<view class="sheet" @tap.stop>
<text class="sheet-kicker">立即确认</text>
@@ -127,7 +108,7 @@ import type {
AdminMemberDetail,
ScheduleSlotPreview,
} from '@mp-pilates/shared'
import { MembershipStatus, TIME_PERIODS, TimeSlotStatus, TimeSlotSource } from '@mp-pilates/shared'
import { MembershipStatus, TIME_PERIODS, TimeSlotStatus } from '@mp-pilates/shared'
import CustomNavBar from '../../components/CustomNavBar.vue'
import DateSelector from '../../components/DateSelector.vue'
import TimePeriodFilter from '../../components/TimePeriodFilter.vue'
@@ -147,7 +128,6 @@ const selectedDate = ref(formatDate(new Date()))
const selectedPeriod = ref<PeriodKey>(null)
const slots = ref<ScheduleSlotPreview[]>([])
const slotsLoading = ref(false)
const customStart = ref('10:00')
const confirmVisible = ref(false)
const pendingSlot = ref<ScheduleSlotPreview | null>(null)
const arranging = ref(false)
@@ -175,34 +155,27 @@ const deductHint = computed(() => {
return '将立即确认该课并扣除 1 次,会员无需再确认。'
})
const publishedSlots = computed(() =>
slots.value.filter((slot): slot is ScheduleSlotPreview & { id: string } =>
Boolean(slot.isPublished && slot.id),
),
)
const filteredSlots = computed(() => {
if (!selectedPeriod.value) return slots.value
if (!selectedPeriod.value) return publishedSlots.value
const period = TIME_PERIODS[selectedPeriod.value]
return slots.value.filter((slot) => slot.startTime >= period.start && slot.startTime < period.end)
return publishedSlots.value.filter((slot) => slot.startTime >= period.start && slot.startTime < period.end)
})
const customEnd = computed(() => addHour(customStart.value))
function addHour(time: string): string {
const [hours, minutes] = time.split(':').map(Number)
const total = hours * 60 + (minutes || 0) + 60
const nextHours = Math.floor(total / 60) % 24
const nextMinutes = total % 60
return `${String(nextHours).padStart(2, '0')}:${String(nextMinutes).padStart(2, '0')}`
}
function canPickSlot(slot: ScheduleSlotPreview): boolean {
if (!canArrange.value) return false
if (isSlotPast(slot.date, slot.startTime)) return false
if (slot.status === TimeSlotStatus.CLOSED) return false
if (slot.isPublished && (slot.status === TimeSlotStatus.FULL || slot.bookedCount >= slot.capacity)) {
return false
}
if (slot.status === TimeSlotStatus.FULL || slot.bookedCount >= slot.capacity) return false
return true
}
function slotLabel(slot: ScheduleSlotPreview): string {
if (!slot.isPublished || !slot.id) return '未发布时段'
if (slot.status === TimeSlotStatus.CLOSED) return '已关闭'
if (slot.status === TimeSlotStatus.FULL || slot.bookedCount >= slot.capacity) return '已满员'
if (isSlotPast(slot.date, slot.startTime)) return '已过点'
@@ -210,7 +183,6 @@ function slotLabel(slot: ScheduleSlotPreview): string {
}
function slotActionLabel(slot: ScheduleSlotPreview): string {
if (!slot.isPublished || !slot.id) return '加开'
if (!canPickSlot(slot)) return '—'
return '安排'
}
@@ -239,63 +211,26 @@ function onDateSelect(date: string) {
loadSlots(date)
}
function onCustomStartChange(e: { detail: { value: string } }) {
customStart.value = e.detail.value
}
function goSchedule() {
uni.navigateTo({ url: '/pages/admin/schedule' })
}
function onPickSlot(slot: ScheduleSlotPreview) {
if (!canPickSlot(slot)) return
if (!canPickSlot(slot) || !slot.id) return
pendingSlot.value = slot
confirmVisible.value = true
}
function onCustomArrange() {
if (!canArrange.value) {
uni.showToast({ title: '请先开通有效会员卡', icon: 'none' })
return
}
if (customEnd.value <= customStart.value) {
uni.showToast({ title: '结束时间必须晚于开始时间', icon: 'none' })
return
}
if (isSlotPast(selectedDate.value, customStart.value)) {
uni.showToast({ title: '不能安排已经过去的时间', icon: 'none' })
return
}
pendingSlot.value = {
id: null,
date: selectedDate.value,
startTime: customStart.value,
endTime: customEnd.value,
capacity: 1,
bookedCount: 0,
status: TimeSlotStatus.OPEN,
source: TimeSlotSource.MANUAL,
templateId: null,
isPublished: false,
}
confirmVisible.value = true
}
async function confirmArrange() {
const slot = pendingSlot.value
const membership = selectedMembership.value
if (!slot || !membership || arranging.value) return
if (!slot?.id || !membership || arranging.value) return
arranging.value = true
try {
let timeSlotId = slot.id
await adminStore.arrangeMemberBooking({
userId: userId.value,
membershipId: membership.id,
...(timeSlotId ? { timeSlotId } : {}),
date: slot.date,
startTime: slot.startTime.slice(0, 5),
endTime: slot.endTime.slice(0, 5),
capacity: slot.capacity || 1,
timeSlotId: slot.id,
})
confirmVisible.value = false
uni.showToast({ title: '已安排并确认', icon: 'success' })
@@ -539,61 +474,6 @@ onMounted(async () => {
color: $accent-color;
}
.custom-block {
margin: 12rpx 24rpx 40rpx;
padding: 24rpx;
background: #fff;
border-radius: 20rpx;
border: 1rpx dashed rgba(180, 160, 130, 0.28);
}
.custom-title {
display: block;
font-size: 22rpx;
letter-spacing: 3rpx;
color: $text-hint;
margin-bottom: 16rpx;
}
.custom-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.custom-picker {
flex: 1;
display: flex;
flex-direction: column;
gap: 4rpx;
}
.custom-picker-text {
font-size: 30rpx;
font-weight: 700;
color: $text-primary;
font-family: 'DIN Alternate', 'Helvetica Neue', sans-serif;
}
.custom-picker-arrow {
font-size: 20rpx;
color: $text-hint;
}
.custom-btn {
padding: 16rpx 22rpx;
border-radius: 14rpx;
background: $brand-color;
}
.custom-btn--disabled { opacity: 0.4; }
.custom-btn-text {
font-size: 24rpx;
font-weight: 700;
color: #fff8f0;
}
.mask {
position: fixed;
inset: 0;