Commit 934865dd authored by Data Governance Dev's avatar Data Governance Dev

refactor(web2): store + 兜底配置改为列表结构 — 多连接支持

- dbPresets.js:DB_PRESETS 改为列表结构(每 db_type 是 Connection[])
- useDbConnection.js
  - state.currentType: string → state.currentConnection: {db_type, id}
  - presets[db_type] 从单 dict 改为 Connection[]
  - 新增 currentType computed(便捷暴露 db_type)
  - 新增 setCurrentConnection(db_type, id) 替代 setDbType(type)
  - localStorage 新 key web2.dbConnection.current,序列化用 "db_type::id"
  - API 拉回后校准:当前连接丢失则 fallback 到第一个连接
parent 562894a9
/** /**
* 数据库连接预设(前端本地兜底) * 数据库连接预设(前端本地兜底)
* *
* 来源:web/configs/db_defaults.yaml(同步结构 + display 字段) * 来源:web2/configs/db_defaults.yaml(与 web1 完全分离,2026-08-17 起)
* - MySQL / 达梦 (DM) / Oracle 三套配置 * - 列表结构:每个 db_type 是 Connection[],可配多套连接
* - 改这里 = 改 web/configs/db_defaults.yaml(保持两端结构一致) * - 改这里 = 改 web2/configs/db_defaults.yaml
* *
* ⚠️ 安全:本文件已入库,password 字段必须留空字符串。 * ⚠️ 安全:本文件已入库,password 字段必须留空字符串。
* 真实密码由运行时 GET /api/db-defaults 注入到 store.presets.password。 * 真实密码由运行时 GET /api/db-defaults 注入到 store.presets[*][i].password。
* 后端不通时 password 为空 → 测试连接会失败(这是预期行为,不是 bug)。 * 后端不通时 password 为空 → 测试连接会失败(这是预期行为,不是 bug)。
* *
* 字段说明: * 字段说明:
* id 必填,连接级唯一标识(同 db_type 内唯一)
* host / port / user / database / display —— 可入库(无敏感信息) * host / port / user / database / display —— 可入库(无敏感信息)
* password —— 留空 * password —— 留空
* oracle_client_dir —— 可入库(路径,非凭据) * oracle_client_dir —— 可入库(路径,非凭据)
*/ */
export const DB_TYPES = [ export const DB_TYPES = [
...@@ -21,41 +22,65 @@ export const DB_TYPES = [ ...@@ -21,41 +22,65 @@ export const DB_TYPES = [
{ value: 'oracle', label: 'Oracle' }, { value: 'oracle', label: 'Oracle' },
] ]
/**
* 兜底预设:列表结构,与后端 yaml 一致。
* 当 /api/db-defaults 拉取失败时使用。
*/
export const DB_PRESETS = { export const DB_PRESETS = {
// ── MySQL ── // ── MySQL(一个 db_type,多套连接) ──
mysql: { mysql: [
db_type: 'mysql', {
host: '192.168.20.10', id: 'smart-build',
port: 3306, db_type: 'mysql',
user: 'root', host: '192.168.20.10',
password: '', port: 3306,
database: 'smart-build', user: 'root',
display: '智能建造', password: '',
oracle_client_dir: '', database: 'smart-build',
}, display: '智能建造',
oracle_client_dir: '',
},
{
id: 'smart-build-backup',
db_type: 'mysql',
host: '192.168.20.11',
port: 3306,
user: 'root',
password: '',
database: 'smart-build',
display: '智能建造(备份)',
oracle_client_dir: '',
},
],
// ── 达梦 (DM) ── // ── 达梦 (DM) ──
dameng: { dameng: [
db_type: 'dameng', {
host: '192.168.20.24', id: 'sanjin',
port: 5236, db_type: 'dameng',
user: 'HGWXZJ', host: '192.168.20.24',
password: '', port: 5236,
database: 'HGWXZJ', user: 'HGWXZJ',
display: '三金系统', password: '',
oracle_client_dir: '', database: 'HGWXZJ',
}, display: '三金系统',
oracle_client_dir: '',
},
],
// ── Oracle ── // ── Oracle ──
oracle: { oracle: [
db_type: 'oracle', {
host: '192.168.20.196', id: 'huanggang-gjj',
port: 1521, db_type: 'oracle',
user: 'GJJ80', host: '192.168.20.196',
password: '', port: 1521,
database: 'ZFGJJ', user: 'GJJ80',
display: '黄冈公积金', password: '',
oracle_client_dir: database: 'ZFGJJ',
'D:\\Oracle\\instantclient-basic-windows.x64-12.2.0.1.0\\instantclient_12_2', display: '黄冈公积金',
}, oracle_client_dir:
} 'D:\\Oracle\\instantclient-basic-windows.x64-12.2.0.1.0\\instantclient_12_2',
},
],
}
\ No newline at end of file
...@@ -7,33 +7,78 @@ ...@@ -7,33 +7,78 @@
* 用 DB_PRESETS 常量兜底 * 用 DB_PRESETS 常量兜底
* *
* 状态: * 状态:
* currentType 当前选中的 db_type(写到 localStorage) * currentConnection 当前选中的具体连接({db_type, id} | null,写到 localStorage)
* presets 三套预设(响应式;API 拉回来后会替换) * presets 各 db_type 的连接列表(响应式;API 拉回来后会替换)
* loading 是否正在拉 API * loading 是否正在拉 API
*
* 数据结构(2026-08-17 起改为列表):
* presets = {
* mysql: [{id, host, port, ...}, {id, host, port, ...}], // 可多个连接
* dameng: [{id, host, port, ...}],
* oracle: [{id, host, port, ...}],
* }
* *
* 设计取舍: * 设计取舍:
* - 不上 Pinia:当前只一个全局 db 状态,reactive 单例够用 * - 不上 Pinia:当前只一个全局 db 状态,reactive 单例够用
* - 密码不进 localStorage:只存 db_type,需要连接时从 presets 实时取 * - 密码不进 localStorage:只存 db_type+id,需要连接时从 presets 实时取
* - localStorage key 从 STORAGE_KEY(旧:dbConnection.type)改为新 key;
* 旧 key 数据会被忽略(fallback 到第一个连接的 id)
*/ */
import { reactive, ref, watch, computed } from 'vue' import { reactive, ref, watch, computed } from 'vue'
import { DB_PRESETS as FALLBACK_PRESETS } from '@/config/dbPresets' import { DB_PRESETS as FALLBACK_PRESETS } from '@/config/dbPresets'
const STORAGE_KEY = 'web2.dbConnection.type' const STORAGE_KEY = 'web2.dbConnection.current'
const API_TIMEOUT_MS = 4000 const API_TIMEOUT_MS = 4000
function _loadInitialType() { /** 把 {db_type, id} 对象序列化成可写 localStorage 的字符串 */
function _serializeConn(c) {
if (!c) return null
return `${c.db_type}::${c.id}`
}
/** 把 localStorage 里的字符串反序列化成 {db_type, id} */
function _deserializeConn(s) {
if (!s || typeof s !== 'string') return null
const [db_type, id] = s.split('::')
if (!db_type || !id) return null
return { db_type, id }
}
/** 从预设里找指定 {db_type, id} 的连接 */
function _findConn(presetsObj, db_type, id) {
if (!presetsObj || !db_type || !id) return null
const list = presetsObj[db_type]
if (!Array.isArray(list)) return null
return list.find((c) => c && c.id === id) || null
}
/** 默认连接:第一个 db_type 的第一个连接 */
function _defaultConn(presetsObj) {
if (!presetsObj) return null
const firstType = Object.keys(presetsObj)[0]
if (!firstType) return null
const firstConn = presetsObj[firstType]?.[0]
if (!firstConn) return null
return { db_type: firstType, id: firstConn.id }
}
function _loadInitialConn(presetsObj) {
try { try {
const saved = localStorage.getItem(STORAGE_KEY) const saved = localStorage.getItem(STORAGE_KEY)
if (saved && FALLBACK_PRESETS[saved]) return saved const parsed = _deserializeConn(saved)
if (parsed && _findConn(presetsObj, parsed.db_type, parsed.id)) {
return parsed
}
} catch (_) { } catch (_) {
/* localStorage 不可用 */ /* localStorage 不可用 */
} }
return 'mysql' return _defaultConn(presetsObj)
} }
const state = reactive({ const state = reactive({
currentType: _loadInitialType(), /** 当前选中的连接 {db_type, id} */
currentConnection: _loadInitialConn(FALLBACK_PRESETS),
/** 是否已经尝试过拉 API(true 后不再重试,避免启动时反复请求) */ /** 是否已经尝试过拉 API(true 后不再重试,避免启动时反复请求) */
apiLoaded: false, apiLoaded: false,
}) })
...@@ -43,30 +88,39 @@ const presets = ref({ ...FALLBACK_PRESETS }) ...@@ -43,30 +88,39 @@ const presets = ref({ ...FALLBACK_PRESETS })
const loading = ref(false) const loading = ref(false)
watch( watch(
() => state.currentType, () => state.currentConnection,
(v) => { (v) => {
try { try {
localStorage.setItem(STORAGE_KEY, v) localStorage.setItem(STORAGE_KEY, _serializeConn(v))
} catch (_) { } catch (_) {
/* 静默 */ /* 静默 */
} }
}, },
{ deep: true },
) )
/** 当前选中的完整连接信息(响应式) */ /** 当前选中的完整连接信息(响应式) */
const currentConnection = computed(() => presets.value[state.currentType]) const currentConnection = computed(() => {
const c = state.currentConnection
if (!c) return null
return _findConn(presets.value, c.db_type, c.id)
})
/** 当前连接的 db_type(便捷 computed,给 IdCardTool 这类只关心 db_type 的地方用) */
const currentType = computed(() => state.currentConnection?.db_type || null)
/** 切换数据库类型 */ /** 切换到指定连接 */
function setDbType(type) { function setCurrentConnection(db_type, id) {
if (!presets.value[type]) { const conn = _findConn(presets.value, db_type, id)
console.warn(`[db] 未知数据库类型: ${type}`) if (!conn) {
return console.warn(`[db] 未知连接: ${db_type}::${id}`)
return false
} }
state.currentType = type state.currentConnection = { db_type, id }
return true
} }
/** /** 从后端拉 db-defaults,覆盖本地 presets。
* 从后端拉 db-defaults,覆盖本地 presets。
* 失败/超时 → 保留 FALLBACK_PRESETS 不变,apiLoaded=true 表示「不再重试」。 * 失败/超时 → 保留 FALLBACK_PRESETS 不变,apiLoaded=true 表示「不再重试」。
*/ */
async function loadPresetsFromApi() { async function loadPresetsFromApi() {
...@@ -81,7 +135,15 @@ async function loadPresetsFromApi() { ...@@ -81,7 +135,15 @@ async function loadPresetsFromApi() {
const data = await r.json() const data = await r.json()
if (data && typeof data === 'object' && Object.keys(data).length > 0) { if (data && typeof data === 'object' && Object.keys(data).length > 0) {
presets.value = data presets.value = data
console.log('[db] 已从后端加载 presets:', Object.keys(data)) console.log('[db] 已从后端加载 presets:', {
keys: Object.keys(data),
counts: Object.fromEntries(Object.entries(data).map(([k, v]) => [k, Array.isArray(v) ? v.length : 0])),
})
// 拉回来后校准当前连接:丢了/换预设了就回到默认第一个
const cur = state.currentConnection
if (!cur || !_findConn(data, cur.db_type, cur.id)) {
state.currentConnection = _defaultConn(data)
}
} else { } else {
console.warn('[db] 后端返回空,使用本地兜底预设') console.warn('[db] 后端返回空,使用本地兜底预设')
} }
...@@ -94,7 +156,7 @@ async function loadPresetsFromApi() { ...@@ -94,7 +156,7 @@ async function loadPresetsFromApi() {
} }
/** /**
* 测试当前选中的连接(不切换 currentType,只发请求) * 测试当前选中的连接(不切换 currentConnection,只发请求)
* @returns {Promise<{ok: boolean, message: string}>} * @returns {Promise<{ok: boolean, message: string}>}
*/ */
async function testCurrentConnection() { async function testCurrentConnection() {
...@@ -107,9 +169,8 @@ async function testCurrentConnection() { ...@@ -107,9 +169,8 @@ async function testCurrentConnection() {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
// db_type 必须用 state.currentType(已知 db_type 一定有值), // conn 里已含 db_type(后端注入),直接用
// 不能用 conn.db_type —— 后端 YAML 预设里没有这个字段。 db_type: conn.db_type,
db_type: state.currentType,
host: conn.host, host: conn.host,
port: conn.port, port: conn.port,
user: conn.user, user: conn.user,
...@@ -128,5 +189,14 @@ async function testCurrentConnection() { ...@@ -128,5 +189,14 @@ async function testCurrentConnection() {
} }
export function useDbConnection() { export function useDbConnection() {
return { state, presets, loading, currentConnection, setDbType, loadPresetsFromApi, testCurrentConnection } return {
} state,
presets,
loading,
currentConnection, // 当前具体连接(响应式)
currentType, // 当前连接的 db_type(便捷 computed)
setCurrentConnection, // (db_type, id) => bool
loadPresetsFromApi,
testCurrentConnection,
}
}
\ 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