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

291 lines
13 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>
<h3>数据管理</h3>
<el-tabs v-model="activeTab" class="section-gap-tabs">
<el-tab-pane label="Excel导入" name="import">
<el-card>
<template #header>Excel导入 <el-tag type="success" size="small">🤖 智能识别</el-tag></template>
<el-upload drag :auto-upload="false" :on-change="handleFile" :on-remove="handleRemove" multiple accept=".xlsx,.xls">
<el-icon :size="32"><UploadFilled /></el-icon>
<div>拖拽或点击上传Excel文件</div>
<div class="upload-hint">支持 .xlsx .xls,自动识别利润表/现金流量表/资产负债表/KPI数据表</div>
</el-upload>
<div v-if="files.length > 0" class="tag-list">
<el-tag v-for="(f, i) in files" :key="i" closable class="file-tag" @close="removeFile(i)">{{ f.name }}</el-tag>
</div>
<el-button v-if="files.length > 0" type="primary" class="upload-actions" :loading="uploading" @click="doImport">🤖 智能导入 {{ files.length }} 个文件</el-button>
<div v-if="result" class="result-area"><el-alert :title="result" type="success" show-icon /></div>
</el-card>
</el-tab-pane>
<el-tab-pane label="数据源管理" name="sources">
<!-- ⚠️ 数据链路诚实化:真实取数通道状态(2026-09-08 P0) -->
<el-alert v-if="channelLoading" title="加载数据链路状态..." type="info" show-icon :closable="false" class="channel-alert" />
<template v-else>
<el-alert v-if="realChannel && realChannel.latest_inject_at" type="success" :closable="false" class="channel-alert">
<template #title>
✅ 真实数据通道(当前在用):{{ realChannel.channel }}
<span class="channel-inline">最近注入:{{ realChannel.latest_period }}({{ realChannel.latest_inject_at }},共 {{ realChannel.injected_rows }} 行)</span>
<span v-if="realChannel.landing_dir_mtime" class="channel-inline">落盘目录最新文件:{{ realChannel.landing_dir_mtime }}</span>
</template>
<div class="channel-desc">
数据链路:DAMA 从智享通网页读取科目余额表 → 落盘 {{ realChannel.landing_dir }} → 财务Bot 注入 CMA(subjects + kpi_values 双写,source_type='zxt-sync')。
该通道为陕西酣客财务原始数据的真实来源,每日/结账后由 DAMA 例行执行。
</div>
</el-alert>
<el-alert v-else type="info" :closable="false" class="channel-alert" title="当前账套暂无 zxt 智享通注入数据(可能为企业数据未接入智享通通道)" />
</template>
<el-card>
<template #header>
<div class="card-header-flex">
<span>数据源配置</span>
<el-button type="primary" size="small" @click="openSourceForm()">新增数据源</el-button>
</div>
</template>
<el-table :data="sources" v-loading="sourcesLoading" class="full-width-table">
<el-table-column prop="name" label="名称" min-width="140" />
<el-table-column prop="source_type" label="类型" width="90">
<template #default="{ row }">
<el-tag :type="sourceTypeTag(row.source_type)" size="small">{{ row.source_type }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="api_endpoint" label="API地址" min-width="200" show-overflow-tooltip />
<el-table-column prop="sync_type" label="同步方式" width="90">
<template #default="{ row }">
<el-tag size="small" :type="row.sync_type==='realtime'?'success':'info'">{{ row.sync_type }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="110">
<template #default="{ row }">
<el-tag :type="row.status==='active'?'success':'danger'" size="small">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="last_sync_at" label="上次同步" width="160" />
<el-table-column label="操作" width="150">
<template #default="{ row }">
<el-button size="small" @click="openSourceForm(row)">编辑</el-button>
<el-button size="small" type="danger" @click="doDeleteSource(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!sourcesLoading && sources.length === 0" description="暂无数据源配置" />
<div v-if="!sourcesLoading && sources.length === 0" class="legacy-note">
💡 ERP 直连通道已于 2026-08-31 停用,历史遗留 ERP 配置已按决策清理(2026-09-08)。
企业财务数据由上方 zxt 智享通通道自动归集(source_type='zxt-sync'),无需在此配置。
</div>
</el-card>
<!-- 真实取数映射(采集器实际使用) -->
<el-card v-if="valueSources.length > 0" class="vs-card">
<template #header>
<div class="card-header-flex">
<span>📡 实际取数映射(采集器使用,kpi_value_sources)</span>
</div>
</template>
<div class="channel-desc">06:30 定时采集(scripts/kpi_value_collector.py)按此映射写入 kpi_values(source_type='auto_collect'),与上方「数据源配置」相互独立。</div>
<el-table :data="valueSources" class="full-width-table">
<el-table-column prop="kpi_code" label="KPI编码" min-width="130" />
<el-table-column prop="kpi_name" label="KPI名称" min-width="130" />
<el-table-column prop="source_table" label="源表" min-width="140" />
<el-table-column prop="source_field" label="源字段" min-width="120" />
<el-table-column prop="aggregate" label="聚合" width="90" />
<el-table-column prop="status" label="状态" width="90">
<template #default="{ row }">
<el-tag :type="row.status==='active'?'success':'danger'" size="small">{{ row.status }}</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
</el-tab-pane>
</el-tabs>
<MyDialog v-model="showSourceForm" :title="sourceForm.id ? '编辑数据源' : '新增数据源'" :width="550">
<el-alert v-if="!sourceForm.id && sourceForm.source_type==='erp'" type="warning" :closable="false" class="form-warn"
title="ERP 直连通道已停用(2026-08-31)。ERP 类型数据源仅作记录、不会执行同步;企业财务数据由 zxt 智享通通道供给。" />
<el-form :model="sourceForm" label-width="120px">
<el-form-item label="名称"><el-input v-model="sourceForm.name" /></el-form-item>
<el-form-item label="类型">
<el-select v-model="sourceForm.source_type" @change="onSourceTypeChange">
<el-option label="ERP" value="erp" />
<el-option label="业务系统" value="business" />
<el-option label="Excel" value="excel" />
<el-option label="手工" value="manual" />
</el-select>
</el-form-item>
<el-form-item v-if="sourceForm.source_type==='erp'" label="通道说明">
<el-alert type="warning" :closable="false" show-icon title="该类型数据源不会执行同步(通道已停用),仅作记录" />
</el-form-item>
<el-form-item label="API地址"><el-input v-model="sourceForm.api_endpoint" placeholder="https://..." /></el-form-item>
<el-form-item label="API Key"><el-input v-model="sourceForm.api_key" type="password" show-password /></el-form-item>
<el-form-item label="SQL查询"><el-input v-model="sourceForm.query_sql" type="textarea" :rows="3" placeholder="可选,用于查询型数据源" /></el-form-item>
<el-form-item label="同步方式">
<el-select v-model="sourceForm.sync_type">
<el-option label="实时" value="realtime" />
<el-option label="定时批量" value="batch" />
<el-option label="手动" value="manual" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showSourceForm=false">取消</el-button>
<el-button type="primary" @click="saveSource" :loading="sourceSaving">保存</el-button>
</template>
</MyDialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
import { dataApi, budgetApi } from '../api/index'
import MyDialog from '../components/MyDialog.vue'
const activeTab = ref('import')
// === Excel导入 ===
const files = ref<any[]>([])
const uploading = ref(false)
const result = ref('')
function handleFile(f: any) {
if (f.raw && !files.value.some(ex => ex.name === f.raw.name && ex.size === f.raw.size)) {
files.value.push(f.raw)
}
}
function handleRemove(f: any) {
files.value = files.value.filter(ex => !(ex.name === f.name && ex.size === f.size))
}
function removeFile(i: number) {
files.value.splice(i, 1)
}
async function doImport() {
if (files.value.length === 0) return
uploading.value = true
result.value = ''
const msgs: string[] = []
for (const f of files.value) {
try {
const r: any = await dataApi.importExcelSmart(f)
const msg = r.message || ''
msgs.push(`📄 ${f.name}: ${msg}`)
} catch (e: any) {
msgs.push(`❌ ${f.name}: ${e?.response?.data?.detail || '导入失败'}`)
}
}
files.value = []
result.value = msgs.join('\n')
uploading.value = false
}
// === 数据链路状态(2026-09-08 诚实化) ===
const channelLoading = ref(false)
const realChannel = ref<any>(null)
const valueSources = ref<any[]>([])
async function loadChannelStatus() {
channelLoading.value = true
try {
const r: any = await dataApi.channelStatus()
const d = r.data || r
realChannel.value = d.real_channel || null
} catch (e) { /* 非阻塞:状态卡片加载失败不影响列表 */ }
channelLoading.value = false
}
async function loadValueSources() {
try {
const r: any = await budgetApi.valueSources({ status: 'active' })
const d = Array.isArray(r) ? r : (r.data || [])
valueSources.value = d
} catch (e) { valueSources.value = [] }
}
// === 数据源管理 ===
const sources = ref<any[]>([])
const sourcesLoading = ref(false)
const showSourceForm = ref(false)
const sourceSaving = ref(false)
const sourceForm = ref<any>({ source_type: 'manual', sync_type: 'manual' })
function sourceTypeTag(t: string) {
return ({ erp: 'primary', business: 'warning', excel: 'success', manual: 'info' } as any)[t] || 'info'
}
function onSourceTypeChange() {
// 触发表单内 ERP 遗留说明显示
sourceForm.value = { ...sourceForm.value }
}
async function loadSources() {
sourcesLoading.value = true
try {
const r: any = await dataApi.listSources()
sources.value = r.data || []
} catch (e) { ElMessage.error('加载数据源失败') }
sourcesLoading.value = false
}
function openSourceForm(row?: any) {
if (row) {
sourceForm.value = { ...row }
} else {
sourceForm.value = { source_type: 'manual', sync_type: 'manual' }
}
showSourceForm.value = true
}
async function saveSource() {
sourceSaving.value = true
try {
if (sourceForm.value.id) {
await dataApi.updateSource(sourceForm.value.id, sourceForm.value)
ElMessage.success('已更新')
} else {
await dataApi.createSource(sourceForm.value)
ElMessage.success('已创建')
}
showSourceForm.value = false
loadSources()
loadChannelStatus()
} catch (e) { ElMessage.error('保存失败') }
sourceSaving.value = false
}
async function doDeleteSource(row: any) {
try {
await ElMessageBox.confirm(`确认删除数据源「${row.name}」?`, '确认')
await dataApi.deleteSource(row.id)
ElMessage.success('已删除')
loadSources()
loadChannelStatus()
} catch (e: any) {
if (e !== 'cancel') ElMessage.error('删除失败')
}
}
onMounted(() => {
loadSources()
loadChannelStatus()
loadValueSources()
})
</script>
<style scoped>
.data-page { }
.data-page .card-header-flex { display: flex; justify-content: space-between; align-items: center; }
.upload-hint { color: #999; font-size: 12px; }
.tag-list { margin-top: 8px; }
.tag-list .file-tag { margin: 2px; }
.upload-actions { margin-top: 12px; }
.result-area { margin-top: 12px; white-space: pre-wrap; }
.channel-alert { margin-bottom: 12px; }
.channel-inline { margin-left: 12px; color: #606266; font-weight: normal; }
.channel-desc { margin-top: 4px; color: #606266; font-size: 12px; line-height: 1.7; }
.vs-card { margin-top: 12px; }
.legacy-note { margin-top: 8px; color: #b88230; font-size: 12px; line-height: 1.7; }
.form-warn { margin-bottom: 12px; }
</style>