From d055a8e1a0b19e43137c8ae1943ad09b1e680252 Mon Sep 17 00:00:00 2001 From: yexb <553699424@qq.com> Date: Fri, 29 May 2026 15:10:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E7=BB=9F=E4=B8=80=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E8=BF=90=E7=BB=B4=E8=8F=9C=E5=8D=95=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E5=B9=B6=E6=B7=BB=E5=8A=A0=E8=A3=85=E7=BD=AE=E5=8D=95?= =?UTF-8?q?=E4=BD=8D=E5=8F=8A=E7=9B=91=E6=B5=8B=E7=82=B9=E9=99=90=E5=80=BC?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 统一数据库监控菜单路径到 /system-ops/dbms 入口 - 添加 isDbmsMenu 函数处理多种数据库菜单路径匹配 - 在动态路由中增加多个数据库监控路径的重定向规则 - 添加设备单位配置功能包括新增 EquipmentUnitForm 接口定义 - 添加监测点限值配置功能包括新增 OverlimitDetail 接口定义 - 在装置表单中添加单位配置按钮并集成单位调试功能 - 在监测点表单中添加限值配置按钮并集成限值调试功能 - 添加电压等级变更时的默认容量和变比同步逻辑 - 配置监测点表单中的线路类型选择选项 - 添加装置表单中比率输入组的高度紧凑样式 - 新增数据库运维静态路由配置和别名支持 --- frontend/src/api/system/dbms/index.ts | 62 +++ .../src/api/system/dbms/interface/index.ts | 187 +++++++ frontend/src/api/tools/addLedger/index.ts | 9 + .../api/tools/addLedger/interface/index.ts | 44 ++ .../modules/check-dbms-route-contract.mjs | 55 +++ frontend/src/routers/modules/dynamicRouter.ts | 12 +- frontend/src/routers/modules/staticRouter.ts | 19 + frontend/src/stores/modules/auth.ts | 22 + .../dbms/components/DbmsConnectionDialog.vue | 252 ++++++++++ .../dbms/components/DbmsConnectionTree.vue | 256 ++++++++++ .../components/DbmsConnectionTypeDialog.vue | 250 ++++++++++ .../dbms/components/DbmsOperationTable.vue | 215 ++++++++ .../dbms/components/DbmsTaskPanel.vue | 298 ++++++++++++ .../dbms/components/DbmsTaskStatusCard.vue | 258 ++++++++++ .../dbms/components/DbmsToolbar.vue | 122 +++++ .../dbms/components/DbmsWorkspace.vue | 237 +++++++++ .../views/system-ops/dbms/components/types.ts | 48 ++ ...heck-connection-dialog-layout-contract.mjs | 47 ++ .../check-connection-type-dialog-contract.mjs | 87 ++++ .../contracts/check-initial-api-contract.mjs | 29 ++ .../check-workbench-layout-contract.mjs | 38 ++ frontend/src/views/system-ops/dbms/index.vue | 457 ++++++++++++++++++ .../views/system-ops/dbms/utils/normalize.ts | 95 ++++ .../system-ops/dbms/utils/taskPayload.ts | 176 +++++++ .../addLedger/components/EquipmentForm.vue | 24 +- .../components/LedgerContextPanel.vue | 6 + .../tools/addLedger/components/LineForm.vue | 85 +++- .../addLedger/components/ledgerForm.scss | 8 + .../check-form-alignment-contract.mjs | 6 + .../check-line-voltage-default-contract.mjs | 43 ++ .../check-unit-overlimit-debug-contract.mjs | 66 +++ frontend/src/views/tools/addLedger/index.vue | 224 +++++++++ .../views/tools/addLedger/utils/ledgerData.ts | 85 +++- 33 files changed, 3790 insertions(+), 32 deletions(-) create mode 100644 frontend/src/api/system/dbms/index.ts create mode 100644 frontend/src/api/system/dbms/interface/index.ts create mode 100644 frontend/src/routers/modules/check-dbms-route-contract.mjs create mode 100644 frontend/src/views/system-ops/dbms/components/DbmsConnectionDialog.vue create mode 100644 frontend/src/views/system-ops/dbms/components/DbmsConnectionTree.vue create mode 100644 frontend/src/views/system-ops/dbms/components/DbmsConnectionTypeDialog.vue create mode 100644 frontend/src/views/system-ops/dbms/components/DbmsOperationTable.vue create mode 100644 frontend/src/views/system-ops/dbms/components/DbmsTaskPanel.vue create mode 100644 frontend/src/views/system-ops/dbms/components/DbmsTaskStatusCard.vue create mode 100644 frontend/src/views/system-ops/dbms/components/DbmsToolbar.vue create mode 100644 frontend/src/views/system-ops/dbms/components/DbmsWorkspace.vue create mode 100644 frontend/src/views/system-ops/dbms/components/types.ts create mode 100644 frontend/src/views/system-ops/dbms/contracts/check-connection-dialog-layout-contract.mjs create mode 100644 frontend/src/views/system-ops/dbms/contracts/check-connection-type-dialog-contract.mjs create mode 100644 frontend/src/views/system-ops/dbms/contracts/check-initial-api-contract.mjs create mode 100644 frontend/src/views/system-ops/dbms/contracts/check-workbench-layout-contract.mjs create mode 100644 frontend/src/views/system-ops/dbms/index.vue create mode 100644 frontend/src/views/system-ops/dbms/utils/normalize.ts create mode 100644 frontend/src/views/system-ops/dbms/utils/taskPayload.ts create mode 100644 frontend/src/views/tools/addLedger/contracts/check-line-voltage-default-contract.mjs create mode 100644 frontend/src/views/tools/addLedger/contracts/check-unit-overlimit-debug-contract.mjs diff --git a/frontend/src/api/system/dbms/index.ts b/frontend/src/api/system/dbms/index.ts new file mode 100644 index 0000000..2551180 --- /dev/null +++ b/frontend/src/api/system/dbms/index.ts @@ -0,0 +1,62 @@ +import http from '@/api' +import type { Dbms } from '@/api/system/dbms/interface' + +export const getDbmsOverview = () => { + return http.get('/database/overview', {}, { loading: false }) +} + +export const getDbmsConnectionList = (params: Dbms.ConnectionListParams) => { + return http.post('/database/connections/list', params, { loading: false }) +} + +export const addDbmsConnection = (params: Dbms.ConnectionPayload) => { + return http.post('/database/connections/add', params) +} + +export const updateDbmsConnection = (params: Dbms.ConnectionPayload) => { + return http.post('/database/connections/update', params) +} + +export const deleteDbmsConnection = (params: Dbms.DeleteConnectionParams) => { + return http.post('/database/connections/delete', params) +} + +export const testDbmsConnection = (params: Dbms.TestConnectionParams) => { + return http.post('/database/connections/test', params) +} + +export const getDbmsTableList = (params: Dbms.TableListParams) => { + return http.post('/database/connections/tables', params) +} + +export const createDbmsBackupTask = (params: Dbms.CreateBackupParams) => { + return http.post('/database/backups/create', params) +} + +export const getDbmsBackupTaskList = (params: Dbms.TaskListParams) => { + return http.post('/database/backups/tasks/list', params, { loading: false }) +} + +export const getDbmsBackupTaskStatus = (taskId: string) => { + return http.get('/database/backups/tasks/status', { taskId }, { loading: false }) +} + +export const getDbmsBackupFileList = (params: Dbms.FileListParams) => { + return http.post('/database/backups/files/list', params, { loading: false }) +} + +export const createDbmsRestoreTask = (params: Dbms.CreateRestoreParams) => { + return http.post('/database/restores/create', params) +} + +export const getDbmsRestoreTaskStatus = (taskId: string) => { + return http.get('/database/restores/tasks/status', { taskId }, { loading: false }) +} + +export const deleteDbmsBackupFile = (params: Dbms.DeleteBackupFileParams) => { + return http.post('/database/delete/backup-file', params) +} + +export const deleteDbmsTask = (params: Dbms.DeleteTaskParams) => { + return http.post('/database/delete/task', params) +} diff --git a/frontend/src/api/system/dbms/interface/index.ts b/frontend/src/api/system/dbms/interface/index.ts new file mode 100644 index 0000000..22cd20c --- /dev/null +++ b/frontend/src/api/system/dbms/interface/index.ts @@ -0,0 +1,187 @@ +import type { ReqPage, ResPage } from '@/api/interface' + +export namespace Dbms { + export type DbType = 'ORACLE' | 'MYSQL' + export type ConnectType = 'SERVICE_NAME' | 'SID' + export type BackupStrategy = 'DATA_PUMP' | 'JDBC_EXPORT' + export type BackupMode = 'FULL_TABLE' | 'TIME_RANGE' | 'SIZE_SPLIT' + export type OperationType = 'BACKUP' | 'RESTORE' + export type TaskStatus = 'WAITING' | 'RUNNING' | 'SUCCESS' | 'FAIL' | 'FAILED' | 'CANCELLED' + export type RestoreMode = 'SKIP' | 'APPEND' | 'TRUNCATE' | 'REPLACE' + + export interface Overview { + menuName: string + menuCode: string + path: string + status: string + description: string + } + + export interface ConnectionListParams extends ReqPage { + connectionName?: string + dbType?: DbType + schemaName?: string + } + + export interface ConnectionRecord { + id: string + connectionName: string + dbType: DbType + host: string + port: number + connectType: ConnectType + serviceName?: string | null + sid?: string | null + schemaName?: string | null + username: string + savePassword: 0 | 1 + directoryName?: string | null + directoryPath?: string | null + extraConfigJson?: string | null + remark?: string | null + lastTestStatus?: string | null + lastTestMessage?: string | null + lastTestTime?: string | null + state?: number + createTime?: string + updateTime?: string + } + + export interface ConnectionPayload { + id?: string + connectionName: string + dbType: DbType + host: string + port: number + connectType: ConnectType + serviceName?: string | null + sid?: string | null + schemaName?: string | null + username: string + password?: string | null + savePassword: 0 | 1 + directoryName?: string | null + directoryPath?: string | null + extraConfigJson?: string | null + remark?: string | null + } + + export interface DeleteConnectionParams { + id: string + } + + export interface TestConnectionParams { + connectionId?: string + connection?: ConnectionPayload + temporaryPassword?: string + } + + export interface TestConnectionResult { + success: boolean + message: string + } + + export interface TableListParams { + connectionId: string + temporaryPassword?: string + schemaName?: string + } + + export interface TableRecord { + owner: string + tableName: string + comments?: string | null + } + + export interface CreateBackupParams { + connectionId: string + backupStrategy?: BackupStrategy + schemaName?: string + targetNames?: string[] + backupMode?: BackupMode + timeColumn?: string | null + startTime?: string | null + endTime?: string | null + maxFileSizeMb?: number | null + directoryName?: string | null + temporaryPassword?: string + } + + export interface CreateRestoreParams { + connectionId: string + backupFileId: string + restoreMode?: RestoreMode + targetSchemaName?: string + temporaryPassword?: string + overwriteConfirmText?: string | null + } + + export interface TaskCreateResult { + taskId: string + taskNo: string + taskStatus: TaskStatus + } + + export interface TaskListParams extends ReqPage { + connectionId?: string + taskStatus?: TaskStatus | '' + } + + export interface TaskRecord { + id: string + taskNo: string + connectionId: string + dbType: DbType + operationType: OperationType + backupStrategy?: BackupStrategy | null + taskStatus: TaskStatus + schemaName?: string | null + targetNamesJson?: string | null + resultMessage?: string | null + progressPercent?: number | null + startedAt?: string | null + finishedAt?: string | null + createTime?: string + updateTime?: string + } + + export interface FileListParams extends ReqPage { + connectionId?: string + taskId?: string + backupStrategy?: BackupStrategy | '' + } + + export interface BackupFileRecord { + id: string + taskId: string + connectionId: string + dbType: DbType + backupStrategy: BackupStrategy + fileFormat?: string | null + schemaName?: string | null + targetNamesJson?: string | null + backupMode?: BackupMode | null + fileName: string + filePath?: string | null + logFileName?: string | null + logFilePath?: string | null + fileSize?: number | null + checksum?: string | null + state?: number + createTime?: string + } + + export interface DeleteBackupFileParams { + backupFileId: string + confirmText: string + } + + export interface DeleteTaskParams { + taskId: string + confirmText: string + } + + export interface ConnectionPageData extends ResPage {} + export interface TaskPageData extends ResPage {} + export interface BackupFilePageData extends ResPage {} +} diff --git a/frontend/src/api/tools/addLedger/index.ts b/frontend/src/api/tools/addLedger/index.ts index 875b53d..2829019 100644 --- a/frontend/src/api/tools/addLedger/index.ts +++ b/frontend/src/api/tools/addLedger/index.ts @@ -63,6 +63,7 @@ const toAddLedgerLinePayload = (params: AddLedger.LineForm) => { basicCapacity: params.basic_capacity, protocolCapacity: params.protocol_capacity, devCapacity: params.dev_capacity, + lineType: params.lineType, monitorObj: params.monitor_obj, isGovern: params.is_govern, monitorUser: params.monitor_user, @@ -172,6 +173,14 @@ export const saveAddLedgerEquipment = (params: AddLedger.EquipmentForm) => { return requestAddLedger('post', '/equipment/save', toAddLedgerEquipmentPayload(params)) } +export const getAddLedgerEquipmentUnit = (params: { devId: string }) => { + return requestAddLedger('get', '/equipment/unit', params) +} + +export const saveAddLedgerEquipmentUnit = (params: AddLedger.EquipmentUnitForm) => { + return requestAddLedger('post', '/equipment/unit/save', params) +} + export const saveAddLedgerLine = (params: AddLedger.LineForm) => { return requestAddLedger('post', '/line/save', toAddLedgerLinePayload(params)) } diff --git a/frontend/src/api/tools/addLedger/interface/index.ts b/frontend/src/api/tools/addLedger/interface/index.ts index 43f377f..b5eb882 100644 --- a/frontend/src/api/tools/addLedger/interface/index.ts +++ b/frontend/src/api/tools/addLedger/interface/index.ts @@ -67,6 +67,48 @@ export namespace AddLedger { upgrade?: number } + export interface EquipmentUnitForm { + devId: string + unitFrequency?: string + unitFrequencyDev?: string + phaseVoltage?: string + lineVoltage?: string + voltageDev?: string + uvoltageDev?: string + ieffective?: string + singleP?: string + singleViewP?: string + singleNoP?: string + totalActiveP?: string + totalViewP?: string + totalNoP?: string + vfundEffective?: string + ifund?: string + fundActiveP?: string + fundNoP?: string + vdistortion?: string + vharmonicRate?: string + iharmonic?: string + pharmonic?: string + iiharmonic?: string + positiveV?: string + noPositiveV?: string + } + + export interface OverlimitDetail { + id?: string + freqDev?: number + voltageFluctuation?: number + voltageDev?: number + uvoltageDev?: number + ubalance?: number + shortUbalance?: number + flicker?: number + uaberrance?: number + iNeg?: number + [key: string]: string | number | undefined + } + export interface LineForm { id?: string line_id?: string @@ -85,10 +127,12 @@ export namespace AddLedger { basic_capacity?: number protocol_capacity?: number dev_capacity?: number + lineType?: number monitor_obj?: string is_govern?: number monitor_user?: string is_important?: number + overlimit?: OverlimitDetail } export type NodeDetail = EngineeringForm | ProjectForm | EquipmentForm | LineForm diff --git a/frontend/src/routers/modules/check-dbms-route-contract.mjs b/frontend/src/routers/modules/check-dbms-route-contract.mjs new file mode 100644 index 0000000..b98c05c --- /dev/null +++ b/frontend/src/routers/modules/check-dbms-route-contract.mjs @@ -0,0 +1,55 @@ +import { readFileSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..') +const read = path => readFileSync(resolve(root, path), 'utf8') + +const staticRouterSource = read('routers/modules/staticRouter.ts') +const authStoreSource = read('stores/modules/auth.ts') + +const expectedPaths = [ + '/systemMonitor/dbms', + '/systemMonitor/dbms/index', + '/systemMonitor/databaseMonitor', + '/systemMonitor/databaseMonitor/index', + '/systemMonitor/database-monitor', + '/systemMonitor/database-monitor/index', + '/system-ops/database-monitor', + '/system-ops/database-monitor/index' +] + +const errors = [] + +const dbmsRouteIndex = staticRouterSource.indexOf("name: 'systemOpsDbms'") +if (dbmsRouteIndex === -1) { + errors.push('staticRouter.ts must define the systemOpsDbms route') +} else { + const nextRouteIndex = staticRouterSource.indexOf('\n {', dbmsRouteIndex + 1) + const routeBlock = staticRouterSource.slice(dbmsRouteIndex, nextRouteIndex === -1 ? undefined : nextRouteIndex) + for (const path of expectedPaths) { + if (!routeBlock.includes(`'${path}'`)) { + errors.push(`systemOpsDbms route alias must include ${path}`) + } + } +} + +for (const snippet of [ + 'function isDbmsMenu', + "if (isDbmsMenu(menu))", + "return '/system-ops/dbms'", + "menu.name = 'systemOpsDbms'", + "menu.component = '@/views/system-ops/dbms/index.vue'" +]) { + if (!authStoreSource.includes(snippet)) { + errors.push(`auth.ts must include DBMS menu normalization snippet: ${snippet}`) + } +} + +if (errors.length) { + console.error('dbms route contract failed:') + for (const error of errors) console.error(`- ${error}`) + process.exit(1) +} + +console.log('dbms route contract passed') diff --git a/frontend/src/routers/modules/dynamicRouter.ts b/frontend/src/routers/modules/dynamicRouter.ts index 702670a..7a00002 100644 --- a/frontend/src/routers/modules/dynamicRouter.ts +++ b/frontend/src/routers/modules/dynamicRouter.ts @@ -25,7 +25,16 @@ const COMPONENT_PATH_ALIASES: Record = { '/steady/steady-trend': '/steady/steadyTrend', '/steady/steady-trend/index': '/steady/steadyTrend/index', '/steady/check-square': '/steady/checksquare', - '/steady/check-square/index': '/steady/checksquare/index' + '/steady/check-square/index': '/steady/checksquare/index', + // 数据库监控菜单统一落到 system-ops/dbms 页面,兼容后端菜单常见 component 写法。 + '/systemMonitor/dbms': '/system-ops/dbms', + '/systemMonitor/dbms/index': '/system-ops/dbms/index', + '/systemMonitor/databaseMonitor': '/system-ops/dbms', + '/systemMonitor/databaseMonitor/index': '/system-ops/dbms/index', + '/systemMonitor/database-monitor': '/system-ops/dbms', + '/systemMonitor/database-monitor/index': '/system-ops/dbms/index', + '/system-ops/database-monitor': '/system-ops/dbms', + '/system-ops/database-monitor/index': '/system-ops/dbms/index' } const STATIC_ROUTE_NAMES = new Set([ 'layout', @@ -42,6 +51,7 @@ const STATIC_ROUTE_NAMES = new Set([ 'checksquare', 'systemMonitor', 'diskMonitor', + 'systemOpsDbms', '403', '404', '500' diff --git a/frontend/src/routers/modules/staticRouter.ts b/frontend/src/routers/modules/staticRouter.ts index 1696d79..16783fd 100644 --- a/frontend/src/routers/modules/staticRouter.ts +++ b/frontend/src/routers/modules/staticRouter.ts @@ -198,6 +198,25 @@ export const staticRouter: RouteRecordRaw[] = [ title: '磁盘监控' } }, + { + path: '/system-ops/dbms', + name: 'systemOpsDbms', + alias: [ + '/systemMonitor/dbms', + '/systemMonitor/dbms/index', + '/systemMonitor/databaseMonitor', + '/systemMonitor/databaseMonitor/index', + '/systemMonitor/database-monitor', + '/systemMonitor/database-monitor/index', + '/system-ops/database-monitor', + '/system-ops/database-monitor/index' + ], + component: () => import('@/views/system-ops/dbms/index.vue'), + meta: { + cacheName: 'DbmsView', + title: '数据库运维' + } + }, { path: '/:pathMatch(.*)*', component: () => import('@/components/ErrorMessage/404.vue') diff --git a/frontend/src/stores/modules/auth.ts b/frontend/src/stores/modules/auth.ts index 1e4cced..2601e6e 100644 --- a/frontend/src/stores/modules/auth.ts +++ b/frontend/src/stores/modules/auth.ts @@ -156,6 +156,13 @@ function normalizeBusinessMenu(menu: any): any { menu.component = '@/views/steady/checksquare/index.vue' } + if (isDbmsMenu(menu)) { + // 数据库运维菜单后端存在 systemMonitor/dbms 等历史路径,统一收敛到当前静态页面入口。 + menu.path = '/system-ops/dbms' + menu.name = 'systemOpsDbms' + menu.component = '@/views/system-ops/dbms/index.vue' + } + return menu } @@ -211,10 +218,25 @@ function isChecksquareMenu(menu: any): boolean { return title.includes('数据验证') } +function isDbmsMenu(menu: any): boolean { + const normalizedName = String(menu?.name ?? '').toLowerCase().replace(/[-_]/g, '') + const normalizedPath = String(menu?.path ?? '').toLowerCase().replace(/[-_]/g, '') + const normalizedComponent = String(menu?.component ?? '').toLowerCase().replace(/[-_]/g, '') + const title = String(menu?.meta?.title ?? menu?.title ?? '') + + if (normalizedName === 'systemopsdbms' || normalizedName === 'dbms') return true + if (normalizedPath.includes('systemmonitor/dbms') || normalizedPath.includes('systemops/dbms')) return true + if (normalizedPath.includes('databasemonitor') || normalizedComponent.includes('databasemonitor')) return true + if (normalizedComponent.includes('systemmonitor/dbms') || normalizedComponent.includes('systemops/dbms')) return true + + return title.includes('数据库') && (title.includes('运维') || title.includes('监控')) +} + export function resolveBusinessMenuPath(menu: Menu.MenuOptions): string { if (isEventListMenu(menu)) return '/eventList/index' if (isChecksquareMenu(menu)) return '/checksquare/index' if (isSteadyTrendMenu(menu)) return '/steadyTrend/index' + if (isDbmsMenu(menu)) return '/system-ops/dbms' return isSteadyDataViewMenu(menu) ? '/steadyDataView/index' : menu.path } diff --git a/frontend/src/views/system-ops/dbms/components/DbmsConnectionDialog.vue b/frontend/src/views/system-ops/dbms/components/DbmsConnectionDialog.vue new file mode 100644 index 0000000..56befc7 --- /dev/null +++ b/frontend/src/views/system-ops/dbms/components/DbmsConnectionDialog.vue @@ -0,0 +1,252 @@ + + + + + diff --git a/frontend/src/views/system-ops/dbms/components/DbmsConnectionTree.vue b/frontend/src/views/system-ops/dbms/components/DbmsConnectionTree.vue new file mode 100644 index 0000000..d705cf0 --- /dev/null +++ b/frontend/src/views/system-ops/dbms/components/DbmsConnectionTree.vue @@ -0,0 +1,256 @@ + + + + + diff --git a/frontend/src/views/system-ops/dbms/components/DbmsConnectionTypeDialog.vue b/frontend/src/views/system-ops/dbms/components/DbmsConnectionTypeDialog.vue new file mode 100644 index 0000000..5c08771 --- /dev/null +++ b/frontend/src/views/system-ops/dbms/components/DbmsConnectionTypeDialog.vue @@ -0,0 +1,250 @@ + + + + + diff --git a/frontend/src/views/system-ops/dbms/components/DbmsOperationTable.vue b/frontend/src/views/system-ops/dbms/components/DbmsOperationTable.vue new file mode 100644 index 0000000..94e9e2e --- /dev/null +++ b/frontend/src/views/system-ops/dbms/components/DbmsOperationTable.vue @@ -0,0 +1,215 @@ + + + + + diff --git a/frontend/src/views/system-ops/dbms/components/DbmsTaskPanel.vue b/frontend/src/views/system-ops/dbms/components/DbmsTaskPanel.vue new file mode 100644 index 0000000..e69ba69 --- /dev/null +++ b/frontend/src/views/system-ops/dbms/components/DbmsTaskPanel.vue @@ -0,0 +1,298 @@ + + + + + diff --git a/frontend/src/views/system-ops/dbms/components/DbmsTaskStatusCard.vue b/frontend/src/views/system-ops/dbms/components/DbmsTaskStatusCard.vue new file mode 100644 index 0000000..aea93e6 --- /dev/null +++ b/frontend/src/views/system-ops/dbms/components/DbmsTaskStatusCard.vue @@ -0,0 +1,258 @@ + + + + + diff --git a/frontend/src/views/system-ops/dbms/components/DbmsToolbar.vue b/frontend/src/views/system-ops/dbms/components/DbmsToolbar.vue new file mode 100644 index 0000000..b2c20e5 --- /dev/null +++ b/frontend/src/views/system-ops/dbms/components/DbmsToolbar.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/frontend/src/views/system-ops/dbms/components/DbmsWorkspace.vue b/frontend/src/views/system-ops/dbms/components/DbmsWorkspace.vue new file mode 100644 index 0000000..461017c --- /dev/null +++ b/frontend/src/views/system-ops/dbms/components/DbmsWorkspace.vue @@ -0,0 +1,237 @@ + + + + + diff --git a/frontend/src/views/system-ops/dbms/components/types.ts b/frontend/src/views/system-ops/dbms/components/types.ts new file mode 100644 index 0000000..05c76bb --- /dev/null +++ b/frontend/src/views/system-ops/dbms/components/types.ts @@ -0,0 +1,48 @@ +import type { Dbms } from '@/api/system/dbms/interface' +import type { DbmsBackupFormModel, DbmsRestoreFormModel } from '../utils/taskPayload' + +export interface DbmsConnectionQuery { + connectionName: string + schemaName: string +} + +export interface DbmsTaskQuery { + taskStatus: Dbms.TaskStatus | '' +} + +export interface DbmsFileQuery { + taskId: string +} + +export interface BackupSubmitPayload { + form: DbmsBackupFormModel +} + +export interface RestoreSubmitPayload { + form: DbmsRestoreFormModel +} + +export type DbmsWorkspaceSection = 'overview' | 'connections' | 'tables' | 'views' | 'backup' | 'tasks' + +export type DbmsToolbarCommand = + | 'connect' + | 'newConnection' + | 'newQuery' + | 'tables' + | 'views' + | 'functions' + | 'users' + | 'backup' + | 'automation' + | 'model' + | 'bi' + +export type DbmsTreeNodeType = 'connection' | 'schema' | 'tableGroup' | 'viewGroup' + +export interface DbmsTreeNode { + id: string + label: string + type: DbmsTreeNodeType + connection?: Dbms.ConnectionRecord + children?: DbmsTreeNode[] +} diff --git a/frontend/src/views/system-ops/dbms/contracts/check-connection-dialog-layout-contract.mjs b/frontend/src/views/system-ops/dbms/contracts/check-connection-dialog-layout-contract.mjs new file mode 100644 index 0000000..1d0ad0a --- /dev/null +++ b/frontend/src/views/system-ops/dbms/contracts/check-connection-dialog-layout-contract.mjs @@ -0,0 +1,47 @@ +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const currentDir = path.dirname(fileURLToPath(import.meta.url)) +const pageDir = path.join(currentDir, '..') +const dialogSource = fs.readFileSync(path.join(pageDir, 'components/DbmsConnectionDialog.vue'), 'utf8') +const payloadSource = fs.readFileSync(path.join(pageDir, 'utils/taskPayload.ts'), 'utf8') + +const checks = [ + ['dialog uses compact Navicat-like width', /width="630px"/.test(dialogSource)], + ['dialog uses shared runtime size class', /class="dbms-connection-size-dialog"/.test(dialogSource)], + ['dialog renders connection flow header', /class="connection-flow"/.test(dialogSource)], + ['dialog keeps Basic connection type visible', /model-value="Basic"/.test(dialogSource)], + [ + 'dialog exposes service name and SID radio choices', + /el-radio[\s\S]*SERVICE_NAME[\s\S]*el-radio[\s\S]*SID/.test(dialogSource) + ], + [ + 'dialog keeps only common connection fields in the visible form', + !/label="Schema"|label="Directory"|label="目录路径"|label="扩展配置"|label="备注"/.test(dialogSource) + ], + [ + 'dialog keeps selected database type for payload', + /buildConnectionPayload\(form,\s*selectedDbType\.value\)/.test(dialogSource) + ], + [ + 'dialog uses fixed viewport-relative height', + /:global\(\.dbms-connection-size-dialog\.el-dialog\)[\s\S]*height:\s*calc\(100vh - 170px\)/.test(dialogSource) + ], + ['dialog constrains height to viewport', /max-height:\s*calc\(100vh - 170px\)/.test(dialogSource)], + ['dialog avoids large fixed bottom whitespace', !/margin:\s*0 auto 178px/.test(dialogSource)], + [ + 'new Oracle connection defaults service name to ORCL', + /serviceName:\s*resolveText\(record\?\.serviceName\)\s*\|\|\s*'ORCL'/.test(payloadSource) + ] +] + +const failures = checks.filter(([, passed]) => !passed) + +if (failures.length) { + console.error('dbms connection dialog layout contract failed:') + failures.forEach(([message]) => console.error(`- ${message}`)) + process.exit(1) +} + +console.log('dbms connection dialog layout contract passed') diff --git a/frontend/src/views/system-ops/dbms/contracts/check-connection-type-dialog-contract.mjs b/frontend/src/views/system-ops/dbms/contracts/check-connection-type-dialog-contract.mjs new file mode 100644 index 0000000..5b42f19 --- /dev/null +++ b/frontend/src/views/system-ops/dbms/contracts/check-connection-type-dialog-contract.mjs @@ -0,0 +1,87 @@ +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const currentDir = path.dirname(fileURLToPath(import.meta.url)) +const pageDir = path.join(currentDir, '..') +const missingFiles = [] +const read = file => { + const filePath = path.join(pageDir, file) + if (!fs.existsSync(filePath)) { + missingFiles.push(file) + return '' + } + return fs.readFileSync(filePath, 'utf8') +} + +const files = { + page: 'index.vue', + selector: 'components/DbmsConnectionTypeDialog.vue', + connectionDialog: 'components/DbmsConnectionDialog.vue', + taskPayload: 'utils/taskPayload.ts', + apiTypes: '../../../api/system/dbms/interface/index.ts' +} + +const checks = [ + [ + 'page renders connection type dialog', + /\s*openConnectionTypeDialog\(\)/.test(read(files.page)) + ], + [ + 'new connection command opens type selector', + /\bnewConnection:\s*\(\)\s*=>\s*openConnectionTypeDialog\(\)/.test(read(files.page)) + ], + ['selector defaults to Oracle', /selectedType\s*=\s*ref\('ORACLE'\)/.test(read(files.selector))], + ['selector width matches connection form dialog', /width="630px"/.test(read(files.selector))], + ['selector uses shared runtime size class', /dbms-connection-size-dialog/.test(read(files.selector))], + ['selector uses own stretch class', /dbms-connection-type-size-dialog/.test(read(files.selector))], + ['selector height matches connection form dialog', /height:\s*calc\(100vh - 170px\)/.test(read(files.selector))], + [ + 'selector height constraint matches connection form dialog', + /max-height:\s*calc\(100vh - 170px\)/.test(read(files.selector)) + ], + [ + 'selector body stretches content area', + /dbms-connection-type-size-dialog \.el-dialog__body\)[\s\S]*display:\s*flex/.test(read(files.selector)) + ], + ['selector content fills dialog body', /connection-type-dialog[\s\S]*flex:\s*1/.test(read(files.selector))], + ['selector only exposes Oracle and MySQL', /type:\s*'ORACLE'[\s\S]*type:\s*'MYSQL'/.test(read(files.selector))], + ['selector keeps next action explicit', /emit\('next',\s*selectedType\.value\)/.test(read(files.selector))], + [ + 'page blocks MySQL until backend is available', + /if\s*\(dbType\s*===\s*'MYSQL'\)[\s\S]*MySQL 连接配置暂未接入/.test(read(files.page)) + ], + ['connection form displays selected database type', /selectedDbType/.test(read(files.connectionDialog))], + [ + 'connection form open accepts selected database type', + /open = \(nextMode: 'add' \| 'edit', record\?: Dbms\.ConnectionRecord, dbType: Dbms\.DbType = 'ORACLE'\)/.test( + read(files.connectionDialog) + ) + ], + [ + 'connection form payload uses selected database type', + /buildConnectionPayload\(form,\s*selectedDbType\.value\)/.test(read(files.connectionDialog)) + ], + [ + 'payload builder accepts database type', + /buildConnectionPayload[\s\S]*dbType: Dbms\.DbType = 'ORACLE'/.test(read(files.taskPayload)) + ], + ['api type allows MySQL selection', /export type DbType = 'ORACLE' \| 'MYSQL'/.test(read(files.apiTypes))] +] + +const failures = [ + ...missingFiles.map(file => [`required file exists: ${file}`, false]), + ...checks.filter(([, passed]) => !passed) +] + +if (failures.length) { + console.error('dbms connection type dialog contract failed:') + failures.forEach(([message]) => console.error(`- ${message}`)) + process.exit(1) +} + +console.log('dbms connection type dialog contract passed') diff --git a/frontend/src/views/system-ops/dbms/contracts/check-initial-api-contract.mjs b/frontend/src/views/system-ops/dbms/contracts/check-initial-api-contract.mjs new file mode 100644 index 0000000..d14f097 --- /dev/null +++ b/frontend/src/views/system-ops/dbms/contracts/check-initial-api-contract.mjs @@ -0,0 +1,29 @@ +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const currentDir = path.dirname(fileURLToPath(import.meta.url)) +const pageDir = path.join(currentDir, '..') +const read = file => fs.readFileSync(path.join(pageDir, file), 'utf8') + +const pageSource = read('index.vue') + +const onMountedBlock = pageSource.match(/onMounted\(\(\)\s*=>\s*\{[\s\S]*?\n\}\)/)?.[0] ?? '' + +const checks = [ + ['page should not auto load dbms overview on menu open', !onMountedBlock.includes('loadOverview()')], + ['page should not auto load dbms connections on menu open', !onMountedBlock.includes('loadConnections()')], + ['page should not auto load dbms tasks on menu open', !onMountedBlock.includes('loadTasks()')], + ['page should not auto load dbms files on menu open', !onMountedBlock.includes('loadFiles()')], + ['connection tree keeps manual refresh entry', / !passed) + +if (failures.length) { + console.error('dbms initial api contract failed:') + failures.forEach(([message]) => console.error(`- ${message}`)) + process.exit(1) +} + +console.log('dbms initial api contract passed') diff --git a/frontend/src/views/system-ops/dbms/contracts/check-workbench-layout-contract.mjs b/frontend/src/views/system-ops/dbms/contracts/check-workbench-layout-contract.mjs new file mode 100644 index 0000000..e2c0320 --- /dev/null +++ b/frontend/src/views/system-ops/dbms/contracts/check-workbench-layout-contract.mjs @@ -0,0 +1,38 @@ +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const currentDir = path.dirname(fileURLToPath(import.meta.url)) +const pageDir = path.join(currentDir, '..') +const read = file => fs.readFileSync(path.join(pageDir, file), 'utf8') + +const files = { + page: 'index.vue', + toolbar: 'components/DbmsToolbar.vue', + tree: 'components/DbmsConnectionTree.vue', + workspace: 'components/DbmsWorkspace.vue' +} + +const checks = [ + ['page renders dbms workbench shell', /class="dbms-workbench"/, read(files.page)], + ['page uses top toolbar', / !passed) + +if (failures.length) { + console.error('dbms workbench layout contract failed:') + failures.forEach(([message]) => console.error(`- ${message}`)) + process.exit(1) +} + +console.log('dbms workbench layout contract passed') diff --git a/frontend/src/views/system-ops/dbms/index.vue b/frontend/src/views/system-ops/dbms/index.vue new file mode 100644 index 0000000..a54b1a8 --- /dev/null +++ b/frontend/src/views/system-ops/dbms/index.vue @@ -0,0 +1,457 @@ + + + + + diff --git a/frontend/src/views/system-ops/dbms/utils/normalize.ts b/frontend/src/views/system-ops/dbms/utils/normalize.ts new file mode 100644 index 0000000..49c09cb --- /dev/null +++ b/frontend/src/views/system-ops/dbms/utils/normalize.ts @@ -0,0 +1,95 @@ +import type { TagProps } from 'element-plus' +import type { Dbms } from '@/api/system/dbms/interface' + +export const DELETE_CONFIRM_TEXT = '确认删除' +export const OVERWRITE_CONFIRM_TEXT = '确认覆盖' + +export const resolveText = (...values: unknown[]) => { + for (const value of values) { + if (value === null || value === undefined) continue + const text = String(value).trim() + if (text) return text + } + + return '' +} + +export const resolveNumber = (...values: unknown[]) => { + for (const value of values) { + if (value === null || value === undefined || value === '') continue + const parsed = Number(value) + if (Number.isFinite(parsed)) return parsed + } + + return 0 +} + +export const taskStatusMeta: Record = { + WAITING: { label: '等待中', type: 'info' }, + RUNNING: { label: '执行中', type: 'warning' }, + SUCCESS: { label: '成功', type: 'success' }, + FAIL: { label: '失败', type: 'danger' }, + FAILED: { label: '失败', type: 'danger' }, + CANCELLED: { label: '已取消', type: 'info' } +} + +export const connectionStatusMeta: Record = { + SUCCESS: { label: '成功', type: 'success' }, + FAIL: { label: '失败', type: 'danger' }, + FAILED: { label: '失败', type: 'danger' } +} + +export const backupModeLabels: Record = { + FULL_TABLE: '全表', + TIME_RANGE: '按时间范围', + SIZE_SPLIT: '按文件大小' +} + +export const restoreModeLabels: Record = { + SKIP: '跳过已存在', + APPEND: '追加', + TRUNCATE: '清空后导入', + REPLACE: '替换' +} + +export const operationTypeLabels: Record = { + BACKUP: '备份', + RESTORE: '恢复' +} + +export const getTaskStatusMeta = (status?: string | null) => { + return taskStatusMeta[resolveText(status)] ?? { label: resolveText(status) || '未知', type: 'info' as const } +} + +export const getConnectionStatusMeta = (status?: string | null) => { + return connectionStatusMeta[resolveText(status)] ?? { label: resolveText(status) || '未测试', type: 'info' as const } +} + +export const isTerminalTaskStatus = (status?: string | null) => { + return ['SUCCESS', 'FAIL', 'FAILED', 'CANCELLED'].includes(resolveText(status)) +} + +export const parseJsonArrayText = (value?: string | null) => { + const text = resolveText(value) + if (!text) return '' + + try { + const parsed = JSON.parse(text) + if (Array.isArray(parsed)) { + return parsed.join('、') + } + } catch { + return text + } + + return text +} + +export const formatFileSize = (value?: number | null) => { + const size = resolveNumber(value) + if (!size) return '--' + if (size < 1024) return `${size} B` + if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB` + if (size < 1024 * 1024 * 1024) return `${(size / 1024 / 1024).toFixed(2)} MB` + return `${(size / 1024 / 1024 / 1024).toFixed(2)} GB` +} diff --git a/frontend/src/views/system-ops/dbms/utils/taskPayload.ts b/frontend/src/views/system-ops/dbms/utils/taskPayload.ts new file mode 100644 index 0000000..49a3fac --- /dev/null +++ b/frontend/src/views/system-ops/dbms/utils/taskPayload.ts @@ -0,0 +1,176 @@ +import type { Dbms } from '@/api/system/dbms/interface' +import { DELETE_CONFIRM_TEXT, OVERWRITE_CONFIRM_TEXT, resolveText } from './normalize' + +export interface DbmsConnectionFormModel { + id: string + connectionName: string + host: string + port: number + connectType: Dbms.ConnectType + serviceName: string + sid: string + schemaName: string + username: string + password: string + savePassword: 0 | 1 + directoryName: string + directoryPath: string + extraConfigJson: string + remark: string +} + +export interface DbmsBackupFormModel { + backupStrategy: Dbms.BackupStrategy + schemaName: string + targetNames: string[] + backupMode: Dbms.BackupMode + timeColumn: string + timeRange: string[] + maxFileSizeMb: number | null + directoryName: string + temporaryPassword: string +} + +export interface DbmsRestoreFormModel { + backupFileId: string + restoreMode: Dbms.RestoreMode + targetSchemaName: string + temporaryPassword: string + overwriteConfirmText: string +} + +export const createConnectionForm = (record?: Partial | null): DbmsConnectionFormModel => ({ + id: resolveText(record?.id), + connectionName: resolveText(record?.connectionName), + host: resolveText(record?.host), + port: Number(record?.port) || 1521, + connectType: record?.connectType || 'SERVICE_NAME', + serviceName: resolveText(record?.serviceName) || 'ORCL', + sid: resolveText(record?.sid), + schemaName: resolveText(record?.schemaName), + username: resolveText(record?.username), + password: '', + savePassword: record?.savePassword === 1 ? 1 : 0, + directoryName: resolveText(record?.directoryName) || 'DATA_PUMP_DIR', + directoryPath: resolveText(record?.directoryPath), + extraConfigJson: resolveText(record?.extraConfigJson), + remark: resolveText(record?.remark) +}) + +export const createBackupForm = (): DbmsBackupFormModel => ({ + backupStrategy: 'DATA_PUMP', + schemaName: '', + targetNames: [], + backupMode: 'FULL_TABLE', + timeColumn: '', + timeRange: [], + maxFileSizeMb: 512, + directoryName: 'DATA_PUMP_DIR', + temporaryPassword: '' +}) + +export const createRestoreForm = (): DbmsRestoreFormModel => ({ + backupFileId: '', + restoreMode: 'SKIP', + targetSchemaName: '', + temporaryPassword: '', + overwriteConfirmText: '' +}) + +export const buildConnectionPayload = ( + form: DbmsConnectionFormModel, + dbType: Dbms.DbType = 'ORACLE' +): Dbms.ConnectionPayload => ({ + id: form.id || undefined, + connectionName: form.connectionName.trim(), + dbType, + host: form.host.trim(), + port: Number(form.port), + connectType: form.connectType, + serviceName: form.connectType === 'SERVICE_NAME' ? form.serviceName.trim() || null : null, + sid: form.connectType === 'SID' ? form.sid.trim() || null : null, + schemaName: form.schemaName.trim() || null, + username: form.username.trim(), + password: form.password.trim() || null, + savePassword: form.savePassword, + directoryName: form.directoryName.trim() || null, + directoryPath: form.directoryPath.trim() || null, + extraConfigJson: form.extraConfigJson.trim() || null, + remark: form.remark.trim() || null +}) + +export const buildBackupPayload = ( + connection: Dbms.ConnectionRecord, + form: DbmsBackupFormModel +): Dbms.CreateBackupParams => ({ + connectionId: connection.id, + backupStrategy: form.backupStrategy, + schemaName: form.schemaName.trim() || connection.schemaName || undefined, + targetNames: [...form.targetNames], + backupMode: form.backupMode, + timeColumn: form.backupMode === 'TIME_RANGE' ? form.timeColumn.trim() || null : null, + startTime: form.backupMode === 'TIME_RANGE' ? form.timeRange[0] || null : null, + endTime: form.backupMode === 'TIME_RANGE' ? form.timeRange[1] || null : null, + maxFileSizeMb: form.backupMode === 'SIZE_SPLIT' ? form.maxFileSizeMb : null, + directoryName: form.directoryName.trim() || connection.directoryName || null, + temporaryPassword: form.temporaryPassword.trim() || undefined +}) + +export const buildRestorePayload = ( + connection: Dbms.ConnectionRecord, + form: DbmsRestoreFormModel +): Dbms.CreateRestoreParams => { + const overwriteConfirmText = ['TRUNCATE', 'REPLACE'].includes(form.restoreMode) + ? form.overwriteConfirmText.trim() + : null + + return { + connectionId: connection.id, + backupFileId: form.backupFileId, + restoreMode: form.restoreMode, + targetSchemaName: form.targetSchemaName.trim() || connection.schemaName || undefined, + temporaryPassword: form.temporaryPassword.trim() || undefined, + overwriteConfirmText + } +} + +export const buildTaskListParams = ( + pageNum: number, + pageSize: number, + connectionId?: string, + taskStatus?: Dbms.TaskStatus | '' +): Dbms.TaskListParams => ({ + pageNum, + pageSize, + connectionId: connectionId || undefined, + taskStatus: taskStatus || undefined +}) + +export const buildFileListParams = ( + pageNum: number, + pageSize: number, + connectionId?: string, + taskId?: string +): Dbms.FileListParams => ({ + pageNum, + pageSize, + connectionId: connectionId || undefined, + taskId: taskId || undefined, + backupStrategy: 'DATA_PUMP' +}) + +export const buildDeleteBackupFilePayload = (backupFileId: string): Dbms.DeleteBackupFileParams => ({ + backupFileId, + confirmText: DELETE_CONFIRM_TEXT +}) + +export const buildDeleteTaskPayload = (taskId: string): Dbms.DeleteTaskParams => ({ + taskId, + confirmText: DELETE_CONFIRM_TEXT +}) + +export const isOverwriteRestoreMode = (restoreMode: Dbms.RestoreMode) => { + return ['TRUNCATE', 'REPLACE'].includes(restoreMode) +} + +export const isOverwriteConfirmMatched = (confirmText: string) => confirmText.trim() === OVERWRITE_CONFIRM_TEXT diff --git a/frontend/src/views/tools/addLedger/components/EquipmentForm.vue b/frontend/src/views/tools/addLedger/components/EquipmentForm.vue index 711ee25..cc4c4ae 100644 --- a/frontend/src/views/tools/addLedger/components/EquipmentForm.vue +++ b/frontend/src/views/tools/addLedger/components/EquipmentForm.vue @@ -4,11 +4,22 @@
装置配置
-
- 保存装置 - - 删除装置 +
+ + {{ unitActionLabel }} +
@@ -70,7 +81,7 @@