Commit 1dffa7f9 authored by Data Governance Dev's avatar Data Governance Dev

feat(web3): 编辑场景自动从 DB 同步字段注释/类型

背景:
cn / type 故意不持久化(plan §4),编辑时按 source_table 重新从数据源拉。
但用户每次都得手动点「测试连接」走 loadColumns,体验差。

改动(只动 web3/src/components/TaskModal.vue):

1. 新增 refreshColumnsForEdit():
   - 用存储的 conn_json(含匹配的预设密码或后端返回的密码)+ source_table
     直接调 listColumns API
   - 按 field_key 索引 merge 进现有 fieldList(不动 stored rules,只覆盖 cn/type/isNullable)
   - DB 中已不存在的字段 → 保留 stored 状态 + 顶部 warning
   - DB 中新出现的字段(无 stored rules)→ 不主动加,避免污染用户视角
   - API 失败(密码空/DB 不通/表被删)→ 静默退化,不弹错误

2. form.password 回填优先级:task 自带 > 预设 > 用户手输
   (旧版只靠预设匹配回填,手动建的任务没匹配预设就拿不到密码)

3. 触发时机:编辑场景打开弹窗后,props.task.field_list 异步到位时
   (不能挂 open watch:父组件 openEdit 是「同步 set row + 异步 getTask」
   两步,同步阶段 field_list 还是 [],refreshColumnsForEdit 会 no-op)

4. 字段表头部「↻ 重新同步字段」按钮:DB 中途变更后手动刷新
   v-if=props.task && form.table && fieldList.length > 0

5. preset 加载用 props.task.db_type(不再用 form.dbType 默认 'MySQL'):
   编辑 Oracle 任务时,open watch 同步阶段 preset 匹配拿的是 MySQL 预设,
   永远匹配不上;watch(form.dbType) 异步重载又来不及。
   改为先用 targetDbType=props.task?.db_type||form.dbType 加载。

