feat(auth): 统一数据库运维菜单路由并添加装置单位及监测点限值配置功能
- 统一数据库监控菜单路径到 /system-ops/dbms 入口 - 添加 isDbmsMenu 函数处理多种数据库菜单路径匹配 - 在动态路由中增加多个数据库监控路径的重定向规则 - 添加设备单位配置功能包括新增 EquipmentUnitForm 接口定义 - 添加监测点限值配置功能包括新增 OverlimitDetail 接口定义 - 在装置表单中添加单位配置按钮并集成单位调试功能 - 在监测点表单中添加限值配置按钮并集成限值调试功能 - 添加电压等级变更时的默认容量和变比同步逻辑 - 配置监测点表单中的线路类型选择选项 - 添加装置表单中比率输入组的高度紧凑样式 - 新增数据库运维静态路由配置和别名支持
This commit is contained in:
457
frontend/src/views/system-ops/dbms/index.vue
Normal file
457
frontend/src/views/system-ops/dbms/index.vue
Normal file
@@ -0,0 +1,457 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user