【调整】SSH地址支持域名形式

【新增】支持自定义监控端口
【新增】通知类型-企业微信
【新增】申请证书(Buypass)、自定义ACME服务器地址
【新增】授权API管理(namesilo、Bunny、Gcore、name.com、京东云)
This commit is contained in:
chudong
2025-06-07 17:37:42 +08:00
parent 87ae1c9570
commit f0d83d23c6
106 changed files with 1570 additions and 223 deletions

View File

@@ -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
}
},
)

View File

@@ -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()

View File

@@ -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

View File

@@ -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) }
}
}

View File

@@ -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>
)
},

View File

@@ -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()
}

View File

@@ -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">

View File

@@ -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>
)
},

View File

@@ -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>
)
},
})

View File

@@ -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,
}
}

View File

@@ -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',

View File

@@ -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,

View File

@@ -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,
// 方法