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

feat(web2): 新增 IdCardTable 组件 — 违规标签列 + 列拖拽 + 列宽自适应

- 第 1 列固定「违规」摘要(width=200, min-width=160)
  - el-tooltip 展示详细原因(前 N 字符截断 + 悬停看完整)
  - 违规标签: red 圆角徽章 + 警示图标
- 数据列:身份证列加宽到 220px + 蓝字 + User 图标
  - 其余列 min-width=120,列可拖拽(el-table-column 默认 resizable)
- 表头 tooltip:字段名 / 字段注释 / 数据类型 三行
- 单元格:身份证列若值不合规显示红字(粗略校验,真实校验在后端)
- 防爆:rows > 5000 时显示「仅展示前 5000 行」提示
- 内部字段(_ 开头)自动从 dataColumns 过滤掉
parent 2dda2414
<!--
共通表格组件 —— 展示「问题表」的字段元数据 + 违规行
设计:
- 第一列:违规摘要(小标签)+ tooltip 详细原因
- 身份证列:放第一位(columnOrder[0])+ User 图标 + 蓝字 + 加宽到 220px
- 列可拖拽:el-table-column 默认 resizable=true,配合 border 即可拖拽列宽
- 表头:hover tooltip 显示字段名 + 字段注释 + 数据类型
- 单元格:身份证列若值不合规显示红字(粗略校验,真实校验在后端)
- 防爆:rows 超过 truncateAt 时给提示文案
Props:
columns [{ column_name, column_comment, data_type, ... }] 字段元数据
columnOrder [string] 显示顺序(身份证列须在 [0])
rows [{ col_name: value, ..., _id_card_violation: str}] 表格行数据(_id_card_violation 是后端注入)
idCardColumn string 身份证字段名(用于单元格高亮)
truncateAt int 多少行后显示截断提示(默认 5000)
-->
<template>
<el-table
:data="displayedRows"
size="small"
border
stripe
class="idcard-table"
>
<!-- 第 1 列:违规摘要(每行都显示) -->
<el-table-column
label="违规"
width="200"
min-width="160"
fixed="left"
align="left"
header-align="center"
>
<template #default="{ row }">
<el-tooltip
v-if="row._id_card_violation"
placement="top"
effect="light"
:show-after="100"
:disabled="!row._id_card_violation"
>
<template #content>
<div class="idcard-table__tip-reason">
<div class="idcard-table__tip-reason-head">不合规原因</div>
<div class="idcard-table__tip-reason-body">{{ row._id_card_violation }}</div>
</div>
</template>
<el-tag size="small" type="danger" effect="dark" class="idcard-table__reason-tag">
<el-icon class="idcard-table__reason-icon"><WarningFilled /></el-icon>
<span class="idcard-table__reason-text">{{ truncate(row._id_card_violation, 14) }}</span>
</el-tag>
</el-tooltip>
<span v-else class="idcard-table__reason-empty">—</span>
</template>
</el-table-column>
<!-- 数据列:身份证列强制加宽 -->
<el-table-column
v-for="colName in dataColumns"
:key="colName"
:prop="colName"
:min-width="isIdCard(colName) ? 220 : 120"
:width="isIdCard(colName) ? 220 : undefined"
resizable
show-overflow-tooltip
>
<template #header>
<el-tooltip
placement="top"
effect="light"
:show-after="100"
>
<template #content>
<div class="idcard-table__tip">
<div class="idcard-table__tip-name">{{ colName }}</div>
<div v-if="commentOf(colName)" class="idcard-table__tip-comment">
{{ commentOf(colName) }}
</div>
<div v-if="typeOf(colName)" class="idcard-table__tip-type">
{{ typeOf(colName) }}
</div>
</div>
</template>
<span :class="{ 'idcard-table__head-id': isIdCard(colName) }">
{{ colName }}
<el-icon v-if="isIdCard(colName)" class="idcard-table__head-icon">
<User />
</el-icon>
</span>
</el-tooltip>
</template>
<template #default="{ row }">
<span :class="cellClass(colName, row[colName])">
{{ formatCell(row[colName]) }}
</span>
</template>
</el-table-column>
<template #empty>
<el-empty description="该表无违规行" />
</template>
</el-table>
<div v-if="rows.length > truncateAt" class="idcard-table__trunc">
⚠ 仅展示前 {{ truncateAt }} 行(防爆上限;实际违规行 {{ rows.length }} 条)
</div>
</template>
<script setup>
import { computed } from 'vue'
import { User, WarningFilled } from '@element-plus/icons-vue'
const props = defineProps({
columns: {
type: Array,
required: true,
},
columnOrder: {
type: Array,
default: () => [],
},
rows: {
type: Array,
default: () => [],
},
idCardColumn: {
type: String,
default: '',
},
truncateAt: {
type: Number,
default: 5000,
},
})
/* 字段名 → 元数据查找表(O(1)) */
const colMap = computed(() => {
const m = new Map()
for (const c of props.columns || []) {
m.set(c.column_name, c)
}
return m
})
/**
* 数据列顺序:
* - columnOrder 优先(调用方控制,身份证列已放第 1)
* - 兜底:用 columns 的原始顺序
* - 过滤掉 rows 里不存在的字段(避免空列)
*/
const dataColumns = computed(() => {
const explicit = props.columnOrder?.length
? props.columnOrder
: (props.columns || []).map((c) => c.column_name)
if (!props.rows?.length) return explicit
const seen = new Set()
for (const r of props.rows) {
for (const k of Object.keys(r)) {
// 跳过后端注入的内部字段
if (!k.startsWith('_')) seen.add(k)
}
}
return explicit.filter((n) => seen.has(n))
})
const displayedRows = computed(() => props.rows.slice(0, props.truncateAt))
function commentOf(colName) {
return colMap.value.get(colName)?.column_comment || ''
}
function typeOf(colName) {
return colMap.value.get(colName)?.data_type || ''
}
function isIdCard(colName) {
return !!props.idCardColumn && colName === props.idCardColumn
}
/**
* 单元格样式:身份证列 + 值不合法 → 红字
* (粗略校验,真实校验在后端)
*/
function cellClass(colName, v) {
if (!isIdCard(colName)) return ''
if (v === null || v === undefined || v === '') return 'idcard-table__empty'
const s = String(v).trim()
if (s.length === 18 && /^\d{17}[0-9X]$/.test(s)) return ''
return 'idcard-table__bad'
}
function formatCell(v) {
if (v === null || v === undefined) return '∅'
if (typeof v === 'object') return JSON.stringify(v)
return String(v)
}
/**
* 违规原因截断:用于 cell 标签里只显示前 N 个字符。
* 例:「校验位计算结果应为 'X',实际为 '1'」 → 「校验位计算…」 (6 字)
*/
function truncate(s, n) {
if (!s) return ''
return s.length > n ? s.slice(0, n) + '…' : s
}
</script>
<style scoped>
.idcard-table {
margin-top: 4px;
}
/* 违规标签列 */
.idcard-table__reason-tag {
cursor: help;
max-width: 100%;
overflow: hidden;
}
.idcard-table__reason-icon {
margin-right: 3px;
font-size: 11px;
vertical-align: -1px;
}
.idcard-table__reason-text {
display: inline-block;
vertical-align: middle;
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.idcard-table__reason-empty {
display: inline-block;
width: 100%;
text-align: center;
color: #c0c4cc;
}
.idcard-table__tip-reason {
max-width: 320px;
font-size: 13px;
line-height: 1.6;
}
.idcard-table__tip-reason-head {
font-weight: 600;
color: #f56c6c;
margin-bottom: 4px;
padding-bottom: 4px;
border-bottom: 1px solid #fde2e2;
}
.idcard-table__tip-reason-body {
color: #303133;
white-space: pre-wrap;
word-break: break-word;
}
/* 表头:身份证列加蓝字 + User 图标 */
.idcard-table__head-id {
display: inline-flex;
align-items: center;
gap: 4px;
color: #409eff;
font-weight: 600;
}
.idcard-table__head-icon {
font-size: 12px;
}
/* tooltip 三行:字段名 / 字段注释 / 类型 */
.idcard-table__tip {
font-size: 12px;
line-height: 1.6;
max-width: 320px;
}
.idcard-table__tip-name {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-weight: 600;
color: #303133;
font-size: 13px;
margin-bottom: 4px;
}
.idcard-table__tip-comment {
color: #606266;
}
.idcard-table__tip-type {
color: #909399;
margin-top: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 11px;
}
/* 单元格 */
.idcard-table__bad {
color: #f56c6c;
font-weight: 600;
}
.idcard-table__empty {
color: #c0c4cc;
}
/* 截断提示 */
.idcard-table__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