mirror of
https://gitee.com/mirrors/AllinSSL.git
synced 2026-03-10 00:31:10 +08:00
【调整】监控页面整体优化
This commit is contained in:
@@ -50,7 +50,7 @@ export const updateSiteMonitor = Mock.mock(/\/siteMonitor\/upd_site_monitor/, 'p
|
||||
}))
|
||||
|
||||
// 删除站点监控
|
||||
export const deleteSiteMonitor = Mock.mock(/\/siteMonitor\/del_site_monitor/, 'post', () => ({
|
||||
export const deleteSiteMonitor = Mock.mock(/\/monitor\/del_monitor/, 'post', () => ({
|
||||
code: 0,
|
||||
count: 0,
|
||||
data: null,
|
||||
|
||||
@@ -3,9 +3,16 @@ import type { useAxiosReturn } from '@baota/hooks/axios'
|
||||
import type {
|
||||
AddSiteMonitorParams,
|
||||
DeleteSiteMonitorParams,
|
||||
FileImportMonitorParams,
|
||||
FileImportMonitorResponse,
|
||||
GetErrorRecordParams,
|
||||
GetErrorRecordResponse,
|
||||
GetMonitorDetailParams,
|
||||
GetMonitorDetailResponse,
|
||||
SetSiteMonitorParams,
|
||||
SiteMonitorListParams,
|
||||
SiteMonitorListResponse,
|
||||
TemplateDownloadParams,
|
||||
UpdateSiteMonitorParams,
|
||||
} from '@/types/monitor' // Sorted types
|
||||
import type { AxiosResponseData } from '@/types/public'
|
||||
@@ -21,15 +28,17 @@ import { useApi } from '@api/index'
|
||||
export const getSiteMonitorList = (
|
||||
params?: SiteMonitorListParams,
|
||||
): useAxiosReturn<SiteMonitorListResponse, SiteMonitorListParams> =>
|
||||
useApi<SiteMonitorListResponse, SiteMonitorListParams>('/v1/siteMonitor/get_list', params)
|
||||
useApi<SiteMonitorListResponse, SiteMonitorListParams>('/v1/monitor/get_list', params)
|
||||
|
||||
/**
|
||||
* @description 新增站点监控
|
||||
* @param {AddSiteMonitorParams} [params] 请求参数
|
||||
* @returns {useAxiosReturn<AxiosResponseData, AddSiteMonitorParams>} 新增站点监控的组合式 API 调用封装。包含响应数据、加载状态及执行函数。
|
||||
*/
|
||||
export const addSiteMonitor = (params?: AddSiteMonitorParams): useAxiosReturn<AxiosResponseData, AddSiteMonitorParams> =>
|
||||
useApi<AxiosResponseData, AddSiteMonitorParams>('/v1/siteMonitor/add_site_monitor', params)
|
||||
export const addSiteMonitor = (
|
||||
params?: AddSiteMonitorParams,
|
||||
): useAxiosReturn<AxiosResponseData, AddSiteMonitorParams> =>
|
||||
useApi<AxiosResponseData, AddSiteMonitorParams>('/v1/monitor/add_monitor', params)
|
||||
|
||||
/**
|
||||
* @description 修改站点监控
|
||||
@@ -39,7 +48,7 @@ export const addSiteMonitor = (params?: AddSiteMonitorParams): useAxiosReturn<Ax
|
||||
export const updateSiteMonitor = (
|
||||
params?: UpdateSiteMonitorParams,
|
||||
): useAxiosReturn<AxiosResponseData, UpdateSiteMonitorParams> =>
|
||||
useApi<AxiosResponseData, UpdateSiteMonitorParams>('/v1/siteMonitor/upd_site_monitor', params)
|
||||
useApi<AxiosResponseData, UpdateSiteMonitorParams>('/v1/monitor/upd_monitor', params)
|
||||
|
||||
/**
|
||||
* @description 删除站点监控
|
||||
@@ -49,12 +58,61 @@ export const updateSiteMonitor = (
|
||||
export const deleteSiteMonitor = (
|
||||
params?: DeleteSiteMonitorParams,
|
||||
): useAxiosReturn<AxiosResponseData, DeleteSiteMonitorParams> =>
|
||||
useApi<AxiosResponseData, DeleteSiteMonitorParams>('/v1/siteMonitor/del_site_monitor', params)
|
||||
useApi<AxiosResponseData, DeleteSiteMonitorParams>('/v1/monitor/del_monitor', params)
|
||||
|
||||
/**
|
||||
* @description 启用/禁用站点监控
|
||||
* @param {SetSiteMonitorParams} [params] 请求参数
|
||||
* @returns {useAxiosReturn<AxiosResponseData, SetSiteMonitorParams>} 启用/禁用站点监控的组合式 API 调用封装。包含响应数据、加载状态及执行函数。
|
||||
*/
|
||||
export const setSiteMonitor = (params?: SetSiteMonitorParams): useAxiosReturn<AxiosResponseData, SetSiteMonitorParams> =>
|
||||
useApi<AxiosResponseData, SetSiteMonitorParams>('/v1/siteMonitor/set_site_monitor', params)
|
||||
export const setSiteMonitor = (
|
||||
params?: SetSiteMonitorParams,
|
||||
): useAxiosReturn<AxiosResponseData, SetSiteMonitorParams> =>
|
||||
useApi<AxiosResponseData, SetSiteMonitorParams>('/v1/monitor/set_monitor', params)
|
||||
|
||||
/**
|
||||
* @description 获取监控详情信息
|
||||
* @param {GetMonitorDetailParams} [params] 请求参数
|
||||
* @returns {useAxiosReturn<GetMonitorDetailResponse, GetMonitorDetailParams>} 获取监控详情信息的组合式 API 调用封装。包含响应数据、加载状态及执行函数。
|
||||
*/
|
||||
export const getMonitorDetail = (
|
||||
params?: GetMonitorDetailParams,
|
||||
): useAxiosReturn<GetMonitorDetailResponse, GetMonitorDetailParams> =>
|
||||
useApi<GetMonitorDetailResponse, GetMonitorDetailParams>('/v1/monitor/get_monitor_info', params)
|
||||
|
||||
/**
|
||||
* @description 获取监控错误记录
|
||||
* @param {GetErrorRecordParams} [params] 请求参数
|
||||
* @returns {useAxiosReturn<GetErrorRecordResponse, GetErrorRecordParams>} 获取监控错误记录的组合式 API 调用封装。包含响应数据、加载状态及执行函数。
|
||||
*/
|
||||
export const getMonitorErrorRecord = (
|
||||
params?: GetErrorRecordParams,
|
||||
): useAxiosReturn<GetErrorRecordResponse, GetErrorRecordParams> =>
|
||||
useApi<GetErrorRecordResponse, GetErrorRecordParams>('/v1/monitor/get_err_record', params)
|
||||
|
||||
/**
|
||||
* @description 文件导入监控
|
||||
* @param {FileImportMonitorParams} [params] 请求参数
|
||||
* @returns {useAxiosReturn<FileImportMonitorResponse, FileImportMonitorParams>} 文件导入监控的组合式 API 调用封装。包含响应数据、加载状态及执行函数。
|
||||
*/
|
||||
export const fileImportMonitor = (
|
||||
params?: FileImportMonitorParams,
|
||||
): useAxiosReturn<FileImportMonitorResponse, FileImportMonitorParams> =>
|
||||
useApi<FileImportMonitorResponse, FileImportMonitorParams>('/v1/monitor/file_add_monitor', params)
|
||||
|
||||
/**
|
||||
* @description 下载监控模板
|
||||
* @param {TemplateDownloadParams} params 请求参数
|
||||
* @returns {Promise<Blob>} 返回模板文件的Blob对象
|
||||
*/
|
||||
export const downloadMonitorTemplate = async (params: TemplateDownloadParams): Promise<Blob> => {
|
||||
const response = await fetch(`/v1/monitor/template?type=${params.type}`, {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`下载模板失败: ${response.statusText}`)
|
||||
}
|
||||
|
||||
return response.blob()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/* 通知提供商多选组件样式 */
|
||||
|
||||
/* 主容器样式 */
|
||||
.notifyProviderMultiSelect {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 选择器容器样式 - 确保有足够空间显示多选标签 */
|
||||
.selectContainer {
|
||||
width: 100%;
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
/* 修复 NSelect 输入框区域的垂直对齐问题 */
|
||||
.selectContainer :global(.n-base-selection) {
|
||||
align-items: center !important;
|
||||
min-height: 2.5rem !important;
|
||||
}
|
||||
|
||||
/* 修复选择器输入框内容区域对齐 */
|
||||
.selectContainer :global(.n-base-selection-input) {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
min-height: 2.5rem !important;
|
||||
}
|
||||
|
||||
/* 修复标签容器的垂直对齐 */
|
||||
.selectContainer :global(.n-base-selection-tags) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
min-height: 2.5rem;
|
||||
padding: 0.25rem;
|
||||
max-height: none !important; /* 移除高度限制,允许换行 */
|
||||
align-items: center !important;
|
||||
align-content: center !important;
|
||||
}
|
||||
|
||||
/* 修复下拉箭头的垂直对齐 */
|
||||
.selectContainer :global(.n-base-suffix) {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* 修复清除按钮的垂直对齐 */
|
||||
.selectContainer :global(.n-base-clear) {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* 修复输入框整体的垂直对齐 */
|
||||
.selectContainer :global(.n-base-selection-input-tag) {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
min-height: 2.5rem !important;
|
||||
}
|
||||
|
||||
/* 确保占位符文本垂直居中 */
|
||||
.selectContainer :global(.n-base-selection-placeholder) {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
line-height: 2.5rem !important;
|
||||
}
|
||||
|
||||
/* 修复多选模式下的输入框对齐 */
|
||||
.selectContainer :global(.n-base-selection-input-tag input) {
|
||||
line-height: 2.5rem !important;
|
||||
height: 2.5rem !important;
|
||||
}
|
||||
|
||||
/* 标签样式优化 */
|
||||
.selectContainer :global(.n-tag) {
|
||||
max-width: 100%;
|
||||
flex-shrink: 0;
|
||||
margin: 2px 4px 2px 0 !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
/* 标签内容样式 */
|
||||
.selectContainer :global(.n-tag__content) {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 0.25rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* 响应式布局 - 保持按钮原始样式 */
|
||||
|
||||
/* 确保选择器在不同屏幕尺寸下的响应式表现 */
|
||||
@media (max-width: 640px) {
|
||||
.selectContainer {
|
||||
min-height: 3rem;
|
||||
}
|
||||
|
||||
.selectContainer :global(.n-base-selection-tags) {
|
||||
min-height: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 优化标签在小屏幕上的显示 */
|
||||
@media (max-width: 480px) {
|
||||
.selectContainer :global(.n-tag) {
|
||||
font-size: 0.75rem;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.selectContainer :global(.n-tag__content) {
|
||||
gap: 0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 确保图标不会被压缩 */
|
||||
.selectContainer :global(.svg-icon) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 优化空状态显示 */
|
||||
.emptyState {
|
||||
text-align: center;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
/* 保持按钮原始样式,不做额外修改 */
|
||||
@@ -0,0 +1,229 @@
|
||||
// External Libraries
|
||||
import { defineComponent, computed } from 'vue'
|
||||
import { NButton, NDivider, NFlex, NFormItemGi, NGi, NGrid, NSelect, NText, NCheckboxGroup, NCheckbox } from 'naive-ui'
|
||||
|
||||
// Type Imports
|
||||
import type { VNode, PropType } from 'vue'
|
||||
import type { SelectOption } from 'naive-ui'
|
||||
import type { NotifyProviderOption, NotifyProviderMultiSelectProps } from './types'
|
||||
|
||||
// Absolute Internal Imports - Components
|
||||
import SvgIcon from '@components/SvgIcon'
|
||||
// Absolute Internal Imports - Utilities / Others
|
||||
import { $t } from '@locales/index'
|
||||
|
||||
// Relative Internal Imports - Controller
|
||||
import { useNotifyProviderMultiSelectController } from './useController'
|
||||
// Relative Internal Imports - Styles
|
||||
import styles from './index.module.css'
|
||||
|
||||
/**
|
||||
* @description 通知提供商多选组件。允许用户从列表中选择多个通知渠道。
|
||||
* @example
|
||||
* <NotifyProviderMultiSelect
|
||||
* path="form.channels"
|
||||
* v-model:value="selectedChannelValues"
|
||||
* valueType="type"
|
||||
* :isAddMode="true"
|
||||
* @update:value="(options) => handleChannelsChange(options)"
|
||||
* />
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'NotifyProviderMultiSelect',
|
||||
props: {
|
||||
/**
|
||||
* 表单项的路径,用于表单校验或上下文。
|
||||
* @default ''
|
||||
*/
|
||||
path: {
|
||||
type: String as PropType<NotifyProviderMultiSelectProps['path']>,
|
||||
default: '',
|
||||
},
|
||||
/**
|
||||
* 当前选中的值数组 (对应 NotifyProviderOption['value'][] 数组)。
|
||||
* @default []
|
||||
*/
|
||||
value: {
|
||||
type: Array as PropType<NotifyProviderMultiSelectProps['value']>,
|
||||
default: () => [],
|
||||
},
|
||||
/**
|
||||
* 决定 `props.value` 和 `NotifyProviderOption.value` 字段是基于原始提供商的 `value` 还是 `type`。
|
||||
* - 'value': 使用原始提供商的 `value` 字段作为 `NotifyProviderOption.value`。
|
||||
* - 'type': 使用原始提供商的 `type` 字段作为 `NotifyProviderOption.value`。
|
||||
* @default 'value'
|
||||
*/
|
||||
valueType: {
|
||||
type: String as PropType<NotifyProviderMultiSelectProps['valueType']>,
|
||||
default: 'value',
|
||||
validator: (val: string) => ['value', 'type'].includes(val),
|
||||
},
|
||||
/**
|
||||
* 是否为添加模式,显示额外的"新增渠道"和"刷新"按钮。
|
||||
* @default false
|
||||
*/
|
||||
isAddMode: {
|
||||
type: Boolean as PropType<NotifyProviderMultiSelectProps['isAddMode']>,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* 最大选择数量限制。
|
||||
* @default undefined
|
||||
*/
|
||||
maxCount: {
|
||||
type: Number as PropType<NotifyProviderMultiSelectProps['maxCount']>,
|
||||
default: undefined,
|
||||
},
|
||||
/**
|
||||
* 是否禁用。
|
||||
* @default false
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean as PropType<NotifyProviderMultiSelectProps['disabled']>,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
/**
|
||||
* @event update:value - 当选中的通知提供商更新时触发。
|
||||
* @param {NotifyProviderOption[]} options - 选中的通知提供商的完整对象数组。
|
||||
*/
|
||||
emits: {
|
||||
'update:value': (payload: NotifyProviderOption[]) => {
|
||||
return (
|
||||
Array.isArray(payload) &&
|
||||
payload.every(
|
||||
(item) => typeof item === 'object' && item !== null && 'label' in item && 'value' in item && 'type' in item,
|
||||
)
|
||||
)
|
||||
},
|
||||
},
|
||||
setup(props: NotifyProviderMultiSelectProps, { emit }) {
|
||||
const { selectOptions, goToAddNotifyProvider, handleMultiSelectUpdate, fetchNotifyProviderData } =
|
||||
useNotifyProviderMultiSelectController(props, emit)
|
||||
|
||||
/**
|
||||
* @description 渲染多选标签 (Tag)。
|
||||
* @param {object} params - Naive UI 传递的选项包装对象。
|
||||
* @param {SelectOption} params.option - 当前选项的数据。
|
||||
* @returns {VNode} 渲染后的 VNode。
|
||||
*/
|
||||
const renderMultiSelectTag = ({ option }: { option: SelectOption }): VNode => {
|
||||
// 将 SelectOption 转换为 NotifyProviderOption
|
||||
const notifyOption = option as NotifyProviderOption & SelectOption
|
||||
|
||||
// 处理长标签文本的截断
|
||||
const truncateText = (text: string, maxLength: number = 20): string => {
|
||||
if (text.length <= maxLength) return text
|
||||
return text.slice(0, maxLength) + '...'
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex items-center max-w-full h-full">
|
||||
{notifyOption.label ? (
|
||||
<NFlex align="center" justify="center" size="small" class="min-w-0 flex-1 h-full">
|
||||
<SvgIcon icon={`notify-${notifyOption.type || ''}`} size="1.4rem" class="flex-shrink-0" />
|
||||
<span
|
||||
class="text-[12px] truncate min-w-0 block"
|
||||
title={notifyOption.label}
|
||||
style={{
|
||||
maxWidth: '120px',
|
||||
lineHeight: '1.5',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{truncateText(notifyOption.label)}
|
||||
</span>
|
||||
</NFlex>
|
||||
) : (
|
||||
<NText depth="3" class="text-[12px] flex items-center h-full">
|
||||
{$t('t_0_1745887835267')}
|
||||
</NText>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 渲染下拉列表中的选项标签。
|
||||
* @param {SelectOption} option - 当前选项的数据。
|
||||
* @returns {VNode} 渲染后的 VNode。
|
||||
*/
|
||||
const renderLabel = (option: SelectOption): VNode => {
|
||||
// 将 SelectOption 转换为 NotifyProviderOption
|
||||
const notifyOption = option as NotifyProviderOption & SelectOption
|
||||
return (
|
||||
<NFlex align="center" size="small">
|
||||
<SvgIcon icon={`notify-${notifyOption.type || ''}`} size="1.6rem" />
|
||||
<NText>{notifyOption.label}</NText>
|
||||
</NFlex>
|
||||
)
|
||||
}
|
||||
|
||||
// 转换选项格式以兼容 NSelect
|
||||
const naiveSelectOptions = computed(() => {
|
||||
return selectOptions.value.map((option): SelectOption & NotifyProviderOption => ({
|
||||
...option,
|
||||
// 确保兼容 NSelect 的 SelectOption 接口
|
||||
}))
|
||||
})
|
||||
|
||||
return () => (
|
||||
<div class={styles.notifyProviderMultiSelect}>
|
||||
<NGrid cols={24}>
|
||||
<NFormItemGi
|
||||
span={props.isAddMode ? 13 : 24}
|
||||
label={$t('t_1_1745887832941') /* 通知渠道 */}
|
||||
path={props.path}
|
||||
>
|
||||
<div class={styles.selectContainer}>
|
||||
<NSelect
|
||||
class="w-full"
|
||||
style={{
|
||||
'--n-tag-text-color': 'var(--n-text-color)',
|
||||
'--n-tag-border-radius': '6px',
|
||||
'min-height': '2.5rem',
|
||||
'--n-height': '2.5rem',
|
||||
'--n-height-medium': '2.5rem',
|
||||
}}
|
||||
options={naiveSelectOptions.value}
|
||||
renderLabel={renderLabel}
|
||||
renderTag={renderMultiSelectTag}
|
||||
filterable
|
||||
clearable
|
||||
multiple
|
||||
maxTagCount="responsive"
|
||||
placeholder={$t('t_0_1745887835267')}
|
||||
value={props.value} // 直接使用 props.value 数组
|
||||
onUpdateValue={handleMultiSelectUpdate}
|
||||
disabled={props.disabled}
|
||||
v-slots={{
|
||||
empty: () => (
|
||||
<div class={styles.emptyState}>
|
||||
<NText depth="3" class="text-[1.4rem]">
|
||||
{selectOptions.value.length === 0 ? $t('t_0_1745887835267') : '暂无匹配的通知渠道'}
|
||||
</NText>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</NFormItemGi>
|
||||
{props.isAddMode && (
|
||||
<NGi span={11}>
|
||||
<div class="flex items-center h-full">
|
||||
<NDivider vertical />
|
||||
<NButton class="mx-[8px]" onClick={goToAddNotifyProvider} ghost>
|
||||
{$t('t_2_1745887834248')}
|
||||
</NButton>
|
||||
<NButton onClick={fetchNotifyProviderData} ghost>
|
||||
{$t('t_0_1746497662220')}
|
||||
</NButton>
|
||||
</div>
|
||||
</NGi>
|
||||
)}
|
||||
</NGrid>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
111
frontend/apps/allin-ssl/src/components/notifyProviderMultiSelect/types.d.ts
vendored
Normal file
111
frontend/apps/allin-ssl/src/components/notifyProviderMultiSelect/types.d.ts
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
// Type Imports
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
/**
|
||||
* @description 通知提供商选项的类型定义
|
||||
*/
|
||||
export interface NotifyProviderOption {
|
||||
/**
|
||||
* 选项的显示标签
|
||||
*/
|
||||
label: string
|
||||
/**
|
||||
* 选项的实际值 (用于多选的 v-model)
|
||||
*/
|
||||
value: string
|
||||
/**
|
||||
* 选项的原始类型 (例如 'email', 'sms'), 用于图标显示等业务逻辑
|
||||
*/
|
||||
type: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 从 Store 获取的原始通知提供商项目类型
|
||||
*/
|
||||
export interface RawProviderItem {
|
||||
/**
|
||||
* 显示标签
|
||||
*/
|
||||
label: string
|
||||
/**
|
||||
* 实际值
|
||||
*/
|
||||
value: string
|
||||
/**
|
||||
* 原始类型
|
||||
*/
|
||||
type: string
|
||||
/**
|
||||
* 允许其他可能的属性
|
||||
*/
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* @description NotifyProviderMultiSelect 组件的 Props 定义
|
||||
*/
|
||||
export interface NotifyProviderMultiSelectProps {
|
||||
/**
|
||||
* 表单项的路径,用于表单校验或上下文
|
||||
* @default ''
|
||||
*/
|
||||
path: string
|
||||
/**
|
||||
* 当前选中的值数组 (对应 NotifyProviderOption['value'][])
|
||||
* @default []
|
||||
*/
|
||||
value: string[]
|
||||
/**
|
||||
* 决定 `props.value` 和 `NotifyProviderOption.value` 字段是基于原始提供商的 `value` 还是 `type`
|
||||
* - 'value': 使用原始提供商的 `value` 字段作为 `NotifyProviderOption.value`
|
||||
* - 'type': 使用原始提供商的 `type` 字段作为 `NotifyProviderOption.value`
|
||||
* @default 'value'
|
||||
*/
|
||||
valueType: 'value' | 'type'
|
||||
/**
|
||||
* 是否为添加模式,显示额外的按钮
|
||||
* @default false
|
||||
*/
|
||||
isAddMode: boolean
|
||||
/**
|
||||
* 最大选择数量限制
|
||||
* @default undefined
|
||||
*/
|
||||
maxCount?: number
|
||||
/**
|
||||
* 是否禁用
|
||||
* @default false
|
||||
*/
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* @description NotifyProviderMultiSelect 组件的 Emits 定义
|
||||
*/
|
||||
export interface NotifyProviderMultiSelectEmits {
|
||||
(e: 'update:value', payload: NotifyProviderOption[]): void
|
||||
}
|
||||
|
||||
// Controller暴露给View的类型
|
||||
export interface NotifyProviderMultiSelectControllerExposes {
|
||||
/**
|
||||
* 内部选中的完整通知提供商对象数组
|
||||
*/
|
||||
selectedOptionsFull: Ref<NotifyProviderOption[]>
|
||||
/**
|
||||
* 格式化后用于多选组件的选项列表
|
||||
*/
|
||||
selectOptions: Ref<NotifyProviderOption[]>
|
||||
/**
|
||||
* 打开通知渠道配置页面的方法
|
||||
*/
|
||||
goToAddNotifyProvider: () => void
|
||||
/**
|
||||
* 处理多选值更新的方法
|
||||
*/
|
||||
handleMultiSelectUpdate: (values: string[]) => void
|
||||
/**
|
||||
* 手动刷新通知提供商列表的方法
|
||||
*/
|
||||
fetchNotifyProviderData: () => void
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// External Libraries
|
||||
import { ref, watch, computed } from 'vue'
|
||||
|
||||
// Type Imports
|
||||
import type {
|
||||
NotifyProviderOption,
|
||||
NotifyProviderMultiSelectControllerExposes,
|
||||
NotifyProviderMultiSelectProps,
|
||||
RawProviderItem,
|
||||
} from './types'
|
||||
|
||||
// Absolute Internal Imports - Store
|
||||
import { useStore } from '@layout/useStore'
|
||||
// Absolute Internal Imports - Config
|
||||
import { MessagePushConfig } from '@config/data'
|
||||
|
||||
/**
|
||||
* @description NotifyProviderMultiSelect 组件的控制器逻辑
|
||||
* @param props - 组件的 props
|
||||
* @param emit - 组件的 emit 函数
|
||||
* @returns {NotifyProviderMultiSelectControllerExposes} 暴露给视图的响应式数据和方法
|
||||
*/
|
||||
export function useNotifyProviderMultiSelectController(
|
||||
props: NotifyProviderMultiSelectProps,
|
||||
emit: (event: 'update:value', payload: NotifyProviderOption[]) => void,
|
||||
): NotifyProviderMultiSelectControllerExposes {
|
||||
const { fetchNotifyProvider, notifyProvider } = useStore()
|
||||
|
||||
// 内部存储当前选中的完整 NotifyProviderOption 对象数组
|
||||
const selectedOptionsFull = ref<NotifyProviderOption[]>([])
|
||||
// 存储多选组件使用的选项列表
|
||||
const selectOptions = ref<NotifyProviderOption[]>([])
|
||||
|
||||
/**
|
||||
* @description 从 MessagePushConfig 生成备用选项列表
|
||||
*/
|
||||
const fallbackOptions = computed<NotifyProviderOption[]>(() => {
|
||||
return Object.entries(MessagePushConfig).map(([key, config]) => ({
|
||||
label: config.name,
|
||||
value: props.valueType === 'value' ? key : config.type,
|
||||
type: config.type,
|
||||
}))
|
||||
})
|
||||
|
||||
/**
|
||||
* @description 根据当前选中的值数组更新内部完整选中项数组
|
||||
* @param currentSelectedValues - 当前选中的值数组 (字符串数组)
|
||||
*/
|
||||
const updateInternalSelectedOptions = (currentSelectedValues: string[]): void => {
|
||||
if (!currentSelectedValues || currentSelectedValues.length === 0) {
|
||||
selectedOptionsFull.value = []
|
||||
return
|
||||
}
|
||||
|
||||
const newSelectedOptions: NotifyProviderOption[] = []
|
||||
|
||||
currentSelectedValues.forEach((value) => {
|
||||
// 首先尝试从当前选项列表中查找
|
||||
let foundOption = selectOptions.value.find((option) => option.value === value)
|
||||
|
||||
if (!foundOption) {
|
||||
// 如果在当前选项列表中找不到,尝试从备用选项中查找
|
||||
foundOption = fallbackOptions.value.find((option) => option.value === value)
|
||||
}
|
||||
|
||||
if (foundOption) {
|
||||
newSelectedOptions.push({ ...foundOption })
|
||||
} else {
|
||||
// 如果都找不到,创建一个临时选项
|
||||
newSelectedOptions.push({
|
||||
label: value,
|
||||
value: value,
|
||||
type: '',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
selectedOptionsFull.value = newSelectedOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 打开通知渠道配置页面
|
||||
*/
|
||||
const goToAddNotifyProvider = (): void => {
|
||||
window.open('/settings?tab=notification', '_blank')
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 处理多选组件的值更新事件
|
||||
* @param newSelectedValues - 多选组件更新的 modelValue (字符串数组)
|
||||
*/
|
||||
const handleMultiSelectUpdate = (newSelectedValues: string[]): void => {
|
||||
updateInternalSelectedOptions(newSelectedValues)
|
||||
emit('update:value', [...selectedOptionsFull.value]) // Emit a copy of the array
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 外部调用以刷新通知提供商列表
|
||||
*/
|
||||
const fetchNotifyProviderData = (): void => {
|
||||
fetchNotifyProvider()
|
||||
}
|
||||
|
||||
// 监听父组件传入的 props.value (可能是初始值或外部更改)
|
||||
watch(
|
||||
() => props.value,
|
||||
(newVal) => {
|
||||
// 确保提供商列表已加载或正在加载,然后再尝试更新选中项细节
|
||||
if (selectOptions.value.length === 0 && newVal && newVal.length > 0) {
|
||||
fetchNotifyProviderData() // 如果列表为空且有 props.value,触发加载
|
||||
}
|
||||
updateInternalSelectedOptions(newVal)
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
)
|
||||
|
||||
// 监听从 Store 获取的原始通知提供商列表,并进行转换
|
||||
watch(
|
||||
() => notifyProvider.value,
|
||||
(rawProviders) => {
|
||||
if (rawProviders && rawProviders.length > 0) {
|
||||
// 如果 Store 中有数据,使用 Store 数据
|
||||
selectOptions.value = rawProviders.map((item: RawProviderItem) => ({
|
||||
label: item.label,
|
||||
// `value` 字段给多选组件使用,根据 props.valueType 决定其来源
|
||||
value: props.valueType === 'value' ? item.value : item.type,
|
||||
// `type` 字段始终为原始提供商的 type,用于 SvgIcon
|
||||
type: item.type,
|
||||
}))
|
||||
} else {
|
||||
// 如果 Store 中没有数据,使用备用数据源
|
||||
selectOptions.value = fallbackOptions.value
|
||||
}
|
||||
|
||||
// Store 数据更新后,基于当前 props.value 重新更新内部完整选中项
|
||||
updateInternalSelectedOptions(props.value)
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
)
|
||||
|
||||
// 初始化时如果 Store 为空,先使用备用数据
|
||||
if (!notifyProvider.value || notifyProvider.value.length === 0) {
|
||||
selectOptions.value = fallbackOptions.value
|
||||
// 尝试获取 Store 数据
|
||||
fetchNotifyProviderData()
|
||||
}
|
||||
|
||||
return {
|
||||
selectedOptionsFull,
|
||||
selectOptions,
|
||||
goToAddNotifyProvider,
|
||||
handleMultiSelectUpdate,
|
||||
fetchNotifyProviderData,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineComponent, PropType } from 'vue'
|
||||
import { NTag } from 'naive-ui'
|
||||
import { NTag, NFlex } from 'naive-ui'
|
||||
|
||||
// 类型导入
|
||||
import type { AuthApiTypeIconProps } from './types'
|
||||
@@ -24,11 +24,11 @@ export default defineComponent({
|
||||
name: 'AuthApiTypeIcon',
|
||||
props: {
|
||||
/**
|
||||
* 图标类型键。
|
||||
* 图标类型键。支持单个字符串或字符串数组。
|
||||
* 该键用于从 /lib/data.tsx 配置中查找对应的图标和名称。
|
||||
*/
|
||||
icon: {
|
||||
type: String as PropType<AuthApiTypeIconProps['icon']>,
|
||||
type: [String, Array] as PropType<AuthApiTypeIconProps['icon']>,
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
@@ -47,17 +47,41 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props: AuthApiTypeIconProps) {
|
||||
const { iconPath, typeName } = useAuthApiTypeIconController(props)
|
||||
const { iconPath, typeName, iconItems } = useAuthApiTypeIconController(props)
|
||||
|
||||
return () => (
|
||||
<NTag
|
||||
type={props.type}
|
||||
size="small"
|
||||
class="w-auto text-ellipsis overflow-hidden whitespace-normal p-[.6rem] h-auto"
|
||||
>
|
||||
<SvgIcon icon={iconPath.value} size="1.2rem" class="mr-[0.4rem]" />
|
||||
<span>{props.text && <span class="text-[12px]">{typeName.value}</span>}</span>
|
||||
</NTag>
|
||||
)
|
||||
return () => {
|
||||
// 如果是多个图标,显示多个标签
|
||||
if (Array.isArray(props.icon) && props.icon.length > 1) {
|
||||
return (
|
||||
<NFlex size="small" wrap={true} style="gap: 4px; flex-wrap: wrap;">
|
||||
{iconItems.value.map((item, index) => (
|
||||
<NTag
|
||||
key={item.key}
|
||||
type={props.type}
|
||||
size="small"
|
||||
class="w-auto text-ellipsis overflow-hidden whitespace-normal p-[.6rem] h-auto mb-1"
|
||||
style="margin-right: 4px; max-width: 100%;"
|
||||
>
|
||||
<SvgIcon icon={item.iconPath} size="1.2rem" class="mr-[0.4rem] flex-shrink-0" />
|
||||
{props.text && <span class="text-[12px] truncate">{item.typeName}</span>}
|
||||
</NTag>
|
||||
))}
|
||||
</NFlex>
|
||||
)
|
||||
}
|
||||
|
||||
// 单个图标的显示(包括数组只有一个元素的情况)
|
||||
return (
|
||||
<NTag
|
||||
type={props.type}
|
||||
size="small"
|
||||
class="w-auto text-ellipsis overflow-hidden whitespace-normal p-[.6rem] h-auto"
|
||||
style="max-width: 100%;"
|
||||
>
|
||||
<SvgIcon icon={iconPath.value} size="1.2rem" class="mr-[0.4rem] flex-shrink-0" />
|
||||
{props.text && <span class="text-[12px] truncate">{typeName.value}</span>}
|
||||
</NTag>
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
*/
|
||||
export interface AuthApiTypeIconProps {
|
||||
/**
|
||||
* 图标类型键。
|
||||
* 图标类型键。支持单个字符串或字符串数组。
|
||||
* 该键用于从 /lib/data.tsx 配置中查找对应的图标和名称。
|
||||
* 如果未在配置中找到,将尝试使用 'default' 图标,并直接显示该键作为文本。
|
||||
* 当传入数组时,会显示多个图标标签。
|
||||
*/
|
||||
icon: string
|
||||
icon: string | string[]
|
||||
/**
|
||||
* NTag 的类型。
|
||||
* @default 'default'
|
||||
@@ -20,6 +21,15 @@ export interface AuthApiTypeIconProps {
|
||||
text?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 图标项的类型定义
|
||||
*/
|
||||
export interface IconItem {
|
||||
iconPath: string
|
||||
typeName: string
|
||||
key: string
|
||||
}
|
||||
|
||||
/**
|
||||
* useAuthApiTypeIconController Composable 函数暴露的接口。
|
||||
*/
|
||||
@@ -28,6 +38,8 @@ export interface AuthApiTypeIconControllerExposes {
|
||||
iconPath: globalThis.ComputedRef<string>
|
||||
/** 计算得到的类型名称,用于显示 */
|
||||
typeName: globalThis.ComputedRef<string>
|
||||
/** 计算得到的所有图标项,用于多图标显示 */
|
||||
iconItems: globalThis.ComputedRef<IconItem[]>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -53,34 +53,65 @@ if (ApiProjectConfig.btwaf) {
|
||||
|
||||
/**
|
||||
* @function useAuthApiTypeIconController
|
||||
* @description AuthApiTypeIcon 组件的控制器逻辑。
|
||||
* @description AuthApiTypeIcon 组件的控制器逻辑,支持单个或多个图标。
|
||||
* @param props - 组件的 props。
|
||||
* @returns {AuthApiTypeIconControllerExposes} 控制器暴露给视图的数据和方法。
|
||||
*/
|
||||
export function useAuthApiTypeIconController(props: AuthApiTypeIconProps): AuthApiTypeIconControllerExposes {
|
||||
/**
|
||||
* @computed iconPath
|
||||
* @description 根据 props.icon 计算 SvgIcon 所需的图标名称。
|
||||
* @description 根据 props.icon 计算 SvgIcon 所需的图标名称。支持单个或多个图标。
|
||||
*/
|
||||
const iconPath = computed<string>(() => {
|
||||
const isNotify = notifyKeys.has(props.icon)
|
||||
// 如果是数组,取第一个作为主要图标
|
||||
const iconKey = Array.isArray(props.icon) ? props.icon[0] : props.icon
|
||||
if (!iconKey) return 'resources-default'
|
||||
|
||||
const isNotify = notifyKeys.has(iconKey)
|
||||
const RESOURCE_PREFIX = isNotify ? 'notify-' : 'resources-'
|
||||
// 从映射表中获取图标文件名,如果找不到则使用 'default'
|
||||
const iconStem = iconFileMap[props.icon] || 'default'
|
||||
const iconStem = iconFileMap[iconKey] || 'default'
|
||||
return RESOURCE_PREFIX + iconStem
|
||||
})
|
||||
|
||||
/**
|
||||
* @computed typeName
|
||||
* @description 根据 props.icon 获取对应的显示名称。
|
||||
* @description 根据 props.icon 获取对应的显示名称。支持单个或多个名称。
|
||||
*/
|
||||
const typeName = computed<string>(() => {
|
||||
// 从映射表中获取显示名称,如果找不到则直接使用 props.icon
|
||||
return typeNamesMap[props.icon] || props.icon
|
||||
if (Array.isArray(props.icon)) {
|
||||
// 如果是数组,组合多个名称
|
||||
return props.icon
|
||||
.filter(Boolean)
|
||||
.map((iconKey) => typeNamesMap[iconKey] || iconKey)
|
||||
.join(', ')
|
||||
} else {
|
||||
// 单个图标的处理
|
||||
return typeNamesMap[props.icon] || props.icon
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* @computed iconItems
|
||||
* @description 计算所有图标项,用于多图标显示
|
||||
*/
|
||||
const iconItems = computed(() => {
|
||||
const icons = Array.isArray(props.icon) ? props.icon : [props.icon]
|
||||
return icons.filter(Boolean).map((iconKey) => {
|
||||
const isNotify = notifyKeys.has(iconKey)
|
||||
const RESOURCE_PREFIX = isNotify ? 'notify-' : 'resources-'
|
||||
const iconStem = iconFileMap[iconKey] || 'default'
|
||||
return {
|
||||
iconPath: RESOURCE_PREFIX + iconStem,
|
||||
typeName: typeNamesMap[iconKey] || iconKey,
|
||||
key: iconKey,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
iconPath,
|
||||
typeName,
|
||||
iconItems,
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import useRouterEach from './each' // 全局路由守卫
|
||||
|
||||
// 获取路由
|
||||
const { routeGroup, routes } = createRoutes() // 获取路由配置
|
||||
console.log(routeGroup, routes)
|
||||
|
||||
// 创建路由
|
||||
const router = useCreateRouter({
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
background-color: var(--n-merged-td-color);
|
||||
}
|
||||
|
||||
.n-scrollbar > .n-scrollbar-rail.n-scrollbar-rail--horizontal--top, .n-scrollbar + .n-scrollbar-rail.n-scrollbar-rail--horizontal--top {
|
||||
top: 40px !important;
|
||||
}
|
||||
|
||||
.leftPanel .n-tabs-tab {
|
||||
height: 3.2rem;
|
||||
|
||||
@@ -109,7 +109,8 @@ type SshAccessConfig = {
|
||||
host: string
|
||||
port: number
|
||||
user: string
|
||||
} & ({ mode: 'password'; password: string; key?: never } | { mode: 'key'; key: string; password?: never })
|
||||
password?: string // 密码字段:密码模式下作为登录密码,密钥模式下作为私钥密码(可选)
|
||||
} & ({ mode: 'password'; key?: never } | { mode: 'key'; key: string })
|
||||
|
||||
/**
|
||||
* 阿里云授权配置
|
||||
@@ -487,4 +488,4 @@ export interface GetPluginsActionsResponse extends AxiosResponseData {
|
||||
description: string
|
||||
params: string
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
132
frontend/apps/allin-ssl/src/types/monitor.d.ts
vendored
132
frontend/apps/allin-ssl/src/types/monitor.d.ts
vendored
@@ -9,21 +9,26 @@ export interface SiteMonitorListParams {
|
||||
|
||||
/** 站点监控项 */
|
||||
export interface SiteMonitorItem {
|
||||
active: number
|
||||
ca: string
|
||||
cert_domain: string
|
||||
create_time: string
|
||||
cycle: number
|
||||
end_day: string
|
||||
end_time: string
|
||||
except_end_time: string
|
||||
id: number
|
||||
last_time: string
|
||||
name: string
|
||||
report_type: string
|
||||
site_domain: string
|
||||
state: string
|
||||
update_time: string
|
||||
active: number // Monitor active status (1 = active, 0 = inactive)
|
||||
advance_day: number // Days in advance for notification
|
||||
ca: string // Certificate Authority (e.g., "sslTrus")
|
||||
common_name: string // Certificate common name (e.g., "*.bt.cn")
|
||||
create_time: string // Creation timestamp (YYYY-MM-DD HH:mm:ss)
|
||||
cycle: number // Monitoring cycle
|
||||
days_left: number // Days remaining until expiration
|
||||
except_end_time: string // Expected end time (YYYY-MM-DD HH:mm:ss)
|
||||
id: number // Unique identifier
|
||||
last_time: string // Last check time (YYYY-MM-DD HH:mm:ss)
|
||||
monitor_type: string // Type of monitoring (e.g., "https")
|
||||
name: string // Monitor name
|
||||
not_after: string // Certificate valid until (YYYY-MM-DD HH:mm:ss)
|
||||
not_before: string // Certificate valid from (YYYY-MM-DD HH:mm:ss)
|
||||
repeat_send_gap: number | null // Gap between repeat notifications
|
||||
report_types: string | string[] // Notification types (e.g., "dingtalk" or ["dingtalk", "mail"])
|
||||
sans: string // Subject Alternative Names
|
||||
target: string // Monitoring target URL/domain
|
||||
update_time: string // Last update time (YYYY-MM-DD HH:mm:ss)
|
||||
valid: number // Certificate validity status (1 = valid, 0 = invalid)
|
||||
}
|
||||
|
||||
/** 站点监控列表响应 */
|
||||
@@ -34,9 +39,13 @@ export interface SiteMonitorListResponse extends AxiosResponseData {
|
||||
/** 新增站点监控请求参数 */
|
||||
export interface AddSiteMonitorParams {
|
||||
name: string
|
||||
domain: string
|
||||
target: string
|
||||
monitor_type: string // 监控类型 (e.g., "https", "smtp")
|
||||
report_types: string | string[] // 支持单个或多个通知类型
|
||||
cycle: number
|
||||
report_type: string
|
||||
repeat_send_gap: number // 重复发送间隔(次数)
|
||||
active: number // 启用状态 (1 = 启用, 0 = 禁用)
|
||||
advance_day: number // 提前天数
|
||||
}
|
||||
|
||||
/** 修改站点监控请求参数 */
|
||||
@@ -53,3 +62,92 @@ export interface SetSiteMonitorParams {
|
||||
id: number
|
||||
active: number
|
||||
}
|
||||
|
||||
/** 证书链节点信息 */
|
||||
export interface CertChainNode {
|
||||
common_name: string // 通用名称
|
||||
subject: string // 主题信息
|
||||
issuer: string // 颁发者信息
|
||||
children?: CertChainNode[] // 子证书链节点
|
||||
}
|
||||
|
||||
/** 监控详情信息 */
|
||||
export interface MonitorDetailInfo {
|
||||
ca: string // 证书颁发机构
|
||||
cert_chain: CertChainNode // 证书链信息
|
||||
common_name: string // 通用名称
|
||||
days_left: number // 剩余天数
|
||||
err_count: number // 错误次数
|
||||
last_time: string // 上次检测时间 (YYYY-MM-DD HH:mm:ss)
|
||||
monitor_type: string // 监控类型
|
||||
name: string // 监控名称
|
||||
not_after: string // 证书到期时间
|
||||
not_before: string // 证书生效时间
|
||||
sans: string // 主题备用名称
|
||||
target: string // 监控目标
|
||||
tls_version?: string // 支持的TLS版本(可选字段)
|
||||
valid: number // 验证状态 (1=有效, 0=无效)
|
||||
verify_error: string // 验证错误信息
|
||||
}
|
||||
|
||||
/** 获取监控详情请求参数 */
|
||||
export interface GetMonitorDetailParams {
|
||||
id: number
|
||||
}
|
||||
|
||||
/** 获取监控详情响应 */
|
||||
export interface GetMonitorDetailResponse extends AxiosResponseData {
|
||||
data: MonitorDetailInfo
|
||||
}
|
||||
|
||||
/** 错误记录项 */
|
||||
export interface ErrorRecord {
|
||||
create_time: string // 创建时间 (YYYY-MM-DD HH:mm:ss)
|
||||
id: number // 记录ID
|
||||
info: string // 附加信息
|
||||
monitor_id: number // 监控ID
|
||||
msg: string // 错误消息内容
|
||||
}
|
||||
|
||||
/** 获取错误记录请求参数 */
|
||||
export interface GetErrorRecordParams {
|
||||
id: number // 监控ID
|
||||
p: number // 页码
|
||||
limit: number // 每页条数
|
||||
}
|
||||
|
||||
/** 获取错误记录响应 */
|
||||
export interface GetErrorRecordResponse extends AxiosResponseData {
|
||||
count: number // 总记录数
|
||||
data: ErrorRecord[] // 错误记录数组
|
||||
}
|
||||
|
||||
/** 文件导入监控请求参数 */
|
||||
export interface FileImportMonitorParams {
|
||||
file: File // 上传的文件
|
||||
}
|
||||
|
||||
/** 文件导入监控响应 */
|
||||
export interface FileImportMonitorResponse extends AxiosResponseData {
|
||||
data: {
|
||||
success_count: number // 成功导入数量
|
||||
failed_count: number // 失败导入数量
|
||||
failed_items?: string[] // 失败的项目列表(可选)
|
||||
}
|
||||
}
|
||||
|
||||
/** 模板下载请求参数 */
|
||||
export interface TemplateDownloadParams {
|
||||
type: 'txt' | 'csv' | 'json' | 'xlsx' // 模板类型
|
||||
}
|
||||
|
||||
/** 支持的文件格式类型 */
|
||||
export type SupportedFileType = 'txt' | 'csv' | 'json' | 'xlsx'
|
||||
|
||||
/** 文件上传状态 */
|
||||
export interface FileUploadStatus {
|
||||
uploading: boolean
|
||||
progress: number
|
||||
error?: string
|
||||
success?: boolean
|
||||
}
|
||||
|
||||
@@ -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版本选择下拉框
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -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>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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('添加失败')
|
||||
|
||||
@@ -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天
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
36
frontend/apps/allin-ssl/types/components.d.ts
vendored
36
frontend/apps/allin-ssl/types/components.d.ts
vendored
@@ -8,30 +8,32 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AddNode: typeof import('./../src/components/FlowChart/components/other/addNode/index.tsx')['default']
|
||||
Alias: typeof import('./../src/components/FlowChart/lib/alias.tsx')['default']
|
||||
AddNode: typeof import('./../src/components/flowChart/components/other/addNode/index.tsx')['default']
|
||||
Alias: typeof import('./../src/components/flowChart/lib/alias.tsx')['default']
|
||||
BaseLayout: typeof import('./../src/components/BaseLayout/index.tsx')['default']
|
||||
BaseNode: typeof import('./../src/components/FlowChart/components/base/baseNode/index.tsx')['default']
|
||||
BranchNode: typeof import('./../src/components/FlowChart/components/base/branchNode/index.tsx')['default']
|
||||
BaseNode: typeof import('./../src/components/flowChart/components/base/baseNode/index.tsx')['default']
|
||||
BranchNode: typeof import('./../src/components/flowChart/components/base/branchNode/index.tsx')['default']
|
||||
CAProviderSelect: typeof import('./../src/components/CAProviderSelect/index.tsx')['default']
|
||||
ConditionNode: typeof import('./../src/components/FlowChart/components/base/conditionNode/index.tsx')['default']
|
||||
Config: typeof import('./../src/components/FlowChart/lib/config.tsx')['default']
|
||||
DnsProviderSelect: typeof import('./../src/components/DnsProviderSelect/index.tsx')['default']
|
||||
Drawer: typeof import('./../src/components/FlowChart/components/other/drawer.tsx')['default']
|
||||
EndNode: typeof import('./../src/components/FlowChart/components/base/endNode.tsx')['default']
|
||||
ErrorNode: typeof import('./../src/components/FlowChart/components/base/errorNode/index.tsx')['default']
|
||||
FlowChart: typeof import('./../src/components/FlowChart/index.tsx')['default']
|
||||
ConditionNode: typeof import('./../src/components/flowChart/components/base/conditionNode/index.tsx')['default']
|
||||
Config: typeof import('./../src/components/flowChart/lib/config.tsx')['default']
|
||||
Demo: typeof import('./../src/components/notifyProviderMultiSelect/demo.tsx')['default']
|
||||
DnsProviderSelect: typeof import('./../src/components/dnsProviderSelect/index.tsx')['default']
|
||||
Drawer: typeof import('./../src/components/flowChart/components/other/drawer.tsx')['default']
|
||||
EndNode: typeof import('./../src/components/flowChart/components/base/endNode.tsx')['default']
|
||||
ErrorNode: typeof import('./../src/components/flowChart/components/base/errorNode/index.tsx')['default']
|
||||
FlowChart: typeof import('./../src/components/flowChart/index.tsx')['default']
|
||||
LogDisplay: typeof import('./../src/components/LogDisplay/index.tsx')['default']
|
||||
NodeWrap: typeof import('./../src/components/FlowChart/components/render/nodeWrap.tsx')['default']
|
||||
NotifyProviderSelect: typeof import('./../src/components/NotifyProviderSelect/index.tsx')['default']
|
||||
NodeWrap: typeof import('./../src/components/flowChart/components/render/nodeWrap.tsx')['default']
|
||||
NotifyProviderMultiSelect: typeof import('./../src/components/notifyProviderMultiSelect/index.tsx')['default']
|
||||
NotifyProviderSelect: typeof import('./../src/components/notifyProviderSelect/index.tsx')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SvgIcon: typeof import('./../src/components/SvgIcon/index.tsx')['default']
|
||||
SvgIcon: typeof import('./../src/components/svgIcon/index.tsx')['default']
|
||||
TableEmptyState: typeof import('./../src/components/TableEmptyState/index.tsx')['default']
|
||||
TypeIcon: typeof import('./../src/components/TypeIcon/index.tsx')['default']
|
||||
TypeIcon: typeof import('./../src/components/typeIcon/index.tsx')['default']
|
||||
UpdateLogModal: typeof import('./../src/components/UpdateLogModal/index.tsx')['default']
|
||||
UseController: typeof import('./../src/components/CAProviderSelect/useController.tsx')['default']
|
||||
UseStore: typeof import('./../src/components/FlowChart/useStore.tsx')['default']
|
||||
Verify: typeof import('./../src/components/FlowChart/lib/verify.tsx')['default']
|
||||
UseStore: typeof import('./../src/components/flowChart/useStore.tsx')['default']
|
||||
Verify: typeof import('./../src/components/flowChart/lib/verify.tsx')['default']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ export default defineConfig({
|
||||
// targetDir: 'allinssl-gitlab',
|
||||
// discardChanges: true,
|
||||
// },
|
||||
|
||||
{
|
||||
repo: 'https://github.com/allinssl/allinssl.git',
|
||||
branch: '1.0.7',
|
||||
|
||||
Reference in New Issue
Block a user