Commit 91d91703 authored by Data Governance Dev's avatar Data Governance Dev

feat(web2): IdCardTool UI — 折叠结果面板 + 弃用 SSE 改轮询 + done 分支兜底

- 弃用 SSE(EventSource)
  - 原方案 push 大 payload + Vue 响应式触发 1000+ 行 el-table 重渲,主线程卡 2s+ 导致页面「无响应」
  - 改为 setInterval(1000ms) 轮询 + 立即首次触发
- 删除大进度卡片(3 指标 + el-progress)
  - 改为单行扫描提示:Loading 图标 + 「已发现 X 张问题表 / Y 行违规(Zs)」
- 结果区改为 el-collapse 折叠面板(每张表一个 collapse-item)
- 新增 violationsByTable + liveBadRowCount 两个 ref(模板读这两个,不直接读 scanResult)
- polling 累加 + done 兜底:
  - running 阶段 partial 来的表 push 到 violationsByTable(dedupe by renderedTableNames Set)
  - done 分支兜底:若 violationsByTable 为空,从 data.response.violations_by_table 同步灌入
    (解决扫描太快<1s 时 running partial 没机会被观察到的问题)
- 错误处理:
  - 404 = scan_id 过期(10 分钟 TTL),友好提示
  - 网络瞬断不停轮询,下次自动恢复
