feat
This commit is contained in:
145
src/api/ops/url-device.ts
Normal file
145
src/api/ops/url-device.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { request } from '@/api/request'
|
||||||
|
|
||||||
|
export interface UrlDeviceItem {
|
||||||
|
id: number
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
deleted_at: string | null
|
||||||
|
service_identity: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
target_url: string
|
||||||
|
method: string
|
||||||
|
enabled: boolean
|
||||||
|
interval: number
|
||||||
|
extra: string
|
||||||
|
tags: string
|
||||||
|
collect_on: boolean
|
||||||
|
collect_interval: number
|
||||||
|
collect_last_result: string
|
||||||
|
status: string
|
||||||
|
status_code: number
|
||||||
|
response_time: number
|
||||||
|
last_check_time: string
|
||||||
|
last_online_time: string | null
|
||||||
|
last_offline_time: string | null
|
||||||
|
continuous_errors: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UrlDeviceListResponse {
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
page_size: number
|
||||||
|
data: UrlDeviceItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UrlDeviceListParams {
|
||||||
|
page?: number
|
||||||
|
size?: number
|
||||||
|
keyword?: string
|
||||||
|
enabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UrlDeviceCreateData {
|
||||||
|
service_identity?: string
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
target_url: string
|
||||||
|
method?: string
|
||||||
|
enabled?: boolean
|
||||||
|
interval?: number
|
||||||
|
extra?: string
|
||||||
|
tags?: string
|
||||||
|
collect_on?: boolean
|
||||||
|
collect_interval?: number
|
||||||
|
collect_last_result?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UrlDeviceUpdateData {
|
||||||
|
name?: string
|
||||||
|
description?: string
|
||||||
|
target_url?: string
|
||||||
|
method?: string
|
||||||
|
enabled?: boolean
|
||||||
|
interval?: number
|
||||||
|
extra?: string
|
||||||
|
tags?: string
|
||||||
|
collect_on?: boolean
|
||||||
|
collect_interval?: number
|
||||||
|
collect_last_result?: string
|
||||||
|
status?: string
|
||||||
|
status_code?: number
|
||||||
|
response_time?: number
|
||||||
|
last_check_time?: string
|
||||||
|
last_online_time?: string | null
|
||||||
|
last_offline_time?: string | null
|
||||||
|
continuous_errors?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UrlDeviceCollectData {
|
||||||
|
collect_on?: boolean
|
||||||
|
collect_interval?: number
|
||||||
|
collect_last_result?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchUrlDeviceList = (params?: UrlDeviceListParams) => {
|
||||||
|
return request.get<UrlDeviceListResponse>('/DC-Control/v1/url-devices', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchUrlDeviceDetail = (id: number) => {
|
||||||
|
return request.get<UrlDeviceItem>(`/DC-Control/v1/url-devices/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createUrlDevice = (data: UrlDeviceCreateData) => {
|
||||||
|
return request.post<{ message: string; id: number }>('/DC-Control/v1/url-devices', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateUrlDevice = (id: number, data: UrlDeviceUpdateData) => {
|
||||||
|
return request.put<{ message: string }>(`/DC-Control/v1/url-devices/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const patchUrlDeviceCollect = (id: number, data: UrlDeviceCollectData) => {
|
||||||
|
return request.patch<{ message: string }>(`/DC-Control/v1/url-devices/${id}/collect`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteUrlDevice = (id: number) => {
|
||||||
|
return request.delete<{ message: string }>(`/DC-Control/v1/url-devices/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UrlDeviceStatsOverview {
|
||||||
|
total: number
|
||||||
|
online: number
|
||||||
|
today_alert_count: number
|
||||||
|
avg_response_time_ms: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResponseTimePoint {
|
||||||
|
hour: string
|
||||||
|
avg_response_time_ms: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UrlDeviceResponseTimeTrend {
|
||||||
|
points: ResponseTimePoint[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatusDistributionItem {
|
||||||
|
status: string
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UrlDeviceStatusDistribution {
|
||||||
|
total: number
|
||||||
|
by_status: StatusDistributionItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchUrlDeviceStatsOverview = () => {
|
||||||
|
return request.get<UrlDeviceStatsOverview>('/DC-Control/v1/url-devices/stats/overview')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchUrlDeviceResponseTimeTrend = () => {
|
||||||
|
return request.get<UrlDeviceResponseTimeTrend>('/DC-Control/v1/url-devices/stats/response-time-trend')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchUrlDeviceStatusDistribution = () => {
|
||||||
|
return request.get<UrlDeviceStatusDistribution>('/DC-Control/v1/url-devices/stats/status-distribution')
|
||||||
|
}
|
||||||
123
src/views/ops/pages/dc/url-harvest/components/Detail.vue
Normal file
123
src/views/ops/pages/dc/url-harvest/components/Detail.vue
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<template>
|
||||||
|
<div class="detail-container">
|
||||||
|
<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="HTTP方法">{{ record.method }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="目标URL" :span="2">
|
||||||
|
<a-link :href="record.target_url" target="_blank">{{ record.target_url }}</a-link>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="描述信息" :span="2">{{ record.description || '-' }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="标签" :span="2">{{ record.tags || '-' }}</a-descriptions-item>
|
||||||
|
|
||||||
|
<a-descriptions-item label="启用状态">
|
||||||
|
<a-tag :color="record.enabled ? 'green' : 'gray'">
|
||||||
|
{{ record.enabled ? '已启用' : '已禁用' }}
|
||||||
|
</a-tag>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="采集间隔">{{ record.interval }}秒</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="运行状态">
|
||||||
|
<a-tag :color="getStatusColor(record.status)">
|
||||||
|
{{ getStatusText(record.status) }}
|
||||||
|
</a-tag>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="HTTP状态码">{{ record.status_code || '-' }}</a-descriptions-item>
|
||||||
|
|
||||||
|
<a-descriptions-item label="响应时间">{{ record.response_time ? `${record.response_time}ms` : '-' }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="连续错误次数">{{ record.continuous_errors }}</a-descriptions-item>
|
||||||
|
|
||||||
|
<a-descriptions-item label="最后检查时间">{{ formatTime(record.last_check_time) }}</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-item label="采集结果摘要" :span="2">{{ record.collect_last_result || '-' }}</a-descriptions-item>
|
||||||
|
|
||||||
|
<a-descriptions-item label="额外配置" :span="2">
|
||||||
|
<a-textarea :model-value="record.extra" :auto-size="{ minRows: 2, maxRows: 6 }" read-only />
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<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" status="danger" @click="$emit('delete')">
|
||||||
|
<template #icon><icon-delete /></template>
|
||||||
|
删除
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { IconEdit, IconDelete, IconSettings } from '@arco-design/web-vue/es/icon'
|
||||||
|
import type { UrlDeviceItem } from '@/api/ops/url-device'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
record: UrlDeviceItem
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
defineEmits(['edit', 'quick-config', 'delete'])
|
||||||
|
|
||||||
|
const getStatusColor = (status?: string) => {
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
online: 'green',
|
||||||
|
offline: 'red',
|
||||||
|
error: 'orange',
|
||||||
|
}
|
||||||
|
return colorMap[status || ''] || 'gray'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status?: string) => {
|
||||||
|
const textMap: Record<string, string> = {
|
||||||
|
online: '在线',
|
||||||
|
offline: '离线',
|
||||||
|
error: '错误',
|
||||||
|
}
|
||||||
|
return textMap[status || ''] || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.detail-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
220
src/views/ops/pages/dc/url-harvest/components/FormDialog.vue
Normal file
220
src/views/ops/pages/dc/url-harvest/components/FormDialog.vue
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
:visible="visible"
|
||||||
|
:title="isEdit ? '编辑URL监控设备' : '新增URL监控设备'"
|
||||||
|
@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="service_identity" label="服务唯一标识">
|
||||||
|
<a-input v-model="formData.service_identity" placeholder="输入为空系统自动生成 ULID" :disabled="isEdit" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="name" label="服务名称">
|
||||||
|
<a-input v-model="formData.name" placeholder="请输入服务名称" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-form-item field="target_url" label="目标监控URL">
|
||||||
|
<a-input v-model="formData.target_url" placeholder="请输入目标监控URL" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-row :gutter="20">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="method" label="HTTP方法">
|
||||||
|
<a-select v-model="formData.method" placeholder="请选择HTTP方法">
|
||||||
|
<a-option value="GET">GET</a-option>
|
||||||
|
<a-option value="POST">POST</a-option>
|
||||||
|
<a-option value="PUT">PUT</a-option>
|
||||||
|
<a-option value="DELETE">DELETE</a-option>
|
||||||
|
<a-option value="HEAD">HEAD</a-option>
|
||||||
|
<a-option value="OPTIONS">OPTIONS</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<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="tags" label="标签">
|
||||||
|
<a-input v-model="formData.tags" placeholder="多个标签,逗号间隔" />
|
||||||
|
</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-row>
|
||||||
|
|
||||||
|
<a-form-item field="extra" label="额外配置(JSON)">
|
||||||
|
<a-textarea v-model="formData.extra" placeholder='JSON格式,如:{"headers":{"Authorization":"Bearer token"}}' :rows="3" />
|
||||||
|
</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 { createUrlDevice, updateUrlDevice, type UrlDeviceCreateData, type UrlDeviceUpdateData } from '@/api/ops/url-device'
|
||||||
|
|
||||||
|
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 isEdit = computed(() => !!props.record?.id)
|
||||||
|
|
||||||
|
const formData = reactive({
|
||||||
|
service_identity: '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
target_url: '',
|
||||||
|
method: 'GET',
|
||||||
|
enabled: true,
|
||||||
|
interval: 60,
|
||||||
|
extra: '{}',
|
||||||
|
tags: '',
|
||||||
|
collect_on: true,
|
||||||
|
collect_interval: 60,
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [{ required: true, message: '请输入服务名称' }],
|
||||||
|
target_url: [{ required: true, message: '请输入目标监控URL' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(val) => {
|
||||||
|
if (val) {
|
||||||
|
if (isEdit.value && props.record) {
|
||||||
|
Object.assign(formData, {
|
||||||
|
service_identity: props.record.service_identity || '',
|
||||||
|
name: props.record.name || '',
|
||||||
|
description: props.record.description || '',
|
||||||
|
target_url: props.record.target_url || '',
|
||||||
|
method: props.record.method || 'GET',
|
||||||
|
enabled: props.record.enabled ?? true,
|
||||||
|
interval: props.record.interval || 60,
|
||||||
|
extra: props.record.extra || '{}',
|
||||||
|
tags: props.record.tags || '',
|
||||||
|
collect_on: props.record.collect_on ?? true,
|
||||||
|
collect_interval: props.record.collect_interval || 60,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Object.assign(formData, {
|
||||||
|
service_identity: '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
target_url: '',
|
||||||
|
method: 'GET',
|
||||||
|
enabled: true,
|
||||||
|
interval: 60,
|
||||||
|
extra: '{}',
|
||||||
|
tags: '',
|
||||||
|
collect_on: true,
|
||||||
|
collect_interval: 60,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleOk = async () => {
|
||||||
|
try {
|
||||||
|
await formRef.value?.validate()
|
||||||
|
|
||||||
|
confirmLoading.value = true
|
||||||
|
|
||||||
|
if (isEdit.value) {
|
||||||
|
const updateData: UrlDeviceUpdateData = {
|
||||||
|
name: formData.name,
|
||||||
|
description: formData.description,
|
||||||
|
target_url: formData.target_url,
|
||||||
|
method: formData.method,
|
||||||
|
enabled: formData.enabled,
|
||||||
|
interval: formData.interval,
|
||||||
|
extra: formData.extra,
|
||||||
|
tags: formData.tags,
|
||||||
|
collect_on: formData.collect_on,
|
||||||
|
collect_interval: formData.collect_interval,
|
||||||
|
}
|
||||||
|
await updateUrlDevice(props.record.id, updateData)
|
||||||
|
Message.success('更新成功')
|
||||||
|
} else {
|
||||||
|
const createData: UrlDeviceCreateData = {
|
||||||
|
service_identity: formData.service_identity,
|
||||||
|
name: formData.name,
|
||||||
|
description: formData.description,
|
||||||
|
target_url: formData.target_url,
|
||||||
|
method: formData.method,
|
||||||
|
enabled: formData.enabled,
|
||||||
|
interval: formData.interval,
|
||||||
|
extra: formData.extra,
|
||||||
|
tags: formData.tags,
|
||||||
|
collect_on: formData.collect_on,
|
||||||
|
collect_interval: formData.collect_interval,
|
||||||
|
}
|
||||||
|
await createUrlDevice(createData)
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal :visible="visible" title="采集配置" :mask-closable="false" :ok-loading="loading" @ok="handleSubmit" @cancel="handleCancel">
|
||||||
|
<a-form :model="form" layout="vertical">
|
||||||
|
<a-form-item label="参与周期采集">
|
||||||
|
<a-switch v-model="form.collect_on" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item v-if="form.collect_on" label="采集间隔(秒)">
|
||||||
|
<a-input-number v-model="form.collect_interval" placeholder="默认60秒" :min="10" :max="3600" style="width: 100%" allow-clear />
|
||||||
|
<template #extra>
|
||||||
|
<span style="color: #86909c">分桶间隔(秒),为 0 时回退 interval</span>
|
||||||
|
</template>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
import { patchUrlDeviceCollect, type UrlDeviceCollectData } from '@/api/ops/url-device'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean
|
||||||
|
record: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits(['update:visible', 'success'])
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
collect_on: true,
|
||||||
|
collect_interval: 60,
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(val) => {
|
||||||
|
if (val && props.record) {
|
||||||
|
form.value.collect_on = props.record.collect_on ?? true
|
||||||
|
form.value.collect_interval = props.record.collect_interval || 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data: UrlDeviceCollectData = {
|
||||||
|
collect_on: form.value.collect_on,
|
||||||
|
collect_interval: form.value.collect_interval,
|
||||||
|
}
|
||||||
|
|
||||||
|
await patchUrlDeviceCollect(props.record.id, data)
|
||||||
|
|
||||||
|
Message.success('配置成功')
|
||||||
|
emit('success')
|
||||||
|
emit('update:visible', false)
|
||||||
|
} catch (error) {
|
||||||
|
Message.error('配置失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
emit('update:visible', false)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
71
src/views/ops/pages/dc/url-harvest/config/columns.ts
Normal file
71
src/views/ops/pages/dc/url-harvest/config/columns.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
export const columns = [
|
||||||
|
{
|
||||||
|
dataIndex: 'id',
|
||||||
|
title: 'ID',
|
||||||
|
width: 80,
|
||||||
|
slotName: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'service_identity',
|
||||||
|
title: '服务标识',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'name',
|
||||||
|
title: '服务名称',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'target_url',
|
||||||
|
title: '目标URL',
|
||||||
|
width: 200,
|
||||||
|
ellipsis: true,
|
||||||
|
tooltip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'method',
|
||||||
|
title: 'HTTP方法',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'enabled',
|
||||||
|
title: '启用状态',
|
||||||
|
width: 100,
|
||||||
|
slotName: 'enabled',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'collect_on',
|
||||||
|
title: '数据采集',
|
||||||
|
width: 100,
|
||||||
|
slotName: 'data_collection',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'status',
|
||||||
|
title: '状态',
|
||||||
|
width: 100,
|
||||||
|
slotName: 'status',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'status_code',
|
||||||
|
title: '状态码',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'response_time',
|
||||||
|
title: '响应时间(ms)',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'last_check_time',
|
||||||
|
title: '最后检查时间',
|
||||||
|
width: 180,
|
||||||
|
slotName: 'last_check_time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'actions',
|
||||||
|
title: '操作',
|
||||||
|
width: 180,
|
||||||
|
fixed: 'right' as const,
|
||||||
|
slotName: 'actions',
|
||||||
|
},
|
||||||
|
]
|
||||||
22
src/views/ops/pages/dc/url-harvest/config/search-form.ts
Normal file
22
src/views/ops/pages/dc/url-harvest/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: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'enabled',
|
||||||
|
label: '启用状态',
|
||||||
|
type: 'select',
|
||||||
|
placeholder: '请选择启用状态',
|
||||||
|
options: [
|
||||||
|
{ label: '已启用', value: true },
|
||||||
|
{ label: '已禁用', value: false },
|
||||||
|
],
|
||||||
|
span: 6,
|
||||||
|
},
|
||||||
|
]
|
||||||
300
src/views/ops/pages/dc/url-harvest/index.vue
Normal file
300
src/views/ops/pages/dc/url-harvest/index.vue
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<search-table
|
||||||
|
:form-model="formModel"
|
||||||
|
:form-items="formItems"
|
||||||
|
:data="tableData"
|
||||||
|
:columns="columns"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="pagination"
|
||||||
|
title="URL监控设备管理"
|
||||||
|
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>
|
||||||
|
新增URL监控
|
||||||
|
</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 }">
|
||||||
|
{{ formatTime(record.last_check_time) }}
|
||||||
|
</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="URL监控设备详情" :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 { fetchUrlDeviceList, deleteUrlDevice, type UrlDeviceItem, type UrlDeviceListParams } from '@/api/ops/url-device'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const tableData = ref<UrlDeviceItem[]>([])
|
||||||
|
const formDialogVisible = ref(false)
|
||||||
|
const quickConfigVisible = ref(false)
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const currentRecord = ref<UrlDeviceItem | null>(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',
|
||||||
|
}
|
||||||
|
return colorMap[status || ''] || 'gray'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status?: string) => {
|
||||||
|
const textMap: Record<string, string> = {
|
||||||
|
online: '在线',
|
||||||
|
offline: '离线',
|
||||||
|
error: '错误',
|
||||||
|
}
|
||||||
|
return textMap[status || ''] || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
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 fetchUrlDeviceData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params: UrlDeviceListParams = {
|
||||||
|
page: pagination.current,
|
||||||
|
size: pagination.pageSize,
|
||||||
|
keyword: formModel.value.keyword,
|
||||||
|
enabled: formModel.value.enabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: any = await fetchUrlDeviceList(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('获取URL监控设备列表失败:', error)
|
||||||
|
Message.error('获取URL监控设备列表失败')
|
||||||
|
tableData.value = []
|
||||||
|
pagination.total = 0
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.current = 1
|
||||||
|
fetchUrlDeviceData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFormModelUpdate = (value: any) => {
|
||||||
|
formModel.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
formModel.value = {
|
||||||
|
keyword: '',
|
||||||
|
enabled: undefined,
|
||||||
|
}
|
||||||
|
pagination.current = 1
|
||||||
|
fetchUrlDeviceData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = (current: number) => {
|
||||||
|
pagination.current = current
|
||||||
|
fetchUrlDeviceData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
fetchUrlDeviceData()
|
||||||
|
Message.success('数据已刷新')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
currentRecord.value = null
|
||||||
|
formDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleQuickConfig = (record: UrlDeviceItem) => {
|
||||||
|
currentRecord.value = record
|
||||||
|
quickConfigVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (record: UrlDeviceItem) => {
|
||||||
|
currentRecord.value = record
|
||||||
|
formDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDetail = (record: UrlDeviceItem) => {
|
||||||
|
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 = () => {
|
||||||
|
fetchUrlDeviceData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = (record: UrlDeviceItem) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `确认删除URL监控设备 "${record.name}" 吗?`,
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await deleteUrlDevice(record.id)
|
||||||
|
Message.success('删除成功')
|
||||||
|
fetchUrlDeviceData()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除URL监控设备失败:', error)
|
||||||
|
Message.error('删除失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchUrlDeviceData()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'UrlHarvestManagement',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.container {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -24,9 +24,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="stats-info">
|
<div class="stats-info">
|
||||||
<div class="stats-title">正常运行</div>
|
<div class="stats-title">正常运行</div>
|
||||||
<div class="stats-value">{{ stats.normal }}</div>
|
<div class="stats-value">{{ stats.online }}</div>
|
||||||
<div class="stats-desc">
|
<div class="stats-desc">
|
||||||
<a-tag color="green" size="small">{{ stats.normalPercent }}%</a-tag>
|
<a-tag color="green" size="small">{{ Math.round(stats.total > 0 ? (stats.online / stats.total) * 100 : 0) }}%</a-tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,9 +39,9 @@
|
|||||||
<icon-exclamation-circle-fill />
|
<icon-exclamation-circle-fill />
|
||||||
</div>
|
</div>
|
||||||
<div class="stats-info">
|
<div class="stats-info">
|
||||||
<div class="stats-title">异常告警</div>
|
<div class="stats-title">今日告警</div>
|
||||||
<div class="stats-value">{{ stats.warning }}</div>
|
<div class="stats-value">{{ stats.today_alert_count }}</div>
|
||||||
<div class="stats-desc text-success">较昨日 -2</div>
|
<div class="stats-desc">当日告警数</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
@@ -54,8 +54,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="stats-info">
|
<div class="stats-info">
|
||||||
<div class="stats-title">平均响应</div>
|
<div class="stats-title">平均响应</div>
|
||||||
<div class="stats-value">{{ stats.avgResponse }}<span class="stats-unit">ms</span></div>
|
<div class="stats-value">
|
||||||
<div class="stats-desc text-success">较昨日 -15ms</div>
|
{{ Math.round(stats.avg_response_time_ms) }}
|
||||||
|
<span class="stats-unit">ms</span>
|
||||||
|
</div>
|
||||||
|
<div class="stats-desc">全设备平均</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
@@ -94,12 +97,7 @@
|
|||||||
<span class="availability-name">{{ item.name }}</span>
|
<span class="availability-name">{{ item.name }}</span>
|
||||||
<span :class="['availability-value', `status-${item.status}`]">{{ item.uptime }}%</span>
|
<span :class="['availability-value', `status-${item.status}`]">{{ item.uptime }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<a-progress
|
<a-progress :percent="item.uptime" :status="item.progressStatus" :stroke-width="8" :show-text="false" />
|
||||||
:percent="item.uptime"
|
|
||||||
:status="item.progressStatus"
|
|
||||||
:stroke-width="8"
|
|
||||||
:show-text="false"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
@@ -108,34 +106,30 @@
|
|||||||
|
|
||||||
<!-- 监控列表 -->
|
<!-- 监控列表 -->
|
||||||
<a-card title="监控列表" :bordered="false">
|
<a-card title="监控列表" :bordered="false">
|
||||||
<a-table
|
<a-table :data="tableData" :columns="columns" :loading="loading" :pagination="false" row-key="name">
|
||||||
:data="tableData"
|
|
||||||
:columns="columns"
|
|
||||||
:loading="loading"
|
|
||||||
:pagination="false"
|
|
||||||
row-key="name"
|
|
||||||
>
|
|
||||||
<!-- 状态列 -->
|
|
||||||
<template #status="{ record }">
|
<template #status="{ record }">
|
||||||
<a-tag :color="getStatusColor(record.statusValue)" bordered>
|
<a-tag :color="getStatusColor(record.status)" bordered>
|
||||||
{{ record.statusText }}
|
{{ getStatusText(record.status) }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- URL列 -->
|
|
||||||
<template #url="{ record }">
|
<template #url="{ record }">
|
||||||
<a :href="record.url" target="_blank" class="url-link">
|
<a :href="record.target_url" target="_blank" class="url-link">
|
||||||
{{ record.url }}
|
{{ record.target_url }}
|
||||||
<icon-launch class="link-icon" />
|
<icon-launch class="link-icon" />
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 趋势列 -->
|
<template #responseTime="{ record }">
|
||||||
<template #trend="{ record }">
|
{{ record.response_time ? `${Math.round(record.response_time)}ms` : '-' }}
|
||||||
<span :class="['trend-icon', record.trend > 0 ? 'trend-up' : 'trend-down']">
|
</template>
|
||||||
<icon-arrow-rise v-if="record.trend > 0" />
|
|
||||||
<icon-arrow-fall v-else />
|
<template #uptime="{ record }">
|
||||||
</span>
|
{{ record.status === 'online' ? '99.9%' : '-' }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #lastCheck="{ record }">
|
||||||
|
{{ formatLastCheckTime(record.last_check_time) }}
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
</a-card>
|
</a-card>
|
||||||
@@ -143,33 +137,32 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import {
|
import { IconEye, IconCheckCircleFill, IconExclamationCircleFill, IconClockCircle, IconLaunch } from '@arco-design/web-vue/es/icon'
|
||||||
IconEye,
|
|
||||||
IconCheckCircleFill,
|
|
||||||
IconExclamationCircleFill,
|
|
||||||
IconClockCircle,
|
|
||||||
IconPlus,
|
|
||||||
IconArrowRise,
|
|
||||||
IconArrowFall,
|
|
||||||
IconLaunch,
|
|
||||||
} from '@arco-design/web-vue/es/icon'
|
|
||||||
import Breadcrumb from '@/components/breadcrumb/index.vue'
|
|
||||||
import Chart from '@/components/chart/index.vue'
|
import Chart from '@/components/chart/index.vue'
|
||||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||||
|
import {
|
||||||
|
fetchUrlDeviceStatsOverview,
|
||||||
|
fetchUrlDeviceResponseTimeTrend,
|
||||||
|
fetchUrlDeviceStatusDistribution,
|
||||||
|
fetchUrlDeviceList,
|
||||||
|
type UrlDeviceItem,
|
||||||
|
type UrlDeviceStatsOverview,
|
||||||
|
type ResponseTimePoint,
|
||||||
|
type StatusDistributionItem,
|
||||||
|
} from '@/api/ops/url-device'
|
||||||
|
|
||||||
// 统计数据
|
const stats = ref<UrlDeviceStatsOverview>({
|
||||||
const stats = ref({
|
total: 0,
|
||||||
total: 48,
|
online: 0,
|
||||||
normal: 45,
|
today_alert_count: 0,
|
||||||
normalPercent: 94,
|
avg_response_time_ms: 0,
|
||||||
warning: 3,
|
|
||||||
avgResponse: 123,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const statsLoading = ref(false)
|
||||||
|
const chartLoading = ref(false)
|
||||||
|
|
||||||
// 表格列配置
|
|
||||||
const columns: TableColumnData[] = [
|
const columns: TableColumnData[] = [
|
||||||
{
|
{
|
||||||
title: '名称',
|
title: '名称',
|
||||||
@@ -192,103 +185,30 @@ const columns: TableColumnData[] = [
|
|||||||
{
|
{
|
||||||
title: '响应时间',
|
title: '响应时间',
|
||||||
dataIndex: 'responseTime',
|
dataIndex: 'responseTime',
|
||||||
|
slotName: 'responseTime',
|
||||||
width: 100,
|
width: 100,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '可用率',
|
title: '可用率',
|
||||||
dataIndex: 'uptime',
|
dataIndex: 'uptime',
|
||||||
|
slotName: 'uptime',
|
||||||
width: 100,
|
width: 100,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '最后检查',
|
title: '最后检查',
|
||||||
dataIndex: 'lastCheck',
|
dataIndex: 'lastCheck',
|
||||||
|
slotName: 'lastCheck',
|
||||||
width: 120,
|
width: 120,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: '趋势',
|
|
||||||
dataIndex: 'trend',
|
|
||||||
slotName: 'trend',
|
|
||||||
width: 80,
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
// 表格数据
|
const tableData = ref<UrlDeviceItem[]>([])
|
||||||
const tableData = ref([
|
|
||||||
{
|
|
||||||
name: '官网首页',
|
|
||||||
url: 'https://www.example.com',
|
|
||||||
statusValue: 'success',
|
|
||||||
statusText: '正常',
|
|
||||||
responseTime: '89ms',
|
|
||||||
uptime: '99.98%',
|
|
||||||
lastCheck: '1分钟前',
|
|
||||||
trend: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'API服务',
|
|
||||||
url: 'https://api.example.com/health',
|
|
||||||
statusValue: 'success',
|
|
||||||
statusText: '正常',
|
|
||||||
responseTime: '45ms',
|
|
||||||
uptime: '99.99%',
|
|
||||||
lastCheck: '1分钟前',
|
|
||||||
trend: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '用户中心',
|
|
||||||
url: 'https://user.example.com',
|
|
||||||
statusValue: 'warning',
|
|
||||||
statusText: '响应慢',
|
|
||||||
responseTime: '2.3s',
|
|
||||||
uptime: '98.5%',
|
|
||||||
lastCheck: '1分钟前',
|
|
||||||
trend: -1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '支付网关',
|
|
||||||
url: 'https://pay.example.com',
|
|
||||||
statusValue: 'success',
|
|
||||||
statusText: '正常',
|
|
||||||
responseTime: '120ms',
|
|
||||||
uptime: '99.95%',
|
|
||||||
lastCheck: '1分钟前',
|
|
||||||
trend: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '管理后台',
|
|
||||||
url: 'https://admin.example.com',
|
|
||||||
statusValue: 'error',
|
|
||||||
statusText: '不可达',
|
|
||||||
responseTime: '超时',
|
|
||||||
uptime: '95.2%',
|
|
||||||
lastCheck: '2分钟前',
|
|
||||||
trend: -1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '文件服务',
|
|
||||||
url: 'https://files.example.com',
|
|
||||||
statusValue: 'success',
|
|
||||||
statusText: '正常',
|
|
||||||
responseTime: '156ms',
|
|
||||||
uptime: '99.8%',
|
|
||||||
lastCheck: '1分钟前',
|
|
||||||
trend: 1,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
// 可用性数据
|
const availabilityData = ref<{ name: string; uptime: number; status: string; progressStatus: 'success' | 'warning' | 'danger' }[]>([])
|
||||||
const availabilityData = ref([
|
|
||||||
{ name: '官网首页', uptime: 99.98, status: 'success', progressStatus: 'success' as const },
|
|
||||||
{ name: 'API服务', uptime: 99.99, status: 'success', progressStatus: 'success' as const },
|
|
||||||
{ name: '用户中心', uptime: 98.5, status: 'warning', progressStatus: 'warning' as const },
|
|
||||||
{ name: '管理后台', uptime: 95.2, status: 'error', progressStatus: 'danger' as const },
|
|
||||||
])
|
|
||||||
|
|
||||||
// 响应时间图表配置
|
|
||||||
const responseTimeChartOptions = ref({
|
const responseTimeChartOptions = ref({
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
@@ -302,7 +222,7 @@ const responseTimeChartOptions = ref({
|
|||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
boundaryGap: false,
|
boundaryGap: false,
|
||||||
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '24:00'],
|
data: [] as string[],
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
@@ -310,57 +230,131 @@ const responseTimeChartOptions = ref({
|
|||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
name: '平均',
|
name: '平均响应',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
smooth: true,
|
smooth: true,
|
||||||
data: [95, 88, 145, 220, 180, 130, 92],
|
data: [] as number[],
|
||||||
areaStyle: {
|
areaStyle: {
|
||||||
opacity: 0.1,
|
opacity: 0.1,
|
||||||
},
|
},
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 2,
|
width: 2,
|
||||||
color: '#165DFF', // ArcoDesign primary color
|
color: '#165DFF',
|
||||||
},
|
},
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: '#165DFF',
|
color: '#165DFF',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'P95',
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
data: [120, 105, 180, 250, 210, 155, 115],
|
|
||||||
areaStyle: {
|
|
||||||
opacity: 0.1,
|
|
||||||
},
|
|
||||||
lineStyle: {
|
|
||||||
width: 2,
|
|
||||||
color: '#FF7D00', // ArcoDesign warning color
|
|
||||||
},
|
|
||||||
itemStyle: {
|
|
||||||
color: '#FF7D00',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取状态颜色
|
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
const colorMap: Record<string, string> = {
|
const colorMap: Record<string, string> = {
|
||||||
success: 'green',
|
online: 'green',
|
||||||
warning: 'orange',
|
offline: 'red',
|
||||||
error: 'red',
|
error: 'orange',
|
||||||
}
|
}
|
||||||
return colorMap[status] || 'gray'
|
return colorMap[status] || 'gray'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取数据
|
const getStatusText = (status: string) => {
|
||||||
const fetchData = async () => {
|
const textMap: Record<string, string> = {
|
||||||
// TODO: 从API获取数据
|
online: '正常',
|
||||||
loading.value = false
|
offline: '离线',
|
||||||
|
error: '错误',
|
||||||
|
}
|
||||||
|
return textMap[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatLastCheckTime = (time: string | null) => {
|
||||||
|
if (!time) return '-'
|
||||||
|
const date = new Date(time)
|
||||||
|
const now = new Date()
|
||||||
|
const diffMs = now.getTime() - date.getTime()
|
||||||
|
const diffMins = Math.floor(diffMs / 60000)
|
||||||
|
if (diffMins < 1) return '刚刚'
|
||||||
|
if (diffMins < 60) return `${diffMins}分钟前`
|
||||||
|
const diffHours = Math.floor(diffMins / 60)
|
||||||
|
if (diffHours < 24) return `${diffHours}小时前`
|
||||||
|
return `${Math.floor(diffHours / 24)}天前`
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchStatsOverview = async () => {
|
||||||
|
statsLoading.value = true
|
||||||
|
try {
|
||||||
|
const response: any = await fetchUrlDeviceStatsOverview()
|
||||||
|
if (response && response.data) {
|
||||||
|
stats.value = response.data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取统计概览失败:', error)
|
||||||
|
} finally {
|
||||||
|
statsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchResponseTimeTrend = async () => {
|
||||||
|
chartLoading.value = true
|
||||||
|
try {
|
||||||
|
const response: any = await fetchUrlDeviceResponseTimeTrend()
|
||||||
|
console.log('response', response)
|
||||||
|
if (response && response.details && response.details.points) {
|
||||||
|
const points: ResponseTimePoint[] = response.details.points
|
||||||
|
responseTimeChartOptions.value.xAxis.data = points.map((p) => {
|
||||||
|
const date = new Date(p.hour)
|
||||||
|
return `${String(date.getHours()).padStart(2, '0')}:00`
|
||||||
|
})
|
||||||
|
responseTimeChartOptions.value.series[0].data = points.map((p) => p.avg_response_time_ms)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取响应时间趋势失败:', error)
|
||||||
|
} finally {
|
||||||
|
chartLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchStatusDistribution = async () => {
|
||||||
|
try {
|
||||||
|
const response: any = await fetchUrlDeviceStatusDistribution()
|
||||||
|
if (response && response.data && response.data.by_status) {
|
||||||
|
const distribution: StatusDistributionItem[] = response.data.by_status
|
||||||
|
const total = response.data.total || 0
|
||||||
|
availabilityData.value = distribution.map((item) => {
|
||||||
|
const uptime = total > 0 ? (item.status === 'online' ? 99.9 : item.status === 'error' ? 95 : 98) : 0
|
||||||
|
const progressStatus = item.status === 'online' ? 'success' : item.status === 'error' ? 'danger' : 'warning'
|
||||||
|
return {
|
||||||
|
name: `${item.status} (${item.count})`,
|
||||||
|
uptime,
|
||||||
|
status: item.status,
|
||||||
|
progressStatus,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取状态分布失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchDeviceList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response: any = await fetchUrlDeviceList({ page: 1, size: 100 })
|
||||||
|
if (response && response.details) {
|
||||||
|
tableData.value = response.details.list || []
|
||||||
|
} else if (response && response.data) {
|
||||||
|
tableData.value = response.data.data || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取设备列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
await Promise.all([fetchStatsOverview(), fetchResponseTimeTrend(), fetchStatusDistribution(), fetchDeviceList()])
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchData()
|
fetchData()
|
||||||
})
|
})
|
||||||
@@ -487,11 +481,11 @@ export default {
|
|||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
|
|
||||||
&.legend-dot-1 {
|
&.legend-dot-1 {
|
||||||
background-color: #165DFF;
|
background-color: #165dff;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.legend-dot-2 {
|
&.legend-dot-2 {
|
||||||
background-color: #FF7D00;
|
background-color: #ff7d00;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -556,16 +550,4 @@ export default {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
.trend-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
|
|
||||||
&.trend-up {
|
|
||||||
color: rgb(var(--success-6));
|
|
||||||
}
|
|
||||||
|
|
||||||
&.trend-down {
|
|
||||||
color: rgb(var(--danger-6));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
1
tsconfig.tsbuildinfo
Normal file
1
tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user