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 class="stats-info">
|
||||
<div class="stats-title">正常运行</div>
|
||||
<div class="stats-value">{{ stats.normal }}</div>
|
||||
<div class="stats-value">{{ stats.online }}</div>
|
||||
<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>
|
||||
@@ -39,9 +39,9 @@
|
||||
<icon-exclamation-circle-fill />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">异常告警</div>
|
||||
<div class="stats-value">{{ stats.warning }}</div>
|
||||
<div class="stats-desc text-success">较昨日 -2</div>
|
||||
<div class="stats-title">今日告警</div>
|
||||
<div class="stats-value">{{ stats.today_alert_count }}</div>
|
||||
<div class="stats-desc">当日告警数</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
@@ -54,8 +54,11 @@
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">平均响应</div>
|
||||
<div class="stats-value">{{ stats.avgResponse }}<span class="stats-unit">ms</span></div>
|
||||
<div class="stats-desc text-success">较昨日 -15ms</div>
|
||||
<div class="stats-value">
|
||||
{{ Math.round(stats.avg_response_time_ms) }}
|
||||
<span class="stats-unit">ms</span>
|
||||
</div>
|
||||
<div class="stats-desc">全设备平均</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
@@ -94,12 +97,7 @@
|
||||
<span class="availability-name">{{ item.name }}</span>
|
||||
<span :class="['availability-value', `status-${item.status}`]">{{ item.uptime }}%</span>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="item.uptime"
|
||||
:status="item.progressStatus"
|
||||
:stroke-width="8"
|
||||
:show-text="false"
|
||||
/>
|
||||
<a-progress :percent="item.uptime" :status="item.progressStatus" :stroke-width="8" :show-text="false" />
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
@@ -108,34 +106,30 @@
|
||||
|
||||
<!-- 监控列表 -->
|
||||
<a-card title="监控列表" :bordered="false">
|
||||
<a-table
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="name"
|
||||
>
|
||||
<!-- 状态列 -->
|
||||
<a-table :data="tableData" :columns="columns" :loading="loading" :pagination="false" row-key="name">
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.statusValue)" bordered>
|
||||
{{ record.statusText }}
|
||||
<a-tag :color="getStatusColor(record.status)" bordered>
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- URL列 -->
|
||||
<template #url="{ record }">
|
||||
<a :href="record.url" target="_blank" class="url-link">
|
||||
{{ record.url }}
|
||||
<a :href="record.target_url" target="_blank" class="url-link">
|
||||
{{ record.target_url }}
|
||||
<icon-launch class="link-icon" />
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<!-- 趋势列 -->
|
||||
<template #trend="{ record }">
|
||||
<span :class="['trend-icon', record.trend > 0 ? 'trend-up' : 'trend-down']">
|
||||
<icon-arrow-rise v-if="record.trend > 0" />
|
||||
<icon-arrow-fall v-else />
|
||||
</span>
|
||||
<template #responseTime="{ record }">
|
||||
{{ record.response_time ? `${Math.round(record.response_time)}ms` : '-' }}
|
||||
</template>
|
||||
|
||||
<template #uptime="{ record }">
|
||||
{{ record.status === 'online' ? '99.9%' : '-' }}
|
||||
</template>
|
||||
|
||||
<template #lastCheck="{ record }">
|
||||
{{ formatLastCheckTime(record.last_check_time) }}
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
@@ -143,33 +137,32 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import {
|
||||
IconEye,
|
||||
IconCheckCircleFill,
|
||||
IconExclamationCircleFill,
|
||||
IconClockCircle,
|
||||
IconPlus,
|
||||
IconArrowRise,
|
||||
IconArrowFall,
|
||||
IconLaunch,
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
import Breadcrumb from '@/components/breadcrumb/index.vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { IconEye, IconCheckCircleFill, IconExclamationCircleFill, IconClockCircle, IconLaunch } from '@arco-design/web-vue/es/icon'
|
||||
import Chart from '@/components/chart/index.vue'
|
||||
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({
|
||||
total: 48,
|
||||
normal: 45,
|
||||
normalPercent: 94,
|
||||
warning: 3,
|
||||
avgResponse: 123,
|
||||
const stats = ref<UrlDeviceStatsOverview>({
|
||||
total: 0,
|
||||
online: 0,
|
||||
today_alert_count: 0,
|
||||
avg_response_time_ms: 0,
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const statsLoading = ref(false)
|
||||
const chartLoading = ref(false)
|
||||
|
||||
// 表格列配置
|
||||
const columns: TableColumnData[] = [
|
||||
{
|
||||
title: '名称',
|
||||
@@ -192,103 +185,30 @@ const columns: TableColumnData[] = [
|
||||
{
|
||||
title: '响应时间',
|
||||
dataIndex: 'responseTime',
|
||||
slotName: 'responseTime',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '可用率',
|
||||
dataIndex: 'uptime',
|
||||
slotName: 'uptime',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '最后检查',
|
||||
dataIndex: 'lastCheck',
|
||||
slotName: 'lastCheck',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '趋势',
|
||||
dataIndex: 'trend',
|
||||
slotName: 'trend',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
},
|
||||
]
|
||||
|
||||
// 表格数据
|
||||
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 tableData = ref<UrlDeviceItem[]>([])
|
||||
|
||||
// 可用性数据
|
||||
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 availabilityData = ref<{ name: string; uptime: number; status: string; progressStatus: 'success' | 'warning' | 'danger' }[]>([])
|
||||
|
||||
// 响应时间图表配置
|
||||
const responseTimeChartOptions = ref({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
@@ -302,7 +222,7 @@ const responseTimeChartOptions = ref({
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '24:00'],
|
||||
data: [] as string[],
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
@@ -310,57 +230,131 @@ const responseTimeChartOptions = ref({
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '平均',
|
||||
name: '平均响应',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [95, 88, 145, 220, 180, 130, 92],
|
||||
data: [] as number[],
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#165DFF', // ArcoDesign primary color
|
||||
color: '#165DFF',
|
||||
},
|
||||
itemStyle: {
|
||||
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 colorMap: Record<string, string> = {
|
||||
success: 'green',
|
||||
warning: 'orange',
|
||||
error: 'red',
|
||||
online: 'green',
|
||||
offline: 'red',
|
||||
error: 'orange',
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
// TODO: 从API获取数据
|
||||
loading.value = false
|
||||
const getStatusText = (status: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
online: '正常',
|
||||
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(() => {
|
||||
fetchData()
|
||||
})
|
||||
@@ -487,11 +481,11 @@ export default {
|
||||
margin-right: 6px;
|
||||
|
||||
&.legend-dot-1 {
|
||||
background-color: #165DFF;
|
||||
background-color: #165dff;
|
||||
}
|
||||
|
||||
&.legend-dot-2 {
|
||||
background-color: #FF7D00;
|
||||
background-color: #ff7d00;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -556,16 +550,4 @@ export default {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.trend-icon {
|
||||
font-size: 16px;
|
||||
|
||||
&.trend-up {
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
&.trend-down {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</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