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

<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>