parent f2291cf0
...@@ -86,108 +86,83 @@ ...@@ -86,108 +86,83 @@
</el-form> </el-form>
</el-card> </el-card>
<!-- 扫描进度条 --> <!-- 扫描中状态行(极简:每张表完成后会立刻追加到下方结果区) -->
<el-card v-if="scanning" class="idcard-page__progress" shadow="never"> <div v-if="scanning" class="idcard-page__scanning-tip">
<div class="idcard-page__progress-head"> <el-icon class="idcard-page__scanning-icon"><Loading /></el-icon>
<el-icon class="idcard-page__progress-icon"><Loading /></el-icon> <span>
<span class="idcard-page__progress-title">正在扫描身份证合规性</span> 正在扫描 · 已发现
<span class="idcard-page__progress-stat"> <strong>{{ violationsByTable.length }}</strong> 张问题表 /
{{ progress.done }} / {{ progress.total || '?' }} <strong>{{ liveBadRowCount.toLocaleString() }}</strong> 行违规
</span>
<span class="idcard-page__scanning-elapsed">
({{ ((elapsedMsRef) / 1000).toFixed(1) }}s)
</span> </span>
<span class="idcard-page__progress-elapsed">({{ (progress.elapsed_ms / 1000).toFixed(1) }}s)</span>
</div>
<el-progress
:percentage="progressPercent"
:stroke-width="14"
:text-inside="true"
:status="progress.status === 'error' ? 'exception' : ''"
/>
<div class="idcard-page__progress-current">
当前:<code>{{ progress.current_column || '初始化 …' }}</code>
</div> </div>
</el-card>
<!-- 扫描结果 --> <!-- 扫描结果(轮询追加:每张表完成后立刻渲染,可折叠) -->
<el-card v-if="scanResult" class="idcard-page__result" shadow="never"> <el-card v-if="scanResult || violationsByTable.length" class="idcard-page__result" shadow="never">
<!-- 概览统计 --> <!-- 概览统计 -->
<div class="idcard-page__overview"> <div class="idcard-page__overview">
<div class="idcard-page__stat"> <div class="idcard-page__stat">
<div class="idcard-page__stat-label">数据库</div> <div class="idcard-page__stat-label">数据库</div>
<div class="idcard-page__stat-value">{{ scanResult.overview.database }}</div> <div class="idcard-page__stat-value">{{ scanResult?.overview?.database || '—' }}</div>
</div> </div>
<div class="idcard-page__stat"> <div class="idcard-page__stat">
<div class="idcard-page__stat-label">命中列</div> <div class="idcard-page__stat-label">匹配列</div>
<div class="idcard-page__stat-value"> <div class="idcard-page__stat-value">{{ scanResult?.overview?.matched_columns ?? '—' }}</div>
{{ scanResult.overview.matched_columns }}
<span class="idcard-page__stat-sub">/ {{ scanResult.overview.total_columns_in_db }}</span>
</div>
</div> </div>
<div class="idcard-page__stat"> <div class="idcard-page__stat">
<div class="idcard-page__stat-label">违规列</div> <div class="idcard-page__stat-label">违规列</div>
<div class="idcard-page__stat-value idcard-page__stat-value--warn"> <div class="idcard-page__stat-value idcard-page__stat-value--warn">
{{ scanResult.overview.columns_with_violations }} {{ scanResult?.overview?.columns_with_violations ?? '—' }}
</div>
</div>
<div class="idcard-page__stat">
<div class="idcard-page__stat-label">问题表</div>
<div class="idcard-page__stat-value idcard-page__stat-value--warn">
{{ violationsByTable.length }}
</div> </div>
</div> </div>
<div class="idcard-page__stat"> <div class="idcard-page__stat">
<div class="idcard-page__stat-label">问题行</div> <div class="idcard-page__stat-label">问题行</div>
<div class="idcard-page__stat-value idcard-page__stat-value--danger"> <div class="idcard-page__stat-value idcard-page__stat-value--danger">
{{ scanResult.overview.total_bad_rows }} {{ liveBadRowCount.toLocaleString() }}
</div> </div>
</div> </div>
<div class="idcard-page__stat"> <div class="idcard-page__stat" v-if="scanResult?.overview">
<div class="idcard-page__stat-label">耗时</div> <div class="idcard-page__stat-label">{{ scanning ? '耗时(实时)' : '总耗时' }}</div>
<div class="idcard-page__stat-value">{{ scanResult.overview.elapsed_ms }} ms</div> <div class="idcard-page__stat-value">{{ scanResult.overview.elapsed_ms }} ms</div>
</div> </div>
</div> </div>
<!-- 命中列清单 --> <!-- 问题表折叠列表 -->
<template v-if="scanResult.matches.length"> <template v-if="violationsByTable.length">
<h4 class="idcard-page__section-title">命中的列({{ scanResult.matches.length }})</h4> <div class="idcard-page__section-head">
<el-table :data="scanResult.matches" size="small" border stripe>
<el-table-column prop="table_name" label="表名" min-width="180" />
<el-table-column prop="column_name" label="列名" min-width="160" />
<el-table-column prop="column_comment" label="列注释" min-width="160" show-overflow-tooltip />
<el-table-column prop="data_type" label="类型" min-width="100" />
<el-table-column prop="distinct_count" label="distinct" width="90" align="right" />
<el-table-column prop="invalid_count" label="违规数" width="90" align="right">
<template #default="{ row }">
<el-tag v-if="row.invalid_count > 0" type="danger" size="small">
{{ row.invalid_count }}
</el-tag>
<span v-else class="idcard-page__ok">0</span>
</template>
</el-table-column>
<el-table-column label="违反规则" min-width="220">
<template #default="{ row }">
<el-tag
v-for="(cnt, rule) in row.violations"
:key="rule"
size="small"
type="warning"
effect="plain"
style="margin-right: 4px"
>
{{ ruleLabels[rule] || rule }} ×{{ cnt }}
</el-tag>
<span v-if="!row.invalid_count" class="idcard-page__ok">无违规</span>
</template>
</el-table-column>
</el-table>
</template>
<!-- 有问题的表(折叠) -->
<template v-if="scanResult.violations_by_table.length">
<h4 class="idcard-page__section-title"> <h4 class="idcard-page__section-title">
有问题的表({{ scanResult.violations_by_table.length }}) 问题表({{ violationsByTable.length }})
<span class="idcard-page__section-sub">· {{ liveBadRowCount.toLocaleString() }} 行违规</span>
</h4> </h4>
<el-collapse v-model="activeTableNames" class="idcard-page__collapse"> <div class="idcard-page__section-actions">
<el-button text size="small" @click="expandAll">
<el-icon><Plus /></el-icon>
展开全部
</el-button>
<el-button text size="small" @click="collapseAll">
<el-icon><Minus /></el-icon>
折叠全部
</el-button>
</div>
</div>
<el-collapse v-model="expandedTables" class="idcard-page__collapse">
<el-collapse-item <el-collapse-item
v-for="t in scanResult.violations_by_table" v-for="t in violationsByTable"
:key="t.table_name" :key="t.table_name"
:name="t.table_name" :name="t.table_name"
class="idcard-page__collapse-item"
> >
<template #title> <template #title>
<div class="idcard-page__table-title"> <div class="idcard-page__collapse-head">
<el-icon class="idcard-page__table-icon"><Grid /></el-icon> <el-icon class="idcard-page__table-icon"><Grid /></el-icon>
<span class="idcard-page__table-name">{{ t.table_name }}</span> <span class="idcard-page__table-name">{{ t.table_name }}</span>
<el-tag <el-tag
...@@ -211,34 +186,32 @@ ...@@ -211,34 +186,32 @@
> >
{{ ruleLabels[r] || r }} {{ ruleLabels[r] || r }}
</el-tag> </el-tag>
<el-tag
v-for="idCol in idColumnsOf(t)"
:key="`idcol-${idCol}`"
size="small"
type="primary"
effect="plain"
class="idcard-page__table-idcol"
>
身份证列:{{ idCol }}
</el-tag>
</div> </div>
</template> </template>
<el-table :data="t.bad_rows" size="small" border stripe max-height="400"> <IdCardTable
<el-table-column :columns="t.columns"
v-for="col in t.columns" :column-order="buildColumnOrder(t)"
:key="col.column_name" :rows="t.bad_rows"
:prop="col.column_name" :id-card-column="idColumnsOf(t)[0] || ''"
:label="`${col.column_name}${col.column_comment ? ' · ' + col.column_comment : ''}`" :truncate-at="5000"
min-width="120" />
show-overflow-tooltip
>
<template #default="{ row }">
<span :class="{ 'idcard-page__bad': isIdCardColumn(t, col) && !isValidValue(row[col.column_name]) }">
{{ formatCell(row[col.column_name]) }}
</span>
</template>
</el-table-column>
</el-table>
<div v-if="t.bad_row_count >= 5000" class="idcard-page__trunc">
⚠ 仅展示前 5000 行(防爆上限)
</div>
</el-collapse-item> </el-collapse-item>
</el-collapse> </el-collapse>
</template> </template>
<el-empty <el-empty
v-else-if="scanResult.matches.length" v-else-if="!scanning"
description="所有命中列均无违规 ✓" description="所有命中列均无违规 ✓"
/> />
</el-card> </el-card>
...@@ -259,12 +232,15 @@ import { ...@@ -259,12 +232,15 @@ import {
Warning, Warning,
Grid, Grid,
Loading, Loading,
Plus,
Minus,
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import { import {
DEFAULT_NAME_KEYWORDS, DEFAULT_NAME_KEYWORDS,
DEFAULT_COMMENT_KEYWORDS, DEFAULT_COMMENT_KEYWORDS,
} from '@/config/idCardKeywords' } from '@/config/idCardKeywords'
import { useDbConnection } from '@/stores/useDbConnection' import { useDbConnection } from '@/stores/useDbConnection'
import IdCardTable from '@/components/IdCardTable.vue'
const { state, currentConnection } = useDbConnection() const { state, currentConnection } = useDbConnection()
...@@ -285,24 +261,20 @@ const query = reactive({ ...@@ -285,24 +261,20 @@ const query = reactive({
commentKw: DEFAULT_COMMENT_KEYWORDS.join(', '), commentKw: DEFAULT_COMMENT_KEYWORDS.join(', '),
}) })
const querying = ref(false) const querying = ref(false)
/* ──── 扫描结果(响应式:流式追加) ──── */
// scanResult 是顶层包装,violations_by_table 用 ref 数组以便 push
const scanResult = ref(null) const scanResult = ref(null)
const activeTableNames = ref([]) const violationsByTable = ref([]) // 流式 push 进来
const expandedTables = ref([]) // 当前展开的表名(el-collapse v-model)
const liveBadRowCount = ref(0) // 已发现违规行总数(实时累加)
/* ──── 扫描进度(多线程版) ──── */ /* ──── 扫描状态 ──── */
const scanning = ref(false) const scanning = ref(false)
const scanId = ref(null) const scanId = ref(null)
const progress = reactive({ const elapsedMsRef = ref(0) // 客户端自维护的「已耗时」(轮询响应里也有,仅做显示兜底)
done: 0, const polledTablesCount = ref(0) // 服务端报告的「已完成表数」
total: 0, const polledTablesTotal = ref(0) // 服务端报告的「预计总表数」
status: 'pending', // pending | running | done | error
current_column: null,
elapsed_ms: 0,
error: null,
})
const progressPercent = computed(() => {
if (!progress.total) return 0
return Math.min(100, Math.round((progress.done / progress.total) * 100))
})
let pollTimer = null let pollTimer = null
function stopPolling() { function stopPolling() {
...@@ -323,51 +295,70 @@ function parseKeywords(text) { ...@@ -323,51 +295,70 @@ function parseKeywords(text) {
] ]
} }
/** 展开 / 折叠全部 */
function expandAll() {
expandedTables.value = violationsByTable.value.map((t) => t.table_name)
}
function collapseAll() {
expandedTables.value = []
}
function onReset() { function onReset() {
query.nameKw = DEFAULT_NAME_KEYWORDS.join(', ') query.nameKw = DEFAULT_NAME_KEYWORDS.join(', ')
query.commentKw = DEFAULT_COMMENT_KEYWORDS.join(', ') query.commentKw = DEFAULT_COMMENT_KEYWORDS.join(', ')
scanResult.value = null // 停轮询 + 清空结果(后端 scan 继续在后台跑完,前端不再显示)
activeTableNames.value = []
// 取消可能还在跑的后台扫描(前端只是放弃显示,后端继续跑完)
stopPolling() stopPolling()
scanResult.value = null
violationsByTable.value = []
expandedTables.value = []
liveBadRowCount.value = 0
elapsedMsRef.value = 0
polledTablesCount.value = 0
polledTablesTotal.value = 0
scanning.value = false scanning.value = false
querying.value = false querying.value = false
scanId.value = null scanId.value = null
} }
/** /**
* 判断一张表的某列是不是本次扫描的「身份证列」(命中列之一)。 * 这张表的「身份证列」有哪些。
* 用于在折叠表格里高亮问题单元格。 *
* 数据来源:扫描结果里的 matches 数组(每条 = {table_name, column_name, ...}),
* 用 table_name 反查属于这张表的命中列。一张表可能命中多个身份证列(少见)。
*/ */
const matchedIdCardCols = ref(new Set()) function idColumnsOf(t) {
function isIdCardColumn(t, col) { const set = new Set()
return matchedIdCardCols.value.has(`${t.table_name}.${col.column_name}`) for (const m of (scanResult.value?.matches || [])) {
if (m.table_name === t.table_name) set.add(m.column_name)
}
return [...set]
} }
/** /**
* 判断单元格值是否合法(粗略,用于单元格红字样式)。 * 给一张问题表构造「显示列顺序」:
* 真值校验在后端,前端只做颜色提示。 * - 身份证列放第一位(按命中顺序,多个身份证列也都在前面)
* - 其余列按 t.columns 的原始顺序(ordinal_position)
*
* 数据列不一定都出现(bad_rows 里某些列可能是 NULL 没拉回)—— 顺序只是
* 「用户想看到的顺序」,实际渲染时 IdCardTable 会再过滤一遍。
*/ */
function isValidValue(v) { function buildColumnOrder(t) {
if (v === null || v === undefined || v === '') return false const idCols = idColumnsOf(t)
const s = String(v).trim() const allNames = (t.columns || []).map((c) => c.column_name)
return s.length === 18 && /^\d{17}[0-9X]$/.test(s) const idSet = new Set(idCols)
} const others = allNames.filter((n) => !idSet.has(n))
return [...idCols, ...others]
function formatCell(v) {
if (v === null || v === undefined) return '∅'
if (typeof v === 'object') return JSON.stringify(v)
return String(v)
} }
/** /**
* 查询:调 /api/id-card/scan(异步版) * 提交扫描 → 拿到 scan_id → 每秒轮询 /partial 拉取已完成的违规表
* 流程: * 流程:
* 1. POST → 立即拿到 scan_id(不阻塞) * 1. POST /api/id-card/scan → scan_id
* 2. setInterval 轮询 /api/id-card/scan/{scan_id}/progress 拿进度(更新 el-progress) * 2. setInterval(1000ms) GET /api/id-card/scan/{scan_id}/partial
* 3. progress.status=done → 拉 /api/id-card/scan/{scan_id}/result 拿完整结果 → 渲染 * - status=done → 停轮询 + 拿完整 response + 成功提示
* 4. progress.status=error → 弹错误 → 停轮询 * - status=error → 停轮询 + 错误提示
* 5. 任何 fetch 失败 → 停轮询 + 弹错误 * - status=running→ 把 response.violations_by_table 中新出现的表 push 到 UI
* 3. 前端去重:按 table_name 跳过已经渲染过的表,避免表格重新挂载闪烁
*/ */
async function onQuery() { async function onQuery() {
const names = parseKeywords(query.nameKw) const names = parseKeywords(query.nameKw)
...@@ -386,10 +377,13 @@ async function onQuery() { ...@@ -386,10 +377,13 @@ async function onQuery() {
querying.value = true querying.value = true
scanning.value = true scanning.value = true
scanResult.value = null scanResult.value = null
Object.assign(progress, { violationsByTable.value = []
done: 0, total: 0, status: 'pending', expandedTables.value = []
current_column: null, elapsed_ms: 0, error: null, liveBadRowCount.value = 0
}) elapsedMsRef.value = 0
polledTablesCount.value = 0
polledTablesTotal.value = 0
renderedTableNames.value = new Set()
// 1. POST 启动扫描 // 1. POST 启动扫描
let started let started
...@@ -435,87 +429,105 @@ async function onQuery() { ...@@ -435,87 +429,105 @@ async function onQuery() {
const startResp = await started.json() const startResp = await started.json()
scanId.value = startResp.scan_id scanId.value = startResp.scan_id
// 2. 开始轮询(每 800ms 一次) // 2. 开始每秒轮询
pollTimer = setInterval(pollProgress, 800) startPolling(startResp.scan_id)
pollProgress() // 立即跑一次(不等 800ms) }
const renderedTableNames = ref(new Set())
function startPolling(sid) {
stopPolling()
// 立即跑一次(不等 1000ms)
pollOnce(sid)
pollTimer = setInterval(() => pollOnce(sid), 1000)
} }
async function pollProgress() { async function pollOnce(sid) {
if (!scanId.value) return if (!sid || !scanning.value) return
try { try {
const r = await fetch(`/api/id-card/scan/${scanId.value}/progress`) const r = await fetch(`/api/id-card/scan/${sid}/partial`)
if (!r.ok) { if (!r.ok) {
// 404 = scan_id 已过期(10 分钟 TTL);停轮询并提示 // 404 = scan_id 已过期(10 分钟 TTL)
stopPolling() stopPolling()
scanning.value = false scanning.value = false
querying.value = false querying.value = false
if (r.status === 404) { if (r.status === 404) {
ElMessage.warning('扫描任务已过期(10 分钟后自动清理),请重新查询') ElMessage.warning('扫描任务已过期(10 分钟后自动清理),请重新查询')
} else { } else {
ElMessage.error(`拉取进度失败(HTTP ${r.status})`) ElMessage.error(`拉取结果失败(HTTP ${r.status})`)
} }
return return
} }
const p = await r.json() const data = await r.json()
Object.assign(progress, p)
if (p.status === 'done') { // 更新状态
stopPolling() if (typeof data.tables_done === 'number') polledTablesCount.value = data.tables_done
await fetchResult() if (typeof data.tables_total === 'number') polledTablesTotal.value = data.tables_total
} else if (p.status === 'error') { if (typeof data.elapsed_ms === 'number') elapsedMsRef.value = data.elapsed_ms
if (data.status === 'done') {
// 扫描完成 → 完整结果在 data.response
stopPolling() stopPolling()
scanning.value = false const finalResp = data.response
querying.value = false scanResult.value = finalResp
ElMessageBox.alert(p.error || '未知错误', '❌ 扫描失败', { type: 'error' })
// ★ 兜底:把最终结果里的 violations_by_table 同步到 violationsByTable + liveBadRowCount。
// 当数据库扫描很快(几百毫秒结束)时,running 阶段的 partial 可能一次都拿不到表,
// 只到 done 这一拍才有完整 violations_by_table —— 必须在这里灌进去,否则折叠列表为空。
// 仍走 dedupe 逻辑,避免 running 阶段已 push 的表重复。
const finalTables = finalResp?.violations_by_table || []
if (finalTables.length && violationsByTable.value.length === 0) {
for (const t of finalTables) {
if (!renderedTableNames.value.has(t.table_name)) {
renderedTableNames.value.add(t.table_name)
violationsByTable.value.push(t)
liveBadRowCount.value += (t.bad_row_count || 0)
}
} }
// status=pending/running 继续轮询
} catch (e) {
// 网络瞬断:不停轮询(下次会自动好),只提示一次
console.warn('[scan] 拉取进度失败:', e.message)
} }
}
async function fetchResult() {
if (!scanId.value) return
try {
const r = await fetch(`/api/id-card/scan/${scanId.value}/result`)
if (!r.ok) {
scanning.value = false scanning.value = false
querying.value = false querying.value = false
let detail ElMessage.success(
try { detail = await r.json() } catch { detail = { detail: `HTTP ${r.status}` } } `扫描完成:问题表 ${violationsByTable.value.length} 张,违规行 ${liveBadRowCount.value.toLocaleString()} 条(${(elapsedMsRef.value / 1000).toFixed(1)}s)`,
ElMessageBox.alert(
detail.detail || JSON.stringify(detail),
`❌ 拉取结果失败(HTTP ${r.status})`,
{ type: 'error' },
) )
return return
} }
const data = await r.json()
// 后端 ScanResultResponse 包装:{ scan_id, status, response: IdCardScanResponse }
const result = data.response
scanResult.value = result
// 记录命中列集合(用于单元格红字高亮)
matchedIdCardCols.value = new Set(
(result.matches || []).map((m) => `${m.table_name}.${m.column_name}`),
)
// 默认展开第一个问题表 if (data.status === 'error') {
if (result.violations_by_table?.length) { stopPolling()
activeTableNames.value = [result.violations_by_table[0].table_name] scanning.value = false
querying.value = false
ElMessageBox.alert(data.error || '未知错误', '❌ 扫描失败', { type: 'error' })
return
} }
ElMessage.success( // status=running/pending:把新增的 violations_by_table push 到 UI
`扫描完成:匹配 ${result.overview.matched_columns} 列,` const newTables = data.violations_by_table || []
+ `违规 ${result.overview.columns_with_violations} 列,` for (const t of newTables) {
+ `问题行 ${result.overview.total_bad_rows} 条(${result.overview.elapsed_ms}ms)`, if (!renderedTableNames.value.has(t.table_name)) {
) renderedTableNames.value.add(t.table_name)
violationsByTable.value.push(t)
liveBadRowCount.value += (t.bad_row_count || 0)
}
}
// 初始化 scanResult 壳(让概览区出现)
if (!scanResult.value && violationsByTable.value.length > 0) {
scanResult.value = {
overview: {
database: '',
matched_columns: 0,
columns_with_violations: 0,
total_bad_rows: 0,
elapsed_ms: 0,
},
matches: [],
violations_by_table: violationsByTable.value,
}
}
} catch (e) { } catch (e) {
ElMessage.error(`拉取结果失败: ${e.message}`) // 网络瞬断:不停轮询(下次会自动好),只 console.warn
} finally { console.warn('[poll] 拉取结果失败:', e.message)
scanning.value = false
querying.value = false
} }
} }
...@@ -608,20 +620,26 @@ const rules = [ ...@@ -608,20 +620,26 @@ const rules = [
margin-bottom: 12px; margin-bottom: 12px;
} }
/* 扫描进度条 */ /* 扫描中状态行(极简一行) */
.idcard-page__progress { .idcard-page__scanning-tip {
margin-bottom: 12px;
}
.idcard-page__progress-head {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
margin-bottom: 8px; padding: 10px 14px;
font-size: 14px; margin-bottom: 12px;
background: #f5f7fa;
border-left: 3px solid #409eff;
border-radius: 4px;
font-size: 13px;
color: #606266;
}
.idcard-page__scanning-tip strong {
color: #303133; color: #303133;
font-weight: 600;
margin: 0 2px;
} }
.idcard-page__progress-icon { .idcard-page__scanning-icon {
font-size: 16px; font-size: 14px;
color: #409eff; color: #409eff;
animation: idcard-spin 1s linear infinite; animation: idcard-spin 1s linear infinite;
} }
...@@ -629,32 +647,13 @@ const rules = [ ...@@ -629,32 +647,13 @@ const rules = [
from { transform: rotate(0deg); } from { transform: rotate(0deg); }
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
.idcard-page__progress-title { .idcard-page__scanning-elapsed {
font-weight: 600;
}
.idcard-page__progress-stat {
margin-left: auto; margin-left: auto;
font-weight: 600;
color: #409eff;
font-variant-numeric: tabular-nums;
}
.idcard-page__progress-elapsed {
color: #909399; color: #909399;
font-size: 12px; font-size: 12px;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
.idcard-page__progress-current {
margin-top: 8px;
font-size: 12px;
color: #606266;
}
.idcard-page__progress-current code {
background: #f5f7fa;
padding: 2px 6px;
border-radius: 3px;
color: #303133;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.idcard-page__form { .idcard-page__form {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
...@@ -778,18 +777,41 @@ const rules = [ ...@@ -778,18 +777,41 @@ const rules = [
font-weight: 600; font-weight: 600;
color: #303133; color: #303133;
} }
.idcard-page__section-sub {
font-size: 13px;
font-weight: normal;
color: #909399;
margin-left: 4px;
}
.idcard-page__section-head {
display: flex;
align-items: center;
justify-content: space-between;
margin: 16px 0 4px;
}
.idcard-page__section-actions {
display: flex;
gap: 4px;
}
/* 折叠表 */ /* 问题表列表(折叠) */
.idcard-page__collapse { .idcard-page__collapse {
border: 1px solid #ebeef5; border: 1px solid #ebeef5;
border-radius: 4px; border-radius: 4px;
} }
.idcard-page__table-title { .idcard-page__collapse-item {
border-bottom: 1px solid #ebeef5;
}
.idcard-page__collapse-item:last-child {
border-bottom: none;
}
.idcard-page__collapse-head {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
flex: 1; flex-wrap: wrap;
min-width: 0; font-size: 13px;
width: 100%;
} }
.idcard-page__table-icon { .idcard-page__table-icon {
color: #409eff; color: #409eff;
...@@ -797,9 +819,26 @@ const rules = [ ...@@ -797,9 +819,26 @@ const rules = [
.idcard-page__table-name { .idcard-page__table-name {
font-weight: 600; font-weight: 600;
color: #303133; color: #303133;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
} }
.idcard-page__table-comment { .idcard-page__table-comment {
margin-right: auto; color: #606266;
}
.idcard-page__table-idcol {
margin-left: 4px;
}
/* 折叠态时,整行可点;展开时表格 padding */
.idcard-page__collapse-item :deep(.el-collapse-item__header) {
padding: 8px 12px;
background: #fafbfc;
font-size: 13px;
}
.idcard-page__collapse-item :deep(.el-collapse-item__content) {
padding: 8px 12px 16px;
}
.idcard-page__collapse-item :deep(.el-collapse-item__header):hover {
background: #f0f4f9;
} }
.idcard-page__ok { .idcard-page__ok {
...@@ -809,12 +848,4 @@ const rules = [ ...@@ -809,12 +848,4 @@ const rules = [
color: #f56c6c; color: #f56c6c;
font-weight: 600; font-weight: 600;
} }
.idcard-page__trunc {
margin-top: 8px;
padding: 6px 10px;
font-size: 12px;
color: #e6a23c;
background: #fdf6ec;
border-radius: 4px;
}
</style> </style>
\ No newline at end of file
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