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

feat(web2): IdCardTool 进度条 UI + 异步扫描轮询

配合后端三端点改造前端:
- POST 立即拿 scan_id → 不阻塞 UI
- setInterval 800ms 轮询 /progress 更新 el-progress
- status=done → 拉 /result → 渲染命中列清单 + 折叠问题表

UI 改造:
- 新增「扫描进度」卡片:旋转 Loading 图标 + el-progress 百分比 +
  done/total 计数 + 耗时 + 当前列名
- 「重置」按钮同时停轮询、清进度状态

onQuery 拆三个函数:
- onQuery()      启动 + 立即首次轮询
- pollProgress() 拉进度;status=done/error 自动收尾
- fetchResult()  拉最终结果

踩坑(production build 不会报 import 缺失):
之前改 IdCardTool 加 computed() 时漏了 import 'computed',
dev 模式 Vite 红屏能看出来,production build 只打包不校验,
运行时 ReferenceError 只出现在浏览器 console,server log 完全没痕迹。
这次提交补回 import。

踩坑见 work-logs/2026-08-17.md 第七章。
parent c4ed1b5c
...@@ -3,35 +3,818 @@ ...@@ -3,35 +3,818 @@
<el-card class="idcard-page__header" shadow="never"> <el-card class="idcard-page__header" shadow="never">
<h2 class="idcard-page__title">身份证治理工具</h2> <h2 class="idcard-page__title">身份证治理工具</h2>
<p class="idcard-page__desc"> <p class="idcard-page__desc">
占位页面。该工具将在后续迭代中实现:身份证号码识别、校验位计算、地区码归属地解析等。 依据 <strong>GB 11643-1999《公民身份号码》</strong>、<strong>GB/T 2260《中华人民共和国行政区划代码》</strong>、
<strong>GB/T 7408《数据元和交换格式 信息交换 日期和时间表示法》</strong>,
对数据库内身份证字段进行合规性校验。每条规则 hover ❓ 查看详情。
</p> </p>
</el-card> </el-card>
<el-empty description="功能开发中……" /> <!-- 规则说明卡片 -->
<el-row :gutter="12" class="idcard-page__grid">
<el-col
v-for="rule in rules"
:key="rule.title"
:xs="12"
:sm="8"
:md="6"
:lg="4"
>
<el-tooltip placement="top" effect="light" :show-after="100">
<template #content>
<div class="idcard-tooltip">
<div class="idcard-tooltip__head">
<el-icon :style="{ color: rule.color }">
<component :is="rule.icon" />
</el-icon>
<span>{{ rule.title }}</span>
</div>
<ul class="idcard-tooltip__list">
<li v-for="(line, i) in rule.detail" :key="i">{{ line }}</li>
</ul>
</div>
</template>
<el-card class="idcard-page__card" shadow="hover">
<div class="idcard-page__card-inner">
<el-icon class="idcard-page__card-icon" :style="{ color: rule.color }">
<component :is="rule.icon" />
</el-icon>
<span class="idcard-page__card-title">{{ rule.title }}</span>
<el-icon class="idcard-page__card-info"><InfoFilled /></el-icon>
</div>
</el-card>
</el-tooltip>
</el-col>
</el-row>
<!-- 字段匹配关键字 + 查询 -->
<el-card class="idcard-page__query" shadow="never">
<el-form :inline="true" class="idcard-page__form" @submit.prevent>
<el-form-item label="表字段关键字">
<el-input
v-model="query.nameKw"
placeholder="英文列名,多个用逗号分隔"
clearable
style="width: 360px"
/>
</el-form-item>
<el-form-item label="字段注释关键字">
<el-input
v-model="query.commentKw"
placeholder="中文注释词,多个用逗号分隔"
clearable
style="width: 280px"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:icon="Search"
:loading="querying"
:disabled="!state.currentType"
@click="onQuery"
>
查询
</el-button>
<el-button text @click="onReset">重置</el-button>
</el-form-item>
<el-form-item v-if="!state.currentType">
<span class="idcard-page__hint">
<el-icon><Warning /></el-icon>
请先在右上角选择数据库连接
</span>
</el-form-item>
</el-form>
</el-card>
<!-- 扫描进度条 -->
<el-card v-if="scanning" class="idcard-page__progress" shadow="never">
<div class="idcard-page__progress-head">
<el-icon class="idcard-page__progress-icon"><Loading /></el-icon>
<span class="idcard-page__progress-title">正在扫描身份证合规性</span>
<span class="idcard-page__progress-stat">
{{ progress.done }} / {{ progress.total || '?' }}
</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>
</el-card>
<!-- 扫描结果 -->
<el-card v-if="scanResult" class="idcard-page__result" shadow="never">
<!-- 概览统计 -->
<div class="idcard-page__overview">
<div class="idcard-page__stat">
<div class="idcard-page__stat-label">数据库</div>
<div class="idcard-page__stat-value">{{ scanResult.overview.database }}</div>
</div>
<div class="idcard-page__stat">
<div class="idcard-page__stat-label">命中列</div>
<div class="idcard-page__stat-value">
{{ scanResult.overview.matched_columns }}
<span class="idcard-page__stat-sub">/ {{ scanResult.overview.total_columns_in_db }}</span>
</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">
{{ 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--danger">
{{ scanResult.overview.total_bad_rows }}
</div>
</div>
<div class="idcard-page__stat">
<div class="idcard-page__stat-label">耗时</div>
<div class="idcard-page__stat-value">{{ scanResult.overview.elapsed_ms }} ms</div>
</div>
</div>
<!-- 命中列清单 -->
<template v-if="scanResult.matches.length">
<h4 class="idcard-page__section-title">命中的列({{ scanResult.matches.length }})</h4>
<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">
有问题的表({{ scanResult.violations_by_table.length }})
</h4>
<el-collapse v-model="activeTableNames" class="idcard-page__collapse">
<el-collapse-item
v-for="t in scanResult.violations_by_table"
:key="t.table_name"
:name="t.table_name"
>
<template #title>
<div class="idcard-page__table-title">
<el-icon class="idcard-page__table-icon"><Grid /></el-icon>
<span class="idcard-page__table-name">{{ t.table_name }}</span>
<el-tag
v-if="t.table_comment"
size="small"
type="info"
effect="plain"
class="idcard-page__table-comment"
>
{{ t.table_comment }}
</el-tag>
<el-tag size="small" type="danger" effect="dark">
{{ t.bad_row_count }} 行违规
</el-tag>
<el-tag
v-for="r in t.rule_summary"
:key="r"
size="small"
type="warning"
effect="plain"
>
{{ ruleLabels[r] || r }}
</el-tag>
</div>
</template>
<el-table :data="t.bad_rows" size="small" border stripe max-height="400">
<el-table-column
v-for="col in t.columns"
:key="col.column_name"
:prop="col.column_name"
:label="`${col.column_name}${col.column_comment ? ' · ' + col.column_comment : ''}`"
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>
</template>
<el-empty
v-else-if="scanResult.matches.length"
description="所有命中列均无违规 ✓"
/>
</el-card>
</div> </div>
</template> </template>
<script setup> <script setup>
// 暂时无业务逻辑 import { reactive, ref, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Document,
Location,
Calendar,
Histogram,
Lock,
InfoFilled,
Search,
Warning,
Grid,
Loading,
} from '@element-plus/icons-vue'
import {
DEFAULT_NAME_KEYWORDS,
DEFAULT_COMMENT_KEYWORDS,
} from '@/config/idCardKeywords'
import { useDbConnection } from '@/stores/useDbConnection'
const { state, currentConnection } = useDbConnection()
/* ──── 规则 ID → 中文标签(前端展示) ──── */
const ruleLabels = {
EMPTY: '空值',
LENGTH: '长度',
CHARSET: '字符集',
CHECK_DIGIT: '校验位',
ADDRESS_PREFIX: '地址码',
BIRTH_DATE: '出生日期',
ORDER_CODE: '顺序码',
}
/* ──── 查询表单状态 ──── */
const query = reactive({
nameKw: DEFAULT_NAME_KEYWORDS.join(', '),
commentKw: DEFAULT_COMMENT_KEYWORDS.join(', '),
})
const querying = ref(false)
const scanResult = ref(null)
const activeTableNames = ref([])
/* ──── 扫描进度(多线程版) ──── */
const scanning = ref(false)
const scanId = ref(null)
const progress = reactive({
done: 0,
total: 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
function stopPolling() {
if (pollTimer) {
clearInterval(pollTimer)
pollTimer = null
}
}
function parseKeywords(text) {
return [
...new Set(
(text || '')
.split(/[,,\s]+/)
.map((s) => s.trim())
.filter(Boolean),
),
]
}
function onReset() {
query.nameKw = DEFAULT_NAME_KEYWORDS.join(', ')
query.commentKw = DEFAULT_COMMENT_KEYWORDS.join(', ')
scanResult.value = null
activeTableNames.value = []
// 取消可能还在跑的后台扫描(前端只是放弃显示,后端继续跑完)
stopPolling()
scanning.value = false
querying.value = false
scanId.value = null
}
/**
* 判断一张表的某列是不是本次扫描的「身份证列」(命中列之一)。
* 用于在折叠表格里高亮问题单元格。
*/
const matchedIdCardCols = ref(new Set())
function isIdCardColumn(t, col) {
return matchedIdCardCols.value.has(`${t.table_name}.${col.column_name}`)
}
/**
* 判断单元格值是否合法(粗略,用于单元格红字样式)。
* 真值校验在后端,前端只做颜色提示。
*/
function isValidValue(v) {
if (v === null || v === undefined || v === '') return false
const s = String(v).trim()
return s.length === 18 && /^\d{17}[0-9X]$/.test(s)
}
function formatCell(v) {
if (v === null || v === undefined) return '∅'
if (typeof v === 'object') return JSON.stringify(v)
return String(v)
}
/**
* 查询:调 /api/id-card/scan(异步版)
* 流程:
* 1. POST → 立即拿到 scan_id(不阻塞)
* 2. setInterval 轮询 /api/id-card/scan/{scan_id}/progress 拿进度(更新 el-progress)
* 3. progress.status=done → 拉 /api/id-card/scan/{scan_id}/result 拿完整结果 → 渲染
* 4. progress.status=error → 弹错误 → 停轮询
* 5. 任何 fetch 失败 → 停轮询 + 弹错误
*/
async function onQuery() {
const names = parseKeywords(query.nameKw)
const comments = parseKeywords(query.commentKw)
if (!names.length && !comments.length) {
ElMessage.warning('请至少输入一个表字段关键字或字段注释关键字')
return
}
const conn = currentConnection.value
if (!conn || !state.currentType) {
ElMessage.warning('请先在右上角选择数据库连接')
return
}
// 重置状态
querying.value = true
scanning.value = true
scanResult.value = null
Object.assign(progress, {
done: 0, total: 0, status: 'pending',
current_column: null, elapsed_ms: 0, error: null,
})
// 1. POST 启动扫描
let started
try {
started = await fetch('/api/id-card/scan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
db_type: state.currentType,
host: conn.host,
port: conn.port,
user: conn.user,
password: conn.password,
database: conn.database,
charset: 'utf8mb4',
connect_timeout: 10,
oracle_client_dir: conn.oracle_client_dir || null,
name_keywords: names,
comment_keywords: comments,
max_full_rows_per_column: 5000,
max_workers: 5,
}),
})
} catch (e) {
scanning.value = false
querying.value = false
ElMessage.error(`启动扫描失败: ${e.message}`)
return
}
if (!started.ok) {
scanning.value = false
querying.value = false
let detail
try { detail = await started.json() } catch { detail = { detail: `HTTP ${started.status}` } }
const detailMsg = Array.isArray(detail.detail)
? detail.detail.map((d) => `${d.loc?.join('.') || ''}: ${d.msg}`).join('\n')
: (detail.detail || JSON.stringify(detail))
ElMessageBox.alert(detailMsg, `❌ 启动扫描失败(HTTP ${started.status})`, { type: 'error' })
return
}
const startResp = await started.json()
scanId.value = startResp.scan_id
// 2. 开始轮询(每 800ms 一次)
pollTimer = setInterval(pollProgress, 800)
pollProgress() // 立即跑一次(不等 800ms)
}
async function pollProgress() {
if (!scanId.value) return
try {
const r = await fetch(`/api/id-card/scan/${scanId.value}/progress`)
if (!r.ok) {
// 404 = scan_id 已过期(10 分钟 TTL);停轮询并提示
stopPolling()
scanning.value = false
querying.value = false
if (r.status === 404) {
ElMessage.warning('扫描任务已过期(10 分钟后自动清理),请重新查询')
} else {
ElMessage.error(`拉取进度失败(HTTP ${r.status})`)
}
return
}
const p = await r.json()
Object.assign(progress, p)
if (p.status === 'done') {
stopPolling()
await fetchResult()
} else if (p.status === 'error') {
stopPolling()
scanning.value = false
querying.value = false
ElMessageBox.alert(p.error || '未知错误', '❌ 扫描失败', { type: 'error' })
}
// 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
querying.value = false
let detail
try { detail = await r.json() } catch { detail = { detail: `HTTP ${r.status}` } }
ElMessageBox.alert(
detail.detail || JSON.stringify(detail),
`❌ 拉取结果失败(HTTP ${r.status})`,
{ type: 'error' },
)
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 (result.violations_by_table?.length) {
activeTableNames.value = [result.violations_by_table[0].table_name]
}
ElMessage.success(
`扫描完成:匹配 ${result.overview.matched_columns} 列,`
+ `违规 ${result.overview.columns_with_violations} 列,`
+ `问题行 ${result.overview.total_bad_rows} 条(${result.overview.elapsed_ms}ms)`,
)
} catch (e) {
ElMessage.error(`拉取结果失败: ${e.message}`)
} finally {
scanning.value = false
querying.value = false
}
}
/* ──── 身份证合规性规则(前端展示用) ──── */
const rules = [
{
title: '长度与字符集',
icon: Document,
color: '#409eff',
detail: [
'标准长度为 18 位',
'早期(1999 年前)有 15 位老号(无校验位、年份仅 2 位)',
'前 17 位必须是数字 0–9',
'第 18 位是数字 0–9 或大写 X(不能小写 x)',
'不允许空格、连字符、全角字符、其他符号',
'不允许全部相同(如 111111111111111111)或全部 0',
],
},
{
title: '地址码(前 6 位)',
icon: Location,
color: '#67c23a',
detail: [
'依据 GB/T 2260《中华人民共和国行政区划代码》',
'前 2 位不能为 00(省/自治区/直辖市必须存在)',
'第 3–4 位一般不能为 00(直辖市下辖区除外)',
'第 5–6 位不能为 00(街道/乡镇级)',
'地址码必须在 GB/T 2260 当前发布的行政区划表中存在',
'已撤销的行政区划按发证时校验——实现时只校验「最新表 + 历史主表」',
],
},
{
title: '出生日期(第 7–14 位)',
icon: Calendar,
color: '#e6a23c',
detail: [
'依据 GB/T 7408《数据元和交换格式 日期和时间表示法》',
'格式 YYYYMMDD,共 8 位数字',
'必须是真实存在的日期(月份 01–12,含闰年 2 月 29 日)',
'不能晚于当前日期(不能是未来出生)',
'年份下限追溯至 1900 年前后(身份证制度自 1985 年实施)',
'15 位老号升 18 位:在 yyMMdd 前插入 19',
],
},
{
title: '顺序码(第 15–17 位)',
icon: Histogram,
color: '#f56c6c',
detail: [
'同一地区、同一天出生的人按顺序编号',
'第 17 位奇数 = 男,偶数 = 女',
'有效范围 001–999;000 通常作为预留/特殊用途',
'顺序码本身不强制唯一(同地区同日同性别可能多组 001–999)',
],
},
{
title: '校验位(第 18 位)',
icon: Lock,
color: '#909399',
detail: [
'依据 GB 11643-1999 §6',
'加权和:每位 × 加权因子(7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4 2),求和 S',
'取模:Y = S mod 11',
'查表映射:Y=0→1, 1→0, 2→X, 3→9, 4→8, 5→7, 6→6, 7→5, 8→4, 9→3, 10→2',
'查表结果必须与身份证第 18 位一致,否则视为伪号',
'15 位老号无校验位,转 18 位时必须重算校验位',
],
},
]
</script> </script>
<style scoped> <style scoped>
.idcard-page {
max-width: 1200px;
margin: 0 auto;
}
.idcard-page__header { .idcard-page__header {
margin-bottom: 20px; margin-bottom: 12px;
} }
.idcard-page__title { .idcard-page__title {
margin: 0 0 8px; margin: 0 0 4px;
font-size: 20px; font-size: 18px;
color: #303133; color: #303133;
} }
.idcard-page__desc { .idcard-page__desc {
margin: 0; margin: 0;
color: #606266; color: #606266;
font-size: 14px; font-size: 13px;
line-height: 1.6; line-height: 1.6;
} }
</style>
/* 查询表单 */
.idcard-page__query {
margin-bottom: 12px;
}
/* 扫描进度条 */
.idcard-page__progress {
margin-bottom: 12px;
}
.idcard-page__progress-head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
font-size: 14px;
color: #303133;
}
.idcard-page__progress-icon {
font-size: 16px;
color: #409eff;
animation: idcard-spin 1s linear infinite;
}
@keyframes idcard-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.idcard-page__progress-title {
font-weight: 600;
}
.idcard-page__progress-stat {
margin-left: auto;
font-weight: 600;
color: #409eff;
font-variant-numeric: tabular-nums;
}
.idcard-page__progress-elapsed {
color: #909399;
font-size: 12px;
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 {
display: flex;
flex-wrap: wrap;
align-items: center;
row-gap: 4px;
}
.idcard-page__form :deep(.el-form-item) {
margin-bottom: 4px;
}
.idcard-page__hint {
color: #e6a23c;
font-size: 13px;
display: inline-flex;
align-items: center;
gap: 4px;
}
/* 规则卡片网格 */
.idcard-page__grid {
margin: 0 !important;
margin-bottom: 12px !important;
}
.idcard-page__card {
margin: 0;
cursor: pointer;
border-radius: 6px;
}
.idcard-page__card :deep(.el-card__body) {
padding: 10px 12px;
}
.idcard-page__card-inner {
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.idcard-page__card-icon {
font-size: 16px;
flex-shrink: 0;
}
.idcard-page__card-title {
flex: 1;
font-size: 13px;
font-weight: 600;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.idcard-page__card-info {
font-size: 14px;
color: #c0c4cc;
flex-shrink: 0;
transition: color 0.15s;
}
.idcard-page__card:hover .idcard-page__card-info {
color: #409eff;
}
.idcard-tooltip {
max-width: 360px;
font-size: 13px;
line-height: 1.7;
color: #303133;
}
.idcard-tooltip__head {
display: flex;
align-items: center;
gap: 6px;
font-weight: 600;
margin-bottom: 6px;
padding-bottom: 6px;
border-bottom: 1px solid #ebeef5;
}
.idcard-tooltip__head .el-icon {
font-size: 16px;
}
.idcard-tooltip__list {
margin: 0;
padding-left: 18px;
color: #606266;
}
.idcard-tooltip__list li {
margin-bottom: 2px;
}
/* 结果区 */
.idcard-page__result {
margin-top: 4px;
}
.idcard-page__overview {
display: flex;
gap: 24px;
flex-wrap: wrap;
padding: 12px 16px;
background: #f5f7fa;
border-radius: 6px;
margin-bottom: 16px;
}
.idcard-page__stat-label {
font-size: 12px;
color: #909399;
margin-bottom: 4px;
}
.idcard-page__stat-value {
font-size: 20px;
font-weight: 600;
color: #303133;
}
.idcard-page__stat-value--warn { color: #e6a23c; }
.idcard-page__stat-value--danger { color: #f56c6c; }
.idcard-page__stat-sub {
font-size: 13px;
font-weight: normal;
color: #909399;
}
.idcard-page__section-title {
margin: 16px 0 8px;
font-size: 14px;
font-weight: 600;
color: #303133;
}
/* 折叠表 */
.idcard-page__collapse {
border: 1px solid #ebeef5;
border-radius: 4px;
}
.idcard-page__table-title {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
}
.idcard-page__table-icon {
color: #409eff;
}
.idcard-page__table-name {
font-weight: 600;
color: #303133;
}
.idcard-page__table-comment {
margin-right: auto;
}
.idcard-page__ok {
color: #67c23a;
}
.idcard-page__bad {
color: #f56c6c;
font-weight: 600;
}
.idcard-page__trunc {
margin-top: 8px;
padding: 6px 10px;
font-size: 12px;
color: #e6a23c;
background: #fdf6ec;
border-radius: 4px;
}
</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