场馆审核等修改

master
lion 1 week ago
parent 5a42c95965
commit 5bccbf38a6

@ -171,8 +171,7 @@ function applyVenuePcRangeToday() {
type DashboardVenuePcRow = Pick<HikVenueRangeTotalRow, 'venueId' | 'venueName' | 'enter'>
/** 区间内优先用 venuesRangeTotals单日或无区间汇总时用 venues当日快照按「进入人数」降序 */
const dashboardVenuePcRows = computed((): DashboardVenuePcRow[] => {
const p = pcData.value
function parseVenuePcRowsFromResponse(p: HikPeopleCountingResponse | null): DashboardVenuePcRow[] {
if (!p || p.code !== 200) return []
let raw: DashboardVenuePcRow[] = []
const rangeTotals = p.venuesRangeTotals
@ -196,7 +195,9 @@ const dashboardVenuePcRows = computed((): DashboardVenuePcRow[] => {
if (d !== 0) return d
return String(a.venueId).localeCompare(String(b.venueId), undefined, { numeric: true })
})
})
}
const dashboardVenuePcRows = computed((): DashboardVenuePcRow[] => parseVenuePcRowsFromResponse(pcData.value))
function dashboardVenuePcSummary(ctx: { data: DashboardVenuePcRow[] }) {
const rows = Array.isArray(ctx.data) ? ctx.data : []
@ -322,6 +323,238 @@ const ticketGrabStats = ref<{
const dailyVerifyLabels = computed(() => ticketGrabStats.value?.daily_verify_matrix?.date_labels ?? [])
const dailyVerifyRows = computed(() => ticketGrabStats.value?.daily_verify_matrix?.rows ?? [])
const exportingVenuePc = ref(false)
const exportingDashboard = ref(false)
function sanitizeExcelSheetName(name: string): string {
return name.replace(/[\\/?*[\]:]/g, '_').slice(0, 31) || 'Sheet1'
}
/** Excel sheet 名不可重复;重名时自动追加 (2)、(3)… */
function appendDashboardWorkbookSheet(
wb: XLSX.WorkBook,
ws: XLSX.WorkSheet,
desiredName: string,
) {
const base = sanitizeExcelSheetName(desiredName)
let name = base
let n = 2
while (wb.SheetNames.includes(name)) {
const suffix = `(${n})`
name = sanitizeExcelSheetName(`${base.slice(0, Math.max(1, 31 - suffix.length))}${suffix}`)
n += 1
}
XLSX.utils.book_append_sheet(wb, ws, name)
}
function dashboardExportFilename(): string {
const d = new Date()
const p = (n: number) => String(n).padStart(2, '0')
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}统计.xlsx`
}
function buildMetricSheet(pairs: Array<[string, string | number]>) {
return XLSX.utils.aoa_to_sheet([['指标', '数值'], ...pairs.map(([k, v]) => [k, v])])
}
function buildVenuePcExportSheet(rangeLabel: string, rows: DashboardVenuePcRow[], note?: string) {
if (note) {
return XLSX.utils.aoa_to_sheet([['统计区间', rangeLabel], ['说明', note]])
}
if (!rows.length) {
return XLSX.utils.aoa_to_sheet([['统计区间', rangeLabel], [], ['说明', '暂无数据']])
}
const enterSum = rows.reduce((s, r) => s + (Number(r.enter) || 0), 0)
return XLSX.utils.aoa_to_sheet([
['统计区间', rangeLabel],
[],
['场馆ID', '场馆名称', '入馆总人数'],
...rows.map((r) => [r.venueId, r.venueName, r.enter]),
['合计', '—', enterSum],
])
}
type VenuePcExportPreset = {
start: string
end: string
periodLabel: string
sheetName: string
}
function buildVenuePcExportPreset(kind: 'today' | 'week' | 'month' | 'year'): VenuePcExportPreset {
const today = todayCalendar()
const todayStr = ymdCalendar(today)
if (kind === 'today') {
return {
start: todayStr,
end: todayStr,
periodLabel: todayStr,
sheetName: `各场馆人数统计-当天(${todayStr})`,
}
}
if (kind === 'week') {
const mon = startOfCalendarWeekMonday(today)
const start = ymdCalendar(mon)
const end = ymdCalendar(minCalendarDate(endOfCalendarWeekSunday(mon), today))
return {
start,
end,
periodLabel: `${start}${end}`,
sheetName: `各场馆人数统计-本周(${start}${end})`,
}
}
if (kind === 'month') {
const first = new Date(today.getFullYear(), today.getMonth(), 1)
const last = new Date(today.getFullYear(), today.getMonth() + 1, 0)
const start = ymdCalendar(first)
const end = ymdCalendar(minCalendarDate(last, today))
return {
start,
end,
periodLabel: `${start}${end}`,
sheetName: `各场馆人数统计-本月(${start}${end})`,
}
}
const first = new Date(today.getFullYear(), 0, 1)
const last = new Date(today.getFullYear(), 11, 31)
const start = ymdCalendar(first)
const end = ymdCalendar(minCalendarDate(last, today))
return {
start,
end,
periodLabel: `${start}${end}`,
sheetName: `各场馆人数统计-本年(${start}${end})`,
}
}
function dashboardTicketGrabVerifyRatePctStr(): string {
const t = stats.value.ticket_grab_schedule_counts
if (!t || t.booked_people <= 0) return '0%'
const p = t.verify_rate_pct
return `${p != null ? p : 0}%`
}
async function exportDashboardExcel() {
if (exportingDashboard.value) return
exportingDashboard.value = true
try {
const wb = XLSX.utils.book_new()
const s = stats.value
const summary = s.summary ?? ({} as typeof s.summary)
const act = s.activity_schedule_counts ?? ({} as ActivityScheduleCountsBlock)
const tg = s.ticket_grab_schedule_counts ?? ({} as TicketGrabScheduleCountBlock)
const publishRanking = Array.isArray(s.activity_publish_ranking) ? s.activity_publish_ranking : []
const liveRanking = Array.isArray(s.live_people_ranking) ? s.live_people_ranking : []
const corePairs: Array<[string, string | number]> = [
['现有场馆', summary.venues_count ?? 0],
['总预约次数', summary.reservation_order_count ?? 0],
]
if (isSuperAdmin.value) {
corePairs.push(['总用户数', summary.wechat_user_count ?? 0])
}
corePairs.push(['预约用户数', summary.user_count ?? 0])
if (isSuperAdmin.value) {
corePairs.push(['总访问量', summary.home_visit_total ?? 0])
corePairs.push(['今日访问量', summary.home_visit_today ?? 0])
}
appendDashboardWorkbookSheet(wb, buildMetricSheet(corePairs), '核心数据统计')
appendDashboardWorkbookSheet(
wb,
buildMetricSheet([
['活动数', act.total ?? 0],
['总场次', act.total_sessions ?? 0],
['未开始', act.not_started ?? 0],
['进行中', act.ongoing ?? 0],
['已结束', act.ended ?? 0],
['已发布活动场馆', act.published_venues_count ?? 0],
]),
'活动统计',
)
appendDashboardWorkbookSheet(
wb,
buildMetricSheet([
['总场数', tg.total ?? 0],
['未开始', tg.not_started ?? 0],
['进行中', tg.ongoing ?? 0],
['已结束', tg.ended ?? 0],
['核销率', dashboardTicketGrabVerifyRatePctStr()],
['已核销人数', tg.verified_people ?? 0],
['已约人数', tg.booked_people ?? 0],
]),
'抢票统计',
)
const publishRows = publishRanking.map((r, i) => [
i + 1,
r.venue_id,
r.venue_name,
r.published_count,
r.published_sessions_count,
])
appendDashboardWorkbookSheet(
wb,
XLSX.utils.aoa_to_sheet([
['排名', '场馆ID', '场馆名称', '发布活动数', '发布场次数'],
...publishRows,
]),
'活动发布排行',
)
const liveRows = liveRanking.map((r, i) => [i + 1, r.venue_id, r.venue_name, r.live_count])
appendDashboardWorkbookSheet(
wb,
XLSX.utils.aoa_to_sheet([['排名', '场馆ID', '场馆名称', '在馆人数'], ...liveRows]),
'实时人数排行',
)
const pcPresets: VenuePcExportPreset[] = (
['today', 'week', 'month', 'year'] as const
).map((k) => buildVenuePcExportPreset(k))
if (!getPeopleCountingEndpoint()) {
for (const preset of pcPresets) {
appendDashboardWorkbookSheet(
wb,
buildVenuePcExportSheet(preset.periodLabel, [], '未配置客流统计接口'),
preset.sheetName,
)
}
} else {
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 = 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
}
}
function exportVenuePeopleExcel() {
const picked = pickVenuePcRangeFromPicker()
@ -563,6 +796,9 @@ 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>
</header>
<!-- 概览参考双卡片 数据统计 | 待办事项 -->

Loading…
Cancel
Save