master
lion 2 months ago
parent b614fd09a2
commit 6a7407aceb

@ -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"
/>

Loading…
Cancel
Save