|
|
|
|
@ -140,8 +140,9 @@ type BookingDayRow = {
|
|
|
|
|
/** 可选;空=不限制开始,仅需在截止前 */
|
|
|
|
|
booking_opens_at: string
|
|
|
|
|
booking_deadline_at: string
|
|
|
|
|
day_quota: number
|
|
|
|
|
/** 有值时 H5 场次「人数限制」展示此处文案,否则展示名额数字 */
|
|
|
|
|
/** 未填写时表格为空,提交前须填 */
|
|
|
|
|
day_quota: number | undefined
|
|
|
|
|
/** 有值时 H5 场次「人数限制」展示此处文案,否则展示可预约人数数字 */
|
|
|
|
|
quota_note?: string
|
|
|
|
|
booked_count?: number
|
|
|
|
|
}
|
|
|
|
|
@ -222,104 +223,6 @@ function formatYmdInShanghai(iso: string | undefined | null): string {
|
|
|
|
|
}).format(d)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type TimeSlotForm = { start: string; end: string }
|
|
|
|
|
|
|
|
|
|
function parseSpecificTimeToSlots(raw?: string | null): TimeSlotForm[] {
|
|
|
|
|
if (!raw?.trim()) return []
|
|
|
|
|
const s = raw.trim()
|
|
|
|
|
if (s.startsWith('[')) {
|
|
|
|
|
try {
|
|
|
|
|
const arr = JSON.parse(s) as unknown
|
|
|
|
|
if (Array.isArray(arr)) {
|
|
|
|
|
return arr
|
|
|
|
|
.map((x): TimeSlotForm | null => {
|
|
|
|
|
if (typeof x !== 'string') return null
|
|
|
|
|
const p = String(x).split('-').map((t) => t.trim())
|
|
|
|
|
if (p.length >= 2 && p[0] && p[1]) {
|
|
|
|
|
return { start: p[0].slice(0, 5), end: p[1].slice(0, 5) }
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
})
|
|
|
|
|
.filter((x): x is TimeSlotForm => x != null)
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
/* 非 JSON */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const p = s.split('-').map((t) => t.trim())
|
|
|
|
|
if (p.length >= 2 && p[0] && p[1]) {
|
|
|
|
|
return [{ start: p[0].replace(/\s.*/, '').slice(0, 5), end: p[1].replace(/\s.*/, '').slice(0, 5) }]
|
|
|
|
|
}
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function slotsToSpecificTimePayload(slots: TimeSlotForm[]): string | null {
|
|
|
|
|
const parts = slots
|
|
|
|
|
.map((r) => {
|
|
|
|
|
const a = String(r.start || '').trim()
|
|
|
|
|
const b = String(r.end || '').trim()
|
|
|
|
|
if (!a || !b) return ''
|
|
|
|
|
return `${a}-${b}`
|
|
|
|
|
})
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
if (!parts.length) return null
|
|
|
|
|
return JSON.stringify(parts)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function activitySpecificTimeDisplay(raw?: string | null): string {
|
|
|
|
|
const slots = parseSpecificTimeToSlots(raw)
|
|
|
|
|
if (!slots.length) return String(raw || '').trim()
|
|
|
|
|
return slots.map((s) => `${s.start}-${s.end}`).join(',')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 活动起止日(YYYY-MM-DD)内的每一个日历日,东八区 */
|
|
|
|
|
function listActivityDateYmdsInclusive(startYmd: string, endYmd: string): string[] {
|
|
|
|
|
const s = String(startYmd || '').trim().slice(0, 10)
|
|
|
|
|
const e = String(endYmd || '').trim().slice(0, 10)
|
|
|
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(s) || !/^\d{4}-\d{2}-\d{2}$/.test(e) || s > e) return []
|
|
|
|
|
const fmt = new Intl.DateTimeFormat('en-CA', {
|
|
|
|
|
timeZone: 'Asia/Shanghai',
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
day: '2-digit',
|
|
|
|
|
})
|
|
|
|
|
const bump = (cur: string): string => {
|
|
|
|
|
const [y, mo, d] = cur.split('-').map(Number)
|
|
|
|
|
const ms =
|
|
|
|
|
new Date(
|
|
|
|
|
`${y}-${String(mo).padStart(2, '0')}-${String(d).padStart(2, '0')}T12:00:00+08:00`,
|
|
|
|
|
).getTime() + 86400000
|
|
|
|
|
return fmt.format(new Date(ms))
|
|
|
|
|
}
|
|
|
|
|
const out: string[] = []
|
|
|
|
|
let cur = s
|
|
|
|
|
while (cur <= e) {
|
|
|
|
|
out.push(cur)
|
|
|
|
|
if (cur === e) break
|
|
|
|
|
cur = bump(cur)
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 校验 HH:mm 且结束早于同一场次日下的开始则无意义 */
|
|
|
|
|
function isValidHmPair(startHm: string, endHm: string): boolean {
|
|
|
|
|
const p = /^(\d{1,2}):(\d{2})$/
|
|
|
|
|
const a = p.exec(String(startHm || '').trim())
|
|
|
|
|
const b = p.exec(String(endHm || '').trim())
|
|
|
|
|
if (!a || !b) return false
|
|
|
|
|
const asm = (+a[1]) * 60 + (+a[2])
|
|
|
|
|
const bsm = (+b[1]) * 60 + (+b[2])
|
|
|
|
|
return bsm > asm && asm >= 0 && asm < 1440 && bsm <= 1439
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function combineYmdAndHmToDatetime(ymd: string, hm: string): string {
|
|
|
|
|
const p = /^(\d{1,2}):(\d{2})$/.exec(String(hm || '').trim())
|
|
|
|
|
if (!p) return `${ymd} 09:00:00`
|
|
|
|
|
const h = Math.min(23, Math.max(0, +p[1]))
|
|
|
|
|
const mi = Math.min(59, Math.max(0, +p[2]))
|
|
|
|
|
return `${ymd} ${String(h).padStart(2, '0')}:${String(mi).padStart(2, '0')}:00`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseShanghaiLocalToMs(datetimeYmdHmss: string): number {
|
|
|
|
|
let s = String(datetimeYmdHmss || '').trim().replace(' ', 'T')
|
|
|
|
|
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(s)) s += ':00'
|
|
|
|
|
@ -327,120 +230,6 @@ function parseShanghaiLocalToMs(datetimeYmdHmss: string): number {
|
|
|
|
|
return new Date(`${s}+08:00`).getTime()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatDatetimeShanghai(ms: number): string {
|
|
|
|
|
if (!Number.isFinite(ms)) return ''
|
|
|
|
|
const d = new Date(ms)
|
|
|
|
|
const p = new Intl.DateTimeFormat('en-CA', {
|
|
|
|
|
timeZone: 'Asia/Shanghai',
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
day: '2-digit',
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
minute: '2-digit',
|
|
|
|
|
second: '2-digit',
|
|
|
|
|
hour12: false,
|
|
|
|
|
})
|
|
|
|
|
.formatToParts(d)
|
|
|
|
|
.reduce<Record<string, string>>((acc, x) => {
|
|
|
|
|
if (x.type !== 'literal') acc[x.type] = x.value
|
|
|
|
|
return acc
|
|
|
|
|
}, {})
|
|
|
|
|
const ymd = `${p.year}-${p.month}-${p.day}`
|
|
|
|
|
const t = `${(p.hour || '').padStart(2, '0')}:${(p.minute || '').padStart(2, '0')}:${(p.second || '00').padStart(2, '0')}`
|
|
|
|
|
return `${ymd} ${t}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildSessionDatetimePairsFromActivityForm(): Array<{ session_start_at: string; session_end_at: string }> {
|
|
|
|
|
const dates = listActivityDateYmdsInclusive(form.start_at, form.end_at)
|
|
|
|
|
const slots = form.specific_time_slots.filter((r) =>
|
|
|
|
|
isValidHmPair(String(r.start || '').trim(), String(r.end || '').trim()),
|
|
|
|
|
)
|
|
|
|
|
const pairs: Array<{ session_start_at: string; session_end_at: string }> = []
|
|
|
|
|
for (const ymd of dates) {
|
|
|
|
|
for (const sl of slots) {
|
|
|
|
|
pairs.push({
|
|
|
|
|
session_start_at: combineYmdAndHmToDatetime(ymd, sl.start),
|
|
|
|
|
session_end_at: combineYmdAndHmToDatetime(ymd, sl.end),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return pairs
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 场次开始调整后:预约截止不再自动填充;仅当已填写截止时与预约开始一起做规则校正 */
|
|
|
|
|
function ensureOnlineBookingTimesConsistent(row: BookingDayRow): void {
|
|
|
|
|
if (normalizeReservationKind(form.reservation_type) !== 'online') return
|
|
|
|
|
const sessStartMs = parseShanghaiLocalToMs(row.session_start_at)
|
|
|
|
|
if (!Number.isFinite(sessStartMs)) return
|
|
|
|
|
const ymd = row.session_start_at.trim().slice(0, 10)
|
|
|
|
|
const dlRaw = String(row.booking_deadline_at || '').trim()
|
|
|
|
|
if (!dlRaw) {
|
|
|
|
|
row.booking_opens_at = `${ymd} 00:00:00`
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
let deadlineMs = parseShanghaiLocalToMs(dlRaw)
|
|
|
|
|
if (!Number.isFinite(deadlineMs)) return
|
|
|
|
|
deadlineMs = Math.min(deadlineMs, sessStartMs - 60 * 1000)
|
|
|
|
|
row.booking_deadline_at = formatDatetimeShanghai(deadlineMs)
|
|
|
|
|
let opensMs = parseShanghaiLocalToMs(`${ymd} 00:00:00`)
|
|
|
|
|
if (!Number.isFinite(opensMs) || opensMs > deadlineMs) {
|
|
|
|
|
opensMs = deadlineMs - 60 * 1000
|
|
|
|
|
}
|
|
|
|
|
row.booking_opens_at = formatDatetimeShanghai(opensMs)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const suppressSessionScheduleSync = ref(0)
|
|
|
|
|
|
|
|
|
|
const MAX_AUTO_SESSION_ROWS = 200
|
|
|
|
|
|
|
|
|
|
/** 根据「活动日期 + 具体时间」回填各场次的开始/结束(必要时增补空场次行) */
|
|
|
|
|
function syncBookingSessionTimesFromSchedule(): void {
|
|
|
|
|
if (!visible.value || !canSaveSessionsWithActivity.value) return
|
|
|
|
|
if (suppressSessionScheduleSync.value > 0) return
|
|
|
|
|
const pairs = buildSessionDatetimePairsFromActivityForm()
|
|
|
|
|
if (!pairs.length) return
|
|
|
|
|
|
|
|
|
|
const rt = normalizeReservationKind(form.reservation_type)
|
|
|
|
|
while (
|
|
|
|
|
pairs.length > bookingForm.days.length
|
|
|
|
|
&& bookingForm.days.length < MAX_AUTO_SESSION_ROWS
|
|
|
|
|
) {
|
|
|
|
|
addBookingDayRow()
|
|
|
|
|
}
|
|
|
|
|
const n = Math.min(pairs.length, bookingForm.days.length)
|
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
|
|
|
const row = bookingForm.days[i]
|
|
|
|
|
row.session_start_at = pairs[i].session_start_at
|
|
|
|
|
row.session_end_at = pairs[i].session_end_at
|
|
|
|
|
if (rt === 'online') ensureOnlineBookingTimesConsistent(row)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 与 sync 解耦的驱动签名:suppress 或关弹窗时为 __off__,避免 deep watch + 新对象导致递归 effect */
|
|
|
|
|
const sessionScheduleDrivingSignature = computed(() => {
|
|
|
|
|
if (!visible.value || suppressSessionScheduleSync.value > 0) return '__off__'
|
|
|
|
|
const slotKey = form.specific_time_slots
|
|
|
|
|
.map((x) => `${String(x.start ?? '').trim()}|${String(x.end ?? '').trim()}`)
|
|
|
|
|
.join(';')
|
|
|
|
|
return `${form.start_at}\n${form.end_at}\n${form.reservation_type}\n${slotKey}`
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
let inSessionScheduleSync = false
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
sessionScheduleDrivingSignature,
|
|
|
|
|
(sig) => {
|
|
|
|
|
if (sig === '__off__' || inSessionScheduleSync) return
|
|
|
|
|
inSessionScheduleSync = true
|
|
|
|
|
try {
|
|
|
|
|
syncBookingSessionTimesFromSchedule()
|
|
|
|
|
} finally {
|
|
|
|
|
inSessionScheduleSync = false
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{ flush: 'post' },
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
function normalizeReservationKind(t?: string | null): string {
|
|
|
|
|
const s = String(t || '').trim()
|
|
|
|
|
if (!s || s === 'online' || s === 'none' || s === 'paid_study') return s || 'online'
|
|
|
|
|
@ -467,7 +256,7 @@ function formatActivityTableDateRange(record: Activity): string {
|
|
|
|
|
return s || e || '-'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 新增/编辑弹窗场次步骤顶栏:带出当前表单活动日期与时间说明 */
|
|
|
|
|
/** 新增/编辑弹窗场次步骤顶栏:带出当前表单活动日期 */
|
|
|
|
|
const activityBookingStepIntro = computed(() => {
|
|
|
|
|
const r: Activity = {
|
|
|
|
|
id: editId.value ?? 0,
|
|
|
|
|
@ -478,9 +267,7 @@ const activityBookingStepIntro = computed(() => {
|
|
|
|
|
is_active: !!form.is_active,
|
|
|
|
|
}
|
|
|
|
|
const datePart = formatActivityTableDateRange(r)
|
|
|
|
|
const timePart =
|
|
|
|
|
activitySpecificTimeDisplay(form.specific_time).trim() || '—'
|
|
|
|
|
return `本次活动日期 ${datePart},具体时间 ${timePart},请配置场次,保存时将一并提交活动信息与场次。`
|
|
|
|
|
return `本次活动日期 ${datePart},请逐项手动填写各场次的场次开始、场次结束${normalizeReservationKind(form.reservation_type) === 'online' ? '、预约开始与预约截止' : ''}及可预约人数;保存时将一并提交活动信息与场次。`
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
/** 审核弹窗内「提交人」展示文案 */
|
|
|
|
|
@ -516,8 +303,6 @@ const form = reactive({
|
|
|
|
|
check_in_meeting_point: '',
|
|
|
|
|
lat: undefined as number | undefined,
|
|
|
|
|
lng: undefined as number | undefined,
|
|
|
|
|
specific_time: '',
|
|
|
|
|
specific_time_slots: [] as TimeSlotForm[],
|
|
|
|
|
external_url: '',
|
|
|
|
|
title: '',
|
|
|
|
|
contact_name: '',
|
|
|
|
|
@ -552,9 +337,35 @@ const formErrors = reactive<Record<string, string>>({
|
|
|
|
|
detail_html: '',
|
|
|
|
|
contact_name: '',
|
|
|
|
|
contact_phone: '',
|
|
|
|
|
specific_time_slots: '',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
/** openCreate/openEdit 批量赋值时不要触发「切换活动性质清空文案」逻辑 */
|
|
|
|
|
const suppressReservationTypeBookingReset = ref(0)
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => form.reservation_type,
|
|
|
|
|
() => {
|
|
|
|
|
if (!visible.value || suppressReservationTypeBookingReset.value > 0) return
|
|
|
|
|
const z = normalizeReservationKind(form.reservation_type)
|
|
|
|
|
if (z === 'online') {
|
|
|
|
|
form.booking_method_note = '平台预约'
|
|
|
|
|
form.fee_note = '免费'
|
|
|
|
|
formErrors.booking_method_note = ''
|
|
|
|
|
formErrors.fee_note = ''
|
|
|
|
|
} else if (z === 'none') {
|
|
|
|
|
form.booking_method_note = ''
|
|
|
|
|
form.fee_note = '免费'
|
|
|
|
|
formErrors.booking_method_note = ''
|
|
|
|
|
formErrors.fee_note = ''
|
|
|
|
|
} else if (z === 'paid_study') {
|
|
|
|
|
form.booking_method_note = ''
|
|
|
|
|
form.fee_note = ''
|
|
|
|
|
formErrors.booking_method_note = ''
|
|
|
|
|
formErrors.fee_note = ''
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const activityDateRange = computed({
|
|
|
|
|
get(): [string, string] | undefined {
|
|
|
|
|
if (form.start_at && form.end_at) return [form.start_at, form.end_at]
|
|
|
|
|
@ -610,10 +421,13 @@ const showBookingAudienceFields = computed(
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const sessionRulesHint = computed(() => {
|
|
|
|
|
const groupNote =
|
|
|
|
|
'团体每单最少人数以及团体每单最多人数不是场次的可预约人数,仅限制每场次团体用户可预约的人数;'
|
|
|
|
|
const sameDay = '每场次的开始时间与结束时间须为同一天内;'
|
|
|
|
|
if (normalizeReservationKind(form.reservation_type) === 'online') {
|
|
|
|
|
return '每场次的开始时间与结束时间须为同一天内;预约开始、预约截止时间须填写;预约截止时间须早于场次开始时间;预约开始不得晚于预约截止。更改名额不可低于已约人数。'
|
|
|
|
|
return `${groupNote}${sameDay}场次开始、场次结束、预约开始、预约截止时间须填写;预约截止时间须早于场次开始时间;预约开始不得晚于预约截止;更改可预约人数不可低于已约人数;`
|
|
|
|
|
}
|
|
|
|
|
return '每场次的开始时间与结束时间须为同一天内'
|
|
|
|
|
return `${groupNote}${sameDay}场次开始、场次结束须填写;更改可预约人数不可低于已约人数;`
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const sessionTableScrollX = computed(() =>
|
|
|
|
|
@ -622,46 +436,14 @@ const sessionTableScrollX = computed(() =>
|
|
|
|
|
|
|
|
|
|
const activityModalTitle = computed(() => (isCreate.value ? '新增活动' : '编辑活动'))
|
|
|
|
|
|
|
|
|
|
function unifiedBookingContextActivity(): Activity | null {
|
|
|
|
|
if (!form.start_at && !form.end_at) return null
|
|
|
|
|
return {
|
|
|
|
|
id: editId.value ?? 0,
|
|
|
|
|
venue_id: Number(form.venue_id) || 0,
|
|
|
|
|
title: form.title,
|
|
|
|
|
start_at: form.start_at,
|
|
|
|
|
end_at: form.end_at,
|
|
|
|
|
reservation_type: form.reservation_type,
|
|
|
|
|
is_active: !!form.is_active,
|
|
|
|
|
schedule_status: form.display_schedule_status,
|
|
|
|
|
} as Activity
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function defaultSessionTimesForActivity(act: Activity | null): { start: string; end: string } {
|
|
|
|
|
const d = act?.start_at
|
|
|
|
|
? formatYmdInShanghai(String(act.start_at))
|
|
|
|
|
: new Intl.DateTimeFormat('en-CA', {
|
|
|
|
|
timeZone: 'Asia/Shanghai',
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
day: '2-digit',
|
|
|
|
|
}).format(new Date())
|
|
|
|
|
return {
|
|
|
|
|
start: `${d} 09:00:00`,
|
|
|
|
|
end: `${d} 11:00:00`,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addBookingDayRow() {
|
|
|
|
|
const act = unifiedBookingContextActivity()
|
|
|
|
|
const { start, end } = defaultSessionTimesForActivity(act)
|
|
|
|
|
const dayStr = start.slice(0, 10)
|
|
|
|
|
bookingForm.days.push({
|
|
|
|
|
session_name: `场次 ${bookingForm.days.length + 1}`,
|
|
|
|
|
session_start_at: start,
|
|
|
|
|
session_end_at: end,
|
|
|
|
|
booking_opens_at: `${dayStr} 00:00:00`,
|
|
|
|
|
session_start_at: '',
|
|
|
|
|
session_end_at: '',
|
|
|
|
|
booking_opens_at: '',
|
|
|
|
|
booking_deadline_at: '',
|
|
|
|
|
day_quota: 1,
|
|
|
|
|
day_quota: undefined,
|
|
|
|
|
quota_note: '',
|
|
|
|
|
booked_count: 0,
|
|
|
|
|
})
|
|
|
|
|
@ -677,13 +459,52 @@ function removeBookingDayRow(index: number) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeBookingDayTime(raw: unknown): string {
|
|
|
|
|
const s = String(raw ?? '').trim().replace('T', ' ')
|
|
|
|
|
let s = String(raw ?? '').trim().replace('T', ' ')
|
|
|
|
|
if (!s) return ''
|
|
|
|
|
return s.length >= 19 ? s.slice(0, 19) : s
|
|
|
|
|
s = s.replace(/\.\d+Z?$/, '').trim()
|
|
|
|
|
const m = /^(\d{4}-\d{2}-\d{2})\s+(\d{1,2}):(\d{2})(?::\d{2})?/.exec(s)
|
|
|
|
|
if (m) {
|
|
|
|
|
const h = Math.min(23, Math.max(0, +m[2]))
|
|
|
|
|
const mi = Math.min(59, Math.max(0, +m[3]))
|
|
|
|
|
return `${m[1]} ${String(h).padStart(2, '0')}:${String(mi).padStart(2, '0')}`
|
|
|
|
|
}
|
|
|
|
|
return s.length > 16 ? s.slice(0, 16) : s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sessionDatetimeYmdPart(raw: string): string {
|
|
|
|
|
const m = /^(\d{4}-\d{2}-\d{2})/.exec(String(raw ?? '').trim())
|
|
|
|
|
return m ? m[1] : ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 日历格对应本地日期的 YYYY-MM-DD(与活动日期控件一致) */
|
|
|
|
|
function calendarYmdFromPickerDate(d: Date): string {
|
|
|
|
|
const y = d.getFullYear()
|
|
|
|
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
|
|
|
|
const day = String(d.getDate()).padStart(2, '0')
|
|
|
|
|
return `${y}-${m}-${day}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 场次开始/结束:仅可选择活动日期范围内(含首尾)的日历日(Arco 传入多为 Dayjs) */
|
|
|
|
|
function sessionDayDisabledDate(cell?: unknown): boolean {
|
|
|
|
|
if (cell == null) return false
|
|
|
|
|
const startYmd = String(form.start_at || '').trim().slice(0, 10)
|
|
|
|
|
const endYmd = String(form.end_at || '').trim().slice(0, 10)
|
|
|
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(startYmd) || !/^\d{4}-\d{2}-\d{2}$/.test(endYmd)) return false
|
|
|
|
|
|
|
|
|
|
const c = cell as { format?: (f: string) => string }
|
|
|
|
|
let curYmd: string
|
|
|
|
|
if (typeof c.format === 'function') {
|
|
|
|
|
curYmd = c.format('YYYY-MM-DD')
|
|
|
|
|
} else if (cell instanceof Date) {
|
|
|
|
|
curYmd = calendarYmdFromPickerDate(cell)
|
|
|
|
|
} else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(curYmd)) return false
|
|
|
|
|
return curYmd < startYmd || curYmd > endYmd
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadBookingForEditActivity(activityId: number) {
|
|
|
|
|
suppressSessionScheduleSync.value += 1
|
|
|
|
|
activityModalBookingLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const { data } = await http.get(`/activities/${activityId}/booking-settings`)
|
|
|
|
|
@ -696,6 +517,7 @@ async function loadBookingForEditActivity(activityId: number) {
|
|
|
|
|
const days = Array.isArray(data?.days) ? data.days : []
|
|
|
|
|
bookingForm.days = days.map((d: Record<string, unknown>) => {
|
|
|
|
|
const nid = Number(d.id)
|
|
|
|
|
const dq = Number(d.day_quota)
|
|
|
|
|
return {
|
|
|
|
|
id: Number.isFinite(nid) && nid > 0 ? nid : undefined,
|
|
|
|
|
session_name: String(d.session_name ?? ''),
|
|
|
|
|
@ -703,7 +525,7 @@ async function loadBookingForEditActivity(activityId: number) {
|
|
|
|
|
session_end_at: normalizeBookingDayTime(d.session_end_at),
|
|
|
|
|
booking_opens_at: normalizeBookingDayTime(d.booking_opens_at),
|
|
|
|
|
booking_deadline_at: normalizeBookingDayTime(d.booking_deadline_at),
|
|
|
|
|
day_quota: Math.max(0, Number(d.day_quota) || 0),
|
|
|
|
|
day_quota: Number.isFinite(dq) && dq >= 1 ? dq : undefined,
|
|
|
|
|
quota_note: String(d.quota_note ?? ''),
|
|
|
|
|
booked_count: Math.max(0, Number(d.booked_count) || 0),
|
|
|
|
|
}
|
|
|
|
|
@ -713,9 +535,6 @@ async function loadBookingForEditActivity(activityId: number) {
|
|
|
|
|
throw e
|
|
|
|
|
} finally {
|
|
|
|
|
activityModalBookingLoading.value = false
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
suppressSessionScheduleSync.value -= 1
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -733,8 +552,34 @@ function validateBookingFormInternal(): boolean {
|
|
|
|
|
Message.warning(`第 ${i + 1} 行请填写场次名称`)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if (!d.session_start_at || !d.session_end_at) {
|
|
|
|
|
Message.warning(`第 ${i + 1} 行请填写场次开始与结束时间`)
|
|
|
|
|
const startN = normalizeBookingDayTime(d.session_start_at)
|
|
|
|
|
const endN = normalizeBookingDayTime(d.session_end_at)
|
|
|
|
|
if (!String(startN).trim() || !String(endN).trim()) {
|
|
|
|
|
Message.warning(`第 ${i + 1} 行请填写场次开始与场次结束时间`)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
const yStart = sessionDatetimeYmdPart(startN)
|
|
|
|
|
const yEnd = sessionDatetimeYmdPart(endN)
|
|
|
|
|
if (!yStart || !yEnd || yStart !== yEnd) {
|
|
|
|
|
Message.warning(`第 ${i + 1} 行场次开始与场次结束须为同一天内`)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
const actStart = String(form.start_at || '').trim().slice(0, 10)
|
|
|
|
|
const actEnd = String(form.end_at || '').trim().slice(0, 10)
|
|
|
|
|
if (/^\d{4}-\d{2}-\d{2}$/.test(actStart) && /^\d{4}-\d{2}-\d{2}$/.test(actEnd)) {
|
|
|
|
|
if (yStart < actStart || yStart > actEnd) {
|
|
|
|
|
Message.warning(`第 ${i + 1} 行场次日期须落在活动日期 ${actStart}~${actEnd} 范围内`)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const sm = parseShanghaiLocalToMs(startN)
|
|
|
|
|
const em = parseShanghaiLocalToMs(endN)
|
|
|
|
|
if (!Number.isFinite(sm) || !Number.isFinite(em)) {
|
|
|
|
|
Message.warning(`第 ${i + 1} 行场次开始或场次结束时间格式无效`)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if (em <= sm) {
|
|
|
|
|
Message.warning(`第 ${i + 1} 行场次结束时间须晚于场次开始时间`)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if (rt === 'online' && !String(d.booking_deadline_at || '').trim()) {
|
|
|
|
|
@ -745,11 +590,35 @@ function validateBookingFormInternal(): boolean {
|
|
|
|
|
Message.warning(`第 ${i + 1} 行请填写预约开始时间`)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if (rt === 'online') {
|
|
|
|
|
const opensN = normalizeBookingDayTime(d.booking_opens_at)
|
|
|
|
|
const dlN = normalizeBookingDayTime(d.booking_deadline_at)
|
|
|
|
|
const opensMs = parseShanghaiLocalToMs(opensN)
|
|
|
|
|
const dlMs = parseShanghaiLocalToMs(dlN)
|
|
|
|
|
if (!Number.isFinite(opensMs) || !Number.isFinite(dlMs)) {
|
|
|
|
|
Message.warning(`第 ${i + 1} 行预约开始或预约截止时间格式无效`)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if (opensMs > dlMs) {
|
|
|
|
|
Message.warning(`第 ${i + 1} 行预约开始不得晚于预约截止`)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if (dlMs >= sm) {
|
|
|
|
|
Message.warning(`第 ${i + 1} 行预约截止时间须早于场次开始时间`)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const booked = Math.max(0, Number(d.booked_count) || 0)
|
|
|
|
|
const minQ = Math.max(1, booked)
|
|
|
|
|
if (!d.day_quota || d.day_quota < minQ) {
|
|
|
|
|
const quotaRaw = d.day_quota
|
|
|
|
|
if (quotaRaw === undefined || quotaRaw === null || !Number.isFinite(Number(quotaRaw))) {
|
|
|
|
|
Message.warning(`第 ${i + 1} 行请填写可预约人数`)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
const quotaNum = Math.floor(Number(quotaRaw))
|
|
|
|
|
if (quotaNum < minQ) {
|
|
|
|
|
Message.warning(
|
|
|
|
|
`第 ${i + 1} 行预约名额须≥${minQ}${booked > 0 ? `(已约 ${booked} 人)` : ''}`,
|
|
|
|
|
`第 ${i + 1} 行可预约人数须≥${minQ}${booked > 0 ? `(已约 ${booked} 人)` : ''}`,
|
|
|
|
|
)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
@ -758,11 +627,11 @@ function validateBookingFormInternal(): boolean {
|
|
|
|
|
aud !== 'individual'
|
|
|
|
|
&& bookingForm.max_people_per_order < bookingForm.min_people_per_order
|
|
|
|
|
) {
|
|
|
|
|
Message.warning('每单最多人数必须大于等于每单最少人数')
|
|
|
|
|
Message.warning('团体每单最多人数必须大于等于团体每单最少人数')
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if (aud !== 'individual' && bookingForm.max_people_per_order === 1) {
|
|
|
|
|
Message.warning('团体或个人+团体时,每单最多人数不可为 1,且须大于等于每单最少人数')
|
|
|
|
|
Message.warning('团体或个人+团体时,团体每单最多人数不可为 1,且须大于等于团体每单最少人数')
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
@ -785,7 +654,7 @@ function buildBookingPayload(): Record<string, unknown> {
|
|
|
|
|
session_start_at: d.session_start_at,
|
|
|
|
|
session_end_at: d.session_end_at,
|
|
|
|
|
booking_deadline_at: deadlineIn || null,
|
|
|
|
|
day_quota: d.day_quota,
|
|
|
|
|
day_quota: Math.floor(Number(d.day_quota)),
|
|
|
|
|
quota_note: (d.quota_note || '').trim() || null,
|
|
|
|
|
}
|
|
|
|
|
if (rt === 'online') {
|
|
|
|
|
@ -909,14 +778,6 @@ function formSignature() {
|
|
|
|
|
return JSON.stringify(form)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addSpecificTimeRow() {
|
|
|
|
|
form.specific_time_slots.push({ start: '09:00', end: '10:00' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeSpecificTimeRow(i: number) {
|
|
|
|
|
form.specific_time_slots.splice(i, 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function editFlowSignature() {
|
|
|
|
|
return JSON.stringify({
|
|
|
|
|
form: formSignature(),
|
|
|
|
|
@ -1646,6 +1507,8 @@ async function loadMe() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openCreate() {
|
|
|
|
|
suppressReservationTypeBookingReset.value += 1
|
|
|
|
|
try {
|
|
|
|
|
isCreate.value = true
|
|
|
|
|
editId.value = null
|
|
|
|
|
Object.keys(formErrors).forEach((key) => { formErrors[key] = '' })
|
|
|
|
|
@ -1657,8 +1520,6 @@ function openCreate() {
|
|
|
|
|
form.check_in_meeting_point = ''
|
|
|
|
|
form.lat = undefined
|
|
|
|
|
form.lng = undefined
|
|
|
|
|
form.specific_time = ''
|
|
|
|
|
form.specific_time_slots = []
|
|
|
|
|
form.external_url = ''
|
|
|
|
|
form.title = ''
|
|
|
|
|
form.contact_name = ''
|
|
|
|
|
@ -1682,10 +1543,17 @@ function openCreate() {
|
|
|
|
|
resetEditors()
|
|
|
|
|
captureFormBaseline()
|
|
|
|
|
visible.value = true
|
|
|
|
|
} finally {
|
|
|
|
|
void nextTick(() => {
|
|
|
|
|
suppressReservationTypeBookingReset.value -= 1
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openEdit(row: Activity) {
|
|
|
|
|
if (!assertCanOpenActivityEditor(row)) return
|
|
|
|
|
suppressReservationTypeBookingReset.value += 1
|
|
|
|
|
try {
|
|
|
|
|
isCreate.value = false
|
|
|
|
|
editId.value = row.id
|
|
|
|
|
bookingForm.booking_audience = 'both'
|
|
|
|
|
@ -1711,8 +1579,6 @@ function openEdit(row: Activity) {
|
|
|
|
|
form.check_in_meeting_point = row.check_in_meeting_point || ''
|
|
|
|
|
form.lat = parseCoord(row.lat)
|
|
|
|
|
form.lng = parseCoord(row.lng)
|
|
|
|
|
form.specific_time = row.specific_time || ''
|
|
|
|
|
form.specific_time_slots = parseSpecificTimeToSlots(row.specific_time)
|
|
|
|
|
form.external_url = ''
|
|
|
|
|
form.title = row.title
|
|
|
|
|
form.contact_name = row.contact_name ?? ''
|
|
|
|
|
@ -1732,6 +1598,11 @@ function openEdit(row: Activity) {
|
|
|
|
|
captureFormBaseline()
|
|
|
|
|
visible.value = true
|
|
|
|
|
void loadBookingForEditActivity(row.id).catch(() => undefined)
|
|
|
|
|
} finally {
|
|
|
|
|
void nextTick(() => {
|
|
|
|
|
suppressReservationTypeBookingReset.value -= 1
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onSearch() {
|
|
|
|
|
@ -1883,16 +1754,6 @@ function validateForm(): boolean {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (reservationTypeSupportsSessionSettings(form.reservation_type)) {
|
|
|
|
|
const hasValidSlot = form.specific_time_slots.some((row) =>
|
|
|
|
|
isValidHmPair(String(row.start || '').trim(), String(row.end || '').trim()))
|
|
|
|
|
if (!hasValidSlot) {
|
|
|
|
|
formErrors.specific_time_slots =
|
|
|
|
|
'请至少添加一个具体时间时段,并完整填写开始与结束(格式 HH:mm,结束须晚于开始)'
|
|
|
|
|
isValid = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return isValid
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -1904,7 +1765,7 @@ function onCancelUnifiedModal() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 已通过校验后的实际保存(由确认弹窗的 onBeforeOk 调用) */
|
|
|
|
|
async function executeUnifiedActivitySave(specificPayload: string | null): Promise<void> {
|
|
|
|
|
async function executeUnifiedActivitySave(): Promise<void> {
|
|
|
|
|
const shouldSyncBooking =
|
|
|
|
|
canSaveSessionsWithActivity.value && reservationTypeSupportsSessionSettings(form.reservation_type)
|
|
|
|
|
|
|
|
|
|
@ -1916,7 +1777,6 @@ async function executeUnifiedActivitySave(specificPayload: string | null): Promi
|
|
|
|
|
reservation_type: effectiveRt,
|
|
|
|
|
location: form.location.trim(),
|
|
|
|
|
check_in_meeting_point: form.check_in_meeting_point.trim() || null,
|
|
|
|
|
specific_time: specificPayload,
|
|
|
|
|
external_url: null,
|
|
|
|
|
title: form.title.trim(),
|
|
|
|
|
summary: form.summary.trim() || null,
|
|
|
|
|
@ -1940,6 +1800,7 @@ async function executeUnifiedActivitySave(specificPayload: string | null): Promi
|
|
|
|
|
payload.offline_reservation_method = null
|
|
|
|
|
payload.booking_method_note = form.booking_method_note.trim() || null
|
|
|
|
|
payload.ticket_fee_note = form.fee_note.trim() || null
|
|
|
|
|
payload.specific_time = null
|
|
|
|
|
} else if (!isCreate.value && editContextRowSnapshot.value) {
|
|
|
|
|
const snap = editContextRowSnapshot.value
|
|
|
|
|
payload.offline_reservation_method = snap.offline_reservation_method ?? null
|
|
|
|
|
@ -1983,8 +1844,6 @@ function submitUnifiedActivity() {
|
|
|
|
|
Message.warning('请填写所有必填项')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const specificPayload = slotsToSpecificTimePayload(form.specific_time_slots)
|
|
|
|
|
form.specific_time = specificPayload ?? ''
|
|
|
|
|
|
|
|
|
|
const shouldSyncBooking =
|
|
|
|
|
canSaveSessionsWithActivity.value && reservationTypeSupportsSessionSettings(form.reservation_type)
|
|
|
|
|
@ -2001,7 +1860,7 @@ function submitUnifiedActivity() {
|
|
|
|
|
async onBeforeOk() {
|
|
|
|
|
saving.value = true
|
|
|
|
|
try {
|
|
|
|
|
await executeUnifiedActivitySave(specificPayload)
|
|
|
|
|
await executeUnifiedActivitySave()
|
|
|
|
|
return true
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
Message.error(error?.response?.data?.message ?? '保存失败')
|
|
|
|
|
@ -2232,6 +2091,14 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
<span class="activity-audit-inline-label">活动名称</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ auditActivityRecord.title || '—' }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">活动性质</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ reservationTypeLabel(auditActivityRecord.reservation_type) }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">{{ auditBookingWayLabel(auditActivityRecord) }}</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ auditActivityRecord.booking_method_note?.trim() || '—' }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">举办场馆</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ auditActivityRecord.venue?.name || '—' }}</div>
|
|
|
|
|
@ -2244,29 +2111,13 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
<span class="activity-audit-inline-label">联系电话</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ auditActivityRecord.contact_phone || '—' }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">活动日期</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ formatActivityTableDateRange(auditActivityRecord) }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">具体时间</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ activitySpecificTimeDisplay(auditActivityRecord.specific_time) || '—' }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">活动性质</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ reservationTypeLabel(auditActivityRecord.reservation_type) }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">{{ auditBookingWayLabel(auditActivityRecord) }}</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ auditActivityRecord.booking_method_note?.trim() || '—' }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">费用</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ auditFeeDisplay(auditActivityRecord) }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="auditShowExternalUrl(auditActivityRecord)" class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">外链地址</span>
|
|
|
|
|
<div class="activity-audit-inline-value activity-audit-break">{{ auditActivityRecord.external_url || '—' }}</div>
|
|
|
|
|
<div class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">活动日期</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ formatActivityTableDateRange(auditActivityRecord) }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-audit-inline-row activity-audit-inline-row--tags">
|
|
|
|
|
<span class="activity-audit-inline-label">标签</span>
|
|
|
|
|
@ -2279,6 +2130,10 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="auditShowExternalUrl(auditActivityRecord)" class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">外链地址</span>
|
|
|
|
|
<div class="activity-audit-inline-value activity-audit-break">{{ auditActivityRecord.external_url || '—' }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">活动地点</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ auditActivityRecord.location?.trim() ? auditActivityRecord.location : (auditActivityRecord.address || '—') }}</div>
|
|
|
|
|
@ -2333,18 +2188,6 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="activity-audit-stack">
|
|
|
|
|
<div class="activity-audit-stack__label">活动详情</div>
|
|
|
|
|
<div class="activity-audit-stack__body">
|
|
|
|
|
<div
|
|
|
|
|
v-if="auditActivityRecord.detail_html?.trim()"
|
|
|
|
|
class="activity-audit-rich"
|
|
|
|
|
v-html="auditActivityRecord.detail_html"
|
|
|
|
|
/>
|
|
|
|
|
<span v-else class="activity-audit-muted">—</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
v-if="auditActivityRecord && reservationTypeSupportsSessionSettings(auditActivityRecord.reservation_type)"
|
|
|
|
|
class="activity-audit-stack"
|
|
|
|
|
@ -2375,7 +2218,7 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
data-index="booking_deadline_at"
|
|
|
|
|
:width="184"
|
|
|
|
|
/>
|
|
|
|
|
<a-table-column title="名额" data-index="day_quota" :width="100" />
|
|
|
|
|
<a-table-column title="可预约人数" data-index="day_quota" :width="100" />
|
|
|
|
|
<a-table-column title="说明" :width="220" :ellipsis="true" :tooltip="true">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<span>{{ ((record as BookingDayRow).quota_note || '').trim() || '—' }}</span>
|
|
|
|
|
@ -2393,6 +2236,18 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="activity-audit-stack">
|
|
|
|
|
<div class="activity-audit-stack__label">活动详情</div>
|
|
|
|
|
<div class="activity-audit-stack__body">
|
|
|
|
|
<div
|
|
|
|
|
v-if="auditActivityRecord.detail_html?.trim()"
|
|
|
|
|
class="activity-audit-rich"
|
|
|
|
|
v-html="auditActivityRecord.detail_html"
|
|
|
|
|
/>
|
|
|
|
|
<span v-else class="activity-audit-muted">—</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
v-if="
|
|
|
|
|
auditActivityRecord.reservation_type === 'online'
|
|
|
|
|
@ -2602,7 +2457,7 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
</template>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
<a-row :gutter="16" class="admin-modal-form__full">
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
<a-col :xs="24" :md="10">
|
|
|
|
|
<a-form-item
|
|
|
|
|
label="活动日期"
|
|
|
|
|
required
|
|
|
|
|
@ -2620,54 +2475,7 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
</template>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
<a-form-item
|
|
|
|
|
:required="reservationTypeSupportsSessionSettings(form.reservation_type)"
|
|
|
|
|
:validate-status="formErrors.specific_time_slots ? 'error' : undefined"
|
|
|
|
|
>
|
|
|
|
|
<template #label>
|
|
|
|
|
<span class="activity-time-label-with-action">
|
|
|
|
|
<span>具体时间</span>
|
|
|
|
|
<a-button type="primary" size="mini" class="activity-add-time-slot-btn" @click="addSpecificTimeRow">
|
|
|
|
|
添加时段
|
|
|
|
|
</a-button>
|
|
|
|
|
</span>
|
|
|
|
|
</template>
|
|
|
|
|
<a-space direction="vertical" fill style="width: 100%">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(slot, sidx) in form.specific_time_slots"
|
|
|
|
|
:key="'slot-' + sidx"
|
|
|
|
|
class="activity-time-slot-row"
|
|
|
|
|
>
|
|
|
|
|
<a-time-picker
|
|
|
|
|
v-model="slot.start"
|
|
|
|
|
class="activity-specific-time-slot-picker"
|
|
|
|
|
format="HH:mm"
|
|
|
|
|
value-format="HH:mm"
|
|
|
|
|
placeholder="开始"
|
|
|
|
|
size="small"
|
|
|
|
|
/>
|
|
|
|
|
<span class="activity-time-slot-row__tilde">至</span>
|
|
|
|
|
<a-time-picker
|
|
|
|
|
v-model="slot.end"
|
|
|
|
|
class="activity-specific-time-slot-picker"
|
|
|
|
|
format="HH:mm"
|
|
|
|
|
value-format="HH:mm"
|
|
|
|
|
placeholder="结束"
|
|
|
|
|
size="small"
|
|
|
|
|
/>
|
|
|
|
|
<a-button class="activity-time-slot-row__del" size="mini" status="danger" @click="removeSpecificTimeRow(sidx)">
|
|
|
|
|
删除
|
|
|
|
|
</a-button>
|
|
|
|
|
</div>
|
|
|
|
|
<span v-if="!form.specific_time_slots.length" class="activity-time-slots-empty-hint">暂无时段,点击「添加时段」</span>
|
|
|
|
|
</a-space>
|
|
|
|
|
<template v-if="formErrors.specific_time_slots" #help>
|
|
|
|
|
<span style="color: #f53f3f;">{{ formErrors.specific_time_slots }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
<a-col :xs="24" :md="14">
|
|
|
|
|
<a-form-item label="标签">
|
|
|
|
|
<div class="activity-form-tags">
|
|
|
|
|
<div class="activity-form-tags__line">
|
|
|
|
|
@ -2783,7 +2591,7 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
{{ activityBookingStepIntro }}
|
|
|
|
|
</a-typography-text>
|
|
|
|
|
<a-typography-text v-else type="warning" style="display: block; margin-bottom: 12px">
|
|
|
|
|
活动已结束:本次保存将不更新场次与预约名额(仅活动基本信息)。
|
|
|
|
|
活动已结束:本次保存将不更新场次与可预约人数(仅活动基本信息)。
|
|
|
|
|
</a-typography-text>
|
|
|
|
|
<a-form v-if="showBookingAudienceFields" layout="vertical">
|
|
|
|
|
<a-row :gutter="16">
|
|
|
|
|
@ -2797,12 +2605,12 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
<a-col v-if="bookingForm.booking_audience !== 'individual'" :span="8">
|
|
|
|
|
<a-form-item label="每单最少人数">
|
|
|
|
|
<a-form-item label="团体每单最少人数">
|
|
|
|
|
<a-input-number v-model="bookingForm.min_people_per_order" :min="1" style="width: 100%" />
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
<a-col v-if="bookingForm.booking_audience !== 'individual'" :span="8">
|
|
|
|
|
<a-form-item label="每单最多人数">
|
|
|
|
|
<a-form-item label="团体每单最多人数">
|
|
|
|
|
<a-input-number
|
|
|
|
|
v-model="bookingForm.max_people_per_order"
|
|
|
|
|
:min="2"
|
|
|
|
|
@ -2843,8 +2651,11 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
v-model="bookingForm.days[rowIndex].session_start_at"
|
|
|
|
|
show-time
|
|
|
|
|
format="YYYY-MM-DD HH:mm"
|
|
|
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
value-format="YYYY-MM-DD HH:mm"
|
|
|
|
|
:time-picker-props="{ format: 'HH:mm' }"
|
|
|
|
|
:disabled-date="sessionDayDisabledDate"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
placeholder="必填"
|
|
|
|
|
:disabled="!canSaveSessionsWithActivity"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
@ -2855,8 +2666,11 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
v-model="bookingForm.days[rowIndex].session_end_at"
|
|
|
|
|
show-time
|
|
|
|
|
format="YYYY-MM-DD HH:mm"
|
|
|
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
value-format="YYYY-MM-DD HH:mm"
|
|
|
|
|
:time-picker-props="{ format: 'HH:mm' }"
|
|
|
|
|
:disabled-date="sessionDayDisabledDate"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
placeholder="必填"
|
|
|
|
|
:disabled="!canSaveSessionsWithActivity"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
@ -2867,8 +2681,10 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
v-model="bookingForm.days[rowIndex].booking_opens_at"
|
|
|
|
|
show-time
|
|
|
|
|
format="YYYY-MM-DD HH:mm"
|
|
|
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
value-format="YYYY-MM-DD HH:mm"
|
|
|
|
|
:time-picker-props="{ format: 'HH:mm' }"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
placeholder="必填"
|
|
|
|
|
:disabled="!canSaveSessionsWithActivity"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
@ -2879,17 +2695,21 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
v-model="bookingForm.days[rowIndex].booking_deadline_at"
|
|
|
|
|
show-time
|
|
|
|
|
format="YYYY-MM-DD HH:mm"
|
|
|
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
value-format="YYYY-MM-DD HH:mm"
|
|
|
|
|
:time-picker-props="{ format: 'HH:mm' }"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
placeholder="必填"
|
|
|
|
|
:disabled="!canSaveSessionsWithActivity"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="名额" :width="100">
|
|
|
|
|
<a-table-column title="可预约人数" :width="100">
|
|
|
|
|
<template #cell="{ rowIndex }">
|
|
|
|
|
<a-input-number
|
|
|
|
|
v-model="bookingForm.days[rowIndex].day_quota"
|
|
|
|
|
:min="Math.max(1, Number(bookingForm.days[rowIndex].booked_count) || 0)"
|
|
|
|
|
placeholder="必填"
|
|
|
|
|
allow-clear
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
:disabled="!canSaveSessionsWithActivity"
|
|
|
|
|
/>
|
|
|
|
|
@ -2903,7 +2723,7 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
<template #cell="{ rowIndex }">
|
|
|
|
|
<a-input
|
|
|
|
|
v-model="bookingForm.days[rowIndex].quota_note"
|
|
|
|
|
placeholder="可与名额配合说明"
|
|
|
|
|
placeholder="可与可预约人数配合说明"
|
|
|
|
|
allow-clear
|
|
|
|
|
:disabled="!canSaveSessionsWithActivity"
|
|
|
|
|
/>
|
|
|
|
|
|