From cd5e8b5f2d71e7fb1224e32593071d6955670759 Mon Sep 17 00:00:00 2001 From: zxr <271055687@qq.com> Date: Fri, 26 Jun 2026 12:51:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E6=89=A7=E8=A1=8C1-19?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/kb/document.ts | 3 + src/api/kb/knowledge.ts | 68 ++ src/api/message.ts | 77 +- src/api/ops/asset.ts | 27 + src/api/ops/automation.ts | 87 +++ src/api/ops/bigScreen.ts | 104 +++ src/api/ops/businessSystem.ts | 201 ++++++ src/api/ops/datacenter.ts | 10 + src/api/ops/dcControl.ts | 176 +++++ src/api/ops/feedbackTicket.ts | 6 + src/api/ops/governance.ts | 81 +++ src/api/ops/incident.ts | 52 ++ src/api/ops/ipam.ts | 10 + src/api/ops/logs.ts | 97 +++ src/api/ops/netarchTopo.ts | 28 + src/api/ops/noticeChannel.ts | 32 + src/api/ops/rack.ts | 5 + src/api/ops/rawEvent.ts | 40 ++ src/api/ops/report.ts | 29 +- src/api/ops/unit.ts | 5 + src/components/index.ts | 2 + src/components/message-box/index.vue | 101 ++- src/components/message-box/list.vue | 45 +- src/components/page-state/index.vue | 89 +++ src/components/search-table/index.vue | 119 ++- src/directive/permission/index.ts | 68 +- src/router/local-menu-flat.ts | 67 +- src/router/local-menu-items.ts | 165 +++++ src/router/routes/modules/ops.ts | 88 +++ src/router/routes/modules/visualization.ts | 10 + src/views/ops/pages/alert/incidents/index.vue | 531 ++++++++++++++ .../notice/components/ChannelDetailDialog.vue | 6 +- .../notice/components/ChannelFormDialog.vue | 231 +++++- src/views/ops/pages/alert/notice/index.vue | 12 +- .../ops/pages/alert/raw-events/index.vue | 381 ++++++++++ src/views/ops/pages/automation/index.vue | 432 +++++++++++ src/views/ops/pages/big-screen/index.vue | 482 +++++++++++++ .../ops/pages/business-system/detail.vue | 679 ++++++++++++++++++ src/views/ops/pages/business-system/index.vue | 359 +++++++++ .../ops/pages/datacenter/house/index.vue | 45 +- .../ops/pages/feedback/all/config/columns.ts | 24 + src/views/ops/pages/feedback/all/index.vue | 44 ++ .../components/TicketDetailDialog.vue | 75 ++ .../feedback/components/TicketFormDialog.vue | 62 +- .../ops/pages/feedback/undo/config/columns.ts | 4 + src/views/ops/pages/feedback/undo/index.vue | 4 + src/views/ops/pages/governance/index.vue | 318 ++++++++ src/views/ops/pages/kb/items/index.vue | 10 + .../ops/pages/log-mgmt/entries/index.vue | 57 +- .../ops/pages/log-mgmt/syslog-rules/index.vue | 32 +- .../pages/log-mgmt/trap-dictionary/index.vue | 44 +- .../pages/monitor/collection-health/index.vue | 250 +++++++ .../topo/components/NodeDetailDialog.vue | 6 + .../topo/components/NodeEditDialog.vue | 7 + src/views/ops/pages/netarch/topo/index.vue | 123 +++- .../netarch/topo/services/topoService.ts | 1 + src/views/ops/pages/netarch/topo/types.ts | 1 + src/views/ops/pages/report/fault/index.vue | 43 ++ src/views/ops/pages/report/topn/index.vue | 20 +- .../ops/pages/resource-context/index.vue | 408 +++++++++++ .../system-settings/audit-logs/index.vue | 197 +++++ .../danger-approvals/index.vue | 246 +++++++ .../system-settings/system-logs/index.vue | 12 +- .../visualization/ops-big-screen/index.vue | 81 +++ 64 files changed, 7014 insertions(+), 105 deletions(-) create mode 100644 src/api/kb/knowledge.ts create mode 100644 src/api/ops/automation.ts create mode 100644 src/api/ops/bigScreen.ts create mode 100644 src/api/ops/businessSystem.ts create mode 100644 src/api/ops/governance.ts create mode 100644 src/api/ops/incident.ts create mode 100644 src/api/ops/rawEvent.ts create mode 100644 src/components/page-state/index.vue create mode 100644 src/views/ops/pages/alert/incidents/index.vue create mode 100644 src/views/ops/pages/alert/raw-events/index.vue create mode 100644 src/views/ops/pages/automation/index.vue create mode 100644 src/views/ops/pages/big-screen/index.vue create mode 100644 src/views/ops/pages/business-system/detail.vue create mode 100644 src/views/ops/pages/business-system/index.vue create mode 100644 src/views/ops/pages/governance/index.vue create mode 100644 src/views/ops/pages/monitor/collection-health/index.vue create mode 100644 src/views/ops/pages/resource-context/index.vue create mode 100644 src/views/ops/pages/system-settings/audit-logs/index.vue create mode 100644 src/views/ops/pages/system-settings/danger-approvals/index.vue create mode 100644 src/views/visualization/ops-big-screen/index.vue diff --git a/src/api/kb/document.ts b/src/api/kb/document.ts index 0c4dedc..0ad9410 100644 --- a/src/api/kb/document.ts +++ b/src/api/kb/document.ts @@ -35,6 +35,7 @@ export interface Document { tags: string attachments: string | null related_docs: string | null + detection_point_ids: string | null metadata: string | null keywords: string remarks: string @@ -66,6 +67,7 @@ export interface CreateDocumentParams { sub_category?: string keywords?: string tags?: string + detection_point_ids?: string remarks?: string } @@ -80,6 +82,7 @@ export interface UpdateDocumentParams { sub_category?: string keywords?: string tags?: string + detection_point_ids?: string remarks?: string } diff --git a/src/api/kb/knowledge.ts b/src/api/kb/knowledge.ts new file mode 100644 index 0000000..5097005 --- /dev/null +++ b/src/api/kb/knowledge.ts @@ -0,0 +1,68 @@ +import { request } from '@/api/request' + +export interface DetectionPoint { + id: number + resource_category: string + metric_code: string + alert_rule_id: number + fault_type: string + knowledge_type: 'document' | 'faq' + knowledge_id: number + knowledge_title: string + enabled: boolean + remarks: string +} + +export interface KnowledgeRecommendation { + knowledge_type: 'document' | 'faq' + knowledge_id: number + title: string + status: string + resource_category: string + metric_code: string + alert_rule_id: number + fault_type: string + detection_point_id: number + score: number + reason: string +} + +export interface IncidentKnowledgeRelation { + id: number + incident_id: number + ticket_id: number + knowledge_type: 'document' | 'faq' + knowledge_id: number + knowledge_title: string + relation_type: string + source: string + score: number + reason: string + created_at: string +} + +export const fetchKnowledgeRecommendations = (params: { + resource_category?: string + metric_code?: string + alert_rule_id?: number + fault_type?: string + limit?: number +}) => request.get('/Kb/v1/knowledge/recommend', { params }) + +export const fetchIncidentKnowledge = (incidentId: number) => request.get(`/Kb/v1/knowledge/incidents/${incidentId}/relations`) + +export const fetchTicketKnowledge = (ticketId: number) => request.get('/Kb/v1/knowledge/relations', { params: { ticket_id: ticketId } }) + +export const relateIncidentKnowledge = (data: { + incident_id?: number + ticket_id?: number + knowledge_type: 'document' | 'faq' + knowledge_id: number + knowledge_title?: string + relation_type?: string + source?: string + score?: number + reason?: string +}) => request.post('/Kb/v1/knowledge/relations', data) + +export const createPostmortemDraft = (data: Record) => request.post('/Kb/v1/knowledge/postmortem/draft', data) diff --git a/src/api/message.ts b/src/api/message.ts index 984f4c9..5f5a9f4 100644 --- a/src/api/message.ts +++ b/src/api/message.ts @@ -1,4 +1,5 @@ import axios from 'axios' +import { request } from '@/api/request' export interface MessageRecord { id: number @@ -10,19 +11,89 @@ export interface MessageRecord { time: string status: 0 | 1 messageType?: number + deliveryStatus?: string + channelType?: string + attemptCount?: number + lastError?: string + incidentId?: number + alertRecordId?: number + nextRetryAt?: string } export type MessageListType = MessageRecord[] -export function queryMessageList() { - return axios.post('/api/message/list') +interface ApiResponse { + code?: number + message?: string + data?: T + details?: T +} + +/** 通知投递历史 */ +export interface NotificationDeliveryRecord { + id: number + incident_id: number + alert_record_id: number + channel_id: number + channel_type: string + target: string + subject: string + content: string + status: string + attempt_count: number + next_retry_at?: string + last_error?: string + trace_id?: string + delivered_at?: string + created_at?: string + updated_at?: string +} + +/** 投递历史查询参数 */ +export interface NotificationDeliveryQuery { + page?: number + page_size?: number + keyword?: string + incident_id?: number + status?: string + channel_type?: string +} + +/** 分页结果 */ +export interface PageResult { + total: number + page: number + page_size: number + data: T[] +} + +function unwrapData(response: ApiResponse | T): T { + const wrapped = response as ApiResponse + return (wrapped.details ?? wrapped.data ?? response) as T +} + +/** 查询站内消息列表 */ +export async function queryMessageList() { + const response = await request.get>('/Alert/v1/message/list') + return { data: unwrapData(response) || [] } } interface MessageStatus { ids: number[] } +/** 标记站内消息已读 */ export function setMessageStatus(data: MessageStatus) { - return axios.post('/api/message/read', data) + return request.post>('/Alert/v1/message/read', data) +} + +/** 查询通知投递历史 */ +export function queryNotificationDeliveryHistory(params?: NotificationDeliveryQuery) { + return request.get>>('/Alert/v1/notification/deliveries', { params: params || {} }) +} + +/** 人工补发通知 */ +export function resendNotificationDelivery(id: number) { + return request.post>(`/Alert/v1/notification/deliveries/${id}/resend`) } export interface ChatRecord { diff --git a/src/api/ops/asset.ts b/src/api/ops/asset.ts index d20bb3d..6d3daf2 100644 --- a/src/api/ops/asset.ts +++ b/src/api/ops/asset.ts @@ -105,6 +105,11 @@ export const createAsset = (data: AssetForm) => { return request.post('/Assets/v1/asset/create', data) } +/** 批量导入资产 */ +export const bulkImportAssets = (data: { dry_run?: boolean; items: any[] }) => { + return request.post('/Assets/v1/asset/bulk_import', data) +} + /** 更新资产 */ export const updateAsset = (data: AssetForm) => { return request.put('/Assets/v1/asset/update', data) @@ -146,3 +151,25 @@ export const fetchFloorOptions = (datacenterId: number) => { export const fetchRackOptions = (params?: { datacenter_id?: number; floor_id?: number }) => { return request.post('/Assets/v1/rack/list', params || {}) } + +/** 绑定资产和监控资源 */ +export const linkAssetResource = (data: { + asset_id: number + resource_uid: string + display_name?: string + business_system_id?: number + health_status?: string + alert_status?: string +}) => { + return request.post('/Assets/v1/asset/resource/link', data) +} + +/** 查询资产绑定的监控资源 */ +export const fetchAssetResourceBindings = (assetId: number) => { + return request.get(`/Assets/v1/asset/resource/${assetId}`) +} + +/** 解除资产和监控资源绑定 */ +export const unlinkAssetResource = (assetId: number, resourceUid: string) => { + return request.delete(`/Assets/v1/asset/resource/${assetId}`, { params: { resource_uid: resourceUid } }) +} diff --git a/src/api/ops/automation.ts b/src/api/ops/automation.ts new file mode 100644 index 0000000..b0c84e1 --- /dev/null +++ b/src/api/ops/automation.ts @@ -0,0 +1,87 @@ +import { request } from '@/api/request' + +export interface AutomationScript { + id: number + name: string + resource_category: string + command_template: string + approval_level: string + rollback_command?: string + rollback_required: boolean + description?: string + status: string + created_at?: string + updated_at?: string +} + +export interface AutomationExecution { + id: number + script_id: number + mode: string + status: string + resource_uid: string + rendered_command: string + incident_id?: number + ticket_id?: number + approval_level: string + approved_by?: string + approved_at?: string + operator?: string + started_at?: string + finished_at?: string + timeout_sec?: number + stdout?: string + stderr?: string + exit_code?: number + error?: string + audit_trail?: string + created_at?: string + updated_at?: string +} + +export interface AutomationRunRequest { + resource_uid: string + incident_id?: number + ticket_id?: number + operator?: string + approver?: string + approved?: boolean + mode?: 'execute' | 'rollback' + variables?: Record +} + +export interface PageResponse { + total: number + page: number + page_size: number + data: T[] +} + +export const fetchAutomationScripts = (params?: Record) => + request.get<{ details: PageResponse }>('/DC-Control/v1/automation/scripts', { params }) + +export const createAutomationScript = (data: Partial) => + request.post<{ details: AutomationScript }>('/DC-Control/v1/automation/scripts', data) + +export const updateAutomationScript = (id: number, data: Partial) => + request.put(`/DC-Control/v1/automation/scripts/${id}`, data) + +export const deleteAutomationScript = (id: number) => request.delete(`/DC-Control/v1/automation/scripts/${id}`) + +export const dryRunAutomationScript = (id: number, data: AutomationRunRequest) => + request.post<{ details: AutomationExecution }>(`/DC-Control/v1/automation/scripts/${id}/dry-run`, data) + +export const requestAutomationExecution = (id: number, data: AutomationRunRequest) => + request.post<{ details: AutomationExecution }>(`/DC-Control/v1/automation/scripts/${id}/executions`, data) + +export const executeAutomationScript = (id: number, data: AutomationRunRequest) => + request.post<{ details: AutomationExecution }>(`/DC-Control/v1/automation/scripts/${id}/execute`, data) + +export const rollbackAutomationScript = (id: number, data: AutomationRunRequest) => + request.post<{ details: AutomationExecution }>(`/DC-Control/v1/automation/scripts/${id}/rollback`, data) + +export const fetchAutomationExecutions = (params?: Record) => + request.get<{ details: PageResponse }>('/DC-Control/v1/automation/executions', { params }) + +export const approveAutomationExecution = (id: number, data: { approver: string; operator?: string }) => + request.post<{ details: AutomationExecution }>(`/DC-Control/v1/automation/executions/${id}/approve`, data) diff --git a/src/api/ops/bigScreen.ts b/src/api/ops/bigScreen.ts new file mode 100644 index 0000000..e2fe527 --- /dev/null +++ b/src/api/ops/bigScreen.ts @@ -0,0 +1,104 @@ +import { request } from '@/api/request' + +export interface ApiResponse { + code: number + data?: T + details?: T + message?: string +} + +export interface BigScreen { + id: number + name: string + description?: string + thumbnail?: string + group_id?: number + goview_project_id?: string + width: number + height: number + status: number + publish_status: 'draft' | 'published' | 'offline' | string + is_public?: boolean + creator_name?: string + created_at?: string + updated_at?: string +} + +export interface BigScreenGroup { + id: number + name: string + description?: string + sort_order: number + status: number +} + +export interface BigScreenPublish { + id: number + screen_id: number + goview_project_id: string + version: string + publish_type: number + status: number + publisher_name?: string + created_at?: string +} + +export interface BigScreenCarousel { + id: number + name: string + screen_ids: Record + interval_seconds: number + enabled: boolean +} + +export interface BigScreenSnapshot { + id: number + screen_id: number + publish_id?: number + file_name: string + file_path: string + file_size: number + checksum?: string + exporter_name?: string + created_at?: string +} + +export interface PageResult { + total: number + page: number + page_size: number + data: T[] +} + +export const fetchBigScreens = (params?: { page?: number; page_size?: number; keyword?: string }) => + request.get>>('/Mgt/v1/screen', { params }) + +export const createBigScreen = (data: Partial) => + request.post>('/Mgt/v1/screen', data) + +export const updateBigScreen = (id: number, data: Partial) => + request.put>(`/Mgt/v1/screen/${id}`, data) + +export const publishBigScreen = (data: { screen_id: number; publish_type: 1 | 2; change_log?: string }) => + request.post>('/Mgt/v1/publish', data) + +export const fetchBigScreenPublishes = (params: { screen_id: number; page?: number; page_size?: number }) => + request.get>>('/Mgt/v1/publish', { params }) + +export const fetchBigScreenGroups = () => + request.get>('/Mgt/v1/big-screen/groups') + +export const createBigScreenGroup = (data: Partial) => + request.post>('/Mgt/v1/big-screen/groups', data) + +export const fetchBigScreenCarousels = () => + request.get>('/Mgt/v1/big-screen/carousels') + +export const createBigScreenCarousel = (data: Partial) => + request.post>('/Mgt/v1/big-screen/carousels', data) + +export const fetchBigScreenSnapshots = (params?: { screen_id?: number }) => + request.get>('/Mgt/v1/big-screen/snapshots', { params }) + +export const createBigScreenSnapshot = (data: Partial) => + request.post>('/Mgt/v1/big-screen/snapshots', data) diff --git a/src/api/ops/businessSystem.ts b/src/api/ops/businessSystem.ts new file mode 100644 index 0000000..7bb6473 --- /dev/null +++ b/src/api/ops/businessSystem.ts @@ -0,0 +1,201 @@ +import { request } from '@/api/request' + +/** 业务系统基础信息 */ +export interface BusinessSystemItem { + id: number + code: string + name: string + system_type: string + owner_user_id?: number + department_id?: number + description?: string + status: string + is_builtin: boolean + health_score?: number + health_level?: string + resource_count?: number + active_incident_count?: number + created_at?: string + updated_at?: string +} + +/** 分页响应 */ +export interface PageResult { + total: number + page: number + page_size: number + data: T[] +} + +/** 业务系统列表查询参数 */ +export interface BusinessSystemListParams { + page?: number + size?: number + keyword?: string + status?: string +} + +/** 健康分扣分明细 */ +export interface BusinessHealthFactor { + key: string + label: string + score_delta: number + status: string + description: string +} + +/** 业务健康摘要 */ +export interface BusinessHealth { + business_system_id: number + health: { + score: number + level: string + deductions: Record + } + signal_counts: { + resources: number + url_probes: number + active_incidents: number + sla_status: string + } +} + +/** 业务系统资源成员 */ +export interface BusinessResourceMember { + id: number + resource_uid: string + resource_id?: number + role: string + criticality: string + weight: number + description?: string +} + +/** 业务拓扑节点 */ +export interface BusinessTopologyNode { + id?: number + node_id: string + resource_uid: string + label: string + type: string + status: string +} + +/** 业务拓扑边 */ +export interface BusinessTopologyEdge { + id?: number + source_node_id: string + target_node_id: string + topology_id?: number +} + +/** 业务系统依赖关系 */ +export interface BusinessDependency { + id: number + business_system_id: number + depends_on_business_system_id: number + dependency_type: string + criticality: string + description?: string +} + +/** 业务拓扑 */ +export interface BusinessTopology { + business_system_id: number + resources: unknown[] + dependencies: BusinessDependency[] + nodes: BusinessTopologyNode[] + links: BusinessTopologyEdge[] +} + +/** 业务时间线项 */ +export interface BusinessTimelineItem { + source: 'raw_event' | 'alert' | 'incident' | 'ticket' | 'note' | 'document' + source_id: string + occurred_at: string + title: string + message: string + severity?: string + status?: string + metadata?: Record +} + +/** 业务笔记 */ +export interface BusinessNote { + id: number + business_system_id: number + title?: string + content: string + author_user_id?: number + author_name?: string + visibility: string + occurred_at: string + created_at: string +} + +/** 关联知识或文档 */ +export interface BusinessDocumentLink { + id: number + business_system_id: number + title: string + url: string + link_type: string + description?: string + created_at: string +} + +/** 列表响应 */ +export interface ListResult { + list: T[] + count: number +} + +/** 获取业务系统列表 */ +export const fetchBusinessSystems = (params?: BusinessSystemListParams) => + request.get<{ code?: number; details?: PageResult; message?: string }>('/DC-Control/v1/business-systems', { params }) + +/** 获取业务系统详情 */ +export const fetchBusinessSystemDetail = (id: number) => + request.get<{ code?: number; details?: BusinessSystemItem; message?: string }>(`/DC-Control/v1/business-systems/${id}`) + +/** 获取业务健康摘要 */ +export const fetchBusinessHealth = (id: number) => + request.get<{ code?: number; details?: BusinessHealth; message?: string }>(`/DC-Control/v1/business-systems/${id}/health`) + +/** 获取业务资源成员 */ +export const fetchBusinessMembers = (id: number) => + request.get<{ code?: number; details?: ListResult; message?: string }>(`/DC-Control/v1/business-systems/${id}/resources`) + +/** 获取业务拓扑 */ +export const fetchBusinessTopology = (id: number) => + request.get<{ code?: number; details?: BusinessTopology; message?: string }>(`/DC-Control/v1/business-systems/${id}/topology`) + +/** 获取业务系统依赖 */ +export const fetchBusinessDependencies = (id: number) => + request.get<{ code?: number; details?: ListResult; message?: string }>(`/DC-Control/v1/business-systems/${id}/dependencies`) + +/** 创建业务系统依赖 */ +export const createBusinessDependency = (id: number, data: Partial) => + request.post<{ code?: number; details?: BusinessDependency; message?: string }>(`/DC-Control/v1/business-systems/${id}/dependencies`, data) + +/** 获取业务时间线 */ +export const fetchBusinessTimeline = (id: number, params?: { limit?: number }) => + request.get<{ code?: number; details?: ListResult; message?: string }>(`/DC-Control/v1/business-systems/${id}/timeline`, { + params, + }) + +/** 获取业务笔记 */ +export const fetchBusinessNotes = (id: number) => + request.get<{ code?: number; details?: ListResult; message?: string }>(`/DC-Control/v1/business-systems/${id}/notes`) + +/** 创建业务笔记 */ +export const createBusinessNote = (id: number, data: { title: string; content: string }) => + request.post<{ code?: number; details?: BusinessNote; message?: string }>(`/DC-Control/v1/business-systems/${id}/notes`, data) + +/** 获取业务文档链接 */ +export const fetchBusinessDocumentLinks = (id: number) => + request.get<{ code?: number; details?: ListResult; message?: string }>(`/DC-Control/v1/business-systems/${id}/documents`) + +/** 新增业务文档链接 */ +export const createBusinessDocumentLink = (id: number, data: Partial) => + request.post<{ code?: number; details?: BusinessDocumentLink; message?: string }>(`/DC-Control/v1/business-systems/${id}/documents`, data) diff --git a/src/api/ops/datacenter.ts b/src/api/ops/datacenter.ts index b3d74b7..2646f21 100644 --- a/src/api/ops/datacenter.ts +++ b/src/api/ops/datacenter.ts @@ -27,6 +27,11 @@ export const createDatacenter = (data: any) => { return request.post('/Assets/v1/datacenter/create', data) } +/** 批量导入数据中心 */ +export const bulkImportDatacenters = (data: { dry_run?: boolean; items: any[] }) => { + return request.post('/Assets/v1/datacenter/bulk_import', data) +} + /** 更新数据中心 */ export const updateDatacenter = (data: any) => { return request.put('/Assets/v1/datacenter/update', data) @@ -56,3 +61,8 @@ export const fetchDatacenterByCity = (cityId: number) => { export const fetchDatacenterTree = () => { return request.get('/Assets/v1/datacenter/tree') } + +/** 获取3D机房后端导出契约数据 */ +export const fetchDatacenterThreeDExport = (params?: { datacenter_ids?: string; room_ids?: string }) => { + return request.get('/Assets/v1/three-d/export', { params }) +} diff --git a/src/api/ops/dcControl.ts b/src/api/ops/dcControl.ts index 6022699..bfaf975 100644 --- a/src/api/ops/dcControl.ts +++ b/src/api/ops/dcControl.ts @@ -40,3 +40,179 @@ export const fetchCollectorStatistics = () => request.get('/DC-Control/v1/statis /** 获取 许可证信息 */ export const fetchLicenseInfo = () => request.get<{ code?: number; data?: LicenceConfig; message?: string }>('/DC-Control/v1/license') + +export interface PageResult { + total: number + page: number + page_size: number + data: T[] +} + +export interface OptionItem { + id?: number + label: string + value: string +} + +export interface ControlResource { + id: number + resource_uid: string + resource_category: string + service_identity: string + display_name: string + description?: string + status: string + business_system_id?: number + owner_user_id?: number + department_id?: number + asset_id?: number + datacenter_id?: number + room_id?: number + rack_id?: number + unit_start?: number + unit_end?: number + location_ref?: string + tags?: string + source_service?: string + source_table?: string + source_id?: number + created_at?: string + updated_at?: string +} + +export interface ControlResourceType { + id: number + resource_category: string + display_name: string + metric_template?: string + supported_collect_method?: string + description?: string + enabled: boolean +} + +export interface ControlMetricDefinition { + id: number + metric_code: string + metric_name: string + resource_category: string + unit?: string + threshold_suggestion?: string + collect_method?: string + description?: string + enabled: boolean +} + +export interface ControlMetricSeries { + id: number + resource_uid: string + metric_code: string + collection_task_id?: string + storage_driver: string + database_name?: string + table_name?: string + series_key?: string + retention_policy?: string + enabled: boolean +} + +export interface ControlBusinessSystem { + id: number + code: string + name: string + system_type: string + owner_user_id?: number + department_id?: number + description?: string + status: string + is_builtin: boolean +} + +export interface ControlCollectionStatus { + id: number + resource_uid: string + resource_category: string + service_identity: string + collect_method: string + schedule_interval: number + retry_policy?: string + last_status: string + last_error?: string + last_sample_time?: string + trace_id?: string + source_service?: string + created_at?: string + updated_at?: string +} + +export interface ResourceListParams { + page?: number + size?: number + keyword?: string + resource_category?: string + business_system_id?: number +} + +export interface MetricSeriesListParams { + page?: number + size?: number + keyword?: string + resource_uid?: string +} + +export const fetchControlResources = (params?: ResourceListParams) => + request.get<{ code?: number; details?: PageResult; message?: string }>('/DC-Control/v1/resources', { params }) + +export const fetchControlResourceOptions = (params?: { resource_category?: string }) => + request.get<{ code?: number; details?: { list: OptionItem[]; count: number }; message?: string }>('/DC-Control/v1/resources/options', { params }) + +export const createControlResource = (data: Partial) => + request.post<{ code?: number; details?: ControlResource; message?: string }>('/DC-Control/v1/resources', data) + +export const updateControlResource = (id: number, data: Partial) => + request.put<{ code?: number; details?: ControlResource; message?: string }>(`/DC-Control/v1/resources/${id}`, data) + +export const deleteControlResource = (id: number) => + request.delete<{ code?: number; details?: { message: string }; message?: string }>(`/DC-Control/v1/resources/${id}`) + +export const backfillControlResources = () => + request.post<{ code?: number; details?: { message: string }; message?: string }>('/DC-Control/v1/resources/backfill') + +export const fetchControlResourceTypes = (params?: { page?: number; size?: number; keyword?: string }) => + request.get<{ code?: number; details?: PageResult; message?: string }>('/DC-Control/v1/resource-types', { params }) + +export const fetchControlResourceTypeOptions = () => + request.get<{ code?: number; details?: { list: OptionItem[]; count: number }; message?: string }>('/DC-Control/v1/resource-types/options') + +export const fetchControlMetricDefinitions = (params?: { page?: number; size?: number; keyword?: string; resource_category?: string }) => + request.get<{ code?: number; details?: PageResult; message?: string }>( + '/DC-Control/v1/metric-definitions', + { params } + ) + +export const fetchControlMetricDefinitionOptions = (params?: { resource_category?: string }) => + request.get<{ code?: number; details?: { list: OptionItem[]; count: number }; message?: string }>( + '/DC-Control/v1/metric-definitions/options', + { params } + ) + +export const fetchControlMetricSeries = (params?: MetricSeriesListParams) => + request.get<{ code?: number; details?: PageResult; message?: string }>('/DC-Control/v1/metric-series', { params }) + +export const fetchControlBusinessSystems = (params?: { page?: number; size?: number; keyword?: string; status?: string }) => + request.get<{ code?: number; details?: PageResult; message?: string }>('/DC-Control/v1/business-systems', { params }) + +export const fetchControlBusinessSystemOptions = () => + request.get<{ code?: number; details?: { list: OptionItem[]; count: number }; message?: string }>('/DC-Control/v1/business-systems/options') + +export const fetchCollectionStatus = (params?: { + page?: number + size?: number + keyword?: string + resource_category?: string + resource_uid?: string + status?: string +}) => + request.get<{ code?: number; details?: PageResult; message?: string }>( + '/DC-Control/v1/collection/status', + { params } + ) diff --git a/src/api/ops/feedbackTicket.ts b/src/api/ops/feedbackTicket.ts index 3f6beeb..7eed437 100644 --- a/src/api/ops/feedbackTicket.ts +++ b/src/api/ops/feedbackTicket.ts @@ -67,6 +67,9 @@ export const fetchFeedbackTickets = (data?: { creator_id?: number assignee_id?: number category_id?: number + incident_id?: number + resource_uid?: string + business_system_id?: number keyword?: string /** 格式 YYYY-MM-DD HH:MM:SS */ start_time?: string @@ -147,3 +150,6 @@ export const deleteFeedbackTicketRelation = (relationId: number) => request.dele /** 获取 工单统计数据 */ export const fetchFeedbackTicketStatistics = () => request.get('/Feedback/v1/tickets/statistics') + +/** 获取 工单 SLA/SLO 统计数据 */ +export const fetchFeedbackTicketSLAStatistics = () => request.get('/Feedback/v1/tickets/sla/statistics') diff --git a/src/api/ops/governance.ts b/src/api/ops/governance.ts new file mode 100644 index 0000000..61fe4e7 --- /dev/null +++ b/src/api/ops/governance.ts @@ -0,0 +1,81 @@ +import { request } from '@/api/request' + +export interface AlertQualityScore { + rule_id: number + resource_uid: string + business_system_id: number + window_start: string + window_end: string + score: number + repeat_count: number + notify_count: number + avg_ack_seconds: number + avg_resolve_seconds: number + reopen_rate: number + factors: string[] +} + +export interface NoiseBucket { + rule_id: number + resource_uid: string + business_system_id: number + window_start: string + window_end: string + alert_count: number + notify_count: number + noise_score: number +} + +export interface RuleSuggestion { + type: string + priority: string + rule_id: number + resource_uid?: string + business_system_id?: number + reason: string + action: string +} + +export interface CapacityRisk { + resource_uid: string + metric_code: string + unit: string + current_value: number + threshold: number + slope_per_hour: number + risk_level: string + predicted_at?: string + sample_count: number +} + +export interface SloDashboardBucket { + total: number + response_breached: number + resolve_breached: number + breached: number + breach_rate: number +} + +export interface SloDashboard { + total_closed: number + response_breached: number + resolve_breached: number + overall_breached: number + breach_rate: number + by_business_system: Record + by_resource_category: Record +} + +export const fetchAlertQualityScores = (params?: { hours?: number }) => + request.get('/Alert/v1/governance/quality', { params }) + +export const fetchAlertNoise = (params?: { hours?: number }) => + request.get('/Alert/v1/governance/noise', { params }) + +export const fetchRuleSuggestions = (params?: { hours?: number }) => + request.get('/Alert/v1/governance/suggestions', { params }) + +export const fetchCapacityRisk = (params?: { resource_uid?: string; metric_code?: string }) => + request.get('/DC-Control/v1/risk/capacity', { params }) + +export const fetchSloDashboard = () => request.get('/Feedback/v1/tickets/slo/dashboard') diff --git a/src/api/ops/incident.ts b/src/api/ops/incident.ts new file mode 100644 index 0000000..92d01e6 --- /dev/null +++ b/src/api/ops/incident.ts @@ -0,0 +1,52 @@ +import { request } from '@/api/request' + +export interface Incident { + id: number + created_at: string + updated_at: string + incident_no: string + dedup_key: string + status: string + severity: string + resource_uid: string + business_system_id: number + owner_user_id: number + summary: string + description: string + labels: string + annotations: string + raw_event_id: number + latest_alert_record_id: number + alert_count: number + suppressed: boolean + suppression_reason: string + opened_at: string + assigned_at?: string + in_progress_at?: string + suspended_at?: string + resolved_at?: string + closed_at?: string +} + +export const fetchIncidents = (params: { + page?: number + page_size?: number + keyword?: string + status?: string + severity?: string + resource_uid?: string + business_system_id?: number + owner_user_id?: number + suppressed?: boolean + sort?: string + order?: string +}) => request.get('/Alert/v1/incidents/list', { params }) + +export const fetchIncidentDetail = (id: number) => request.get(`/Alert/v1/incidents/get/${id}`) + +export const fetchIncidentAlerts = (id: number) => request.get(`/Alert/v1/incidents/${id}/alerts`) + +export const fetchIncidentTimeline = (id: number) => request.get(`/Alert/v1/incidents/${id}/timeline`) + +export const transitionIncident = (id: number, data: { status: string; comment?: string; operator?: string }) => + request.post(`/Alert/v1/incidents/${id}/transition`, data) diff --git a/src/api/ops/ipam.ts b/src/api/ops/ipam.ts index b0fad33..4b94972 100644 --- a/src/api/ops/ipam.ts +++ b/src/api/ops/ipam.ts @@ -53,6 +53,8 @@ export interface IPAddressItem { source_type: SourceType owner_type: string owner_id: number + resource_uid?: string + asset_id?: number remark: string tags: string created_at: string @@ -90,6 +92,8 @@ export interface IPAddressFormData { source_type?: SourceType owner_type?: string owner_id?: number + resource_uid?: string + asset_id?: number remark?: string tags?: string } @@ -133,6 +137,8 @@ export interface IPSubnetItem { group_name?: string gateway: string vlan: string + resource_uid?: string + asset_id?: number total: number used: number available: number @@ -147,6 +153,8 @@ export interface IPSubnetListParams { page?: number size?: number group_id?: number + resource_uid?: string + asset_id?: number keyword?: string } @@ -165,6 +173,8 @@ export interface IPSubnetFormData { group_id?: number gateway?: string vlan?: string + resource_uid?: string + asset_id?: number reserved_ranges_json?: string description?: string } diff --git a/src/api/ops/logs.ts b/src/api/ops/logs.ts index 032bf04..e25941b 100644 --- a/src/api/ops/logs.ts +++ b/src/api/ops/logs.ts @@ -27,6 +27,7 @@ export interface LogEvent { severity_code: string trap_oid: string alert_sent: boolean + dispatch_status: string } export interface LogEntriesParams { @@ -73,8 +74,12 @@ export interface SyslogRule { priority: number device_name_contains: string keyword_regex: string + source_match: string + message_regex: string alert_name: string severity_code: string + severity_mapping_json: string + resource_uid_extract_regex: string policy_id: number } @@ -97,9 +102,14 @@ export interface TrapDictionaryEntry { created_at?: string updated_at?: string oid_prefix: string + vendor: string + oid: string + name: string title: string description: string severity_code: string + severity_mapping_json: string + parse_expression: string recovery_message: string enabled: boolean } @@ -116,6 +126,69 @@ export interface TrapShield { time_windows_json: string } +export interface AuditLog { + id: number + created_at: string + trace_id: string + source_service: string + actor_id: string + actor_name: string + action: string + object_type: string + object_id: string + operation_risk: string + approval_id: string + request_method: string + request_path: string + client_ip: string + before_json: string + after_json: string + result: string + error_message: string +} + +export interface AuditLogParams { + page?: number + page_size?: number + source_service?: string + actor_id?: string + action?: string + object_type?: string + object_id?: string + operation_risk?: string + result?: string +} + +export interface DangerousApproval { + id: number + created_at: string + updated_at: string + request_id: string + source_service: string + action: string + object_type: string + object_id: string + requester_id: string + requester_name: string + reason: string + before_json: string + after_json: string + status: string + reviewer_id: string + reviewer_name: string + review_comment: string + reviewed_at: string +} + +export interface ApprovalParams { + page?: number + page_size?: number + source_service?: string + status?: string + requester_id?: string + object_type?: string +} + export function fetchLogEntries(params?: LogEntriesParams) { return request.get(`${BASE}/entries`, { params }) } @@ -128,6 +201,10 @@ export function retryAlertOutbox(id: number) { return request.post(`${BASE}/alert-outbox/${id}/retry`) } +export function replayLogEntry(id: number) { + return request.post(`${BASE}/entries/${id}/replay`) +} + export function fetchSyslogRules() { return request.get(`${BASE}/syslog-rules`) } @@ -191,3 +268,23 @@ export function updateTrapSuppression(id: number, data: Partial) { export function deleteTrapSuppression(id: number) { return request.delete(`${BASE}/trap-suppressions/${id}`) } + +export function fetchAuditLogs(params?: AuditLogParams) { + return request.get(`${BASE}/audit/logs`, { params }) +} + +export function fetchDangerousApprovals(params?: ApprovalParams) { + return request.get(`${BASE}/audit/approvals`, { params }) +} + +export function createDangerousApproval(data: Partial) { + return request.post(`${BASE}/audit/approvals`, data) +} + +export function approveDangerousApproval(id: number, data: { reviewer_id: string; reviewer_name?: string; review_comment?: string }) { + return request.post(`${BASE}/audit/approvals/${id}/approve`, data) +} + +export function rejectDangerousApproval(id: number, data: { reviewer_id: string; reviewer_name?: string; review_comment?: string }) { + return request.post(`${BASE}/audit/approvals/${id}/reject`, data) +} diff --git a/src/api/ops/netarchTopo.ts b/src/api/ops/netarchTopo.ts index d2deff1..2eb0a38 100644 --- a/src/api/ops/netarchTopo.ts +++ b/src/api/ops/netarchTopo.ts @@ -68,6 +68,7 @@ export interface TopologyNode { label: string type: string ip?: string + resource_uid?: string status?: string alerts?: number traffic?: string @@ -83,6 +84,30 @@ export interface TopologyNode { updated_at?: string } +export interface ImpactResource { + resource_uid: string + display_name?: string + business_system_id?: number + direction?: 'upstream' | 'downstream' + depth?: number +} + +export interface AffectedIP { + ip_address: string + resource_uid?: string + asset_id?: number + subnet_id?: number +} + +export interface ImpactAnalysis { + resource_uid: string + incident_id?: number + upstream_resources: ImpactResource[] + downstream_resources: ImpactResource[] + business_systems: number[] + affected_ips: AffectedIP[] +} + // ==================== 分组管理接口 ==================== /** 获取分组列表 */ @@ -126,6 +151,9 @@ export const fetchTopologyView = (id: number) => request.get(`/DC-Control/v1/top /** 获取拓扑图数据(用于前端可视化) */ export const fetchTopologyGraph = (id: number) => request.get(`/DC-Control/v1/topologies/${id}/graph`) +export const fetchImpactAnalysis = (params: { resource_uid: string; topology_id?: number; incident_id?: number; depth?: number }) => + request.get('/DC-Control/v1/impact/analyze', { params }) + // ==================== 节点管理接口 ==================== /** 创建节点 */ diff --git a/src/api/ops/noticeChannel.ts b/src/api/ops/noticeChannel.ts index 1b26084..e302904 100644 --- a/src/api/ops/noticeChannel.ts +++ b/src/api/ops/noticeChannel.ts @@ -24,6 +24,22 @@ export const createNoticeChannel = (data: { retry_interval?: number enabled?: boolean description?: string + email_smtp_host?: string + email_smtp_port?: number + email_username?: string + email_password?: string + email_from?: string + email_to?: string[] + email_cc?: string[] + email_bcc?: string[] + email_use_tls?: boolean + sms_provider?: string + sms_access_key_id?: string + sms_access_key_secret?: string + sms_sign_name?: string + sms_template_code?: string + sms_phones?: string[] + in_app_targets?: string[] }) => { return request.post('/Alert/v1/channel/create', data) } @@ -43,6 +59,22 @@ export const updateNoticeChannel = (data: { retry_interval?: number enabled?: boolean description?: string + email_smtp_host?: string + email_smtp_port?: number + email_username?: string + email_password?: string + email_from?: string + email_to?: string[] + email_cc?: string[] + email_bcc?: string[] + email_use_tls?: boolean + sms_provider?: string + sms_access_key_id?: string + sms_access_key_secret?: string + sms_sign_name?: string + sms_template_code?: string + sms_phones?: string[] + in_app_targets?: string[] }) => { return request.post('/Alert/v1/channel/update', data) } diff --git a/src/api/ops/rack.ts b/src/api/ops/rack.ts index f100f25..b8c0fbf 100644 --- a/src/api/ops/rack.ts +++ b/src/api/ops/rack.ts @@ -41,6 +41,11 @@ export const createRack = (data: any) => { return request.post('/Assets/v1/rack/create', data) } +/** 批量导入机柜并初始化U位 */ +export const bulkImportRacks = (data: { dry_run?: boolean; items: any[] }) => { + return request.post('/Assets/v1/rack/bulk_import', data) +} + /** 更新机柜 */ export const updateRack = (data: any) => { return request.put('/Assets/v1/rack/update', data) diff --git a/src/api/ops/rawEvent.ts b/src/api/ops/rawEvent.ts new file mode 100644 index 0000000..fcc487b --- /dev/null +++ b/src/api/ops/rawEvent.ts @@ -0,0 +1,40 @@ +import { request } from '@/api/request' + +export interface RawEvent { + id: number + created_at: string + updated_at: string + source_type: string + resource_uid: string + event_time: string + severity: string + title: string + message: string + labels: string + annotations: string + parse_status: string + raw_payload: string + trace_id: string + alert_record_id: number + replay_count: number + last_replayed_at?: string +} + +export const fetchRawEvents = (params: { + page?: number + page_size?: number + keyword?: string + source_type?: string + resource_uid?: string + severity?: string + parse_status?: string + trace_id?: string + start_time?: string + end_time?: string + sort?: string + order?: string +}) => request.get('/Alert/v1/raw-events/list', { params }) + +export const fetchRawEventDetail = (id: number) => request.get(`/Alert/v1/raw-events/get/${id}`) + +export const replayRawEvent = (id: number) => request.post(`/Alert/v1/raw-events/replay/${id}`) diff --git a/src/api/ops/report.ts b/src/api/ops/report.ts index 39ba345..e82f748 100644 --- a/src/api/ops/report.ts +++ b/src/api/ops/report.ts @@ -18,6 +18,7 @@ export enum ReportType { FAULT = 'fault', SERVER = 'server', NETWORK_DEVICE = 'network_device', + EVIDENCE = 'evidence', } export enum ReportStatus { @@ -150,7 +151,7 @@ export interface HistoryReportParams { } export interface TopNReportParams { - data_source: 'dc-host' | 'dc-network' | 'dc-database' | 'dc-middleware' + data_source: 'dc-host' | 'dc-network' | 'dc-database' | 'dc-middleware' | 'alert' metric_id?: string metric_name?: string target_identities: string[] @@ -163,6 +164,28 @@ export interface TopNReportParams { collector_identity?: string } +/** 证据导出事件行 */ +export interface EvidenceEvent { + event_time: string + action: string + actor?: string + message?: string + source_id?: string + details?: Record +} + +/** POST /reports/evidence/export */ +export interface EvidenceExportParams { + incident_id: number + resource_uid?: string + business_system_id?: number + title?: string + timeline?: EvidenceEvent[] + notifications?: EvidenceEvent[] + ticket_logs?: EvidenceEvent[] + audit_logs?: EvidenceEvent[] +} + export interface GenerateReportParams { report_type: ReportType title?: string @@ -195,6 +218,10 @@ export const generateReport = (data: GenerateReportParams) => export const createReportAsyncJob = (data: GenerateReportParams) => request.post>('/DC-Control/v1/reports/jobs', data) +/** 生成 incident 证据导出记录 */ +export const exportEvidenceReport = (data: EvidenceExportParams) => + request.post>('/DC-Control/v1/reports/evidence/export', data) + /** 指标发现(时间窗内有样本的 metric_name) */ export const fetchReportMetricsAvailable = (params: { data_source: string diff --git a/src/api/ops/unit.ts b/src/api/ops/unit.ts index ddc8494..df22082 100644 --- a/src/api/ops/unit.ts +++ b/src/api/ops/unit.ts @@ -49,3 +49,8 @@ export const cancelReservation = (data: { rack_id: number; start_unit: number; e export const updateUnitStatus = (data: { rack_id: number; start_unit: number; end_unit: number; status: 'available' | 'disabled' }) => { return request.put('/Assets/v1/unit/status', data) } + +/** 获取U位容量趋势 */ +export const fetchUnitTrend = (params?: { datacenter_id?: number; months?: number }) => { + return request.get('/Assets/v1/capacity/unit/trend', { params }) +} diff --git a/src/components/index.ts b/src/components/index.ts index c44a072..bf0bc34 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -8,6 +8,7 @@ import Chart from './chart/index.vue' import SearchForm from './search-form/index.vue' import DataTable from './data-table/index.vue' import SearchTable from './search-table/index.vue' +import PageState from './page-state/index.vue' // Manually introduce ECharts modules to reduce packing size @@ -31,5 +32,6 @@ export default { Vue.component('SearchForm', SearchForm) Vue.component('DataTable', DataTable) Vue.component('SearchTable', SearchTable) + Vue.component('PageState', PageState) }, } diff --git a/src/components/message-box/index.vue b/src/components/message-box/index.vue index 4e80410..0add560 100644 --- a/src/components/message-box/index.vue +++ b/src/components/message-box/index.vue @@ -8,7 +8,13 @@ - + + + + + + + diff --git a/src/components/page-state/index.vue b/src/components/page-state/index.vue new file mode 100644 index 0000000..35d5cb2 --- /dev/null +++ b/src/components/page-state/index.vue @@ -0,0 +1,89 @@ + + + + + + + diff --git a/src/components/search-table/index.vue b/src/components/search-table/index.vue index fd5204b..4c524ec 100644 --- a/src/components/search-table/index.vue +++ b/src/components/search-table/index.vue @@ -20,43 +20,53 @@ - - - - - - + + + + + + + @@ -105,6 +115,26 @@ const props = defineProps({ type: Boolean, default: false, }, + status: { + type: String as PropType<'loading' | 'empty' | 'error' | 'success' | 'partial' | 'unauthorized' | undefined>, + default: undefined, + }, + loadingText: { + type: String, + default: '加载中', + }, + emptyText: { + type: String, + default: '暂无数据', + }, + errorText: { + type: String, + default: '数据暂时不可用,请稍后重试。', + }, + partialText: { + type: String, + default: '部分数据加载失败,当前页面展示可用数据。', + }, pagination: { type: Object as PropType<{ current: number @@ -191,6 +221,17 @@ const slotColumns = computed(() => { return props.columns.filter((col) => col.slotName) }) +const stateStatus = computed(() => { + if (props.status) return props.status + if (props.loading) return 'loading' + if (!props.data.length) return 'empty' + return 'success' +}) + +const responsiveScroll = computed(() => { + return props.scroll || { x: 'max-content' } +}) + const handleFormModelUpdate = (value: Record) => { emit('update:formModel', value) } @@ -246,4 +287,10 @@ export default { .search-table-container { padding: 0 20px 20px 20px; } + +@media (max-width: 768px) { + .search-table-container { + padding: 0 12px 12px; + } +} diff --git a/src/directive/permission/index.ts b/src/directive/permission/index.ts index e1f5386..be5d607 100644 --- a/src/directive/permission/index.ts +++ b/src/directive/permission/index.ts @@ -1,22 +1,66 @@ import { DirectiveBinding } from 'vue' import { useUserStore } from '@/store' -function checkPermission(el: HTMLElement, binding: DirectiveBinding) { - const { value } = binding +type PermissionValue = string | string[] | { value?: string | string[]; mode?: 'remove' | 'disable' } + +function toList(value: PermissionValue): string[] { + if (Array.isArray(value)) return value + if (typeof value === 'string') return [value] + if (value && typeof value === 'object') { + if (Array.isArray(value.value)) return value.value + if (typeof value.value === 'string') return [value.value] + } + return [] +} + +function collectPermissionCodes(input: any, out: Set) { + if (!input) return + if (typeof input === 'string') { + out.add(input) + return + } + if (Array.isArray(input)) { + input.forEach((item) => collectPermissionCodes(item, out)) + return + } + if (typeof input === 'object') { + const code = input.code || input.permission || input.auth || input.value + if (typeof code === 'string') out.add(code) + collectPermissionCodes(input.children, out) + collectPermissionCodes(input.permissions, out) + collectPermissionCodes(input.pmn_application, out) + } +} + +function hasPermission(value: PermissionValue) { + const expected = toList(value) + if (expected.length === 0) { + throw new Error(`need permissions! Like v-permission="['admin','system:user:create']"`) + } + const userStore = useUserStore() - const { role } = userStore + const role = userStore.role || userStore.userInfo?.role || '' + if (role === '*' || expected.includes('*') || expected.includes(role)) return true - if (Array.isArray(value)) { - if (value.length > 0) { - const permissionValues = value + const codes = new Set() + collectPermissionCodes(userStore.userInfo?.permissions, codes) + collectPermissionCodes(userStore.userInfo?.pmn_application, codes) + collectPermissionCodes(userStore.userInfo?.roles, codes) + return expected.some((item) => codes.has(item)) +} - const hasPermission = permissionValues.includes(role) - if (!hasPermission && el.parentNode) { - el.parentNode.removeChild(el) - } +function checkPermission(el: HTMLElement, binding: DirectiveBinding) { + const value = binding.value as PermissionValue + const mode = typeof value === 'object' && !Array.isArray(value) ? value.mode || 'remove' : 'remove' + if (!hasPermission(value)) { + if (mode === 'disable') { + el.setAttribute('disabled', 'true') + el.classList.add('is-disabled') + return + } + if (el.parentNode) { + el.parentNode.removeChild(el) } - } else { - throw new Error(`need roles! Like v-permission="['admin','user']"`) } } diff --git a/src/router/local-menu-flat.ts b/src/router/local-menu-flat.ts index 73cd8d6..b2c46a9 100644 --- a/src/router/local-menu-flat.ts +++ b/src/router/local-menu-flat.ts @@ -63,6 +63,51 @@ export const localMenuFlatItems: MenuItem[] = [ web_url: 'https://ops.apinb.com/view/#/project/management', created_at: '2026-01-25T10:44:15.33024+08:00', }, + { + id: 12060, + identity: '019ca000-0001-7000-8000-000000000060', + title: '业务系统视图', + title_en: 'Business Systems', + code: 'ops:业务系统视图', + description: '业务健康、拓扑、影响范围和时间线', + app_id: 2, + menu_path: '/business-system', + menu_icon: 'Cluster', + type: 1, + sort_key: 4.2, + created_at: '2026-06-25T10:00:00+08:00', + }, + { + id: 12061, + identity: '019ca000-0001-7000-8000-000000000061', + title: '业务系统', + title_en: 'Business System View', + code: 'ops:业务系统视图:业务系统', + app_id: 2, + parent_id: 12060, + menu_path: '/business-system', + component: 'ops/pages/business-system', + menu_icon: 'appstore', + type: 1, + sort_key: 4.3, + created_at: '2026-06-25T10:00:00+08:00', + }, + { + id: 12062, + identity: '019ca000-0001-7000-8000-000000000062', + title: '业务系统详情', + title_en: 'Business System Detail', + code: 'ops:业务系统视图:详情', + app_id: 2, + parent_id: 12060, + menu_path: '/business-system/detail', + component: 'ops/pages/business-system/detail', + menu_icon: 'appstore', + type: 1, + sort_key: 4.4, + hide_menu: true, + created_at: '2026-06-25T10:00:00+08:00', + }, { id: 16, identity: '019b591d-00f4-73a0-bbdb-aa7da79ed390', @@ -554,6 +599,22 @@ export const localMenuFlatItems: MenuItem[] = [ sort_key: 27, created_at: '2025-12-26T13:23:52.126081+08:00', }, + { + id: 12050, + identity: '019c9000-0001-7000-8000-000000000050', + title: '原始事件池', + title_en: 'Raw Events', + code: 'ops:告警管理:原始事件池', + description: '告警管理 - 原始事件池', + app_id: 2, + parent_id: 39, + menu_path: '/alert/raw-events', + component: 'ops/pages/alert/raw-events', + menu_icon: 'appstore', + type: 1, + sort_key: 28, + created_at: '2026-06-25T10:00:00+08:00', + }, { id: 43, identity: '019b591d-029e-7c52-ac1d-d94263e00f8e', @@ -566,7 +627,7 @@ export const localMenuFlatItems: MenuItem[] = [ menu_path: '/alert/tackle', menu_icon: 'appstore', type: 1, - sort_key: 28, + sort_key: 29, created_at: '2025-12-26T13:23:52.094807+08:00', }, { @@ -581,7 +642,7 @@ export const localMenuFlatItems: MenuItem[] = [ menu_path: '/alert/history', menu_icon: 'appstore', type: 1, - sort_key: 29, + sort_key: 30, created_at: '2025-12-26T13:23:52.110362+08:00', }, { @@ -596,7 +657,7 @@ export const localMenuFlatItems: MenuItem[] = [ menu_path: '/alert/template', menu_icon: 'appstore', type: 1, - sort_key: 30, + sort_key: 31, created_at: '2025-12-26T13:23:52.047548+08:00', }, { diff --git a/src/router/local-menu-items.ts b/src/router/local-menu-items.ts index b3b78a8..9917023 100644 --- a/src/router/local-menu-items.ts +++ b/src/router/local-menu-items.ts @@ -17,6 +17,22 @@ export const localMenuItems: MenuItem[] = [ created_at: '2025-12-26T13:23:51.54067+08:00', children: [], }, + { + id: 12030, + identity: '019c7200-0001-7000-8000-000000000030', + title: '资源上下文', + title_en: 'Resource Context', + code: 'ops:资源上下文', + description: '统一资源、业务系统、资产绑定、指标序列和关联告警上下文', + app_id: 2, + menu_path: '/resource-context', + component: 'ops/pages/resource-context', + menu_icon: 'Cluster', + type: 1, + sort_key: 2, + created_at: '2026-06-26T10:00:00+08:00', + children: [], + }, { id: 13, identity: '019b591d-00c3-7955-aa1b-80b5a0c8d6bd', @@ -31,6 +47,23 @@ export const localMenuItems: MenuItem[] = [ sort_key: 1, created_at: '2025-12-26T13:23:51.62748+08:00', children: [ + { + id: 12070, + identity: '019ca000-0001-7000-8000-000000000070', + title: 'OPS 大屏管理', + title_en: 'OPS Big Screens', + code: 'ops:可视化大屏管理:ops大屏管理', + description: '大屏分组、权限、轮播、发布和快照管理', + app_id: 2, + parent_id: 13, + menu_path: '/ops/big-screen', + component: 'ops/pages/big-screen', + menu_icon: 'appstore', + type: 1, + sort_key: 1, + created_at: '2026-06-25T10:00:00+08:00', + children: [], + }, { id: 14, identity: '019b591d-00dc-7486-aa93-51e798d3253a', @@ -67,6 +100,87 @@ export const localMenuItems: MenuItem[] = [ }, ], }, + { + id: 12060, + identity: '019ca000-0001-7000-8000-000000000060', + title: '业务系统视图', + title_en: 'Business Systems', + code: 'ops:业务系统视图', + description: '业务健康、拓扑、影响范围和时间线', + app_id: 2, + menu_path: '/business-system', + menu_icon: 'Cluster', + type: 1, + sort_key: 2, + created_at: '2026-06-25T10:00:00+08:00', + children: [ + { + id: 12061, + identity: '019ca000-0001-7000-8000-000000000061', + title: '业务系统', + title_en: 'Business System View', + code: 'ops:业务系统视图:业务系统', + app_id: 2, + parent_id: 12060, + menu_path: '/business-system', + component: 'ops/pages/business-system', + menu_icon: 'appstore', + type: 1, + sort_key: 2, + created_at: '2026-06-25T10:00:00+08:00', + children: [], + }, + { + id: 12062, + identity: '019ca000-0001-7000-8000-000000000062', + title: '业务系统详情', + title_en: 'Business System Detail', + code: 'ops:业务系统视图:详情', + app_id: 2, + parent_id: 12060, + menu_path: '/business-system/detail', + component: 'ops/pages/business-system/detail', + menu_icon: 'appstore', + type: 1, + sort_key: 2, + hide_menu: true, + created_at: '2026-06-25T10:00:00+08:00', + children: [], + }, + ], + }, + { + id: 12080, + identity: '019ca000-0001-7000-8000-000000000080', + title: '运营治理', + title_en: 'Governance', + code: 'ops:运营治理', + description: '告警质量、噪声、建议、容量风险和SLO', + app_id: 2, + menu_path: '/governance', + component: 'ops/pages/governance', + menu_icon: 'Dashboard', + type: 1, + sort_key: 2, + created_at: '2026-06-25T10:00:00+08:00', + children: [], + }, + { + id: 12090, + identity: '019ca000-0001-7000-8000-000000000090', + title: '自动化处置', + title_en: 'Automation', + code: 'ops:自动化处置', + description: '自动化脚本、审批、执行、回滚和审计记录', + app_id: 2, + menu_path: '/automation', + component: 'ops/pages/automation', + menu_icon: 'Thunderbolt', + type: 1, + sort_key: 2, + created_at: '2026-06-25T10:00:00+08:00', + children: [], + }, { id: 16, identity: '019b591d-00f4-73a0-bbdb-aa7da79ed390', @@ -193,6 +307,23 @@ export const localMenuItems: MenuItem[] = [ sort_key: 4, created_at: '2025-12-26T13:23:51.77834+08:00', children: [ + { + id: 12022, + identity: '019c7100-0001-7000-8000-000000000022', + title: '采集健康度', + title_en: 'Collection Health', + code: 'ops:综合监控:采集健康度', + description: '综合监控 - 采集任务最近状态与失败原因', + app_id: 2, + parent_id: 23, + menu_path: '/monitor/collection-health', + menu_icon: 'appstore', + component: 'ops/pages/monitor/collection-health', + type: 1, + sort_key: 5, + created_at: '2026-06-24T10:00:00+08:00', + children: [], + }, { id: 31, identity: '019b591d-01e3-7adc-b10f-26550a6e3700', @@ -596,6 +727,40 @@ export const localMenuItems: MenuItem[] = [ created_at: '2025-12-26T13:23:52.126081+08:00', children: [], }, + { + id: 12050, + identity: '019c9000-0001-7000-8000-000000000050', + title: '原始事件池', + title_en: 'Raw Events', + code: 'ops:告警管理:原始事件池', + description: '告警管理 - 原始事件池', + app_id: 2, + parent_id: 39, + menu_path: '/alert/raw-events', + component: 'ops/pages/alert/raw-events', + menu_icon: 'appstore', + type: 1, + sort_key: 7, + created_at: '2026-06-25T10:00:00+08:00', + children: [], + }, + { + id: 12051, + identity: '019c9000-0001-7000-8000-000000000051', + title: 'Incident 事件', + title_en: 'Incidents', + code: 'ops:告警管理:Incident事件', + description: '告警管理 - Incident 事件归并与状态机', + app_id: 2, + parent_id: 39, + menu_path: '/alert/incidents', + component: 'ops/pages/alert/incidents', + menu_icon: 'appstore', + type: 1, + sort_key: 7, + created_at: '2026-06-25T10:00:00+08:00', + children: [], + }, { id: 43, identity: '019b591d-029e-7c52-ac1d-d94263e00f8e', diff --git a/src/router/routes/modules/ops.ts b/src/router/routes/modules/ops.ts index e97f4f6..40d7b8d 100644 --- a/src/router/routes/modules/ops.ts +++ b/src/router/routes/modules/ops.ts @@ -12,6 +12,94 @@ const OPS: AppRouteRecordRaw = { order: 3, }, children: [ + { + path: 'resource-context', + alias: ['/resource-context'], + name: 'OpsResourceContext', + component: () => import('@/views/ops/pages/resource-context/index.vue'), + meta: { + locale: 'menu.ops.resourceContext', + requiresAuth: true, + roles: ['*'], + }, + }, + { + path: 'alert/raw-events', + alias: ['/alert/raw-events'], + name: 'OpsAlertRawEvents', + component: () => import('@/views/ops/pages/alert/raw-events/index.vue'), + meta: { + locale: 'menu.ops.alert.rawEvents', + requiresAuth: true, + roles: ['*'], + }, + }, + { + path: 'alert/incidents', + alias: ['/alert/incidents'], + name: 'OpsAlertIncidents', + component: () => import('@/views/ops/pages/alert/incidents/index.vue'), + meta: { + locale: 'menu.ops.alert.incidents', + requiresAuth: true, + roles: ['*'], + }, + }, + { + path: 'business-system', + alias: ['/business-system'], + name: 'OpsBusinessSystem', + component: () => import('@/views/ops/pages/business-system/index.vue'), + meta: { + locale: 'menu.ops.businessSystem', + requiresAuth: true, + roles: ['*'], + }, + }, + { + path: 'business-system/detail', + alias: ['/business-system/detail'], + name: 'OpsBusinessSystemDetail', + component: () => import('@/views/ops/pages/business-system/detail.vue'), + meta: { + locale: 'menu.ops.businessSystem.detail', + requiresAuth: true, + roles: ['*'], + hideInMenu: true, + }, + }, + { + path: 'big-screen', + name: 'OpsBigScreen', + component: () => import('@/views/ops/pages/big-screen/index.vue'), + meta: { + locale: 'menu.ops.bigScreen', + requiresAuth: true, + roles: ['*'], + }, + }, + { + path: 'governance', + alias: ['/governance'], + name: 'OpsGovernance', + component: () => import('@/views/ops/pages/governance/index.vue'), + meta: { + locale: 'menu.ops.governance', + requiresAuth: true, + roles: ['*'], + }, + }, + { + path: 'automation', + alias: ['/automation'], + name: 'OpsAutomation', + component: () => import('@/views/ops/pages/automation/index.vue'), + meta: { + locale: 'menu.ops.automation', + requiresAuth: true, + roles: ['*'], + }, + }, // { // path: 'menu-management', // name: 'MenuManagement', diff --git a/src/router/routes/modules/visualization.ts b/src/router/routes/modules/visualization.ts index 8ec4756..d998434 100644 --- a/src/router/routes/modules/visualization.ts +++ b/src/router/routes/modules/visualization.ts @@ -12,6 +12,16 @@ const VISUALIZATION: AppRouteRecordRaw = { order: 1, }, children: [ + { + path: 'ops-big-screen', + name: 'OpsBigScreenVisualization', + component: () => import('@/views/visualization/ops-big-screen/index.vue'), + meta: { + locale: 'menu.visualization.opsBigScreen', + requiresAuth: true, + roles: ['admin'], + }, + }, { path: 'data-analysis', name: 'DataAnalysis', diff --git a/src/views/ops/pages/alert/incidents/index.vue b/src/views/ops/pages/alert/incidents/index.vue new file mode 100644 index 0000000..666b628 --- /dev/null +++ b/src/views/ops/pages/alert/incidents/index.vue @@ -0,0 +1,531 @@ + + + + + + + diff --git a/src/views/ops/pages/alert/notice/components/ChannelDetailDialog.vue b/src/views/ops/pages/alert/notice/components/ChannelDetailDialog.vue index bf55156..08aecc2 100644 --- a/src/views/ops/pages/alert/notice/components/ChannelDetailDialog.vue +++ b/src/views/ops/pages/alert/notice/components/ChannelDetailDialog.vue @@ -128,11 +128,7 @@ const loading = ref(false) const channelTypeMap: Record = { email: { name: '邮件', color: 'blue' }, sms: { name: '短信', color: 'cyan' }, - webhook: { name: 'Webhook', color: 'purple' }, - dingtalk: { name: '钉钉', color: 'red' }, - wechat: { name: '企业微信', color: 'green' }, - feishu: { name: '飞书', color: 'orange' }, - slack: { name: 'Slack', color: 'arcoblue' }, + in_app: { name: '站内消息', color: 'green' }, } // 获取渠道类型名称 diff --git a/src/views/ops/pages/alert/notice/components/ChannelFormDialog.vue b/src/views/ops/pages/alert/notice/components/ChannelFormDialog.vue index c6a67a1..09b7408 100644 --- a/src/views/ops/pages/alert/notice/components/ChannelFormDialog.vue +++ b/src/views/ops/pages/alert/notice/components/ChannelFormDialog.vue @@ -17,18 +17,95 @@ 邮件 短信 - Webhook - 钉钉 - 企业微信 - 飞书 - Slack + 站内消息 + + + + + + @@ -139,6 +216,22 @@ interface NoticeChannel { retry_interval?: number enabled?: boolean description?: string + email_smtp_host?: string + email_smtp_port?: number + email_username?: string + email_password?: string + email_from?: string + email_to?: string[] | string + email_cc?: string[] | string + email_bcc?: string[] | string + email_use_tls?: boolean + sms_provider?: string + sms_access_key_id?: string + sms_access_key_secret?: string + sms_sign_name?: string + sms_template_code?: string + sms_phones?: string[] | string + in_app_targets?: string[] | string } interface Props { @@ -177,6 +270,22 @@ const form = ref({ retry_interval: 60, enabled: true, description: '', + email_smtp_host: '', + email_smtp_port: 25, + email_username: '', + email_password: '', + email_from: '', + email_to: [] as string[], + email_cc: [] as string[], + email_bcc: [] as string[], + email_use_tls: false, + sms_provider: '', + sms_access_key_id: '', + sms_access_key_secret: '', + sms_sign_name: '', + sms_template_code: '', + sms_phones: [] as string[], + in_app_targets: [] as string[], }) // 是否为编辑模式 @@ -268,6 +377,22 @@ watch( retry_interval: props.channel.retry_interval ?? 60, enabled: props.channel.enabled !== undefined ? props.channel.enabled : true, description: props.channel.description || '', + email_smtp_host: props.channel.email_smtp_host || '', + email_smtp_port: props.channel.email_smtp_port ?? 25, + email_username: props.channel.email_username || '', + email_password: props.channel.email_password || '', + email_from: props.channel.email_from || '', + email_to: parseStringArray(props.channel.email_to), + email_cc: parseStringArray(props.channel.email_cc), + email_bcc: parseStringArray(props.channel.email_bcc), + email_use_tls: props.channel.email_use_tls || false, + sms_provider: props.channel.sms_provider || '', + sms_access_key_id: props.channel.sms_access_key_id || '', + sms_access_key_secret: props.channel.sms_access_key_secret || '', + sms_sign_name: props.channel.sms_sign_name || '', + sms_template_code: props.channel.sms_template_code || '', + sms_phones: parseStringArray(props.channel.sms_phones), + in_app_targets: parseStringArray(props.channel.in_app_targets), } // 解析静默时间段 @@ -298,6 +423,22 @@ watch( retry_interval: 60, enabled: true, description: '', + email_smtp_host: '', + email_smtp_port: 25, + email_username: '', + email_password: '', + email_from: '', + email_to: [], + email_cc: [], + email_bcc: [], + email_use_tls: false, + sms_provider: '', + sms_access_key_id: '', + sms_access_key_secret: '', + sms_sign_name: '', + sms_template_code: '', + sms_phones: [], + in_app_targets: [], } quietHoursEnabled.value = false quietHoursStart.value = '22:00' @@ -311,6 +452,11 @@ watch( const handleOk = async () => { const valid = await formRef.value?.validate() if (valid) return + const channelError = validateChannelConfig() + if (channelError) { + Message.error(channelError) + return + } submitting.value = true try { @@ -330,6 +476,7 @@ const handleOk = async () => { enabled: form.value.enabled, description: form.value.description, } + appendChannelPayload(data) let res if (isEdit.value && props.channel?.id) { @@ -356,6 +503,78 @@ const handleOk = async () => { } } +const parseStringArray = (value?: string[] | string) => { + if (!value) return [] + if (Array.isArray(value)) return value + try { + const parsed = JSON.parse(value) + return Array.isArray(parsed) ? parsed : [] + } catch (error) { + return value + .split(',') + .map((item) => item.trim()) + .filter(Boolean) + } +} + +const isMaskedSecret = (value?: string) => { + return value === '******' +} + +const validateChannelConfig = () => { + if (form.value.type === 'email') { + if (!form.value.email_smtp_host || !form.value.email_smtp_port || !form.value.email_username || !form.value.email_from || !form.value.email_to.length) { + return '请补齐邮件渠道必填字段' + } + if (!form.value.email_password) return '请输入 SMTP 密码/授权码' + } + if (form.value.type === 'sms') { + if ( + !form.value.sms_provider || + !form.value.sms_access_key_id || + !form.value.sms_sign_name || + !form.value.sms_template_code || + !form.value.sms_phones.length + ) { + return '请补齐短信渠道必填字段' + } + if (!form.value.sms_access_key_secret) return '请输入 AccessKey Secret' + } + if (form.value.type === 'in_app' && !form.value.in_app_targets.length) { + return '请至少配置一个站内消息目标' + } + return '' +} + +const appendChannelPayload = (data: any) => { + if (form.value.type === 'email') { + data.email_smtp_host = form.value.email_smtp_host + data.email_smtp_port = form.value.email_smtp_port + data.email_username = form.value.email_username + if (!isMaskedSecret(form.value.email_password)) { + data.email_password = form.value.email_password + } + data.email_from = form.value.email_from + data.email_to = form.value.email_to + data.email_cc = form.value.email_cc + data.email_bcc = form.value.email_bcc + data.email_use_tls = form.value.email_use_tls + } + if (form.value.type === 'sms') { + data.sms_provider = form.value.sms_provider + data.sms_access_key_id = form.value.sms_access_key_id + if (!isMaskedSecret(form.value.sms_access_key_secret)) { + data.sms_access_key_secret = form.value.sms_access_key_secret + } + data.sms_sign_name = form.value.sms_sign_name + data.sms_template_code = form.value.sms_template_code + data.sms_phones = form.value.sms_phones + } + if (form.value.type === 'in_app') { + data.in_app_targets = form.value.in_app_targets + } +} + // 取消 const handleCancel = () => { emit('update:visible', false) diff --git a/src/views/ops/pages/alert/notice/index.vue b/src/views/ops/pages/alert/notice/index.vue index ce1a147..98ce042 100644 --- a/src/views/ops/pages/alert/notice/index.vue +++ b/src/views/ops/pages/alert/notice/index.vue @@ -108,11 +108,7 @@ const formItems = computed(() => [ options: [ { label: '邮件', value: 'email' }, { label: '短信', value: 'sms' }, - { label: 'Webhook', value: 'webhook' }, - { label: '钉钉', value: 'dingtalk' }, - { label: '企业微信', value: 'wechat' }, - { label: '飞书', value: 'feishu' }, - { label: 'Slack', value: 'slack' }, + { label: '站内消息', value: 'in_app' }, ], }, ]) @@ -167,11 +163,7 @@ const currentChannelId = ref(null) const channelTypeMap: Record = { email: { name: '邮件', color: 'blue' }, sms: { name: '短信', color: 'cyan' }, - webhook: { name: 'Webhook', color: 'purple' }, - dingtalk: { name: '钉钉', color: 'red' }, - wechat: { name: '企业微信', color: 'green' }, - feishu: { name: '飞书', color: 'orange' }, - slack: { name: 'Slack', color: 'arcoblue' }, + in_app: { name: '站内消息', color: 'green' }, } // 获取渠道类型名称 diff --git a/src/views/ops/pages/alert/raw-events/index.vue b/src/views/ops/pages/alert/raw-events/index.vue new file mode 100644 index 0000000..bee1620 --- /dev/null +++ b/src/views/ops/pages/alert/raw-events/index.vue @@ -0,0 +1,381 @@ + + + + + + + diff --git a/src/views/ops/pages/automation/index.vue b/src/views/ops/pages/automation/index.vue new file mode 100644 index 0000000..7fb42e8 --- /dev/null +++ b/src/views/ops/pages/automation/index.vue @@ -0,0 +1,432 @@ + + + + + + + diff --git a/src/views/ops/pages/big-screen/index.vue b/src/views/ops/pages/big-screen/index.vue new file mode 100644 index 0000000..9714de8 --- /dev/null +++ b/src/views/ops/pages/big-screen/index.vue @@ -0,0 +1,482 @@ + + + + + + + diff --git a/src/views/ops/pages/business-system/detail.vue b/src/views/ops/pages/business-system/detail.vue new file mode 100644 index 0000000..8d8769b --- /dev/null +++ b/src/views/ops/pages/business-system/detail.vue @@ -0,0 +1,679 @@ + + + + + + + diff --git a/src/views/ops/pages/business-system/index.vue b/src/views/ops/pages/business-system/index.vue new file mode 100644 index 0000000..feb7336 --- /dev/null +++ b/src/views/ops/pages/business-system/index.vue @@ -0,0 +1,359 @@ + + + + + + + diff --git a/src/views/ops/pages/datacenter/house/index.vue b/src/views/ops/pages/datacenter/house/index.vue index 2830475..3537daf 100644 --- a/src/views/ops/pages/datacenter/house/index.vue +++ b/src/views/ops/pages/datacenter/house/index.vue @@ -61,6 +61,12 @@ 新建数据中心 + + + 3D契约 + @@ -114,6 +120,16 @@ +
+ + 数据中心 {{ threeDSummary.datacenter_count || 0 }} + 机房 {{ threeDSummary.room_count || 0 }} + 机柜 {{ threeDSummary.rack_count || 0 }} + U位 {{ threeDSummary.unit_count || 0 }} + 设备 {{ threeDSummary.device_count || 0 }} + +
+ import { ref, reactive, computed, onMounted } from 'vue' import { Message, Modal } from '@arco-design/web-vue' -import { IconPlus } from '@arco-design/web-vue/es/icon' +import { IconApps, IconPlus } from '@arco-design/web-vue/es/icon' import type { FormItem } from '@/components/search-form/types' import SearchTable from '@/components/search-table/index.vue' import { searchFormConfig } from './config/search-form' import { columns as columnsConfig } from './config/columns' -import { fetchDatacenterList, deleteDatacenter, fetchProvinceList, fetchCityList } from '@/api/ops/datacenter' +import { fetchDatacenterList, deleteDatacenter, fetchProvinceList, fetchCityList, fetchDatacenterThreeDExport } from '@/api/ops/datacenter' import DatacenterDetailDialog from './components/DatacenterDetailDialog.vue' import DatacenterFormDialog from './components/DatacenterFormDialog.vue' @@ -150,6 +166,8 @@ const statusMap: Record = { // 状态管理 const loading = ref(false) +const threeDLoading = ref(false) +const threeDSummary = ref(null) const tableData = ref([]) const formModel = ref({ keyword: '', @@ -313,6 +331,24 @@ const handleRefresh = () => { Message.success('数据已刷新') } +const handleLoadThreeDExport = async () => { + threeDLoading.value = true + try { + const res: any = await fetchDatacenterThreeDExport() + if (res.code === 0) { + threeDSummary.value = res.details?.summary || res.data?.summary || null + Message.success('3D契约摘要已更新') + } else { + Message.error(res.message || '获取3D契约失败') + } + } catch (error) { + console.error('获取3D契约失败:', error) + Message.error('获取3D契约失败') + } finally { + threeDLoading.value = false + } +} + // 新建数据中心 const handleCreate = () => { editingDatacenter.value = null @@ -378,4 +414,9 @@ export default { .container { margin-top: 20px; } + +.three-d-summary { + margin-top: 12px; + padding: 12px 0; +} diff --git a/src/views/ops/pages/feedback/all/config/columns.ts b/src/views/ops/pages/feedback/all/config/columns.ts index 3b8be39..a70c257 100644 --- a/src/views/ops/pages/feedback/all/config/columns.ts +++ b/src/views/ops/pages/feedback/all/config/columns.ts @@ -37,6 +37,30 @@ export const columns: TableColumnData[] = [ slotName: 'priority', width: 100, }, + { + title: 'Incident', + dataIndex: 'incident_id', + slotName: 'incident', + width: 120, + }, + { + title: '响应截止', + dataIndex: 'response_deadline', + slotName: 'response_deadline', + width: 180, + }, + { + title: '解决截止', + dataIndex: 'resolve_deadline', + slotName: 'resolve_deadline', + width: 180, + }, + { + title: 'SLA', + dataIndex: 'sla_violated', + slotName: 'sla_status', + width: 110, + }, { title: '创建人', dataIndex: 'creator_name', diff --git a/src/views/ops/pages/feedback/all/index.vue b/src/views/ops/pages/feedback/all/index.vue index 3ca1b61..f93eceb 100644 --- a/src/views/ops/pages/feedback/all/index.vue +++ b/src/views/ops/pages/feedback/all/index.vue @@ -45,6 +45,28 @@ + + + + + + + + - + @@ -142,7 +156,7 @@ import { Message, Modal } from '@arco-design/web-vue' import type { FormItem } from '@/components/search-form/types' import SearchTable from '@/components/search-table/index.vue' import type { TableColumnData } from '@arco-design/web-vue/es/table/interface' -import { fetchAlertOutbox, fetchLogEntries, retryAlertOutbox, unwrapLogsPayload, type AlertOutbox, type LogEvent } from '@/api/ops/logs' +import { fetchAlertOutbox, fetchLogEntries, replayLogEntry, retryAlertOutbox, unwrapLogsPayload, type AlertOutbox, type LogEvent } from '@/api/ops/logs' import { fetchSeverityOptions } from '@/api/ops/alertPolicy' const loading = ref(false) @@ -247,6 +261,7 @@ const columns = computed(() => [ { title: '命中方式', dataIndex: 'match_method', width: 90 }, { title: '级别', dataIndex: 'severity_code', slotName: 'severity_code', width: 110 }, { title: 'OID', dataIndex: 'trap_oid', width: 140, ellipsis: true, tooltip: true }, + { title: '分发状态', dataIndex: 'dispatch_status', slotName: 'dispatch_status', width: 110 }, { title: '原始报文', dataIndex: 'raw_payload', @@ -264,7 +279,7 @@ const columns = computed(() => [ title: '操作', dataIndex: 'operations', slotName: 'operations', - width: 88, + width: 132, fixed: 'right', }, ]) @@ -410,6 +425,17 @@ function openOutboxDrawer() { fetchOutboxList() } +function showUnparsedQueue() { + formModel.value = { + ...formModel.value, + dispatch_status: 'pending', + } + pagination.current = 1 + outboxStatus.value = 'pending' + fetchList() + openOutboxDrawer() +} + function handleOutboxSearch() { outboxPagination.current = 1 fetchOutboxList() @@ -469,6 +495,27 @@ function handleOutboxRetry(id: number) { }) } +function handleReplayLog(row: LogEvent) { + Modal.confirm({ + title: '确认重放', + content: `重放日志 #${row.id} 并重新投递到原始事件池?`, + onOk: async () => { + try { + const res: any = await replayLogEntry(row.id) + if (typeof res.code === 'number' && res.code !== 0) { + Message.error(res.message || res.msg || '重放失败') + return + } + Message.success('已提交重放') + fetchList() + if (outboxVisible.value) fetchOutboxList() + } catch (e: any) { + Message.error(e?.message || '重放失败') + } + }, + }) +} + function openPayloadModal(payload: string) { let text = payload || '' try { diff --git a/src/views/ops/pages/log-mgmt/syslog-rules/index.vue b/src/views/ops/pages/log-mgmt/syslog-rules/index.vue index 17a668b..a83b347 100644 --- a/src/views/ops/pages/log-mgmt/syslog-rules/index.vue +++ b/src/views/ops/pages/log-mgmt/syslog-rules/index.vue @@ -65,9 +65,21 @@ + + + + + + + + + + + + @@ -141,9 +153,13 @@ const emptyForm = (): SyslogRule => ({ enabled: true, priority: 0, device_name_contains: '', + source_match: '', keyword_regex: '', + message_regex: '', alert_name: '', severity_code: '', + severity_mapping_json: '', + resource_uid_extract_regex: '', policy_id: 0, }) @@ -280,7 +296,9 @@ const columns = computed(() => [ { title: '优先级', dataIndex: 'priority', width: 80 }, { title: '启用', dataIndex: 'enabled', slotName: 'enabled', width: 88 }, { title: '设备名包含', dataIndex: 'device_name_contains', ellipsis: true, tooltip: true }, + { title: '来源匹配', dataIndex: 'source_match', ellipsis: true, tooltip: true }, { title: '关键字正则', dataIndex: 'keyword_regex', ellipsis: true, tooltip: true }, + { title: '消息正则', dataIndex: 'message_regex', ellipsis: true, tooltip: true }, { title: '告警名', dataIndex: 'alert_name', ellipsis: true, tooltip: true }, { title: '级别', dataIndex: 'severity_code', slotName: 'severity_code', width: 110 }, { title: '策略ID', dataIndex: 'policy_id', width: 88 }, @@ -321,7 +339,15 @@ function severityColor(code: string) { function applyFilter(list: SyslogRule[]) { const kw = formModel.value.keyword?.trim() if (!kw) return list - return list.filter((r) => r.name?.includes(kw) || r.alert_name?.includes(kw) || r.keyword_regex?.includes(kw)) + return list.filter( + (r) => + r.name?.includes(kw) || + r.alert_name?.includes(kw) || + r.keyword_regex?.includes(kw) || + r.message_regex?.includes(kw) || + r.source_match?.includes(kw) || + r.resource_uid_extract_regex?.includes(kw) + ) } function slicePage(list: SyslogRule[]) { @@ -393,9 +419,13 @@ function openEdit(row: SyslogRule) { enabled: row.enabled, priority: row.priority, device_name_contains: row.device_name_contains ?? '', + source_match: row.source_match ?? '', keyword_regex: row.keyword_regex ?? '', + message_regex: row.message_regex ?? '', alert_name: row.alert_name ?? '', severity_code: row.severity_code ?? '', + severity_mapping_json: row.severity_mapping_json ?? '', + resource_uid_extract_regex: row.resource_uid_extract_regex ?? '', policy_id: row.policy_id ?? 0, }) modalVisible.value = true diff --git a/src/views/ops/pages/log-mgmt/trap-dictionary/index.vue b/src/views/ops/pages/log-mgmt/trap-dictionary/index.vue index 5a3979b..cd22752 100644 --- a/src/views/ops/pages/log-mgmt/trap-dictionary/index.vue +++ b/src/views/ops/pages/log-mgmt/trap-dictionary/index.vue @@ -47,9 +47,24 @@ @cancel="modalVisible = false" > + + + + + + + + + + + + + + + @@ -82,6 +97,12 @@ + + + + + + @@ -120,9 +141,14 @@ const formRef = ref() const emptyForm = (): TrapDictionaryEntry => ({ oid_prefix: '', + vendor: '', + oid: '', + name: '', title: '', description: '', severity_code: '', + severity_mapping_json: '', + parse_expression: '', recovery_message: '', enabled: true, }) @@ -186,7 +212,10 @@ const formItems = computed(() => [{ field: 'keyword', label: '关键 const columns = computed(() => [ { title: 'ID', dataIndex: 'id', width: 72 }, + { title: '厂商', dataIndex: 'vendor', width: 100, ellipsis: true, tooltip: true }, + { title: '精确 OID', dataIndex: 'oid', ellipsis: true, tooltip: true }, { title: 'OID 前缀', dataIndex: 'oid_prefix', ellipsis: true, tooltip: true }, + { title: '名称', dataIndex: 'name', ellipsis: true, tooltip: true }, { title: '标题', dataIndex: 'title', ellipsis: true, tooltip: true }, { title: '级别', dataIndex: 'severity_code', slotName: 'severity_code', width: 110 }, { title: '启用', dataIndex: 'enabled', slotName: 'enabled', width: 88 }, @@ -228,7 +257,15 @@ function severityColor(code: string) { function applyFilter(list: TrapDictionaryEntry[]) { const kw = formModel.value.keyword?.trim() if (!kw) return list - return list.filter((r) => r.oid_prefix?.includes(kw) || r.title?.includes(kw) || r.description?.includes(kw)) + return list.filter( + (r) => + r.oid_prefix?.includes(kw) || + r.oid?.includes(kw) || + r.vendor?.includes(kw) || + r.name?.includes(kw) || + r.title?.includes(kw) || + r.description?.includes(kw) + ) } function slicePage(list: TrapDictionaryEntry[]) { @@ -295,10 +332,15 @@ function openEdit(row: TrapDictionaryEntry) { if (!row.id) return editingId.value = row.id Object.assign(formData, { + vendor: row.vendor ?? '', + oid: row.oid ?? '', oid_prefix: row.oid_prefix, + name: row.name ?? '', title: row.title, description: row.description ?? '', severity_code: row.severity_code ?? '', + severity_mapping_json: row.severity_mapping_json ?? '', + parse_expression: row.parse_expression ?? '', recovery_message: row.recovery_message ?? '', enabled: row.enabled, }) diff --git a/src/views/ops/pages/monitor/collection-health/index.vue b/src/views/ops/pages/monitor/collection-health/index.vue new file mode 100644 index 0000000..a7a71c3 --- /dev/null +++ b/src/views/ops/pages/monitor/collection-health/index.vue @@ -0,0 +1,250 @@ + + + + + + + diff --git a/src/views/ops/pages/netarch/topo/components/NodeDetailDialog.vue b/src/views/ops/pages/netarch/topo/components/NodeDetailDialog.vue index 6679ca4..c8619be 100644 --- a/src/views/ops/pages/netarch/topo/components/NodeDetailDialog.vue +++ b/src/views/ops/pages/netarch/topo/components/NodeDetailDialog.vue @@ -31,6 +31,12 @@ + +
+
资源UID
+
{{ nodeData.resource_uid }}
+
+
设备状态
diff --git a/src/views/ops/pages/netarch/topo/components/NodeEditDialog.vue b/src/views/ops/pages/netarch/topo/components/NodeEditDialog.vue index b24b4eb..a7b7749 100644 --- a/src/views/ops/pages/netarch/topo/components/NodeEditDialog.vue +++ b/src/views/ops/pages/netarch/topo/components/NodeEditDialog.vue @@ -28,6 +28,10 @@ + + + + @@ -63,6 +67,7 @@ const formData = ref({ label: '', type: 'server' as DeviceType, ip: '', + resource_uid: '', traffic: '', description: '', }) @@ -93,6 +98,7 @@ watch( label: props.node.data?.label || '', type: props.node.data?.type || 'server', ip: props.node.data?.ip || '', + resource_uid: props.node.data?.resource_uid || '', traffic: props.node.data?.traffic || '', description: props.node.data?.description || '', } @@ -101,6 +107,7 @@ watch( label: '', type: 'server', ip: '', + resource_uid: '', traffic: '', description: '', } diff --git a/src/views/ops/pages/netarch/topo/index.vue b/src/views/ops/pages/netarch/topo/index.vue index bdc396a..48e1cb5 100644 --- a/src/views/ops/pages/netarch/topo/index.vue +++ b/src/views/ops/pages/netarch/topo/index.vue @@ -50,6 +50,46 @@
+ + @@ -170,6 +210,11 @@ const currentTopologyId = computed(() => { return id ? parseInt(id as string) : null }) +const currentIncidentId = computed(() => { + const id = route.query.incident_id + return id ? parseInt(id as string) : null +}) + // 根据路由判断高度 const containerHeight = computed(() => { return route.path.includes('/netarch/auto-topo') ? 'calc(100vh - 170px)' : '100vh' @@ -207,6 +252,9 @@ const isNewEdge = computed(() => { return !edges.value.some((e: any) => e.id === selectedEdge.value?.id) }) +const impactLoading = ref(false) +const impactData = ref(null) + // ==================== 生命周期 ==================== // 初始化数据 @@ -245,6 +293,7 @@ const loadData = async () => { label: node.label, type: node.type, ip: node.ip, + resource_uid: node.resource_uid, status: node.status || 'normal', alerts: node.alerts || 0, traffic: node.traffic, @@ -350,6 +399,7 @@ const onConnect = async (connection: any) => { const onNodeClick = (event: any) => { selectedNode.value = event.node nodeActionDialogOpen.value = true + loadImpactAnalysis(event.node) } // 边点击 @@ -417,6 +467,7 @@ const handleSaveNode = async (nodeId: string | null, nodeData: Partial label: nodeData.label!, type: nodeData.type!, ip: nodeData.ip, + resource_uid: nodeData.resource_uid, status: nodeData.status, alerts: nodeData.alerts, traffic: nodeData.traffic, @@ -431,6 +482,7 @@ const handleSaveNode = async (nodeId: string | null, nodeData: Partial label: nodeData.label!, type: nodeData.type!, ip: nodeData.ip, + resource_uid: nodeData.resource_uid, status: nodeData.status || 'normal', alerts: nodeData.alerts || 0, traffic: nodeData.traffic, @@ -451,6 +503,29 @@ const handleSaveNode = async (nodeId: string | null, nodeData: Partial selectedNode.value = null } +const loadImpactAnalysis = async (node: any) => { + const resourceUID = node?.data?.resource_uid + if (!resourceUID) { + impactData.value = null + return + } + try { + impactLoading.value = true + const response: any = await TopoAPI.fetchImpactAnalysis({ + resource_uid: resourceUID, + topology_id: currentTopologyId.value || undefined, + incident_id: currentIncidentId.value || undefined, + depth: 2, + }) + impactData.value = response.details || response.data || null + } catch (error) { + console.error('加载影响分析失败:', error) + impactData.value = null + } finally { + impactLoading.value = false + } +} + // 删除节点 const handleDeleteNode = () => { nodeActionDialogOpen.value = false @@ -635,8 +710,9 @@ const resetTopology = async () => { position: node.data?.position || { x: Math.random() * 800, y: Math.random() * 600 }, data: { label: node.label, - type: node.type, - ...node.data, + type: node.type, + resource_uid: node.resource_uid, + ...node.data, }, })) || [] } catch (e) { @@ -711,6 +787,49 @@ const getNodeColor = (node: any) => { position: relative; } +.impact-sidebar { + flex: 0 0 300px; + height: 100%; + padding: 16px; + overflow: auto; + border-left: 1px solid var(--color-border-2); + background: var(--color-bg-1); +} + +.impact-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 12px; +} + +.impact-title { + font-size: 16px; + font-weight: 600; + color: var(--color-text-1); +} + +.impact-spin { + width: 100%; +} + +.impact-resource { + padding: 8px; + font-size: 13px; + color: var(--color-text-2); + word-break: break-all; + background: var(--color-fill-2); + border-radius: 4px; +} + +.impact-section-title { + margin-bottom: 8px; + font-size: 13px; + font-weight: 600; + color: var(--color-text-1); +} + .flow-wrapper { flex-grow: 1; width: 100%; diff --git a/src/views/ops/pages/netarch/topo/services/topoService.ts b/src/views/ops/pages/netarch/topo/services/topoService.ts index fd20aca..db7524e 100644 --- a/src/views/ops/pages/netarch/topo/services/topoService.ts +++ b/src/views/ops/pages/netarch/topo/services/topoService.ts @@ -143,6 +143,7 @@ export class TopoService { label: node.label, type: node.type, ip: node.ip, + resource_uid: node.resource_uid, status: node.status || 'normal', alerts: node.alerts || 0, traffic: node.traffic, diff --git a/src/views/ops/pages/netarch/topo/types.ts b/src/views/ops/pages/netarch/topo/types.ts index 428bf52..4cd3d3e 100644 --- a/src/views/ops/pages/netarch/topo/types.ts +++ b/src/views/ops/pages/netarch/topo/types.ts @@ -9,6 +9,7 @@ export interface NodeData { label: string // 节点标签/名称 type: DeviceType // 节点类型 ip?: string // 节点IP地址 + resource_uid?: string // 统一资源 UID status?: DeviceStatus // 节点状态 alerts?: number // 告警数量 traffic?: string // 流量信息(如"100Mbps") diff --git a/src/views/ops/pages/report/fault/index.vue b/src/views/ops/pages/report/fault/index.vue index 06e944e..25200fb 100644 --- a/src/views/ops/pages/report/fault/index.vue +++ b/src/views/ops/pages/report/fault/index.vue @@ -45,6 +45,10 @@ 查看内容 + + + 证据导出 + @@ -232,6 +236,7 @@ import { generateReport, fetchReportContent, exportReport, + exportEvidenceReport, ReportType, type ReportRecord, type FaultReportParams, @@ -651,6 +656,44 @@ const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => { } } +const handleExportEvidence = async (record?: ReportRecord) => { + const targetRecord = record || selectedRecord.value + if (!targetRecord) { + Message.warning('请选择故障报表记录') + return + } + + try { + const contentRes = await fetchReportContent(targetRecord.id) + const incidents = contentRes.code === 0 && contentRes.details ? contentRes.details.incidents || [] : [] + const timeline = incidents.map((item: any) => ({ + event_time: item.started_at, + action: 'incident_lifecycle', + actor: 'dc-control', + message: item.summary || item.alert_name || item.dedupe_key, + source_id: item.dedupe_key, + details: item, + })) + const res = await exportEvidenceReport({ + incident_id: targetRecord.id, + title: `${targetRecord.title}-证据导出`, + timeline, + notifications: [], + ticket_logs: [], + audit_logs: [], + }) + if (res.code === 0 && res.details) { + Message.success('证据导出记录已生成') + fetchList() + } else { + Message.error(res.message || '证据导出失败') + } + } catch (error: any) { + console.error('证据导出失败:', error) + Message.error(error.message || '证据导出失败') + } +} + // 格式化标签 const formatLabel = (key: string) => { const labelMap: Record = { diff --git a/src/views/ops/pages/report/topn/index.vue b/src/views/ops/pages/report/topn/index.vue index 6d1c2be..f9ecc53 100644 --- a/src/views/ops/pages/report/topn/index.vue +++ b/src/views/ops/pages/report/topn/index.vue @@ -92,6 +92,7 @@ 网络 数据库 中间件 + 告警频率
@@ -108,7 +109,7 @@ :loading="metricOptionsLoading" :options="metricOptions" :placeholder="generateForm.data_source ? '请选择逻辑指标' : '请先选择数据源'" - :disabled="!generateForm.data_source" + :disabled="!generateForm.data_source || generateForm.data_source === 'alert'" style="width: 100%" /> @@ -130,10 +131,13 @@ :placeholder=" generateForm.data_source ? '请选择或搜索目标(可多选)' : '请先选择数据源' " - :disabled="!generateForm.data_source" + :disabled="!generateForm.data_source || generateForm.data_source === 'alert'" :max-tag-count="3" style="width: 100%" /> +
+ 告警频率 TopN 按 dedupe_key/告警名聚合,无需选择目标标识。 +
TopN 当前实现可能对「网络」数据源无法出数;选项来自网络设备服务列表,供对齐标识使用。
@@ -376,6 +380,10 @@ watch( (ds) => { generateForm.value.target_identities = [] generateForm.value.metric_id = '' + if (ds === 'alert') { + generateForm.value.metric_id = 'alert_frequency' + return + } loadTargetIdentityOptions(ds) loadMetricRegistryOptions(ds) }, @@ -521,12 +529,12 @@ const handleGenerate = async () => { return } - if (!generateForm.value.metric_id) { + if (generateForm.value.data_source !== 'alert' && !generateForm.value.metric_id) { Message.warning('请选择指标') return } - if (generateForm.value.target_identities.length === 0) { + if (generateForm.value.data_source !== 'alert' && generateForm.value.target_identities.length === 0) { Message.warning('请选择目标标识') return } @@ -541,8 +549,8 @@ const handleGenerate = async () => { try { const params: TopNReportParams = { data_source: generateForm.value.data_source as any, - metric_id: generateForm.value.metric_id, - target_identities: generateForm.value.target_identities, + metric_id: generateForm.value.data_source === 'alert' ? 'alert_frequency' : generateForm.value.metric_id, + target_identities: generateForm.value.data_source === 'alert' ? [] : generateForm.value.target_identities, start_time: generateForm.value.timeRange[0], end_time: generateForm.value.timeRange[1], } diff --git a/src/views/ops/pages/resource-context/index.vue b/src/views/ops/pages/resource-context/index.vue new file mode 100644 index 0000000..eb15d87 --- /dev/null +++ b/src/views/ops/pages/resource-context/index.vue @@ -0,0 +1,408 @@ + + + + + + + diff --git a/src/views/ops/pages/system-settings/audit-logs/index.vue b/src/views/ops/pages/system-settings/audit-logs/index.vue new file mode 100644 index 0000000..74c7600 --- /dev/null +++ b/src/views/ops/pages/system-settings/audit-logs/index.vue @@ -0,0 +1,197 @@ + + + + + diff --git a/src/views/ops/pages/system-settings/danger-approvals/index.vue b/src/views/ops/pages/system-settings/danger-approvals/index.vue new file mode 100644 index 0000000..a3a6293 --- /dev/null +++ b/src/views/ops/pages/system-settings/danger-approvals/index.vue @@ -0,0 +1,246 @@ + + + + + diff --git a/src/views/ops/pages/system-settings/system-logs/index.vue b/src/views/ops/pages/system-settings/system-logs/index.vue index 104e77b..8eadc27 100644 --- a/src/views/ops/pages/system-settings/system-logs/index.vue +++ b/src/views/ops/pages/system-settings/system-logs/index.vue @@ -27,6 +27,14 @@ + + + + + + + + @@ -42,8 +50,10 @@ import LogMgmtSyslogRules from '@/views/ops/pages/log-mgmt/syslog-rules/index.vu import LogMgmtTrapRules from '@/views/ops/pages/log-mgmt/trap-rules/index.vue' import LogMgmtTrapDictionary from '@/views/ops/pages/log-mgmt/trap-dictionary/index.vue' import LogMgmtTrapSuppressions from '@/views/ops/pages/log-mgmt/trap-suppressions/index.vue' +import AuditLogs from '@/views/ops/pages/system-settings/audit-logs/index.vue' +import DangerApprovals from '@/views/ops/pages/system-settings/danger-approvals/index.vue' -const activeKey = ref<'entries' | 'syslog-rules' | 'trap-rules' | 'trap-dictionary' | 'trap-suppressions'>('entries') +const activeKey = ref<'entries' | 'syslog-rules' | 'trap-rules' | 'trap-dictionary' | 'trap-suppressions' | 'audit-logs' | 'danger-approvals'>('entries') + + + +