|
|
|
|
@ -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>
|
|
|
|
|
|
|
|
|
|
<!-- 概览:参考双卡片 — 数据统计 | 待办事项 -->
|
|
|
|
|
|