【调整】监控页面整体优化

This commit is contained in:
chenzhihua
2025-07-17 11:04:36 +08:00
parent 624fc06f3e
commit 260601055f
81 changed files with 3316 additions and 925 deletions

View File

@@ -363,10 +363,20 @@ export const useApiFormController = (props: ApiFormControllerProps): ApiFormCont
password: {
trigger: 'input',
validator: (rule: FormItemRule, value: string, callback: (error?: Error) => void) => {
// SSH 类型的密码字段在密码模式下是必填的,在密钥模式下是可选的
if (param.value.type === 'ssh') {
const sshConfig = param.value.config as SshAccessConfig
if (sshConfig?.mode === 'password' && !value) {
return callback(new Error($t('t_0_1747711335067')))
}
// 密钥模式下密码是可选的,不需要验证
callback()
return
}
if (!value) {
const mapTips = {
westcn: $t('t_1_1747365603108'),
ssh: $t('t_0_1747711335067'),
lecdn: '请输入密码',
}
return callback(new Error(mapTips[param.value.type as keyof typeof mapTips]))
@@ -593,8 +603,9 @@ export const useApiFormController = (props: ApiFormControllerProps): ApiFormCont
// 根据不同类型渲染不同的表单项
switch (param.value.type) {
case 'ssh':
items.push(
case 'ssh': {
// SSH 基础配置项
const sshBaseItems = [
useFormCustom(() => {
return (
<NGrid cols={24} xGap={4}>
@@ -616,18 +627,43 @@ export const useApiFormController = (props: ApiFormControllerProps): ApiFormCont
{ label: $t('t_48_1745289355714'), value: 'password' },
{ label: $t('t_1_1746667588689'), value: 'key' },
]),
(param.value.config as SshAccessConfig)?.mode === 'password'
? useFormInput($t('t_48_1745289355714'), 'config.password', {
]
// 根据认证模式添加对应的字段
const sshAuthItems = []
if ((param.value.config as SshAccessConfig)?.mode === 'password') {
sshAuthItems.push(
useFormInput($t('t_48_1745289355714'), 'config.password', {
type: 'password',
showPasswordOn: 'click',
allowInput: noSideSpace,
}),
)
} else if ((param.value.config as SshAccessConfig)?.mode === 'key') {
sshAuthItems.push(
useFormTextarea($t('t_1_1746667588689'), 'config.key', {
rows: 3,
placeholder: $t('t_0_1747709067998'),
}),
// 私钥密码输入框(使用 password 字段)
useFormInput(
'私钥密码',
'config.password',
{
type: 'password',
showPasswordOn: 'click',
allowInput: noSideSpace,
})
: useFormTextarea($t('t_1_1746667588689'), 'config.key', {
rows: 3,
placeholder: $t('t_0_1747709067998'),
}),
)
placeholder: '请输入私钥密码(可选)',
},
{ showRequireMark: false },
),
)
}
// 合并所有 SSH 配置项
items.push(...sshBaseItems, ...sshAuthItems)
break
}
case '1panel':
items.push(
// 1Panel版本选择下拉框

View File

@@ -0,0 +1,646 @@
import { defineComponent, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { NButton, NCard, NSpin, NIcon, NSpace, NText, NEmpty, NDataTable, NPagination } from 'naive-ui'
import { ArrowLeft, Information, ErrorOutline } from '@vicons/carbon'
import { useThemeCssVar } from '@baota/naive-ui/theme'
import { useTable } from '@baota/naive-ui/hooks'
// 工具和钩子
import { useError } from '@baota/hooks/error'
// API和类型
import { getMonitorDetail, getMonitorErrorRecord } from '@/api/monitor'
import type { MonitorDetailInfo, ErrorRecord, GetErrorRecordParams, CertChainNode } from '@/types/monitor'
/**
* 错误列表卡片组件
*/
const ErrorListCard = defineComponent({
name: 'ErrorListCard',
props: {
monitorId: {
type: Number,
required: true,
},
},
setup(props) {
const { handleError } = useError()
/**
* 格式化日期时间
*/
const formatDateTime = (dateStr: string): string => {
if (!dateStr) return '-'
return new Date(dateStr).toLocaleString('zh-CN')
}
console.log('ErrorListCard 渲染monitorId:', props.monitorId)
// 错误记录表格配置
const errorColumns = [
{
title: '错误时间',
key: 'create_time',
width: 200,
render: (row: ErrorRecord) => (
<span class="text-[1.4rem] sm:text-[1.5rem] font-mono">{formatDateTime(row.create_time)}</span>
),
},
{
title: '错误消息',
key: 'msg',
render: (row: ErrorRecord) => (
<div class="text-[1.4rem] sm:text-[1.5rem] text-red-600 dark:text-red-400 break-words leading-relaxed">
<div class="max-w-full overflow-hidden">
<div class="whitespace-pre-wrap break-all">{row.msg || '-'}</div>
</div>
</div>
),
},
]
// 错误记录请求函数
const errorRecordRequest = async <T = ErrorRecord,>(params: GetErrorRecordParams) => {
console.log('🔍 错误记录请求开始,参数:', params)
console.log('🔍 API端点: /v1/monitor/get_err_record')
try {
const apiInstance = getMonitorErrorRecord(params)
console.log('🔍 API实例创建成功:', apiInstance)
const response = await apiInstance.fetch()
console.log('✅ 错误记录API响应成功:', response)
const { data, count, status, message } = response
console.log('📊 响应详情:', { data, count, status, message })
const result = {
list: (data || []) as T[],
total: count || 0,
}
console.log('🎯 错误记录处理结果:', result)
return result
} catch (error: unknown) {
console.error('❌ 错误记录请求失败:', error)
if (error instanceof Error) {
console.error('❌ 错误消息:', error.message)
console.error('❌ 错误堆栈:', error.stack)
}
handleError(error).default('获取错误记录失败,请稍后重试')
return { list: [] as T[], total: 0 }
}
}
// 创建表格实例
const {
TableComponent,
PageComponent,
loading: errorLoading,
fetch: fetchErrorList,
} = useTable<ErrorRecord, GetErrorRecordParams>({
config: errorColumns,
request: errorRecordRequest,
defaultValue: ref({
id: props.monitorId,
p: 1,
limit: 10,
}),
alias: { page: 'p', pageSize: 'limit' },
watchValue: ['p', 'limit'], // 监听分页参数变化
})
// 组件挂载时加载错误记录
onMounted(async () => {
console.log('🚀 ErrorListCard 挂载,开始加载错误记录')
console.log('🚀 监控ID:', props.monitorId)
// 使用useTable的fetch方法加载数据
try {
console.log('📊 使用useTable fetch方法...')
await fetchErrorList()
console.log('✅ 错误记录加载完成')
} catch (error) {
console.error('❌ 错误记录加载失败:', error)
}
})
return () => (
<NCard
title="错误列表"
class="h-fit [&_.n-card-header_.n-card-header__main]:text-[1.8rem] [&_.n-card-header_.n-card-header__main]:font-medium"
bordered
>
{{
'header-extra': () => (
<NIcon size="20" color="var(--n-error-color)">
<ErrorOutline />
</NIcon>
),
default: () => (
<div class="space-y-4">
<NSpin show={errorLoading.value}>
<TableComponent>
{{
empty: () => (
<NEmpty
description="暂无错误记录"
size="large"
class="[&_.n-empty__description]:text-[1.6rem] py-8"
>
{{
icon: () => (
<NIcon size="48" color="var(--n-text-color-disabled)">
<ErrorOutline />
</NIcon>
),
}}
</NEmpty>
),
}}
</TableComponent>
</NSpin>
<div class="flex justify-end mt-4">
<PageComponent />
</div>
</div>
),
}}
</NCard>
)
},
})
/**
* @component MonitorDetailView
* @description 监控详情页面组件
* 负责展示监控的详细信息,包括基本信息和证书内容信息
*/
export default defineComponent({
name: 'MonitorDetailView',
setup() {
const route = useRoute()
const router = useRouter()
const { handleError } = useError()
// 响应式数据
const loading = ref(false)
const detailData = ref<MonitorDetailInfo | null>(null)
const monitorId = ref<number>(Number(route.query.id))
// 获取主题CSS变量
const cssVars = useThemeCssVar([
'contentPadding',
'borderColor',
'headerHeight',
'iconColorHover',
'successColor',
'errorColor',
'warningColor',
'primaryColor',
])
/**
* 返回监控列表页面
*/
const goBack = (): void => {
router.push('/monitor')
}
/**
* 获取监控详情数据
*/
const fetchDetailData = async (): Promise<void> => {
if (!monitorId.value) {
handleError(new Error('监控ID无效')).default('无效的监控ID请返回监控列表重试')
return
}
try {
loading.value = true
const { data, status } = await getMonitorDetail({ id: monitorId.value }).fetch()
if (status && data) {
detailData.value = data
} else {
handleError(new Error('获取监控详情失败')).default('获取监控详情失败,请稍后重试')
}
} catch (error) {
handleError(error).default('获取监控详情失败,请稍后重试')
} finally {
loading.value = false
}
}
/**
* 格式化日期时间
*/
const formatDateTime = (dateStr: string): string => {
if (!dateStr) return '-'
return new Date(dateStr).toLocaleString('zh-CN')
}
/**
* 格式化证书有效期范围
*/
const formatValidityPeriod = (notBefore: string, notAfter: string): string => {
if (!notBefore || !notAfter) return '-'
const startDate = formatDateTime(notBefore)
const endDate = formatDateTime(notAfter)
return `${startDate}${endDate}`
}
/**
* 获取验证状态显示文本和颜色
*/
const getValidStatus = (valid: number) => {
return valid === 1
? { text: '有效', color: 'var(--n-success-color)' }
: { text: '无效', color: 'var(--n-error-color)' }
}
/**
* 获取剩余天数的颜色
*/
const getDaysLeftColor = (daysLeft: number): string => {
if (daysLeft <= 7) return 'var(--n-error-color)'
if (daysLeft <= 30) return 'var(--n-warning-color)'
return 'var(--n-success-color)'
}
/**
* 递归渲染证书链节点
*/
const renderCertChainNode = (node: CertChainNode, level: number = 0, index: number = 0): JSX.Element[] => {
const elements: JSX.Element[] = []
// 确定证书类型和样式
const getCertTypeInfo = (level: number, hasChildren: boolean) => {
if (level === 0) {
return {
label: '终端证书',
color: 'bg-green-500',
textColor: 'text-green-700 dark:text-green-400',
}
} else if (hasChildren) {
return {
label: `中间证书 #${index + 1}`,
color: 'bg-blue-500',
textColor: 'text-blue-700 dark:text-blue-400',
}
} else {
return {
label: '根证书',
color: 'bg-purple-500',
textColor: 'text-purple-700 dark:text-purple-400',
}
}
}
const typeInfo = getCertTypeInfo(level, node.children && node.children.length > 0)
// 渲染当前节点
elements.push(
<div
key={`cert-${level}-${index}`}
class="flex items-center space-x-3 p-3 bg-white dark:bg-gray-800 rounded-lg shadow-sm"
style={{ marginLeft: `${level * 1.5}rem` }}
>
<div class={`w-3 h-3 ${typeInfo.color} rounded-full flex-shrink-0 shadow-sm`}></div>
<div class="flex-1">
<span class={`text-[1.4rem] sm:text-[1.5rem] font-medium ${typeInfo.textColor}`}>{typeInfo.label}</span>
<div class="text-[1.3rem] sm:text-[1.4rem] text-gray-600 dark:text-gray-400 font-mono mt-1 break-words">
{node.common_name}
</div>
</div>
</div>,
)
// 递归渲染子节点
if (node.children && node.children.length > 0) {
node.children.forEach((child: CertChainNode, childIndex: number) => {
elements.push(...renderCertChainNode(child, level + 1, childIndex))
})
}
return elements
}
// 组件挂载时获取数据
onMounted(() => {
fetchDetailData()
})
return () => (
<div class="mx-auto max-w-[1800px] w-full p-4 sm:p-6 lg:p-8" style={cssVars.value}>
<NSpin show={loading.value}>
{/* 页面头部 */}
<div class="mb-6 sm:mb-8">
<NSpace align="center" class="mb-4 sm:mb-5">
<NButton
size="medium"
type="default"
onClick={goBack}
class="text-[1.4rem] sm:text-[1.5rem]"
renderIcon={() => (
<NIcon>
<ArrowLeft />
</NIcon>
)}
>
</NButton>
</NSpace>
<h1 class="text-[2.2rem] sm:text-[2.4rem] lg:text-[2.6rem] font-semibold text-gray-800 dark:text-gray-200 break-words leading-tight">
{detailData.value?.name || '监控详情'} -
</h1>
</div>
{/* 内容区域 */}
{detailData.value ? (
<div class="space-y-6 sm:space-y-8 lg:space-y-10">
{/* 合并的监控和证书详情模块 */}
<NCard
title="监控详情与证书信息"
class="[&_.n-card-header_.n-card-header__main]:text-[1.8rem] [&_.n-card-header_.n-card-header__main]:font-medium"
bordered
>
{{
'header-extra': () => (
<NIcon size="24" color="var(--n-primary-color)">
<Information />
</NIcon>
),
default: () => (
<div class="space-y-10">
{/* 核心状态信息 - 最重要 */}
<div>
<h4 class="font-semibold mb-6 text-primary text-[1.9rem] sm:text-[2rem] border-b-2 border-primary/20 pb-3">
</h4>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<div class="bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/30 dark:to-blue-800/30 p-5 rounded-xl border border-blue-200 dark:border-blue-700">
<NText
depth="3"
class="text-[1.5rem] sm:text-[1.6rem] font-medium text-blue-700 dark:text-blue-300"
>
</NText>
<div class="mt-3 font-bold text-[1.7rem] sm:text-[1.8rem]">
{detailData.value && (
<span style={{ color: getValidStatus(detailData.value.valid).color }}>
{getValidStatus(detailData.value.valid).text}
</span>
)}
</div>
</div>
<div class="bg-gradient-to-br from-green-50 to-green-100 dark:from-green-900/30 dark:to-green-800/30 p-5 rounded-xl border border-green-200 dark:border-green-700">
<NText
depth="3"
class="text-[1.5rem] sm:text-[1.6rem] font-medium text-green-700 dark:text-green-300"
>
</NText>
<div class="mt-3 font-bold text-[1.7rem] sm:text-[1.8rem]">
{detailData.value && (
<span style={{ color: getDaysLeftColor(detailData.value.days_left) }}>
{detailData.value.days_left}
</span>
)}
</div>
</div>
<div class="bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/30 dark:to-purple-800/30 p-5 rounded-xl border border-purple-200 dark:border-purple-700">
<NText
depth="3"
class="text-[1.5rem] sm:text-[1.6rem] font-medium text-purple-700 dark:text-purple-300"
>
</NText>
<div class="mt-3 font-bold text-[1.7rem] sm:text-[1.8rem]">
<span
style={{
color:
(detailData.value?.err_count || 0) > 0
? 'var(--n-error-color)'
: 'var(--n-success-color)',
}}
>
{detailData.value?.err_count || 0}
</span>
</div>
</div>
<div class="bg-gradient-to-br from-orange-50 to-orange-100 dark:from-orange-900/30 dark:to-orange-800/30 p-5 rounded-xl border border-orange-200 dark:border-orange-700">
<NText
depth="3"
class="text-[1.5rem] sm:text-[1.6rem] font-medium text-orange-700 dark:text-orange-300"
>
</NText>
<div class="mt-3 font-bold text-[1.7rem] sm:text-[1.8rem] uppercase">
{detailData.value?.monitor_type || '-'}
</div>
</div>
</div>
</div>
{/* 监控配置信息 */}
<div>
<h4 class="font-semibold mb-6 text-primary text-[1.9rem] sm:text-[2rem] border-b-2 border-primary/20 pb-3">
</h4>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="space-y-5">
<div class="bg-gray-50 dark:bg-gray-800/50 p-5 rounded-lg">
<NText depth="3" class="text-[1.5rem] sm:text-[1.6rem] font-medium">
</NText>
<div class="mt-3 font-medium text-[1.6rem] sm:text-[1.7rem] break-words">
{detailData.value?.name || '-'}
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-800/50 p-5 rounded-lg">
<NText depth="3" class="text-[1.5rem] sm:text-[1.6rem] font-medium">
</NText>
<div class="mt-3 font-medium text-[1.6rem] sm:text-[1.7rem]">
<a
href={`https://${detailData.value?.target}`}
target="_blank"
rel="noopener noreferrer"
class="text-primary hover:underline break-all"
>
{detailData.value?.target || '-'}
</a>
</div>
</div>
</div>
<div class="space-y-5">
<div class="bg-gray-50 dark:bg-gray-800/50 p-5 rounded-lg">
<NText depth="3" class="text-[1.5rem] sm:text-[1.6rem] font-medium">
</NText>
<div class="mt-3 font-medium text-[1.6rem] sm:text-[1.7rem]">
{detailData.value?.ca || '-'}
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-800/50 p-5 rounded-lg">
<NText depth="3" class="text-[1.5rem] sm:text-[1.6rem] font-medium">
</NText>
<div class="mt-3 font-medium text-[1.6rem] sm:text-[1.7rem] break-words">
{formatDateTime(detailData.value?.last_time || '')}
</div>
</div>
{detailData.value?.tls_version && (
<div class="bg-gray-50 dark:bg-gray-800/50 p-5 rounded-lg">
<NText depth="3" class="text-[1.5rem] sm:text-[1.6rem] font-medium">
TLS版本
</NText>
<div class="mt-3 font-medium text-[1.6rem] sm:text-[1.7rem]">
{detailData.value.tls_version}
</div>
</div>
)}
</div>
</div>
</div>
{/* 证书基本信息 */}
<div>
<h4 class="font-semibold mb-6 text-success text-[1.9rem] sm:text-[2rem] border-b-2 border-success/20 pb-3">
</h4>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="bg-green-50 dark:bg-green-900/20 p-6 rounded-xl border border-green-200 dark:border-green-800">
<NText
depth="3"
class="text-[1.5rem] sm:text-[1.6rem] font-medium text-green-700 dark:text-green-400"
>
(CN)
</NText>
<div class="mt-3 font-mono text-[1.6rem] sm:text-[1.7rem] text-green-800 dark:text-green-300 break-all leading-relaxed">
{detailData.value?.common_name || '-'}
</div>
</div>
<div class="bg-blue-50 dark:bg-blue-900/20 p-6 rounded-xl border border-blue-200 dark:border-blue-800">
<NText
depth="3"
class="text-[1.5rem] sm:text-[1.6rem] font-medium text-blue-700 dark:text-blue-400"
>
(SAN)
</NText>
<div class="mt-3 font-mono text-[1.6rem] sm:text-[1.7rem] text-blue-800 dark:text-blue-300 break-all leading-relaxed">
{detailData.value?.sans || '-'}
</div>
</div>
</div>
</div>
{/* 有效期详情 */}
<div>
<h4 class="font-semibold mb-6 text-success text-[1.9rem] sm:text-[2rem] border-b-2 border-success/20 pb-3">
</h4>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-900/30 dark:to-emerald-900/30 p-6 rounded-xl border border-green-200 dark:border-green-700">
<NText
depth="3"
class="text-[1.5rem] sm:text-[1.6rem] font-medium text-green-700 dark:text-green-300"
>
</NText>
<div class="mt-3 font-mono text-[1.6rem] sm:text-[1.7rem] text-green-600 dark:text-green-400 break-words">
{formatDateTime(detailData.value?.not_before || '')}
</div>
</div>
<div class="bg-gradient-to-br from-orange-50 to-red-50 dark:from-orange-900/30 dark:to-red-900/30 p-6 rounded-xl border border-orange-200 dark:border-orange-700">
<NText
depth="3"
class="text-[1.5rem] sm:text-[1.6rem] font-medium text-orange-700 dark:text-orange-300"
>
</NText>
<div class="mt-3 font-mono text-[1.6rem] sm:text-[1.7rem] text-orange-600 dark:text-orange-400 break-words">
{formatDateTime(detailData.value?.not_after || '')}
</div>
</div>
<div class="bg-gradient-to-br from-purple-50 to-indigo-50 dark:from-purple-900/30 dark:to-indigo-900/30 p-6 rounded-xl border border-purple-200 dark:border-purple-700 md:col-span-2 lg:col-span-1">
<NText
depth="3"
class="text-[1.5rem] sm:text-[1.6rem] font-medium text-purple-700 dark:text-purple-300"
>
</NText>
<div class="mt-3 font-bold text-[1.7rem] sm:text-[1.8rem]">
{detailData.value && (
<span style={{ color: getDaysLeftColor(detailData.value.days_left) }}>
{detailData.value.days_left}
</span>
)}
</div>
</div>
</div>
<div class="mt-6 bg-gray-50 dark:bg-gray-800/50 p-5 rounded-lg">
<NText depth="3" class="text-[1.5rem] sm:text-[1.6rem] font-medium">
</NText>
<div class="mt-3 font-medium text-[1.6rem] sm:text-[1.7rem] break-words">
{formatValidityPeriod(
detailData.value?.not_before || '',
detailData.value?.not_after || '',
)}
</div>
</div>
</div>
{/* 证书链路信息 - 视觉增强 */}
{detailData.value?.cert_chain && (
<div>
<h4 class="font-semibold mb-6 text-success text-[1.9rem] sm:text-[2rem] border-b-2 border-success/20 pb-3">
</h4>
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 p-8 rounded-xl border border-blue-200 dark:border-blue-800">
<div class="space-y-5">{renderCertChainNode(detailData.value.cert_chain)}</div>
</div>
</div>
)}
{/* 验证错误信息 */}
{detailData.value?.verify_error && (
<div>
<h4 class="font-semibold mb-6 text-error text-[1.9rem] sm:text-[2rem] border-b-2 border-error/20 pb-3">
</h4>
<div class="bg-red-50 dark:bg-red-900/20 p-6 rounded-xl border border-red-200 dark:border-red-800">
<div class="text-red-600 dark:text-red-300 text-[1.6rem] sm:text-[1.7rem] leading-relaxed break-words">
{detailData.value.verify_error}
</div>
</div>
</div>
)}
</div>
),
}}
</NCard>
{/* 错误列表模块 */}
<ErrorListCard monitorId={monitorId.value} />
</div>
) : (
!loading.value && (
<NCard bordered class="text-center [&_.n-empty__description]:text-[1.6rem]">
<NEmpty description="未找到监控详情数据" size="large">
{{
extra: () => (
<NButton type="primary" class="text-[1.4rem]" onClick={goBack}>
</NButton>
),
}}
</NEmpty>
</NCard>
)
)}
</NSpin>
</div>
)
},
})

View File

@@ -0,0 +1,299 @@
import { defineComponent, ref, computed } from 'vue'
import { NTabs, NTabPane, NUpload, NUploadDragger, NButton, NSpace, NText, NIcon, NCard, NDivider } from 'naive-ui'
import { CloudUploadOutline, DocumentOutline, DownloadOutline } from '@vicons/ionicons5'
import { $t } from '@locales/index'
import { useMessage } from '@baota/naive-ui/hooks'
import { useError } from '@baota/hooks/error'
import { fileImportMonitor, downloadMonitorTemplate } from '@/api/monitor'
import type { UploadFileInfo } from 'naive-ui'
import type { SupportedFileType, FileUploadStatus } from '@/types/monitor'
/**
* 导入监控弹窗组件
* @description 提供文件导入和模板下载功能的弹窗界面
*/
export default defineComponent({
name: 'ImportMonitorModal',
setup(_, { emit }) {
// 消息提示和错误处理
const message = useMessage()
const { handleError } = useError()
// 当前激活的标签页
const activeTab = ref<'import' | 'template'>('import')
// 文件上传状态
const uploadStatus = ref<FileUploadStatus>({
uploading: false,
progress: 0,
success: false,
})
// 支持的文件格式
const supportedFormats: SupportedFileType[] = ['txt', 'csv', 'json', 'xlsx']
// 文件格式验证
const validateFileType = (file: File): boolean => {
const extension = file.name.split('.').pop()?.toLowerCase() as SupportedFileType
return supportedFormats.includes(extension)
}
// 文件大小验证限制为10MB
const validateFileSize = (file: File): boolean => {
const maxSize = 10 * 1024 * 1024 // 10MB
return file.size <= maxSize
}
/**
* 处理文件上传前的验证
*/
const handleBeforeUpload = (data: { file: UploadFileInfo; fileList: UploadFileInfo[] }): boolean => {
const file = data.file.file
if (!file) return false
// 验证文件类型
if (!validateFileType(file)) {
message.error($t('t_9_1753000000001'))
return false
}
// 验证文件大小
if (!validateFileSize(file)) {
message.error($t('t_10_1753000000001'))
return false
}
return true
}
/**
* 处理文件上传
*/
const handleFileUpload = async (options: {
file: UploadFileInfo
onProgress: (e: { percent: number }) => void
}) => {
const file = options.file.file
if (!file) return
try {
uploadStatus.value = {
uploading: true,
progress: 0,
success: false,
}
// 模拟上传进度
const progressInterval = setInterval(() => {
if (uploadStatus.value.progress < 90) {
uploadStatus.value.progress += 10
options.onProgress({ percent: uploadStatus.value.progress })
}
}, 200)
// 创建FormData并上传文件
const formData = new FormData()
formData.append('file', file)
// 使用原生fetch进行文件上传因为useApi可能不支持FormData
const response = await fetch('/v1/monitor/file_add_monitor', {
method: 'POST',
body: formData,
})
if (!response.ok) {
throw new Error(`上传失败: ${response.statusText}`)
}
const result = await response.json()
clearInterval(progressInterval)
uploadStatus.value = {
uploading: false,
progress: 100,
success: true,
}
options.onProgress({ percent: 100 })
// 显示上传结果
if (result.data) {
const { success_count, failed_count } = result.data
message.success(
$t('t_14_1753000000001')
.replace('{success}', success_count.toString())
.replace('{failed}', failed_count.toString()),
)
// 通知父组件刷新数据
emit('success')
} else {
message.success($t('t_15_1753000000001'))
emit('success')
}
} catch (error) {
uploadStatus.value = {
uploading: false,
progress: 0,
success: false,
error: $t('t_13_1753000000001'),
}
handleError(error).default($t('t_16_1753000000001'))
}
}
/**
* 下载模板文件
*/
const handleDownloadTemplate = async (type: SupportedFileType) => {
try {
// 使用原生fetch下载模板文件
const response = await fetch(`/v1/monitor/template?type=${type}`, {
method: 'GET',
})
if (!response.ok) {
throw new Error(`下载模板失败: ${response.statusText}`)
}
const blob = await response.blob()
// 根据文件类型设置正确的文件名
const fileName = `monitor_template.${type}`
// 创建下载链接
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = fileName
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
message.success(`${type.toUpperCase()} ${$t('t_17_1753000000001')}`)
} catch (error) {
handleError(error).default($t('t_18_1753000000001'))
}
}
// 计算上传提示文本
const uploadTipText = computed(() => {
if (uploadStatus.value.uploading) {
return `${$t('t_11_1753000000001')} ${uploadStatus.value.progress}%`
}
if (uploadStatus.value.success) {
return $t('t_12_1753000000001')
}
if (uploadStatus.value.error) {
return uploadStatus.value.error
}
return $t('t_4_1753000000001')
})
return () => (
<div class="import-monitor-modal">
<NTabs value={activeTab.value} onUpdateValue={(value) => (activeTab.value = value as 'import' | 'template')}>
{/* 文件导入标签页 */}
<NTabPane name="import" tab={$t('t_1_1753000000001')}>
<div class="p-6">
<NCard title={$t('t_3_1753000000001')} class="mb-4">
<NUpload
multiple={false}
accept=".txt,.csv,.json,.xlsx"
showFileList={false}
onBeforeUpload={handleBeforeUpload}
customRequest={handleFileUpload}
>
<NUploadDragger class="min-h-[200px]">
<div class="text-center">
<NIcon size={48} class="text-primary mb-4">
<CloudUploadOutline />
</NIcon>
<NText class="text-lg block mb-2">{uploadTipText.value}</NText>
<NText depth="3" class="text-sm">
{$t('t_5_1753000000001')}
</NText>
</div>
</NUploadDragger>
</NUpload>
</NCard>
<NDivider />
<NCard title={$t('t_6_1753000000001')} class="mt-4">
<div class="space-y-3">
<div>
<NText strong>CSV格式</NText>
<NText depth="3" class="ml-2">
,,,
</NText>
</div>
<div>
<NText strong>JSON格式</NText>
<NText depth="3" class="ml-2">{`[{"name":"","domain":"","protocol":"","port":""}]`}</NText>
</div>
<div>
<NText strong>Excel格式</NText>
<NText depth="3" class="ml-2">
</NText>
</div>
</div>
</NCard>
</div>
</NTabPane>
{/* 模板下载标签页 */}
<NTabPane name="template" tab={$t('t_2_1753000000001')}>
<div class="p-6">
<NCard title={$t('t_7_1753000000001')}>
<NText class="block mb-6" depth="3">
{$t('t_8_1753000000001')}
</NText>
<NSpace vertical size="large">
{supportedFormats.map((format) => (
<div key={format} class="flex items-center justify-between p-4 border border-gray-200 rounded-lg">
<div class="flex items-center">
<NIcon size={24} class="mr-3 text-primary">
<DocumentOutline />
</NIcon>
<div>
<NText strong class="block">
{format.toUpperCase()}
</NText>
<NText depth="3" class="text-sm">
{format === 'xlsx' ? 'Excel' : format.toUpperCase()}
</NText>
</div>
</div>
<NButton
type="primary"
size="small"
onClick={() => handleDownloadTemplate(format)}
v-slots={{
icon: () => (
<NIcon>
<DownloadOutline />
</NIcon>
),
}}
>
</NButton>
</div>
))}
</NSpace>
</NCard>
</div>
</NTabPane>
</NTabs>
</div>
)
},
})

View File

@@ -1,9 +1,10 @@
import { defineComponent, onMounted } from 'vue'
import { NButton, NInput } from 'naive-ui'
import { NButton, NSpace } from 'naive-ui'
import { Search } from '@vicons/carbon'
import { $t } from '@locales/index'
import { useThemeCssVar } from '@baota/naive-ui/theme'
import { RouterView } from '@baota/router'
import { useController } from './useController'
@@ -18,8 +19,17 @@ export default defineComponent({
name: 'MonitorManage',
setup() {
// 使用控制器获取数据和方法
const { TableComponent, PageComponent, SearchComponent, fetch, openAddForm, isDetectionAddMonitor } =
useController()
const {
TableComponent,
PageComponent,
ColumnSettingsComponent,
SearchComponent,
fetch,
openAddForm,
openImportForm,
isDetectionAddMonitor,
hasChildRoutes,
} = useController()
// 获取主题CSS变量
const cssVar = useThemeCssVar(['contentPadding', 'borderColor', 'headerHeight', 'iconColorHover'])
@@ -36,36 +46,50 @@ export default defineComponent({
return () => (
<div class="h-full flex flex-col" style={cssVar.value}>
<div class="mx-auto max-w-[1600px] w-full p-6">
<BaseComponent
v-slots={{
// 头部左侧区域 - 添加按钮
headerLeft: () => (
<NButton type="primary" size="large" class="px-5" onClick={openAddForm}>
{$t('t_11_1745289354516')}
</NButton>
),
// 头部右侧区域 - 搜索框
headerRight: () => <SearchComponent placeholder={$t('t_12_1745289356974')} />,
// 内容区域 - 监控表格
content: () => (
<div class="rounded-lg">
<TableComponent
size="medium"
scroll-x="1800"
v-slots={{
empty: () => <EmptyState addButtonText={$t('t_11_1745289354516')} onAddClick={openAddForm} />,
}}
/>
</div>
),
// 底部右侧区域 - 分页组件
footerRight: () => (
<div class="mt-4 flex justify-end">
<PageComponent />
</div>
),
}}
></BaseComponent>
{hasChildRoutes.value ? (
<RouterView />
) : (
<BaseComponent
v-slots={{
// 头部左侧区域 - 添加按钮和导入按钮
headerLeft: () => (
<NSpace>
<NButton type="primary" size="large" class="px-5" onClick={openAddForm}>
{$t('t_11_1745289354516')}
</NButton>
<NButton type="default" size="large" class="px-5" onClick={openImportForm}>
{$t('t_0_1753000000001')}
</NButton>
</NSpace>
),
// 头部右侧区域 - 搜索框和列设置
headerRight: () => (
<NSpace align="center" size="medium">
<SearchComponent placeholder={$t('t_12_1745289356974')} />
<ColumnSettingsComponent />
</NSpace>
),
// 内容区域 - 监控表格
content: () => (
<div class="rounded-lg">
<TableComponent
size="medium"
scroll-x="1800"
v-slots={{
empty: () => <EmptyState addButtonText={$t('t_11_1745289354516')} onAddClick={openAddForm} />,
}}
/>
</div>
),
// 底部右侧区域 - 分页组件
footerRight: () => (
<div class="mt-4 flex justify-end">
<PageComponent />
</div>
),
}}
></BaseComponent>
)}
</div>
</div>
)

View File

@@ -1,6 +1,6 @@
import { computed, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { FormRules, NButton, NSpace, NSwitch, type DataTableColumns } from 'naive-ui'
import { FormRules, NButton, NSpace, NSwitch, NText, NDivider, type DataTableColumns } from 'naive-ui'
// 钩子和工具
import {
@@ -20,11 +20,12 @@ import { $t } from '@locales/index'
// Store和组件
import { useStore } from './useStore'
import MonitorForm from './components/AddMonitorModel'
import NotifyProviderSelect from '@components/NotifyProviderSelect'
import ImportMonitorModal from './components/ImportMonitorModal'
import NotifyProviderMultiSelect from '@components/notifyProviderMultiSelect'
import TypeIcon from '@components/TypeIcon'
// 类型导入
import type { Ref } from 'vue'
import type { Ref, ComputedRef } from 'vue'
import type {
AddSiteMonitorParams,
SiteMonitorItem,
@@ -39,6 +40,7 @@ interface MonitorControllerExposes {
// 表格相关
TableComponent: ReturnType<typeof useTable>['TableComponent']
PageComponent: ReturnType<typeof useTable>['PageComponent']
ColumnSettingsComponent: ReturnType<typeof useTable>['ColumnSettingsComponent']
SearchComponent: ReturnType<typeof useSearch>['SearchComponent']
loading: Ref<boolean>
// param: Ref<SiteMonitorListParams>
@@ -47,7 +49,12 @@ interface MonitorControllerExposes {
// 表单和操作相关
openAddForm: () => void
openImportForm: () => void
openDetailPage: (row: SiteMonitorItem) => void
isDetectionAddMonitor: () => void
// 路由相关
hasChildRoutes: ComputedRef<boolean>
}
// 从Store中获取方法
@@ -99,6 +106,9 @@ export const useController = (): MonitorControllerExposes => {
const route = useRoute()
const router = useRouter()
// 判断是否为子路由
const hasChildRoutes = computed(() => route.path !== '/monitor')
/**
* 创建表格列配置
* @description 定义监控表格的列结构和渲染方式
@@ -112,22 +122,27 @@ export const useController = (): MonitorControllerExposes => {
},
{
title: $t('t_17_1745227838561'),
key: 'site_domain',
key: 'target',
width: 180,
render: (row: SiteMonitorItem) => {
return (
<NButton tag="a" text type="primary" href={`https://${row.site_domain}`} target="_blank">
{row.site_domain}
</NButton>
<a
href={`https://${row.target}`}
target="_blank"
rel="noopener noreferrer"
style="color: var(--primary-color); text-decoration: none;"
>
{row.target}
</a>
)
},
},
{
title: $t('t_14_1745289354902'),
key: 'cert_domain',
key: 'common_name',
width: 180,
render: (row: SiteMonitorItem) => {
return row.cert_domain || '-'
return row.common_name || '-'
},
},
{
@@ -137,14 +152,29 @@ export const useController = (): MonitorControllerExposes => {
},
{
title: $t('t_16_1745289354902'),
key: 'state',
key: 'valid',
width: 100,
render: (row: SiteMonitorItem) => {
return row.valid === 1 ? '有效' : '无效'
},
},
{
title: $t('t_17_1745289355715'),
key: 'end_time',
key: 'not_after',
width: 150,
render: (row: SiteMonitorItem) => row.end_time + '(' + row.end_day + ')',
render: (row: SiteMonitorItem) => {
// 检查到期时间和剩余天数是否有效
const hasValidNotAfter = row.not_after && row.not_after !== 'undefined' && row.not_after.trim() !== ''
const hasValidDaysLeft = row.days_left !== undefined && row.days_left !== null && !isNaN(row.days_left)
// 如果任一数据无效,显示占位符
if (!hasValidNotAfter || !hasValidDaysLeft) {
return '-'
}
// 正常情况下显示完整的到期时间信息
return `${row.not_after}(${row.days_left}天)`
},
},
{
title: $t('t_2_1750399515511'),
@@ -166,10 +196,29 @@ export const useController = (): MonitorControllerExposes => {
},
{
title: $t('t_18_1745289354598'),
key: 'report_type',
width: 150,
key: 'report_types',
width: 200, // 增加宽度以适应多选标签
render: (row: SiteMonitorItem) => {
return <TypeIcon icon={row.report_type} />
// 确保 report_types 数据格式正确处理
let reportTypes: string | string[]
if (typeof row.report_types === 'string') {
// 如果是逗号分隔的字符串,转换为数组以支持多选显示
reportTypes = row.report_types ? row.report_types.split(',').filter(Boolean) : []
} else if (Array.isArray(row.report_types)) {
// 如果已经是数组,直接使用
reportTypes = row.report_types
} else {
// 其他情况,设为空数组
reportTypes = []
}
// 如果没有通知类型,显示占位符
if (!reportTypes || (Array.isArray(reportTypes) && reportTypes.length === 0)) {
return <span style="color: var(--n-text-color-disabled); font-size: 12px;">-</span>
}
return <TypeIcon icon={reportTypes} />
},
},
{
@@ -188,12 +237,15 @@ export const useController = (): MonitorControllerExposes => {
{
title: $t('t_8_1745215914610'),
key: 'actions',
width: 150,
width: 200,
fixed: 'right' as const,
align: 'right',
render: (row: SiteMonitorItem) => {
return (
<NSpace justify="end">
<NButton size="tiny" strong secondary type="info" onClick={() => openDetailPage(row)}>
</NButton>
<NButton size="tiny" strong secondary type="primary" onClick={() => openEditForm(row)}>
{$t('t_11_1745215915429')}
</NButton>
@@ -210,13 +262,16 @@ export const useController = (): MonitorControllerExposes => {
* 表格实例
* @description 创建表格实例并管理相关状态
*/
const { TableComponent, PageComponent, loading, param, fetch } = useTable<SiteMonitorItem, SiteMonitorListParams>({
const { TableComponent, PageComponent, ColumnSettingsComponent, loading, param, fetch } = useTable<
SiteMonitorItem,
SiteMonitorListParams
>({
config: createColumns(),
request: fetchMonitorList,
defaultValue: { p: 1, limit: 10, search: '' },
alias: { page: 'p', pageSize: 'limit' },
watchValue: ['p', 'limit'],
storage: 'monitorPageSize',
storage: 'monitorColumnSettings',
})
// 搜索实例
@@ -243,6 +298,22 @@ export const useController = (): MonitorControllerExposes => {
})
}
/**
* 打开导入监控弹窗
* @description 显示导入监控的弹窗
*/
const openImportForm = (): void => {
useModal({
title: $t('t_0_1753000000001'),
area: 600,
component: ImportMonitorModal,
footer: false,
onUpdateShow(show) {
if (!show) fetch()
},
})
}
/**
* 打开编辑监控弹窗
* @description 显示编辑监控的表单弹窗
@@ -261,6 +332,18 @@ export const useController = (): MonitorControllerExposes => {
})
}
/**
* 打开监控详情页面
* @description 跳转到监控详情页面
* @param {SiteMonitorItem} row - 监控项数据
*/
const openDetailPage = (row: SiteMonitorItem): void => {
router.push({
path: '/monitor/detail',
query: { id: row.id.toString() },
})
}
/**
* 确认删除监控
* @description 显示删除确认对话框
@@ -302,13 +385,20 @@ export const useController = (): MonitorControllerExposes => {
}
return {
loading,
fetch,
// 表格相关
TableComponent,
PageComponent,
ColumnSettingsComponent,
SearchComponent,
isDetectionAddMonitor,
loading,
fetch,
// 表单和操作相关
openAddForm,
openImportForm,
openDetailPage,
isDetectionAddMonitor,
// 路由相关
hasChildRoutes,
}
}
@@ -327,7 +417,7 @@ interface MonitorFormControllerExposes {
*/
export const useMonitorFormController = (data: UpdateSiteMonitorParams | null = null): MonitorFormControllerExposes => {
// 表单工具
const { useFormInput, useFormCustom, useFormInputNumber } = useFormHooks()
const { useFormInput, useFormCustom, useFormInputNumber, useFormSelect, useFormSwitch } = useFormHooks()
// 加载遮罩
const { open: openLoad, close: closeLoad } = useLoadingMask({ text: '正在提交信息,请稍后...' })
@@ -335,27 +425,71 @@ export const useMonitorFormController = (data: UpdateSiteMonitorParams | null =
// 消息和对话框
const { confirm } = useModalHooks()
/**
* 创建分组标题
* @param title 分组标题文本
* @returns 分组标题配置
*/
const createGroupTitle = (title: string) => {
return useFormCustom(() => <NDivider style="margin: 12px 0 8px 0; font-weight: 500;">{title}</NDivider>)
}
/**
* 表单配置
* @description 定义表单字段和布局
*/
const config = computed(() => [
useFormInput('名称', 'name'),
useFormInput('域名/IP地址', 'domain'),
useFormInput('域名/IP地址', 'target'),
useFormSelect('协议类型', 'monitor_type', [
{ label: 'HTTPS', value: 'https' },
{ label: 'SMTP', value: 'smtp' },
]),
useFormInputNumber('周期(分钟)', 'cycle', { class: 'w-full' }),
useFormCustom(() => {
// 确保 report_types 是数组格式
const currentValue = Array.isArray(monitorForm.value.report_types)
? monitorForm.value.report_types
: monitorForm.value.report_types
? typeof monitorForm.value.report_types === 'string'
? monitorForm.value.report_types.split(',').filter(Boolean)
: [monitorForm.value.report_types]
: []
return (
<NotifyProviderSelect
path="report_type"
<NotifyProviderMultiSelect
path="report_types"
isAddMode={true}
value={monitorForm.value.report_type}
value={currentValue}
valueType="type"
onUpdate:value={(item) => {
monitorForm.value.report_type = item.value
onUpdate:value={(items) => {
// 将选中的多个值转换为数组格式,存储类型值
monitorForm.value.report_types = items.map((item) => item.type || item.value)
}}
/>
)
}),
// 到期提醒设置分组
createGroupTitle('到期提醒设置'),
useFormInputNumber('提前天数', 'advance_day', { class: 'w-full', min: 1, max: 365 }),
useFormCustom(() => {
const advanceDay = monitorForm.value.advance_day || 90
return (
<NText
depth="3"
style="font-size: 12px; margin-top: -8px; margin-bottom: 8px; display: block; color: var(--n-text-color-disabled);"
>
{advanceDay}
</NText>
)
}),
// 连续失败通知设置分组
createGroupTitle('连续失败通知设置'),
useFormInputNumber('重复发送间隔(次数)', 'repeat_send_gap', { class: 'w-full', min: 1, max: 100 }),
useFormSwitch('启用状态', 'active', {
checkedValue: 1,
uncheckedValue: 0,
}),
])
/**
@@ -363,11 +497,11 @@ export const useMonitorFormController = (data: UpdateSiteMonitorParams | null =
*/
const rules = {
name: { required: true, message: '请输入名称', trigger: 'input' },
domain: {
target: {
required: true,
message: '请输入正确的域名或IP地址',
trigger: 'input',
validator: (rule: any, value: any, callback: any) => {
validator: (_rule: any, value: any, callback: any) => {
if (!isDomainWithPort(value)) {
callback(new Error('请输入正确的域名或IP地址支持域名:端口或IP:端口格式)'))
} else {
@@ -375,8 +509,30 @@ export const useMonitorFormController = (data: UpdateSiteMonitorParams | null =
}
},
},
monitor_type: { required: true, message: '请选择协议类型', trigger: 'change' },
cycle: { required: true, message: '请输入周期', trigger: 'input', type: 'number', min: 1, max: 365 },
report_type: { required: true, message: '请选择消息通知类型', trigger: 'change' },
report_types: {
required: true,
message: '请选择消息通知类型',
trigger: 'change',
validator: (_rule: any, value: any, callback: any) => {
if (!value || (Array.isArray(value) && value.length === 0)) {
callback(new Error('请至少选择一种消息通知类型'))
} else {
callback()
}
},
},
advance_day: { required: true, message: '请输入提前天数', trigger: 'input', type: 'number', min: 1, max: 365 },
repeat_send_gap: {
required: true,
message: '请输入重复发送间隔',
trigger: 'input',
type: 'number',
min: 1,
max: 100,
},
active: { required: true, message: '请选择启用状态', trigger: 'change', type: 'number' },
} as FormRules
/**
@@ -387,10 +543,9 @@ export const useMonitorFormController = (data: UpdateSiteMonitorParams | null =
const request = async (params: AddSiteMonitorParams | UpdateSiteMonitorParams): Promise<void> => {
try {
if (data) {
await updateExistingMonitor({ ...params, id: data.id })
await updateExistingMonitor({ ...params, id: data.id } as UpdateSiteMonitorParams)
} else {
const { id, ...rest } = params
await addNewMonitor(rest)
await addNewMonitor(params as AddSiteMonitorParams)
}
} catch (error) {
handleError(error).default('添加失败')

View File

@@ -51,9 +51,13 @@ export const useMonitorStore = defineStore('monitor-store', (): MonitorStoreExpo
const monitorForm = ref<AddSiteMonitorParams & UpdateSiteMonitorParams>({
id: 0,
name: '',
domain: '',
target: '',
monitor_type: 'https', // 默认协议类型为HTTPS
report_types: [], // 默认为空数组,支持多选
cycle: 1,
report_type: '',
repeat_send_gap: 10, // 默认重复发送间隔10次
active: 1, // 默认启用状态
advance_day: 90, // 默认提前90天
})
// -------------------- 方法定义 --------------------
@@ -84,7 +88,13 @@ export const useMonitorStore = defineStore('monitor-store', (): MonitorStoreExpo
*/
const addNewMonitor = async (params: AddSiteMonitorParams): Promise<boolean> => {
try {
const { fetch, message } = addSiteMonitor(params)
// 转换 report_types 数组为逗号分隔的字符串
const processedParams = {
...params,
report_types: Array.isArray(params.report_types) ? params.report_types.join(',') : params.report_types || '',
}
const { fetch, message } = addSiteMonitor(processedParams)
message.value = true
await fetch()
return true
@@ -102,7 +112,13 @@ export const useMonitorStore = defineStore('monitor-store', (): MonitorStoreExpo
*/
const updateExistingMonitor = async (params: UpdateSiteMonitorParams): Promise<boolean> => {
try {
const { fetch, message } = updateSiteMonitor(params)
// 转换 report_types 数组为逗号分隔的字符串
const processedParams = {
...params,
report_types: Array.isArray(params.report_types) ? params.report_types.join(',') : params.report_types || '',
}
const { fetch, message } = updateSiteMonitor(processedParams)
message.value = true
await fetch()
return true
@@ -150,12 +166,37 @@ export const useMonitorStore = defineStore('monitor-store', (): MonitorStoreExpo
/**
* 更新监控表单
* @description 用于编辑时填充表单数据
* @description 用于编辑时填充表单数据,支持单选到多选的数据转换
* @param {UpdateSiteMonitorParams | null} params - 更新监控参数
*/
const updateMonitorForm = (params: UpdateSiteMonitorParams | null = monitorForm.value): void => {
const { id, name, domain, cycle, report_type } = params || monitorForm.value
monitorForm.value = { id, name, domain, cycle, report_type }
const { id, name, target, monitor_type, report_types, cycle, repeat_send_gap, active, advance_day } =
params || monitorForm.value
// 处理 report_types 的数据格式转换
let processedReportTypes: string[]
if (typeof report_types === 'string') {
// 如果是逗号分隔的字符串,转换为数组
processedReportTypes = report_types ? report_types.split(',').filter(Boolean) : []
} else if (Array.isArray(report_types)) {
// 如果已经是数组,直接使用
processedReportTypes = report_types
} else {
// 其他情况,设为空数组
processedReportTypes = []
}
monitorForm.value = {
id,
name,
target,
monitor_type,
report_types: processedReportTypes,
cycle,
repeat_send_gap,
active,
advance_day,
}
}
/**
@@ -166,9 +207,13 @@ export const useMonitorStore = defineStore('monitor-store', (): MonitorStoreExpo
monitorForm.value = {
id: 0,
name: '',
domain: '',
target: '',
monitor_type: 'https', // 默认协议类型为HTTPS
report_types: [], // 重置为空数组
cycle: 1,
report_type: '',
repeat_send_gap: 10, // 默认重复发送间隔10次
active: 1, // 默认启用状态
advance_day: 90, // 默认提前90天
}
}