This commit is contained in:
2026-04-11 21:08:34 +08:00
parent af7c652ed4
commit a0ca86d98d
14 changed files with 1855 additions and 1 deletions

View File

@@ -117,3 +117,70 @@ export const fetchSecurityMetricsLatest = (serviceIdentity: string) => {
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: '未知',
}

View File

@@ -148,3 +148,14 @@ export const fetchStorageMetricsLatest = (serviceIdentity: string) => {
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)
}

View 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>

View 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>

View File

@@ -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>

View 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',
},
]

View 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,
},
]

View 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>

View 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>

View 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>

View 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',
},
]

View 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,
},
]

View 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