291 lines
13 KiB
Vue
291 lines
13 KiB
Vue
<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>
|