327 lines
12 KiB
Vue
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>
|