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

feat(web2): DbConnectionPopover 多连接列表 + IdCardTool 适配

- DbConnectionPopover.vue
  - 触发器显示:「display · host:port」
  - 弹框按 db_type 分组(MySQL / 达梦 / Oracle),每组下显示该 type 的所有连接
  - 每行:连接名 + host:port + 测试/切换按钮
  - 切换按钮独立判断 isCurrent(db_type, id),不再按 db_type 切换
  - 弹框宽度 280 → 320(容纳 host:port 信息)
- IdCardTool.vue
  - state.currentType 引用全部替换为 currentConnection?.host
  - db_type 从 conn.db_type 取(store 已注入),不再用 state.currentType
parent 934865dd
<!--
数据库连接选择器(Header 右上角 popover)
2026-08-17 起:每个 db_type 下可配多套连接,UI 列表化展示 + 独立切换。
- 同一 db_type(如 mysql)可能有多个连接(生产 / 备份 / 测试...)
- 当前激活的是具体某一个连接(state.currentConnection = {db_type, id})
- 弹框分组:按 db_type 分组,每组下面列出该 type 的所有连接
触发器显示:当前连接的 display + host:port
弹框列表:每个连接一行,点击「切换」按钮切到该连接
-->
<template> <template>
<el-popover <el-popover
:width="280" :width="320"
placement="bottom-end" placement="bottom-end"
trigger="click" trigger="click"
> >
...@@ -15,67 +26,89 @@ ...@@ -15,67 +26,89 @@
> >
<el-icon><Connection /></el-icon> <el-icon><Connection /></el-icon>
<span class="db-trigger__label"> <span class="db-trigger__label">
{{ currentLabel }} · {{ currentConn.host }}:{{ currentConn.port }} {{ triggerLabel }}
</span> </span>
<el-icon class="db-trigger__arrow"><ArrowDown /></el-icon> <el-icon class="db-trigger__arrow"><ArrowDown /></el-icon>
</el-button> </el-button>
</template> </template>
<!-- 弹出内容:极简列表 --> <!-- 弹出内容:分组列表 -->
<div class="db-popover"> <div class="db-popover">
<div class="db-popover__title"> <div class="db-popover__title">
<el-icon><Connection /></el-icon> <el-icon><Connection /></el-icon>
<span>数据库连接</span> <span>数据库连接</span>
</div> </div>
<ul class="db-popover__list"> <div v-if="!hasAnyConnection" class="db-popover__empty">
<li 暂无可用连接,请检查 web2/configs/db_defaults.yaml
v-for="opt in dbTypes" </div>
:key="opt.value"
:class="['db-popover__item', { 'is-active': opt.value === state.currentType }]" <!-- 按 db_type 分组,每组下面是该 type 的所有连接 -->
> <div
<div class="db-popover__brand"> v-for="opt in dbTypes"
<img :key="opt.value"
v-if="brandOf(opt.value).url && !logoFailed[opt.value]" class="db-popover__group"
:src="brandOf(opt.value).url" >
:alt="opt.label" <div class="db-popover__group-head">
class="db-popover__logo" <img
@error="onLogoError(opt.value)" v-if="brandOf(opt.value).url && !logoFailed[opt.value]"
/> :src="brandOf(opt.value).url"
<span :alt="opt.label"
v-else class="db-popover__logo"
class="db-popover__logo db-popover__logo--fallback" @error="onLogoError(opt.value)"
:style="{ background: brandOf(opt.value).color }" />
> <span
<el-icon><component :is="brandOf(opt.value).fallbackIcon" /></el-icon> v-else
</span> class="db-popover__logo db-popover__logo--fallback"
<span class="db-popover__name"> :style="{ background: brandOf(opt.value).color }"
{{ presets[opt.value]?.display || presets[opt.value]?.database || opt.label }} >
</span> <el-icon><component :is="brandOf(opt.value).fallbackIcon" /></el-icon>
</div> </span>
<div class="db-popover__actions"> <span class="db-popover__group-name">{{ opt.label }}</span>
<el-button <span class="db-popover__group-count">{{ (presets[opt.value] || []).length }}</span>
link </div>
size="small"
:loading="testing === opt.value" <ul class="db-popover__list">
:title="`测试 ${opt.label}`" <li
@click="onTest(opt.value)" v-for="conn in presets[opt.value] || []"
> :key="conn.id"
<el-icon><VideoPlay /></el-icon> :class="['db-popover__item', { 'is-active': isCurrent(opt.value, conn.id) }]"
</el-button> >
<el-button <div class="db-popover__conn">
type="primary" <div class="db-popover__conn-name">
link {{ conn.display || conn.database || conn.id }}
size="small" </div>
:disabled="opt.value === state.currentType" <div class="db-popover__conn-host">
:title="`切换到 ${opt.label}`" {{ conn.host }}:{{ conn.port }}
@click="onSwitch(opt.value)" </div>
> </div>
<el-icon><Position /></el-icon> <div class="db-popover__actions">
</el-button> <el-button
</div> link
</li> size="small"
</ul> :loading="testing === `${opt.value}::${conn.id}`"
:title="`测试 ${conn.display || conn.id}`"
@click="onTest(opt.value, conn.id)"
>
<el-icon><VideoPlay /></el-icon>
</el-button>
<el-button
type="primary"
link
size="small"
:disabled="isCurrent(opt.value, conn.id)"
:title="`切换到 ${conn.display || conn.id}`"
@click="onSwitch(opt.value, conn.id)"
>
<el-icon><Position /></el-icon>
</el-button>
</div>
</li>
<li v-if="!(presets[opt.value] || []).length" class="db-popover__none">
该类型无连接
</li>
</ul>
</div>
</div> </div>
</el-popover> </el-popover>
</template> </template>
...@@ -92,46 +125,57 @@ const { ...@@ -92,46 +125,57 @@ const {
presets, presets,
loading, loading,
currentConnection, currentConnection,
setDbType, setCurrentConnection,
} = useDbConnection() } = useDbConnection()
const dbTypes = DB_TYPES const dbTypes = DB_TYPES
/** 当前正在测试的 db_type(null = 没在测试) */ /** 当前正在测试的连接 key("db_type::id" 格式);null = 没在测试 */
const testing = ref(null) const testing = ref(null)
/** 哪些 db_type 的 logo 加载失败过 → 切到 fallback icon */ /** 哪些 db_type 的 logo 加载失败过 → 切到 fallback icon */
const logoFailed = reactive({}) const logoFailed = reactive({})
const currentConn = computed(() => currentConnection.value || {}) const currentConn = computed(() => currentConnection.value || {})
const currentLabel = computed( /** 触发器显示:「display · host:port」 */
() => dbTypes.find((d) => d.value === state.currentType)?.label || state.currentType, const triggerLabel = computed(() => {
const c = currentConn.value
if (!c.host) return '未选择连接'
const name = c.display || c.database || c.id
return `${name} · ${c.host}:${c.port}`
})
const connected = computed(() => !!currentConn.value?.host)
const hasAnyConnection = computed(() =>
Object.values(presets.value || {}).some((v) => Array.isArray(v) && v.length > 0),
) )
const connected = computed(() => !!state.currentType)
/** 是否是当前激活连接 */
function isCurrent(dbType, id) {
const cur = state.currentConnection
return !!cur && cur.db_type === dbType && cur.id === id
}
function brandOf(dbType) { function brandOf(dbType) {
return DB_LOGOS[dbType] || { url: null, color: '#909399', fallbackIcon: null } return DB_LOGOS[dbType] || { url: null, color: '#909399', fallbackIcon: null }
} }
function onLogoError(dbType) { function onLogoError(dbType) {
// 网络问题 / CDN 失效 → 切到 Element Plus icon 兜底,不再尝试
logoFailed[dbType] = true logoFailed[dbType] = true
} }
/** /**
* 测试指定 db_type 的连接 * 测试指定连接
*/ */
async function onTest(dbType) { async function onTest(dbType, id) {
const conn = presets.value[dbType] const conn = (presets.value[dbType] || []).find((c) => c.id === id)
if (!conn) { if (!conn) {
ElMessage.warning(`${dbType} 预设不存在`) ElMessage.warning(`连接不存在:${dbType}::${id}`)
return return
} }
testing.value = dbType testing.value = `${dbType}::${id}`
try { try {
const r = await fetch('/api/connect/test', { const r = await fetch('/api/connect/test', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
// db_type 用参数(永远有值),不从 conn 取(API YAML 没这字段)
db_type: dbType, db_type: dbType,
host: conn.host, host: conn.host,
port: conn.port, port: conn.port,
...@@ -152,7 +196,7 @@ async function onTest(dbType) { ...@@ -152,7 +196,7 @@ async function onTest(dbType) {
: (detail.detail || JSON.stringify(detail)) : (detail.detail || JSON.stringify(detail))
ElMessageBox.alert( ElMessageBox.alert(
`HTTP ${r.status}\n\n${detailMsg}`, `HTTP ${r.status}\n\n${detailMsg}`,
`❌ 测试失败(${dbType})`, `❌ 测试失败(${conn.display || conn.id})`,
{ confirmButtonText: '好的', type: 'error' }, { confirmButtonText: '好的', type: 'error' },
) )
return return
...@@ -162,20 +206,20 @@ async function onTest(dbType) { ...@@ -162,20 +206,20 @@ async function onTest(dbType) {
if (data.ok) { if (data.ok) {
ElMessageBox.alert( ElMessageBox.alert(
data.message || '连接成功', data.message || '连接成功',
`✅ ${dbType} 测试通过`, `✅ ${conn.display || conn.id} 测试通过`,
{ confirmButtonText: '好的', type: 'success' }, { confirmButtonText: '好的', type: 'success' },
) )
} else { } else {
ElMessageBox.alert( ElMessageBox.alert(
data.message || '后端返回 ok=false 但无 message', data.message || '后端返回 ok=false 但无 message',
`❌ ${dbType} 测试失败`, `❌ ${conn.display || conn.id} 测试失败`,
{ confirmButtonText: '好的', type: 'error' }, { confirmButtonText: '好的', type: 'error' },
) )
} }
} catch (e) { } catch (e) {
ElMessageBox.alert( ElMessageBox.alert(
`请求失败: ${e.message}`, `请求失败: ${e.message}`,
`❌ ${dbType} 网络错误`, `❌ ${conn.display || conn.id} 网络错误`,
{ confirmButtonText: '好的', type: 'error' }, { confirmButtonText: '好的', type: 'error' },
) )
} finally { } finally {
...@@ -183,11 +227,16 @@ async function onTest(dbType) { ...@@ -183,11 +227,16 @@ async function onTest(dbType) {
} }
} }
/** 切换当前连接到指定 db_type */ /** 切换当前连接到指定 {db_type, id} */
function onSwitch(dbType) { function onSwitch(dbType, id) {
if (dbType === state.currentType) return if (isCurrent(dbType, id)) return
setDbType(dbType) const ok = setCurrentConnection(dbType, id)
const label = dbTypes.find((d) => d.value === dbType)?.label || dbType if (!ok) {
ElMessage.warning(`连接不存在:${dbType}::${id}`)
return
}
const conn = (presets.value[dbType] || []).find((c) => c.id === id)
const label = conn?.display || conn?.id || `${dbType}::${id}`
ElMessage.success(`已切换到 ${label}`) ElMessage.success(`已切换到 ${label}`)
document.activeElement?.blur?.() document.activeElement?.blur?.()
} }
...@@ -225,10 +274,48 @@ function onSwitch(dbType) { ...@@ -225,10 +274,48 @@ function onSwitch(dbType) {
color: #409eff; color: #409eff;
} }
.db-popover__empty {
padding: 16px;
text-align: center;
color: #909399;
font-size: 13px;
}
/* 分组:每个 db_type 一组 */
.db-popover__group {
margin-bottom: 10px;
}
.db-popover__group:last-child {
margin-bottom: 0;
}
.db-popover__group-head {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 4px 4px 0;
font-size: 12px;
color: #909399;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.db-popover__group-name {
flex: 1;
}
.db-popover__group-count {
background: #f0f2f5;
color: #606266;
padding: 0 6px;
border-radius: 8px;
font-size: 11px;
font-weight: 500;
line-height: 16px;
}
/* 列表 */ /* 列表 */
.db-popover__list { .db-popover__list {
list-style: none; list-style: none;
margin: 0; margin: 4px 0 0;
padding: 0; padding: 0;
} }
.db-popover__item { .db-popover__item {
...@@ -239,6 +326,7 @@ function onSwitch(dbType) { ...@@ -239,6 +326,7 @@ function onSwitch(dbType) {
border-radius: 6px; border-radius: 6px;
transition: background 0.15s; transition: background 0.15s;
cursor: default; cursor: default;
gap: 6px;
} }
.db-popover__item:hover { .db-popover__item:hover {
background: #f5f7fa; background: #f5f7fa;
...@@ -246,19 +334,19 @@ function onSwitch(dbType) { ...@@ -246,19 +334,19 @@ function onSwitch(dbType) {
.db-popover__item.is-active { .db-popover__item.is-active {
background: #ecf5ff; background: #ecf5ff;
} }
.db-popover__item.is-active .db-popover__name { .db-popover__item.is-active .db-popover__conn-name {
color: #409eff; color: #409eff;
font-weight: 600; font-weight: 600;
} }
/* logo + 数据库名(一行) */ .db-popover__none {
.db-popover__brand { padding: 4px 10px;
display: flex; color: #c0c4cc;
align-items: center; font-size: 12px;
gap: 8px; font-style: italic;
flex: 1;
min-width: 0;
} }
/* logo */
.db-popover__logo { .db-popover__logo {
width: 20px; width: 20px;
height: 20px; height: 20px;
...@@ -271,17 +359,31 @@ function onSwitch(dbType) { ...@@ -271,17 +359,31 @@ function onSwitch(dbType) {
justify-content: center; justify-content: center;
border-radius: 4px; border-radius: 4px;
color: #fff; color: #fff;
font-size: 12px; font-size: 11px;
} }
.db-popover__logo--fallback .el-icon { .db-popover__logo--fallback .el-icon {
font-size: 14px; font-size: 14px;
} }
.db-popover__name {
/* 连接信息:两行(名字 + host:port) */
.db-popover__conn {
flex: 1;
min-width: 0;
}
.db-popover__conn-name {
font-size: 14px; font-size: 14px;
color: #303133; color: #303133;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
line-height: 1.3;
}
.db-popover__conn-host {
font-size: 11px;
color: #909399;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
line-height: 1.3;
margin-top: 2px;
} }
/* 操作按钮 */ /* 操作按钮 */
...@@ -293,4 +395,4 @@ function onSwitch(dbType) { ...@@ -293,4 +395,4 @@ function onSwitch(dbType) {
.db-popover__actions .el-icon { .db-popover__actions .el-icon {
font-size: 16px; font-size: 16px;
} }
</style> </style>
\ No newline at end of file
...@@ -70,14 +70,14 @@ ...@@ -70,14 +70,14 @@
type="primary" type="primary"
:icon="Search" :icon="Search"
:loading="querying" :loading="querying"
:disabled="!state.currentType" :disabled="!currentConnection?.host"
@click="onQuery" @click="onQuery"
> >
查询 查询
</el-button> </el-button>
<el-button text @click="onReset">重置</el-button> <el-button text @click="onReset">重置</el-button>
</el-form-item> </el-form-item>
<el-form-item v-if="!state.currentType"> <el-form-item v-if="!currentConnection?.host">
<span class="idcard-page__hint"> <span class="idcard-page__hint">
<el-icon><Warning /></el-icon> <el-icon><Warning /></el-icon>
请先在右上角选择数据库连接 请先在右上角选择数据库连接
...@@ -368,7 +368,7 @@ async function onQuery() { ...@@ -368,7 +368,7 @@ async function onQuery() {
return return
} }
const conn = currentConnection.value const conn = currentConnection.value
if (!conn || !state.currentType) { if (!conn || !conn.host) {
ElMessage.warning('请先在右上角选择数据库连接') ElMessage.warning('请先在右上角选择数据库连接')
return return
} }
...@@ -392,7 +392,8 @@ async function onQuery() { ...@@ -392,7 +392,8 @@ async function onQuery() {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
db_type: state.currentType, // conn 里已含 db_type(store 从预设 / 后端注入),直接用
db_type: conn.db_type,
host: conn.host, host: conn.host,
port: conn.port, port: conn.port,
user: conn.user, user: conn.user,
......
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