Files
one-pipe-system/src/views/operations-reports/activation/index.vue
luo a6ce24f2f2
Some checks failed
构建并部署前端到测试环境 / build-and-deploy (push) Failing after 50m13s
fix: some
2026-09-20 10:22:39 +08:00

278 lines
8.3 KiB
Vue

<template>
<ArtTableFullScreen>
<div class="operations-report-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
:show-expand="false"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" :loading="exporting" @click="handleExport">导出</ElButton>
<span v-if="snapshotText" class="report-snapshot">{{ snapshotText }}</span>
<ElTag v-if="exportTask" :type="exportStatusType" class="report-export-tag">
{{ exportTask.status_name || '处理中' }}
</ElTag>
<ElButton v-if="canDownload" type="success" link @click="downloadExport">
下载
</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable :loading="loading" :data="tableData" :pagination="false" :marginTop="10">
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { ExportTaskService, OperationsReportsService } from '@/api/modules'
import { useAsyncTaskPolling } from '@/composables/useAsyncTaskPolling'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { ExportTaskStatus, type ExportTaskDetail } from '@/types/api'
import type { ActivationSummaryData, ActivationSummaryItem } from '@/types/api'
import type { SearchFormItem } from '@/types'
import { ACTIVATION_GROUP_OPTIONS, ACTIVATION_METRIC_COLUMNS, formatMetric } from '../constants'
defineOptions({ name: 'OperationsActivationReport' })
const pad = (value: number) => String(value).padStart(2, '0')
const formatDateTimeValue = (date: Date) => {
const offset = -date.getTimezoneOffset()
const sign = offset >= 0 ? '+' : '-'
const abs = Math.abs(offset)
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(
date.getHours()
)}:${pad(date.getMinutes())}:${pad(date.getSeconds())}${sign}${pad(
Math.floor(abs / 60)
)}:${pad(abs % 60)}`
}
const getDefaultRange = () => {
const end = new Date()
const start = new Date(end.getTime() - 29 * 24 * 60 * 60 * 1000)
start.setHours(0, 0, 0, 0)
return [formatDateTimeValue(start), formatDateTimeValue(end)]
}
const searchForm = reactive({
dateRange: getDefaultRange() as string[],
group_by: ''
})
const loading = ref(false)
const exporting = ref(false)
const summary = ref<ActivationSummaryData | null>(null)
const searchFormItems: SearchFormItem[] = [
{
label: '统计时间',
prop: 'dateRange',
type: 'datetimerange',
config: {
type: 'datetimerange',
rangeSeparator: '至',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'
}
},
{
label: '分组维度',
prop: 'group_by',
type: 'select',
config: { clearable: true, placeholder: '请选择分组维度' },
options: () => ACTIVATION_GROUP_OPTIONS
}
]
const columnOptions = [
{ label: '分组', prop: 'group_name' },
...ACTIVATION_METRIC_COLUMNS.map((col) => ({ label: col.label, prop: col.key }))
]
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'group_name',
label: '分组',
minWidth: 160,
fixed: 'left',
showOverflowTooltip: true,
formatter: (row: ActivationSummaryItem) => row.group_name || row.group_key || '-'
},
...ACTIVATION_METRIC_COLUMNS.map((col) => ({
prop: col.key,
label: col.label,
minWidth: col.minWidth,
align: 'right' as const,
formatter: (row: ActivationSummaryItem) => formatMetric(row[col.key], col)
}))
])
const snapshotText = computed(() => {
const dates = summary.value?.snapshot_dates
return dates?.length ? `数据快照:${dates.join('、')}` : ''
})
const tableData = computed(() => {
const data = summary.value
if (!data) return []
const rows = [...(data.items || [])]
if (data.totals) {
rows.push({ ...data.totals, group_name: '合计' })
}
return rows
})
const buildQuery = () => ({
start_time: searchForm.dateRange?.[0] || undefined,
end_time: searchForm.dateRange?.[1] || undefined,
group_by: searchForm.group_by || undefined
})
const getTableData = async () => {
if (!searchForm.dateRange?.[0] || !searchForm.dateRange?.[1]) {
ElMessage.warning('请选择统计时间')
return
}
loading.value = true
try {
const summaryRes = await OperationsReportsService.getActivationSummary(buildQuery())
if (summaryRes.code === 0 && summaryRes.data) {
summary.value = summaryRes.data
} else {
summary.value = null
ElMessage.error(summaryRes.msg || '获取设备激活汇总失败')
}
} catch (error) {
console.error('获取设备激活报表失败:', error)
summary.value = null
ElMessage.error('获取设备激活报表失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
getTableData()
}
const handleReset = () => {
searchForm.dateRange = getDefaultRange()
searchForm.group_by = ''
getTableData()
}
const handleRefresh = () => {
getTableData()
}
const exportTask = computed(() => exportPolling.task.value)
const canDownload = computed(
() =>
exportTask.value?.status === ExportTaskStatus.COMPLETED && !!exportTask.value?.download_url
)
const exportStatusType = computed(() => {
const status = exportTask.value?.status
if (status === ExportTaskStatus.COMPLETED) return 'success'
if (status === ExportTaskStatus.FAILED) return 'danger'
if (status === ExportTaskStatus.CANCELED) return 'info'
return 'warning'
})
const exportPolling = useAsyncTaskPolling<ExportTaskDetail>({
storageKey: 'operations-report-active:activation',
fetchTask: async (taskId) => {
const res = await ExportTaskService.getExportTaskDetail(taskId)
if (res.code === 403) {
const error = new Error('暂无权限查看导出任务') as Error & { status?: number }
error.status = 403
throw error
}
if (res.code !== 0 || !res.data) throw new Error(res.msg || '获取导出任务详情失败')
return res.data
},
isForbidden: (error: any) => error?.status === 403 || error?.response?.status === 403
})
const handleExport = async () => {
if (!searchForm.dateRange?.[0] || !searchForm.dateRange?.[1]) {
ElMessage.warning('请选择统计时间')
return
}
exporting.value = true
try {
const res = await OperationsReportsService.exportActivationSummary({
...buildQuery(),
format: 'xlsx'
})
if (res.code !== 0 || !res.data) {
ElMessage.error(res.msg || '导出失败')
return
}
ElMessage.success(res.data.message || '导出任务已创建,请稍候')
await exportPolling.start(res.data.task_id)
} catch (error) {
console.error('导出设备激活汇总失败:', error)
ElMessage.error('导出设备激活汇总失败')
} finally {
exporting.value = false
}
}
const downloadExport = () => {
const url = exportTask.value?.download_url
if (!url) {
ElMessage.warning('当前任务暂无可用下载地址')
return
}
window.open(url, '_blank')
}
onMounted(() => {
getTableData()
})
</script>
<style lang="scss" scoped>
.operations-report-page {
display: flex;
flex-direction: column;
gap: 10px;
height: 100%;
overflow: auto;
}
.report-snapshot {
font-size: 12px;
font-weight: 400;
margin-left: 20px;
color: var(--el-text-color-secondary);
}
.report-export-tag {
margin-left: 12px;
}
</style>