- 统一数据库监控菜单路径到 /system-ops/dbms 入口 - 添加 isDbmsMenu 函数处理多种数据库菜单路径匹配 - 在动态路由中增加多个数据库监控路径的重定向规则 - 添加设备单位配置功能包括新增 EquipmentUnitForm 接口定义 - 添加监测点限值配置功能包括新增 OverlimitDetail 接口定义 - 在装置表单中添加单位配置按钮并集成单位调试功能 - 在监测点表单中添加限值配置按钮并集成限值调试功能 - 添加电压等级变更时的默认容量和变比同步逻辑 - 配置监测点表单中的线路类型选择选项 - 添加装置表单中比率输入组的高度紧凑样式 - 新增数据库运维静态路由配置和别名支持
458 lines
15 KiB
Vue
458 lines
15 KiB
Vue
<template>
|
|
<div class="table-box dbms-page">
|
|
<div class="dbms-workbench">
|
|
<DbmsToolbar :has-selected-connection="Boolean(selectedConnection)" @command="handleToolbarCommand" />
|
|
|
|
<div class="dbms-main" :class="{ 'is-tree-collapsed': treeCollapsed }">
|
|
<aside class="dbms-tree-panel">
|
|
<DbmsConnectionTree
|
|
:connections="connections"
|
|
:loading="connectionLoading"
|
|
:collapsed="treeCollapsed"
|
|
@toggle="treeCollapsed = !treeCollapsed"
|
|
@refresh="loadConnections"
|
|
@select-connection="handleSelectConnection"
|
|
@select-section="handleSelectSection"
|
|
/>
|
|
</aside>
|
|
|
|
<main class="dbms-workspace-panel">
|
|
<DbmsWorkspace
|
|
:active-section="activeSection"
|
|
:selected-connection="selectedConnection"
|
|
:tables="tables"
|
|
:files="files"
|
|
:tasks="tasks"
|
|
:task-query="taskQuery"
|
|
:file-query="fileQuery"
|
|
:table-loading="tableLoading"
|
|
:task-loading="taskLoading"
|
|
:file-loading="fileLoading"
|
|
:task-total="taskTotal"
|
|
:task-page-num="taskPage.pageNum"
|
|
:task-page-size="taskPage.pageSize"
|
|
:file-total="fileTotal"
|
|
:file-page-num="filePage.pageNum"
|
|
:file-page-size="filePage.pageSize"
|
|
@edit-connection="row => openConnectionDialog('edit', row)"
|
|
@load-tables="loadTables"
|
|
@backup="handleCreateBackup"
|
|
@restore="handleCreateRestore"
|
|
@refresh-tasks="loadTasks"
|
|
@refresh-files="loadFiles"
|
|
@search-tasks="handleSearchTasks"
|
|
@search-files="handleSearchFiles"
|
|
@check-task="handleCheckTask"
|
|
@delete-task="handleDeleteTask"
|
|
@delete-file="handleDeleteFile"
|
|
@task-page-change="handleTaskPageChange"
|
|
@task-size-change="handleTaskSizeChange"
|
|
@file-page-change="handleFilePageChange"
|
|
@file-size-change="handleFileSizeChange"
|
|
/>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<DbmsConnectionTypeDialog ref="connectionTypeDialogRef" @next="handleSelectConnectionType" />
|
|
<DbmsConnectionDialog ref="connectionDialogRef" @save="handleSaveConnection" @test="handleTestConnectionPayload" />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { reactive, ref } from 'vue'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import type { Dbms } from '@/api/system/dbms/interface'
|
|
import {
|
|
addDbmsConnection,
|
|
createDbmsBackupTask,
|
|
createDbmsRestoreTask,
|
|
deleteDbmsBackupFile,
|
|
deleteDbmsConnection,
|
|
deleteDbmsTask,
|
|
getDbmsBackupFileList,
|
|
getDbmsBackupTaskList,
|
|
getDbmsBackupTaskStatus,
|
|
getDbmsConnectionList,
|
|
getDbmsRestoreTaskStatus,
|
|
getDbmsTableList,
|
|
testDbmsConnection,
|
|
updateDbmsConnection
|
|
} from '@/api/system/dbms'
|
|
import DbmsConnectionDialog from './components/DbmsConnectionDialog.vue'
|
|
import DbmsConnectionTypeDialog from './components/DbmsConnectionTypeDialog.vue'
|
|
import DbmsConnectionTree from './components/DbmsConnectionTree.vue'
|
|
import DbmsToolbar from './components/DbmsToolbar.vue'
|
|
import DbmsWorkspace from './components/DbmsWorkspace.vue'
|
|
import type {
|
|
DbmsConnectionQuery,
|
|
DbmsFileQuery,
|
|
DbmsTaskQuery,
|
|
DbmsToolbarCommand,
|
|
DbmsWorkspaceSection
|
|
} from './components/types'
|
|
import {
|
|
buildBackupPayload,
|
|
buildDeleteBackupFilePayload,
|
|
buildDeleteTaskPayload,
|
|
buildFileListParams,
|
|
buildRestorePayload,
|
|
buildTaskListParams
|
|
} from './utils/taskPayload'
|
|
import { DELETE_CONFIRM_TEXT, isTerminalTaskStatus } from './utils/normalize'
|
|
|
|
defineOptions({
|
|
name: 'DbmsView'
|
|
})
|
|
|
|
type ConnectionDialogExpose = {
|
|
open: (mode: 'add' | 'edit', record?: Dbms.ConnectionRecord, dbType?: Dbms.DbType) => void
|
|
close: () => void
|
|
}
|
|
|
|
type ConnectionTypeDialogExpose = {
|
|
open: () => void
|
|
close: () => void
|
|
}
|
|
|
|
const connectionTypeDialogRef = ref<ConnectionTypeDialogExpose>()
|
|
const connectionDialogRef = ref<ConnectionDialogExpose>()
|
|
const connections = ref<Dbms.ConnectionRecord[]>([])
|
|
const selectedConnection = ref<Dbms.ConnectionRecord | null>(null)
|
|
const tables = ref<Dbms.TableRecord[]>([])
|
|
const tasks = ref<Dbms.TaskRecord[]>([])
|
|
const files = ref<Dbms.BackupFileRecord[]>([])
|
|
const activeSection = ref<DbmsWorkspaceSection>('overview')
|
|
const treeCollapsed = ref(false)
|
|
|
|
const connectionLoading = ref(false)
|
|
const tableLoading = ref(false)
|
|
const taskLoading = ref(false)
|
|
const fileLoading = ref(false)
|
|
const connectionTotal = ref(0)
|
|
const taskTotal = ref(0)
|
|
const fileTotal = ref(0)
|
|
|
|
const connectionPage = reactive({ pageNum: 1, pageSize: 1000 })
|
|
const taskPage = reactive({ pageNum: 1, pageSize: 10 })
|
|
const filePage = reactive({ pageNum: 1, pageSize: 10 })
|
|
const connectionQuery = reactive<DbmsConnectionQuery>({ connectionName: '', schemaName: '' })
|
|
const taskQuery = reactive<DbmsTaskQuery>({ taskStatus: '' })
|
|
const fileQuery = reactive<DbmsFileQuery>({ taskId: '' })
|
|
|
|
const loadConnections = async () => {
|
|
connectionLoading.value = true
|
|
try {
|
|
const result = await getDbmsConnectionList({
|
|
pageNum: connectionPage.pageNum,
|
|
pageSize: connectionPage.pageSize,
|
|
connectionName: connectionQuery.connectionName || undefined,
|
|
schemaName: connectionQuery.schemaName || undefined,
|
|
dbType: 'ORACLE'
|
|
})
|
|
connections.value = result.data.records || []
|
|
connectionTotal.value = result.data.total || 0
|
|
} finally {
|
|
connectionLoading.value = false
|
|
}
|
|
}
|
|
|
|
const loadTables = async () => {
|
|
if (!selectedConnection.value) {
|
|
ElMessage.warning('请先选择连接')
|
|
return
|
|
}
|
|
|
|
tableLoading.value = true
|
|
try {
|
|
const result = await getDbmsTableList({
|
|
connectionId: selectedConnection.value.id,
|
|
schemaName: selectedConnection.value.schemaName || undefined
|
|
})
|
|
tables.value = result.data || []
|
|
} finally {
|
|
tableLoading.value = false
|
|
}
|
|
}
|
|
|
|
const loadTasks = async () => {
|
|
taskLoading.value = true
|
|
try {
|
|
const result = await getDbmsBackupTaskList(
|
|
buildTaskListParams(taskPage.pageNum, taskPage.pageSize, selectedConnection.value?.id, taskQuery.taskStatus)
|
|
)
|
|
tasks.value = result.data.records || []
|
|
taskTotal.value = result.data.total || 0
|
|
} finally {
|
|
taskLoading.value = false
|
|
}
|
|
}
|
|
|
|
const loadFiles = async () => {
|
|
fileLoading.value = true
|
|
try {
|
|
const result = await getDbmsBackupFileList(
|
|
buildFileListParams(filePage.pageNum, filePage.pageSize, selectedConnection.value?.id, fileQuery.taskId)
|
|
)
|
|
files.value = result.data.records || []
|
|
fileTotal.value = result.data.total || 0
|
|
} finally {
|
|
fileLoading.value = false
|
|
}
|
|
}
|
|
|
|
const openConnectionDialog = (mode: 'add' | 'edit', row?: Dbms.ConnectionRecord) => {
|
|
connectionDialogRef.value?.open(mode, row)
|
|
}
|
|
|
|
const openConnectionTypeDialog = () => {
|
|
connectionTypeDialogRef.value?.open()
|
|
}
|
|
|
|
const handleSelectConnectionType = (dbType: Dbms.DbType) => {
|
|
if (dbType === 'MYSQL') {
|
|
ElMessage.info('MySQL 连接配置暂未接入')
|
|
return
|
|
}
|
|
|
|
connectionTypeDialogRef.value?.close()
|
|
connectionDialogRef.value?.open('add', undefined, dbType)
|
|
}
|
|
|
|
const handleSelectSection = async (section: DbmsWorkspaceSection) => {
|
|
activeSection.value = section
|
|
if (section === 'tables') {
|
|
await loadTables()
|
|
}
|
|
}
|
|
|
|
const handleToolbarCommand = async (command: DbmsToolbarCommand) => {
|
|
const commandMap: Partial<Record<DbmsToolbarCommand, () => void | Promise<void>>> = {
|
|
connect: () => openConnectionTypeDialog(),
|
|
newConnection: () => openConnectionTypeDialog(),
|
|
newQuery: () => {
|
|
ElMessage.info('查询编辑器接口暂未接入')
|
|
},
|
|
tables: () => handleSelectSection('tables'),
|
|
views: () => {
|
|
activeSection.value = 'views'
|
|
},
|
|
backup: () => {
|
|
activeSection.value = 'backup'
|
|
}
|
|
}
|
|
|
|
await commandMap[command]?.()
|
|
}
|
|
|
|
const handleSearchConnections = (query: DbmsConnectionQuery) => {
|
|
Object.assign(connectionQuery, query)
|
|
connectionPage.pageNum = 1
|
|
loadConnections()
|
|
}
|
|
|
|
const handleSelectConnection = (row: Dbms.ConnectionRecord) => {
|
|
if (selectedConnection.value?.id === row.id) return
|
|
selectedConnection.value = row
|
|
tables.value = []
|
|
activeSection.value = 'overview'
|
|
taskPage.pageNum = 1
|
|
filePage.pageNum = 1
|
|
loadTasks()
|
|
loadFiles()
|
|
}
|
|
|
|
const handleSaveConnection = async (payload: Dbms.ConnectionPayload) => {
|
|
if (payload.id) {
|
|
await updateDbmsConnection(payload)
|
|
ElMessage.success('连接已更新')
|
|
} else {
|
|
await addDbmsConnection(payload)
|
|
ElMessage.success('连接已新增')
|
|
}
|
|
connectionDialogRef.value?.close()
|
|
await loadConnections()
|
|
}
|
|
|
|
const handleTestConnectionPayload = async (payload: Dbms.TestConnectionParams) => {
|
|
const result = await testDbmsConnection(payload)
|
|
ElMessage[result.data.success ? 'success' : 'error'](result.data.message || (result.data.success ? '连接成功' : '连接失败'))
|
|
await loadConnections()
|
|
}
|
|
|
|
const handleTestStoredConnection = (row: Dbms.ConnectionRecord) => {
|
|
handleTestConnectionPayload({ connectionId: row.id })
|
|
}
|
|
|
|
const handleDeleteConnection = async (row: Dbms.ConnectionRecord) => {
|
|
await ElMessageBox.confirm(`确认删除连接“${row.connectionName}”?`, '删除确认', {
|
|
type: 'warning',
|
|
confirmButtonText: DELETE_CONFIRM_TEXT,
|
|
cancelButtonText: '取消'
|
|
})
|
|
await deleteDbmsConnection({ id: row.id })
|
|
if (selectedConnection.value?.id === row.id) {
|
|
selectedConnection.value = null
|
|
tables.value = []
|
|
}
|
|
ElMessage.success('连接已删除')
|
|
await loadConnections()
|
|
}
|
|
|
|
const pollTaskStatus = async (taskId: string, operationType: Dbms.OperationType) => {
|
|
// 异步任务创建后短轮询状态,避免页面停留在 WAITING 无反馈。
|
|
for (let index = 0; index < 8; index += 1) {
|
|
const result =
|
|
operationType === 'RESTORE' ? await getDbmsRestoreTaskStatus(taskId) : await getDbmsBackupTaskStatus(taskId)
|
|
if (isTerminalTaskStatus(result.data.taskStatus)) break
|
|
await new Promise(resolve => window.setTimeout(resolve, 1500))
|
|
}
|
|
await loadTasks()
|
|
await loadFiles()
|
|
}
|
|
|
|
const handleCreateBackup = async ({ form }: { form: import('./utils/taskPayload').DbmsBackupFormModel }) => {
|
|
if (!selectedConnection.value) return
|
|
const result = await createDbmsBackupTask(buildBackupPayload(selectedConnection.value, form))
|
|
ElMessage.success(`备份任务已创建:${result.data.taskNo}`)
|
|
activeSection.value = 'tasks'
|
|
await loadTasks()
|
|
pollTaskStatus(result.data.taskId, 'BACKUP')
|
|
}
|
|
|
|
const handleCreateRestore = async ({ form }: { form: import('./utils/taskPayload').DbmsRestoreFormModel }) => {
|
|
if (!selectedConnection.value) return
|
|
// 覆盖类恢复必须由后端确认文案兜底,前端只负责传递明确确认值。
|
|
const result = await createDbmsRestoreTask(buildRestorePayload(selectedConnection.value, form))
|
|
ElMessage.success(`恢复任务已创建:${result.data.taskNo}`)
|
|
activeSection.value = 'tasks'
|
|
await loadTasks()
|
|
pollTaskStatus(result.data.taskId, 'RESTORE')
|
|
}
|
|
|
|
const handleCheckTask = async (row: Dbms.TaskRecord) => {
|
|
const result =
|
|
row.operationType === 'RESTORE' ? await getDbmsRestoreTaskStatus(row.id) : await getDbmsBackupTaskStatus(row.id)
|
|
ElMessage.info(result.data.resultMessage || '任务状态已刷新')
|
|
await loadTasks()
|
|
}
|
|
|
|
const handleDeleteTask = async (row: Dbms.TaskRecord) => {
|
|
await ElMessageBox.confirm(`确认删除任务“${row.taskNo}”?`, '删除确认', {
|
|
type: 'warning',
|
|
confirmButtonText: DELETE_CONFIRM_TEXT,
|
|
cancelButtonText: '取消'
|
|
})
|
|
await deleteDbmsTask(buildDeleteTaskPayload(row.id))
|
|
ElMessage.success('任务记录已删除')
|
|
await loadTasks()
|
|
}
|
|
|
|
const handleDeleteFile = async (row: Dbms.BackupFileRecord) => {
|
|
await ElMessageBox.confirm(`确认删除备份文件“${row.fileName}”?`, '删除确认', {
|
|
type: 'warning',
|
|
confirmButtonText: DELETE_CONFIRM_TEXT,
|
|
cancelButtonText: '取消'
|
|
})
|
|
await deleteDbmsBackupFile(buildDeleteBackupFilePayload(row.id))
|
|
ElMessage.success('备份文件已删除')
|
|
await loadFiles()
|
|
}
|
|
|
|
const handleSearchTasks = (query: DbmsTaskQuery) => {
|
|
Object.assign(taskQuery, query)
|
|
taskPage.pageNum = 1
|
|
loadTasks()
|
|
}
|
|
|
|
const handleSearchFiles = (query: DbmsFileQuery) => {
|
|
Object.assign(fileQuery, query)
|
|
filePage.pageNum = 1
|
|
loadFiles()
|
|
}
|
|
|
|
const handleTaskPageChange = (page: number) => {
|
|
taskPage.pageNum = page
|
|
loadTasks()
|
|
}
|
|
|
|
const handleTaskSizeChange = (size: number) => {
|
|
taskPage.pageSize = size
|
|
taskPage.pageNum = 1
|
|
loadTasks()
|
|
}
|
|
|
|
const handleFilePageChange = (page: number) => {
|
|
filePage.pageNum = page
|
|
loadFiles()
|
|
}
|
|
|
|
const handleFileSizeChange = (size: number) => {
|
|
filePage.pageSize = size
|
|
filePage.pageNum = 1
|
|
loadFiles()
|
|
}
|
|
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.dbms-page {
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
height: 100%;
|
|
min-height: 0;
|
|
padding: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.dbms-workbench {
|
|
display: flex;
|
|
flex: 1;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
background: var(--el-bg-color-page);
|
|
border: 1px solid var(--el-border-color-light);
|
|
}
|
|
|
|
.dbms-main {
|
|
display: grid;
|
|
grid-template-columns: 292px minmax(0, 1fr);
|
|
gap: 8px;
|
|
flex: 1;
|
|
min-height: 0;
|
|
padding: 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.dbms-main.is-tree-collapsed {
|
|
grid-template-columns: 0 minmax(0, 1fr);
|
|
}
|
|
|
|
.dbms-tree-panel {
|
|
position: relative;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.dbms-main.is-tree-collapsed .dbms-tree-panel {
|
|
z-index: 4;
|
|
overflow: visible;
|
|
}
|
|
|
|
.dbms-workspace-panel {
|
|
display: flex;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
@media (max-width: 1280px) {
|
|
.dbms-main:not(.is-tree-collapsed) {
|
|
grid-template-columns: 260px minmax(0, 1fr);
|
|
}
|
|
}
|
|
</style>
|