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>
<el-popover
:width="280"
:width="320"
placement="bottom-end"
trigger="click"
>
......@@ -15,26 +26,30 @@
>
<el-icon><Connection /></el-icon>
<span class="db-trigger__label">
{{ currentLabel }} · {{ currentConn.host }}:{{ currentConn.port }}
{{ triggerLabel }}
</span>
<el-icon class="db-trigger__arrow"><ArrowDown /></el-icon>
</el-button>
</template>
<!-- 弹出内容:极简列表 -->
<!-- 弹出内容:分组列表 -->
<div class="db-popover">
<div class="db-popover__title">
<el-icon><Connection /></el-icon>
<span>数据库连接</span>
</div>
<ul class="db-popover__list">
<li
<div v-if="!hasAnyConnection" class="db-popover__empty">
暂无可用连接,请检查 web2/configs/db_defaults.yaml
</div>
<!-- 按 db_type 分组,每组下面是该 type 的所有连接 -->
<div
v-for="opt in dbTypes"
:key="opt.value"
:class="['db-popover__item', { 'is-active': opt.value === state.currentType }]"
class="db-popover__group"
>
<div class="db-popover__brand">
<div class="db-popover__group-head">
<img
v-if="brandOf(opt.value).url && !logoFailed[opt.value]"
:src="brandOf(opt.value).url"
......@@ -49,17 +64,31 @@
>
<el-icon><component :is="brandOf(opt.value).fallbackIcon" /></el-icon>
</span>
<span class="db-popover__name">
{{ presets[opt.value]?.display || presets[opt.value]?.database || opt.label }}
</span>
<span class="db-popover__group-name">{{ opt.label }}</span>
<span class="db-popover__group-count">{{ (presets[opt.value] || []).length }}</span>
</div>
<ul class="db-popover__list">
<li
v-for="conn in presets[opt.value] || []"
:key="conn.id"
:class="['db-popover__item', { 'is-active': isCurrent(opt.value, conn.id) }]"
>
<div class="db-popover__conn">
<div class="db-popover__conn-name">
{{ conn.display || conn.database || conn.id }}
</div>
<div class="db-popover__conn-host">
{{ conn.host }}:{{ conn.port }}
</div>
</div>
<div class="db-popover__actions">
<el-button
link
size="small"
:loading="testing === opt.value"
:title="`测试 ${opt.label}`"
@click="onTest(opt.value)"
:loading="testing === `${opt.value}::${conn.id}`"
:title="`测试 ${conn.display || conn.id}`"
@click="onTest(opt.value, conn.id)"
>
<el-icon><VideoPlay /></el-icon>
</el-button>
......@@ -67,16 +96,20 @@
type="primary"
link
size="small"
:disabled="opt.value === state.currentType"
:title="`切换到 ${opt.label}`"
@click="onSwitch(opt.value)"
: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>
</el-popover>
</template>
......@@ -92,46 +125,57 @@ const {
presets,
loading,
currentConnection,
setDbType,
setCurrentConnection,
} = useDbConnection()
const dbTypes = DB_TYPES
/** 当前正在测试的 db_type(null = 没在测试) */
/** 当前正在测试的连接 key("db_type::id" 格式);null = 没在测试 */
const testing = ref(null)
/** 哪些 db_type 的 logo 加载失败过 → 切到 fallback icon */
const logoFailed = reactive({})
const currentConn = computed(() => currentConnection.value || {})
const currentLabel = computed(
() => dbTypes.find((d) => d.value === state.currentType)?.label || state.currentType,
/** 触发器显示:「display · host:port」 */
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) {
return DB_LOGOS[dbType] || { url: null, color: '#909399', fallbackIcon: null }
}
function onLogoError(dbType) {
// 网络问题 / CDN 失效 → 切到 Element Plus icon 兜底,不再尝试
logoFailed[dbType] = true
}
/**
* 测试指定 db_type 的连接
* 测试指定连接
*/
async function onTest(dbType) {
const conn = presets.value[dbType]
async function onTest(dbType, id) {
const conn = (presets.value[dbType] || []).find((c) => c.id === id)
if (!conn) {
ElMessage.warning(`${dbType} 预设不存在`)
ElMessage.warning(`连接不存在:${dbType}::${id}`)
return
}
testing.value = dbType
testing.value = `${dbType}::${id}`
try {
const r = await fetch('/api/connect/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
// db_type 用参数(永远有值),不从 conn 取(API YAML 没这字段)
db_type: dbType,
host: conn.host,
port: conn.port,
......@@ -152,7 +196,7 @@ async function onTest(dbType) {
: (detail.detail || JSON.stringify(detail))
ElMessageBox.alert(
`HTTP ${r.status}\n\n${detailMsg}`,
`❌ 测试失败(${dbType})`,
`❌ 测试失败(${conn.display || conn.id})`,
{ confirmButtonText: '好的', type: 'error' },
)
return
......@@ -162,20 +206,20 @@ async function onTest(dbType) {
if (data.ok) {
ElMessageBox.alert(
data.message || '连接成功',
`✅ ${dbType} 测试通过`,
`✅ ${conn.display || conn.id} 测试通过`,
{ confirmButtonText: '好的', type: 'success' },
)
} else {
ElMessageBox.alert(
data.message || '后端返回 ok=false 但无 message',
`❌ ${dbType} 测试失败`,
`❌ ${conn.display || conn.id} 测试失败`,
{ confirmButtonText: '好的', type: 'error' },
)
}
} catch (e) {
ElMessageBox.alert(
`请求失败: ${e.message}`,
`❌ ${dbType} 网络错误`,
`❌ ${conn.display || conn.id} 网络错误`,
{ confirmButtonText: '好的', type: 'error' },
)
} finally {
......@@ -183,11 +227,16 @@ async function onTest(dbType) {
}
}
/** 切换当前连接到指定 db_type */
function onSwitch(dbType) {
if (dbType === state.currentType) return
setDbType(dbType)
const label = dbTypes.find((d) => d.value === dbType)?.label || dbType
/** 切换当前连接到指定 {db_type, id} */
function onSwitch(dbType, id) {
if (isCurrent(dbType, id)) return
const ok = setCurrentConnection(dbType, id)
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}`)
document.activeElement?.blur?.()
}
......@@ -225,10 +274,48 @@ function onSwitch(dbType) {
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 {
list-style: none;
margin: 0;
margin: 4px 0 0;
padding: 0;
}
.db-popover__item {
......@@ -239,6 +326,7 @@ function onSwitch(dbType) {
border-radius: 6px;
transition: background 0.15s;
cursor: default;
gap: 6px;
}
.db-popover__item:hover {
background: #f5f7fa;
......@@ -246,19 +334,19 @@ function onSwitch(dbType) {
.db-popover__item.is-active {
background: #ecf5ff;
}
.db-popover__item.is-active .db-popover__name {
.db-popover__item.is-active .db-popover__conn-name {
color: #409eff;
font-weight: 600;
}
/* logo + 数据库名(一行) */
.db-popover__brand {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
.db-popover__none {
padding: 4px 10px;
color: #c0c4cc;
font-size: 12px;
font-style: italic;
}
/* logo */
.db-popover__logo {
width: 20px;
height: 20px;
......@@ -271,17 +359,31 @@ function onSwitch(dbType) {
justify-content: center;
border-radius: 4px;
color: #fff;
font-size: 12px;
font-size: 11px;
}
.db-popover__logo--fallback .el-icon {
font-size: 14px;
}
.db-popover__name {
/* 连接信息:两行(名字 + host:port) */
.db-popover__conn {
flex: 1;
min-width: 0;
}
.db-popover__conn-name {
font-size: 14px;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
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;
}
/* 操作按钮 */
......
......@@ -70,14 +70,14 @@
type="primary"
:icon="Search"
:loading="querying"
:disabled="!state.currentType"
:disabled="!currentConnection?.host"
@click="onQuery"
>
查询
</el-button>
<el-button text @click="onReset">重置</el-button>
</el-form-item>
<el-form-item v-if="!state.currentType">
<el-form-item v-if="!currentConnection?.host">
<span class="idcard-page__hint">
<el-icon><Warning /></el-icon>
请先在右上角选择数据库连接
......@@ -368,7 +368,7 @@ async function onQuery() {
return
}
const conn = currentConnection.value
if (!conn || !state.currentType) {
if (!conn || !conn.host) {
ElMessage.warning('请先在右上角选择数据库连接')
return
}
......@@ -392,7 +392,8 @@ async function onQuery() {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
db_type: state.currentType,
// conn 里已含 db_type(store 从预设 / 后端注入),直接用
db_type: conn.db_type,
host: conn.host,
port: conn.port,
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