feat(reports): 接入真实报表分析及分组明细与异步导出

This commit is contained in:
xy
2026-09-30 18:27:26 +08:00
parent e5f0a5ec97
commit f155c5154f
14 changed files with 928 additions and 982 deletions
@@ -1,91 +1,111 @@
/**
* 报表分析的数据装载与筛选:四个菜单共用同一个数据集与同一份筛选条件。
*
* 数据集放在模块级而不是每个页面各取一次:四个菜单的数据源完全一样,
* 各取一次会生成四份互不相干的演示数据 —— 从「签署总览」切到「签署量分析」,
* 累计发起数会变,看起来像统计口径不同,其实只是随机数不同。
*/
import { computed, ref } from 'vue'
import { computed, ref, watch } from 'vue'
import {
getReportAnalyticsDataset,
buildReportAnalyticsQuery,
getReportAnalytics,
getReportOptions,
type ReportAnalyticsDataset,
type ReportTaskRow,
type ReportOptions,
} from '@/api/workbench/report-analytics'
import { toUserMessage } from '@/utils/api-response'
import { filterReportTasks, resolveDateWindow } from './aggregate'
import { resolveDateWindow } from './aggregate'
import { reportFilter } from './filter-store'
const dataset = ref<ReportAnalyticsDataset | null>(null)
const options = ref<ReportOptions | null>(null)
const loading = ref(false)
const errorMessage = ref('')
const optionsError = ref('')
let pending: Promise<void> | null = null
/**
* 装载数据集。
*
* 并发去重:页面挂载与筛选条刷新可能同时触发,重复请求没有意义。
* 只取一次成功后就不再重取 —— 演示数据是确定性的,重取只会换掉「数据截至」的时刻。
*/
export function loadReportDataset(): Promise<void> {
if (dataset.value) {
return Promise.resolve()
}
pending ??= (async () => {
loading.value = true
errorMessage.value = ''
let requestSequence = 0
let pendingOptions: Promise<void> | null = null
let filterWatcherInstalled = false
let debounceTimer: ReturnType<typeof setTimeout> | null = null
export function loadReportOptions(): Promise<void> {
if (options.value) return Promise.resolve()
pendingOptions ??= (async () => {
optionsError.value = ''
try {
dataset.value = await getReportAnalyticsDataset()
} catch {
errorMessage.value = '报表数据加载失败,请稍后重试'
options.value = await getReportOptions()
} catch (error) {
optionsError.value = toUserMessage(error, '报表筛选项加载失败')
} finally {
loading.value = false
pending = null
pendingOptions = null
}
})()
return pending
return pendingOptions
}
export async function loadReportDataset(): Promise<void> {
const sequence = ++requestSequence
dataset.value = null
const dateWindow = resolveDateWindow(reportFilter)
if (!dateWindow) {
errorMessage.value = ''
loading.value = false
return
}
loading.value = true
errorMessage.value = ''
try {
const query = buildReportAnalyticsQuery(reportFilter, dateWindow)
const result = await getReportAnalytics(query)
if (sequence === requestSequence) dataset.value = result
} catch (error) {
if (sequence === requestSequence) errorMessage.value = toUserMessage(error, '报表数据加载失败')
} finally {
if (sequence === requestSequence) loading.value = false
}
}
function installFilterWatcher() {
if (filterWatcherInstalled) return
filterWatcherInstalled = true
watch(
() => [
reportFilter.dateRange,
reportFilter.customFrom,
reportFilter.customTo,
reportFilter.campus,
reportFilter.department,
reportFilter.category,
reportFilter.method,
],
() => {
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => void loadReportDataset(), 250)
},
)
}
/** 仅供演示 / 验证脚本:清掉缓存后重新装载 */
export function resetReportDataset(): void {
dataset.value = null
pending = null
requestSequence++
if (debounceTimer) clearTimeout(debounceTimer)
}
export function useReportDataset() {
/** 当前筛选对应的时间窗口;null 表示自定义区间还没填完 */
installFilterWatcher()
const window = computed(() => resolveDateWindow(reportFilter))
const allTasks = computed<ReportTaskRow[]>(() => dataset.value?.tasks ?? [])
/** 当前筛选下的任务明细:四个页面的所有数字都从这里算 */
const tasks = computed<ReportTaskRow[]>(() => filterReportTasks(allTasks.value, reportFilter))
const available = computed(() => dataset.value?.available ?? false)
const available = computed(() => dataset.value !== null)
const generatedAt = computed(() => dataset.value?.generatedAt ?? '')
/**
* 自定义区间只填了一头时为 true。
* 这种半截条件既不该按半截筛,也不该悄悄套默认窗口,页面据此提示用户补全。
*/
const customRangeIncomplete = computed(
() =>
reportFilter.dateRange === 'custom' && (!reportFilter.customFrom || !reportFilter.customTo),
)
return {
allTasks,
tasks,
dataset,
options,
window,
available,
generatedAt,
loading,
errorMessage,
optionsError,
customRangeIncomplete,
}
}