master
lion 2 weeks ago
parent f064b928f5
commit 7c9ce8a097

@ -136,8 +136,14 @@ const filters = reactive({
is_active: undefined as '1' | '0' | undefined, is_active: undefined as '1' | '0' | undefined,
schedule_status: undefined as 'not_started' | 'ongoing' | 'ended' | undefined, schedule_status: undefined as 'not_started' | 'ongoing' | 'ended' | undefined,
audit_status: undefined as 'approved' | 'pending' | 'rejected' | undefined, audit_status: undefined as 'approved' | 'pending' | 'rejected' | undefined,
/** 活动开始日期区间 [start, end] YYYY-MM-DD */ /** 活动开始日 ≥ */
dateRange: undefined as string[] | undefined, start_at_from: undefined as string | undefined,
/** 活动开始日 ≤(可空) */
start_at_to: undefined as string | undefined,
/** 活动结束日 ≥ */
end_at_from: undefined as string | undefined,
/** 活动结束日 ≤(可空) */
end_at_to: undefined as string | undefined,
}) })
const exportActivitiesLoading = ref(false) const exportActivitiesLoading = ref(false)
@ -152,8 +158,10 @@ function buildActivitiesListParams(page: number, pageSize: number) {
is_active: filters.is_active, is_active: filters.is_active,
schedule_status: filters.schedule_status, schedule_status: filters.schedule_status,
audit_status: filters.audit_status, audit_status: filters.audit_status,
start_date: filters.dateRange?.[0] || undefined, start_at_from: filters.start_at_from || undefined,
end_date: filters.dateRange?.[1] || undefined, start_at_to: filters.start_at_to || undefined,
end_at_from: filters.end_at_from || undefined,
end_at_to: filters.end_at_to || undefined,
} }
} }
@ -1022,7 +1030,7 @@ const activityListScrollX = computed(() => {
let w = let w =
50 + 50 +
220 + 220 +
200 + 280 +
130 + 130 +
100 + 100 +
80 + 80 +
@ -2143,12 +2151,33 @@ async function removeActivity(row: Activity) {
<a-option value="pending">待审核</a-option> <a-option value="pending">待审核</a-option>
<a-option value="rejected">已退回</a-option> <a-option value="rejected">已退回</a-option>
</a-select> </a-select>
<a-range-picker <a-date-picker
v-model="filters.dateRange" v-model="filters.start_at_from"
allow-clear allow-clear
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
:placeholder="['活动时间起', '活动时间止']" placeholder="活动开始时间起"
style="width: 280px" style="width: 168px"
/>
<a-date-picker
v-model="filters.start_at_to"
allow-clear
value-format="YYYY-MM-DD"
placeholder="活动开始时间止"
style="width: 168px"
/>
<a-date-picker
v-model="filters.end_at_from"
allow-clear
value-format="YYYY-MM-DD"
placeholder="活动结束时间起"
style="width: 168px"
/>
<a-date-picker
v-model="filters.end_at_to"
allow-clear
value-format="YYYY-MM-DD"
placeholder="活动结束时间止"
style="width: 168px"
/> />
<a-button type="primary" @click="onSearch"></a-button> <a-button type="primary" @click="onSearch"></a-button>
<a-button type="primary" @click="openCreate"></a-button> <a-button type="primary" @click="openCreate"></a-button>
@ -2178,7 +2207,7 @@ async function removeActivity(row: Activity) {
:ellipsis="true" :ellipsis="true"
:tooltip="true" :tooltip="true"
/> />
<a-table-column title="活动日期" :width="200" :min-width="150" :ellipsis="true" :tooltip="true"> <a-table-column title="活动日期" :width="280" :min-width="220" :ellipsis="true" :tooltip="true">
<template #cell="{ record }">{{ formatActivityTableDateRange(record as Activity) }}</template> <template #cell="{ record }">{{ formatActivityTableDateRange(record as Activity) }}</template>
</a-table-column> </a-table-column>
<a-table-column v-if="!isVenueAdmin()" title="场馆" :width="180" :min-width="120" :ellipsis="true" :tooltip="true"> <a-table-column v-if="!isVenueAdmin()" title="场馆" :width="180" :min-width="120" :ellipsis="true" :tooltip="true">

@ -73,6 +73,8 @@ const venuesList = ref<VenueMini[]>([])
const filterVenueId = ref<number | undefined>(undefined) const filterVenueId = ref<number | undefined>(undefined)
const filterActivityId = ref<number | undefined>(undefined) const filterActivityId = ref<number | undefined>(undefined)
const activityOptions = ref<{ label: string; value: number }[]>([]) const activityOptions = ref<{ label: string; value: number }[]>([])
const activitySearchLoading = ref(false)
let activitySearchTimer: ReturnType<typeof setTimeout> | null = null
function isSuperAdmin() { function isSuperAdmin() {
return currentUser.value?.full_admin_access === true return currentUser.value?.full_admin_access === true
@ -97,23 +99,39 @@ async function loadVenuesAndActivities() {
await loadActivityOptions() await loadActivityOptions()
} }
async function loadActivityOptions() { async function loadActivityOptions(keyword = '') {
activitySearchLoading.value = true
try { try {
const params: Record<string, unknown> = { page: 1, page_size: 500 } const params: Record<string, unknown> = { limit: 500 }
const kw = keyword.trim()
if (kw) params.keyword = kw
if (isSuperAdmin() && filterVenueId.value != null && filterVenueId.value > 0) { if (isSuperAdmin() && filterVenueId.value != null && filterVenueId.value > 0) {
params.venue_id = filterVenueId.value params.venue_id = filterVenueId.value
} }
const { data } = await http.get('/activities', { params }) const { data } = await http.get('/activities/options', { params })
const list = data?.data ?? [] const list = (data?.data ?? []) as { id: number; title: string }[]
activityOptions.value = list.map((a: { id: number; title: string }) => ({ const mapped = list.map((a) => ({ label: a.title, value: a.id }))
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 { } catch {
activityOptions.value = [] activityOptions.value = []
} finally {
activitySearchLoading.value = false
} }
} }
function onActivitySelectSearch(value: string) {
if (activitySearchTimer) clearTimeout(activitySearchTimer)
activitySearchTimer = setTimeout(() => {
void loadActivityOptions(value)
}, 300)
}
/** 列表与导出共用:不含分页参数 */ /** 列表与导出共用:不含分页参数 */
function activityRegistrationsQueryParams(): Record<string, unknown> { function activityRegistrationsQueryParams(): Record<string, unknown> {
const params: Record<string, unknown> = { const params: Record<string, unknown> = {
@ -240,8 +258,9 @@ function onPageChange(p: number) {
v-if="isSuperAdmin()" v-if="isSuperAdmin()"
v-model="filterVenueId" v-model="filterVenueId"
allow-clear allow-clear
placeholder="全部场馆" allow-search
style="width: 200px" placeholder="搜索或选择场馆"
style="width: 220px"
> >
<a-option v-for="v in venuesList" :key="v.id" :value="v.id">{{ v.name }}</a-option> <a-option v-for="v in venuesList" :key="v.id" :value="v.id">{{ v.name }}</a-option>
</a-select> </a-select>
@ -249,9 +268,13 @@ function onPageChange(p: number) {
v-model="filterActivityId" v-model="filterActivityId"
allow-clear allow-clear
allow-search allow-search
placeholder="全部活动" :filter-option="false"
style="width: 260px" :loading="activitySearchLoading"
placeholder="搜索或选择活动"
style="width: 280px"
@search="onActivitySelectSearch"
@change="onSearch" @change="onSearch"
@clear="() => loadActivityOptions()"
> >
<a-option v-for="opt in activityOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</a-option> <a-option v-for="opt in activityOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</a-option>
</a-select> </a-select>

@ -1,5 +1,5 @@
<script setup lang="ts"> <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 { Message } from '@arco-design/web-vue'
import { http } from '../../api/http' import { http } from '../../api/http'
import { formatDateTimeZh, formatDateZh } from '../../utils/datetime' import { formatDateTimeZh, formatDateZh } from '../../utils/datetime'
@ -36,6 +36,9 @@ type Reservation = {
activity_day?: ActivityDayRef | null activity_day?: ActivityDayRef | null
} }
type CurrentUser = { role?: string; full_admin_access?: boolean }
type VenueMini = { id: number; name: string }
const loading = ref(false) const loading = ref(false)
const rows = ref<Reservation[]>([]) const rows = ref<Reservation[]>([])
const tokenInput = ref('') const tokenInput = ref('')
@ -72,6 +75,82 @@ const dateRange = ref<string[]>([])
const listPagination = reactive({ current: 1, pageSize: 10 }) const listPagination = reactive({ current: 1, pageSize: 10 })
const exportVerifyLoading = ref(false) 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() { function exportVerifyXlsx() {
if (exportVerifyLoading.value) return if (exportVerifyLoading.value) return
exportVerifyLoading.value = true exportVerifyLoading.value = true
@ -93,16 +172,21 @@ function exportVerifyXlsx() {
async function loadRows() { async function loadRows() {
loading.value = true loading.value = true
try { try {
const { data } = await http.get('/reservations', { const params: Record<string, unknown> = {
params: { status: statusFilter.value,
status: statusFilter.value, keyword: keyword.value || undefined,
keyword: keyword.value || undefined, start_date: dateRange.value?.[0] || undefined,
start_date: dateRange.value?.[0] || undefined, end_date: dateRange.value?.[1] || undefined,
end_date: dateRange.value?.[1] || undefined, date_field: 'activity_day',
date_field: 'activity_day', reservation_kind: 'activity',
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 rows.value = data
listPagination.current = 1 listPagination.current = 1
} catch (error: any) { } 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> </script>
<template> <template>
@ -153,6 +248,31 @@ onMounted(loadRows)
<a-radio value="cancelled">已取消</a-radio> <a-radio value="cancelled">已取消</a-radio>
<a-radio value="expired">已过期</a-radio> <a-radio value="expired">已过期</a-radio>
</a-radio-group> </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" /> <a-input v-model="keyword" placeholder="报名人/手机/token" allow-clear style="width: 220px" />
<span class="verify-filter-label">场次日期</span> <span class="verify-filter-label">场次日期</span>
<a-range-picker v-model="dateRange" style="width: 260px" /> <a-range-picker v-model="dateRange" style="width: 260px" />

Loading…
Cancel
Save