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

408 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="confidence-page">
<!-- 页面Header -->
<div class="page-header">
<div class="header-left">
<h3><el-icon :size="22"><DataAnalysis /></el-icon> 分析结论置信度</h3>
<el-tag type="warning" size="small" effect="plain">{{ currentPeriod }}</el-tag>
</div>
<div class="header-right">
<el-input
v-model="searchPeriod"
placeholder="期间 (YYYY-MM)"
clearable
style="width: 180px; margin-right: 8px;"
@keyup.enter="loadResults"
/>
<el-button type="primary" size="small" @click="showCreateDialog = true">
<el-icon><Plus /></el-icon> 新增分析
</el-button>
<el-button size="small" @click="loadResults" :loading="loading">刷新</el-button>
</div>
</div>
<!-- 置信度统计概览 -->
<div class="stats-cards">
<div class="stat-card">
<div class="stat-num">{{ totalResults }}</div>
<div class="stat-label">分析结论总数</div>
</div>
<div class="stat-card">
<div class="stat-num" style="color: #67c23a;">{{ highConfidenceCount }}</div>
<div class="stat-label">高置信度 85%</div>
</div>
<div class="stat-card">
<div class="stat-num" style="color: #e6a23c;">{{ midConfidenceCount }}</div>
<div class="stat-label">中等置信度 50-70%</div>
</div>
<div class="stat-card">
<div class="stat-num" style="color: #f56c6c;">{{ lowConfidenceCount }}</div>
<div class="stat-label">低置信度 &lt;50%</div>
</div>
</div>
<!-- 置信度评分说明 -->
<el-card class="legend-card" shadow="never">
<div class="legend-title">📊 置信度评分标准</div>
<div class="legend-grid">
<div class="legend-item" v-for="item in confidenceLevels" :key="item.level">
<el-tag :type="item.type" size="small">{{ item.level }}</el-tag>
<span class="legend-desc">{{ item.desc }}</span>
</div>
</div>
</el-card>
<!-- 分析结论列表 -->
<div class="results-list" v-loading="loading">
<el-empty v-if="results.length === 0 && !loading" description="暂无分析结论,点击「新增分析」添加" />
<el-card v-for="r in results" :key="r.id" class="result-card" shadow="hover">
<div class="result-head">
<div class="result-meta">
<el-tag size="small" effect="plain">{{ r.period }}</el-tag>
<span class="kpi-tag" v-if="r.kpi_name">{{ r.kpi_name }}</span>
</div>
<div class="result-actions">
<el-button size="small" text type="danger" @click="confirmDelete(r)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
<div class="result-body">
<div class="conclusion-text">{{ r.结论 }}</div>
<div class="confidence-bar-wrap">
<span class="confidence-label">置信度</span>
<el-progress
:percentage="parseInt(r.置信度)"
:color="confidenceColor(parseInt(r.置信度))"
:stroke-width="16"
:text-inside="true"
:format="() => r.置信度"
/>
</div>
<div class="detail-grid">
<div class="detail-item" v-if="r.数据来源">
<span class="detail-label">📁 数据来源</span>
<span class="detail-val">{{ r.数据来源 }}</span>
</div>
<div class="detail-item" v-if="r.计算逻辑">
<span class="detail-label">🧮 计算逻辑</span>
<span class="detail-val">{{ r.计算逻辑 }}</span>
</div>
<div class="detail-item" v-if="r.可比基准">
<span class="detail-label">📊 可比基准</span>
<span class="detail-val">{{ r.可比基准 }}</span>
</div>
<div class="detail-item" v-if="r.局限">
<span class="detail-label">⚠️ 局限</span>
<span class="detail-val limitation">{{ r.局限 }}</span>
</div>
</div>
<div class="data-flags">
<el-tag
v-for="flag in dataFlags(r)"
:key="flag.label"
:type="flag.active ? 'success' : 'info'"
size="small"
effect="plain"
>
{{ flag.active ? '✅' : '⬜' }} {{ flag.label }}
</el-tag>
</div>
</div>
</el-card>
</div>
<!-- 新增分析结论对话框 -->
<el-dialog v-model="showCreateDialog" title="新增分析结论" width="640px" :close-on-click-modal="false">
<el-form :model="form" label-width="100px" label-position="top">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="期间" required>
<el-input v-model="form.period" placeholder="YYYY-MM" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="KPI编码">
<el-input v-model="form.kpi_code" placeholder="如 FB_CHANNEL_RATE" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="分析结论" required>
<el-input v-model="form.conclusion" type="textarea" :rows="2" placeholder="输入分析结论内容" />
</el-form-item>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="数据来源">
<el-input v-model="form.data_source" placeholder="如: 科目余额表·本期发生列" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="可比基准">
<el-input v-model="form.comparable_benchmark" placeholder="如: H1渠补率82.8%" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="计算逻辑">
<el-input v-model="form.calculation_logic" type="textarea" :rows="2" placeholder="如: 捐赠支出682,597 / 收入1,293,236 = 52.8%" />
</el-form-item>
<el-form-item label="局限说明">
<el-input v-model="form.limitations" type="textarea" :rows="2" placeholder="如: 6月单月可能受季节性影响" />
</el-form-item>
<el-divider>数据完备度决定置信度</el-divider>
<el-row :gutter="16">
<el-col :span="6">
<el-checkbox v-model="form.has_actual">有实际值</el-checkbox>
</el-col>
<el-col :span="6">
<el-checkbox v-model="form.has_target">有目标值</el-checkbox>
</el-col>
<el-col :span="6">
<el-checkbox v-model="form.has_trend">有历史趋势</el-checkbox>
</el-col>
<el-col :span="6">
<el-checkbox v-model="form.has_review">人工复核</el-checkbox>
</el-col>
</el-row>
<div class="calc-preview">
根据所选数据完备度置信度将自动计算为
<el-tag :type="previewConfidence >= 85 ? 'success' : previewConfidence >= 50 ? 'warning' : 'danger'" effect="dark" class="preview-tag">
{{ previewConfidence }}%
</el-tag>
</div>
</el-form>
<template #footer>
<el-button @click="showCreateDialog = false">取消</el-button>
<el-button type="primary" @click="submitResult" :loading="submitting">提交</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { analysisApi } from '../api/index'
import { DataAnalysis, Plus, Delete } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const loading = ref(false)
const submitting = ref(false)
const results = ref<any[]>([])
const searchPeriod = ref('')
const showCreateDialog = ref(false)
const currentPeriod = computed(() => {
const now = new Date()
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
})
const form = ref({
period: currentPeriod.value,
conclusion: '',
data_source: '',
calculation_logic: '',
comparable_benchmark: '',
limitations: '',
kpi_code: '',
kpi_name: '',
has_actual: false,
has_target: false,
has_trend: false,
has_review: false,
})
// 置信度评分标准
const confidenceLevels = [
{ level: '95%', type: 'success' as const, desc: '有全部数据 + 人工复核 — 结论极为可靠' },
{ level: '85%', type: 'success' as const, desc: '有实际值 + 目标值 + 历史趋势 — 结论高度可信' },
{ level: '70%', type: 'warning' as const, desc: '有实际值 + 目标值 — 结论较为可信' },
{ level: '50%', type: 'warning' as const, desc: '有实际值 — 结论基本可信' },
{ level: '30%', type: 'danger' as const, desc: '无实际值,基于推测 — 结论仅供参考' },
]
// 预览置信度
const previewConfidence = computed(() => {
const { has_actual, has_target, has_trend, has_review } = form.value
if (has_review) return 95
if (has_actual && has_target && has_trend) return 85
if (has_actual && has_target) return 70
if (has_actual) return 50
return 30
})
// 统计数据
const totalResults = computed(() => results.value.length)
const highConfidenceCount = computed(() => results.value.filter(r => parseInt(r.置信度) >= 85).length)
const midConfidenceCount = computed(() => {
const v = parseInt(r.置信度)
return results.value.filter(r => {
const c = parseInt(r.置信度)
return c >= 50 && c < 85
}).length
})
const lowConfidenceCount = computed(() => results.value.filter(r => parseInt(r.置信度) < 50).length)
function confidenceColor(score: number): string {
if (score >= 85) return '#67c23a'
if (score >= 50) return '#e6a23c'
return '#f56c6c'
}
function dataFlags(r: any) {
return [
{ label: '实际值', active: r.has_actual },
{ label: '目标值', active: r.has_target },
{ label: '趋势', active: r.has_trend },
{ label: '复核', active: r.has_review },
]
}
async function loadResults() {
loading.value = true
try {
const params: any = {}
if (searchPeriod.value) params.period = searchPeriod.value
const res: any = await analysisApi.getResults(params)
results.value = res.results || []
} catch (e: any) {
console.error('加载分析结论失败', e)
ElMessage.error('加载失败')
} finally {
loading.value = false
}
}
async function submitResult() {
if (!form.value.period || !form.value.conclusion) {
ElMessage.warning('请填写期间和分析结论')
return
}
submitting.value = true
try {
const res: any = await analysisApi.createResult({
period: form.value.period,
conclusion: form.value.conclusion,
data_source: form.value.data_source || undefined,
calculation_logic: form.value.calculation_logic || undefined,
comparable_benchmark: form.value.comparable_benchmark || undefined,
limitations: form.value.limitations || undefined,
kpi_code: form.value.kpi_code || undefined,
kpi_name: form.value.kpi_name || undefined,
has_actual: form.value.has_actual,
has_target: form.value.has_target,
has_trend: form.value.has_trend,
has_review: form.value.has_review,
})
ElMessage.success(`✅ 分析结论已保存,置信度 ${res.置信度}`)
showCreateDialog.value = false
resetForm()
loadResults()
} catch (e: any) {
console.error('提交失败', e)
ElMessage.error('提交失败')
} finally {
submitting.value = false
}
}
function confirmDelete(r: any) {
ElMessageBox.confirm(`确定删除「${r.结论?.substring(0, 30)}...」这条结论?`, '确认删除', {
confirmButtonText: '删除',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
try {
await analysisApi.deleteResult(r.id)
ElMessage.success('已删除')
loadResults()
} catch {
ElMessage.error('删除失败')
}
}).catch(() => {})
}
function resetForm() {
form.value = {
period: currentPeriod.value,
conclusion: '',
data_source: '',
calculation_logic: '',
comparable_benchmark: '',
limitations: '',
kpi_code: '',
kpi_name: '',
has_actual: false,
has_target: false,
has_trend: false,
has_review: false,
}
}
onMounted(loadResults)
</script>
<style scoped>
.confidence-page { padding: 0; max-width: 1200px; margin: 0 auto; }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 12px; }
.header-left { display: flex; align-items: center; gap: 12px; }
.header-left h3 { margin: 0; font-size: 20px; color: #303133; }
.header-right { display: flex; align-items: center; }
.stats-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 20px; }
.stat-card {
background: #fff; border-radius: 10px; padding: 20px; text-align: center;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
.stat-num { font-size: 32px; font-weight: bold; color: #409EFF; line-height: 1.2; }
.stat-label { font-size: 13px; color: #909399; margin-top: 4px; }
.legend-card { margin-bottom: 20px; }
.legend-card :deep(.el-card__body) { padding: 16px 20px; }
.legend-title { font-size: 14px; font-weight: 600; margin-bottom: 10px; color: #303133; }
.legend-grid { display: flex; flex-wrap: wrap; gap: 12px; }
.legend-item { display: flex; align-items: center; gap: 6px; }
.legend-desc { font-size: 12px; color: #606266; }
.results-list { display: flex; flex-direction: column; gap: 14px; }
.result-card { border-radius: 10px; }
.result-card :deep(.el-card__body) { padding: 20px; }
.result-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.result-meta { display: flex; align-items: center; gap: 8px; }
.kpi-tag { font-size: 12px; color: #409EFF; background: #ecf5ff; padding: 2px 8px; border-radius: 4px; }
.result-actions { display: flex; gap: 4px; }
.conclusion-text { font-size: 15px; font-weight: 500; color: #303133; margin-bottom: 14px; line-height: 1.6; }
.confidence-bar-wrap { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
.confidence-label { font-size: 13px; color: #909399; white-space: nowrap; width: 60px; }
.confidence-bar-wrap :deep(.el-progress) { flex: 1; }
.confidence-bar-wrap :deep(.el-progress-bar__innerText) { font-size: 12px; font-weight: 600; }
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 14px; }
.detail-item { display: flex; flex-direction: column; gap: 2px; }
.detail-label { font-size: 11px; color: #909399; }
.detail-val { font-size: 13px; color: #303133; word-break: break-all; }
.detail-val.limitation { color: #e6a23c; }
.data-flags { display: flex; gap: 8px; flex-wrap: wrap; border-top: 1px solid #f0f0f0; padding-top: 12px; }
.calc-preview { margin-top: 16px; padding: 12px 16px; background: #f5f7fa; border-radius: 8px; font-size: 13px; color: #606266; }
.preview-tag { margin-left: 8px; font-size: 14px; font-weight: 600; }
@media (max-width: 768px) {
.stats-cards { grid-template-columns: repeat(2, 1fr); }
.detail-grid { grid-template-columns: 1fr; }
}
</style>