|
|
|
|
@ -1,5 +1,5 @@
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { onMounted, reactive, ref } from 'vue'
|
|
|
|
|
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'
|
|
|
|
|
@ -36,6 +36,9 @@ type Reservation = {
|
|
|
|
|
activity_day?: ActivityDayRef | null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type CurrentUser = { role?: string; full_admin_access?: boolean }
|
|
|
|
|
type VenueMini = { id: number; name: string }
|
|
|
|
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
const rows = ref<Reservation[]>([])
|
|
|
|
|
const tokenInput = ref('')
|
|
|
|
|
@ -72,6 +75,82 @@ const dateRange = ref<string[]>([])
|
|
|
|
|
const listPagination = reactive({ current: 1, pageSize: 10 })
|
|
|
|
|
const exportVerifyLoading = ref(false)
|
|
|
|
|
|
|
|
|
|
const currentUser = ref<CurrentUser | null>(null)
|
|
|
|
|
const venuesList = ref<VenueMini[]>([])
|
|
|
|
|
const filterVenueId = ref<number | undefined>(undefined)
|
|
|
|
|
const filterActivityId = ref<number | undefined>(undefined)
|
|
|
|
|
const activityOptions = ref<{ label: string; value: number }[]>([])
|
|
|
|
|
const activitySearchLoading = ref(false)
|
|
|
|
|
let activitySearchTimer: ReturnType<typeof setTimeout> | null = null
|
|
|
|
|
|
|
|
|
|
function isVenueAdmin() {
|
|
|
|
|
return currentUser.value?.role === 'venue_admin'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 超管 / 平台管理员(非场馆管理员)可见场馆、活动筛选 */
|
|
|
|
|
function canFilterByVenueAndActivity() {
|
|
|
|
|
if (!currentUser.value) return false
|
|
|
|
|
return !isVenueAdmin()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadMe() {
|
|
|
|
|
try {
|
|
|
|
|
const { data } = await http.get('/me')
|
|
|
|
|
currentUser.value = data as CurrentUser
|
|
|
|
|
} catch {
|
|
|
|
|
currentUser.value = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadVenues() {
|
|
|
|
|
if (!canFilterByVenueAndActivity()) {
|
|
|
|
|
venuesList.value = []
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const { data } = await http.get('/venues')
|
|
|
|
|
venuesList.value = Array.isArray(data) ? (data as VenueMini[]) : []
|
|
|
|
|
} catch {
|
|
|
|
|
venuesList.value = []
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadActivityOptions(keywordText = '') {
|
|
|
|
|
if (!canFilterByVenueAndActivity()) {
|
|
|
|
|
activityOptions.value = []
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
activitySearchLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const params: Record<string, unknown> = { limit: 500 }
|
|
|
|
|
const kw = keywordText.trim()
|
|
|
|
|
if (kw) params.keyword = kw
|
|
|
|
|
if (filterVenueId.value != null && filterVenueId.value > 0) {
|
|
|
|
|
params.venue_id = filterVenueId.value
|
|
|
|
|
}
|
|
|
|
|
const { data } = await http.get('/activities/options', { params })
|
|
|
|
|
const list = (data?.data ?? []) as { id: number; title: string }[]
|
|
|
|
|
const mapped = list.map((a) => ({ label: a.title, value: a.id }))
|
|
|
|
|
const selectedId = filterActivityId.value
|
|
|
|
|
if (selectedId != null && selectedId > 0 && !mapped.some((o) => o.value === selectedId)) {
|
|
|
|
|
const prev = activityOptions.value.find((o) => o.value === selectedId)
|
|
|
|
|
if (prev) mapped.unshift(prev)
|
|
|
|
|
}
|
|
|
|
|
activityOptions.value = mapped
|
|
|
|
|
} catch {
|
|
|
|
|
activityOptions.value = []
|
|
|
|
|
} finally {
|
|
|
|
|
activitySearchLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onActivitySelectSearch(value: string) {
|
|
|
|
|
if (activitySearchTimer) clearTimeout(activitySearchTimer)
|
|
|
|
|
activitySearchTimer = setTimeout(() => {
|
|
|
|
|
void loadActivityOptions(value)
|
|
|
|
|
}, 300)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function exportVerifyXlsx() {
|
|
|
|
|
if (exportVerifyLoading.value) return
|
|
|
|
|
exportVerifyLoading.value = true
|
|
|
|
|
@ -93,16 +172,21 @@ function exportVerifyXlsx() {
|
|
|
|
|
async function loadRows() {
|
|
|
|
|
loading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const { data } = await http.get('/reservations', {
|
|
|
|
|
params: {
|
|
|
|
|
status: statusFilter.value,
|
|
|
|
|
keyword: keyword.value || undefined,
|
|
|
|
|
start_date: dateRange.value?.[0] || undefined,
|
|
|
|
|
end_date: dateRange.value?.[1] || undefined,
|
|
|
|
|
date_field: 'activity_day',
|
|
|
|
|
reservation_kind: 'activity',
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
const params: Record<string, unknown> = {
|
|
|
|
|
status: statusFilter.value,
|
|
|
|
|
keyword: keyword.value || undefined,
|
|
|
|
|
start_date: dateRange.value?.[0] || undefined,
|
|
|
|
|
end_date: dateRange.value?.[1] || undefined,
|
|
|
|
|
date_field: 'activity_day',
|
|
|
|
|
reservation_kind: 'activity',
|
|
|
|
|
}
|
|
|
|
|
if (canFilterByVenueAndActivity() && filterVenueId.value != null && filterVenueId.value > 0) {
|
|
|
|
|
params.venue_id = filterVenueId.value
|
|
|
|
|
}
|
|
|
|
|
if (canFilterByVenueAndActivity() && filterActivityId.value != null && filterActivityId.value > 0) {
|
|
|
|
|
params.activity_id = filterActivityId.value
|
|
|
|
|
}
|
|
|
|
|
const { data } = await http.get('/reservations', { params })
|
|
|
|
|
rows.value = data
|
|
|
|
|
listPagination.current = 1
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
@ -133,7 +217,18 @@ async function verifyToken() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(loadRows)
|
|
|
|
|
watch(filterVenueId, async () => {
|
|
|
|
|
filterActivityId.value = undefined
|
|
|
|
|
await loadActivityOptions()
|
|
|
|
|
void loadRows()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
await loadMe()
|
|
|
|
|
await loadVenues()
|
|
|
|
|
await loadActivityOptions()
|
|
|
|
|
await loadRows()
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
@ -153,6 +248,31 @@ onMounted(loadRows)
|
|
|
|
|
<a-radio value="cancelled">已取消</a-radio>
|
|
|
|
|
<a-radio value="expired">已过期</a-radio>
|
|
|
|
|
</a-radio-group>
|
|
|
|
|
<a-select
|
|
|
|
|
v-if="canFilterByVenueAndActivity()"
|
|
|
|
|
v-model="filterVenueId"
|
|
|
|
|
allow-clear
|
|
|
|
|
allow-search
|
|
|
|
|
placeholder="搜索或选择场馆"
|
|
|
|
|
style="width: 220px"
|
|
|
|
|
>
|
|
|
|
|
<a-option v-for="v in venuesList" :key="v.id" :value="v.id">{{ v.name }}</a-option>
|
|
|
|
|
</a-select>
|
|
|
|
|
<a-select
|
|
|
|
|
v-if="canFilterByVenueAndActivity()"
|
|
|
|
|
v-model="filterActivityId"
|
|
|
|
|
allow-clear
|
|
|
|
|
allow-search
|
|
|
|
|
:filter-option="false"
|
|
|
|
|
:loading="activitySearchLoading"
|
|
|
|
|
placeholder="搜索或选择活动"
|
|
|
|
|
style="width: 280px"
|
|
|
|
|
@search="onActivitySelectSearch"
|
|
|
|
|
@change="onSearchList"
|
|
|
|
|
@clear="() => loadActivityOptions()"
|
|
|
|
|
>
|
|
|
|
|
<a-option v-for="opt in activityOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</a-option>
|
|
|
|
|
</a-select>
|
|
|
|
|
<a-input v-model="keyword" placeholder="报名人/手机/token" allow-clear style="width: 220px" />
|
|
|
|
|
<span class="verify-filter-label">场次日期</span>
|
|
|
|
|
<a-range-picker v-model="dateRange" style="width: 260px" />
|
|
|
|
|
|