Commit a9fe801f authored by Data Governance Dev's avatar Data Governance Dev

feat(ui): 治理→分析改名、国标清单移至弹窗、进度条/日志上下布局、空字段Tab改为按表汇总可展开

parent 8a88a956
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据治理工具</title> <title>数据分析工具</title>
<!-- 前端依赖全部本地化(避免 CDN 被墙/慢) --> <!-- 前端依赖全部本地化(避免 CDN 被墙/慢) -->
<!-- Element Plus CSS --> <!-- Element Plus CSS -->
<link rel="stylesheet" href="/static/lib/element-plus/index.css"> <link rel="stylesheet" href="/static/lib/element-plus/index.css">
...@@ -23,12 +23,12 @@ ...@@ -23,12 +23,12 @@
<div class="header-inner"> <div class="header-inner">
<div class="brand"> <div class="brand">
<el-icon><DataAnalysis /></el-icon> <el-icon><DataAnalysis /></el-icon>
<span class="brand-title">数据治理工具</span> <span class="brand-title">数据分析工具</span>
<el-tag size="small" type="info" effect="plain">v1.0</el-tag> <el-tag size="small" type="info" effect="plain">v1.0</el-tag>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<el-button @click="loadHistory" :icon="Refresh">历史记录</el-button> <el-button @click="loadHistory" :icon="Refresh">历史记录</el-button>
<el-button @click="loadStandards" :icon="Medal">国标清单</el-button> <el-button @click="showStandardsDialog = true" :icon="Medal">国标清单</el-button>
</div> </div>
</div> </div>
</el-header> </el-header>
...@@ -113,7 +113,7 @@ ...@@ -113,7 +113,7 @@
<el-icon><CircleCheck /></el-icon>测试连接 <el-icon><CircleCheck /></el-icon>测试连接
</el-button> </el-button>
<el-button type="success" @click="startJob" :disabled="!connected" :loading="submitting"> <el-button type="success" @click="startJob" :disabled="!connected" :loading="submitting">
<el-icon><VideoPlay /></el-icon>开始治理 <el-icon><VideoPlay /></el-icon>开始分析
</el-button> </el-button>
<el-button v-if="currentJob" @click="cancelJob" type="danger" plain> <el-button v-if="currentJob" @click="cancelJob" type="danger" plain>
<el-icon><Close /></el-icon>取消任务 <el-icon><Close /></el-icon>取消任务
...@@ -123,44 +123,35 @@ ...@@ -123,44 +123,35 @@
</el-form> </el-form>
</el-card> </el-card>
<!-- 步骤 2:国标清单 --> <!-- 步骤 2:进度 + 日志(上下结构) -->
<el-card class="section-card" shadow="hover">
<template #header>
<div class="card-header">
<el-icon><Medal /></el-icon>
<span class="card-title">② 已注册的国标校验标准({{ standards.length }} 项)</span>
</div>
</template>
<el-table :data="standards" stripe size="small">
<el-table-column prop="id" label="标准编号" width="120" />
<el-table-column prop="name" label="标准名称" width="240" />
<el-table-column prop="description" label="校验说明" />
<el-table-column label="适用字段名">
<template #default="{ row }">
<el-tag v-for="f in row.applies_to_fields" :key="f" size="small" type="info" effect="plain" style="margin-right: 4px">{{ f }}</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 步骤 3:进度 + 日志 -->
<el-card v-if="currentJob" class="section-card" shadow="hover"> <el-card v-if="currentJob" class="section-card" shadow="hover">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<el-icon><Loading /></el-icon> <el-icon><Loading /></el-icon>
<span class="card-title">③ 治理进度</span> <span class="card-title">② 分析进度</span>
<el-tag :type="statusTagType">{{ jobStatus.status }}</el-tag> <el-tag :type="statusTagType">{{ jobStatus.status }}</el-tag>
</div> </div>
</template> </template>
<!-- 上:进度条 + 状态信息 -->
<div class="progress-block">
<el-progress :percentage="jobStatus.progress_pct" :status="jobStatus.status === 'failed' ? 'exception' : (jobStatus.status === 'completed' ? 'success' : '')" /> <el-progress :percentage="jobStatus.progress_pct" :status="jobStatus.status === 'failed' ? 'exception' : (jobStatus.status === 'completed' ? 'success' : '')" />
<div class="progress-info"> <div class="progress-info">
<div>当前: {{ jobStatus.current_step_title || '—' }}</div> <div>当前: {{ jobStatus.current_step_title || '—' }}</div>
<div>已完成: {{ jobStatus.steps_completed.length }} / {{ jobStatus.steps_planned.length }}</div> <div>已完成: {{ jobStatus.steps_completed.length }} / {{ jobStatus.steps_planned.length }}</div>
<div v-if="jobStatus.error" class="error-info">错误: {{ jobStatus.error }}</div> <div v-if="jobStatus.error" class="error-info">错误: {{ jobStatus.error }}</div>
</div> </div>
<el-collapse v-model="logOpen" class="log-collapse"> </div>
<el-collapse-item title="实时日志(点击展开)" name="logs">
<div class="log-container"> <!-- 下:实时日志(默认展开) -->
<div class="log-block">
<div class="log-header">
<span class="log-title">实时日志({{ logs.length }} 条)</span>
<el-button size="small" text @click="logExpanded = !logExpanded">
{{ logExpanded ? '收起' : '展开' }}
</el-button>
</div>
<div v-show="logExpanded" class="log-container">
<div v-for="(log, idx) in logs" :key="idx" :class="['log-line', `log-${log.level.toLowerCase()}`]"> <div v-for="(log, idx) in logs" :key="idx" :class="['log-line', `log-${log.level.toLowerCase()}`]">
<span class="log-ts">{{ log.ts.substring(11, 19) }}</span> <span class="log-ts">{{ log.ts.substring(11, 19) }}</span>
<span class="log-level">{{ log.level }}</span> <span class="log-level">{{ log.level }}</span>
...@@ -168,16 +159,15 @@ ...@@ -168,16 +159,15 @@
<span class="log-msg">{{ log.message }}</span> <span class="log-msg">{{ log.message }}</span>
</div> </div>
</div> </div>
</el-collapse-item> </div>
</el-collapse>
</el-card> </el-card>
<!-- 步骤 4:结果展示(多级表格) --> <!-- 步骤 3:结果展示(多级表格) -->
<el-card v-if="jobResult" class="section-card" shadow="hover"> <el-card v-if="jobResult" class="section-card" shadow="hover">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<el-icon><DataLine /></el-icon> <el-icon><DataLine /></el-icon>
<span class="card-title">④ 治理结果</span> <span class="card-title">③ 分析结果</span>
<div> <div>
<el-button v-if="jobResult.reports.markdown" size="small" @click="downloadReport('md')"> <el-button v-if="jobResult.reports.markdown" size="small" @click="downloadReport('md')">
<el-icon><Download /></el-icon>Markdown <el-icon><Download /></el-icon>Markdown
...@@ -203,6 +193,10 @@ ...@@ -203,6 +193,10 @@
<div class="kpi-value">{{ jobResult.overview.high_empty_fields }}</div> <div class="kpi-value">{{ jobResult.overview.high_empty_fields }}</div>
<div class="kpi-label">高空字段</div> <div class="kpi-label">高空字段</div>
</div> </div>
<div class="kpi-card kpi-empty-mid">
<div class="kpi-value">{{ jobResult.overview.mid_empty_fields }}</div>
<div class="kpi-label">中空字段</div>
</div>
<div class="kpi-card kpi-missing"> <div class="kpi-card kpi-missing">
<div class="kpi-value">{{ jobResult.overview.missing_comments }}</div> <div class="kpi-value">{{ jobResult.overview.missing_comments }}</div>
<div class="kpi-label">缺注释字段</div> <div class="kpi-label">缺注释字段</div>
...@@ -278,30 +272,57 @@ ...@@ -278,30 +272,57 @@
<!-- 大范围空字段 --> <!-- 大范围空字段 -->
<el-tab-pane label="大范围空字段" name="empty"> <el-tab-pane label="大范围空字段" name="empty">
<el-alert :title="`扫描 ${jobResult.sections.empty_fields?.summary?.total_tables_scanned || 0} 张表,发现 ${jobResult.sections.empty_fields?.summary?.high_empty_fields_count || 0} 个高空字段`" type="info" :closable="false" /> <el-alert :title="`扫描 ${jobResult.sections.empty_fields?.summary?.total_tables_scanned || 0} 张表,发现 ${jobResult.sections.empty_fields?.summary?.high_empty_fields_count || 0} 个高空字段、${jobResult.sections.empty_fields?.summary?.mid_empty_fields_count || 0} 个中空字段,涉及 ${jobResult.sections.empty_fields?.summary?.tables_with_issues_count || 0} 张表`" type="info" :closable="false" />
<el-table :data="jobResult.sections.empty_fields?.summary?.high_empty_fields || []" stripe size="small" style="margin-top: 12px"> <!-- 按表汇总(可展开查看字段明细) -->
<el-table-column prop="table_name" label="表名" width="180" fixed /> <h4 style="margin-top: 16px">按表统计</h4>
<el-table-column prop="column_name" label="字段名" width="160" fixed /> <el-table :data="jobResult.sections.empty_fields?.per_table || []" stripe size="small">
<el-table-column type="expand">
<template #default="{ row }">
<div v-if="row.high_empty_fields && row.high_empty_fields.length > 0">
<h5 style="margin: 8px 0; color: #f56c6c">🔴 高空字段(空值率 ≥ 80%)</h5>
<el-table :data="row.high_empty_fields" size="small" border>
<el-table-column prop="column_name" label="字段名" width="160" />
<el-table-column prop="data_type" label="类型" width="100" /> <el-table-column prop="data_type" label="类型" width="100" />
<el-table-column prop="column_comment" label="注释" width="160" /> <el-table-column prop="column_comment" label="注释" />
<el-table-column prop="total_rows" label="总行数" width="100" sortable /> <el-table-column prop="empty_count" label="空值数" width="100" />
<el-table-column label="空值数" width="100" sortable> <el-table-column label="空值率" width="150">
<template #default="{ row }">{{ row.empty_count }}</template> <template #default="{ row: f }">
<el-progress :percentage="Math.round(f.empty_rate * 100)" :stroke-width="12" status="exception" />
</template>
</el-table-column>
</el-table>
</div>
<div v-if="row.mid_empty_fields && row.mid_empty_fields.length > 0" style="margin-top: 12px">
<h5 style="margin: 8px 0; color: #e6a23c">🟡 中空字段(空值率 ≥ 50%)</h5>
<el-table :data="row.mid_empty_fields" size="small" border>
<el-table-column prop="column_name" label="字段名" width="160" />
<el-table-column prop="data_type" label="类型" width="100" />
<el-table-column prop="column_comment" label="注释" />
<el-table-column prop="empty_count" label="空值数" width="100" />
<el-table-column label="空值率" width="150">
<template #default="{ row: f }">
<el-progress :percentage="Math.round(f.empty_rate * 100)" :stroke-width="12" status="warning" />
</template>
</el-table-column>
</el-table>
</div>
</template>
</el-table-column> </el-table-column>
<el-table-column label="空值率" width="120" sortable> <el-table-column prop="table_name" label="表名" width="200" />
<el-table-column prop="table_comment" label="表注释" />
<el-table-column prop="total_rows" label="总行数" width="100" sortable />
<el-table-column prop="high_count" label="高空字段" width="110" sortable>
<template #default="{ row }"> <template #default="{ row }">
<el-progress :percentage="Math.round(row.empty_rate * 100)" :stroke-width="12" :status="row.empty_rate >= 0.8 ? 'exception' : 'warning'" /> <el-tag v-if="row.high_count > 0" type="danger" size="small">{{ row.high_count }}</el-tag>
<span v-else class="muted">0</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="LLM 分类" width="160"> <el-table-column prop="mid_count" label="中空字段" width="110" sortable>
<template #default="{ row }"> <template #default="{ row }">
<el-tag v-if="row.llm_classification" :type="row.llm_classification === 'missing' ? 'danger' : 'success'" size="small"> <el-tag v-if="row.mid_count > 0" type="warning" size="small">{{ row.mid_count }}</el-tag>
{{ row.llm_classification }} <span v-else class="muted">0</span>
</el-tag>
<span v-else class="muted">—</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="llm_reasoning" label="LLM 判断依据" />
</el-table> </el-table>
</el-tab-pane> </el-tab-pane>
...@@ -397,7 +418,7 @@ ...@@ -397,7 +418,7 @@
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<el-icon><Histogram /></el-icon> <el-icon><Histogram /></el-icon>
<span class="card-title">⑤ 历史治理记录({{ history.length }} 次)</span> <span class="card-title">④ 历史分析记录({{ history.length }} 次)</span>
</div> </div>
</template> </template>
<el-table :data="history" stripe size="small"> <el-table :data="history" stripe size="small">
...@@ -423,6 +444,26 @@ ...@@ -423,6 +444,26 @@
</el-card> </el-card>
</el-main> </el-main>
</el-container> </el-container>
<!-- 国标清单对话框 -->
<el-dialog v-model="showStandardsDialog" title="已注册的国标校验标准" width="700px">
<div v-if="standards.length === 0" style="text-align: center; padding: 20px; color: #909399;">
国标清单为空,<el-link type="primary" @click="loadStandards">点击刷新</el-link>
</div>
<div v-else class="standards-list">
<div v-for="(s, idx) in standards" :key="s.id" class="standard-item" style="margin-bottom: 16px; padding: 12px; background: #fafafa; border-radius: 6px;">
<div class="standard-header" style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
<el-tag size="small" type="warning" effect="dark">{{ s.id }}</el-tag>
<span class="standard-name" style="font-weight: 600;">{{ s.name }}</span>
</div>
<div class="standard-desc" style="font-size: 13px; color: #606266; margin-bottom: 8px;">{{ s.description }}</div>
<div class="standard-fields">
<span style="font-size: 12px; color: #909399;">适用字段:</span>
<el-tag v-for="f in s.applies_to_fields" :key="f" size="small" type="info" effect="plain" style="margin-right: 4px;">{{ f }}</el-tag>
</div>
</div>
</div>
</el-dialog>
</div> </div>
<script> <script>
...@@ -465,7 +506,8 @@ ...@@ -465,7 +506,8 @@
error: null, error: null,
}); });
const logs = ref([]); const logs = ref([]);
const logOpen = ref(false); const logExpanded = ref(true);
const showStandardsDialog = ref(false);
let logEventSource = null; let logEventSource = null;
let pollTimer = null; let pollTimer = null;
...@@ -516,7 +558,7 @@ ...@@ -516,7 +558,7 @@
} }
} }
// ── 启动治理 ── // ── 启动分析 ──
async function startJob() { async function startJob() {
submitting.value = true; submitting.value = true;
jobResult.value = null; jobResult.value = null;
...@@ -564,7 +606,7 @@ ...@@ -564,7 +606,7 @@
if (r.status === 'completed') { if (r.status === 'completed') {
clearInterval(pollTimer); clearInterval(pollTimer);
await loadResult(jobId); await loadResult(jobId);
ElementPlus.ElMessage.success('治理完成'); ElementPlus.ElMessage.success('分析完成');
loadHistory(); loadHistory();
} else if (r.status === 'failed' || r.status === 'cancelled') { } else if (r.status === 'failed' || r.status === 'cancelled') {
clearInterval(pollTimer); clearInterval(pollTimer);
...@@ -600,7 +642,7 @@ ...@@ -600,7 +642,7 @@
async function downloadHistoryReport(row, fmt) { async function downloadHistoryReport(row, fmt) {
const ext = fmt === 'docx' ? 'docx' : 'md'; const ext = fmt === 'docx' ? 'docx' : 'md';
const filename = `数据治理报告_${row.database}.${ext}`; const filename = `数据分析报告_${row.database}.${ext}`;
window.open(`/api/reports/${row.database}/${row.run_id}/${filename}`, '_blank'); window.open(`/api/reports/${row.database}/${row.run_id}/${filename}`, '_blank');
} }
...@@ -622,7 +664,7 @@ ...@@ -622,7 +664,7 @@
form, testing, submitting, connected, testResult, form, testing, submitting, connected, testResult,
stepsList, standards, stepsList, standards,
currentJob, jobStatus, statusTagType, currentJob, jobStatus, statusTagType,
logs, logOpen, logs, logExpanded, showStandardsDialog,
jobResult, activeTab, jobResult, activeTab,
history, history,
testConnection, startJob, cancelJob, testConnection, startJob, cancelJob,
......
/* 数据治理工具样式 */ /* 数据分析工具样式 */
* { * {
box-sizing: border-box; box-sizing: border-box;
...@@ -100,7 +100,14 @@ body { ...@@ -100,7 +100,14 @@ body {
margin-left: 8px; margin-left: 8px;
} }
/* ── 进度条 ── */ /* ── 进度条(上) ── */
.progress-block {
margin-bottom: 20px;
}
.progress-block .el-progress {
margin-bottom: 8px;
}
.progress-info { .progress-info {
display: flex; display: flex;
gap: 24px; gap: 24px;
...@@ -114,6 +121,25 @@ body { ...@@ -114,6 +121,25 @@ body {
font-weight: 500; font-weight: 500;
} }
/* ── 日志区(下) ── */
.log-block {
border-top: 1px solid #ebeef5;
padding-top: 12px;
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.log-title {
font-size: 13px;
font-weight: 600;
color: #606266;
}
.log-collapse { .log-collapse {
margin-top: 12px; margin-top: 12px;
} }
...@@ -189,6 +215,7 @@ body { ...@@ -189,6 +215,7 @@ body {
.kpi-card.kpi-tables { border-left-color: #1890ff; } .kpi-card.kpi-tables { border-left-color: #1890ff; }
.kpi-card.kpi-fields { border-left-color: #13c2c2; } .kpi-card.kpi-fields { border-left-color: #13c2c2; }
.kpi-card.kpi-empty { border-left-color: #faad14; } .kpi-card.kpi-empty { border-left-color: #faad14; }
.kpi-card.kpi-empty-mid { border-left-color: #e6a23c; }
.kpi-card.kpi-missing { border-left-color: #fa8c16; } .kpi-card.kpi-missing { border-left-color: #fa8c16; }
.kpi-card.kpi-length { border-left-color: #f5222d; } .kpi-card.kpi-length { border-left-color: #f5222d; }
.kpi-card.kpi-std { border-left-color: #eb2f96; } .kpi-card.kpi-std { border-left-color: #eb2f96; }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment