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