Merge branch 'main' of https://git.apinb.com/ops/front
This commit is contained in:
705
src/views/ops/pages/dc/database/components/Detail.vue
Normal file
705
src/views/ops/pages/dc/database/components/Detail.vue
Normal file
@@ -0,0 +1,705 @@
|
||||
<template>
|
||||
<div class="server-detail">
|
||||
<div class="detail-header">
|
||||
<div class="server-info">
|
||||
<h2>{{ record.name || '办公PC详情' }}</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="办公PC名称">{{ record.name || '-' }}</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 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>
|
||||
211
src/views/ops/pages/dc/database/components/FormDialog.vue
Normal file
211
src/views/ops/pages/dc/database/components/FormDialog.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<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="办公PC名称">
|
||||
<a-input v-model="formData.name" placeholder="请输入办公PC名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="20">
|
||||
<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-col :span="12">
|
||||
<a-form-item field="tags" label="标签">
|
||||
<a-input v-model="formData.tags" placeholder="多个标签,逗号间隔" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item field="location" label="位置信息">
|
||||
<a-input
|
||||
v-model="formData.location"
|
||||
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>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="快捷配置"
|
||||
:mask-closable="false"
|
||||
:ok-loading="loading"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form :model="form" layout="vertical">
|
||||
<a-form-item label="远程访问端口">
|
||||
<a-input-number
|
||||
v-model="form.remote_port"
|
||||
placeholder="请输入远程访问端口,为空则不可远程访问"
|
||||
:min="1"
|
||||
:max="65535"
|
||||
style="width: 100%"
|
||||
allow-clear
|
||||
/>
|
||||
<template #extra>
|
||||
<span style="color: #86909c">为空则不可远程访问</span>
|
||||
</template>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="Agent URL配置">
|
||||
<a-input
|
||||
v-model="form.agent_url"
|
||||
placeholder="请输入Agent URL"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
record: any
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const form = ref({
|
||||
remote_port: undefined as number | undefined,
|
||||
agent_url: '',
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val && props.record) {
|
||||
form.value.remote_port = props.record.remote_port
|
||||
form.value.agent_url = props.record.agent_url || ''
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const handleSubmit = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
// 更新记录
|
||||
props.record.remote_port = form.value.remote_port
|
||||
props.record.agent_url = form.value.agent_url
|
||||
props.record.remote_access = !!form.value.remote_port
|
||||
props.record.agent_config = !!form.value.agent_url
|
||||
|
||||
Message.success('配置成功')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} catch (error) {
|
||||
Message.error('配置失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
310
src/views/ops/pages/dc/database/components/RemoteControl.vue
Normal file
310
src/views/ops/pages/dc/database/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>
|
||||
92
src/views/ops/pages/dc/database/config/columns.ts
Normal file
92
src/views/ops/pages/dc/database/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: 180,
|
||||
fixed: 'right' as const,
|
||||
slotName: 'actions',
|
||||
},
|
||||
]
|
||||
40
src/views/ops/pages/dc/database/config/search-form.ts
Normal file
40
src/views/ops/pages/dc/database/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,
|
||||
},
|
||||
]
|
||||
676
src/views/ops/pages/dc/database/index.vue
Normal file
676
src/views/ops/pages/dc/database/index.vue
Normal file
@@ -0,0 +1,676 @@
|
||||
<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-space>
|
||||
<a-button
|
||||
v-if="!record.agent_config"
|
||||
type="outline"
|
||||
size="small"
|
||||
@click="handleQuickConfig(record)"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-settings />
|
||||
</template>
|
||||
快捷配置
|
||||
</a-button>
|
||||
<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>
|
||||
</a-space>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<FormDialog
|
||||
v-model:visible="formDialogVisible"
|
||||
:record="currentRecord"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
|
||||
<!-- 快捷配置对话框 -->
|
||||
<QuickConfigDialog
|
||||
v-model:visible="quickConfigVisible"
|
||||
: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,
|
||||
IconSettings
|
||||
} 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 FormDialog from '../pc/components/FormDialog.vue'
|
||||
import QuickConfigDialog from '../pc/components/QuickConfigDialog.vue'
|
||||
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 formDialogVisible = ref(false)
|
||||
const quickConfigVisible = ref(false)
|
||||
const currentRecord = ref<any>(null)
|
||||
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 = () => {
|
||||
currentRecord.value = null
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 快捷配置
|
||||
const handleQuickConfig = (record: any) => {
|
||||
currentRecord.value = record
|
||||
quickConfigVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑数据库
|
||||
const handleEdit = (record: any) => {
|
||||
currentRecord.value = record
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 表单提交成功
|
||||
const handleFormSuccess = () => {
|
||||
fetchServers()
|
||||
}
|
||||
|
||||
// 重启数据库
|
||||
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 handleRemoteControl = (record: any) => {
|
||||
const url = router.resolve({
|
||||
path: '/dc/remote',
|
||||
query: {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
ip: record.ip,
|
||||
status: record.status,
|
||||
},
|
||||
}).href
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
// 删除数据库
|
||||
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>
|
||||
705
src/views/ops/pages/dc/middleware/components/Detail.vue
Normal file
705
src/views/ops/pages/dc/middleware/components/Detail.vue
Normal file
@@ -0,0 +1,705 @@
|
||||
<template>
|
||||
<div class="server-detail">
|
||||
<div class="detail-header">
|
||||
<div class="server-info">
|
||||
<h2>{{ record.name || '办公PC详情' }}</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="办公PC名称">{{ record.name || '-' }}</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 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>
|
||||
211
src/views/ops/pages/dc/middleware/components/FormDialog.vue
Normal file
211
src/views/ops/pages/dc/middleware/components/FormDialog.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<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="办公PC名称">
|
||||
<a-input v-model="formData.name" placeholder="请输入办公PC名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="20">
|
||||
<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-col :span="12">
|
||||
<a-form-item field="tags" label="标签">
|
||||
<a-input v-model="formData.tags" placeholder="多个标签,逗号间隔" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item field="location" label="位置信息">
|
||||
<a-input
|
||||
v-model="formData.location"
|
||||
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>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="快捷配置"
|
||||
:mask-closable="false"
|
||||
:ok-loading="loading"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form :model="form" layout="vertical">
|
||||
<a-form-item label="远程访问端口">
|
||||
<a-input-number
|
||||
v-model="form.remote_port"
|
||||
placeholder="请输入远程访问端口,为空则不可远程访问"
|
||||
:min="1"
|
||||
:max="65535"
|
||||
style="width: 100%"
|
||||
allow-clear
|
||||
/>
|
||||
<template #extra>
|
||||
<span style="color: #86909c">为空则不可远程访问</span>
|
||||
</template>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="Agent URL配置">
|
||||
<a-input
|
||||
v-model="form.agent_url"
|
||||
placeholder="请输入Agent URL"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
record: any
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const form = ref({
|
||||
remote_port: undefined as number | undefined,
|
||||
agent_url: '',
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val && props.record) {
|
||||
form.value.remote_port = props.record.remote_port
|
||||
form.value.agent_url = props.record.agent_url || ''
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const handleSubmit = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
// 更新记录
|
||||
props.record.remote_port = form.value.remote_port
|
||||
props.record.agent_url = form.value.agent_url
|
||||
props.record.remote_access = !!form.value.remote_port
|
||||
props.record.agent_config = !!form.value.agent_url
|
||||
|
||||
Message.success('配置成功')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} catch (error) {
|
||||
Message.error('配置失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
310
src/views/ops/pages/dc/middleware/components/RemoteControl.vue
Normal file
310
src/views/ops/pages/dc/middleware/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>
|
||||
92
src/views/ops/pages/dc/middleware/config/columns.ts
Normal file
92
src/views/ops/pages/dc/middleware/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: 180,
|
||||
fixed: 'right' as const,
|
||||
slotName: 'actions',
|
||||
},
|
||||
]
|
||||
40
src/views/ops/pages/dc/middleware/config/search-form.ts
Normal file
40
src/views/ops/pages/dc/middleware/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,
|
||||
},
|
||||
]
|
||||
676
src/views/ops/pages/dc/middleware/index.vue
Normal file
676
src/views/ops/pages/dc/middleware/index.vue
Normal file
@@ -0,0 +1,676 @@
|
||||
<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-space>
|
||||
<a-button
|
||||
v-if="!record.agent_config"
|
||||
type="outline"
|
||||
size="small"
|
||||
@click="handleQuickConfig(record)"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-settings />
|
||||
</template>
|
||||
快捷配置
|
||||
</a-button>
|
||||
<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>
|
||||
</a-space>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<FormDialog
|
||||
v-model:visible="formDialogVisible"
|
||||
:record="currentRecord"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
|
||||
<!-- 快捷配置对话框 -->
|
||||
<QuickConfigDialog
|
||||
v-model:visible="quickConfigVisible"
|
||||
: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,
|
||||
IconSettings
|
||||
} 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 FormDialog from '../pc/components/FormDialog.vue'
|
||||
import QuickConfigDialog from '../pc/components/QuickConfigDialog.vue'
|
||||
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 formDialogVisible = ref(false)
|
||||
const quickConfigVisible = ref(false)
|
||||
const currentRecord = ref<any>(null)
|
||||
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 = () => {
|
||||
currentRecord.value = null
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 快捷配置
|
||||
const handleQuickConfig = (record: any) => {
|
||||
currentRecord.value = record
|
||||
quickConfigVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑中间件
|
||||
const handleEdit = (record: any) => {
|
||||
currentRecord.value = record
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 表单提交成功
|
||||
const handleFormSuccess = () => {
|
||||
fetchServers()
|
||||
}
|
||||
|
||||
// 重启中间件
|
||||
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 handleRemoteControl = (record: any) => {
|
||||
const url = router.resolve({
|
||||
path: '/dc/remote',
|
||||
query: {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
ip: record.ip,
|
||||
status: record.status,
|
||||
},
|
||||
}).href
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
// 删除中间件
|
||||
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>
|
||||
705
src/views/ops/pages/dc/network/components/Detail.vue
Normal file
705
src/views/ops/pages/dc/network/components/Detail.vue
Normal file
@@ -0,0 +1,705 @@
|
||||
<template>
|
||||
<div class="server-detail">
|
||||
<div class="detail-header">
|
||||
<div class="server-info">
|
||||
<h2>{{ record.name || '办公PC详情' }}</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="办公PC名称">{{ record.name || '-' }}</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 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>
|
||||
211
src/views/ops/pages/dc/network/components/FormDialog.vue
Normal file
211
src/views/ops/pages/dc/network/components/FormDialog.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<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="办公PC名称">
|
||||
<a-input v-model="formData.name" placeholder="请输入办公PC名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="20">
|
||||
<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-col :span="12">
|
||||
<a-form-item field="tags" label="标签">
|
||||
<a-input v-model="formData.tags" placeholder="多个标签,逗号间隔" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item field="location" label="位置信息">
|
||||
<a-input
|
||||
v-model="formData.location"
|
||||
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>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="快捷配置"
|
||||
:mask-closable="false"
|
||||
:ok-loading="loading"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form :model="form" layout="vertical">
|
||||
<a-form-item label="远程访问端口">
|
||||
<a-input-number
|
||||
v-model="form.remote_port"
|
||||
placeholder="请输入远程访问端口,为空则不可远程访问"
|
||||
:min="1"
|
||||
:max="65535"
|
||||
style="width: 100%"
|
||||
allow-clear
|
||||
/>
|
||||
<template #extra>
|
||||
<span style="color: #86909c">为空则不可远程访问</span>
|
||||
</template>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="Agent URL配置">
|
||||
<a-input
|
||||
v-model="form.agent_url"
|
||||
placeholder="请输入Agent URL"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
record: any
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const form = ref({
|
||||
remote_port: undefined as number | undefined,
|
||||
agent_url: '',
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val && props.record) {
|
||||
form.value.remote_port = props.record.remote_port
|
||||
form.value.agent_url = props.record.agent_url || ''
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const handleSubmit = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
// 更新记录
|
||||
props.record.remote_port = form.value.remote_port
|
||||
props.record.agent_url = form.value.agent_url
|
||||
props.record.remote_access = !!form.value.remote_port
|
||||
props.record.agent_config = !!form.value.agent_url
|
||||
|
||||
Message.success('配置成功')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} catch (error) {
|
||||
Message.error('配置失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
310
src/views/ops/pages/dc/network/components/RemoteControl.vue
Normal file
310
src/views/ops/pages/dc/network/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>
|
||||
92
src/views/ops/pages/dc/network/config/columns.ts
Normal file
92
src/views/ops/pages/dc/network/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: 180,
|
||||
fixed: 'right' as const,
|
||||
slotName: 'actions',
|
||||
},
|
||||
]
|
||||
40
src/views/ops/pages/dc/network/config/search-form.ts
Normal file
40
src/views/ops/pages/dc/network/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,
|
||||
},
|
||||
]
|
||||
676
src/views/ops/pages/dc/network/index.vue
Normal file
676
src/views/ops/pages/dc/network/index.vue
Normal file
@@ -0,0 +1,676 @@
|
||||
<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-space>
|
||||
<a-button
|
||||
v-if="!record.agent_config"
|
||||
type="outline"
|
||||
size="small"
|
||||
@click="handleQuickConfig(record)"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-settings />
|
||||
</template>
|
||||
快捷配置
|
||||
</a-button>
|
||||
<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>
|
||||
</a-space>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<FormDialog
|
||||
v-model:visible="formDialogVisible"
|
||||
:record="currentRecord"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
|
||||
<!-- 快捷配置对话框 -->
|
||||
<QuickConfigDialog
|
||||
v-model:visible="quickConfigVisible"
|
||||
: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,
|
||||
IconSettings
|
||||
} 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 FormDialog from '../pc/components/FormDialog.vue'
|
||||
import QuickConfigDialog from '../pc/components/QuickConfigDialog.vue'
|
||||
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 formDialogVisible = ref(false)
|
||||
const quickConfigVisible = ref(false)
|
||||
const currentRecord = ref<any>(null)
|
||||
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 = () => {
|
||||
currentRecord.value = null
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 快捷配置
|
||||
const handleQuickConfig = (record: any) => {
|
||||
currentRecord.value = record
|
||||
quickConfigVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑网络设备
|
||||
const handleEdit = (record: any) => {
|
||||
currentRecord.value = record
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 表单提交成功
|
||||
const handleFormSuccess = () => {
|
||||
fetchServers()
|
||||
}
|
||||
|
||||
// 重启网络设备
|
||||
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 handleRemoteControl = (record: any) => {
|
||||
const url = router.resolve({
|
||||
path: '/dc/remote',
|
||||
query: {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
ip: record.ip,
|
||||
status: record.status,
|
||||
},
|
||||
}).href
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
// 删除网络设备
|
||||
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>
|
||||
705
src/views/ops/pages/dc/pc/components/Detail.vue
Normal file
705
src/views/ops/pages/dc/pc/components/Detail.vue
Normal file
@@ -0,0 +1,705 @@
|
||||
<template>
|
||||
<div class="server-detail">
|
||||
<div class="detail-header">
|
||||
<div class="server-info">
|
||||
<h2>{{ record.name || '办公PC详情' }}</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="办公PC名称">{{ record.name || '-' }}</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 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>
|
||||
211
src/views/ops/pages/dc/pc/components/FormDialog.vue
Normal file
211
src/views/ops/pages/dc/pc/components/FormDialog.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<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="办公PC名称">
|
||||
<a-input v-model="formData.name" placeholder="请输入办公PC名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="20">
|
||||
<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-col :span="12">
|
||||
<a-form-item field="tags" label="标签">
|
||||
<a-input v-model="formData.tags" placeholder="多个标签,逗号间隔" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item field="location" label="位置信息">
|
||||
<a-input
|
||||
v-model="formData.location"
|
||||
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>
|
||||
@@ -7,7 +7,7 @@
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="服务器及PC管理"
|
||||
title="办公PC管理"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@@ -21,7 +21,7 @@
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
新增
|
||||
新增办公PC
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
@@ -156,8 +156,8 @@
|
||||
</search-table>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ServerFormDialog
|
||||
v-model:visible="formDialogVisible"
|
||||
<FormDialog
|
||||
v-model:visible="dialogVisible"
|
||||
:record="currentRecord"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
@@ -194,9 +194,9 @@ import {
|
||||
fetchPCList,
|
||||
deletePC,
|
||||
} from '@/api/ops/pc'
|
||||
import ServerFormDialog from './components/ServerFormDialog.vue'
|
||||
import FormDialog from './components/FormDialog.vue'
|
||||
import QuickConfigDialog from './components/QuickConfigDialog.vue'
|
||||
import ServerDetail from './components/ServerDetail.vue'
|
||||
import Detail from './components/Detail.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -334,7 +334,7 @@ const formModel = ref({
|
||||
status: undefined,
|
||||
})
|
||||
|
||||
const formDialogVisible = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const quickConfigVisible = ref(false)
|
||||
const currentRecord = ref<any>(null)
|
||||
|
||||
@@ -460,7 +460,7 @@ const handleRefresh = () => {
|
||||
// 新增PC
|
||||
const handleAdd = () => {
|
||||
currentRecord.value = null
|
||||
formDialogVisible.value = true
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 快捷配置
|
||||
@@ -472,7 +472,7 @@ const handleQuickConfig = (record: any) => {
|
||||
// 编辑PC
|
||||
const handleEdit = (record: any) => {
|
||||
currentRecord.value = record
|
||||
formDialogVisible.value = true
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 详情 - 在当前窗口打开
|
||||
@@ -492,7 +492,7 @@ const handleDetail = (record: any) => {
|
||||
const handleRestart = (record: any) => {
|
||||
Modal.confirm({
|
||||
title: '确认重启',
|
||||
content: `确认重启服务器/PC ${record.name} 吗?`,
|
||||
content: `确认重启办公PC ${record.name} 吗?`,
|
||||
onOk: () => {
|
||||
Message.info('正在发送重启指令...')
|
||||
},
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="快捷配置"
|
||||
:mask-closable="false"
|
||||
:ok-loading="loading"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form :model="form" layout="vertical">
|
||||
<a-form-item label="远程访问端口">
|
||||
<a-input-number
|
||||
v-model="form.remote_port"
|
||||
placeholder="请输入远程访问端口,为空则不可远程访问"
|
||||
:min="1"
|
||||
:max="65535"
|
||||
style="width: 100%"
|
||||
allow-clear
|
||||
/>
|
||||
<template #extra>
|
||||
<span style="color: #86909c">为空则不可远程访问</span>
|
||||
</template>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="Agent URL配置">
|
||||
<a-input
|
||||
v-model="form.agent_url"
|
||||
placeholder="请输入Agent URL"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
record: any
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const form = ref({
|
||||
remote_port: undefined as number | undefined,
|
||||
agent_url: '',
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val && props.record) {
|
||||
form.value.remote_port = props.record.remote_port
|
||||
form.value.agent_url = props.record.agent_url || ''
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const handleSubmit = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
// 更新记录
|
||||
props.record.remote_port = form.value.remote_port
|
||||
props.record.agent_url = form.value.agent_url
|
||||
props.record.remote_access = !!form.value.remote_port
|
||||
props.record.agent_config = !!form.value.agent_url
|
||||
|
||||
Message.success('配置成功')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} catch (error) {
|
||||
Message.error('配置失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
310
src/views/ops/pages/dc/server/components/RemoteControl.vue
Normal file
310
src/views/ops/pages/dc/server/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>
|
||||
@@ -195,8 +195,8 @@ import {
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import { searchFormConfig } from './config/search-form'
|
||||
import ServerFormDialog from '../pc/components/ServerFormDialog.vue'
|
||||
import QuickConfigDialog from '../pc/components/QuickConfigDialog.vue'
|
||||
import ServerFormDialog from './components/ServerFormDialog.vue'
|
||||
import QuickConfigDialog from './components/QuickConfigDialog.vue'
|
||||
import { columns as columnsConfig } from './config/columns'
|
||||
import {
|
||||
fetchServerList,
|
||||
|
||||
Reference in New Issue
Block a user