Files
cma-management/frontend/src/views/BIReportCenter.vue
T

327 lines
12 KiB
Vue

<template>
<div>
<div class="flex-between section-gap">
<h3 class="page-title">BI报表中心</h3>
<div>
<el-button type="primary" @click="showTemplateDialog = true">从模板创建</el-button>
<el-button @click="showNewReport = true">新建分析报表</el-button>
</div>
</div>
<!-- 预置模板 -->
<el-card shadow="never" class="section-gap">
<template #header>预置报表模板</template>
<el-row :gutter="16">
<el-col v-for="tpl in templates" :key="tpl.id" :span="24 / templates.length || 1">
<el-card shadow="hover" :body-style="{ padding: '16px', cursor: 'pointer' }" @click="useTemplate(tpl)">
<div class="tpl-card-body">
<div class="tpl-icon">
<span v-if="tpl.report_type === 'overview'">📊</span>
<span v-else-if="tpl.report_type === 'trend'">📈</span>
<span v-else-if="tpl.report_type === 'comparison'">📋</span>
<span v-else-if="tpl.report_type === 'topn'">🔝</span>
<span v-else>🔗</span>
</div>
<div class="tpl-name">{{ tpl.name }}</div>
<div class="tpl-desc">{{ tpl.config?.description || '' }}</div>
</div>
</el-card>
</el-col>
</el-row>
</el-card>
<!-- 分析引擎 -->
<el-card v-if="analysisResult" shadow="never" class="section-gap">
<template #header>
<div class="card-header-flex">
<span>{{ currentReportName }}</span>
<div>
<el-button size="small" @click="showSaveDialog = true">保存报表</el-button>
<el-button size="small" @click="exportData">导出CSV</el-button>
<el-button size="small" @click="analysisResult = null">关闭</el-button>
</div>
</div>
</template>
<!-- 图表区域 -->
<v-chart :option="analysisChartOption" autoresize class="bi-chart" />
<!-- 数据表格 -->
<el-table :data="analysisResult.rows" size="small" border class="bi-data-table" max-height="400">
<el-table-column prop="kpi_code" label="编码" width="100" />
<el-table-column prop="kpi_name" label="KPI名称" min-width="150" />
<el-table-column prop="dimension" label="维度" width="80">
<template #default="{ row }">{{ { finance: '财务', customer: '客户', process: '流程', learning: '学习' }[row.dimension] || row.dimension }}</template>
</el-table-column>
<el-table-column prop="current_value" label="当前值" width="100" />
<el-table-column prop="current_period" label="期间" width="100" />
<el-table-column prop="target_value" label="目标值" width="100" />
<el-table-column prop="unit" label="单位" width="60" />
</el-table>
</el-card>
<!-- 已保存报表 -->
<el-card shadow="never" class="section-gap">
<template #header>已保存报表</template>
<el-table :data="reports" size="small" v-loading="loading">
<el-table-column prop="name" label="报表名称" min-width="200" />
<el-table-column prop="chart_type" label="图表类型" width="100" />
<el-table-column prop="is_shared" label="分享状态" width="100">
<template #default="{ row }">{{ row.is_shared ? '已分享' : '私有' }}</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="160" />
<el-table-column label="操作" width="160">
<template #default="{ row }">
<el-button size="small" @click="openReport(row)">打开</el-button>
<el-button size="small" type="danger" plain @click="deleteReport(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!loading && reports.length === 0" description="暂无已保存报表" />
</el-card>
<!-- 模板选择对话框 -->
<el-dialog v-model="showTemplateDialog" title="选择报表模板" :width="400">
<el-table :data="templates" highlight-current-row @row-click="tpl => { selectedTemplate = tpl; }" :show-header="false">
<el-table-column prop="name" label="模板" min-width="120" />
<el-table-column prop="report_type" label="类型" width="100">
<template #default="{ row }">
<el-tag size="small">{{ row.report_type }}</el-tag>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="showTemplateDialog = false">取消</el-button>
<el-button type="primary" @click="confirmTemplate" :disabled="!selectedTemplate">使用此模板</el-button>
</template>
</el-dialog>
<!-- 新建分析报表 -->
<el-dialog v-model="showNewReport" title="新建分析报表" :width="600">
<el-form label-width="100px">
<el-form-item label="报表名称">
<el-input v-model="newReportName" placeholder="输入报表名称" />
</el-form-item>
<el-form-item label="维度筛选">
<el-checkbox-group v-model="selectedDims">
<el-checkbox label="finance">财务</el-checkbox>
<el-checkbox label="customer">客户</el-checkbox>
<el-checkbox label="process">流程</el-checkbox>
<el-checkbox label="learning">学习</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="图表类型">
<el-select v-model="chartType" class="full-width-select">
<el-option label="自动" value="auto" />
<el-option label="柱状图" value="bar" />
<el-option label="折线图" value="line" />
<el-option label="饼图" value="pie" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showNewReport = false">取消</el-button>
<el-button type="primary" @click="doAnalyze" :loading="analyzing">生成报表</el-button>
</template>
</el-dialog>
<!-- 保存报表对话框 -->
<el-dialog v-model="showSaveDialog" title="保存报表" :width="400">
<el-form label-width="80px">
<el-form-item label="报表名称">
<el-input v-model="saveReportName" placeholder="输入报表名称" />
</el-form-item>
<el-form-item label="是否分享">
<el-switch v-model="saveShare" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showSaveDialog = false">取消</el-button>
<el-button type="primary" @click="saveReport" :loading="saving">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus'
import VChart from 'vue-echarts'
import 'echarts'
import { biReportApi, kpiApi } from '../api/index'
const templates = ref<any[]>([])
const reports = ref<any[]>([])
const loading = ref(false)
const analyzing = ref(false)
const saving = ref(false)
// 模板对话框
const showTemplateDialog = ref(false)
const selectedTemplate = ref<any>(null)
// 新建报表
const showNewReport = ref(false)
const newReportName = ref('')
const selectedDims = ref(['finance', 'customer', 'process', 'learning'])
const chartType = ref('auto')
// 分析结果
const analysisResult = ref<any>(null)
const currentReportName = ref('')
// 保存
const showSaveDialog = ref(false)
const saveReportName = ref('')
const saveShare = ref(false)
const currentConfig = ref<any>(null)
const analysisChartOption = computed(() => {
if (!analysisResult.value) return {}
const rows = analysisResult.value.rows || []
const chartTypeVal = analysisResult.value.chart_type || 'auto'
if (chartTypeVal === 'bar' || chartTypeVal === 'auto') {
return {
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: rows.map(r => r.kpi_code), axisLabel: { rotate: 45, fontSize: 10 } },
yAxis: { type: 'value' },
series: [
{ name: '当前值', type: 'bar', data: rows.map(r => r.current_value), itemStyle: { color: '#409eff' } },
{ name: '目标值', type: 'bar', data: rows.map(r => r.target_value), itemStyle: { color: '#e6a23c' } },
],
grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true },
}
}
if (chartTypeVal === 'line') {
return {
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: rows.map(r => r.kpi_code) },
yAxis: { type: 'value' },
series: [{
type: 'line', data: rows.map(r => r.current_value),
smooth: true, lineStyle: { width: 2 },
areaStyle: { color: 'rgba(64,158,255,0.1)' },
}],
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
}
}
return {}
})
async function loadData() {
try {
const [tr, rr]: any = await Promise.all([
biReportApi.templates(),
biReportApi.list(),
])
templates.value = tr.data || []
reports.value = rr.data || []
} catch (e) {}
}
function useTemplate(tpl: any) {
showTemplateDialog.value = false
currentReportName.value = tpl.name
selectedDims.value = tpl.config?.dimensions || ['finance', 'customer', 'process', 'learning']
chartType.value = tpl.config?.chart_type || 'auto'
const config: any = {
dimensions: selectedDims.value,
period_start: '',
period_end: '',
}
if (tpl.config?.window_months) {
const d = new Date()
d.setMonth(d.getMonth() - tpl.config.window_months)
config.period_start = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
}
currentConfig.value = config
doAnalyze()
}
function confirmTemplate() {
if (selectedTemplate.value) {
useTemplate(selectedTemplate.value)
}
}
async function doAnalyze() {
analyzing.value = true
try {
const config: any = {
dimensions: selectedDims.value,
chart_type: chartType.value,
}
if (currentConfig.value?.period_start) config.period_start = currentConfig.value.period_start
const r: any = await biReportApi.analyze({ config, chart_type: chartType.value })
analysisResult.value = r
showNewReport.value = false
} catch (e: any) {
ElMessage.error(e?.response?.data?.detail || '分析失败')
}
analyzing.value = false
}
async function saveReport() {
saving.value = true
try {
await biReportApi.create({
name: saveReportName.value || currentReportName.value || '未命名报表',
config: currentConfig.value || analysisResult.value?.config || {},
chart_type: chartType.value,
is_shared: saveShare.value ? 1 : 0,
})
ElMessage.success('保存成功')
showSaveDialog.value = false
await loadData()
} catch (e) { ElMessage.error('保存失败') }
saving.value = false
}
async function openReport(report: any) {
currentReportName.value = report.name
chartType.value = report.chart_type || 'auto'
currentConfig.value = report.config || {}
await doAnalyze()
}
async function deleteReport(id: number) {
try {
await biReportApi.delete(id)
ElMessage.success('已删除')
await loadData()
} catch (e) { ElMessage.error('删除失败') }
}
function exportData() {
if (!analysisResult.value) return
const rows = analysisResult.value.rows || []
const header = 'KPI编码,KPI名称,维度,当前值,期间,目标值,单位\n'
const csv = header + rows.map((r: any) =>
`${r.kpi_code},${r.kpi_name},${r.dimension},${r.current_value ?? ''},${r.current_period ?? ''},${r.target_value ?? ''},${r.unit ?? ''}`
).join('\n')
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `报表_${Date.now()}.csv`
a.click()
URL.revokeObjectURL(url)
}
onMounted(() => { loadData() })
</script>
<style scoped>
.card-header-flex { display: flex; justify-content: space-between; align-items: center; }
.tpl-card-body { text-align: center; }
.tpl-icon { font-size: 32px; margin-bottom: 8px; }
.tpl-name { font-weight: 600; }
.tpl-desc { font-size: 12px; color: #999; margin-top: 4px; }
.bi-chart { height: 350px; width: 100%; }
.bi-data-table { width: 100%; margin-top: 12px; }
.full-width-select { width: 100%; }
</style>