|
|
|
|
@ -3,13 +3,15 @@ import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
|
|
|
|
import { useRoute } from 'vue-router'
|
|
|
|
|
import { Message } from '@arco-design/web-vue'
|
|
|
|
|
import RichEditorField from '../../components/RichEditorField.vue'
|
|
|
|
|
import { IconEye, IconEyeInvisible } from '@arco-design/web-vue/es/icon'
|
|
|
|
|
import { buildVerifyPortalPublicUrl } from '../../api/h5Http'
|
|
|
|
|
import { http } from '../../api/http'
|
|
|
|
|
import { useModalDirtyGuard } from '../../composables/useModalDirtyGuard'
|
|
|
|
|
import { useUnsavedChangesGuard } from '../../composables/useUnsavedChangesGuard'
|
|
|
|
|
import { listTableRowIndex } from '../../utils/listTableRowIndex'
|
|
|
|
|
|
|
|
|
|
/** 主列表列宽约 1174px,勿用全局 3220 撑宽列 */
|
|
|
|
|
const ACTIVITY_LIST_SCROLL_X = 1570
|
|
|
|
|
const ACTIVITY_LIST_SCROLL_X = 1820
|
|
|
|
|
|
|
|
|
|
type Venue = {
|
|
|
|
|
id: number
|
|
|
|
|
@ -23,15 +25,19 @@ type Venue = {
|
|
|
|
|
reservation_notice?: string
|
|
|
|
|
open_time?: string
|
|
|
|
|
}
|
|
|
|
|
type CurrentUser = { role: string; venues: Venue[]; full_admin_access?: boolean }
|
|
|
|
|
type CurrentUser = { id?: number; role: string; venues: Venue[]; full_admin_access?: boolean }
|
|
|
|
|
type Activity = {
|
|
|
|
|
id: number
|
|
|
|
|
venue_id: number
|
|
|
|
|
/** 后台创建人;平台超管代录可为空,仅超管可改此类活动 */
|
|
|
|
|
submitted_by?: number | null
|
|
|
|
|
title: string
|
|
|
|
|
contact_name?: string | null
|
|
|
|
|
summary?: string
|
|
|
|
|
booking_audience?: string | null
|
|
|
|
|
location?: string | null
|
|
|
|
|
/** 活动报到集合地点(可选) */
|
|
|
|
|
check_in_meeting_point?: string | null
|
|
|
|
|
total_quota?: number
|
|
|
|
|
start_at?: string
|
|
|
|
|
end_at?: string
|
|
|
|
|
@ -55,6 +61,7 @@ type Activity = {
|
|
|
|
|
reservation_type?: string
|
|
|
|
|
specific_time?: string | null
|
|
|
|
|
offline_reservation_method?: string | null
|
|
|
|
|
ticket_fee_note?: string | null
|
|
|
|
|
external_url?: string | null
|
|
|
|
|
/** 线上:已报名人数;H5/统计用 */
|
|
|
|
|
registered_count?: number
|
|
|
|
|
@ -89,6 +96,295 @@ const btsActivityId = ref<number | null>(null)
|
|
|
|
|
const btsModalTitle = ref('上传花絮')
|
|
|
|
|
const btsImages = ref<Array<{ type: 'image'; url: string }>>([])
|
|
|
|
|
const btsSaving = ref(false)
|
|
|
|
|
|
|
|
|
|
type BookingDayRow = {
|
|
|
|
|
id?: number
|
|
|
|
|
session_name: string
|
|
|
|
|
session_start_at: string
|
|
|
|
|
session_end_at: string
|
|
|
|
|
/** 可选;空=不限制开始,仅需在截止前 */
|
|
|
|
|
booking_opens_at: string
|
|
|
|
|
booking_deadline_at: string
|
|
|
|
|
day_quota: number
|
|
|
|
|
/** 有值时 H5 场次「人数限制」展示此处文案,否则展示名额数字 */
|
|
|
|
|
quota_note?: string
|
|
|
|
|
booked_count?: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bookingModalVisible = ref(false)
|
|
|
|
|
const bookingSaving = ref(false)
|
|
|
|
|
const bookingActivityRef = ref<Activity | null>(null)
|
|
|
|
|
const bookingForm = reactive({
|
|
|
|
|
booking_audience: 'both' as 'individual' | 'group' | 'both',
|
|
|
|
|
min_people_per_order: 1,
|
|
|
|
|
max_people_per_order: 10,
|
|
|
|
|
days: [] as BookingDayRow[],
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
function defaultSessionTimesForActivity(act: Activity | null): { start: string; end: string; deadline: 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`,
|
|
|
|
|
deadline: `${d} 08:30:00`,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addBookingDayRow() {
|
|
|
|
|
const act = bookingActivityRef.value
|
|
|
|
|
const { start, end, deadline } = defaultSessionTimesForActivity(act)
|
|
|
|
|
bookingForm.days.push({
|
|
|
|
|
session_name: `场次 ${bookingForm.days.length + 1}`,
|
|
|
|
|
session_start_at: start,
|
|
|
|
|
session_end_at: end,
|
|
|
|
|
booking_opens_at: '',
|
|
|
|
|
booking_deadline_at: deadline,
|
|
|
|
|
day_quota: 30,
|
|
|
|
|
quota_note: '',
|
|
|
|
|
booked_count: 0,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeBookingDayRow(index: number) {
|
|
|
|
|
const row = bookingForm.days[index]
|
|
|
|
|
if (row?.booked_count && row.booked_count > 0) {
|
|
|
|
|
Message.warning('该场次已有预约,无法删除')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
bookingForm.days.splice(index, 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeBookingDayTime(raw: unknown): string {
|
|
|
|
|
const s = String(raw ?? '').trim().replace('T', ' ')
|
|
|
|
|
if (!s) return ''
|
|
|
|
|
return s.length >= 19 ? s.slice(0, 19) : s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function openBookingModal(row: Activity) {
|
|
|
|
|
if (!assertCanEditActivityRow(row, '配置场次')) return
|
|
|
|
|
if (row.reservation_type !== 'online') {
|
|
|
|
|
Message.warning('仅「需要报名」方式可配置场次')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
bookingActivityRef.value = row
|
|
|
|
|
bookingSaving.value = false
|
|
|
|
|
try {
|
|
|
|
|
const { data } = await http.get(`/activities/${row.id}/booking-settings`)
|
|
|
|
|
bookingForm.booking_audience = (data?.booking_audience as 'individual' | 'group' | 'both') || 'both'
|
|
|
|
|
bookingForm.min_people_per_order = Math.max(1, Number(data?.min_people_per_order) || 1)
|
|
|
|
|
bookingForm.max_people_per_order = Math.max(
|
|
|
|
|
bookingForm.min_people_per_order,
|
|
|
|
|
Number(data?.max_people_per_order) || Math.max(10, bookingForm.min_people_per_order),
|
|
|
|
|
)
|
|
|
|
|
const days = Array.isArray(data?.days) ? data.days : []
|
|
|
|
|
bookingForm.days = days.map((d: Record<string, unknown>) => {
|
|
|
|
|
const nid = Number(d.id)
|
|
|
|
|
return {
|
|
|
|
|
id: Number.isFinite(nid) && nid > 0 ? nid : undefined,
|
|
|
|
|
session_name: String(d.session_name ?? ''),
|
|
|
|
|
session_start_at: normalizeBookingDayTime(d.session_start_at),
|
|
|
|
|
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),
|
|
|
|
|
quota_note: String(d.quota_note ?? ''),
|
|
|
|
|
booked_count: Math.max(0, Number(d.booked_count) || 0),
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
if (bookingForm.days.length === 0) {
|
|
|
|
|
addBookingDayRow()
|
|
|
|
|
}
|
|
|
|
|
bookingModalVisible.value = true
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
Message.error(e?.response?.data?.message ?? '加载场次失败')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveBookingSettings() {
|
|
|
|
|
const act = bookingActivityRef.value
|
|
|
|
|
if (!act) return
|
|
|
|
|
if (bookingForm.days.length === 0) {
|
|
|
|
|
Message.warning('请至少添加一个场次')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
for (let i = 0; i < bookingForm.days.length; i++) {
|
|
|
|
|
const d = bookingForm.days[i]
|
|
|
|
|
if (!d.session_name.trim()) {
|
|
|
|
|
Message.warning(`第 ${i + 1} 行请填写场次名称`)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (!d.session_start_at || !d.session_end_at || !d.booking_deadline_at) {
|
|
|
|
|
Message.warning(`第 ${i + 1} 行请填写场次开始、结束与预约截止时间`)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (!d.day_quota || d.day_quota < 1) {
|
|
|
|
|
Message.warning(`第 ${i + 1} 行预约名额须≥1`)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
bookingForm.booking_audience !== 'individual'
|
|
|
|
|
&& bookingForm.max_people_per_order < bookingForm.min_people_per_order
|
|
|
|
|
) {
|
|
|
|
|
Message.warning('每单最多人数不能小于最少人数')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const payload: Record<string, unknown> = {
|
|
|
|
|
booking_audience: bookingForm.booking_audience,
|
|
|
|
|
days: bookingForm.days.map((d) => {
|
|
|
|
|
const row: Record<string, unknown> = {
|
|
|
|
|
session_name: d.session_name.trim(),
|
|
|
|
|
session_start_at: d.session_start_at,
|
|
|
|
|
session_end_at: d.session_end_at,
|
|
|
|
|
booking_deadline_at: d.booking_deadline_at,
|
|
|
|
|
day_quota: d.day_quota,
|
|
|
|
|
quota_note: (d.quota_note || '').trim() || null,
|
|
|
|
|
}
|
|
|
|
|
const opens = (d.booking_opens_at || '').trim()
|
|
|
|
|
row.booking_opens_at = opens || null
|
|
|
|
|
if (d.id && d.id > 0) {
|
|
|
|
|
row.id = d.id
|
|
|
|
|
}
|
|
|
|
|
return row
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
if (bookingForm.booking_audience !== 'individual') {
|
|
|
|
|
payload.min_people_per_order = bookingForm.min_people_per_order
|
|
|
|
|
payload.max_people_per_order = bookingForm.max_people_per_order
|
|
|
|
|
}
|
|
|
|
|
bookingSaving.value = true
|
|
|
|
|
try {
|
|
|
|
|
await http.put(`/activities/${act.id}/booking-settings`, payload)
|
|
|
|
|
Message.success('场次已保存')
|
|
|
|
|
bookingModalVisible.value = false
|
|
|
|
|
await loadAll()
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
Message.error(e?.response?.data?.message ?? '保存失败')
|
|
|
|
|
} finally {
|
|
|
|
|
bookingSaving.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ActVerifyCredRow = {
|
|
|
|
|
id: number
|
|
|
|
|
venue_id: number
|
|
|
|
|
venue_name?: string
|
|
|
|
|
username: string
|
|
|
|
|
password_plain?: string | null
|
|
|
|
|
note?: string | null
|
|
|
|
|
created_at?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const actVerifyVisible = ref(false)
|
|
|
|
|
const actVerifyLoading = ref(false)
|
|
|
|
|
const actVerifyActivityId = ref<number | null>(null)
|
|
|
|
|
const actVerifyActivityTitle = ref('')
|
|
|
|
|
const actVerifyVenueName = ref('')
|
|
|
|
|
const actVerifyUrl = ref('')
|
|
|
|
|
const actVerifyCreds = ref<ActVerifyCredRow[]>([])
|
|
|
|
|
const actVerifyCredForm = reactive({ username: '', password: '', note: '' })
|
|
|
|
|
const actVerifyCredSaving = ref(false)
|
|
|
|
|
const actPasswordReveal = ref<Record<number, boolean>>({})
|
|
|
|
|
const ACT_VERIFY_PASSWORD_HINT = '至少 6 位,最长 72 位'
|
|
|
|
|
|
|
|
|
|
function validateActivityVerifyPassword(pw: string): string | null {
|
|
|
|
|
if (pw.length < 6) return '密码至少 6 位'
|
|
|
|
|
if (pw.length > 72) return '密码最长 72 位'
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadActVerifyData(activityId: number) {
|
|
|
|
|
const { data } = await http.get<{ verify_portal_code: string; credentials: ActVerifyCredRow[] }>(
|
|
|
|
|
`/activities/${activityId}/verify-portal`,
|
|
|
|
|
)
|
|
|
|
|
actVerifyUrl.value = buildVerifyPortalPublicUrl(data.verify_portal_code)
|
|
|
|
|
actVerifyCreds.value = data.credentials || []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function openActivityVerify(row: Activity) {
|
|
|
|
|
if (!assertCanOpenActivityVerify(row)) return
|
|
|
|
|
if (row.reservation_type !== 'online') {
|
|
|
|
|
Message.warning('仅「需要报名」的活动可配置专用核销')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
actVerifyActivityId.value = row.id
|
|
|
|
|
actVerifyActivityTitle.value = row.title || ''
|
|
|
|
|
actVerifyVenueName.value = row.venue?.name || ''
|
|
|
|
|
actPasswordReveal.value = {}
|
|
|
|
|
actVerifyVisible.value = true
|
|
|
|
|
actVerifyLoading.value = true
|
|
|
|
|
actVerifyCredForm.username = ''
|
|
|
|
|
actVerifyCredForm.password = ''
|
|
|
|
|
actVerifyCredForm.note = ''
|
|
|
|
|
try {
|
|
|
|
|
await loadActVerifyData(row.id)
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
Message.error(e?.response?.data?.message ?? '加载核销配置失败')
|
|
|
|
|
actVerifyVisible.value = false
|
|
|
|
|
} finally {
|
|
|
|
|
actVerifyLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function copyActVerifyUrl() {
|
|
|
|
|
void navigator.clipboard.writeText(actVerifyUrl.value)
|
|
|
|
|
Message.success('核销链接已复制')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleActPasswordReveal(id: number) {
|
|
|
|
|
actPasswordReveal.value = { ...actPasswordReveal.value, [id]: !actPasswordReveal.value[id] }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function addActVerifyCredential() {
|
|
|
|
|
if (!canEditActivityVerifyCredentials()) return
|
|
|
|
|
if (!actVerifyActivityId.value) return
|
|
|
|
|
if (!actVerifyCredForm.username.trim() || !actVerifyCredForm.password) {
|
|
|
|
|
Message.warning('请填写用户名与密码')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const pwdErr = validateActivityVerifyPassword(actVerifyCredForm.password)
|
|
|
|
|
if (pwdErr) {
|
|
|
|
|
Message.warning(pwdErr)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
actVerifyCredSaving.value = true
|
|
|
|
|
try {
|
|
|
|
|
await http.post(`/activities/${actVerifyActivityId.value}/verify-credentials`, {
|
|
|
|
|
username: actVerifyCredForm.username.trim(),
|
|
|
|
|
password: actVerifyCredForm.password,
|
|
|
|
|
note: actVerifyCredForm.note.trim() || undefined,
|
|
|
|
|
})
|
|
|
|
|
Message.success('已添加')
|
|
|
|
|
actVerifyCredForm.username = ''
|
|
|
|
|
actVerifyCredForm.password = ''
|
|
|
|
|
actVerifyCredForm.note = ''
|
|
|
|
|
await loadActVerifyData(actVerifyActivityId.value)
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
Message.error(e?.response?.data?.message ?? '添加失败')
|
|
|
|
|
} finally {
|
|
|
|
|
actVerifyCredSaving.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function removeActVerifyCredential(row: ActVerifyCredRow) {
|
|
|
|
|
if (!canEditActivityVerifyCredentials()) return
|
|
|
|
|
if (!actVerifyActivityId.value) return
|
|
|
|
|
try {
|
|
|
|
|
await http.delete(`/activities/${actVerifyActivityId.value}/verify-credentials/${row.id}`)
|
|
|
|
|
Message.success('已删除')
|
|
|
|
|
actVerifyCreds.value = actVerifyCreds.value.filter((c) => c.id !== row.id)
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
Message.error(e?.response?.data?.message ?? '删除失败')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isCreate = ref(true)
|
|
|
|
|
const editId = ref<number | null>(null)
|
|
|
|
|
const formBaseline = ref('')
|
|
|
|
|
@ -144,11 +440,15 @@ function formatActivityTableDateRange(record: Activity): string {
|
|
|
|
|
|
|
|
|
|
const form = reactive({
|
|
|
|
|
venue_id: undefined as number | undefined,
|
|
|
|
|
reservation_type: 'phone' as 'phone' | 'wechat_mp' | 'offline_visit' | 'none',
|
|
|
|
|
/** 需要报名 online / 无需报名 none / 或自定义文案(仅展示,与 online 逻辑不同) */
|
|
|
|
|
reservation_type: 'online',
|
|
|
|
|
is_hot: false,
|
|
|
|
|
/** 门票说明:免费/收费,提交至 offline_reservation_method */
|
|
|
|
|
ticket_note: 'free' as 'free' | 'paid',
|
|
|
|
|
/** 收费说明:仅门票为收费时使用,提交 ticket_fee_note */
|
|
|
|
|
fee_note: '',
|
|
|
|
|
location: '',
|
|
|
|
|
check_in_meeting_point: '',
|
|
|
|
|
lat: undefined as number | undefined,
|
|
|
|
|
lng: undefined as number | undefined,
|
|
|
|
|
specific_time: '',
|
|
|
|
|
@ -321,6 +621,56 @@ function isSuperAdmin() {
|
|
|
|
|
return currentUser.value?.full_admin_access === true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 非超管仅能维护本人提交的活动 */
|
|
|
|
|
function canEditActivityRow(row: Activity): boolean {
|
|
|
|
|
if (isSuperAdmin()) return true
|
|
|
|
|
const sid = row.submitted_by
|
|
|
|
|
if (sid == null) return false
|
|
|
|
|
return Number(sid) === Number(currentUser.value?.id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 场馆管理员可查看绑定场馆所举办活动的核销信息(只读),不要求本人创建 */
|
|
|
|
|
function canViewActivityVerifyAsVenueAdmin(row: Activity): boolean {
|
|
|
|
|
if (!isVenueAdmin()) return false
|
|
|
|
|
const vid = row.venue_id
|
|
|
|
|
if (vid == null) return false
|
|
|
|
|
return (currentUser.value?.venues ?? []).some((v) => Number(v.id) === Number(vid))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 打开核销管理:创建人/超管,或(场馆管理员且活动场馆已绑定) */
|
|
|
|
|
function canOpenActivityVerify(row: Activity): boolean {
|
|
|
|
|
return canEditActivityRow(row) || canViewActivityVerifyAsVenueAdmin(row)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function assertCanOpenActivityVerify(row: Activity): boolean {
|
|
|
|
|
if (canOpenActivityVerify(row)) return true
|
|
|
|
|
if (isVenueAdmin()) {
|
|
|
|
|
Message.warning('仅可查看已绑定场馆举办的活动核销信息')
|
|
|
|
|
} else {
|
|
|
|
|
Message.warning(
|
|
|
|
|
row.submitted_by == null
|
|
|
|
|
? '该平台代录的活动仅超级管理员可管理核销'
|
|
|
|
|
: '只能管理本人提交的活动核销',
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function assertCanEditActivityRow(row: Activity, action: string): boolean {
|
|
|
|
|
if (canEditActivityRow(row)) return true
|
|
|
|
|
Message.warning(
|
|
|
|
|
row.submitted_by == null
|
|
|
|
|
? `该平台代录的活动仅超级管理员可${action}`
|
|
|
|
|
: `只能${action}本人提交的活动`,
|
|
|
|
|
)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 活动核销账号:场馆管理员仅可查看链接与密码,不可增删改 */
|
|
|
|
|
function canEditActivityVerifyCredentials() {
|
|
|
|
|
return !isVenueAdmin()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function auditStatusLabel(s?: string | null) {
|
|
|
|
|
if (s === 'pending') return '待审核'
|
|
|
|
|
if (s === 'rejected') return '已退回'
|
|
|
|
|
@ -389,11 +739,20 @@ function openAuditActivity(row: Activity) {
|
|
|
|
|
async function auditSubmitApprove(markHot: boolean) {
|
|
|
|
|
const row = auditActivityRecord.value
|
|
|
|
|
if (!row?.id) return
|
|
|
|
|
const approvedId = row.id
|
|
|
|
|
const openVerifyAfter = isSuperAdmin() && row.reservation_type === 'online'
|
|
|
|
|
try {
|
|
|
|
|
await http.post(`/activities/${row.id}/audit/approve`, { mark_hot: markHot })
|
|
|
|
|
Message.success('审核已通过')
|
|
|
|
|
auditActivityVisible.value = false
|
|
|
|
|
await loadAll()
|
|
|
|
|
if (openVerifyAfter) {
|
|
|
|
|
const found = rows.value.find((r) => r.id === approvedId)
|
|
|
|
|
if (found) {
|
|
|
|
|
await nextTick()
|
|
|
|
|
openActivityVerify(found)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
Message.error(error?.response?.data?.message ?? '操作失败')
|
|
|
|
|
}
|
|
|
|
|
@ -430,6 +789,7 @@ function activityHasBehindScenesImages(row: Activity): boolean {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openBehindScenesModal(row: Activity) {
|
|
|
|
|
if (!assertCanEditActivityRow(row, '上传花絮')) return
|
|
|
|
|
btsActivityId.value = row.id
|
|
|
|
|
btsModalTitle.value = activityHasBehindScenesImages(row) ? '查看花絮' : '上传花絮'
|
|
|
|
|
const raw = Array.isArray(row.behind_scenes_media) ? row.behind_scenes_media : []
|
|
|
|
|
@ -686,14 +1046,15 @@ function confirmMapPick() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function reservationTypeLabel(t?: string | null) {
|
|
|
|
|
if (t === 'phone') return '电话预约'
|
|
|
|
|
if (t === 'wechat_mp') return '公众号预约'
|
|
|
|
|
if (t === 'offline_visit') return '线下预约'
|
|
|
|
|
if (t === 'none') return '无需预约'
|
|
|
|
|
if (t === 'offline') return '线下预约'
|
|
|
|
|
if (t === 'other') return '外链跳转'
|
|
|
|
|
if (t === 'online') return '线上预约'
|
|
|
|
|
return t || '—'
|
|
|
|
|
const s = String(t || '').trim()
|
|
|
|
|
if (s === 'online') return '需要报名'
|
|
|
|
|
if (s === 'none') return '无需报名'
|
|
|
|
|
if (s === 'phone') return '电话预约'
|
|
|
|
|
if (s === 'wechat_mp') return '公众号预约'
|
|
|
|
|
if (s === 'offline_visit' || s === 'offline') return '线下预约'
|
|
|
|
|
if (s === 'other') return '外链跳转'
|
|
|
|
|
if (!s) return '—'
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function statNum(n: unknown): string {
|
|
|
|
|
@ -748,9 +1109,11 @@ function openCreate() {
|
|
|
|
|
editId.value = null
|
|
|
|
|
Object.keys(formErrors).forEach((key) => { formErrors[key] = '' })
|
|
|
|
|
form.venue_id = isVenueAdmin() ? currentUser.value?.venues?.[0]?.id : venues.value[0]?.id
|
|
|
|
|
form.reservation_type = 'phone'
|
|
|
|
|
form.reservation_type = 'online'
|
|
|
|
|
form.ticket_note = 'free'
|
|
|
|
|
form.fee_note = ''
|
|
|
|
|
form.location = ''
|
|
|
|
|
form.check_in_meeting_point = ''
|
|
|
|
|
form.lat = undefined
|
|
|
|
|
form.lng = undefined
|
|
|
|
|
form.specific_time = ''
|
|
|
|
|
@ -775,29 +1138,19 @@ function openCreate() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openEdit(row: Activity) {
|
|
|
|
|
if (!assertCanEditActivityRow(row, '编辑')) return
|
|
|
|
|
isCreate.value = false
|
|
|
|
|
editId.value = row.id
|
|
|
|
|
Object.keys(formErrors).forEach((key) => { formErrors[key] = '' })
|
|
|
|
|
form.venue_id = row.venue_id
|
|
|
|
|
const t = row.reservation_type || 'phone'
|
|
|
|
|
if (t === 'online') {
|
|
|
|
|
form.reservation_type = 'phone'
|
|
|
|
|
form.ticket_note = 'free'
|
|
|
|
|
} else if (t === 'offline') {
|
|
|
|
|
form.reservation_type = 'offline_visit'
|
|
|
|
|
form.reservation_type = String(row.reservation_type ?? 'online').trim() || 'online'
|
|
|
|
|
{
|
|
|
|
|
const m = String(row.offline_reservation_method || '')
|
|
|
|
|
form.ticket_note = m === 'paid' ? 'paid' : 'free'
|
|
|
|
|
} else if (t === 'other') {
|
|
|
|
|
form.reservation_type = 'wechat_mp'
|
|
|
|
|
form.ticket_note = 'free'
|
|
|
|
|
} else if (t === 'phone' || t === 'wechat_mp' || t === 'offline_visit' || t === 'none') {
|
|
|
|
|
form.reservation_type = t
|
|
|
|
|
form.ticket_note = row.offline_reservation_method === 'paid' ? 'paid' : 'free'
|
|
|
|
|
} else {
|
|
|
|
|
form.reservation_type = 'phone'
|
|
|
|
|
form.ticket_note = 'free'
|
|
|
|
|
}
|
|
|
|
|
form.fee_note = row.ticket_fee_note || ''
|
|
|
|
|
form.location = row.location || ''
|
|
|
|
|
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 || ''
|
|
|
|
|
@ -939,6 +1292,14 @@ function validateForm(): boolean {
|
|
|
|
|
formErrors.location = '请填写活动地点'
|
|
|
|
|
isValid = false
|
|
|
|
|
}
|
|
|
|
|
const rt = String(form.reservation_type || '').trim()
|
|
|
|
|
if (!rt) {
|
|
|
|
|
formErrors.reservation_type = '请选择或填写报名方式'
|
|
|
|
|
isValid = false
|
|
|
|
|
} else if (rt.length > 32) {
|
|
|
|
|
formErrors.reservation_type = '报名方式最长 32 个字符'
|
|
|
|
|
isValid = false
|
|
|
|
|
}
|
|
|
|
|
if (!form.ticket_note) {
|
|
|
|
|
formErrors.ticket_note = '请选择门票说明'
|
|
|
|
|
isValid = false
|
|
|
|
|
@ -956,10 +1317,13 @@ async function submit() {
|
|
|
|
|
|
|
|
|
|
const payload = {
|
|
|
|
|
venue_id: form.venue_id,
|
|
|
|
|
reservation_type: form.reservation_type,
|
|
|
|
|
reservation_type: String(form.reservation_type || '').trim(),
|
|
|
|
|
location: form.location.trim(),
|
|
|
|
|
check_in_meeting_point: form.check_in_meeting_point.trim() || null,
|
|
|
|
|
specific_time: form.specific_time.trim() || null,
|
|
|
|
|
offline_reservation_method: form.ticket_note === 'paid' ? 'paid' : 'free',
|
|
|
|
|
ticket_fee_note:
|
|
|
|
|
form.ticket_note === 'paid' ? (form.fee_note.trim() || null) : null,
|
|
|
|
|
external_url: null,
|
|
|
|
|
title: form.title.trim(),
|
|
|
|
|
summary: form.summary.trim() || null,
|
|
|
|
|
@ -1017,6 +1381,7 @@ function onPageChange(p: number) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function toggleStatus(row: Activity) {
|
|
|
|
|
if (!assertCanEditActivityRow(row, '上下架')) return
|
|
|
|
|
try {
|
|
|
|
|
await http.post(`/activities/${row.id}/toggle`)
|
|
|
|
|
Message.success('状态已切换')
|
|
|
|
|
@ -1027,6 +1392,7 @@ async function toggleStatus(row: Activity) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function removeActivity(row: Activity) {
|
|
|
|
|
if (!assertCanEditActivityRow(row, '删除')) return
|
|
|
|
|
try {
|
|
|
|
|
await http.delete(`/activities/${row.id}`)
|
|
|
|
|
Message.success('删除成功')
|
|
|
|
|
@ -1056,11 +1422,16 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
>
|
|
|
|
|
<a-option v-for="v in venues" :key="v.id" :value="v.id">{{ v.name }}</a-option>
|
|
|
|
|
</a-select>
|
|
|
|
|
<a-select v-model="filters.reservation_type" allow-clear placeholder="报名方式" style="width: 140px">
|
|
|
|
|
<a-option value="phone">电话预约</a-option>
|
|
|
|
|
<a-option value="wechat_mp">公众号预约</a-option>
|
|
|
|
|
<a-option value="offline_visit">线下预约</a-option>
|
|
|
|
|
<a-option value="none">无需预约</a-option>
|
|
|
|
|
<a-select
|
|
|
|
|
v-model="filters.reservation_type"
|
|
|
|
|
allow-clear
|
|
|
|
|
allow-create
|
|
|
|
|
allow-search
|
|
|
|
|
placeholder="报名方式"
|
|
|
|
|
style="width: 200px"
|
|
|
|
|
>
|
|
|
|
|
<a-option value="online">需要报名</a-option>
|
|
|
|
|
<a-option value="none">无需报名</a-option>
|
|
|
|
|
</a-select>
|
|
|
|
|
<a-select v-model="filters.is_active" allow-clear placeholder="上架状态" style="width: 130px">
|
|
|
|
|
<a-option value="1">上架</a-option>
|
|
|
|
|
@ -1144,20 +1515,35 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="操作" :width="320" :min-width="300" fixed="right" align="left">
|
|
|
|
|
<a-table-column title="操作" :width="460" :min-width="420" fixed="right" align="left">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<a-space wrap :size="4" justify="start">
|
|
|
|
|
<a-button type="text" @click="openEdit(record)">编辑</a-button>
|
|
|
|
|
<a-button v-if="canEditActivityRow(record as Activity)" type="text" @click="openEdit(record)">编辑</a-button>
|
|
|
|
|
<a-button
|
|
|
|
|
v-if="canEditActivityRow(record as Activity) && (record as Activity).reservation_type === 'online'"
|
|
|
|
|
type="text"
|
|
|
|
|
@click="openBookingModal(record as Activity)"
|
|
|
|
|
>场次设置</a-button>
|
|
|
|
|
<a-button
|
|
|
|
|
v-if="canOpenActivityVerify(record as Activity) && (record as Activity).reservation_type === 'online'"
|
|
|
|
|
type="text"
|
|
|
|
|
@click="openActivityVerify(record as Activity)"
|
|
|
|
|
>核销管理</a-button>
|
|
|
|
|
<template v-if="isSuperAdmin() && (record.audit_status === 'pending' || record.audit_status === 'rejected')">
|
|
|
|
|
<a-button type="text" @click="openAuditActivity(record)">审核</a-button>
|
|
|
|
|
</template>
|
|
|
|
|
<a-button
|
|
|
|
|
v-if="record.schedule_status === 'ended'"
|
|
|
|
|
v-if="canEditActivityRow(record as Activity) && record.schedule_status === 'ended'"
|
|
|
|
|
type="text"
|
|
|
|
|
@click="openBehindScenesModal(record as Activity)"
|
|
|
|
|
>{{ activityHasBehindScenesImages(record as Activity) ? '查看花絮' : '上传花絮' }}</a-button>
|
|
|
|
|
<a-button type="text" status="warning" @click="toggleStatus(record)">{{ record.is_active ? '下架' : '上架' }}</a-button>
|
|
|
|
|
<a-popconfirm content="确认删除该活动?" @ok="removeActivity(record)">
|
|
|
|
|
<a-button
|
|
|
|
|
v-if="canEditActivityRow(record as Activity)"
|
|
|
|
|
type="text"
|
|
|
|
|
status="warning"
|
|
|
|
|
@click="toggleStatus(record)"
|
|
|
|
|
>{{ record.is_active ? '下架' : '上架' }}</a-button>
|
|
|
|
|
<a-popconfirm v-if="canEditActivityRow(record as Activity)" content="确认删除该活动?" @ok="removeActivity(record)">
|
|
|
|
|
<a-button type="text" status="danger">删除</a-button>
|
|
|
|
|
</a-popconfirm>
|
|
|
|
|
</a-space>
|
|
|
|
|
@ -1167,6 +1553,213 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
</a-table>
|
|
|
|
|
</a-card>
|
|
|
|
|
|
|
|
|
|
<a-modal
|
|
|
|
|
v-model:visible="bookingModalVisible"
|
|
|
|
|
:title="bookingActivityRef ? `场次设置 · ${bookingActivityRef.title}` : '场次设置'"
|
|
|
|
|
width="1180px"
|
|
|
|
|
:body-style="{ maxHeight: '78vh', overflow: 'auto' }"
|
|
|
|
|
:esc-to-close="true"
|
|
|
|
|
@cancel="bookingModalVisible = false"
|
|
|
|
|
>
|
|
|
|
|
<template #footer>
|
|
|
|
|
<a-button @click="bookingModalVisible = false">关闭</a-button>
|
|
|
|
|
<a-button type="primary" :loading="bookingSaving" @click="saveBookingSettings">保存场次</a-button>
|
|
|
|
|
</template>
|
|
|
|
|
<a-form layout="vertical">
|
|
|
|
|
<a-row :gutter="16">
|
|
|
|
|
<a-col :span="8">
|
|
|
|
|
<a-form-item label="预约对象">
|
|
|
|
|
<a-select v-model="bookingForm.booking_audience">
|
|
|
|
|
<a-option value="individual">个人</a-option>
|
|
|
|
|
<a-option value="group">团体</a-option>
|
|
|
|
|
<a-option value="both">个人+团体</a-option>
|
|
|
|
|
</a-select>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
<a-col v-if="bookingForm.booking_audience !== 'individual'" :span="8">
|
|
|
|
|
<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-input-number v-model="bookingForm.max_people_per_order" :min="1" style="width: 100%" />
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
</a-row>
|
|
|
|
|
</a-form>
|
|
|
|
|
<div style="margin-bottom: 8px; color: var(--color-text-3); font-size: 12px">
|
|
|
|
|
场次须同一天内;预约截止须早于场次开始;预约开始可空(空=截止前任意时刻均可约,有值则仅在「开始~截止」内可约)。名额不可低于已约人数。「说明」填写后,H5 场次不展示名额人数而展示该说明。
|
|
|
|
|
</div>
|
|
|
|
|
<a-button long type="outline" style="margin-bottom: 12px" @click="addBookingDayRow">添加场次</a-button>
|
|
|
|
|
<a-table
|
|
|
|
|
:data="bookingForm.days"
|
|
|
|
|
:pagination="false"
|
|
|
|
|
:bordered="{ cell: true }"
|
|
|
|
|
size="small"
|
|
|
|
|
:scroll="{ x: 1380 }"
|
|
|
|
|
>
|
|
|
|
|
<template #columns>
|
|
|
|
|
<a-table-column title="场次名称" :width="112">
|
|
|
|
|
<template #cell="{ rowIndex }">
|
|
|
|
|
<a-input v-model="bookingForm.days[rowIndex].session_name" placeholder="名称" />
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="场次开始" :width="160">
|
|
|
|
|
<template #cell="{ rowIndex }">
|
|
|
|
|
<a-date-picker
|
|
|
|
|
v-model="bookingForm.days[rowIndex].session_start_at"
|
|
|
|
|
show-time
|
|
|
|
|
format="YYYY-MM-DD HH:mm"
|
|
|
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="场次结束" :width="160">
|
|
|
|
|
<template #cell="{ rowIndex }">
|
|
|
|
|
<a-date-picker
|
|
|
|
|
v-model="bookingForm.days[rowIndex].session_end_at"
|
|
|
|
|
show-time
|
|
|
|
|
format="YYYY-MM-DD HH:mm"
|
|
|
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="预约开始" :width="164">
|
|
|
|
|
<template #title>
|
|
|
|
|
<span>预约开始</span>
|
|
|
|
|
<span style="color: var(--color-text-3); font-weight: normal; font-size: 12px">(可空)</span>
|
|
|
|
|
</template>
|
|
|
|
|
<template #cell="{ rowIndex }">
|
|
|
|
|
<a-date-picker
|
|
|
|
|
v-model="bookingForm.days[rowIndex].booking_opens_at"
|
|
|
|
|
show-time
|
|
|
|
|
format="YYYY-MM-DD HH:mm"
|
|
|
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
allow-clear
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="预约截止" :width="160">
|
|
|
|
|
<template #cell="{ rowIndex }">
|
|
|
|
|
<a-date-picker
|
|
|
|
|
v-model="bookingForm.days[rowIndex].booking_deadline_at"
|
|
|
|
|
show-time
|
|
|
|
|
format="YYYY-MM-DD HH:mm"
|
|
|
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="名额" :width="80">
|
|
|
|
|
<template #cell="{ rowIndex }">
|
|
|
|
|
<a-input-number v-model="bookingForm.days[rowIndex].day_quota" :min="1" style="width: 100%" />
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="说明" :width="140">
|
|
|
|
|
<template #title>
|
|
|
|
|
<span>说明</span>
|
|
|
|
|
<span style="color: var(--color-text-3); font-weight: normal; font-size: 12px">(可选)</span>
|
|
|
|
|
</template>
|
|
|
|
|
<template #cell="{ rowIndex }">
|
|
|
|
|
<a-input
|
|
|
|
|
v-model="bookingForm.days[rowIndex].quota_note"
|
|
|
|
|
placeholder="有值则 H5 显示此说明替代名额数"
|
|
|
|
|
allow-clear
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="已约" :width="56">
|
|
|
|
|
<template #cell="{ rowIndex }">
|
|
|
|
|
{{ bookingForm.days[rowIndex].booked_count ?? 0 }}
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="" :width="68" fixed="right">
|
|
|
|
|
<template #cell="{ rowIndex }">
|
|
|
|
|
<a-button type="text" status="danger" @click="removeBookingDayRow(rowIndex)">删除</a-button>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table>
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
|
|
|
|
<a-modal
|
|
|
|
|
v-model:visible="actVerifyVisible"
|
|
|
|
|
:title="actVerifyActivityTitle ? `核销管理 · ${actVerifyActivityTitle}` : '核销管理'"
|
|
|
|
|
width="840px"
|
|
|
|
|
:footer="false"
|
|
|
|
|
:esc-to-close="true"
|
|
|
|
|
>
|
|
|
|
|
<a-spin :loading="actVerifyLoading" style="width: 100%">
|
|
|
|
|
<a-typography-paragraph type="secondary" style="margin-bottom: 12px">
|
|
|
|
|
<template v-if="canEditActivityVerifyCredentials()">
|
|
|
|
|
每个活动有<strong>独立核销登录链接</strong>(短码仅作用于本活动)。可为本活动所属场馆添加多组核销账号;<strong>场馆后台账号不可</strong>登录核销页。活动结束后按规则自动失效。
|
|
|
|
|
</template>
|
|
|
|
|
<template v-else>
|
|
|
|
|
您可查看本活动的<strong>核销登录链接</strong>与核销账号密码。<strong>场馆后台账号不可</strong>登录核销页。如需增删账号请联系平台管理员。
|
|
|
|
|
</template>
|
|
|
|
|
</a-typography-paragraph>
|
|
|
|
|
<div v-if="actVerifyVenueName" style="margin-bottom: 12px; color: var(--color-text-2); font-size: 13px">
|
|
|
|
|
举办场馆:{{ actVerifyVenueName }}
|
|
|
|
|
</div>
|
|
|
|
|
<a-form layout="vertical">
|
|
|
|
|
<a-form-item label="本活动专用核销链接">
|
|
|
|
|
<a-space>
|
|
|
|
|
<a-input :model-value="actVerifyUrl" readonly style="width: 500px" />
|
|
|
|
|
<a-button type="primary" @click="copyActVerifyUrl">复制链接</a-button>
|
|
|
|
|
</a-space>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-form>
|
|
|
|
|
<template v-if="canEditActivityVerifyCredentials()">
|
|
|
|
|
<a-divider orientation="left">添加核销账号</a-divider>
|
|
|
|
|
<a-space style="margin-bottom: 8px; flex-wrap: wrap; align-items: flex-start">
|
|
|
|
|
<a-input v-model="actVerifyCredForm.username" placeholder="用户名" style="width: 160px" allow-clear />
|
|
|
|
|
<a-input-password v-model="actVerifyCredForm.password" placeholder="密码" style="width: 180px" allow-clear />
|
|
|
|
|
<a-input v-model="actVerifyCredForm.note" placeholder="备注" style="width: 160px" allow-clear />
|
|
|
|
|
<a-button type="primary" :loading="actVerifyCredSaving" @click="addActVerifyCredential">添加</a-button>
|
|
|
|
|
</a-space>
|
|
|
|
|
<div style="margin-bottom: 12px; color: var(--color-text-3); font-size: 12px">密码要求:{{ ACT_VERIFY_PASSWORD_HINT }}</div>
|
|
|
|
|
</template>
|
|
|
|
|
<a-table :data="actVerifyCreds" :pagination="false" size="small" row-key="id">
|
|
|
|
|
<template #columns>
|
|
|
|
|
<a-table-column title="场馆" data-index="venue_name" />
|
|
|
|
|
<a-table-column title="用户名" data-index="username" />
|
|
|
|
|
<a-table-column title="密码" :width="168">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<a-space :size="4">
|
|
|
|
|
<span>{{
|
|
|
|
|
actPasswordReveal[(record as ActVerifyCredRow).id]
|
|
|
|
|
? (record as ActVerifyCredRow).password_plain || '—'
|
|
|
|
|
: '*****'
|
|
|
|
|
}}</span>
|
|
|
|
|
<a-button
|
|
|
|
|
type="text"
|
|
|
|
|
size="mini"
|
|
|
|
|
@click="toggleActPasswordReveal((record as ActVerifyCredRow).id)"
|
|
|
|
|
>
|
|
|
|
|
<IconEye v-if="!actPasswordReveal[(record as ActVerifyCredRow).id]" />
|
|
|
|
|
<IconEyeInvisible v-else />
|
|
|
|
|
</a-button>
|
|
|
|
|
</a-space>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="备注" data-index="note" />
|
|
|
|
|
<a-table-column title="创建时间" data-index="created_at" />
|
|
|
|
|
<a-table-column v-if="canEditActivityVerifyCredentials()" title="操作" :width="90">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<a-popconfirm content="确认删除?" @ok="removeActVerifyCredential(record as ActVerifyCredRow)">
|
|
|
|
|
<a-button type="text" size="mini" status="danger">删除</a-button>
|
|
|
|
|
</a-popconfirm>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table>
|
|
|
|
|
</a-spin>
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
|
|
|
|
<a-modal
|
|
|
|
|
v-model:visible="auditActivityVisible"
|
|
|
|
|
title="审核活动"
|
|
|
|
|
@ -1266,6 +1859,15 @@ 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 class="activity-audit-static-text activity-audit-static-text--fill">
|
|
|
|
|
{{ auditActivityRecord.check_in_meeting_point?.trim() ? auditActivityRecord.check_in_meeting_point : '—' }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="activity-audit-stack">
|
|
|
|
|
<div class="activity-audit-stack__label">活动图片</div>
|
|
|
|
|
<div class="activity-audit-stack__body">
|
|
|
|
|
@ -1412,12 +2014,15 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
<a-form-item label="具体时间">
|
|
|
|
|
<a-input v-model="form.specific_time" placeholder="如:每日 14:00–16:00;或 活动当日上午" allow-clear />
|
|
|
|
|
</a-form-item>
|
|
|
|
|
<a-form-item label="报名方式" required :help="formErrors.reservation_type">
|
|
|
|
|
<a-select v-model="form.reservation_type">
|
|
|
|
|
<a-option value="phone">电话预约</a-option>
|
|
|
|
|
<a-option value="wechat_mp">公众号预约</a-option>
|
|
|
|
|
<a-option value="offline_visit">线下预约</a-option>
|
|
|
|
|
<a-option value="none">无需预约</a-option>
|
|
|
|
|
<a-form-item label="报名方式" required>
|
|
|
|
|
<a-select
|
|
|
|
|
v-model="form.reservation_type"
|
|
|
|
|
allow-create
|
|
|
|
|
allow-search
|
|
|
|
|
placeholder="可选「需要报名」「无需报名」,或输入自定义文案(仅前端展示,最长 32 字)"
|
|
|
|
|
>
|
|
|
|
|
<a-option value="online">需要报名</a-option>
|
|
|
|
|
<a-option value="none">无需报名</a-option>
|
|
|
|
|
</a-select>
|
|
|
|
|
<template v-if="formErrors.reservation_type" #help>
|
|
|
|
|
<span style="color: #f53f3f;">{{ formErrors.reservation_type }}</span>
|
|
|
|
|
@ -1432,6 +2037,16 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
<span style="color: #f53f3f;">{{ formErrors.ticket_note }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
<a-form-item v-if="form.ticket_note === 'paid'" label="收费说明" class="admin-modal-form__full">
|
|
|
|
|
<a-textarea
|
|
|
|
|
v-model="form.fee_note"
|
|
|
|
|
placeholder="选填。门票为收费时填写,将在 H5 活动详情「门票说明」下方展示。"
|
|
|
|
|
:max-length="1000"
|
|
|
|
|
allow-clear
|
|
|
|
|
show-word-limit
|
|
|
|
|
:auto-size="{ minRows: 2, maxRows: 6 }"
|
|
|
|
|
/>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
<a-form-item label="标签">
|
|
|
|
|
<div class="activity-form-tags">
|
|
|
|
|
<div class="activity-form-tags__line">
|
|
|
|
|
@ -1506,6 +2121,13 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
<span style="color: #f53f3f;">{{ formErrors.location }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
<a-form-item label="活动报到集合地点" class="admin-modal-form__full">
|
|
|
|
|
<a-input
|
|
|
|
|
v-model="form.check_in_meeting_point"
|
|
|
|
|
placeholder="可选,报到或集合的具体位置(如:××馆南门集合)"
|
|
|
|
|
allow-clear
|
|
|
|
|
/>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
<a-form-item label="活动图片" class="admin-modal-form__full">
|
|
|
|
|
<div class="activity-cover-carousel-wrap">
|
|
|
|
|
<div class="activity-cover-carousel-row__col">
|
|
|
|
|
@ -1878,4 +2500,3 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
padding-right: 6px;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
|