mirror of
https://gitee.com/mirrors/AllinSSL.git
synced 2026-03-09 16:21:10 +08:00
【调整】SSH地址支持域名形式
【新增】支持自定义监控端口 【新增】通知类型-企业微信 【新增】申请证书(Buypass)、自定义ACME服务器地址 【新增】授权API管理(namesilo、Bunny、Gcore、name.com、京东云)
This commit is contained in:
@@ -27,7 +27,7 @@ import {
|
||||
useLoadingMask,
|
||||
} from '@baota/naive-ui/hooks'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
import { isEmail, isIp, isPort, isUrl } from '@baota/utils/business'
|
||||
import { isEmail, isIp, isPort, isUrl, isDomain } from '@baota/utils/business'
|
||||
import { $t } from '@locales/index'
|
||||
import { useStore } from './useStore'
|
||||
import { ApiProjectConfig } from '@config/data'
|
||||
@@ -42,6 +42,11 @@ import type {
|
||||
CloudnsAccessConfig,
|
||||
AwsAccessConfig,
|
||||
AzureAccessConfig,
|
||||
NamesiloAccessConfig,
|
||||
NamedotcomAccessConfig,
|
||||
BunnyAccessConfig,
|
||||
GcoreAccessConfig,
|
||||
JdcloudAccessConfig,
|
||||
} from '@/types/access'
|
||||
import type { VNode, Ref } from 'vue'
|
||||
import { testAccess } from '@/api/access'
|
||||
@@ -306,8 +311,8 @@ export const useApiFormController = (props: ApiFormControllerProps): ApiFormCont
|
||||
required: true,
|
||||
trigger: 'input',
|
||||
validator: (rule: FormItemRule, value: string, callback: (error?: Error) => void) => {
|
||||
if (!isIp(value)) {
|
||||
return callback(new Error($t('t_0_1745317313835')))
|
||||
if (!isIp(value) && !isDomain(value)) {
|
||||
return callback(new Error($t('t_0_1749119980577')))
|
||||
}
|
||||
callback()
|
||||
},
|
||||
@@ -663,6 +668,27 @@ export const useApiFormController = (props: ApiFormControllerProps): ApiFormCont
|
||||
useFormInput('Environment', 'config.environment', { allowInput: noSideSpace, placeholder: 'public' }),
|
||||
)
|
||||
break
|
||||
case 'namesilo':
|
||||
items.push(useFormInput('API Key', 'config.api_key', { allowInput: noSideSpace }))
|
||||
break
|
||||
case 'namedotcom':
|
||||
items.push(
|
||||
useFormInput('Username', 'config.username', { allowInput: noSideSpace }),
|
||||
useFormInput('API Token', 'config.api_token', { allowInput: noSideSpace }),
|
||||
)
|
||||
break
|
||||
case 'bunny':
|
||||
items.push(useFormInput('API Key', 'config.api_key', { allowInput: noSideSpace }))
|
||||
break
|
||||
case 'gcore':
|
||||
items.push(useFormInput('API Token', 'config.api_token', { allowInput: noSideSpace }))
|
||||
break
|
||||
case 'jdcloud':
|
||||
items.push(
|
||||
useFormInput('Access Key ID', 'config.access_key_id', { allowInput: noSideSpace }),
|
||||
useFormInput('Secret Access Key', 'config.secret_access_key', { allowInput: noSideSpace }),
|
||||
)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -760,6 +786,33 @@ export const useApiFormController = (props: ApiFormControllerProps): ApiFormCont
|
||||
environment: '',
|
||||
} as AzureAccessConfig
|
||||
break
|
||||
case 'namesilo':
|
||||
param.value.config = {
|
||||
api_key: '',
|
||||
} as NamesiloAccessConfig
|
||||
break
|
||||
case 'namedotcom':
|
||||
param.value.config = {
|
||||
username: '',
|
||||
api_token: '',
|
||||
} as NamedotcomAccessConfig
|
||||
break
|
||||
case 'bunny':
|
||||
param.value.config = {
|
||||
api_key: '',
|
||||
} as BunnyAccessConfig
|
||||
break
|
||||
case 'gcore':
|
||||
param.value.config = {
|
||||
api_token: '',
|
||||
} as GcoreAccessConfig
|
||||
break
|
||||
case 'jdcloud':
|
||||
param.value.config = {
|
||||
access_key_id: '',
|
||||
secret_access_key: '',
|
||||
} as JdcloudAccessConfig
|
||||
break
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NFormItem, NInputNumber } from 'naive-ui'
|
||||
import { NFormItem, NInputNumber, NSwitch } from 'naive-ui'
|
||||
import { useForm, useFormHooks, useModalHooks } from '@baota/naive-ui/hooks'
|
||||
import { useStore } from '@components/FlowChart/useStore'
|
||||
import { $t } from '@locales/index'
|
||||
@@ -29,6 +29,9 @@ export default defineComponent({
|
||||
name_server: '',
|
||||
skip_check: 0,
|
||||
algorithm: 'RSA2048',
|
||||
close_cname: 0,
|
||||
max_wait: undefined,
|
||||
ignore_check: 0,
|
||||
},
|
||||
}),
|
||||
},
|
||||
@@ -86,7 +89,8 @@ export default defineComponent({
|
||||
'onUpdate:value': (val: { value: string; ca: string; email: string }) => {
|
||||
param.value.eabId = val.value
|
||||
param.value.ca = val.ca
|
||||
if (val.value) param.value.email = val.email
|
||||
// 始终更新邮件,确保 Let's Encrypt 和 Buypass 的邮件能正确显示
|
||||
param.value.email = val.email
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@@ -138,15 +142,12 @@ export default defineComponent({
|
||||
},
|
||||
{ showRequireMark: false },
|
||||
),
|
||||
useFormInput(
|
||||
$t('t_0_1747106957037'),
|
||||
'name_server',
|
||||
useFormSwitch(
|
||||
$t('t_2_1749204567193'),
|
||||
'close_cname',
|
||||
{
|
||||
placeholder: $t('t_1_1747106961747'),
|
||||
allowInput: noSideSpace,
|
||||
onInput: (val: string) => {
|
||||
param.value.name_server = val.replace(/,/g, ',').replace(/;/g, ',') // 中文逗号分隔
|
||||
},
|
||||
checkedValue: 1,
|
||||
uncheckedValue: 0,
|
||||
},
|
||||
{ showRequireMark: false },
|
||||
),
|
||||
@@ -159,6 +160,62 @@ export default defineComponent({
|
||||
},
|
||||
{ showRequireMark: false },
|
||||
),
|
||||
// 只有在跳过预检查关闭时才显示DNS递归服务器、预检查超时时间和忽略预检查结果
|
||||
...(param.value.skip_check === 0
|
||||
? [
|
||||
useFormInput(
|
||||
$t('t_0_1747106957037'),
|
||||
'name_server',
|
||||
{
|
||||
placeholder: $t('t_1_1747106961747'),
|
||||
allowInput: noSideSpace,
|
||||
onInput: (val: string) => {
|
||||
param.value.name_server = val.replace(/,/g, ',').replace(/;/g, ',') // 中文逗号分隔
|
||||
},
|
||||
},
|
||||
{ showRequireMark: false },
|
||||
),
|
||||
{
|
||||
type: 'custom' as const,
|
||||
render: () => {
|
||||
return (
|
||||
<NFormItem label={$t('t_0_1749263105073')} path="max_wait">
|
||||
<NInputNumber
|
||||
v-model:value={(param.value as ApplyNodeConfig & { max_wait?: number }).max_wait}
|
||||
showButton={false}
|
||||
min={1}
|
||||
class="w-full"
|
||||
placeholder={$t('t_1_1749263104936')}
|
||||
/>
|
||||
</NFormItem>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'custom' as const,
|
||||
render: () => {
|
||||
return (
|
||||
<NFormItem label={$t('t_2_1749263103765')} path="ignore_check">
|
||||
<div class="flex items-center">
|
||||
<span class="text-[1.4rem] mr-[1.2rem]">{$t('t_3_1749263104237')}</span>
|
||||
<NSwitch
|
||||
v-model:value={param.value.ignore_check}
|
||||
checkedValue={1}
|
||||
uncheckedValue={0}
|
||||
class="mx-[.5rem]"
|
||||
v-slots={{
|
||||
checked: () => $t('t_4_1749263101853'),
|
||||
unchecked: () => $t('t_5_1749263101934'),
|
||||
}}
|
||||
/>
|
||||
<span class="text-[1.4rem] ml-[1.2rem]">{$t('t_6_1749263103891')}</span>
|
||||
</div>
|
||||
</NFormItem>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
: []),
|
||||
useFormHelp([
|
||||
@@ -183,6 +240,7 @@ export default defineComponent({
|
||||
confirm(async (close) => {
|
||||
try {
|
||||
await example.value?.validate()
|
||||
|
||||
updateNodeConfig(props.node.id, data.value) // 更新节点配置
|
||||
isRefreshNode.value = props.node.id // 刷新节点
|
||||
close()
|
||||
|
||||
@@ -35,6 +35,7 @@ const {
|
||||
workflowFormData,
|
||||
deleteExistingWorkflow,
|
||||
executeExistingWorkflow,
|
||||
stopExistingWorkflow,
|
||||
setWorkflowActive,
|
||||
setWorkflowExecType,
|
||||
caFormData,
|
||||
@@ -220,7 +221,7 @@ export const useController = () => {
|
||||
useModal({
|
||||
title: workflow ? `【${workflow.name}】 - ${$t('t_9_1745215914666')}` : $t('t_9_1745215914666'),
|
||||
component: HistoryModal,
|
||||
area: 800,
|
||||
area: 850,
|
||||
componentProps: { id: workflow.id.toString() },
|
||||
})
|
||||
}
|
||||
@@ -422,6 +423,23 @@ export const useHistoryController = (id: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 停止工作流执行
|
||||
* @param {WorkflowHistoryItem} historyItem - 工作流历史记录项
|
||||
*/
|
||||
const handleStopWorkflow = async (historyItem: WorkflowHistoryItem) => {
|
||||
useDialog({
|
||||
title: $t('t_0_1749204565782'),
|
||||
content: $t('t_1_1749204570473'),
|
||||
onPositiveClick: async () => {
|
||||
await stopExistingWorkflow(historyItem.id)
|
||||
await fetch() // 刷新历史记录表格
|
||||
// 触发外部主表格刷新
|
||||
refreshTable.value = true
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 创建历史记录表格列配置
|
||||
* @returns {DataTableColumn<WorkflowHistoryItem>[]} 返回表格列配置数组
|
||||
@@ -430,7 +448,7 @@ export const useHistoryController = (id: string) => {
|
||||
{
|
||||
title: $t('t_4_1745227838558'),
|
||||
key: 'create_time',
|
||||
width: 230,
|
||||
width: 200,
|
||||
render: (row: WorkflowHistoryItem) => {
|
||||
// 处理数字类型的时间戳
|
||||
return row.create_time ? row.create_time : '-'
|
||||
@@ -439,7 +457,7 @@ export const useHistoryController = (id: string) => {
|
||||
{
|
||||
title: $t('t_5_1745227839906'),
|
||||
key: 'end_time',
|
||||
width: 230,
|
||||
width: 200,
|
||||
render: (row: WorkflowHistoryItem) => {
|
||||
// 处理数字类型的时间戳
|
||||
return row.end_time ? row.end_time : '-'
|
||||
@@ -448,7 +466,7 @@ export const useHistoryController = (id: string) => {
|
||||
{
|
||||
title: $t('t_6_1745227838798'),
|
||||
key: 'exec_type',
|
||||
width: 110,
|
||||
width: 120,
|
||||
render: (row: WorkflowHistoryItem) => (
|
||||
<NTag type={row.exec_type === 'auto' ? 'info' : 'default'} size="small" bordered={false}>
|
||||
{row.exec_type === 'auto' ? $t('t_2_1745215915397') : $t('t_3_1745215914237')}
|
||||
@@ -461,9 +479,14 @@ export const useHistoryController = (id: string) => {
|
||||
key: 'actions',
|
||||
fixed: 'right',
|
||||
align: 'right',
|
||||
width: 80,
|
||||
width: 180,
|
||||
render: (row: WorkflowHistoryItem) => (
|
||||
<NSpace justify="end">
|
||||
<NSpace justify="end" size="small">
|
||||
{row.status === 'running' && (
|
||||
<NButton size="tiny" strong secondary type="error" onClick={() => handleStopWorkflow(row)}>
|
||||
{$t('t_0_1749204565782')}
|
||||
</NButton>
|
||||
)}
|
||||
<NButton
|
||||
size="tiny"
|
||||
strong
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
executeWorkflow,
|
||||
updateWorkflowExecType,
|
||||
enableWorkflow,
|
||||
stopWorkflow,
|
||||
} from '@/api/workflow'
|
||||
import { getEabList, addEab, deleteEab } from '@/api/access'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
@@ -17,6 +18,7 @@ import type {
|
||||
WorkflowItem,
|
||||
UpdateWorkflowExecTypeParams,
|
||||
EnableWorkflowParams,
|
||||
StopWorkflowParams,
|
||||
} from '@/types/workflow'
|
||||
import type { EabItem, EabListParams, EabAddParams } from '@/types/access'
|
||||
import type { TableResponse } from '@baota/naive-ui/types/table'
|
||||
@@ -148,6 +150,22 @@ export const useWorkflowStore = defineStore('workflow-store', () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止工作流执行
|
||||
* @description 停止指定工作流的执行
|
||||
* @param {string} id - 工作流ID
|
||||
* @returns {Promise<void>} 停止执行结果
|
||||
*/
|
||||
const stopExistingWorkflow = async (id: string) => {
|
||||
try {
|
||||
const { message, fetch } = stopWorkflow({ id })
|
||||
message.value = true
|
||||
await fetch()
|
||||
} catch (error) {
|
||||
handleError(error).default($t('t_1_1747895712756'))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取CA授权列表
|
||||
* @param {EabListParams} params - 请求参数
|
||||
@@ -217,6 +235,7 @@ export const useWorkflowStore = defineStore('workflow-store', () => {
|
||||
fetchWorkflowHistory,
|
||||
deleteExistingWorkflow,
|
||||
executeExistingWorkflow,
|
||||
stopExistingWorkflow,
|
||||
setWorkflowActive,
|
||||
setWorkflowExecType,
|
||||
fetchEabList,
|
||||
@@ -234,4 +253,4 @@ export const useWorkflowStore = defineStore('workflow-store', () => {
|
||||
export const useStore = () => {
|
||||
const store = useWorkflowStore()
|
||||
return { ...store, ...storeToRefs(store) }
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,15 @@
|
||||
// 外部库依赖
|
||||
import { Transition, type Component as ComponentType, h, defineComponent, ref, onMounted, computed, watch } from 'vue' // 添加 watch
|
||||
import {
|
||||
Transition,
|
||||
type Component as ComponentType,
|
||||
h,
|
||||
defineComponent,
|
||||
ref,
|
||||
onMounted,
|
||||
computed,
|
||||
watch,
|
||||
onUnmounted,
|
||||
} from 'vue' // 添加 watch, onUnmounted
|
||||
import { NBadge, NIcon, NLayout, NLayoutContent, NLayoutHeader, NLayoutSider, NMenu, NTooltip } from 'naive-ui'
|
||||
import { RouterView } from 'vue-router'
|
||||
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@vicons/antd'
|
||||
@@ -12,7 +22,12 @@ import { useController } from './useController'
|
||||
import { $t } from '@locales/index'
|
||||
// 内部模块导入 - 样式
|
||||
import styles from './index.module.css'
|
||||
|
||||
// 内部模块导入 - API
|
||||
import { getVersion } from '@api/setting'
|
||||
// 内部模块导入 - 组件
|
||||
import UpdateLogModal from '@/components/UpdateLogModal'
|
||||
// 内部模块导入 - 类型
|
||||
import type { VersionData } from '@/types/setting'
|
||||
|
||||
/**
|
||||
* @description 基础布局组件,包含侧边栏导航、头部信息和内容区域。
|
||||
@@ -35,9 +50,39 @@ export default defineComponent({
|
||||
'actionColor',
|
||||
'layoutContentBackgroundColor',
|
||||
'siderLoginHeight', // 确保这个变量在 Naive UI 主题中存在或已自定义
|
||||
'contentPadding'
|
||||
'contentPadding',
|
||||
])
|
||||
|
||||
// 版本检查相关状态
|
||||
const hasUpdate = ref(false)
|
||||
const versionData = ref<VersionData | null>(null)
|
||||
const showUpdateModal = ref(false)
|
||||
const checkTimer = ref<NodeJS.Timeout | null>(null)
|
||||
|
||||
// 版本检查API
|
||||
const versionApi = getVersion()
|
||||
|
||||
// 检查版本更新
|
||||
const checkVersion = async () => {
|
||||
try {
|
||||
await versionApi.fetch()
|
||||
if (versionApi.data.value && versionApi.data.value.data) {
|
||||
const data = versionApi.data.value.data
|
||||
versionData.value = data
|
||||
hasUpdate.value = data.update === '1'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查版本更新失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 点击版本号
|
||||
const handleVersionClick = () => {
|
||||
if (hasUpdate.value && versionData.value) {
|
||||
showUpdateModal.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const siderWidth = ref(200)
|
||||
const siderCollapsedWidth = ref(60)
|
||||
|
||||
@@ -50,13 +95,28 @@ export default defineComponent({
|
||||
if (isMobile.value || isNarrowScreen.value) {
|
||||
isCollapsed.value = true
|
||||
}
|
||||
|
||||
// 初始检查版本
|
||||
checkVersion()
|
||||
|
||||
// 设置定时检查版本更新(每30分钟检查一次)
|
||||
checkTimer.value = setInterval(checkVersion, 30 * 60 * 1000)
|
||||
})
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
if (checkTimer.value) {
|
||||
clearInterval(checkTimer.value)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听屏幕宽度变化,自动折叠/展开菜单
|
||||
watch(isNarrowScreen, (newValue) => {
|
||||
if (newValue && !isMobile.value) { // 仅在非移动设备且宽度小于1100px时处理
|
||||
if (newValue && !isMobile.value) {
|
||||
// 仅在非移动设备且宽度小于1100px时处理
|
||||
isCollapsed.value = true
|
||||
} else if (!newValue && !isMobile.value) { // 宽度大于1100px且非移动设备时
|
||||
} else if (!newValue && !isMobile.value) {
|
||||
// 宽度大于1100px且非移动设备时
|
||||
isCollapsed.value = false
|
||||
}
|
||||
})
|
||||
@@ -109,13 +169,13 @@ export default defineComponent({
|
||||
class={[styles.sider, siderDynamicClass.value].join(' ')}
|
||||
bordered
|
||||
>
|
||||
<div class={`${styles.logoContainer} ${
|
||||
// Logo 容器的 'active' 状态 (仅在桌面端且折叠时应用)
|
||||
// 在移动端,由于 NLayoutSider 自身宽度不变,不应用 active 样式来改变 Logo 区域布局
|
||||
(isMobile.value ? false : isCollapsed.value)
|
||||
? styles.logoContainerActive
|
||||
: ''
|
||||
}`}>
|
||||
<div
|
||||
class={`${styles.logoContainer} ${
|
||||
// Logo 容器的 'active' 状态 (仅在桌面端且折叠时应用)
|
||||
// 在移动端,由于 NLayoutSider 自身宽度不变,不应用 active 样式来改变 Logo 区域布局
|
||||
(isMobile.value ? false : isCollapsed.value) ? styles.logoContainerActive : ''
|
||||
}`}
|
||||
>
|
||||
{/* Logo 显示逻辑 */}
|
||||
{(isMobile.value ? false : isCollapsed.value) ? (
|
||||
// 折叠时的 Logo (仅桌面端)
|
||||
@@ -134,11 +194,11 @@ export default defineComponent({
|
||||
<NTooltip placement="right" trigger="hover">
|
||||
{{
|
||||
trigger: () => (
|
||||
<div
|
||||
class={styles.menuToggleButton}
|
||||
onClick={() => toggleCollapse()}
|
||||
>
|
||||
<NIcon size={20}><MenuFoldOutlined /></NIcon> {/* 图标大小调整为 20 */}
|
||||
<div class={styles.menuToggleButton} onClick={() => toggleCollapse()}>
|
||||
<NIcon size={20}>
|
||||
<MenuFoldOutlined />
|
||||
</NIcon>{' '}
|
||||
{/* 图标大小调整为 20 */}
|
||||
</div>
|
||||
),
|
||||
default: () => <span>{$t('t_4_1744098802046')}</span>,
|
||||
@@ -172,9 +232,7 @@ export default defineComponent({
|
||||
{{
|
||||
trigger: () => (
|
||||
<div class={styles.headerMenuToggleButton} onClick={() => toggleCollapse()}>
|
||||
<NIcon size={20}>
|
||||
{isCollapsed.value ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
</NIcon>
|
||||
<NIcon size={20}>{isCollapsed.value ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}</NIcon>
|
||||
</div>
|
||||
),
|
||||
default: () => <span>展开主菜单</span>,
|
||||
@@ -183,8 +241,13 @@ export default defineComponent({
|
||||
</div>
|
||||
)}
|
||||
<div class={styles.systemInfo}>
|
||||
<NBadge value={1} show={false} dot>
|
||||
<span class="px-1 sm:px-[.5rem] cursor-pointer">v1.0.4</span>
|
||||
<NBadge value={1} show={hasUpdate.value} dot>
|
||||
<span
|
||||
class="px-[.8rem] sm:px-[.5rem] py-[.4rem] cursor-pointer hover:text-primary transition-colors text-[1.4rem] font-medium"
|
||||
onClick={handleVersionClick}
|
||||
>
|
||||
v1.0.4
|
||||
</span>
|
||||
</NBadge>
|
||||
</div>
|
||||
</NLayoutHeader>
|
||||
@@ -199,9 +262,10 @@ export default defineComponent({
|
||||
</NLayoutContent>
|
||||
</NLayout>
|
||||
{/* 移动端菜单展开时的背景遮罩 */}
|
||||
{showBackdrop.value && (
|
||||
<div class={styles.mobileMenuBackdrop} onClick={() => toggleCollapse()}></div>
|
||||
)}
|
||||
{showBackdrop.value && <div class={styles.mobileMenuBackdrop} onClick={() => toggleCollapse()}></div>}
|
||||
|
||||
{/* 更新日志弹窗 */}
|
||||
<UpdateLogModal v-model:show={showUpdateModal.value} versionData={versionData.value} />
|
||||
</NLayout>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
useLoadingMask,
|
||||
} from '@baota/naive-ui/hooks'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
import { isDomain } from '@baota/utils/business'
|
||||
import { isDomain, isPort, isIp } from '@baota/utils/business'
|
||||
import { $t } from '@locales/index'
|
||||
|
||||
// Store和组件
|
||||
@@ -64,6 +64,31 @@ const {
|
||||
// 错误处理
|
||||
const { handleError } = useError()
|
||||
|
||||
/**
|
||||
* 验证域名(或IP)+端口格式
|
||||
* @param value - 要验证的值
|
||||
* @returns {boolean} 如果是有效的域名、IP地址或它们加端口的格式,则返回 true
|
||||
*/
|
||||
const isDomainWithPort = (value: string): boolean => {
|
||||
if (!value) return false
|
||||
|
||||
// 检查是否包含端口号
|
||||
const parts = value.split(':')
|
||||
|
||||
if (parts.length === 1) {
|
||||
// 只有域名或IP,验证域名或IP地址
|
||||
return isDomain(value) || isIp(value)
|
||||
} else if (parts.length === 2) {
|
||||
// 域名/IP+端口格式
|
||||
const [host, port] = parts
|
||||
if (!host || !port) return false
|
||||
return (isDomain(host) || isIp(host)) && isPort(port)
|
||||
}
|
||||
|
||||
// 超过一个冒号,格式不正确(IPv6除外,但这里暂不处理IPv6+端口的复杂情况)
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 监控管理业务逻辑控制器
|
||||
* @description 处理监控列表页面的业务逻辑,包括表格展示、添加、编辑、删除等操作
|
||||
@@ -318,7 +343,7 @@ export const useMonitorFormController = (data: UpdateSiteMonitorParams | null =
|
||||
*/
|
||||
const config = computed(() => [
|
||||
useFormInput('名称', 'name'),
|
||||
useFormInput('域名', 'domain'),
|
||||
useFormInput('域名/IP地址', 'domain'),
|
||||
useFormInputNumber('周期(分钟)', 'cycle', { class: 'w-full' }),
|
||||
useFormCustom(() => {
|
||||
return (
|
||||
@@ -342,11 +367,11 @@ export const useMonitorFormController = (data: UpdateSiteMonitorParams | null =
|
||||
name: { required: true, message: '请输入名称', trigger: 'input' },
|
||||
domain: {
|
||||
required: true,
|
||||
message: '请输入正确的域名',
|
||||
message: '请输入正确的域名或IP地址',
|
||||
trigger: 'input',
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (!isDomain(value)) {
|
||||
callback(new Error('请输入正确的域名'))
|
||||
if (!isDomainWithPort(value)) {
|
||||
callback(new Error('请输入正确的域名或IP地址(支持域名:端口或IP:端口格式)'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
|
||||
@@ -1,20 +1,65 @@
|
||||
import { NCard, NSpace, NDescriptions, NDescriptionsItem, NIcon, NButton } from 'naive-ui'
|
||||
import { NCard, NSpace, NDescriptions, NDescriptionsItem, NIcon, NButton, NBadge, NAlert } from 'naive-ui'
|
||||
import { $t } from '@locales/index'
|
||||
import { LogoGithub } from '@vicons/ionicons5'
|
||||
import { getVersion } from '@api/setting'
|
||||
import type { VersionData } from '@/types/setting'
|
||||
/**
|
||||
* 关于我们标签页组件
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'AboutSettings',
|
||||
setup() {
|
||||
// 版本检查相关状态
|
||||
const versionData = ref<VersionData | null>(null)
|
||||
const hasUpdate = ref(false)
|
||||
|
||||
// 版本检查API
|
||||
const versionApi = getVersion()
|
||||
|
||||
// 检查版本更新
|
||||
const checkVersion = async () => {
|
||||
try {
|
||||
await versionApi.fetch()
|
||||
if (versionApi.data.value && versionApi.data.value.data) {
|
||||
const data = versionApi.data.value.data
|
||||
versionData.value = data
|
||||
hasUpdate.value = data.update === '1'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查版本更新失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到GitHub
|
||||
const goToGitHub = () => {
|
||||
window.open('https://github.com/allinssl/allinssl', '_blank')
|
||||
}
|
||||
|
||||
// 组件挂载时检查版本
|
||||
onMounted(() => {
|
||||
checkVersion()
|
||||
})
|
||||
|
||||
return () => (
|
||||
<div class="about-settings">
|
||||
<NCard title={$t('t_4_1745833932780')} class="mb-4">
|
||||
<NSpace vertical size={24}>
|
||||
<NDescriptions bordered>
|
||||
<NDescriptionsItem label={$t('t_5_1745833933241')}>
|
||||
<div class="flex items-center">
|
||||
<span class="text-[2rem] font-medium">v1.0.4</span>
|
||||
<div class="flex items-center space-x-[1.2rem]">
|
||||
<span class="text-[2.0rem] font-medium">v1.0.4</span>
|
||||
{hasUpdate.value && versionData.value && (
|
||||
<div class="relative">
|
||||
<NBadge value="NEW" type="success" offset={[4, -3]}>
|
||||
<span
|
||||
class="text-[1.4rem] text-primary cursor-pointer font-medium inline-block px-[.8rem] py-[.4rem]"
|
||||
onClick={goToGitHub}
|
||||
>
|
||||
{versionData.value.new_version} 可用
|
||||
</span>
|
||||
</NBadge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label={$t('t_29_1746667589773')}>
|
||||
@@ -22,7 +67,7 @@ export default defineComponent({
|
||||
<NIcon size="20" class="text-gray-600">
|
||||
<LogoGithub />
|
||||
</NIcon>
|
||||
<NButton text tag="a" href="https://github.com/allinssl/allinssl" target="_blank" type="primary">
|
||||
<NButton text onClick={goToGitHub} type="primary">
|
||||
https://github.com/allinssl/allinssl
|
||||
</NButton>
|
||||
</div>
|
||||
@@ -31,6 +76,33 @@ export default defineComponent({
|
||||
</NSpace>
|
||||
</NCard>
|
||||
|
||||
{/* 新版本信息卡片 */}
|
||||
{hasUpdate.value && versionData.value && (
|
||||
<NCard title="发现新版本" class="mb-4">
|
||||
<NAlert type="info" title={`新版本 ${versionData.value.new_version} 已发布`} class="mb-[1.6rem]">
|
||||
<div class="text-[1.4rem]">
|
||||
<div class="mb-[1.2rem] text-[1.4rem]">发布日期: {versionData.value.date}</div>
|
||||
<div class="mb-[1.2rem] text-[1.4rem]">
|
||||
<strong>更新内容:</strong>
|
||||
</div>
|
||||
<div class="whitespace-pre-line text-gray-700 text-[1.3rem] leading-relaxed">
|
||||
{versionData.value.log.replace(/\\r\\n/g, '\n').replace(/\\n/g, '\n')}
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<NButton size="medium" type="primary" onClick={goToGitHub}>
|
||||
<div class="flex items-center">
|
||||
<NIcon size="18" class="mr-2">
|
||||
<LogoGithub />
|
||||
</NIcon>
|
||||
前往GitHub下载
|
||||
</div>
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</NAlert>
|
||||
</NCard>
|
||||
)}
|
||||
|
||||
<NCard title={$t('t_13_1745833933630')} class="mb-4">
|
||||
<div class="about-content">
|
||||
<p class="text-gray-700 leading-relaxed">
|
||||
|
||||
@@ -65,6 +65,19 @@ export default defineComponent({
|
||||
return () => (
|
||||
<div class="webhook-channel-form">
|
||||
<WebhookForm labelPlacement="top"></WebhookForm>
|
||||
|
||||
{/* 模板变量说明 */}
|
||||
<div class="mt-4 p-4 bg-gray-50 rounded-md">
|
||||
<div class="font-medium text-gray-700 mb-3 text-xl">模板变量将在发送时替换成实际值:</div>
|
||||
<div class="text-gray-600 space-y-3 text-lg">
|
||||
<div>
|
||||
<code class="px-2 py-1 bg-gray-200 rounded text-lg font-mono">__subject__</code>:通知主题
|
||||
</div>
|
||||
<div>
|
||||
<code class="px-2 py-1 bg-gray-200 rounded text-lg font-mono">__body__</code>:通知内容
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { useForm, useModalHooks } from '@baota/naive-ui/hooks'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
import { useWecomChannelFormController } from './useController'
|
||||
import { useStore } from '@settings/useStore'
|
||||
|
||||
import type { ReportWecom, ReportType } from '@/types/setting'
|
||||
|
||||
/**
|
||||
* 企业微信通知渠道表单组件
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'WecomChannelModel',
|
||||
props: {
|
||||
data: {
|
||||
type: Object as PropType<ReportType<ReportWecom> | null>,
|
||||
default: () => null,
|
||||
},
|
||||
},
|
||||
setup(props: { data: ReportType<ReportWecom> | null }) {
|
||||
const { handleError } = useError()
|
||||
const { confirm } = useModalHooks()
|
||||
const { fetchNotifyChannels } = useStore()
|
||||
const { config, rules, wecomChannelForm, submitForm } = useWecomChannelFormController()
|
||||
|
||||
if (props.data) {
|
||||
const { name, config } = props.data
|
||||
wecomChannelForm.value = {
|
||||
name,
|
||||
...config,
|
||||
}
|
||||
}
|
||||
// 使用表单hooks
|
||||
const {
|
||||
component: WecomForm,
|
||||
example,
|
||||
data,
|
||||
} = useForm({
|
||||
config,
|
||||
defaultValue: wecomChannelForm,
|
||||
rules,
|
||||
})
|
||||
|
||||
// 关联确认按钮
|
||||
confirm(async (close) => {
|
||||
try {
|
||||
const { name, ...other } = data.value
|
||||
await example.value?.validate()
|
||||
const res = await submitForm(
|
||||
{
|
||||
type: 'workwx',
|
||||
name: name || '',
|
||||
config: other,
|
||||
},
|
||||
example,
|
||||
props.data?.id,
|
||||
)
|
||||
|
||||
fetchNotifyChannels()
|
||||
if (res) close()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
})
|
||||
|
||||
return () => (
|
||||
<div class="wecom-channel-form">
|
||||
<WecomForm labelPlacement="top"></WecomForm>
|
||||
|
||||
{/* 模板变量说明 */}
|
||||
<div class="mt-4 p-4 bg-gray-50 rounded-md">
|
||||
<div class="font-medium text-gray-700 mb-3 text-xl">模板变量将在发送时替换成实际值:</div>
|
||||
<div class="text-gray-600 space-y-3 text-lg">
|
||||
<div>
|
||||
<code class="px-2 py-1 bg-gray-200 rounded text-lg font-mono">__subject__</code>:通知主题
|
||||
</div>
|
||||
<div>
|
||||
<code class="px-2 py-1 bg-gray-200 rounded text-lg font-mono">__body__</code>:通知内容
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 pt-3 border-t border-gray-200">
|
||||
<a
|
||||
href="https://developer.work.weixin.qq.com/document/path/91770"
|
||||
target="_blank"
|
||||
class="hover:opacity-80 text-xl"
|
||||
style="color: #20a50a"
|
||||
>
|
||||
📖 查看企业微信机器人消息格式教程
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -3,13 +3,21 @@ import { useFormHooks, useLoadingMask } from '@baota/naive-ui/hooks'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
import { $t } from '@locales/index'
|
||||
import { useStore } from '@settings/useStore'
|
||||
import type { ReportMail, ReportFeishu, ReportWebhook, ReportDingtalk, AddReportParams } from '@/types/setting'
|
||||
import type {
|
||||
ReportMail,
|
||||
ReportFeishu,
|
||||
ReportWebhook,
|
||||
ReportDingtalk,
|
||||
ReportWecom,
|
||||
AddReportParams,
|
||||
} from '@/types/setting'
|
||||
|
||||
const {
|
||||
emailChannelForm,
|
||||
feishuChannelForm,
|
||||
webhookChannelForm,
|
||||
dingtalkChannelForm,
|
||||
wecomChannelForm,
|
||||
addReportChannel,
|
||||
updateReportChannel,
|
||||
} = useStore()
|
||||
@@ -359,3 +367,100 @@ export const useDingtalkChannelFormController = () => {
|
||||
submitForm,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 企业微信通知渠道表单控制器
|
||||
* @function useWecomChannelFormController
|
||||
* @description 提供企业微信通知渠道表单的配置、规则和提交方法
|
||||
* @returns {object} 返回表单相关配置、规则和方法
|
||||
*/
|
||||
export const useWecomChannelFormController = () => {
|
||||
const { open: openLoad, close: closeLoad } = useLoadingMask({ text: $t('t_0_1746667592819') })
|
||||
/**
|
||||
* 表单验证规则
|
||||
* @type {FormRules}
|
||||
*/
|
||||
const rules: FormRules = {
|
||||
name: {
|
||||
required: true,
|
||||
trigger: ['input', 'blur'],
|
||||
message: $t('t_25_1746773349596'),
|
||||
},
|
||||
url: {
|
||||
required: true,
|
||||
trigger: ['input', 'blur'],
|
||||
message: '请输入企业微信webhook地址',
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单配置
|
||||
* @type {ComputedRef<FormConfig>}
|
||||
* @description 生成企业微信通知渠道表单的字段配置
|
||||
*/
|
||||
const config = computed(() => [
|
||||
useFormInput($t('t_2_1745289353944'), 'name'),
|
||||
useFormInput('企业微信WebHook地址', 'url'),
|
||||
useFormTextarea(
|
||||
'推送数据格式',
|
||||
'data',
|
||||
{
|
||||
placeholder: `请输入企业微信推送数据格式,支持模板变量 __subject__ 和 __body__
|
||||
|
||||
示例格式:
|
||||
{
|
||||
"msgtype": "news",
|
||||
"news": {
|
||||
"articles": [
|
||||
{
|
||||
"title": "__subject__",
|
||||
"description": "__body__。",
|
||||
"url": "https://allinssl.com/",
|
||||
"picurl": "https://allinssl.com/logo.svg"
|
||||
}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
rows: 12,
|
||||
},
|
||||
{ showRequireMark: false },
|
||||
),
|
||||
])
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
* @async
|
||||
* @function submitForm
|
||||
* @description 验证并提交企业微信通知渠道表单
|
||||
* @param {any} params - 表单参数
|
||||
* @param {Ref<FormInst>} formRef - 表单实例引用
|
||||
* @returns {Promise<boolean>} 提交成功返回true,失败返回false
|
||||
*/
|
||||
const submitForm = async (
|
||||
{ config, ...other }: AddReportParams<ReportWecom>,
|
||||
formRef: Ref<FormInst | null>,
|
||||
id?: number,
|
||||
) => {
|
||||
try {
|
||||
openLoad()
|
||||
if (id) {
|
||||
await updateReportChannel({ id, config: JSON.stringify(config), ...other })
|
||||
} else {
|
||||
await addReportChannel({ config: JSON.stringify(config), ...other })
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
return false
|
||||
} finally {
|
||||
closeLoad()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
config,
|
||||
rules,
|
||||
wecomChannelForm,
|
||||
submitForm,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export default defineComponent({
|
||||
openAddFeishuChannelModal,
|
||||
openAddWebhookChannelModal,
|
||||
openAddDingtalkChannelModal,
|
||||
openAddWecomChannelModal,
|
||||
editChannelConfig,
|
||||
testChannelConfig,
|
||||
confirmDeleteChannel,
|
||||
@@ -64,6 +65,12 @@ export default defineComponent({
|
||||
{$t('t_1_1746676859550')}
|
||||
</NButton>
|
||||
)
|
||||
} else if (type === 'workwx') {
|
||||
return (
|
||||
<NButton strong secondary type="primary" onClick={() => openAddWecomChannelModal(getConfiguredCount(type))}>
|
||||
{$t('t_1_1746676859550')}
|
||||
</NButton>
|
||||
)
|
||||
}
|
||||
// 其他渠道暂未支持
|
||||
return (
|
||||
@@ -100,7 +107,7 @@ export default defineComponent({
|
||||
color: '#1677ff',
|
||||
},
|
||||
{
|
||||
type: 'wecom',
|
||||
type: 'workwx',
|
||||
name: $t('t_7_1746676857191'),
|
||||
description: $t('t_8_1746676860457'),
|
||||
color: '#07c160',
|
||||
|
||||
@@ -10,6 +10,7 @@ import EmailChannelModel from './components/channel/EmailChannelModel'
|
||||
import FeishuChannelModel from './components/channel/FeishuChannelModel'
|
||||
import WebhookChannelModel from './components/channel/WebhookChannelModel'
|
||||
import DingtalkChannelModel from './components/channel/DingtalkChannelModel'
|
||||
import WecomChannelModel from './components/channel/WecomChannelModel'
|
||||
import type { ReportMail, SaveSettingParams, ReportType } from '@/types/setting'
|
||||
|
||||
const {
|
||||
@@ -178,6 +179,25 @@ export const useController = () => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开添加企业微信通知渠道弹窗
|
||||
* @function openAddWecomChannelModal
|
||||
* @description 打开添加企业微信通知渠道的模态框,并在关闭后刷新通知渠道列表
|
||||
* @returns {void} 无返回值
|
||||
*/
|
||||
const openAddWecomChannelModal = (limit: number = 1) => {
|
||||
if (limit >= 1) {
|
||||
message.warning('企业微信通知渠道已达到上限')
|
||||
return
|
||||
}
|
||||
useModal({
|
||||
title: '添加企业微信通知',
|
||||
area: 650,
|
||||
component: WecomChannelModel,
|
||||
footer: true,
|
||||
})
|
||||
}
|
||||
|
||||
// 处理启用状态切换
|
||||
const handleEnableChange = async (item: ReportType<ReportMail>) => {
|
||||
useDialog({
|
||||
@@ -263,6 +283,17 @@ export const useController = () => {
|
||||
footer: true,
|
||||
onClose: () => fetchNotifyChannels(),
|
||||
})
|
||||
} else if (item.type === 'workwx') {
|
||||
useModal({
|
||||
title: '编辑企业微信通知',
|
||||
area: 650,
|
||||
component: WecomChannelModel,
|
||||
componentProps: {
|
||||
data: item,
|
||||
},
|
||||
footer: true,
|
||||
onClose: () => fetchNotifyChannels(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +305,13 @@ export const useController = () => {
|
||||
* @returns {void} 无返回值
|
||||
*/
|
||||
const testChannelConfig = (item: ReportType<any>) => {
|
||||
if (item.type !== 'mail' && item.type !== 'feishu' && item.type !== 'webhook' && item.type !== 'dingtalk') {
|
||||
if (
|
||||
item.type !== 'mail' &&
|
||||
item.type !== 'feishu' &&
|
||||
item.type !== 'webhook' &&
|
||||
item.type !== 'dingtalk' &&
|
||||
item.type !== 'workwx'
|
||||
) {
|
||||
message.warning($t('t_19_1746773352558'))
|
||||
return
|
||||
}
|
||||
@@ -283,6 +320,7 @@ export const useController = () => {
|
||||
feishu: $t('t_34_1746773350153'),
|
||||
webhook: $t('t_3_1748591484673'),
|
||||
dingtalk: $t('t_32_1746773348993'),
|
||||
workwx: $t('t_33_1746773350932'),
|
||||
}
|
||||
const { open, close } = useLoadingMask({ text: $t('t_4_1748591492587', { type: typeMap[item.type] }) })
|
||||
useDialog({
|
||||
@@ -337,6 +375,7 @@ export const useController = () => {
|
||||
openAddFeishuChannelModal,
|
||||
openAddWebhookChannelModal,
|
||||
openAddDingtalkChannelModal,
|
||||
openAddWecomChannelModal,
|
||||
handleEnableChange,
|
||||
editChannelConfig,
|
||||
testChannelConfig,
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
ReportFeishu,
|
||||
ReportWebhook,
|
||||
ReportDingtalk,
|
||||
ReportWecom,
|
||||
} from '@/types/setting'
|
||||
|
||||
const { handleError } = useError()
|
||||
@@ -63,7 +64,7 @@ export const useSettingsStore = defineStore('settings-store', () => {
|
||||
const channelTypes = ref<Record<string, string>>({
|
||||
mail: $t('t_68_1745289354676'),
|
||||
dingtalk: $t('t_32_1746773348993'),
|
||||
wecom: $t('t_33_1746773350932'),
|
||||
workwx: $t('t_33_1746773350932'),
|
||||
feishu: $t('t_34_1746773350153'),
|
||||
webhook: 'WebHook',
|
||||
})
|
||||
@@ -107,6 +108,26 @@ export const useSettingsStore = defineStore('settings-store', () => {
|
||||
secret: '', // 钉钉webhook加密密钥(可选)
|
||||
})
|
||||
|
||||
// 企业微信通知渠道表单
|
||||
const wecomChannelForm = ref<ReportWecom>({
|
||||
name: '',
|
||||
enabled: '1',
|
||||
url: '', // 企业微信webhook地址
|
||||
data: `{
|
||||
"msgtype": "news",
|
||||
"news": {
|
||||
"articles": [
|
||||
{
|
||||
"title": "__subject__",
|
||||
"description": "__body__。",
|
||||
"url": "https://allinssl.com/",
|
||||
"picurl": "https://allinssl.com/logo.svg"
|
||||
}
|
||||
]
|
||||
}
|
||||
}`, // 企业微信推送数据格式
|
||||
})
|
||||
|
||||
// 关于页面数据
|
||||
const aboutInfo = ref({
|
||||
version: '1.0.0',
|
||||
@@ -250,6 +271,7 @@ export const useSettingsStore = defineStore('settings-store', () => {
|
||||
feishuChannelForm,
|
||||
webhookChannelForm,
|
||||
dingtalkChannelForm,
|
||||
wecomChannelForm,
|
||||
aboutInfo,
|
||||
|
||||
// 方法
|
||||
|
||||
Reference in New Issue
Block a user