踩坑:见 work-logs/2026-08-21.md(同步/异步时序类 + 密码可见性)。
parent 03e74bbb
......@@ -170,8 +170,18 @@
<!-- Tab3 校验规则(字段列表) -->
<div class="tab-panel" :class="{ active: activeTab === 'rule' }">
<div class="form-hint" style="margin-bottom: 12px; color: #606266; font-size: 13px;">
字段列表为所选数据表的全部字段;点击对应字段右侧"规则设置"可查看/配置该字段的校验规则;勾选"默认展示"则该字段在校验结果中默认展示。
<div class="form-hint" style="margin-bottom: 12px; color: #606266; font-size: 13px; display: flex; align-items: center; justify-content: space-between; gap: 12px;">
<span style="flex: 1;">字段列表为所选数据表的全部字段;点击对应字段右侧"规则设置"可查看/配置该字段的校验规则;勾选"默认展示"则该字段在校验结果中默认展示。</span>
<!-- 编辑场景:用现有 conn_json 重新拉一次 DB 列元数据,覆盖 cn/type;DB 中途变更后手动刷新 -->
<button
v-if="props.task && form.table && fieldList.length > 0"
type="button"
class="btn"
style="padding: 3px 10px; font-size: 12px;"
:disabled="refreshingColumns"
:title="refreshingColumns ? '正在同步...' : '数据库结构可能已变更,点此从数据源重新拉取字段注释/类型(不会动已配置的规则)'"
@click="refreshColumnsForEdit"
>{{ refreshingColumns ? '同步中…' : '↻ 重新同步字段' }}</button>
</div>
<div v-if="loadingColumns" class="form-hint" style="margin-bottom: 12px;">正在加载字段列表...</div>
<div v-else-if="!form.table" class="form-hint" style="margin-bottom: 12px;">请先在「数据连接」Tab 选择数据表。</div>
......@@ -298,6 +308,10 @@ const tableOptions = ref([])
const loadingSchema = ref(false)
const loadingTable = ref(false)
const loadingColumns = ref(false)
// 编辑场景「重新同步字段」按钮专用:与 loadColumns 的 loadingColumns 解耦——
// loadColumns 会把整张表藏掉(listColumns → 重建 fieldList)
// refreshColumnsForEdit 只更新 cn/type,表格保留可见,按钮显示「同步中…」
const refreshingColumns = ref(false)
const form = reactive({
name: '',
......@@ -473,6 +487,59 @@ watch(() => form.schema, (newVal, oldVal) => {
}
})
// ── 编辑场景专用:用现有 conn_json 重新从 DB 拉列元数据(cn / type)补回字段表 ──────
// 设计依据:cn / type 故意不持久化(plan §4 + 用户原话),编辑时按 source_table 重拉;
// 但用户每次都得手动点「测试连接」体验差,本函数把「打开弹窗」就当成一次隐式刷新。
// 行为约定:
// - 不动 stored rules(按 field_key 索引合并),只覆盖 cn / type / isNullable
// - DB 中已不存在的字段 → 保留 stored 状态(cn='' type=''),顶部 warning
// - DB 中新出现的字段(没有 stored rules)→ 不主动加,避免污染用户视角;
// 真要加新字段,用户改 form.table 走 loadColumns 那条路会自然带上
// - API 失败(密码空 / DB 不通 / 表被删)→ 静默退化(用户没主动触发,自动同步失败不该打扰)
// - 与 loadColumns 解耦:loadColumns 是「重建 fieldList(rules 备份后回填)」,
// 本函数是「in-place 覆盖 cn/type」,共存无冲突
async function refreshColumnsForEdit() {
if (!props.task?.source_table) return // 新建任务 / 旧任务没存表名 → 跳过
if (!form.host || !form.port || !form.db || !form.user) {
// 连接信息缺失(旧任务没存 conn)—— 静默放弃,让 cn='' 显示
return
}
refreshingColumns.value = true
try {
const r = await listColumns(form, props.task.source_table)
if (!r.ok || !Array.isArray(r.columns) || r.columns.length === 0) {
// 表已删 / DB 不通 / 列表为空 → 静默(不影响旧数据展示)
return
}
// 按 column_name 索引,方便 O(1) 查找 merge
const colByKey = new Map(r.columns.map((c) => [c.column_name, c]))
let missingCount = 0
fieldList.value = fieldList.value.map((f) => {
const c = colByKey.get(f.key)
if (!c) {
missingCount++
return f // stored 字段已不在 DB → 原样保留
}
return {
...f,
cn: c.column_comment || '', // 字段注释(无 → 空串)
type: c.column_type || c.data_type || '', // 数据类型显示(VARCHAR(50) 等)
isNullable: c.is_nullable, // Y/N(后续 P4 必填校验可能用)
}
})
if (missingCount > 0) {
ElMessage.warning(
`有 ${missingCount} 个字段在数据库「${props.task.source_table}」中已不存在,请检查(字段注释/类型暂留空)`
)
}
} catch (e) {
// 静默:不弹错误(自动同步失败不该打扰;用户可手动去「数据连接」Tab 测试)
console.warn('[TaskModal] 自动同步字段元数据失败:', e)
} finally {
refreshingColumns.value = false
}
}
// 表变了 → 拉该表的字段(数据字典),刷新校验规则 Tab
watch(() => form.table, (newVal, oldVal) => {
if (newVal !== oldVal) {
......@@ -514,7 +581,12 @@ function _fillFieldListFromTask() {
}))
}
watch(() => props.task?.field_list, () => {
if (props.open) _fillFieldListFromTask()
if (!props.open) return
_fillFieldListFromTask()
// fieldList 回填后才做 cn/type 同步:父组件 openEdit 是「同步 set row + 异步 getTask」
// 两步,open watch 同步阶段 field_list 还是 [],那时 _fillFieldListFromTask 是 no-op,
// 在那里调 refreshColumnsForEdit 也是 no-op。挂到 field_list watch 上才能保证顺序对。
refreshColumnsForEdit()
})
watch(() => props.open, async (v) => {
......@@ -526,8 +598,13 @@ watch(() => props.open, async (v) => {
schemaOptions.value = []
tableOptions.value = []
selectedPresetId.value = null
// 加载当前 dbType 对应的预设列表
await loadPresets(form.dbType)
// 关键:用**正确的** dbType 加载预设列表
// 编辑场景 form.dbType 默认是 'MySQL',但 task.db_type 可能是 'Oracle' / '达梦 DM'
// 旧版直接 await loadPresets(form.dbType) 会拿到 MySQL 预设,
// 后面 form.dbType 改值才触发 watch(form.dbType) 重载,但那是异步的,
// 同步阶段的 preset 匹配用的是错的 dbType 的预设 → 永远匹配不上
const targetDbType = props.task?.db_type || form.dbType
await loadPresets(targetDbType)
if (props.task) {
// 编辑场景:从 task 还原全部字段(含数据库连接)
form.name = props.task.name || ''
......@@ -539,7 +616,9 @@ watch(() => props.open, async (v) => {
form.port = props.task.port || ''
form.db = props.task.db || ''
form.user = props.task.user || ''
form.password = '' // 密码不回填(安全),但下面会自动从匹配的预设里补回来
// 密码:优先用 task 自带的(创建时存的;2026-08-21 起详情接口返回);
// 下面有匹配的预设时会被预设的密码覆盖(用户主动切预设的场景)
form.password = props.task.password || ''
form.oracleClientDir = props.task.oracle_client_dir || '' // 编辑场景:从 task 回填,保留连库所需信息
form.schema = props.task.schema || ''
// 数据表:后端存的是「t_user_info」纯名,前端直接展示
......@@ -563,6 +642,9 @@ watch(() => props.open, async (v) => {
form.password = matched.password || ''
selectedPresetId.value = matched.id
}
// 注:cn/type 自动同步放在 props.task.field_list watch 里调,
// 因为父组件 openEdit 是「同步 set row + 异步 getTask」两步,
// 这里同步阶段 field_list 还是空,refreshColumnsForEdit 是 no-op。
} else {
// 新增场景:默认值(不写死假数据)
form.name = ''
......
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