You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
287 lines
10 KiB
287 lines
10 KiB
<script setup lang="ts">
|
|
import { onMounted, reactive, ref, watch } from 'vue'
|
|
import { Message } from '@arco-design/web-vue'
|
|
import { http } from '../../api/http'
|
|
import { formatDateTimeZh, formatDateZh } from '../../utils/datetime'
|
|
import { bookingTypeLabel } from '../../utils/bookingType'
|
|
import { listTableRowIndex } from '../../utils/listTableRowIndex'
|
|
import { reservationStatusLabel } from '../../utils/reservationStatus'
|
|
|
|
const TG_REGISTRATIONS_LIST_SCROLL_X = 1680
|
|
|
|
type Row = {
|
|
id: number
|
|
visitor_name: string
|
|
visitor_phone?: string | null
|
|
id_card?: string | null
|
|
booking_type?: string | null
|
|
ticket_count?: number
|
|
status: 'pending' | 'verified' | 'cancelled' | 'expired'
|
|
qr_token: string
|
|
created_at: string
|
|
verified_at?: string | null
|
|
entry_date?: string | null
|
|
venue?: { id: number; name: string }
|
|
ticket_grab_event?: { id: number; title: string }
|
|
}
|
|
|
|
type TicketGrabEventOption = { id: number; title: string }
|
|
|
|
const loading = ref(false)
|
|
const status = ref<'all' | 'pending' | 'verified' | 'cancelled' | 'expired'>('pending')
|
|
const keyword = ref('')
|
|
const ticketGrabEventId = ref<number | undefined>(undefined)
|
|
const dateRange = ref<string[]>([])
|
|
const pagination = reactive({ current: 1, pageSize: 10, total: 0 })
|
|
const rows = ref<Row[]>([])
|
|
const ticketGrabEvents = ref<TicketGrabEventOption[]>([])
|
|
|
|
type CurrentUser = { role?: string; full_admin_access?: boolean }
|
|
type VenueMini = { id: number; name: string }
|
|
|
|
const currentUser = ref<CurrentUser | null>(null)
|
|
const venuesList = ref<VenueMini[]>([])
|
|
const filterVenueId = ref<number | undefined>(undefined)
|
|
|
|
function isSuperAdmin() {
|
|
return currentUser.value?.full_admin_access === true
|
|
}
|
|
|
|
async function loadMe() {
|
|
try {
|
|
const { data } = await http.get('/me')
|
|
currentUser.value = data as CurrentUser
|
|
} catch {
|
|
currentUser.value = null
|
|
}
|
|
}
|
|
|
|
async function loadVenues() {
|
|
try {
|
|
const { data } = await http.get('/venues')
|
|
venuesList.value = Array.isArray(data) ? (data as VenueMini[]) : []
|
|
} catch {
|
|
venuesList.value = []
|
|
}
|
|
}
|
|
|
|
async function loadTicketGrabEvents() {
|
|
try {
|
|
const { data } = await http.get<{ data: { id: number; title: string }[] }>('/ticket-grab-events/options', {
|
|
params: { limit: 2000 },
|
|
})
|
|
ticketGrabEvents.value = data.data ?? []
|
|
} catch {
|
|
ticketGrabEvents.value = []
|
|
}
|
|
}
|
|
|
|
async function loadRows() {
|
|
loading.value = true
|
|
try {
|
|
const params: Record<string, unknown> = {
|
|
reservation_kind: 'ticket_grab',
|
|
ticket_grab_event_id: ticketGrabEventId.value || undefined,
|
|
status: status.value,
|
|
keyword: keyword.value || undefined,
|
|
start_date: dateRange.value?.[0] || undefined,
|
|
end_date: dateRange.value?.[1] || undefined,
|
|
page: pagination.current,
|
|
page_size: pagination.pageSize,
|
|
}
|
|
if (isSuperAdmin() && filterVenueId.value != null && filterVenueId.value > 0) {
|
|
params.venue_id = filterVenueId.value
|
|
}
|
|
const { data } = await http.get('/activity-registrations', { params })
|
|
rows.value = data.data
|
|
pagination.total = data.total
|
|
} catch (e: any) {
|
|
Message.error(e?.response?.data?.message ?? '加载失败')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function onPageChange(cur: number) {
|
|
pagination.current = cur
|
|
void loadRows()
|
|
}
|
|
|
|
function onPageSizeChange(s: number) {
|
|
pagination.pageSize = s
|
|
pagination.current = 1
|
|
void loadRows()
|
|
}
|
|
|
|
function tgRegistrationInactiveBodyCellClass(record: unknown) {
|
|
const st = String((record as { status?: string })?.status ?? '').trim()
|
|
return st === 'cancelled' || st === 'expired' ? 'reg-row-inactive-cell' : undefined
|
|
}
|
|
|
|
function tgRegistrationInactiveBodyCellStyle(record: unknown) {
|
|
const st = String((record as { status?: string })?.status ?? '').trim()
|
|
if (st === 'cancelled' || st === 'expired') {
|
|
return { backgroundColor: 'var(--color-fill-2)' }
|
|
}
|
|
return {}
|
|
}
|
|
|
|
watch(filterVenueId, () => {
|
|
pagination.current = 1
|
|
void loadRows()
|
|
})
|
|
|
|
onMounted(async () => {
|
|
await loadMe()
|
|
await loadVenues()
|
|
await loadTicketGrabEvents()
|
|
await loadRows()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<a-card title="抢票管理 / 抢票报名" :bordered="false">
|
|
<a-space direction="vertical" fill>
|
|
<a-space wrap :size="12">
|
|
<a-select
|
|
v-if="isSuperAdmin()"
|
|
v-model="filterVenueId"
|
|
allow-clear
|
|
allow-search
|
|
placeholder="搜索或选择场馆"
|
|
style="width: 260px"
|
|
>
|
|
<a-option v-for="v in venuesList" :key="v.id" :value="v.id">{{ v.name }}</a-option>
|
|
</a-select>
|
|
<a-select
|
|
v-model="ticketGrabEventId"
|
|
allow-clear
|
|
placeholder="抢票活动(全部)"
|
|
style="width: 260px"
|
|
allow-search
|
|
@change="
|
|
() => {
|
|
pagination.current = 1
|
|
loadRows()
|
|
}
|
|
"
|
|
>
|
|
<a-option v-for="e in ticketGrabEvents" :key="e.id" :value="e.id">{{ e.title }}</a-option>
|
|
</a-select>
|
|
<a-radio-group v-model="status" type="button" size="small" @change="loadRows">
|
|
<a-radio value="all">全部</a-radio>
|
|
<a-radio value="pending">待核销</a-radio>
|
|
<a-radio value="verified">已核销</a-radio>
|
|
<a-radio value="cancelled">已取消</a-radio>
|
|
<a-radio value="expired">已过期</a-radio>
|
|
</a-radio-group>
|
|
<a-input v-model="keyword" placeholder="姓名 / 身份证 / token" allow-clear style="width: 220px" />
|
|
<a-range-picker v-model="dateRange" style="width: 260px" />
|
|
<a-button
|
|
type="primary"
|
|
@click="
|
|
() => {
|
|
pagination.current = 1
|
|
loadRows()
|
|
}
|
|
"
|
|
>
|
|
查询
|
|
</a-button>
|
|
</a-space>
|
|
|
|
<a-table
|
|
class="tg-registrations-table"
|
|
:scroll="{ x: TG_REGISTRATIONS_LIST_SCROLL_X }"
|
|
:data="rows"
|
|
:loading="loading"
|
|
row-key="id"
|
|
:pagination="{
|
|
current: pagination.current,
|
|
pageSize: pagination.pageSize,
|
|
total: pagination.total,
|
|
showTotal: true,
|
|
onChange: onPageChange,
|
|
onPageSizeChange,
|
|
}"
|
|
>
|
|
<template #columns>
|
|
<a-table-column title="" :width="50" :ellipsis="true" :tooltip="true" :body-cell-class="tgRegistrationInactiveBodyCellClass"
|
|
:body-cell-style="tgRegistrationInactiveBodyCellStyle">
|
|
<template #cell="{ rowIndex }">{{
|
|
listTableRowIndex(rowIndex, pagination.current, pagination.pageSize)
|
|
}}</template>
|
|
</a-table-column>
|
|
<a-table-column title="抢票活动" :width="200" :ellipsis="true" :tooltip="true" :body-cell-class="tgRegistrationInactiveBodyCellClass"
|
|
:body-cell-style="tgRegistrationInactiveBodyCellStyle">
|
|
<template #cell="{ record }">{{ record.ticket_grab_event?.title ?? '-' }}</template>
|
|
</a-table-column>
|
|
<a-table-column title="场馆" :width="160" :ellipsis="true" :tooltip="true" :body-cell-class="tgRegistrationInactiveBodyCellClass"
|
|
:body-cell-style="tgRegistrationInactiveBodyCellStyle">
|
|
<template #cell="{ record }">{{ record.venue?.name ?? '-' }}</template>
|
|
</a-table-column>
|
|
<a-table-column title="姓名" data-index="visitor_name" :width="100" :body-cell-class="tgRegistrationInactiveBodyCellClass"
|
|
:body-cell-style="tgRegistrationInactiveBodyCellStyle" />
|
|
<a-table-column title="身份证" data-index="id_card" :width="180" :ellipsis="true" :tooltip="true" :body-cell-class="tgRegistrationInactiveBodyCellClass"
|
|
:body-cell-style="tgRegistrationInactiveBodyCellStyle" />
|
|
<a-table-column title="入馆日" :width="120" :body-cell-class="tgRegistrationInactiveBodyCellClass"
|
|
:body-cell-style="tgRegistrationInactiveBodyCellStyle">
|
|
<template #cell="{ record }">{{
|
|
record.entry_date ? formatDateZh(String(record.entry_date)) : '-'
|
|
}}</template>
|
|
</a-table-column>
|
|
<a-table-column title="预约类型" :width="100" :body-cell-class="tgRegistrationInactiveBodyCellClass"
|
|
:body-cell-style="tgRegistrationInactiveBodyCellStyle">
|
|
<template #cell="{ record }">{{ bookingTypeLabel(record.booking_type, record.ticket_count) }}</template>
|
|
</a-table-column>
|
|
<a-table-column title="票数" :width="80" :body-cell-class="tgRegistrationInactiveBodyCellClass"
|
|
:body-cell-style="tgRegistrationInactiveBodyCellStyle">
|
|
<template #cell="{ record }">{{ record.ticket_count ?? 1 }}</template>
|
|
</a-table-column>
|
|
<a-table-column title="状态" :width="100" :body-cell-class="tgRegistrationInactiveBodyCellClass"
|
|
:body-cell-style="tgRegistrationInactiveBodyCellStyle">
|
|
<template #cell="{ record }">
|
|
<a-tag
|
|
:color="
|
|
record.status === 'verified'
|
|
? 'green'
|
|
: record.status === 'pending'
|
|
? 'arcoblue'
|
|
: 'gray'
|
|
"
|
|
>{{ reservationStatusLabel(record.status) }}</a-tag
|
|
>
|
|
</template>
|
|
</a-table-column>
|
|
<a-table-column title="下单时间" :width="170" :body-cell-class="tgRegistrationInactiveBodyCellClass"
|
|
:body-cell-style="tgRegistrationInactiveBodyCellStyle">
|
|
<template #cell="{ record }">{{ formatDateTimeZh(record.created_at) }}</template>
|
|
</a-table-column>
|
|
<a-table-column title="核销时间" :width="170" :body-cell-class="tgRegistrationInactiveBodyCellClass"
|
|
:body-cell-style="tgRegistrationInactiveBodyCellStyle">
|
|
<template #cell="{ record }">{{
|
|
record.verified_at ? formatDateTimeZh(String(record.verified_at)) : '-'
|
|
}}</template>
|
|
</a-table-column>
|
|
<a-table-column title="核销 Token" :width="220" :ellipsis="true" :tooltip="true" :body-cell-class="tgRegistrationInactiveBodyCellClass"
|
|
:body-cell-style="tgRegistrationInactiveBodyCellStyle">
|
|
<template #cell="{ record }">
|
|
<span style="font-family: monospace; font-size: 12px">{{ record.qr_token }}</span>
|
|
</template>
|
|
</a-table-column>
|
|
</template>
|
|
</a-table>
|
|
</a-space>
|
|
</a-card>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.tg-registrations-table :deep(.arco-table-td.reg-row-inactive-cell) {
|
|
background-color: var(--color-fill-2) !important;
|
|
}
|
|
|
|
.tg-registrations-table :deep(.arco-table-tr:hover .arco-table-td.reg-row-inactive-cell) {
|
|
background-color: var(--color-fill-3) !important;
|
|
}
|
|
</style>
|