master
lion 2 days ago
parent 04d3340fa2
commit 520152477f

@ -55,6 +55,9 @@ const stats = ref({
role: '',
venue_id: undefined as number | undefined,
activity_id: undefined as number | undefined,
venue_ids: [] as number[],
people_counting_venue_ids: [] as number[],
show_venue_people_stats: false,
},
summary: {
activity_sessions: 0,
@ -197,7 +200,30 @@ function parseVenuePcRowsFromResponse(p: HikPeopleCountingResponse | null): Dash
})
}
const dashboardVenuePcRows = computed((): DashboardVenuePcRow[] => parseVenuePcRowsFromResponse(pcData.value))
const isVenueAdminOnlyDashboard = computed(() => isVenueAdmin.value && !isSuperAdmin.value)
function filterVenuePcRowsForScope(rows: DashboardVenuePcRow[]): DashboardVenuePcRow[] {
if (!isVenueAdminOnlyDashboard.value) return rows
const scopedIds = stats.value.scope.people_counting_venue_ids
if (!scopedIds.length) return []
const allowed = new Set(scopedIds.map((id) => String(id)))
return rows.filter((row) => allowed.has(String(row.venueId)))
}
const showVenuePeopleStatsBlock = computed(() => {
if (isVenueAdminOnlyDashboard.value) {
return stats.value.scope.show_venue_people_stats === true && peopleCountingConfigured.value
}
return true
})
const venuePeopleStatsTitle = computed(() =>
isVenueAdminOnlyDashboard.value ? '场馆人数统计' : '各场馆人数统计',
)
const dashboardVenuePcRows = computed((): DashboardVenuePcRow[] =>
filterVenuePcRowsForScope(parseVenuePcRowsFromResponse(pcData.value)),
)
type VenuePcViewMode = 'chart' | 'table'
const venuePcViewMode = ref<VenuePcViewMode>('chart')
@ -414,7 +440,10 @@ type VenuePcExportPreset = {
sheetName: string
}
function buildVenuePcExportPreset(kind: 'today' | 'week' | 'month' | 'year'): VenuePcExportPreset {
function buildVenuePcExportPreset(
kind: 'today' | 'week' | 'month' | 'year',
statsLabel = '各场馆人数统计',
): VenuePcExportPreset {
const today = todayCalendar()
const todayStr = ymdCalendar(today)
if (kind === 'today') {
@ -422,7 +451,7 @@ function buildVenuePcExportPreset(kind: 'today' | 'week' | 'month' | 'year'): Ve
start: todayStr,
end: todayStr,
periodLabel: todayStr,
sheetName: `各场馆人数统计-当天(${todayStr})`,
sheetName: `${statsLabel}-当天(${todayStr})`,
}
}
if (kind === 'week') {
@ -433,7 +462,7 @@ function buildVenuePcExportPreset(kind: 'today' | 'week' | 'month' | 'year'): Ve
start,
end,
periodLabel: `${start}${end}`,
sheetName: `各场馆人数统计-本周(${start}${end})`,
sheetName: `${statsLabel}-本周(${start}${end})`,
}
}
if (kind === 'month') {
@ -445,7 +474,7 @@ function buildVenuePcExportPreset(kind: 'today' | 'week' | 'month' | 'year'): Ve
start,
end,
periodLabel: `${start}${end}`,
sheetName: `各场馆人数统计-本月(${start}${end})`,
sheetName: `${statsLabel}-本月(${start}${end})`,
}
}
const first = new Date(today.getFullYear(), 0, 1)
@ -456,7 +485,7 @@ function buildVenuePcExportPreset(kind: 'today' | 'week' | 'month' | 'year'): Ve
start,
end,
periodLabel: `${start}${end}`,
sheetName: `各场馆人数统计-本年(${start}${end})`,
sheetName: `${statsLabel}-本年(${start}${end})`,
}
}
@ -467,7 +496,63 @@ function dashboardTicketGrabVerifyRatePctStr(): string {
return `${p != null ? p : 0}%`
}
async function exportVenueAdminDashboardExcel() {
if (exportingDashboard.value) return
exportingDashboard.value = true
try {
const wb = XLSX.utils.book_new()
const act = stats.value.activity_schedule_counts ?? ({} as ActivityScheduleCountsBlock)
appendDashboardWorkbookSheet(
wb,
buildMetricSheet([
['活动数', act.total ?? 0],
['总场次', act.total_sessions ?? 0],
['未开始', act.not_started ?? 0],
['进行中', act.ongoing ?? 0],
['已结束', act.ended ?? 0],
]),
'活动统计',
)
const statsLabel = venuePeopleStatsTitle.value
if (stats.value.scope.show_venue_people_stats && getPeopleCountingEndpoint()) {
const pcPresets: VenuePcExportPreset[] = (['today', 'week', 'month', 'year'] as const).map((k) =>
buildVenuePcExportPreset(k, statsLabel),
)
const pcResponses = await Promise.all(
pcPresets.map((preset) => fetchHikPeopleCounting(preset.start, preset.end).catch(() => null)),
)
pcPresets.forEach((preset, idx) => {
const resp = pcResponses[idx]
const rows = filterVenuePcRowsForScope(parseVenuePcRowsFromResponse(resp))
const note =
resp && resp.code !== 200 ? resp.message || `接口返回错误码 ${resp.code}` : undefined
appendDashboardWorkbookSheet(
wb,
buildVenuePcExportSheet(preset.periodLabel, rows, note),
preset.sheetName,
)
})
}
XLSX.writeFile(wb, dashboardExportFilename())
Message.success('数据看板已导出')
} catch (err: unknown) {
console.error('[工作台] 场馆管理员导出失败', err)
const msg =
err instanceof Error ? err.message : typeof err === 'string' ? err : '导出失败'
Message.error(msg.includes('already exists') ? '导出失败Sheet 名称重复,请刷新后重试' : msg || '导出失败')
} finally {
exportingDashboard.value = false
}
}
async function exportDashboardExcel() {
if (isVenueAdminOnlyDashboard.value) {
await exportVenueAdminDashboardExcel()
return
}
if (exportingDashboard.value) return
exportingDashboard.value = true
try {
@ -563,7 +648,7 @@ async function exportDashboardExcel() {
)
pcPresets.forEach((preset, idx) => {
const resp = pcResponses[idx]
const rows = parseVenuePcRowsFromResponse(resp)
const rows = filterVenuePcRowsForScope(parseVenuePcRowsFromResponse(resp))
const note =
resp && resp.code !== 200 ? resp.message || `接口返回错误码 ${resp.code}` : undefined
appendDashboardWorkbookSheet(
@ -603,13 +688,14 @@ function exportVenuePeopleExcel() {
}
exportingVenuePc.value = true
try {
const statsLabel = venuePeopleStatsTitle.value
const table = [['场馆ID', '场馆名称', '入馆总人数'], ...rows.map((r) => [r.venueId, r.venueName, r.enter])]
const enterSum = rows.reduce((s, r) => s + (Number(r.enter) || 0), 0)
table.push(['合计', '—', enterSum])
const ws = XLSX.utils.aoa_to_sheet(table)
const wb = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(wb, ws, '各场馆人数统计')
const filename = `${picked.start}${picked.end}各场馆人数统计.xlsx`
XLSX.utils.book_append_sheet(wb, ws, sanitizeExcelSheetName(statsLabel))
const filename = `${picked.start}${picked.end}${statsLabel}.xlsx`
XLSX.writeFile(wb, filename)
Message.success('已导出')
} finally {
@ -629,7 +715,7 @@ const hasPendingTodoRows = computed(
() => pendingActivityItems.value.length > 0 || pendingVenueItems.value.length > 0,
)
const dashOverviewSplit = computed(() => isSuperAdmin.value || isVenueAdmin.value)
const dashOverviewSplit = computed(() => isSuperAdmin.value)
const todoEmptyPlaceholder = computed(() =>
isVenueAdmin.value ? '暂无已退回活动' : '暂无待审核事项',
@ -809,7 +895,10 @@ async function exportDailyVerifyExcel() {
onMounted(async () => {
await loadMe()
await loadStats()
if (getPeopleCountingEndpoint()) {
const shouldLoadVenuePeople =
getPeopleCountingEndpoint() &&
(!isVenueAdminOnlyDashboard.value || stats.value.scope.show_venue_people_stats === true)
if (shouldLoadVenuePeople) {
applyVenuePcRangeToday()
await loadVenuePeopleRange({ silentSuccess: true })
}
@ -830,14 +919,20 @@ onMounted(async () => {
<h1 class="dashboard-page__title">工作台</h1>
<span class="dashboard-page__subtitle">数据看板</span>
</div>
<a-button type="primary" size="small" :loading="exportingDashboard" @click="exportDashboardExcel">
<a-button
v-if="isSuperAdmin || isVenueAdminOnlyDashboard"
type="primary"
size="small"
:loading="exportingDashboard"
@click="exportDashboardExcel"
>
导出数据
</a-button>
</header>
<!-- 概览参考双卡片 数据统计 | 待办事项 -->
<section class="dash-bundle" aria-label="">
<div class="dash-overview-dual" :class="{ 'dash-overview-dual--split': dashOverviewSplit }">
<div v-if="isSuperAdmin" class="dash-overview-dual" :class="{ 'dash-overview-dual--split': dashOverviewSplit }">
<div class="dash-core-pack">
<article class="dash-metric-card dash-metric-card--core">
<header class="dash-metric-card__head">
@ -879,7 +974,7 @@ onMounted(async () => {
</article>
</div>
<article v-if="isSuperAdmin || isVenueAdmin" class="dash-metric-card dash-metric-card--todo">
<article v-if="isSuperAdmin" class="dash-metric-card dash-metric-card--todo">
<header class="dash-metric-card__head dash-metric-card__head--todo">
<div class="dash-metric-card__icon dash-metric-card__icon--todo" aria-hidden="true">
<IconOrderedList />
@ -926,7 +1021,7 @@ onMounted(async () => {
</article>
</div>
<div class="dash-schedule-dual">
<div class="dash-schedule-dual" :class="{ 'dash-schedule-dual--single': isVenueAdminOnlyDashboard }">
<article class="dash-metric-card">
<header class="dash-metric-card__head">
<div class="dash-metric-card__icon" aria-hidden="true">
@ -958,7 +1053,7 @@ onMounted(async () => {
<div class="dash-stat-cell__value">{{ stats.activity_schedule_counts.ended }}</div>
<div class="dash-stat-cell__label">已结束</div>
</div>
<div class="dash-stat-cell dash-stat-cell--indigo">
<div v-if="!isVenueAdminOnlyDashboard" class="dash-stat-cell dash-stat-cell--indigo">
<div class="dash-stat-cell__value">{{ stats.activity_schedule_counts.published_venues_count ?? 0 }}</div>
<div class="dash-stat-cell__label">已发布活动场馆</div>
</div>
@ -966,7 +1061,7 @@ onMounted(async () => {
</div>
</article>
<article class="dash-metric-card">
<article v-if="isSuperAdmin" class="dash-metric-card">
<header class="dash-metric-card__head">
<div class="dash-metric-card__icon dash-metric-card__icon--schedule-tg" aria-hidden="true">
<IconGift />
@ -1003,7 +1098,7 @@ onMounted(async () => {
</article>
</div>
<div class="dash-rank-dual">
<div v-if="isSuperAdmin" class="dash-rank-dual">
<article class="dash-metric-card dash-metric-card--rank">
<header class="dash-metric-card__head">
<div class="dash-metric-card__icon dash-metric-card__icon--rank-pub" aria-hidden="true">
@ -1066,14 +1161,14 @@ onMounted(async () => {
</article>
</div>
<div class="dash-venue-pc-bundle">
<div v-if="showVenuePeopleStatsBlock" class="dash-venue-pc-bundle">
<article class="dash-metric-card dash-metric-card--venue-pc">
<header class="dash-metric-card__head">
<div class="dash-metric-card__icon dash-metric-card__icon--venue-pc" aria-hidden="true">
<IconBarChart />
</div>
<div class="dash-metric-card__head-main">
<h2 class="dash-metric-card__title">各场馆人数统计</h2>
<h2 class="dash-metric-card__title">{{ venuePeopleStatsTitle }}</h2>
</div>
</header>
<div class="dash-metric-card__body dash-metric-card__body--venue-pc">
@ -1543,6 +1638,10 @@ onMounted(async () => {
grid-template-columns: 1fr 1fr;
align-items: stretch;
}
.dash-schedule-dual--single {
grid-template-columns: 1fr;
}
}
.dash-metric-card__icon--schedule-tg {

Loading…
Cancel
Save