Commit 8d71b57f authored by Data Governance Dev's avatar Data Governance Dev

feat(web3): 导出 Excel 加违规单元格浅红 + Review Note 写规则详细

页面上数据明细的红格 + hover tooltip 在 Excel 里丢了,
业务人员拿到的导出文件看不到「哪几格不合规 / 为什么不合规」,
还得回系统查。

src/utils/excel.js 加 applyErrorHighlight():
- 数据行起始偏移 = 2(前两行是注释行 + 字段名行)
- row.errorCells.includes(cols[C].key) → 给那个 cell 加浅红底 + 写 Excel Note
- 浅红 #FEF0F0 + 红字 #F56C6C,跟页面上 .cell-error 同色
- Note 内容每行一条 issue:规则名:desc(name 空时回退 desc)
  —— 跟 ResultTable.errTip 同款格式;Note 空间够,把 desc 详细全展开
- 作者标「数据治理工具」方便业务识别
- 调用方零改动:errorCells/issues 已在 DataQualityView 透传,excel.js 直接读
- 不传 errorCells/issues 的复用场景安全:length===0 直接 continue 跳过
parent a9e0336b
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
// //
// 设计要点: // 设计要点:
// - 复用 web2 IdCardTable.vue 里的「注释行 + 字段名行 + 数据」三行表头格式 // - 复用 web2 IdCardTable.vue 里的「注释行 + 字段名行 + 数据」三行表头格式
// - 用 xlsx-js-style(支持 cell.s 边框 + 字体 + 背景) // - 用 xlsx-js-style(支持 cell.s 边框 + 字体 + 背景 + cell.c 备注)
// - 列定义兼容两种来源: // - 列定义兼容两种来源:
// ① 直接传 [{ key, cn, comment? }](resultFields 直接 map 即可) // ① 直接传 [{ key, cn, comment? }](resultFields 直接 map 即可)
// ② 传 string[](仅字段名,无注释行) // ② 传 string[](仅字段名,无注释行)
...@@ -13,6 +13,12 @@ ...@@ -13,6 +13,12 @@
// 第 1 行:字段注释(column_comment,缺则留空) // 第 1 行:字段注释(column_comment,缺则留空)
// 第 2 行:字段显示名(cn —— 中文表头;用户原始字段名为保留显示给业务人员) // 第 2 行:字段显示名(cn —— 中文表头;用户原始字段名为保留显示给业务人员)
// 这样业务人员打开 Excel 一眼能看懂字段含义,也能看到 DB 里的实际字段名 // 这样业务人员打开 Excel 一眼能看懂字段含义,也能看到 DB 里的实际字段名
//
// 违规高亮(2026-08-24):
// - rows 上若带 row.errorCells: [key,...] + row.issues: [{field, name, desc},...]
// 会给对应单元格加浅红背景(与页面上 .cell-error 同色 #FEF0F0)
// 并写 cell note(Review → Note):列出违反的规则名 + 规则详细
// - 跟页面表现一致 —— 用户在 Excel 里看到红格 + 悬浮 note 就知道这条违规是为什么
// ============================================================================ // ============================================================================
import * as XLSX from 'xlsx-js-style' import * as XLSX from 'xlsx-js-style'
...@@ -21,6 +27,7 @@ import * as XLSX from 'xlsx-js-style' ...@@ -21,6 +27,7 @@ import * as XLSX from 'xlsx-js-style'
* *
* @param {Object} opts * @param {Object} opts
* @param {Array<Object>} opts.rows 数据行 [{ colKey: value, ... }] * @param {Array<Object>} opts.rows 数据行 [{ colKey: value, ... }]
* 可选:errorCells/issues 给违规单元格加红 + note
* @param {Array<{ key: string, cn: string, comment?: string }>|string[]} opts.columns * @param {Array<{ key: string, cn: string, comment?: string }>|string[]} opts.columns
* 字段定义;string[] 时退化为「无注释行」 * 字段定义;string[] 时退化为「无注释行」
* @param {string} [opts.filename='export.xlsx'] 输出文件名 * @param {string} [opts.filename='export.xlsx'] 输出文件名
...@@ -76,6 +83,9 @@ export async function exportToExcel(opts) { ...@@ -76,6 +83,9 @@ export async function exportToExcel(opts) {
// 样式:所有 cell 加细边框 + 前两行加粗 + 浅灰背景 // 样式:所有 cell 加细边框 + 前两行加粗 + 浅灰背景
applyAllBorders(ws) applyAllBorders(ws)
applyHeaderStyle(ws, cols.length) applyHeaderStyle(ws, cols.length)
// 违规高亮 + cell note(依赖前两行已经定好,下面用 (r,c) 偏移算到数据行)
// 数据行从第 2 行开始(0-based: 0=commentRow, 1=nameRow, 2=第一条数据)
applyErrorHighlight(ws, rows, cols, 2)
// 冻结前 2 行(滚动时表头固定) // 冻结前 2 行(滚动时表头固定)
ws['!freeze'] = { xSplit: 0, ySplit: 2 } ws['!freeze'] = { xSplit: 0, ySplit: 2 }
...@@ -181,4 +191,62 @@ function applyHeaderStyle(ws, cols) { ...@@ -181,4 +191,62 @@ function applyHeaderStyle(ws, cols) {
} }
} }
} }
}
/**
* 违规高亮(2026-08-24):
* - 给违反规则的单元格加浅红背景(与页面上 .cell-error 同色 #FEF0F0)
* - 给单元格写 Excel 备注(Review → Note):列出违反的规则名 + 规则详细
*
* 入参:
* ws: xlsx worksheet
* rows: 原始数据行(与生成 aoa 用同一份),含 row.errorCells / row.issues
* cols: 归一后的列定义 [{ key, cn, comment }]
* startRow: 数据行在 sheet 里的起始行(0-based);前两行是注释 + 字段名,所以是 2
*
* 行级映射:aoa 第 i 行(i>=startRow)对应 rows[i-startRow]
* 列级映射:col C 对应 cols[C].key,去 rows 找 row.errorCells.includes(cols[C].key)
*/
function applyErrorHighlight(ws, rows, cols, startRow) {
// 浅红 —— 跟页面 .cell-error { background: #FEF0F0 } 同色(去 # 号大写)
const ERROR_FILL = { patternType: 'solid', fgColor: { rgb: 'FFFEF0F0' } }
// 红字 —— 跟页面 .cell-error 的文字颜色一致(#F56C6C)
const ERROR_FONT = { color: { rgb: 'FFF56C6C' } }
for (let i = 0; i < rows.length; i++) {
const row = rows[i]
if (!row) continue
const errorCells = Array.isArray(row.errorCells) ? row.errorCells : []
if (errorCells.length === 0) continue
for (let C = 0; C < cols.length; C++) {
const colKey = cols[C].key
if (!errorCells.includes(colKey)) continue
const addr = XLSX.utils.encode_cell({ r: startRow + i, c: C })
const cell = ws[addr]
if (!cell) continue
// 浅红底 + 红字
cell.s = {
...(cell.s || {}),
fill: ERROR_FILL,
font: { ...(cell.s?.font || {}), ...ERROR_FONT },
}
// 写 Excel 备注(Review → Note)
const issues = (row.issues || []).filter((it) => it && it.field === colKey)
if (issues.length > 0) {
const noteText = issues
.map((it) => {
// 跟 ResultTable.errTip 同款:name + desc 都列(note 空间够,比 cell tooltip 宽松)
const tag = it.name && it.name.trim() ? it.name : '(未命名规则)'
const desc = it.desc || ''
return desc ? `${tag}:${desc}` : tag
})
.join('\n')
cell.c = [{ t: noteText, a: '数据治理工具' }]
}
}
}
} }
\ 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