feat
This commit is contained in:
@@ -37,7 +37,8 @@
|
||||
"vue": "^3.5.29",
|
||||
"vue-echarts": "^8.0.1",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-router": "5"
|
||||
"vue-router": "5",
|
||||
"vue-web-terminal": "^3.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arco-plugins/vite-vue": "^1.4.6",
|
||||
|
||||
@@ -20,6 +20,8 @@ export interface MenuItem {
|
||||
web_url?: string; // 嵌入的网页URL
|
||||
component?: string; // 路由文件地址
|
||||
is_new_tab?: boolean; // 是否在新窗口打开
|
||||
is_full?: boolean; // 单独的页面,不包含菜单
|
||||
hide_menu?: boolean; // 隐藏菜单栏的菜单
|
||||
}
|
||||
|
||||
/** 获取菜单 */
|
||||
|
||||
40
src/api/ops/pc.ts
Normal file
40
src/api/ops/pc.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
/** 获取PC列表(分页) */
|
||||
export const fetchPCList = (data?: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
keyword?: string;
|
||||
datacenter_id?: number;
|
||||
rack_id?: number;
|
||||
status?: string;
|
||||
sort?: string;
|
||||
order?: string;
|
||||
}) => {
|
||||
return request.post("/Assets/v1/pc/list", data || {});
|
||||
};
|
||||
|
||||
/** 获取PC详情 */
|
||||
export const fetchPCDetail = (id: number) => {
|
||||
return request.get(`/Assets/v1/pc/detail/${id}`);
|
||||
};
|
||||
|
||||
/** 创建PC */
|
||||
export const createPC = (data: any) => {
|
||||
return request.post("/Assets/v1/pc/create", data);
|
||||
};
|
||||
|
||||
/** 更新PC */
|
||||
export const updatePC = (data: any) => {
|
||||
return request.put("/Assets/v1/pc/update", data);
|
||||
};
|
||||
|
||||
/** 删除PC */
|
||||
export const deletePC = (id: number) => {
|
||||
return request.delete(`/Assets/v1/pc/delete/${id}`);
|
||||
};
|
||||
|
||||
/** 获取机柜列表(用于下拉选择) */
|
||||
export const fetchRackListForSelect = (datacenterId?: number) => {
|
||||
return request.get("/Assets/v1/rack/all", { params: { datacenter_id: datacenterId } });
|
||||
};
|
||||
39
src/api/ops/report.ts
Normal file
39
src/api/ops/report.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
// ============ 监测指标类接口 ============
|
||||
|
||||
/** 监测指标TOPN */
|
||||
export const fetchMetricsTopN = (params: any) =>
|
||||
request.get("/DC-Control/v1/reports/metrics/topn", { params });
|
||||
|
||||
/** 监测指标汇总 */
|
||||
export const fetchMetricsSummary = (params: any) =>
|
||||
request.get("/DC-Control/v1/reports/metrics/summary", { params });
|
||||
|
||||
/** 监测指标汇总导出 */
|
||||
export const exportMetricsSummary = (params: any) =>
|
||||
request.get("/DC-Control/v1/reports/metrics/export", { params, responseType: 'blob' });
|
||||
|
||||
// ============ 流量报表接口 ============
|
||||
|
||||
/** 流量报表汇总 */
|
||||
export const fetchTrafficSummary = (params: any) =>
|
||||
request.get("/DC-Control/v1/reports/traffic/summary", { params });
|
||||
|
||||
/** 流量报表趋势 */
|
||||
export const fetchTrafficTrend = (params: any) =>
|
||||
request.get("/DC-Control/v1/reports/traffic/trend", { params });
|
||||
|
||||
/** 流量报表导出 */
|
||||
export const exportTrafficReport = (params: any) =>
|
||||
request.get("/DC-Control/v1/reports/traffic/export", { params, responseType: 'blob' });
|
||||
|
||||
// ============ 状态报表接口 ============
|
||||
|
||||
/** 服务器状态报表 */
|
||||
export const fetchServerStatus = (params: any) =>
|
||||
request.get("/DC-Control/v1/reports/servers/status", { params });
|
||||
|
||||
/** 网络设备状态报表 */
|
||||
export const fetchNetworkDeviceStatus = (params: any) =>
|
||||
request.get("/DC-Control/v1/reports/network-devices/status", { params });
|
||||
40
src/api/ops/server.ts
Normal file
40
src/api/ops/server.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
/** 获取服务器列表(分页) */
|
||||
export const fetchServerList = (data?: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
keyword?: string;
|
||||
datacenter_id?: number;
|
||||
rack_id?: number;
|
||||
status?: string;
|
||||
sort?: string;
|
||||
order?: string;
|
||||
}) => {
|
||||
return request.post("/Assets/v1/server/list", data || {});
|
||||
};
|
||||
|
||||
/** 获取服务器详情 */
|
||||
export const fetchServerDetail = (id: number) => {
|
||||
return request.get(`/Assets/v1/server/detail/${id}`);
|
||||
};
|
||||
|
||||
/** 创建服务器 */
|
||||
export const createServer = (data: any) => {
|
||||
return request.post("/Assets/v1/server/create", data);
|
||||
};
|
||||
|
||||
/** 更新服务器 */
|
||||
export const updateServer = (data: any) => {
|
||||
return request.put("/Assets/v1/server/update", data);
|
||||
};
|
||||
|
||||
/** 删除服务器 */
|
||||
export const deleteServer = (id: number) => {
|
||||
return request.delete(`/Assets/v1/server/delete/${id}`);
|
||||
};
|
||||
|
||||
/** 获取机柜列表(用于下拉选择) */
|
||||
export const fetchRackListForSelect = (datacenterId?: number) => {
|
||||
return request.get("/Assets/v1/rack/all", { params: { datacenter_id: datacenterId } });
|
||||
};
|
||||
31
src/api/ops/template.ts
Normal file
31
src/api/ops/template.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
/** 获取工单模板列表 */
|
||||
export const fetchTemplates = (data?: {
|
||||
page?: number,
|
||||
page_size?: number,
|
||||
status?: string
|
||||
}) => {
|
||||
return request.get("/Feedback/v1/templates", data ? { params: data } : undefined);
|
||||
};
|
||||
|
||||
/** 创建工单模板 */
|
||||
export const createTemplate = (data: any) => request.post("/Feedback/v1/templates", data);
|
||||
|
||||
/** 更新工单模板 */
|
||||
export const updateTemplate = (id: number, data: any) => request.put(`/Feedback/v1/templates/${id}`, data);
|
||||
|
||||
/** 获取工单模板详情 */
|
||||
export const fetchTemplateDetail = (id: number) => request.get(`/Feedback/v1/templates/${id}`);
|
||||
|
||||
/** 删除工单模板 */
|
||||
export const deleteTemplate = (id: number) => request.delete(`/Feedback/v1/templates/${id}`);
|
||||
|
||||
/** 启用工单模板 */
|
||||
export const activateTemplate = (id: number) => request.post(`/Feedback/v1/templates/${id}/activate`);
|
||||
|
||||
/** 禁用工单模板 */
|
||||
export const deactivateTemplate = (id: number) => request.post(`/Feedback/v1/templates/${id}/deactivate`);
|
||||
|
||||
/** 按模板创建工单 */
|
||||
export const createTicketByTemplate = (id: number) => request.post(`/Feedback/v1/templates/${id}/create-ticket`);
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<a-layout class="layout" :class="{ mobile: appStore.hideMenu }">
|
||||
<div v-if="navbar && !route?.meta?.isNewTab" class="layout-navbar">
|
||||
<div v-if="navbar && !route?.meta?.isNewTab && !route?.meta?.is_full" class="layout-navbar">
|
||||
<NavBar />
|
||||
</div>
|
||||
<a-layout>
|
||||
<a-layout>
|
||||
<a-layout-sider
|
||||
v-if="renderMenu"
|
||||
v-show="!hideMenu && !route?.meta?.isNewTab"
|
||||
v-show="!hideMenu && !route?.meta?.isNewTab && !route?.meta?.is_full"
|
||||
class="layout-sider"
|
||||
:breakpoint="'xl'"
|
||||
:collapsible="true"
|
||||
@@ -132,8 +132,8 @@ watch(
|
||||
)
|
||||
|
||||
const paddingStyle = computed(() => {
|
||||
const paddingLeft = renderMenu.value && !hideMenu.value ? { paddingLeft: `${menuWidth.value}px` } : {}
|
||||
const paddingTop = navbar.value ? { paddingTop: navbarHeight } : {}
|
||||
const paddingLeft = renderMenu.value && !hideMenu.value && !route?.meta?.is_full ? { paddingLeft: `${menuWidth.value}px` } : {}
|
||||
const paddingTop = navbar.value && !route?.meta?.is_full ? { paddingTop: navbarHeight } : {}
|
||||
return { ...paddingLeft, ...paddingTop }
|
||||
})
|
||||
const setCollapsed = (val: boolean) => {
|
||||
|
||||
@@ -33,6 +33,8 @@ export default {
|
||||
'menu.ops.systemSettings.menuManagement': 'Menu Management',
|
||||
'menu.ops.systemSettings.systemLogs': 'System Logs',
|
||||
'menu.ops.webTest': 'Web Test',
|
||||
'menu.ops.report': 'Report Management',
|
||||
'menu.ops.report.history': 'Report History',
|
||||
'menu.management': 'Menu Management',
|
||||
'menu.addRoot': 'Add Root Menu',
|
||||
'menu.tip': 'Click a menu item to edit, hover to show action buttons',
|
||||
|
||||
@@ -33,6 +33,8 @@ export default {
|
||||
'menu.ops.systemSettings.menuManagement': '菜单管理',
|
||||
'menu.ops.systemSettings.systemLogs': '系统日志',
|
||||
'menu.ops.webTest': '网页测试',
|
||||
'menu.ops.report': '报表管理',
|
||||
'menu.ops.report.history': '报表历史',
|
||||
'menu.management': '菜单管理',
|
||||
'menu.addRoot': '添加根菜单',
|
||||
'menu.tip': '点击菜单项可编辑,悬停显示操作按钮',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DEFAULT_LAYOUT } from './routes/base'
|
||||
import { DEFAULT_LAYOUT, STANDALONE_LAYOUT } from './routes/base'
|
||||
import type { AppRouteRecordRaw } from './routes/types'
|
||||
import type { TreeNodeBase } from '@/utils/tree'
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface ServerMenuItem extends TreeNodeBase {
|
||||
requiresAuth?: boolean
|
||||
roles?: string[]
|
||||
children?: ServerMenuItem[]
|
||||
is_full?: boolean // 是否为独立页面(不包含菜单栏)
|
||||
hide_menu?: boolean // 是否隐藏菜单栏
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
@@ -74,42 +76,60 @@ export function transformMenuToRoutes(menuItems: ServerMenuItem[]): AppRouteReco
|
||||
const routes: AppRouteRecordRaw[] = []
|
||||
|
||||
for (const item of menuItems) {
|
||||
// 根据 is_full 决定如何设置 component
|
||||
let routeComponent: AppRouteRecordRaw['component']
|
||||
|
||||
if (item.is_full) {
|
||||
// 独立页面:直接加载视图组件,不使用布局
|
||||
if (item.component) {
|
||||
routeComponent = loadViewComponent(item.component)
|
||||
} else {
|
||||
// 如果没有 component,使用默认重定向页面
|
||||
routeComponent = () => import('@/views/redirect/index.vue')
|
||||
}
|
||||
} else {
|
||||
// 非独立页面:使用默认布局
|
||||
routeComponent = DEFAULT_LAYOUT
|
||||
}
|
||||
|
||||
const route: AppRouteRecordRaw = {
|
||||
path: item.menu_path || '',
|
||||
name: item.title || item.name || `menu_${item.id}`,
|
||||
component: routeComponent,
|
||||
meta: {
|
||||
// ...item,
|
||||
locale: item.locale || item.title,
|
||||
requiresAuth: item.requiresAuth !== false,
|
||||
icon: item.icon || item?.menu_icon,
|
||||
order: item.sort_key ?? item.order,
|
||||
hideInMenu: item.hideInMenu,
|
||||
hideInMenu: item.hideInMenu || item.hide_menu,
|
||||
hideChildrenInMenu: item.hideChildrenInMenu,
|
||||
roles: item.roles,
|
||||
isNewTab: item.is_new_tab,
|
||||
},
|
||||
component: DEFAULT_LAYOUT,
|
||||
}
|
||||
|
||||
// 处理子菜单
|
||||
if (item.children && item.children.length > 0) {
|
||||
// 传递父级的 component 和 path 给子路由处理函数
|
||||
route.children = transformChildRoutes(item.children, item.component, item.menu_path)
|
||||
} else if (item.component) {
|
||||
// 一级菜单没有 children 但有 component,创建一个空路径的子路由
|
||||
const routeName = route.name
|
||||
route.children = [
|
||||
{
|
||||
path: item.menu_path || '',
|
||||
name: typeof routeName === 'string' ? `${routeName}Index` : `menu_${item.id}_index`,
|
||||
component: loadViewComponent(item.component),
|
||||
meta: {
|
||||
locale: item.locale || item.title,
|
||||
requiresAuth: item.requiresAuth !== false,
|
||||
isNewTab: item.is_new_tab,
|
||||
// 非独立页面需要处理子菜单
|
||||
if (!item.is_full) {
|
||||
if (item.children && item.children.length > 0) {
|
||||
// 传递父级的 component 和 path 给子路由处理函数
|
||||
route.children = transformChildRoutes(item.children, item.component, item.menu_path, false)
|
||||
} else if (item.component) {
|
||||
// 一级菜单没有 children 但有 component,创建一个空路径的子路由
|
||||
const routeName = route.name
|
||||
route.children = [
|
||||
{
|
||||
path: item.menu_path || '',
|
||||
name: typeof routeName === 'string' ? `${routeName}Index` : `menu_${item.id}_index`,
|
||||
component: loadViewComponent(item.component),
|
||||
meta: {
|
||||
locale: item.locale || item.title,
|
||||
requiresAuth: item.requiresAuth !== false,
|
||||
isNewTab: item.is_new_tab,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
routes.push(route)
|
||||
@@ -158,12 +178,14 @@ function extractRelativePath(childPath: string, parentPath: string): string {
|
||||
* @param children 子菜单项
|
||||
* @param parentComponent 父级菜单的 component 字段(用于子菜单没有 component 时继承)
|
||||
* @param parentPath 父级菜单的路径(用于计算相对路径)
|
||||
* @param parentIsFull 父级菜单的 is_full 字段
|
||||
* @returns 子路由配置数组
|
||||
*/
|
||||
function transformChildRoutes(
|
||||
children: ServerMenuItem[],
|
||||
parentComponent?: string,
|
||||
parentPath?: string
|
||||
parentPath?: string,
|
||||
parentIsFull?: boolean
|
||||
): AppRouteRecordRaw[] {
|
||||
return children.map((child) => {
|
||||
// 优先使用子菜单自己的 component,否则继承父级的 component
|
||||
@@ -180,6 +202,7 @@ function transformChildRoutes(
|
||||
locale: child.locale || child.title,
|
||||
requiresAuth: child.requiresAuth !== false,
|
||||
roles: child.roles,
|
||||
hideInMenu: child.hideInMenu || child.hide_menu,
|
||||
},
|
||||
component: componentPath
|
||||
? loadViewComponent(componentPath)
|
||||
@@ -191,7 +214,8 @@ function transformChildRoutes(
|
||||
route.children = transformChildRoutes(
|
||||
child.children,
|
||||
child.component || parentComponent,
|
||||
childFullPath // 传递当前子菜单的完整路径作为下一层的父路径
|
||||
childFullPath, // 传递当前子菜单的完整路径作为下一层的父路径
|
||||
child.is_full || parentIsFull // 传递 is_full 标志
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { REDIRECT_ROUTE_NAME } from '@/router/constants'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const DEFAULT_LAYOUT = () => import('@/layout/default-layout.vue')
|
||||
export const STANDALONE_LAYOUT = () => import('@/layout/standalone-layout.vue')
|
||||
|
||||
export const REDIRECT_MAIN: RouteRecordRaw = {
|
||||
path: '/redirect',
|
||||
|
||||
@@ -73,6 +73,16 @@ const OPS: AppRouteRecordRaw = {
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'report/history',
|
||||
name: 'ReportHistory',
|
||||
component: () => import('@/views/ops/pages/report/history/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.ops.report.history',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
45
src/router/routes/modules/remote.ts
Normal file
45
src/router/routes/modules/remote.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { DEFAULT_LAYOUT } from '../base'
|
||||
import { AppRouteRecordRaw } from '../types'
|
||||
|
||||
const REMOTE: AppRouteRecordRaw = {
|
||||
path: '/dc',
|
||||
name: 'DC',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: 'menu.dc',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-desktop',
|
||||
order: 99,
|
||||
hideInMenu: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'detail',
|
||||
name: 'DCDetail',
|
||||
component: () => import('@/views/ops/pages/dc/detail/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.dc.detail',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
hideInMenu: true,
|
||||
is_full: true,
|
||||
isNewTab: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'remote',
|
||||
name: 'DCRemote',
|
||||
component: () => import('@/views/ops/pages/dc/remote/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.dc.remote',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
hideInMenu: true,
|
||||
is_full: true,
|
||||
isNewTab: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default REMOTE
|
||||
@@ -0,0 +1,430 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="isEdit ? `编辑工单模板 - ${form.template_name}` : '新建工单模板'"
|
||||
:width="800"
|
||||
:footer="false"
|
||||
@update:visible="handleVisibleChange"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="模板名称" field="template_name">
|
||||
<a-input v-model="form.template_name" placeholder="请输入模板名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="模板编码" field="template_code">
|
||||
<a-input
|
||||
v-model="form.template_code"
|
||||
placeholder="请输入模板编码"
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
<template v-if="isEdit" #extra>
|
||||
<span style="color: var(--color-text-3); font-size: 12px;">模板编码创建后不可修改</span>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="描述" field="description">
|
||||
<a-textarea
|
||||
v-model="form.description"
|
||||
placeholder="请输入模板描述"
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-divider orientation="left">工单默认值</a-divider>
|
||||
|
||||
<a-form-item label="默认标题" field="title">
|
||||
<a-input v-model="form.title" placeholder="请输入工单默认标题" />
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="类型" field="type">
|
||||
<a-select v-model="form.type" placeholder="请选择类型" allow-clear>
|
||||
<a-option value="incident">事件</a-option>
|
||||
<a-option value="request">请求</a-option>
|
||||
<a-option value="change">变更</a-option>
|
||||
<a-option value="problem">问题</a-option>
|
||||
<a-option value="consultation">咨询</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="优先级" field="priority">
|
||||
<a-select v-model="form.priority" placeholder="请选择优先级" allow-clear>
|
||||
<a-option value="low">低</a-option>
|
||||
<a-option value="medium">中</a-option>
|
||||
<a-option value="high">高</a-option>
|
||||
<a-option value="critical">紧急</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="默认分类ID" field="category_id">
|
||||
<a-input-number
|
||||
v-model="form.category_id"
|
||||
placeholder="请输入分类ID"
|
||||
:min="0"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="默认处理人ID" field="assignee_id">
|
||||
<a-input-number
|
||||
v-model="form.assignee_id"
|
||||
placeholder="请输入处理人ID"
|
||||
:min="0"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="默认内容" field="content">
|
||||
<a-textarea
|
||||
v-model="form.content"
|
||||
placeholder="请输入工单默认内容"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-divider orientation="left">触发配置</a-divider>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="触发类型" field="trigger_type">
|
||||
<a-select v-model="form.trigger_type" placeholder="请选择触发类型" allow-clear>
|
||||
<a-option value="manual">手动</a-option>
|
||||
<a-option value="scheduled">定时</a-option>
|
||||
<a-option value="event">事件</a-option>
|
||||
<a-option value="api">API</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="定时表达式"
|
||||
field="cron_expression"
|
||||
:disabled="form.trigger_type !== 'scheduled'"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.cron_expression"
|
||||
placeholder="请输入Cron表达式"
|
||||
:disabled="form.trigger_type !== 'scheduled'"
|
||||
/>
|
||||
<template v-if="form.trigger_type === 'scheduled'" #extra>
|
||||
<span style="color: var(--color-text-3); font-size: 12px;">示例: 0 0 * * *</span>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="触发规则" field="trigger_rule">
|
||||
<a-textarea
|
||||
v-model="form.trigger_rule"
|
||||
placeholder='请输入JSON格式的触发规则,例如: {"key": "value"}'
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-divider orientation="left">自动化配置</a-divider>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="自动分配" field="auto_assign">
|
||||
<a-switch v-model="form.auto_assign" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="自动接单" field="auto_accept">
|
||||
<a-switch v-model="form.auto_accept" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="SLA(分钟)" field="sla_duration">
|
||||
<a-input-number
|
||||
v-model="form.sla_duration"
|
||||
placeholder="请输入SLA"
|
||||
:min="0"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="分配规则" field="assign_rule">
|
||||
<a-textarea
|
||||
v-model="form.assign_rule"
|
||||
placeholder='请输入JSON格式的分配规则,例如: {"rule": "value"}'
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-divider orientation="left">表单与通知配置</a-divider>
|
||||
|
||||
<a-form-item label="字段配置" field="field_config">
|
||||
<a-textarea
|
||||
v-model="form.field_config"
|
||||
placeholder='请输入JSON格式的字段配置,例如: {"fields": []}'
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="通知配置" field="notify_config">
|
||||
<a-textarea
|
||||
v-model="form.notify_config"
|
||||
placeholder='请输入JSON格式的通知配置,例如: {"channels": []}'
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-divider orientation="left">扩展配置</a-divider>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="标签" field="tags">
|
||||
<a-textarea
|
||||
v-model="form.tags"
|
||||
placeholder='请输入JSON格式的标签,例如: ["tag1", "tag2"]'
|
||||
:auto-size="{ minRows: 2, maxRows: 3 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="元数据" field="metadata">
|
||||
<a-textarea
|
||||
v-model="form.metadata"
|
||||
placeholder='请输入JSON格式的元数据,例如: {"key": "value"}'
|
||||
:auto-size="{ minRows: 2, maxRows: 3 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" html-type="submit" :loading="loading">
|
||||
{{ isEdit ? '更新' : '创建' }}
|
||||
</a-button>
|
||||
<a-button @click="handleCancel">取消</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch, computed } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { createTemplate, updateTemplate, fetchTemplateDetail } from '@/api/ops/template'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
templateId?: number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref()
|
||||
const loading = ref(false)
|
||||
|
||||
const isEdit = computed(() => !!props.templateId)
|
||||
|
||||
const form = reactive({
|
||||
template_name: '',
|
||||
template_code: '',
|
||||
description: '',
|
||||
title: '',
|
||||
type: '',
|
||||
priority: '',
|
||||
category_id: undefined as number | undefined,
|
||||
assignee_id: undefined as number | undefined,
|
||||
content: '',
|
||||
trigger_type: '',
|
||||
trigger_rule: '',
|
||||
cron_expression: '',
|
||||
sla_duration: undefined as number | undefined,
|
||||
field_config: '',
|
||||
auto_assign: false,
|
||||
auto_accept: false,
|
||||
assign_rule: '',
|
||||
notify_config: '',
|
||||
tags: '',
|
||||
metadata: '',
|
||||
})
|
||||
|
||||
const rules = {
|
||||
template_name: [{ required: true, message: '请输入模板名称' }],
|
||||
template_code: [{ required: true, message: '请输入模板编码' }],
|
||||
title: [{ required: true, message: '请输入默认标题' }],
|
||||
}
|
||||
|
||||
// 监听弹窗显示状态,如果是编辑模式则加载详情
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (val) => {
|
||||
if (val && props.templateId) {
|
||||
await loadTemplateDetail()
|
||||
} else if (val && !props.templateId) {
|
||||
resetForm()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 加载模板详情
|
||||
const loadTemplateDetail = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await fetchTemplateDetail(props.templateId!)
|
||||
if (res.code === 0 && res.details) {
|
||||
const data = res.details
|
||||
Object.assign(form, {
|
||||
template_name: data.template_name || '',
|
||||
template_code: data.template_code || '',
|
||||
description: data.description || '',
|
||||
title: data.title || '',
|
||||
type: data.type || '',
|
||||
priority: data.priority || '',
|
||||
category_id: data.category_id,
|
||||
assignee_id: data.assignee_id,
|
||||
content: data.content || '',
|
||||
trigger_type: data.trigger_type || '',
|
||||
trigger_rule: data.trigger_rule || '',
|
||||
cron_expression: data.cron_expression || '',
|
||||
sla_duration: data.sla_duration,
|
||||
field_config: data.field_config || '',
|
||||
auto_assign: data.auto_assign || false,
|
||||
auto_accept: data.auto_accept || false,
|
||||
assign_rule: data.assign_rule || '',
|
||||
notify_config: data.notify_config || '',
|
||||
tags: data.tags || '',
|
||||
metadata: data.metadata || '',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载模板详情失败:', error)
|
||||
Message.error('加载模板详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formRef.value?.resetFields()
|
||||
Object.assign(form, {
|
||||
template_name: '',
|
||||
template_code: '',
|
||||
description: '',
|
||||
title: '',
|
||||
type: '',
|
||||
priority: '',
|
||||
category_id: undefined,
|
||||
assignee_id: undefined,
|
||||
content: '',
|
||||
trigger_type: '',
|
||||
trigger_rule: '',
|
||||
cron_expression: '',
|
||||
sla_duration: undefined,
|
||||
field_config: '',
|
||||
auto_assign: false,
|
||||
auto_accept: false,
|
||||
assign_rule: '',
|
||||
notify_config: '',
|
||||
tags: '',
|
||||
metadata: '',
|
||||
})
|
||||
}
|
||||
|
||||
// 验证JSON格式
|
||||
const validateJSON = (value: string) => {
|
||||
if (!value || value.trim() === '') return true
|
||||
try {
|
||||
JSON.parse(value)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const valid = await formRef.value?.validate()
|
||||
if (!valid) return
|
||||
|
||||
// 验证JSON字段
|
||||
const jsonFields = ['trigger_rule', 'field_config', 'assign_rule', 'notify_config', 'tags', 'metadata']
|
||||
for (const field of jsonFields) {
|
||||
const value = form[field as keyof typeof form] as string
|
||||
if (value && value.trim() !== '' && !validateJSON(value)) {
|
||||
Message.error(`${field} 必须是合法的 JSON 格式`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
// 编辑时不包含 template_code
|
||||
const submitData: any = { ...form }
|
||||
if (isEdit.value) {
|
||||
delete submitData.template_code
|
||||
}
|
||||
|
||||
const res = isEdit.value
|
||||
? await updateTemplate(props.templateId!, submitData)
|
||||
: await createTemplate(submitData)
|
||||
|
||||
if (res.code === 0) {
|
||||
Message.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
emit('success')
|
||||
handleCancel()
|
||||
} else {
|
||||
Message.error(res.msg || (isEdit.value ? '更新失败' : '创建失败'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('提交失败:', error)
|
||||
Message.error(error.msg || '提交失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
resetForm()
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理可见性变化
|
||||
const handleVisibleChange = (value: boolean) => {
|
||||
emit('update:visible', value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TemplateFormDialog',
|
||||
}
|
||||
</script>
|
||||
113
src/views/ops/pages/alert/template/config/columns.ts
Normal file
113
src/views/ops/pages/alert/template/config/columns.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
|
||||
export const columns: TableColumnData[] = [
|
||||
{
|
||||
title: '模板名称',
|
||||
dataIndex: 'template_name',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '模板编码',
|
||||
dataIndex: 'template_code',
|
||||
width: 160,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '默认标题',
|
||||
dataIndex: 'title',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
slotName: 'type',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '优先级',
|
||||
dataIndex: 'priority',
|
||||
slotName: 'priority',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '默认分类',
|
||||
dataIndex: 'category_id',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '默认处理人',
|
||||
dataIndex: 'assignee_id',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '触发类型',
|
||||
dataIndex: 'trigger_type',
|
||||
slotName: 'trigger_type',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '定时表达式',
|
||||
dataIndex: 'cron_expression',
|
||||
width: 160,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '自动分配',
|
||||
dataIndex: 'auto_assign',
|
||||
slotName: 'auto_assign',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '自动接单',
|
||||
dataIndex: 'auto_accept',
|
||||
slotName: 'auto_accept',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: 'SLA(分钟)',
|
||||
dataIndex: 'sla_duration',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '使用次数',
|
||||
dataIndex: 'usage_count',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
dataIndex: 'creator_name',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
slotName: 'created_at',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '最后修改',
|
||||
dataIndex: 'updated_at',
|
||||
slotName: 'updated_at',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'actions',
|
||||
slotName: 'actions',
|
||||
fixed: 'right',
|
||||
width: 300,
|
||||
},
|
||||
]
|
||||
15
src/views/ops/pages/alert/template/config/search-form.ts
Normal file
15
src/views/ops/pages/alert/template/config/search-form.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { FormItem } from '@/components/search-form/types';
|
||||
|
||||
export const searchFormConfig: FormItem[] = [
|
||||
{
|
||||
field: 'status',
|
||||
label: '状态',
|
||||
type: 'select',
|
||||
placeholder: '请选择状态',
|
||||
options: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '启用', value: 'active' },
|
||||
{ label: '禁用', value: 'inactive' },
|
||||
],
|
||||
},
|
||||
];
|
||||
396
src/views/ops/pages/alert/template/index.vue
Normal file
396
src/views/ops/pages/alert/template/index.vue
Normal file
@@ -0,0 +1,396 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="工单模板管理"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<!-- 工具栏额外按钮 -->
|
||||
<template #toolbar-extra>
|
||||
<a-button type="primary" @click="handleCreate">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
新建模板
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 状态 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="record.status === 'active' ? 'green' : 'gray'">
|
||||
{{ record.status === 'active' ? '启用' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 类型 -->
|
||||
<template #type="{ record }">
|
||||
<a-tag>{{ getTypeText(record.type) }}</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 优先级 -->
|
||||
<template #priority="{ record }">
|
||||
<a-tag :color="getPriorityColor(record.priority)">
|
||||
{{ getPriorityText(record.priority) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 触发类型 -->
|
||||
<template #trigger_type="{ record }">
|
||||
<a-tag>{{ getTriggerTypeText(record.trigger_type) }}</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 自动分配 -->
|
||||
<template #auto_assign="{ record }">
|
||||
<a-tag :color="record.auto_assign ? 'green' : 'gray'">
|
||||
{{ record.auto_assign ? '是' : '否' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 自动接单 -->
|
||||
<template #auto_accept="{ record }">
|
||||
<a-tag :color="record.auto_accept ? 'green' : 'gray'">
|
||||
{{ record.auto_accept ? '是' : '否' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 创建时间 -->
|
||||
<template #created_at="{ record }">
|
||||
{{ formatDate(record.created_at) }}
|
||||
</template>
|
||||
|
||||
<!-- 最后修改 -->
|
||||
<template #updated_at="{ record }">
|
||||
{{ formatDate(record.updated_at) }}
|
||||
</template>
|
||||
|
||||
<!-- 操作 -->
|
||||
<template #actions="{ record }">
|
||||
<a-space size="small">
|
||||
<a-button type="text" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="record.status === 'inactive'"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleActivate(record)"
|
||||
>
|
||||
启用
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="record.status === 'active'"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleDeactivate(record)"
|
||||
>
|
||||
禁用
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="record.status === 'active'"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleCreateTicket(record)"
|
||||
>
|
||||
按模板创建
|
||||
</a-button>
|
||||
<a-button type="text" size="small" status="danger" @click="handleDelete(record)">
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
<!-- 新建/编辑模板弹窗 -->
|
||||
<template-form-dialog
|
||||
v-model:visible="formDialogVisible"
|
||||
:template-id="currentTemplateId"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
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 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 {
|
||||
fetchTemplates,
|
||||
deleteTemplate,
|
||||
activateTemplate,
|
||||
deactivateTemplate,
|
||||
createTicketByTemplate,
|
||||
} from '@/api/ops/template'
|
||||
import TemplateFormDialog from './components/TemplateFormDialog.vue'
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表单模型
|
||||
const formModel = ref({
|
||||
status: '',
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 表单项配置
|
||||
const formItems = computed<FormItem[]>(() => searchFormConfig)
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed(() => columnsConfig)
|
||||
|
||||
// 表单弹窗
|
||||
const formDialogVisible = ref(false)
|
||||
const currentTemplateId = ref<number | undefined>(undefined)
|
||||
|
||||
// 获取模板列表
|
||||
const fetchTemplatesData = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize,
|
||||
}
|
||||
|
||||
if (formModel.value.status) {
|
||||
params.status = formModel.value.status
|
||||
}
|
||||
|
||||
const res = await fetchTemplates(params)
|
||||
|
||||
tableData.value = res.details?.items || []
|
||||
pagination.total = res.details?.total || 0
|
||||
} catch (error) {
|
||||
console.error('获取模板列表失败:', error)
|
||||
Message.error('获取模板列表失败')
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
// 获取类型文本
|
||||
const getTypeText = (type: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
incident: '事件',
|
||||
request: '请求',
|
||||
change: '变更',
|
||||
problem: '问题',
|
||||
consultation: '咨询',
|
||||
}
|
||||
return typeMap[type] || '-'
|
||||
}
|
||||
|
||||
// 获取优先级文本
|
||||
const getPriorityText = (priority: string) => {
|
||||
const priorityMap: Record<string, string> = {
|
||||
low: '低',
|
||||
medium: '中',
|
||||
high: '高',
|
||||
critical: '紧急',
|
||||
}
|
||||
return priorityMap[priority] || '-'
|
||||
}
|
||||
|
||||
// 获取优先级颜色
|
||||
const getPriorityColor = (priority: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
low: 'blue',
|
||||
medium: 'green',
|
||||
high: 'orange',
|
||||
critical: 'red',
|
||||
}
|
||||
return colorMap[priority] || 'gray'
|
||||
}
|
||||
|
||||
// 获取触发类型文本
|
||||
const getTriggerTypeText = (triggerType: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
manual: '手动',
|
||||
scheduled: '定时',
|
||||
event: '事件',
|
||||
api: 'API',
|
||||
}
|
||||
return typeMap[triggerType] || '-'
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchTemplatesData()
|
||||
}
|
||||
|
||||
// 处理表单模型更新
|
||||
const handleFormModelUpdate = (value: any) => {
|
||||
formModel.value = {
|
||||
...formModel.value,
|
||||
...value,
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
status: '',
|
||||
}
|
||||
pagination.current = 1
|
||||
fetchTemplatesData()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
fetchTemplatesData()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchTemplatesData()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 新建模板
|
||||
const handleCreate = () => {
|
||||
currentTemplateId.value = undefined
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑模板
|
||||
const handleEdit = (record: any) => {
|
||||
currentTemplateId.value = record.id
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 启用模板
|
||||
const handleActivate = async (record: any) => {
|
||||
try {
|
||||
const res = await activateTemplate(record.id)
|
||||
if (res.code === 0) {
|
||||
Message.success('启用成功')
|
||||
fetchTemplatesData()
|
||||
} else {
|
||||
Message.error(res.msg || '启用失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启用模板失败:', error)
|
||||
Message.error('启用模板失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 禁用模板
|
||||
const handleDeactivate = async (record: any) => {
|
||||
try {
|
||||
const res = await deactivateTemplate(record.id)
|
||||
if (res.code === 0) {
|
||||
Message.success('禁用成功')
|
||||
fetchTemplatesData()
|
||||
} else {
|
||||
Message.error(res.msg || '禁用失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('禁用模板失败:', error)
|
||||
Message.error('禁用模板失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除模板
|
||||
const handleDelete = (record: any) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除模板「${record.template_name}」吗?删除后将无法恢复,且可能影响历史工单的引用。`,
|
||||
okText: '删除',
|
||||
okButtonProps: { status: 'danger' },
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res = await deleteTemplate(record.id)
|
||||
if (res.code === 0) {
|
||||
Message.success('删除成功')
|
||||
fetchTemplatesData()
|
||||
} else {
|
||||
Message.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除模板失败:', error)
|
||||
Message.error('删除模板失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 按模板创建工单
|
||||
const handleCreateTicket = async (record: any) => {
|
||||
try {
|
||||
const res = await createTicketByTemplate(record.id)
|
||||
if (res.code === 0) {
|
||||
Message.success('工单创建成功')
|
||||
// 可以在这里跳转到工单详情页
|
||||
// if (res.details?.id) {
|
||||
// router.push(`/feedback/ticket/${res.details.id}`)
|
||||
// }
|
||||
fetchTemplatesData()
|
||||
} else {
|
||||
Message.error(res.msg || '工单创建失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('按模板创建工单失败:', error)
|
||||
Message.error(error.msg || '工单创建失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 表单提交成功
|
||||
const handleFormSuccess = () => {
|
||||
fetchTemplatesData()
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
onMounted(() => {
|
||||
fetchTemplatesData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TemplateList',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
754
src/views/ops/pages/dc/detail/index.vue
Normal file
754
src/views/ops/pages/dc/detail/index.vue
Normal file
@@ -0,0 +1,754 @@
|
||||
<template>
|
||||
<div class="server-detail-page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<a-button type="text" @click="goBack">
|
||||
<template #icon><icon-left /></template>
|
||||
返回
|
||||
</a-button>
|
||||
<a-divider direction="vertical" />
|
||||
<span class="server-name">{{ serverName }}</span>
|
||||
<a-tag :color="getStatusColor(serverStatus)" size="small">{{ getStatusText(serverStatus) }}</a-tag>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<a-button @click="handleRemoteControl">
|
||||
<template #icon><icon-desktop /></template>
|
||||
远程登录
|
||||
</a-button>
|
||||
<a-button type="primary" @click="handleRestart">
|
||||
<template #icon><icon-refresh /></template>
|
||||
重启
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<div class="page-body">
|
||||
<a-tabs v-model:active-key="activeTab" class="detail-tabs">
|
||||
<a-tab-pane key="overview" title="实例详情">
|
||||
<a-descriptions :column="2" bordered class="info-descriptions">
|
||||
<a-descriptions-item label="唯一标识">{{ record.unique_id || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="服务器名称">{{ record.name || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="服务器类型">{{ getServerTypeText(record.server_type) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="操作系统">{{ record.os || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="位置信息">{{ record.location || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="标签">{{ record.tags || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="IP地址">{{ record.ip || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="远程端口">{{ record.remote_port || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="Agent URL">{{ record.agent_url || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="数据采集">
|
||||
<a-tag :color="record.data_collection ? 'green' : 'gray'">
|
||||
{{ record.data_collection ? '已开启' : '未开启' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采集间隔">{{ record.collection_interval ? `${record.collection_interval}分钟` : '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="备注信息" :span="2">{{ record.remark || '-' }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="monitor" title="监控">
|
||||
<div class="monitor-section">
|
||||
<div class="time-selector">
|
||||
<a-radio-group v-model="timeRange" type="button">
|
||||
<a-radio value="1h">1小时</a-radio>
|
||||
<a-radio value="3h">3小时</a-radio>
|
||||
<a-radio value="6h">6小时</a-radio>
|
||||
<a-radio value="12h">12小时</a-radio>
|
||||
<a-radio value="1d">1天</a-radio>
|
||||
<a-radio value="3d">3天</a-radio>
|
||||
<a-radio value="7d">7天</a-radio>
|
||||
<a-radio value="14d">14天</a-radio>
|
||||
<a-radio value="custom">自定义</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="20" class="charts-row">
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">CPU使用率_宿主机视角 (%)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="cpuMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
<a-option value="min">最小值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="cpuOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot green"></span>(ECS)CPU使用率_平均值</span>
|
||||
<span class="legend-item"><span class="legend-dot blue"></span>(ECS)CPU使用率_最小值</span>
|
||||
<span class="legend-item"><span class="legend-dot purple"></span>(ECS)CPU使用率_最大值</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">总带宽 (bit/s)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="bandwidthMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="bandwidthOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot green"></span>内网流入带宽</span>
|
||||
<span class="legend-item"><span class="legend-dot blue"></span>内网流出带宽</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">公网带宽 (bit/s)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="publicBandwidthMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="publicBandwidthOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot blue"></span>公网流入带宽</span>
|
||||
<span class="legend-item"><span class="legend-dot green"></span>公网流出带宽</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="20" class="charts-row">
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">公网流出带宽使用率 (%)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="bandwidthUsageMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="bandwidthUsageOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot blue"></span>(ECS)公网流出带宽使用率</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">磁盘读取写入BPS (bytes/s)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="diskMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="diskBpsOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot blue"></span>(ECS)所有磁盘读取BPS</span>
|
||||
<span class="legend-item"><span class="legend-dot green"></span>(ECS)所有磁盘写入BPS</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">磁盘读取写入IOPS (Count/Second)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="diskIopsMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="diskIopsOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot blue"></span>(ECS)所有磁盘每秒读取次数</span>
|
||||
<span class="legend-item"><span class="legend-dot green"></span>(ECS)所有磁盘每秒写入次数</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="20" class="charts-row">
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">同时连接数 (Count)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="connectionMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="connectionOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot blue"></span>同时连接数</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">RDMA流量使用信息 (bit/s)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="rdmaMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="rdmaOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"></span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">RDMA资源使用信息 (Count)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="rdmaResourceMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="rdmaResourceOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"></span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="operation" title="操作记录">
|
||||
<div class="operation-records">
|
||||
<a-empty description="暂无操作记录" />
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { IconLeft, IconDesktop, IconRefresh, IconSettings } from '@arco-design/web-vue/es/icon'
|
||||
import VChart from 'vue-echarts'
|
||||
import { use } from 'echarts/core'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { LineChart } from 'echarts/charts'
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
GridComponent,
|
||||
DataZoomComponent,
|
||||
} from 'echarts/components'
|
||||
|
||||
use([
|
||||
CanvasRenderer,
|
||||
LineChart,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
GridComponent,
|
||||
DataZoomComponent,
|
||||
])
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 服务器信息
|
||||
const serverId = computed(() => route.query.id as string)
|
||||
const serverName = computed(() => route.query.name as string || '服务器详情')
|
||||
const serverStatus = computed(() => route.query.status as string || 'offline')
|
||||
|
||||
// 模拟从API获取数据
|
||||
const record = ref<any>({
|
||||
id: serverId.value,
|
||||
name: route.query.name as string || '服务器详情',
|
||||
status: route.query.status as string || 'offline',
|
||||
server_type: 'physical',
|
||||
os: 'CentOS 7.9',
|
||||
unique_id: `SERVER-${serverId.value}`,
|
||||
location: '机房A-机柜01',
|
||||
tags: '生产环境,核心服务',
|
||||
ip: route.query.ip as string || '-',
|
||||
remote_port: 22,
|
||||
agent_url: 'http://192.168.1.100:8080',
|
||||
data_collection: true,
|
||||
collection_interval: 5,
|
||||
remark: '核心业务服务器',
|
||||
})
|
||||
|
||||
const activeTab = ref('overview')
|
||||
const timeRange = ref('1h')
|
||||
const cpuMetric = ref('average')
|
||||
const bandwidthMetric = ref('average')
|
||||
const publicBandwidthMetric = ref('average')
|
||||
const bandwidthUsageMetric = ref('average')
|
||||
const diskMetric = ref('average')
|
||||
const diskIopsMetric = ref('average')
|
||||
const connectionMetric = ref('average')
|
||||
const rdmaMetric = ref('average')
|
||||
const rdmaResourceMetric = ref('average')
|
||||
|
||||
const generateTimeData = (count: number) => {
|
||||
const now = Date.now()
|
||||
const data = []
|
||||
for (let i = count - 1; i >= 0; i--) {
|
||||
data.push(new Date(now - i * 60000).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }))
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
const generateRandomData = (count: number, min: number, max: number) => {
|
||||
return Array.from({ length: count }, () => Math.floor(Math.random() * (max - min + 1)) + min)
|
||||
}
|
||||
|
||||
const timeData = generateTimeData(60)
|
||||
|
||||
const baseOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||
borderColor: '#e5e6eb',
|
||||
textStyle: {
|
||||
color: '#1d2129',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: timeData,
|
||||
axisLabel: {
|
||||
fontSize: 10,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
fontSize: 10,
|
||||
},
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const cpuOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [
|
||||
{
|
||||
name: '平均值',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 20, 80),
|
||||
lineStyle: { color: '#00b42a' },
|
||||
itemStyle: { color: '#00b42a' },
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(0, 180, 42, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(0, 180, 42, 0.05)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '最小值',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 10, 40),
|
||||
lineStyle: { color: '#165dff' },
|
||||
itemStyle: { color: '#165dff' },
|
||||
},
|
||||
{
|
||||
name: '最大值',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 50, 95),
|
||||
lineStyle: { color: '#722ed1' },
|
||||
itemStyle: { color: '#722ed1' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const bandwidthOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [
|
||||
{
|
||||
name: '内网流入带宽',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 100000, 500000),
|
||||
lineStyle: { color: '#00b42a' },
|
||||
itemStyle: { color: '#00b42a' },
|
||||
},
|
||||
{
|
||||
name: '内网流出带宽',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 50000, 300000),
|
||||
lineStyle: { color: '#165dff' },
|
||||
itemStyle: { color: '#165dff' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const publicBandwidthOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [
|
||||
{
|
||||
name: '公网流入带宽',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 50000, 200000),
|
||||
lineStyle: { color: '#165dff' },
|
||||
itemStyle: { color: '#165dff' },
|
||||
},
|
||||
{
|
||||
name: '公网流出带宽',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 30000, 150000),
|
||||
lineStyle: { color: '#00b42a' },
|
||||
itemStyle: { color: '#00b42a' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const bandwidthUsageOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [
|
||||
{
|
||||
name: '公网流出带宽使用率',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 5, 30),
|
||||
lineStyle: { color: '#165dff' },
|
||||
itemStyle: { color: '#165dff' },
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(22, 93, 255, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(22, 93, 255, 0.05)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const diskBpsOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [
|
||||
{
|
||||
name: '磁盘读取BPS',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 10000, 100000),
|
||||
lineStyle: { color: '#165dff' },
|
||||
itemStyle: { color: '#165dff' },
|
||||
},
|
||||
{
|
||||
name: '磁盘写入BPS',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 20000, 150000),
|
||||
lineStyle: { color: '#00b42a' },
|
||||
itemStyle: { color: '#00b42a' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const diskIopsOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [
|
||||
{
|
||||
name: '磁盘读取IOPS',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 50, 500),
|
||||
lineStyle: { color: '#165dff' },
|
||||
itemStyle: { color: '#165dff' },
|
||||
},
|
||||
{
|
||||
name: '磁盘写入IOPS',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 100, 800),
|
||||
lineStyle: { color: '#00b42a' },
|
||||
itemStyle: { color: '#00b42a' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const connectionOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [
|
||||
{
|
||||
name: '同时连接数',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 100, 500),
|
||||
lineStyle: { color: '#165dff' },
|
||||
itemStyle: { color: '#165dff' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const rdmaOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [],
|
||||
}))
|
||||
|
||||
const rdmaResourceOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [],
|
||||
}))
|
||||
|
||||
const getStatusColor = (status?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
online: 'green',
|
||||
offline: 'red',
|
||||
maintenance: 'orange',
|
||||
retired: 'gray',
|
||||
}
|
||||
return colorMap[status || ''] || 'gray'
|
||||
}
|
||||
|
||||
const getStatusText = (status?: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
online: '在线',
|
||||
offline: '离线',
|
||||
maintenance: '维护中',
|
||||
retired: '已退役',
|
||||
}
|
||||
return textMap[status || ''] || '-'
|
||||
}
|
||||
|
||||
const getServerTypeText = (type?: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
physical: '物理服务器',
|
||||
virtual: '虚拟服务器',
|
||||
cloud: '云服务器',
|
||||
}
|
||||
return typeMap[type || ''] || '-'
|
||||
}
|
||||
|
||||
const handleRemoteControl = () => {
|
||||
router.push({
|
||||
path: '/dc/remote',
|
||||
query: {
|
||||
id: record.value.id,
|
||||
name: record.value.name,
|
||||
ip: record.value.ip,
|
||||
status: record.value.status,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleRestart = () => {
|
||||
Modal.confirm({
|
||||
title: '确认重启',
|
||||
content: `确认重启服务器 ${record.value.name} 吗?`,
|
||||
onOk: () => {
|
||||
Message.info('正在发送重启指令...')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.server-detail-page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f0f2f5;
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 24px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e5e6eb;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.server-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
|
||||
.detail-tabs {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
|
||||
:deep(.arco-tabs-header) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-descriptions {
|
||||
:deep(.arco-descriptions-item-label) {
|
||||
width: 140px;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
}
|
||||
|
||||
.monitor-section {
|
||||
.time-selector {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.charts-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-col {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.chart-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.chart-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #4e5969;
|
||||
|
||||
.legend-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
|
||||
&.green {
|
||||
background: #00b42a;
|
||||
}
|
||||
|
||||
&.blue {
|
||||
background: #165dff;
|
||||
}
|
||||
|
||||
&.purple {
|
||||
background: #722ed1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.operation-records {
|
||||
padding: 40px 0;
|
||||
}
|
||||
</style>
|
||||
310
src/views/ops/pages/dc/pc/components/RemoteControl.vue
Normal file
310
src/views/ops/pages/dc/pc/components/RemoteControl.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<div class="remote-control">
|
||||
<!-- 顶部栏 -->
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<span class="title">{{ record.name || '远程控制' }}</span>
|
||||
<a-tag :color="getStatusColor(record.status)" size="small">{{ getStatusText(record.status) }}</a-tag>
|
||||
</div>
|
||||
<a-button size="small" @click="handleClose">
|
||||
<template #icon><icon-close /></template>
|
||||
关闭
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 登录界面 -->
|
||||
<div v-if="!isConnected" class="login-box">
|
||||
<a-card title="SSH 登录" class="login-card">
|
||||
<a-form :model="loginForm" layout="vertical">
|
||||
<a-form-item label="连接协议">
|
||||
<a-radio-group v-model="loginForm.protocol" type="button">
|
||||
<a-radio value="ssh">SSH</a-radio>
|
||||
<a-radio value="tat">免密连接</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="16">
|
||||
<a-form-item label="主机地址">
|
||||
<a-input v-model="loginForm.host" placeholder="IP或域名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="端口">
|
||||
<a-input-number v-model="loginForm.port" :min="1" :max="65535" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="用户名">
|
||||
<a-input v-model="loginForm.username" placeholder="请输入用户名" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="密码">
|
||||
<a-input-password v-model="loginForm.password" placeholder="请输入密码" />
|
||||
</a-form-item>
|
||||
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleLogin" :loading="loginLoading">
|
||||
连接
|
||||
</a-button>
|
||||
<a-button @click="handleClose">取消</a-button>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 终端界面 -->
|
||||
<div v-else class="terminal-box">
|
||||
<div class="terminal-toolbar">
|
||||
<span class="terminal-info">已连接: {{ loginForm.username }}@{{ loginForm.host }}:{{ loginForm.port }}</span>
|
||||
<a-space>
|
||||
<a-button size="small" @click="handleDisconnect">断开</a-button>
|
||||
<a-button size="small" @click="handleFullscreen">全屏</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="terminal-content" ref="terminalRef">
|
||||
<div v-for="(line, index) in terminalLines" :key="index" class="terminal-line">
|
||||
<span v-if="line.type === 'input'" class="prompt">{{ line.prompt }}</span>
|
||||
<span :class="line.type">{{ line.content }}</span>
|
||||
</div>
|
||||
<div class="terminal-input">
|
||||
<span class="prompt">{{ prompt }}</span>
|
||||
<input
|
||||
ref="inputRef"
|
||||
v-model="currentInput"
|
||||
@keyup.enter="handleCommand"
|
||||
class="command-input"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, nextTick, onMounted, computed } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { IconClose } from '@arco-design/web-vue/es/icon'
|
||||
|
||||
interface Props {
|
||||
record: any
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const isConnected = ref(false)
|
||||
const loginLoading = ref(false)
|
||||
const terminalRef = ref<HTMLElement>()
|
||||
const inputRef = ref<HTMLInputElement>()
|
||||
|
||||
const loginForm = ref({
|
||||
protocol: 'ssh',
|
||||
host: props.record?.ip || '',
|
||||
port: 22,
|
||||
username: 'root',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const terminalLines = ref<Array<{type: string, content: string, prompt?: string}>>([
|
||||
{ type: 'output', content: `Welcome to ${props.record?.name || 'Server'}!` },
|
||||
{ type: 'output', content: `Last login: ${new Date().toLocaleString()}` },
|
||||
])
|
||||
|
||||
const currentInput = ref('')
|
||||
const prompt = computed(() => `${loginForm.value.username}@${loginForm.value.host}:~# `)
|
||||
|
||||
const getStatusColor = (status?: string) => {
|
||||
const map: Record<string, string> = { online: 'green', offline: 'red', maintenance: 'orange' }
|
||||
return map[status || ''] || 'gray'
|
||||
}
|
||||
|
||||
const getStatusText = (status?: string) => {
|
||||
const map: Record<string, string> = { online: '在线', offline: '离线', maintenance: '维护中' }
|
||||
return map[status || ''] || '-'
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!loginForm.value.host || !loginForm.value.username) {
|
||||
Message.warning('请填写完整信息')
|
||||
return
|
||||
}
|
||||
loginLoading.value = true
|
||||
await new Promise(r => setTimeout(r, 1000))
|
||||
isConnected.value = true
|
||||
loginLoading.value = false
|
||||
Message.success('连接成功')
|
||||
nextTick(() => inputRef.value?.focus())
|
||||
}
|
||||
|
||||
const handleCommand = () => {
|
||||
const cmd = currentInput.value.trim()
|
||||
if (!cmd) return
|
||||
|
||||
terminalLines.value.push({ type: 'input', content: cmd, prompt: prompt.value })
|
||||
|
||||
const commands: Record<string, string> = {
|
||||
help: '可用命令: help, ls, pwd, whoami, date, clear, exit',
|
||||
ls: 'bin boot dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var',
|
||||
pwd: '/root',
|
||||
whoami: loginForm.value.username,
|
||||
date: new Date().toString(),
|
||||
}
|
||||
|
||||
if (cmd === 'clear') {
|
||||
terminalLines.value = []
|
||||
} else if (cmd === 'exit') {
|
||||
handleDisconnect()
|
||||
} else {
|
||||
terminalLines.value.push({ type: 'output', content: commands[cmd] || `命令未找到: ${cmd}` })
|
||||
}
|
||||
|
||||
currentInput.value = ''
|
||||
nextTick(() => {
|
||||
terminalRef.value?.scrollTo(0, terminalRef.value.scrollHeight)
|
||||
})
|
||||
}
|
||||
|
||||
const handleDisconnect = () => {
|
||||
isConnected.value = false
|
||||
terminalLines.value = [
|
||||
{ type: 'output', content: `Welcome to ${props.record?.name || 'Server'}!` },
|
||||
{ type: 'output', content: `Last login: ${new Date().toLocaleString()}` },
|
||||
]
|
||||
Message.info('已断开连接')
|
||||
}
|
||||
|
||||
const handleFullscreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
terminalRef.value?.requestFullscreen()
|
||||
} else {
|
||||
document.exitFullscreen()
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.record?.ip) {
|
||||
loginForm.value.host = props.record.ip
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.remote-control {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f0f2f5;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 24px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e5e6eb;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-box {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
|
||||
.login-card {
|
||||
width: 480px;
|
||||
}
|
||||
}
|
||||
|
||||
.terminal-box {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 16px;
|
||||
background: #1e1e1e;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.terminal-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
background: #2d2d2d;
|
||||
border-bottom: 1px solid #3a3a3a;
|
||||
|
||||
.terminal-info {
|
||||
color: #c9cdd4;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.terminal-content {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
|
||||
.terminal-line {
|
||||
margin-bottom: 4px;
|
||||
color: #fff;
|
||||
|
||||
.prompt {
|
||||
color: #00ff00;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&.input {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.output {
|
||||
color: #c9cdd4;
|
||||
}
|
||||
}
|
||||
|
||||
.terminal-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.prompt {
|
||||
color: #00ff00;
|
||||
margin-right: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.command-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: #fff;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
caret-color: #00ff00;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
714
src/views/ops/pages/dc/pc/components/ServerDetail.vue
Normal file
714
src/views/ops/pages/dc/pc/components/ServerDetail.vue
Normal file
@@ -0,0 +1,714 @@
|
||||
<template>
|
||||
<div class="server-detail">
|
||||
<div class="detail-header">
|
||||
<div class="server-info">
|
||||
<h2>{{ record.name || '服务器详情' }}</h2>
|
||||
<div class="info-tags">
|
||||
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
|
||||
<a-tag color="blue">{{ record.server_type || '未知类型' }}</a-tag>
|
||||
<a-tag color="cyan">{{ record.os || '未知系统' }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a-button @click="handleRemoteControl">
|
||||
<template #icon>
|
||||
<icon-desktop />
|
||||
</template>
|
||||
远程登录
|
||||
</a-button>
|
||||
<a-button type="primary" @click="handleRestart">
|
||||
<template #icon>
|
||||
<icon-refresh />
|
||||
</template>
|
||||
重启
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-tabs v-model:active-tab="activeTab" class="detail-tabs">
|
||||
<a-tab-pane key="overview" title="实例详情">
|
||||
<a-descriptions :column="2" bordered class="info-descriptions">
|
||||
<a-descriptions-item label="唯一标识">{{ record.unique_id || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="服务器名称">{{ record.name || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="服务器类型">{{ getServerTypeText(record.server_type) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="操作系统">{{ record.os || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="位置信息">{{ record.location || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="标签">{{ record.tags || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="IP地址">{{ record.ip || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="远程端口">{{ record.remote_port || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="Agent URL">{{ record.agent_url || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="数据采集">
|
||||
<a-tag :color="record.data_collection ? 'green' : 'gray'">
|
||||
{{ record.data_collection ? '已开启' : '未开启' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采集间隔">{{ record.collection_interval ? `${record.collection_interval}分钟` : '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="备注信息" :span="2">{{ record.remark || '-' }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="monitor" title="监控">
|
||||
<div class="monitor-section">
|
||||
<div class="time-selector">
|
||||
<a-radio-group v-model="timeRange" type="button">
|
||||
<a-radio value="1h">1小时</a-radio>
|
||||
<a-radio value="3h">3小时</a-radio>
|
||||
<a-radio value="6h">6小时</a-radio>
|
||||
<a-radio value="12h">12小时</a-radio>
|
||||
<a-radio value="1d">1天</a-radio>
|
||||
<a-radio value="3d">3天</a-radio>
|
||||
<a-radio value="7d">7天</a-radio>
|
||||
<a-radio value="14d">14天</a-radio>
|
||||
<a-radio value="custom">自定义</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="20" class="charts-row">
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">CPU使用率_宿主机视角 (%)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="cpuMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
<a-option value="min">最小值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="cpuOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot green"></span>(ECS)CPU使用率_平均值</span>
|
||||
<span class="legend-item"><span class="legend-dot blue"></span>(ECS)CPU使用率_最小值</span>
|
||||
<span class="legend-item"><span class="legend-dot purple"></span>(ECS)CPU使用率_最大值</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">总带宽 (bit/s)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="bandwidthMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="bandwidthOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot green"></span>内网流入带宽</span>
|
||||
<span class="legend-item"><span class="legend-dot blue"></span>内网流出带宽</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">公网带宽 (bit/s)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="publicBandwidthMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="publicBandwidthOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot blue"></span>公网流入带宽</span>
|
||||
<span class="legend-item"><span class="legend-dot green"></span>公网流出带宽</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="20" class="charts-row">
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">公网流出带宽使用率 (%)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="bandwidthUsageMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="bandwidthUsageOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot blue"></span>(ECS)公网流出带宽使用率</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">磁盘读取写入BPS (bytes/s)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="diskMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="diskBpsOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot blue"></span>(ECS)所有磁盘读取BPS</span>
|
||||
<span class="legend-item"><span class="legend-dot green"></span>(ECS)所有磁盘写入BPS</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">磁盘读取写入IOPS (Count/Second)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="diskIopsMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="diskIopsOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot blue"></span>(ECS)所有磁盘每秒读取次数</span>
|
||||
<span class="legend-item"><span class="legend-dot green"></span>(ECS)所有磁盘每秒写入次数</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="20" class="charts-row">
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">同时连接数 (Count)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="connectionMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="connectionOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot blue"></span>同时连接数</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">RDMA流量使用信息 (bit/s)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="rdmaMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="rdmaOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"></span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">RDMA资源使用信息 (Count)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="rdmaResourceMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="rdmaResourceOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"></span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="operation" title="操作记录">
|
||||
<div class="operation-records">
|
||||
<a-empty description="暂无操作记录" />
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { IconDesktop, IconRefresh, IconSettings } from '@arco-design/web-vue/es/icon'
|
||||
import VChart from 'vue-echarts'
|
||||
import { use } from 'echarts/core'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { LineChart } from 'echarts/charts'
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
GridComponent,
|
||||
DataZoomComponent,
|
||||
} from 'echarts/components'
|
||||
|
||||
use([
|
||||
CanvasRenderer,
|
||||
LineChart,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
GridComponent,
|
||||
DataZoomComponent,
|
||||
])
|
||||
|
||||
interface Props {
|
||||
record: any
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits(['remote-control', 'restart', 'close'])
|
||||
|
||||
const activeTab = ref('overview')
|
||||
const timeRange = ref('1h')
|
||||
const cpuMetric = ref('average')
|
||||
const bandwidthMetric = ref('average')
|
||||
const publicBandwidthMetric = ref('average')
|
||||
const bandwidthUsageMetric = ref('average')
|
||||
const diskMetric = ref('average')
|
||||
const diskIopsMetric = ref('average')
|
||||
const connectionMetric = ref('average')
|
||||
const rdmaMetric = ref('average')
|
||||
const rdmaResourceMetric = ref('average')
|
||||
|
||||
const generateTimeData = (count: number) => {
|
||||
const now = Date.now()
|
||||
const data = []
|
||||
for (let i = count - 1; i >= 0; i--) {
|
||||
data.push(new Date(now - i * 60000).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }))
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
const generateRandomData = (count: number, min: number, max: number) => {
|
||||
return Array.from({ length: count }, () => Math.floor(Math.random() * (max - min + 1)) + min)
|
||||
}
|
||||
|
||||
const timeData = generateTimeData(60)
|
||||
|
||||
const baseOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||
borderColor: '#e5e6eb',
|
||||
textStyle: {
|
||||
color: '#1d2129',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: timeData,
|
||||
axisLabel: {
|
||||
fontSize: 10,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
fontSize: 10,
|
||||
},
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const cpuOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [
|
||||
{
|
||||
name: '平均值',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 20, 80),
|
||||
lineStyle: { color: '#00b42a' },
|
||||
itemStyle: { color: '#00b42a' },
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(0, 180, 42, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(0, 180, 42, 0.05)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '最小值',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 10, 40),
|
||||
lineStyle: { color: '#165dff' },
|
||||
itemStyle: { color: '#165dff' },
|
||||
},
|
||||
{
|
||||
name: '最大值',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 50, 95),
|
||||
lineStyle: { color: '#722ed1' },
|
||||
itemStyle: { color: '#722ed1' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const bandwidthOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [
|
||||
{
|
||||
name: '内网流入带宽',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 100000, 500000),
|
||||
lineStyle: { color: '#00b42a' },
|
||||
itemStyle: { color: '#00b42a' },
|
||||
},
|
||||
{
|
||||
name: '内网流出带宽',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 50000, 300000),
|
||||
lineStyle: { color: '#165dff' },
|
||||
itemStyle: { color: '#165dff' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const publicBandwidthOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [
|
||||
{
|
||||
name: '公网流入带宽',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 50000, 200000),
|
||||
lineStyle: { color: '#165dff' },
|
||||
itemStyle: { color: '#165dff' },
|
||||
},
|
||||
{
|
||||
name: '公网流出带宽',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 30000, 150000),
|
||||
lineStyle: { color: '#00b42a' },
|
||||
itemStyle: { color: '#00b42a' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const bandwidthUsageOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [
|
||||
{
|
||||
name: '公网流出带宽使用率',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 5, 30),
|
||||
lineStyle: { color: '#165dff' },
|
||||
itemStyle: { color: '#165dff' },
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(22, 93, 255, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(22, 93, 255, 0.05)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const diskBpsOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [
|
||||
{
|
||||
name: '磁盘读取BPS',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 10000, 100000),
|
||||
lineStyle: { color: '#165dff' },
|
||||
itemStyle: { color: '#165dff' },
|
||||
},
|
||||
{
|
||||
name: '磁盘写入BPS',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 20000, 150000),
|
||||
lineStyle: { color: '#00b42a' },
|
||||
itemStyle: { color: '#00b42a' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const diskIopsOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [
|
||||
{
|
||||
name: '磁盘读取IOPS',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 50, 500),
|
||||
lineStyle: { color: '#165dff' },
|
||||
itemStyle: { color: '#165dff' },
|
||||
},
|
||||
{
|
||||
name: '磁盘写入IOPS',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 100, 800),
|
||||
lineStyle: { color: '#00b42a' },
|
||||
itemStyle: { color: '#00b42a' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const connectionOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [
|
||||
{
|
||||
name: '同时连接数',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 100, 500),
|
||||
lineStyle: { color: '#165dff' },
|
||||
itemStyle: { color: '#165dff' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const rdmaOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [],
|
||||
}))
|
||||
|
||||
const rdmaResourceOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [],
|
||||
}))
|
||||
|
||||
const getStatusColor = (status?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
online: 'green',
|
||||
offline: 'red',
|
||||
maintenance: 'orange',
|
||||
retired: 'gray',
|
||||
}
|
||||
return colorMap[status || ''] || 'gray'
|
||||
}
|
||||
|
||||
const getStatusText = (status?: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
online: '在线',
|
||||
offline: '离线',
|
||||
maintenance: '维护中',
|
||||
retired: '已退役',
|
||||
}
|
||||
return textMap[status || ''] || '-'
|
||||
}
|
||||
|
||||
const getServerTypeText = (type?: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
physical: '物理服务器',
|
||||
virtual: '虚拟服务器',
|
||||
cloud: '云服务器',
|
||||
}
|
||||
return typeMap[type || ''] || '-'
|
||||
}
|
||||
|
||||
const handleRemoteControl = () => {
|
||||
emit('remote-control')
|
||||
}
|
||||
|
||||
const handleRestart = () => {
|
||||
Message.info('正在发送重启指令...')
|
||||
emit('restart')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.server-detail {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #e5e6eb;
|
||||
|
||||
.server-info {
|
||||
h2 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.info-tags {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
:deep(.arco-tabs-header) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-descriptions {
|
||||
:deep(.arco-descriptions-item-label) {
|
||||
width: 140px;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
}
|
||||
|
||||
.monitor-section {
|
||||
.time-selector {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.charts-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-col {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e6eb;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.chart-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.chart-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #e5e6eb;
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
|
||||
.legend-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
|
||||
&.green {
|
||||
background: #00b42a;
|
||||
}
|
||||
|
||||
&.blue {
|
||||
background: #165dff;
|
||||
}
|
||||
|
||||
&.purple {
|
||||
background: #722ed1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.operation-records {
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
235
src/views/ops/pages/dc/pc/components/ServerFormDialog.vue
Normal file
235
src/views/ops/pages/dc/pc/components/ServerFormDialog.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="isEdit ? '编辑服务器/PC' : '新增服务器/PC'"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleUpdateVisible"
|
||||
:confirm-loading="confirmLoading"
|
||||
width="800px"
|
||||
>
|
||||
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical">
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="unique_id" label="唯一标识">
|
||||
<a-input
|
||||
v-model="formData.unique_id"
|
||||
placeholder="输入为空系统自动生成UUID"
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="name" label="服务器名称">
|
||||
<a-input v-model="formData.name" placeholder="请输入服务器名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="server_type" label="服务器类型">
|
||||
<a-select v-model="formData.server_type" placeholder="请选择服务器类型">
|
||||
<a-option value="physical">物理服务器</a-option>
|
||||
<a-option value="virtual">虚拟服务器</a-option>
|
||||
<a-option value="cloud">云服务器</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="os" label="操作系统">
|
||||
<a-select v-model="formData.os" placeholder="请选择操作系统">
|
||||
<a-option value="windows">Windows</a-option>
|
||||
<a-option value="linux">Linux</a-option>
|
||||
<a-option value="other">其它</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item field="location" label="位置/机房信息">
|
||||
<a-input-group>
|
||||
<a-input
|
||||
v-model="formData.location"
|
||||
placeholder="请输入或选择位置信息"
|
||||
/>
|
||||
<a-select
|
||||
v-model="selectedLocation"
|
||||
placeholder="选择位置"
|
||||
style="width: 200px"
|
||||
@change="handleLocationSelect"
|
||||
>
|
||||
<a-option
|
||||
v-for="location in locationOptions"
|
||||
:key="location.value"
|
||||
:value="location.value"
|
||||
>
|
||||
{{ location.label }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="tags" label="服务器标签">
|
||||
<a-input v-model="formData.tags" placeholder="多个标签,逗号间隔" />
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="ip" label="IP地址">
|
||||
<a-input v-model="formData.ip" placeholder="可以输入多个IP,逗号做分隔" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="remote_port" label="远程访问端口">
|
||||
<a-input v-model="formData.remote_port" placeholder="为空则不可远程访问" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item field="agent_url" label="Agent URL配置">
|
||||
<a-input v-model="formData.agent_url" placeholder="请输入Agent URL配置" />
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="data_collection" label="数据采集">
|
||||
<a-switch v-model="formData.data_collection" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item v-if="formData.data_collection" field="collection_interval" label="采集时间">
|
||||
<a-select v-model="formData.collection_interval" placeholder="请选择采集时间">
|
||||
<a-option :value="1">1分钟</a-option>
|
||||
<a-option :value="5">5分钟</a-option>
|
||||
<a-option :value="10">10分钟</a-option>
|
||||
<a-option :value="30">30分钟</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item field="remark" label="备注信息">
|
||||
<a-textarea
|
||||
v-model="formData.remark"
|
||||
placeholder="请输入备注信息"
|
||||
:rows="4"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import type { FormInstance } from '@arco-design/web-vue'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
record?: any
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
record: () => ({}),
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const confirmLoading = ref(false)
|
||||
const selectedLocation = ref('')
|
||||
|
||||
const isEdit = computed(() => !!props.record?.id)
|
||||
|
||||
const formData = reactive({
|
||||
unique_id: '',
|
||||
name: '',
|
||||
server_type: '',
|
||||
os: '',
|
||||
location: '',
|
||||
tags: '',
|
||||
ip: '',
|
||||
remote_port: '',
|
||||
agent_url: '',
|
||||
data_collection: false,
|
||||
collection_interval: 5,
|
||||
remark: '',
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入服务器名称' }],
|
||||
server_type: [{ required: true, message: '请选择服务器类型' }],
|
||||
os: [{ required: true, message: '请选择操作系统' }],
|
||||
}
|
||||
|
||||
const locationOptions = ref([
|
||||
{ label: 'A数据中心-3层-24机柜-5U位', value: 'A数据中心-3层-24机柜-5U位' },
|
||||
{ label: 'A数据中心-3层-24机柜-6U位', value: 'A数据中心-3层-24机柜-6U位' },
|
||||
{ label: 'B数据中心-1层-12机柜-1U位', value: 'B数据中心-1层-12机柜-1U位' },
|
||||
{ label: 'B数据中心-1层-12机柜-2U位', value: 'B数据中心-1层-12机柜-2U位' },
|
||||
{ label: 'C数据中心-2层-8机柜-3U位', value: 'C数据中心-2层-8机柜-3U位' },
|
||||
])
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
if (isEdit.value && props.record) {
|
||||
Object.assign(formData, props.record)
|
||||
} else {
|
||||
Object.assign(formData, {
|
||||
unique_id: '',
|
||||
name: '',
|
||||
server_type: '',
|
||||
os: '',
|
||||
location: '',
|
||||
tags: '',
|
||||
ip: '',
|
||||
remote_port: '',
|
||||
agent_url: '',
|
||||
data_collection: false,
|
||||
collection_interval: 5,
|
||||
remark: '',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const handleLocationSelect = (value: string) => {
|
||||
formData.location = value
|
||||
}
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
|
||||
if (!formData.unique_id) {
|
||||
formData.unique_id = uuidv4()
|
||||
}
|
||||
|
||||
confirmLoading.value = true
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
Message.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
emit('success')
|
||||
handleCancel()
|
||||
} catch (error) {
|
||||
console.error('验证失败:', error)
|
||||
} finally {
|
||||
confirmLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateVisible = (value: boolean) => {
|
||||
emit('update:visible', value)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
81
src/views/ops/pages/dc/pc/config/columns.ts
Normal file
81
src/views/ops/pages/dc/pc/config/columns.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
export const columns = [
|
||||
{
|
||||
dataIndex: 'id',
|
||||
title: 'ID',
|
||||
width: 80,
|
||||
slotName: 'id',
|
||||
},
|
||||
{
|
||||
dataIndex: 'unique_id',
|
||||
title: '唯一标识',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
dataIndex: 'name',
|
||||
title: '名称',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
dataIndex: 'os',
|
||||
title: '操作系统',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
dataIndex: 'location',
|
||||
title: '位置信息',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
dataIndex: 'tags',
|
||||
title: '标签',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
dataIndex: 'ip',
|
||||
title: 'IP地址',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
dataIndex: 'remote_access',
|
||||
title: '远程访问',
|
||||
width: 100,
|
||||
slotName: 'remote_access',
|
||||
},
|
||||
{
|
||||
dataIndex: 'agent_config',
|
||||
title: 'Agent配置',
|
||||
width: 150,
|
||||
slotName: 'agent_config',
|
||||
},
|
||||
{
|
||||
dataIndex: 'cpu',
|
||||
title: 'CPU使用率',
|
||||
width: 150,
|
||||
slotName: 'cpu',
|
||||
},
|
||||
{
|
||||
dataIndex: 'memory',
|
||||
title: '内存使用率',
|
||||
width: 150,
|
||||
slotName: 'memory',
|
||||
},
|
||||
{
|
||||
dataIndex: 'disk',
|
||||
title: '硬盘使用率',
|
||||
width: 150,
|
||||
slotName: 'disk',
|
||||
},
|
||||
{
|
||||
dataIndex: 'status',
|
||||
title: '状态',
|
||||
width: 100,
|
||||
slotName: 'status',
|
||||
},
|
||||
{
|
||||
dataIndex: 'actions',
|
||||
title: '操作',
|
||||
width: 100,
|
||||
fixed: 'right' as const,
|
||||
slotName: 'actions',
|
||||
},
|
||||
]
|
||||
40
src/views/ops/pages/dc/pc/config/search-form.ts
Normal file
40
src/views/ops/pages/dc/pc/config/search-form.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
|
||||
export const searchFormConfig: FormItem[] = [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: '关键词',
|
||||
type: 'input',
|
||||
placeholder: '请输入PC名称、编码或IP',
|
||||
span: 6,
|
||||
},
|
||||
{
|
||||
field: 'datacenter_id',
|
||||
label: '数据中心',
|
||||
type: 'select',
|
||||
placeholder: '请选择数据中心',
|
||||
options: [], // 需要动态加载
|
||||
span: 6,
|
||||
},
|
||||
{
|
||||
field: 'rack_id',
|
||||
label: '机柜',
|
||||
type: 'select',
|
||||
placeholder: '请选择机柜',
|
||||
options: [], // 需要动态加载
|
||||
span: 6,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '状态',
|
||||
type: 'select',
|
||||
placeholder: '请选择状态',
|
||||
options: [
|
||||
{ label: '在线', value: 'online' },
|
||||
{ label: '离线', value: 'offline' },
|
||||
{ label: '维护中', value: 'maintenance' },
|
||||
{ label: '已退役', value: 'retired' },
|
||||
],
|
||||
span: 6,
|
||||
},
|
||||
]
|
||||
562
src/views/ops/pages/dc/pc/index.vue
Normal file
562
src/views/ops/pages/dc/pc/index.vue
Normal file
@@ -0,0 +1,562 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="服务器及PC管理"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
新增
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- ID -->
|
||||
<template #id="{ record }">
|
||||
{{ record.id }}
|
||||
</template>
|
||||
|
||||
<!-- 远程访问 -->
|
||||
<template #remote_access="{ record }">
|
||||
<a-tag :color="record.remote_access ? 'green' : 'gray'">
|
||||
{{ record.remote_access ? '已开启' : '未开启' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- Agent配置 -->
|
||||
<template #agent_config="{ record }">
|
||||
<a-tag :color="record.agent_config ? 'green' : 'gray'">
|
||||
{{ record.agent_config ? '已配置' : '未配置' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- CPU -->
|
||||
<template #cpu="{ record }">
|
||||
<div class="resource-display">
|
||||
<div class="resource-info">
|
||||
<span class="resource-label">CPU</span>
|
||||
<span class="resource-value">{{ record.cpu_info?.value || 0 }}%</span>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="(record.cpu_info?.value || 0) / 100"
|
||||
:color="getProgressColor(record.cpu_info?.value || 0)"
|
||||
size="small"
|
||||
:show-text="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 内存 -->
|
||||
<template #memory="{ record }">
|
||||
<div class="resource-display">
|
||||
<div class="resource-info">
|
||||
<span class="resource-label">内存</span>
|
||||
<span class="resource-value">{{ record.memory_info?.value || 0 }}%</span>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="(record.memory_info?.value || 0) / 100"
|
||||
:color="getProgressColor(record.memory_info?.value || 0)"
|
||||
size="small"
|
||||
:show-text="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 硬盘 -->
|
||||
<template #disk="{ record }">
|
||||
<div class="resource-display">
|
||||
<div class="resource-info">
|
||||
<span class="resource-label">硬盘</span>
|
||||
<span class="resource-value">{{ record.disk_info?.value || 0 }}%</span>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="(record.disk_info?.value || 0) / 100"
|
||||
:color="getProgressColor(record.disk_info?.value || 0)"
|
||||
size="small"
|
||||
:show-text="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 状态 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 操作栏 - 下拉菜单 -->
|
||||
<template #actions="{ record }">
|
||||
<a-dropdown trigger="hover">
|
||||
<a-button type="primary" size="small">
|
||||
管理
|
||||
<icon-down />
|
||||
</a-button>
|
||||
<template #content>
|
||||
<a-doption @click="handleRestart(record)">
|
||||
<template #icon>
|
||||
<icon-refresh />
|
||||
</template>
|
||||
重启
|
||||
</a-doption>
|
||||
<a-doption @click="handleDetail(record)">
|
||||
<template #icon>
|
||||
<icon-eye />
|
||||
</template>
|
||||
详情
|
||||
</a-doption>
|
||||
<a-doption @click="handleEdit(record)">
|
||||
<template #icon>
|
||||
<icon-edit />
|
||||
</template>
|
||||
编辑
|
||||
</a-doption>
|
||||
<a-doption @click="handleRemoteControl(record)">
|
||||
<template #icon>
|
||||
<icon-desktop />
|
||||
</template>
|
||||
远程控制
|
||||
</a-doption>
|
||||
<a-doption @click="handleDelete(record)" style="color: rgb(var(--danger-6))">
|
||||
<template #icon>
|
||||
<icon-delete />
|
||||
</template>
|
||||
删除
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ServerFormDialog
|
||||
v-model:visible="formDialogVisible"
|
||||
:record="currentRecord"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import {
|
||||
IconPlus,
|
||||
IconDown,
|
||||
IconEdit,
|
||||
IconDesktop,
|
||||
IconDelete,
|
||||
IconRefresh,
|
||||
IconEye
|
||||
} 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 {
|
||||
fetchPCList,
|
||||
deletePC,
|
||||
} from '@/api/ops/pc'
|
||||
import ServerFormDialog from './components/ServerFormDialog.vue'
|
||||
import ServerDetail from './components/ServerDetail.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// Mock 假数据
|
||||
const mockPCData = [
|
||||
{
|
||||
id: 1,
|
||||
unique_id: 'PC-2024-0001',
|
||||
name: '开发PC-01',
|
||||
os: 'Windows 11',
|
||||
location: '数据中心A-1楼-办公区01',
|
||||
tags: '开发,前端',
|
||||
ip: '192.168.1.201',
|
||||
remote_access: true,
|
||||
agent_config: true,
|
||||
cpu_info: { value: 35, total: '8核', used: '2.8核' },
|
||||
memory_info: { value: 52, total: '16GB', used: '8.3GB' },
|
||||
disk_info: { value: 65, total: '512GB', used: '333GB' },
|
||||
status: 'online',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
unique_id: 'PC-2024-0002',
|
||||
name: '测试PC-01',
|
||||
os: 'Windows 10',
|
||||
location: '数据中心A-1楼-办公区02',
|
||||
tags: '测试,自动化',
|
||||
ip: '192.168.1.202',
|
||||
remote_access: true,
|
||||
agent_config: true,
|
||||
cpu_info: { value: 28, total: '4核', used: '1.1核' },
|
||||
memory_info: { value: 45, total: '8GB', used: '3.6GB' },
|
||||
disk_info: { value: 72, total: '256GB', used: '184GB' },
|
||||
status: 'online',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
unique_id: 'PC-2024-0003',
|
||||
name: '设计PC-01',
|
||||
os: 'macOS Sonoma',
|
||||
location: '数据中心A-2楼-设计室01',
|
||||
tags: '设计,创意',
|
||||
ip: '192.168.1.203',
|
||||
remote_access: false,
|
||||
agent_config: false,
|
||||
cpu_info: { value: 0, total: '12核', used: '0核' },
|
||||
memory_info: { value: 0, total: '32GB', used: '0GB' },
|
||||
disk_info: { value: 0, total: '1TB', used: '0GB' },
|
||||
status: 'offline',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
unique_id: 'PC-2024-0004',
|
||||
name: '运维PC-01',
|
||||
os: 'Windows 11',
|
||||
location: '数据中心B-1楼-运维室01',
|
||||
tags: '运维,监控',
|
||||
ip: '192.168.2.201',
|
||||
remote_access: true,
|
||||
agent_config: true,
|
||||
cpu_info: { value: 42, total: '6核', used: '2.5核' },
|
||||
memory_info: { value: 58, total: '16GB', used: '9.3GB' },
|
||||
disk_info: { value: 68, total: '512GB', used: '348GB' },
|
||||
status: 'online',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
unique_id: 'PC-2024-0005',
|
||||
name: '财务PC-01',
|
||||
os: 'Windows 10',
|
||||
location: '数据中心B-2楼-财务室01',
|
||||
tags: '财务,报表',
|
||||
ip: '192.168.2.202',
|
||||
remote_access: true,
|
||||
agent_config: true,
|
||||
cpu_info: { value: 22, total: '4核', used: '0.9核' },
|
||||
memory_info: { value: 38, total: '8GB', used: '3.0GB' },
|
||||
disk_info: { value: 55, total: '256GB', used: '141GB' },
|
||||
status: 'online',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
unique_id: 'PC-2024-0006',
|
||||
name: '备用PC-01',
|
||||
os: 'Windows 11',
|
||||
location: '数据中心A-1楼-备用室01',
|
||||
tags: '备用,测试',
|
||||
ip: '192.168.1.204',
|
||||
remote_access: false,
|
||||
agent_config: false,
|
||||
cpu_info: { value: 0, total: '4核', used: '0核' },
|
||||
memory_info: { value: 0, total: '8GB', used: '0GB' },
|
||||
disk_info: { value: 0, total: '256GB', used: '0GB' },
|
||||
status: 'maintenance',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
unique_id: 'PC-2024-0007',
|
||||
name: '开发PC-02',
|
||||
os: 'Ubuntu 22.04',
|
||||
location: '数据中心A-1楼-办公区03',
|
||||
tags: '开发,后端',
|
||||
ip: '192.168.1.205',
|
||||
remote_access: true,
|
||||
agent_config: true,
|
||||
cpu_info: { value: 48, total: '8核', used: '3.8核' },
|
||||
memory_info: { value: 62, total: '16GB', used: '9.9GB' },
|
||||
disk_info: { value: 58, total: '512GB', used: '297GB' },
|
||||
status: 'online',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
unique_id: 'PC-2024-0008',
|
||||
name: '产品PC-01',
|
||||
os: 'Windows 11',
|
||||
location: '数据中心B-1楼-产品室01',
|
||||
tags: '产品,设计',
|
||||
ip: '192.168.2.203',
|
||||
remote_access: true,
|
||||
agent_config: true,
|
||||
cpu_info: { value: 32, total: '6核', used: '1.9核' },
|
||||
memory_info: { value: 45, total: '16GB', used: '7.2GB' },
|
||||
disk_info: { value: 62, total: '512GB', used: '317GB' },
|
||||
status: 'online',
|
||||
},
|
||||
]
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const formModel = ref({
|
||||
keyword: '',
|
||||
datacenter_id: undefined,
|
||||
rack_id: undefined,
|
||||
status: undefined,
|
||||
})
|
||||
|
||||
const formDialogVisible = ref(false)
|
||||
const currentRecord = ref<any>(null)
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 表单项配置
|
||||
const formItems = computed<FormItem[]>(() => searchFormConfig)
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed(() => columnsConfig)
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
online: 'green',
|
||||
offline: 'red',
|
||||
maintenance: 'orange',
|
||||
retired: 'gray',
|
||||
}
|
||||
return colorMap[status || ''] || 'gray'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status?: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
online: '在线',
|
||||
offline: '离线',
|
||||
maintenance: '维护中',
|
||||
retired: '已退役',
|
||||
}
|
||||
return textMap[status || ''] || '-'
|
||||
}
|
||||
|
||||
// 获取进度条颜色
|
||||
const getProgressColor = (value: number) => {
|
||||
if (value >= 90) return '#F53F3F' // 红色
|
||||
if (value >= 70) return '#FF7D00' // 橙色
|
||||
if (value >= 50) return '#FFD00B' // 黄色
|
||||
return '#00B42A' // 绿色
|
||||
}
|
||||
|
||||
// 获取PC列表(使用 Mock 数据)
|
||||
const fetchPCs = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
// 使用 Mock 数据
|
||||
tableData.value = mockPCData
|
||||
pagination.total = mockPCData.length
|
||||
|
||||
// 如果有搜索条件,进行过滤
|
||||
if (formModel.value.keyword || formModel.value.status) {
|
||||
let filteredData = [...mockPCData]
|
||||
|
||||
if (formModel.value.keyword) {
|
||||
const keyword = formModel.value.keyword.toLowerCase()
|
||||
filteredData = filteredData.filter(item =>
|
||||
item.name.toLowerCase().includes(keyword) ||
|
||||
item.unique_id.toLowerCase().includes(keyword) ||
|
||||
item.ip.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
|
||||
if (formModel.value.status) {
|
||||
filteredData = filteredData.filter(item => item.status === formModel.value.status)
|
||||
}
|
||||
|
||||
tableData.value = filteredData
|
||||
pagination.total = filteredData.length
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取PC列表失败:', error)
|
||||
Message.error('获取PC列表失败')
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchPCs()
|
||||
}
|
||||
|
||||
// 处理表单模型更新
|
||||
const handleFormModelUpdate = (value: any) => {
|
||||
formModel.value = value
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
keyword: '',
|
||||
datacenter_id: undefined,
|
||||
rack_id: undefined,
|
||||
status: undefined,
|
||||
}
|
||||
pagination.current = 1
|
||||
fetchPCs()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
fetchPCs()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchPCs()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 新增PC
|
||||
const handleAdd = () => {
|
||||
currentRecord.value = null
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑PC
|
||||
const handleEdit = (record: any) => {
|
||||
currentRecord.value = record
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 详情 - 跳转到独立页面
|
||||
const handleDetail = (record: any) => {
|
||||
router.push({
|
||||
path: '/dc/detail',
|
||||
query: {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
ip: record.ip,
|
||||
status: record.status,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 重启
|
||||
const handleRestart = (record: any) => {
|
||||
Modal.confirm({
|
||||
title: '确认重启',
|
||||
content: `确认重启服务器/PC ${record.name} 吗?`,
|
||||
onOk: () => {
|
||||
Message.info('正在发送重启指令...')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 远程控制 - 跳转到独立页面
|
||||
const handleRemoteControl = (record: any) => {
|
||||
router.push({
|
||||
path: '/dc/remote',
|
||||
query: {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
ip: record.ip,
|
||||
status: record.status,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 表单提交成功
|
||||
const handleFormSuccess = () => {
|
||||
fetchPCs()
|
||||
}
|
||||
|
||||
// 删除PC
|
||||
const handleDelete = async (record: any) => {
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确认删除PC ${record.name} 吗?`,
|
||||
onOk: async () => {
|
||||
// Mock 删除操作
|
||||
const index = mockPCData.findIndex(item => item.id === record.id)
|
||||
if (index > -1) {
|
||||
mockPCData.splice(index, 1)
|
||||
Message.success('删除成功')
|
||||
fetchPCs()
|
||||
} else {
|
||||
Message.error('删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('删除PC失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
fetchPCs()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'DataCenterPC',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.resource-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 4px 0;
|
||||
|
||||
.resource-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
> div {
|
||||
display: inline-block;
|
||||
}
|
||||
.resource-value {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: rgb(var(--text-1));
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-progress) {
|
||||
margin: 0;
|
||||
|
||||
.arco-progress-bar-bg {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.arco-progress-bar {
|
||||
border-radius: 2px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
481
src/views/ops/pages/dc/remote/index.vue
Normal file
481
src/views/ops/pages/dc/remote/index.vue
Normal file
@@ -0,0 +1,481 @@
|
||||
<template>
|
||||
<div class="remote-control-page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<a-button type="text" @click="goBack">
|
||||
<template #icon><icon-left /></template>
|
||||
返回
|
||||
</a-button>
|
||||
<a-divider direction="vertical" />
|
||||
<span class="server-name">{{ serverName }}</span>
|
||||
<a-tag :color="getStatusColor(serverStatus)" size="small">{{ getStatusText(serverStatus) }}</a-tag>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<a-button v-if="isConnected" size="small" @click="handleDisconnect">
|
||||
<template #icon><icon-poweroff /></template>
|
||||
断开连接
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<div class="page-body">
|
||||
<!-- 左侧连接信息 -->
|
||||
<div v-if="!isConnected" class="connection-panel">
|
||||
<a-card title="SSH 远程连接" class="connection-card">
|
||||
<a-form :model="connectionForm" layout="vertical">
|
||||
<a-form-item label="连接协议">
|
||||
<a-radio-group v-model="connectionForm.protocol" type="button">
|
||||
<a-radio value="ssh">SSH</a-radio>
|
||||
<a-radio value="tat">免密连接</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="16">
|
||||
<a-form-item label="主机地址">
|
||||
<a-input v-model="connectionForm.host" placeholder="IP或域名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="端口">
|
||||
<a-input-number v-model="connectionForm.port" :min="1" :max="65535" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="用户名">
|
||||
<a-input v-model="connectionForm.username" placeholder="请输入用户名" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="密码">
|
||||
<a-input-password v-model="connectionForm.password" placeholder="请输入密码" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" long @click="handleConnect" :loading="connectLoading">
|
||||
<template #icon><icon-play-circle /></template>
|
||||
开始连接
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 连接历史 -->
|
||||
<a-card title="连接历史" class="history-card">
|
||||
<a-list :bordered="false">
|
||||
<a-list-item v-for="(item, index) in connectionHistory" :key="index" @click="loadConnection(item)">
|
||||
<a-list-item-meta :title="item.name" :description="`${item.username}@${item.host}:${item.port}`" />
|
||||
<template #actions>
|
||||
<a-button type="text" size="small" @click.stop="removeHistory(index)">
|
||||
<icon-delete />
|
||||
</a-button>
|
||||
</template>
|
||||
</a-list-item>
|
||||
<a-empty v-if="connectionHistory.length === 0" description="暂无连接记录" />
|
||||
</a-list>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 右侧终端 -->
|
||||
<div class="terminal-panel" :class="{ 'full-width': isConnected }">
|
||||
<div class="terminal-header">
|
||||
<div class="terminal-info">
|
||||
<icon-desktop v-if="isConnected" style="color: #00b42a; margin-right: 8px;" />
|
||||
<icon-desktop v-else style="color: #86909c; margin-right: 8px;" />
|
||||
<span>{{ isConnected ? `已连接: ${connectionForm.username}@${connectionForm.host}` : '未连接' }}</span>
|
||||
</div>
|
||||
<a-space v-if="isConnected">
|
||||
<a-button size="small" @click="clearTerminal">
|
||||
<template #icon><icon-eraser /></template>
|
||||
清屏
|
||||
</a-button>
|
||||
<a-button size="small" @click="toggleFullscreen">
|
||||
<template #icon><icon-fullscreen /></template>
|
||||
{{ isFullscreen ? '退出全屏' : '全屏' }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<div class="terminal-content" v-if="isConnected">
|
||||
<Terminal
|
||||
ref="terminalRef"
|
||||
:name="terminalName"
|
||||
:show-log-data="showLogData"
|
||||
@execCmd="onExecCmd"
|
||||
@onKeydown="onKeydown"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="terminal-placeholder">
|
||||
<a-empty description="请先配置连接信息">
|
||||
<template #image>
|
||||
<icon-desktop style="font-size: 48px; color: #86909c;" />
|
||||
</template>
|
||||
</a-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, nextTick, onMounted, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { Terminal } from 'vue-web-terminal'
|
||||
import {
|
||||
IconLeft,
|
||||
IconPlayCircle,
|
||||
IconDelete,
|
||||
IconDesktop,
|
||||
IconPoweroff,
|
||||
IconEraser,
|
||||
IconFullscreen,
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 服务器信息
|
||||
const serverId = computed(() => route.query.id as string)
|
||||
const serverName = computed(() => route.query.name as string || '远程控制')
|
||||
const serverStatus = computed(() => route.query.status as string || 'offline')
|
||||
|
||||
// 连接状态
|
||||
const isConnected = ref(false)
|
||||
const connectLoading = ref(false)
|
||||
const isFullscreen = ref(false)
|
||||
const terminalRef = ref()
|
||||
|
||||
// 终端配置
|
||||
const terminalName = ref('ssh-terminal')
|
||||
const showLogData = ref(false)
|
||||
|
||||
// 连接表单
|
||||
const connectionForm = ref({
|
||||
protocol: 'ssh',
|
||||
host: (route.query.ip as string) || '',
|
||||
port: 22,
|
||||
username: 'root',
|
||||
password: '',
|
||||
})
|
||||
|
||||
// 连接历史(从 localStorage 读取)
|
||||
const connectionHistory = ref<Array<{name: string, host: string, port: number, username: string}>>([])
|
||||
|
||||
onMounted(() => {
|
||||
const history = localStorage.getItem('ssh_connection_history')
|
||||
if (history) {
|
||||
connectionHistory.value = JSON.parse(history)
|
||||
}
|
||||
})
|
||||
|
||||
const getStatusColor = (status?: string) => {
|
||||
const map: Record<string, string> = { online: 'green', offline: 'red', maintenance: 'orange' }
|
||||
return map[status || ''] || 'gray'
|
||||
}
|
||||
|
||||
const getStatusText = (status?: string) => {
|
||||
const map: Record<string, string> = { online: '在线', offline: '离线', maintenance: '维护中' }
|
||||
return map[status || ''] || '-'
|
||||
}
|
||||
|
||||
const handleConnect = async () => {
|
||||
if (!connectionForm.value.host || !connectionForm.value.username) {
|
||||
Message.warning('请填写完整信息')
|
||||
return
|
||||
}
|
||||
|
||||
connectLoading.value = true
|
||||
|
||||
// 模拟连接
|
||||
await new Promise(r => setTimeout(r, 1500))
|
||||
|
||||
isConnected.value = true
|
||||
connectLoading.value = false
|
||||
|
||||
// 显示欢迎信息
|
||||
if (terminalRef.value) {
|
||||
terminalRef.value.pushMessage({
|
||||
class: 'success',
|
||||
content: `Welcome to ${serverName.value}!`
|
||||
})
|
||||
terminalRef.value.pushMessage({
|
||||
class: 'success',
|
||||
content: `Connected to ${connectionForm.value.host}`
|
||||
})
|
||||
terminalRef.value.pushMessage({
|
||||
class: 'info',
|
||||
content: `Last login: ${new Date().toLocaleString()}`
|
||||
})
|
||||
terminalRef.value.pushMessage({
|
||||
class: 'info',
|
||||
content: ''
|
||||
})
|
||||
terminalRef.value.pushMessage({
|
||||
class: 'info',
|
||||
content: 'Type "help" for available commands.'
|
||||
})
|
||||
}
|
||||
|
||||
// 保存连接历史
|
||||
saveConnectionHistory()
|
||||
|
||||
Message.success('连接成功')
|
||||
}
|
||||
|
||||
const saveConnectionHistory = () => {
|
||||
const newItem = {
|
||||
name: serverName.value,
|
||||
host: connectionForm.value.host,
|
||||
port: connectionForm.value.port,
|
||||
username: connectionForm.value.username,
|
||||
}
|
||||
|
||||
// 去重并添加到开头
|
||||
connectionHistory.value = connectionHistory.value.filter(
|
||||
item => !(item.host === newItem.host && item.username === newItem.username)
|
||||
)
|
||||
connectionHistory.value.unshift(newItem)
|
||||
|
||||
// 最多保存10条
|
||||
if (connectionHistory.value.length > 10) {
|
||||
connectionHistory.value = connectionHistory.value.slice(0, 10)
|
||||
}
|
||||
|
||||
localStorage.setItem('ssh_connection_history', JSON.stringify(connectionHistory.value))
|
||||
}
|
||||
|
||||
const loadConnection = (item: any) => {
|
||||
connectionForm.value.host = item.host
|
||||
connectionForm.value.port = item.port
|
||||
connectionForm.value.username = item.username
|
||||
}
|
||||
|
||||
const removeHistory = (index: number) => {
|
||||
connectionHistory.value.splice(index, 1)
|
||||
localStorage.setItem('ssh_connection_history', JSON.stringify(connectionHistory.value))
|
||||
}
|
||||
|
||||
// 终端命令处理
|
||||
const onExecCmd = (key: string, command: string, success: boolean, terminalName: string) => {
|
||||
const cmd = command.trim()
|
||||
if (!cmd) return
|
||||
|
||||
// 执行命令
|
||||
const commands: Record<string, () => void> = {
|
||||
help: () => {
|
||||
terminalRef.value.pushMessage({
|
||||
class: 'info',
|
||||
content: 'Available commands:'
|
||||
})
|
||||
terminalRef.value.pushMessage({
|
||||
class: 'info',
|
||||
content: ' help - Show this help message'
|
||||
})
|
||||
terminalRef.value.pushMessage({
|
||||
class: 'info',
|
||||
content: ' ls - List directory contents'
|
||||
})
|
||||
terminalRef.value.pushMessage({
|
||||
class: 'info',
|
||||
content: ' pwd - Print working directory'
|
||||
})
|
||||
terminalRef.value.pushMessage({
|
||||
class: 'info',
|
||||
content: ' whoami - Print current user'
|
||||
})
|
||||
terminalRef.value.pushMessage({
|
||||
class: 'info',
|
||||
content: ' date - Print system date'
|
||||
})
|
||||
terminalRef.value.pushMessage({
|
||||
class: 'info',
|
||||
content: ' clear - Clear terminal screen'
|
||||
})
|
||||
terminalRef.value.pushMessage({
|
||||
class: 'info',
|
||||
content: ' exit - Disconnect from server'
|
||||
})
|
||||
},
|
||||
ls: () => {
|
||||
terminalRef.value.pushMessage({
|
||||
class: 'success',
|
||||
content: 'bin boot dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var'
|
||||
})
|
||||
},
|
||||
pwd: () => {
|
||||
terminalRef.value.pushMessage({
|
||||
class: 'success',
|
||||
content: '/root'
|
||||
})
|
||||
},
|
||||
whoami: () => {
|
||||
terminalRef.value.pushMessage({
|
||||
class: 'success',
|
||||
content: connectionForm.value.username
|
||||
})
|
||||
},
|
||||
date: () => {
|
||||
terminalRef.value.pushMessage({
|
||||
class: 'success',
|
||||
content: new Date().toString()
|
||||
})
|
||||
},
|
||||
clear: () => {
|
||||
terminalRef.value.clear()
|
||||
},
|
||||
exit: () => {
|
||||
handleDisconnect()
|
||||
}
|
||||
}
|
||||
|
||||
if (commands[cmd]) {
|
||||
commands[cmd]()
|
||||
} else {
|
||||
terminalRef.value.pushMessage({
|
||||
class: 'error',
|
||||
content: `bash: ${cmd}: command not found`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const onKeydown = (key: string, event: KeyboardEvent) => {
|
||||
// 可以在这里处理特殊按键事件
|
||||
}
|
||||
|
||||
const handleDisconnect = () => {
|
||||
isConnected.value = false
|
||||
Message.info('已断开连接')
|
||||
}
|
||||
|
||||
const clearTerminal = () => {
|
||||
if (terminalRef.value) {
|
||||
terminalRef.value.clear()
|
||||
}
|
||||
}
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
const terminalContent = document.querySelector('.terminal-content')
|
||||
if (!document.fullscreenElement) {
|
||||
terminalContent?.requestFullscreen()
|
||||
isFullscreen.value = true
|
||||
} else {
|
||||
document.exitFullscreen()
|
||||
isFullscreen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.remote-control-page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f0f2f5;
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 24px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e5e6eb;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.server-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
|
||||
.connection-panel {
|
||||
width: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
overflow-y: auto;
|
||||
|
||||
.connection-card {
|
||||
:deep(.arco-card-body) {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.history-card {
|
||||
flex: 1;
|
||||
|
||||
:deep(.arco-list-item) {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #f7f8fa;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.terminal-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #1e1e1e;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
&.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.terminal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background: #2d2d2d;
|
||||
border-bottom: 1px solid #3a3a3a;
|
||||
|
||||
.terminal-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #c9cdd4;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.terminal-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.t-window) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.terminal-placeholder {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
92
src/views/ops/pages/dc/server/config/columns.ts
Normal file
92
src/views/ops/pages/dc/server/config/columns.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
export const columns = [
|
||||
{
|
||||
dataIndex: 'id',
|
||||
title: 'ID',
|
||||
width: 80,
|
||||
slotName: 'id',
|
||||
},
|
||||
{
|
||||
dataIndex: 'unique_id',
|
||||
title: '唯一标识',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
dataIndex: 'name',
|
||||
title: '名称',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
dataIndex: 'type',
|
||||
title: '类型',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
dataIndex: 'os',
|
||||
title: '操作系统',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
dataIndex: 'location',
|
||||
title: '位置信息',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
dataIndex: 'tags',
|
||||
title: '标签',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
dataIndex: 'ip',
|
||||
title: 'IP地址',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
dataIndex: 'remote_access',
|
||||
title: '远程访问',
|
||||
width: 100,
|
||||
slotName: 'remote_access',
|
||||
},
|
||||
{
|
||||
dataIndex: 'agent_config',
|
||||
title: 'Agent配置',
|
||||
width: 150,
|
||||
slotName: 'agent_config',
|
||||
},
|
||||
{
|
||||
dataIndex: 'cpu',
|
||||
title: 'CPU使用率',
|
||||
width: 150,
|
||||
slotName: 'cpu',
|
||||
},
|
||||
{
|
||||
dataIndex: 'memory',
|
||||
title: '内存使用率',
|
||||
width: 150,
|
||||
slotName: 'memory',
|
||||
},
|
||||
{
|
||||
dataIndex: 'disk',
|
||||
title: '硬盘使用率',
|
||||
width: 150,
|
||||
slotName: 'disk',
|
||||
},
|
||||
{
|
||||
dataIndex: 'data_collection',
|
||||
title: '数据采集',
|
||||
width: 100,
|
||||
slotName: 'data_collection',
|
||||
},
|
||||
{
|
||||
dataIndex: 'status',
|
||||
title: '状态',
|
||||
width: 100,
|
||||
slotName: 'status',
|
||||
},
|
||||
{
|
||||
dataIndex: 'actions',
|
||||
title: '操作',
|
||||
width: 100,
|
||||
fixed: 'right' as const,
|
||||
slotName: 'actions',
|
||||
},
|
||||
]
|
||||
40
src/views/ops/pages/dc/server/config/search-form.ts
Normal file
40
src/views/ops/pages/dc/server/config/search-form.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
|
||||
export const searchFormConfig: FormItem[] = [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: '关键词',
|
||||
type: 'input',
|
||||
placeholder: '请输入服务器名称、编码或IP',
|
||||
span: 6,
|
||||
},
|
||||
{
|
||||
field: 'datacenter_id',
|
||||
label: '数据中心',
|
||||
type: 'select',
|
||||
placeholder: '请选择数据中心',
|
||||
options: [], // 需要动态加载
|
||||
span: 6,
|
||||
},
|
||||
{
|
||||
field: 'rack_id',
|
||||
label: '机柜',
|
||||
type: 'select',
|
||||
placeholder: '请选择机柜',
|
||||
options: [], // 需要动态加载
|
||||
span: 6,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '状态',
|
||||
type: 'select',
|
||||
placeholder: '请选择状态',
|
||||
options: [
|
||||
{ label: '在线', value: 'online' },
|
||||
{ label: '离线', value: 'offline' },
|
||||
{ label: '维护中', value: 'maintenance' },
|
||||
{ label: '已退役', value: 'retired' },
|
||||
],
|
||||
span: 6,
|
||||
},
|
||||
]
|
||||
631
src/views/ops/pages/dc/server/index.vue
Normal file
631
src/views/ops/pages/dc/server/index.vue
Normal file
@@ -0,0 +1,631 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="服务器管理"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
新增服务器
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- ID -->
|
||||
<template #id="{ record }">
|
||||
{{ record.id }}
|
||||
</template>
|
||||
|
||||
<!-- 远程访问 -->
|
||||
<template #remote_access="{ record }">
|
||||
<a-tag :color="record.remote_access ? 'green' : 'gray'">
|
||||
{{ record.remote_access ? '已开启' : '未开启' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- Agent配置 -->
|
||||
<template #agent_config="{ record }">
|
||||
<a-tag :color="record.agent_config ? 'green' : 'gray'">
|
||||
{{ record.agent_config ? '已配置' : '未配置' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- CPU -->
|
||||
<template #cpu="{ record }">
|
||||
<div class="resource-display">
|
||||
<div class="resource-info">
|
||||
<span class="resource-label">CPU</span>
|
||||
<span class="resource-value">{{ record.cpu_info?.value || 0 }}%</span>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="(record.cpu_info?.value || 0) / 100"
|
||||
:color="getProgressColor(record.cpu_info?.value || 0)"
|
||||
size="small"
|
||||
:show-text="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 内存 -->
|
||||
<template #memory="{ record }">
|
||||
<div class="resource-display">
|
||||
<div class="resource-info">
|
||||
<span class="resource-laebl">内存</span>
|
||||
<span class="resource-value">{{ record.memory_info?.value || 0 }}%</span>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="(record.memory_info?.value || 0) / 100"
|
||||
:color="getProgressColor(record.memory_info?.value || 0)"
|
||||
size="small"
|
||||
:show-text="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 硬盘 -->
|
||||
<template #disk="{ record }">
|
||||
<div class="resource-display">
|
||||
<div class="resource-info">
|
||||
<span class="resource-label">硬盘</span>
|
||||
<span class="resource-value">{{ record.disk_info?.value || 0 }}%</span>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="(record.disk_info?.value || 0) / 100"
|
||||
:color="getProgressColor(record.disk_info?.value || 0)"
|
||||
size="small"
|
||||
:show-text="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 数据采集 -->
|
||||
<template #data_collection="{ record }">
|
||||
<a-tag :color="record.data_collection ? 'green' : 'gray'">
|
||||
{{ record.data_collection ? '已启用' : '未启用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 状态 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 操作栏 - 下拉菜单 -->
|
||||
<template #actions="{ record }">
|
||||
<a-dropdown trigger="hover">
|
||||
<a-button type="primary" size="small">
|
||||
管理
|
||||
<icon-down />
|
||||
</a-button>
|
||||
<template #content>
|
||||
<a-doption @click="handleRestart(record)">
|
||||
<template #icon>
|
||||
<icon-refresh />
|
||||
</template>
|
||||
重启
|
||||
</a-doption>
|
||||
<a-doption @click="handleDetail(record)">
|
||||
<template #icon>
|
||||
<icon-eye />
|
||||
</template>
|
||||
详情
|
||||
</a-doption>
|
||||
<a-doption @click="handleEdit(record)">
|
||||
<template #icon>
|
||||
<icon-edit />
|
||||
</template>
|
||||
编辑
|
||||
</a-doption>
|
||||
<a-doption @click="handleRemoteControl(record)">
|
||||
<template #icon>
|
||||
<icon-desktop />
|
||||
</template>
|
||||
远程控制
|
||||
</a-doption>
|
||||
<a-doption @click="handleDelete(record)" style="color: rgb(var(--danger-6))">
|
||||
<template #icon>
|
||||
<icon-delete />
|
||||
</template>
|
||||
删除
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</search-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import {
|
||||
IconPlus,
|
||||
IconDown,
|
||||
IconEdit,
|
||||
IconDesktop,
|
||||
IconDelete,
|
||||
IconRefresh,
|
||||
IconEye
|
||||
} 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 {
|
||||
fetchServerList,
|
||||
deleteServer,
|
||||
} from '@/api/ops/server'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// Mock 假数据
|
||||
const mockServerData = [
|
||||
{
|
||||
id: 1,
|
||||
unique_id: 'SRV-2024-0001',
|
||||
name: 'Web服务器-01',
|
||||
type: 'Web服务器',
|
||||
os: 'CentOS 7.9',
|
||||
location: '数据中心A-1楼-机柜01-U1',
|
||||
tags: 'Web,应用',
|
||||
ip: '192.168.1.101',
|
||||
remote_access: true,
|
||||
agent_config: true,
|
||||
cpu: '8核 Intel Xeon',
|
||||
memory: '32GB',
|
||||
disk: '1TB SSD',
|
||||
cpu_info: { value: 45, total: '8核', used: '3.6核' },
|
||||
memory_info: { value: 62, total: '32GB', used: '19.8GB' },
|
||||
disk_info: { value: 78, total: '1TB', used: '780GB' },
|
||||
data_collection: true,
|
||||
status: 'online',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
unique_id: 'SRV-2024-0002',
|
||||
name: '数据库服务器-01',
|
||||
type: '数据库服务器',
|
||||
os: 'Ubuntu 22.04',
|
||||
location: '数据中心A-1楼-机柜02-U1',
|
||||
tags: '数据库,MySQL',
|
||||
ip: '192.168.1.102',
|
||||
remote_access: true,
|
||||
agent_config: true,
|
||||
cpu: '16核 AMD EPYC',
|
||||
memory: '64GB',
|
||||
disk: '2TB NVMe',
|
||||
cpu_info: { value: 78, total: '16核', used: '12.5核' },
|
||||
memory_info: { value: 85, total: '64GB', used: '54.4GB' },
|
||||
disk_info: { value: 92, total: '2TB', used: '1.84TB' },
|
||||
data_collection: true,
|
||||
status: 'online',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
unique_id: 'SRV-2024-0003',
|
||||
name: '应用服务器-01',
|
||||
type: '应用服务器',
|
||||
os: 'Windows Server 2019',
|
||||
location: '数据中心A-2楼-机柜05-U2',
|
||||
tags: '应用,.NET',
|
||||
ip: '192.168.1.103',
|
||||
remote_access: false,
|
||||
agent_config: false,
|
||||
cpu: '4核 Intel Xeon',
|
||||
memory: '16GB',
|
||||
disk: '500GB SSD',
|
||||
cpu_info: { value: 0, total: '4核', used: '0核' },
|
||||
memory_info: { value: 0, total: '16GB', used: '0GB' },
|
||||
disk_info: { value: 0, total: '500GB', used: '0GB' },
|
||||
data_collection: false,
|
||||
status: 'offline',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
unique_id: 'SRV-2024-0004',
|
||||
name: '缓存服务器-01',
|
||||
type: '缓存服务器',
|
||||
os: 'CentOS 8.5',
|
||||
location: '数据中心A-2楼-机柜06-U1',
|
||||
tags: '缓存,Redis',
|
||||
ip: '192.168.1.104',
|
||||
remote_access: true,
|
||||
agent_config: true,
|
||||
cpu: '8核 Intel Xeon',
|
||||
memory: '32GB',
|
||||
disk: '1TB SSD',
|
||||
cpu_info: { value: 35, total: '8核', used: '2.8核' },
|
||||
memory_info: { value: 68, total: '32GB', used: '21.8GB' },
|
||||
disk_info: { value: 42, total: '1TB', used: '420GB' },
|
||||
data_collection: true,
|
||||
status: 'online',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
unique_id: 'SRV-2024-0005',
|
||||
name: '文件服务器-01',
|
||||
type: '文件服务器',
|
||||
os: 'Debian 11',
|
||||
location: '数据中心B-1楼-机柜03-U1',
|
||||
tags: '文件,NFS',
|
||||
ip: '192.168.2.101',
|
||||
remote_access: true,
|
||||
agent_config: true,
|
||||
cpu: '12核 Intel Xeon',
|
||||
memory: '48GB',
|
||||
disk: '10TB HDD',
|
||||
cpu_info: { value: 28, total: '12核', used: '3.4核' },
|
||||
memory_info: { value: 45, total: '48GB', used: '21.6GB' },
|
||||
disk_info: { value: 88, total: '10TB', used: '8.8TB' },
|
||||
data_collection: true,
|
||||
status: 'maintenance',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
unique_id: 'SRV-2024-0006',
|
||||
name: '测试服务器-01',
|
||||
type: '测试服务器',
|
||||
os: 'CentOS 7.9',
|
||||
location: '数据中心B-2楼-机柜10-U1',
|
||||
tags: '测试,开发',
|
||||
ip: '192.168.2.102',
|
||||
remote_access: false,
|
||||
agent_config: false,
|
||||
cpu: '4核 Intel Xeon',
|
||||
memory: '8GB',
|
||||
disk: '256GB SSD',
|
||||
cpu_info: { value: 0, total: '4核', used: '0核' },
|
||||
memory_info: { value: 0, total: '8GB', used: '0GB' },
|
||||
disk_info: { value: 0, total: '256GB', used: '0GB' },
|
||||
data_collection: false,
|
||||
status: 'retired',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
unique_id: 'SRV-2024-0007',
|
||||
name: '监控服务器-01',
|
||||
type: '监控服务器',
|
||||
os: 'Ubuntu 20.04',
|
||||
location: '数据中心A-1楼-机柜08-U1',
|
||||
tags: '监控,Prometheus',
|
||||
ip: '192.168.1.105',
|
||||
remote_access: true,
|
||||
agent_config: true,
|
||||
cpu: '8核 Intel Xeon',
|
||||
memory: '32GB',
|
||||
disk: '1TB SSD',
|
||||
cpu_info: { value: 55, total: '8核', used: '4.4核' },
|
||||
memory_info: { value: 72, total: '32GB', used: '23.0GB' },
|
||||
disk_info: { value: 65, total: '1TB', used: '650GB' },
|
||||
data_collection: true,
|
||||
status: 'online',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
unique_id: 'SRV-2024-0008',
|
||||
name: '备份服务器-01',
|
||||
type: '备份服务器',
|
||||
os: 'Rocky Linux 9',
|
||||
location: '数据中心B-1楼-机柜04-U1',
|
||||
tags: '备份,存储',
|
||||
ip: '192.168.2.103',
|
||||
remote_access: true,
|
||||
agent_config: true,
|
||||
cpu: '16核 AMD EPYC',
|
||||
memory: '64GB',
|
||||
disk: '20TB HDD',
|
||||
cpu_info: { value: 42, total: '16核', used: '6.7核' },
|
||||
memory_info: { value: 38, total: '64GB', used: '24.3GB' },
|
||||
disk_info: { value: 75, total: '20TB', used: '15TB' },
|
||||
data_collection: true,
|
||||
status: 'online',
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
unique_id: 'SRV-2024-0009',
|
||||
name: 'CI/CD服务器-01',
|
||||
type: 'CI/CD服务器',
|
||||
os: 'Ubuntu 22.04',
|
||||
location: '数据中心A-2楼-机柜07-U1',
|
||||
tags: 'CI/CD,Jenkins',
|
||||
ip: '192.168.1.106',
|
||||
remote_access: true,
|
||||
agent_config: true,
|
||||
cpu: '8核 Intel Xeon',
|
||||
memory: '16GB',
|
||||
disk: '500GB SSD',
|
||||
cpu_info: { value: 68, total: '8核', used: '5.4核' },
|
||||
memory_info: { value: 75, total: '16GB', used: '12GB' },
|
||||
disk_info: { value: 55, total: '500GB', used: '275GB' },
|
||||
data_collection: true,
|
||||
status: 'online',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
unique_id: 'SRV-2024-0010',
|
||||
name: '日志服务器-01',
|
||||
type: '日志服务器',
|
||||
os: 'CentOS Stream 9',
|
||||
location: '数据中心B-2楼-机柜12-U1',
|
||||
tags: '日志,ELK',
|
||||
ip: '192.168.2.104',
|
||||
remote_access: true,
|
||||
agent_config: true,
|
||||
cpu: '12核 Intel Xeon',
|
||||
memory: '48GB',
|
||||
disk: '2TB SSD',
|
||||
cpu_info: { value: 0, total: '12核', used: '0核' },
|
||||
memory_info: { value: 0, total: '48GB', used: '0GB' },
|
||||
disk_info: { value: 0, total: '2TB', used: '0TB' },
|
||||
data_collection: true,
|
||||
status: 'offline',
|
||||
},
|
||||
]
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const formModel = ref({
|
||||
keyword: '',
|
||||
datacenter_id: undefined,
|
||||
rack_id: undefined,
|
||||
status: undefined,
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 表单项配置
|
||||
const formItems = computed<FormItem[]>(() => searchFormConfig)
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed(() => columnsConfig)
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
online: 'green',
|
||||
offline: 'red',
|
||||
maintenance: 'orange',
|
||||
retired: 'gray',
|
||||
}
|
||||
return colorMap[status || ''] || 'gray'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status?: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
online: '在线',
|
||||
offline: '离线',
|
||||
maintenance: '维护中',
|
||||
retired: '已退役',
|
||||
}
|
||||
return textMap[status || ''] || '-'
|
||||
}
|
||||
|
||||
// 获取进度条颜色
|
||||
const getProgressColor = (value: number) => {
|
||||
if (value >= 90) return '#F53F3F' // 红色
|
||||
if (value >= 70) return '#FF7D00' // 橙色
|
||||
if (value >= 50) return '#FFD00B' // 黄色
|
||||
return '#00B42A' // 绿色
|
||||
}
|
||||
|
||||
// 获取服务器列表(使用 Mock 数据)
|
||||
const fetchServers = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
// 使用 Mock 数据
|
||||
tableData.value = mockServerData
|
||||
pagination.total = mockServerData.length
|
||||
|
||||
// 如果有搜索条件,进行过滤
|
||||
if (formModel.value.keyword || formModel.value.status) {
|
||||
let filteredData = [...mockServerData]
|
||||
|
||||
if (formModel.value.keyword) {
|
||||
const keyword = formModel.value.keyword.toLowerCase()
|
||||
filteredData = filteredData.filter(item =>
|
||||
item.name.toLowerCase().includes(keyword) ||
|
||||
item.unique_id.toLowerCase().includes(keyword) ||
|
||||
item.ip.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
|
||||
if (formModel.value.status) {
|
||||
filteredData = filteredData.filter(item => item.status === formModel.value.status)
|
||||
}
|
||||
|
||||
tableData.value = filteredData
|
||||
pagination.total = filteredData.length
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取服务器列表失败:', error)
|
||||
Message.error('获取服务器列表失败')
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchServers()
|
||||
}
|
||||
|
||||
// 处理表单模型更新
|
||||
const handleFormModelUpdate = (value: any) => {
|
||||
formModel.value = value
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
keyword: '',
|
||||
datacenter_id: undefined,
|
||||
rack_id: undefined,
|
||||
status: undefined,
|
||||
}
|
||||
pagination.current = 1
|
||||
fetchServers()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
fetchServers()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchServers()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 新增服务器
|
||||
const handleAdd = () => {
|
||||
Message.info('新增服务器功能待实现')
|
||||
// TODO: 实现新增服务器对话框
|
||||
}
|
||||
|
||||
// 重启服务器
|
||||
const handleRestart = (record: any) => {
|
||||
Modal.confirm({
|
||||
title: '确认重启',
|
||||
content: `确认重启服务器 ${record.name} 吗?`,
|
||||
onOk: () => {
|
||||
Message.info('正在发送重启指令...')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 查看详情 - 跳转到独立页面
|
||||
const handleDetail = (record: any) => {
|
||||
router.push({
|
||||
path: '/dc/detail',
|
||||
query: {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
ip: record.ip,
|
||||
status: record.status,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑服务器
|
||||
const handleEdit = (record: any) => {
|
||||
Message.info('编辑服务器功能待实现')
|
||||
// TODO: 实现编辑服务器对话框
|
||||
}
|
||||
|
||||
// 远程控制 - 跳转到独立页面
|
||||
const handleRemoteControl = (record: any) => {
|
||||
router.push({
|
||||
path: '/dc/remote',
|
||||
query: {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
ip: record.ip,
|
||||
status: record.status,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 删除服务器
|
||||
const handleDelete = async (record: any) => {
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确认删除服务器 ${record.name} 吗?`,
|
||||
onOk: async () => {
|
||||
// Mock 删除操作
|
||||
const index = mockServerData.findIndex(item => item.id === record.id)
|
||||
if (index > -1) {
|
||||
mockServerData.splice(index, 1)
|
||||
Message.success('删除成功')
|
||||
fetchServers()
|
||||
} else {
|
||||
Message.error('删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('删除服务器失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
fetchServers()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'DataCenterServer',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.resource-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 4px 0;
|
||||
|
||||
.resource-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
> div {
|
||||
display: inline-block;
|
||||
}
|
||||
.resource-value {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: rgb(var(--text-1));
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-progress) {
|
||||
margin: 0;
|
||||
|
||||
.arco-progress-bar-bg {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.arco-progress-bar {
|
||||
border-radius: 2px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,315 @@
|
||||
<template>
|
||||
<div class="metrics-summary-panel">
|
||||
<!-- 查询表单 -->
|
||||
<a-form :model="formModel" layout="inline" class="search-form">
|
||||
<a-form-item label="数据源" field="data_source">
|
||||
<a-select
|
||||
v-model="formModel.data_source"
|
||||
placeholder="请选择数据源"
|
||||
style="width: 180px"
|
||||
@change="handleDataSourceChange"
|
||||
>
|
||||
<a-option value="dc-host">主机</a-option>
|
||||
<a-option value="dc-network">网络设备</a-option>
|
||||
<a-option value="dc-database">数据库</a-option>
|
||||
<a-option value="dc-middleware">中间件</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="指标名称" field="metric_names">
|
||||
<a-input
|
||||
v-model="formModel.metric_names"
|
||||
placeholder="多个指标名称,逗号分隔"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="标识" field="identities">
|
||||
<a-input
|
||||
v-model="formModel.identities"
|
||||
:placeholder="identityPlaceholder"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="聚合方式" field="aggregation">
|
||||
<a-select
|
||||
v-model="formModel.aggregation"
|
||||
placeholder="请选择聚合方式"
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-option value="avg">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
<a-option value="min">最小值</a-option>
|
||||
<a-option value="sum">求和</a-option>
|
||||
<a-option value="count">计数</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="时间范围" field="timeRange">
|
||||
<a-range-picker
|
||||
v-model="formModel.timeRange"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 380px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="loading" @click="handleSearch">
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
重置
|
||||
</a-button>
|
||||
<a-button :loading="exporting" @click="handleExport">
|
||||
导出 CSV
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 结果表格 -->
|
||||
<a-table
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:bordered="false"
|
||||
stripe
|
||||
class="result-table"
|
||||
@page-change="handlePageChange"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column title="标识" data-index="group_key" width="200" />
|
||||
<a-table-column title="指标名称" data-index="metric_name" width="180" />
|
||||
<a-table-column title="指标单位" data-index="metric_unit" width="100" />
|
||||
<a-table-column title="聚合值" data-index="value" width="120">
|
||||
<template #cell="{ record }">
|
||||
<span>{{ formatValue(record.value) }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="样本数" data-index="sample_count" width="100" />
|
||||
<a-table-column title="聚合方式" data-index="aggregation" width="100" />
|
||||
<a-table-column title="数据源" data-index="data_source" width="120" />
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<a-empty v-if="!loading && tableData.length === 0" description="暂无数据" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { fetchMetricsSummary, exportMetricsSummary } from '@/api/ops/report'
|
||||
|
||||
// 表单模型
|
||||
const formModel = reactive({
|
||||
data_source: 'dc-host',
|
||||
metric_names: '',
|
||||
identities: '',
|
||||
aggregation: 'avg',
|
||||
timeRange: [],
|
||||
})
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const exporting = ref(false)
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 标识字段占位符
|
||||
const identityPlaceholder = computed(() => {
|
||||
if (formModel.data_source === 'dc-host') {
|
||||
return '服务器标识,必填'
|
||||
}
|
||||
return '服务标识,必填'
|
||||
})
|
||||
|
||||
// 处理数据源变化
|
||||
const handleDataSourceChange = () => {
|
||||
formModel.identities = ''
|
||||
}
|
||||
|
||||
// 格式化数值
|
||||
const formatValue = (value: number) => {
|
||||
if (value === null || value === undefined) return '-'
|
||||
return Number(value).toFixed(2)
|
||||
}
|
||||
|
||||
// 构建查询参数
|
||||
const buildParams = () => {
|
||||
const params: any = {
|
||||
data_source: formModel.data_source,
|
||||
metric_names: formModel.metric_names,
|
||||
aggregation: formModel.aggregation,
|
||||
start_time: formModel.timeRange[0],
|
||||
end_time: formModel.timeRange[1],
|
||||
}
|
||||
|
||||
// 根据数据源添加标识
|
||||
if (formModel.data_source === 'dc-host') {
|
||||
params.server_identities = formModel.identities
|
||||
} else {
|
||||
params.service_identities = formModel.identities
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
// 查询
|
||||
const handleSearch = async () => {
|
||||
// 验证必填项
|
||||
if (!formModel.data_source) {
|
||||
Message.warning('请选择数据源')
|
||||
return
|
||||
}
|
||||
if (!formModel.metric_names) {
|
||||
Message.warning('请输入指标名称')
|
||||
return
|
||||
}
|
||||
if (!formModel.identities) {
|
||||
Message.warning('请输入标识')
|
||||
return
|
||||
}
|
||||
if (!formModel.timeRange || formModel.timeRange.length !== 2) {
|
||||
Message.warning('请选择时间范围')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const params = buildParams()
|
||||
const res = await fetchMetricsSummary(params)
|
||||
|
||||
if (res.code === 0) {
|
||||
const data = res.data?.data || []
|
||||
tableData.value = data
|
||||
pagination.total = res.data?.count || 0
|
||||
pagination.current = 1
|
||||
|
||||
if (tableData.value.length === 0) {
|
||||
Message.info('未查询到数据')
|
||||
}
|
||||
} else {
|
||||
Message.error(res.message || '查询失败')
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('查询失败:', error)
|
||||
Message.error(error.message || '查询失败')
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.data_source = 'dc-host'
|
||||
formModel.metric_names = ''
|
||||
formModel.identities = ''
|
||||
formModel.aggregation = 'avg'
|
||||
formModel.timeRange = []
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
pagination.current = 1
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
// 如果需要分页查询,可以在这里实现
|
||||
}
|
||||
|
||||
// 导出
|
||||
const handleExport = async () => {
|
||||
// 验证必填项
|
||||
if (!formModel.data_source) {
|
||||
Message.warning('请选择数据源')
|
||||
return
|
||||
}
|
||||
if (!formModel.metric_names) {
|
||||
Message.warning('请输入指标名称')
|
||||
return
|
||||
}
|
||||
if (!formModel.identities) {
|
||||
Message.warning('请输入标识')
|
||||
return
|
||||
}
|
||||
if (!formModel.timeRange || formModel.timeRange.length !== 2) {
|
||||
Message.warning('请选择时间范围')
|
||||
return
|
||||
}
|
||||
|
||||
exporting.value = true
|
||||
|
||||
try {
|
||||
const params = buildParams()
|
||||
params.utf8_bom = 'true'
|
||||
|
||||
const blob = await exportMetricsSummary(params)
|
||||
|
||||
// 创建下载链接
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
|
||||
// 生成文件名
|
||||
const now = new Date()
|
||||
const timestamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`
|
||||
link.download = `metrics_summary_${timestamp}.csv`
|
||||
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
Message.success('导出成功')
|
||||
} catch (error: any) {
|
||||
console.error('导出失败:', error)
|
||||
Message.error(error.message || '导出失败')
|
||||
} finally {
|
||||
exporting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'MetricsSummaryPanel',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.metrics-summary-panel {
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background-color: var(--color-fill-2);
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.arco-form-item) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-table {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<div class="metrics-topn-panel">
|
||||
<!-- 查询表单 -->
|
||||
<a-form :model="formModel" layout="inline" class="search-form">
|
||||
<a-form-item label="数据源" field="data_source">
|
||||
<a-select
|
||||
v-model="formModel.data_source"
|
||||
placeholder="请选择数据源"
|
||||
style="width: 180px"
|
||||
@change="handleDataSourceChange"
|
||||
>
|
||||
<a-option value="dc-host">主机</a-option>
|
||||
<a-option value="dc-network">网络设备</a-option>
|
||||
<a-option value="dc-database">数据库</a-option>
|
||||
<a-option value="dc-middleware">中间件</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="指标名称" field="metric_name">
|
||||
<a-input
|
||||
v-model="formModel.metric_name"
|
||||
placeholder="请输入指标名称"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="聚合方式" field="aggregation">
|
||||
<a-select
|
||||
v-model="formModel.aggregation"
|
||||
placeholder="请选择聚合方式"
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-option value="avg">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
<a-option value="min">最小值</a-option>
|
||||
<a-option value="sum">求和</a-option>
|
||||
<a-option value="count">计数</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="排序" field="order">
|
||||
<a-select
|
||||
v-model="formModel.order"
|
||||
style="width: 100px"
|
||||
>
|
||||
<a-option value="desc">降序</a-option>
|
||||
<a-option value="asc">升序</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="数量" field="limit">
|
||||
<a-input-number
|
||||
v-model="formModel.limit"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
style="width: 120px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="标识" field="identities">
|
||||
<a-input
|
||||
v-model="formModel.identities"
|
||||
:placeholder="identityPlaceholder"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="时间范围" field="timeRange">
|
||||
<a-range-picker
|
||||
v-model="formModel.timeRange"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 380px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="loading" @click="handleSearch">
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 结果表格 -->
|
||||
<a-table
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
:bordered="false"
|
||||
stripe
|
||||
class="result-table"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column title="排名" data-index="rank" width="80" />
|
||||
<a-table-column title="标识" data-index="group_key" width="200" />
|
||||
<a-table-column title="指标名称" data-index="metric_name" width="180" />
|
||||
<a-table-column title="指标单位" data-index="metric_unit" width="100" />
|
||||
<a-table-column title="聚合值" data-index="value" width="120">
|
||||
<template #cell="{ record }">
|
||||
<span>{{ formatValue(record.value) }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="样本数" data-index="sample_count" width="100" />
|
||||
<a-table-column title="聚合方式" data-index="aggregation" width="100" />
|
||||
<a-table-column title="数据源" data-index="data_source" width="120" />
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<a-empty v-if="!loading && tableData.length === 0" description="暂无数据" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { fetchMetricsTopN } from '@/api/ops/report'
|
||||
|
||||
// 表单模型
|
||||
const formModel = reactive({
|
||||
data_source: 'dc-host',
|
||||
metric_name: '',
|
||||
aggregation: 'avg',
|
||||
order: 'desc',
|
||||
limit: 10,
|
||||
identities: '',
|
||||
timeRange: [],
|
||||
})
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 标识字段占位符
|
||||
const identityPlaceholder = computed(() => {
|
||||
if (formModel.data_source === 'dc-host') {
|
||||
return '多个服务器标识,逗号分隔'
|
||||
}
|
||||
return '多个服务标识,逗号分隔'
|
||||
})
|
||||
|
||||
// 处理数据源变化
|
||||
const handleDataSourceChange = () => {
|
||||
formModel.identities = ''
|
||||
}
|
||||
|
||||
// 格式化数值
|
||||
const formatValue = (value: number) => {
|
||||
if (value === null || value === undefined) return '-'
|
||||
return Number(value).toFixed(2)
|
||||
}
|
||||
|
||||
// 查询
|
||||
const handleSearch = async () => {
|
||||
// 验证必填项
|
||||
if (!formModel.data_source) {
|
||||
Message.warning('请选择数据源')
|
||||
return
|
||||
}
|
||||
if (!formModel.metric_name) {
|
||||
Message.warning('请输入指标名称')
|
||||
return
|
||||
}
|
||||
if (!formModel.timeRange || formModel.timeRange.length !== 2) {
|
||||
Message.warning('请选择时间范围')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
data_source: formModel.data_source,
|
||||
metric_name: formModel.metric_name,
|
||||
aggregation: formModel.aggregation,
|
||||
order: formModel.order,
|
||||
limit: formModel.limit,
|
||||
start_time: formModel.timeRange[0],
|
||||
end_time: formModel.timeRange[1],
|
||||
}
|
||||
|
||||
// 根据数据源添加标识过滤
|
||||
if (formModel.identities) {
|
||||
if (formModel.data_source === 'dc-host') {
|
||||
params.server_identities = formModel.identities
|
||||
} else {
|
||||
params.service_identities = formModel.identities
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetchMetricsTopN(params)
|
||||
|
||||
if (res.code === 0) {
|
||||
tableData.value = res.data?.items || []
|
||||
if (tableData.value.length === 0) {
|
||||
Message.info('未查询到数据')
|
||||
}
|
||||
} else {
|
||||
Message.error(res.message || '查询失败')
|
||||
tableData.value = []
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('查询失败:', error)
|
||||
Message.error(error.message || '查询失败')
|
||||
tableData.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.data_source = 'dc-host'
|
||||
formModel.metric_name = ''
|
||||
formModel.aggregation = 'avg'
|
||||
formModel.order = 'desc'
|
||||
formModel.limit = 10
|
||||
formModel.identities = ''
|
||||
formModel.timeRange = []
|
||||
tableData.value = []
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'MetricsTopNPanel',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.metrics-topn-panel {
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background-color: var(--color-fill-2);
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.arco-form-item) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-table {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,300 @@
|
||||
<template>
|
||||
<div class="network-device-status-panel">
|
||||
<!-- 查询表单 -->
|
||||
<a-form :model="formModel" layout="inline" class="search-form">
|
||||
<a-form-item label="服务标识" field="service_identities">
|
||||
<a-input
|
||||
v-model="formModel.service_identities"
|
||||
placeholder="多个标识,逗号分隔"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="指标名称" field="metric_names">
|
||||
<a-input
|
||||
v-model="formModel.metric_names"
|
||||
placeholder="多个指标名称,逗号分隔(可选)"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="聚合方式" field="aggregation">
|
||||
<a-select
|
||||
v-model="formModel.aggregation"
|
||||
placeholder="请选择聚合方式"
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-option value="avg">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
<a-option value="min">最小值</a-option>
|
||||
<a-option value="sum">求和</a-option>
|
||||
<a-option value="count">计数</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="时间范围" field="timeRange">
|
||||
<a-range-picker
|
||||
v-model="formModel.timeRange"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 380px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="loading" @click="handleSearch">
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 结果表格 -->
|
||||
<a-table
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
:bordered="false"
|
||||
stripe
|
||||
class="result-table"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column title="服务标识" data-index="service_identity" width="200" fixed="left" />
|
||||
<a-table-column title="服务器标识" data-index="server_identity" width="180" />
|
||||
<a-table-column title="名称" data-index="name" width="150" />
|
||||
<a-table-column title="类型" data-index="type" width="120" />
|
||||
<a-table-column title="主机" data-index="host" width="150" />
|
||||
<a-table-column title="状态" data-index="status" width="100">
|
||||
<template #cell="{ record }">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ record.status || '-' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="响应时间" data-index="response_time" width="120">
|
||||
<template #cell="{ record }">
|
||||
<span>{{ formatResponseTime(record.response_time) }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="运行时间" data-index="uptime" width="120">
|
||||
<template #cell="{ record }">
|
||||
<span>{{ formatUptime(record.uptime) }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="启用" data-index="enabled" width="80">
|
||||
<template #cell="{ record }">
|
||||
<span>{{ record.enabled ? '是' : '否' }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="最后检查时间" data-index="last_check_time" width="180">
|
||||
<template #cell="{ record }">
|
||||
<span>{{ formatTime(record.last_check_time) }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="指标数据" width="300">
|
||||
<template #cell="{ record }">
|
||||
<div v-if="record.metrics && Object.keys(record.metrics).length > 0" class="metrics-cell">
|
||||
<div v-for="(value, key) in record.metrics" :key="key" class="metric-item">
|
||||
<span class="metric-name">{{ key }}:</span>
|
||||
<span class="metric-value">{{ formatMetricValue(value) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<a-empty v-if="!loading && tableData.length === 0" description="暂无数据" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { fetchNetworkDeviceStatus } from '@/api/ops/report'
|
||||
|
||||
// 表单模型
|
||||
const formModel = reactive({
|
||||
service_identities: '',
|
||||
metric_names: '',
|
||||
aggregation: 'avg',
|
||||
timeRange: [],
|
||||
})
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
if (!status) return 'gray'
|
||||
const statusMap: Record<string, string> = {
|
||||
online: 'green',
|
||||
offline: 'red',
|
||||
warning: 'orange',
|
||||
unknown: 'gray',
|
||||
}
|
||||
return statusMap[status.toLowerCase()] || 'gray'
|
||||
}
|
||||
|
||||
// 格式化响应时间
|
||||
const formatResponseTime = (time: number | null) => {
|
||||
if (time === null || time === undefined) return '-'
|
||||
return `${time.toFixed(2)} ms`
|
||||
}
|
||||
|
||||
// 格式化运行时间
|
||||
const formatUptime = (seconds: number | null) => {
|
||||
if (seconds === null || seconds === undefined) return '-'
|
||||
|
||||
const days = Math.floor(seconds / 86400)
|
||||
const hours = Math.floor((seconds % 86400) / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}天 ${hours}小时`
|
||||
} else if (hours > 0) {
|
||||
return `${hours}小时 ${minutes}分钟`
|
||||
} else {
|
||||
return `${minutes}分钟`
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time: string | null) => {
|
||||
if (!time) return '-'
|
||||
try {
|
||||
const date = new Date(time)
|
||||
return date.toLocaleString('zh-CN')
|
||||
} catch {
|
||||
return time
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化指标值
|
||||
const formatMetricValue = (metric: any) => {
|
||||
if (!metric) return '-'
|
||||
const { value, metric_unit } = metric
|
||||
if (value === null || value === undefined) return '-'
|
||||
|
||||
const formattedValue = typeof value === 'number' ? value.toFixed(2) : value
|
||||
return `${formattedValue} ${metric_unit || ''}`.trim()
|
||||
}
|
||||
|
||||
// 查询
|
||||
const handleSearch = async () => {
|
||||
// 验证必填项
|
||||
if (!formModel.service_identities) {
|
||||
Message.warning('请输入服务标识')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果填写了指标名称,必须同时填写时间范围
|
||||
if (formModel.metric_names && (!formModel.timeRange || formModel.timeRange.length !== 2)) {
|
||||
Message.warning('填写指标名称时必须选择时间范围')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果填写了时间范围,必须同时填写指标名称
|
||||
if (formModel.timeRange && formModel.timeRange.length === 2 && !formModel.metric_names) {
|
||||
Message.warning('选择时间范围时必须填写指标名称')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
service_identities: formModel.service_identities,
|
||||
}
|
||||
|
||||
if (formModel.metric_names) {
|
||||
params.metric_names = formModel.metric_names
|
||||
params.aggregation = formModel.aggregation
|
||||
params.start_time = formModel.timeRange[0]
|
||||
params.end_time = formModel.timeRange[1]
|
||||
}
|
||||
|
||||
const res = await fetchNetworkDeviceStatus(params)
|
||||
|
||||
if (res.code === 0) {
|
||||
tableData.value = res.data?.data || []
|
||||
if (tableData.value.length === 0) {
|
||||
Message.info('未查询到数据')
|
||||
}
|
||||
} else {
|
||||
Message.error(res.message || '查询失败')
|
||||
tableData.value = []
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('查询失败:', error)
|
||||
Message.error(error.message || '查询失败')
|
||||
tableData.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.service_identities = ''
|
||||
formModel.metric_names = ''
|
||||
formModel.aggregation = 'avg'
|
||||
formModel.timeRange = []
|
||||
tableData.value = []
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'NetworkDeviceStatusPanel',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.network-device-status-panel {
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background-color: var(--color-fill-2);
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.arco-form-item) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-table {
|
||||
background-color: #fff;
|
||||
|
||||
.metrics-cell {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.metric-item {
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
|
||||
.metric-name {
|
||||
color: var(--color-text-2);
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
color: var(--color-text-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,266 @@
|
||||
<template>
|
||||
<div class="server-status-panel">
|
||||
<!-- 查询表单 -->
|
||||
<a-form :model="formModel" layout="inline" class="search-form">
|
||||
<a-form-item label="服务器标识" field="server_identities">
|
||||
<a-input
|
||||
v-model="formModel.server_identities"
|
||||
placeholder="多个标识,逗号分隔"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="指标名称" field="metric_names">
|
||||
<a-input
|
||||
v-model="formModel.metric_names"
|
||||
placeholder="多个指标名称,逗号分隔(可选)"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="聚合方式" field="aggregation">
|
||||
<a-select
|
||||
v-model="formModel.aggregation"
|
||||
placeholder="请选择聚合方式"
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-option value="avg">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
<a-option value="min">最小值</a-option>
|
||||
<a-option value="sum">求和</a-option>
|
||||
<a-option value="count">计数</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="时间范围" field="timeRange">
|
||||
<a-range-picker
|
||||
v-model="formModel.timeRange"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 380px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="loading" @click="handleSearch">
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 结果表格 -->
|
||||
<a-table
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
:bordered="false"
|
||||
stripe
|
||||
class="result-table"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column title="服务器标识" data-index="server_identity" width="180" fixed="left" />
|
||||
<a-table-column title="名称" data-index="name" width="150" />
|
||||
<a-table-column title="主机" data-index="host" width="150" />
|
||||
<a-table-column title="IP 地址" data-index="ip_address" width="150" />
|
||||
<a-table-column title="状态" data-index="status" width="100">
|
||||
<template #cell="{ record }">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ record.status || '-' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="启用" data-index="enable" width="80">
|
||||
<template #cell="{ record }">
|
||||
<span>{{ record.enable ? '是' : '否' }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="最后检查时间" data-index="last_check_time" width="180">
|
||||
<template #cell="{ record }">
|
||||
<span>{{ formatTime(record.last_check_time) }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="指标数据" width="300">
|
||||
<template #cell="{ record }">
|
||||
<div v-if="record.metrics && Object.keys(record.metrics).length > 0" class="metrics-cell">
|
||||
<div v-for="(value, key) in record.metrics" :key="key" class="metric-item">
|
||||
<span class="metric-name">{{ key }}:</span>
|
||||
<span class="metric-value">{{ formatMetricValue(value) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<a-empty v-if="!loading && tableData.length === 0" description="暂无数据" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { fetchServerStatus } from '@/api/ops/report'
|
||||
|
||||
// 表单模型
|
||||
const formModel = reactive({
|
||||
server_identities: '',
|
||||
metric_names: '',
|
||||
aggregation: 'avg',
|
||||
timeRange: [],
|
||||
})
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
if (!status) return 'gray'
|
||||
const statusMap: Record<string, string> = {
|
||||
online: 'green',
|
||||
offline: 'red',
|
||||
warning: 'orange',
|
||||
unknown: 'gray',
|
||||
}
|
||||
return statusMap[status.toLowerCase()] || 'gray'
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time: string | null) => {
|
||||
if (!time) return '-'
|
||||
try {
|
||||
const date = new Date(time)
|
||||
return date.toLocaleString('zh-CN')
|
||||
} catch {
|
||||
return time
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化指标值
|
||||
const formatMetricValue = (metric: any) => {
|
||||
if (!metric) return '-'
|
||||
const { value, metric_unit } = metric
|
||||
if (value === null || value === undefined) return '-'
|
||||
|
||||
const formattedValue = typeof value === 'number' ? value.toFixed(2) : value
|
||||
return `${formattedValue} ${metric_unit || ''}`.trim()
|
||||
}
|
||||
|
||||
// 查询
|
||||
const handleSearch = async () => {
|
||||
// 验证必填项
|
||||
if (!formModel.server_identities) {
|
||||
Message.warning('请输入服务器标识')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果填写了指标名称,必须同时填写时间范围
|
||||
if (formModel.metric_names && (!formModel.timeRange || formModel.timeRange.length !== 2)) {
|
||||
Message.warning('填写指标名称时必须选择时间范围')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果填写了时间范围,必须同时填写指标名称
|
||||
if (formModel.timeRange && formModel.timeRange.length === 2 && !formModel.metric_names) {
|
||||
Message.warning('选择时间范围时必须填写指标名称')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
server_identities: formModel.server_identities,
|
||||
}
|
||||
|
||||
if (formModel.metric_names) {
|
||||
params.metric_names = formModel.metric_names
|
||||
params.aggregation = formModel.aggregation
|
||||
params.start_time = formModel.timeRange[0]
|
||||
params.end_time = formModel.timeRange[1]
|
||||
}
|
||||
|
||||
const res = await fetchServerStatus(params)
|
||||
|
||||
if (res.code === 0) {
|
||||
tableData.value = res.data?.data || []
|
||||
if (tableData.value.length === 0) {
|
||||
Message.info('未查询到数据')
|
||||
}
|
||||
} else {
|
||||
Message.error(res.message || '查询失败')
|
||||
tableData.value = []
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('查询失败:', error)
|
||||
Message.error(error.message || '查询失败')
|
||||
tableData.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.server_identities = ''
|
||||
formModel.metric_names = ''
|
||||
formModel.aggregation = 'avg'
|
||||
formModel.timeRange = []
|
||||
tableData.value = []
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ServerStatusPanel',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.server-status-panel {
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background-color: var(--color-fill-2);
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.arco-form-item) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-table {
|
||||
background-color: #fff;
|
||||
|
||||
.metrics-cell {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.metric-item {
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
|
||||
.metric-name {
|
||||
color: var(--color-text-2);
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
color: var(--color-text-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,350 @@
|
||||
<template>
|
||||
<div class="traffic-summary-panel">
|
||||
<!-- 查询表单 -->
|
||||
<a-form :model="formModel" layout="inline" class="search-form">
|
||||
<a-form-item label="拓扑 ID" field="topology_id">
|
||||
<a-input-number
|
||||
v-model="formModel.topology_id"
|
||||
:min="0"
|
||||
placeholder="拓扑 ID"
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="链路 ID" field="link_id">
|
||||
<a-input-number
|
||||
v-model="formModel.link_id"
|
||||
:min="0"
|
||||
placeholder="链路 ID"
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="节点 ID" field="node_ids">
|
||||
<a-input
|
||||
v-model="formModel.node_ids"
|
||||
placeholder="多个节点 ID,逗号分隔"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="时间范围" field="timeRange">
|
||||
<a-range-picker
|
||||
v-model="formModel.timeRange"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 380px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="loading" @click="handleSearch">
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
重置
|
||||
</a-button>
|
||||
<a-button :loading="exporting" @click="handleExport">
|
||||
导出 CSV
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 汇总信息卡片 -->
|
||||
<div v-if="summaryData.totals" class="summary-cards">
|
||||
<a-card title="流量汇总" :bordered="false" class="summary-card">
|
||||
<a-descriptions :column="3" bordered>
|
||||
<a-descriptions-item v-for="(value, key) in displayTotals" :key="key" :label="formatLabel(key)">
|
||||
{{ formatValue(key, value) }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 节点流量表格 -->
|
||||
<a-divider v-if="summaryData.by_node && summaryData.by_node.length > 0">节点流量明细</a-divider>
|
||||
|
||||
<a-table
|
||||
v-if="summaryData.by_node && summaryData.by_node.length > 0"
|
||||
:data="summaryData.by_node"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
:bordered="false"
|
||||
stripe
|
||||
class="result-table"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column title="节点 ID" data-index="node_id" width="200" fixed="left" />
|
||||
<a-table-column
|
||||
v-for="column in dynamicColumns"
|
||||
:key="String(column.key)"
|
||||
:title="column.title"
|
||||
:data-index="String(column.key)"
|
||||
:width="150"
|
||||
>
|
||||
<template #cell="{ record }">
|
||||
<span>{{ formatValue(column.key, record[column.key]) }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<a-empty v-if="!loading && !summaryData.totals" description="暂无数据" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { fetchTrafficSummary, exportTrafficReport } from '@/api/ops/report'
|
||||
|
||||
// 表单模型
|
||||
const formModel = reactive({
|
||||
topology_id: 0,
|
||||
link_id: 0,
|
||||
node_ids: '',
|
||||
timeRange: [],
|
||||
})
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const exporting = ref(false)
|
||||
|
||||
// 汇总数据
|
||||
const summaryData = reactive<any>({
|
||||
totals: null,
|
||||
by_node: [],
|
||||
})
|
||||
|
||||
// 显示的汇总字段(过滤掉元数据)
|
||||
const displayTotals = computed(() => {
|
||||
if (!summaryData.totals) return {}
|
||||
|
||||
const totals: any = { ...summaryData.totals }
|
||||
delete totals.topology_id
|
||||
delete totals.link_id
|
||||
delete totals.node_ids
|
||||
|
||||
return totals
|
||||
})
|
||||
|
||||
// 动态列(从第一条记录中提取所有字段,排除 node_id)
|
||||
const dynamicColumns = computed(() => {
|
||||
if (!summaryData.by_node || summaryData.by_node.length === 0) return []
|
||||
|
||||
const firstRecord = summaryData.by_node[0]
|
||||
const columns = []
|
||||
|
||||
for (const key in firstRecord) {
|
||||
if (key !== 'node_id') {
|
||||
columns.push({
|
||||
key,
|
||||
title: formatLabel(key),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return columns
|
||||
})
|
||||
|
||||
// 格式化标签
|
||||
const formatLabel = (key: string) => {
|
||||
const labelMap: Record<string, string> = {
|
||||
total_in_bytes: '总入流量(字节)',
|
||||
total_out_bytes: '总出流量(字节)',
|
||||
total_bytes: '总流量(字节)',
|
||||
avg_latency: '平均延迟(ms)',
|
||||
max_latency: '最大延迟(ms)',
|
||||
min_latency: '最小延迟(ms)',
|
||||
peak_bandwidth: '峰值带宽(Mbps)',
|
||||
avg_bandwidth: '平均带宽(Mbps)',
|
||||
total_packets: '总包数',
|
||||
packet_loss_rate: '丢包率(%)',
|
||||
total_connections: '总连接数',
|
||||
avg_connections: '平均连接数',
|
||||
peak_connections: '峰值连接数',
|
||||
}
|
||||
|
||||
return labelMap[key] || key
|
||||
}
|
||||
|
||||
// 格式化数值
|
||||
const formatValue = (key: string, value: any) => {
|
||||
if (value === null || value === undefined) return '-'
|
||||
|
||||
// 带宽转换为 Mbps
|
||||
if (key.includes('bandwidth') && typeof value === 'number') {
|
||||
return (value / 1024 / 1024).toFixed(2)
|
||||
}
|
||||
|
||||
// 丢包率转换为百分比
|
||||
if (key === 'packet_loss_rate' && typeof value === 'number') {
|
||||
return (value * 100).toFixed(2) + '%'
|
||||
}
|
||||
|
||||
// 字节数转换为更易读的单位
|
||||
if (key.includes('bytes') && typeof value === 'number') {
|
||||
if (value > 1024 * 1024 * 1024) {
|
||||
return (value / 1024 / 1024 / 1024).toFixed(2) + ' GB'
|
||||
} else if (value > 1024 * 1024) {
|
||||
return (value / 1024 / 1024).toFixed(2) + ' MB'
|
||||
} else if (value > 1024) {
|
||||
return (value / 1024).toFixed(2) + ' KB'
|
||||
}
|
||||
return value + ' B'
|
||||
}
|
||||
|
||||
// 数字类型保留两位小数
|
||||
if (typeof value === 'number') {
|
||||
return value.toFixed(2)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// 构建查询参数
|
||||
const buildParams = () => {
|
||||
const params: any = {
|
||||
start_time: formModel.timeRange[0],
|
||||
end_time: formModel.timeRange[1],
|
||||
kind: 'summary',
|
||||
}
|
||||
|
||||
if (formModel.topology_id > 0) {
|
||||
params.topology_id = formModel.topology_id
|
||||
}
|
||||
if (formModel.link_id > 0) {
|
||||
params.link_id = formModel.link_id
|
||||
}
|
||||
if (formModel.node_ids) {
|
||||
params.node_ids = formModel.node_ids
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
// 查询
|
||||
const handleSearch = async () => {
|
||||
// 验证必填项
|
||||
if (!formModel.timeRange || formModel.timeRange.length !== 2) {
|
||||
Message.warning('请选择时间范围')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const params = buildParams()
|
||||
const res = await fetchTrafficSummary(params)
|
||||
|
||||
if (res.code === 0) {
|
||||
summaryData.totals = res.data?.totals || null
|
||||
summaryData.by_node = res.data?.by_node || []
|
||||
|
||||
if (!summaryData.totals && summaryData.by_node.length === 0) {
|
||||
Message.info('未查询到数据')
|
||||
}
|
||||
} else {
|
||||
Message.error(res.message || '查询失败')
|
||||
summaryData.totals = null
|
||||
summaryData.by_node = []
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('查询失败:', error)
|
||||
Message.error(error.message || '查询失败')
|
||||
summaryData.totals = null
|
||||
summaryData.by_node = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.topology_id = 0
|
||||
formModel.link_id = 0
|
||||
formModel.node_ids = ''
|
||||
formModel.timeRange = []
|
||||
summaryData.totals = null
|
||||
summaryData.by_node = []
|
||||
}
|
||||
|
||||
// 导出
|
||||
const handleExport = async () => {
|
||||
// 验证必填项
|
||||
if (!formModel.timeRange || formModel.timeRange.length !== 2) {
|
||||
Message.warning('请选择时间范围')
|
||||
return
|
||||
}
|
||||
|
||||
exporting.value = true
|
||||
|
||||
try {
|
||||
const params = buildParams()
|
||||
params.utf8_bom = 'true'
|
||||
|
||||
const blob = await exportTrafficReport(params)
|
||||
|
||||
// 创建下载链接
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
|
||||
// 生成文件名
|
||||
const now = new Date()
|
||||
const timestamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`
|
||||
link.download = `traffic_summary_${timestamp}.csv`
|
||||
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
Message.success('导出成功')
|
||||
} catch (error: any) {
|
||||
console.error('导出失败:', error)
|
||||
Message.error(error.message || '导出失败')
|
||||
} finally {
|
||||
exporting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TrafficSummaryPanel',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.traffic-summary-panel {
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background-color: var(--color-fill-2);
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.arco-form-item) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-cards {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.summary-card {
|
||||
:deep(.arco-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.result-table {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,530 @@
|
||||
<template>
|
||||
<div class="traffic-trend-panel">
|
||||
<!-- 查询表单 -->
|
||||
<a-form :model="formModel" layout="inline" class="search-form">
|
||||
<a-form-item label="拓扑 ID" field="topology_id">
|
||||
<a-input-number
|
||||
v-model="formModel.topology_id"
|
||||
:min="0"
|
||||
placeholder="拓扑 ID"
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="链路 ID" field="link_id">
|
||||
<a-input-number
|
||||
v-model="formModel.link_id"
|
||||
:min="0"
|
||||
placeholder="链路 ID"
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="节点 ID" field="node_ids">
|
||||
<a-input
|
||||
v-model="formModel.node_ids"
|
||||
placeholder="多个节点 ID,逗号分隔"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="时间粒度" field="granularity">
|
||||
<a-select
|
||||
v-model="formModel.granularity"
|
||||
placeholder="请选择时间粒度"
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-option value="minute">分钟</a-option>
|
||||
<a-option value="hour">小时</a-option>
|
||||
<a-option value="day">天</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="时间范围" field="timeRange">
|
||||
<a-range-picker
|
||||
v-model="formModel.timeRange"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 380px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="loading" @click="handleSearch">
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
重置
|
||||
</a-button>
|
||||
<a-button :loading="exporting" @click="handleExport">
|
||||
导出 CSV
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 趋势图表 -->
|
||||
<div v-if="trendData.length > 0" class="chart-section">
|
||||
<a-card title="流量趋势图" :bordered="false">
|
||||
<div ref="chartRef" class="chart-container" />
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 趋势数据表格 -->
|
||||
<a-divider v-if="trendData.length > 0">趋势数据明细</a-divider>
|
||||
|
||||
<a-table
|
||||
v-if="trendData.length > 0"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:bordered="false"
|
||||
stripe
|
||||
class="result-table"
|
||||
@page-change="handlePageChange"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column title="时间" data-index="time" width="180" fixed="left" />
|
||||
<a-table-column title="节点 ID" data-index="node_id" width="180" />
|
||||
<a-table-column
|
||||
v-for="column in dynamicColumns"
|
||||
:key="String(column.key)"
|
||||
:title="column.title"
|
||||
:data-index="String(column.key)"
|
||||
:width="150"
|
||||
>
|
||||
<template #cell="{ record }">
|
||||
<span>{{ formatValue(column.key, record[column.key]) }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<a-empty v-if="!loading && trendData.length === 0" description="暂无数据" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { fetchTrafficTrend, exportTrafficReport } from '@/api/ops/report'
|
||||
|
||||
// 表单模型
|
||||
const formModel = reactive({
|
||||
topology_id: 0,
|
||||
link_id: 0,
|
||||
node_ids: '',
|
||||
granularity: 'hour',
|
||||
timeRange: [],
|
||||
})
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const exporting = ref(false)
|
||||
|
||||
// 趋势数据
|
||||
const trendData = ref<any[]>([])
|
||||
|
||||
// 表格数据(分页)
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 图表引用
|
||||
const chartRef = ref<HTMLElement>()
|
||||
let chartInstance: echarts.ECharts | null = null
|
||||
|
||||
// 动态列(从第一条记录中提取所有字段,排除 time、timestamp、node_id)
|
||||
const dynamicColumns = computed(() => {
|
||||
if (trendData.value.length === 0) return []
|
||||
|
||||
const firstRecord = trendData.value[0]
|
||||
const columns = []
|
||||
|
||||
for (const key in firstRecord) {
|
||||
if (key !== 'time' && key !== 'timestamp' && key !== 'node_id') {
|
||||
columns.push({
|
||||
key,
|
||||
title: formatLabel(key),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return columns
|
||||
})
|
||||
|
||||
// 格式化标签
|
||||
const formatLabel = (key: string) => {
|
||||
const labelMap: Record<string, string> = {
|
||||
in_bytes: '入流量(字节)',
|
||||
out_bytes: '出流量(字节)',
|
||||
total_bytes: '总流量(字节)',
|
||||
latency: '延迟(ms)',
|
||||
packet_loss: '丢包率(%)',
|
||||
bandwidth: '带宽(Mbps)',
|
||||
connections: '连接数',
|
||||
}
|
||||
|
||||
return labelMap[key] || key
|
||||
}
|
||||
|
||||
// 格式化数值
|
||||
const formatValue = (key: string, value: any) => {
|
||||
if (value === null || value === undefined) return '-'
|
||||
|
||||
// 带宽转换为 Mbps
|
||||
if (key.includes('bandwidth') && typeof value === 'number') {
|
||||
return (value / 1024 / 1024).toFixed(2)
|
||||
}
|
||||
|
||||
// 丢包率转换为百分比
|
||||
if (key.includes('packet_loss') && typeof value === 'number') {
|
||||
return (value * 100).toFixed(2) + '%'
|
||||
}
|
||||
|
||||
// 字节数转换为更易读的单位
|
||||
if (key.includes('bytes') && typeof value === 'number') {
|
||||
if (value > 1024 * 1024 * 1024) {
|
||||
return (value / 1024 / 1024 / 1024).toFixed(2) + ' GB'
|
||||
} else if (value > 1024 * 1024) {
|
||||
return (value / 1024 / 1024).toFixed(2) + ' MB'
|
||||
} else if (value > 1024) {
|
||||
return (value / 1024).toFixed(2) + ' KB'
|
||||
}
|
||||
return value + ' B'
|
||||
}
|
||||
|
||||
// 数字类型保留两位小数
|
||||
if (typeof value === 'number') {
|
||||
return value.toFixed(2)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return
|
||||
|
||||
chartInstance = echarts.init(chartRef.value)
|
||||
renderChart()
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
}
|
||||
|
||||
// 渲染图表
|
||||
const renderChart = () => {
|
||||
if (!chartInstance || trendData.value.length === 0) return
|
||||
|
||||
// 提取时间轴
|
||||
const timeAxis = [...new Set(trendData.value.map(item => item.time))].sort()
|
||||
|
||||
// 按节点分组数据
|
||||
const nodeGroups: Record<string, any[]> = {}
|
||||
trendData.value.forEach(item => {
|
||||
if (!nodeGroups[item.node_id]) {
|
||||
nodeGroups[item.node_id] = []
|
||||
}
|
||||
nodeGroups[item.node_id].push(item)
|
||||
})
|
||||
|
||||
// 构建系列数据
|
||||
const series: any[] = []
|
||||
const legend: string[] = []
|
||||
|
||||
// 为每个节点创建系列
|
||||
for (const nodeId in nodeGroups) {
|
||||
const nodeData = nodeGroups[nodeId]
|
||||
|
||||
// 创建时间索引映射
|
||||
const timeIndexMap: Record<string, any> = {}
|
||||
nodeData.forEach(item => {
|
||||
timeIndexMap[item.time] = item
|
||||
})
|
||||
|
||||
// 按时间轴顺序填充数据
|
||||
const totalBytesData = timeAxis.map(time => {
|
||||
const item = timeIndexMap[time]
|
||||
return item ? item.total_bytes : null
|
||||
})
|
||||
|
||||
const latencyData = timeAxis.map(time => {
|
||||
const item = timeIndexMap[time]
|
||||
return item ? item.latency : null
|
||||
})
|
||||
|
||||
series.push({
|
||||
name: `${nodeId} - 总流量`,
|
||||
type: 'line',
|
||||
data: totalBytesData,
|
||||
yAxisIndex: 0,
|
||||
smooth: true,
|
||||
})
|
||||
|
||||
series.push({
|
||||
name: `${nodeId} - 延迟`,
|
||||
type: 'line',
|
||||
data: latencyData,
|
||||
yAxisIndex: 1,
|
||||
smooth: true,
|
||||
})
|
||||
|
||||
legend.push(`${nodeId} - 总流量`)
|
||||
legend.push(`${nodeId} - 延迟`)
|
||||
}
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: legend,
|
||||
top: 10,
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '3%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: timeAxis,
|
||||
boundaryGap: false,
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '流量(字节)',
|
||||
position: 'left',
|
||||
axisLabel: {
|
||||
formatter: (value: number) => {
|
||||
if (value > 1024 * 1024 * 1024) {
|
||||
return (value / 1024 / 1024 / 1024).toFixed(2) + ' GB'
|
||||
} else if (value > 1024 * 1024) {
|
||||
return (value / 1024 / 1024).toFixed(2) + ' MB'
|
||||
}
|
||||
return value
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '延迟(ms)',
|
||||
position: 'right',
|
||||
},
|
||||
],
|
||||
series,
|
||||
}
|
||||
|
||||
chartInstance.setOption(option)
|
||||
}
|
||||
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize()
|
||||
}
|
||||
}
|
||||
|
||||
// 构建查询参数
|
||||
const buildParams = () => {
|
||||
const params: any = {
|
||||
start_time: formModel.timeRange[0],
|
||||
end_time: formModel.timeRange[1],
|
||||
kind: 'trend',
|
||||
granularity: formModel.granularity,
|
||||
}
|
||||
|
||||
if (formModel.topology_id > 0) {
|
||||
params.topology_id = formModel.topology_id
|
||||
}
|
||||
if (formModel.link_id > 0) {
|
||||
params.link_id = formModel.link_id
|
||||
}
|
||||
if (formModel.node_ids) {
|
||||
params.node_ids = formModel.node_ids
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
// 更新表格数据
|
||||
const updateTableData = () => {
|
||||
const start = (pagination.current - 1) * pagination.pageSize
|
||||
const end = start + pagination.pageSize
|
||||
tableData.value = trendData.value.slice(start, end)
|
||||
pagination.total = trendData.value.length
|
||||
}
|
||||
|
||||
// 查询
|
||||
const handleSearch = async () => {
|
||||
// 验证必填项
|
||||
if (!formModel.timeRange || formModel.timeRange.length !== 2) {
|
||||
Message.warning('请选择时间范围')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const params = buildParams()
|
||||
const res = await fetchTrafficTrend(params)
|
||||
|
||||
if (res.code === 0) {
|
||||
trendData.value = res.data?.data || []
|
||||
pagination.total = trendData.value.length
|
||||
pagination.current = 1
|
||||
|
||||
if (trendData.value.length > 0) {
|
||||
updateTableData()
|
||||
await nextTick()
|
||||
if (!chartInstance) {
|
||||
initChart()
|
||||
} else {
|
||||
renderChart()
|
||||
}
|
||||
}
|
||||
|
||||
if (trendData.value.length === 0) {
|
||||
Message.info('未查询到数据')
|
||||
}
|
||||
} else {
|
||||
Message.error(res.message || '查询失败')
|
||||
trendData.value = []
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('查询失败:', error)
|
||||
Message.error(error.message || '查询失败')
|
||||
trendData.value = []
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.topology_id = 0
|
||||
formModel.link_id = 0
|
||||
formModel.node_ids = ''
|
||||
formModel.granularity = 'hour'
|
||||
formModel.timeRange = []
|
||||
trendData.value = []
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
pagination.current = 1
|
||||
|
||||
if (chartInstance) {
|
||||
chartInstance.clear()
|
||||
}
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
updateTableData()
|
||||
}
|
||||
|
||||
// 导出
|
||||
const handleExport = async () => {
|
||||
// 验证必填项
|
||||
if (!formModel.timeRange || formModel.timeRange.length !== 2) {
|
||||
Message.warning('请选择时间范围')
|
||||
return
|
||||
}
|
||||
|
||||
exporting.value = true
|
||||
|
||||
try {
|
||||
const params = buildParams()
|
||||
params.utf8_bom = 'true'
|
||||
|
||||
const blob = await exportTrafficReport(params)
|
||||
|
||||
// 创建下载链接
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
|
||||
// 生成文件名
|
||||
const now = new Date()
|
||||
const timestamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`
|
||||
link.download = `traffic_trend_${timestamp}.csv`
|
||||
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
Message.success('导出成功')
|
||||
} catch (error: any) {
|
||||
console.error('导出失败:', error)
|
||||
Message.error(error.message || '导出失败')
|
||||
} finally {
|
||||
exporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
// 可选:自动加载默认数据
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
chartInstance = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TrafficTrendPanel',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.traffic-trend-panel {
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background-color: var(--color-fill-2);
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.arco-form-item) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-section {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-table {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
85
src/views/ops/pages/report/history/index.vue
Normal file
85
src/views/ops/pages/report/history/index.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<a-card :bordered="false">
|
||||
<template #title>
|
||||
<div class="page-title">报表中心</div>
|
||||
</template>
|
||||
|
||||
<a-tabs v-model:active-key="activeTab" @change="handleTabChange">
|
||||
<!-- 监测指标 TOPN -->
|
||||
<a-tab-pane key="metrics-topn" title="监测指标 TOPN">
|
||||
<metrics-topn-panel />
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 监测指标汇总 -->
|
||||
<a-tab-pane key="metrics-summary" title="监测指标汇总">
|
||||
<metrics-summary-panel />
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 流量报表汇总 -->
|
||||
<a-tab-pane key="traffic-summary" title="流量报表汇总">
|
||||
<traffic-summary-panel />
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 流量报表趋势 -->
|
||||
<a-tab-pane key="traffic-trend" title="流量报表趋势">
|
||||
<traffic-trend-panel />
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 服务器状态 -->
|
||||
<a-tab-pane key="server-status" title="服务器状态">
|
||||
<server-status-panel />
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 网络设备状态 -->
|
||||
<a-tab-pane key="network-status" title="网络设备状态">
|
||||
<network-status-panel />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import MetricsTopNPanel from './components/MetricsTopNPanel.vue'
|
||||
import MetricsSummaryPanel from './components/MetricsSummaryPanel.vue'
|
||||
import TrafficSummaryPanel from './components/TrafficSummaryPanel.vue'
|
||||
import TrafficTrendPanel from './components/TrafficTrendPanel.vue'
|
||||
import ServerStatusPanel from './components/ServerStatusPanel.vue'
|
||||
import NetworkStatusPanel from './components/NetworkStatusPanel.vue'
|
||||
|
||||
// 当前激活的标签页
|
||||
const activeTab = ref('metrics-topn')
|
||||
|
||||
// 标签页切换
|
||||
const handleTabChange = (key: string) => {
|
||||
console.log('切换到标签页:', key)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ReportHistory',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 20px;
|
||||
|
||||
.page-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-content) {
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-nav) {
|
||||
padding: 0 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -98,6 +98,20 @@
|
||||
<span class="switch-label">嵌入网页</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="12">
|
||||
<a-form-item field="is_full">
|
||||
<a-switch v-model="formData.is_full" />
|
||||
<span class="switch-label">单独页面(不包含菜单)</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="12">
|
||||
<a-form-item field="hide_menu">
|
||||
<a-switch v-model="formData.hide_menu" />
|
||||
<span class="switch-label">隐藏菜单栏</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 网页嵌入配置 -->
|
||||
@@ -190,6 +204,8 @@ const formData = ref<MenuRouteRequest>({
|
||||
is_web_page: false,
|
||||
web_url: '',
|
||||
is_new_tab: false,
|
||||
is_full: false,
|
||||
hide_menu: false,
|
||||
// 编辑时需要的额外字段
|
||||
id: undefined,
|
||||
identity: undefined,
|
||||
@@ -232,6 +248,8 @@ const resetForm = () => {
|
||||
is_web_page: false,
|
||||
web_url: '',
|
||||
is_new_tab: false,
|
||||
is_full: false,
|
||||
hide_menu: false,
|
||||
id: undefined,
|
||||
identity: undefined,
|
||||
app_id: undefined,
|
||||
@@ -267,6 +285,8 @@ watch(
|
||||
is_web_page: initialValues.is_web_page || false,
|
||||
web_url: initialValues.web_url || '',
|
||||
is_new_tab: initialValues.is_new_tab || false,
|
||||
is_full: initialValues.is_full || false,
|
||||
hide_menu: initialValues.hide_menu || false,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user