Files
CN_Tool_client/frontend/src/views/system-ops/dbms/index.vue
yexb d055a8e1a0 feat(auth): 统一数据库运维菜单路由并添加装置单位及监测点限值配置功能
- 统一数据库监控菜单路径到 /system-ops/dbms 入口
- 添加 isDbmsMenu 函数处理多种数据库菜单路径匹配
- 在动态路由中增加多个数据库监控路径的重定向规则
- 添加设备单位配置功能包括新增 EquipmentUnitForm 接口定义
- 添加监测点限值配置功能包括新增 OverlimitDetail 接口定义
- 在装置表单中添加单位配置按钮并集成单位调试功能
- 在监测点表单中添加限值配置按钮并集成限值调试功能
- 添加电压等级变更时的默认容量和变比同步逻辑
- 配置监测点表单中的线路类型选择选项
- 添加装置表单中比率输入组的高度紧凑样式
- 新增数据库运维静态路由配置和别名支持
2026-05-29 15:10:14 +08:00

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>