feat(auth): 统一数据库运维菜单路由并添加装置单位及监测点限值配置功能
- 统一数据库监控菜单路径到 /system-ops/dbms 入口 - 添加 isDbmsMenu 函数处理多种数据库菜单路径匹配 - 在动态路由中增加多个数据库监控路径的重定向规则 - 添加设备单位配置功能包括新增 EquipmentUnitForm 接口定义 - 添加监测点限值配置功能包括新增 OverlimitDetail 接口定义 - 在装置表单中添加单位配置按钮并集成单位调试功能 - 在监测点表单中添加限值配置按钮并集成限值调试功能 - 添加电压等级变更时的默认容量和变比同步逻辑 - 配置监测点表单中的线路类型选择选项 - 添加装置表单中比率输入组的高度紧凑样式 - 新增数据库运维静态路由配置和别名支持
This commit is contained in:
62
frontend/src/api/system/dbms/index.ts
Normal file
62
frontend/src/api/system/dbms/index.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import http from '@/api'
|
||||||
|
import type { Dbms } from '@/api/system/dbms/interface'
|
||||||
|
|
||||||
|
export const getDbmsOverview = () => {
|
||||||
|
return http.get<Dbms.Overview>('/database/overview', {}, { loading: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDbmsConnectionList = (params: Dbms.ConnectionListParams) => {
|
||||||
|
return http.post<Dbms.ConnectionPageData>('/database/connections/list', params, { loading: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addDbmsConnection = (params: Dbms.ConnectionPayload) => {
|
||||||
|
return http.post<boolean>('/database/connections/add', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateDbmsConnection = (params: Dbms.ConnectionPayload) => {
|
||||||
|
return http.post<boolean>('/database/connections/update', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteDbmsConnection = (params: Dbms.DeleteConnectionParams) => {
|
||||||
|
return http.post<boolean>('/database/connections/delete', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const testDbmsConnection = (params: Dbms.TestConnectionParams) => {
|
||||||
|
return http.post<Dbms.TestConnectionResult>('/database/connections/test', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDbmsTableList = (params: Dbms.TableListParams) => {
|
||||||
|
return http.post<Dbms.TableRecord[]>('/database/connections/tables', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createDbmsBackupTask = (params: Dbms.CreateBackupParams) => {
|
||||||
|
return http.post<Dbms.TaskCreateResult>('/database/backups/create', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDbmsBackupTaskList = (params: Dbms.TaskListParams) => {
|
||||||
|
return http.post<Dbms.TaskPageData>('/database/backups/tasks/list', params, { loading: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDbmsBackupTaskStatus = (taskId: string) => {
|
||||||
|
return http.get<Dbms.TaskRecord>('/database/backups/tasks/status', { taskId }, { loading: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDbmsBackupFileList = (params: Dbms.FileListParams) => {
|
||||||
|
return http.post<Dbms.BackupFilePageData>('/database/backups/files/list', params, { loading: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createDbmsRestoreTask = (params: Dbms.CreateRestoreParams) => {
|
||||||
|
return http.post<Dbms.TaskCreateResult>('/database/restores/create', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDbmsRestoreTaskStatus = (taskId: string) => {
|
||||||
|
return http.get<Dbms.TaskRecord>('/database/restores/tasks/status', { taskId }, { loading: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteDbmsBackupFile = (params: Dbms.DeleteBackupFileParams) => {
|
||||||
|
return http.post<boolean>('/database/delete/backup-file', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteDbmsTask = (params: Dbms.DeleteTaskParams) => {
|
||||||
|
return http.post<boolean>('/database/delete/task', params)
|
||||||
|
}
|
||||||
187
frontend/src/api/system/dbms/interface/index.ts
Normal file
187
frontend/src/api/system/dbms/interface/index.ts
Normal file
@@ -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<ConnectionRecord> {}
|
||||||
|
export interface TaskPageData extends ResPage<TaskRecord> {}
|
||||||
|
export interface BackupFilePageData extends ResPage<BackupFileRecord> {}
|
||||||
|
}
|
||||||
@@ -63,6 +63,7 @@ const toAddLedgerLinePayload = (params: AddLedger.LineForm) => {
|
|||||||
basicCapacity: params.basic_capacity,
|
basicCapacity: params.basic_capacity,
|
||||||
protocolCapacity: params.protocol_capacity,
|
protocolCapacity: params.protocol_capacity,
|
||||||
devCapacity: params.dev_capacity,
|
devCapacity: params.dev_capacity,
|
||||||
|
lineType: params.lineType,
|
||||||
monitorObj: params.monitor_obj,
|
monitorObj: params.monitor_obj,
|
||||||
isGovern: params.is_govern,
|
isGovern: params.is_govern,
|
||||||
monitorUser: params.monitor_user,
|
monitorUser: params.monitor_user,
|
||||||
@@ -172,6 +173,14 @@ export const saveAddLedgerEquipment = (params: AddLedger.EquipmentForm) => {
|
|||||||
return requestAddLedger<AddLedger.EquipmentForm>('post', '/equipment/save', toAddLedgerEquipmentPayload(params))
|
return requestAddLedger<AddLedger.EquipmentForm>('post', '/equipment/save', toAddLedgerEquipmentPayload(params))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getAddLedgerEquipmentUnit = (params: { devId: string }) => {
|
||||||
|
return requestAddLedger<AddLedger.EquipmentUnitForm>('get', '/equipment/unit', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const saveAddLedgerEquipmentUnit = (params: AddLedger.EquipmentUnitForm) => {
|
||||||
|
return requestAddLedger<AddLedger.EquipmentUnitForm>('post', '/equipment/unit/save', params)
|
||||||
|
}
|
||||||
|
|
||||||
export const saveAddLedgerLine = (params: AddLedger.LineForm) => {
|
export const saveAddLedgerLine = (params: AddLedger.LineForm) => {
|
||||||
return requestAddLedger<AddLedger.LineForm>('post', '/line/save', toAddLedgerLinePayload(params))
|
return requestAddLedger<AddLedger.LineForm>('post', '/line/save', toAddLedgerLinePayload(params))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,48 @@ export namespace AddLedger {
|
|||||||
upgrade?: number
|
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 {
|
export interface LineForm {
|
||||||
id?: string
|
id?: string
|
||||||
line_id?: string
|
line_id?: string
|
||||||
@@ -85,10 +127,12 @@ export namespace AddLedger {
|
|||||||
basic_capacity?: number
|
basic_capacity?: number
|
||||||
protocol_capacity?: number
|
protocol_capacity?: number
|
||||||
dev_capacity?: number
|
dev_capacity?: number
|
||||||
|
lineType?: number
|
||||||
monitor_obj?: string
|
monitor_obj?: string
|
||||||
is_govern?: number
|
is_govern?: number
|
||||||
monitor_user?: string
|
monitor_user?: string
|
||||||
is_important?: number
|
is_important?: number
|
||||||
|
overlimit?: OverlimitDetail
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NodeDetail = EngineeringForm | ProjectForm | EquipmentForm | LineForm
|
export type NodeDetail = EngineeringForm | ProjectForm | EquipmentForm | LineForm
|
||||||
|
|||||||
55
frontend/src/routers/modules/check-dbms-route-contract.mjs
Normal file
55
frontend/src/routers/modules/check-dbms-route-contract.mjs
Normal file
@@ -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')
|
||||||
@@ -25,7 +25,16 @@ const COMPONENT_PATH_ALIASES: Record<string, string> = {
|
|||||||
'/steady/steady-trend': '/steady/steadyTrend',
|
'/steady/steady-trend': '/steady/steadyTrend',
|
||||||
'/steady/steady-trend/index': '/steady/steadyTrend/index',
|
'/steady/steady-trend/index': '/steady/steadyTrend/index',
|
||||||
'/steady/check-square': '/steady/checksquare',
|
'/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([
|
const STATIC_ROUTE_NAMES = new Set([
|
||||||
'layout',
|
'layout',
|
||||||
@@ -42,6 +51,7 @@ const STATIC_ROUTE_NAMES = new Set([
|
|||||||
'checksquare',
|
'checksquare',
|
||||||
'systemMonitor',
|
'systemMonitor',
|
||||||
'diskMonitor',
|
'diskMonitor',
|
||||||
|
'systemOpsDbms',
|
||||||
'403',
|
'403',
|
||||||
'404',
|
'404',
|
||||||
'500'
|
'500'
|
||||||
|
|||||||
@@ -198,6 +198,25 @@ export const staticRouter: RouteRecordRaw[] = [
|
|||||||
title: '磁盘监控'
|
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(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
component: () => import('@/components/ErrorMessage/404.vue')
|
component: () => import('@/components/ErrorMessage/404.vue')
|
||||||
|
|||||||
@@ -156,6 +156,13 @@ function normalizeBusinessMenu(menu: any): any {
|
|||||||
menu.component = '@/views/steady/checksquare/index.vue'
|
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
|
return menu
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,10 +218,25 @@ function isChecksquareMenu(menu: any): boolean {
|
|||||||
return title.includes('数据验证')
|
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 {
|
export function resolveBusinessMenuPath(menu: Menu.MenuOptions): string {
|
||||||
if (isEventListMenu(menu)) return '/eventList/index'
|
if (isEventListMenu(menu)) return '/eventList/index'
|
||||||
if (isChecksquareMenu(menu)) return '/checksquare/index'
|
if (isChecksquareMenu(menu)) return '/checksquare/index'
|
||||||
if (isSteadyTrendMenu(menu)) return '/steadyTrend/index'
|
if (isSteadyTrendMenu(menu)) return '/steadyTrend/index'
|
||||||
|
if (isDbmsMenu(menu)) return '/system-ops/dbms'
|
||||||
return isSteadyDataViewMenu(menu) ? '/steadyDataView/index' : menu.path
|
return isSteadyDataViewMenu(menu) ? '/steadyDataView/index' : menu.path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
class="dbms-connection-size-dialog"
|
||||||
|
:title="dialogTitle"
|
||||||
|
width="630px"
|
||||||
|
destroy-on-close
|
||||||
|
@closed="resetForm"
|
||||||
|
>
|
||||||
|
<div class="connection-flow">
|
||||||
|
<div class="flow-node">
|
||||||
|
<el-icon><Connection /></el-icon>
|
||||||
|
<span>数据库连接</span>
|
||||||
|
</div>
|
||||||
|
<div class="flow-line"></div>
|
||||||
|
<div class="flow-node">
|
||||||
|
<el-icon><Coin /></el-icon>
|
||||||
|
<span>{{ selectedDbType }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form ref="formRef" :model="form" :rules="formRules" label-width="96px" class="connection-form">
|
||||||
|
<div class="form-body">
|
||||||
|
<el-form-item label="连接名称" prop="connectionName">
|
||||||
|
<el-input v-model="form.connectionName" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="连接类型">
|
||||||
|
<el-select model-value="Basic" disabled>
|
||||||
|
<el-option label="Basic" value="Basic" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="主机地址" prop="host">
|
||||||
|
<el-input v-model="form.host" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="端口" prop="port">
|
||||||
|
<el-input v-model.number="form.port" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="form.connectType === 'SERVICE_NAME'" label="服务名" prop="serviceName">
|
||||||
|
<el-input v-model="form.serviceName" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-else label="SID" prop="sid">
|
||||||
|
<el-input v-model="form.sid" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label=" ">
|
||||||
|
<el-radio-group v-model="form.connectType">
|
||||||
|
<el-radio value="SERVICE_NAME">服务名称</el-radio>
|
||||||
|
<el-radio value="SID">SID</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="用户名" prop="username">
|
||||||
|
<el-input v-model="form.username" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="密码">
|
||||||
|
<el-input v-model="form.password" type="password" show-password clearable />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="保存密码">
|
||||||
|
<el-checkbox v-model="savePasswordChecked" />
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button type="primary" plain :icon="Connection" @click="handleTest">测试连接</el-button>
|
||||||
|
<div class="footer-actions">
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, reactive, ref } from 'vue'
|
||||||
|
import { Coin, Connection } from '@element-plus/icons-vue'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import type { Dbms } from '@/api/system/dbms/interface'
|
||||||
|
import { buildConnectionPayload, createConnectionForm, type DbmsConnectionFormModel } from '../utils/taskPayload'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'DbmsConnectionDialog'
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
save: [payload: Dbms.ConnectionPayload]
|
||||||
|
test: [payload: Dbms.TestConnectionParams]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const mode = ref<'add' | 'edit'>('add')
|
||||||
|
const selectedDbType = ref<Dbms.DbType>('ORACLE')
|
||||||
|
const form = reactive<DbmsConnectionFormModel>(createConnectionForm())
|
||||||
|
|
||||||
|
const dialogTitle = computed(() => (mode.value === 'add' ? '新增数据库连接' : '编辑数据库连接'))
|
||||||
|
const savePasswordChecked = computed({
|
||||||
|
get: () => form.savePassword === 1,
|
||||||
|
set: value => {
|
||||||
|
form.savePassword = value ? 1 : 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const formRules: FormRules<DbmsConnectionFormModel> = {
|
||||||
|
connectionName: [{ required: true, message: '请输入连接名称', trigger: 'blur' }],
|
||||||
|
host: [{ required: true, message: '请输入主机地址', trigger: 'blur' }],
|
||||||
|
port: [{ required: true, message: '请输入端口', trigger: 'blur' }],
|
||||||
|
connectType: [{ required: true, message: '请选择连接类型', trigger: 'change' }],
|
||||||
|
serviceName: [{ required: true, message: '请输入服务名', trigger: 'blur' }],
|
||||||
|
sid: [{ required: true, message: '请输入 SID', trigger: 'blur' }],
|
||||||
|
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyForm = (nextForm: DbmsConnectionFormModel) => {
|
||||||
|
Object.assign(form, nextForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
applyForm(createConnectionForm())
|
||||||
|
selectedDbType.value = 'ORACLE'
|
||||||
|
formRef.value?.clearValidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = (nextMode: 'add' | 'edit', record?: Dbms.ConnectionRecord, dbType: Dbms.DbType = 'ORACLE') => {
|
||||||
|
mode.value = nextMode
|
||||||
|
selectedDbType.value = record?.dbType || dbType
|
||||||
|
applyForm(createConnectionForm(record))
|
||||||
|
visible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const validate = async () => Boolean(await formRef.value?.validate())
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!(await validate())) return
|
||||||
|
emit('save', buildConnectionPayload(form, selectedDbType.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTest = async () => {
|
||||||
|
if (!(await validate())) return
|
||||||
|
const payload = buildConnectionPayload(form, selectedDbType.value)
|
||||||
|
emit('test', form.id ? { connectionId: form.id, temporaryPassword: form.password.trim() } : { connection: payload })
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open,
|
||||||
|
close: () => {
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:global(.dbms-connection-size-dialog.el-dialog) {
|
||||||
|
height: calc(100vh - 170px);
|
||||||
|
max-height: calc(100vh - 170px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dbms-connection-size-dialog .el-dialog__body) {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-flow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 18px;
|
||||||
|
padding: 4px 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-node {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 96px;
|
||||||
|
color: #111827;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-node .el-icon {
|
||||||
|
color: #8c8c8c;
|
||||||
|
font-size: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-line {
|
||||||
|
width: 114px;
|
||||||
|
height: 1px;
|
||||||
|
background: #bdbdbd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-form {
|
||||||
|
max-width: 480px;
|
||||||
|
margin: 0 auto 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-body {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-body :deep(.el-select),
|
||||||
|
.form-body :deep(.el-input-number) {
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-body :deep(.el-input) {
|
||||||
|
max-width: 330px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-body :deep(.el-form-item) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.connection-form {
|
||||||
|
max-width: none;
|
||||||
|
margin-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-body :deep(.el-input) {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-line {
|
||||||
|
width: 72px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
<template>
|
||||||
|
<section class="card dbms-tree-card" :class="{ 'is-collapsed': collapsed }">
|
||||||
|
<div v-show="collapsed" class="collapsed-panel">
|
||||||
|
<el-tooltip content="展开连接树" placement="right">
|
||||||
|
<el-button class="panel-toggle" type="primary" :icon="ArrowRight" circle @click="emit('toggle')" />
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="!collapsed" class="expanded-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span class="panel-title">连接树</span>
|
||||||
|
<el-tooltip content="收起连接树" placement="top">
|
||||||
|
<el-button class="panel-toggle" type="primary" :icon="ArrowLeft" circle @click="emit('toggle')" />
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tree-search-row">
|
||||||
|
<el-input v-model="keyword" clearable placeholder="搜索连接、Schema" />
|
||||||
|
<el-button :icon="Refresh" circle :loading="loading" @click="emit('refresh')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-scrollbar class="tree-scrollbar">
|
||||||
|
<el-tree
|
||||||
|
class="dbms-tree"
|
||||||
|
:data="filteredTreeData"
|
||||||
|
node-key="id"
|
||||||
|
default-expand-all
|
||||||
|
highlight-current
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
:props="{ label: 'label', children: 'children' }"
|
||||||
|
@node-click="handleNodeClick"
|
||||||
|
>
|
||||||
|
<template #default="{ data }">
|
||||||
|
<div class="tree-node" :class="`is-${data.type}`">
|
||||||
|
<span class="node-main">
|
||||||
|
<el-icon :class="['node-icon', `is-${data.type}`]">
|
||||||
|
<component :is="resolveNodeIcon(data.type)" />
|
||||||
|
</el-icon>
|
||||||
|
<span class="node-name">{{ data.label }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import type { Component } from 'vue'
|
||||||
|
import { ArrowLeft, ArrowRight, Coin, Collection, Folder, Refresh, View } from '@element-plus/icons-vue'
|
||||||
|
import type { Dbms } from '@/api/system/dbms/interface'
|
||||||
|
import type { DbmsTreeNode, DbmsTreeNodeType, DbmsWorkspaceSection } from './types'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'DbmsConnectionTree'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
connections: Dbms.ConnectionRecord[]
|
||||||
|
loading: boolean
|
||||||
|
collapsed: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
toggle: []
|
||||||
|
refresh: []
|
||||||
|
selectConnection: [connection: Dbms.ConnectionRecord]
|
||||||
|
selectSection: [section: DbmsWorkspaceSection]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const keyword = ref('')
|
||||||
|
const nodeIcons: Record<DbmsTreeNodeType, Component> = {
|
||||||
|
connection: Coin,
|
||||||
|
schema: Collection,
|
||||||
|
tableGroup: Folder,
|
||||||
|
viewGroup: View
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveNodeIcon = (type: DbmsTreeNodeType) => nodeIcons[type]
|
||||||
|
|
||||||
|
const treeData = computed<DbmsTreeNode[]>(() =>
|
||||||
|
props.connections.map(connection => {
|
||||||
|
const schemaLabel = connection.schemaName || connection.username || '默认 Schema'
|
||||||
|
return {
|
||||||
|
id: `connection-${connection.id}`,
|
||||||
|
label: connection.connectionName,
|
||||||
|
type: 'connection',
|
||||||
|
connection,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: `schema-${connection.id}`,
|
||||||
|
label: schemaLabel,
|
||||||
|
type: 'schema',
|
||||||
|
connection,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: `tables-${connection.id}`,
|
||||||
|
label: '表',
|
||||||
|
type: 'tableGroup',
|
||||||
|
connection
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `views-${connection.id}`,
|
||||||
|
label: '视图',
|
||||||
|
type: 'viewGroup',
|
||||||
|
connection
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const filterTree = (nodes: DbmsTreeNode[], value: string): DbmsTreeNode[] => {
|
||||||
|
if (!value) return nodes
|
||||||
|
const normalizedValue = value.toLowerCase()
|
||||||
|
return nodes
|
||||||
|
.map(node => {
|
||||||
|
const children = node.children ? filterTree(node.children, value) : []
|
||||||
|
if (node.label.toLowerCase().includes(normalizedValue) || children.length) {
|
||||||
|
return { ...node, children }
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
.filter(Boolean) as DbmsTreeNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredTreeData = computed(() => filterTree(treeData.value, keyword.value.trim()))
|
||||||
|
|
||||||
|
const handleNodeClick = (node: DbmsTreeNode) => {
|
||||||
|
if (node.connection) {
|
||||||
|
emit('selectConnection', node.connection)
|
||||||
|
}
|
||||||
|
if (node.type === 'tableGroup') {
|
||||||
|
emit('selectSection', 'tables')
|
||||||
|
} else if (node.type === 'viewGroup') {
|
||||||
|
emit('selectSection', 'views')
|
||||||
|
} else {
|
||||||
|
emit('selectSection', 'overview')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.dbms-tree-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbms-tree-card:not(.is-collapsed) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbms-tree-card.is-collapsed {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 4;
|
||||||
|
align-items: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: visible;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expanded-panel {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsed-panel,
|
||||||
|
.panel-header,
|
||||||
|
.tree-search-row,
|
||||||
|
.tree-node,
|
||||||
|
.node-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsed-panel {
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header,
|
||||||
|
.tree-node {
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-search-row {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-scrollbar {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-main {
|
||||||
|
min-width: 0;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-icon {
|
||||||
|
flex: none;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-icon.is-connection {
|
||||||
|
color: var(--el-color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-icon.is-schema {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-icon.is-tableGroup {
|
||||||
|
color: var(--el-color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-icon.is-viewGroup {
|
||||||
|
color: var(--el-color-info);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
class="dbms-connection-size-dialog dbms-connection-type-size-dialog"
|
||||||
|
title="新增连接"
|
||||||
|
width="630px"
|
||||||
|
destroy-on-close
|
||||||
|
@closed="resetSelection"
|
||||||
|
>
|
||||||
|
<div class="connection-type-dialog">
|
||||||
|
<aside class="type-filter">
|
||||||
|
<div class="filter-title">连接筛选</div>
|
||||||
|
<el-checkbox-group v-model="checkedTypes">
|
||||||
|
<el-checkbox v-for="item in dbTypeOptions" :key="item.type" :label="item.type">
|
||||||
|
<span class="filter-option">
|
||||||
|
<span class="db-icon" :class="item.iconClass">{{ item.shortName }}</span>
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
</span>
|
||||||
|
</el-checkbox>
|
||||||
|
</el-checkbox-group>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section class="type-content">
|
||||||
|
<div class="type-header">
|
||||||
|
<span>选择一个连接类型:</span>
|
||||||
|
<el-input v-model="keyword" placeholder="搜索" clearable />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="type-section-title">全部</div>
|
||||||
|
<div class="type-grid">
|
||||||
|
<button
|
||||||
|
v-for="item in filteredOptions"
|
||||||
|
:key="item.type"
|
||||||
|
class="type-card"
|
||||||
|
type="button"
|
||||||
|
:class="{ 'is-active': selectedType === item.type }"
|
||||||
|
@click="selectedType = item.type"
|
||||||
|
>
|
||||||
|
<span class="db-logo" :class="item.iconClass">{{ item.shortName }}</span>
|
||||||
|
<span class="db-name">{{ item.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleNext">下一步</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import type { Dbms } from '@/api/system/dbms/interface'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'DbmsConnectionTypeDialog'
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
next: [dbType: Dbms.DbType]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const dbTypeOptions: Array<{
|
||||||
|
type: Dbms.DbType
|
||||||
|
label: string
|
||||||
|
shortName: string
|
||||||
|
iconClass: string
|
||||||
|
}> = [
|
||||||
|
{ type: 'ORACLE', label: 'Oracle', shortName: 'O', iconClass: 'is-oracle' },
|
||||||
|
{ type: 'MYSQL', label: 'MySQL', shortName: 'My', iconClass: 'is-mysql' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const selectedType = ref<Dbms.DbType>('ORACLE')
|
||||||
|
const checkedTypes = ref<Dbms.DbType[]>(['ORACLE', 'MYSQL'])
|
||||||
|
const keyword = ref('')
|
||||||
|
|
||||||
|
const filteredOptions = computed(() => {
|
||||||
|
const normalizedKeyword = keyword.value.trim().toLowerCase()
|
||||||
|
return dbTypeOptions.filter(item => {
|
||||||
|
const isChecked = checkedTypes.value.includes(item.type)
|
||||||
|
const isMatched = !normalizedKeyword || item.label.toLowerCase().includes(normalizedKeyword)
|
||||||
|
return isChecked && isMatched
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const resetSelection = () => {
|
||||||
|
selectedType.value = 'ORACLE'
|
||||||
|
checkedTypes.value = ['ORACLE', 'MYSQL']
|
||||||
|
keyword.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
emit('next', selectedType.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open: () => {
|
||||||
|
visible.value = true
|
||||||
|
},
|
||||||
|
close: () => {
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:global(.dbms-connection-size-dialog.el-dialog) {
|
||||||
|
height: calc(100vh - 170px);
|
||||||
|
max-height: calc(100vh - 170px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dbms-connection-type-size-dialog .el-dialog__body) {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-type-dialog {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 180px minmax(0, 1fr);
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-filter {
|
||||||
|
padding: 14px;
|
||||||
|
border-right: 1px solid var(--el-border-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-title,
|
||||||
|
.type-section-title {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-option {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.db-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-content {
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-header :deep(.el-input) {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(108px, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
max-width: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-card {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 92px;
|
||||||
|
padding: 12px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-card:hover,
|
||||||
|
.type-card.is-active {
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.db-logo {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-oracle {
|
||||||
|
background: #f80036;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-mysql {
|
||||||
|
background: #00758f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.db-name {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.connection-type-dialog {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-filter {
|
||||||
|
border-right: 0;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-header {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-header :deep(.el-input) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
<template>
|
||||||
|
<section class="table-main card dbms-connection-card">
|
||||||
|
<div class="table-header">
|
||||||
|
<div class="header-button-lf">
|
||||||
|
<el-button type="primary" :icon="CirclePlus" @click="emit('add')">新增连接</el-button>
|
||||||
|
<el-button type="primary" plain :icon="Connection" :disabled="!selectedRow" @click="handleTestSelected">
|
||||||
|
测试连接
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" plain :icon="Delete" :disabled="!selectedRow" @click="handleDeleteSelected">
|
||||||
|
删除连接
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-button-ri">
|
||||||
|
<el-input
|
||||||
|
v-model="localQuery.connectionName"
|
||||||
|
class="query-input"
|
||||||
|
placeholder="连接名称"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="emitQuery"
|
||||||
|
@clear="emitQuery"
|
||||||
|
/>
|
||||||
|
<el-input
|
||||||
|
v-model="localQuery.schemaName"
|
||||||
|
class="query-input"
|
||||||
|
placeholder="Schema"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="emitQuery"
|
||||||
|
@clear="emitQuery"
|
||||||
|
/>
|
||||||
|
<el-button :icon="Search" @click="emitQuery">查询</el-button>
|
||||||
|
<el-button circle :icon="Refresh" @click="emit('refresh')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-body">
|
||||||
|
<el-table
|
||||||
|
:data="rows"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
height="100%"
|
||||||
|
highlight-current-row
|
||||||
|
v-loading="loading"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
@row-dblclick="row => emit('edit', row)"
|
||||||
|
>
|
||||||
|
<el-table-column prop="connectionName" label="连接名称" min-width="150" fixed="left" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="host" label="主机" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="port" label="端口" width="90" align="center" />
|
||||||
|
<el-table-column prop="connectType" label="连接类型" min-width="120" align="center" />
|
||||||
|
<el-table-column prop="schemaName" label="Schema" min-width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="username" label="用户名" min-width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="directoryName" label="Directory" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column label="保存密码" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.savePassword === 1 ? 'success' : 'info'" effect="light">
|
||||||
|
{{ row.savePassword === 1 ? '是' : '否' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="测试状态" width="110" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getConnectionStatusMeta(row.lastTestStatus).type" effect="light">
|
||||||
|
{{ getConnectionStatusMeta(row.lastTestStatus).label }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="lastTestMessage" label="测试消息" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link :icon="EditPen" @click="emit('edit', row)">编辑</el-button>
|
||||||
|
<el-button type="primary" link :icon="Connection" @click="emit('test', row)">测试</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-footer">
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
layout="total, prev, pager, next, sizes"
|
||||||
|
:total="total"
|
||||||
|
:page-size="pageSize"
|
||||||
|
:current-page="pageNum"
|
||||||
|
@current-change="page => emit('page-change', page)"
|
||||||
|
@size-change="size => emit('size-change', size)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref, watch } from 'vue'
|
||||||
|
import { CirclePlus, Connection, Delete, EditPen, Refresh, Search } from '@element-plus/icons-vue'
|
||||||
|
import type { Dbms } from '@/api/system/dbms/interface'
|
||||||
|
import { getConnectionStatusMeta } from '../utils/normalize'
|
||||||
|
import type { DbmsConnectionQuery } from './types'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'DbmsOperationTable'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
rows: Dbms.ConnectionRecord[]
|
||||||
|
query: DbmsConnectionQuery
|
||||||
|
loading: boolean
|
||||||
|
total: number
|
||||||
|
pageNum: number
|
||||||
|
pageSize: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
add: []
|
||||||
|
edit: [row: Dbms.ConnectionRecord]
|
||||||
|
delete: [row: Dbms.ConnectionRecord]
|
||||||
|
test: [row: Dbms.ConnectionRecord]
|
||||||
|
select: [row: Dbms.ConnectionRecord | null]
|
||||||
|
refresh: []
|
||||||
|
search: [query: DbmsConnectionQuery]
|
||||||
|
'page-change': [page: number]
|
||||||
|
'size-change': [size: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const selectedRow = ref<Dbms.ConnectionRecord | null>(null)
|
||||||
|
const localQuery = reactive<DbmsConnectionQuery>({
|
||||||
|
connectionName: props.query.connectionName,
|
||||||
|
schemaName: props.query.schemaName
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.query,
|
||||||
|
query => {
|
||||||
|
localQuery.connectionName = query.connectionName
|
||||||
|
localQuery.schemaName = query.schemaName
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const emitQuery = () => {
|
||||||
|
emit('search', {
|
||||||
|
connectionName: localQuery.connectionName,
|
||||||
|
schemaName: localQuery.schemaName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (row?: Dbms.ConnectionRecord) => {
|
||||||
|
selectedRow.value = row || null
|
||||||
|
emit('select', selectedRow.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTestSelected = () => {
|
||||||
|
if (selectedRow.value) emit('test', selectedRow.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteSelected = () => {
|
||||||
|
if (selectedRow.value) emit('delete', selectedRow.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.dbms-connection-card {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-button-lf,
|
||||||
|
.header-button-ri {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-input {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
margin-top: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex: none;
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1280px) {
|
||||||
|
.table-header {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-button-ri {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
298
frontend/src/views/system-ops/dbms/components/DbmsTaskPanel.vue
Normal file
298
frontend/src/views/system-ops/dbms/components/DbmsTaskPanel.vue
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
<template>
|
||||||
|
<section class="card dbms-task-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="selected-info">
|
||||||
|
<div class="section-title">备份与恢复</div>
|
||||||
|
<div class="section-description">
|
||||||
|
当前连接:{{ selectedConnection?.connectionName || '未选择' }}
|
||||||
|
<span v-if="selectedConnection"> / {{ selectedConnection.host }}:{{ selectedConnection.port }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" plain :icon="Refresh" :disabled="!selectedConnection" @click="emit('load-tables')">
|
||||||
|
加载表
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<el-tabs v-model="activeTab" class="operation-tabs">
|
||||||
|
<el-tab-pane label="创建备份" name="backup">
|
||||||
|
<el-form ref="backupFormRef" :model="backupForm" :rules="backupRules" label-width="112px">
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="备份策略">
|
||||||
|
<el-select v-model="backupForm.backupStrategy">
|
||||||
|
<el-option label="DATA_PUMP" value="DATA_PUMP" />
|
||||||
|
<el-option label="JDBC_EXPORT" value="JDBC_EXPORT" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Schema">
|
||||||
|
<el-input v-model="backupForm.schemaName" :placeholder="selectedConnection?.schemaName || '默认 Schema'" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备份模式">
|
||||||
|
<el-select v-model="backupForm.backupMode">
|
||||||
|
<el-option
|
||||||
|
v-for="(label, value) in backupModeLabels"
|
||||||
|
:key="value"
|
||||||
|
:label="label"
|
||||||
|
:value="value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Directory">
|
||||||
|
<el-input
|
||||||
|
v-model="backupForm.directoryName"
|
||||||
|
:placeholder="selectedConnection?.directoryName || 'DATA_PUMP_DIR'"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="backupForm.backupMode === 'TIME_RANGE'" label="时间字段" prop="timeColumn">
|
||||||
|
<el-input v-model="backupForm.timeColumn" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="backupForm.backupMode === 'SIZE_SPLIT'" label="文件上限">
|
||||||
|
<el-input-number v-model="backupForm.maxFileSizeMb" :min="1" controls-position="right" />
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form-item v-if="backupForm.backupMode === 'TIME_RANGE'" label="时间范围" prop="timeRange">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="backupForm.timeRange"
|
||||||
|
type="datetimerange"
|
||||||
|
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
start-placeholder="开始时间"
|
||||||
|
end-placeholder="结束时间"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="表选择">
|
||||||
|
<el-select
|
||||||
|
v-model="backupForm.targetNames"
|
||||||
|
multiple
|
||||||
|
filterable
|
||||||
|
collapse-tags
|
||||||
|
collapse-tags-tooltip
|
||||||
|
placeholder="不选则按后端默认范围"
|
||||||
|
:loading="tableLoading"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="table in tables"
|
||||||
|
:key="table.tableName"
|
||||||
|
:label="table.comments ? `${table.tableName} / ${table.comments}` : table.tableName"
|
||||||
|
:value="table.tableName"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="临时密码">
|
||||||
|
<el-input v-model="backupForm.temporaryPassword" type="password" show-password clearable />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<el-button type="primary" :icon="Promotion" :disabled="!selectedConnection" @click="handleBackupSubmit">
|
||||||
|
创建备份任务
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="创建恢复" name="restore">
|
||||||
|
<el-form ref="restoreFormRef" :model="restoreForm" :rules="restoreRules" label-width="112px">
|
||||||
|
<el-form-item label="备份文件" prop="backupFileId">
|
||||||
|
<el-select v-model="restoreForm.backupFileId" filterable placeholder="请选择备份文件">
|
||||||
|
<el-option
|
||||||
|
v-for="file in files"
|
||||||
|
:key="file.id"
|
||||||
|
:label="file.fileName"
|
||||||
|
:value="file.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="恢复模式">
|
||||||
|
<el-select v-model="restoreForm.restoreMode">
|
||||||
|
<el-option
|
||||||
|
v-for="(label, value) in restoreModeLabels"
|
||||||
|
:key="value"
|
||||||
|
:label="label"
|
||||||
|
:value="value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="目标 Schema">
|
||||||
|
<el-input
|
||||||
|
v-model="restoreForm.targetSchemaName"
|
||||||
|
:placeholder="selectedConnection?.schemaName || '默认 Schema'"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
<el-form-item label="临时密码">
|
||||||
|
<el-input v-model="restoreForm.temporaryPassword" type="password" show-password clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="isOverwriteRestoreMode(restoreForm.restoreMode)" label="覆盖确认" prop="overwriteConfirmText">
|
||||||
|
<el-input v-model="restoreForm.overwriteConfirmText" placeholder="请输入:确认覆盖" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<el-button type="primary" :icon="Promotion" :disabled="!selectedConnection" @click="handleRestoreSubmit">
|
||||||
|
创建恢复任务
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref, watch } from 'vue'
|
||||||
|
import { Promotion, Refresh } from '@element-plus/icons-vue'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import type { Dbms } from '@/api/system/dbms/interface'
|
||||||
|
import {
|
||||||
|
createBackupForm,
|
||||||
|
createRestoreForm,
|
||||||
|
isOverwriteConfirmMatched,
|
||||||
|
isOverwriteRestoreMode,
|
||||||
|
type DbmsBackupFormModel,
|
||||||
|
type DbmsRestoreFormModel
|
||||||
|
} from '../utils/taskPayload'
|
||||||
|
import { backupModeLabels, restoreModeLabels } from '../utils/normalize'
|
||||||
|
import type { BackupSubmitPayload, RestoreSubmitPayload } from './types'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'DbmsTaskPanel'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
selectedConnection: Dbms.ConnectionRecord | null
|
||||||
|
tables: Dbms.TableRecord[]
|
||||||
|
files: Dbms.BackupFileRecord[]
|
||||||
|
tableLoading: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'load-tables': []
|
||||||
|
backup: [payload: BackupSubmitPayload]
|
||||||
|
restore: [payload: RestoreSubmitPayload]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const activeTab = ref('backup')
|
||||||
|
const backupFormRef = ref<FormInstance>()
|
||||||
|
const restoreFormRef = ref<FormInstance>()
|
||||||
|
const backupForm = reactive<DbmsBackupFormModel>(createBackupForm())
|
||||||
|
const restoreForm = reactive<DbmsRestoreFormModel>(createRestoreForm())
|
||||||
|
|
||||||
|
const backupRules: FormRules<DbmsBackupFormModel> = {
|
||||||
|
timeColumn: [{ required: true, message: '请输入时间字段', trigger: 'blur' }],
|
||||||
|
timeRange: [{ required: true, message: '请选择时间范围', trigger: 'change' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreRules: FormRules<DbmsRestoreFormModel> = {
|
||||||
|
backupFileId: [{ required: true, message: '请选择备份文件', trigger: 'change' }],
|
||||||
|
overwriteConfirmText: [
|
||||||
|
{
|
||||||
|
validator: (_rule, value, callback) => {
|
||||||
|
if (isOverwriteRestoreMode(restoreForm.restoreMode) && !isOverwriteConfirmMatched(String(value || ''))) {
|
||||||
|
callback(new Error('请输入确认覆盖'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.selectedConnection,
|
||||||
|
connection => {
|
||||||
|
backupForm.schemaName = connection?.schemaName || ''
|
||||||
|
backupForm.directoryName = connection?.directoryName || 'DATA_PUMP_DIR'
|
||||||
|
restoreForm.targetSchemaName = connection?.schemaName || ''
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleBackupSubmit = async () => {
|
||||||
|
if (!props.selectedConnection) return
|
||||||
|
if (!(await backupFormRef.value?.validate())) return
|
||||||
|
emit('backup', { form: { ...backupForm, targetNames: [...backupForm.targetNames], timeRange: [...backupForm.timeRange] } })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRestoreSubmit = async () => {
|
||||||
|
if (!props.selectedConnection) return
|
||||||
|
if (!(await restoreFormRef.value?.validate())) return
|
||||||
|
emit('restore', { form: { ...restoreForm } })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.dbms-task-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-info {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-description {
|
||||||
|
margin-top: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 12px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-tabs :deep(.el-select),
|
||||||
|
.operation-tabs :deep(.el-date-editor) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid :deep(.el-input-number) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
<template>
|
||||||
|
<section class="card dbms-status-card">
|
||||||
|
<el-tabs v-model="activeTab" class="status-tabs">
|
||||||
|
<el-tab-pane label="任务记录" name="tasks">
|
||||||
|
<div class="table-header">
|
||||||
|
<div class="header-button-lf">
|
||||||
|
<el-select v-model="localTaskQuery.taskStatus" class="query-select" clearable placeholder="任务状态" @change="emitTaskQuery">
|
||||||
|
<el-option label="等待中" value="WAITING" />
|
||||||
|
<el-option label="执行中" value="RUNNING" />
|
||||||
|
<el-option label="成功" value="SUCCESS" />
|
||||||
|
<el-option label="失败" value="FAIL" />
|
||||||
|
<el-option label="已取消" value="CANCELLED" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="header-button-ri">
|
||||||
|
<el-button circle :icon="Refresh" @click="emit('refresh-tasks')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-body">
|
||||||
|
<el-table :data="tasks" border stripe height="100%" v-loading="taskLoading">
|
||||||
|
<el-table-column prop="taskNo" label="任务号" min-width="180" fixed="left" show-overflow-tooltip />
|
||||||
|
<el-table-column label="类型" width="90" align="center">
|
||||||
|
<template #default="{ row }">{{ getOperationTypeLabel(row.operationType) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getTaskStatusMeta(row.taskStatus).type" effect="light">
|
||||||
|
{{ getTaskStatusMeta(row.taskStatus).label }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="progressPercent" label="进度" width="150" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-progress :percentage="Number(row.progressPercent || 0)" :stroke-width="8" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="schemaName" label="Schema" min-width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column label="对象" min-width="160" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ parseJsonArrayText(row.targetNamesJson) || '--' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="resultMessage" label="结果消息" min-width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="startedAt" label="开始时间" min-width="170" />
|
||||||
|
<el-table-column prop="finishedAt" label="结束时间" min-width="170" />
|
||||||
|
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link :icon="Refresh" @click="emit('check-task', row)">状态</el-button>
|
||||||
|
<el-button type="primary" link :icon="Delete" @click="emit('delete-task', row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<div class="table-footer">
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
layout="total, prev, pager, next, sizes"
|
||||||
|
:total="taskTotal"
|
||||||
|
:page-size="taskPageSize"
|
||||||
|
:current-page="taskPageNum"
|
||||||
|
@current-change="page => emit('task-page-change', page)"
|
||||||
|
@size-change="size => emit('task-size-change', size)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="备份文件" name="files">
|
||||||
|
<div class="table-header">
|
||||||
|
<div class="header-button-lf">
|
||||||
|
<el-input
|
||||||
|
v-model="localFileQuery.taskId"
|
||||||
|
class="query-input"
|
||||||
|
placeholder="任务 ID"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="emitFileQuery"
|
||||||
|
@clear="emitFileQuery"
|
||||||
|
/>
|
||||||
|
<el-button :icon="Search" @click="emitFileQuery">查询</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="header-button-ri">
|
||||||
|
<el-button circle :icon="Refresh" @click="emit('refresh-files')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-body">
|
||||||
|
<el-table :data="files" border stripe height="100%" v-loading="fileLoading">
|
||||||
|
<el-table-column prop="fileName" label="文件名" min-width="220" fixed="left" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="schemaName" label="Schema" min-width="110" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="backupMode" label="备份模式" min-width="110" align="center" />
|
||||||
|
<el-table-column label="对象" min-width="150" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ parseJsonArrayText(row.targetNamesJson) || '--' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="大小" width="120" align="center">
|
||||||
|
<template #default="{ row }">{{ formatFileSize(row.fileSize) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="filePath" label="路径" min-width="260" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="logFileName" label="日志文件" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="createTime" label="创建时间" min-width="170" />
|
||||||
|
<el-table-column label="操作" width="100" fixed="right" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link :icon="Delete" @click="emit('delete-file', row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<div class="table-footer">
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
layout="total, prev, pager, next, sizes"
|
||||||
|
:total="fileTotal"
|
||||||
|
:page-size="filePageSize"
|
||||||
|
:current-page="filePageNum"
|
||||||
|
@current-change="page => emit('file-page-change', page)"
|
||||||
|
@size-change="size => emit('file-size-change', size)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref, watch } from 'vue'
|
||||||
|
import { Delete, Refresh, Search } from '@element-plus/icons-vue'
|
||||||
|
import type { Dbms } from '@/api/system/dbms/interface'
|
||||||
|
import {
|
||||||
|
formatFileSize,
|
||||||
|
getTaskStatusMeta,
|
||||||
|
operationTypeLabels,
|
||||||
|
parseJsonArrayText
|
||||||
|
} from '../utils/normalize'
|
||||||
|
import type { DbmsFileQuery, DbmsTaskQuery } from './types'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'DbmsTaskStatusCard'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tasks: Dbms.TaskRecord[]
|
||||||
|
files: Dbms.BackupFileRecord[]
|
||||||
|
taskQuery: DbmsTaskQuery
|
||||||
|
fileQuery: DbmsFileQuery
|
||||||
|
taskLoading: boolean
|
||||||
|
fileLoading: boolean
|
||||||
|
taskTotal: number
|
||||||
|
taskPageNum: number
|
||||||
|
taskPageSize: number
|
||||||
|
fileTotal: number
|
||||||
|
filePageNum: number
|
||||||
|
filePageSize: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'refresh-tasks': []
|
||||||
|
'refresh-files': []
|
||||||
|
'search-tasks': [query: DbmsTaskQuery]
|
||||||
|
'search-files': [query: DbmsFileQuery]
|
||||||
|
'check-task': [row: Dbms.TaskRecord]
|
||||||
|
'delete-task': [row: Dbms.TaskRecord]
|
||||||
|
'delete-file': [row: Dbms.BackupFileRecord]
|
||||||
|
'task-page-change': [page: number]
|
||||||
|
'task-size-change': [size: number]
|
||||||
|
'file-page-change': [page: number]
|
||||||
|
'file-size-change': [size: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const activeTab = ref('tasks')
|
||||||
|
const localTaskQuery = reactive<DbmsTaskQuery>({ taskStatus: props.taskQuery.taskStatus })
|
||||||
|
const localFileQuery = reactive<DbmsFileQuery>({ taskId: props.fileQuery.taskId })
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.taskQuery,
|
||||||
|
query => {
|
||||||
|
localTaskQuery.taskStatus = query.taskStatus
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.fileQuery,
|
||||||
|
query => {
|
||||||
|
localFileQuery.taskId = query.taskId
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const emitTaskQuery = () => {
|
||||||
|
emit('search-tasks', { taskStatus: localTaskQuery.taskStatus })
|
||||||
|
}
|
||||||
|
|
||||||
|
const emitFileQuery = () => {
|
||||||
|
emit('search-files', { taskId: localFileQuery.taskId })
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOperationTypeLabel = (operationType: Dbms.OperationType) => {
|
||||||
|
return operationTypeLabels[operationType] || operationType
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.dbms-status-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tabs :deep(.el-tabs__content),
|
||||||
|
.status-tabs :deep(.el-tab-pane) {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-button-lf,
|
||||||
|
.header-button-ri {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-select,
|
||||||
|
.query-input {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
margin-top: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex: none;
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
122
frontend/src/views/system-ops/dbms/components/DbmsToolbar.vue
Normal file
122
frontend/src/views/system-ops/dbms/components/DbmsToolbar.vue
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dbms-toolbar">
|
||||||
|
<button
|
||||||
|
v-for="item in toolbarItems"
|
||||||
|
:key="item.command"
|
||||||
|
class="toolbar-item"
|
||||||
|
type="button"
|
||||||
|
:class="{ 'is-disabled': item.disabled }"
|
||||||
|
:disabled="item.disabled"
|
||||||
|
@click="emit('command', item.command)"
|
||||||
|
>
|
||||||
|
<el-icon class="toolbar-icon">
|
||||||
|
<component :is="item.icon" />
|
||||||
|
</el-icon>
|
||||||
|
<span class="toolbar-label">{{ item.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Component } from 'vue'
|
||||||
|
import {
|
||||||
|
Coin,
|
||||||
|
Connection,
|
||||||
|
DataAnalysis,
|
||||||
|
FolderOpened,
|
||||||
|
Grid,
|
||||||
|
Link,
|
||||||
|
Monitor,
|
||||||
|
Operation,
|
||||||
|
RefreshRight,
|
||||||
|
Search,
|
||||||
|
User,
|
||||||
|
View
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import type { DbmsToolbarCommand } from './types'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'DbmsToolbar'
|
||||||
|
})
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
hasSelectedConnection: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
command: [command: DbmsToolbarCommand]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const toolbarItems: Array<{
|
||||||
|
label: string
|
||||||
|
command: DbmsToolbarCommand
|
||||||
|
icon: Component
|
||||||
|
disabled?: boolean
|
||||||
|
}> = [
|
||||||
|
{ label: '连接', command: 'connect', icon: Link },
|
||||||
|
{ label: '新建查询', command: 'newQuery', icon: Search },
|
||||||
|
{ label: '表', command: 'tables', icon: Grid },
|
||||||
|
{ label: '视图', command: 'views', icon: View },
|
||||||
|
{ label: '函数', command: 'functions', icon: Operation, disabled: true },
|
||||||
|
{ label: '用户', command: 'users', icon: User, disabled: true },
|
||||||
|
{ label: '备份', command: 'backup', icon: FolderOpened },
|
||||||
|
{ label: '自动运行', command: 'automation', icon: RefreshRight, disabled: true },
|
||||||
|
{ label: '模型', command: 'model', icon: Coin, disabled: true },
|
||||||
|
{ label: 'BI', command: 'bi', icon: DataAnalysis, disabled: true },
|
||||||
|
{ label: '新建连接', command: 'newConnection', icon: Connection }
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.dbms-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 2px;
|
||||||
|
flex: none;
|
||||||
|
min-height: 68px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-item {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 72px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-item:hover:not(.is-disabled) {
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
border-color: var(--el-color-primary-light-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-item.is-disabled {
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-item.is-disabled .toolbar-icon {
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-label {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
237
frontend/src/views/system-ops/dbms/components/DbmsWorkspace.vue
Normal file
237
frontend/src/views/system-ops/dbms/components/DbmsWorkspace.vue
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
<template>
|
||||||
|
<section class="dbms-workspace">
|
||||||
|
<div class="workspace-header">
|
||||||
|
<div class="workspace-title">
|
||||||
|
<span>{{ title }}</span>
|
||||||
|
<small v-if="selectedConnection">{{ selectedConnection.host }}:{{ selectedConnection.port }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="workspace-actions">
|
||||||
|
<el-button v-if="selectedConnection" type="primary" plain :icon="Connection" @click="emit('edit-connection', selectedConnection)">
|
||||||
|
连接信息
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="activeSection === 'tables'" :icon="Refresh" :loading="tableLoading" @click="emit('load-tables')">
|
||||||
|
刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!selectedConnection" class="workspace-empty">
|
||||||
|
<el-empty description="请先在左侧选择数据库连接" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="activeSection === 'overview'" class="overview-grid">
|
||||||
|
<div class="info-panel">
|
||||||
|
<div class="info-title">连接信息</div>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="连接名">{{ selectedConnection.connectionName }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="数据库类型">{{ selectedConnection.dbType }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="主机">{{ selectedConnection.host }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="端口">{{ selectedConnection.port }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="Schema">{{ selectedConnection.schemaName || '--' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="用户">{{ selectedConnection.username }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="activeSection === 'tables'" class="table-workspace">
|
||||||
|
<el-table :data="tables" border stripe height="100%" v-loading="tableLoading">
|
||||||
|
<el-table-column prop="tableName" label="Name" min-width="220" fixed="left" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="owner" label="Owner" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="comments" label="Comments" min-width="260" show-overflow-tooltip />
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="activeSection === 'views'" class="workspace-empty">
|
||||||
|
<el-empty description="暂无视图接口,后端提供视图列表后可在此展示" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DbmsTaskPanel
|
||||||
|
v-else-if="activeSection === 'backup'"
|
||||||
|
:selected-connection="selectedConnection"
|
||||||
|
:tables="tables"
|
||||||
|
:files="files"
|
||||||
|
:table-loading="tableLoading"
|
||||||
|
@load-tables="emit('load-tables')"
|
||||||
|
@backup="payload => emit('backup', payload)"
|
||||||
|
@restore="payload => emit('restore', payload)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DbmsTaskStatusCard
|
||||||
|
v-else-if="activeSection === 'tasks'"
|
||||||
|
:tasks="tasks"
|
||||||
|
:files="files"
|
||||||
|
:task-query="taskQuery"
|
||||||
|
:file-query="fileQuery"
|
||||||
|
:task-loading="taskLoading"
|
||||||
|
:file-loading="fileLoading"
|
||||||
|
:task-total="taskTotal"
|
||||||
|
:task-page-num="taskPageNum"
|
||||||
|
:task-page-size="taskPageSize"
|
||||||
|
:file-total="fileTotal"
|
||||||
|
:file-page-num="filePageNum"
|
||||||
|
:file-page-size="filePageSize"
|
||||||
|
@refresh-tasks="emit('refresh-tasks')"
|
||||||
|
@refresh-files="emit('refresh-files')"
|
||||||
|
@search-tasks="query => emit('search-tasks', query)"
|
||||||
|
@search-files="query => emit('search-files', query)"
|
||||||
|
@check-task="row => emit('check-task', row)"
|
||||||
|
@delete-task="row => emit('delete-task', row)"
|
||||||
|
@delete-file="row => emit('delete-file', row)"
|
||||||
|
@task-page-change="page => emit('task-page-change', page)"
|
||||||
|
@task-size-change="size => emit('task-size-change', size)"
|
||||||
|
@file-page-change="page => emit('file-page-change', page)"
|
||||||
|
@file-size-change="size => emit('file-size-change', size)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-else class="workspace-empty">
|
||||||
|
<el-empty description="当前能力暂未接入" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { Connection, Refresh } from '@element-plus/icons-vue'
|
||||||
|
import type { Dbms } from '@/api/system/dbms/interface'
|
||||||
|
import DbmsTaskPanel from './DbmsTaskPanel.vue'
|
||||||
|
import DbmsTaskStatusCard from './DbmsTaskStatusCard.vue'
|
||||||
|
import type {
|
||||||
|
BackupSubmitPayload,
|
||||||
|
DbmsFileQuery,
|
||||||
|
DbmsTaskQuery,
|
||||||
|
DbmsWorkspaceSection,
|
||||||
|
RestoreSubmitPayload
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'DbmsWorkspace'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
activeSection: DbmsWorkspaceSection
|
||||||
|
selectedConnection: Dbms.ConnectionRecord | null
|
||||||
|
tables: Dbms.TableRecord[]
|
||||||
|
files: Dbms.BackupFileRecord[]
|
||||||
|
tasks: Dbms.TaskRecord[]
|
||||||
|
taskQuery: DbmsTaskQuery
|
||||||
|
fileQuery: DbmsFileQuery
|
||||||
|
tableLoading: boolean
|
||||||
|
taskLoading: boolean
|
||||||
|
fileLoading: boolean
|
||||||
|
taskTotal: number
|
||||||
|
taskPageNum: number
|
||||||
|
taskPageSize: number
|
||||||
|
fileTotal: number
|
||||||
|
filePageNum: number
|
||||||
|
filePageSize: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'edit-connection': [row: Dbms.ConnectionRecord]
|
||||||
|
'load-tables': []
|
||||||
|
backup: [payload: BackupSubmitPayload]
|
||||||
|
restore: [payload: RestoreSubmitPayload]
|
||||||
|
'refresh-tasks': []
|
||||||
|
'refresh-files': []
|
||||||
|
'search-tasks': [query: DbmsTaskQuery]
|
||||||
|
'search-files': [query: DbmsFileQuery]
|
||||||
|
'check-task': [row: Dbms.TaskRecord]
|
||||||
|
'delete-task': [row: Dbms.TaskRecord]
|
||||||
|
'delete-file': [row: Dbms.BackupFileRecord]
|
||||||
|
'task-page-change': [page: number]
|
||||||
|
'task-size-change': [size: number]
|
||||||
|
'file-page-change': [page: number]
|
||||||
|
'file-size-change': [size: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const sectionTitles: Record<DbmsWorkspaceSection, string> = {
|
||||||
|
overview: '连接概览',
|
||||||
|
connections: '连接配置',
|
||||||
|
tables: '表',
|
||||||
|
views: '视图',
|
||||||
|
backup: '备份与恢复',
|
||||||
|
tasks: '任务与备份文件'
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = computed(() => sectionTitles[props.activeSection] || '工作区')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.dbms-workspace {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex: none;
|
||||||
|
min-height: 46px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-title {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-title small {
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-actions {
|
||||||
|
display: flex;
|
||||||
|
flex: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-empty,
|
||||||
|
.overview-grid,
|
||||||
|
.table-workspace {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-workspace {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel {
|
||||||
|
max-width: 820px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-title {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
48
frontend/src/views/system-ops/dbms/components/types.ts
Normal file
48
frontend/src/views/system-ops/dbms/components/types.ts
Normal file
@@ -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[]
|
||||||
|
}
|
||||||
@@ -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')
|
||||||
@@ -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',
|
||||||
|
/<DbmsConnectionTypeDialog[\s\S]*@next="handleSelectConnectionType"/.test(read(files.page))
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'connect command opens type selector',
|
||||||
|
/\bconnect:\s*\(\)\s*=>\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<Dbms\.DbType>\('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')
|
||||||
@@ -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', /<DbmsConnectionTree[\s\S]*@refresh="loadConnections"/.test(pageSource)]
|
||||||
|
]
|
||||||
|
|
||||||
|
const failures = checks.filter(([, passed]) => !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')
|
||||||
@@ -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', /<DbmsToolbar[\s\S]*@command="handleToolbarCommand"/, read(files.page)],
|
||||||
|
['page uses collapsible connection tree', /<DbmsConnectionTree[\s\S]*:collapsed="treeCollapsed"/, read(files.page)],
|
||||||
|
['page uses right workspace', /<DbmsWorkspace[\s\S]*:active-section="activeSection"/, read(files.page)],
|
||||||
|
['toolbar exposes Navicat-like commands', /command:\s*'connect'[\s\S]*command:\s*'backup'[\s\S]*command:\s*'bi'/, read(files.toolbar)],
|
||||||
|
['connection tree supports collapse toggle', /emit\('toggle'\)/, read(files.tree)],
|
||||||
|
['connection tree groups tables and views', /type:\s*'tableGroup'[\s\S]*type:\s*'viewGroup'/, read(files.tree)],
|
||||||
|
['workspace shows table grid', /<el-table[\s\S]*:data="tables"/, read(files.workspace)],
|
||||||
|
['workspace keeps view empty state explicit', /暂无视图接口/, read(files.workspace)],
|
||||||
|
['workspace can open task panel', /<DbmsTaskPanel/, read(files.workspace)],
|
||||||
|
['workspace can open task status card', /<DbmsTaskStatusCard/, read(files.workspace)]
|
||||||
|
]
|
||||||
|
|
||||||
|
const failures = checks.filter(([, passed]) => !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')
|
||||||
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>
|
||||||
95
frontend/src/views/system-ops/dbms/utils/normalize.ts
Normal file
95
frontend/src/views/system-ops/dbms/utils/normalize.ts
Normal file
@@ -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<string, { label: string; type: TagProps['type'] }> = {
|
||||||
|
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<string, { label: string; type: TagProps['type'] }> = {
|
||||||
|
SUCCESS: { label: '成功', type: 'success' },
|
||||||
|
FAIL: { label: '失败', type: 'danger' },
|
||||||
|
FAILED: { label: '失败', type: 'danger' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const backupModeLabels: Record<Dbms.BackupMode, string> = {
|
||||||
|
FULL_TABLE: '全表',
|
||||||
|
TIME_RANGE: '按时间范围',
|
||||||
|
SIZE_SPLIT: '按文件大小'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const restoreModeLabels: Record<Dbms.RestoreMode, string> = {
|
||||||
|
SKIP: '跳过已存在',
|
||||||
|
APPEND: '追加',
|
||||||
|
TRUNCATE: '清空后导入',
|
||||||
|
REPLACE: '替换'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const operationTypeLabels: Record<Dbms.OperationType, string> = {
|
||||||
|
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`
|
||||||
|
}
|
||||||
176
frontend/src/views/system-ops/dbms/utils/taskPayload.ts
Normal file
176
frontend/src/views/system-ops/dbms/utils/taskPayload.ts
Normal file
@@ -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<Dbms.ConnectionRecord> | 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
|
||||||
@@ -4,11 +4,22 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="section-title">装置配置</div>
|
<div class="section-title">装置配置</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!readonly" class="form-actions">
|
<div class="form-actions">
|
||||||
<el-button type="primary" :icon="Check" :loading="saving" @click="emit('save')">保存装置</el-button>
|
<el-button
|
||||||
<el-button type="danger" plain :icon="Delete" :disabled="!localForm.id" @click="emit('delete')">
|
type="primary"
|
||||||
删除装置
|
plain
|
||||||
|
:icon="Setting"
|
||||||
|
:disabled="!localForm.id"
|
||||||
|
@click="emit('open-unit-debug', localForm.id || '', isUnitReadonly)"
|
||||||
|
>
|
||||||
|
{{ unitActionLabel }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<template v-if="!readonly">
|
||||||
|
<el-button type="primary" :icon="Check" :loading="saving" @click="emit('save')">保存装置</el-button>
|
||||||
|
<el-button type="danger" plain :icon="Delete" :disabled="!localForm.id" @click="emit('delete')">
|
||||||
|
删除装置
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -70,7 +81,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, ref, watch } from 'vue'
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
import { Check, Delete } from '@element-plus/icons-vue'
|
import { Check, Delete, Setting } from '@element-plus/icons-vue'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
import type { AddLedger } from '@/api/tools/addLedger/interface'
|
import type { AddLedger } from '@/api/tools/addLedger/interface'
|
||||||
|
|
||||||
@@ -89,6 +100,7 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:form': [form: AddLedger.EquipmentForm]
|
'update:form': [form: AddLedger.EquipmentForm]
|
||||||
|
'open-unit-debug': [devId: string, readonly: boolean]
|
||||||
save: []
|
save: []
|
||||||
delete: []
|
delete: []
|
||||||
}>()
|
}>()
|
||||||
@@ -152,6 +164,8 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const isSimpleMode = computed(() => props.mode === 'simple')
|
const isSimpleMode = computed(() => props.mode === 'simple')
|
||||||
|
const isUnitReadonly = computed(() => Boolean(props.readonly))
|
||||||
|
const unitActionLabel = computed(() => (isUnitReadonly.value ? '单位查询' : '单位配置'))
|
||||||
const formRules = computed<FormRules<AddLedger.EquipmentForm>>(() => ({
|
const formRules = computed<FormRules<AddLedger.EquipmentForm>>(() => ({
|
||||||
name: [{ required: true, message: '请输入装置名称', trigger: 'blur' }],
|
name: [{ required: true, message: '请输入装置名称', trigger: 'blur' }],
|
||||||
mac: [{ required: true, message: '请输入装置网络参数', trigger: 'blur' }],
|
mac: [{ required: true, message: '请输入装置网络参数', trigger: 'blur' }],
|
||||||
|
|||||||
@@ -79,6 +79,7 @@
|
|||||||
:device-type-options="deviceTypeOptions"
|
:device-type-options="deviceTypeOptions"
|
||||||
:device-model-options="deviceModelOptions"
|
:device-model-options="deviceModelOptions"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
|
@open-unit-debug="devId => emit('open-unit-debug', devId, false)"
|
||||||
@save="emit('save-equipment')"
|
@save="emit('save-equipment')"
|
||||||
@delete="emit('delete')"
|
@delete="emit('delete')"
|
||||||
/>
|
/>
|
||||||
@@ -90,6 +91,7 @@
|
|||||||
:device-model-options="deviceModelOptions"
|
:device-model-options="deviceModelOptions"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
readonly
|
readonly
|
||||||
|
@open-unit-debug="devId => emit('open-unit-debug', devId, true)"
|
||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
@@ -123,6 +125,7 @@
|
|||||||
:saving="saving"
|
:saving="saving"
|
||||||
:line-no-options="lineNoOptions"
|
:line-no-options="lineNoOptions"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
|
@open-overlimit-debug="lineId => emit('open-overlimit-debug', lineId, false)"
|
||||||
@save="emit('save-line')"
|
@save="emit('save-line')"
|
||||||
@delete="emit('delete')"
|
@delete="emit('delete')"
|
||||||
/>
|
/>
|
||||||
@@ -133,6 +136,7 @@
|
|||||||
:line-no-options="allLineNoOptions"
|
:line-no-options="allLineNoOptions"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
readonly
|
readonly
|
||||||
|
@open-overlimit-debug="lineId => emit('open-overlimit-debug', lineId, true)"
|
||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
@@ -211,6 +215,8 @@ const emit = defineEmits<{
|
|||||||
'save-project': []
|
'save-project': []
|
||||||
'save-equipment': []
|
'save-equipment': []
|
||||||
'save-line': []
|
'save-line': []
|
||||||
|
'open-unit-debug': [devId: string, readonly: boolean]
|
||||||
|
'open-overlimit-debug': [lineId: string, readonly: boolean]
|
||||||
delete: []
|
delete: []
|
||||||
'add-project': []
|
'add-project': []
|
||||||
'add-equipment': []
|
'add-equipment': []
|
||||||
|
|||||||
@@ -4,11 +4,22 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="section-title">监测点配置</div>
|
<div class="section-title">监测点配置</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!readonly" class="form-actions">
|
<div class="form-actions">
|
||||||
<el-button type="primary" :icon="Check" :loading="saving" @click="emit('save')">保存测点</el-button>
|
<el-button
|
||||||
<el-button type="danger" plain :icon="Delete" :disabled="!lineId" @click="emit('delete')">
|
type="primary"
|
||||||
删除测点
|
plain
|
||||||
|
:icon="View"
|
||||||
|
:disabled="!lineId"
|
||||||
|
@click="emit('open-overlimit-debug', lineId, isOverlimitReadonly)"
|
||||||
|
>
|
||||||
|
{{ overlimitActionLabel }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<template v-if="!readonly">
|
||||||
|
<el-button type="primary" :icon="Check" :loading="saving" @click="emit('save')">保存测点</el-button>
|
||||||
|
<el-button type="danger" plain :icon="Delete" :disabled="!lineId" @click="emit('delete')">
|
||||||
|
删除测点
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -44,7 +55,7 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="电压等级" prop="vol_grade">
|
<el-form-item label="电压等级" prop="vol_grade">
|
||||||
<el-select v-model="localForm.vol_grade" clearable placeholder="请选择电压等级">
|
<el-select v-model="localForm.vol_grade" clearable placeholder="请选择电压等级" @change="handleVoltageChange">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in voltageOptions"
|
v-for="item in voltageOptions"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
@@ -53,6 +64,11 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item v-if="!isSimpleMode" label="线路类型" prop="lineType">
|
||||||
|
<el-select v-model="localForm.lineType" clearable placeholder="请选择线路类型">
|
||||||
|
<el-option v-for="item in lineTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item v-if="!isSimpleMode" label="安装位置" prop="position">
|
<el-form-item v-if="!isSimpleMode" label="安装位置" prop="position">
|
||||||
<el-input v-model="localForm.position" maxlength="80" clearable placeholder="请输入安装位置" />
|
<el-input v-model="localForm.position" maxlength="80" clearable placeholder="请输入安装位置" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -150,7 +166,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, ref, watch } from 'vue'
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
import { Check, Delete } from '@element-plus/icons-vue'
|
import { Check, Delete, View } from '@element-plus/icons-vue'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
import type { AddLedger } from '@/api/tools/addLedger/interface'
|
import type { AddLedger } from '@/api/tools/addLedger/interface'
|
||||||
|
|
||||||
@@ -168,10 +184,17 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:form': [form: AddLedger.LineForm]
|
'update:form': [form: AddLedger.LineForm]
|
||||||
|
'open-overlimit-debug': [lineId: string, readonly: boolean]
|
||||||
save: []
|
save: []
|
||||||
delete: []
|
delete: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
type LineVoltageDefault = {
|
||||||
|
capacity: number
|
||||||
|
ptRatio?: number
|
||||||
|
pt2Ratio?: number
|
||||||
|
}
|
||||||
|
|
||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
const syncingFromProp = ref(false)
|
const syncingFromProp = ref(false)
|
||||||
const localForm = reactive<AddLedger.LineForm>({
|
const localForm = reactive<AddLedger.LineForm>({
|
||||||
@@ -192,6 +215,7 @@ const localForm = reactive<AddLedger.LineForm>({
|
|||||||
basic_capacity: undefined,
|
basic_capacity: undefined,
|
||||||
protocol_capacity: undefined,
|
protocol_capacity: undefined,
|
||||||
dev_capacity: undefined,
|
dev_capacity: undefined,
|
||||||
|
lineType: 0,
|
||||||
monitor_obj: '',
|
monitor_obj: '',
|
||||||
is_govern: 0,
|
is_govern: 0,
|
||||||
monitor_user: '',
|
monitor_user: '',
|
||||||
@@ -204,17 +228,63 @@ const conTypeOptions: AddLedger.SelectOption<number>[] = [
|
|||||||
{ label: 'V 型接线', value: 2 }
|
{ label: 'V 型接线', value: 2 }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const lineTypeOptions: AddLedger.SelectOption<number>[] = [
|
||||||
|
{ label: '主网', value: 0 },
|
||||||
|
{ label: '配网', value: 1 }
|
||||||
|
]
|
||||||
|
|
||||||
const voltageOptions: AddLedger.SelectOption<number>[] = [
|
const voltageOptions: AddLedger.SelectOption<number>[] = [
|
||||||
{ label: '0.38kV', value: 0.38 },
|
{ label: '0.38kV', value: 0.38 },
|
||||||
|
{ label: '6kV', value: 6 },
|
||||||
{ label: '10kV', value: 10 },
|
{ label: '10kV', value: 10 },
|
||||||
|
{ label: '20kV', value: 20 },
|
||||||
{ label: '35kV', value: 35 },
|
{ label: '35kV', value: 35 },
|
||||||
|
{ label: '66kV', value: 66 },
|
||||||
{ label: '110kV', value: 110 },
|
{ label: '110kV', value: 110 },
|
||||||
{ label: '220kV', value: 220 },
|
{ label: '220kV', value: 220 },
|
||||||
{ label: '500kV', value: 500 }
|
{ label: '330kV', value: 330 },
|
||||||
|
{ label: '500kV', value: 500 },
|
||||||
|
{ label: '750kV', value: 750 },
|
||||||
|
{ label: '1000kV', value: 1000 }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const voltageDefaultMap = new Map<number, LineVoltageDefault>([
|
||||||
|
[0.38, { capacity: 10, ptRatio: 380, pt2Ratio: 380 }],
|
||||||
|
[6, { capacity: 100, ptRatio: 6000, pt2Ratio: 100 }],
|
||||||
|
[10, { capacity: 100, ptRatio: 10000, pt2Ratio: 100 }],
|
||||||
|
[20, { capacity: 200, ptRatio: 20000, pt2Ratio: 100 }],
|
||||||
|
[35, { capacity: 250, ptRatio: 35000, pt2Ratio: 100 }],
|
||||||
|
[66, { capacity: 500, ptRatio: 66000, pt2Ratio: 100 }],
|
||||||
|
[110, { capacity: 750, ptRatio: 110000, pt2Ratio: 100 }],
|
||||||
|
[220, { capacity: 2000, ptRatio: 220000, pt2Ratio: 100 }],
|
||||||
|
[330, { capacity: 3000, ptRatio: 330000, pt2Ratio: 100 }],
|
||||||
|
[500, { capacity: 4500, ptRatio: 500000, pt2Ratio: 100 }],
|
||||||
|
[750, { capacity: 7000, ptRatio: 750000, pt2Ratio: 100 }],
|
||||||
|
[1000, { capacity: 9000, ptRatio: 1000000, pt2Ratio: 100 }]
|
||||||
|
])
|
||||||
|
|
||||||
const lineId = computed(() => localForm.line_id || localForm.id || '')
|
const lineId = computed(() => localForm.line_id || localForm.id || '')
|
||||||
const isSimpleMode = computed(() => props.mode === 'simple')
|
const isSimpleMode = computed(() => props.mode === 'simple')
|
||||||
|
const isOverlimitReadonly = computed(() => Boolean(props.readonly))
|
||||||
|
const overlimitActionLabel = computed(() => (isOverlimitReadonly.value ? '限值查询' : '限值配置'))
|
||||||
|
|
||||||
|
const applyVoltageDefault = (voltage?: number) => {
|
||||||
|
const preset = voltageDefaultMap.get(Number(voltage)) || { capacity: 10, ptRatio: undefined, pt2Ratio: undefined }
|
||||||
|
|
||||||
|
// 电压等级切换时按参考表同步默认容量和变比,保留用户后续手动调整入口。
|
||||||
|
localForm.short_circuit_capacity = preset.capacity
|
||||||
|
localForm.basic_capacity = preset.capacity
|
||||||
|
localForm.protocol_capacity = preset.capacity
|
||||||
|
localForm.dev_capacity = preset.capacity
|
||||||
|
localForm.pt_ratio = preset.ptRatio
|
||||||
|
localForm.pt2_ratio = preset.pt2Ratio
|
||||||
|
localForm.ct_ratio = 300
|
||||||
|
localForm.ct2_ratio = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleVoltageChange = (value?: number) => {
|
||||||
|
applyVoltageDefault(value)
|
||||||
|
}
|
||||||
|
|
||||||
const syncLocalForm = (form: AddLedger.LineForm) => {
|
const syncLocalForm = (form: AddLedger.LineForm) => {
|
||||||
localForm.id = form.id || ''
|
localForm.id = form.id || ''
|
||||||
@@ -234,6 +304,7 @@ const syncLocalForm = (form: AddLedger.LineForm) => {
|
|||||||
localForm.basic_capacity = form.basic_capacity
|
localForm.basic_capacity = form.basic_capacity
|
||||||
localForm.protocol_capacity = form.protocol_capacity
|
localForm.protocol_capacity = form.protocol_capacity
|
||||||
localForm.dev_capacity = form.dev_capacity
|
localForm.dev_capacity = form.dev_capacity
|
||||||
|
localForm.lineType = form.lineType ?? 0
|
||||||
localForm.monitor_obj = form.monitor_obj || ''
|
localForm.monitor_obj = form.monitor_obj || ''
|
||||||
localForm.is_govern = form.is_govern ?? 0
|
localForm.is_govern = form.is_govern ?? 0
|
||||||
localForm.monitor_user = form.monitor_user || ''
|
localForm.monitor_user = form.monitor_user || ''
|
||||||
|
|||||||
@@ -76,6 +76,10 @@
|
|||||||
row-gap: 2px;
|
row-gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ledger-line-form :deep(.ratio-form-item) {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
.ledger-form.form-three :deep(.el-form-item),
|
.ledger-form.form-three :deep(.el-form-item),
|
||||||
.ledger-form.form-simple :deep(.el-form-item) {
|
.ledger-form.form-simple :deep(.el-form-item) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -105,6 +109,10 @@
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ledger-line-form .ratio-input-group {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
.ratio-input-group :deep(.ratio-field) {
|
.ratio-input-group :deep(.ratio-field) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ const expectations = [
|
|||||||
/\.ledger-line-form \.ratio-input-group :deep\(\.ratio-field\) \{[\s\S]*min-height:\s*32px;[\s\S]*margin-bottom:\s*0;[\s\S]*\}/.test(
|
/\.ledger-line-form \.ratio-input-group :deep\(\.ratio-field\) \{[\s\S]*min-height:\s*32px;[\s\S]*margin-bottom:\s*0;[\s\S]*\}/.test(
|
||||||
formStyleSource
|
formStyleSource
|
||||||
)
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'line form ratio group keeps compact single-row height',
|
||||||
|
/\.ledger-line-form :deep\(\.ratio-form-item\)[\s\S]*height:\s*32px;[\s\S]*\.ledger-line-form \.ratio-input-group \{[\s\S]*height:\s*32px;[\s\S]*\}/.test(
|
||||||
|
formStyleSource
|
||||||
|
)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
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.resolve(currentDir, '..')
|
||||||
|
|
||||||
|
const lineFormSource = fs.readFileSync(path.join(pageDir, 'components', 'LineForm.vue'), 'utf8')
|
||||||
|
const ledgerDataSource = fs.readFileSync(path.join(pageDir, 'utils', 'ledgerData.ts'), 'utf8')
|
||||||
|
|
||||||
|
const expectations = [
|
||||||
|
['voltage preset table is maintained in LineForm', /const voltageDefaultMap = new Map<number, LineVoltageDefault>/.test(lineFormSource)],
|
||||||
|
['0.38kV preset fills 10MVA capacities and 380:380 PT', /\[0\.38,\s*\{ capacity: 10, ptRatio: 380, pt2Ratio: 380 \}\]/.test(lineFormSource)],
|
||||||
|
['6kV preset fills 100MVA capacities and 6000:100 PT', /\[6,\s*\{ capacity: 100, ptRatio: 6000, pt2Ratio: 100 \}\]/.test(lineFormSource)],
|
||||||
|
['1000kV preset fills 9000MVA capacities and 1000000:100 PT', /\[1000,\s*\{ capacity: 9000, ptRatio: 1000000, pt2Ratio: 100 \}\]/.test(lineFormSource)],
|
||||||
|
['voltage options include image reference levels', /const voltageOptions:[\s\S]*0\.38[\s\S]*6[\s\S]*10[\s\S]*20[\s\S]*35[\s\S]*66[\s\S]*110[\s\S]*220[\s\S]*330[\s\S]*500[\s\S]*750[\s\S]*1000/.test(lineFormSource)],
|
||||||
|
['voltage change applies CT default 300:5', /localForm\.ct_ratio = 300[\s\S]*localForm\.ct2_ratio = 5/.test(lineFormSource)],
|
||||||
|
['voltage change applies four capacity fields together', /localForm\.short_circuit_capacity = preset\.capacity[\s\S]*localForm\.basic_capacity = preset\.capacity[\s\S]*localForm\.protocol_capacity = preset\.capacity[\s\S]*localForm\.dev_capacity = preset\.capacity/.test(lineFormSource)],
|
||||||
|
['voltage change applies PT ratio from preset', /localForm\.pt_ratio = preset\.ptRatio[\s\S]*localForm\.pt2_ratio = preset\.pt2Ratio/.test(lineFormSource)],
|
||||||
|
['voltage select change triggers defaults', /@change="handleVoltageChange"/.test(lineFormSource)],
|
||||||
|
['prop sync does not call voltage defaults', /const syncLocalForm = \(form: AddLedger\.LineForm\) => \{(?![\s\S]*applyVoltageDefault)[\s\S]*?\n\}/.test(lineFormSource)],
|
||||||
|
['empty line defaults to 10kV', /vol_grade:\s*10/.test(ledgerDataSource)],
|
||||||
|
['empty line defaults CT ratio to 300:5', /ct_ratio:\s*300[\s\S]*ct2_ratio:\s*5/.test(ledgerDataSource)],
|
||||||
|
['empty line resolves 10kV voltage default', /const lineVoltageDefault = resolveLineVoltageDefault\(10\)/.test(ledgerDataSource)],
|
||||||
|
['empty line loads 10kV PT ratio from voltage defaults', /pt_ratio:\s*lineVoltageDefault\.ptRatio[\s\S]*pt2_ratio:\s*lineVoltageDefault\.pt2Ratio/.test(ledgerDataSource)],
|
||||||
|
['empty line loads 10kV capacity defaults', /short_circuit_capacity:\s*lineVoltageDefault\.capacity[\s\S]*basic_capacity:\s*lineVoltageDefault\.capacity[\s\S]*protocol_capacity:\s*lineVoltageDefault\.capacity[\s\S]*dev_capacity:\s*lineVoltageDefault\.capacity/.test(ledgerDataSource)],
|
||||||
|
['line detail replaces legacy 1:1 ratio placeholders from voltage defaults', /const shouldUseVoltageRatioDefault =[\s\S]*left === undefined \|\| right === undefined \|\| \(left === 1 && right === 1\)/.test(ledgerDataSource)],
|
||||||
|
['line detail replaces legacy 10MVA placeholders from voltage defaults', /const shouldUseVoltageCapacityDefault =[\s\S]*value === undefined \|\| value === 10/.test(ledgerDataSource)],
|
||||||
|
['line detail normalization applies voltage defaults to loaded records', /const lineVoltageDefault = resolveLineVoltageDefault\(volGrade\)[\s\S]*ct_ratio: shouldUseVoltageRatioDefault\(ctRatio, ct2Ratio\) \? 300 : ctRatio[\s\S]*pt_ratio: shouldUseVoltageRatioDefault\(ptRatio, pt2Ratio\) \? lineVoltageDefault\.ptRatio : ptRatio[\s\S]*short_circuit_capacity: shouldUseVoltageCapacityDefault\(shortCircuitCapacity\)[\s\S]*\? lineVoltageDefault\.capacity[\s\S]*: shortCircuitCapacity/.test(ledgerDataSource)]
|
||||||
|
]
|
||||||
|
|
||||||
|
const failures = expectations.filter(([, matched]) => !matched)
|
||||||
|
|
||||||
|
if (failures.length) {
|
||||||
|
console.error('addLedger line voltage default contract check failed:')
|
||||||
|
for (const [name] of failures) {
|
||||||
|
console.error(`- ${name}`)
|
||||||
|
}
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('addLedger line voltage default contract check passed')
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
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.resolve(currentDir, '..')
|
||||||
|
const apiDir = path.resolve(pageDir, '..', '..', '..', 'api', 'tools', 'addLedger')
|
||||||
|
|
||||||
|
const apiSource = fs.readFileSync(path.join(apiDir, 'index.ts'), 'utf8')
|
||||||
|
const interfaceSource = fs.readFileSync(path.join(apiDir, 'interface', 'index.ts'), 'utf8')
|
||||||
|
const contextPanelSource = fs.readFileSync(path.join(pageDir, 'components', 'LedgerContextPanel.vue'), 'utf8')
|
||||||
|
const equipmentFormSource = fs.readFileSync(path.join(pageDir, 'components', 'EquipmentForm.vue'), 'utf8')
|
||||||
|
const lineFormSource = fs.readFileSync(path.join(pageDir, 'components', 'LineForm.vue'), 'utf8')
|
||||||
|
const pageSource = fs.readFileSync(path.join(pageDir, 'index.vue'), 'utf8')
|
||||||
|
|
||||||
|
const expectations = [
|
||||||
|
['API exposes equipment unit query endpoint', /\/equipment\/unit['"]/.test(apiSource)],
|
||||||
|
['API exposes equipment unit save endpoint', /\/equipment\/unit\/save['"]/.test(apiSource)],
|
||||||
|
['interfaces include equipment unit form type', /interface EquipmentUnitForm/.test(interfaceSource)],
|
||||||
|
['line form type carries overlimit detail', /overlimit\?:\s*OverlimitDetail/.test(interfaceSource)],
|
||||||
|
['equipment form opens unit debug dialog', /open-unit-debug/.test(equipmentFormSource)],
|
||||||
|
['equipment form switches unit action label by readonly state', /unitActionLabel[\s\S]*\?\s*['"]单位查询['"]\s*:\s*['"]单位配置['"]/.test(equipmentFormSource)],
|
||||||
|
['line form switches overlimit action label by readonly state', /overlimitActionLabel[\s\S]*\?\s*['"]限值查询['"]\s*:\s*['"]限值配置['"]/.test(lineFormSource)],
|
||||||
|
[
|
||||||
|
'equipment unit debug action is visible outside readonly edit actions',
|
||||||
|
/<div class="form-actions">[\s\S]*open-unit-debug[\s\S]*<template v-if="!readonly">/.test(equipmentFormSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'line overlimit action is visible outside readonly edit actions',
|
||||||
|
/<div class="form-actions">[\s\S]*open-overlimit-debug[\s\S]*<template v-if="!readonly">/.test(lineFormSource)
|
||||||
|
],
|
||||||
|
['context panel wires editable unit config event', /open-unit-debug="devId\s*=>\s*emit\('open-unit-debug',\s*devId,\s*false\)"/.test(contextPanelSource)],
|
||||||
|
['context panel wires readonly unit query event', /open-unit-debug="devId\s*=>\s*emit\('open-unit-debug',\s*devId,\s*true\)"/.test(contextPanelSource)],
|
||||||
|
['context panel wires editable overlimit config event', /open-overlimit-debug="lineId\s*=>\s*emit\('open-overlimit-debug',\s*lineId,\s*false\)"/.test(contextPanelSource)],
|
||||||
|
['context panel wires readonly overlimit query event', /open-overlimit-debug="lineId\s*=>\s*emit\('open-overlimit-debug',\s*lineId,\s*true\)"/.test(contextPanelSource)],
|
||||||
|
[
|
||||||
|
'equipment unit debug dialog uses two-column internal scroll form',
|
||||||
|
/unit-debug-form[\s\S]*grid-template-columns:\s*repeat\(2,\s*minmax\(0,\s*1fr\)\)/.test(pageSource) &&
|
||||||
|
/unit-debug-form[\s\S]*overflow-y:\s*auto/.test(pageSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'overlimit debug dialog uses two-column internal scroll form instead of table',
|
||||||
|
/class="overlimit-debug-form"/.test(pageSource) &&
|
||||||
|
/overlimit-debug-form[\s\S]*grid-template-columns:\s*repeat\(2,\s*minmax\(0,\s*1fr\)\)/.test(pageSource) &&
|
||||||
|
/overlimit-debug-form[\s\S]*overflow-y:\s*auto/.test(pageSource) &&
|
||||||
|
!/<el-table v-else :data="overlimitDebug\.rows"/.test(pageSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'overlimit debug dialog filters id and resolves chinese labels',
|
||||||
|
/\.filter\(\(\[key,\s*value\]\)\s*=>\s*key\s*!==\s*['"]id['"][\s\S]*resolveOverlimitDebugLabel/.test(pageSource) &&
|
||||||
|
/const overlimitDebugLabelMap[\s\S]*频率偏差限值[\s\S]*电压波动限值/.test(pageSource)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
const failures = expectations.filter(([, matched]) => !matched)
|
||||||
|
|
||||||
|
if (failures.length) {
|
||||||
|
console.error('addLedger unit and overlimit debug contract check failed:')
|
||||||
|
for (const [name] of failures) {
|
||||||
|
console.error(`- ${name}`)
|
||||||
|
}
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('addLedger unit and overlimit debug contract check passed')
|
||||||
@@ -54,6 +54,8 @@
|
|||||||
@save-project="handleSaveProject"
|
@save-project="handleSaveProject"
|
||||||
@save-equipment="handleSaveEquipment"
|
@save-equipment="handleSaveEquipment"
|
||||||
@save-line="handleSaveLine"
|
@save-line="handleSaveLine"
|
||||||
|
@open-unit-debug="handleOpenUnitDebug"
|
||||||
|
@open-overlimit-debug="handleOpenOverlimitDebug"
|
||||||
@delete="handleDeleteNode"
|
@delete="handleDeleteNode"
|
||||||
@add-project="handleAddProject"
|
@add-project="handleAddProject"
|
||||||
@add-equipment="handleAddEquipment"
|
@add-equipment="handleAddEquipment"
|
||||||
@@ -77,6 +79,53 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<el-dialog v-model="unitDebug.visible" :title="unitDebug.readonly ? '设备单位查询' : '设备单位配置'" width="760px" destroy-on-close>
|
||||||
|
<el-form
|
||||||
|
v-loading="unitDebug.loading"
|
||||||
|
:model="unitDebug.form"
|
||||||
|
label-width="150px"
|
||||||
|
class="unit-debug-form"
|
||||||
|
:disabled="unitDebug.loading || unitDebug.readonly"
|
||||||
|
>
|
||||||
|
<el-form-item v-for="item in unitDebugFields" :key="item.key" :label="item.label">
|
||||||
|
<el-input v-model="unitDebug.form[item.key]" clearable maxlength="32" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="unitDebug.visible = false">{{ unitDebug.readonly ? '关闭' : '取消' }}</el-button>
|
||||||
|
<el-button v-if="!unitDebug.readonly" type="primary" :loading="unitDebug.saving" @click="handleSaveUnitDebug">
|
||||||
|
保存单位
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="overlimitDebug.visible"
|
||||||
|
:title="overlimitDebug.readonly ? '监测点限值查询' : '监测点限值配置'"
|
||||||
|
width="860px"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<el-empty v-if="!overlimitDebug.rows.length" description="当前监测点详情未返回限值数据" />
|
||||||
|
<el-form v-else label-width="150px" class="overlimit-debug-form" :disabled="overlimitDebug.readonly">
|
||||||
|
<el-form-item v-for="row in overlimitDebug.rows" :key="row.key" :label="row.label">
|
||||||
|
<el-input
|
||||||
|
v-if="!overlimitDebug.readonly"
|
||||||
|
v-model="row.value"
|
||||||
|
clearable
|
||||||
|
maxlength="32"
|
||||||
|
@change="value => handleOverlimitValueChange(row.key, value)"
|
||||||
|
/>
|
||||||
|
<span v-else class="debug-readonly-value">{{ row.value }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="overlimitDebug.visible = false">{{ overlimitDebug.readonly ? '关闭' : '取消' }}</el-button>
|
||||||
|
<el-button v-if="!overlimitDebug.readonly" type="primary" @click="handleSaveOverlimitDebug">
|
||||||
|
保存限值
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -87,10 +136,12 @@ import { CirclePlus } from '@element-plus/icons-vue'
|
|||||||
import {
|
import {
|
||||||
deleteAddLedgerNode,
|
deleteAddLedgerNode,
|
||||||
getAddLedgerDetail,
|
getAddLedgerDetail,
|
||||||
|
getAddLedgerEquipmentUnit,
|
||||||
getAddLedgerTree,
|
getAddLedgerTree,
|
||||||
getAvailableLineNos,
|
getAvailableLineNos,
|
||||||
saveAddLedgerEngineering,
|
saveAddLedgerEngineering,
|
||||||
saveAddLedgerEquipment,
|
saveAddLedgerEquipment,
|
||||||
|
saveAddLedgerEquipmentUnit,
|
||||||
saveAddLedgerLine,
|
saveAddLedgerLine,
|
||||||
saveAddLedgerProject
|
saveAddLedgerProject
|
||||||
} from '@/api/tools/addLedger'
|
} from '@/api/tools/addLedger'
|
||||||
@@ -155,6 +206,59 @@ const projectForm = ref<AddLedger.ProjectForm>(createEmptyProjectForm())
|
|||||||
const equipmentForm = ref<AddLedger.EquipmentForm>(createEmptyEquipmentForm())
|
const equipmentForm = ref<AddLedger.EquipmentForm>(createEmptyEquipmentForm())
|
||||||
const lineForm = ref<AddLedger.LineForm>(createEmptyLineForm())
|
const lineForm = ref<AddLedger.LineForm>(createEmptyLineForm())
|
||||||
|
|
||||||
|
const unitDebugFields: Array<{ key: keyof AddLedger.EquipmentUnitForm; label: string }> = [
|
||||||
|
{ key: 'unitFrequency', label: '频率单位' },
|
||||||
|
{ key: 'unitFrequencyDev', label: '频率偏差单位' },
|
||||||
|
{ key: 'phaseVoltage', label: '相电压单位' },
|
||||||
|
{ key: 'lineVoltage', label: '线电压单位' },
|
||||||
|
{ key: 'voltageDev', label: '电压上偏差单位' },
|
||||||
|
{ key: 'uvoltageDev', label: '电压下偏差单位' },
|
||||||
|
{ key: 'ieffective', label: '电流有效值单位' },
|
||||||
|
{ key: 'singleP', label: '单相有功功率单位' },
|
||||||
|
{ key: 'singleViewP', label: '单相视在功率单位' },
|
||||||
|
{ key: 'singleNoP', label: '单相无功功率单位' },
|
||||||
|
{ key: 'totalActiveP', label: '总有功功率单位' },
|
||||||
|
{ key: 'totalViewP', label: '总视在功率单位' },
|
||||||
|
{ key: 'totalNoP', label: '总无功功率单位' },
|
||||||
|
{ key: 'vfundEffective', label: '基波电压有效值单位' },
|
||||||
|
{ key: 'ifund', label: '基波电流单位' },
|
||||||
|
{ key: 'fundActiveP', label: '基波有功功率单位' },
|
||||||
|
{ key: 'fundNoP', label: '基波无功功率单位' },
|
||||||
|
{ key: 'vdistortion', label: '电压总谐波畸变率单位' },
|
||||||
|
{ key: 'vharmonicRate', label: '谐波电压含有率单位' },
|
||||||
|
{ key: 'iharmonic', label: '谐波电流单位' },
|
||||||
|
{ key: 'pharmonic', label: '谐波有功功率单位' },
|
||||||
|
{ key: 'iiharmonic', label: '间谐波电流单位' },
|
||||||
|
{ key: 'positiveV', label: '正序电压单位' },
|
||||||
|
{ key: 'noPositiveV', label: '零序/负序电压单位' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const unitDebug = reactive({
|
||||||
|
visible: false,
|
||||||
|
loading: false,
|
||||||
|
saving: false,
|
||||||
|
readonly: false,
|
||||||
|
form: { devId: '' } as AddLedger.EquipmentUnitForm
|
||||||
|
})
|
||||||
|
|
||||||
|
const overlimitDebug = reactive({
|
||||||
|
visible: false,
|
||||||
|
readonly: false,
|
||||||
|
rows: [] as Array<{ key: string; label: string; value: string }>
|
||||||
|
})
|
||||||
|
|
||||||
|
const overlimitDebugLabelMap: Record<string, string> = {
|
||||||
|
freqDev: '频率偏差限值',
|
||||||
|
voltageFluctuation: '电压波动限值',
|
||||||
|
voltageDev: '电压上偏差限值',
|
||||||
|
uvoltageDev: '电压下偏差限值',
|
||||||
|
ubalance: '三相电压不平衡限值',
|
||||||
|
shortUbalance: '短时电压不平衡限值',
|
||||||
|
flicker: '闪变限值',
|
||||||
|
uaberrance: '电压总谐波畸变率限值',
|
||||||
|
iNeg: '负序电流限值'
|
||||||
|
}
|
||||||
|
|
||||||
const draftIds = {
|
const draftIds = {
|
||||||
engineering: '__draft_engineering__',
|
engineering: '__draft_engineering__',
|
||||||
project: '__draft_project__',
|
project: '__draft_project__',
|
||||||
@@ -847,6 +951,90 @@ const handleSaveLine = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOpenUnitDebug = async (devIdFromForm = '', readonly = false) => {
|
||||||
|
const devId = devIdFromForm || equipmentForm.value.id || selectedNode.value?.id || ''
|
||||||
|
if (!devId) {
|
||||||
|
ElMessage.warning('请先选择或保存设备后再查看单位配置')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
unitDebug.visible = true
|
||||||
|
unitDebug.loading = true
|
||||||
|
unitDebug.readonly = readonly
|
||||||
|
unitDebug.form = { devId }
|
||||||
|
try {
|
||||||
|
const response = await getAddLedgerEquipmentUnit({ devId })
|
||||||
|
unitDebug.form = {
|
||||||
|
...(response.data || {}),
|
||||||
|
devId
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
unitDebug.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveUnitDebug = async () => {
|
||||||
|
if (unitDebug.readonly) return
|
||||||
|
if (!unitDebug.form.devId) return
|
||||||
|
|
||||||
|
unitDebug.saving = true
|
||||||
|
try {
|
||||||
|
await saveAddLedgerEquipmentUnit(unitDebug.form)
|
||||||
|
ElMessage.success('设备单位保存成功')
|
||||||
|
unitDebug.visible = false
|
||||||
|
} finally {
|
||||||
|
unitDebug.saving = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDebugValue = (value: unknown) => {
|
||||||
|
if (value === null || value === undefined || value === '') return '--'
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveOverlimitDebugLabel = (key: string) => {
|
||||||
|
const fixedLabel = overlimitDebugLabelMap[key]
|
||||||
|
if (fixedLabel) return fixedLabel
|
||||||
|
|
||||||
|
const harmonicMatch = key.match(/^(uharm|iharm|inuharm)_?(\d+)$/)
|
||||||
|
if (!harmonicMatch) return key
|
||||||
|
|
||||||
|
const [, type, order] = harmonicMatch
|
||||||
|
if (type === 'uharm') return `${order}次谐波电压限值`
|
||||||
|
if (type === 'iharm') return `${order}次谐波电流限值`
|
||||||
|
return `${order}次间谐波电流限值`
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpenOverlimitDebug = (lineIdFromForm = '', readonly = false) => {
|
||||||
|
const lineId = lineIdFromForm || lineForm.value.line_id || lineForm.value.id || selectedNode.value?.id || ''
|
||||||
|
if (!lineId) {
|
||||||
|
ElMessage.warning('请先选择或保存测点后再查看限值配置')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlimit = lineForm.value.overlimit || {}
|
||||||
|
overlimitDebug.readonly = readonly
|
||||||
|
overlimitDebug.rows = Object.entries(overlimit)
|
||||||
|
.filter(([key, value]) => key !== 'id' && value !== undefined && value !== null && value !== '')
|
||||||
|
.map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
label: resolveOverlimitDebugLabel(key),
|
||||||
|
value: formatDebugValue(value)
|
||||||
|
}))
|
||||||
|
overlimitDebug.visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOverlimitValueChange = (key: string, value: string) => {
|
||||||
|
lineForm.value.overlimit = {
|
||||||
|
...(lineForm.value.overlimit || {}),
|
||||||
|
[key]: value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveOverlimitDebug = () => {
|
||||||
|
ElMessage.warning('限值由保存测点后自动重新计算,请保存测点以更新限值')
|
||||||
|
}
|
||||||
|
|
||||||
const resolveActiveNodeId = () => {
|
const resolveActiveNodeId = () => {
|
||||||
if (activeLevel.value === 0) return engineeringForm.value.id
|
if (activeLevel.value === 0) return engineeringForm.value.id
|
||||||
if (activeLevel.value === 1) return projectForm.value.id
|
if (activeLevel.value === 1) return projectForm.value.id
|
||||||
@@ -991,10 +1179,46 @@ onMounted(async () => {
|
|||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.unit-debug-form,
|
||||||
|
.overlimit-debug-form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
max-height: 58vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
column-gap: 18px;
|
||||||
|
row-gap: 10px;
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit-debug-form :deep(.el-form-item),
|
||||||
|
.overlimit-debug-form :deep(.el-form-item) {
|
||||||
|
min-width: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit-debug-form :deep(.el-input),
|
||||||
|
.overlimit-debug-form :deep(.el-input) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-readonly-value {
|
||||||
|
min-width: 0;
|
||||||
|
line-height: 32px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.add-ledger-layout {
|
.add-ledger-layout {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: minmax(260px, 0.45fr) minmax(0, 1fr);
|
grid-template-rows: minmax(260px, 0.45fr) minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.unit-debug-form,
|
||||||
|
.overlimit-debug-form {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,6 +6,36 @@ export type LedgerContextIds = {
|
|||||||
deviceId: string
|
deviceId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LineVoltageDefault = {
|
||||||
|
capacity: number
|
||||||
|
ptRatio?: number
|
||||||
|
pt2Ratio?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineVoltageDefaultMap = new Map<number, LineVoltageDefault>([
|
||||||
|
[0.38, { capacity: 10, ptRatio: 380, pt2Ratio: 380 }],
|
||||||
|
[6, { capacity: 100, ptRatio: 6000, pt2Ratio: 100 }],
|
||||||
|
[10, { capacity: 100, ptRatio: 10000, pt2Ratio: 100 }],
|
||||||
|
[20, { capacity: 200, ptRatio: 20000, pt2Ratio: 100 }],
|
||||||
|
[35, { capacity: 250, ptRatio: 35000, pt2Ratio: 100 }],
|
||||||
|
[66, { capacity: 500, ptRatio: 66000, pt2Ratio: 100 }],
|
||||||
|
[110, { capacity: 750, ptRatio: 110000, pt2Ratio: 100 }],
|
||||||
|
[220, { capacity: 2000, ptRatio: 220000, pt2Ratio: 100 }],
|
||||||
|
[330, { capacity: 3000, ptRatio: 330000, pt2Ratio: 100 }],
|
||||||
|
[500, { capacity: 4500, ptRatio: 500000, pt2Ratio: 100 }],
|
||||||
|
[750, { capacity: 7000, ptRatio: 750000, pt2Ratio: 100 }],
|
||||||
|
[1000, { capacity: 9000, ptRatio: 1000000, pt2Ratio: 100 }]
|
||||||
|
])
|
||||||
|
|
||||||
|
const resolveLineVoltageDefault = (voltage?: number): LineVoltageDefault => {
|
||||||
|
return lineVoltageDefaultMap.get(Number(voltage)) || { capacity: 10, ptRatio: undefined, pt2Ratio: undefined }
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldUseVoltageRatioDefault = (left?: number, right?: number) =>
|
||||||
|
left === undefined || right === undefined || (left === 1 && right === 1)
|
||||||
|
|
||||||
|
const shouldUseVoltageCapacityDefault = (value?: number) => value === undefined || value === 10
|
||||||
|
|
||||||
export function generateGuidText() {
|
export function generateGuidText() {
|
||||||
return window.crypto.randomUUID().replace(/-/g, '')
|
return window.crypto.randomUUID().replace(/-/g, '')
|
||||||
}
|
}
|
||||||
@@ -50,6 +80,8 @@ export function createEmptyEquipmentForm(parentProjectId = '', parentEngineering
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createEmptyLineForm(parentDeviceId = ''): AddLedger.LineForm {
|
export function createEmptyLineForm(parentDeviceId = ''): AddLedger.LineForm {
|
||||||
|
const lineVoltageDefault = resolveLineVoltageDefault(10)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: '',
|
id: '',
|
||||||
line_id: '',
|
line_id: '',
|
||||||
@@ -58,16 +90,17 @@ export function createEmptyLineForm(parentDeviceId = ''): AddLedger.LineForm {
|
|||||||
name: '',
|
name: '',
|
||||||
line_no: undefined,
|
line_no: undefined,
|
||||||
conType: undefined,
|
conType: undefined,
|
||||||
vol_grade: undefined,
|
vol_grade: 10,
|
||||||
position: '',
|
position: '',
|
||||||
ct_ratio: undefined,
|
ct_ratio: 300,
|
||||||
ct2_ratio: undefined,
|
ct2_ratio: 5,
|
||||||
pt_ratio: undefined,
|
pt_ratio: lineVoltageDefault.ptRatio,
|
||||||
pt2_ratio: undefined,
|
pt2_ratio: lineVoltageDefault.pt2Ratio,
|
||||||
short_circuit_capacity: undefined,
|
short_circuit_capacity: lineVoltageDefault.capacity,
|
||||||
basic_capacity: undefined,
|
basic_capacity: lineVoltageDefault.capacity,
|
||||||
protocol_capacity: undefined,
|
protocol_capacity: lineVoltageDefault.capacity,
|
||||||
dev_capacity: undefined,
|
dev_capacity: lineVoltageDefault.capacity,
|
||||||
|
lineType: 0,
|
||||||
monitor_obj: '',
|
monitor_obj: '',
|
||||||
is_govern: 0,
|
is_govern: 0,
|
||||||
monitor_user: '',
|
monitor_user: '',
|
||||||
@@ -211,6 +244,16 @@ export const normalizeLineDetail = (
|
|||||||
): AddLedger.LineForm => {
|
): AddLedger.LineForm => {
|
||||||
const data = (detail || {}) as Record<string, unknown>
|
const data = (detail || {}) as Record<string, unknown>
|
||||||
const lineId = resolveString(data, 'line_id', 'lineId', 'id') || node?.id || generateGuidText()
|
const lineId = resolveString(data, 'line_id', 'lineId', 'id') || node?.id || generateGuidText()
|
||||||
|
const volGrade = resolveNumber(data, 'vol_grade', 'volGrade') ?? 10
|
||||||
|
const lineVoltageDefault = resolveLineVoltageDefault(volGrade)
|
||||||
|
const ctRatio = resolveNumber(data, 'ct_ratio', 'ctRatio')
|
||||||
|
const ct2Ratio = resolveNumber(data, 'ct2_ratio', 'ct2Ratio')
|
||||||
|
const ptRatio = resolveNumber(data, 'pt_ratio', 'ptRatio')
|
||||||
|
const pt2Ratio = resolveNumber(data, 'pt2_ratio', 'pt2Ratio')
|
||||||
|
const shortCircuitCapacity = resolveNumber(data, 'short_circuit_capacity', 'shortCircuitCapacity')
|
||||||
|
const basicCapacity = resolveNumber(data, 'basic_capacity', 'basicCapacity')
|
||||||
|
const protocolCapacity = resolveNumber(data, 'protocol_capacity', 'protocolCapacity')
|
||||||
|
const devCapacity = resolveNumber(data, 'dev_capacity', 'devCapacity')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: resolveString(data, 'id') || lineId,
|
id: resolveString(data, 'id') || lineId,
|
||||||
@@ -220,20 +263,24 @@ export const normalizeLineDetail = (
|
|||||||
name: resolveString(data, 'name') || node?.name || '',
|
name: resolveString(data, 'name') || node?.name || '',
|
||||||
line_no: resolveNumber(data, 'line_no', 'lineNo'),
|
line_no: resolveNumber(data, 'line_no', 'lineNo'),
|
||||||
conType: resolveNumber(data, 'conType'),
|
conType: resolveNumber(data, 'conType'),
|
||||||
vol_grade: resolveNumber(data, 'vol_grade', 'volGrade'),
|
vol_grade: volGrade,
|
||||||
position: resolveString(data, 'position'),
|
position: resolveString(data, 'position'),
|
||||||
ct_ratio: resolveNumber(data, 'ct_ratio', 'ctRatio'),
|
ct_ratio: shouldUseVoltageRatioDefault(ctRatio, ct2Ratio) ? 300 : ctRatio,
|
||||||
ct2_ratio: resolveNumber(data, 'ct2_ratio', 'ct2Ratio'),
|
ct2_ratio: shouldUseVoltageRatioDefault(ctRatio, ct2Ratio) ? 5 : ct2Ratio,
|
||||||
pt_ratio: resolveNumber(data, 'pt_ratio', 'ptRatio'),
|
pt_ratio: shouldUseVoltageRatioDefault(ptRatio, pt2Ratio) ? lineVoltageDefault.ptRatio : ptRatio,
|
||||||
pt2_ratio: resolveNumber(data, 'pt2_ratio', 'pt2Ratio'),
|
pt2_ratio: shouldUseVoltageRatioDefault(ptRatio, pt2Ratio) ? lineVoltageDefault.pt2Ratio : pt2Ratio,
|
||||||
short_circuit_capacity: resolveNumber(data, 'short_circuit_capacity', 'shortCircuitCapacity'),
|
short_circuit_capacity: shouldUseVoltageCapacityDefault(shortCircuitCapacity)
|
||||||
basic_capacity: resolveNumber(data, 'basic_capacity', 'basicCapacity'),
|
? lineVoltageDefault.capacity
|
||||||
protocol_capacity: resolveNumber(data, 'protocol_capacity', 'protocolCapacity'),
|
: shortCircuitCapacity,
|
||||||
dev_capacity: resolveNumber(data, 'dev_capacity', 'devCapacity'),
|
basic_capacity: shouldUseVoltageCapacityDefault(basicCapacity) ? lineVoltageDefault.capacity : basicCapacity,
|
||||||
|
protocol_capacity: shouldUseVoltageCapacityDefault(protocolCapacity) ? lineVoltageDefault.capacity : protocolCapacity,
|
||||||
|
dev_capacity: shouldUseVoltageCapacityDefault(devCapacity) ? lineVoltageDefault.capacity : devCapacity,
|
||||||
|
lineType: resolveNumber(data, 'lineType', 'line_type') ?? 0,
|
||||||
monitor_obj: resolveString(data, 'monitor_obj', 'monitorObj'),
|
monitor_obj: resolveString(data, 'monitor_obj', 'monitorObj'),
|
||||||
is_govern: resolveNumber(data, 'is_govern', 'isGovern') ?? 0,
|
is_govern: resolveNumber(data, 'is_govern', 'isGovern') ?? 0,
|
||||||
monitor_user: resolveString(data, 'monitor_user', 'monitorUser'),
|
monitor_user: resolveString(data, 'monitor_user', 'monitorUser'),
|
||||||
is_important: resolveNumber(data, 'is_important', 'isImportant') ?? 0
|
is_important: resolveNumber(data, 'is_important', 'isImportant') ?? 0,
|
||||||
|
overlimit: (data.overlimit || data.overLimit) as AddLedger.OverlimitDetail | undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user