Commit 11d7089d authored by Data Governance Dev's avatar Data Governance Dev

feat(web3): 数据库连接 Tab 缩减 + 弹窗 form-grid 修复

- 任务弹窗「数据连接」Tab 数据库选项 6→3:保留 MySQL / Oracle / 达梦 DM,移除 SQL Server / PostgreSQL / JDBC 自定义
- global.css .modal 加宽 880→1000;.form-grid 列最小宽度 320px,右半边输入框不再被裁切
- docs/db-schema.md task 表 db_type 列注释同步
parent e2c16e6e
......@@ -87,7 +87,7 @@ task_group (1) ──< (N) task (1) ──< (N) field (1) ──< (N) rule
| `source_table` | TEXT | NULL | 该 sqlite 文件里的表名 |
| `status` | TEXT | NOT NULL DEFAULT `'启用'` | 启用 / 停用;**P3 阶段 UI 不暴露**,仅 DB 列保留(未来扩展用)|
| `description` | TEXT | NULL | 任务说明 |
| `db_type` | TEXT | NOT NULL DEFAULT `'MySQL'` | UI 冗余:MySQL / Oracle / SQL Server / PostgreSQL / 达梦 DM / JDBC 自定义 |
| `db_type` | TEXT | NOT NULL DEFAULT `'MySQL'` | UI 冗余:MySQL / Oracle / 达梦 DM(2026-08-20 起暂只支持这三种,连接实现参考 web2)|
| `conn_json` | TEXT | NULL | UI 数据连接表单 JSON(详见下表)|
| `created_at` | TIMESTAMP | NOT NULL DEFAULT CURRENT_TIMESTAMP | |
| `updated_at` | TIMESTAMP | NOT NULL DEFAULT CURRENT_TIMESTAMP,onupdate 自动 | |
......
......@@ -47,6 +47,31 @@
<!-- Tab2 数据连接 -->
<div class="tab-panel" :class="{ active: activeTab === 'conn' }">
<div class="form-section">
<div class="form-section-title">历史连接</div>
<div class="form-grid">
<div class="form-row full">
<span class="form-label">选择历史连接</span>
<div class="form-control">
<select
class="select-input"
v-model="selectedPresetId"
:disabled="loadingPresets"
@change="applyPreset(selectedPresetId)"
>
<option :value="null" disabled>
{{ loadingPresets ? '正在加载历史连接...' : (presetOptions.length === 0 ? '暂无历史连接(直接填写下方连接信息即可)' : '请选择历史连接(可选,或下方手动填写)') }}
</option>
<option v-for="p in presetOptions" :key="p.id" :value="p.id">
{{ p.display }}({{ p.db_type }} · {{ p.host }}:{{ p.port }}/{{ p.database }})
</option>
</select>
<div class="form-hint">选完预设,下方连接字段会自动填充(含密码);如未匹配到当前连接方式,可直接跳过。</div>
</div>
</div>
</div>
</div>
<div class="form-section">
<div class="form-section-title">连接方式</div>
<div class="conn-tabs">
......@@ -93,8 +118,11 @@
<div class="form-row full">
<span class="form-label"></span>
<div class="form-control" style="display: flex; gap: 8px; align-items: center;">
<button class="btn" @click="testConn">测试连接</button>
<span v-if="connResult" class="form-hint" style="color: #67c23a;">✓ {{ connResult }}</span>
<button class="btn" :disabled="testing" @click="testConn">
{{ testing ? '测试中...' : '测试连接' }}
</button>
<span v-if="connResult.ok === true" class="form-hint" style="color: #67c23a;">✓ {{ connResult.message }}</span>
<span v-else-if="connResult.ok === false" class="form-hint" style="color: #f56c6c;">✗ {{ connResult.message }}</span>
</div>
</div>
</div>
......@@ -106,18 +134,22 @@
<div class="form-row">
<span class="form-label"><span class="required">*</span>Schema</span>
<div class="form-control">
<select class="select-input" v-model="form.schema">
<select class="select-input" v-model="form.schema" :disabled="schemaOptions.length === 0">
<option v-if="schemaOptions.length === 0" value="" disabled>请先测试连接</option>
<option v-for="s in schemaOptions" :key="s" :value="s">{{ s }}</option>
</select>
<div v-if="loadingSchema" class="form-hint">正在加载 schema 列表...</div>
</div>
</div>
<div class="form-row">
<span class="form-label"><span class="required">*</span>选择数据表</span>
<div class="form-control">
<select class="select-input" v-model="form.table">
<option v-for="t in tableOptions" :key="t" :value="t">{{ t }}</option>
<select class="select-input" v-model="form.table" :disabled="tableOptions.length === 0">
<option v-if="tableOptions.length === 0" value="" disabled>{{ form.schema ? '请选择数据表' : '请先选择 schema' }}</option>
<option v-for="t in tableOptions" :key="t.table_name" :value="t.table_name">{{ t.table_name }}{{ t.table_comment ? `(${t.table_comment})` : '' }}</option>
</select>
<div class="form-hint">选择数据表后自动加载该表全部字段</div>
<div v-if="loadingTable" class="form-hint">正在加载表列表...</div>
<div v-else class="form-hint">选择数据表后自动加载该表全部字段</div>
</div>
</div>
</div>
......@@ -179,6 +211,8 @@ import { computed, inject, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { fieldsByType, fieldRuleSamples } from '@/data/mockData'
import RuleModal from '@/components/RuleModal.vue'
import { testConnection, listSchemas, listTables } from '@/api/connections'
import { listConnectionPresets, presetToForm } from '@/api/connectionPresets'
// 从 MainLayout 拿分组码表
const taskGroups = inject('taskGroups', ref([]))
......@@ -200,31 +234,170 @@ const tabs = [
const activeTab = ref('basic')
const groupOptions = computed(() => taskGroups.value.map((g) => g.name))
const dbTypes = ['MySQL', 'Oracle', 'SQL Server', 'PostgreSQL', '达梦 DM', 'JDBC 自定义']
const schemaOptions = ['public', 'customer', 'sys']
const tableOptions = ['t_user_info(用户信息表)', 't_customer(客户主表)', 't_employee(员工档案表)']
const dbTypes = ['MySQL', 'Oracle', '达梦 DM']
// 历史连接预设(下拉选择器)
const presetOptions = ref([]) // 全部预设列表(按 db_type/ord 排序)
const selectedPresetId = ref(null) // 当前选中的预设 id(null=未选/自定义)
const loadingPresets = ref(false)
async function loadPresets(dbTypeLabel) {
loadingPresets.value = true
try {
const list = await listConnectionPresets(dbTypeLabel)
presetOptions.value = list || []
} catch (e) {
ElMessage.warning(`加载历史连接失败:${e.message}`)
presetOptions.value = []
} finally {
loadingPresets.value = false
}
}
// 把选中的预设回填到 form(仅连接字段;schema/表 需要重新测试连接)
function applyPreset(presetId) {
const p = presetOptions.value.find((x) => x.id === presetId)
if (!p) return
const fields = presetToForm(p)
Object.assign(form, fields)
// 切换后清掉旧的 schema/表 + 测试结果(不同环境/不同账号时可能连不上)
schemaOptions.value = []
tableOptions.value = []
form.schema = ''
form.table = ''
connResult.value = { ok: null, message: '' }
ElMessage.success(`已载入「${p.display}」的连接信息`)
}
// schema / 表:测试连接成功后从后端拉,不再写死 mock
const schemaOptions = ref([])
const tableOptions = ref([])
const loadingSchema = ref(false)
const loadingTable = ref(false)
const form = reactive({
name: '身份证_用户信息表',
name: '',
group: '身份证',
desc: '对用户信息表中的身份证号、性别、出生日期进行格式与一致性校验',
desc: '',
dbType: 'MySQL',
connName: 'customer_db_prod',
host: '192.168.101.126',
port: '3306',
db: 'customer_db',
user: 'data_check',
password: '******',
jdbcParams: 'useUnicode=true&characterEncoding=utf8&useSSL=false',
schema: 'public',
table: 't_user_info(用户信息表)',
connName: '',
host: '',
port: '',
db: '',
user: '',
password: '',
jdbcParams: '',
schema: '',
table: '',
})
const connResult = ref('')
function testConn() {
connResult.value = '连接成功(耗时 86ms)'
// 测试连接:调用真实后端
// connResult.ok: null = 未测试 / true = 成功 / false = 失败
const testing = ref(false)
const connResult = ref({ ok: null, message: '' })
async function testConn() {
// 前端必填校验
if (!form.host || !form.port || !form.db || !form.user) {
ElMessage.warning('请填写完整的连接信息(主机/端口/数据库名/用户名)')
return
}
testing.value = true
connResult.value = { ok: null, message: '' }
try {
const r = await testConnection(form)
connResult.value = { ok: r.ok, message: r.message }
if (r.ok) {
ElMessage.success(r.message)
// 成功 → 自动拉 schema → 表
await loadSchemas()
} else {
ElMessage.error(r.message)
schemaOptions.value = []
tableOptions.value = []
}
} catch (e) {
connResult.value = { ok: false, message: e.message }
ElMessage.error(`测试失败:${e.message}`)
schemaOptions.value = []
tableOptions.value = []
} finally {
testing.value = false
}
}
async function loadSchemas() {
loadingSchema.value = true
schemaOptions.value = []
tableOptions.value = []
form.schema = ''
form.table = ''
try {
const r = await listSchemas(form)
if (r.ok) {
schemaOptions.value = r.schemas || []
if (schemaOptions.value.length > 0) {
// 默认选第一个;如果之前 form.schema 还在新列表里,保留
if (!schemaOptions.value.includes(form.schema)) {
form.schema = schemaOptions.value[0]
}
await loadTables()
} else {
ElMessage.warning('该连接下未找到任何 schema')
}
} else {
ElMessage.warning(`加载 schema 失败:${r.message || '未知错误'}`)
}
} catch (e) {
ElMessage.warning(`加载 schema 失败:${e.message}`)
} finally {
loadingSchema.value = false
}
}
async function loadTables() {
if (!form.schema) {
tableOptions.value = []
return
}
loadingTable.value = true
tableOptions.value = []
form.table = ''
try {
const r = await listTables(form)
if (r.ok) {
tableOptions.value = r.tables || []
if (tableOptions.value.length === 0) {
ElMessage.warning(`schema「${form.schema}」下未找到任何表`)
}
} else {
ElMessage.warning(`加载表列表失败:${r.message || '未知错误'}`)
}
} catch (e) {
ElMessage.warning(`加载表列表失败:${e.message}`)
} finally {
loadingTable.value = false
}
}
// schema 变了 → 重拉表
watch(() => form.schema, (newVal, oldVal) => {
if (newVal && newVal !== oldVal) {
loadTables()
}
})
// dbType 变了 → 清掉 schema/表(不同方言的 schema 概念不同);同时重拉预设列表
watch(() => form.dbType, async (newVal) => {
schemaOptions.value = []
tableOptions.value = []
form.schema = ''
form.table = ''
connResult.value = { ok: null, message: '' }
selectedPresetId.value = null // 切 dbType 后清掉选中(当前 dbType 的预设列表会刷新)
await loadPresets(newVal)
})
// 字段列表:根据分组反推 type,选对应 mock 字段
const fieldList = ref([])
function rebuildFieldList() {
......@@ -254,10 +427,16 @@ function rebuildFieldList() {
})
}
watch(() => props.open, (v) => {
watch(() => props.open, async (v) => {
if (v) {
activeTab.value = 'basic'
connResult.value = ''
testing.value = false
connResult.value = { ok: null, message: '' }
schemaOptions.value = []
tableOptions.value = []
selectedPresetId.value = null
// 加载当前 dbType 对应的预设列表
await loadPresets(form.dbType)
if (props.task) {
// 编辑场景:从 task 还原全部字段(含数据库连接)
// 注意:密码不回填到前端(安全考虑),保留空让用户重输
......@@ -272,24 +451,25 @@ watch(() => props.open, (v) => {
form.user = props.task.user || ''
form.password = '' // 密码不回填,重新输入
form.jdbcParams = props.task.jdbc_params || ''
form.schema = props.task.schema || 'public'
form.schema = props.task.schema || ''
// 数据表:后端存的是「t_user_info」纯名,前端直接展示
form.table = props.task.source_table || ''
// 注:编辑场景不自动拉 schema/表 —— 等用户点「测试连接」再拉
} else {
// 新增场景:恢复默认值
form.name = '身份证_用户信息表'
// 新增场景:默认值(不写死假数据)
form.name = ''
form.group = '身份证'
form.desc = '对用户信息表中的身份证号、性别、出生日期进行格式与一致性校验'
form.desc = ''
form.dbType = 'MySQL'
form.connName = 'customer_db_prod'
form.host = '192.168.101.126'
form.port = '3306'
form.db = 'customer_db'
form.user = 'data_check'
form.password = '******'
form.jdbcParams = 'useUnicode=true&characterEncoding=utf8&useSSL=false'
form.schema = 'public'
form.table = 't_user_info'
form.connName = ''
form.host = ''
form.port = ''
form.db = ''
form.user = ''
form.password = ''
form.jdbcParams = ''
form.schema = ''
form.table = ''
}
rebuildFieldList()
}
......@@ -329,7 +509,7 @@ function onSave() {
group: form.group,
db_type: form.dbType,
data_source_path: null, // P5 接入 data-sources 后再选本地 sqlite
source_table: form.table ? form.table.split('(')[0] : null,
source_table: form.table || null,
description: form.desc,
conn_json: connJson,
}
......
......@@ -309,7 +309,7 @@ table.dq-table tbody tr:last-child td { border-bottom: none; }
.modal {
background: #fff;
border-radius: 6px;
width: 880px;
width: 1000px;
max-width: 94vw;
max-height: 90vh;
display: flex;
......@@ -372,7 +372,7 @@ table.dq-table tbody tr:last-child td { border-bottom: none; }
border-left: 3px solid #409eff;
margin-bottom: 12px;
}
.form-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px 20px; }
.form-grid { display: grid; grid-template-columns: minmax(320px, 1fr) minmax(320px, 1fr); gap: 14px 20px; }
.form-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
.form-row.full { grid-column: 1 / -1; }
.form-label {
......
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