feat
This commit is contained in:
@@ -117,3 +117,70 @@ export const fetchSecurityMetricsLatest = (serviceIdentity: string) => {
|
|||||||
params: { service_identity: serviceIdentity },
|
params: { service_identity: serviceIdentity },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 采集配置补丁参数 */
|
||||||
|
export interface SecurityServicePatchData {
|
||||||
|
collect_on?: boolean
|
||||||
|
collect_interval?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 采集配置补丁 */
|
||||||
|
export const patchSecurityServiceCollect = (id: number, data: SecurityServicePatchData) => {
|
||||||
|
return request.patch<{ code: number; message: string; details: { message: string } }>(`/DC-Control/v1/security/${id}/collect`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 告警聚合查询参数 */
|
||||||
|
export interface SecurityMetricsAggregateParams {
|
||||||
|
service_identity: string
|
||||||
|
metric_name: string
|
||||||
|
start_time: string
|
||||||
|
end_time: string
|
||||||
|
aggregation: 'avg' | 'max' | 'min' | 'sum' | 'count'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 告警聚合值 */
|
||||||
|
export interface SecurityMetricsAggregateData {
|
||||||
|
metric_name: string
|
||||||
|
aggregation: string
|
||||||
|
value: number
|
||||||
|
unit: string
|
||||||
|
start_time: string
|
||||||
|
end_time: string
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取告警聚合值 */
|
||||||
|
export const fetchSecurityMetricsAggregate = (params: SecurityMetricsAggregateParams) => {
|
||||||
|
return request.get<{ code: number; message: string; details: SecurityMetricsAggregateData }>(
|
||||||
|
'/DC-Control/v1/services/metrics/security/aggregate',
|
||||||
|
{ params }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 安全设备类型映射 */
|
||||||
|
export const SECURITY_TYPE_MAP: Record<string, string> = {
|
||||||
|
firewall: '防火墙',
|
||||||
|
waf: 'WAF',
|
||||||
|
ids: 'IDS',
|
||||||
|
ips: 'IPS',
|
||||||
|
vpn: 'VPN',
|
||||||
|
other: '其他',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 安全设备类型选项 */
|
||||||
|
export const SECURITY_TYPE_OPTIONS = [
|
||||||
|
{ label: '防火墙', value: 'firewall' },
|
||||||
|
{ label: 'WAF', value: 'waf' },
|
||||||
|
{ label: 'IDS', value: 'ids' },
|
||||||
|
{ label: 'IPS', value: 'ips' },
|
||||||
|
{ label: 'VPN', value: 'vpn' },
|
||||||
|
{ label: '其他', value: 'other' },
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 运行状态映射 */
|
||||||
|
export const STATUS_MAP: Record<string, string> = {
|
||||||
|
online: '在线',
|
||||||
|
offline: '离线',
|
||||||
|
error: '异常',
|
||||||
|
unknown: '未知',
|
||||||
|
}
|
||||||
|
|||||||
@@ -148,3 +148,14 @@ export const fetchStorageMetricsLatest = (serviceIdentity: string) => {
|
|||||||
params: { service_identity: serviceIdentity },
|
params: { service_identity: serviceIdentity },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 采集配置补丁 */
|
||||||
|
export interface StoragePatchData {
|
||||||
|
collect_on?: boolean
|
||||||
|
collect_interval?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 采集配置补丁更新 */
|
||||||
|
export const patchStorage = (id: number, data: StoragePatchData) => {
|
||||||
|
return request.patch<{ message: string }>(`/DC-Control/v1/storage/${id}/collect`, data)
|
||||||
|
}
|
||||||
|
|||||||
202
src/views/ops/pages/dc/security/components/Detail.vue
Normal file
202
src/views/ops/pages/dc/security/components/Detail.vue
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<template>
|
||||||
|
<div class="detail-container">
|
||||||
|
<a-divider orientation="left">基础信息</a-divider>
|
||||||
|
<a-descriptions :column="2" bordered>
|
||||||
|
<a-descriptions-item label="ID">{{ record.id }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="服务标识">{{ record.service_identity }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="服务名称">{{ record.name }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="设备类型">
|
||||||
|
{{ SECURITY_TYPE_MAP[record.type] || record.type }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="OID">{{ record.oid || '-' }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="服务器标识">{{ record.server_identity || '-' }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="描述信息" :span="2">{{ record.description || '-' }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="创建时间">{{ formatTime(record.created_at) }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="更新时间">{{ formatTime(record.updated_at) }}</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
|
||||||
|
<a-divider orientation="left">采集配置</a-divider>
|
||||||
|
<a-descriptions :column="2" bordered>
|
||||||
|
<a-descriptions-item label="采集地址" :span="2">
|
||||||
|
<a-link v-if="record.agent_config" :href="record.agent_config" target="_blank">{{ record.agent_config }}</a-link>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="启用周期采集">
|
||||||
|
<a-tag :color="record.collect_on ? 'green' : 'gray'">
|
||||||
|
{{ record.collect_on ? '已启用' : '未启用' }}
|
||||||
|
</a-tag>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="采集间隔">{{ record.collect_interval }}秒</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="采集参数" :span="2">{{ record.collect_args || '-' }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="采集结果" :span="2">{{ record.collect_last_result || '-' }}</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
|
||||||
|
<a-divider orientation="left">运行状态</a-divider>
|
||||||
|
<a-descriptions :column="2" bordered>
|
||||||
|
<a-descriptions-item label="运行状态">
|
||||||
|
<a-tag :color="getStatusColor(record.status)">
|
||||||
|
{{ STATUS_MAP[record.status] || record.status }}
|
||||||
|
</a-tag>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="状态码">{{ record.status_code || '-' }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="状态信息" :span="2">{{ record.status_message || '-' }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="响应时间">
|
||||||
|
{{ record.response_time ? `${record.response_time.toFixed(2)} ms` : '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="连续错误">
|
||||||
|
<a-tag v-if="record.continuous_errors > 0" color="red">{{ record.continuous_errors }}</a-tag>
|
||||||
|
<span v-else>0</span>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="最近检查">{{ formatTime(record.last_check_time) }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="运行时长">{{ formatUptime(record.uptime) }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="最近在线">{{ formatTime(record.last_online_time) }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="最近离线">{{ formatTime(record.last_offline_time) }}</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
|
||||||
|
<div class="action-bar">
|
||||||
|
<a-space>
|
||||||
|
<a-button type="primary" @click="$emit('edit')">
|
||||||
|
<template #icon><icon-edit /></template>
|
||||||
|
编辑
|
||||||
|
</a-button>
|
||||||
|
<a-button type="outline" @click="$emit('quick-config')">
|
||||||
|
<template #icon><icon-settings /></template>
|
||||||
|
采集配置
|
||||||
|
</a-button>
|
||||||
|
<a-button type="outline" @click="handleViewMetrics">
|
||||||
|
<template #icon><icon-dashboard /></template>
|
||||||
|
查看最新指标
|
||||||
|
</a-button>
|
||||||
|
<a-button type="outline" status="danger" @click="$emit('delete')">
|
||||||
|
<template #icon><icon-delete /></template>
|
||||||
|
删除
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-drawer v-model:visible="metricsVisible" :width="800" title="最新指标数据" :footer="false" unmount-on-close>
|
||||||
|
<a-spin :loading="metricsLoading" style="width: 100%">
|
||||||
|
<div v-if="metricsData && metricsData.metrics && metricsData.metrics.length > 0">
|
||||||
|
<a-alert type="info" style="margin-bottom: 16px">
|
||||||
|
<template #message>
|
||||||
|
<div>最新采集时间: {{ formatTime(metricsData.latest_timestamp) || '-' }}</div>
|
||||||
|
<div>指标数量: {{ metricsData.count }}</div>
|
||||||
|
</template>
|
||||||
|
</a-alert>
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col v-for="(item, index) in metricsData.metrics" :key="index" :span="8">
|
||||||
|
<a-card class="metric-card" :bordered="false">
|
||||||
|
<div class="metric-name">{{ item.metric_name }}</div>
|
||||||
|
<div class="metric-value">{{ item.metric_value }} {{ item.metric_unit || '' }}</div>
|
||||||
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
<a-empty v-else description="暂无指标数据" />
|
||||||
|
</a-spin>
|
||||||
|
</a-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
import { IconEdit, IconDelete, IconSettings, IconDashboard } from '@arco-design/web-vue/es/icon'
|
||||||
|
import {
|
||||||
|
SECURITY_TYPE_MAP,
|
||||||
|
STATUS_MAP,
|
||||||
|
fetchSecurityMetricsLatest,
|
||||||
|
type SecurityServiceItem,
|
||||||
|
type SecurityMetricsLatestData,
|
||||||
|
} from '@/api/ops/security'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
record: SecurityServiceItem
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
defineEmits(['edit', 'quick-config', 'delete'])
|
||||||
|
|
||||||
|
const metricsVisible = ref(false)
|
||||||
|
const metricsLoading = ref(false)
|
||||||
|
const metricsData = ref<SecurityMetricsLatestData | null>(null)
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
online: 'green',
|
||||||
|
offline: 'gray',
|
||||||
|
error: 'red',
|
||||||
|
unknown: 'gray',
|
||||||
|
}
|
||||||
|
return colorMap[status] || 'gray'
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleViewMetrics = async () => {
|
||||||
|
metricsVisible.value = true
|
||||||
|
metricsLoading.value = true
|
||||||
|
try {
|
||||||
|
const response: any = await fetchSecurityMetricsLatest(props.record.service_identity)
|
||||||
|
metricsData.value = response?.details || null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取最新指标失败:', error)
|
||||||
|
Message.error('获取最新指标失败')
|
||||||
|
metricsData.value = null
|
||||||
|
} finally {
|
||||||
|
metricsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (time?: string) => {
|
||||||
|
if (!time) return '-'
|
||||||
|
const date = new Date(time)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatUptime = (uptime: number) => {
|
||||||
|
if (!uptime || uptime === 0) return '-'
|
||||||
|
const days = Math.floor(uptime / 86400)
|
||||||
|
const hours = Math.floor((uptime % 86400) / 3600)
|
||||||
|
const minutes = Math.floor((uptime % 3600) / 60)
|
||||||
|
if (days > 0) return `${days}天 ${hours}小时`
|
||||||
|
if (hours > 0) return `${hours}小时 ${minutes}分钟`
|
||||||
|
return `${minutes}分钟`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.detail-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
:deep(.arco-card-body) {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
257
src/views/ops/pages/dc/security/components/FormDialog.vue
Normal file
257
src/views/ops/pages/dc/security/components/FormDialog.vue
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
:visible="visible"
|
||||||
|
:title="isEdit ? '编辑安全设备服务' : '新增安全设备服务'"
|
||||||
|
@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="name" label="服务名称">
|
||||||
|
<a-input v-model="formData.name" placeholder="请输入服务名称" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="type" label="设备类型">
|
||||||
|
<a-select v-model="formData.type" placeholder="请选择设备类型">
|
||||||
|
<a-option v-for="item in SECURITY_TYPE_OPTIONS" :key="item.value" :value="item.value">
|
||||||
|
{{ item.label }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="20">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="server_identity" label="服务器标识">
|
||||||
|
<a-input v-model="formData.server_identity" placeholder="请输入服务器标识" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="oid" label="OID">
|
||||||
|
<a-input v-model="formData.oid" placeholder="请输入OID" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="20">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="interval" label="检查间隔(秒)">
|
||||||
|
<a-input-number v-model="formData.interval" placeholder="默认60秒" :min="10" :max="3600" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-form-item field="description" label="描述信息">
|
||||||
|
<a-textarea v-model="formData.description" placeholder="请输入描述信息" :rows="2" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item field="agent_config" label="采集地址(HTTP/HTTPS GET)">
|
||||||
|
<a-input v-model="formData.agent_config" placeholder="周期采集地址,返回 JSON 格式指标数组" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-row :gutter="20">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="enabled" label="启用监控">
|
||||||
|
<a-switch v-model="formData.enabled" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="collect_on" label="启用周期采集">
|
||||||
|
<a-switch v-model="formData.collect_on" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="20">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item v-if="formData.collect_on" field="collect_interval" label="采集间隔(秒)">
|
||||||
|
<a-input-number v-model="formData.collect_interval" placeholder="默认60秒" :min="10" :max="3600" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="collect_args" label="采集参数">
|
||||||
|
<a-input v-model="formData.collect_args" placeholder="JSON 格式采集参数" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-form-item field="extra" label="扩展配置(JSON)">
|
||||||
|
<a-textarea v-model="formData.extra" placeholder="请输入 JSON 格式扩展配置" :rows="2" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item field="policy_ids" label="告警策略">
|
||||||
|
<a-select v-model="formData.policy_ids" placeholder="请选择告警策略" multiple allow-clear>
|
||||||
|
<a-option v-for="policy in policyOptions" :key="policy.id" :value="policy.id">
|
||||||
|
{{ policy.name }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
import type { FormInstance } from '@arco-design/web-vue'
|
||||||
|
import { createSecurityService, updateSecurityService, SECURITY_TYPE_OPTIONS, type SecurityServiceFormData } from '@/api/ops/security'
|
||||||
|
import { fetchPolicyOptions, type PolicyOptionItem } from '@/api/ops/alertPolicy'
|
||||||
|
|
||||||
|
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 policyOptions = ref<PolicyOptionItem[]>([])
|
||||||
|
|
||||||
|
const isEdit = computed(() => !!props.record?.id)
|
||||||
|
|
||||||
|
const formData = reactive({
|
||||||
|
service_identity: '',
|
||||||
|
name: '',
|
||||||
|
type: '',
|
||||||
|
server_identity: '',
|
||||||
|
oid: '',
|
||||||
|
interval: 60,
|
||||||
|
description: '',
|
||||||
|
agent_config: '',
|
||||||
|
enabled: true,
|
||||||
|
collect_on: true,
|
||||||
|
collect_interval: 60,
|
||||||
|
collect_args: '',
|
||||||
|
extra: '',
|
||||||
|
policy_ids: [] as number[],
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [{ required: true, message: '请输入服务名称' }],
|
||||||
|
type: [{ required: true, message: '请选择设备类型' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadPolicyOptions = async () => {
|
||||||
|
try {
|
||||||
|
const response: any = await fetchPolicyOptions({ enabled: true })
|
||||||
|
if (Array.isArray(response)) {
|
||||||
|
policyOptions.value = response
|
||||||
|
} else if (response && response.details) {
|
||||||
|
policyOptions.value = Array.isArray(response.details) ? response.details : response.details.data || []
|
||||||
|
} else {
|
||||||
|
policyOptions.value = []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载告警策略列表失败:', error)
|
||||||
|
policyOptions.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(val) => {
|
||||||
|
if (val) {
|
||||||
|
if (isEdit.value && props.record) {
|
||||||
|
Object.assign(formData, {
|
||||||
|
service_identity: props.record.service_identity || '',
|
||||||
|
name: props.record.name || '',
|
||||||
|
type: props.record.type || '',
|
||||||
|
server_identity: props.record.server_identity || '',
|
||||||
|
oid: props.record.oid || '',
|
||||||
|
interval: props.record.interval || 60,
|
||||||
|
description: props.record.description || '',
|
||||||
|
agent_config: props.record.agent_config || '',
|
||||||
|
enabled: props.record.enabled ?? true,
|
||||||
|
collect_on: props.record.collect_on ?? true,
|
||||||
|
collect_interval: props.record.collect_interval || 60,
|
||||||
|
collect_args: props.record.collect_args || '',
|
||||||
|
extra: props.record.extra || '',
|
||||||
|
policy_ids: props.record.policy_ids || [],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Object.assign(formData, {
|
||||||
|
service_identity: '',
|
||||||
|
name: '',
|
||||||
|
type: '',
|
||||||
|
server_identity: '',
|
||||||
|
oid: '',
|
||||||
|
interval: 60,
|
||||||
|
description: '',
|
||||||
|
agent_config: '',
|
||||||
|
enabled: true,
|
||||||
|
collect_on: true,
|
||||||
|
collect_interval: 60,
|
||||||
|
collect_args: '',
|
||||||
|
extra: '',
|
||||||
|
policy_ids: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleOk = async () => {
|
||||||
|
try {
|
||||||
|
await formRef.value?.validate()
|
||||||
|
|
||||||
|
confirmLoading.value = true
|
||||||
|
|
||||||
|
const submitData: SecurityServiceFormData = {
|
||||||
|
service_identity: formData.service_identity,
|
||||||
|
name: formData.name,
|
||||||
|
type: formData.type,
|
||||||
|
server_identity: formData.server_identity,
|
||||||
|
oid: formData.oid,
|
||||||
|
interval: formData.interval,
|
||||||
|
description: formData.description,
|
||||||
|
agent_config: formData.agent_config,
|
||||||
|
enabled: formData.enabled,
|
||||||
|
collect_on: formData.collect_on,
|
||||||
|
collect_interval: formData.collect_interval,
|
||||||
|
collect_args: formData.collect_args,
|
||||||
|
extra: formData.extra,
|
||||||
|
policy_ids: formData.policy_ids,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEdit.value) {
|
||||||
|
await updateSecurityService(props.record.id, submitData)
|
||||||
|
Message.success('更新成功')
|
||||||
|
} else {
|
||||||
|
await createSecurityService(submitData)
|
||||||
|
Message.success('创建成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('success')
|
||||||
|
handleCancel()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('操作失败:', error)
|
||||||
|
Message.error('操作失败')
|
||||||
|
} finally {
|
||||||
|
confirmLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpdateVisible = (value: boolean) => {
|
||||||
|
emit('update:visible', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
emit('update:visible', false)
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadPolicyOptions()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
:visible="visible"
|
||||||
|
title="采集配置"
|
||||||
|
@ok="handleOk"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
@update:visible="handleUpdateVisible"
|
||||||
|
:confirm-loading="confirmLoading"
|
||||||
|
width="500px"
|
||||||
|
>
|
||||||
|
<a-form ref="formRef" :model="formData" layout="vertical">
|
||||||
|
<a-form-item field="collect_on" label="启用周期采集">
|
||||||
|
<a-switch v-model="formData.collect_on" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item v-if="formData.collect_on" field="collect_interval" label="采集间隔(秒)">
|
||||||
|
<a-input-number v-model="formData.collect_interval" placeholder="默认60秒" :min="10" :max="3600" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-alert type="warning" style="margin-top: 16px">快速修改采集配置,无需编辑完整表单</a-alert>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, reactive, watch } from 'vue'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
import type { FormInstance } from '@arco-design/web-vue'
|
||||||
|
import { patchSecurityServiceCollect, type SecurityServicePatchData } from '@/api/ops/security'
|
||||||
|
|
||||||
|
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 formData = reactive({
|
||||||
|
collect_on: true,
|
||||||
|
collect_interval: 60,
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(val) => {
|
||||||
|
if (val && props.record) {
|
||||||
|
formData.collect_on = props.record.collect_on ?? true
|
||||||
|
formData.collect_interval = props.record.collect_interval || 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleOk = async () => {
|
||||||
|
try {
|
||||||
|
confirmLoading.value = true
|
||||||
|
|
||||||
|
const patchData: SecurityServicePatchData = {
|
||||||
|
collect_on: formData.collect_on,
|
||||||
|
collect_interval: formData.collect_interval,
|
||||||
|
}
|
||||||
|
|
||||||
|
await patchSecurityServiceCollect(props.record.id, patchData)
|
||||||
|
Message.success('采集配置已更新')
|
||||||
|
emit('success')
|
||||||
|
handleCancel()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新采集配置失败:', error)
|
||||||
|
Message.error('更新失败')
|
||||||
|
} finally {
|
||||||
|
confirmLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpdateVisible = (value: boolean) => {
|
||||||
|
emit('update:visible', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
emit('update:visible', false)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
86
src/views/ops/pages/dc/security/config/columns.ts
Normal file
86
src/views/ops/pages/dc/security/config/columns.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { SECURITY_TYPE_MAP, STATUS_MAP } from '@/api/ops/security'
|
||||||
|
|
||||||
|
export const columns = [
|
||||||
|
{
|
||||||
|
dataIndex: 'id',
|
||||||
|
title: 'ID',
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'name',
|
||||||
|
title: '服务名称',
|
||||||
|
width: 150,
|
||||||
|
ellipsis: true,
|
||||||
|
tooltip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'service_identity',
|
||||||
|
title: '服务标识',
|
||||||
|
width: 150,
|
||||||
|
ellipsis: true,
|
||||||
|
tooltip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'type',
|
||||||
|
title: '设备类型',
|
||||||
|
width: 100,
|
||||||
|
render: ({ record }: any) => {
|
||||||
|
return SECURITY_TYPE_MAP[record.type] || record.type
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'server_identity',
|
||||||
|
title: '服务器标识',
|
||||||
|
width: 150,
|
||||||
|
ellipsis: true,
|
||||||
|
tooltip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'enabled',
|
||||||
|
title: '启用状态',
|
||||||
|
width: 100,
|
||||||
|
slotName: 'enabled',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'collect_on',
|
||||||
|
title: '采集状态',
|
||||||
|
width: 100,
|
||||||
|
slotName: 'collectOn',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'collect_interval',
|
||||||
|
title: '采集间隔(秒)',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'status',
|
||||||
|
title: '运行状态',
|
||||||
|
width: 100,
|
||||||
|
slotName: 'status',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'response_time',
|
||||||
|
title: '响应时间',
|
||||||
|
width: 120,
|
||||||
|
slotName: 'responseTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'last_check_time',
|
||||||
|
title: '最近检查',
|
||||||
|
width: 180,
|
||||||
|
slotName: 'lastCheckTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'continuous_errors',
|
||||||
|
title: '连续错误',
|
||||||
|
width: 100,
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'actions',
|
||||||
|
title: '操作',
|
||||||
|
width: 180,
|
||||||
|
fixed: 'right' as const,
|
||||||
|
slotName: 'actions',
|
||||||
|
},
|
||||||
|
]
|
||||||
31
src/views/ops/pages/dc/security/config/search-form.ts
Normal file
31
src/views/ops/pages/dc/security/config/search-form.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import type { FormItem } from '@/components/search-form/types'
|
||||||
|
import { SECURITY_TYPE_OPTIONS } from '@/api/ops/security'
|
||||||
|
|
||||||
|
export const searchFormConfig: FormItem[] = [
|
||||||
|
{
|
||||||
|
field: 'keyword',
|
||||||
|
label: '关键词',
|
||||||
|
type: 'input',
|
||||||
|
placeholder: '名称/标识模糊搜索',
|
||||||
|
span: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'enabled',
|
||||||
|
label: '启用状态',
|
||||||
|
type: 'select',
|
||||||
|
placeholder: '请选择启用状态',
|
||||||
|
options: [
|
||||||
|
{ label: '已启用', value: true },
|
||||||
|
{ label: '已禁用', value: false },
|
||||||
|
],
|
||||||
|
span: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'type',
|
||||||
|
label: '设备类型',
|
||||||
|
type: 'select',
|
||||||
|
placeholder: '请选择设备类型',
|
||||||
|
options: SECURITY_TYPE_OPTIONS,
|
||||||
|
span: 6,
|
||||||
|
},
|
||||||
|
]
|
||||||
305
src/views/ops/pages/dc/security/index.vue
Normal file
305
src/views/ops/pages/dc/security/index.vue
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
<template #enabled="{ record }">
|
||||||
|
<a-tag :color="record.enabled ? 'green' : 'gray'">
|
||||||
|
{{ record.enabled ? '已启用' : '已禁用' }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #collectOn="{ record }">
|
||||||
|
<a-tag :color="record.collect_on ? 'green' : 'gray'">
|
||||||
|
{{ record.collect_on ? '已启用' : '未启用' }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #status="{ record }">
|
||||||
|
<a-tag :color="getStatusColor(record.status)">
|
||||||
|
{{ STATUS_MAP[record.status] || '未知' }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #responseTime="{ record }">
|
||||||
|
<span v-if="record.response_time">{{ record.response_time.toFixed(2) }} ms</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #lastCheckTime="{ record }">
|
||||||
|
<span>{{ formatTime(record.last_check_time) }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions="{ record }">
|
||||||
|
<a-space>
|
||||||
|
<a-dropdown trigger="hover">
|
||||||
|
<a-button type="primary" size="small">
|
||||||
|
管理
|
||||||
|
<icon-down />
|
||||||
|
</a-button>
|
||||||
|
<template #content>
|
||||||
|
<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="handleQuickConfig(record)">
|
||||||
|
<template #icon>
|
||||||
|
<icon-settings />
|
||||||
|
</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" />
|
||||||
|
|
||||||
|
<a-drawer v-model:visible="detailVisible" :width="800" title="安全设备服务详情" :footer="false" unmount-on-close>
|
||||||
|
<Detail
|
||||||
|
v-if="currentRecord"
|
||||||
|
:record="currentRecord"
|
||||||
|
@edit="handleDetailEdit"
|
||||||
|
@quick-config="handleDetailQuickConfig"
|
||||||
|
@delete="handleDetailDelete"
|
||||||
|
/>
|
||||||
|
</a-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, reactive, computed } from 'vue'
|
||||||
|
import { Message, Modal } from '@arco-design/web-vue'
|
||||||
|
import { IconPlus, IconDown, IconEdit, IconDelete, 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 './components/FormDialog.vue'
|
||||||
|
import QuickConfigDialog from './components/QuickConfigDialog.vue'
|
||||||
|
import Detail from './components/Detail.vue'
|
||||||
|
import { columns as columnsConfig } from './config/columns'
|
||||||
|
import {
|
||||||
|
fetchSecurityServiceList,
|
||||||
|
deleteSecurityService,
|
||||||
|
STATUS_MAP,
|
||||||
|
type SecurityServiceItem,
|
||||||
|
type SecurityServiceListParams,
|
||||||
|
} from '@/api/ops/security'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const tableData = ref<SecurityServiceItem[]>([])
|
||||||
|
const formDialogVisible = ref(false)
|
||||||
|
const quickConfigVisible = ref(false)
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const currentRecord = ref<SecurityServiceItem | null>(null)
|
||||||
|
const formModel = ref({
|
||||||
|
keyword: '',
|
||||||
|
enabled: undefined as boolean | undefined,
|
||||||
|
type: undefined as string | undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const pagination = reactive({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const formItems = computed<FormItem[]>(() => searchFormConfig)
|
||||||
|
const columns = computed(() => columnsConfig)
|
||||||
|
|
||||||
|
const fetchSecurityServiceData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params: SecurityServiceListParams = {
|
||||||
|
page: pagination.current,
|
||||||
|
size: pagination.pageSize,
|
||||||
|
keyword: formModel.value.keyword,
|
||||||
|
enabled: formModel.value.enabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formModel.value.type) {
|
||||||
|
;(params as any).type = formModel.value.type
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: any = await fetchSecurityServiceList(params)
|
||||||
|
|
||||||
|
if (response && response.details) {
|
||||||
|
tableData.value = response.details?.data || []
|
||||||
|
pagination.total = response.details?.total || 0
|
||||||
|
} else {
|
||||||
|
tableData.value = []
|
||||||
|
pagination.total = 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取安全设备服务列表失败:', error)
|
||||||
|
Message.error('获取安全设备服务列表失败')
|
||||||
|
tableData.value = []
|
||||||
|
pagination.total = 0
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.current = 1
|
||||||
|
fetchSecurityServiceData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFormModelUpdate = (value: any) => {
|
||||||
|
formModel.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
formModel.value = {
|
||||||
|
keyword: '',
|
||||||
|
enabled: undefined,
|
||||||
|
type: undefined,
|
||||||
|
}
|
||||||
|
pagination.current = 1
|
||||||
|
fetchSecurityServiceData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = (current: number) => {
|
||||||
|
pagination.current = current
|
||||||
|
fetchSecurityServiceData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
fetchSecurityServiceData()
|
||||||
|
Message.success('数据已刷新')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
currentRecord.value = null
|
||||||
|
formDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleQuickConfig = (record: SecurityServiceItem) => {
|
||||||
|
currentRecord.value = record
|
||||||
|
quickConfigVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (record: SecurityServiceItem) => {
|
||||||
|
currentRecord.value = record
|
||||||
|
formDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDetail = (record: SecurityServiceItem) => {
|
||||||
|
currentRecord.value = record
|
||||||
|
detailVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDetailEdit = () => {
|
||||||
|
detailVisible.value = false
|
||||||
|
formDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDetailQuickConfig = () => {
|
||||||
|
detailVisible.value = false
|
||||||
|
quickConfigVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDetailDelete = () => {
|
||||||
|
detailVisible.value = false
|
||||||
|
if (currentRecord.value) {
|
||||||
|
handleDelete(currentRecord.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFormSuccess = () => {
|
||||||
|
fetchSecurityServiceData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = (record: SecurityServiceItem) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `确认删除安全设备服务 "${record.name}" 吗?`,
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await deleteSecurityService(record.id)
|
||||||
|
Message.success('删除成功')
|
||||||
|
fetchSecurityServiceData()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除安全设备服务失败:', error)
|
||||||
|
Message.error('删除失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
online: 'green',
|
||||||
|
offline: 'gray',
|
||||||
|
error: 'red',
|
||||||
|
unknown: 'gray',
|
||||||
|
}
|
||||||
|
return colorMap[status] || 'gray'
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (time?: string) => {
|
||||||
|
if (!time) return '-'
|
||||||
|
const date = new Date(time)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchSecurityServiceData()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'SecurityServiceManagement',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.container {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
182
src/views/ops/pages/dc/storage/components/StorageDetail.vue
Normal file
182
src/views/ops/pages/dc/storage/components/StorageDetail.vue
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
<template>
|
||||||
|
<a-drawer
|
||||||
|
:visible="visible"
|
||||||
|
title="存储设备详情"
|
||||||
|
placement="right"
|
||||||
|
width="600px"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
@update:visible="handleUpdateVisible"
|
||||||
|
>
|
||||||
|
<div class="detail-container">
|
||||||
|
<a-spin :loading="loading" style="width: 100%">
|
||||||
|
<a-descriptions title="基础信息" :column="2" bordered>
|
||||||
|
<a-descriptions-item label="ID">{{ detailData?.id }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="服务标识">{{ detailData?.service_identity }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="名称">{{ detailData?.name }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="类型">{{ detailData?.type }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="分类">{{ detailData?.category || '-' }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="OID">{{ detailData?.oid || '-' }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="服务器标识">{{ detailData?.server_identity || '-' }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="标签">{{ detailData?.tags || '-' }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="描述" :span="2">{{ detailData?.description || '-' }}</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
|
||||||
|
<a-descriptions title="采集配置" :column="2" bordered style="margin-top: 20px">
|
||||||
|
<a-descriptions-item label="Agent配置">{{ detailData?.agent_config || '-' }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="启用采集">
|
||||||
|
<a-tag :color="detailData?.collect_on ? 'green' : 'gray'">
|
||||||
|
{{ detailData?.collect_on ? '已启用' : '未启用' }}
|
||||||
|
</a-tag>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="采集间隔">
|
||||||
|
{{ detailData?.collect_interval ? `${detailData?.collect_interval}秒` : '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="采集参数">{{ detailData?.collect_args || '-' }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="采集结果" :span="2">{{ detailData?.collect_last_result || '-' }}</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
|
||||||
|
<a-descriptions title="运行状态" :column="2" bordered style="margin-top: 20px">
|
||||||
|
<a-descriptions-item label="状态">
|
||||||
|
<a-tag :color="getStatusColor(detailData?.status)">
|
||||||
|
{{ getStatusText(detailData?.status) }}
|
||||||
|
</a-tag>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="状态码">{{ detailData?.status_code || '-' }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="状态消息" :span="2">{{ detailData?.status_message || '-' }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="响应时间">
|
||||||
|
{{ detailData?.response_time ? `${detailData?.response_time}ms` : '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="最后检查时间">{{ formatDateTime(detailData?.last_check_time) }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="最后在线时间">{{ formatDateTime(detailData?.last_online_time) }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="最后离线时间">{{ formatDateTime(detailData?.last_offline_time) }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="连续错误次数">{{ detailData?.continuous_errors || 0 }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="运行时长">{{ detailData?.uptime ? `${detailData?.uptime}秒` : '-' }}</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
|
||||||
|
<a-descriptions title="最新指标" :column="2" bordered style="margin-top: 20px" v-if="metricsData?.metrics?.length">
|
||||||
|
<a-descriptions-item label="数据时间">{{ formatDateTime(metricsData?.latest_timestamp) }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="指标数量">{{ metricsData?.count || 0 }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item v-for="metric in metricsData?.metrics" :key="metric.metric_name" :label="metric.metric_name">
|
||||||
|
{{ metric.metric_value }} {{ metric.metric_unit }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
<a-empty v-else-if="!loading && !metricsData?.metrics?.length" description="暂无指标数据" style="margin-top: 20px" />
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</a-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
import { fetchStorageDetail, fetchStorageMetricsLatest } from '@/api/ops/storage'
|
||||||
|
import type { StorageItem, StorageMetricsLatestResponse } from '@/api/ops/storage'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean
|
||||||
|
record?: StorageItem | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
record: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible'])
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const detailData = ref<StorageItem | null>(null)
|
||||||
|
const metricsData = ref<StorageMetricsLatestResponse | null>(null)
|
||||||
|
|
||||||
|
const getStatusColor = (status?: string) => {
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
online: 'green',
|
||||||
|
offline: 'red',
|
||||||
|
error: 'orange',
|
||||||
|
unknown: 'gray',
|
||||||
|
}
|
||||||
|
return colorMap[status || ''] || 'gray'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status?: string) => {
|
||||||
|
const textMap: Record<string, string> = {
|
||||||
|
online: '在线',
|
||||||
|
offline: '离线',
|
||||||
|
error: '异常',
|
||||||
|
unknown: '未知',
|
||||||
|
}
|
||||||
|
return textMap[status || ''] || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDateTime = (dateTime: string | null | undefined) => {
|
||||||
|
if (!dateTime || dateTime === '0001-01-01T00:00:00Z') {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const date = new Date(dateTime)
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
|
} catch {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchDetail = async () => {
|
||||||
|
if (!props.record?.id) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res: any = await fetchStorageDetail(props.record.id)
|
||||||
|
if (res.code === 0) {
|
||||||
|
detailData.value = res.details
|
||||||
|
|
||||||
|
if (detailData.value?.service_identity) {
|
||||||
|
const metricsRes: any = await fetchStorageMetricsLatest(detailData.value.service_identity)
|
||||||
|
if (metricsRes.code === 0) {
|
||||||
|
metricsData.value = metricsRes.details
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Message.error(res.message || '获取详情失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取详情失败:', error)
|
||||||
|
Message.error('获取详情失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(val) => {
|
||||||
|
if (val && props.record) {
|
||||||
|
fetchDetail()
|
||||||
|
} else {
|
||||||
|
detailData.value = null
|
||||||
|
metricsData.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleUpdateVisible = (value: boolean) => {
|
||||||
|
emit('update:visible', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
emit('update:visible', false)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.detail-container {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
243
src/views/ops/pages/dc/storage/components/StorageFormDialog.vue
Normal file
243
src/views/ops/pages/dc/storage/components/StorageFormDialog.vue
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
:visible="visible"
|
||||||
|
:title="isEdit ? '编辑存储设备' : '新增存储设备'"
|
||||||
|
@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="name" label="名称" required>
|
||||||
|
<a-input v-model="formData.name" placeholder="请输入存储设备名称" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="type" label="类型" required>
|
||||||
|
<a-select v-model="formData.type" placeholder="请选择类型">
|
||||||
|
<a-option value="nas">NAS存储</a-option>
|
||||||
|
<a-option value="san">SAN存储</a-option>
|
||||||
|
<a-option value="das">DAS存储</a-option>
|
||||||
|
<a-option value="cloud">云存储</a-option>
|
||||||
|
<a-option value="other">其他</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="category" label="分类">
|
||||||
|
<a-input v-model="formData.category" placeholder="请输入分类" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="20">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="oid" label="OID">
|
||||||
|
<a-input v-model="formData.oid" placeholder="请输入OID" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="server_identity" label="服务器标识">
|
||||||
|
<a-input v-model="formData.server_identity" placeholder="请输入关联服务器标识" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="20">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="enabled" label="启用状态">
|
||||||
|
<a-switch v-model="formData.enabled" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="interval" label="检查间隔(秒)">
|
||||||
|
<a-input-number v-model="formData.interval" :min="10" :max="3600" placeholder="默认60秒" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="20">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="collect_on" label="启用采集">
|
||||||
|
<a-switch v-model="formData.collect_on" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item v-if="formData.collect_on" field="collect_interval" label="采集间隔(秒)">
|
||||||
|
<a-input-number v-model="formData.collect_interval" :min="10" :max="3600" placeholder="默认60秒" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-form-item field="agent_config" label="Agent配置URL">
|
||||||
|
<a-input v-model="formData.agent_config" placeholder="请输入Agent配置地址" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item field="collect_args" label="采集参数">
|
||||||
|
<a-textarea v-model="formData.collect_args" placeholder="请输入采集参数(JSON格式)" :rows="3" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item field="tags" label="标签">
|
||||||
|
<a-input v-model="formData.tags" placeholder="多个标签逗号分隔" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item field="extra" label="扩展信息">
|
||||||
|
<a-textarea v-model="formData.extra" placeholder="请输入扩展信息(JSON格式)" :rows="3" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item field="description" label="描述">
|
||||||
|
<a-textarea v-model="formData.description" 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 type { FormInstance } from '@arco-design/web-vue'
|
||||||
|
import { createStorage, updateStorage } from '@/api/ops/storage'
|
||||||
|
import type { StorageCreateData, StorageItem } from '@/api/ops/storage'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean
|
||||||
|
record?: StorageItem | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
record: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'success'])
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const confirmLoading = ref(false)
|
||||||
|
|
||||||
|
const isEdit = computed(() => !!props.record?.id)
|
||||||
|
|
||||||
|
const formData = reactive<StorageCreateData>({
|
||||||
|
name: '',
|
||||||
|
type: '',
|
||||||
|
category: '',
|
||||||
|
oid: '',
|
||||||
|
server_identity: '',
|
||||||
|
description: '',
|
||||||
|
enabled: true,
|
||||||
|
interval: 60,
|
||||||
|
extra: '',
|
||||||
|
tags: '',
|
||||||
|
agent_config: '',
|
||||||
|
collect_on: true,
|
||||||
|
collect_args: '',
|
||||||
|
collect_interval: 60,
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [{ required: true, message: '请输入名称' }],
|
||||||
|
type: [{ required: true, message: '请选择类型' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(val) => {
|
||||||
|
if (val) {
|
||||||
|
if (isEdit.value && props.record) {
|
||||||
|
Object.assign(formData, {
|
||||||
|
name: props.record.name || '',
|
||||||
|
type: props.record.type || '',
|
||||||
|
category: props.record.category || '',
|
||||||
|
oid: props.record.oid || '',
|
||||||
|
server_identity: props.record.server_identity || '',
|
||||||
|
description: props.record.description || '',
|
||||||
|
enabled: props.record.enabled ?? true,
|
||||||
|
interval: props.record.interval || 60,
|
||||||
|
extra: props.record.extra || '',
|
||||||
|
tags: props.record.tags || '',
|
||||||
|
agent_config: props.record.agent_config || '',
|
||||||
|
collect_on: props.record.collect_on ?? true,
|
||||||
|
collect_args: props.record.collect_args || '',
|
||||||
|
collect_interval: props.record.collect_interval || 60,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Object.assign(formData, {
|
||||||
|
name: '',
|
||||||
|
type: '',
|
||||||
|
category: '',
|
||||||
|
oid: '',
|
||||||
|
server_identity: '',
|
||||||
|
description: '',
|
||||||
|
enabled: true,
|
||||||
|
interval: 60,
|
||||||
|
extra: '',
|
||||||
|
tags: '',
|
||||||
|
agent_config: '',
|
||||||
|
collect_on: true,
|
||||||
|
collect_args: '',
|
||||||
|
collect_interval: 60,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleOk = async () => {
|
||||||
|
try {
|
||||||
|
await formRef.value?.validate()
|
||||||
|
|
||||||
|
confirmLoading.value = true
|
||||||
|
|
||||||
|
const submitData: StorageCreateData = {
|
||||||
|
name: formData.name,
|
||||||
|
type: formData.type,
|
||||||
|
category: formData.category,
|
||||||
|
oid: formData.oid,
|
||||||
|
server_identity: formData.server_identity,
|
||||||
|
description: formData.description,
|
||||||
|
enabled: formData.enabled,
|
||||||
|
interval: formData.interval,
|
||||||
|
extra: formData.extra,
|
||||||
|
tags: formData.tags,
|
||||||
|
agent_config: formData.agent_config,
|
||||||
|
collect_on: formData.collect_on,
|
||||||
|
collect_args: formData.collect_args,
|
||||||
|
collect_interval: formData.collect_interval,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEdit.value && props.record?.id) {
|
||||||
|
const res: any = await updateStorage(props.record.id, submitData)
|
||||||
|
if (res.code === 0) {
|
||||||
|
Message.success('更新成功')
|
||||||
|
emit('success')
|
||||||
|
handleCancel()
|
||||||
|
} else {
|
||||||
|
Message.error(res.message || '更新失败')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const res: any = await createStorage(submitData)
|
||||||
|
if (res.code === 0) {
|
||||||
|
Message.success('创建成功')
|
||||||
|
emit('success')
|
||||||
|
handleCancel()
|
||||||
|
} else {
|
||||||
|
Message.error(res.message || '创建失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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>
|
||||||
74
src/views/ops/pages/dc/storage/config/columns.ts
Normal file
74
src/views/ops/pages/dc/storage/config/columns.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
export const columns = [
|
||||||
|
{
|
||||||
|
dataIndex: 'id',
|
||||||
|
title: 'ID',
|
||||||
|
width: 80,
|
||||||
|
slotName: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'name',
|
||||||
|
title: '名称',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'service_identity',
|
||||||
|
title: '服务标识',
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'type',
|
||||||
|
title: '类型',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'server_identity',
|
||||||
|
title: '服务器标识',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'enabled',
|
||||||
|
title: '启用状态',
|
||||||
|
width: 100,
|
||||||
|
slotName: 'enabled',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'collect_on',
|
||||||
|
title: '数据采集',
|
||||||
|
width: 100,
|
||||||
|
slotName: 'data_collection',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'collect_interval',
|
||||||
|
title: '采集间隔(秒)',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'status',
|
||||||
|
title: '运行状态',
|
||||||
|
width: 100,
|
||||||
|
slotName: 'status',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'response_time',
|
||||||
|
title: '响应时间(ms)',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'last_check_time',
|
||||||
|
title: '最后检查时间',
|
||||||
|
width: 180,
|
||||||
|
slotName: 'last_check_time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'continuous_errors',
|
||||||
|
title: '连续错误次数',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'actions',
|
||||||
|
title: '操作',
|
||||||
|
width: 200,
|
||||||
|
fixed: 'right' as const,
|
||||||
|
slotName: 'actions',
|
||||||
|
},
|
||||||
|
]
|
||||||
22
src/views/ops/pages/dc/storage/config/search-form.ts
Normal file
22
src/views/ops/pages/dc/storage/config/search-form.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type { FormItem } from '@/components/search-form/types'
|
||||||
|
|
||||||
|
export const searchFormConfig: FormItem[] = [
|
||||||
|
{
|
||||||
|
field: 'keyword',
|
||||||
|
label: '关键词',
|
||||||
|
type: 'input',
|
||||||
|
placeholder: '请输入名称/标识',
|
||||||
|
span: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'enabled',
|
||||||
|
label: '启用状态',
|
||||||
|
type: 'select',
|
||||||
|
placeholder: '请选择启用状态',
|
||||||
|
options: [
|
||||||
|
{ label: '启用', value: true },
|
||||||
|
{ label: '停用', value: false },
|
||||||
|
],
|
||||||
|
span: 8,
|
||||||
|
},
|
||||||
|
]
|
||||||
286
src/views/ops/pages/dc/storage/index.vue
Normal file
286
src/views/ops/pages/dc/storage/index.vue
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
<template #id="{ record }">
|
||||||
|
{{ record.id }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #enabled="{ record }">
|
||||||
|
<a-tag :color="record.enabled ? 'green' : 'gray'">
|
||||||
|
{{ record.enabled ? '启用' : '停用' }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #data_collection="{ record }">
|
||||||
|
<a-tag :color="record.collect_on ? 'green' : 'gray'">
|
||||||
|
{{ record.collect_on ? '已启用' : '未启用' }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #status="{ record }">
|
||||||
|
<a-tag :color="getStatusColor(record.status)">
|
||||||
|
{{ getStatusText(record.status) }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #last_check_time="{ record }">
|
||||||
|
{{ formatDateTime(record.last_check_time) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions="{ record }">
|
||||||
|
<a-space>
|
||||||
|
<a-button type="outline" size="small" @click="handleDetail(record)">
|
||||||
|
<template #icon>
|
||||||
|
<icon-eye />
|
||||||
|
</template>
|
||||||
|
详情
|
||||||
|
</a-button>
|
||||||
|
<a-dropdown trigger="hover">
|
||||||
|
<a-button type="primary" size="small">
|
||||||
|
管理
|
||||||
|
<icon-down />
|
||||||
|
</a-button>
|
||||||
|
<template #content>
|
||||||
|
<a-doption @click="handleEdit(record)">
|
||||||
|
<template #icon>
|
||||||
|
<icon-edit />
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<StorageFormDialog v-model:visible="formDialogVisible" :record="currentRecord" @success="handleFormSuccess" />
|
||||||
|
|
||||||
|
<StorageDetail v-model:visible="detailVisible" :record="currentRecord" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
|
import { Message, Modal } from '@arco-design/web-vue'
|
||||||
|
import { IconPlus, IconDown, IconEdit, IconDelete, IconEye } from '@arco-design/web-vue/es/icon'
|
||||||
|
import type { FormItem } from '@/components/search-form/types'
|
||||||
|
import SearchTable from '@/components/search-table/index.vue'
|
||||||
|
import { searchFormConfig } from './config/search-form'
|
||||||
|
import StorageFormDialog from './components/StorageFormDialog.vue'
|
||||||
|
import StorageDetail from './components/StorageDetail.vue'
|
||||||
|
import { columns as columnsConfig } from './config/columns'
|
||||||
|
import { fetchStorageList, deleteStorage } from '@/api/ops/storage'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const tableData = ref<any[]>([])
|
||||||
|
const formDialogVisible = ref(false)
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const currentRecord = ref<any>(null)
|
||||||
|
const formModel = ref({
|
||||||
|
keyword: '',
|
||||||
|
enabled: undefined as boolean | 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',
|
||||||
|
error: 'orange',
|
||||||
|
unknown: 'gray',
|
||||||
|
}
|
||||||
|
return colorMap[status || ''] || 'gray'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status?: string) => {
|
||||||
|
const textMap: Record<string, string> = {
|
||||||
|
online: '在线',
|
||||||
|
offline: '离线',
|
||||||
|
error: '异常',
|
||||||
|
unknown: '未知',
|
||||||
|
}
|
||||||
|
return textMap[status || ''] || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDateTime = (dateTime: string | null | undefined) => {
|
||||||
|
if (!dateTime || dateTime === '0001-01-01T00:00:00Z') {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const date = new Date(dateTime)
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
|
} catch {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchStorages = async () => {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params: any = {
|
||||||
|
page: pagination.current,
|
||||||
|
size: pagination.pageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formModel.value.keyword) {
|
||||||
|
params.keyword = formModel.value.keyword
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formModel.value.enabled !== undefined) {
|
||||||
|
params.enabled = formModel.value.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: any = await fetchStorageList(params)
|
||||||
|
|
||||||
|
if (res.code === 0) {
|
||||||
|
const responseData = res.details || {}
|
||||||
|
tableData.value = responseData.data || []
|
||||||
|
pagination.total = responseData.total || 0
|
||||||
|
} else {
|
||||||
|
Message.error(res.message || '获取存储设备列表失败')
|
||||||
|
tableData.value = []
|
||||||
|
pagination.total = 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取存储设备列表失败:', error)
|
||||||
|
Message.error('获取存储设备列表失败')
|
||||||
|
tableData.value = []
|
||||||
|
pagination.total = 0
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.current = 1
|
||||||
|
fetchStorages()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFormModelUpdate = (value: any) => {
|
||||||
|
formModel.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
formModel.value = {
|
||||||
|
keyword: '',
|
||||||
|
enabled: undefined,
|
||||||
|
}
|
||||||
|
pagination.current = 1
|
||||||
|
fetchStorages()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = (current: number) => {
|
||||||
|
pagination.current = current
|
||||||
|
fetchStorages()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
fetchStorages()
|
||||||
|
Message.success('数据已刷新')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
currentRecord.value = null
|
||||||
|
formDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (record: any) => {
|
||||||
|
currentRecord.value = record
|
||||||
|
formDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDetail = (record: any) => {
|
||||||
|
currentRecord.value = record
|
||||||
|
detailVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFormSuccess = () => {
|
||||||
|
fetchStorages()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (record: any) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `确认删除存储设备 ${record.name} 吗?`,
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
const res: any = await deleteStorage(record.id)
|
||||||
|
if (res.code === 0) {
|
||||||
|
Message.success('删除成功')
|
||||||
|
fetchStorages()
|
||||||
|
} else {
|
||||||
|
Message.error(res.message || '删除失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除存储设备失败:', error)
|
||||||
|
Message.error('删除失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchStorages()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'StorageDevice',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.container {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user