diff --git a/frontend/.sync-project-history b/frontend/.sync-project-history new file mode 100644 index 0000000..e69de29 diff --git a/frontend/apps/allin-ssl/src/api/access.ts b/frontend/apps/allin-ssl/src/api/access.ts new file mode 100644 index 0000000..3f0e71a --- /dev/null +++ b/frontend/apps/allin-ssl/src/api/access.ts @@ -0,0 +1,131 @@ +// Type imports +import type { useAxiosReturn } from '@baota/hooks/axios' +import type { + AccessListParams, + AccessListResponse, + AddAccessParams, + DeleteAccessParams, + GetAccessAllListParams, + GetAccessAllListResponse, + UpdateAccessParams, + // CA授权相关类型 + EabListParams, + EabListResponse, + EabAddParams, + EabUpdateParams, + EabDeleteParams, + EabGetAllListParams, + EabGetAllListResponse, + // 新增类型 + TestAccessParams, + GetSitesParams, + GetSitesResponse, +} from '@/types/access' // Sorted types +import type { AxiosResponseData } from '@/types/public' + +// Relative internal imports +import { useApi } from '@api/index' + +/** + * @description 获取授权列表 + * @param {AccessListParams} [params] 请求参数 + * @returns {useAxiosReturn} 获取授权列表的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const getAccessList = (params?: AccessListParams): useAxiosReturn => + useApi('/v1/access/get_list', params) + +/** + * @description 新增授权 + * @param {AddAccessParams} [params] 请求参数 + * @returns {useAxiosReturn>} 新增授权的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const addAccess = ( + params?: AddAccessParams, +): useAxiosReturn> => + useApi>('/v1/access/add_access', params) + +/** + * @description 修改授权 + * @param {UpdateAccessParams} [params] 请求参数 + * @returns {useAxiosReturn>} 修改授权的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const updateAccess = ( + params?: UpdateAccessParams, +): useAxiosReturn> => + useApi>('/v1/access/upd_access', params) + +/** + * @description 删除授权 + * @param {DeleteAccessParams} [params] 请求参数 + * @returns {useAxiosReturn} 删除授权的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const deleteAccess = (params?: DeleteAccessParams): useAxiosReturn => + useApi('/v1/access/del_access', params) + +/** + * @description 获取DNS提供商列表 + * @param {GetAccessAllListParams} [params] 请求参数 + * @returns {useAxiosReturn} 获取DNS提供商列表的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const getAccessAllList = ( + params?: GetAccessAllListParams, +): useAxiosReturn => + useApi('/v1/access/get_all', params) + +/** + * @description 获取CA授权列表 + * @param {EabListParams} [params] 请求参数 + * @returns {useAxiosReturn} 获取CA授权列表的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const getEabList = (params?: EabListParams): useAxiosReturn => + useApi('/v1/access/get_eab_list', params) + +/** + * @description 添加CA授权 + * @param {EabAddParams} [params] 请求参数 + * @returns {useAxiosReturn} 添加CA授权的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const addEab = (params?: EabAddParams): useAxiosReturn => + useApi('/v1/access/add_eab', params) + +/** + * @description 修改CA授权 + * @param {EabUpdateParams} [params] 请求参数 + * @returns {useAxiosReturn} 修改CA授权的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const updateEab = (params?: EabUpdateParams): useAxiosReturn => + useApi('/v1/access/upd_eab', params) + +/** + * @description 删除CA授权 + * @param {EabDeleteParams} [params] 请求参数 + * @returns {useAxiosReturn} 删除CA授权的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const deleteEab = (params?: EabDeleteParams): useAxiosReturn => + useApi('/v1/access/del_eab', params) + +/** + * @description 获取CA授权列表下拉框 + * @param {EabGetAllListParams} [params] 请求参数 + * @returns {useAxiosReturn} 获取CA授权列表下拉框的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const getAllEabList = ( + params?: EabGetAllListParams, +): useAxiosReturn => + useApi('/v1/access/get_all_eab', params) + +/** + * @description 测试授权API + * @param {TestAccessParams} [params] 请求参数 + * @returns {useAxiosReturn} 测试授权的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const testAccess = (params?: TestAccessParams): useAxiosReturn => + useApi('/v1/access/test_access', params) + +/** + * @description 获取网站列表 + * @param {GetSitesParams} [params] 请求参数 + * @returns {useAxiosReturn} 获取网站列表的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const getSites = (params?: GetSitesParams): useAxiosReturn => + useApi('/v1/access/get_sites', params) diff --git a/frontend/apps/allin-ssl/src/api/cert.ts b/frontend/apps/allin-ssl/src/api/cert.ts new file mode 100644 index 0000000..f3c9a31 --- /dev/null +++ b/frontend/apps/allin-ssl/src/api/cert.ts @@ -0,0 +1,61 @@ +// External library dependencies +import axios, { AxiosResponse } from 'axios' + +// Type imports +import type { useAxiosReturn } from '@baota/hooks/axios' +import type { + ApplyCertParams, + ApplyCertResponse, + CertListParams, + CertListResponse, + DeleteCertParams, + DeleteCertResponse, + DownloadCertParams, + DownloadCertResponse, // Ensuring this type is imported + UploadCertParams, + UploadCertResponse, +} from '@/types/cert' // Path alias and sorted types + +// Relative internal imports +import { useApi } from '@api/index' + +/** + * @description 获取证书列表 + * @param {CertListParams} [params] 请求参数 + * @returns {useAxiosReturn} 获取证书列表的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const getCertList = (params?: CertListParams): useAxiosReturn => + useApi('/v1/cert/get_list', params) + +/** + * @description 申请证书 + * @param {ApplyCertParams} [params] 请求参数 + * @returns {useAxiosReturn} 申请证书的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const applyCert = (params?: ApplyCertParams): useAxiosReturn => + useApi('/v1/cert/apply_cert', params) + +/** + * @description 上传证书 + * @param {UploadCertParams} [params] 请求参数 + * @returns {useAxiosReturn} 上传证书的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const uploadCert = (params?: UploadCertParams): useAxiosReturn => + useApi('/v1/cert/upload_cert', params) + +/** + * @description 删除证书 + * @param {DeleteCertParams} [params] 请求参数 + * @returns {useAxiosReturn} 删除证书的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const deleteCert = (params?: DeleteCertParams): useAxiosReturn => + useApi('/v1/cert/del_cert', params) + +/** + * @description 下载证书 + * @param {DownloadCertParams} [params] 请求参数 + * @returns {Promise>} 下载结果的 Promise 对象。 + */ +export const downloadCert = (params?: DownloadCertParams): Promise> => { + return axios.get('/v1/cert/download', { params }) +} diff --git a/frontend/apps/allin-ssl/src/api/index.ts b/frontend/apps/allin-ssl/src/api/index.ts new file mode 100644 index 0000000..0cc228e --- /dev/null +++ b/frontend/apps/allin-ssl/src/api/index.ts @@ -0,0 +1,98 @@ +// External Libraries (sorted alphabetically by module path) +import { HttpClient, useAxios, useAxiosReturn } from '@baota/hooks/axios' +import { errorMiddleware } from '@baota/hooks/axios/model' +import { isDev } from '@baota/utils/browser' +import { AxiosError } from 'axios' +import MD5 from 'crypto-js/md5' + +// Type Imports (sorted alphabetically by module path) +import type { AxiosResponseData } from '@/types/public' +import type { Ref } from 'vue' + +// Relative Internal Imports (sorted alphabetically by module path) +import { router } from '@router/index' + +/** + * @description 处理返回数据,如果状态码为 401 或 404 + * @param {AxiosError} error 错误对象 + * @returns {AxiosError} 错误对象 + */ +export const responseHandleStatusCode = errorMiddleware((error: AxiosError) => { + // 处理 401 状态码 + if (error.status === 401) { + router.push(`/login`) + } + // 处理404状态码 + if (error.status === 404) { + // router.go(0) // 刷新页面 + } + return error +}) + +/** + * @description 返回数据 + * @param {T} data 数据 + * @returns {AxiosResponseData} 返回数据 + */ +export const useApiReturn = (data: T, message?: string): AxiosResponseData => { + return { + code: 200, + count: 0, + data, + message: message || '请求返回值错误,请检查', + status: false, + } as AxiosResponseData +} + +/** + * @description 创建http客户端实例 + */ +export const instance = new HttpClient({ + baseURL: isDev() ? '/api' : '/', + timeout: 50000, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + middlewares: [responseHandleStatusCode], +}) + +/** + * @description API Token 结构 + */ +interface ApiTokenResult { + api_token: string + timestamp: number +} + +/** + * @description 创建api token + * @returns {ApiTokenResult} 包含API token和时间戳的对象 + */ +export const createApiToken = (): ApiTokenResult => { + const now = new Date().getTime() + const apiKey = '123456' // 注意: 此处为硬编码密钥,建议后续优化 + const api_token = MD5(now + MD5(apiKey).toString()).toString() + return { api_token, timestamp: now } +} + +/** + * @description 创建axios请求 + * @param {string} url 请求地址 + * @param {Z} [params] 请求参数 + * @returns {useAxiosReturn} 返回结果 + */ +export const useApi = >(url: string, params?: Z) => { + const { urlRef, paramsRef, ...other } = useAxios(instance) + const apiParams = createApiToken() + urlRef.value = url + paramsRef.value = isDev() ? { ...(params || {}), ...apiParams } : params || {} + return { urlRef, paramsRef: paramsRef as Ref, ...other } as useAxiosReturn +} + +// 导出所有模块 +export * from './public' +export * from './workflow' +export * from './cert' +export * from './access' +export * from './monitor' +export * from './setting' diff --git a/frontend/apps/allin-ssl/src/api/monitor.ts b/frontend/apps/allin-ssl/src/api/monitor.ts new file mode 100644 index 0000000..0fef03d --- /dev/null +++ b/frontend/apps/allin-ssl/src/api/monitor.ts @@ -0,0 +1,60 @@ +// Type imports +import type { useAxiosReturn } from '@baota/hooks/axios' +import type { + AddSiteMonitorParams, + DeleteSiteMonitorParams, + SetSiteMonitorParams, + SiteMonitorListParams, + SiteMonitorListResponse, + UpdateSiteMonitorParams, +} from '@/types/monitor' // Sorted types +import type { AxiosResponseData } from '@/types/public' + +// Relative internal imports +import { useApi } from '@api/index' + +/** + * @description 获取站点监控列表 + * @param {SiteMonitorListParams} [params] 请求参数 + * @returns {useAxiosReturn} 获取站点监控列表的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const getSiteMonitorList = ( + params?: SiteMonitorListParams, +): useAxiosReturn => + useApi('/v1/siteMonitor/get_list', params) + +/** + * @description 新增站点监控 + * @param {AddSiteMonitorParams} [params] 请求参数 + * @returns {useAxiosReturn} 新增站点监控的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const addSiteMonitor = (params?: AddSiteMonitorParams): useAxiosReturn => + useApi('/v1/siteMonitor/add_site_monitor', params) + +/** + * @description 修改站点监控 + * @param {UpdateSiteMonitorParams} [params] 请求参数 + * @returns {useAxiosReturn} 修改站点监控的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const updateSiteMonitor = ( + params?: UpdateSiteMonitorParams, +): useAxiosReturn => + useApi('/v1/siteMonitor/upd_site_monitor', params) + +/** + * @description 删除站点监控 + * @param {DeleteSiteMonitorParams} [params] 请求参数 + * @returns {useAxiosReturn} 删除站点监控的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const deleteSiteMonitor = ( + params?: DeleteSiteMonitorParams, +): useAxiosReturn => + useApi('/v1/siteMonitor/del_site_monitor', params) + +/** + * @description 启用/禁用站点监控 + * @param {SetSiteMonitorParams} [params] 请求参数 + * @returns {useAxiosReturn} 启用/禁用站点监控的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const setSiteMonitor = (params?: SetSiteMonitorParams): useAxiosReturn => + useApi('/v1/siteMonitor/set_site_monitor', params) diff --git a/frontend/apps/allin-ssl/src/api/public.ts b/frontend/apps/allin-ssl/src/api/public.ts new file mode 100644 index 0000000..de7a202 --- /dev/null +++ b/frontend/apps/allin-ssl/src/api/public.ts @@ -0,0 +1,47 @@ +// External library dependencies +import axios, { type AxiosResponse } from 'axios' + +// Type imports +import type { useAxiosReturn } from '@baota/hooks/axios' +import type { + AxiosResponseData, + GetOverviewsParams, + GetOverviewsResponse, + loginCodeResponse, // Added this type based on usage + loginParams, + loginResponse, +} from '@/types/public' // Sorted types + +// Relative internal imports +import { useApi } from '@api/index' + +/** + * @description 登录 + * @param {loginParams} [params] 登录参数 + * @returns {useAxiosReturn} 登录操作的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const login = (params?: loginParams): useAxiosReturn => + useApi('/v1/login/sign', params) + +/** + * @description 获取登录验证码 + * @returns {Promise>} 获取登录验证码的 Promise 对象。 + */ +export const getLoginCode = (): Promise> => { + return axios.get('/v1/login/get_code') +} + +/** + * @description 登出 + * @returns {useAxiosReturn} 登出操作的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const signOut = (): useAxiosReturn => + useApi('/v1/login/sign-out', {}) + +/** + * @description 获取首页概览 + * @param {GetOverviewsParams} [params] 请求参数 + * @returns {useAxiosReturn} 获取首页概览数据的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const getOverviews = (params?: GetOverviewsParams): useAxiosReturn => + useApi('/v1/overview/get_overviews', params) diff --git a/frontend/apps/allin-ssl/src/api/setting.ts b/frontend/apps/allin-ssl/src/api/setting.ts new file mode 100644 index 0000000..4f702ca --- /dev/null +++ b/frontend/apps/allin-ssl/src/api/setting.ts @@ -0,0 +1,73 @@ +// Type imports +import type { useAxiosReturn } from '@baota/hooks/axios' +import type { AxiosResponseData } from '@/types/public' +import type { + AddReportParams, + DeleteReportParams, + GetReportListParams, + GetReportListResponse, + GetSettingParams, + GetSettingResponse, + SaveSettingParams, + TestReportParams, + UpdateReportParams, +} from '@/types/setting' // Sorted types + +// Relative internal imports +import { useApi } from '@api/index' + +/** + * @description 获取系统设置 + * @param {GetSettingParams} [params] 请求参数 + * @returns {useAxiosReturn} 获取系统设置的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const getSystemSetting = (params?: GetSettingParams): useAxiosReturn => + useApi('/v1/setting/get_setting', params) + +/** + * @description 保存系统设置 + * @param {SaveSettingParams} [params] 请求参数 + * @returns {useAxiosReturn} 保存系统设置的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const saveSystemSetting = (params?: SaveSettingParams): useAxiosReturn => + useApi('/v1/setting/save_setting', params) + +/** + * @description 添加告警 + * @param {AddReportParams} [params] 请求参数 + * @returns {useAxiosReturn} 添加告警的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const addReport = (params?: AddReportParams): useAxiosReturn => + useApi('/v1/report/add_report', params) + +/** + * @description 更新告警 + * @param {UpdateReportParams} [params] 请求参数 + * @returns {useAxiosReturn} 更新告警的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const updateReport = (params?: UpdateReportParams): useAxiosReturn => + useApi('/v1/report/upd_report', params) + +/** + * @description 删除告警 + * @param {DeleteReportParams} [params] 请求参数 + * @returns {useAxiosReturn} 删除告警的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const deleteReport = (params?: DeleteReportParams): useAxiosReturn => + useApi('/v1/report/del_report', params) + +/** + * @description 测试告警 + * @param {TestReportParams} [params] 请求参数 + * @returns {useAxiosReturn} 测试告警的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const testReport = (params?: TestReportParams): useAxiosReturn => + useApi('/v1/report/notify_test', params) + +/** + * @description 获取告警类型列表 + * @param {GetReportListParams} [params] 请求参数 + * @returns {useAxiosReturn} 获取告警类型列表的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const getReportList = (params?: GetReportListParams): useAxiosReturn => + useApi('/v1/report/get_list', params) diff --git a/frontend/apps/allin-ssl/src/api/workflow.ts b/frontend/apps/allin-ssl/src/api/workflow.ts new file mode 100644 index 0000000..daa049f --- /dev/null +++ b/frontend/apps/allin-ssl/src/api/workflow.ts @@ -0,0 +1,97 @@ +// Type imports +import type { useAxiosReturn } from '@baota/hooks/axios' +import type { AxiosResponseData } from '@/types/public' +import type { + AddWorkflowParams, + DeleteWorkflowParams, + EnableWorkflowParams, + ExecuteWorkflowParams, + UpdateWorkflowExecTypeParams, + UpdateWorkflowParams, + WorkflowHistoryDetailParams, + WorkflowHistoryParams, + WorkflowHistoryResponse, + WorkflowListParams, + WorkflowListResponse, +} from '@/types/workflow' // Sorted types + +// Relative internal imports +import { useApi } from '@api/index' + +/** + * @description 获取工作流列表 + * @param {WorkflowListParams} [params] 请求参数 + * @returns {useAxiosReturn} 获取工作流列表的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const getWorkflowList = (params?: WorkflowListParams): useAxiosReturn => + useApi('/v1/workflow/get_list', params) + +/** + * @description 新增工作流 + * @param {AddWorkflowParams} [params] 请求参数 + * @returns {useAxiosReturn} 新增工作流的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const addWorkflow = (params?: AddWorkflowParams): useAxiosReturn => + useApi('/v1/workflow/add_workflow', params) + +/** + * @description 修改工作流 + * @param {UpdateWorkflowParams} [params] 请求参数 + * @returns {useAxiosReturn} 修改工作流的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const updateWorkflow = (params?: UpdateWorkflowParams): useAxiosReturn => + useApi('/v1/workflow/upd_workflow', params) + +/** + * @description 删除工作流 + * @param {DeleteWorkflowParams} [params] 请求参数 + * @returns {useAxiosReturn} 删除工作流的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const deleteWorkflow = (params?: DeleteWorkflowParams): useAxiosReturn => + useApi('/v1/workflow/del_workflow', params) + +/** + * @description 获取工作流执行历史 + * @param {WorkflowHistoryParams} [params] 请求参数 + * @returns {useAxiosReturn} 获取工作流执行历史的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const getWorkflowHistory = ( + params?: WorkflowHistoryParams, +): useAxiosReturn => + useApi('/v1/workflow/get_workflow_history', params) + +/** + * @description 获取工作流执行历史详情 + * @param {WorkflowHistoryDetailParams} [params] 请求参数 + * @returns {useAxiosReturn} 获取工作流执行历史详情的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const getWorkflowHistoryDetail = ( + params?: WorkflowHistoryDetailParams, +): useAxiosReturn => + useApi('/v1/workflow/get_exec_log', params) + +/** + * @description 手动执行工作流 + * @param {ExecuteWorkflowParams} [params] 请求参数 + * @returns {useAxiosReturn} 手动执行工作流的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const executeWorkflow = (params?: ExecuteWorkflowParams): useAxiosReturn => + useApi('/v1/workflow/execute_workflow', params) + +/** + * @description 修改工作流执行方式 + * @param {UpdateWorkflowExecTypeParams} [params] 请求参数 + * @returns {useAxiosReturn} 修改工作流执行方式的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const updateWorkflowExecType = ( + params?: UpdateWorkflowExecTypeParams, +): useAxiosReturn => + useApi('/v1/workflow/exec_type', params) + +/** + * @description 启用工作流或禁用工作流 + * @param {EnableWorkflowParams} [params] 请求参数 + * @returns {useAxiosReturn} 启用或禁用工作流的组合式 API 调用封装。包含响应数据、加载状态及执行函数。 + */ +export const enableWorkflow = (params?: EnableWorkflowParams): useAxiosReturn => + useApi('/v1/workflow/active', params) diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/cert/google.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/cert/google.svg new file mode 100644 index 0000000..df1f83d --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/cert/google.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/cert/letsencrypt.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/cert/letsencrypt.svg new file mode 100644 index 0000000..dbd31ca --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/cert/letsencrypt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/cert/sslcom.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/cert/sslcom.svg new file mode 100644 index 0000000..d16d680 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/cert/sslcom.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/cert/zerossl.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/cert/zerossl.svg new file mode 100644 index 0000000..97ab3ff --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/cert/zerossl.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/flow/apply.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/flow/apply.svg new file mode 100644 index 0000000..b26c429 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/flow/apply.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/flow/branch.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/flow/branch.svg new file mode 100644 index 0000000..5db1ca5 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/flow/branch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/flow/deploy.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/flow/deploy.svg new file mode 100644 index 0000000..6870e01 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/flow/deploy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/flow/error.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/flow/error.svg new file mode 100644 index 0000000..5f2a3b1 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/flow/error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/flow/notify.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/flow/notify.svg new file mode 100644 index 0000000..2126612 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/flow/notify.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/flow/success.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/flow/success.svg new file mode 100644 index 0000000..bcb2d7b --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/flow/success.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/flow/upload.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/flow/upload.svg new file mode 100644 index 0000000..554e68c --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/flow/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/notify/dingtalk.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/notify/dingtalk.svg new file mode 100644 index 0000000..4fa9eb8 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/notify/dingtalk.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/notify/feishu.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/notify/feishu.svg new file mode 100644 index 0000000..4586fdc --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/notify/feishu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/notify/mail.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/notify/mail.svg new file mode 100644 index 0000000..b982b6e --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/notify/mail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/notify/webhook.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/notify/webhook.svg new file mode 100644 index 0000000..4dc779e --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/notify/webhook.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/notify/wecom.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/notify/wecom.svg new file mode 100644 index 0000000..5ae81bd --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/notify/wecom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/1panel.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/1panel.svg new file mode 100644 index 0000000..03eaf32 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/1panel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/acmeca.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/acmeca.svg new file mode 100644 index 0000000..47a6ec9 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/acmeca.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/acmehttpreq.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/acmehttpreq.svg new file mode 100644 index 0000000..945f1d9 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/acmehttpreq.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/aliyun.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/aliyun.svg new file mode 100644 index 0000000..42475db --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/aliyun.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/aws.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/aws.svg new file mode 100644 index 0000000..f8f42c7 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/aws.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/azure.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/azure.svg new file mode 100644 index 0000000..68a3df9 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/azure.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/baidu.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/baidu.svg new file mode 100644 index 0000000..908b513 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/baidu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/baiducloud.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/baiducloud.svg new file mode 100644 index 0000000..15bef60 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/baiducloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/baishan.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/baishan.svg new file mode 100644 index 0000000..6fe5c47 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/baishan.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/baotapanel.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/baotapanel.svg new file mode 100644 index 0000000..51df752 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/baotapanel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/baotawaf.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/baotawaf.svg new file mode 100644 index 0000000..51df752 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/baotawaf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/btpanel.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/btpanel.svg new file mode 100644 index 0000000..2e84ee1 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/btpanel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/btwaf.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/btwaf.svg new file mode 100644 index 0000000..2e84ee1 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/btwaf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/bunny.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/bunny.svg new file mode 100644 index 0000000..545808c --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/bunny.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/buypass.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/buypass.svg new file mode 100644 index 0000000..c334fb7 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/buypass.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/byteplus.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/byteplus.svg new file mode 100644 index 0000000..74ff547 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/byteplus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/cachefly.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/cachefly.svg new file mode 100644 index 0000000..146125b --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/cachefly.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/cdnfly.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/cdnfly.svg new file mode 100644 index 0000000..4e01480 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/cdnfly.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/close.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/close.svg new file mode 100644 index 0000000..3b6bafc --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/cloudflare.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/cloudflare.svg new file mode 100644 index 0000000..1fb8346 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/cloudflare.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/cloudns.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/cloudns.svg new file mode 100644 index 0000000..3464a4e --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/cloudns.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/cmcccloud.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/cmcccloud.svg new file mode 100644 index 0000000..fb6db24 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/cmcccloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/desec.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/desec.svg new file mode 100644 index 0000000..1c03f72 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/desec.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/digitalocean.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/digitalocean.svg new file mode 100644 index 0000000..9225944 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/digitalocean.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/dingtalk.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/dingtalk.svg new file mode 100644 index 0000000..035ac5a --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/dingtalk.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/discord.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/discord.svg new file mode 100644 index 0000000..f673f95 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/discord.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/dnsla.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/dnsla.svg new file mode 100644 index 0000000..8de7ef0 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/dnsla.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/dogecloud.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/dogecloud.svg new file mode 100644 index 0000000..8c4758d --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/dogecloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/duckdns.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/duckdns.svg new file mode 100644 index 0000000..3ee7f08 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/duckdns.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/dynv6.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/dynv6.svg new file mode 100644 index 0000000..41ed6b8 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/dynv6.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/edgio.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/edgio.svg new file mode 100644 index 0000000..9a407cc --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/edgio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/email.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/email.svg new file mode 100644 index 0000000..be3f72e --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/email.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/flexcdn.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/flexcdn.svg new file mode 100644 index 0000000..f17a468 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/flexcdn.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/gcore.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/gcore.svg new file mode 100644 index 0000000..4f0c9d8 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/gcore.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/gname.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/gname.svg new file mode 100644 index 0000000..c787cd1 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/gname.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/godaddy.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/godaddy.svg new file mode 100644 index 0000000..77c85d4 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/godaddy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/goedge.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/goedge.svg new file mode 100644 index 0000000..1f32d31 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/goedge.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/google.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/google.svg new file mode 100644 index 0000000..0561af6 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/google.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/hetzner.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/hetzner.svg new file mode 100644 index 0000000..c934b15 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/hetzner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/huaweicloud.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/huaweicloud.svg new file mode 100644 index 0000000..6525670 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/huaweicloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/jdcloud.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/jdcloud.svg new file mode 100644 index 0000000..59ca74a --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/jdcloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/kubernetes.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/kubernetes.svg new file mode 100644 index 0000000..37976e3 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/kubernetes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/lark.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/lark.svg new file mode 100644 index 0000000..e954218 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/lark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/lecdn.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/lecdn.svg new file mode 100644 index 0000000..f9c18fa --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/lecdn.svg @@ -0,0 +1 @@ +LeCDN \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/letsencrypt.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/letsencrypt.svg new file mode 100644 index 0000000..c5c74f3 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/letsencrypt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/local.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/local.svg new file mode 100644 index 0000000..ebca682 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/local.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/mattermost.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/mattermost.svg new file mode 100644 index 0000000..3e2afcd --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/mattermost.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/namecheap.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/namecheap.svg new file mode 100644 index 0000000..bc8c760 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/namecheap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/namedotcom.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/namedotcom.svg new file mode 100644 index 0000000..f7315bb --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/namedotcom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/namesilo.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/namesilo.svg new file mode 100644 index 0000000..366fa0f --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/namesilo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/netcup.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/netcup.svg new file mode 100644 index 0000000..87aa0d4 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/netcup.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/netlify.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/netlify.svg new file mode 100644 index 0000000..c7ab549 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/netlify.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/ns1.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/ns1.svg new file mode 100644 index 0000000..24d801d --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/ns1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/plus.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/plus.svg new file mode 100644 index 0000000..eb70c78 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/porkbun.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/porkbun.svg new file mode 100644 index 0000000..8ee90d6 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/porkbun.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/powerdns.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/powerdns.svg new file mode 100644 index 0000000..8c12d83 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/powerdns.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/proxmoxve.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/proxmoxve.svg new file mode 100644 index 0000000..01a52aa --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/proxmoxve.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/qiniu.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/qiniu.svg new file mode 100644 index 0000000..d2a5d6c --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/qiniu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/rainyun.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/rainyun.svg new file mode 100644 index 0000000..d8789b7 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/rainyun.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/ratpanel.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/ratpanel.svg new file mode 100644 index 0000000..aa8ca55 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/ratpanel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/safeline.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/safeline.svg new file mode 100644 index 0000000..ba1ffb2 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/safeline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/slack.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/slack.svg new file mode 100644 index 0000000..64acfef --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/slack.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/ssh.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/ssh.svg new file mode 100644 index 0000000..56e8dae --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/ssh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/sslcom.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/sslcom.svg new file mode 100644 index 0000000..4134352 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/sslcom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/subtract.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/subtract.svg new file mode 100644 index 0000000..f954379 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/subtract.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/telegram.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/telegram.svg new file mode 100644 index 0000000..5efa713 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/telegram.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/tencentcloud.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/tencentcloud.svg new file mode 100644 index 0000000..0616b67 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/tencentcloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/tips.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/tips.svg new file mode 100644 index 0000000..72aa6c0 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/tips.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/ucloud.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/ucloud.svg new file mode 100644 index 0000000..2cfe52c --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/ucloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/unicloud.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/unicloud.svg new file mode 100644 index 0000000..cb01bbb --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/unicloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/upyun.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/upyun.svg new file mode 100644 index 0000000..5ce3b41 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/upyun.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/vercel.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/vercel.svg new file mode 100644 index 0000000..a0501c1 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/volcengine.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/volcengine.svg new file mode 100644 index 0000000..7b67134 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/volcengine.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/wangsu.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/wangsu.svg new file mode 100644 index 0000000..96cf3ac --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/wangsu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/webhook.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/webhook.svg new file mode 100644 index 0000000..4da99df --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/webhook.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/wecom.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/wecom.svg new file mode 100644 index 0000000..0fa6497 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/wecom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/westcn.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/westcn.svg new file mode 100644 index 0000000..013e194 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/westcn.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/zerossl.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/zerossl.svg new file mode 100644 index 0000000..e2ab7c4 --- /dev/null +++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/zerossl.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/components/BaseLayout/index.tsx b/frontend/apps/allin-ssl/src/components/BaseLayout/index.tsx new file mode 100644 index 0000000..069ea29 --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/BaseLayout/index.tsx @@ -0,0 +1,69 @@ +import { defineComponent, type Slots } from 'vue' +/** + * @description 基础组件 + * @example + * ```tsx + * + * + * + * + * + * + * + * + * + * + * ``` + */ +export default defineComponent({ + name: 'BaseComponent', + setup(_, { slots }: { slots: Slots }) { + // 获取插槽内容,支持驼峰和短横线两种命名方式 + const slotHL = slots['header-left'] || slots.headerLeft + const slotHR = slots['header-right'] || slots.headerRight + const slotHeader = slots.header // 新增对 #header 插槽的支持 + const slotFL = slots['footer-left'] || slots.footerLeft + const slotFR = slots['footer-right'] || slots.footerRight + const slotFooter = slots.footer // 新增对 #footer 插槽的支持 + + return () => ( +
+ {/* 头部区域: 优先使用 #header 插槽,如果不存在则尝试 #header-left 和 #header-right */} + {slotHeader ? ( +
{slotHeader()}
+ ) : ( + (slotHL || slotHR) && ( +
+
{slotHL && slotHL()}
+
{slotHR && slotHR()}
+
+ ) + )} + + {/* 内容区域 */} +
+ {slots.content && slots.content()} +
+ + {/* 底部区域: 优先使用 #footer 插槽,如果不存在则尝试 #footer-left 和 #footer-right */} + {slotFooter ? ( +
{slotFooter()}
+ ) : ( + (slotFL || slotFR) && ( +
+
{slotFL && slotFL()}
+
{slotFR && slotFR()}
+
+ ) + )} + + {/* 弹窗区域 */} + {slots.popup && slots.popup()} +
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/components/CAProviderSelect/index.tsx b/frontend/apps/allin-ssl/src/components/CAProviderSelect/index.tsx new file mode 100644 index 0000000..196615a --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/CAProviderSelect/index.tsx @@ -0,0 +1,153 @@ +import { defineComponent, VNode } from 'vue' +import { NButton, NFormItemGi, NGrid, NSelect, NText, NSpin, NFlex } from 'naive-ui' + +// 类型导入 +import type { CAProviderSelectProps, CAProviderOption, CAProviderSelectEmits } from './types' + +// 绝对内部导入 - Controller +import { useCAProviderSelectController } from './useController' +// 绝对内部导入 - Components +import SvgIcon from '@components/SvgIcon' +// 绝对内部导入 - Utilities +import { $t } from '@locales/index' + +/** + * @component CAProviderSelect + * @description CA授权选择组件,支持选择Let's Encrypt和其他CA授权,并提供跳转到CA授权管理页面的功能。 + * 遵循 MVC/MV* 模式,将业务逻辑、状态管理与视图渲染分离。 + * + * @example 基础使用 + * + * + * @property {string} path - 表单路径,用于表单校验。 + * @property {string} value - 当前选中的值 (通过 v-model:value 绑定)。 + * @property {string} ca - 当前选中的CA类型 (通过 v-model:ca 绑定)。 + * @property {string} email - 邮箱地址 (通过 v-model:email 绑定),当 value 不为空时会被自动赋值。 + * @property {boolean} [disabled=false] - 是否禁用。 + * @property {string} [customClass] - 自定义CSS类名。 + * + * @emits update:value - (value: { value: string; ca: string }) 当选择的CA授权变更时触发,传递值和CA类型。 + * @emits update:email - (email: string) 当 value 不为空时触发,传递邮箱地址。 + */ +export default defineComponent({ + name: 'CAProviderSelect', + props: { + path: { + type: String, + required: true, + }, + value: { + type: String, + required: true, + default: '', + }, + ca: { + type: String, + required: true, + }, + email: { + type: String, + required: true, + }, + disabled: { + type: Boolean, + default: false, + }, + customClass: { + type: String, + default: '', + }, + }, + emits: { + 'update:value': (value: { value: string; ca: string }) => true, + 'update:email': (email: string) => true, + }, + setup(props: CAProviderSelectProps, { emit }: { emit: CAProviderSelectEmits }) { + const { + isLoading, + caProviderRef, + param, + handleUpdateValue, + handleFilter, + goToAddCAProvider, + errorMessage, + loadCAProviders, + } = useCAProviderSelectController(props, emit) + + /** + * 渲染标签 + * @param option - 选项 + * @returns 渲染后的VNode + */ + const renderLabel = (option: CAProviderOption): VNode => { + return ( + + + {option.label} + + ) + } + + /** + * 渲染单选标签 + * @param option - 选项 (Record 来自 naive-ui 的类型) + * @returns 渲染后的VNode + */ + const renderSingleSelectTag = ({ option }: { option: CAProviderOption }): VNode => { + return ( +
+ {option.label ? renderLabel(option) : {$t('t_0_1747990228780')}} +
+ ) + } + + return () => ( + + + + renderSingleSelectTag({ option: option as CAProviderOption })} + filterable + filter={(pattern: string, option: any) => handleFilter(pattern, option as CAProviderOption)} + placeholder={$t('t_0_1747990228780')} + value={param.value.value} // 使用 controller 中的 param.value.value + onUpdateValue={handleUpdateValue} + disabled={props.disabled} + v-slots={{ + header: () => { + return ( +
goToAddCAProvider('addCAForm')} + > + {$t('t_1_1748052860539')} +
+ ) + }, + empty: () => { + return {errorMessage.value || $t('t_2_1747990228008')} + }, + }} + /> +
+ + goToAddCAProvider('caManage')} disabled={props.disabled}> + {$t('t_0_1747903670020')} + + loadCAProviders()} loading={isLoading.value} disabled={props.disabled}> + {$t('t_0_1746497662220')} + + +
+
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/components/CAProviderSelect/types.d.ts b/frontend/apps/allin-ssl/src/components/CAProviderSelect/types.d.ts new file mode 100644 index 0000000..508ab1a --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/CAProviderSelect/types.d.ts @@ -0,0 +1,72 @@ +/** + * @interface CAProviderOption + * @description CA授权选项的结构 + */ +export interface CAProviderOption { + label: string + value: string + ca: string + email: string +} + +/** + * @interface CAProviderSelectProps + * @description CAProviderSelect 组件的 Props 定义 + */ +export interface CAProviderSelectProps { + /** + * @property path + * @description 表单路径,用于 naive-ui 表单校验 + */ + path: string + /** + * @property value + * @description 当前选中的值 + */ + value: string + /** + * @property ca + * @description 当前选中的CA类型 + */ + ca: string + /** + * @property email + * @description 邮箱地址,当 value 不为空时会被赋值 + */ + email: string + /** + * @property disabled + * @description 是否禁用选择器 + * @default false + */ + disabled?: boolean + /** + * @property customClass + * @description 自定义CSS类名,应用于 NGrid 组件 + */ + customClass?: string +} + +/** + * @interface CAProviderSelectEmits + * @description CAProviderSelect 组件的 Emits 定义 + */ +export interface CAProviderSelectEmits { + (e: 'update:value', value: { value: string; ca: string; email: string }): void + (e: 'update:email', email: string): void +} + +/** + * @interface CAProviderControllerExposes + * @description useCAProviderSelectController 返回对象的类型接口 + */ +export interface CAProviderControllerExposes { + param: import('vue').Ref + caProviderRef: import('vue').Ref + isLoading: import('vue').Ref + errorMessage: import('vue').Ref + goToAddCAProvider: () => void + handleUpdateValue: (value: string) => void + loadCAProviders: () => Promise + handleFilter: (pattern: string, option: CAProviderOption) => boolean +} diff --git a/frontend/apps/allin-ssl/src/components/CAProviderSelect/useController.tsx b/frontend/apps/allin-ssl/src/components/CAProviderSelect/useController.tsx new file mode 100644 index 0000000..e31c552 --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/CAProviderSelect/useController.tsx @@ -0,0 +1,159 @@ +import { ref, watch, onMounted } from 'vue' +import type { CAProviderSelectProps, CAProviderOption, CAProviderSelectEmits } from './types' + +// 绝对内部导入 - API +import { getAllEabList } from '@api/access' +// 绝对内部导入 - Hooks +import { useError } from '@baota/hooks/error' +// 绝对内部导入 - Utilities +import { $t } from '@locales/index' + +/** + * @function useCAProviderSelectController + * @description CAProviderSelect 组件的控制器逻辑 + * @param props - 组件的 props + * @param emit - 组件的 emit 函数 + * @returns {CAProviderControllerExposes} 控制器暴露给视图的数据和方法 + */ +export function useCAProviderSelectController(props: CAProviderSelectProps, emit: CAProviderSelectEmits) { + const { handleError } = useError() + + const param = ref({ + label: '', + value: '', + ca: '', + email: '', + }) + const caProviderRef = ref([]) + const isLoading = ref(false) + const errorMessage = ref('') + + /** + * @function goToAddCAProvider + * @description 跳转到CA授权管理页面 + */ + const goToAddCAProvider = (type: string) => { + window.open(`/auto-deploy?type=${type}`, '_blank') + } + + /** + * @function handleUpdateType + * @description 根据当前 param.value 更新 param 对象的 label 和 ca,并 emit 更新事件 + */ + const handleUpdateType = () => { + const selectedProvider = caProviderRef.value.find((item) => item.value === param.value.value) + + if (selectedProvider) { + param.value = { + label: selectedProvider.label, + value: selectedProvider.value, + ca: selectedProvider.ca, + email: selectedProvider.email, + } + } else if (caProviderRef.value.length > 0 && param.value.value === '') { + // 如果 param.value 为空(例如初始状态或清空后),且 caProviderRef 列表不为空,则默认选中第一个 + param.value = { + label: caProviderRef.value[0]?.label || '', + value: caProviderRef.value[0]?.value || '', + ca: caProviderRef.value[0]?.ca || '', + email: caProviderRef.value[0]?.email || '', + } + } + + // 当 value 不为空时,将其赋值给 email 字段 + if (param.value.value !== '') { + emit('update:email', param.value.email) + } + + emit('update:value', { value: param.value.value, ca: param.value.ca, email: param.value.email }) + } + + /** + * @function handleUpdateValue + * @description 更新 param.value 并触发类型更新 + * @param value - 新的选中值 + */ + const handleUpdateValue = (value: string) => { + param.value.value = value + handleUpdateType() + } + + /** + * @function loadCAProviders + * @description 加载CA授权选项 + */ + const loadCAProviders = async () => { + isLoading.value = true + errorMessage.value = '' + try { + // 添加Let's Encrypt作为首选项 + const letsEncryptOption: CAProviderOption = { + label: "Let's Encrypt", + value: '', + ca: 'letsencrypt', + email: '', + } + + // 获取其他CA授权列表 + const { data } = await getAllEabList({ ca: '' }).fetch() + const eabOptions: CAProviderOption[] = (data || []).map((item) => ({ + label: item.name, + value: item.id.toString(), + ca: item.ca, + email: item.mail, + })) + + // 合并选项,Let's Encrypt在首位 + caProviderRef.value = [letsEncryptOption, ...eabOptions] + + // 数据加载后,如果 props.value 有值,尝试根据 props.value 初始化 param + if (props.value) { + handleUpdateValue(props.value) + } else { + handleUpdateType() // 确保在 caProviderRef 更新后,param 也得到相应更新 + } + } catch (error) { + errorMessage.value = typeof error === 'string' ? error : $t('t_3_1747990229599') + handleError(error) + } finally { + isLoading.value = false + } + } + + /** + * @function handleFilter + * @description NSelect 组件的搜索过滤函数 + * @param pattern - 搜索文本 + * @param option - 当前选项 + * @returns {boolean} 是否匹配 + */ + const handleFilter = (pattern: string, option: CAProviderOption): boolean => { + return option.label.toLowerCase().includes(pattern.toLowerCase()) + } + + watch( + () => props.value, + (newValue) => { + // 仅当外部 props.value 与内部 param.value.value 不一致时才更新 + if (newValue !== param.value.value) { + handleUpdateValue(newValue) + } + }, + { immediate: true }, + ) + + onMounted(() => { + loadCAProviders() + }) + + return { + param, + caProviderRef, + isLoading, + errorMessage, + goToAddCAProvider, + handleUpdateValue, + loadCAProviders, + handleFilter, + } +} diff --git a/frontend/apps/allin-ssl/src/components/DnsProviderSelect/index.tsx b/frontend/apps/allin-ssl/src/components/DnsProviderSelect/index.tsx new file mode 100644 index 0000000..073fb76 --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/DnsProviderSelect/index.tsx @@ -0,0 +1,159 @@ +import { defineComponent, PropType, VNode } from 'vue' +import { NButton, NFormItemGi, NGrid, NSelect, NText, NSpin, NFlex } from 'naive-ui' + +// 类型导入 +import type { DnsProviderSelectProps, DnsProviderOption, DnsProviderType, DnsProviderSelectEmits } from './types' + +// 绝对内部导入 - Controller +import { useDnsProviderSelectController } from './useController' +// 绝对内部导入 - Components +import SvgIcon from '@components/SvgIcon' +// 绝对内部导入 - Utilities +import { $t } from '@locales/index' + +/** + * @component DnsProviderSelect + * @description DNS提供商选择组件,支持多种DNS提供商类型,并提供刷新和跳转到授权页面的功能。 + * 遵循 MVC/MV* 模式,将业务逻辑、状态管理与视图渲染分离。 + * + * @example 基础使用 + * + * + * @property {DnsProviderType} type - DNS提供商类型。 + * @property {string} path - 表单路径,用于表单校验。 + * @property {string} value - 当前选中的值 (通过 v-model:value 绑定)。 + * @property {'value' | 'type'} valueType - 表单值的类型,决定 emit 'update:value' 时传递的是选项的 'value' 还是 'type'。 + * @property {boolean} isAddMode - 是否为添加模式,显示添加和刷新按钮。 + * @property {boolean} [disabled=false] - 是否禁用。 + * @property {string} [customClass] - 自定义CSS类名。 + * + * @emits update:value - (value: DnsProviderOption) 当选择的DNS提供商变更时触发,传递整个选项对象。 + */ +export default defineComponent({ + name: 'DnsProviderSelect', + props: { + type: { + type: String as PropType, + required: true, + }, + path: { + type: String, + required: true, + }, + value: { + type: String, + required: true, + }, + valueType: { + type: String as PropType<'value' | 'type'>, + default: 'value', + }, + isAddMode: { + type: Boolean, + default: true, + }, + disabled: { + type: Boolean, + default: false, + }, + customClass: { + type: String, + default: '', + }, + }, + emits: ['update:value'] as unknown as DnsProviderSelectEmits, // 类型断言以匹配严格的 emits 定义 + + setup(props: DnsProviderSelectProps, { emit }: { emit: DnsProviderSelectEmits }) { + const controller = useDnsProviderSelectController(props, emit) + + /** + * 渲染标签 + * @param option - 选项 + * @returns 渲染后的VNode + */ + const renderLabel = (option: DnsProviderOption): VNode => { + return ( + + + {option.label} + + ) + } + + /** + * 渲染单选标签 + * @param option - 选项 (Record 来自 naive-ui 的类型) + * @returns 渲染后的VNode + */ + const renderSingleSelectTag = ({ option }: { option: DnsProviderOption }): VNode => { + return ( +
+ {option.label ? ( + renderLabel(option) + ) : ( + + {props.type === 'dns' ? $t('t_0_1747019621052') : $t('t_0_1746858920894')} + + )} +
+ ) + } + + return () => ( + + + + + renderSingleSelectTag({ option: option as DnsProviderOption }) + } + filter={(pattern: string, option: any) => controller.handleFilter(pattern, option as DnsProviderOption)} + placeholder={props.type === 'dns' ? $t('t_3_1745490735059') : $t('t_0_1746858920894')} + value={controller.param.value.value} // 使用 controller 中的 param.value.value + onUpdateValue={controller.handleUpdateValue} + disabled={props.disabled} + v-slots={{ + empty: () => { + return ( + + {controller.errorMessage.value || + (props.type === 'dns' ? $t('t_1_1746858922914') : $t('t_2_1746858923964'))} + + ) + }, + }} + /> + + {props.isAddMode && ( + + + {props.type === 'dns' ? $t('t_1_1746004861166') : $t('t_3_1746858920060')} + + controller.loadDnsProviders(props.type)} + loading={controller.isLoading.value} + disabled={props.disabled} + > + {$t('t_0_1746497662220')} + + + )} + + + ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/components/DnsProviderSelect/types.d.ts b/frontend/apps/allin-ssl/src/components/DnsProviderSelect/types.d.ts new file mode 100644 index 0000000..2c29121 --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/DnsProviderSelect/types.d.ts @@ -0,0 +1,95 @@ +// 类型导入 (如果需要) +// import type { SomeOtherType } from '@/types/someModule'; + +/** + * @interface DnsProviderOption + * @description DNS提供商选项的结构 + */ +export interface DnsProviderOption { + label: string + value: string + type: string +} + +/** + * @type DnsProviderType + * @description DNS提供商的具体类型 + */ +export type DnsProviderType = + | 'aliyun' + | 'tencentcloud' + | 'baidu' + | 'qiniu' + | 'huaweicloud' + | 'cloudflare' + | 'dns' + | 'btpanel' + | '1panel' + | 'ssh' + | '' + +/** + * @interface DnsProviderSelectProps + * @description DnsProviderSelect 组件的 Props 定义 + */ +export interface DnsProviderSelectProps { + /** + * @property type + * @description 表单类型,用于获取不同的下拉列表 + */ + type: DnsProviderType + /** + * @property path + * @description 表单路径,用于 naive-ui 表单校验 + */ + path: string + /** + * @property value + * @description 当前选中的值 + */ + value: string + /** + * @property valueType + * @description 表单值的类型,决定 emit 出去的是选项的 value 还是 type + */ + valueType: 'value' | 'type' + /** + * @property isAddMode + * @description 是否为添加模式,控制是否显示"添加"和"刷新"按钮 + */ + isAddMode: boolean + /** + * @property disabled + * @description 是否禁用选择器 + * @default false + */ + disabled?: boolean + /** + * @property customClass + * @description 自定义CSS类名,应用于 NGrid 组件 + */ + customClass?: string +} + +/** + * @interface DnsProviderSelectEmits + * @description DnsProviderSelect 组件的 Emits 定义 + */ +export interface DnsProviderSelectEmits { + (e: 'update:value', value: DnsProviderOption): void +} + +/** + * @interface DnsProviderControllerExposes + * @description useDnsProviderSelectController 返回对象的类型接口 + */ +export interface DnsProviderControllerExposes { + param: import('vue').Ref + dnsProviderRef: import('vue').Ref + isLoading: import('vue').Ref + errorMessage: import('vue').Ref + goToAddDnsProvider: () => void + handleUpdateValue: (value: string) => void + loadDnsProviders: (type?: DnsProviderType) => Promise + handleFilter: (pattern: string, option: DnsProviderOption) => boolean +} diff --git a/frontend/apps/allin-ssl/src/components/DnsProviderSelect/useController.tsx b/frontend/apps/allin-ssl/src/components/DnsProviderSelect/useController.tsx new file mode 100644 index 0000000..40a2574 --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/DnsProviderSelect/useController.tsx @@ -0,0 +1,190 @@ +import { ref, watch, onMounted, onUnmounted } from 'vue' +import type { DnsProviderSelectProps, DnsProviderOption, DnsProviderType, DnsProviderSelectEmits } from './types' + +// 绝对内部导入 - Stores +import { useStore } from '@layout/useStore' +// 绝对内部导入 - Hooks +import { useError } from '@baota/hooks/error' +// 绝对内部导入 - Utilities +import { $t } from '@locales/index' + +/** + * @function useDnsProviderSelectController + * @description DnsProviderSelect 组件的控制器逻辑 + * @param props - 组件的 props + * @param emit - 组件的 emit 函数 + * @returns {DnsProviderControllerExposes} 控制器暴露给视图的数据和方法 + */ +export function useDnsProviderSelectController(props: DnsProviderSelectProps, emit: DnsProviderSelectEmits) { + const { handleError } = useError() + const { fetchDnsProvider, resetDnsProvider, dnsProvider } = useStore() + + const param = ref({ + label: '', + value: '', + type: '', + }) + const dnsProviderRef = ref([]) + const isLoading = ref(false) + const errorMessage = ref('') + + /** + * @function goToAddDnsProvider + * @description 跳转到DNS提供商授权页面 + */ + const goToAddDnsProvider = () => { + window.open('/auth-api-manage', '_blank') + } + + /** + * @function handleUpdateType + * @description 根据当前 param.value 更新 param 对象的 label 和 type,并 emit 更新事件 + */ + const handleUpdateType = () => { + const selectedProvider = dnsProvider.value.find((item) => { + // 根据 props.valueType 决定是比较 item.value 还是 item.type + const valueToCompare = props.valueType === 'value' ? item.value : item.type + return valueToCompare === param.value.value + }) + if (selectedProvider) { + param.value = { + label: selectedProvider.label, + value: props.valueType === 'value' ? selectedProvider.value : selectedProvider.type, + type: props.valueType === 'value' ? selectedProvider.type : selectedProvider.value, + } + emit('update:value', { ...param.value }) + } else { + // 如果找不到匹配的选项,只有在 param.value.value 为空时才设置默认值 + // 这样可以避免覆盖用户设置的初始值 + if (param.value.value === '' && dnsProvider.value.length > 0) { + param.value = { + label: dnsProvider.value[0]?.label || '', + value: props.valueType === 'value' ? dnsProvider.value[0]?.value || '' : dnsProvider.value[0]?.type || '', + type: props.valueType === 'value' ? dnsProvider.value[0]?.type || '' : dnsProvider.value[0]?.value || '', + } + emit('update:value', { ...param.value }) + } + // 如果 param.value.value 不为空但找不到匹配项,保持当前值不变 + // 这种情况可能是数据还未加载完成或者选项暂时不可用 + } + } + + /** + * @function handleUpdateValue + * @description 更新 param.value 并触发类型更新 + * @param value - 新的选中值 + */ + const handleUpdateValue = (value: string) => { + param.value.value = value + handleUpdateType() + } + + /** + * @function loadDnsProviders + * @description 加载DNS提供商选项 + * @param type - DNS提供商类型,默认为 props.type + */ + const loadDnsProviders = async (type: DnsProviderType = props.type) => { + isLoading.value = true + errorMessage.value = '' + try { + await fetchDnsProvider(type) + // 数据加载后,优先使用 props.value 设置初始值 + if (props.value) { + // 直接设置 param.value.value,然后调用 handleUpdateType 来更新完整信息 + param.value.value = props.value + handleUpdateType() + } else { + // 只有在 props.value 为空时才考虑设置默认值 + handleUpdateType() + } + } catch (error) { + errorMessage.value = typeof error === 'string' ? error : $t('t_0_1746760933542') // '获取DNS提供商列表失败' + handleError(error) + } finally { + isLoading.value = false + } + } + + /** + * @function handleFilter + * @description NSelect 组件的搜索过滤函数 + * @param pattern - 搜索文本 + * @param option - 当前选项 + * @returns {boolean} 是否匹配 + */ + const handleFilter = (pattern: string, option: DnsProviderOption): boolean => { + return option.label.toLowerCase().includes(pattern.toLowerCase()) + } + + watch( + () => dnsProvider.value, + (newVal) => { + dnsProviderRef.value = + newVal.map((item) => ({ + label: item.label, + value: props.valueType === 'value' ? item.value : item.type, + type: props.valueType === 'value' ? item.type : item.value, // 确保 type 也被正确映射 + })) || [] + + // 当 dnsProvider 列表更新后,重新评估 param 的值 + const currentParamExists = dnsProviderRef.value.some((opt) => opt.value === param.value.value) + + if (currentParamExists) { + // 如果当前值在新列表中存在,更新完整信息 + handleUpdateType() + } else if (props.value && dnsProviderRef.value.some((opt) => opt.value === props.value)) { + // 如果 props.value 在新列表中存在,使用 props.value + param.value.value = props.value + handleUpdateType() + } else if (param.value.value === '') { + // 只有在当前值为空时才设置默认值 + if (dnsProviderRef.value.length > 0) { + param.value.value = dnsProviderRef.value[0]?.value || '' + handleUpdateType() + } + } + // 如果当前值不为空但在新列表中不存在,保持当前值不变 + // 这样可以避免在数据加载过程中意外覆盖用户设置的值 + }, + { deep: true }, // 监听 dnsProvider 数组内部变化 + ) + + watch( + () => props.value, + (newValue) => { + // 仅当外部 props.value 与内部 param.value.value 不一致时才更新 + // 避免因子组件 emit('update:value') 导致父组件 props.value 更新,从而再次触发此 watch 造成的循环 + if (newValue !== param.value.value) { + handleUpdateValue(newValue) + } + }, + { immediate: true }, + ) + + watch( + () => props.type, + (newType) => { + loadDnsProviders(newType) + }, + ) + + onMounted(async () => { + await loadDnsProviders(props.type) + }) + + onUnmounted(() => { + resetDnsProvider() + }) + + return { + param, + dnsProviderRef, + isLoading, + errorMessage, + goToAddDnsProvider, + handleUpdateValue, + loadDnsProviders, + handleFilter, + } +} diff --git a/frontend/apps/allin-ssl/src/components/FlowChart/components/base/baseNode/index.module.css b/frontend/apps/allin-ssl/src/components/FlowChart/components/base/baseNode/index.module.css new file mode 100644 index 0000000..ca6f457 --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/FlowChart/components/base/baseNode/index.module.css @@ -0,0 +1,182 @@ +.node { + @apply flex flex-col items-center relative mx-[1.2rem]; +} + +.nodeArrows::before { + content: ''; + position: absolute; + top: -1.2rem; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0.4rem; + border-style: solid; + border-width: 0.8rem 0.6rem 0.4rem; + border-color: #cacaca transparent transparent; + background-color: #f5f5f7; +} + +.nodeContent { + display: flex; + flex-direction: column; + align-items: center; + width: 20rem; + min-height: 8rem; + font-size: 1.4rem; + box-shadow: .2rem .2rem .5rem .2rem rgba(0, 0, 0, 0.2); + white-space: normal; + word-break: break-word; + position: relative; + box-sizing: border-box; + border-radius: 0.5rem; + transition: box-shadow 0.1s; +} + +.nodeContent:hover { + box-shadow: 0.3rem 0.3rem .6rem 0.3rem rgba(0, 0, 0, 0.2); +} + +.nodeSelected { + box-shadow: 0 0 0 2px #1e83e9; + border: 1px solid #1e83e9; +} + +.nodeHeader { + @apply w-full flex relative items-center justify-center bg-[#1e83e9] rounded-t-[0.5rem] p-[0.5rem_1rem] text-white box-border; +} + +.nodeHeaderBranch{ + @apply flex-1 justify-between; +} + +.nodeCondition{ + min-height: 5rem; +} + +.nodeConditionHeader { + min-height: 5rem; + border-radius: 1rem; + color: #333 !important; + background-color: #f8fafc !important; +} + +.nodeConditionHeader input{ + color: #333 !important; +} + +.nodeConditionHeader input:focus{ + background-color: #efefef !important; +} + + +.nodeConditionHeader .nodeIcon{ + color: #333 !important; +} + +.nodeIcon { + @apply text-[1.6rem]; +} + +.nodeHeaderTitle { + @apply flex flex-row items-center justify-center relative px-[2rem]; +} + +.nodeHeaderTitleText { + @apply max-w-[11rem] min-w-[2rem] mr-[0.5rem] whitespace-nowrap overflow-hidden text-ellipsis; +} + +.nodeHeaderTitleInput { + @apply w-auto ; +} + +.nodeHeaderTitleInput input { + @apply w-full text-center border border-none rounded px-2 py-1 text-[#fff] focus:outline-none bg-transparent; +} + +.nodeHeaderTitleInput input:focus { + @apply border-[#1e83e9] text-[#333]; +} + +.nodeHeaderTitleEdit { + @apply w-[3rem] cursor-pointer hidden; +} + +.nodeHeaderTitle:hover .nodeHeaderTitleEdit { + @apply inline; +} + +.nodeClose { + @apply text-[1.6rem] text-center cursor-pointer; +} + +.nodeBody { + @apply w-full flex-1 flex flex-col justify-center bg-white rounded-b-[0.5rem] p-[1rem] text-[#5a5e66] cursor-pointer box-border; +} + +.nodeConditionBody { + @apply bg-[#f8fafc] rounded-[0.5rem]; +} + +.nodeError { + + box-shadow: 0 0 1rem 0.2rem rgba(243, 5, 5, 0.5); +} + +.nodeError:hover { + box-shadow: 0 0 1.2rem 0.4rem rgba(243, 5, 5, 0.5); +} + +.nodeErrorMsg { + @apply absolute top-1/2 -translate-y-1/2 -right-[5.5rem] z-[1]; +} + +.nodeErrorMsgBox { + @apply relative; +} + +.nodeErrorIcon { + @apply w-[2.5rem] h-[2.5rem] cursor-pointer; +} + +.nodeErrorTips { + position: absolute; + z-index: 3; + top: 50%; + transform: translateY(-50%); + left: 4.5rem; + min-width: 15rem; + background-color: white; + border-radius: 0.5rem; + box-shadow: 0.5rem 0.5rem 1rem 0.2rem rgba(0, 0, 0, 0.2); + display: flex; + padding: 1.6rem; +} + +.nodeErrorTips::before { + content: ''; + width: 0; + height: 0; + border-width: 1rem; + border-style: solid; + position: absolute; + top: 50%; + left: -2rem; + transform: translateY(-50%); + border-color: transparent #FFFFFF transparent transparent; +} + +.nodeMove { + @apply absolute top-1/2 -translate-y-1/2; +} + +.nodeMoveLeft { + @apply -left-[3rem]; +} + +.nodeMoveRight { + @apply -right-[3rem]; +} + +.nodeMoveIcon { + @apply w-[3.5rem] h-[3.5rem] cursor-pointer; +} \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/components/FlowChart/components/base/baseNode/index.tsx b/frontend/apps/allin-ssl/src/components/FlowChart/components/base/baseNode/index.tsx new file mode 100644 index 0000000..56352ae --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/FlowChart/components/base/baseNode/index.tsx @@ -0,0 +1,248 @@ +import { v4 as uuidv4 } from 'uuid' +import { markRaw } from 'vue' +import { useStore } from '@components/FlowChart/useStore' +import nodeOptions from '@components/FlowChart/lib/config' +import { useDialog } from '@baota/naive-ui/hooks' +import { $t } from '@locales/index' +import { CONDITION, EXECUTE_RESULT_CONDITION, START } from '@components/FlowChart/lib/alias' +import { useNodeValidator } from '@components/FlowChart/lib/verify' + +import AddNode from '@components/FlowChart/components/other/addNode/index' +import SvgIcon from '@components/SvgIcon' + +import type { BaseNodeData, NodeNum, BaseRenderNodeOptions, BaseNodeProps } from '@components/FlowChart/types' + +import styles from './index.module.css' +import ErrorNode from '../errorNode/index' + +export default defineComponent({ + name: 'BaseNode', + props: { + // 节点数据 + node: { + type: Object as PropType, + required: true, // 自读 + }, + }, + setup(props: BaseNodeProps) { + // 注入任务节点组件映射 + const taskComponents = inject('taskComponents', {}) as Record + + // ====================== 基础状态数据 ====================== + const { validator, validate } = useNodeValidator() // 验证器 + const tempNodeId = ref(props.node.id || uuidv4()) // 节点id + const config = ref>(nodeOptions[props.node.type]() || {}) // 节点配置 + const nodeNameRef = ref(null) // 节点名称输入框 + const isShowEditNodeName = ref(false) // 是否显示编辑节点名称 + const inputValue = ref(props.node.name) // 输入框值 + const renderNodeContent = ref() // 节点组件 + const nodeContentRef = ref() // 节点内容 + const { removeNode, updateNode, selectedNodeId, selectedNode } = useStore() + + // ====================== 节点状态数据 ====================== + // 错误状态 + const errorState = ref({ + isError: false, + message: null as string | null, + showTips: false, + }) + + // ====================== 计算属性 ====================== + // 是否是开始节点 + const isStart = computed(() => props.node.type === START) + + // 是否可以删除 + const isRemoved = computed(() => config.value?.operateNode?.remove) + + // 是否是条件节点 + const isCondition = computed(() => [CONDITION, EXECUTE_RESULT_CONDITION].includes(props.node.type)) + + // 根据节点类型获取图标 + const typeIcon: ComputedRef = computed(() => { + const type = { + success: 'flow-success', + fail: 'flow-error', + } + // console.log(props.node.config?.type) + if (props.node.type === EXECUTE_RESULT_CONDITION) + return (type[props.node.config?.type as keyof typeof type] || '') as string + return '' + }) + + // 根据节点类型获取图标颜色 + const typeIconColor: ComputedRef = computed(() => { + if (props.node.type === EXECUTE_RESULT_CONDITION) return (props.node.config?.type || '') as string + return '#FFFFFF' + }) + + // ====================== 数据监听与副作用 ====================== + // 监听节点数据,更新节点配置 + watch( + () => props.node, + () => { + config.value = nodeOptions[props.node.type as NodeNum]() // 更新节点配置 + inputValue.value = props.node.name // 更新节点名称 + tempNodeId.value = props.node.id || uuidv4() // 更新节点id + validator.validateAll() // 验证器验证 + + // 使用注入的taskComponents而不是props传递 + const nodeType = props.node.type + const nodeComponentKey = `${nodeType}Node` + // 如果taskComponents中有对应的组件就使用,否则使用ErrorNode + if (taskComponents && taskComponents[nodeComponentKey]) { + renderNodeContent.value = markRaw(taskComponents[nodeComponentKey]) + } else { + renderNodeContent.value = markRaw( + defineAsyncComponent({ + loader: () => import('@components/FlowChart/components/base/errorNode'), + loadingComponent: () =>
Loading...
, + errorComponent: () => , + }), + ) + } + }, + { immediate: true }, + ) + + // ====================== 节点操作方法 ====================== + /** + * @description 显示错误提示 + * @param {boolean} flag - 是否显示 + */ + const showErrorTips = (flag: boolean) => { + errorState.value.showTips = flag + } + + /** + * @description 删除选中节点 + * @param {MouseEvent} ev - 事件对象 + * @param {string} id - 节点id + * @param {BaseNodeData} node - 节点数据 + */ + const removeFindNode = (ev: MouseEvent, id: string, node: BaseNodeData) => { + const validator = validate(id) + if (validator.valid) { + useDialog({ + type: 'warning', + title: $t('t_1_1745765875247', { name: node.name }), + content: node.type === CONDITION ? $t('t_2_1745765875918') : $t('t_3_1745765920953'), + onPositiveClick: () => removeNode(id), + }) + } + // 如果节点类型是条件节点或验证不通过,则删除节点 + if ([EXECUTE_RESULT_CONDITION].includes(node.type) || !validator.valid) { + removeNode(id) + } + ev.stopPropagation() + ev.preventDefault() + } + + /** + * @description 点击节点 + */ + const handleNodeClick = () => { + console.log('nodeContentRef', nodeContentRef.value) + if ( + nodeContentRef.value?.handleNodeClick && + props.node.type !== CONDITION && + props.node.type !== EXECUTE_RESULT_CONDITION + ) { + selectedNodeId.value = props.node.id || '' + nodeContentRef.value.handleNodeClick(selectedNode) + } + } + + // ====================== 事件处理函数 ====================== + // 回车保存 + const keyupSaveNodeName = (e: KeyboardEvent) => { + if (e.keyCode === 13) { + isShowEditNodeName.value = false + } + } + + // 保存节点名称 + const saveNodeName = (e: Event) => { + const target = e.target as HTMLInputElement + inputValue.value = target.value + updateNode(tempNodeId.value, { name: inputValue.value }) + } + + // ====================== 渲染函数 ====================== + return () => ( +
+
+ {/* 节点头部 */} +
+ {/* 节点图标 */} + {typeIcon.value ? ( + + ) : null} + + {/* 节点标题 */} +
+
+ e.stopPropagation()} + onInput={saveNodeName} + onBlur={() => (isShowEditNodeName.value = false)} + onKeyup={keyupSaveNodeName} + /> +
+
+ + {/* 删除按钮 */} + {isRemoved.value && ( + removeFindNode(ev, tempNodeId.value, props.node)} + class="flex items-center justify-center absolute top-[50%] right-[1rem] -mt-[.9rem]" + > + + + )} +
+ {/* 节点主体 */} + {!isCondition.value ? ( +
+ {renderNodeContent.value && + h(renderNodeContent.value, { + id: props.node.id, + node: props.node || {}, + class: 'text-center', + ref: nodeContentRef, + })} +
+ ) : null} + {/* 错误提示 */} + {errorState.value.showTips && ( +
+
+ showErrorTips(true)} onMouseleave={() => showErrorTips(false)}> + + + {errorState.value.message &&
{errorState.value.message}
} +
+
+ )} +
+ {/* 添加节点组件 */} + +
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/components/FlowChart/components/base/branchNode/index.module.css b/frontend/apps/allin-ssl/src/components/FlowChart/components/base/branchNode/index.module.css new file mode 100644 index 0000000..3bb2cfb --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/FlowChart/components/base/branchNode/index.module.css @@ -0,0 +1,69 @@ +.flowNodeBranch { + @apply flex flex-col justify-center w-full relative max-w-full overflow-visible; +} + +/* 多列分支样式 */ +.multipleColumns { + @apply w-full +} + +.flowNodeBranchBox { + @apply flex flex-row w-full flex-nowrap min-h-[50px] relative overflow-visible; +} + +/* 有嵌套分支的容器样式 */ +.hasNestedBranch { + @apply w-full justify-around; +} + +.flowNodeBranchCol { + @apply flex flex-col items-center border-t-2 border-b-2 border-[#cacaca] pt-[50px] bg-[#f8fafc] flex-1 relative max-w-[50%]; +} + + +/* 有嵌套分支时列宽调整 */ +.hasNestedBranch .flowNodeBranchCol { + @apply w-full; +} + +/* 多级嵌套分支样式调整 */ +.flowNodeBranchCol .flowNodeBranchCol { + @apply min-w-[20rem] w-[24rem] +} + + +.flowNodeBranchCol::before { + @apply content-[''] absolute top-0 left-0 right-0 bottom-0 z-0 m-auto w-[2px] h-full bg-[#cacaca]; +} + +.coverLine { + @apply absolute h-[8px] w-[calc(50%-1px)] bg-[#f8fafc]; +} + +.topLeftCoverLine { + @apply -top-[4px] left-0; +} + +.topRightCoverLine { + @apply -top-[4px] right-0; +} + +.bottomLeftCoverLine { + @apply -bottom-[4px] left-0; +} + +.bottomRightCoverLine { + @apply -bottom-[4px] right-0; +} + +.rightCoverLine{ + @apply absolute w-[2px] bg-[#f8fafc] top-0 right-0 h-full; +} + +.leftCoverLine{ + @apply absolute w-[2px] bg-[#f8fafc] top-0 left-0 h-full; +} + +.flowConditionNodeAdd { + @apply absolute left-1/2 -translate-x-1/2 -top-[15px] flex justify-center items-center z-[2] w-[70px] h-[30px] text-[12px] text-[#1c84c6] bg-white rounded-[20px] cursor-pointer shadow-md; +} \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/components/FlowChart/components/base/branchNode/index.tsx b/frontend/apps/allin-ssl/src/components/FlowChart/components/base/branchNode/index.tsx new file mode 100644 index 0000000..b803ae5 --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/FlowChart/components/base/branchNode/index.tsx @@ -0,0 +1,113 @@ +import { v4 as uuidv4 } from 'uuid' +import nodeOptions from '@components/FlowChart/lib/config' +import { useStore } from '@components/FlowChart/useStore' +import { CONDITION } from '@components/FlowChart/lib/alias' + +import NodeWrap from '@components/FlowChart/components/render/nodeWrap' +import AddNode from '@components/FlowChart/components/other/addNode' +import styles from './index.module.css' +import type { BaseRenderNodeOptions, BranchNodeData } from '@components/FlowChart/types' + +export default defineComponent({ + name: 'BranchNode', + props: { + node: { + type: Object as () => BranchNodeData, + default: () => ({}), + }, + }, + + setup(props: { node: BranchNodeData }) { + const { addNode } = useStore() // 流程图数据 + const config = ref>(nodeOptions[props.node.type]() || {}) // 节点配置 + + watch( + () => props.node.type, + (newVal) => { + config.value = nodeOptions[newVal]() || {} + }, + ) + + // 添加分支 + const addCondition = () => { + const tempNodeId = uuidv4() // 临时节点id + addNode( + props.node.id || '', + CONDITION, + { + id: tempNodeId, + name: `分支${(props.node.conditionNodes?.length || 0) + 1}`, + }, + props.node.conditionNodes?.length, + ) + } + + // 计算容器类名,根据分支数量调整样式 + const getContainerClass = () => { + const count = props.node.conditionNodes?.length || 0 + const baseClass = styles.flowNodeBranch + + // 分支数量多时添加特殊类 + if (count > 3) { + return `${baseClass} ${styles.multipleColumns}` + } + + return baseClass + } + + // 计算分支盒子类名,处理多层嵌套情况 + const getBoxClass = () => { + // 检查是否有嵌套的分支节点 + const hasNestedBranch = props.node.conditionNodes?.some( + (node) => node.childNode && ['branch', 'execute_result_branch'].includes(node.childNode.type), + ) + + const baseClass = styles.flowNodeBranchBox + if (hasNestedBranch) { + return `${baseClass} ${styles.hasNestedBranch}` + } + + return baseClass + } + + return () => ( +
+ {config.value.operateNode?.addBranch && ( +
+ {config.value.operateNode?.addBranchTitle || '添加分支'} +
+ )} +
+ {props.node.conditionNodes?.map((condition, index: number) => ( +
+ {/* 条件节点 */} + + {/* 用来遮挡最左列的线 */} + {index === 0 && ( +
+
+
+
+
+ )} + {/* 用来遮挡最右列的线 */} + {index === (props.node.conditionNodes?.length || 0) - 1 && ( +
+
+
+
+
+ )} +
+ ))} +
+ +
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/components/FlowChart/components/base/conditionNode/index.tsx b/frontend/apps/allin-ssl/src/components/FlowChart/components/base/conditionNode/index.tsx new file mode 100644 index 0000000..32766ff --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/FlowChart/components/base/conditionNode/index.tsx @@ -0,0 +1,111 @@ +import { v4 as uuidv4 } from 'uuid' +import nodeOptions from '@components/FlowChart/lib/config' +import { useStore } from '@components/FlowChart/useStore' +import { CONDITION } from '@components/FlowChart/lib/alias' + +import NodeWrap from '@components/FlowChart/components/render/nodeWrap' +import AddNode from '@components/FlowChart/components/other/addNode' +import styles from '../branchNode/index.module.css' +import type { BaseRenderNodeOptions, ExecuteResultBranchNodeData } from '@components/FlowChart/types' + +export default defineComponent({ + name: 'BranchNode', + props: { + node: { + type: Object as () => ExecuteResultBranchNodeData, + default: () => ({}), + }, + }, + + setup(props: { node: ExecuteResultBranchNodeData }) { + const { addNode } = useStore() // 流程图数据 + const config = ref>(nodeOptions[props.node.type]() || {}) // 节点配置 + + watch( + () => props.node.type, + (newVal) => { + config.value = nodeOptions[newVal]() || {} + }, + ) + + // 添加条件 + const addCondition = () => { + const tempNodeId = uuidv4() // 临时节点id + addNode( + props.node.id || '', + CONDITION, + { + id: tempNodeId, + name: `分支${(props.node.conditionNodes?.length || 0) + 1}`, + }, + props.node.conditionNodes?.length, + ) + } + + // 计算容器类名,根据分支数量调整样式 + const getContainerClass = () => { + const count = props.node.conditionNodes?.length || 0 + const baseClass = styles.flowNodeBranch + + // 分支数量多时添加特殊类 + if (count > 3) { + return `${baseClass} ${styles.multipleColumns}` + } + + return baseClass + } + + // 计算分支盒子类名,处理多层嵌套情况 + const getBoxClass = () => { + // 检查是否有嵌套的分支节点 + const hasNestedBranch = props.node.conditionNodes?.some( + (node) => node.childNode && ['branch', 'execute_result_branch'].includes(node.childNode.type), + ) + const baseClass = styles.flowNodeBranchBox + if (hasNestedBranch) { + return `${baseClass} ${styles.hasNestedBranch}` + } + return baseClass + } + + return () => ( +
+ {config.value.operateNode?.addBranch && ( +
+ {config.value.operateNode?.addBranchTitle || '添加分支'} +
+ )} +
+ {props.node.conditionNodes?.map((condition, index: number) => ( +
+ {/* 条件节点 */} + + {/* 用来遮挡最左列的线 */} + {index === 0 && ( +
+
+
+
+
+ )} + {/* 用来遮挡最右列的线 */} + {index === (props.node.conditionNodes?.length || 0) - 1 && ( +
+
+
+
+
+ )} +
+ ))} +
+ +
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/components/FlowChart/components/base/endNode.tsx b/frontend/apps/allin-ssl/src/components/FlowChart/components/base/endNode.tsx new file mode 100644 index 0000000..533b5ab --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/FlowChart/components/base/endNode.tsx @@ -0,0 +1,11 @@ +export default defineComponent({ + name: 'EndNode', + setup() { + return () => ( +
+
+
流程结束
+
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/components/FlowChart/components/base/errorNode/index.tsx b/frontend/apps/allin-ssl/src/components/FlowChart/components/base/errorNode/index.tsx new file mode 100644 index 0000000..bd9d75a --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/FlowChart/components/base/errorNode/index.tsx @@ -0,0 +1,14 @@ +import { BaseNodeData } from '@components/FlowChart/types' + +export default defineComponent({ + name: 'BranchNode', + props: { + node: { + type: Object as () => BaseNodeData, + default: () => ({}), + }, + }, + setup() { + return () =>
渲染节点失败,请检查类型是否支持
+ }, +}) diff --git a/frontend/apps/allin-ssl/src/components/FlowChart/components/other/addNode/index.module.css b/frontend/apps/allin-ssl/src/components/FlowChart/components/other/addNode/index.module.css new file mode 100644 index 0000000..ba8605d --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/FlowChart/components/other/addNode/index.module.css @@ -0,0 +1,129 @@ +.add { + position: relative; + display: flex; + justify-content: center; + align-items: center; + padding: 4rem 0; +} + +.add::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: -1; + margin: auto; + width: 0.2rem; + height: 100%; + background-color: #cacaca; +} + +.addBtn { + position: absolute; + left: 50%; + top: 50%; + margin-left: -1.2rem; + margin-top: -2rem; + display: flex; + justify-content: center; + align-items: center; + width: 2.4rem; + height: 2.4rem; + border-radius: 4rem; + background-color: #1c84c6; + box-shadow: 0.5rem 0.5rem 1rem 0.2rem rgba(0, 0, 0, 0.2); + transition-property: width, height; + transition-duration: 0.1s; + display: flex; + justify-content: center; + align-items: center; +} + +/* .addBtn:hover { + width: 3.5rem; + height: 3.5rem; +} */ + +.addBtnIcon { + font-weight: bold; + color: #FFFFFF; + cursor: pointer; +} + +.addSelectBox { + position: absolute; + z-index: 9999999999999999; + top: -.8rem; + min-width: 160px; + padding: 4px; + list-style-type: none; + background-color: #ffffff; + background-clip: padding-box; + border-radius: 8px; + outline: none; + box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05); +} + +.addSelectBox::before { + content: ''; + width: 0; + height: 0; + border: 1rem solid; + position: absolute; + top: 1rem; +} + +.addSelectItem { + margin: 0; + width: 100%; + padding: 5px 12px; + color: rgba(0, 0, 0, 0.88); + font-weight: normal; + font-size: 14px; + line-height: 1.5714285714285714; + cursor: pointer; + transition: all 0.2s; + border-radius: 4px; + display: flex; + align-items: center; +} + +.addSelectItem:hover { + background-color: #1e83e9 !important; + color: #FFFFFF !important; +} + +.addSelectItemIcon { + width: 1.2rem; + height: 1.2rem; + margin-right: 1rem; +} + +.addSelectItemTitle { + font-size: 1.4rem; +} + +.addSelected { + background-color: #1e83e9 !important; + color: #FFFFFF !important; +} + +.addLeft { + right: 3.4rem; +} + +.addLeft::before { + right: -2rem; + border-color: transparent transparent transparent #FFFFFF; +} + +.addRight { + left: 3.4rem; +} + +.addRight::before { + left: -2rem; + border-color: transparent #FFFFFF transparent transparent; +} \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/components/FlowChart/components/other/addNode/index.tsx b/frontend/apps/allin-ssl/src/components/FlowChart/components/other/addNode/index.tsx new file mode 100644 index 0000000..e453a07 --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/FlowChart/components/other/addNode/index.tsx @@ -0,0 +1,93 @@ +import { useAddNodeController } from '@components/FlowChart/useController' +import SvgIcon from '@components/SvgIcon' +import styles from './index.module.css' + +import type { + NodeIcon, + NodeNum, + NodeTitle, + BaseNodeData, + BranchNodeData, + BaseRenderNodeOptions, +} from '@components/FlowChart/types' +import nodeOptions from '@components/FlowChart/lib/config' + +interface NodeSelect { + title: NodeTitle + type: NodeNum + icon: NodeIcon + selected: boolean +} + +export default defineComponent({ + name: 'AddNode', + props: { + node: { + type: Object as PropType, + default: () => ({}), + }, + }, + setup(props) { + const { + isShowAddNodeSelect, + nodeSelectList, + addNodeBtnRef, + addNodeSelectRef, + addNodeSelectPostion, + showNodeSelect, + addNodeData, + itemNodeSelected, + excludeNodeSelectList, + } = useAddNodeController() + + const config = ref>() // 节点配置 + + watch( + () => props.node.type, + (newVal) => { + config.value = nodeOptions[newVal]() || {} + }, + ) + + return () => ( +
+
showNodeSelect(true, props.node.type as NodeNum)} + onMouseleave={() => showNodeSelect(false)} + > + + {isShowAddNodeSelect.value && ( +
    + {nodeSelectList.value.map((item: NodeSelect) => { + // 判断类型是否支持添加 + if (!excludeNodeSelectList.value?.includes(item.type)) { + return ( +
  • addNodeData(props.node, item.type)} + onMouseenter={itemNodeSelected} + > + +
    {item.title.name}
    +
  • + ) + } + return null + })} +
+ )} +
+
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/components/FlowChart/components/other/drawer.tsx b/frontend/apps/allin-ssl/src/components/FlowChart/components/other/drawer.tsx new file mode 100644 index 0000000..44973b8 --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/FlowChart/components/other/drawer.tsx @@ -0,0 +1,82 @@ +import { NEmpty } from 'naive-ui' +import type { BaseNodeData } from '@components/FlowChart/types' +import { $t } from '@locales/index' + +type AsyncComponentLoader = () => Promise + +/** + * 节点配置抽屉组件 + * 用于显示节点配置界面,根据节点类型动态加载对应的配置组件 + */ +export default defineComponent({ + name: 'FlowChartDrawer', + props: { + /** + * 节点数据 + */ + node: { + type: Object as PropType, + default: null, + }, + }, + setup(props) { + /** + * 节点配置组件Map + */ + const nodeConfigComponents = shallowRef>({}) + + /** + * 动态导入节点配置组件 + * 使用import.meta.glob导入所有drawer组件 + * 1. 任务节点配置组件 + * 2. 基础节点配置组件 + */ + const taskDrawers: Record = import.meta.glob('../task/*/drawer.tsx') as Record< + string, + AsyncComponentLoader + > + + // 预加载所有抽屉组件 + const loadComponents = () => { + // 加载任务节点组件 + Object.keys(taskDrawers).forEach((path) => { + const matches = path.match(/\.\.\/task\/(\w+)\/drawer\.tsx/) + if (matches && matches[1]) { + const nodeType = matches[1].replace('Node', '').toLowerCase() + const loaderFn = taskDrawers[path] + if (loaderFn) { + nodeConfigComponents.value[nodeType] = defineAsyncComponent(loaderFn) + } + } + }) + } + + /** + * 渲染节点配置组件 + */ + const renderConfigComponent = computed(() => { + if (!props.node || !props.node.type) { + return h(NEmpty, { + description: $t('t_2_1744870863419'), + }) + } + const nodeType = props.node.type + // 查找对应类型的配置组件 + if (nodeConfigComponents.value[nodeType]) { + return h(nodeConfigComponents.value[nodeType], { node: props.node }) + } + // 找不到对应的配置组件时显示提示 + return h(NEmpty, { + description: $t('t_3_1744870864615'), + }) + }) + + loadComponents() + + return () => ( +
+ {renderConfigComponent.value} +
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/components/FlowChart/components/render/nodeWrap.tsx b/frontend/apps/allin-ssl/src/components/FlowChart/components/render/nodeWrap.tsx new file mode 100644 index 0000000..c0453c1 --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/FlowChart/components/render/nodeWrap.tsx @@ -0,0 +1,71 @@ +import { BRANCH, EXECUTE_RESULT_BRANCH } from '@components/FlowChart/lib/alias' +import BranchNode from '@components/FlowChart/components/base/branchNode' +import ConditionNode from '@components/FlowChart/components/base/conditionNode' +import BaseNode from '@components/FlowChart/components/base/baseNode' +import NodeWrap from '@components/FlowChart/components/render/nodeWrap' + +import type { + BaseNodeData, + BranchNodeData, + ExecuteResultBranchNodeData, + NodeWrapProps, +} from '@components/FlowChart/types' + +// 自定义样式 +const styles = { + flowNodeWrap: 'flex flex-col items-center w-full relative', + flowNodeWrapNested: 'nested-node-wrap w-full', + flowNodeWrapDeep: 'deep-nested-node-wrap w-full', +} + +export default defineComponent({ + name: 'NodeWrap', + props: { + // 节点数据 + node: { + type: Object as PropType, + default: () => ({}), + }, + // 嵌套深度 + depth: { + type: Number, + default: 0, + }, + }, + setup(props: NodeWrapProps) { + // 计算当前节点的嵌套深度样式类 + const getDepthClass = () => { + if (props.depth && props.depth > 1) { + return props.depth > 2 ? styles.flowNodeWrapDeep : styles.flowNodeWrapNested + } + return styles.flowNodeWrap + } + + return { + getDepthClass, + } + }, + render() { + if (!this.node) return null + const currentDepth = this.depth || 0 + const nextDepth = currentDepth + 1 + + return ( +
+ {/* 判断是否为分支节点或普通节点 */} + {this.node.type === BRANCH ? : null} + + {/* 判断是否为条件节点 */} + {this.node.type === EXECUTE_RESULT_BRANCH ? ( + + ) : null} + + {/* 判断是否为普通节点 */} + {![BRANCH, EXECUTE_RESULT_BRANCH].includes(this.node.type) ? : null} + + {/* 判断是否存在子节点 */} + {this.node.childNode?.type && } +
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/components/FlowChart/index.module.css b/frontend/apps/allin-ssl/src/components/FlowChart/index.module.css new file mode 100644 index 0000000..5a2cb09 --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/FlowChart/index.module.css @@ -0,0 +1,44 @@ +.flowContainer { + @apply flex relative box-border w-full h-[calc(100vh-19rem)] overflow-x-auto overflow-y-auto p-[1rem]; +} + +.flowProcess { + @apply relative h-full w-full; +} + +.flowZoom { + @apply flex fixed items-center justify-between h-[4rem] w-[12.5rem] bottom-[4rem] z-[99]; +} + +.flowZoomIcon { + @apply w-[2.5rem] h-[2.5rem] cursor-pointer border flex items-center justify-center; + border-color: var(--n-border-color); +} + +/* 嵌套节点包装器样式 */ +.nested-node-wrap { + @apply flex flex-col items-center relative; + max-width: 100%; +} + +.deep-nested-node-wrap { + @apply flex flex-col items-center relative; + max-width: 100%; +} + +/* 右侧配置区样式 */ +.configPanel { + @apply flex flex-col w-[360px] min-w-[360px] z-10 rounded-lg; +} + +.configHeader { + @apply flex items-center justify-between px-4 py-3 border-b border-gray-200 ; +} + +.configContent { + @apply flex-1 overflow-y-auto ; +} + +.emptyTip { + @apply flex items-center justify-center h-full text-gray-400 ; +} \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/components/FlowChart/index.tsx b/frontend/apps/allin-ssl/src/components/FlowChart/index.tsx new file mode 100644 index 0000000..91c9500 --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/FlowChart/index.tsx @@ -0,0 +1,111 @@ +import { NButton, NIcon, NInput } from 'naive-ui' +import { SaveOutlined, ArrowLeftOutlined } from '@vicons/antd' +import { $t } from '@locales/index' +import SvgIcon from '@components/SvgIcon' + +import { useController } from './useController' +import { useStore } from './useStore' + +import EndNode from './components/base/endNode' +import NodeWrap from './components/render/nodeWrap' + +import styles from './index.module.css' +import type { FlowNode, FlowNodeProps } from './types' +import { useThemeCssVar } from '@baota/naive-ui/theme' + +export default defineComponent({ + name: 'FlowChart', + props: { + isEdit: { + type: Boolean, + default: false, + }, + type: { + type: String as PropType<'quick' | 'advanced'>, + default: 'quick', + }, + node: { + type: Object as PropType, + default: () => ({}), + }, + // 任务节点列表 + taskComponents: { + type: Object as PropType>, + default: () => ({}), + }, + }, + setup(props: FlowNodeProps, { slots }) { + const cssVars = useThemeCssVar([ + 'borderColor', + 'dividerColor', + 'textColor1', + 'textColor2', + 'primaryColor', + 'primaryColorHover', + 'bodyColor', + ]) + const { flowData, selectedNodeId, flowZoom, resetFlowData } = useStore() + const { initData, handleSaveConfig, handleZoom, goBack } = useController({ + type: props?.type, + node: props?.node, + isEdit: props?.isEdit, + }) + // 提供任务节点组件映射给后代组件使用 + provide('taskComponents', props.taskComponents) + onMounted(initData) + onUnmounted(resetFlowData) + return () => ( +
+
+
+ + + + + {$t('t_0_1744861190562')} + +
+
+ +
+
+ + + + + {$t('t_2_1744861190040')} + +
+
+
+ {/* 左侧流程容器 */} +
+ {/* 流程容器*/} +
+ {/* 渲染流程节点 */} + + {/* 流程结束节点 */} + +
+ {/* 缩放控制区 */} +
+
handleZoom(1)}> + +
+ {flowZoom.value}% +
handleZoom(2)}> + +
+
+
+
+ {/* 保留原有插槽 */} + {slots.default?.()} +
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/components/FlowChart/lib/alias.tsx b/frontend/apps/allin-ssl/src/components/FlowChart/lib/alias.tsx new file mode 100644 index 0000000..279ac9d --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/FlowChart/lib/alias.tsx @@ -0,0 +1,12 @@ +// 基础节点 +export const END = 'end' // 结束节点 +export const DEFAULT = 'default' // 默认节点 +export const START = 'start' // 开始节点 +export const BRANCH = 'branch' // 分支节点(并行分支和条件分支) +export const CONDITION = 'condition' // 条件子节点 +export const EXECUTE_RESULT_BRANCH = 'execute_result_branch' // 执行结果节点 +export const EXECUTE_RESULT_CONDITION = 'execute_result_condition' // 执行结果条件节点 +export const UPLOAD = 'upload' // 上传节点 +export const NOTIFY = 'notify' // 通知节点 +export const APPLY = 'apply' // 申请节点 +export const DEPLOY = 'deploy' // 部署节点 diff --git a/frontend/apps/allin-ssl/src/components/FlowChart/lib/config.tsx b/frontend/apps/allin-ssl/src/components/FlowChart/lib/config.tsx new file mode 100644 index 0000000..dc515de --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/FlowChart/lib/config.tsx @@ -0,0 +1,277 @@ +import { deepMerge } from '@baota/utils/data' +import { v4 as uuidv4 } from 'uuid' +import { + APPLY, + BRANCH, + CONDITION, + DEPLOY, + NOTIFY, + UPLOAD, + EXECUTE_RESULT_BRANCH, + EXECUTE_RESULT_CONDITION, + START, +} from './alias' +import { + BaseRenderNodeOptions, + NodeOptions, + BaseNodeData, + ExecuteResultConditionNodeData, + ExecuteResultBranchNodeData, +} from '../types' + +const nodeOptions = {} as NodeOptions + +// 基础节点配置 +const baseOptions = ( + options: Partial> & { defaultNode?: T }, +): BaseRenderNodeOptions => { + const defaultOptions: BaseRenderNodeOptions = { + title: { + name: '', // 节点标题 + color: '#FFFFFF', // 节点标题颜色 + bgColor: '#3CB371', // 节点标题背景颜色 + }, + icon: { + name: '', // 节点图标 + color: '#3CB371', // 节点图标颜色 + }, + operateNode: { + add: true, // 节点是否可以追加 + sort: 1, // 节点排序,用于排序节点的显示优先级,主要用于配置节点的操作 + addBranch: false, // 节点是否可以添加分支 + edit: true, // 节点是否可以编辑 + remove: true, // 节点是否可以删除 + onSupportNode: [], // 节点不支持添加的节点类型 + }, + isHasDrawer: false, // 节点是否可以进行配置 + defaultNode: {} as T, + } + return deepMerge(defaultOptions, options) as BaseRenderNodeOptions +} + +// ------------------------------ 基础节点配置 ------------------------------ + +// // 结束节点 +// nodeOptions[END] = baseOptions({ +// title: { name: '结束' }, +// icon: { name: END }, +// addNode: false, +// removedNode: false, +// hasDrawer: false, +// hiddenNode: true, +// }) + +// // 默认节点 +// nodeOptions[DEFAULT] = baseOptions({ +// title: { name: '默认' }, +// icon: { name: DEFAULT }, +// addNode: true, +// hasDrawer: true, +// defaultNode: { +// name: '默认', +// type: DEFAULT, +// config: {}, +// childNode: null, +// }, +// }) + +// ------------------------------ 执行业务节点配置 ------------------------------ + +// 开始节点 +nodeOptions[START] = () => + baseOptions({ + title: { name: '开始' }, + operateNode: { onSupportNode: [EXECUTE_RESULT_BRANCH], remove: false, edit: false, add: false }, + defaultNode: { + id: uuidv4(), + name: '开始', + type: START, + config: { + exec_type: 'manual', + }, + childNode: null, + }, + }) + +// 申请节点 +nodeOptions[APPLY] = () => + baseOptions({ + title: { name: '申请' }, + icon: { name: APPLY }, + operateNode: { sort: 1 }, + defaultNode: { + id: uuidv4(), + name: '申请', + type: APPLY, + config: { + domains: '', + email: '', + eabId: '', + ca: 'letsencrypt', + proxy: '', + end_day: 30, + provider: '', + provider_id: '', + algorithm: 'RSA2048', + skip_check: 0, + }, + childNode: null, + }, + }) + +// 上传节点 +nodeOptions[UPLOAD] = () => + baseOptions({ + title: { name: '上传' }, + icon: { name: UPLOAD }, + operateNode: { sort: 2, onSupportNode: [EXECUTE_RESULT_BRANCH] }, + defaultNode: { + id: uuidv4(), + name: '上传', + type: UPLOAD, + config: { + cert_id: '', + cert: '', + key: '', + }, + childNode: null, + }, + }) + +// 部署节点 +nodeOptions[DEPLOY] = () => + baseOptions({ + title: { name: '部署' }, + icon: { name: DEPLOY }, + operateNode: { sort: 3 }, + defaultNode: { + id: uuidv4(), + name: '部署', + type: DEPLOY, + inputs: [], + config: { + provider: '', + provider_id: '', + skip: 1, + inputs: { + fromNodeId: '', + name: '', + }, + }, + childNode: null, + }, + }) + +// 通知节点 +nodeOptions[NOTIFY] = () => + baseOptions({ + title: { name: '通知' }, + icon: { name: NOTIFY }, + operateNode: { sort: 4 }, + defaultNode: { + id: uuidv4(), + name: '通知', + type: NOTIFY, + config: { + provider: '', + provider_id: '', + subject: '', + body: '', + }, + childNode: null, + }, + }) + +// 分支节点 +nodeOptions[BRANCH] = () => + baseOptions({ + title: { name: '并行分支' }, + icon: { name: BRANCH }, + operateNode: { sort: 5, addBranch: true }, + defaultNode: { + id: uuidv4(), + name: '并行分支', + type: BRANCH, + conditionNodes: [ + { + id: uuidv4(), + name: '分支1', + type: CONDITION, + config: {}, + childNode: null, + }, + { + id: uuidv4(), + name: '分支2', + type: CONDITION, + config: {}, + childNode: null, + }, + ], + }, + }) + +// 条件节点 +nodeOptions[CONDITION] = () => + baseOptions({ + title: { name: '分支1' }, + icon: { name: CONDITION }, + operateNode: { add: false, onSupportNode: [EXECUTE_RESULT_BRANCH] }, + defaultNode: { + id: uuidv4(), + name: '分支1', + type: CONDITION, + icon: { name: CONDITION }, + config: {}, + childNode: null, + }, + }) + +// 执行结构分支 +nodeOptions[EXECUTE_RESULT_BRANCH] = () => + baseOptions({ + title: { name: '执行结果分支' }, + icon: { name: BRANCH }, + operateNode: { sort: 7, onSupportNode: [EXECUTE_RESULT_BRANCH] }, + defaultNode: { + id: uuidv4(), + name: '执行结果分支', + type: EXECUTE_RESULT_BRANCH, + conditionNodes: [ + { + id: uuidv4(), + name: '若当前节点执行成功…', + type: EXECUTE_RESULT_CONDITION, + icon: { name: 'success' }, + config: { type: 'success' }, + childNode: null, + }, + { + id: uuidv4(), + name: '若当前节点执行失败…', + type: EXECUTE_RESULT_CONDITION, + icon: { name: 'error' }, + config: { type: 'fail' }, + childNode: null, + }, + ], + }, + }) + +// 执行结构条件 +nodeOptions[EXECUTE_RESULT_CONDITION] = () => + baseOptions({ + title: { name: '执行结构条件' }, + icon: { name: BRANCH }, + operateNode: { add: false, onSupportNode: [EXECUTE_RESULT_BRANCH] }, + defaultNode: { + id: uuidv4(), + name: '若前序节点执行失败…', + type: EXECUTE_RESULT_CONDITION, + icon: { name: 'SUCCESS' }, + config: { type: 'SUCCESS' }, + childNode: null, + }, + }) + +export default nodeOptions diff --git a/frontend/apps/allin-ssl/src/components/FlowChart/lib/verify.tsx b/frontend/apps/allin-ssl/src/components/FlowChart/lib/verify.tsx new file mode 100644 index 0000000..6c77bcf --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/FlowChart/lib/verify.tsx @@ -0,0 +1,411 @@ +import { ref, onUnmounted } from 'vue' +import { ValidatorResult, ValidatorFunction, RuleItem, ValidatorDescriptor } from '../types' + +/** + * 验证器类 + * 用于管理所有节点的验证函数和验证结果 + */ +class Validator { + // 存储所有节点的验证函数 + private validators: Map = new Map() + // 存储所有节点的验证结果 + private validationResults: Map = new Map() + // 存储所有节点的数据 + private valuesMap: Map = new Map() + // 存储节点规则验证状态 + public rulesMap: Map = new Map() + + /** + * 注册验证器 + * @param nodeId - 节点ID + * @param validator - 验证函数 + */ + register(nodeId: string, validator: ValidatorFunction) { + this.validators.set(nodeId, validator) + this.validate(nodeId) + } + + /** + * 注销验证器 + * @param nodeId - 节点ID + */ + unregister(nodeId: string) { + this.validators.delete(nodeId) + this.validationResults.delete(nodeId) + this.valuesMap.delete(nodeId) + } + + unregisterAll() { + this.validators.clear() + this.validationResults.clear() + this.valuesMap.clear() + } + + /** + * 注册兼容async-validator的规则 + * @param nodeId - 节点ID + * @param descriptor - 验证规则描述符 + * @param initialValues - 初始值 + */ + registerCompatValidator(nodeId: string, descriptor: ValidatorDescriptor, initialValues?: Record) { + if (initialValues) { + this.valuesMap.set(nodeId, { ...initialValues }) + } else { + this.valuesMap.set(nodeId, {}) + } + + const validator = () => { + return this.validateWithRules(nodeId, descriptor) + } + this.validators.set(nodeId, validator) + } + + /** + * 设置节点的值 + * @param nodeId - 节点ID + * @param key - 属性名 + * @param value - 属性值 + */ + setValue(nodeId: string, key: string, value: any) { + const values = this.valuesMap.get(nodeId) || {} + values[key] = value + this.valuesMap.set(nodeId, values) + } + + /** + * 批量设置节点的值 + * @param nodeId - 节点ID + * @param values - 属性值集合 + */ + setValues(nodeId: string, values: Record) { + const currentValues = this.valuesMap.get(nodeId) || {} + this.valuesMap.set(nodeId, { ...currentValues, ...values }) + } + + /** + * 获取节点的值 + * @param nodeId - 节点ID + * @param key - 属性名 + */ + getValue(nodeId: string, key: string) { + const values = this.valuesMap.get(nodeId) || {} + return values[key] + } + + /** + * 获取节点的所有值 + * @param nodeId - 节点ID + */ + getValues(nodeId: string) { + return this.valuesMap.get(nodeId) || {} + } + + /** + * 使用兼容async-validator的规则验证数据 + * @param nodeId - 节点ID + * @param descriptor - 验证规则描述符 + * @returns 验证结果 + */ + validateWithRules(nodeId: string, descriptor: ValidatorDescriptor): ValidatorResult { + const values = this.valuesMap.get(nodeId) || {} + for (const field in descriptor) { + const rules = Array.isArray(descriptor[field]) + ? (descriptor[field] as RuleItem[]) + : [descriptor[field] as RuleItem] + const value = values[field] + if (field in values) { + for (const rule of rules) { + // 检查必填 + if (rule.required && (value === undefined || value === null || value === '')) { + const message = rule.message || `${field}是必填项` + return { valid: false, message } + } + + // 如果值为空但不是必填,则跳过其他验证 + if ((value === undefined || value === null || value === '') && !rule.required) { + continue + } + + // 检查类型 + if (rule.type && !this.validateType(rule.type, value)) { + const message = rule.message || `${field}的类型应为${rule.type}` + return { valid: false, message } + } + + // 检查格式 + if (rule.pattern && !rule.pattern.test(String(value))) { + const message = rule.message || `${field}格式不正确` + return { valid: false, message } + } + + // 检查长度范围 + if (rule.type === 'string' || rule.type === 'array') { + const length = value.length || 0 + + if (rule.len !== undefined && length !== rule.len) { + const message = rule.message || `${field}的长度应为${rule.len}` + return { valid: false, message } + } + + if (rule.min !== undefined && length < rule.min) { + const message = rule.message || `${field}的长度不应小于${rule.min}` + return { valid: false, message } + } + + if (rule.max !== undefined && length > rule.max) { + const message = rule.message || `${field}的长度不应大于${rule.max}` + return { valid: false, message } + } + } + + // 检查数值范围 + if (rule.type === 'number') { + if (rule.len !== undefined && value !== rule.len) { + const message = rule.message || `${field}应等于${rule.len}` + return { valid: false, message } + } + + if (rule.min !== undefined && value < rule.min) { + const message = rule.message || `${field}不应小于${rule.min}` + return { valid: false, message } + } + + if (rule.max !== undefined && value > rule.max) { + const message = rule.message || `${field}不应大于${rule.max}` + return { valid: false, message } + } + } + + // 检查枚举值 + if (rule.enum && !rule.enum.includes(value)) { + const message = rule.message || `${field}的值不在允许范围内` + return { valid: false, message } + } + + // 检查空白字符 + if (rule.whitespace && rule.type === 'string' && !value.trim()) { + const message = rule.message || `${field}不能只包含空白字符` + return { valid: false, message } + } + + // 自定义验证器 + if (rule.validator) { + try { + const result = rule.validator(rule, value, undefined) + + if (result === false) { + const message = rule.message || `${field}验证失败` + return { valid: false, message } + } + + if (result instanceof Error) { + return { valid: false, message: result.message } + } + + if (Array.isArray(result) && result.length > 0 && result[0] instanceof Error) { + return { valid: false, message: result[0].message } + } + } catch (error) { + return { valid: false, message: error instanceof Error ? error.message : `${field}验证出错` } + } + } + } + } + } + return { valid: true, message: '' } + } + + /** + * 验证值的类型 + * @param type - 类型 + * @param value - 值 + * @returns 是否通过验证 + */ + private validateType(type: string, value: any): boolean { + switch (type) { + case 'string': + return typeof value === 'string' + case 'number': + return typeof value === 'number' && !isNaN(value) + case 'boolean': + return typeof value === 'boolean' + case 'method': + return typeof value === 'function' + case 'regexp': + return value instanceof RegExp + case 'integer': + return typeof value === 'number' && Number.isInteger(value) + case 'float': + return typeof value === 'number' && !Number.isInteger(value) + case 'array': + return Array.isArray(value) + case 'object': + return typeof value === 'object' && !Array.isArray(value) && value !== null + case 'enum': + return true // 枚举类型在单独的规则中验证 + case 'date': + return value instanceof Date + case 'url': + try { + new URL(value) + return true + } catch (e) { + return false + } + case 'email': + return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value) + default: + return true + } + } + + /** + * 验证指定节点 + * @param nodeId - 节点ID + * @returns ValidatorResult - 验证结果 + */ + validate(nodeId: string) { + const validator = this.validators.get(nodeId) + if (validator) { + const result = validator() + this.validationResults.set(nodeId, result) + return result + } + return { valid: false, message: '' } + } + + /** + * 验证所有已注册的节点 + * @returns 包含所有节点验证结果的对象 + */ + validateAll() { + let allValid = true + const results: Record = {} + this.validators.forEach((validator, nodeId) => { + const result = this.validate(nodeId) + results[nodeId] = result + if (!result.valid) { + allValid = false + } + }) + return { + valid: allValid, + results, + } + } + + /** + * 获取指定节点的验证结果 + * @param nodeId - 节点ID + * @returns ValidatorResult - 验证结果 + */ + getValidationResult(nodeId: string): ValidatorResult { + return this.validationResults.get(nodeId) || { valid: true, message: '' } + } +} + +// 创建全局验证器实例 +const validator = new Validator() + +/** + * 节点验证器 Hook + * 提供节点验证相关的功能 + * @returns 包含验证相关方法和状态的对象 + */ +export function useNodeValidator() { + // 响应式的验证结果 + const validationResult = ref({ valid: false, message: '' }) + + /** + * 注册验证器函数 + * @param nodeId - 节点ID + * @param validateFn - 验证函数 + */ + const registerValidator = (nodeId: string, validateFn: ValidatorFunction) => { + validator.register(nodeId, validateFn) + validationResult.value = validator.getValidationResult(nodeId) + } + + /** + * 注册兼容async-validator的规则 + * @param nodeId - 节点ID + * @param descriptor - 验证规则描述符 + * @param initialValues - 初始值 + */ + const registerCompatValidator = ( + nodeId: string, + descriptor: ValidatorDescriptor, + initialValues?: Record, + ) => { + validator.registerCompatValidator(nodeId, descriptor, initialValues) + validationResult.value = validator.getValidationResult(nodeId) + } + + /** + * 设置字段值 + * @param nodeId - 节点ID + * @param key - 字段名 + * @param value - 字段值 + */ + const setFieldValue = (nodeId: string, key: string, value: any) => { + validator.setValue(nodeId, key, value) + } + + /** + * 批量设置字段值 + * @param nodeId - 节点ID + * @param values - 字段值集合 + */ + const setFieldValues = (nodeId: string, values: Record) => { + validator.setValues(nodeId, values) + } + + /** + * 获取字段值 + * @param nodeId - 节点ID + * @param key - 字段名 + */ + const getFieldValue = (nodeId: string, key: string) => { + return validator.getValue(nodeId, key) + } + + /** + * 获取所有字段值 + * @param nodeId - 节点ID + */ + const getFieldValues = (nodeId: string) => { + return validator.getValues(nodeId) + } + + /** + * 执行验证 + * @param nodeId - 节点ID + * @returns ValidatorResult - 验证结果 + */ + const validate = (nodeId: string) => { + const result = validator.validate(nodeId) + validationResult.value = result + return result + } + + /** + * 注销验证器 + * @param nodeId - 节点ID + */ + const unregisterValidator = (nodeId: string) => { + validator.unregister(nodeId) + } + + return { + validationResult, // 验证结果(响应式) + registerValidator, // 注册验证器方法 + registerCompatValidator, // 注册兼容async-validator的规则 + setFieldValue, // 设置字段值 + setFieldValues, // 批量设置字段值 + getFieldValue, // 获取字段值 + getFieldValues, // 获取所有字段值 + validate, // 执行验证方法 + unregisterValidator, // 注销验证器方法 + validator, // 验证器实例 + } +} diff --git a/frontend/apps/allin-ssl/src/components/FlowChart/mock/index.ts b/frontend/apps/allin-ssl/src/components/FlowChart/mock/index.ts new file mode 100644 index 0000000..9d0de1a --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/FlowChart/mock/index.ts @@ -0,0 +1,83 @@ +export default { + name: '', + childNode: { + id: 'start-1', + name: '开始', + type: 'start', + config: { + exec_type: 'auto', + type: 'day', + hour: 1, + minute: 0, + }, + childNode: { + id: 'apply-1', + name: '申请证书', + type: 'apply', + config: { + domains: '', + email: '', + eabId: '', + ca: 'letsencrypt', + proxy: '', + end_day: 30, + provider: '', + provider_id: '', + algorithm: 'RSA2048', + skip_check: 0, + }, + childNode: { + id: 'deploy-1', + name: '部署', + type: 'deploy', + inputs: [], + config: { + provider: '', + provider_id: '', + skip: 1, + inputs: { + fromNodeId: '', + name: '', + }, + }, + childNode: { + id: 'execute', + name: '执行结果', + type: 'execute_result_branch', + config: { fromNodeId: 'deploy-1' }, + conditionNodes: [ + { + id: 'execute-success', + name: '执行成功', + type: 'execute_result_condition', + config: { + fromNodeId: '', + type: 'success', + }, + }, + { + id: 'execute-failure', + name: '执行失败', + type: 'execute_result_condition', + config: { + fromNodeId: '', + type: 'fail', + }, + }, + ], + childNode: { + id: 'notify-1', + name: '通知任务', + type: 'notify', + config: { + provider: '', + provider_id: '', + subject: '', + body: '', + }, + }, + }, + }, + }, + }, +} diff --git a/frontend/apps/allin-ssl/src/components/FlowChart/types.d.ts b/frontend/apps/allin-ssl/src/components/FlowChart/types.d.ts new file mode 100644 index 0000000..abee0fd --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/FlowChart/types.d.ts @@ -0,0 +1,362 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + APPLY, + BRANCH, + CONDITION, + DEPLOY, + END, + EXECUTE_RESULT_BRANCH, + EXECUTE_RESULT_CONDITION, + NOTIFY, + START, + UPLOAD, + DEFAULT, +} from './lib/alias' +import type { FormRules } from 'naive-ui' +import type { Component } from 'vue' + +// 拖拽效果 +export interface FlowNodeProps { + isEdit: boolean + type: 'quick' | 'advanced' + node: FlowNode + taskComponents?: Record +} + +export interface FlowNode { + id: string + name: string + childNode: BaseNodeData +} + +// 添加节点选项 +export interface NodeSelect { + title: NodeTitle + type: NodeNum + icon: NodeIcon + selected: boolean +} + +// 节点标题配置 +export interface NodeTitle { + name: string + color?: string + bgColor?: string +} + +// 节点图标配置 +export interface NodeIcon { + name: string + color?: string +} + +// 操作节点配置,用于渲染节点的操作功能 +export interface operateNodeOptions { + add?: boolean // 是否显示添加节点 + sort?: number // 节点排序,用于排序节点的显示优先级,主要用于配置节点的操作 + addBranch?: boolean // 是否显示添加分支节点,仅在节点类型为分支节点时有效 + addBranchTitle?: string // 添加分支节点标题,仅在节点类型为分支节点时有效 + edit?: boolean // 是否可编辑 + remove?: boolean // 是否可删除 + onSupportNode?: NodeNum[] // 不支持添加的节点类型 +} + +// 基础节点渲染配置,用于渲染节点 +export interface BaseRenderNodeOptions { + title: NodeTitle // 节点标题 + icon?: NodeIcon // 节点图标 + isAddNode?: boolean // 是否显示添加节点 + isHasDrawer?: boolean // 是否显示抽屉 + operateNode?: operateNodeOptions // 是否显示操作节点 + defaultNode?: T // 默认节点数据 -- 节点数据,用于组合相应结构 +} + +// 基础节点数据 +export interface BaseNodeData> { + type: NodeNum // 节点类型 + id?: string // 节点id,用于编辑 + name: string // 节点名称 + icon?: NodeIcon // 节点图标 + inputs?: Record[] // 输入,用于连接其他节点的数据 + config?: T // 参数,用于配置当前的节点 + childNode?: BaseNodeData | BranchNodeData | null // 子节点 +} + +// 分支节点数据 +export interface BranchNodeData = Record> extends BaseNodeData { + type: typeof BRANCH // 节点类型 + conditionNodes: ConditionNodeData[] // 子节点 +} + +// 执行条件分支节点数据 +export interface ExecuteResultBranchNodeData extends BaseNodeData { + type: typeof EXECUTE_RESULT_BRANCH // 节点类型 + conditionNodes: ExecuteResultConditionNodeData[] // 子节点 +} + +// 执行结果条件节点数据 +interface ExecuteResultCondition { + type: 'success' | 'fail' + [key: string]: unknown +} + +// 条件节点数据 +export interface ExecuteResultConditionNodeData extends BaseNodeData { + type: typeof EXECUTE_RESULT_CONDITION // 节点类型 + config: ExecuteResultCondition +} + +// 节点类型 +export type NodeNum = + | typeof START // 开始节点 + | typeof DEFAULT // 默认节点 + | typeof END // 结束节点 + | typeof BRANCH // 分支节点 + | typeof CONDITION // 条件节点 + | typeof EXECUTE_RESULT_BRANCH // 执行结果分支节点(if) + | typeof EXECUTE_RESULT_CONDITION // 执行结果条件节点 + | typeof UPLOAD // 上传节点(业务) + | typeof NOTIFY // 通知节点(业务) + | typeof APPLY // 申请节点(业务) + | typeof DEPLOY // 部署节点(业务) + +// 节点配置映射 +export type NodeOptions = { + [START]: () => BaseRenderNodeOptions + [END]: () => BaseRenderNodeOptions + [DEFAULT]: () => BaseRenderNodeOptions + [BRANCH]: () => BaseRenderNodeOptions + [CONDITION]: () => BaseRenderNodeOptions + [EXECUTE_RESULT_BRANCH]: () => BaseRenderNodeOptions + [EXECUTE_RESULT_CONDITION]: () => BaseRenderNodeOptions + [UPLOAD]: () => BaseRenderNodeOptions + [NOTIFY]: () => BaseRenderNodeOptions + [APPLY]: () => BaseRenderNodeOptions + [DEPLOY]: () => BaseRenderNodeOptions +} + +// 基础节点配置 +interface BaseNodeProps { + node: BaseNodeData> +} + +// 定义组件包装器接受的props +export interface NodeWrapProps { + node?: BaseNodeData | BranchNodeData | ExecuteResultBranchNodeData + depth?: number +} + +/** + * 验证结果接口 + * @property valid - 验证是否通过 + * @property message - 验证失败时的提示信息 + */ +export interface ValidatorResult { + valid: boolean + message: string +} + +/** + * 验证函数类型定义 + * @returns ValidatorResult - 返回验证结果对象 + */ +export type ValidatorFunction = () => ValidatorResult + +/** + * 兼容async-validator的类型定义 + */ +export interface RuleItem { + type?: string + required?: boolean + pattern?: RegExp + min?: number + max?: number + len?: number + enum?: Array + whitespace?: boolean + message?: string + validator?: ( + rule: RuleItem, + value: any, + callback?: (error?: Error) => void, + ) => boolean | Error | Error[] | Promise + asyncValidator?: (rule: RuleItem, value: any, callback?: (error?: Error) => void) => Promise + transform?: (value: any) => any + [key: string]: any +} + +/** + * 兼容async-validator的描述符类型 + */ +export type ValidatorDescriptor = FormRules + +// 定义组件接收的参数类型(开始节点) +export interface StartNodeConfig { + // 执行模式:auto-自动,manual-手动 + exec_type: 'auto' | 'manual' + // 执行周期类型 + type?: 'month' | 'day' | 'week' + month?: number + week?: number + hour?: number + minute?: number +} + +// 定义组件接收的参数类型(申请节点) +export interface ApplyNodeConfig { + // 基本选项 + domains: string // 域名 + email: string // 邮箱 + eabId: string // CA授权ID(EAB ID) + ca: string // CA类型 + proxy: string // 代理地址 + provider_id: string // DNS提供商授权ID + provider: string // DNS提供商 + end_day: number // 续签间隔 + // 高级功能 + name_server: string // DNS递归服务器 + skip_check: number // 跳过检查 + algorithm: string // 数字证书算法 + // 高级功能 + // algorithm: 'RSA2048' | 'RSA3072' | 'RSA4096' | 'RSA8192' | 'EC256' | 'EC384' // 数字证书算法 + // dnsServer?: string // 指定DNS解析服务器 + // dnsTimeout?: number // DNS超时时间 + // dnsTtl?: number // DNS解析TTL时间 + // disableCnameFollow: boolean // 关闭CNAME跟随 + // disableAutoRenew: boolean // 关闭ARI续期 + // renewInterval?: number // 续签间隔 +} + +// 部署节点配置 +export interface DeployConfig< + Z extends Record, + T extends + | 'ssh' + | 'btpanel' + | '1panel' + | 'btpanel-site' + | '1panel-site' + | 'tencentcloud-cdn' + | 'tencentcloud-cos' + | 'tencentcloud-waf' + | 'tencentcloud-teo' + | 'aliyun-cdn' + | 'aliyun-oss' + | 'aliyun-waf' + | 'baidu-cdn' + | 'qiniu-cdn' + | 'qiniu-oss' + | 'safeline-site' + | 'safeline-panel' + | 'btpanel-dockersite', +> { + provider: T + provider_id: string + skip: 1 | 0 + [key: string]: Z +} + +// export interface DeployPanelConfig {} + +// 部署节点配置(ssh) +export interface DeploySSHConfig { + certPath: string // 证书文件路径 + keyPath: string // 私钥文件路径 + beforeCmd: string // 前置命令 + afterCmd?: string // 后置命令 +} + +// 部署本地节点配置 +export interface DeployLocalConfig extends DeploySSHConfig { + [key: string]: unknown +} + +// 部署节点配置(宝塔面板) +export interface DeployBTPanelSiteConfig { + siteName: string +} + +// 部署节点配置(1Panel) +export interface Deploy1PanelConfig { + site_id: string +} +// 部署节点配置(1Panel站点) +export interface Deploy1PanelSiteConfig extends Deploy1PanelConfig { + [key: string]: unknown +} + +// 部署腾讯云CDN/阿里云CDN +export interface DeployCDNConfig { + domain: string +} + +// 部署阿里云WAF +export interface DeployWAFConfig { + domain: string + region: string +} + +// 部署腾讯云COS/阿里云OSS +export interface DeployStorageConfig { + domain: string + region: string + bucket: string +} + +// 部署节点配置(雷池WAF) +export interface DeploySafelineConfig { + [key: string]: unknown +} + +// 部署节点配置(雷池WAF站点) +export interface DeploySafelineSiteConfig extends DeployBTPanelSiteConfig { + [key: string]: unknown +} + +// 部署宝塔docker站点 +export interface DeployBTPanelDockerSiteConfig extends DeployBTPanelSiteConfig { + [key: string]: unknown +} + +// 部署节点配置 +export type DeployNodeConfig = DeployConfig< + | DeploySSHConfig // 部署节点配置(ssh) + | DeployLocalConfig // 部署节点配置(本地) + | DeployBTPanelConfig // 部署节点配置(宝塔面板) + | DeployBTPanelSiteConfig // 部署节点配置(宝塔面板站点) + | Deploy1PanelConfig // 部署节点配置(1Panel) + | Deploy1PanelSiteConfig // 部署节点配置(1Panel站点) + | DeployCDNConfig // 部署节点配置(腾讯云CDN/阿里云CDN) + | DeployWAFConfig // 部署节点配置(阿里云WAF) + | DeployStorageConfig // 部署节点配置(腾讯云COS/阿里云OSS) + | DeploySafelineConfig // 部署节点配置(雷池WAF) + | DeploySafelineSiteConfig // 部署节点配置(雷池WAF站点) + | DeployBTPanelDockerSiteConfig // 部署节点配置(宝塔docker站点) +> + +// 部署节点输入配置 +export interface DeployNodeInputsConfig { + name: string + fromNodeId: string +} + +// 定义通知节点配置类型 +interface NotifyNodeConfig { + provider: string + provider_id: string + subject: string + body: string +} + +// 定义上传节点配置类型 +interface UploadNodeConfig { + cert_id: string + cert: string + key: string +} + +// 部署节点配置(ssh) +// export type DeployNodeConfigSSH = DeployNodeConfig<'ssh', DeploySSHConfig> + +// 部署节点配置(宝塔面板) +// export type DeployNodeConfigBTPanel = DeployNodeConfig<'btpanel', DeployBTPanelConfig> diff --git a/frontend/apps/allin-ssl/src/components/FlowChart/useController.tsx b/frontend/apps/allin-ssl/src/components/FlowChart/useController.tsx new file mode 100644 index 0000000..3fd8c44 --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/FlowChart/useController.tsx @@ -0,0 +1,214 @@ +import { v4 as uuidv4 } from 'uuid' +import { useMessage } from '@baota/naive-ui/hooks' +import { $t } from '@locales/index' +import { useStore } from '@components/FlowChart/useStore' +import { useStore as useWorkflowViewStore } from '@autoDeploy/children/workflowView/useStore' +import { useNodeValidator } from '@components/FlowChart/lib/verify' +import { useError } from '@baota/hooks/error' + +import type { BaseNodeData, BranchNodeData, FlowNodeProps, NodeNum, StartNodeConfig } from '@components/FlowChart/types' + +const message = useMessage() +const { + flowData, + selectedNodeId, + setflowZoom, + initFlowData, + updateFlowData, + setShowAddNodeSelect, + addNode, + getAddNodeSelect, + resetFlowData, +} = useStore() + +const { workflowData, addNewWorkflow, updateWorkflowData, resetWorkflowData } = useWorkflowViewStore() +const { handleError } = useError() + +/** + * 流程图控制器 + * 用于处理流程图的业务逻辑和用户交互 + * @param {FlowNodeProps} props - 节点数据,可选 + */ +export const useController = (props: FlowNodeProps = { type: 'quick', node: flowData.value, isEdit: false }) => { + // 使用store获取所有需要的方法和状态 + const router = useRouter() + const route = useRoute() + + /** + * 保存节点配置 + * 将当前选中节点的配置保存到流程图中 + */ + const handleSaveConfig = () => { + const { validator } = useNodeValidator() + const res = validator.validateAll() + try { + if (res.valid && flowData.value.name) { + const { active } = workflowData.value + const { id, name, childNode } = flowData.value + const { exec_type, ...exec_time } = childNode.config as unknown as StartNodeConfig + const param = { + name, + active, + content: JSON.stringify(childNode), + exec_type, + exec_time: JSON.stringify(exec_time || {}), + } + if (route.query.isEdit) { + updateWorkflowData({ id, ...param }) + } else { + addNewWorkflow(param) + } + router.push('/auto-deploy') + } else if (!flowData.value.name) { + message.error('保存失败,请输入工作流名称') + } + for (const key in res.results) { + if (res.results.hasOwnProperty(key)) { + const result = res.results[key] as { valid: boolean; message: string } + if (!result.valid) { + message.error(result.message) + break + } + } + } + } catch (error) { + handleError(error).default($t('t_12_1745457489076')) + } + } + + /** + * 运行流程图 + * 触发流程图的执行 + */ + const handleRun = () => { + message.info($t('t_8_1744861189821')) + // 这里可以添加实际的运行逻辑 + } + + /** + * 流程图缩放控制 + * @param {number} type - 缩放类型 1:缩小,2:放大 + */ + const handleZoom = (type: number) => { + setflowZoom(type) + } + + /** + * 返回上一级 + */ + const goBack = () => { + router.back() + } + + /** + * 初始化流程图数据 + */ + const initData = () => { + console.log(props.node, 'init') + resetFlowData() + resetWorkflowData() + // 如果传入了节点数据,使用传入的数据 + if (props.isEdit && props.node) { + console.log(props.node, 'edit') + updateFlowData(props.node) + } else if (props.type === 'quick') { + initFlowData() // 否则使用默认数据初始化 + } else if (props.type === 'advanced') { + updateFlowData(props.node) + } + } + + // 如果传入了node,则当node变化时更新store中的flowData + if (props.node) { + watch( + () => props.node, + (newVal) => { + updateFlowData(newVal) + }, + { deep: true }, + ) + } + + return { + flowData, + selectedNodeId, + handleSaveConfig, + handleZoom, + handleRun, + goBack, + initData, + } +} + +/** + * 添加节点控制器 + * 用于处理添加节点的业务逻辑 + */ +export function useAddNodeController() { + // 使用store获取所有需要的方法和状态 + const store = useStore() + + /** + * 是否显示添加节点选择框 + * @type {Ref} + */ + const isShowAddNodeSelect = ref(false) + + /** + * 定时器 + * @type {Ref} + */ + const timer = ref(null) + + /** + * 显示节点选择 + * @param {boolean} flag - 是否显示 + * @param {NodeNum} [nodeType] - 节点类型 + */ + const showNodeSelect = (flag: boolean, nodeType?: NodeNum) => { + if (!flag) { + clearTimeout(timer.value as number) + timer.value = window.setTimeout(() => { + isShowAddNodeSelect.value = flag + }, 200) as unknown as number + } else { + isShowAddNodeSelect.value = false + isShowAddNodeSelect.value = flag + } + // 设置添加节点选择状态 + if (nodeType) { + setShowAddNodeSelect(flag, nodeType) + } + } + + /** + * 添加节点数据 + * @param {BaseNodeData} parentNode - 父节点 + * @param {NodeNum} type - 生成节点类型 + */ + const addNodeData = (parentNode: BaseNodeData, type: NodeNum) => { + console.log(parentNode, type) + // 设置添加节点选择状态 + isShowAddNodeSelect.value = false + // 判断是否存储节点配置数据 + if (parentNode.id) addNode(parentNode.id, type, { id: uuidv4() }) + } + + /** + * 添加节点选中状态 + */ + const itemNodeSelected = () => { + clearTimeout(timer.value as number) + } + + // 获取添加节点选择 + getAddNodeSelect() + + return { + ...store, + addNodeData, + itemNodeSelected, + isShowAddNodeSelect, + showNodeSelect, + } +} diff --git a/frontend/apps/allin-ssl/src/components/FlowChart/useStore.tsx b/frontend/apps/allin-ssl/src/components/FlowChart/useStore.tsx new file mode 100644 index 0000000..3dd8673 --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/FlowChart/useStore.tsx @@ -0,0 +1,608 @@ +import { formatDate } from '@baota/utils/date' +import { deepMerge } from '@baota/utils/data' +import nodeOptions from '@components/FlowChart/lib/config' +import MockData from '@components/FlowChart/mock' +import type { + NodeIcon, + NodeNum, + NodeTitle, + FlowNode, + BaseNodeData, + NodeSelect, + BranchNodeData, + ExecuteResultBranchNodeData, +} from '@components/FlowChart/types' +import { BRANCH, CONDITION, EXECUTE_RESULT_BRANCH, EXECUTE_RESULT_CONDITION } from '@components/FlowChart/lib/alias' +import { $t } from '@locales/index' + +/** + * 流程图数据存储 + * 用于管理流程图的状态、缩放等数据 + */ +export const useFlowStore = defineStore('flow-store', () => { + const flowData = ref({ + id: '', + name: '', + childNode: { + id: 'start-1', + name: '开始', + type: 'start', + config: { + exec_type: 'manual', + }, + childNode: null, + }, + }) // 流程图数据 + const flowZoom = ref(100) // 流程图缩放比例 + const advancedOptions = ref(false) // 高级选项 + const addNodeSelectList = ref([]) // 添加节点选项列表 + const excludeNodeSelectList = ref([]) // 排除的节点选项列表 + const addNodeBtnRef = ref(null) // 添加节点按钮 + const addNodeSelectRef = ref(null) // 添加节点选择框 + const addNodeSelectPostion = ref(null) // 添加节点选择框位置 + const selectedNodeId = ref(null) // 当前选中的节点ID + const isRefreshNode = ref(null) // 是否刷新节点 + + // 计算添加节点选项列表,排除的节点选项列表 + const nodeSelectList = computed(() => { + return addNodeSelectList.value.filter((item) => !excludeNodeSelectList.value.includes(item.type)) + }) + + /** + * 当前选中的节点数据 + * @type {ComputedRef} + */ + const selectedNode = computed(() => { + if (!selectedNodeId.value) return null + // 使用findNodeRecursive查找节点 + return findNodeRecursive(flowData.value.childNode, selectedNodeId.value) + }) + + /** + * 节点标题 + * @type {ComputedRef} + */ + const nodeTitle = computed(() => { + if (!selectedNode.value) return $t('t_6_1744861190121') + return selectedNode.value.name + }) + + /** + * 获取添加节点选项列表 + * @type {NodeSelect[]} + */ + const getAddNodeSelect = () => { + addNodeSelectList.value = [] + Object.keys(nodeOptions).forEach((key) => { + const item = nodeOptions[key as NodeNum]() + if (item.operateNode?.add) { + addNodeSelectList.value.push({ + title: { name: item.title.name } as NodeTitle, + type: key as NodeNum, + icon: { ...(item.icon || {}) } as NodeIcon, + selected: false, + }) + } + }) + } + + /** + * 添加排除的节点选项列表 + * @param {NodeNum[]} nodeTypes - 节点类型 + */ + const addExcludeNodeSelectList = (nodeTypes: NodeNum[]) => { + excludeNodeSelectList.value = nodeTypes + } + + /** + * 清除排除的节点选项列表 + */ + const clearExcludeNodeSelectList = () => { + excludeNodeSelectList.value = [] + } + + /** + * 显示添加节点选择框 + * @param {boolean} flag - 是否显示添加节点选择框 + */ + const setShowAddNodeSelect = (flag: boolean, nodeType: NodeNum) => { + // 设置排除的节点选项列表 + excludeNodeSelectList.value = nodeOptions[nodeType]().operateNode?.onSupportNode || [] + // 设置添加节点选择框位置 + if (flag && addNodeSelectRef.value && addNodeBtnRef.value) { + const box = addNodeSelectRef.value.getBoundingClientRect() // 添加节点选择框 + const boxWidth = box.width // 添加节点选择框宽度 + const btn = addNodeBtnRef.value.getBoundingClientRect() // 添加节点按钮 + const btnRight = btn.right // 添加节点按钮右侧位置 + const windowWidth = window.innerWidth // 窗口宽度 + addNodeSelectPostion.value = btnRight + boxWidth > windowWidth ? 1 : 2 + } + } + + /** + * 初始化流程图数据 + * 创建一个默认的开始节点作为流程图的起点 + */ + const initFlowData = () => { + const deepMockData = JSON.parse(JSON.stringify(MockData)) + deepMockData.name = '工作流(' + formatDate(new Date(), 'yyyy/MM/dd HH:mm:ss') + ')' + flowData.value = deepMockData + } + + /** + * 重置流程图数据 + * 清空当前流程图的所有数据 + */ + const resetFlowData = () => initFlowData() + + /** + * 递归查找节点 + * @param node 当前节点 + * @param targetId 目标节点ID + * @returns 找到的节点或null + */ + const findNodeRecursive = (node: BaseNodeData | BranchNodeData, targetId: string): BaseNodeData | null => { + if (node.id === targetId) return node + + // 优先检查子节点 + if (node.childNode) { + const found = findNodeRecursive(node.childNode, targetId) + if (found) return found + } + // 再检查条件节点 + if ((node as BranchNodeData).conditionNodes?.length) { + for (const conditionNode of (node as BranchNodeData).conditionNodes) { + const found = findNodeRecursive(conditionNode, targetId) + if (found) return found + } + } + + return null + } + + /** + * 通过节点id查找节点数据 + * @param nodeId 节点id + * @returns 节点数据或null + */ + const getFlowFindNodeData = (nodeId: string): BaseNodeData | null => { + return findNodeRecursive(flowData.value.childNode, nodeId) + } + + /** + * 递归更新节点 + * @param node 当前节点 + * @param targetId 目标节点ID + * @param updateFn 更新函数 + * @param parent 父节点 + * @returns 是否更新成功 + */ + const updateNodeRecursive = ( + node: BaseNodeData | BranchNodeData, + targetId: string, + updateFn: (node: BaseNodeData | BranchNodeData, parent: BaseNodeData | BranchNodeData | null) => void, + parent: BaseNodeData | BranchNodeData | null = null, + ): boolean => { + if (node.id === targetId) { + updateFn(node, parent) + return true + } + + if (node.childNode) { + if (updateNodeRecursive(node.childNode, targetId, updateFn, node)) { + return true + } + } + + if ((node as BranchNodeData).conditionNodes?.length) { + for (const conditionNode of (node as BranchNodeData).conditionNodes) { + if (updateNodeRecursive(conditionNode, targetId, updateFn, node)) { + return true + } + } + } + + return false + } + + /** + * 添加节点 + * @param parentNodeId 父节点ID + * @param nodeType 节点类型 + * @param nodeData 节点数据 + * @param position 插入位置(对于条件节点有效) + */ + const addNode = ( + parentNodeId: string, // 父节点ID + nodeType: NodeNum, // 节点类型 + nodeData: Partial = {}, // 节点数据 + ) => { + // 获取父节点 + const parentNode = getFlowFindNodeData(parentNodeId) + if (!parentNode) { + console.warn(`Parent node with id ${parentNodeId} not found`) + return + } + // 获取支持的节点默认配置 + let newNodeData = deepMerge(nodeOptions[nodeType]().defaultNode as BaseNodeData, nodeData) as + | BaseNodeData + | BranchNodeData // 获取支持的节点默认配置 + + // 更新原始数据 + updateNodeRecursive(flowData.value.childNode, parentNodeId, (node, parent) => { + switch (nodeType) { + case CONDITION: + // console.log('条件节点', node, parent) + if ((node as BranchNodeData).conditionNodes) { + newNodeData.name = `分支${(node as BranchNodeData).conditionNodes.length + 1}` + ;(node as BranchNodeData).conditionNodes.push(newNodeData) + } + break + case BRANCH: + case EXECUTE_RESULT_BRANCH: + // 执行结果分支节点 + if (nodeType === EXECUTE_RESULT_BRANCH) { + newNodeData = { ...newNodeData, config: { fromNodeId: parentNodeId } } + } + + ;(newNodeData as BranchNodeData).conditionNodes[0].childNode = node.childNode + node.childNode = newNodeData + break + default: + // console.log('其他节点', node, parent) + if (node.childNode) newNodeData.childNode = node.childNode // 组件嵌套到 childNode 中 + node.childNode = newNodeData + break + } + }) + } + + /** + * 向上查找数据类型为 apply 或 upload 的节点 + * @param nodeId 起始节点ID + * @returns 符合条件的节点数组 [{name: string, id: string}] + */ + const findApplyUploadNodesUp = ( + nodeId: string, + scanNode: string[] = ['apply', 'upload'], + ): Array<{ name: string; id: string }> => { + const result: Array<{ name: string; id: string }> = [] + + // 递归查找父节点的函数 + const findParentRecursive = ( + currentNode: BaseNodeData | BranchNodeData, + targetId: string, + path: Array = [], + ): Array | null => { + // 检查当前节点是否为目标节点 + if (currentNode.id === targetId) { + return path + } + + // 检查子节点 + if (currentNode.childNode) { + const newPath = [...path, currentNode] + const found = findParentRecursive(currentNode.childNode, targetId, newPath) + if (found) return found + } + + // 检查条件节点 + if ((currentNode as BranchNodeData).conditionNodes?.length) { + for (const conditionNode of (currentNode as BranchNodeData).conditionNodes) { + const newPath = [...path, currentNode] + const found = findParentRecursive(conditionNode, targetId, newPath) + if (found) return found + } + } + + return null + } + + // 从根节点开始查找路径 + const path = findParentRecursive(flowData.value.childNode, nodeId) + // 如果找到路径,筛选出 apply 和 upload 类型的节点 + if (path) { + path.forEach((node) => { + if (scanNode.includes(node.type)) { + result.push({ + name: node.name, + id: node.id as string, + }) + } + }) + } + return result + } + + /** + * 删除节点 + * @param nodeId 要删除的节点ID + * @param deep 是否深度删除(默认false,即子节点上移) + */ + const removeNode = (nodeId: string, deep: boolean = false) => { + const node = getFlowFindNodeData(nodeId) + if (!node) { + console.warn(`Node with id ${nodeId} not found`) + return + } + + // 更新原始数据 + updateNodeRecursive(flowData.value.childNode, nodeId, (node, parent) => { + if (!parent) { + console.warn('Cannot remove root node') + return + } + + const { type, conditionNodes } = parent as BranchNodeData | ExecuteResultBranchNodeData + // 处理条件节点(分支节点、执行结果分支节点) + // console.log(type, conditionNodes, node) + + // 如果当前子节点存在条件节点,需要判断删除后是否支持条件节点,则需要更新 fromNodeId + if (node.childNode?.type === EXECUTE_RESULT_BRANCH && node.childNode?.config) { + node.childNode.config.fromNodeId = parent.id + } + + // console.log(node.childNode, parent) + + // 条件一:当前节点为普通节点 + const nodeTypeList = [CONDITION, EXECUTE_RESULT_CONDITION, BRANCH, EXECUTE_RESULT_BRANCH] + if (!nodeTypeList.includes(node.type) && parent.childNode?.id === nodeId) { + // 处理普通节点 + if (deep) { + // 深度删除,直接移除 + parent.childNode = undefined + } else { + // 非深度删除,子节点上移 + if (node.childNode) { + parent.childNode = node.childNode + } else { + parent.childNode = undefined + } + } + return + } + + // 条件二:当前节点为条件节点 + if (nodeTypeList.includes(node.type)) { + // 条件节点为分支节点或执行结果分支节点 + if (conditionNodes.length === 2) { + // 条件节点为分支节点,则选定对立节点的子节点作为当前节点的子节点 + // console.log('条件节点为分支节点', parent) + if (type === BRANCH) { + updateNodeRecursive(flowData.value.childNode, parent.id as string, (nodes, parents) => { + const index = conditionNodes.findIndex((n) => n.id === nodeId) + const backNode = nodes.childNode + if (index !== -1 && parents) { + parents.childNode = conditionNodes[index === 0 ? 1 : 0].childNode // 将选定对立节点的子节点作为当前节点的子节 + const allChildNode = getNodePropertyToLast(parents, 'childNode') as BaseNodeData + allChildNode.childNode = backNode + } + }) + } else { + updateNodeRecursive(flowData.value.childNode, parent.id as string, (nodes, parents) => { + if (parents) { + if (parent?.childNode?.id) { + parents.childNode = parent.childNode + } else { + parents.childNode = undefined + } + } + }) + } + } else { + const index = (parent as BranchNodeData).conditionNodes.findIndex((n) => n.id === nodeId) + if (index !== -1) { + if (deep) { + // 深度删除,直接移除 + ;(parent as BranchNodeData).conditionNodes.splice(index, 1) + } else { + // 非深度删除,子节点上移 + const targetNode = (parent as BranchNodeData).conditionNodes[index] + if (targetNode?.childNode) { + ;(parent as BranchNodeData).conditionNodes[index] = targetNode.childNode + } else { + ;(parent as BranchNodeData).conditionNodes.splice(index, 1) + } + } + } + } + } + }) + + return flowData.value + } + + /** + * 递归查询节点属性直到最后一层 + * @param node 当前节点 + * @param property 要查询的属性名 + * @returns 最后一层的属性值 + */ + const getNodePropertyToLast = (node: BaseNodeData, property: string) => { + if (!node) return null + const value = (node as any)[property] + if (!value) return node + // 如果属性值是一个对象,继续递归查询 + if (typeof value === 'object' && value !== null) { + return getNodePropertyToLast(value, property) + } + } + + /** + * 更新节点配置 + * @param nodeId 节点ID + * @param config 新的配置数据 + */ + const updateNodeConfig = (nodeId: string, config: Record) => { + const node = getFlowFindNodeData(nodeId) + if (!node) { + console.warn(`Node with id ${nodeId} not found`) + return + } + // 更新原始数据 + updateNodeRecursive(flowData.value.childNode, nodeId, (node) => { + node.config = config + }) + return flowData.value + } + + /** + * 更新节点数据 + * @param nodeId 要更新的节点ID + * @param newNodeData 新的节点数据 + */ + const updateNode = (nodeId: string, newNodeData: Partial, isMergeArray: boolean = true) => { + const node = getFlowFindNodeData(nodeId) + if (!node) { + console.warn(`Node with id ${nodeId} not found`) + return + } + + // 更新原始数据 + updateNodeRecursive(flowData.value.childNode, nodeId, (node) => { + const updatedNode = deepMerge(node, newNodeData, isMergeArray) as BaseNodeData + Object.keys(updatedNode).forEach((key) => { + if (key in node) { + ;(node as any)[key] = updatedNode[key as keyof typeof updatedNode] + } + }) + }) + + return flowData.value + } + + /** + * 检查节点是否存在子节点 + * @param nodeId 节点id + * @returns 是否存在子节点 + */ + const checkFlowNodeChild = (nodeId: string): boolean => { + const node = getFlowFindNodeData(nodeId) + return node ? !!(node.childNode || (node as BranchNodeData).conditionNodes?.length) : false + } + + /** + * 检查是否存在行内节点 + * @param nodeId 节点id + */ + const checkFlowInlineNode = (nodeId: string) => { + const node = getFlowFindNodeData(nodeId) + if (!node || node.type !== 'condition') return + + // 更新原始数据 + updateNodeRecursive(flowData.value.childNode, nodeId, (node) => { + if ((node as BranchNodeData).conditionNodes) { + ;(node as BranchNodeData).conditionNodes = (node as BranchNodeData).conditionNodes.filter( + (n) => n.id !== nodeId, + ) + } + }) + } + + // /** + // * @description 显示节点选择 + // * @param {boolean} flag + // * @param {NodeNum} nodeData + // */ + // const showNodeSelect = (flag: boolean, nodeType?: NodeNum) => { + // if (!flag) { + // clearTimeout(timer.value as number) + // timer.value = window.setTimeout(() => { + // isShowAddNodeSelect.value = flag + // }, 1000) as unknown as null + // } else { + // isShowAddNodeSelect.value = false + // isShowAddNodeSelect.value = flag + // } + // // 设置添加节点选择状态 + // if (nodeType) { + // flowStore.setShowAddNodeSelect(flag, nodeType) + // } + // } + + /** + * 获取流程图数据 + * 返回当前流程图数据的深拷贝,避免直接修改原始数据 + * @returns {Object} 流程图数据的副本 + */ + const getResultData = () => { + return deepMerge({}, flowData.value) + } + + /** + * 更新流程图数据 + * 用新的数据替换当前的流程图数据 + * @param {Object} newData - 新的流程图数据 + */ + const updateFlowData = (newData: FlowNode) => { + flowData.value = newData + } + + /** + * 设置流程图缩放比例 + * 控制流程图的显示大小 + * @param {number} type - 缩放类型:1 表示缩小,2 表示放大 + */ + const setflowZoom = (type: number) => { + if (type === 1 && flowZoom.value > 50) { + flowZoom.value -= 10 + } else if (type === 2 && flowZoom.value < 300) { + flowZoom.value += 10 + } + } + + return { + // 数据 + flowData, // 流程图数据 + flowZoom, // 流程图缩放比例 + selectedNode, // 当前选中的节点 + nodeTitle, // 当前选中的节点标题 + selectedNodeId, // 当前选中的节点ID + isRefreshNode, // 是否刷新节点 + advancedOptions, // 高级选项 + + // 方法 + initFlowData, // 初始化流程图数据 + resetFlowData, // 重置流程图数据 + getResultData, // 获取流程图数据 + updateFlowData, // 更新流程图数据 + setflowZoom, // 设置流程图缩放比例 + + // 添加节点-数据 + addNodeSelectList, // 添加节点选项列表 + nodeSelectList, // 计算添加节点选项列表,排除的节点选项列表 + excludeNodeSelectList, // 排除的节点选项列表 + addNodeBtnRef, // 添加节点按钮 + addNodeSelectRef, // 添加节点选择框 + addNodeSelectPostion, // 添加节点选择框位置 + + // 添加节点-方法 + getAddNodeSelect, // 获取添加节点选项列表 + addExcludeNodeSelectList, // 添加排除的节点选项列表 + clearExcludeNodeSelectList, // 清除排除的节点选项列表 + setShowAddNodeSelect, // 设置显示添加节点选择框 + + // 节点操作 + addNode, + removeNode, + updateNodeConfig, + updateNode, + findApplyUploadNodesUp, // 向上查找 apply 和 upload 类型节点 + checkFlowNodeChild, // 检查节点是否存在子节点 + checkFlowInlineNode, // 检查是否存在行内节点 + } +}) + +/** + * 使用流程图数据存储 + * 提供流程图数据存储的引用和解构 + * @returns {Object} 包含流程图数据存储的引用和解构 + */ +export const useStore = () => { + const flowStore = useFlowStore() + const storeRef = storeToRefs(flowStore) + return { + ...flowStore, + ...storeRef, + } +} diff --git a/frontend/apps/allin-ssl/src/components/LogDisplay/index.tsx b/frontend/apps/allin-ssl/src/components/LogDisplay/index.tsx new file mode 100644 index 0000000..28fc7a8 --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/LogDisplay/index.tsx @@ -0,0 +1,122 @@ +// External Libraries +import { defineComponent, type PropType } from 'vue'; +import { NCard, NSpin, NButton, NSpace, NIcon, NLog, NConfigProvider } from 'naive-ui'; +import hljs from 'highlight.js/lib/core'; // hljs 仍需在此导入以传递给 NConfigProvider + +// Type Imports +import type { LogDisplayProps } from './types'; + +// Absolute Internal Imports +import { $t } from '@locales/index'; +import { DownloadOutline, RefreshOutline } from '@vicons/ionicons5'; + +// Relative Internal Imports +import { useLogDisplayController } from './useController'; + +/** + * @description LogDisplay 组件 (LogViewer) + * @description 用于显示日志内容,支持加载、刷新、下载和自定义高亮。 + */ +export default defineComponent({ + name: 'LogViewer', + props: { + content: { + type: String, + default: '', + }, + loading: { + type: Boolean, + default: false, + }, + enableDownload: { + type: Boolean, + default: true, + }, + downloadFileName: { + type: String, + default: 'logs.txt', + }, + title: { + type: String, + default: () => $t('t_0_1746776194126'), // $t('t_0_1747754231151') + }, + fetchLogs: { + type: Function as PropType<() => Promise>, + default: undefined, // 显式设为 undefined,由 controller 判断 + }, + } as const, // 使用 as const 帮助类型推断 + + setup(props: LogDisplayProps) { + const { + isLoading, + logRef, + logContent, // NLog 的 log prop 应该直接使用 controller.logs.value + cssVarStyles, + refreshLogs, + downloadLogs, + } = useLogDisplayController(props); + + return () => ( + + {{ + header: () => props.title, + 'header-extra': () => ( + + + {{ + icon: () => ( + + + + ), + default: () => $t('t_0_1746497662220'), + }} + + {props.enableDownload && ( + + {{ + icon: () => ( + + + + ), + default: () => $t('t_2_1746776194263'), + }} + + )} + + ), + default: () => ( + + + line.content).join('\n')} // NLog 的 log prop 期望是 string + language="custom-logs" + trim={false} + fontSize={14} + lineHeight={1.5} + class="h-full" // NLog 充满 NSpin + style={{ + // height: '500px', // 改为 flex 布局后,由父容器控制高度 + border: '1px solid var(--n-border-color)', + borderRadius: 'var(--n-border-radius)', // 使用 Naive UI 变量 + padding: '10px', + }} + /> + + + ), + }} + + ) + }, +}); diff --git a/frontend/apps/allin-ssl/src/components/LogDisplay/types.d.ts b/frontend/apps/allin-ssl/src/components/LogDisplay/types.d.ts new file mode 100644 index 0000000..6a89dbd --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/LogDisplay/types.d.ts @@ -0,0 +1,74 @@ +import type { PropType, Ref, ComputedRef, StyleValue } from 'vue'; +import type { NLog } from 'naive-ui'; // 假设 NLog 类型可以这样导入,如果不行,需要找到正确的导入方式或使用更通用的类型 + +/** + * @description 日志行项目结构 + */ +export interface LogLine { + type: string; // 例如: 'default', 'info', 'error', 'warning' + content: string; +} + +/** + * @description LogDisplay 组件的 Props 定义 + */ +export interface LogDisplayProps { + /** + * 日志内容 + */ + content?: string; + /** + * 是否加载中 + */ + loading?: boolean; + /** + * 是否允许下载 + */ + enableDownload?: boolean; + /** + * 下载文件名 + */ + downloadFileName?: string; + /** + * 标题 + */ + title?: string; + /** + * 获取日志方法 + */ + fetchLogs?: () => Promise; +} + +/** + * @description useLogDisplayController 组合式函数暴露的接口 + */ +export interface LogDisplayControllerExposes { + /** + * 内部日志内容引用 + */ + logs: Ref; + /** + * 内部加载状态引用 + */ + isLoading: Ref; + /** + * NLog 组件实例引用 + */ + logRef: Ref | null>; + /** + * 格式化后的日志内容,供 NLog 组件使用 + */ + logContent: ComputedRef; + /** + * 主题相关的 CSS 变量 + */ + cssVarStyles: ComputedRef; + /** + * 刷新日志的方法 + */ + refreshLogs: () => void; + /** + * 下载日志的方法 + */ + downloadLogs: () => void; +} \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/components/LogDisplay/useController.tsx b/frontend/apps/allin-ssl/src/components/LogDisplay/useController.tsx new file mode 100644 index 0000000..bd987c4 --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/LogDisplay/useController.tsx @@ -0,0 +1,157 @@ +// External Libraries +import { ref, watch, computed, onMounted, nextTick, type StyleValue } from 'vue'; +import hljs from 'highlight.js/lib/core'; +import { NLog } from 'naive-ui'; // 确保 NLog 可以作为类型导入,或者其类型定义可用 + +// Type Imports +import type { LogDisplayProps, LogLine, LogDisplayControllerExposes } from './types'; + +// Absolute Internal Imports +import { $t } from '@locales/index'; +import { useThemeCssVar } from '@baota/naive-ui/theme'; // 假设路径正确 + +/** + * @description LogDisplay 组件的控制器逻辑 + * @param props - 组件的 props + * @returns {LogDisplayControllerExposes} 暴露给视图的响应式数据和方法 + */ +export function useLogDisplayController( + props: LogDisplayProps, +): LogDisplayControllerExposes { + const logs = ref(props.content || ''); + const isLoading = ref(props.loading || false); + const logRef = ref | null>(null); + + // 初始化 highlight.js 自定义语言 (仅执行一次) + // 注意: 如果多个 LogDisplay 实例共享页面,此注册是全局的。 + // 如果需要隔离,可能需要更复杂的处理或在应用级别注册。 + if (!hljs.getLanguage('custom-logs')) { + hljs.registerLanguage('custom-logs', () => ({ + contains: [ + { + className: 'info-text', + begin: /\[INFO\]/, + }, + { + className: 'error-text', + begin: /\[ERROR\]/, + }, + { + className: 'warning-text', + begin: /\[WARNING\]/, + }, + { + className: 'date-text', + begin: /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/, + }, + ], + })); + } + + const themeCssVars = useThemeCssVar(['successColor', 'errorColor', 'warningColor', 'successColorPressed']); + const cssVarStyles = computed((): StyleValue => { + // 根据 useThemeCssVar 的实际返回值和 NCard 的 style 需求来构造 + // 示例: return { '--success-color': themeCssVars.value.successColor, ... } + // 为了简单起见,这里直接返回,实际项目中可能需要转换 + return themeCssVars.value as StyleValue; + }); + + // 监听外部 props.content 变化 + watch( + () => props.content, + (newValue) => { + logs.value = newValue || ''; + scrollToBottom(); + }, + ); + + // 监听外部 props.loading 变化 + watch( + () => props.loading, + (newValue) => { + isLoading.value = !!newValue; + }, + ); + + /** + * @description 滚动到日志底部 + */ + const scrollToBottom = () => { + nextTick(() => { + // NLog 的 scrollTo 方法可能需要特定参数或直接操作其内部的滚动元素 + // 此处假设 NLog 实例有 scrollTo 方法且接受 { top: number } + logRef.value?.scrollTo({ top: Number.MAX_SAFE_INTEGER, behavior: 'smooth' }); + }); + }; + + /** + * @description 加载日志内容 + */ + const loadLogs = async () => { + if (!props.fetchLogs) return; + isLoading.value = true; + try { + const result = await props.fetchLogs(); + logs.value = result; + scrollToBottom(); + } catch (error) { + logs.value = `${$t('t_1_1746776198156')}: ${error instanceof Error ? error.message : String(error)}`; + } finally { + isLoading.value = false; + } + }; + + /** + * @description 下载日志 + */ + const downloadLogs = () => { + if (!logs.value) return; + const blob = new Blob([logs.value], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = props.downloadFileName || 'logs.txt'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + /** + * @description 刷新日志 + */ + const refreshLogs = () => { + loadLogs(); + }; + + /** + * @description 将日志内容字符串转换为 NLog 需要的格式 + */ + const logContent = computed((): LogLine[] => { + if (!logs.value) return []; + return logs.value.split('\n').map( + (line): LogLine => ({ + type: 'default', // NLog 可能不需要这个 type,或者可以根据行内容动态设置 + content: line, + }), + ); + }); + + onMounted(() => { + if (props.fetchLogs) { + loadLogs(); + } else if (props.content) { + scrollToBottom() // 如果初始就有 content prop,也尝试滚动到底部 + } + }); + + return { + logs, + isLoading, + logRef, + logContent, + cssVarStyles, + refreshLogs, + downloadLogs, + }; +} \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/components/NotifyProviderSelect/index.tsx b/frontend/apps/allin-ssl/src/components/NotifyProviderSelect/index.tsx new file mode 100644 index 0000000..acb98ad --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/NotifyProviderSelect/index.tsx @@ -0,0 +1,170 @@ +// External Libraries +import { defineComponent, computed } from 'vue' +import { NButton, NDivider, NFlex, NFormItemGi, NGi, NGrid, NSelect, NText } from 'naive-ui' + +// Type Imports +import type { VNode, PropType } from 'vue' +import type { SelectOption } from 'naive-ui' +import type { NotifyProviderOption, NotifyProviderSelectProps } 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 { useNotifyProviderSelectController } from './useController' + +/** + * @description 通知提供商选择组件。允许用户从列表中选择一个通知渠道。 + * @example + * + */ +export default defineComponent({ + name: 'NotifyProviderSelect', + props: { + /** + * 表单项的路径,用于表单校验或上下文。 + * @default '' + */ + path: { + type: String as PropType, + default: '', + }, + /** + * 当前选中的值 (对应 NotifyProviderOption['value'],即 NSelect 的 modelValue)。 + * @default '' + */ + value: { + type: String as PropType, + default: '', + }, + /** + * 决定 `props.value` 和 `NotifyProviderOption.value` 字段是基于原始提供商的 `value` 还是 `type`。 + * - 'value': 使用原始提供商的 `value` 字段作为 `NotifyProviderOption.value`。 + * - 'type': 使用原始提供商的 `type` 字段作为 `NotifyProviderOption.value`。 + * @default 'value' + */ + valueType: { + type: String as PropType, + default: 'value', + validator: (val: string) => ['value', 'type'].includes(val), + }, + /** + * 是否为添加模式,显示额外的"新增渠道"和"刷新"按钮。 + * @default false + */ + isAddMode: { + type: Boolean as PropType, + default: false, + }, + }, + /** + * @event update:value - 当选中的通知提供商更新时触发。 + * @param {NotifyProviderOption} option - 选中的通知提供商的完整对象 (`{ label: string, value: string, type: string }`)。 + */ + emits: { + 'update:value': (payload: NotifyProviderOption) => { + return ( + typeof payload === 'object' && payload !== null && 'label' in payload && 'value' in payload && 'type' in payload + ) + }, + }, + setup(props: NotifyProviderSelectProps, { emit }) { + const { selectOptions, goToAddNotifyProvider, handleSelectUpdate, fetchNotifyProviderData } = + useNotifyProviderSelectController(props, emit) + + /** + * @description 渲染 NSelect 中已选项的标签 (Tag)。 + * @param {object} params - Naive UI 传递的选项包装对象。 + * @param {SelectOption} params.option - 当前选项的数据。 + * @returns {VNode} 渲染后的 VNode。 + */ + const renderSingleSelectTag = ({ option }: { option: SelectOption }): VNode => { + // 将 SelectOption 转换为 NotifyProviderOption + const notifyOption = option as NotifyProviderOption & SelectOption + return ( +
+ {notifyOption.label ? ( + + + {notifyOption.label} + + ) : ( + {$t('t_0_1745887835267')} + )} +
+ ) + } + + /** + * @description 渲染 NSelect 下拉列表中的选项标签。 + * @param {SelectOption} option - 当前选项的数据。 + * @returns {VNode} 渲染后的 VNode。 + */ + const renderLabel = (option: SelectOption): VNode => { + // 将 SelectOption 转换为 NotifyProviderOption + const notifyOption = option as NotifyProviderOption & SelectOption + return ( + + + {notifyOption.label} + + ) + } + + // 转换选项格式以兼容 NSelect + const naiveSelectOptions = computed(() => { + return selectOptions.value.map((option): SelectOption & NotifyProviderOption => ({ + ...option, + // 确保兼容 NSelect 的 SelectOption 接口 + })) + }) + + return () => ( + + + ( +
+ + {selectOptions.value.length === 0 ? $t('t_0_1745887835267') : '暂无匹配的通知渠道'} + +
+ ), + }} + /> +
+ {props.isAddMode && ( + +
+ + + {$t('t_2_1745887834248')} + + + {$t('t_0_1746497662220')} + +
+
+ )} +
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/components/NotifyProviderSelect/types.d.ts b/frontend/apps/allin-ssl/src/components/NotifyProviderSelect/types.d.ts new file mode 100644 index 0000000..8eecbed --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/NotifyProviderSelect/types.d.ts @@ -0,0 +1,95 @@ +// Type Imports +import type { Ref } from 'vue' + +/** + * @description 通知提供商选项的类型定义 + */ +export interface NotifyProviderOption { + /** + * 选项的显示标签 + */ + label: string + /** + * 选项的实际值 (用于 NSelect 的 v-model) + */ + value: string + /** + * 选项的原始类型 (例如 'email', 'sms'), 用于图标显示等业务逻辑 + */ + type: string +} + +/** + * @description 从 Store 获取的原始通知提供商项目类型 + * @remark 请根据实际 Store 中 `notifyProvider.value` 的具体项结构调整此类型。 + */ +export interface RawProviderItem { + /** + * 显示标签 + */ + label: string + /** + * 实际值 + */ + value: string + /** + * 原始类型 + */ + type: string + /** + * 允许其他可能的属性,但建议明确列出所有已知属性以增强类型安全 + */ + [key: string]: any +} + +/** + * @description NotifyProviderSelect 组件的 Props 定义 + */ +export interface NotifyProviderSelectProps { + /** + * 表单项的路径,用于表单校验或上下文 + * @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 +} + +// Controller暴露给View的类型 +export interface NotifyProviderSelectControllerExposes { + /** + * 内部选中的完整通知提供商对象 + */ + selectedOptionFull: Ref + /** + * 格式化后用于 NSelect 的选项列表 + */ + selectOptions: Ref + /** + * 打开通知渠道配置页面的方法 + */ + goToAddNotifyProvider: () => void + /** + * 处理 NSelect 值更新的方法 + */ + handleSelectUpdate: (value: string) => void + /** + * 手动刷新通知提供商列表的方法 + */ + fetchNotifyProviderData: () => void +} diff --git a/frontend/apps/allin-ssl/src/components/NotifyProviderSelect/useController.tsx b/frontend/apps/allin-ssl/src/components/NotifyProviderSelect/useController.tsx new file mode 100644 index 0000000..0980876 --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/NotifyProviderSelect/useController.tsx @@ -0,0 +1,151 @@ +// External Libraries +import { ref, watch, computed } from 'vue' + +// Type Imports +import type { + NotifyProviderOption, + NotifyProviderSelectControllerExposes, + NotifyProviderSelectProps, + RawProviderItem, +} from './types' + +// Absolute Internal Imports - Store +import { useStore } from '@layout/useStore' // 假设这是正确的 Store 路径 +// Absolute Internal Imports - Config +import { MessagePushConfig } from '@config/data' + +/** + * @description NotifyProviderSelect 组件的控制器逻辑 + * @param props - 组件的 props + * @param emit - 组件的 emit 函数 + * @returns {NotifyProviderSelectControllerExposes} 暴露给视图的响应式数据和方法 + */ +export function useNotifyProviderSelectController( + props: NotifyProviderSelectProps, + emit: (event: 'update:value', payload: NotifyProviderOption) => void, +): NotifyProviderSelectControllerExposes { + const { fetchNotifyProvider, notifyProvider } = useStore() + + // 内部存储当前选中的完整 NotifyProviderOption 对象 + const selectedOptionFull = ref({ label: '', value: '', type: '' }) + // 存储 NSelect 使用的选项列表 + const selectOptions = ref([]) + + /** + * @description 从 MessagePushConfig 生成备用选项列表 + */ + const fallbackOptions = computed(() => { + return Object.entries(MessagePushConfig).map(([key, config]) => ({ + label: config.name, + value: props.valueType === 'value' ? key : config.type, + type: config.type, + })) + }) + + /** + * @description 根据 NSelect 的 modelValue (通常是 option.value 字符串) 更新内部完整的选中项对象 + * @param currentSelectValue - NSelect 当前的 modelValue (字符串) + */ + const updateInternalSelectedOption = (currentSelectValue: string) => { + if (!currentSelectValue) { + selectedOptionFull.value = { label: '', value: '', type: '' } + return + } + + // 优先从当前选项列表中查找 + const foundOption = selectOptions.value.find((item) => item.value === currentSelectValue) + if (foundOption) { + selectedOptionFull.value = { ...foundOption } + return + } + + // 如果在当前列表中找不到,尝试从备用选项中查找 + const fallbackOption = fallbackOptions.value.find((item) => item.value === currentSelectValue) + if (fallbackOption) { + selectedOptionFull.value = { ...fallbackOption } + return + } + + // 如果都找不到,创建一个临时选项 + selectedOptionFull.value = { + label: currentSelectValue, + value: currentSelectValue, + type: '', + } + } + + /** + * @description 打开通知渠道配置页面 + */ + const goToAddNotifyProvider = (): void => { + window.open('/settings?tab=notification', '_blank') + } + + /** + * @description 处理 NSelect 组件的值更新事件 + * @param newSelectedValue - NSelect 更新的 modelValue (字符串) + */ + const handleSelectUpdate = (newSelectedValue: string): void => { + updateInternalSelectedOption(newSelectedValue) + emit('update:value', { ...selectedOptionFull.value }) // Emit a copy + } + + /** + * @description 外部调用以刷新通知提供商列表 + */ + const fetchNotifyProviderData = (): void => { + fetchNotifyProvider() + } + + // 监听父组件传入的 props.value (可能是初始值或外部更改) + watch( + () => props.value, + (newVal) => { + // 确保提供商列表已加载或正在加载,然后再尝试更新选中项细节 + if (selectOptions.value.length === 0 && newVal) { + fetchNotifyProviderData() // 如果列表为空且有 props.value,触发加载 + } + updateInternalSelectedOption(newVal) + }, + { immediate: true }, + ) + + // 监听从 Store 获取的原始通知提供商列表,并进行转换 + watch( + () => notifyProvider.value, // notifyProvider.value 应该是原始提供商列表 + (rawProviders) => { + if (rawProviders && rawProviders.length > 0) { + // 如果 Store 中有数据,使用 Store 数据 + selectOptions.value = rawProviders.map((item: RawProviderItem) => ({ + label: item.label, + // `value` 字段给 NSelect 使用,根据 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 (NSelect 的 modelValue) 重新更新内部完整选中项 + updateInternalSelectedOption(props.value) + }, + { immediate: true, deep: true }, + ) + + // 初始化时如果 Store 为空,先使用备用数据 + if (!notifyProvider.value || notifyProvider.value.length === 0) { + selectOptions.value = fallbackOptions.value + // 尝试获取 Store 数据 + fetchNotifyProviderData() + } + + return { + selectedOptionFull, + selectOptions, + goToAddNotifyProvider, + handleSelectUpdate, + fetchNotifyProviderData, + } +} diff --git a/frontend/apps/allin-ssl/src/components/SvgIcon/index.tsx b/frontend/apps/allin-ssl/src/components/SvgIcon/index.tsx new file mode 100644 index 0000000..a4a2133 --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/SvgIcon/index.tsx @@ -0,0 +1,66 @@ +import { defineComponent, computed, PropType } from 'vue' + +/** + * @description Svg 图标组件的属性定义 + */ +interface SvgIconProps { + /** + * 图标的尺寸,例如 '1.8rem', '24px' + * @default '1.8rem' + */ + size: string + /** + * 图标的名称 (不包含 'icon-' 前缀) + * @required + */ + icon: string + /** + * 图标的颜色 + * @default '' (继承父级颜色) + */ + color?: string +} + +/** + * @description SVG 图标组件 + * @example + * + */ +export default defineComponent({ + name: 'SvgIcon', + props: { + /** + * 图标的名称 (不包含 'icon-' 前缀) + */ + icon: { + type: String as PropType, + required: true, + }, + /** + * 图标的颜色 + */ + color: { + type: String as PropType, + default: '', + }, + /** + * 图标的尺寸,例如 '1.8rem', '24px' + */ + size: { + type: String as PropType, + default: '1.8rem', + }, + }, + setup(props: SvgIconProps) { + const iconName = computed(() => `#icon-${props.icon}`) + return () => ( + + ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/components/TableEmptyState/index.tsx b/frontend/apps/allin-ssl/src/components/TableEmptyState/index.tsx new file mode 100644 index 0000000..64e1a08 --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/TableEmptyState/index.tsx @@ -0,0 +1,48 @@ +import { $t } from '@locales/index' +import { NEmpty, NButton } from 'naive-ui' +import { defineComponent, type PropType } from 'vue' // Added PropType, vue after naive-ui if sorted strictly by name + +/** + * @description 表格空状态提示组件,带有添加按钮和社区链接 + * @param {string} addButtonText 添加按钮文本 + * @param {() => void} onAddClick 添加按钮点击事件 + */ +interface TableEmptyStateProps { // Renamed from EmptyActionPromptProps + addButtonText: string + onAddClick: () => void +} + +export default defineComponent({ + name: 'TableEmptyState', // Renamed from EmptyActionPrompt + props: { + addButtonText: { + type: String, + required: true, + }, + onAddClick: { + type: Function as PropType<() => void>, // Use PropType for better type definition + required: true, + }, + }, + setup(props: TableEmptyStateProps) { // Use renamed interface + return () => ( +
+ + {$t('t_1_1747754231838')} + + {props.addButtonText} + + ,{$t('t_2_1747754234999')} + + Issues + + ,{$t('t_3_1747754232000')} + + Star + + ,{$t('t_4_1747754235407')} + +
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/components/TypeIcon/index.tsx b/frontend/apps/allin-ssl/src/components/TypeIcon/index.tsx new file mode 100644 index 0000000..9382caa --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/TypeIcon/index.tsx @@ -0,0 +1,59 @@ +import { defineComponent, PropType } from 'vue' +import { NTag } from 'naive-ui' + +// 类型导入 +import type { AuthApiTypeIconProps } from './types' + +// 绝对路径内部导入 - 组件 +import SvgIcon from '@components/SvgIcon' // 请确保此路径 @components/SvgIcon 是正确的 + +// 相对路径内部导入 - Controllers/Composables +import { useAuthApiTypeIconController } from './useController' + +/** + * @component AuthApiTypeIcon + * @description 用于显示不同授权API或资源类型的图标和文本标签。 + * 数据来源于 /lib/data.tsx。 + * + * @example + * + * + * + */ +export default defineComponent({ + name: 'AuthApiTypeIcon', + props: { + /** + * 图标类型键。 + * 该键用于从 /lib/data.tsx 配置中查找对应的图标和名称。 + */ + icon: { + type: String as PropType, + required: true, + }, + /** + * NTag 的类型。 + */ + type: { + type: String as PropType, + default: 'default', + }, + /** + * 文本是否显示。 + */ + text: { + type: Boolean as PropType, + default: true, + }, + }, + setup(props: AuthApiTypeIconProps) { + const { iconPath, typeName } = useAuthApiTypeIconController(props) + + return () => ( + + + {props.text && {typeName.value}} + + ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/components/TypeIcon/types.d.ts b/frontend/apps/allin-ssl/src/components/TypeIcon/types.d.ts new file mode 100644 index 0000000..f21e848 --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/TypeIcon/types.d.ts @@ -0,0 +1,41 @@ +/** + * AuthApiTypeIcon 组件的 Props 接口。 + */ +export interface AuthApiTypeIconProps { + /** + * 图标类型键。 + * 该键用于从 /lib/data.tsx 配置中查找对应的图标和名称。 + * 如果未在配置中找到,将尝试使用 'default' 图标,并直接显示该键作为文本。 + */ + icon: string + /** + * NTag 的类型。 + * @default 'default' + */ + type?: 'default' | 'primary' | 'info' | 'success' | 'warning' | 'error' + /** + * 文本是否显示。 + * @default true + */ + text?: boolean +} + +/** + * useAuthApiTypeIconController Composable 函数暴露的接口。 + */ +export interface AuthApiTypeIconControllerExposes { + /** 计算得到的图标路径 */ + iconPath: globalThis.ComputedRef + /** 计算得到的类型名称,用于显示 */ + typeName: globalThis.ComputedRef +} + +/** + * ApiProjectConfig 的类型定义。 + * 描述了 API 项目配置的结构。 + */ +export interface ApiProjectConfigType { + name: string + icon: string + hostRelated?: Record> +} diff --git a/frontend/apps/allin-ssl/src/components/TypeIcon/useController.tsx b/frontend/apps/allin-ssl/src/components/TypeIcon/useController.tsx new file mode 100644 index 0000000..be86e66 --- /dev/null +++ b/frontend/apps/allin-ssl/src/components/TypeIcon/useController.tsx @@ -0,0 +1,86 @@ +import { computed } from 'vue' +import type { AuthApiTypeIconProps, AuthApiTypeIconControllerExposes, ApiProjectConfigType } from './types' +import { ApiProjectConfig, MessagePushConfig } from '@config/data' // 从指定路径导入数据 + +// --- 数据处理逻辑 --- +// 这些映射表在模块加载时构建一次,供所有组件实例共享 + +// 用于存储从配置派生的显示名称 +const typeNamesMap: Record = {} +// 用于存储从配置派生的图标文件名(不含前缀/后缀) +const iconFileMap: Record = {} +// 用于存储哪些键是通知类型(影响图标前缀) +const notifyKeys = new Set() + +// 处理 ApiProjectConfig +for (const key in ApiProjectConfig) { + if (Object.prototype.hasOwnProperty.call(ApiProjectConfig, key)) { + const config = ApiProjectConfig[key as keyof typeof ApiProjectConfig] as ApiProjectConfigType + typeNamesMap[key] = config.name + iconFileMap[key] = config.icon + if (config?.hostRelated) { + for (const subKey in config.hostRelated) { + if (Object.prototype.hasOwnProperty.call(config.hostRelated, subKey)) { + const subConfig = config.hostRelated[subKey as keyof typeof config.hostRelated] + // 例如: 'aliyun-cdn', 'aliyun-oss' + const fullKey = `${key}-${subKey}` + if (fullKey) { + typeNamesMap[fullKey] = subConfig?.name?.toString() ?? '' + iconFileMap[fullKey] = config.icon // hostRelated 项通常使用其父配置的图标 + } + } + } + } + } +} + +// 处理 MessagePushConfig +for (const key in MessagePushConfig) { + if (Object.prototype.hasOwnProperty.call(MessagePushConfig, key)) { + const config = MessagePushConfig[key as keyof typeof MessagePushConfig] + typeNamesMap[key] = config.name + // MessagePushConfig 中的 'type' 字段用作图标文件名 + iconFileMap[key] = config.type + notifyKeys.add(key) // 标记为通知类型 + } +} + +// 根据原始组件逻辑,应用特定的图标覆盖 +// 例如:如果 'btwaf' 需要强制使用 'btpanel' 图标,即使其在 ApiProjectConfig.btwaf.icon 中有其他定义 +if (ApiProjectConfig.btwaf) { + iconFileMap['btwaf'] = 'btpanel' // 确保 btwaf 使用宝塔面板图标 +} + +/** + * @function useAuthApiTypeIconController + * @description AuthApiTypeIcon 组件的控制器逻辑。 + * @param props - 组件的 props。 + * @returns {AuthApiTypeIconControllerExposes} 控制器暴露给视图的数据和方法。 + */ +export function useAuthApiTypeIconController(props: AuthApiTypeIconProps): AuthApiTypeIconControllerExposes { + /** + * @computed iconPath + * @description 根据 props.icon 计算 SvgIcon 所需的图标名称。 + */ + const iconPath = computed(() => { + const isNotify = notifyKeys.has(props.icon) + const RESOURCE_PREFIX = isNotify ? 'notify-' : 'resources-' + // 从映射表中获取图标文件名,如果找不到则使用 'default' + const iconStem = iconFileMap[props.icon] || 'default' + return RESOURCE_PREFIX + iconStem + }) + + /** + * @computed typeName + * @description 根据 props.icon 获取对应的显示名称。 + */ + const typeName = computed(() => { + // 从映射表中获取显示名称,如果找不到则直接使用 props.icon + return typeNamesMap[props.icon] || props.icon + }) + + return { + iconPath, + typeName, + } +} diff --git a/frontend/apps/allin-ssl/src/config/data.tsx b/frontend/apps/allin-ssl/src/config/data.tsx new file mode 100644 index 0000000..a737925 --- /dev/null +++ b/frontend/apps/allin-ssl/src/config/data.tsx @@ -0,0 +1,189 @@ +import { $t } from '@locales/index' + +// 消息推送类型 +export interface MessagePushType { + name: string + type: string +} + +// 定义ApiProject接口,包含可选的notApi属性 +export interface ApiProjectType { + name: string + icon: string + type?: string[] + notApi?: boolean + hostRelated?: Record + sort?: number +} + +// $t('t_0_1747886301644') +export const MessagePushConfig = { + mail: { name: $t('t_68_1745289354676'), type: 'mail' }, + wecom: { name: $t('t_33_1746773350932'), type: 'wecom' }, + dingtalk: { name: $t('t_32_1746773348993'), type: 'dingtalk' }, + feishu: { name: $t('t_34_1746773350153'), type: 'feishu' }, + webhook: { name: 'WebHook', type: 'webhook' }, +} + +// CA证书授权 +export const CACertificateAuthorization = { + zerossl: { name: 'ZeroSSL', type: 'zerossl' }, + google: { name: 'Google', type: 'google' }, + sslcom: { name: 'SSL.COM', type: 'sslcom' }, +} + +// 授权API管理 +// 结构说明:{name: '名称', icon: '图标', type: ['类型'], notApi: 是否需要API,默认需要, hostRelated: { default: { name: '默认' } }, sort: 排序} +export const ApiProjectConfig: Record = { + localhost: { + name: $t('t_4_1744958838951'), + icon: 'ssh', + type: ['host'], + notApi: false, + hostRelated: { default: { name: $t('t_4_1744958838951') } }, + sort: 1, + }, + ssh: { + name: 'SSH', + icon: 'ssh', + type: ['host'], + hostRelated: { default: { name: 'SSH' } }, + sort: 2, + }, + btpanel: { + name: $t('t_10_1745735765165'), + icon: 'btpanel', + hostRelated: { + default: { name: $t('t_10_1745735765165') }, + site: { name: $t('t_1_1747886307276') }, + dockersite: { name: $t('t_0_1747994891459') }, + }, + type: ['host'], + sort: 3, + }, + btwaf: { + name: $t('t_3_1747886302848'), + icon: 'btwaf', + hostRelated: { site: { name: $t('t_4_1747886303229') } }, + type: ['host'], + sort: 4, + }, + '1panel': { + name: '1Panel', + icon: '1panel', + hostRelated: { default: { name: '1Panel' }, site: { name: $t('t_2_1747886302053') } }, + type: ['host'], + sort: 5, + }, + aliyun: { + name: $t('t_2_1747019616224'), + icon: 'aliyun', + type: ['host', 'dns'], + hostRelated: { + cdn: { name: $t('t_16_1745735766712') }, + oss: { name: $t('t_2_1746697487164') }, + waf: { name: $t('t_10_1744958860078') }, + }, + sort: 6, + }, + tencentcloud: { + name: $t('t_3_1747019616129'), + icon: 'tencentcloud', + type: ['host', 'dns'], + hostRelated: { + cdn: { name: $t('t_14_1745735766121') }, + cos: { name: $t('t_15_1745735768976') }, + waf: { name: $t('t_9_1744958840634') }, + teo: { name: $t('t_5_1747886301427') }, + }, + sort: 7, + }, + safeline: { + name: $t('t_11_1747886301986'), + icon: 'safeline', + type: ['host'], + hostRelated: { panel: { name: $t('t_1_1747298114192') }, site: { name: $t('t_12_1747886302725') } }, + sort: 8, + }, + qiniu: { + name: $t('t_6_1747886301844'), + icon: 'qiniu', + type: ['host'], + hostRelated: { cdn: { name: $t('t_7_1747886302395') }, oss: { name: $t('t_8_1747886304014') } }, + sort: 9, + }, + huaweicloud: { + name: $t('t_9_1747886301128'), + icon: 'huaweicloud', + type: ['dns', 'host'], + hostRelated: { + cdn: { name: $t('t_9_1747886301128') + 'CDN' }, + }, + sort: 10, + }, + baidu: { + name: $t('t_10_1747886300958'), + icon: 'baidu', + type: ['host', 'dns'], + hostRelated: { + cdn: { name: '百度云CDN' }, + }, + sort: 11, + }, + cloudflare: { + name: 'Cloudflare', + icon: 'cloudflare', + type: ['dns'], + sort: 12, + }, + volcengine: { + name: $t('t_13_1747886301689'), + icon: 'volcengine', + type: ['dns'], + sort: 13, + }, + westcn: { + name: $t('t_14_1747886301884'), + icon: 'westcn', + type: ['dns'], + sort: 14, + }, + godaddy: { + name: 'GoDaddy', + icon: 'godaddy', + type: ['dns'], + sort: 15, + }, + namecheap: { + name: 'Namecheap', + icon: 'namecheap', + type: ['dns'], + sort: 16, + }, + ns1: { + name: 'NS1', + icon: 'ns1', + type: ['dns'], + sort: 17, + }, + cloudns: { + name: 'ClouDNS', + icon: 'cloudns', + type: ['dns'], + sort: 18, + }, + aws: { + name: 'AWS', + icon: 'aws', + type: ['dns'], + sort: 19, + }, + azure: { + name: 'Azure', + icon: 'azure', + type: ['dns'], + sort: 20, + }, +} + + diff --git a/frontend/apps/allin-ssl/src/config/route.tsx b/frontend/apps/allin-ssl/src/config/route.tsx new file mode 100644 index 0000000..019e069 --- /dev/null +++ b/frontend/apps/allin-ssl/src/config/route.tsx @@ -0,0 +1,16 @@ +import { $t } from '@locales/index' + +export default { + sortRoute: [ + { name: 'home', title: $t('t_0_1744258111441') }, + { name: 'autoDeploy', title: $t('t_1_1744258113857') }, + { name: 'certManage', title: $t('t_2_1744258111238') }, + { name: 'certApply', title: $t('t_3_1744258111182') }, + { name: 'authApiManage', title: $t('t_4_1744258111238') }, + { name: 'monitor', title: $t('t_5_1744258110516') }, + { name: 'settings', title: $t('t_6_1744258111153') }, + ], // 路由排序 + frameworkRoute: ['layout'], // 框架路由 + systemRoute: ['login', '404'], // 系统路由 + disabledRoute: [], // 禁用路由 +} diff --git a/frontend/apps/allin-ssl/src/lib/directive.tsx b/frontend/apps/allin-ssl/src/lib/directive.tsx new file mode 100644 index 0000000..9bdfb94 --- /dev/null +++ b/frontend/apps/allin-ssl/src/lib/directive.tsx @@ -0,0 +1,33 @@ +import { App, Directive } from 'vue' + +/** + * 移除输入框中的空格 + * 用法:v-nospace + */ +export const vNospace: Directive = { + mounted(el: HTMLElement) { + el.addEventListener('input', (event: Event) => { + const inputElement = event.target as HTMLInputElement + const newValue = inputElement.value.replace(/\s+/g, '') + + // 直接设置输入元素的值 + if (inputElement.value !== newValue) { + inputElement.value = newValue + // 触发自定义事件,通知父组件值已更改 + el.dispatchEvent(new Event('input', { bubbles: true })) + } + }) + }, +} + +// 导出所有指令的集合,方便批量注册 +export const directives = { + nospace: vNospace, +} + +// 注册所有指令 +export const useDirectives = (app: App, directives: Record) => { + Object.entries(directives).forEach(([key, value]) => { + app.directive(key, value) + }) +} diff --git a/frontend/apps/allin-ssl/src/lib/index.tsx b/frontend/apps/allin-ssl/src/lib/index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/apps/allin-ssl/src/lib/utils.tsx b/frontend/apps/allin-ssl/src/lib/utils.tsx new file mode 100644 index 0000000..8db0f84 --- /dev/null +++ b/frontend/apps/allin-ssl/src/lib/utils.tsx @@ -0,0 +1,20 @@ +/** + * @description 移除输入框中的空格 + * @param {string} value 输入框的值 + * @returns {boolean} 是否为空 + */ +export const noSideSpace = (value: string) => { + return !value.startsWith(' ') && !value.endsWith(' ') +} + +/** + * @description 数字验证 + * @param {string} value 输入框的值 + * @returns {boolean} 是否为数字 + */ +export const onlyAllowNumber = (value: string) => { + return !value || /^\d+$/.test(value) +} + + + diff --git a/frontend/apps/allin-ssl/src/locales/index.ts b/frontend/apps/allin-ssl/src/locales/index.ts new file mode 100644 index 0000000..a56ffd2 --- /dev/null +++ b/frontend/apps/allin-ssl/src/locales/index.ts @@ -0,0 +1,18 @@ +// 自动生成的i18n入口文件 +// 自动生成的i18n入口文件 +import { useLocale } from '@baota/i18n' +import zhCN from './model/zhCN.json' +import enUS from './model/enUS.json' + +// 使用 i18n 插件 +export const { i18n, $t, locale, localeOptions } = useLocale( + { + messages: { zhCN, enUS }, + locale: 'zhCN', + fileExt: 'json' + }, + import.meta.glob([`./model/*.json`], { + eager: false, + }), +) + diff --git a/frontend/apps/allin-ssl/src/locales/model/arDZ.json b/frontend/apps/allin-ssl/src/locales/model/arDZ.json new file mode 100644 index 0000000..8bad316 --- /dev/null +++ b/frontend/apps/allin-ssl/src/locales/model/arDZ.json @@ -0,0 +1,628 @@ +{ + "t_0_1744098811152": "تحذير: لقد دخلتم منطقة غير معروفة، الصفحة التي تحاول زيارتها غير موجودة، يرجى الضغط على الزر للعودة إلى الصفحة الرئيسية.", + "t_1_1744098801860": "رجوع إلى الصفحة الرئيسية", + "t_2_1744098804908": "نصيحة أمنية: إذا كنت تعتقد أن هذا خطأ، يرجى الاتصال بالمدير على الفور", + "t_3_1744098802647": "افتح القائمة الرئيسية", + "t_4_1744098802046": "القائمة الرئيسية القابلة للطي", + "t_1_1744164835667": "AllinSSL", + "t_2_1744164839713": "دخول الحساب", + "t_3_1744164839524": "من فضلك أدخل اسم المستخدم", + "t_4_1744164840458": "من فضلك أدخل كلمة المرور", + "t_5_1744164840468": "تذكر كلمة المرور", + "t_6_1744164838900": "هل نسيت كلمة المرور؟", + "t_7_1744164838625": "في إجراء الدخول", + "t_8_1744164839833": "تسجيل الدخول", + "t_0_1744258111441": "الصفحة الرئيسية", + "t_1_1744258113857": "توزيع آلي", + "t_2_1744258111238": "إدارة الشهادات", + "t_3_1744258111182": "طلب شهادة", + "t_4_1744258111238": "إدارة API التصريح", + "t_5_1744258110516": "مراقبة", + "t_6_1744258111153": "إعدادات", + "t_0_1744861190562": "إرجاع قائمة عملية العمل", + "t_1_1744861189113": "تشغيل", + "t_2_1744861190040": "حفظ", + "t_3_1744861190932": "أختر عقدة لتكوينها", + "t_4_1744861194395": "انقر على النقطة في الشريحة اليسرى من مخطط العمل لتزويده بالتكوين", + "t_5_1744861189528": "تبدأ", + "t_6_1744861190121": "لم يتم اختيار العقدة", + "t_7_1744861189625": "تم حفظ الإعدادات", + "t_8_1744861189821": "بدء عملية العمل", + "t_9_1744861189580": "النقطة المختارة:", + "t_0_1744870861464": "نقطة", + "t_1_1744870861944": "إعداد العقدة", + "t_2_1744870863419": "يرجى اختيار العقدة اليسرى للتكوين", + "t_3_1744870864615": "لم يتم العثور على مكون التكوين لهذا النوع من العقد", + "t_4_1744870861589": "إلغاء", + "t_5_1744870862719": "تحديد", + "t_0_1744875938285": "كل دقيقة", + "t_1_1744875938598": "كل ساعة", + "t_2_1744875938555": "كل يوم", + "t_3_1744875938310": "كل شهر", + "t_4_1744875940750": "تنفيذ تلقائي", + "t_5_1744875940010": "تنفيذ يدوي", + "t_0_1744879616135": "اختبار PID", + "t_1_1744879616555": "الرجاء إدخال PID الاختباري", + "t_2_1744879616413": "فترة التنفيذ", + "t_3_1744879615723": "دقيقة", + "t_4_1744879616168": "من فضلك، أدخل الدقائق", + "t_5_1744879615277": "ساعة", + "t_6_1744879616944": "الرجاء إدخال الساعات", + "t_7_1744879615743": "التاريخ", + "t_8_1744879616493": "اختر التاريخ", + "t_0_1744942117992": "كل أسبوع", + "t_1_1744942116527": "الإثنين", + "t_2_1744942117890": "الثلاثاء", + "t_3_1744942117885": "الأربعاء", + "t_4_1744942117738": "الخميس", + "t_5_1744942117167": "الجمعة", + "t_6_1744942117815": "السبت", + "t_7_1744942117862": "الأحد", + "t_0_1744958839535": "الرجاء إدخال اسم النطاق", + "t_1_1744958840747": "الرجاء إدخال بريدك الإلكتروني", + "t_2_1744958840131": "تنسيق البريد الإلكتروني غير صحيح", + "t_3_1744958840485": "يرجى اختيار مزود DNS للإذن", + "t_4_1744958838951": "تثبيت محلي", + "t_5_1744958839222": "تثبيت SSH", + "t_6_1744958843569": "لوحة بوتا/1 لوحة (تثبيت في شهادة لوحة)", + "t_7_1744958841708": "1 panel (تثبيت على المشروع المحدد لل موقع)", + "t_8_1744958841658": "تencent Cloud CDN/أليCloud CDN", + "t_9_1744958840634": "WAF من Tencent Cloud", + "t_10_1744958860078": "WAF من آليكلاود", + "t_11_1744958840439": "هذا الشهادة المطلوبة تلقائيًا", + "t_12_1744958840387": "قائمة الشهادات الاختيارية", + "t_13_1744958840714": "PEM (*.pem, *.crt, *.key)", + "t_14_1744958839470": "PFX (*.pfx)", + "t_15_1744958840790": "JKS (*.jks)", + "t_16_1744958841116": "POSIX bash (Linux/macOS)", + "t_17_1744958839597": "CMD (Windows)", + "t_18_1744958839895": "PowerShell (Windows)", + "t_19_1744958839297": "شهادة1", + "t_20_1744958839439": "شهادة 2", + "t_21_1744958839305": "خادم 1", + "t_22_1744958841926": "خادم 2", + "t_23_1744958838717": "اللوحة 1", + "t_29_1744958838904": "يوم", + "t_30_1744958843864": "تنسيق الشهادة غير صحيح، يرجى التحقق مما إذا كان يحتوي على العناصر التوضيحية للعناوين والرؤوس الكاملة", + "t_31_1744958844490": "شكل المفتاح الخاص غير صحيح، يرجى التحقق من أن يحتوي على معرف الرأس والساقطة الكاملة للمفتاح الخاص", + "t_0_1745215914686": "اسم التلقائية", + "t_2_1745215915397": "تلقائي", + "t_3_1745215914237": "يدوي", + "t_4_1745215914951": "حالة نشطة", + "t_5_1745215914671": "تفعيل", + "t_6_1745215914104": "إيقاف", + "t_7_1745215914189": "وقت الإنشاء", + "t_8_1745215914610": "عملية", + "t_9_1745215914666": "تاريخ التنفيذ", + "t_10_1745215914342": "تنفيذ", + "t_11_1745215915429": "تعديل", + "t_12_1745215914312": "حذف", + "t_13_1745215915455": "تنفيذ مسار العمل", + "t_14_1745215916235": "نجاح تنفيذ عملية العمل", + "t_15_1745215915743": "فشل تنفيذ عملية العمل", + "t_16_1745215915209": "حذف مسار العمل", + "t_17_1745215915985": "نجاح عملية حذف العملية", + "t_18_1745215915630": "فشل حذف مسار العمل", + "t_1_1745227838776": "الرجاء إدخال اسم الت automatization", + "t_2_1745227839794": "هل أنت متأكد من أنك تريد تنفيذ عملية {name}؟", + "t_3_1745227841567": "هل تؤكد على حذف {name} مسار العمل؟ هذه العملية لا يمكن إلغاؤها.", + "t_4_1745227838558": "وقت التنفيذ", + "t_5_1745227839906": "وقت الانتهاء", + "t_6_1745227838798": "طريقة التنفيذ", + "t_7_1745227838093": "الحالة", + "t_8_1745227838023": "نجاح", + "t_9_1745227838305": "فشل", + "t_10_1745227838234": "في تنفيذ", + "t_11_1745227838422": "غير معروف", + "t_12_1745227838814": "تفاصيل", + "t_13_1745227838275": "تحميل شهادة", + "t_14_1745227840904": "الرجاء إدخال اسم نطاق الشهادة أو اسم العلامة التجارية للبحث عنها", + "t_15_1745227839354": "معا", + "t_16_1745227838930": "شريحة", + "t_17_1745227838561": "اسم النطاق", + "t_18_1745227838154": "العلامة التجارية", + "t_19_1745227839107": "أيام متبقية", + "t_20_1745227838813": "زمن انتهاء الصلاحية", + "t_21_1745227837972": "مصدر", + "t_22_1745227838154": "طلب تلقائي", + "t_23_1745227838699": "تحميل يدوي", + "t_24_1745227839508": "إضافة وقت", + "t_25_1745227838080": "تحميل", + "t_27_1745227838583": "قريب من انتهاء الصلاحية", + "t_28_1745227837903": "طبيعي", + "t_29_1745227838410": "حذف الشهادة", + "t_30_1745227841739": "هل أنت متأكد من أنك تريد حذف هذا الشهادة؟ لا يمكن استعادة هذه العملية.", + "t_31_1745227838461": "تأكيد", + "t_32_1745227838439": "اسم الشهادة", + "t_33_1745227838984": "الرجاء إدخال اسم الشهادة", + "t_34_1745227839375": "محتويات الشهادة (PEM)", + "t_35_1745227839208": "الرجاء إدخال محتويات الشهادة", + "t_36_1745227838958": "محتويات المفتاح الخاص (KEY)", + "t_37_1745227839669": "الرجاء إدخال محتويات المفتاح الخاص", + "t_38_1745227838813": "فشل التحميل", + "t_39_1745227838696": "فشل التحميل", + "t_40_1745227838872": "فشل الحذف", + "t_0_1745289355714": "إضافة API للإذن", + "t_1_1745289356586": "الرجاء إدخال اسم أو نوع API المصرح به", + "t_2_1745289353944": "اسم", + "t_3_1745289354664": "نوع API للاذن", + "t_4_1745289354902": "API للتحرير المسموح به", + "t_5_1745289355718": "حذف API التحقق من الصلاحيات", + "t_6_1745289358340": "هل أنت متأكد من أنك تريد حذف هذا API المصرح به؟ لا يمكن استعادة هذا الإجراء.", + "t_7_1745289355714": "فشل الإضافة", + "t_8_1745289354902": "فشل التحديث", + "t_9_1745289355714": "انتهت صلاحيته {days} يوم", + "t_10_1745289354650": "إدارة المراقبة", + "t_11_1745289354516": "إضافة المراقبة", + "t_12_1745289356974": "الرجاء إدخال اسم المراقبة أو اسم النطاق للبحث عنه", + "t_13_1745289354528": "اسم المراقب", + "t_14_1745289354902": "اسم المجال للمستند", + "t_15_1745289355714": "جهة إصدار الشهادات", + "t_16_1745289354902": "حالة الشهادة", + "t_17_1745289355715": "تاريخ انتهاء صلاحية الشهادة", + "t_18_1745289354598": "قنوات التحذير", + "t_19_1745289354676": "تاريخ آخر فحص", + "t_20_1745289354598": "تعديل الرقابة", + "t_21_1745289354598": "تأكيد الحذف", + "t_22_1745289359036": "لا يمكن استعادة العناصر بعد الحذف. هل أنت متأكد من أنك تريد حذف هذا المراقب؟", + "t_23_1745289355716": "فشل التعديل", + "t_24_1745289355715": "فشل في الإعداد", + "t_25_1745289355721": "من فضلك، أدخل رمز التحقق", + "t_26_1745289358341": "فشل التحقق من النموذج، يرجى التحقق من المحتويات المملوءة", + "t_27_1745289355721": "من فضلك أدخل اسم API المصرح به", + "t_28_1745289356040": "يرجى اختيار نوع API الت�权يز", + "t_29_1745289355850": "الرجاء إدخال عنوان IP للخادم", + "t_30_1745289355718": "من فضلك، أدخل ميناء SSH", + "t_31_1745289355715": "من فضلك أدخل مفتاح SSH", + "t_32_1745289356127": "الرجاء إدخال عنوان بوتا", + "t_33_1745289355721": "الرجاء إدخال مفتاح API", + "t_34_1745289356040": "الرجاء إدخال عنوان 1panel", + "t_35_1745289355714": "من فضلك أدخل AccessKeyId", + "t_36_1745289355715": "من فضلك، أدخل AccessKeySecret", + "t_37_1745289356041": "من فضلك، أدخل SecretId", + "t_38_1745289356419": "من فضلك أدخل مفتاح السر", + "t_39_1745289354902": "نجاح التحديث", + "t_40_1745289355715": "نجاح الإضافة", + "t_41_1745289354902": "نوع", + "t_42_1745289355715": "IP del serveur", + "t_43_1745289354598": "منفذ SSH", + "t_44_1745289354583": "اسم المستخدم", + "t_45_1745289355714": "طريقة التحقق", + "t_46_1745289355723": "تأكيد البصمة البصرية", + "t_47_1745289355715": "تأكيد البصمة", + "t_48_1745289355714": "كلمة المرور", + "t_49_1745289355714": "مفتاح خاص SSH", + "t_50_1745289355715": "الرجاء إدخال مفتاح SSH الخاص", + "t_51_1745289355714": "كلمة المرور الخاصة بالمفتاح الخاص", + "t_52_1745289359565": "إذا كانت المفتاح الخاص يحتوي على كلمة مرور، أدخلها", + "t_53_1745289356446": "عنوان واجهة بوتا", + "t_54_1745289358683": "من فضلك أدخل عنوان لوحة بوتا، مثل: https://bt.example.com", + "t_55_1745289355715": "مفتاح API", + "t_56_1745289355714": "عنوان اللوحة 1", + "t_57_1745289358341": "ادخل عنوان 1panel، مثلًا: https://1panel.example.com", + "t_58_1745289355721": "ادخل معرف AccessKey", + "t_59_1745289356803": "من فضلك ادخل سرية مفتاح الوصول", + "t_60_1745289355715": "الرجاء إدخال اسم المراقبة", + "t_61_1745289355878": "الرجاء إدخال اسم النطاق/IP", + "t_62_1745289360212": "يرجى اختيار فترة التحقق", + "t_63_1745289354897": "5 دقائق", + "t_64_1745289354670": "10 دقائق", + "t_65_1745289354591": "15 دقيقة", + "t_66_1745289354655": "30 دقيقة", + "t_67_1745289354487": "60 دقيقة", + "t_68_1745289354676": "بريد إلكتروني", + "t_69_1745289355721": "رسالة قصيرة", + "t_70_1745289354904": "واتساب", + "t_71_1745289354583": "اسم النطاق/IP", + "t_72_1745289355715": "فترة التحقق", + "t_73_1745289356103": "يرجى اختيار قناة التحذير", + "t_0_1745289808449": "الرجاء إدخال اسم API المصرح به", + "t_0_1745294710530": "حذف المراقبة", + "t_0_1745295228865": "زمن التحديث", + "t_0_1745317313835": "تنسيق عنوان IP للخادم غير صحيح", + "t_1_1745317313096": "خطأ في تنسيق المنفذ", + "t_2_1745317314362": "خطأ في صيغة عنوان URL للوحة", + "t_3_1745317313561": "الرجاء إدخال مفتاح API لوحة التحكم", + "t_4_1745317314054": "الرجاء إدخال AccessKeyId لـ Aliyun", + "t_5_1745317315285": "الرجاء إدخال AccessKeySecret لـ Aliyun", + "t_6_1745317313383": "الرجاء إدخال SecretId لتencent cloud", + "t_7_1745317313831": "من فضلك أدخل SecretKey Tencent Cloud", + "t_0_1745457486299": "ممكّن", + "t_1_1745457484314": "توقف", + "t_2_1745457488661": "التبديل إلى الوضع اليدوي", + "t_3_1745457486983": "التبديل إلى الوضع التلقائي", + "t_4_1745457497303": "بعد التبديل إلى الوضع اليدوي، لن يتم تنفيذ سير العمل تلقائيًا، ولكن لا يزال يمكن تنفيذه يدويًا", + "t_5_1745457494695": "بعد التبديل إلى الوضع التلقائي، سيعمل سير العمل تلقائيًا وفقًا للوقت المحدد", + "t_6_1745457487560": "إغلاق سير العمل الحالي", + "t_7_1745457487185": "تمكين سير العمل الحالي", + "t_8_1745457496621": "بعد الإغلاق، لن يتم تنفيذ سير العمل تلقائيًا ولن يمكن تنفيذه يدويًا. هل تريد المتابعة؟", + "t_9_1745457500045": "بعد التمكين، سيتم تنفيذ تكوين سير العمل تلقائيًا أو يدويًا. متابعة؟", + "t_10_1745457486451": "فشل إضافة سير العمل", + "t_11_1745457488256": "فشل في تعيين طريقة تنفيذ سير العمل", + "t_12_1745457489076": "تمكين أو تعطيل فشل سير العمل", + "t_13_1745457487555": "فشل تنفيذ سير العمل", + "t_14_1745457488092": "فشل في حذف سير العمل", + "t_15_1745457484292": "خروج", + "t_16_1745457491607": "أنت على وشك تسجيل الخروج. هل أنت متأكد أنك تريد الخروج؟", + "t_17_1745457488251": "جاري تسجيل الخروج، يرجى الانتظار...", + "t_18_1745457490931": "إضافة إشعار عبر البريد الإلكتروني", + "t_19_1745457484684": "تم الحفظ بنجاح", + "t_20_1745457485905": "تم الحذف بنجاح", + "t_0_1745464080226": "فشل الحصول على إعدادات النظام", + "t_1_1745464079590": "فشل حفظ الإعدادات", + "t_2_1745464077081": "فشل الحصول على إعدادات الإشعار", + "t_3_1745464081058": "فشل حفظ إعدادات الإشعار", + "t_4_1745464075382": "فشل في الحصول على قائمة قنوات الإخطار", + "t_5_1745464086047": "فشل إضافة قناة إشعار البريد الإلكتروني", + "t_6_1745464075714": "فشل تحديث قناة الإشعارات", + "t_7_1745464073330": "فشل حذف قناة الإشعار", + "t_8_1745464081472": "فشل التحقق من تحديث النسخة", + "t_9_1745464078110": "حفظ الإعدادات", + "t_10_1745464073098": "الإعدادات الأساسية", + "t_0_1745474945127": "اختر نموذج", + "t_0_1745490735213": "الرجاء إدخال اسم سير العمل", + "t_1_1745490731990": "إعدادات", + "t_2_1745490735558": "يرجى إدخال البريد الإلكتروني", + "t_3_1745490735059": "يرجى اختيار موفر DNS", + "t_4_1745490735630": "الرجاء إدخال فاصل التجديد", + "t_5_1745490738285": "الرجاء إدخال اسم النطاق، لا يمكن أن يكون اسم النطاق فارغًا", + "t_6_1745490738548": "الرجاء إدخال البريد الإلكتروني، لا يمكن أن يكون البريد الإلكتروني فارغًا", + "t_7_1745490739917": "الرجاء اختيار موفر DNS، لا يمكن أن يكون موفر DNS فارغًا", + "t_8_1745490739319": "الرجاء إدخال فترة التجديد، فترة التجديد لا يمكن أن تكون فارغة", + "t_1_1745553909483": "تنسيق البريد الإلكتروني غير صحيح، يرجى إدخال بريد صحيح", + "t_2_1745553907423": "لا يمكن أن يكون فاصل التجديد فارغًا", + "t_0_1745735774005": "الرجاء إدخال اسم نطاق الشهادة، أسماء نطاقات متعددة مفصولة بفواصل", + "t_1_1745735764953": "صندوق البريد", + "t_2_1745735773668": "الرجاء إدخال البريد الإلكتروني لتلقي إشعارات من سلطة الشهادات", + "t_3_1745735765112": "موفر DNS", + "t_4_1745735765372": "إضافة", + "t_5_1745735769112": "فترة التجديد (أيام)", + "t_6_1745735765205": "فترة التجديد", + "t_7_1745735768326": "يوم، يتم التجديد تلقائيًا عند الانتهاء", + "t_8_1745735765753": "تم التكوين", + "t_9_1745735765287": "غير مهيأ", + "t_10_1745735765165": "لوحة باغودة", + "t_11_1745735766456": "موقع لوحة باغودا", + "t_12_1745735765571": "لوحة 1Panel", + "t_13_1745735766084": "1Panel موقع إلكتروني", + "t_14_1745735766121": "تنسنت كلاود CDN", + "t_15_1745735768976": "تنسنت كلاود كوس", + "t_16_1745735766712": "ألي بابا كلاود CDN", + "t_18_1745735765638": "نوع النشر", + "t_19_1745735766810": "يرجى اختيار نوع النشر", + "t_20_1745735768764": "الرجاء إدخال مسار النشر", + "t_21_1745735769154": "الرجاء إدخال الأمر البادئة", + "t_22_1745735767366": "الرجاء إدخال الأمر اللاحق", + "t_24_1745735766826": "يرجى إدخال معرف الموقع", + "t_25_1745735766651": "الرجاء إدخال المنطقة", + "t_26_1745735767144": "الرجاء إدخال الحاوية", + "t_27_1745735764546": "الخطوة التالية", + "t_28_1745735766626": "اختر نوع النشر", + "t_29_1745735768933": "تكوين معلمات النشر", + "t_30_1745735764748": "وضع التشغيل", + "t_31_1745735767891": "وضع التشغيل غير مُهيأ", + "t_32_1745735767156": "دورة التشغيل غير مهيأة", + "t_33_1745735766532": "وقت التشغيل غير مضبوط", + "t_34_1745735771147": "ملف الشهادة (تنسيق PEM)", + "t_35_1745735781545": "الرجاء لصق محتوى ملف الشهادة، على سبيل المثال:\n-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----", + "t_36_1745735769443": "ملف المفتاح الخاص (تنسيق KEY)", + "t_37_1745735779980": "الصق محتوى ملف المفتاح الخاص، على سبيل المثال:\n-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----", + "t_38_1745735769521": "محتوى المفتاح الخاص للشهادة لا يمكن أن يكون فارغًا", + "t_39_1745735768565": "تنسيق مفتاح الشهادة الخاص غير صحيح", + "t_40_1745735815317": "محتوى الشهادة لا يمكن أن يكون فارغا", + "t_41_1745735767016": "تنسيق الشهادة غير صحيح", + "t_0_1745738961258": "السابق", + "t_1_1745738963744": "إرسال", + "t_2_1745738969878": "تكوين معلمات النشر، النوع يحدد تكوين المعلمة", + "t_0_1745744491696": "مصدر جهاز النشر", + "t_1_1745744495019": "الرجاء اختيار مصدر جهاز التوزيع", + "t_2_1745744495813": "الرجاء اختيار نوع النشر والنقر فوق التالي", + "t_0_1745744902975": "مصدر النشر", + "t_1_1745744905566": "الرجاء اختيار مصدر النشر", + "t_2_1745744903722": "إضافة المزيد من الأجهزة", + "t_0_1745748292337": "إضافة مصدر النشر", + "t_1_1745748290291": "مصدر الشهادة", + "t_2_1745748298902": "مصدر النشر للنوع الحالي فارغ، يرجى إضافة مصدر نشر أولاً", + "t_3_1745748298161": "لا توجد عقدة طلب في العملية الحالية، يرجى إضافة عقدة طلب أولاً", + "t_4_1745748290292": "إرسال المحتوى", + "t_0_1745765864788": "انقر لتحرير عنوان سير العمل", + "t_1_1745765875247": "حذف العقدة - 【{name}】", + "t_2_1745765875918": "العقدة الحالية تحتوي على عقد فرعية. حذفها سيؤثر على عقد أخرى. هل أنت متأكد أنك تريد الحذف؟", + "t_3_1745765920953": "العقدة الحالية تحتوي على بيانات التكوين، هل أنت متأكد أنك تريد حذفها؟", + "t_4_1745765868807": "الرجاء تحديد نوع النشر قبل المتابعة إلى الخطوة التالية", + "t_0_1745833934390": "يرجى اختيار النوع", + "t_1_1745833931535": "مضيف", + "t_2_1745833931404": "منفذ", + "t_3_1745833936770": "فشل في الحصول على بيانات نظرة عامة على الصفحة الرئيسية", + "t_4_1745833932780": "معلومات النسخة", + "t_5_1745833933241": "الإصدار الحالي", + "t_6_1745833933523": "طريقة التحديث", + "t_7_1745833933278": "أحدث إصدار", + "t_8_1745833933552": "سجل التغييرات", + "t_9_1745833935269": "رمز QR لخدمة العملاء", + "t_10_1745833941691": "امسح رمز QR لإضافة خدمة العملاء", + "t_11_1745833935261": "حساب وي تشات الرسمي", + "t_12_1745833943712": "امسح الكود الضوئي لمتابعة الحساب الرسمي على WeChat", + "t_13_1745833933630": "حول المنتج", + "t_14_1745833932440": "خادم SMTP", + "t_15_1745833940280": "الرجاء إدخال خادم SMTP", + "t_16_1745833933819": "منفذ SMTP", + "t_17_1745833935070": "الرجاء إدخال منفذ SMTP", + "t_18_1745833933989": "اتصال SSL/TLS", + "t_0_1745887835267": "الرجاء اختيار إشعار الرسالة", + "t_1_1745887832941": "إشعار", + "t_2_1745887834248": "إضافة قناة إشعار", + "t_3_1745887835089": "الرجاء إدخال موضوع الإشعار", + "t_4_1745887835265": "يرجى إدخال محتوى الإشعار", + "t_0_1745895057404": "تعديل إعدادات الإشعارات عبر البريد الإلكتروني", + "t_0_1745920566646": "موضوع الإشعار", + "t_1_1745920567200": "محتوى الإخطار", + "t_0_1745936396853": "انقر للحصول على رمز التحقق", + "t_0_1745999035681": "باقي {days} يوم", + "t_1_1745999036289": "قريباً تنتهي الصلاحية {days} يوم", + "t_0_1746000517848": "منتهي الصلاحية", + "t_0_1746001199409": "انتهت الصلاحية", + "t_0_1746004861782": "موفر DNS فارغ", + "t_1_1746004861166": "إضافة مزود DNS", + "t_0_1746497662220": "تحديث", + "t_0_1746519384035": "قيد التشغيل", + "t_0_1746579648713": "تفاصيل سجل التنفيذ", + "t_0_1746590054456": "حالة التنفيذ", + "t_1_1746590060448": "طريقة التشغيل", + "t_0_1746667592819": "جاري تقديم المعلومات، يرجى الانتظار...", + "t_1_1746667588689": "مفتاح", + "t_2_1746667592840": "عنوان URL للوحة", + "t_3_1746667592270": "تجاهل أخطاء شهادة SSL/TLS", + "t_4_1746667590873": "فشل التحقق من النموذج", + "t_5_1746667590676": "سير عمل جديد", + "t_6_1746667592831": "جارٍ تقديم الطلب، يرجى الانتظار...", + "t_7_1746667592468": "يرجى إدخال اسم النطاق الصحيح", + "t_8_1746667591924": "يرجى اختيار طريقة التحليل", + "t_9_1746667589516": "تحديث القائمة", + "t_10_1746667589575": "حرف بدل", + "t_11_1746667589598": "متعدد النطاقات", + "t_12_1746667589733": "شائع", + "t_13_1746667599218": "هو موفر شهادات SSL مجاني مستخدم على نطاق واسع، مناسب للمواقع الشخصية وبيئات الاختبار.", + "t_14_1746667590827": "عدد النطاقات المدعومة", + "t_15_1746667588493": "قطعة", + "t_16_1746667591069": "دعم أحرف البدل", + "t_17_1746667588785": "دعم", + "t_18_1746667590113": "غير مدعوم", + "t_19_1746667589295": "فترة الصلاحية", + "t_20_1746667588453": "يوم", + "t_21_1746667590834": "دعم البرامج الصغيرة", + "t_22_1746667591024": "المواقع المطبقة", + "t_23_1746667591989": "*.example.com، *.demo.com", + "t_24_1746667583520": "*.example.com", + "t_25_1746667590147": "example.com、demo.com", + "t_26_1746667594662": "www.example.com، example.com", + "t_27_1746667589350": "مجاني", + "t_28_1746667590336": "تقديم الآن", + "t_29_1746667589773": "عنوان المشروع", + "t_30_1746667591892": "الرجاء إدخال مسار ملف الشهادة", + "t_31_1746667593074": "الرجاء إدخال مسار ملف المفتاح الخاص", + "t_0_1746673515941": "موفر DNS الحالي فارغ، يرجى إضافة موفر DNS أولاً", + "t_0_1746676862189": "فشل إرسال إشعار الاختبار", + "t_1_1746676859550": "إضافة تكوين", + "t_2_1746676856700": "غير مدعوم بعد", + "t_3_1746676857930": "إشعار البريد الإلكتروني", + "t_4_1746676861473": "إرسال إخطارات التنبيه عبر البريد الإلكتروني", + "t_5_1746676856974": "إشعار DingTalk", + "t_6_1746676860886": "إرسال إشعارات الإنذار عبر روبوت DingTalk", + "t_7_1746676857191": "إشعار WeChat Work", + "t_8_1746676860457": "إرسال تنبيهات الإنذار عبر بوت WeCom", + "t_9_1746676857164": "إشعار Feishu", + "t_10_1746676862329": "إرسال إخطارات الإنذار عبر بوت Feishu", + "t_11_1746676859158": "إشعار WebHook", + "t_12_1746676860503": "إرسال إشعارات الإنذار عبر WebHook", + "t_13_1746676856842": "قناة الإخطار", + "t_14_1746676859019": "قنوات الإعلام المُهيأة", + "t_15_1746676856567": "معطل", + "t_16_1746676855270": "اختبار", + "t_0_1746677882486": "حالة التنفيذ الأخيرة", + "t_0_1746697487119": "اسم النطاق لا يمكن أن يكون فارغًا", + "t_1_1746697485188": "البريد الإلكتروني لا يمكن أن يكون فارغاً", + "t_2_1746697487164": "علي بابا كلاود OSS", + "t_0_1746754500246": "مزود الاستضافة", + "t_1_1746754499371": "مصدر API", + "t_2_1746754500270": "نوع API", + "t_0_1746760933542": "خطأ في الطلب", + "t_0_1746773350551": "مجموع {0}", + "t_1_1746773348701": "لم يتم التنفيذ", + "t_2_1746773350970": "سير العمل الآلي", + "t_3_1746773348798": "العدد الكلي", + "t_4_1746773348957": "فشل التنفيذ", + "t_5_1746773349141": "تنتهي قريبا", + "t_6_1746773349980": "مراقبة في الوقت الحقيقي", + "t_7_1746773349302": "كمية غير طبيعية", + "t_8_1746773351524": "سجلات تنفيذ سير العمل الحديثة", + "t_9_1746773348221": "عرض الكل", + "t_10_1746773351576": "لا توجد سجلات تنفيذ سير العمل", + "t_11_1746773349054": "إنشاء سير العمل", + "t_12_1746773355641": "انقر لإنشاء سير عمل آلي لتحسين الكفاءة", + "t_13_1746773349526": "التقدم بطلب للحصول على شهادة", + "t_14_1746773355081": "انقر للتقدم بطلب وإدارة شهادات SSL لضمان الأمان", + "t_16_1746773356568": "يمكن تكوين قناة إشعار واحدة فقط عبر البريد الإلكتروني كحد أقصى", + "t_17_1746773351220": "تأكيد قناة الإشعارات {0}", + "t_18_1746773355467": "ستبدأ قنوات الإشعار {0} في إرسال تنبيهات.", + "t_19_1746773352558": "قناة الإشعارات الحالية لا تدعم الاختبار", + "t_20_1746773356060": "يتم إرسال البريد الإلكتروني الاختباري، يرجى الانتظار...", + "t_21_1746773350759": "بريد إلكتروني تجريبي", + "t_22_1746773360711": "إرسال بريد إلكتروني اختباري إلى صندوق البريد الحالي المُهيأ، هل تتابع؟", + "t_23_1746773350040": "تأكيد الحذف", + "t_25_1746773349596": "الرجاء إدخال الاسم", + "t_26_1746773353409": "الرجاء إدخال منفذ SMTP الصحيح", + "t_27_1746773352584": "يرجى إدخال كلمة مرور المستخدم", + "t_28_1746773354048": "الرجاء إدخال البريد الإلكتروني الصحيح للمرسل", + "t_29_1746773351834": "الرجاء إدخال البريد الإلكتروني الصحيح", + "t_30_1746773350013": "بريد المرسل الإلكتروني", + "t_31_1746773349857": "تلقي البريد الإلكتروني", + "t_32_1746773348993": "دينغتالک", + "t_33_1746773350932": "WeChat Work", + "t_34_1746773350153": "فيشو", + "t_35_1746773362992": "أداة إدارة دورة حياة شهادات SSL متكاملة تشمل التقديم، الإدارة، النشر والمراقبة.", + "t_36_1746773348989": "طلب الشهادة", + "t_37_1746773356895": "دعم الحصول على شهادات من Let's Encrypt عبر بروتوكول ACME", + "t_38_1746773349796": "إدارة الشهادات", + "t_39_1746773358932": "الإدارة المركزية لجميع شهادات SSL، بما في ذلك الشهادات المرفوعة يدويًا والمطبقة تلقائيًا", + "t_40_1746773352188": "نشر الشهادة", + "t_41_1746773364475": "دعم نشر الشهادات بنقرة واحدة على منصات متعددة مثل علي بابا كلاود، تينسنت كلاود، لوحة باغودا، 1Panel، إلخ.", + "t_42_1746773348768": "مراقبة الموقع", + "t_43_1746773359511": "مراقبة حالة شهادات SSL للموقع في الوقت الفعلي للتحذير المسبق من انتهاء صلاحية الشهادة", + "t_44_1746773352805": "مهمة الأتمتة:", + "t_45_1746773355717": "يدعم المهام المجدولة، تجديد الشهادات تلقائياً ونشرها", + "t_46_1746773350579": "دعم متعدد المنصات", + "t_47_1746773360760": "يدعم طرق التحقق DNS لعدة موفري DNS (Alibaba Cloud، Tencent Cloud، إلخ)", + "t_0_1746773763967": "هل أنت متأكد أنك تريد حذف {0}، قناة الإشعارات؟", + "t_1_1746773763643": "Let's Encrypt وغيرها من الجهات المصدقة تطلب شهادات مجانية تلقائيًا", + "t_0_1746776194126": "تفاصيل السجل", + "t_1_1746776198156": "فشل تحميل السجل:", + "t_2_1746776194263": "تنزيل السجل", + "t_3_1746776195004": "لا توجد معلومات السجل", + "t_0_1746782379424": "المهام الآلية", + "t_0_1746858920894": "يرجى اختيار موفر الاستضافة", + "t_1_1746858922914": "قائمة موفري DNS فارغة، يرجى الإضافة", + "t_2_1746858923964": "قائمة مزودي الاستضافة فارغة، يرجى الإضافة", + "t_3_1746858920060": "إضافة مزود استضافة", + "t_4_1746858917773": "محدد", + "t_0_1747019621052": "الرجاء اختيار مزود استضافة{0}", + "t_1_1747019624067": "انقر لضبط مراقبة الموقع وفهم الحالة في الوقت الحقيقي", + "t_2_1747019616224": "علي بابا كلاود", + "t_3_1747019616129": "تينسنت كلاود", + "t_0_1747040228657": "للمجالات المتعددة، يرجى استخدام فواصل إنجليزية لفصلها، على سبيل المثال: test.com,test.cn", + "t_1_1747040226143": "للمجالات العامة، استخدم علامة النجمة *، على سبيل المثال: *.test.com", + "t_0_1747042966820": "الرجاء إدخال مفتاح Cloudflare API الصحيح", + "t_1_1747042969705": "يرجى إدخال مفتاح API الصحيح لـ BT-Panel", + "t_2_1747042967277": "الرجاء إدخال SecretKey الصحيح لـ Tencent Cloud", + "t_3_1747042967608": "الرجاء إدخال Huawei Cloud SecretKey الصحيح", + "t_4_1747042966254": "الرجاء إدخال مفتاح الوصول Huawei Cloud", + "t_5_1747042965911": "الرجاء إدخال حساب البريد الإلكتروني الصحيح", + "t_0_1747047213730": "إضافة النشر الآلي", + "t_1_1747047213009": "إضافة شهادة", + "t_2_1747047214975": "منصة إدارة شهادات SSL", + "t_3_1747047218669": "خطأ في تنسيق النطاق، يرجى التحقق من تنسيق النطاق", + "t_0_1747106957037": "خادم DNS العودي (اختياري)", + "t_1_1747106961747": "أدخل خوادم DNS العودية (استخدم الفواصل لفصل القيم المتعددة)", + "t_2_1747106957037": "تخطي الفحص المسبق المحلي", + "t_0_1747110184700": "اختيار الشهادة", + "t_1_1747110191587": "إذا كنت بحاجة إلى تعديل محتوى الشهادة والمفتاح، فاختر شهادة مخصصة", + "t_2_1747110193465": "عند اختيار شهادة غير مخصصة، لا يمكن تعديل محتوى الشهادة أو المفتاح", + "t_3_1747110185110": "تحميل وتقديم", + "t_0_1747215751189": "موقع ويف باغودا", + "t_0_1747271295174": "Pagoda WAF - خطأ في تنسيق URL", + "t_1_1747271295484": "الرجاء إدخال مفتاح Pagoda WAF-API", + "t_2_1747271295877": "الرجاء إدخال AccessKey الصحيح لـ Huawei Cloud", + "t_3_1747271294475": "يرجى إدخال Baidu Cloud AccessKey الصحيح", + "t_4_1747271294621": "الرجاء إدخال SecretKey الصحيح لـ Baidu Cloud", + "t_5_1747271291828": "باوتا WAF-URL", + "t_6_1747271296994": "النشر المحلي", + "t_7_1747271292060": "جميع المصادر", + "t_8_1747271290414": "باغودة", + "t_9_1747271284765": "1Panel", + "t_0_1747280814475": "تعديل منفذ SMTP ممنوع", + "t_1_1747280813656": "مسار ملف الشهادة (يدعم تنسيق PEM فقط)", + "t_2_1747280811593": "مسار ملف المفتاح الخاص", + "t_3_1747280812067": "أمر مسبق (اختياري)", + "t_4_1747280811462": "أمر لاحق (اختياري)", + "t_6_1747280809615": "معرّف الموقع", + "t_7_1747280808936": "منطقة", + "t_8_1747280809382": "دلو", + "t_9_1747280810169": "نشر متكرر", + "t_10_1747280816952": "عندما يكون الشهادة هي نفسها كما في النشر الأخير وكان النشر الأخير ناجحًا", + "t_11_1747280809178": "تخطي", + "t_12_1747280809893": "لا تتخطى", + "t_13_1747280810369": "إعادة النشر", + "t_14_1747280811231": "بحث نوع النشر", + "t_0_1747296173751": "اسم الموقع", + "t_1_1747296175494": "الرجاء إدخال اسم الموقع", + "t_0_1747298114839": "موقع Leichi WAF", + "t_1_1747298114192": "ليتشي WAF", + "t_0_1747300383756": "ليتشي WAF - خطأ في تنسيق عنوان URL", + "t_1_1747300384579": "الرجاء إدخال مفتاح BT-WAF الصحيح API", + "t_2_1747300385222": "الرجاء إدخال مفتاح Leichi WAF-API الصحيح", + "t_0_1747365600180": "الرجاء إدخال اسم المستخدم Western Digital", + "t_1_1747365603108": "الرجاء إدخال كلمة مرور ويسترن ديجيتال", + "t_3_1747365600828": "يرجى إدخال مفتاح الوصول AccessKey لمحرك Volcano", + "t_4_1747365600137": "الرجاء إدخال SecretKey لمحرك بركان", + "t_0_1747367069267": "موقع Pagoda docker", + "t_0_1747617113090": "الرجاء إدخال رمز API الخاص بـ Leichi", + "t_1_1747617105179": "API Token", + "t_0_1747647014927": "خوارزمية الشهادة", + "t_0_1747709067998": "الرجاء إدخال مفتاح SSH، المحتوى لا يمكن أن يكون فارغًا", + "t_0_1747711335067": "يرجى إدخال كلمة مرور SSH", + "t_1_1747711335336": "عنوان المضيف", + "t_2_1747711337958": "الرجاء إدخال عنوان المضيف، لا يمكن أن يكون فارغًا", + "t_0_1747754231151": "عارض السجلات", + "t_1_1747754231838": "من فضلك أولاً", + "t_2_1747754234999": "إذا كان لديك أي أسئلة أو اقتراحات، فلا تتردد في تقديمها", + "t_3_1747754232000": "يمكنك أيضًا العثور علينا على Github", + "t_4_1747754235407": "مشاركتك مهمة جدًا لـ AllinSSL، شكرًا لك.", + "t_0_1747817614953": "الرجاء إدخال", + "t_1_1747817639034": "نعم", + "t_2_1747817610671": "لا", + "t_3_1747817612697": "حقل العقدة مطلوب", + "t_4_1747817613325": "الرجاء إدخال اسم مجال صالح", + "t_5_1747817619337": "الرجاء إدخال اسم نطاق صالح، افصل بين عدة نطاقات بفاصلة باللغة الإنجليزية", + "t_6_1747817644358": "الرجاء إدخال عنوان البريد الإلكتروني الخاص بك", + "t_7_1747817613773": "الرجاء إدخال عنوان بريد إلكتروني صالح", + "t_8_1747817614764": "خطأ في العقدة", + "t_9_1747817611448": "النطاق:", + "t_10_1747817611126": "تقدم", + "t_11_1747817612051": "نشر", + "t_12_1747817611391": "رفع", + "t_0_1747886301644": "تكوين إرسال الرسائل", + "t_1_1747886307276": "لوحة باغودا - موقع الويب", + "t_2_1747886302053": "1Panel-موقع ويب", + "t_3_1747886302848": "باغودا WAF", + "t_4_1747886303229": "باغودة WAF-موقع الإنترنت", + "t_5_1747886301427": "Tencent Cloud EdgeOne", + "t_6_1747886301844": "تشينيو كلاود", + "t_7_1747886302395": "كينيو كلاود-CDN", + "t_8_1747886304014": "كيوينو كلاود - OSS", + "t_9_1747886301128": "هواوي كلاود", + "t_10_1747886300958": "بايدو كلود", + "t_11_1747886301986": "برق البركة", + "t_12_1747886302725": "ليتشي WAF-موقع ويب", + "t_13_1747886301689": "محرك البركان", + "t_14_1747886301884": "ويست ديجيتال", + "t_15_1747886301573": "نوع مشروع النشر", + "t_16_1747886308182": "هل أنت متأكد أنك تريد تحديث الصفحة؟ قد يتم فقدان البيانات!", + "t_0_1747895713179": "تنفيذ ناجح", + "t_1_1747895712756": "جارٍ التنفيذ", + "t_0_1747903670020": "إدارة التفويض CA", + "t_2_1747903672640": "تأكيد الحذف", + "t_3_1747903672833": "هل أنت متأكد أنك تريد حذف هذا التفويض CA؟", + "t_4_1747903685371": "إضافة تفويض CA", + "t_5_1747903671439": "الرجاء إدخال ACME EAB KID", + "t_6_1747903672931": "الرجاء إدخال مفتاح HMAC ACME EAB", + "t_7_1747903678624": "يرجى اختيار مزود CA", + "t_8_1747903675532": "الاسم المستعار المصرح به من قبل مزود CA الحالي للتعرف السريع", + "t_9_1747903669360": "موفر CA", + "t_10_1747903662994": "ACME EAB KID", + "t_11_1747903674802": "يرجى إدخال ACME EAB KID المقدم من قبل CA", + "t_12_1747903662994": "ACME EAB HMAC Key", + "t_13_1747903673007": "أدخل ACME EAM HMAC لموفر CA", + "t_0_1747904536291": "AllinSSL منصة مجانية ومفتوحة المصدر لإدارة الشهادات SSL تلقائيًا. تقديم طلبات تلقائية وتجديد ونشر ومراقبة جميع شهادات SSL/TLS بنقرة واحدة، تدعم بيئات متعددة السحابات وعدة جهات اعتماد (coding~)، قل وداعًا للإعدادات المعقدة والتكاليف الباهظة.", + "t_0_1747965909665": "الرجاء إدخال البريد الإلكتروني لربط تفويض CA", + "t_0_1747969933657": "نشر المحطة الطرفية", + "t_0_1747984137443": "الرجاء إدخال مفتاح API الصحيح لـ GoDaddy", + "t_1_1747984133312": "يرجى إدخال سر API GoDaddy", + "t_2_1747984134626": "الرجاء إدخال Qiniu Cloud Access Secret", + "t_3_1747984134586": "الرجاء إدخال مفتاح الوصول Qiniu Cloud", + "t_4_1747984130327": "نسخ", + "t_5_1747984133112": "عند اقتراب وقت الانتهاء", + "t_0_1747990228780": "يرجى اختيار سلطة التصديق", + "t_2_1747990228008": "لا تتوفر بيانات تفويض CA", + "t_3_1747990229599": "فشل في الحصول على قائمة التفويض CA", + "t_4_1747990227956": "التجديد التلقائي (أيام)", + "t_5_1747990228592": "فترة صلاحية الشهادة أقل من", + "t_6_1747990228465": "حان الوقت لتجديد الشهادة الجديدة", + "t_7_1747990227761": "عنوان الوكيل (اختياري)", + "t_8_1747990235316": "يدعم فقط عناوين الوكيل http أو https (مثال: http://proxy.example.com:8080)", + "t_9_1747990229640": "وقت التجديد التلقائي لا يمكن أن يكون فارغًا", + "t_10_1747990232207": "الرجاء اختيار اسم الموقع (يدعم اختيارات متعددة)", + "t_0_1747994891459": "موقع باغودا docker", + "t_0_1748052857931": "هيئة التصديق/التفويض (اختياري)", + "t_1_1748052860539": "إضافة Zerossl، Google، تفويض شهادة CA", + "t_2_1748052862259": "يرجى إدخال معلومات بريدك الإلكتروني لاستلام رسالة التحقق من الشهادة" +} \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/locales/model/frFR.json b/frontend/apps/allin-ssl/src/locales/model/frFR.json new file mode 100644 index 0000000..4c77a50 --- /dev/null +++ b/frontend/apps/allin-ssl/src/locales/model/frFR.json @@ -0,0 +1,628 @@ +{ + "t_0_1744098811152": "Avertissement : Vous avez entré dans une zone inconnue, la page que vous visitez n'existe pas, veuillez cliquer sur le bouton pour revenir à la page d'accueil.", + "t_1_1744098801860": "Retour à l'accueil", + "t_2_1744098804908": "Avis de sécurité : Si vous pensez que c'est une erreur, veuillez contacter l'administrateur immédiatement", + "t_3_1744098802647": "Développer le menu principal", + "t_4_1744098802046": "Menu principal pliable", + "t_1_1744164835667": "AllinSSL", + "t_2_1744164839713": "Connexion du compte", + "t_3_1744164839524": "Veuillez saisir le nom d'utilisateur", + "t_4_1744164840458": "Veuillez saisir le mot de passe", + "t_5_1744164840468": "Rappelez-vous du mot de passe", + "t_6_1744164838900": "Oublié votre mot de passe?", + "t_7_1744164838625": "En cours de connexion", + "t_8_1744164839833": "Se connecter", + "t_0_1744258111441": "Accueil", + "t_1_1744258113857": "Déploiement Automatisé", + "t_2_1744258111238": "Gestion des certificats", + "t_3_1744258111182": "Demande de certificat", + "t_4_1744258111238": "Gestion de l'API d'autorisation", + "t_5_1744258110516": "Surveillance", + "t_6_1744258111153": "Paramètres", + "t_0_1744861190562": "Renvoyer la liste des flux de travail", + "t_1_1744861189113": "Exécuter", + "t_2_1744861190040": "Sauvegarder", + "t_3_1744861190932": "Veuillez sélectionner un nœud à configurer", + "t_4_1744861194395": "Clique sur le nœud dans le diagramme de flux de gauche pour le configurer", + "t_5_1744861189528": "commencer", + "t_6_1744861190121": "Aucun noeud sélectionné", + "t_7_1744861189625": "Configuration enregistrée", + "t_8_1744861189821": "Démarrer le processus", + "t_9_1744861189580": "Nœud sélectionné :", + "t_0_1744870861464": "nœud", + "t_1_1744870861944": "Configuration de noeud", + "t_2_1744870863419": "Veuillez sélectionner le nœud de gauche pour la configuration", + "t_3_1744870864615": "Composant de configuration pour ce type de noeud introuvable", + "t_4_1744870861589": "Annuler", + "t_5_1744870862719": "confirmer", + "t_0_1744875938285": "à chaque minute", + "t_1_1744875938598": "chaque heure", + "t_2_1744875938555": "chaque jour", + "t_3_1744875938310": "chaque mois", + "t_4_1744875940750": "Exécution automatique", + "t_5_1744875940010": "Exécution manuelle", + "t_0_1744879616135": "Test PID", + "t_1_1744879616555": "Veuillez saisir le PID de test", + "t_2_1744879616413": "Cycle d'exécution", + "t_3_1744879615723": "minute", + "t_4_1744879616168": "Veuillez saisir les minutes", + "t_5_1744879615277": "heure", + "t_6_1744879616944": "Veuillez saisir des heures", + "t_7_1744879615743": "Date", + "t_8_1744879616493": "Sélectionnez une date", + "t_0_1744942117992": "chaque semaine", + "t_1_1744942116527": "lundi", + "t_2_1744942117890": "mardi", + "t_3_1744942117885": "Mercredi", + "t_4_1744942117738": "jeudi", + "t_5_1744942117167": "vendredi", + "t_6_1744942117815": "samedi", + "t_7_1744942117862": "dimanche", + "t_0_1744958839535": "Veuillez saisir le nom de domaine", + "t_1_1744958840747": "Veuillez saisir votre adresse e-mail", + "t_2_1744958840131": "Le format de l'e-mail est incorrect", + "t_3_1744958840485": "Veuillez choisir le fournisseur de DNS pour l'autorisation", + "t_4_1744958838951": "Déploiement local", + "t_5_1744958839222": "Déploiement SSH", + "t_6_1744958843569": "Panneau Bao Ta/1 panneau (Déployer sur le certificat du panneau)", + "t_7_1744958841708": "1panneau (Déploiement sur le projet de site spécifié)", + "t_8_1744958841658": "Tencent Cloud CDN/AliCloud CDN", + "t_9_1744958840634": "WAF de Tencent Cloud", + "t_10_1744958860078": "WAF d'Alicloud", + "t_11_1744958840439": "Ce certificat appliqué automatiquement", + "t_12_1744958840387": "Liste des certificats optionnels", + "t_13_1744958840714": "PEM (*.pem, *.crt, *.key)", + "t_14_1744958839470": "PFX (*.pfx)", + "t_15_1744958840790": "JKS (*.jks)", + "t_16_1744958841116": "POSIX bash (Linux/macOS)", + "t_17_1744958839597": "CMD (Windows)", + "t_18_1744958839895": "PowerShell (Windows)", + "t_19_1744958839297": "Certificat 1", + "t_20_1744958839439": "Certificat 2", + "t_21_1744958839305": "Serveur 1", + "t_22_1744958841926": "Serveur 2", + "t_23_1744958838717": "Panneau 1", + "t_29_1744958838904": "jour", + "t_30_1744958843864": "Le format du certificat est incorrect, veuillez vérifier s'il contient les identifiants d'en-tête et de pied de page complets", + "t_31_1744958844490": "Le format de la clé privée est incorrect, veuillez vérifier si elle contient l'identifiant complet de l'en-tête et du pied de page de la clé privée", + "t_0_1745215914686": "Nom d'automatisation", + "t_2_1745215915397": "automatique", + "t_3_1745215914237": "Manuel", + "t_4_1745215914951": "Statut activé", + "t_5_1745215914671": "Activer", + "t_6_1745215914104": "Désactiver", + "t_7_1745215914189": "Heure de création", + "t_8_1745215914610": "Opération", + "t_9_1745215914666": "Historique d'exécution", + "t_10_1745215914342": "exécuter", + "t_11_1745215915429": "Éditer", + "t_12_1745215914312": "Supprimer", + "t_13_1745215915455": "Exécuter le flux de travail", + "t_14_1745215916235": "Exécution du flux de travail réussie", + "t_15_1745215915743": "Échec de l'exécution du flux de travail", + "t_16_1745215915209": "Supprimer le flux de travail", + "t_17_1745215915985": "Suppression du flux de travail réussie", + "t_18_1745215915630": "Échec de la suppression du flux de travail", + "t_1_1745227838776": "Veuillez saisir le nom de l'automatisation", + "t_2_1745227839794": "Êtes-vous sûr de vouloir exécuter le workflow {name}?", + "t_3_1745227841567": "Confirmez-vous la suppression du flux de travail {name} ? Cette action ne peut pas être annulée.", + "t_4_1745227838558": "Temps d'exécution", + "t_5_1745227839906": "Heure de fin", + "t_6_1745227838798": "Méthode d'exécution", + "t_7_1745227838093": "Statut", + "t_8_1745227838023": "Réussite", + "t_9_1745227838305": "échec", + "t_10_1745227838234": "En cours", + "t_11_1745227838422": "inconnu", + "t_12_1745227838814": "Détails", + "t_13_1745227838275": "Télécharger un certificat", + "t_14_1745227840904": "Saisissez le nom de domaine du certificat ou le nom de la marque pour la recherche", + "t_15_1745227839354": "ensemble", + "t_16_1745227838930": "unité", + "t_17_1745227838561": "Nom de domaine", + "t_18_1745227838154": "Marque", + "t_19_1745227839107": "Jours restants", + "t_20_1745227838813": "Heure d'expiration", + "t_21_1745227837972": "Source", + "t_22_1745227838154": "Demande automatique", + "t_23_1745227838699": "Téléversement manuel", + "t_24_1745227839508": "Ajouter une date", + "t_25_1745227838080": "Télécharger", + "t_27_1745227838583": "Bientôt expiré", + "t_28_1745227837903": "normal", + "t_29_1745227838410": "Supprimer le certificat", + "t_30_1745227841739": "Confirmez-vous que vous souhaitez supprimer ce certificat ? Cette action ne peut pas être annulée.", + "t_31_1745227838461": "Confirmer", + "t_32_1745227838439": "Nom du certificat", + "t_33_1745227838984": "Veuillez saisir le nom du certificat", + "t_34_1745227839375": "Contenu du certificat (PEM)", + "t_35_1745227839208": "Veuillez saisir le contenu du certificat", + "t_36_1745227838958": "Contenu de la clé privée (KEY)", + "t_37_1745227839669": "Veuillez saisir le contenu de la clé privée", + "t_38_1745227838813": "Échec du téléchargement", + "t_39_1745227838696": "Échec du téléversement", + "t_40_1745227838872": "Échec de la suppression", + "t_0_1745289355714": "Ajouter l'API d'autorisation", + "t_1_1745289356586": "Veuillez saisir le nom ou le type de l'API autorisée", + "t_2_1745289353944": "Nom", + "t_3_1745289354664": "Type d'API d'autorisation", + "t_4_1745289354902": "API d'édition d'autorisation", + "t_5_1745289355718": "Suppression de l'API d'autorisation", + "t_6_1745289358340": "Êtes-vous sûr de vouloir supprimer cet API autorisé ? Cette action ne peut pas être annulée.", + "t_7_1745289355714": "Échec de l'ajout", + "t_8_1745289354902": "Échec de mise à jour", + "t_9_1745289355714": "Expiré {days} jours", + "t_10_1745289354650": "Gestion de surveillance", + "t_11_1745289354516": "Ajouter une surveillance", + "t_12_1745289356974": "Veuillez saisir le nom de surveillance ou le domaine pour la recherche", + "t_13_1745289354528": "Nom du moniteur", + "t_14_1745289354902": "Domaine du certificat", + "t_15_1745289355714": "Autorité de certification", + "t_16_1745289354902": "Statut du certificat", + "t_17_1745289355715": "Date d'expiration du certificat", + "t_18_1745289354598": "Canaux d'alerte", + "t_19_1745289354676": "Dernière date de vérification", + "t_20_1745289354598": "Édition de surveillance", + "t_21_1745289354598": "Confirmez la suppression", + "t_22_1745289359036": "Les éléments ne peuvent pas être restaurés après suppression. Êtes-vous sûr de vouloir supprimer ce moniteur?", + "t_23_1745289355716": "Échec de la modification", + "t_24_1745289355715": "Échec de la configuration", + "t_25_1745289355721": "Veuillez saisir le code de vérification", + "t_26_1745289358341": "Échec de validation du formulaire, veuillez vérifier le contenu rempli", + "t_27_1745289355721": "Veuillez saisir le nom de l'API autorisée", + "t_28_1745289356040": "Veuillez sélectionner le type d'API d'autorisation", + "t_29_1745289355850": "Veuillez saisir l'IP du serveur", + "t_30_1745289355718": "S'il vous plaît, entrez le port SSH", + "t_31_1745289355715": "Veuillez saisir la clé SSH", + "t_32_1745289356127": "Veuillez saisir l'adresse de Baota", + "t_33_1745289355721": "Veuillez saisir la clé API", + "t_34_1745289356040": "Veuillez saisir l'adresse 1panel", + "t_35_1745289355714": "Veuillez saisir AccessKeyId", + "t_36_1745289355715": "Veuillez saisir AccessKeySecret", + "t_37_1745289356041": "S'il vous plaît, entrez SecretId", + "t_38_1745289356419": "Veuillez saisir la Clé Secrète", + "t_39_1745289354902": "Mise à jour réussie", + "t_40_1745289355715": "Ajout réussi", + "t_41_1745289354902": "Type", + "t_42_1745289355715": "IP du serveur", + "t_43_1745289354598": "Port SSH", + "t_44_1745289354583": "Nom d'utilisateur", + "t_45_1745289355714": "Méthode d'authentification", + "t_46_1745289355723": "Authentification par mot de passe", + "t_47_1745289355715": "Authentification par clé", + "t_48_1745289355714": "Mot de passe", + "t_49_1745289355714": "Clé privée SSH", + "t_50_1745289355715": "Veuillez saisir la clé privée SSH", + "t_51_1745289355714": "Mot de passe de la clé privée", + "t_52_1745289359565": "Si la clé privée a un mot de passe, veuillez saisir", + "t_53_1745289356446": "Adresse du panneau BaoTa", + "t_54_1745289358683": "Veuillez saisir l'adresse du panneau Baota, par exemple : https://bt.example.com", + "t_55_1745289355715": "Clé API", + "t_56_1745289355714": "Adresse du panneau 1", + "t_57_1745289358341": "Saisissez l'adresse 1panel, par exemple : https://1panel.example.com", + "t_58_1745289355721": "Saisissez l'ID AccessKey", + "t_59_1745289356803": "Veuillez saisir le secret d'AccessKey", + "t_60_1745289355715": "Veuillez saisir le nom de surveillance", + "t_61_1745289355878": "Veuillez saisir le domaine/IP", + "t_62_1745289360212": "Veuillez sélectionner le cycle d'inspection", + "t_63_1745289354897": "5 minutes", + "t_64_1745289354670": "10 minutes", + "t_65_1745289354591": "15 minutes", + "t_66_1745289354655": "30 minutes", + "t_67_1745289354487": "60 minutes", + "t_68_1745289354676": "E-mail", + "t_69_1745289355721": "SMS", + "t_70_1745289354904": "WeChat", + "t_71_1745289354583": "Domaine/IP", + "t_72_1745289355715": "Période de contrôle", + "t_73_1745289356103": "Sélectionnez un canal d'alerte", + "t_0_1745289808449": "Veuillez saisir le nom de l'API autorisée", + "t_0_1745294710530": "Supprimer la surveillance", + "t_0_1745295228865": "Heure de mise à jour", + "t_0_1745317313835": "Format de l'adresse IP du serveur incorrect", + "t_1_1745317313096": "Erreur de format de port", + "t_2_1745317314362": "Format incorrect de l'adresse URL du panneau", + "t_3_1745317313561": "Veuillez saisir la clé API du panneau", + "t_4_1745317314054": "Veuillez saisir le AccessKeyId d'Aliyun", + "t_5_1745317315285": "Veuillez saisir le AccessKeySecret d'Aliyun", + "t_6_1745317313383": "S'il vous plaît saisir le SecretId de Tencent Cloud", + "t_7_1745317313831": "Veuillez saisir la SecretKey de Tencent Cloud", + "t_0_1745457486299": "Activé", + "t_1_1745457484314": "Arrêté", + "t_2_1745457488661": "Passer en mode manuel", + "t_3_1745457486983": "Passer en mode automatique", + "t_4_1745457497303": "Après avoir basculé en mode manuel, le flux de travail ne s'exécutera plus automatiquement, mais peut toujours être exécuté manuellement", + "t_5_1745457494695": "Après être passé en mode automatique, le flux de travail s'exécutera automatiquement selon le temps configuré", + "t_6_1745457487560": "Fermer le flux de travail actuel", + "t_7_1745457487185": "Activer le flux de travail actuel", + "t_8_1745457496621": "Après la fermeture, le flux de travail ne s'exécutera plus automatiquement et ne pourra pas être exécuté manuellement. Continuer ?", + "t_9_1745457500045": "Après activation, la configuration du flux de travail s'exécutera automatiquement ou manuellement. Continuer ?", + "t_10_1745457486451": "Échec de l'ajout du flux de travail", + "t_11_1745457488256": "Échec de la définition du mode d'exécution du flux de travail", + "t_12_1745457489076": "Activer ou désactiver l'échec du flux de travail", + "t_13_1745457487555": "Échec de l'exécution du workflow", + "t_14_1745457488092": "Échec de la suppression du flux de travail", + "t_15_1745457484292": "Quitter", + "t_16_1745457491607": "Vous êtes sur le point de vous déconnecter. Êtes-vous sûr de vouloir quitter ?", + "t_17_1745457488251": "Déconnexion en cours, veuillez patienter...", + "t_18_1745457490931": "Ajouter une notification par e-mail", + "t_19_1745457484684": "Enregistré avec succès", + "t_20_1745457485905": "Supprimé avec succès", + "t_0_1745464080226": "Échec de la récupération des paramètres du système", + "t_1_1745464079590": "Échec de l'enregistrement des paramètres", + "t_2_1745464077081": "Échec de la récupération des paramètres de notification", + "t_3_1745464081058": "Échec de l'enregistrement des paramètres de notification", + "t_4_1745464075382": "Échec de la récupération de la liste des canaux de notification", + "t_5_1745464086047": "Échec de l'ajout du canal de notification par email", + "t_6_1745464075714": "Échec de la mise à jour du canal de notification", + "t_7_1745464073330": "Échec de la suppression du canal de notification", + "t_8_1745464081472": "Échec de la vérification de la mise à jour de version", + "t_9_1745464078110": "Enregistrer les paramètres", + "t_10_1745464073098": "Paramètres de base", + "t_0_1745474945127": "Choisir un modèle", + "t_0_1745490735213": "Veuillez saisir le nom du workflow", + "t_1_1745490731990": "Configuration", + "t_2_1745490735558": "Veuillez saisir le format d'e-mail", + "t_3_1745490735059": "Veuillez sélectionner un fournisseur DNS", + "t_4_1745490735630": "Veuillez saisir l'intervalle de renouvellement", + "t_5_1745490738285": "Veuillez entrer le nom de domaine, il ne peut pas être vide", + "t_6_1745490738548": "Veuillez entrer votre email, l'email ne peut pas être vide", + "t_7_1745490739917": "Veuillez sélectionner un fournisseur DNS, le fournisseur DNS ne peut pas être vide", + "t_8_1745490739319": "Veuillez saisir l'intervalle de renouvellement, l'intervalle de renouvellement ne peut pas être vide", + "t_1_1745553909483": "Format d'email incorrect, veuillez saisir un email valide", + "t_2_1745553907423": "L'intervalle de renouvellement ne peut pas être vide", + "t_0_1745735774005": "Veuillez saisir le nom de domaine du certificat, plusieurs noms de domaine séparés par des virgules", + "t_1_1745735764953": "Boîte aux lettres", + "t_2_1745735773668": "Veuillez saisir votre adresse e-mail pour recevoir les notifications de l'autorité de certification", + "t_3_1745735765112": "Fournisseur DNS", + "t_4_1745735765372": "Ajouter", + "t_5_1745735769112": "Intervalle de renouvellement (jours)", + "t_6_1745735765205": "Intervalle de renouvellement", + "t_7_1745735768326": "jour(s), renouvelé automatiquement à l'expiration", + "t_8_1745735765753": "Configuré", + "t_9_1745735765287": "Non configuré", + "t_10_1745735765165": "Panneau Pagode", + "t_11_1745735766456": "Site Web du Panneau Pagode", + "t_12_1745735765571": "Panneau 1Panel", + "t_13_1745735766084": "1Panel site web", + "t_14_1745735766121": "Tencent Cloud CDN", + "t_15_1745735768976": "Tencent Cloud COS", + "t_16_1745735766712": "Alibaba Cloud CDN", + "t_18_1745735765638": "Type de déploiement", + "t_19_1745735766810": "Veuillez sélectionner le type de déploiement", + "t_20_1745735768764": "Veuillez entrer le chemin de déploiement", + "t_21_1745735769154": "Veuillez saisir la commande de préfixe", + "t_22_1745735767366": "Veuillez entrer la commande postérieure", + "t_24_1745735766826": "Veuillez entrer l'ID du site", + "t_25_1745735766651": "Veuillez entrer la région", + "t_26_1745735767144": "Veuillez entrer le seau", + "t_27_1745735764546": "Étape suivante", + "t_28_1745735766626": "Sélectionner le type de déploiement", + "t_29_1745735768933": "Configurer les paramètres de déploiement", + "t_30_1745735764748": "Mode de fonctionnement", + "t_31_1745735767891": "Mode de fonctionnement non configuré", + "t_32_1745735767156": "Cycle d'exécution non configuré", + "t_33_1745735766532": "Durée d'exécution non configurée", + "t_34_1745735771147": "Fichier de certificat (format PEM)", + "t_35_1745735781545": "Veuillez coller le contenu du fichier de certificat, par exemple :\n-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----", + "t_36_1745735769443": "Fichier de clé privée (format KEY)", + "t_37_1745735779980": "Collez le contenu du fichier de clé privée, par exemple:\n-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----", + "t_38_1745735769521": "Le contenu de la clé privée du certificat ne peut pas être vide", + "t_39_1745735768565": "Le format de la clé privée du certificat est incorrect", + "t_40_1745735815317": "Le contenu du certificat ne peut pas être vide", + "t_41_1745735767016": "Format du certificat incorrect", + "t_0_1745738961258": "Précédent", + "t_1_1745738963744": "Soumettre", + "t_2_1745738969878": "Configurer les paramètres de déploiement, le type détermine la configuration des paramètres", + "t_0_1745744491696": "Source de l'appareil de déploiement", + "t_1_1745744495019": "Veuillez sélectionner la source de l'appareil de déploiement", + "t_2_1745744495813": "Veuillez sélectionner le type de déploiement et cliquer sur Suivant", + "t_0_1745744902975": "Source de déploiement", + "t_1_1745744905566": "Veuillez sélectionner la source de déploiement", + "t_2_1745744903722": "Ajouter plus d'appareils", + "t_0_1745748292337": "Ajouter une source de déploiement", + "t_1_1745748290291": "Source du certificat", + "t_2_1745748298902": "La source de déploiement du type actuel est vide, veuillez d'abord ajouter une source de déploiement", + "t_3_1745748298161": "Il n'y a pas de nœud de demande dans le processus actuel, veuillez d'abord ajouter un nœud de demande", + "t_4_1745748290292": "Soumettre le contenu", + "t_0_1745765864788": "Cliquez pour modifier le titre du flux de travail", + "t_1_1745765875247": "Supprimer le nœud - 【{name}】", + "t_2_1745765875918": "Le nœud actuel contient des nœuds enfants. La suppression affectera d'autres nœuds. Confirmez-vous la suppression ?", + "t_3_1745765920953": "Le nœud actuel contient des données de configuration, êtes-vous sûr de vouloir le supprimer ?", + "t_4_1745765868807": "Veuillez sélectionner le type de déploiement avant de passer à l'étape suivante", + "t_0_1745833934390": "Veuillez sélectionner le type", + "t_1_1745833931535": "Hôte", + "t_2_1745833931404": "port", + "t_3_1745833936770": "Échec de la récupération des données de vue d'ensemble de la page d'accueil", + "t_4_1745833932780": "Information de version", + "t_5_1745833933241": "Version actuelle", + "t_6_1745833933523": "Méthode de mise à jour", + "t_7_1745833933278": "Dernière version", + "t_8_1745833933552": "Journal des modifications", + "t_9_1745833935269": "Code QR du Service Client", + "t_10_1745833941691": "Scannez le code QR pour ajouter le service client", + "t_11_1745833935261": "Compte officiel WeChat", + "t_12_1745833943712": "Scannez pour suivre le compte officiel WeChat", + "t_13_1745833933630": "À propos du produit", + "t_14_1745833932440": "Serveur SMTP", + "t_15_1745833940280": "Veuillez entrer le serveur SMTP", + "t_16_1745833933819": "Port SMTP", + "t_17_1745833935070": "Veuillez entrer le port SMTP", + "t_18_1745833933989": "Connexion SSL/TLS", + "t_0_1745887835267": "Veuillez sélectionner la notification de message", + "t_1_1745887832941": "Notification", + "t_2_1745887834248": "Ajouter un canal de notification", + "t_3_1745887835089": "Veuillez saisir le sujet de la notification", + "t_4_1745887835265": "Veuillez saisir le contenu de la notification", + "t_0_1745895057404": "Modifier les paramètres de notification par e-mail", + "t_0_1745920566646": "Sujet de la notification", + "t_1_1745920567200": "Contenu de la notification", + "t_0_1745936396853": "Cliquez pour obtenir le code de vérification", + "t_0_1745999035681": "il reste {days} jours", + "t_1_1745999036289": "Expiration prochaine {days} jours", + "t_0_1746000517848": "Expiré", + "t_0_1746001199409": "Expiré", + "t_0_1746004861782": "Le fournisseur DNS est vide", + "t_1_1746004861166": "Ajouter un fournisseur DNS", + "t_0_1746497662220": "Rafraîchir", + "t_0_1746519384035": "En cours", + "t_0_1746579648713": "Détails de l'historique d'exécution", + "t_0_1746590054456": "État d'exécution", + "t_1_1746590060448": "Méthode de Déclenchement", + "t_0_1746667592819": "Soumission des informations en cours, veuillez patienter...", + "t_1_1746667588689": "Clé", + "t_2_1746667592840": "URL du panneau", + "t_3_1746667592270": "Ignorer les erreurs de certificat SSL/TLS", + "t_4_1746667590873": "Échec de la validation du formulaire", + "t_5_1746667590676": "Nouveau flux de travail", + "t_6_1746667592831": "Soumission de la demande, veuillez patienter...", + "t_7_1746667592468": "Veuillez entrer le nom de domaine correct", + "t_8_1746667591924": "Veuillez sélectionner la méthode d'analyse", + "t_9_1746667589516": "Actualiser la liste", + "t_10_1746667589575": "Joker", + "t_11_1746667589598": "Multi-domaine", + "t_12_1746667589733": "Populaire", + "t_13_1746667599218": "est un fournisseur de certificats SSL gratuits largement utilisé, adapté aux sites personnels et aux environnements de test.", + "t_14_1746667590827": "Nombre de domaines pris en charge", + "t_15_1746667588493": "pièce", + "t_16_1746667591069": "Prise en charge des caractères génériques", + "t_17_1746667588785": "soutien", + "t_18_1746667590113": "Non pris en charge", + "t_19_1746667589295": "Période de validité", + "t_20_1746667588453": "jour", + "t_21_1746667590834": "Prise en charge des mini-programmes", + "t_22_1746667591024": "Sites applicables", + "t_23_1746667591989": "*.example.com, *.demo.com", + "t_24_1746667583520": "*.example.com", + "t_25_1746667590147": "example.com、demo.com", + "t_26_1746667594662": "www.example.com, example.com", + "t_27_1746667589350": "Gratuit", + "t_28_1746667590336": "Postuler maintenant", + "t_29_1746667589773": "Adresse du projet", + "t_30_1746667591892": "Veuillez entrer le chemin du fichier de certificat", + "t_31_1746667593074": "Veuillez entrer le chemin du fichier de clé privée", + "t_0_1746673515941": "Le fournisseur DNS actuel est vide, veuillez d'abord ajouter un fournisseur DNS", + "t_0_1746676862189": "Échec de l'envoi de la notification de test", + "t_1_1746676859550": "Ajouter une Configuration", + "t_2_1746676856700": "Pas encore pris en charge", + "t_3_1746676857930": "Notification par e-mail", + "t_4_1746676861473": "Envoyer des notifications d'alerte par e-mail", + "t_5_1746676856974": "Notification DingTalk", + "t_6_1746676860886": "Envoyer des notifications d'alarme via le robot DingTalk", + "t_7_1746676857191": "Notification WeChat Work", + "t_8_1746676860457": "Envoyer des notifications d'alarme via le bot WeCom", + "t_9_1746676857164": "Notification Feishu", + "t_10_1746676862329": "Envoyer des notifications d'alarme via le bot Feishu", + "t_11_1746676859158": "Notification WebHook", + "t_12_1746676860503": "Envoyer des notifications d'alarme via WebHook", + "t_13_1746676856842": "Canal de notification", + "t_14_1746676859019": "Canaux de notification configurés", + "t_15_1746676856567": "Désactivé", + "t_16_1746676855270": "Test", + "t_0_1746677882486": "Dernier état d'exécution", + "t_0_1746697487119": "Le nom de domaine ne peut pas être vide", + "t_1_1746697485188": "L'e-mail ne peut pas être vide", + "t_2_1746697487164": "Alibaba Cloud OSS", + "t_0_1746754500246": "Fournisseur d'hébergement", + "t_1_1746754499371": "Source de l'API", + "t_2_1746754500270": "Type d'API", + "t_0_1746760933542": "Erreur de requête", + "t_0_1746773350551": "{0} résultats", + "t_1_1746773348701": "Non exécuté", + "t_2_1746773350970": "Workflow automatisé", + "t_3_1746773348798": "Quantité totale", + "t_4_1746773348957": "Échec de l'exécution", + "t_5_1746773349141": "Expire bientôt", + "t_6_1746773349980": "Surveillance en temps réel", + "t_7_1746773349302": "Quantité anormale", + "t_8_1746773351524": "Récents enregistrements d'exécution de flux de travail", + "t_9_1746773348221": "Voir tout", + "t_10_1746773351576": "Aucun enregistrement d'exécution de flux de travail", + "t_11_1746773349054": "Créer un workflow", + "t_12_1746773355641": "Cliquez pour créer un flux de travail automatisé afin d'améliorer l'efficacité", + "t_13_1746773349526": "Demander un certificat", + "t_14_1746773355081": "Cliquez pour demander et gérer les certificats SSL afin d'assurer la sécurité", + "t_16_1746773356568": "Un seul canal de notification par e-mail peut être configuré au maximum", + "t_17_1746773351220": "Confirmer le canal de notification {0}", + "t_18_1746773355467": "Les canaux de notification {0} commenceront à envoyer des alertes.", + "t_19_1746773352558": "Le canal de notification actuel ne prend pas en charge les tests", + "t_20_1746773356060": "Envoi d'un e-mail de test, veuillez patienter...", + "t_21_1746773350759": "E-mail de test", + "t_22_1746773360711": "Envoyer un e-mail de test à la boîte mail configurée actuellement, continuer ?", + "t_23_1746773350040": "Confirmation de suppression", + "t_25_1746773349596": "Veuillez entrer le nom", + "t_26_1746773353409": "Veuillez saisir le bon port SMTP", + "t_27_1746773352584": "Veuillez entrer le mot de passe utilisateur", + "t_28_1746773354048": "Veuillez entrer l'e-mail correct de l'expéditeur", + "t_29_1746773351834": "Veuillez entrer le bon e-mail de réception", + "t_30_1746773350013": "E-mail de l'expéditeur", + "t_31_1746773349857": "Recevoir un e-mail", + "t_32_1746773348993": "DingTalk", + "t_33_1746773350932": "WeChat Work", + "t_34_1746773350153": "Feishu", + "t_35_1746773362992": "Un outil de gestion du cycle de vie complet des certificats SSL intégrant la demande, la gestion, le déploiement et la surveillance.", + "t_36_1746773348989": "Demande de certificat", + "t_37_1746773356895": "Support pour obtenir des certificats de Let's Encrypt via le protocole ACME", + "t_38_1746773349796": "Gestion des certificats", + "t_39_1746773358932": "Gestion centralisée de tous les certificats SSL, y compris les certificats téléchargés manuellement et appliqués automatiquement", + "t_40_1746773352188": "Déploiement de certificat", + "t_41_1746773364475": "Prise en charge du déploiement de certificats en un clic sur plusieurs plateformes telles que Alibaba Cloud, Tencent Cloud, Pagoda Panel, 1Panel, etc.", + "t_42_1746773348768": "Surveillance du site", + "t_43_1746773359511": "Surveillance en temps réel de l'état des certificats SSL du site pour prévenir l'expiration des certificats", + "t_44_1746773352805": "Tâche automatisée :", + "t_45_1746773355717": "Prend en charge les tâches planifiées, renouvellement automatique des certificats et déploiement", + "t_46_1746773350579": "Prise en charge multiplateforme", + "t_47_1746773360760": "Prend en charge les méthodes de vérification DNS pour plusieurs fournisseurs DNS (Alibaba Cloud, Tencent Cloud, etc.)", + "t_0_1746773763967": "Êtes-vous sûr de vouloir supprimer {0}, le canal de notification ?", + "t_1_1746773763643": "Let's Encrypt et d'autres CA demandent automatiquement des certificats gratuits", + "t_0_1746776194126": "Détails du journal", + "t_1_1746776198156": "Échec du chargement du journal :", + "t_2_1746776194263": "Télécharger le journal", + "t_3_1746776195004": "Aucune information de journal", + "t_0_1746782379424": "Tâches automatisées", + "t_0_1746858920894": "Veuillez sélectionner un hébergeur", + "t_1_1746858922914": "La liste des fournisseurs DNS est vide, veuillez ajouter", + "t_2_1746858923964": "La liste des fournisseurs d'hébergement est vide, veuillez ajouter", + "t_3_1746858920060": "Ajouter un fournisseur d'hébergement", + "t_4_1746858917773": "Sélectionné", + "t_0_1747019621052": "Veuillez choisir un fournisseur d'hébergement{0}", + "t_1_1747019624067": "Cliquez pour configurer la surveillance du site et suivre l'état en temps réel", + "t_2_1747019616224": "Alibaba Cloud", + "t_3_1747019616129": "Tencent Cloud", + "t_0_1747040228657": "Pour plusieurs domaines, veuillez utiliser des virgules anglaises pour les séparer, par exemple : test.com,test.cn", + "t_1_1747040226143": "Pour les domaines génériques, utilisez un astérisque *, par exemple : *.test.com", + "t_0_1747042966820": "Veuillez entrer la clé API Cloudflare correcte", + "t_1_1747042969705": "Veuillez entrer la clé API correcte de BT-Panel", + "t_2_1747042967277": "Veuillez entrer le bon SecretKey de Tencent Cloud", + "t_3_1747042967608": "Veuillez entrer la bonne clé secrète Huawei Cloud", + "t_4_1747042966254": "Veuillez saisir la clé d'accès Huawei Cloud", + "t_5_1747042965911": "Veuillez entrer le bon compte email", + "t_0_1747047213730": "Ajouter un déploiement automatisé", + "t_1_1747047213009": "Ajouter un certificat", + "t_2_1747047214975": "Plateforme de Gestion de Certificats SSL", + "t_3_1747047218669": "Erreur de format de domaine, vérifiez le format du domaine", + "t_0_1747106957037": "Serveur DNS récursif (facultatif)", + "t_1_1747106961747": "Saisissez les serveurs DNS récursifs (séparez plusieurs valeurs par des virgules)", + "t_2_1747106957037": "Ignorer la vérification préalable locale", + "t_0_1747110184700": "Sélectionner le certificat", + "t_1_1747110191587": "Si vous devez modifier le contenu du certificat et la clé, choisissez un certificat personnalisé", + "t_2_1747110193465": "Lorsqu'un certificat non personnalisé est sélectionné, ni le contenu du certificat ni la clé ne peuvent être modifiés", + "t_3_1747110185110": "Télécharger et soumettre", + "t_0_1747215751189": "Site Web Pagoda WAF", + "t_0_1747271295174": "Pagoda WAF - Erreur de format d'URL", + "t_1_1747271295484": "Veuillez saisir la clé Pagoda WAF-API", + "t_2_1747271295877": "Veuillez saisir le bon AccessKey Huawei Cloud", + "t_3_1747271294475": "Veuillez saisir le bon Baidu Cloud AccessKey", + "t_4_1747271294621": "Veuillez entrer le bon SecretKey de Baidu Cloud", + "t_5_1747271291828": "Baota WAF-URL", + "t_6_1747271296994": "Déploiement Local", + "t_7_1747271292060": "Toutes les sources", + "t_8_1747271290414": "Pagode", + "t_9_1747271284765": "1Panel", + "t_0_1747280814475": "La modification du port SMTP est interdite", + "t_1_1747280813656": "Chemin du fichier de certificat (format PEM uniquement)", + "t_2_1747280811593": "Chemin du fichier de clé privée", + "t_3_1747280812067": "Commande préalable (facultative)", + "t_4_1747280811462": "Commande postérieure (facultatif)", + "t_6_1747280809615": "ID du site", + "t_7_1747280808936": "Région", + "t_8_1747280809382": "Seau", + "t_9_1747280810169": "Déploiement répété", + "t_10_1747280816952": "Lorsque le certificat est identique au dernier déploiement et que le dernier déploiement a réussi", + "t_11_1747280809178": "Passer", + "t_12_1747280809893": "Ne pas sauter", + "t_13_1747280810369": "Redéploiement", + "t_14_1747280811231": "Rechercher le type de déploiement", + "t_0_1747296173751": "Nom du site", + "t_1_1747296175494": "Veuillez entrer le nom du site Web", + "t_0_1747298114839": "Site Leichi WAF", + "t_1_1747298114192": "Leichi WAF", + "t_0_1747300383756": "Leichi WAF - erreur de format d'URL", + "t_1_1747300384579": "Veuillez entrer la clé API correcte de BT-WAF", + "t_2_1747300385222": "Veuillez saisir la clé correcte de Leichi WAF-API", + "t_0_1747365600180": "Veuillez saisir le nom d'utilisateur Western Digital", + "t_1_1747365603108": "Veuillez entrer le mot de passe de Western Digital", + "t_3_1747365600828": "Veuillez saisir la clé d'accès du moteur Volcano", + "t_4_1747365600137": "Veuillez entrer le SecretKey de Volcano Engine", + "t_0_1747367069267": "Site Pagoda docker", + "t_0_1747617113090": "Veuillez entrer le jeton API de Leichi", + "t_1_1747617105179": "API Token", + "t_0_1747647014927": "Algorithme de certificat", + "t_0_1747709067998": "Veuillez entrer la clé SSH, le contenu ne peut pas être vide", + "t_0_1747711335067": "Veuillez entrer le mot de passe SSH", + "t_1_1747711335336": "Adresse de l'hôte", + "t_2_1747711337958": "Veuillez saisir l'adresse de l'hôte, elle ne peut pas être vide", + "t_0_1747754231151": "Visionneuse de journaux", + "t_1_1747754231838": "Veuillez d'abord", + "t_2_1747754234999": "Si vous avez des questions ou des suggestions, n'hésitez pas à les soulever", + "t_3_1747754232000": "Vous pouvez également nous trouver sur Github", + "t_4_1747754235407": "Votre participation est extrêmement importante pour AllinSSL, merci.", + "t_0_1747817614953": "Veuillez entrer", + "t_1_1747817639034": "oui", + "t_2_1747817610671": "Non", + "t_3_1747817612697": "Le champ du nœud est requis", + "t_4_1747817613325": "Veuillez entrer un nom de domaine valide", + "t_5_1747817619337": "Veuillez entrer un nom de domaine valide, séparez plusieurs domaines par des virgules anglaises", + "t_6_1747817644358": "Veuillez entrer votre adresse e-mail", + "t_7_1747817613773": "Veuillez entrer une adresse e-mail valide", + "t_8_1747817614764": "Erreur de nœud", + "t_9_1747817611448": "Domaine :", + "t_10_1747817611126": "Postuler", + "t_11_1747817612051": "Déploiement", + "t_12_1747817611391": "Téléverser", + "t_0_1747886301644": "Configuration de l'envoi de messages", + "t_1_1747886307276": "Panneau Pagode - Site Web", + "t_2_1747886302053": "1Panel-Site Web", + "t_3_1747886302848": "Pagode WAF", + "t_4_1747886303229": "Pagode WAF-Site Web", + "t_5_1747886301427": "Tencent Cloud EdgeOne", + "t_6_1747886301844": "Qiniu Cloud", + "t_7_1747886302395": "Qiniu Cloud-CDN", + "t_8_1747886304014": "Qiniu Cloud - OSS", + "t_9_1747886301128": "Huawei Cloud", + "t_10_1747886300958": "Baidu Cloud", + "t_11_1747886301986": "Bassin de Tonnerre", + "t_12_1747886302725": "Leichi WAF-Site Web", + "t_13_1747886301689": "Moteur Volcan", + "t_14_1747886301884": "West Digital", + "t_15_1747886301573": "Type de projet de déploiement", + "t_16_1747886308182": "Êtes-vous sûr de vouloir actualiser la page ? Les données peuvent être perdues !", + "t_0_1747895713179": "Exécution réussie", + "t_1_1747895712756": "Exécution en cours", + "t_0_1747903670020": "Gestion d'autorisation CA", + "t_2_1747903672640": "Confirmer la suppression", + "t_3_1747903672833": "Êtes-vous sûr de vouloir supprimer cette autorisation CA ?", + "t_4_1747903685371": "Ajouter une autorisation CA", + "t_5_1747903671439": "Veuillez entrer ACME EAB KID", + "t_6_1747903672931": "Veuillez saisir la clé HMAC ACME EAB", + "t_7_1747903678624": "Veuillez sélectionner le fournisseur CA", + "t_8_1747903675532": "L'alias autorisé par le fournisseur CA actuel pour une identification rapide", + "t_9_1747903669360": "Fournisseur d'AC", + "t_10_1747903662994": "ACME EAB KID", + "t_11_1747903674802": "Veuillez entrer l'ACME EAB KID fourni par le CA", + "t_12_1747903662994": "ACME EAB HMAC Key", + "t_13_1747903673007": "Entrez l'ACME EAM HMAC du fournisseur de CA", + "t_0_1747904536291": "AllinSSL, une plateforme gratuite et open-source de gestion automatisée de certificats SSL. Application, renouvellement, déploiement et surveillance automatisés d'un seul clic pour tous les certificats SSL/TLS, prenant en charge les environnements multi-clouds et plusieurs CA (coding~), dites adieu aux configurations fastidieuses et aux coûts élevés.", + "t_0_1747965909665": "Veuillez entrer l'email pour lier l'autorisation CA", + "t_0_1747969933657": "Déploiement terminal", + "t_0_1747984137443": "Veuillez entrer la bonne clé API GoDaddy", + "t_1_1747984133312": "Veuillez entrer le Secret API de GoDaddy", + "t_2_1747984134626": "Veuillez entrer le Access Secret de Qiniu Cloud", + "t_3_1747984134586": "Veuillez entrer la clé d'accès Qiniu Cloud", + "t_4_1747984130327": "Copier", + "t_5_1747984133112": "Lorsque la date d'expiration approche", + "t_0_1747990228780": "Veuillez sélectionner l'autorité de certification", + "t_2_1747990228008": "Aucune donnée d'autorisation CA disponible", + "t_3_1747990229599": "Échec de l'obtention de la liste d'autorisation CA", + "t_4_1747990227956": "Renouvellement automatique (jours)", + "t_5_1747990228592": "La période de validité du certificat est inférieure à", + "t_6_1747990228465": "Il est temps de renouveler le nouveau certificat", + "t_7_1747990227761": "Adresse proxy (facultatif)", + "t_8_1747990235316": "Ne prend en charge que les adresses proxy http ou https (par exemple, http://proxy.example.com:8080)", + "t_9_1747990229640": "L'heure de renouvellement automatique ne peut pas être vide", + "t_10_1747990232207": "Veuillez sélectionner le nom du site (sélection multiple prise en charge)", + "t_0_1747994891459": "Site web Pagoda docker", + "t_0_1748052857931": "Autorité de Certification/Autorisation (Optionnel)", + "t_1_1748052860539": "Ajouter Zerossl, Google, autorisation de certificat CA", + "t_2_1748052862259": "Veuillez entrer les informations de votre e-mail pour recevoir l'e-mail de vérification du certificat" +} \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/locales/model/jaJP.json b/frontend/apps/allin-ssl/src/locales/model/jaJP.json new file mode 100644 index 0000000..9cd80d7 --- /dev/null +++ b/frontend/apps/allin-ssl/src/locales/model/jaJP.json @@ -0,0 +1,628 @@ +{ + "t_0_1744098811152": "警告:未知のエリアに進入しました。アクセスしようとしたページは存在しません。ボタンをクリックしてホームページに戻ってください。", + "t_1_1744098801860": "ホームに戻る", + "t_2_1744098804908": "安全注意:これが誤りだと思われる場合は、すぐに管理者に連絡してください", + "t_3_1744098802647": "メインメニューを展開する", + "t_4_1744098802046": "折りたたみメインメニュー", + "t_1_1744164835667": "AllinSSL", + "t_2_1744164839713": "アカウントログイン", + "t_3_1744164839524": "ユーザー名を入力してください", + "t_4_1744164840458": "パスワードを入力してください", + "t_5_1744164840468": "パスワードを覚える", + "t_6_1744164838900": "パスワードを忘れたら", + "t_7_1744164838625": "ログイン中", + "t_8_1744164839833": "ログイン", + "t_0_1744258111441": "ホーム", + "t_1_1744258113857": "自動デプロイメント", + "t_2_1744258111238": "証明書管理", + "t_3_1744258111182": "証明書申請", + "t_4_1744258111238": "認証API管理", + "t_5_1744258110516": "監視", + "t_6_1744258111153": "設定", + "t_0_1744861190562": "ワークフローリストの返信", + "t_1_1744861189113": "実行", + "t_2_1744861190040": "保存する", + "t_3_1744861190932": "設定するノードを選んでください", + "t_4_1744861194395": "左側のフローウォークダイアグラムのノードをクリックして設定してください", + "t_5_1744861189528": "始めます", + "t_6_1744861190121": "ノードを選択していない", + "t_7_1744861189625": "設定が保存されました", + "t_8_1744861189821": "ワークフローの開始", + "t_9_1744861189580": "選択ノード:", + "t_0_1744870861464": "ノード", + "t_1_1744870861944": "ノード設定", + "t_2_1744870863419": "左側のノードを選択して設定してください", + "t_3_1744870864615": "このノードタイプの設定コンポーネントが見つかりませんでした", + "t_4_1744870861589": "キャンセル", + "t_5_1744870862719": "確定", + "t_0_1744875938285": "分ごとに", + "t_1_1744875938598": "毎時間", + "t_2_1744875938555": "毎日", + "t_3_1744875938310": "毎月", + "t_4_1744875940750": "自動実行", + "t_5_1744875940010": "手動実行", + "t_0_1744879616135": "テストPID", + "t_1_1744879616555": "テストPIDを入力してください", + "t_2_1744879616413": "実行サイクル", + "t_3_1744879615723": "分", + "t_4_1744879616168": "分を入力してください", + "t_5_1744879615277": "時間", + "t_6_1744879616944": "時間を入力してください", + "t_7_1744879615743": "日付", + "t_8_1744879616493": "日付を選択してください", + "t_0_1744942117992": "毎週", + "t_1_1744942116527": "月曜日", + "t_2_1744942117890": "火曜日", + "t_3_1744942117885": "水曜日", + "t_4_1744942117738": "木曜日", + "t_5_1744942117167": "金曜日", + "t_6_1744942117815": "土曜日", + "t_7_1744942117862": "日曜日", + "t_0_1744958839535": "ドメイン名を入力してください", + "t_1_1744958840747": "メールを入力してください", + "t_2_1744958840131": "メールフォーマットが不正です", + "t_3_1744958840485": "DNSプロバイダーの認証を選択してください", + "t_4_1744958838951": "ローカルデプロイメント", + "t_5_1744958839222": "SSHデプロイメント", + "t_6_1744958843569": "宝塔パネル/1パネル(パネル証明書にデプロイ)", + "t_7_1744958841708": "1パネル(指定のウェブサイトプロジェクトにデプロイ)", + "t_8_1744958841658": "テンセントクラウドCDN/アリクラウドCDN", + "t_9_1744958840634": "腾讯クラウドWAF", + "t_10_1744958860078": "アリクラウドWAF", + "t_11_1744958840439": "この自動申請証明書", + "t_12_1744958840387": "オプションの証明書リスト", + "t_13_1744958840714": "PEM(*.pem、*.crt、*.key)", + "t_14_1744958839470": "PFX(*.pfx)", + "t_15_1744958840790": "JKS (*.jks)", + "t_16_1744958841116": "POSIX bash(Linux/macOS)", + "t_17_1744958839597": "コマンドライン(Windows)", + "t_18_1744958839895": "PowerShell(ウィンドウズ)", + "t_19_1744958839297": "証明書1", + "t_20_1744958839439": "証明書2", + "t_21_1744958839305": "サーバー1", + "t_22_1744958841926": "サーバー2", + "t_23_1744958838717": "パネル1", + "t_29_1744958838904": "日", + "t_30_1744958843864": "証明書のフォーマットが不正です。完全な証明書のヘッダおよびフッタ識別子が含まれているか確認してください。", + "t_31_1744958844490": "プライベートキーフォーマットが不正です。完全なプライベートキーヘッダおよびフッタ識別子が含まれているか確認してください。", + "t_0_1745215914686": "自動化名前", + "t_2_1745215915397": "自動", + "t_3_1745215914237": "手動", + "t_4_1745215914951": "有効状態", + "t_5_1745215914671": "有効にする", + "t_6_1745215914104": "停止", + "t_7_1745215914189": "作成時間", + "t_8_1745215914610": "操作", + "t_9_1745215914666": "実行履歴", + "t_10_1745215914342": "実行", + "t_11_1745215915429": "編集", + "t_12_1745215914312": "削除", + "t_13_1745215915455": "ワークフローの実行", + "t_14_1745215916235": "ワークフローエグゼクション成功", + "t_15_1745215915743": "ワークフローエクセキュション失敗", + "t_16_1745215915209": "ワークフローを削除する", + "t_17_1745215915985": "ワークフローの削除が成功しました", + "t_18_1745215915630": "ワークフローの削除に失敗しました", + "t_1_1745227838776": "自動化名前を入力してください", + "t_2_1745227839794": "{name}ワークフローの実行を確認しますか?", + "t_3_1745227841567": "{name}のワークフローの削除を確認しますか?この操作は元に戻せません。", + "t_4_1745227838558": "実行時間", + "t_5_1745227839906": "終了時間", + "t_6_1745227838798": "実行方法", + "t_7_1745227838093": "状態", + "t_8_1745227838023": "成功", + "t_9_1745227838305": "失敗", + "t_10_1745227838234": "実行中", + "t_11_1745227838422": "不明", + "t_12_1745227838814": "詳細", + "t_13_1745227838275": "証明書のアップロード", + "t_14_1745227840904": "証明書ドメイン名またはブランド名を入力して検索してください", + "t_15_1745227839354": "共同に", + "t_16_1745227838930": "本", + "t_17_1745227838561": "ドメイン名", + "t_18_1745227838154": "ブランド", + "t_19_1745227839107": "残り日数", + "t_20_1745227838813": "期限時間", + "t_21_1745227837972": "出典", + "t_22_1745227838154": "自動申請", + "t_23_1745227838699": "手動アップロード", + "t_24_1745227839508": "時間を追加", + "t_25_1745227838080": "ダウンロード", + "t_27_1745227838583": "切れ替わります", + "t_28_1745227837903": "通常", + "t_29_1745227838410": "証明書を削除する", + "t_30_1745227841739": "この証明書を削除してもよろしいですか?この操作は元に戻せません。", + "t_31_1745227838461": "確認してください", + "t_32_1745227838439": "証明書名前", + "t_33_1745227838984": "証明書の名前を入力してください", + "t_34_1745227839375": "証明書の内容(PEM)", + "t_35_1745227839208": "証明書の内容を入力してください", + "t_36_1745227838958": "プライベートキー内容(KEY)", + "t_37_1745227839669": "プライベートキーの内容を入力してください", + "t_38_1745227838813": "ダウンロード失敗", + "t_39_1745227838696": "アップロードに失敗しました", + "t_40_1745227838872": "削除失敗", + "t_0_1745289355714": "認証APIを追加する", + "t_1_1745289356586": "認証APIの名前またはタイプを入力してください", + "t_2_1745289353944": "名称", + "t_3_1745289354664": "認証APIタイプ", + "t_4_1745289354902": "編集権限API", + "t_5_1745289355718": "認証APIの削除", + "t_6_1745289358340": "この認証されたAPIを削除してもよろしいですか?この操作は元に戻すことができません。", + "t_7_1745289355714": "追加失敗", + "t_8_1745289354902": "アップデート失敗", + "t_9_1745289355714": "{days}日経過", + "t_10_1745289354650": "監視管理", + "t_11_1745289354516": "監視を追加する", + "t_12_1745289356974": "監視名前缀またはドメインを入力して検索してください", + "t_13_1745289354528": "モニタ名称", + "t_14_1745289354902": "証明書ドメイン", + "t_15_1745289355714": "証明書発行機関", + "t_16_1745289354902": "証明書の状態", + "t_17_1745289355715": "証明書の有効期限", + "t_18_1745289354598": "警報チャネル", + "t_19_1745289354676": "最後のチェック時刻", + "t_20_1745289354598": "編集監視", + "t_21_1745289354598": "削除を確認してください", + "t_22_1745289359036": "削除後は復元できません。この監視を削除する場合は確定しますか?", + "t_23_1745289355716": "変更失敗", + "t_24_1745289355715": "設定失敗", + "t_25_1745289355721": "認証コードを入力してください", + "t_26_1745289358341": "フォームのバリデーションに失敗しました、記入内容を確認してください", + "t_27_1745289355721": "認証API名前を入力してください", + "t_28_1745289356040": "認証APIタイプを選択してください", + "t_29_1745289355850": "サーバーIPを入力してください", + "t_30_1745289355718": "SSHポートを入力してください", + "t_31_1745289355715": "SSHキーを入力してください", + "t_32_1745289356127": "宝塔アドレスを入力してください", + "t_33_1745289355721": "APIキーを入力してください", + "t_34_1745289356040": "1panelのアドレスを入力してください", + "t_35_1745289355714": "AccessKeyIdを入力してください", + "t_36_1745289355715": "AccessKeySecretを入力してください", + "t_37_1745289356041": "SecretIdを入力してください", + "t_38_1745289356419": "SecretKeyを入力してください", + "t_39_1745289354902": "更新成功", + "t_40_1745289355715": "追加成功", + "t_41_1745289354902": "タイプ", + "t_42_1745289355715": "サーバーIP", + "t_43_1745289354598": "SSHポート", + "t_44_1745289354583": "ユーザー名", + "t_45_1745289355714": "認証方法", + "t_46_1745289355723": "パスワード認証", + "t_47_1745289355715": "キー認証", + "t_48_1745289355714": "パスワード", + "t_49_1745289355714": "SSHプライベートキー", + "t_50_1745289355715": "SSHプライベートキーを入力してください", + "t_51_1745289355714": "プライベートキーワード", + "t_52_1745289359565": "プライベートキーにパスワードがある場合、入力してください", + "t_53_1745289356446": "宝塔パネルのアドレス", + "t_54_1745289358683": "宝塔パネルのアドレスを入力してください、例えば:https://bt.example.com", + "t_55_1745289355715": "APIキー", + "t_56_1745289355714": "1パネルのアドレス", + "t_57_1745289358341": "1panelのアドレスを入力してください、例えば:https://1panel.example.com", + "t_58_1745289355721": "アクセスキーIDを入力してください", + "t_59_1745289356803": "アクセスキーのシークレットを入力してください", + "t_60_1745289355715": "監視名前を入力してください", + "t_61_1745289355878": "ドメイン/IPを入力してください", + "t_62_1745289360212": "検査サイクルを選択してください", + "t_63_1745289354897": "5分", + "t_64_1745289354670": "10分", + "t_65_1745289354591": "15分", + "t_66_1745289354655": "30分", + "t_67_1745289354487": "60分", + "t_68_1745289354676": "メール", + "t_69_1745289355721": "ショートメッセージ", + "t_70_1745289354904": "ライン", + "t_71_1745289354583": "ドメイン/IP", + "t_72_1745289355715": "検査サイクル", + "t_73_1745289356103": "警報チャンネルを選択してください", + "t_0_1745289808449": "認証APIの名前を入力してください", + "t_0_1745294710530": "監視を削除する", + "t_0_1745295228865": "更新時刻", + "t_0_1745317313835": "サーバーIPアドレスの形式が不正です", + "t_1_1745317313096": "ポートフォーマットエラー", + "t_2_1745317314362": "パネルURLアドレスの形式が不正です", + "t_3_1745317313561": "パネルAPIキーを入力してください", + "t_4_1745317314054": "阿里云アクセスキーIDを入力してください", + "t_5_1745317315285": "阿里云のAccessKeySecretを入力してください", + "t_6_1745317313383": "腾讯云SecretIdを入力してください", + "t_7_1745317313831": "腾讯雲のSecretKeyを入力してください", + "t_0_1745457486299": "有効", + "t_1_1745457484314": "停止しました", + "t_2_1745457488661": "手動モードに切り替え", + "t_3_1745457486983": "自動モードに切り替える", + "t_4_1745457497303": "手動モードに切り替えた後、ワークフローは自動的に実行されなくなりますが、手動で実行することは可能です", + "t_5_1745457494695": "自動モードに切り替えた後、ワークフローは設定された時間に従って自動的に実行されます", + "t_6_1745457487560": "現在のワークフローを閉じる", + "t_7_1745457487185": "現在のワークフローを有効にする", + "t_8_1745457496621": "閉じると、ワークフローは自動的に実行されなくなり、手動でも実行できません。続行しますか?", + "t_9_1745457500045": "有効にすると、ワークフロー設定が自動的に実行されるか、手動で実行されます。続行しますか?", + "t_10_1745457486451": "ワークフローの追加に失敗しました", + "t_11_1745457488256": "ワークフローの実行方法の設定に失敗しました", + "t_12_1745457489076": "ワークフローの失敗を有効または無効にする", + "t_13_1745457487555": "ワークフローの実行に失敗しました", + "t_14_1745457488092": "ワークフローの削除に失敗しました", + "t_15_1745457484292": "終了", + "t_16_1745457491607": "ログアウトしようとしています。ログアウトしますか?", + "t_17_1745457488251": "ログアウト中です、少々お待ちください...", + "t_18_1745457490931": "メール通知を追加", + "t_19_1745457484684": "保存が成功しました", + "t_20_1745457485905": "削除に成功しました", + "t_0_1745464080226": "システム設定の取得に失敗しました", + "t_1_1745464079590": "設定の保存に失敗しました", + "t_2_1745464077081": "通知設定の取得に失敗しました", + "t_3_1745464081058": "通知設定の保存に失敗しました", + "t_4_1745464075382": "通知チャネルリストの取得に失敗しました", + "t_5_1745464086047": "メール通知チャネルの追加に失敗しました", + "t_6_1745464075714": "通知チャネルの更新に失敗しました", + "t_7_1745464073330": "通知チャネルの削除に失敗しました", + "t_8_1745464081472": "バージョン更新の確認に失敗しました", + "t_9_1745464078110": "設定を保存", + "t_10_1745464073098": "基本設定", + "t_0_1745474945127": "テンプレートを選択", + "t_0_1745490735213": "ワークフロー名を入力してください", + "t_1_1745490731990": "設定", + "t_2_1745490735558": "メール形式を入力してください", + "t_3_1745490735059": "DNSプロバイダーを選択してください", + "t_4_1745490735630": "更新間隔を入力してください", + "t_5_1745490738285": "ドメイン名を入力してください。ドメイン名は空にできません", + "t_6_1745490738548": "メールアドレスを入力してください、メールアドレスは空にできません", + "t_7_1745490739917": "DNSプロバイダーを選択してください。DNSプロバイダーは空にできません", + "t_8_1745490739319": "更新間隔を入力してください。更新間隔は空にできません", + "t_1_1745553909483": "メールの形式が正しくありません。正しいメールアドレスを入力してください", + "t_2_1745553907423": "更新間隔は空にできません", + "t_0_1745735774005": "証明書のドメイン名を入力してください。複数のドメイン名はカンマで区切ります", + "t_1_1745735764953": "メールボックス", + "t_2_1745735773668": "証明書発行機関からのメール通知を受け取るためにメールアドレスを入力してください", + "t_3_1745735765112": "DNSプロバイダー", + "t_4_1745735765372": "追加", + "t_5_1745735769112": "更新間隔 (日)", + "t_6_1745735765205": "更新間隔", + "t_7_1745735768326": "日、期限切れ後に自動更新", + "t_8_1745735765753": "設定済み", + "t_9_1745735765287": "未設定", + "t_10_1745735765165": "パゴダパネル", + "t_11_1745735766456": "宝塔パネルのウェブサイト", + "t_12_1745735765571": "1Panelパネル", + "t_13_1745735766084": "1Panelウェブサイト", + "t_14_1745735766121": "Tencent Cloud CDN", + "t_15_1745735768976": "Tencent Cloud COS", + "t_16_1745735766712": "阿里雲CDN", + "t_18_1745735765638": "展開タイプ", + "t_19_1745735766810": "展開タイプを選択してください", + "t_20_1745735768764": "展開パスを入力してください", + "t_21_1745735769154": "前置コマンドを入力してください", + "t_22_1745735767366": "後置コマンドを入力してください", + "t_24_1745735766826": "サイトIDを入力してください", + "t_25_1745735766651": "地域を入力してください", + "t_26_1745735767144": "バケットを入力してください", + "t_27_1745735764546": "次のステップ", + "t_28_1745735766626": "展開タイプを選択", + "t_29_1745735768933": "展開パラメータを設定する", + "t_30_1745735764748": "動作モード", + "t_31_1745735767891": "動作モードが設定されていません", + "t_32_1745735767156": "実行周期が設定されていません", + "t_33_1745735766532": "実行時間が設定されていません", + "t_34_1745735771147": "証明書ファイル(PEM形式)", + "t_35_1745735781545": "証明書ファイルの内容を貼り付けてください。例:\n-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----", + "t_36_1745735769443": "秘密鍵ファイル(KEY 形式)", + "t_37_1745735779980": "秘密キーファイルの内容を貼り付けてください、例:\n-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----", + "t_38_1745735769521": "証明書の秘密鍵の内容は空にできません", + "t_39_1745735768565": "証明書の秘密鍵の形式が正しくありません", + "t_40_1745735815317": "証明書の内容は空にできません", + "t_41_1745735767016": "証明書の形式が正しくありません", + "t_0_1745738961258": "前へ", + "t_1_1745738963744": "提出", + "t_2_1745738969878": "展開パラメータを設定し、タイプによってパラメータの設定が決まる", + "t_0_1745744491696": "展開デバイスのソース", + "t_1_1745744495019": "展開デバイスのソースを選んでください", + "t_2_1745744495813": "展開タイプを選択して、次へをクリックしてください", + "t_0_1745744902975": "デプロイソース", + "t_1_1745744905566": "デプロイソースを選択してください", + "t_2_1745744903722": "さらにデバイスを追加", + "t_0_1745748292337": "デプロイソースの追加", + "t_1_1745748290291": "証明書の出所", + "t_2_1745748298902": "現在のタイプのデプロイソースが空です、デプロイソースを追加してください", + "t_3_1745748298161": "現在のプロセスには申請ノードがありません、まず申請ノードを追加してください", + "t_4_1745748290292": "提出内容", + "t_0_1745765864788": "ワークフロータイトルを編集するにはクリックします", + "t_1_1745765875247": "ノード削除 - 【{name}】", + "t_2_1745765875918": "現在のノードには子ノードが存在します。削除すると他のノードに影響を与えます。削除してもよろしいですか?", + "t_3_1745765920953": "現在のノードには設定データがあります。削除してもよろしいですか?", + "t_4_1745765868807": "デプロイメントタイプを選択してから、次に進んでください", + "t_0_1745833934390": "タイプを選択してください", + "t_1_1745833931535": "ホスト", + "t_2_1745833931404": "ポート", + "t_3_1745833936770": "ホームページの概要データの取得に失敗しました", + "t_4_1745833932780": "バージョン情報", + "t_5_1745833933241": "現在のバージョン", + "t_6_1745833933523": "更新方法", + "t_7_1745833933278": "最新バージョン", + "t_8_1745833933552": "更新履歴", + "t_9_1745833935269": "カスタマーサービスQRコード", + "t_10_1745833941691": "QRコードをスキャンしてカスタマーサービスを追加", + "t_11_1745833935261": "WeChat公式アカウント", + "t_12_1745833943712": "QRコードをスキャンしてWeChat公式アカウントをフォロー", + "t_13_1745833933630": "製品について", + "t_14_1745833932440": "SMTPサーバー", + "t_15_1745833940280": "SMTPサーバーを入力してください", + "t_16_1745833933819": "SMTPポート", + "t_17_1745833935070": "SMTPポートを入力してください", + "t_18_1745833933989": "SSL/TLS接続", + "t_0_1745887835267": "メッセージ通知を選択してください", + "t_1_1745887832941": "通知", + "t_2_1745887834248": "通知チャネルを追加", + "t_3_1745887835089": "通知の件名を入力してください", + "t_4_1745887835265": "通知内容を入力してください", + "t_0_1745895057404": "メール通知設定の変更", + "t_0_1745920566646": "通知主題", + "t_1_1745920567200": "通知内容", + "t_0_1745936396853": "確認コードを取得するにはクリックしてください", + "t_0_1745999035681": "残り{days}日", + "t_1_1745999036289": "まもなく期限切れ {days} 日", + "t_0_1746000517848": "期限切れ", + "t_0_1746001199409": "期限切れ", + "t_0_1746004861782": "DNSプロバイダーが空です", + "t_1_1746004861166": "DNSプロバイダーを追加", + "t_0_1746497662220": "更新", + "t_0_1746519384035": "実行中", + "t_0_1746579648713": "実行履歴の詳細", + "t_0_1746590054456": "実行状態", + "t_1_1746590060448": "トリガー方式", + "t_0_1746667592819": "情報を送信中、少々お待ちください...", + "t_1_1746667588689": "キー", + "t_2_1746667592840": "パネルURL", + "t_3_1746667592270": "SSL/TLS証明書のエラーを無視する", + "t_4_1746667590873": "フォーム検証失敗", + "t_5_1746667590676": "新しいワークフロー", + "t_6_1746667592831": "申請を提出しています、少々お待ちください...", + "t_7_1746667592468": "正しいドメイン名を入力してください", + "t_8_1746667591924": "解析方法を選択してください", + "t_9_1746667589516": "リストを更新", + "t_10_1746667589575": "ワイルドカード", + "t_11_1746667589598": "マルチドメイン", + "t_12_1746667589733": "人気", + "t_13_1746667599218": "広く使用されている無料のSSL証明書プロバイダーで、個人のウェブサイトやテスト環境に適しています。", + "t_14_1746667590827": "サポートされているドメインの数", + "t_15_1746667588493": "個", + "t_16_1746667591069": "ワイルドカードをサポート", + "t_17_1746667588785": "サポート", + "t_18_1746667590113": "サポートされていません", + "t_19_1746667589295": "有効期間", + "t_20_1746667588453": "天", + "t_21_1746667590834": "ミニプログラムをサポート", + "t_22_1746667591024": "対応サイト", + "t_23_1746667591989": "*.example.com、*.demo.com", + "t_24_1746667583520": "*.example.com", + "t_25_1746667590147": "example.com、demo.com", + "t_26_1746667594662": "www.example.com、example.com", + "t_27_1746667589350": "無料", + "t_28_1746667590336": "今すぐ申し込む", + "t_29_1746667589773": "プロジェクトアドレス", + "t_30_1746667591892": "証明書ファイルのパスを入力してください", + "t_31_1746667593074": "秘密鍵ファイルのパスを入力してください", + "t_0_1746673515941": "現在のDNSプロバイダーが空です。まずDNSプロバイダーを追加してください", + "t_0_1746676862189": "テスト通知の送信に失敗しました", + "t_1_1746676859550": "設定を追加", + "t_2_1746676856700": "まだサポートされていません", + "t_3_1746676857930": "メール通知", + "t_4_1746676861473": "メールでアラート通知を送信する", + "t_5_1746676856974": "DingTalk通知", + "t_6_1746676860886": "DingTalkロボットを通じてアラーム通知を送信する", + "t_7_1746676857191": "企業WeChat通知", + "t_8_1746676860457": "WeComボットでアラーム通知を送信", + "t_9_1746676857164": "Feishu通知", + "t_10_1746676862329": "飛書ロボットでアラーム通知を送信する", + "t_11_1746676859158": "WebHook通知", + "t_12_1746676860503": "WebHookを介してアラーム通知を送信する", + "t_13_1746676856842": "通知チャネル", + "t_14_1746676859019": "設定済みの通知チャネル", + "t_15_1746676856567": "無効化", + "t_16_1746676855270": "テスト", + "t_0_1746677882486": "最後の実行状態", + "t_0_1746697487119": "ドメイン名は空にできません", + "t_1_1746697485188": "メールアドレスは空にできません", + "t_2_1746697487164": "アリババクラウドOSS", + "t_0_1746754500246": "ホスティングプロバイダー", + "t_1_1746754499371": "APIソース", + "t_2_1746754500270": "APIタイプ", + "t_0_1746760933542": "リクエストエラー", + "t_0_1746773350551": "合計{0}件", + "t_1_1746773348701": "未実行", + "t_2_1746773350970": "自動化ワークフロー", + "t_3_1746773348798": "総数量", + "t_4_1746773348957": "実行に失敗しました", + "t_5_1746773349141": "まもなく期限切れ", + "t_6_1746773349980": "リアルタイム監視", + "t_7_1746773349302": "異常数量", + "t_8_1746773351524": "最近のワークフロー実行記録", + "t_9_1746773348221": "すべて表示", + "t_10_1746773351576": "ワークフロー実行記録がありません", + "t_11_1746773349054": "ワークフローの作成", + "t_12_1746773355641": "効率を向上させるために自動化されたワークフローを作成するにはクリックしてください", + "t_13_1746773349526": "証明書を申請する", + "t_14_1746773355081": "SSL証明書の申請と管理をクリックして、セキュリティを確保します", + "t_16_1746773356568": "最大で1つのメール通知チャネルしか設定できません", + "t_17_1746773351220": "{0}通知チャネルの確認", + "t_18_1746773355467": "{0}通知チャネルは、アラート通知の送信を開始します。", + "t_19_1746773352558": "現在の通知チャネルはテストをサポートしていません", + "t_20_1746773356060": "テストメールを送信しています、少々お待ちください...", + "t_21_1746773350759": "テストメール", + "t_22_1746773360711": "現在設定されているメールボックスにテストメールを送信します。続けますか?", + "t_23_1746773350040": "削除の確認", + "t_25_1746773349596": "名前を入力してください", + "t_26_1746773353409": "正しいSMTPポートを入力してください", + "t_27_1746773352584": "ユーザーパスワードを入力してください", + "t_28_1746773354048": "正しい送信者のメールアドレスを入力してください", + "t_29_1746773351834": "正しい受信メールを入力してください", + "t_30_1746773350013": "送信者のメール", + "t_31_1746773349857": "受信メール", + "t_32_1746773348993": "ディンタン", + "t_33_1746773350932": "WeChat Work", + "t_34_1746773350153": "飛書", + "t_35_1746773362992": "SSL証明書の申請、管理、展開、監視を統合したライフサイクル管理ツール。", + "t_36_1746773348989": "証明書申請", + "t_37_1746773356895": "ACMEプロトコルを介してLet's Encryptから証明書を取得する", + "t_38_1746773349796": "証明書管理", + "t_39_1746773358932": "すべてのSSL証明書を一元管理、手動アップロードおよび自動申請の証明書を含む", + "t_40_1746773352188": "証明書の展開", + "t_41_1746773364475": "ワンクリックでの証明書のデプロイを複数のプラットフォームでサポート、例えばアリババクラウド、テンセントクラウド、Pagoda Panel、1Panelなど", + "t_42_1746773348768": "サイト監視", + "t_43_1746773359511": "サイトのSSL証明書の状態をリアルタイムで監視し、証明書の有効期限切れを事前に警告します", + "t_44_1746773352805": "自動化タスク:", + "t_45_1746773355717": "スケジュールされたタスクをサポートし、証明書を自動的に更新して展開します", + "t_46_1746773350579": "マルチプラットフォーム対応", + "t_47_1746773360760": "複数のDNSプロバイダー(アリババクラウド、テンセントクラウドなど)のDNS検証方法をサポート", + "t_0_1746773763967": "{0}、通知チャネルを削除してもよろしいですか?", + "t_1_1746773763643": "Let's EncryptなどのCAが無料の証明書を自動的に申請する", + "t_0_1746776194126": "ログの詳細", + "t_1_1746776198156": "ロードログ失敗:", + "t_2_1746776194263": "ログをダウンロード", + "t_3_1746776195004": "ログ情報がありません", + "t_0_1746782379424": "自動化タスク", + "t_0_1746858920894": "ホスティングプロバイダーを選択してください", + "t_1_1746858922914": "DNSプロバイダーリストが空です、追加してください", + "t_2_1746858923964": "ホスティングプロバイダーのリストが空です、追加してください", + "t_3_1746858920060": "ホストプロバイダーを追加", + "t_4_1746858917773": "選択済み", + "t_0_1747019621052": "ホストプロバイダーを選択してください{0}", + "t_1_1747019624067": "クリックしてウェブサイト監視を設定し、リアルタイム状態を把握する", + "t_2_1747019616224": "アリババクラウド", + "t_3_1747019616129": "テンセントクラウド", + "t_0_1747040228657": "複数のドメインは英語のカンマで区切ってください。例:test.com,test.cn", + "t_1_1747040226143": "ワイルドカードドメインにはアスタリスク*を使用してください。例:*.test.com", + "t_0_1747042966820": "正しいCloudflare APIキーを入力してください", + "t_1_1747042969705": "正しい宝塔APIキーを入力してください", + "t_2_1747042967277": "正しいTencent Cloud SecretKeyを入力してください", + "t_3_1747042967608": "正しいHuawei Cloud SecretKeyを入力してください", + "t_4_1747042966254": "Huawei Cloud AccessKeyを入力してください", + "t_5_1747042965911": "正しいメールアカウントを入力してください", + "t_0_1747047213730": "自動デプロイの追加", + "t_1_1747047213009": "証明書を追加", + "t_2_1747047214975": "SSL証明書管理プラットフォーム", + "t_3_1747047218669": "ドメイン形式が間違っています、ドメイン形式を確認してください", + "t_0_1747106957037": "DNS再帰サーバー(オプション)", + "t_1_1747106961747": "DNS 再帰サーバーを入力してください(複数の値は,で区切ってください)", + "t_2_1747106957037": "ローカル事前チェックをスキップ", + "t_0_1747110184700": "証明書を選択", + "t_1_1747110191587": "証明書の内容とキーを変更する必要がある場合は、カスタム証明書を選択してください", + "t_2_1747110193465": "非カスタム証明書を選択した場合、証明書の内容とキーはどちらも変更できません", + "t_3_1747110185110": "アップロードして提出", + "t_0_1747215751189": "宝塔WAFウェブサイト", + "t_0_1747271295174": "Pagoda WAF - URL形式エラー", + "t_1_1747271295484": "宝塔WAF-APIキーを入力してください", + "t_2_1747271295877": "正しいHuaweiクラウドAccessKeyを入力してください", + "t_3_1747271294475": "正しい百度クラウドのAccessKeyを入力してください", + "t_4_1747271294621": "正しい百度クラウドのSecretKeyを入力してください", + "t_5_1747271291828": "宝塔WAF-URL", + "t_6_1747271296994": "ローカルデプロイ", + "t_7_1747271292060": "すべてのソース", + "t_8_1747271290414": "パゴダ", + "t_9_1747271284765": "1Panel", + "t_0_1747280814475": "SMTPポートの変更は禁止されています", + "t_1_1747280813656": "証明書ファイルのパス(PEM形式のみ対応)", + "t_2_1747280811593": "秘密鍵ファイルのパス", + "t_3_1747280812067": "前置コマンド(オプション)", + "t_4_1747280811462": "後置コマンド(オプション)", + "t_6_1747280809615": "サイトID", + "t_7_1747280808936": "地域", + "t_8_1747280809382": "バケット", + "t_9_1747280810169": "重複デプロイ", + "t_10_1747280816952": "前回の展開と同じ証明書で、前回の展開が成功した場合", + "t_11_1747280809178": "スキップ", + "t_12_1747280809893": "スキップしない", + "t_13_1747280810369": "再展開", + "t_14_1747280811231": "展開タイプを検索", + "t_0_1747296173751": "ウェブサイト名", + "t_1_1747296175494": "ウェブサイト名を入力してください", + "t_0_1747298114839": "雷池WAFサイト", + "t_1_1747298114192": "雷池WAF", + "t_0_1747300383756": "雷池WAF - URL形式エラー", + "t_1_1747300384579": "正しいBT-WAF APIキーを入力してください", + "t_2_1747300385222": "正しい雷池WAF-APIキーを入力してください", + "t_0_1747365600180": "Western Digitalのユーザー名を入力してください", + "t_1_1747365603108": "ウェスタンデジタルのパスワードを入力してください", + "t_3_1747365600828": "ボルケーノエンジンのAccessKeyを入力してください", + "t_4_1747365600137": "火山エンジンのSecretKeyを入力してください", + "t_0_1747367069267": "Pagoda dockerサイト", + "t_0_1747617113090": "雷池のAPIトークンを入力してください", + "t_1_1747617105179": "API Token", + "t_0_1747647014927": "証明書アルゴリズム", + "t_0_1747709067998": "SSHキーを入力してください。内容は空にできません。", + "t_0_1747711335067": "SSHパスワードを入力してください", + "t_1_1747711335336": "ホストアドレス", + "t_2_1747711337958": "ホストアドレスを入力してください。空にすることはできません", + "t_0_1747754231151": "ログビューア", + "t_1_1747754231838": "まず", + "t_2_1747754234999": "質問や提案がある場合はお気軽にどうぞ", + "t_3_1747754232000": "Githubでも私たちを見つけることができます", + "t_4_1747754235407": "あなたの参加はAllinSSLにとって非常に重要です、感謝します。", + "t_0_1747817614953": "入力してください", + "t_1_1747817639034": "はい", + "t_2_1747817610671": "いいえ", + "t_3_1747817612697": "ノードフィールドは必須です", + "t_4_1747817613325": "有効なドメイン名を入力してください", + "t_5_1747817619337": "有効なドメイン名を入力してください。複数のドメインは英語のコンマで区切ってください", + "t_6_1747817644358": "メールアドレスを入力してください", + "t_7_1747817613773": "有効なメールアドレスを入力してください", + "t_8_1747817614764": "ノードエラー", + "t_9_1747817611448": "ドメイン:", + "t_10_1747817611126": "申請", + "t_11_1747817612051": "展開", + "t_12_1747817611391": "アップロード", + "t_0_1747886301644": "メッセージプッシュ設定", + "t_1_1747886307276": "宝塔パネル - ウェブサイト", + "t_2_1747886302053": "1Panel-ウェブサイト", + "t_3_1747886302848": "宝塔WAF", + "t_4_1747886303229": "宝塔WAF-ウェブサイト", + "t_5_1747886301427": "Tencent Cloud EdgeOne", + "t_6_1747886301844": "七牛クラウド", + "t_7_1747886302395": "七牛雲-CDN", + "t_8_1747886304014": "七牛雲-OSS", + "t_9_1747886301128": "Huawei Cloud", + "t_10_1747886300958": "百度クラウド", + "t_11_1747886301986": "雷池", + "t_12_1747886302725": "雷池WAF-ウェブサイト", + "t_13_1747886301689": "ボルケーノエンジン", + "t_14_1747886301884": "ウェストデジタル", + "t_15_1747886301573": "プロジェクトタイプの展開", + "t_16_1747886308182": "ページを更新してもよろしいですか?データが失われる可能性があります!", + "t_0_1747895713179": "実行が成功しました", + "t_1_1747895712756": "実行中", + "t_0_1747903670020": "CA認証管理", + "t_2_1747903672640": "削除を確認", + "t_3_1747903672833": "このCA認証を削除してもよろしいですか?", + "t_4_1747903685371": "CA認証を追加", + "t_5_1747903671439": "ACME EAB KIDを入力してください", + "t_6_1747903672931": "ACME EAB HMACキーを入力してください", + "t_7_1747903678624": "CAプロバイダーを選択してください", + "t_8_1747903675532": "現在のCAプロバイダーが承認したエイリアス、迅速な識別に使用", + "t_9_1747903669360": "CAプロバイダー", + "t_10_1747903662994": "ACME EAB KID", + "t_11_1747903674802": "CAプロバイダーのACME EAB KIDを入力してください", + "t_12_1747903662994": "ACME EAB HMAC Key", + "t_13_1747903673007": "CAプロバイダーのACME EAM HMACを入力してください", + "t_0_1747904536291": "AllinSSL オープンソースで無料のSSL証明書自動管理プラットフォーム。すべてのSSL/TLS証明書をワンクリックで自動申請、更新、展開、監視し、マルチクラウド環境と複数のCAをサポート(coding~)、面倒な設定と高額な費用に別れを告げましょう。", + "t_0_1747965909665": "CA認証に使用するメールアドレスを入力してください", + "t_0_1747969933657": "端末展開", + "t_0_1747984137443": "正しいGoDaddy APIキーを入力してください", + "t_1_1747984133312": "GoDaddy APIシークレットを入力してください", + "t_2_1747984134626": "七牛クラウドのAccess Secretを入力してください", + "t_3_1747984134586": "Qiniu CloudのAccess Keyを入力してください", + "t_4_1747984130327": "コピー", + "t_5_1747984133112": "有効期限までの時間が", + "t_0_1747990228780": "証明書発行機関を選択してください", + "t_2_1747990228008": "CA認証データなし", + "t_3_1747990229599": "CA認証リストの取得に失敗しました", + "t_4_1747990227956": "自動更新(日)", + "t_5_1747990228592": "証明書の有効期間が未満", + "t_6_1747990228465": "タイミング、新しい証明書の更新", + "t_7_1747990227761": "プロキシアドレス(オプション)", + "t_8_1747990235316": "http または https プロキシアドレスのみサポートしています(例:http://proxy.example.com:8080)", + "t_9_1747990229640": "自動更新時間は空にできません", + "t_10_1747990232207": "ウェブサイト名を選択してください(複数選択可)", + "t_0_1747994891459": "宝塔dockerサイト", + "t_0_1748052857931": "証明機関/認証(オプション)", + "t_1_1748052860539": "Zerossl、Google、CA証明書認証を追加", + "t_2_1748052862259": "証明書確認メールを受け取るためのメール情報を入力してください" +} \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/locales/model/ptBR.json b/frontend/apps/allin-ssl/src/locales/model/ptBR.json new file mode 100644 index 0000000..5632a49 --- /dev/null +++ b/frontend/apps/allin-ssl/src/locales/model/ptBR.json @@ -0,0 +1,628 @@ +{ + "t_0_1744098811152": "Aviso: Você entrou em uma área desconhecida, a página que você está visitando não existe, por favor, clique no botão para voltar para a página inicial.", + "t_1_1744098801860": "Voltar para a homepage", + "t_2_1744098804908": "Dica de Segurança: Se você acha que isso é um erro, entre em contato com o administrador imediatamente", + "t_3_1744098802647": "Expandir o menu principal", + "t_4_1744098802046": "Menu principal dobrável", + "t_1_1744164835667": "AllinSSL", + "t_2_1744164839713": "Login de Conta", + "t_3_1744164839524": "Por favor, insira o nome de usuário", + "t_4_1744164840458": "Por favor, insira a senha", + "t_5_1744164840468": "Lembrar senha", + "t_6_1744164838900": "Esqueceu sua senha?", + "t_7_1744164838625": "Entrando", + "t_8_1744164839833": "Entrar", + "t_0_1744258111441": "Início", + "t_1_1744258113857": "Implantação Automatizada", + "t_2_1744258111238": "Gestão de Certificados", + "t_3_1744258111182": "Aplicação de certificado", + "t_4_1744258111238": "Gerenciamento de API de autorização", + "t_5_1744258110516": "Monitoramento", + "t_6_1744258111153": "Configurações", + "t_0_1744861190562": "Retornar lista de fluxos de trabalho", + "t_1_1744861189113": "Executar", + "t_2_1744861190040": "Salvar", + "t_3_1744861190932": "Selecione um nó para configurar", + "t_4_1744861194395": "Clique no nó do diagrama de workflow do lado esquerdo para configurá-lo", + "t_5_1744861189528": "iniciar", + "t_6_1744861190121": "Nenhum nó selected", + "t_7_1744861189625": "Configuração salva", + "t_8_1744861189821": "Iniciar fluxo de trabalho", + "t_9_1744861189580": "Nó selecionado:", + "t_0_1744870861464": "nó", + "t_1_1744870861944": "Configuração de nó", + "t_2_1744870863419": "Selecione o nó esquerdo para configuração", + "t_3_1744870864615": "Componente de configuração para esse tipo de nó não encontrado", + "t_4_1744870861589": "Cancelar", + "t_5_1744870862719": "confirmar", + "t_0_1744875938285": "a cada minuto", + "t_1_1744875938598": "a cada hora", + "t_2_1744875938555": "cada dia", + "t_3_1744875938310": "cada mês", + "t_4_1744875940750": "Execução automática", + "t_5_1744875940010": "Execução manual", + "t_0_1744879616135": "Teste PID", + "t_1_1744879616555": "Por favor, insira o PID de teste", + "t_2_1744879616413": "Período de execução", + "t_3_1744879615723": "minuto", + "t_4_1744879616168": "Por favor, insira os minutos", + "t_5_1744879615277": "hora", + "t_6_1744879616944": "Por favor, insira as horas", + "t_7_1744879615743": "Data", + "t_8_1744879616493": "Selecione a data", + "t_0_1744942117992": "cada semana", + "t_1_1744942116527": "segunda-feira", + "t_2_1744942117890": "terça-feira", + "t_3_1744942117885": "Quarta-feira", + "t_4_1744942117738": "quarta-feira", + "t_5_1744942117167": "quinta-feira", + "t_6_1744942117815": "sábado", + "t_7_1744942117862": "domingo", + "t_0_1744958839535": "Por favor, insira o nome do domínio", + "t_1_1744958840747": "Por favor, insira seu e-mail", + "t_2_1744958840131": "Formato de e-mail incorreto", + "t_3_1744958840485": "Selecione o provedor de DNS para autorização", + "t_4_1744958838951": "Implantação Local", + "t_5_1744958839222": "Desempenho SSH", + "t_6_1744958843569": "Painel Bota/1 painel (Instalar no certificado do painel)", + "t_7_1744958841708": "1painel (Deploiamento para o projeto de site especificado)", + "t_8_1744958841658": "Tencent Cloud CDN/AliCloud CDN", + "t_9_1744958840634": "WAF da Tencent Cloud", + "t_10_1744958860078": "Alicloud WAF", + "t_11_1744958840439": "Este certificado aplicado automaticamente", + "t_12_1744958840387": "Lista de certificados opcionais", + "t_13_1744958840714": "PEM (*.pem, *.crt, *.key)", + "t_14_1744958839470": "PFX (*.pfx)", + "t_15_1744958840790": "JKS (*.jks)", + "t_16_1744958841116": "POSIX bash (Linux/macOS)", + "t_17_1744958839597": "Linha de Comando (Windows)", + "t_18_1744958839895": "PowerShell (Windows)", + "t_19_1744958839297": "Certificado 1", + "t_20_1744958839439": "Certificado 2", + "t_21_1744958839305": "Servidor 1", + "t_22_1744958841926": "Servidor 2", + "t_23_1744958838717": "Painel 1", + "t_29_1744958838904": "dia", + "t_30_1744958843864": "O formato do certificado está incorreto, por favor verifique se ele contém os identificadores de cabeçalho e rodapé completos", + "t_31_1744958844490": "O formato da chave privada está incorreto, por favor, verifique se ele contém o identificador completo do cabeçalho e pé de página da chave privada", + "t_0_1745215914686": "Nome de automação", + "t_2_1745215915397": "automático", + "t_3_1745215914237": "Manual", + "t_4_1745215914951": "Estado ativado", + "t_5_1745215914671": "Ativar", + "t_6_1745215914104": "Desativar", + "t_7_1745215914189": "Hora de criação", + "t_8_1745215914610": "Operação", + "t_9_1745215914666": "Histórico de execução", + "t_10_1745215914342": "executar", + "t_11_1745215915429": "Editar", + "t_12_1745215914312": "Excluir", + "t_13_1745215915455": "Executar fluxo de trabalho", + "t_14_1745215916235": "Execução do fluxo de trabalho bem-sucedida", + "t_15_1745215915743": "Execução do fluxo de trabalho falhou", + "t_16_1745215915209": "Excluir workflow", + "t_17_1745215915985": "Deleção do fluxo de trabalho bem-sucedida", + "t_18_1745215915630": "Falha ao excluir fluxo de trabalho", + "t_1_1745227838776": "Por favor, insira o nome da automação", + "t_2_1745227839794": "Tem certeza de que deseja executar o workflow {name}?", + "t_3_1745227841567": "Confirma a exclusão do fluxo de trabalho {name}? Esta ação não pode ser revertida.", + "t_4_1745227838558": "Tempo de execução", + "t_5_1745227839906": "Hora de término", + "t_6_1745227838798": "Método de execução", + "t_7_1745227838093": "Status", + "t_8_1745227838023": "Sucesso", + "t_9_1745227838305": "fracasso", + "t_10_1745227838234": "Em andamento", + "t_11_1745227838422": "desconhecido", + "t_12_1745227838814": "Detalhes", + "t_13_1745227838275": "Enviar certificado", + "t_14_1745227840904": "Insira o nome do domínio do certificado ou o nome da marca para pesquisa", + "t_15_1745227839354": "juntos", + "t_16_1745227838930": "unidade", + "t_17_1745227838561": "Nome de domínio", + "t_18_1745227838154": "Marca", + "t_19_1745227839107": "Dias restantes", + "t_20_1745227838813": "Tempo de expiração", + "t_21_1745227837972": "Fonte", + "t_22_1745227838154": "Aplicação Automática", + "t_23_1745227838699": "Upload manual", + "t_24_1745227839508": "Adicionar tempo", + "t_25_1745227838080": "Baixar", + "t_27_1745227838583": "Próximo de expirar", + "t_28_1745227837903": "normal", + "t_29_1745227838410": "Excluir certificado", + "t_30_1745227841739": "Tem certeza de que deseja excluir este certificado? Esta ação não pode ser revertida.", + "t_31_1745227838461": "Confirmar", + "t_32_1745227838439": "Nome do Certificado", + "t_33_1745227838984": "Por favor, insira o nome do certificado", + "t_34_1745227839375": "Conteúdo do certificado (PEM)", + "t_35_1745227839208": "Por favor, insira o conteúdo do certificado", + "t_36_1745227838958": "Conteúdo da chave privada (KEY)", + "t_37_1745227839669": "Por favor, insira o conteúdo da chave privada", + "t_38_1745227838813": "Falha ao baixar", + "t_39_1745227838696": "Falha ao carregar", + "t_40_1745227838872": "Falha na exclusão", + "t_0_1745289355714": "Adicionar API de autorização", + "t_1_1745289356586": "Por favor, insira o nome ou o tipo do API autorizado", + "t_2_1745289353944": "Nome", + "t_3_1745289354664": "Tipo de API de autorização", + "t_4_1745289354902": "API de autorização de edição", + "t_5_1745289355718": "Remover API de autorização", + "t_6_1745289358340": "Tem certeza de que deseja excluir este API autorizado? Esta ação não pode ser revertida.", + "t_7_1745289355714": "Falha ao adicionar", + "t_8_1745289354902": "Falha na atualização", + "t_9_1745289355714": "Expirado há {days} dias", + "t_10_1745289354650": "Gestão de Monitoramento", + "t_11_1745289354516": "Adicionar monitoramento", + "t_12_1745289356974": "Por favor, insira o nome do monitoramento ou o domínio para pesquisar", + "t_13_1745289354528": "Nome do Monitor", + "t_14_1745289354902": "Domínio do certificado", + "t_15_1745289355714": "Autoridade de Certificação", + "t_16_1745289354902": "Status do certificado", + "t_17_1745289355715": "Data de expiração do certificado", + "t_18_1745289354598": "Canais de alerta", + "t_19_1745289354676": "Última data de verificação", + "t_20_1745289354598": "Edição de Monitoramento", + "t_21_1745289354598": "Confirmar exclusão", + "t_22_1745289359036": "Os itens não podem ser restaurados após a exclusão. Tem certeza de que deseja excluir este monitor?", + "t_23_1745289355716": "Falha na modificação", + "t_24_1745289355715": "Falha na configuração", + "t_25_1745289355721": "Por favor, insira o código de verificação", + "t_26_1745289358341": "Validação do formulário falhou, por favor, verifique o conteúdo preenchido", + "t_27_1745289355721": "Por favor, insira o nome do API autorizado", + "t_28_1745289356040": "Selecione o tipo de API de autorização", + "t_29_1745289355850": "Por favor, insira o IP do servidor", + "t_30_1745289355718": "Por favor, insira a porta SSH", + "t_31_1745289355715": "Por favor, insira a chave SSH", + "t_32_1745289356127": "Por favor, insira o endereço do Baota", + "t_33_1745289355721": "Por favor, insira a chave da API", + "t_34_1745289356040": "Por favor, insira o endereço do 1panel", + "t_35_1745289355714": "Por favor, insira AccessKeyId", + "t_36_1745289355715": "Por favor, insira AccessKeySecret", + "t_37_1745289356041": "Por favor, insira SecretId", + "t_38_1745289356419": "Por favor, insira a Chave Secreta", + "t_39_1745289354902": "Atualização bem-sucedida", + "t_40_1745289355715": "Adição bem-sucedida", + "t_41_1745289354902": "Tipo", + "t_42_1745289355715": "IP do Servidor", + "t_43_1745289354598": "Porta SSH", + "t_44_1745289354583": "Nome de usuário", + "t_45_1745289355714": "Método de autenticação", + "t_46_1745289355723": "Autenticação por senha", + "t_47_1745289355715": "Autenticação de chave", + "t_48_1745289355714": "Senha", + "t_49_1745289355714": "Chave privada SSH", + "t_50_1745289355715": "Por favor, insira a chave privada SSH", + "t_51_1745289355714": "Senha da chave privada", + "t_52_1745289359565": "Se a chave privada tiver uma senha, insira", + "t_53_1745289356446": "Endereço da tela BaoTa", + "t_54_1745289358683": "Por favor, insira o endereço do painel Baota, por exemplo: https://bt.example.com", + "t_55_1745289355715": "Chave API", + "t_56_1745289355714": "Endereço do painel 1", + "t_57_1745289358341": "Insira o endereço do 1panel, por exemplo: https://1panel.example.com", + "t_58_1745289355721": "Insira o ID do AccessKey", + "t_59_1745289356803": "Por favor, insira o segredo do AccessKey", + "t_60_1745289355715": "Por favor, insira o nome do monitoramento", + "t_61_1745289355878": "Por favor, insira o domínio/IP", + "t_62_1745289360212": "Selecione o período de inspeção", + "t_63_1745289354897": "5 minutos", + "t_64_1745289354670": "10 minutos", + "t_65_1745289354591": "15 minutos", + "t_66_1745289354655": "30 minutos", + "t_67_1745289354487": "60 minutos", + "t_68_1745289354676": "E-mail", + "t_69_1745289355721": "SMS", + "t_70_1745289354904": "WeChat", + "t_71_1745289354583": "Domínio/IP", + "t_72_1745289355715": "Período de inspeção", + "t_73_1745289356103": "Selecione o canal de alerta", + "t_0_1745289808449": "Por favor, insira o nome do API autorizado", + "t_0_1745294710530": "Excluir monitoramento", + "t_0_1745295228865": "Data de atualização", + "t_0_1745317313835": "Endereço IP do servidor está no formato incorreto", + "t_1_1745317313096": "Erro de formato de porta", + "t_2_1745317314362": "Formato de endereço da URL da página do painel incorreto", + "t_3_1745317313561": "Por favor, insira a chave API da panela", + "t_4_1745317314054": "Por favor, insira o AccessKeyId da Aliyun", + "t_5_1745317315285": "Por favor, insira o AccessKeySecret da Aliyun", + "t_6_1745317313383": "Por favor, insira o SecretId do Tencent Cloud", + "t_7_1745317313831": "Por favor, insira a SecretKey da Tencent Cloud", + "t_0_1745457486299": "Ativado", + "t_1_1745457484314": "Parado", + "t_2_1745457488661": "Mudar para o modo manual", + "t_3_1745457486983": "Mudar para o modo automático", + "t_4_1745457497303": "Ao mudar para o modo manual, o fluxo de trabalho não será mais executado automaticamente, mas ainda pode ser executado manualmente", + "t_5_1745457494695": "Após mudar para o modo automático, o fluxo de trabalho será executado automaticamente de acordo com o tempo configurado", + "t_6_1745457487560": "Fechar fluxo de trabalho atual", + "t_7_1745457487185": "Ativar fluxo de trabalho atual", + "t_8_1745457496621": "Após o fechamento, o fluxo de trabalho não será mais executado automaticamente e não poderá ser executado manualmente. Continuar?", + "t_9_1745457500045": "Após ativar, a configuração do fluxo de trabalho será executada automaticamente ou manualmente. Continuar?", + "t_10_1745457486451": "Falha ao adicionar fluxo de trabalho", + "t_11_1745457488256": "Falha ao definir o método de execução do fluxo de trabalho", + "t_12_1745457489076": "Ativar ou desativar falha no fluxo de trabalho", + "t_13_1745457487555": "Falha ao executar o fluxo de trabalho", + "t_14_1745457488092": "Falha ao excluir fluxo de trabalho", + "t_15_1745457484292": "Sair", + "t_16_1745457491607": "Você está prestes a sair. Tem certeza de que deseja sair?", + "t_17_1745457488251": "Saindo da conta, por favor aguarde...", + "t_18_1745457490931": "Adicionar notificação por e-mail", + "t_19_1745457484684": "Salvo com sucesso", + "t_20_1745457485905": "Excluído com sucesso", + "t_0_1745464080226": "Falha ao obter as configurações do sistema", + "t_1_1745464079590": "Falha ao salvar configurações", + "t_2_1745464077081": "Falha ao obter configurações de notificação", + "t_3_1745464081058": "Falha ao salvar configurações de notificação", + "t_4_1745464075382": "Falha ao obter a lista de canais de notificação", + "t_5_1745464086047": "Falha ao adicionar canal de notificação por e-mail", + "t_6_1745464075714": "Falha ao atualizar o canal de notificação", + "t_7_1745464073330": "Falha ao excluir o canal de notificação", + "t_8_1745464081472": "Falha ao verificar atualização de versão", + "t_9_1745464078110": "Salvar configurações", + "t_10_1745464073098": "Configurações básicas", + "t_0_1745474945127": "Escolher modelo", + "t_0_1745490735213": "Por favor, insira o nome do fluxo de trabalho", + "t_1_1745490731990": "Configuração", + "t_2_1745490735558": "Por favor, insira o formato de e-mail", + "t_3_1745490735059": "Por favor, selecione um provedor de DNS", + "t_4_1745490735630": "Por favor, insira o intervalo de renovação", + "t_5_1745490738285": "Digite o nome de domínio, o nome de domínio não pode estar vazio", + "t_6_1745490738548": "Por favor, insira o e-mail, o e-mail não pode estar vazio", + "t_7_1745490739917": "Por favor, selecione um provedor DNS, o provedor DNS não pode estar vazio", + "t_8_1745490739319": "Insira o intervalo de renovação, o intervalo de renovação não pode estar vazio", + "t_1_1745553909483": "Formato de e-mail inválido, por favor insira um e-mail correto", + "t_2_1745553907423": "O intervalo de renovação não pode estar vazio", + "t_0_1745735774005": "Digite o nome de domínio do certificado, vários nomes de domínio separados por vírgulas", + "t_1_1745735764953": "Caixa de correio", + "t_2_1745735773668": "Digite seu e-mail para receber notificações da autoridade certificadora", + "t_3_1745735765112": "Provedor de DNS", + "t_4_1745735765372": "Adicionar", + "t_5_1745735769112": "Intervalo de Renovação (Dias)", + "t_6_1745735765205": "Intervalo de renovação", + "t_7_1745735768326": "dias, renovado automaticamente após o vencimento", + "t_8_1745735765753": "Configurado", + "t_9_1745735765287": "Não configurado", + "t_10_1745735765165": "Painel Pagode", + "t_11_1745735766456": "Site do Painel Pagoda", + "t_12_1745735765571": "Painel 1Panel", + "t_13_1745735766084": "1Panel site", + "t_14_1745735766121": "Tencent Cloud CDN", + "t_15_1745735768976": "Tencent Cloud COS", + "t_16_1745735766712": "Alibaba Cloud CDN", + "t_18_1745735765638": "Tipo de Implantação", + "t_19_1745735766810": "Por favor, selecione o tipo de implantação", + "t_20_1745735768764": "Por favor, insira o caminho de implantação", + "t_21_1745735769154": "Por favor, insira o comando de prefixo", + "t_22_1745735767366": "Por favor, insira o comando pós", + "t_24_1745735766826": "Por favor, insira o ID do site", + "t_25_1745735766651": "Por favor, insira a região", + "t_26_1745735767144": "Por favor, insira o balde", + "t_27_1745735764546": "Próximo passo", + "t_28_1745735766626": "Selecionar tipo de implantação", + "t_29_1745735768933": "Configurar parâmetros de implantação", + "t_30_1745735764748": "Modo de operação", + "t_31_1745735767891": "Modo de operação não configurado", + "t_32_1745735767156": "Ciclo de execução não configurado", + "t_33_1745735766532": "Tempo de execução não configurado", + "t_34_1745735771147": "Arquivo de certificado (formato PEM)", + "t_35_1745735781545": "Por favor, cole o conteúdo do arquivo de certificado, por exemplo:\n-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----", + "t_36_1745735769443": "Arquivo de chave privada (formato KEY)", + "t_37_1745735779980": "Cole o conteúdo do arquivo de chave privada, por exemplo:\n-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----", + "t_38_1745735769521": "O conteúdo da chave privada do certificado não pode estar vazio", + "t_39_1745735768565": "O formato da chave privada do certificado está incorreto", + "t_40_1745735815317": "O conteúdo do certificado não pode estar vazio", + "t_41_1745735767016": "Formato do certificado incorreto", + "t_0_1745738961258": "Anterior", + "t_1_1745738963744": "Enviar", + "t_2_1745738969878": "Configurar parâmetros de implantação, o tipo determina a configuração do parâmetro", + "t_0_1745744491696": "Fonte do dispositivo de implantação", + "t_1_1745744495019": "Selecione a fonte do dispositivo de implantação", + "t_2_1745744495813": "Por favor, selecione o tipo de implantação e clique em Avançar", + "t_0_1745744902975": "Fonte de implantação", + "t_1_1745744905566": "Selecione a fonte de implantação", + "t_2_1745744903722": "Adicionar mais dispositivos", + "t_0_1745748292337": "Adicionar fonte de implantação", + "t_1_1745748290291": "Fonte do Certificado", + "t_2_1745748298902": "A origem da implantação do tipo atual está vazia, adicione uma origem de implantação primeiro", + "t_3_1745748298161": "Não há nó de aplicação no processo atual, por favor, adicione um nó de aplicação primeiro", + "t_4_1745748290292": "Enviar conteúdo", + "t_0_1745765864788": "Clique para editar o título do fluxo de trabalho", + "t_1_1745765875247": "Excluir Nó - 【{name}】", + "t_2_1745765875918": "O nó atual possui nós filhos. A exclusão afetará outros nós. Tem certeza de que deseja excluir?", + "t_3_1745765920953": "O nó atual possui dados de configuração, tem certeza que deseja excluí-lo?", + "t_4_1745765868807": "Por favor, selecione o tipo de implantação antes de prosseguir para a próxima etapa", + "t_0_1745833934390": "Por favor, selecione o tipo", + "t_1_1745833931535": "Host", + "t_2_1745833931404": "porta", + "t_3_1745833936770": "Falha ao obter dados de visão geral da página inicial", + "t_4_1745833932780": "Informações da versão", + "t_5_1745833933241": "Versão atual", + "t_6_1745833933523": "Método de atualização", + "t_7_1745833933278": "Última versão", + "t_8_1745833933552": "Registro de alterações", + "t_9_1745833935269": "Código QR do Atendimento ao Cliente", + "t_10_1745833941691": "Escaneie o código QR para adicionar atendimento ao cliente", + "t_11_1745833935261": "Conta Oficial do WeChat", + "t_12_1745833943712": "Escaneie para seguir a conta oficial do WeChat", + "t_13_1745833933630": "Sobre o produto", + "t_14_1745833932440": "Servidor SMTP", + "t_15_1745833940280": "Por favor, insira o servidor SMTP", + "t_16_1745833933819": "Porta SMTP", + "t_17_1745833935070": "Por favor, insira a porta SMTP", + "t_18_1745833933989": "Conexão SSL/TLS", + "t_0_1745887835267": "Por favor, selecione notificação de mensagem", + "t_1_1745887832941": "Notificação", + "t_2_1745887834248": "Adicionar canal de notificação", + "t_3_1745887835089": "Digite o assunto da notificação", + "t_4_1745887835265": "Por favor, insira o conteúdo da notificação", + "t_0_1745895057404": "Modificar configurações de notificação por e-mail", + "t_0_1745920566646": "Assunto da Notificação", + "t_1_1745920567200": "Conteúdo da notificação", + "t_0_1745936396853": "Clique para obter o código de verificação", + "t_0_1745999035681": "faltam {days} dias", + "t_1_1745999036289": "Expirando em breve {days} dias", + "t_0_1746000517848": "Expirado", + "t_0_1746001199409": "Expirado", + "t_0_1746004861782": "Provedor DNS está vazio", + "t_1_1746004861166": "Adicionar provedor de DNS", + "t_0_1746497662220": "Atualizar", + "t_0_1746519384035": "Em execução", + "t_0_1746579648713": "Detalhes do Histórico de Execução", + "t_0_1746590054456": "Status de execução", + "t_1_1746590060448": "Método de Ativação", + "t_0_1746667592819": "Enviando informações, por favor aguarde...", + "t_1_1746667588689": "Chave", + "t_2_1746667592840": "URL do painel", + "t_3_1746667592270": "Ignorar erros de certificado SSL/TLS", + "t_4_1746667590873": "Validação de formulário falhou", + "t_5_1746667590676": "Novo fluxo de trabalho", + "t_6_1746667592831": "Enviando aplicação, por favor aguarde...", + "t_7_1746667592468": "Por favor, insira o nome de domínio correto", + "t_8_1746667591924": "Por favor, selecione o método de análise", + "t_9_1746667589516": "Atualizar lista", + "t_10_1746667589575": "Curinga", + "t_11_1746667589598": "Multidomínio", + "t_12_1746667589733": "Popular", + "t_13_1746667599218": "é um fornecedor de certificados SSL gratuito amplamente utilizado, adequado para sites pessoais e ambientes de teste.", + "t_14_1746667590827": "Número de domínios suportados", + "t_15_1746667588493": "peça", + "t_16_1746667591069": "Suporte a curingas", + "t_17_1746667588785": "suporte", + "t_18_1746667590113": "Não suportado", + "t_19_1746667589295": "Validade", + "t_20_1746667588453": "dia", + "t_21_1746667590834": "Suporte para Mini Programas", + "t_22_1746667591024": "Sites aplicáveis", + "t_23_1746667591989": "*.example.com, *.demo.com", + "t_24_1746667583520": "*.example.com", + "t_25_1746667590147": "example.com、demo.com", + "t_26_1746667594662": "www.example.com, example.com", + "t_27_1746667589350": "Grátis", + "t_28_1746667590336": "Aplicar agora", + "t_29_1746667589773": "Endereço do projeto", + "t_30_1746667591892": "Digite o caminho do arquivo de certificado", + "t_31_1746667593074": "Digite o caminho do arquivo de chave privada", + "t_0_1746673515941": "O provedor de DNS atual está vazio, adicione um provedor de DNS primeiro", + "t_0_1746676862189": "Falha no envio da notificação de teste", + "t_1_1746676859550": "Adicionar Configuração", + "t_2_1746676856700": "Ainda não suportado", + "t_3_1746676857930": "Notificação por e-mail", + "t_4_1746676861473": "Enviar notificações de alerta por e-mail", + "t_5_1746676856974": "Notificação DingTalk", + "t_6_1746676860886": "Enviar notificações de alarme via robô DingTalk", + "t_7_1746676857191": "Notificação do WeChat Work", + "t_8_1746676860457": "Enviar notificações de alarme via bot do WeCom", + "t_9_1746676857164": "Notificação Feishu", + "t_10_1746676862329": "Enviar notificações de alarme via bot Feishu", + "t_11_1746676859158": "Notificação WebHook", + "t_12_1746676860503": "Enviar notificações de alarme via WebHook", + "t_13_1746676856842": "Canal de notificação", + "t_14_1746676859019": "Canais de notificação configurados", + "t_15_1746676856567": "Desativado", + "t_16_1746676855270": "Teste", + "t_0_1746677882486": "Último status de execução", + "t_0_1746697487119": "O nome do domínio não pode estar vazio", + "t_1_1746697485188": "O e-mail não pode estar vazio", + "t_2_1746697487164": "Alibaba Cloud OSS", + "t_0_1746754500246": "Provedor de Hospedagem", + "t_1_1746754499371": "Fonte da API", + "t_2_1746754500270": "Tipo de API", + "t_0_1746760933542": "Erro de solicitação", + "t_0_1746773350551": "Total de {0} itens", + "t_1_1746773348701": "Não executado", + "t_2_1746773350970": "Fluxo de trabalho automatizado", + "t_3_1746773348798": "Quantidade total", + "t_4_1746773348957": "Falha na execução", + "t_5_1746773349141": "Expirando em breve", + "t_6_1746773349980": "Monitoramento em tempo real", + "t_7_1746773349302": "Quantidade anormal", + "t_8_1746773351524": "Registros recentes de execução de fluxo de trabalho", + "t_9_1746773348221": "Ver tudo", + "t_10_1746773351576": "Nenhum registro de execução de fluxo de trabalho", + "t_11_1746773349054": "Criar fluxo de trabalho", + "t_12_1746773355641": "Clique para criar um fluxo de trabalho automatizado para melhorar a eficiência", + "t_13_1746773349526": "Solicitar certificado", + "t_14_1746773355081": "Clique para solicitar e gerenciar certificados SSL para garantir segurança", + "t_16_1746773356568": "No máximo, apenas um canal de notificação por e-mail pode ser configurado", + "t_17_1746773351220": "Confirmar canal de notificação {0}", + "t_18_1746773355467": "Os canais de notificação {0} começarão a enviar alertas.", + "t_19_1746773352558": "O canal de notificação atual não suporta testes", + "t_20_1746773356060": "Enviando e-mail de teste, por favor aguarde...", + "t_21_1746773350759": "E-mail de teste", + "t_22_1746773360711": "Enviar um e-mail de teste para a caixa de correio configurada atualmente, continuar?", + "t_23_1746773350040": "Confirmação de exclusão", + "t_25_1746773349596": "Por favor, insira o nome", + "t_26_1746773353409": "Por favor, insira a porta SMTP correta", + "t_27_1746773352584": "Por favor, insira a senha do usuário", + "t_28_1746773354048": "Por favor, insira o e-mail do remetente correto", + "t_29_1746773351834": "Por favor, insira o e-mail de recebimento correto", + "t_30_1746773350013": "E-mail do remetente", + "t_31_1746773349857": "Receber E-mail", + "t_32_1746773348993": "DingTalk", + "t_33_1746773350932": "WeChat Work", + "t_34_1746773350153": "Feishu", + "t_35_1746773362992": "Uma ferramenta de gerenciamento do ciclo de vida completo de certificados SSL que integra solicitação, gerenciamento, implantação e monitoramento.", + "t_36_1746773348989": "Pedido de Certificado", + "t_37_1746773356895": "Suporte à obtenção de certificados do Let's Encrypt através do protocolo ACME", + "t_38_1746773349796": "Gerenciamento de Certificados", + "t_39_1746773358932": "Gerenciamento centralizado de todos os certificados SSL, incluindo certificados carregados manualmente e aplicados automaticamente", + "t_40_1746773352188": "Implantaçã de certificado", + "t_41_1746773364475": "Suporte à implantação de certificados com um clique em várias plataformas, como Alibaba Cloud, Tencent Cloud, Pagoda Panel, 1Panel, etc.", + "t_42_1746773348768": "Monitoramento do site", + "t_43_1746773359511": "Monitoramento em tempo real do status do certificado SSL do site para alertar sobre a expiração do certificado", + "t_44_1746773352805": "Tarefa automatizada:", + "t_45_1746773355717": "Suporta tarefas agendadas, renova automaticamente os certificados e implanta", + "t_46_1746773350579": "Suporte multiplataforma", + "t_47_1746773360760": "Suporta métodos de verificação DNS para vários provedores de DNS (Alibaba Cloud, Tencent Cloud, etc.)", + "t_0_1746773763967": "Tem certeza que deseja excluir {0}, o canal de notificação?", + "t_1_1746773763643": "Let's Encrypt e outras autoridades de certificação solicitam automaticamente certificados gratuitos", + "t_0_1746776194126": "Detalhes do Log", + "t_1_1746776198156": "Falha ao carregar o log:", + "t_2_1746776194263": "Baixar registro", + "t_3_1746776195004": "Sem informações de log", + "t_0_1746782379424": "Tarefas automatizadas", + "t_0_1746858920894": "Por favor, selecione um provedor de hospedagem", + "t_1_1746858922914": "A lista de provedores DNS está vazia, por favor adicione", + "t_2_1746858923964": "A lista de provedores de hospedagem está vazia, adicione", + "t_3_1746858920060": "Adicionar provedor de hospedagem", + "t_4_1746858917773": "Selecionado", + "t_0_1747019621052": "Selecione um provedor de hospedagem{0}", + "t_1_1747019624067": "Clique para configurar o monitoramento do site e acompanhar o status em tempo real", + "t_2_1747019616224": "Alibaba Cloud", + "t_3_1747019616129": "Tencent Cloud", + "t_0_1747040228657": "Para vários domínios, use vírgulas em inglês para separá-los, por exemplo: test.com,test.cn", + "t_1_1747040226143": "Para domínios curinga, use um asterisco *, por exemplo: *.test.com", + "t_0_1747042966820": "Por favor, insira a chave de API correta do Cloudflare", + "t_1_1747042969705": "Por favor, insira a chave de API correta do BT-Panel", + "t_2_1747042967277": "Por favor, insira o SecretKey correto do Tencent Cloud", + "t_3_1747042967608": "Por favor, insira o SecretKey correto da Huawei Cloud", + "t_4_1747042966254": "Por favor, insira o AccessKey da Huawei Cloud", + "t_5_1747042965911": "Por favor, insira a conta de email correta", + "t_0_1747047213730": "Adicionar implantação automatizada", + "t_1_1747047213009": "Adicionar certificado", + "t_2_1747047214975": "Plataforma de Gerenciamento de Certificados SSL", + "t_3_1747047218669": "Erro de formato de domínio, verifique o formato do domínio", + "t_0_1747106957037": "Servidor recursivo DNS (opcional)", + "t_1_1747106961747": "Digite os servidores DNS recursivos (use vírgulas para separar vários valores)", + "t_2_1747106957037": "Ignorar verificação prévia local", + "t_0_1747110184700": "Selecionar certificado", + "t_1_1747110191587": "Se precisar modificar o conteúdo do certificado e a chave, escolha um certificado personalizado", + "t_2_1747110193465": "Quando um certificado não personalizado é selecionado, nem o conteúdo do certificado nem a chave podem ser modificados", + "t_3_1747110185110": "Enviar e submeter", + "t_0_1747215751189": "Site do Pagoda WAF", + "t_0_1747271295174": "Pagoda WAF - Erro de formato de URL", + "t_1_1747271295484": "Por favor, insira a chave Pagoda WAF-API", + "t_2_1747271295877": "Por favor, insira o AccessKey correto da Huawei Cloud", + "t_3_1747271294475": "Por favor, insira o Baidu Cloud AccessKey correto", + "t_4_1747271294621": "Por favor, insira o SecretKey correto do Baidu Cloud", + "t_5_1747271291828": "Baota WAF-URL", + "t_6_1747271296994": "Implantação Local", + "t_7_1747271292060": "Todas as fontes", + "t_8_1747271290414": "Pagode", + "t_9_1747271284765": "1Panel", + "t_0_1747280814475": "A modificação da porta SMTP é proibida", + "t_1_1747280813656": "Caminho do arquivo de certificado (somente formato PEM)", + "t_2_1747280811593": "Caminho do arquivo de chave privada", + "t_3_1747280812067": "Comando prévio (opcional)", + "t_4_1747280811462": "Comando pós (opcional)", + "t_6_1747280809615": "ID do site", + "t_7_1747280808936": "Região", + "t_8_1747280809382": "Balde", + "t_9_1747280810169": "Implantações repetidas", + "t_10_1747280816952": "Quando o certificado é o mesmo da última implantação e a última implantação foi bem-sucedida", + "t_11_1747280809178": "Pular", + "t_12_1747280809893": "Não pular", + "t_13_1747280810369": "Reimplantação", + "t_14_1747280811231": "Pesquisar tipo de implantação", + "t_0_1747296173751": "Nome do site", + "t_1_1747296175494": "Por favor, insira o nome do site", + "t_0_1747298114839": "Site Leichi WAF", + "t_1_1747298114192": "Leichi WAF", + "t_0_1747300383756": "Leichi WAF - erro de formato de URL", + "t_1_1747300384579": "Por favor, insira a chave de API correta do BT-WAF", + "t_2_1747300385222": "Por favor, insira a chave correta do Leichi WAF-API", + "t_0_1747365600180": "Por favor, insira o nome de usuário da Western Digital", + "t_1_1747365603108": "Por favor, insira a senha da Western Digital", + "t_3_1747365600828": "Por favor, insira a AccessKey do Volcano Engine", + "t_4_1747365600137": "Por favor, insira o SecretKey do Volcano Engine", + "t_0_1747367069267": "Site Pagoda docker", + "t_0_1747617113090": "Por favor, insira o Token API do Leichi", + "t_1_1747617105179": "API Token", + "t_0_1747647014927": "Algoritmo de certificado", + "t_0_1747709067998": "Digite a chave SSH, o conteúdo não pode estar vazio", + "t_0_1747711335067": "Por favor, insira a senha SSH", + "t_1_1747711335336": "Endereço do host", + "t_2_1747711337958": "Por favor, insira o endereço do host, não pode estar vazio", + "t_0_1747754231151": "Visualizador de Logs", + "t_1_1747754231838": "Por favor, primeiro", + "t_2_1747754234999": "Se tiver alguma dúvida ou sugestão, sinta-se à vontade para apresentá-la", + "t_3_1747754232000": "Você também pode nos encontrar no Github", + "t_4_1747754235407": "Sua participação é extremamente importante para o AllinSSL, obrigado.", + "t_0_1747817614953": "Por favor, insira", + "t_1_1747817639034": "sim", + "t_2_1747817610671": "Não", + "t_3_1747817612697": "O campo do nó é obrigatório", + "t_4_1747817613325": "Por favor, digite um nome de domínio válido", + "t_5_1747817619337": "Por favor, insira um nome de domínio válido, separe vários domínios com vírgulas em inglês", + "t_6_1747817644358": "Por favor, digite seu endereço de e-mail", + "t_7_1747817613773": "Por favor, insira um endereço de e-mail válido", + "t_8_1747817614764": "Erro de nó", + "t_9_1747817611448": "Domínio:", + "t_10_1747817611126": "Aplicar", + "t_11_1747817612051": "Implantação", + "t_12_1747817611391": "Enviar", + "t_0_1747886301644": "Configuração de Push de Mensagem", + "t_1_1747886307276": "Painel Pagode - Site", + "t_2_1747886302053": "1Panel-Site", + "t_3_1747886302848": "Pagode WAF", + "t_4_1747886303229": "Pagode WAF-Site", + "t_5_1747886301427": "Tencent Cloud EdgeOne", + "t_6_1747886301844": "Qiniu Cloud", + "t_7_1747886302395": "Qiniu Cloud-CDN", + "t_8_1747886304014": "Qiniu Cloud - OSS", + "t_9_1747886301128": "Huawei Cloud", + "t_10_1747886300958": "Baidu Cloud", + "t_11_1747886301986": "Piscina de Trovão", + "t_12_1747886302725": "Leichi WAF-Site", + "t_13_1747886301689": "Motor Vulcão", + "t_14_1747886301884": "West Digital", + "t_15_1747886301573": "Tipo de projeto de implantação", + "t_16_1747886308182": "Tem certeza de que deseja atualizar a página? Os dados podem ser perdidos!", + "t_0_1747895713179": "Execução bem-sucedida", + "t_1_1747895712756": "Executando", + "t_0_1747903670020": "Gerenciamento de Autorização CA", + "t_2_1747903672640": "Confirmar exclusão", + "t_3_1747903672833": "Tem certeza de que deseja excluir esta autorização CA?", + "t_4_1747903685371": "Adicionar Autorização CA", + "t_5_1747903671439": "Por favor, insira ACME EAB KID", + "t_6_1747903672931": "Digite a chave HMAC ACME EAB", + "t_7_1747903678624": "Por favor, selecione o provedor CA", + "t_8_1747903675532": "O apelido autorizado pelo provedor CA atual para identificação rápida", + "t_9_1747903669360": "Provedor de CA", + "t_10_1747903662994": "ACME EAB KID", + "t_11_1747903674802": "Por favor, insira o ACME EAB KID fornecido pelo CA", + "t_12_1747903662994": "ACME EAB HMAC Key", + "t_13_1747903673007": "Digite o ACME EAM HMAC do provedor de CA", + "t_0_1747904536291": "AllinSSL, uma plataforma gratuita e de código aberto para gerenciamento automatizado de certificados SSL. Aplicação, renovação, implantação e monitoramento automatizados de todos os certificados SSL/TLS com um clique, suportando ambientes multicloud e várias CAs (coding~), diga adeus a configurações complicadas e custos elevados.", + "t_0_1747965909665": "Digite o e-mail para vincular a autorização CA", + "t_0_1747969933657": "Implantação de terminal", + "t_0_1747984137443": "Por favor, insira a chave de API correta do GoDaddy", + "t_1_1747984133312": "Por favor, insira o Segredo da API da GoDaddy", + "t_2_1747984134626": "Por favor, insira o Access Secret do Qiniu Cloud", + "t_3_1747984134586": "Digite a Access Key do Qiniu Cloud", + "t_4_1747984130327": "Copiar", + "t_5_1747984133112": "Quando o tempo de expiração está próximo", + "t_0_1747990228780": "Por favor, selecione a autoridade certificadora", + "t_2_1747990228008": "Nenhum dado de autorização CA disponível", + "t_3_1747990229599": "Falha ao obter a lista de autorização CA", + "t_4_1747990227956": "Renovação automática (dias)", + "t_5_1747990228592": "Período de validade do certificado menor que", + "t_6_1747990228465": "Hora de renovar o novo certificado", + "t_7_1747990227761": "Endereço de Proxy (Opcional)", + "t_8_1747990235316": "Apenas suporta endereços de proxy http ou https (por exemplo, http://proxy.example.com:8080)", + "t_9_1747990229640": "O tempo de renovação automática não pode estar vazio", + "t_10_1747990232207": "Por favor, selecione o nome do site (seleção múltipla suportada)", + "t_0_1747994891459": "Site do Pagoda docker", + "t_0_1748052857931": "Autoridade Certificadora/Autorização (Opcional)", + "t_1_1748052860539": "Adicionar Zerossl, Google, autorização de certificado CA", + "t_2_1748052862259": "Por favor, insira as informações do seu e-mail para receber o e-mail de verificação do certificado" +} \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/locales/model/zhTW.json b/frontend/apps/allin-ssl/src/locales/model/zhTW.json new file mode 100644 index 0000000..7503703 --- /dev/null +++ b/frontend/apps/allin-ssl/src/locales/model/zhTW.json @@ -0,0 +1,627 @@ +{ + "t_0_1744098811152": "警告:您已進入未知區域,所訪問的頁面不存在,請點擊按鈕返回首頁。", + "t_1_1744098801860": "返回首頁", + "t_2_1744098804908": "安全提示:如果您認為這是個錯誤,請立即聯繫管理員", + "t_3_1744098802647": "展開主菜單", + "t_4_1744098802046": "折疊主菜單", + "t_1_1744164835667": "AllinSSL", + "t_2_1744164839713": "帳號登錄", + "t_3_1744164839524": "請輸入用戶名", + "t_4_1744164840458": "請輸入密碼", + "t_5_1744164840468": "記住密碼", + "t_6_1744164838900": "忘記密碼", + "t_7_1744164838625": "登錄中", + "t_8_1744164839833": "登錄", + "t_0_1744258111441": "首頁", + "t_1_1744258113857": "自動部署", + "t_2_1744258111238": "證書管理", + "t_3_1744258111182": "證書申請", + "t_4_1744258111238": "授權API管理", + "t_5_1744258110516": "監控", + "t_6_1744258111153": "設定", + "t_0_1744861190562": "返回工作流程列表", + "t_1_1744861189113": "運行", + "t_2_1744861190040": "儲存", + "t_3_1744861190932": "請選擇一個節點進行配置", + "t_4_1744861194395": "點擊左側流程圖中的節點來配置它", + "t_5_1744861189528": "開始", + "t_6_1744861190121": "未選擇節點", + "t_7_1744861189625": "配置已保存", + "t_8_1744861189821": "開始執行流程", + "t_9_1744861189580": "選中節點:", + "t_0_1744870861464": "節點", + "t_1_1744870861944": "節點配置", + "t_2_1744870863419": "請選擇左側節點進行配置", + "t_3_1744870864615": "未找到該節點類型的配置組件", + "t_4_1744870861589": "取消", + "t_5_1744870862719": "確定", + "t_0_1744875938285": "每分鐘", + "t_1_1744875938598": "每小時", + "t_2_1744875938555": "每天", + "t_3_1744875938310": "每月", + "t_4_1744875940750": "自動執行", + "t_5_1744875940010": "手動執行", + "t_0_1744879616135": "測試PID", + "t_1_1744879616555": "請輸入測試PID", + "t_2_1744879616413": "執行周期", + "t_3_1744879615723": "分鐘", + "t_4_1744879616168": "請輸入分鐘", + "t_5_1744879615277": "小時", + "t_6_1744879616944": "請輸入小時", + "t_7_1744879615743": "日期", + "t_8_1744879616493": "請選擇日期", + "t_0_1744942117992": "每週", + "t_1_1744942116527": "星期一", + "t_2_1744942117890": "星期二", + "t_3_1744942117885": "星期三", + "t_4_1744942117738": "週四", + "t_5_1744942117167": "週五", + "t_6_1744942117815": "週六", + "t_7_1744942117862": "週日", + "t_0_1744958839535": "請輸入域名", + "t_1_1744958840747": "請輸入郵箱", + "t_2_1744958840131": "郵箱格式不正確", + "t_3_1744958840485": "請選擇DNS提供商授權", + "t_4_1744958838951": "本地部署", + "t_5_1744958839222": "SSH部署", + "t_6_1744958843569": "宝塔面板/1面板(部署至面板憑證)", + "t_7_1744958841708": "宝塔面板/1面板(部署至指定網站項目)", + "t_8_1744958841658": "腾讯雲CDN/阿里雲CDN", + "t_9_1744958840634": "腾讯雲WAF", + "t_10_1744958860078": "阿里雲WAF", + "t_11_1744958840439": "本次自動申請的證書", + "t_12_1744958840387": "可選證書清單", + "t_13_1744958840714": "PEM(*.pem,*.crt,*.key)", + "t_14_1744958839470": "PFX(*.pfx)", + "t_15_1744958840790": "JKS(*.jks)", + "t_16_1744958841116": "POSIX bash(Linux/macOS)", + "t_17_1744958839597": "命令行(Windows)", + "t_18_1744958839895": "PowerShell(Windows)", + "t_19_1744958839297": "證書1", + "t_20_1744958839439": "證書2", + "t_21_1744958839305": "伺服器1", + "t_22_1744958841926": "伺服器2", + "t_23_1744958838717": "面板1", + "t_29_1744958838904": "日", + "t_30_1744958843864": "證書格式不正確,請檢查是否包含完整的證書頭尾識別", + "t_31_1744958844490": "私钥格式不正確,請檢查是否包含完整的私钥頭尾識別", + "t_0_1745215914686": "自動化名稱", + "t_2_1745215915397": "自動", + "t_3_1745215914237": "手動", + "t_4_1745215914951": "啟用狀態", + "t_5_1745215914671": "啟用", + "t_6_1745215914104": "停用", + "t_7_1745215914189": "創建時間", + "t_8_1745215914610": "操作", + "t_9_1745215914666": "執行歷史", + "t_10_1745215914342": "執行", + "t_11_1745215915429": "編輯", + "t_12_1745215914312": "刪除", + "t_13_1745215915455": "執行工作流程", + "t_14_1745215916235": "工作流程執行成功", + "t_15_1745215915743": "工作流程執行失敗", + "t_16_1745215915209": "刪除工作流程", + "t_17_1745215915985": "工作流程刪除成功", + "t_18_1745215915630": "工作流程刪除失敗", + "t_1_1745227838776": "請輸入自動化名稱", + "t_2_1745227839794": "確定要執行{name}工作流程嗎?", + "t_3_1745227841567": "確認要刪除{name}工作流程嗎?此操作無法恢復。", + "t_4_1745227838558": "執行時間", + "t_5_1745227839906": "結束時間", + "t_6_1745227838798": "執行方式", + "t_7_1745227838093": "狀態", + "t_8_1745227838023": "成功", + "t_9_1745227838305": "失敗", + "t_10_1745227838234": "執行中", + "t_11_1745227838422": "未知", + "t_12_1745227838814": "詳細", + "t_13_1745227838275": "上傳證書", + "t_14_1745227840904": "請輸入證書域名或品牌名稱搜尋", + "t_15_1745227839354": "共", + "t_16_1745227838930": "條", + "t_17_1745227838561": "域名", + "t_18_1745227838154": "品牌", + "t_19_1745227839107": "剩餘天數", + "t_20_1745227838813": "到期時間", + "t_21_1745227837972": "來源", + "t_22_1745227838154": "自動申請", + "t_23_1745227838699": "手動上傳", + "t_24_1745227839508": "加入時間", + "t_25_1745227838080": "下載", + "t_27_1745227838583": "即將過期", + "t_28_1745227837903": "正常", + "t_29_1745227838410": "刪除證書", + "t_30_1745227841739": "確認要刪除這個證書嗎?此操作無法恢復。", + "t_31_1745227838461": "確認", + "t_32_1745227838439": "證書名稱", + "t_33_1745227838984": "請輸入證書名稱", + "t_34_1745227839375": "證書內容(PEM)", + "t_35_1745227839208": "請輸入證書內容", + "t_36_1745227838958": "私鑰內容(KEY)", + "t_37_1745227839669": "請輸入私鑰內容", + "t_38_1745227838813": "下載失敗", + "t_39_1745227838696": "上傳失敗", + "t_40_1745227838872": "刪除失敗", + "t_0_1745289355714": "添加授權API", + "t_1_1745289356586": "請輸入授權API名稱或類型", + "t_2_1745289353944": "名稱", + "t_3_1745289354664": "授權API類型", + "t_4_1745289354902": "編輯授權API", + "t_5_1745289355718": "刪除授權API", + "t_6_1745289358340": "確定刪除該授權API嗎?此操作無法恢復。", + "t_7_1745289355714": "添加失敗", + "t_8_1745289354902": "更新失敗", + "t_9_1745289355714": "已過期{days}天", + "t_10_1745289354650": "監控管理", + "t_11_1745289354516": "加入監控", + "t_12_1745289356974": "請輸入監控名稱或域名進行搜尋", + "t_13_1745289354528": "監控名稱", + "t_14_1745289354902": "證書域名", + "t_15_1745289355714": "證書發頒機構", + "t_16_1745289354902": "證書狀態", + "t_17_1745289355715": "證書到期時間", + "t_18_1745289354598": "告警管道", + "t_19_1745289354676": "上次檢查時間", + "t_20_1745289354598": "編輯監控", + "t_21_1745289354598": "確認刪除", + "t_22_1745289359036": "刪除後將無法恢復,您確定要刪除該監控嗎?", + "t_23_1745289355716": "修改失敗", + "t_24_1745289355715": "設定失敗", + "t_25_1745289355721": "請輸入驗證碼", + "t_26_1745289358341": "表單驗證失敗,請檢查填寫內容", + "t_27_1745289355721": "請輸入授權API名稱", + "t_28_1745289356040": "請選擇授權API類型", + "t_29_1745289355850": "請輸入伺服器IP", + "t_30_1745289355718": "請輸入SSH端口", + "t_31_1745289355715": "請輸入SSH金鑰", + "t_32_1745289356127": "請輸入寶塔地址", + "t_33_1745289355721": "請輸入API金鑰", + "t_34_1745289356040": "請輸入1panel地址", + "t_35_1745289355714": "請輸入AccessKeyId", + "t_36_1745289355715": "請輸入AccessKeySecret", + "t_37_1745289356041": "請輸入SecretId", + "t_38_1745289356419": "請輸入密鑰", + "t_39_1745289354902": "更新成功", + "t_40_1745289355715": "添加成功", + "t_41_1745289354902": "類型", + "t_42_1745289355715": "伺服器IP", + "t_43_1745289354598": "SSH端口", + "t_44_1745289354583": "用戶名", + "t_45_1745289355714": "認證方式", + "t_46_1745289355723": "密碼驗證", + "t_47_1745289355715": "密钥認證", + "t_48_1745289355714": "密碼", + "t_49_1745289355714": "SSH私鑰", + "t_50_1745289355715": "請輸入SSH私鑰", + "t_51_1745289355714": "私鍵密碼", + "t_52_1745289359565": "如果私钥有密碼,請輸入", + "t_53_1745289356446": "宝塔面板地址", + "t_54_1745289358683": "請輸入宝塔面板地址,例如:https://bt.example.com", + "t_55_1745289355715": "API金鑰", + "t_56_1745289355714": "1面板地址", + "t_57_1745289358341": "請輸入1panel地址,例如:https://1panel.example.com", + "t_58_1745289355721": "請輸入AccessKey ID", + "t_59_1745289356803": "請輸入AccessKey密碼", + "t_60_1745289355715": "請輸入監控名稱", + "t_61_1745289355878": "請輸入域名/IP", + "t_62_1745289360212": "請選擇檢查週期", + "t_63_1745289354897": "5分鐘", + "t_64_1745289354670": "10分鐘", + "t_65_1745289354591": "15分鐘", + "t_66_1745289354655": "30分鐘", + "t_67_1745289354487": "60分鐘", + "t_68_1745289354676": "郵件", + "t_69_1745289355721": "短信", + "t_70_1745289354904": "微信", + "t_71_1745289354583": "域名/IP", + "t_72_1745289355715": "檢查週期", + "t_73_1745289356103": "請選擇告警渠道", + "t_0_1745289808449": "請輸入授權API名稱", + "t_0_1745294710530": "刪除監控", + "t_0_1745295228865": "更新時間", + "t_0_1745317313835": "伺服器IP位址格式錯誤", + "t_1_1745317313096": "端口格式錯誤", + "t_2_1745317314362": "面板URL地址格式錯誤", + "t_3_1745317313561": "請輸入面板API金鑰", + "t_4_1745317314054": "請輸入阿里雲AccessKeyId", + "t_5_1745317315285": "請輸入阿里雲AccessKeySecret", + "t_6_1745317313383": "請輸入腾讯雲SecretId", + "t_7_1745317313831": "請輸入腾讯雲SecretKey", + "t_0_1745457486299": "已啟用", + "t_1_1745457484314": "已停止", + "t_2_1745457488661": "切換為手動模式", + "t_3_1745457486983": "切換為自動模式", + "t_4_1745457497303": "切換為手動模式後,工作流將不再自動執行,但仍可手動執行", + "t_5_1745457494695": "切換為自動模式後,工作流將按照配置的時間自動執行", + "t_6_1745457487560": "關閉當前工作流程", + "t_7_1745457487185": "啟用當前工作流程", + "t_8_1745457496621": "關閉後,工作流將不再自動執行,手動也無法執行,是否繼續?", + "t_9_1745457500045": "啟用後,工作流程配置自動執行,或手動執行,是否繼續?", + "t_10_1745457486451": "添加工作流程失敗", + "t_11_1745457488256": "設置工作流程運行方式失敗", + "t_12_1745457489076": "啟用或禁用工作流程失敗", + "t_13_1745457487555": "執行工作流程失敗", + "t_14_1745457488092": "刪除工作流失敗", + "t_15_1745457484292": "退出", + "t_16_1745457491607": "即將登出,確認要登出嗎?", + "t_17_1745457488251": "正在登出,請稍候...", + "t_18_1745457490931": "新增郵箱通知", + "t_19_1745457484684": "儲存成功", + "t_20_1745457485905": "刪除成功", + "t_0_1745464080226": "獲取系統設置失敗", + "t_1_1745464079590": "設定儲存失敗", + "t_2_1745464077081": "獲取通知設置失敗", + "t_3_1745464081058": "儲存通知設定失敗", + "t_4_1745464075382": "獲取通知渠道列表失敗", + "t_5_1745464086047": "添加郵箱通知渠道失敗", + "t_6_1745464075714": "更新通知渠道失敗", + "t_7_1745464073330": "刪除通知渠道失敗", + "t_8_1745464081472": "檢查版本更新失敗", + "t_9_1745464078110": "儲存設定", + "t_10_1745464073098": "基礎設定", + "t_0_1745474945127": "選擇範本", + "t_0_1745490735213": "請輸入工作流程名稱", + "t_1_1745490731990": "配置", + "t_2_1745490735558": "請輸入電郵格式", + "t_3_1745490735059": "請選擇DNS提供商", + "t_4_1745490735630": "請輸入續簽間隔", + "t_5_1745490738285": "請輸入域名,域名不能為空", + "t_6_1745490738548": "請輸入郵箱,郵箱不能為空", + "t_7_1745490739917": "請選擇DNS提供商,DNS提供商不能為空", + "t_8_1745490739319": "請輸入續簽間隔,續簽間隔不能為空", + "t_1_1745553909483": "郵箱格式錯誤,請輸入正確的郵箱", + "t_2_1745553907423": "續簽間隔不能為空", + "t_0_1745735774005": "請輸入證書域名,多個域名用逗號分隔", + "t_1_1745735764953": "信箱", + "t_2_1745735773668": "請輸入郵箱,用於接收證書頒發機構的郵件通知", + "t_3_1745735765112": "DNS提供商", + "t_4_1745735765372": "添加", + "t_5_1745735769112": "續簽間隔(天)", + "t_6_1745735765205": "續簽間隔時間", + "t_7_1745735768326": "天,到期後自動續簽", + "t_8_1745735765753": "已配置", + "t_9_1745735765287": "未配置", + "t_10_1745735765165": "寶塔面板", + "t_11_1745735766456": "寶塔面板網站", + "t_12_1745735765571": "1Panel面板", + "t_13_1745735766084": "1Panel網站", + "t_14_1745735766121": "騰訊雲CDN", + "t_15_1745735768976": "騰訊雲COS", + "t_16_1745735766712": "阿里雲CDN", + "t_18_1745735765638": "部署類型", + "t_19_1745735766810": "請選擇部署類型", + "t_20_1745735768764": "請輸入部署路徑", + "t_21_1745735769154": "請輸入前置命令", + "t_22_1745735767366": "請輸入後置命令", + "t_24_1745735766826": "請輸入站點ID", + "t_25_1745735766651": "請輸入區域", + "t_26_1745735767144": "請輸入儲存桶", + "t_27_1745735764546": "下一步", + "t_28_1745735766626": "選擇部署類型", + "t_29_1745735768933": "配置部署參數", + "t_30_1745735764748": "運行模式", + "t_31_1745735767891": "運行模式未配置", + "t_32_1745735767156": "運行週期未配置", + "t_33_1745735766532": "運行時間未配置", + "t_34_1745735771147": "證書文件(PEM 格式)", + "t_35_1745735781545": "請貼上證書文件內容,例如:\n-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----", + "t_36_1745735769443": "私鑰文件(KEY 格式)", + "t_37_1745735779980": "請貼上私鑰文件內容,例如:\n-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----", + "t_38_1745735769521": "證書私鑰內容不能為空", + "t_39_1745735768565": "證書私鑰格式不正確", + "t_40_1745735815317": "證書內容不能為空", + "t_41_1745735767016": "證書格式不正確", + "t_0_1745738961258": "上一步", + "t_1_1745738963744": "提交", + "t_2_1745738969878": "配置部署參數,類型決定參數配置", + "t_0_1745744491696": "部署設備來源", + "t_1_1745744495019": "請選擇部署設備來源", + "t_2_1745744495813": "請選擇部署類型後,點擊下一步", + "t_0_1745744902975": "部署來源", + "t_1_1745744905566": "請選擇部署來源", + "t_2_1745744903722": "新增更多設備", + "t_0_1745748292337": "添加部署來源", + "t_1_1745748290291": "證書來源", + "t_2_1745748298902": "當前類型部署來源為空,請先添加部署來源", + "t_3_1745748298161": "當前流程中沒有申請節點,請先添加申請節點", + "t_4_1745748290292": "提交內容", + "t_0_1745765864788": "點擊編輯工作流程標題", + "t_1_1745765875247": "刪除節點-【{name}】", + "t_2_1745765875918": "當前節點存在子節點,刪除後會影響其他節點,是否確認刪除?", + "t_3_1745765920953": "目前節點存在配置數據,是否確認刪除?", + "t_4_1745765868807": "請選擇部署類型後,再進行下一步", + "t_0_1745833934390": "請選擇類型", + "t_1_1745833931535": "主機", + "t_2_1745833931404": "埠", + "t_3_1745833936770": "獲取首頁概覽數據失敗", + "t_4_1745833932780": "版本資訊", + "t_5_1745833933241": "目前版本", + "t_6_1745833933523": "更新方式", + "t_7_1745833933278": "最新版本", + "t_8_1745833933552": "更新日誌", + "t_9_1745833935269": "客服二維碼", + "t_10_1745833941691": "掃碼添加客服", + "t_11_1745833935261": "微信公眾號", + "t_12_1745833943712": "掃碼關注微信公眾號", + "t_13_1745833933630": "關於產品", + "t_14_1745833932440": "SMTP伺服器", + "t_15_1745833940280": "請輸入SMTP伺服器", + "t_16_1745833933819": "SMTP埠", + "t_17_1745833935070": "請輸入SMTP端口", + "t_18_1745833933989": "SSL/TLS連接", + "t_1_1745887832941": "訊息通知", + "t_2_1745887834248": "新增通知渠道", + "t_3_1745887835089": "請輸入通知主題", + "t_4_1745887835265": "請輸入通知內容", + "t_0_1745895057404": "修改郵箱通知配置", + "t_0_1745920566646": "通知主題", + "t_1_1745920567200": "通知內容", + "t_0_1745936396853": "點擊獲取驗證碼", + "t_0_1745999035681": "剩餘{days}天", + "t_1_1745999036289": "即將到期{days}天", + "t_0_1746000517848": "已過期", + "t_0_1746001199409": "已到期", + "t_0_1746004861782": "DNS提供商為空", + "t_1_1746004861166": "新增DNS供應商", + "t_0_1746497662220": "刷新", + "t_0_1746519384035": "運行中", + "t_0_1746579648713": "執行歷史詳情", + "t_0_1746590054456": "執行狀態", + "t_1_1746590060448": "觸發方式", + "t_0_1746667592819": "正在提交資訊,請稍後...", + "t_1_1746667588689": "密鑰", + "t_2_1746667592840": "面板URL", + "t_3_1746667592270": "忽略 SSL/TLS證書錯誤", + "t_4_1746667590873": "表單驗證失敗", + "t_5_1746667590676": "新增工作流程", + "t_6_1746667592831": "正在提交申請,請稍後...", + "t_7_1746667592468": "請輸入正確的域名", + "t_8_1746667591924": "請選擇解析方式", + "t_9_1746667589516": "刷新列表", + "t_10_1746667589575": "通配符", + "t_11_1746667589598": "多域名", + "t_12_1746667589733": "熱門", + "t_13_1746667599218": "是廣泛使用的免費SSL證書提供商,適合個人網站和測試環境。", + "t_14_1746667590827": "支持域名數", + "t_15_1746667588493": "個", + "t_16_1746667591069": "支援萬用字元", + "t_17_1746667588785": "支持", + "t_18_1746667590113": "不支援", + "t_19_1746667589295": "有效期", + "t_20_1746667588453": "天", + "t_21_1746667590834": "支援小程式", + "t_22_1746667591024": "適用網站", + "t_23_1746667591989": "*.example.com、*.demo.com", + "t_24_1746667583520": "*.example.com", + "t_25_1746667590147": "example.com、demo.com", + "t_26_1746667594662": "www.example.com、example.com", + "t_27_1746667589350": "免費", + "t_28_1746667590336": "立即申請", + "t_29_1746667589773": "專案地址", + "t_30_1746667591892": "請輸入憑證檔案路徑", + "t_31_1746667593074": "請輸入私鑰文件路徑", + "t_0_1746673515941": "當前DNS提供商為空,請先添加DNS提供商", + "t_0_1746676862189": "測試通知發送失敗", + "t_1_1746676859550": "新增配置", + "t_2_1746676856700": "暫不支持", + "t_3_1746676857930": "郵件通知", + "t_4_1746676861473": "透過郵件發送警報通知", + "t_5_1746676856974": "釘釘通知", + "t_6_1746676860886": "通過釘釘機器人發送警報通知", + "t_7_1746676857191": "企業微信通知", + "t_8_1746676860457": "通過企業微信機器人發送警報通知", + "t_9_1746676857164": "飛書通知", + "t_10_1746676862329": "通過飛書機器人發送告警通知", + "t_11_1746676859158": "WebHook通知", + "t_12_1746676860503": "通過WebHook發送警報通知", + "t_13_1746676856842": "通知渠道", + "t_14_1746676859019": "已配置的通知頻道", + "t_15_1746676856567": "已停用", + "t_16_1746676855270": "測試", + "t_0_1746677882486": "最後一次執行狀態", + "t_0_1746697487119": "域名不能為空", + "t_1_1746697485188": "郵箱不能為空", + "t_2_1746697487164": "阿里雲OSS", + "t_0_1746754500246": "主機供應商", + "t_1_1746754499371": "API來源", + "t_2_1746754500270": "API 類型", + "t_0_1746760933542": "請求錯誤", + "t_0_1746773350551": "共{0}條", + "t_1_1746773348701": "未執行", + "t_2_1746773350970": "自動化工作流程", + "t_3_1746773348798": "總數量", + "t_4_1746773348957": "執行失敗", + "t_5_1746773349141": "即將到期", + "t_6_1746773349980": "即時監控", + "t_7_1746773349302": "異常數量", + "t_8_1746773351524": "最近工作流程執行紀錄", + "t_9_1746773348221": "查看全部", + "t_10_1746773351576": "暫無工作流執行記錄", + "t_11_1746773349054": "建立工作流程", + "t_12_1746773355641": "點擊創建自動化工作流程,提高效率", + "t_13_1746773349526": "申請證書", + "t_14_1746773355081": "點擊申請和管理SSL證書,保障安全", + "t_16_1746773356568": "最多只能配置一個郵箱通知渠道", + "t_17_1746773351220": "確認{0}通知渠道", + "t_18_1746773355467": "{0}通知渠道,將開始發送告警通知。", + "t_19_1746773352558": "當前通知渠道不支援測試", + "t_20_1746773356060": "正在發送測試郵件,請稍後...", + "t_21_1746773350759": "測試郵件", + "t_22_1746773360711": "發送測試郵件到當前配置的郵箱,是否繼續?", + "t_23_1746773350040": "刪除確認", + "t_25_1746773349596": "請輸入名稱", + "t_26_1746773353409": "請輸入正確的SMTP端口", + "t_27_1746773352584": "請輸入使用者密碼", + "t_28_1746773354048": "請輸入正確的發件人郵箱", + "t_29_1746773351834": "請輸入正確的接收信箱", + "t_30_1746773350013": "寄件人信箱", + "t_31_1746773349857": "接收郵箱", + "t_32_1746773348993": "釘釘", + "t_33_1746773350932": "企業微信", + "t_34_1746773350153": "飛書", + "t_35_1746773362992": "一個集證書申請、管理、部署和監控於一體的SSL證書全生命週期管理工具。", + "t_36_1746773348989": "證書申請", + "t_37_1746773356895": "支援通過ACME協議從Let's Encrypt獲取證書", + "t_38_1746773349796": "證書管理", + "t_39_1746773358932": "集中管理所有SSL證書,包括手動上傳和自動申請的證書", + "t_40_1746773352188": "證書部署", + "t_41_1746773364475": "支援一鍵部署證書到多種平台,如阿里雲、騰訊雲、寶塔面板、1Panel等", + "t_42_1746773348768": "站點監控", + "t_43_1746773359511": "實時監控站點SSL證書狀態,提前預警證書過期", + "t_44_1746773352805": "自動化任務:", + "t_45_1746773355717": "支援定時任務,自動續期證書並部署", + "t_46_1746773350579": "多平台支援", + "t_47_1746773360760": "支援多種DNS提供商(阿里雲、騰訊雲等)的DNS驗證方式", + "t_0_1746773763967": "確定要刪除{0},通知渠道嗎?", + "t_1_1746773763643": "Let's Encrypt等CA自動申請免費證書", + "t_0_1746776194126": "日誌詳情", + "t_1_1746776198156": "載入日誌失敗:", + "t_2_1746776194263": "下載日誌", + "t_3_1746776195004": "暫無日誌資訊", + "t_0_1746782379424": "自動化任務", + "t_0_1746858920894": "請選擇主機提供商", + "t_1_1746858922914": "DNS提供商列表為空,請添加", + "t_2_1746858923964": "主機供應商列表為空,請添加", + "t_3_1746858920060": "新增主機提供商", + "t_4_1746858917773": "已選擇", + "t_0_1747019621052": "請選擇主機提供商{0}", + "t_1_1747019624067": "點擊設置網站監控,掌握實時狀態", + "t_2_1747019616224": "阿里雲", + "t_3_1747019616129": "騰訊雲", + "t_0_1747040228657": "多域名請使用英文逗號分隔,例如:test.com,test.cn", + "t_1_1747040226143": "泛網域請使用*號,例如:*.test.com", + "t_0_1747042966820": "請輸入正確的Cloudflare API密鑰", + "t_1_1747042969705": "請輸入正確的寶塔API密鑰", + "t_2_1747042967277": "請輸入正確的騰訊雲SecretKey", + "t_3_1747042967608": "請輸入正確的華為雲SecretKey", + "t_4_1747042966254": "請輸入華為雲AccessKey", + "t_5_1747042965911": "請輸入正確的郵箱賬號", + "t_0_1747047213730": "添加自動化部署", + "t_1_1747047213009": "添加證書", + "t_2_1747047214975": "SSL證書管理平臺", + "t_3_1747047218669": "域名格式錯誤,請檢查域名格式", + "t_0_1747106957037": "DNS 遞迴伺服器(可選)", + "t_1_1747106961747": "請輸入 DNS 遞歸服務器(多個值請用,隔開)", + "t_2_1747106957037": "跳過本地預檢查", + "t_0_1747110184700": "選擇證書", + "t_1_1747110191587": "如果需要修改證書內容與密鑰,請選擇自定義證書", + "t_2_1747110193465": "當選擇非自訂憑證時,憑證內容與金鑰均不可修改", + "t_3_1747110185110": "上傳並提交", + "t_0_1747215751189": "寶塔WAF網站", + "t_0_1747271295174": "寶塔WAF-URL地址格式錯誤", + "t_1_1747271295484": "請輸入寶塔WAF-API金鑰", + "t_2_1747271295877": "請輸入正確的華為雲AccessKey", + "t_3_1747271294475": "請輸入正確的百度雲AccessKey", + "t_4_1747271294621": "請輸入正確的百度雲SecretKey", + "t_5_1747271291828": "寶塔WAF-URL", + "t_6_1747271296994": "本機部署", + "t_7_1747271292060": "全部來源", + "t_8_1747271290414": "寶塔", + "t_9_1747271284765": "1Panel", + "t_0_1747280814475": "SMTP端口禁止修改", + "t_1_1747280813656": "證書文件路徑(僅支持PEM格式)", + "t_2_1747280811593": "私鑰文件路徑", + "t_3_1747280812067": "前置命令(可選)", + "t_4_1747280811462": "後置命令(可選)", + "t_6_1747280809615": "站點ID", + "t_7_1747280808936": "區域", + "t_8_1747280809382": "儲存桶", + "t_9_1747280810169": "重複部署", + "t_10_1747280816952": "當與上次部署的證書相同且上次部署成功時", + "t_11_1747280809178": "跳過", + "t_12_1747280809893": "不跳過", + "t_13_1747280810369": "重新部署", + "t_14_1747280811231": "搜尋部署類型", + "t_0_1747296173751": "網站名", + "t_1_1747296175494": "請輸入網址名", + "t_0_1747298114839": "雷池WAF站點", + "t_1_1747298114192": "雷池WAF", + "t_0_1747300383756": "雷池WAF-URL地址格式錯誤", + "t_1_1747300384579": "請輸入正確的寶塔WAF-API密鑰", + "t_2_1747300385222": "請輸入正確的雷池WAF-API密鑰", + "t_0_1747365600180": "請輸入西部數碼的用戶名", + "t_1_1747365603108": "請輸入西部數碼的密碼", + "t_3_1747365600828": "請輸入火山引擎的AccessKey", + "t_4_1747365600137": "請輸入火山引擎的SecretKey", + "t_0_1747367069267": "寶塔docker站點", + "t_0_1747617113090": "請輸入雷池的API令牌", + "t_1_1747617105179": "API Token", + "t_0_1747647014927": "證書算法", + "t_0_1747709067998": "請輸入SSH密鑰,內容不能為空", + "t_0_1747711335067": "請輸入SSH密碼", + "t_1_1747711335336": "主機地址", + "t_2_1747711337958": "請輸入主機地址不能為空", + "t_0_1747754231151": "日誌檢視器", + "t_1_1747754231838": "請先", + "t_2_1747754234999": "有問題或建議可提", + "t_3_1747754232000": "也可以在Github上找到我們", + "t_4_1747754235407": "您的參與對AllinSSL極其重要,感謝。", + "t_0_1747817614953": "請輸入", + "t_1_1747817639034": "是", + "t_2_1747817610671": "否", + "t_3_1747817612697": "節點字段必填", + "t_4_1747817613325": "請輸入有效的域名", + "t_5_1747817619337": "請輸入有效的網域名稱,多個網域請用英文逗號分隔", + "t_6_1747817644358": "請輸入電子郵件地址", + "t_7_1747817613773": "請輸入有效的郵箱地址", + "t_8_1747817614764": "節點錯誤", + "t_9_1747817611448": "域名:", + "t_10_1747817611126": "申請", + "t_11_1747817612051": "部署", + "t_12_1747817611391": "上傳", + "t_0_1747886301644": "消息推送配置", + "t_1_1747886307276": "寶塔面板-網站", + "t_2_1747886302053": "1Panel-網站", + "t_3_1747886302848": "寶塔WAF", + "t_4_1747886303229": "寶塔WAF-網站", + "t_5_1747886301427": "騰訊雲EdgeOne", + "t_6_1747886301844": "七牛雲", + "t_7_1747886302395": "七牛雲-CDN", + "t_8_1747886304014": "七牛雲-OSS", + "t_9_1747886301128": "華為雲", + "t_10_1747886300958": "百度雲", + "t_11_1747886301986": "雷池", + "t_12_1747886302725": "雷池WAF-網站", + "t_13_1747886301689": "火山引擎", + "t_14_1747886301884": "西部數碼", + "t_15_1747886301573": "部署項目類型", + "t_16_1747886308182": "您確定要刷新頁面嗎?數據可能會遺失哦!", + "t_0_1747895713179": "執行成功", + "t_1_1747895712756": "正在執行", + "t_0_1747903670020": "CA授權管理", + "t_2_1747903672640": "確定刪除", + "t_3_1747903672833": "確定要刪除此CA授權嗎?", + "t_4_1747903685371": "添加CA授權", + "t_5_1747903671439": "請輸入ACME EAB KID", + "t_6_1747903672931": "請輸入ACME EAB HMAC密鑰", + "t_7_1747903678624": "請選擇CA提供商", + "t_8_1747903675532": "當前CA提供商授權的別名,用於快速識別", + "t_9_1747903669360": "CA提供商", + "t_10_1747903662994": "ACME EAB KID", + "t_11_1747903674802": "請輸入CA提供商的ACME EAB KID", + "t_12_1747903662994": "ACME EAB HMAC Key", + "t_13_1747903673007": "請輸入CA提供商的ACME EAM HMAC", + "t_0_1747904536291": "AllinSSL 開源免費的 SSL 證書自動化管理平台 一鍵自動化申請、續期、部署、監控所有 SSL/TLS 證書,支援跨雲環境和多 CA (coding~),告別繁瑣配置和高昂費用。", + "t_0_1747965909665": "請輸入用於綁定CA授權的郵箱", + "t_0_1747969933657": "終端部署", + "t_0_1747984137443": "請輸入正確的GoDaddy API金鑰", + "t_1_1747984133312": "請輸入GoDaddy API密鑰", + "t_2_1747984134626": "請輸入七牛雲Access Secret", + "t_3_1747984134586": "請輸入七牛雲Access Key", + "t_4_1747984130327": "复制", + "t_5_1747984133112": "當距離到期時間", + "t_0_1747990228780": "請選擇證書頒發機構", + "t_2_1747990228008": "暫無CA授權數據", + "t_3_1747990229599": "獲取CA授權列表失敗", + "t_4_1747990227956": "自動續簽(天)", + "t_5_1747990228592": "證書有效期小於", + "t_6_1747990228465": "天時,續簽新的證書", + "t_7_1747990227761": "代理地址(可選)", + "t_8_1747990235316": "僅支援 http 或 https 代理地址(例如:http://proxy.example.com:8080)", + "t_9_1747990229640": "自動續簽時間不能為空", + "t_10_1747990232207": "請選擇網站名,支援多選網站名稱", + "t_0_1747994891459": "寶塔docker網站", + "t_0_1748052857931": "證書頒發機構/授權(可選)", + "t_1_1748052860539": "新增Zerossl、Google,CA憑證授權", + "t_2_1748052862259": "請輸入郵箱信息,用於接收證書驗證郵件" +} \ No newline at end of file diff --git a/frontend/apps/allin-ssl/src/main.ts b/frontend/apps/allin-ssl/src/main.ts new file mode 100644 index 0000000..7e60509 --- /dev/null +++ b/frontend/apps/allin-ssl/src/main.ts @@ -0,0 +1,29 @@ +import { pinia } from '@baota/pinia' +import { router } from '@router/index' +import { i18n } from '@locales/index' +import { useModalUseDiscrete } from '@baota/naive-ui/hooks' +import App from './App' // 根组件 + +import 'virtual:svg-icons-register' +import 'normalize.css' // 样式修复一致性 +import '@styles/reset.css' // 重置样式 +import '@styles/variable.css' // 全局变量 +import '@styles/transition.css' // 过渡动画 +import '@styles/icon.css' // css 图标 +import '@styles/naive-override.css' // 覆盖 Naive UI 样式 +import { directives, useDirectives } from '@lib/directive' + +// 引入mock +// import '../mock/access' + +const app = createApp(App) +app.use(router) // 路由 +app.use(pinia) // 使用状态管理 +app.use(i18n) // 国际化 +app.mount('#app') // 挂载到DOM + +// 注册自定义指令 +useDirectives(app, directives) + +// 设置资源 +useModalUseDiscrete({ i18n, router, pinia }) diff --git a/frontend/apps/allin-ssl/src/router/each.tsx b/frontend/apps/allin-ssl/src/router/each.tsx new file mode 100644 index 0000000..001299b --- /dev/null +++ b/frontend/apps/allin-ssl/src/router/each.tsx @@ -0,0 +1,30 @@ +import { createDiscreteApi } from 'naive-ui' + +import type { Router, RouteLocationNormalized, NavigationGuardNext } from '@baota/router/each' +import { useCreateRouterEach } from '@baota/router/each' // 全局路由守卫 + +// 创建离散API +const { loadingBar } = createDiscreteApi(['loadingBar']) + +/** + * @description 全局路由守卫 + * @param {Router} router 路由实例 + * @return {void} + */ +const useRouterEach = (router: Router) => + useCreateRouterEach(router, { + beforeEach: (to: RouteLocationNormalized, _: RouteLocationNormalized, next: NavigationGuardNext) => { + // 开始加载 + loadingBar.start() + // 判断当前路由是否存在,如果不存在,则跳转到 404 + if (!router.hasRoute(to.name as string)) { + if (!to.path.includes('/404')) return next({ path: '/404' }) + } + next() + }, + afterEach: (to: RouteLocationNormalized) => { + loadingBar.finish() + }, + }) + +export default useRouterEach diff --git a/frontend/apps/allin-ssl/src/router/import.tsx b/frontend/apps/allin-ssl/src/router/import.tsx new file mode 100644 index 0000000..f6b3b36 --- /dev/null +++ b/frontend/apps/allin-ssl/src/router/import.tsx @@ -0,0 +1,17 @@ +import { getBuildRoutes } from '@baota/router/import' +import routeConfig from '@config/route' + +/** + * @description 创建路由,动态获取路由配置 + * @returns {RouteRecordRaw[]} 路由配置 + */ +export const createRoutes = () => { + const modules = import.meta.glob('../views/*/index.tsx') + const childrenModules = import.meta.glob(`../views/*/children/*/index.tsx`) + return getBuildRoutes(modules, childrenModules, { + framework: routeConfig.frameworkRoute, + system: routeConfig.systemRoute, + sort: routeConfig.sortRoute, + disabled: routeConfig.disabledRoute, + }) +} diff --git a/frontend/apps/allin-ssl/src/router/index.tsx b/frontend/apps/allin-ssl/src/router/index.tsx new file mode 100644 index 0000000..3776f04 --- /dev/null +++ b/frontend/apps/allin-ssl/src/router/index.tsx @@ -0,0 +1,17 @@ +import { createWebHistory, useCreateRouter } from '@baota/router' // 框架路由 +import { createRoutes } from './import' // 自动导入路由配置 +import useRouterEach from './each' // 全局路由守卫 + +// 获取路由 +const { routeGroup, routes } = createRoutes() // 获取路由配置 + +// 创建路由 +const router = useCreateRouter({ + routes: routeGroup, + history: createWebHistory(), +}) + +// 全局路由守卫 +useRouterEach(router) + +export { router, routes } diff --git a/frontend/apps/allin-ssl/src/styles/icon.css b/frontend/apps/allin-ssl/src/styles/icon.css new file mode 100644 index 0000000..c013522 --- /dev/null +++ b/frontend/apps/allin-ssl/src/styles/icon.css @@ -0,0 +1,97 @@ +.lucide--user-round { + display: inline-block; + width: 24px; + height: 24px; + --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Ccircle cx='12' cy='8' r='5'/%3E%3Cpath d='M20 21a8 8 0 0 0-16 0'/%3E%3C/g%3E%3C/svg%3E"); + background-color: currentColor; + -webkit-mask-image: var(--svg); + mask-image: var(--svg); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; +} + +.mynaui--lock-open-password { + display: inline-block; + width: 24px; + height: 24px; + --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M8 10V8c0-2.761 1.239-5 4-5c2.094 0 3.313 1.288 3.78 3.114M3.5 17.8v-4.6c0-1.12 0-1.68.218-2.107a2 2 0 0 1 .874-.875c.428-.217.988-.217 2.108-.217h10.6c1.12 0 1.68 0 2.108.217a2 2 0 0 1 .874.874c.218.428.218.988.218 2.108v4.6c0 1.12 0 1.68-.218 2.108a2 2 0 0 1-.874.874C18.98 21 18.42 21 17.3 21H6.7c-1.12 0-1.68 0-2.108-.218a2 2 0 0 1-.874-.874C3.5 19.481 3.5 18.921 3.5 17.8m8.5-2.05v-.5m4 .5v-.5m-8 .5v-.5'/%3E%3C/svg%3E"); + background-color: currentColor; + -webkit-mask-image: var(--svg); + mask-image: var(--svg); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; +} + +.solar--server-broken { + display: inline-block; + width: 24px; + height: 24px; + --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-width='1.5' d='M13 21H6c-1.886 0-2.828 0-3.414-.586S2 18.886 2 17s0-2.828.586-3.414S4.114 13 6 13h12c1.886 0 2.828 0 3.414.586S22 15.114 22 17s0 2.828-.586 3.414S19.886 21 18 21h-1M11 2h7c1.886 0 2.828 0 3.414.586S22 4.114 22 6s0 2.828-.586 3.414S19.886 10 18 10H6c-1.886 0-2.828 0-3.414-.586S2 7.886 2 6s0-2.828.586-3.414S4.114 2 6 2h1m4 4h7M6 6h2m3 11h7M6 17h2'/%3E%3C/svg%3E"); + background-color: currentColor; + -webkit-mask-image: var(--svg); + mask-image: var(--svg); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; +} + +.icon-park-outline--alarm { + display: inline-block; + width: 48px; + height: 48px; + --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cg fill='none' stroke='%23000' stroke-linejoin='round' stroke-width='4'%3E%3Cpath d='M14 25c0-5.523 4.477-10 10-10s10 4.477 10 10v16H14z'/%3E%3Cpath stroke-linecap='round' d='M24 5v3m11.892 1.328l-1.929 2.298m8.256 8.661l-2.955.521m-33.483-.521l2.955.521m3.373-11.48l1.928 2.298M6 41h37'/%3E%3C/g%3E%3C/svg%3E"); + background-color: currentColor; + -webkit-mask-image: var(--svg); + mask-image: var(--svg); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; +} + +.bitcoin-icons--exit-filled { + display: inline-block; + width: 24px; + height: 24px; + --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='%23000' fill-rule='evenodd' clip-rule='evenodd'%3E%3Cpath d='M15.99 7.823a.75.75 0 0 1 1.061.021l3.49 3.637a.75.75 0 0 1 0 1.038l-3.49 3.637a.75.75 0 0 1-1.082-1.039l2.271-2.367h-6.967a.75.75 0 0 1 0-1.5h6.968l-2.272-2.367a.75.75 0 0 1 .022-1.06'/%3E%3Cpath d='M3.25 4A.75.75 0 0 1 4 3.25h9.455a.75.75 0 0 1 .75.75v3a.75.75 0 1 1-1.5 0V4.75H4.75v14.5h7.954V17a.75.75 0 0 1 1.5 0v3a.75.75 0 0 1-.75.75H4a.75.75 0 0 1-.75-.75z'/%3E%3C/g%3E%3C/svg%3E"); + background-color: currentColor; + -webkit-mask-image: var(--svg); + mask-image: var(--svg); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; +} + +.lucide--settings { + display: inline-block; + width: 24px; + height: 24px; + --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2'/%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3C/g%3E%3C/svg%3E"); + background-color: currentColor; + -webkit-mask-image: var(--svg); + mask-image: var(--svg); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; +} + +.pajamas--log { + display: inline-block; + width: 16px; + height: 16px; + --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M3.5 2.5v11h9v-11zM3 1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zm5 10a.75.75 0 0 1 .75-.75h1.75a.75.75 0 0 1 0 1.5H8.75A.75.75 0 0 1 8 11m-2 1a1 1 0 1 0 0-2a1 1 0 0 0 0 2m2-4a.75.75 0 0 1 .75-.75h1.75a.75.75 0 0 1 0 1.5H8.75A.75.75 0 0 1 8 8M6 9a1 1 0 1 0 0-2a1 1 0 0 0 0 2m2-4a.75.75 0 0 1 .75-.75h1.75a.75.75 0 0 1 0 1.5H8.75A.75.75 0 0 1 8 5M6 6a1 1 0 1 0 0-2a1 1 0 0 0 0 2' clip-rule='evenodd'/%3E%3C/svg%3E"); + background-color: currentColor; + -webkit-mask-image: var(--svg); + mask-image: var(--svg); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; +} diff --git a/frontend/apps/allin-ssl/src/styles/naive-override.css b/frontend/apps/allin-ssl/src/styles/naive-override.css new file mode 100644 index 0000000..01343c1 --- /dev/null +++ b/frontend/apps/allin-ssl/src/styles/naive-override.css @@ -0,0 +1,51 @@ +.n-tabs .n-tabs-nav { + background-color: var(--bt-card-bg-color) !important; + border-radius: 0.8rem; +} +.n-tabs .n-tabs-nav .n-tabs-tab-pad { + display: none; +} +.n-tabs .n-tabs-nav .n-tabs-tab-wrapper { + border-radius: 0.8rem 0.8rem 0.8rem 0.8rem; +} +.n-tabs .n-tabs-tab { + padding: 0 2.2rem; + height: 5.2rem; +} +.n-tabs .n-tabs-tab.n-tabs-tab--active { + background-color: var(--bt-card-bg-color-active) !important; +} +.n-tabs .n-tabs-tab:first-child { + border-radius: 0.8rem 0 0 0.8rem; +} +.n-tabs .n-tabs-tab:last-child { + border-radius: 0 0.8rem 0.8rem 0; +} + + +/* 日志颜色 */ + +.hljs-info-text{ + color: var(--n-success-color) !important; +} +.hljs-date-text{ + color: var(--n-success-color-pressed) !important; +} +.hljs-error-text{ + color: var(--n-error-color) !important; +} +.hljs-warning-text{ + color: var(--n-warning-color) !important; +} + + +/* 表格数据显示 */ +.n-data-table .n-data-table-empty{ + background-color: var(--n-merged-td-color); +} + + +.leftPanel .n-tabs-tab { + height: 3.2rem; + line-height: 3.2rem; +} diff --git a/frontend/apps/allin-ssl/src/styles/reset.css b/frontend/apps/allin-ssl/src/styles/reset.css new file mode 100644 index 0000000..4fbcd19 --- /dev/null +++ b/frontend/apps/allin-ssl/src/styles/reset.css @@ -0,0 +1,75 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body, +#app { + @apply relative m-0 w-full h-full min-h-full text-[62.5%]; +} + +/* 视图全局配置 */ +.n-config-provider, +.n-layout { + @apply h-full; +} +/* end */ + +/* 图片预处理 */ +img { + /* 图片预处理 */ + image-rendering: -o-crisp-edges; /* Opera */ + image-rendering: -moz-crisp-edges; /* Firefox */ + image-rendering: -webkit-optimize-contrast; /*Webkit (non-standard naming) */ + image-rendering: crisp-edges; + -ms-interpolation-mode: nearest-neighbor; /* IE (non-standard property) */ +} +/* end */ + +/** 滚动条覆盖 */ +[data-scroll-top='true']::after, +[data-scroll-bottom='true']::before { + @apply content-[''] absolute w-full h-[.6rem] z-[100]; +} +[data-scroll-top='true']::after { + background-image: -webkit-linear-gradient(top, rgba(220, 220, 220, 0.2), rgba(255, 255, 255, 0)); + top: 0; +} +[data-scroll-bottom='true']::before { + background-image: -webkit-linear-gradient(top, rgba(255, 255, 255, 0), rgba(220, 220, 220, 0.2)); + bottom: 0; +} +/* end */ + +/** 自定义Tab样式 */ +.n-tabs-nav--segment { + background-color: transparent; + padding: 0; +} + +.n-tabs-tab.n-tabs-tab--active { + font-weight: 600; + width: 100%; +} + +.n-tabs-tab { + padding: 8px 16px; + transition: all 0.3s ease; + width: 100%; + height: 45px; + font-size: 18px; + text-align: center; + display: flex; + justify-content: center; + align-items: center; +} + +.n-tabs-tab-wrapper{ + flex: 1 !important; +} +/* end */ + + +.n-data-table .n-data-table-td { + background-color: transparent !important; +} diff --git a/frontend/apps/allin-ssl/src/styles/transition.css b/frontend/apps/allin-ssl/src/styles/transition.css new file mode 100644 index 0000000..fd8196e --- /dev/null +++ b/frontend/apps/allin-ssl/src/styles/transition.css @@ -0,0 +1,91 @@ +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.3s ease; +} + +.fade-enter-from, +.fade-leave-to { + opacity: 0; +} + +/* 从右侧滑入 */ +.slide-right-enter-active, +.slide-right-leave-active { + transition: all 0.3s ease-out; +} + +.slide-right-enter-from { + opacity: 0; + transform: translateX(-20px); +} + +.slide-right-leave-to { + opacity: 0; + transform: translateX(20px); +} + +/* 从左侧滑入 */ +.slide-left-enter-active, +.slide-left-leave-active { + transition: all 0.3s ease-out; +} + +.slide-left-enter-from { + opacity: 0; + transform: translateX(20px); +} + +.slide-left-leave-to { + opacity: 0; + transform: translateX(-20px); +} + +/* 从底部滑入 */ +.slide-up-enter-active, +.slide-up-leave-active { + transition: all 0.3s ease-out; +} + +.slide-up-enter-from { + opacity: 0; + transform: translateY(20px); +} + +.slide-up-leave-to { + opacity: 0; + transform: translateY(-20px); +} + +/* 缩放过渡 */ +.scale-enter-active, +.scale-leave-active { + transition: all 0.3s ease; +} + +.scale-enter-from, +.scale-leave-to { + opacity: 0; + transform: scale(0.9); +} + +/* 从左到右的渐显动画 */ + +.route-slide-enter-active, +.route-slide-leave-active { + transition: + opacity 0.35s ease-out, + transform 0.5s ease; +} + +.route-slide-enter-from { + opacity: 0; + transform: translateX(-40px); +} + +.route-slide-leave-to { + opacity: 0; + transition: + opacity 0.2s ease-in, + transform 0.35s ease-in; + transform: translateX(40px); +} diff --git a/frontend/apps/allin-ssl/src/styles/variable.css b/frontend/apps/allin-ssl/src/styles/variable.css new file mode 100644 index 0000000..ca945d2 --- /dev/null +++ b/frontend/apps/allin-ssl/src/styles/variable.css @@ -0,0 +1,42 @@ +:root { + --n-sider-width: 22rem; /* 侧边栏宽度,已经弃用,naive sider props width 替代 */ + --n-sider-login-height: var(--n-header-height); /* 侧边栏登录栏高度 */ + --n-header-height: 5rem; /* 顶部栏高度 */ + --n-footer-height: 4rem; /* 底部栏高度 */ + --n-main-diff-height: calc(var(--n-header-height)); /* 顶部栏和底部栏高度之和 */ + --n-content-margin: 1.2rem; /* 内容区内边距 */ + --n-content-padding: 1.2rem; /* 内容区内边距 */ + --n-dialog-title-padding: 0; /* 对话框标题内边距 */ + --n-layout-content-background-color: #f8fafc; /* 内容区背景颜色 */ + + /* Home View Custom Colors */ + --color-workflow-bg: rgba(16, 185, 129, 0.08); + --color-workflow-icon-wrapper-bg: rgba(16, 185, 129, 0.15); + + --color-cert-bg: rgba(245, 158, 11, 0.08); + --color-cert-icon-wrapper-bg: rgba(245, 158, 11, 0.15); + + --color-monitor-bg: rgba(139, 92, 246, 0.08); + --color-monitor-icon-wrapper-bg: rgba(139, 92, 246, 0.15); + --color-monitor-text: #8B5CF6; /* 紫色,用于图标和标题 */ + + --color-decorative-element-bg: #f0f9ff; /* 用于替换 Tailwind bg-blue-50 */ +} + +:root.dark { + --n-layout-content-background-color: transparent; /* 内容区背景颜色 */ + + /* Home View Custom Colors - Dark Theme */ + --color-workflow-bg: rgba(var(--n-success-color-rgb), 0.12); /* 使用 Naive UI 暗色主题成功色的 RGB 值,并调整透明度 */ + --color-workflow-icon-wrapper-bg: rgba(var(--n-success-color-rgb), 0.22); + + --color-cert-bg: rgba(var(--n-warning-color-rgb), 0.12); /* 使用 Naive UI 暗色主题警告色的 RGB 值,并调整透明度 */ + --color-cert-icon-wrapper-bg: rgba(var(--n-warning-color-rgb), 0.22); + + --color-monitor-bg: rgba(167, 139, 250, 0.12); /* 调整为较亮的紫色,并设置透明度 */ + --color-monitor-icon-wrapper-bg: rgba(167, 139, 250, 0.22); + --color-monitor-text: #A78BFA; /* 较亮的紫色,用于暗色主题 */ + + --color-decorative-element-bg: rgba(30, 41, 59, 0.7); /* 暗色主题下的装饰背景色 */ +} + diff --git a/frontend/apps/allin-ssl/src/views/404/index.tsx b/frontend/apps/allin-ssl/src/views/404/index.tsx new file mode 100644 index 0000000..c9ec5ce --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/404/index.tsx @@ -0,0 +1,63 @@ +import { useRouter } from 'vue-router' +import { NButton } from 'naive-ui' +import { useThemeCssVar } from '@baota/naive-ui/theme' +import { $t } from '@locales/index' + +// 错误图标 +const errorIcon = (size: number = 16, color: string) => { + return ( + + + + ) +} + +export default defineComponent({ + setup() { + const router = useRouter() + const cssVar = useThemeCssVar(['baseColor', 'textColorBase', 'textColorSecondary', 'textColorDisabled']) + + return () => ( +
+
+
+ 404 +
+
{errorIcon(60, 'var(--n-text-color-base)')}
+
+ {$t('t_0_1744098811152')} +
+ router.push('/')} + > + {$t('t_1_1744098801860')} + +
+ {$t('t_2_1744098804908')} +
+
+
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/authApiManage/components/ApiManageModel.tsx b/frontend/apps/allin-ssl/src/views/authApiManage/components/ApiManageModel.tsx new file mode 100644 index 0000000..3a0c627 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/authApiManage/components/ApiManageModel.tsx @@ -0,0 +1,33 @@ +import { defineComponent } from 'vue' +import { useApiFormController } from '../useController' +import type { AccessItem } from '@/types/access' +import type { PropType } from 'vue' + +/** + * API管理表单组件Props接口 + */ +interface ApiManageFormProps { + data?: AccessItem +} + +/** + * 授权API管理表单组件 + * @description 用于添加和编辑授权API的表单组件 + */ +export default defineComponent({ + name: 'ApiManageForm', + props: { + data: { + type: Object as PropType, + default: () => undefined, + }, + }, + setup(props) { + const { ApiManageForm } = useApiFormController(props as ApiManageFormProps) + return () => ( +
+ +
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/authApiManage/index.tsx b/frontend/apps/allin-ssl/src/views/authApiManage/index.tsx new file mode 100644 index 0000000..01f7fa3 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/authApiManage/index.tsx @@ -0,0 +1,80 @@ +import { defineComponent } from 'vue' +import { NInput, NButton } from 'naive-ui' +import { Search } from '@vicons/carbon' +import { $t } from '@locales/index' +import { useThemeCssVar } from '@baota/naive-ui/theme' +import { useController } from './useController' + +import EmptyState from '@components/TableEmptyState' +import BaseComponent from '@components/BaseLayout' + +/** + * 授权API管理页面组件 + * @description 展示授权API列表、提供添加、编辑、删除等功能的主页面组件 + */ +export default defineComponent({ + name: 'AuthApiManage', + setup() { + const { ApiTable, ApiTablePage, param, fetch, total, openAddForm } = useController() + const cssVar = useThemeCssVar(['contentPadding', 'borderColor', 'headerHeight', 'iconColorHover']) + + return () => ( +
+
+ ( + + {$t('t_0_1745289355714')} + + ), + headerRight: () => ( + { + if (e.key === 'Enter') fetch() + }} + onClear={() => useTimeoutFn(() => fetch(), 100)} + placeholder={$t('t_0_1745289808449')} + clearable + size="large" + class="min-w-[300px]" + v-slots={{ + suffix: () => ( +
+ +
+ ), + }} + /> + ), + content: () => ( +
+ , + }} + /> +
+ ), + footerRight: () => ( +
+ ( + + {$t('t_15_1745227839354')} {total.value} {$t('t_16_1745227838930')} + + ), + }} + /> +
+ ), + }} + /> +
+
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/authApiManage/useController.tsx b/frontend/apps/allin-ssl/src/views/authApiManage/useController.tsx new file mode 100644 index 0000000..69a2aaa --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/authApiManage/useController.tsx @@ -0,0 +1,858 @@ +import { + FormInst, + FormItemRule, + FormProps, + FormRules, + NButton, + NFlex, + NFormItem, + NFormItemGi, + NGrid, + NInput, + NInputNumber, + NSelect, + NSpace, + NTag, + NText, + type DataTableColumns, +} from 'naive-ui' +import { + useModal, + useDialog, + useTable, + useTablePage, + useModalHooks, + useFormHooks, + useForm, + useLoadingMask, +} from '@baota/naive-ui/hooks' +import { useError } from '@baota/hooks/error' +import { isEmail, isIp, isPort, isUrl } from '@baota/utils/business' +import { $t } from '@locales/index' +import { useStore } from './useStore' +import { ApiProjectConfig } from '@config/data' +import type { + AccessItem, + AccessListParams, + AddAccessParams, + SshAccessConfig, + UpdateAccessParams, + NamecheapAccessConfig, + NS1AccessConfig, + CloudnsAccessConfig, + AwsAccessConfig, + AzureAccessConfig, +} from '@/types/access' +import type { VNode, Ref } from 'vue' +import { testAccess } from '@/api/access' + +import ApiManageForm from './components/ApiManageModel' +import SvgIcon from '@components/SvgIcon' +import TypeIcon from '@components/TypeIcon' +import { noSideSpace } from '@lib/utils' +import { JSX } from 'vue/jsx-runtime' + +/** + * 授权API管理控制器接口 + */ +interface AuthApiManageControllerExposes { + loading: Ref + fetch: () => Promise + ApiTable: (props: Record, context: Record) => JSX.Element + ApiTablePage: (props: Record, context: Record) => JSX.Element + param: Ref + total: Ref + openAddForm: () => void +} + +/** + * API表单控制器接口 + */ +interface ApiFormControllerExposes { + ApiManageForm: (props: FormProps, context: Record) => JSX.Element +} + +// 获取Store中的状态和方法 +const { + accessTypeMap, + apiFormProps, + fetchAccessList, + deleteExistingAccess, + addNewAccess, + updateExistingAccess, + resetApiForm, +} = useStore() + +// 错误处理钩子 +const { handleError } = useError() + +/** + * 授权API管理业务逻辑控制器 + * @description 处理授权API相关的业务逻辑,包括列表展示、表单操作等 + * @returns {AuthApiManageControllerExposes} 返回授权API相关的状态数据和处理方法 + */ +export const useController = (): AuthApiManageControllerExposes => { + /** + * 测试授权API + * @param {AccessItem} row - 授权API信息 + */ + const handleTestAccess = async (row: AccessItem): Promise => { + try { + const { fetch, message } = testAccess({ id: row.id, type: row.type }) + message.value = true + fetch() + } catch (error) { + handleError(error) + } + } + + /** + * 创建表格列配置 + * @returns {DataTableColumns} 返回表格列配置数组 + */ + const createColumns = (): DataTableColumns => [ + { + title: $t('t_2_1745289353944'), + key: 'name', + width: 200, + ellipsis: { + tooltip: true, + }, + }, + { + title: $t('t_1_1746754499371'), + key: 'type', + width: 120, + render: (row) => , + }, + { + title: $t('t_2_1746754500270'), + key: 'type', + width: 180, + render: (row) => ( + + {row.access_type?.map((type) => { + return ( + + {accessTypeMap[type as keyof typeof accessTypeMap]} + + ) + })} + + ), + }, + { + title: $t('t_7_1745215914189'), + key: 'create_time', + width: 180, + }, + { + title: $t('t_0_1745295228865'), + key: 'update_time', + width: 180, + }, + { + title: $t('t_8_1745215914610'), + key: 'actions', + width: 240, + align: 'right', + fixed: 'right', + render: (row) => { + return ( + + handleTestAccess(row)}> + {$t('t_16_1746676855270')} + + openEditForm(row)}> + {$t('t_11_1745215915429')} + + confirmDelete(row.id)}> + {$t('t_12_1745215914312')} + + + ) + }, + }, + ] + + // 表格实例 + const { + component: ApiTable, + loading, + param, + total, + fetch, + } = useTable({ + config: createColumns(), + request: fetchAccessList, + defaultValue: { + p: 1, + limit: 10, + search: '', + }, + watchValue: ['p', 'limit'], + }) + + // 分页实例 + const { component: ApiTablePage } = useTablePage({ + param, + total, + alias: { + page: 'p', + pageSize: 'limit', + }, + }) + + /** + * 打开添加授权API弹窗 + */ + const openAddForm = (): void => { + useModal({ + title: $t('t_0_1745289355714'), + area: 500, + component: ApiManageForm, + footer: true, + onUpdateShow: (show) => { + if (!show) fetch() + resetApiForm() + }, + }) + } + + /** + * 打开编辑授权API弹窗 + * @param {AccessItem} row - 授权API信息 + */ + const openEditForm = (row: AccessItem): void => { + useModal({ + title: $t('t_4_1745289354902'), + area: 500, + component: ApiManageForm, + componentProps: { data: row }, + footer: true, + onUpdateShow: (show) => { + if (!show) fetch() + resetApiForm() + }, + }) + } + + /** + * 确认删除授权API + * @param {string} id - 授权API ID + */ + const confirmDelete = (id: string): void => { + useDialog({ + title: $t('t_5_1745289355718'), + content: $t('t_6_1745289358340'), + confirmText: $t('t_5_1744870862719'), + cancelText: $t('t_4_1744870861589'), + onPositiveClick: async () => { + await deleteExistingAccess(id) + await fetch() + }, + }) + } + + // 挂载时,获取数据 + onMounted(fetch) + + return { + loading, + fetch, + ApiTable, + ApiTablePage, + param, + total, + openAddForm, + } +} + +/** + * 表单控制器Props接口 + */ +interface ApiFormControllerProps { + data?: AccessItem +} + +/** + * 授权API表单控制器 + * @description 处理授权API表单相关的业务逻辑 + * @param {ApiFormControllerProps} props - 表单控制器属性 + * @returns {ApiFormControllerExposes} 返回表单控制器对象 + */ +export const useApiFormController = (props: ApiFormControllerProps): ApiFormControllerExposes => { + const { confirm } = useModalHooks() // 弹窗挂载方法 + const { open: openLoad, close: closeLoad } = useLoadingMask({ text: $t('t_0_1746667592819') }) + const { useFormInput, useFormRadioButton, useFormSwitch, useFormTextarea, useFormCustom } = useFormHooks() + const param = (props.data?.id ? ref({ ...props.data, config: JSON.parse(props.data.config) }) : apiFormProps) as Ref< + AddAccessParams | UpdateAccessParams + > + + // 表单规则 + const rules = { + name: { + required: true, + message: $t('t_27_1745289355721'), + trigger: 'input', + }, + type: { + required: true, + message: $t('t_28_1745289356040'), + trigger: 'change', + }, + config: { + host: { + required: true, + trigger: 'input', + validator: (rule: FormItemRule, value: string, callback: (error?: Error) => void) => { + if (!isIp(value)) { + return callback(new Error($t('t_0_1745317313835'))) + } + callback() + }, + }, + port: { + required: true, + trigger: 'input', + validator: (rule: FormItemRule, value: number, callback: (error?: Error) => void) => { + if (!isPort(value.toString())) { + return callback(new Error($t('t_1_1745317313096'))) + } + callback() + }, + }, + user: { + required: true, + trigger: 'input', + message: $t('t_3_1744164839524'), + }, + username: { + required: true, + message: $t('t_0_1747365600180'), + trigger: 'input', + }, + password: { + trigger: 'input', + validator: (rule: FormItemRule, value: string, callback: (error?: Error) => void) => { + if (!value) { + const mapTips = { + westcn: $t('t_1_1747365603108'), + ssh: $t('t_0_1747711335067'), + } + return callback(new Error(mapTips[param.value.type as keyof typeof mapTips])) + } + callback() + }, + }, + key: { + required: true, + message: $t('t_31_1745289355715'), + trigger: 'input', + }, + url: { + required: true, + trigger: 'input', + validator: (rule: FormItemRule, value: string, callback: (error?: Error) => void) => { + if (!isUrl(value)) { + const mapTips = { + btpanel: $t('t_2_1745317314362'), + btwaf: $t('t_0_1747271295174'), + safeline: $t('t_0_1747300383756'), + } + return callback(new Error(mapTips[param.value.type as keyof typeof mapTips])) + } + callback() + }, + }, + api_key: { + trigger: 'input', + validator: (rule: FormItemRule, value: string, callback: (error?: Error) => void) => { + if (!value.length) { + const mapTips = { + cloudflare: $t('t_0_1747042966820'), + btpanel: $t('t_1_1747042969705'), + btwaf: $t('t_1_1747300384579'), + godaddy: $t('t_0_1747984137443'), + ns1: '请输入API Key', + namecheap: '请输入API Key', + } + return callback(new Error(mapTips[param.value.type as keyof typeof mapTips])) + } + callback() + }, + }, + api_secret: { + required: true, + message: $t('t_1_1747984133312'), + trigger: 'input', + }, + access_secret: { + required: true, + message: $t('t_2_1747984134626'), + trigger: 'input', + }, + api_token: { + required: true, + message: $t('t_0_1747617113090'), + trigger: 'input', + }, + access_key_id: { + required: true, + message: $t('t_4_1745317314054'), + trigger: 'input', + }, + access_key_secret: { + required: true, + message: $t('t_5_1745317315285'), + trigger: 'input', + }, + secret_access_key: { + required: true, + message: '请输入Secret Access Key', + trigger: 'input', + }, + api_user: { + required: true, + message: '请输入API User', + trigger: 'input', + }, + auth_id: { + required: true, + message: '请输入Auth ID', + trigger: 'input', + }, + auth_password: { + required: true, + message: '请输入Auth Password', + trigger: 'input', + }, + tenant_id: { + required: true, + message: '请输入Tenant ID', + trigger: 'input', + }, + client_id: { + required: true, + message: '请输入Client ID', + trigger: 'input', + }, + client_secret: { + required: true, + message: '请输入Client Secret', + trigger: 'input', + }, + secret_id: { + required: true, + message: $t('t_6_1745317313383'), + trigger: 'input', + }, + access_key: { + trigger: 'input', + validator: (rule: FormItemRule, value: string, callback: (error?: Error) => void) => { + if (!value) { + const mapTips = { + huawei: $t('t_2_1747271295877'), + baidu: $t('t_3_1747271294475'), + volcengine: $t('t_3_1747365600828'), + qiniu: $t('t_3_1747984134586'), + } + return callback(new Error(mapTips[param.value.type as keyof typeof mapTips])) + } + callback() + }, + }, + secret_key: { + trigger: 'input', + validator: (rule: FormItemRule, value: string, callback: (error?: Error) => void) => { + if (!value.length) { + const mapTips = { + tencentcloud: $t('t_2_1747042967277'), + huawei: $t('t_3_1747042967608'), + baidu: $t('t_4_1747271294621'), + volcengine: $t('t_4_1747365600137'), + } + return callback(new Error(mapTips[param.value.type as keyof typeof mapTips])) + } + callback() + }, + }, + email: { + trigger: 'input', + validator: (rule: FormItemRule, value: string, callback: (error?: Error) => void) => { + if (!isEmail(value)) { + return callback(new Error($t('t_5_1747042965911'))) + } + callback() + }, + }, + }, + } + + // 类型列表 + const typeList = Object.entries(ApiProjectConfig) + .filter(([_, value]) => !(typeof value.notApi === 'boolean' && !value.notApi)) + .map(([key, value]) => ({ + label: value.name, + value: key, + access: value.type || [], + })) + + const typeUrlMap = new Map([ + ['btwaf', '宝塔WAF-URL'], + ['btpanel', '宝塔面板-URL'], + ['1panel', '1Panel-URL'], + ['safeline', '雷池WAF-URL'], + ]) + + // 表单配置 + const config = computed(() => { + const items = [ + useFormInput($t('t_2_1745289353944'), 'name'), + useFormCustom(() => { + return ( + + { + return {$t('t_0_1745833934390')} + }, + }} + /> + + ) + }), + ] + + // 根据不同类型渲染不同的表单项 + switch (param.value.type) { + case 'ssh': + items.push( + useFormCustom(() => { + return ( + + + + + + + + + ) + }), + useFormInput($t('t_44_1745289354583'), 'config.user'), + useFormRadioButton($t('t_45_1745289355714'), 'config.mode', [ + { 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', { + allowInput: noSideSpace, + }) + : useFormTextarea($t('t_1_1746667588689'), 'config.key', { + rows: 3, + placeholder: $t('t_0_1747709067998'), + }), + ) + break + case '1panel': + case 'btpanel': + case 'btwaf': + case 'safeline': + items.push( + useFormInput(typeUrlMap.get(param.value.type) || '', 'config.url', { + allowInput: noSideSpace, + }), + useFormInput( + param.value.type === 'safeline' ? $t('t_1_1747617105179') : $t('t_55_1745289355715'), + param.value.type === 'safeline' ? 'config.api_token' : 'config.api_key', + { + allowInput: noSideSpace, + }, + ), + useFormSwitch( + $t('t_3_1746667592270'), + 'config.ignore_ssl', + { checkedValue: '1', uncheckedValue: '0' }, + { showRequireMark: false }, + ), + ) + break + case 'aliyun': + items.push( + useFormInput('AccessKeyId', 'config.access_key_id', { allowInput: noSideSpace }), + useFormInput('AccessKeySecret', 'config.access_key_secret', { allowInput: noSideSpace }), + ) + break + case 'tencentcloud': + items.push( + useFormInput('SecretId', 'config.secret_id', { allowInput: noSideSpace }), + useFormInput('SecretKey', 'config.secret_key', { allowInput: noSideSpace }), + ) + break + case 'huaweicloud': + case 'baidu': + case 'volcengine': + items.push( + useFormInput('AccessKey', 'config.access_key', { allowInput: noSideSpace }), + useFormInput('SecretKey', 'config.secret_key', { allowInput: noSideSpace }), + ) + break + case 'cloudflare': + items.push( + useFormInput('邮箱', 'config.email', { allowInput: noSideSpace }), + useFormInput('APIKey', 'config.api_key', { allowInput: noSideSpace }), + ) + break + case 'westcn': + items.push( + useFormInput('Username', 'config.username', { allowInput: noSideSpace }), + useFormInput('Password', 'config.password', { allowInput: noSideSpace }), + ) + break + case 'godaddy': + items.push( + useFormInput('API Key', 'config.api_key', { allowInput: noSideSpace }), + useFormInput('API Secret', 'config.api_secret', { allowInput: noSideSpace }), + ) + break + case 'qiniu': + items.push( + useFormInput('AccessKey', 'config.access_key', { allowInput: noSideSpace }), + useFormInput('AccessSecret', 'config.access_secret', { allowInput: noSideSpace }), + ) + break + case 'namecheap': + items.push( + useFormInput('API User', 'config.api_user', { allowInput: noSideSpace }), + useFormInput('API Key', 'config.api_key', { allowInput: noSideSpace }), + ) + break + case 'ns1': + items.push(useFormInput('API Key', 'config.api_key', { allowInput: noSideSpace })) + break + case 'cloudns': + items.push( + useFormInput('Auth ID', 'config.auth_id', { allowInput: noSideSpace }), + useFormInput('Auth Password', 'config.auth_password', { allowInput: noSideSpace }), + ) + break + case 'aws': + items.push( + useFormInput('Access Key ID', 'config.access_key_id', { allowInput: noSideSpace }), + useFormInput('Secret Access Key', 'config.secret_access_key', { allowInput: noSideSpace }), + ) + break + case 'azure': + items.push( + useFormInput('Tenant ID', 'config.tenant_id', { allowInput: noSideSpace }), + useFormInput('Client ID', 'config.client_id', { allowInput: noSideSpace }), + useFormInput('Client Secret', 'config.client_secret', { allowInput: noSideSpace }), + useFormInput('Environment', 'config.environment', { allowInput: noSideSpace, placeholder: 'public' }), + ) + break + default: + break + } + return items + }) + + // 切换类型时,重置表单 + watch( + () => param.value.type, + (newVal) => { + switch (newVal) { + case 'ssh': + param.value.config = { + host: '', + port: 22, + user: 'root', + mode: 'password', + password: '', + } as SshAccessConfig + break + case '1panel': + case 'btpanel': + case 'btwaf': + param.value.config = { + url: '', + api_key: '', + ignore_ssl: '0', + } + break + case 'aliyun': + param.value.config = { + access_key_id: '', + access_key_secret: '', + } + break + case 'baidu': + case 'huaweicloud': + param.value.config = { + access_key: '', + secret_key: '', + } + break + case 'cloudflare': + param.value.config = { + email: '', + api_key: '', + } + break + case 'tencentcloud': + param.value.config = { + secret_id: '', + secret_key: '', + } + break + case 'godaddy': + param.value.config = { + api_key: '', + api_secret: '', + } + break + case 'qiniu': + param.value.config = { + access_key: '', + access_secret: '', + } + break + case 'namecheap': + param.value.config = { + api_user: '', + api_key: '', + } as NamecheapAccessConfig + break + case 'ns1': + param.value.config = { + api_key: '', + } as NS1AccessConfig + break + case 'cloudns': + param.value.config = { + auth_id: '', + auth_password: '', + } as CloudnsAccessConfig + break + case 'aws': + param.value.config = { + access_key_id: '', + secret_access_key: '', + } as AwsAccessConfig + break + case 'azure': + param.value.config = { + tenant_id: '', + client_id: '', + client_secret: '', + environment: '', + } as AzureAccessConfig + break + } + }, + ) + + /** + * 渲染单选标签 + * @param {Record} option - 选项 + * @returns {VNode} 渲染后的VNode + */ + const renderSingleSelectTag = ({ option }: Record): VNode => { + return ( + + {option.label ? ( + renderLabel(option) + ) : ( + {$t('t_0_1745833934390')} + )} + + ) + } + + /** + * 渲染标签 + * @param {Record} option - 选项 + * @returns {VNode} 渲染后的VNode + */ + const renderLabel = (option: { value: string; label: string; access: string[] }): VNode => { + return ( + + + + {option.label} + + + {option.access && + option.access.map((item: string) => { + return ( + + {accessTypeMap[item as keyof typeof accessTypeMap]} + + ) + })} + + + ) + } + + /** + * 提交授权API表单 + * @param {UpdateAccessParams | AddAccessParams} param 请求参数 + * @param {Ref} formRef 表单实例 + */ + const submitApiManageForm = async ( + param: UpdateAccessParams | AddAccessParams, + _formRef: Ref, + ): Promise => { + try { + const data = { ...param, config: JSON.stringify(param.config) } as UpdateAccessParams + if ('id' in param) { + const { id, name, config } = data // 解构出 id, name, config + await updateExistingAccess({ id: id.toString(), name, config } as UpdateAccessParams) + } else { + await addNewAccess(data as AddAccessParams) + } + } catch (error) { + throw handleError(new Error($t('t_4_1746667590873'))) + } + } + + // 使用表单hooks + const { component: ApiManageForm, fetch } = useForm({ + config, + defaultValue: param, + request: submitApiManageForm, + rules: rules as FormRules, + }) + + // 关联确认按钮 + confirm(async (close) => { + try { + openLoad() + await fetch() + resetApiForm() + close() + } catch (error) { + return handleError(error) + } finally { + closeLoad() + } + }) + + return { + ApiManageForm, + } +} + diff --git a/frontend/apps/allin-ssl/src/views/authApiManage/useStore.tsx b/frontend/apps/allin-ssl/src/views/authApiManage/useStore.tsx new file mode 100644 index 0000000..697527a --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/authApiManage/useStore.tsx @@ -0,0 +1,171 @@ +import { getAccessList, addAccess, updateAccess, deleteAccess } from '@api/access' +import { useError } from '@baota/hooks/error' +import { useMessage } from '@baota/naive-ui/hooks' +import { $t } from '@locales/index' + +import type { AccessItem, AccessListParams, AddAccessParams, UpdateAccessParams } from '@/types/access' +import type { TableResponse } from '@baota/naive-ui/types/table' +import type { Ref } from 'vue' + +/** + * 授权API管理状态Store接口定义 + */ +interface AuthApiManageStoreExposes { + // 状态 + apiFormProps: Ref<{ + name: string + type: string + config: Record + }> + accessTypeMap: Record + + // 方法 + fetchAccessList: (params: AccessListParams) => Promise> + addNewAccess: (params: AddAccessParams) => Promise + updateExistingAccess: (params: UpdateAccessParams) => Promise + deleteExistingAccess: (id: string) => Promise + resetApiForm: () => void +} + + + const message = useMessage() // 导入消息钩子 + + /** + * 授权API管理状态 Store + * @description 用于管理授权API相关的状态和操作,包括API列表、类型、分页等 + */ + export const useAuthApiManageStore = defineStore('auth-api-manage-store', (): AuthApiManageStoreExposes => { + const { handleError } = useError() // 导入错误处理钩子 + + // -------------------- 状态定义 -------------------- + /** 添加/编辑API表单 */ + const apiFormProps = ref({ + name: '', + type: 'btpanel', + config: { + url: '', + api_key: '', + ignore_ssl: '0', + }, + }) + + // 表格列配置 + const accessTypeMap = { + dns: $t('t_3_1745735765112'), + host: $t('t_0_1746754500246'), + } + + // -------------------- 请求方法 -------------------- + /** + * 获取授权API列表 + * @description 根据分页和关键词获取授权API列表数据 + * @param {AccessListParams} params - 查询参数 + * @returns {Promise>} 返回列表数据和总数 + */ + const fetchAccessList = async (params: AccessListParams): Promise> => { + try { + const res = await getAccessList(params).fetch() + return { + list: (res.data || []) as T[], + total: res.count, + } + } catch (error) { + handleError(error) + return { list: [] as T[], total: 0 } + } + } + + /** + * 新增授权API + * @description 创建新的授权API配置 + * @param {AddAccessParams} params - 授权API参数 + * @returns {Promise} 操作结果 + */ + const addNewAccess = async (params: AddAccessParams): Promise => { + try { + const { fetch, message: apiMessage } = addAccess(params) + apiMessage.value = true + await fetch() + resetApiForm() + } catch (error) { + if (handleError(error)) message.error($t('t_8_1745289354902')) + throw error + } + } + + /** + * 更新授权API + * @description 更新指定的授权API配置信息 + * @param {UpdateAccessParams} params - 授权API更新参数 + * @returns {Promise} 操作结果 + */ + const updateExistingAccess = async (params: UpdateAccessParams): Promise => { + try { + const { fetch, message: apiMessage } = updateAccess(params) + apiMessage.value = true + await fetch() + resetApiForm() + } catch (error) { + if (handleError(error)) message.error($t('t_40_1745227838872')) + throw error + } + } + + /** + * 删除授权API + * @description 删除指定的授权API配置 + * @param {string} id - 授权API ID + * @returns {Promise} 操作结果 + */ + const deleteExistingAccess = async (id: string): Promise => { + try { + const { fetch, message: apiMessage } = deleteAccess({ id }) + apiMessage.value = true + await fetch() + resetApiForm() + } catch (error) { + if (handleError(error)) message.error($t('t_40_1745227838872')) + throw error + } + } + + /** + * 重置API表单 + * @description 重置表单数据为初始状态 + */ + const resetApiForm = (): void => { + apiFormProps.value = { + name: '', + type: 'btpanel', + config: { + url: '', + api_key: '', + ignore_ssl: '0', + }, + } + } + + // 返回Store暴露的状态和方法 + return { + // 状态 + apiFormProps, + accessTypeMap, + + // 方法 + fetchAccessList, + addNewAccess, + updateExistingAccess, + deleteExistingAccess, + resetApiForm, + } + }) + +/** + * 组合式 API 使用 Store + * @description 提供对授权API管理 Store 的访问,并返回响应式引用 + * @returns {Object} 包含所有 store 状态和方法的对象 + */ +export const useStore = () => { + const store = useAuthApiManageStore() + return { ...store, ...storeToRefs(store) } +} diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/index.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/index.tsx new file mode 100644 index 0000000..c8682e0 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/index.tsx @@ -0,0 +1,55 @@ +import { defineComponent, onBeforeMount, onMounted, ref } from 'vue' +import type { Component } from 'vue' + +import FlowChart from '@components/FlowChart' +import { useStore } from '@autoDeploy/children/workflowView/useStore' +import { useController } from './useController' +/** + * @description 工作流视图主组件,负责加载和渲染流程图。 + */ +export default defineComponent({ + name: 'WorkflowView', + setup() { + const { init } = useController() + const { workflowType, workDefalutNodeData, isEdit } = useStore() + + // 使用import.meta.glob一次性加载所有节点组件 + const modules = import.meta.glob('./node/*/index.tsx', { eager: true }) + + // 创建节点组件映射 + const taskComponents = ref>({}) + + // 初始化任务组件映射 + const initTaskComponents = () => { + const componentsMap: Record = {} + // 获取文件夹名称(对应节点类型)并映射到组件 + Object.entries(modules).forEach(([path, module]) => { + // 获取路径中的节点类型 + const match = path.match(/\/node\/([^/]+)\/index\.tsx$/) + if (match && match[1]) { + const nodeType = match[1] + const componentKey = `${nodeType}Node` + // @ts-ignore + componentsMap[componentKey] = module.default || module + } + }) + taskComponents.value = componentsMap + console.log('已加载节点组件:', Object.keys(componentsMap)) + } + + // 初始化组件 + onBeforeMount(initTaskComponents) + + // 初始化数据 + onMounted(init) + + return () => ( + + ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/lib/BaseNodeValidator.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/lib/BaseNodeValidator.tsx new file mode 100644 index 0000000..0a94ce1 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/lib/BaseNodeValidator.tsx @@ -0,0 +1,85 @@ +import { useNodeValidator } from '@components/FlowChart/lib/verify' +import { useStore } from '@components/FlowChart/useStore' +import { useThemeCssVar } from '@baota/naive-ui/theme' +import { PropType, computed, onUnmounted, watch } from 'vue' + +export interface BaseNodeProps> { + node: { + id: string + config: T + } +} + +export function useBaseNodeValidator>( + props: BaseNodeProps, + rules: any, + renderContent: (valid: boolean, config: T) => any, +) { + // 注册验证器 + const { isRefreshNode } = useStore() + // 初始化节点状态 + const { registerCompatValidator, validate, validationResult, unregisterValidator } = useNodeValidator() + // 主题色 + const cssVar = useThemeCssVar(['warningColor', 'primaryColor']) + + // 是否有效 + const validColor = computed((): string => { + return validationResult.value.valid ? 'var(--n-primary-color)' : 'var(--n-warning-color)' + }) + + // 监听是否刷新节点 + watch( + () => isRefreshNode.value, + (newVal) => { + useTimeoutFn(() => { + registerCompatValidator(props.node.id, rules, props.node.config) + validate(props.node.id) + isRefreshNode.value = null + }, 500) + }, + { immediate: true }, + ) + + onUnmounted(() => unregisterValidator(props.node.id)) + + // 渲染节点状态 + const renderNode = () => ( +
+
{renderContent(validationResult.value.valid, props.node.config)}
+
+ ) + + return { + validationResult, + validColor, + renderNode, + } +} + +// 通用节点组件 +export default defineComponent({ + name: 'BaseNode', + props: { + node: { + type: Object as PropType<{ id: string; config: Record }>, + default: () => ({ id: '', config: {} }), + }, + rules: { + type: Object, + required: true, + }, + renderContent: { + type: Function as PropType<(valid: boolean, config: Record) => any>, + required: true, + }, + }, + setup(props) { + const { renderNode } = useBaseNodeValidator( + props as BaseNodeProps>, + props.rules, + props.renderContent, + ) + + return renderNode + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/lib/DeployUtils.ts b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/lib/DeployUtils.ts new file mode 100644 index 0000000..5a4f396 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/lib/DeployUtils.ts @@ -0,0 +1,238 @@ +import type { FormOption } from './NodeFormConfig' // 假设 FormOption 主要是类型定义 + +import { ApiProjectConfig, ApiProjectType } from '@config/data' +import { $t } from '@locales/index' + +/** + * 部署类型分类 + */ +export const DeployCategories = { + // 默认分类-全部 + ALL: 'all' as const, +} as const + +// 添加动态分类,基于ApiProjectConfig配置 +// 创建一个单独的对象,存储分类映射关系 +export const CategoryMap: Record = {} + +// 初始化分类映射 +Object.entries(ApiProjectConfig).forEach(([key, config]) => { + if (config.type?.includes('host') && config.icon) { + // 将提供商的icon作为分类ID + CategoryMap[key] = config.icon + } +}) + +/** + * 部署提供商类型 + */ +export type DeployProviderType = keyof typeof ApiProjectConfig + +/** + * 支持部署操作的提供商 + * @returns 可部署的提供商类型数组 + */ +export function getDeployableProviders(): DeployProviderType[] { + return Object.keys(ApiProjectConfig).filter((key) => { + const config = ApiProjectConfig[key as DeployProviderType] as ApiProjectType + return Array.isArray(config.type) && config.type.includes('host') + }) as DeployProviderType[] +} + +/** + * 获取部署类型所属分类 + * @param provider 提供商类型 + * @returns 对应的分类 + */ +export function getProviderCategory(provider: string): string { + // 提取主要提供商部分(如 'aliyun-cdn' 返回 'aliyun') + const mainProvider = provider.split('-')[0] as DeployProviderType + + // 从映射中获取分类 + return CategoryMap[mainProvider] || DeployCategories.ALL +} + +/** + * 获取部署类型选项 + * @returns 部署类型选项列表 + */ +export function getDeployTypeOptions(): FormOption[] { + const deployTypeOptions: FormOption[] = [] + const deployableProviders = getDeployableProviders() + + // 遍历所有支持部署的提供商 + deployableProviders.forEach((provider) => { + const providerConfig = ApiProjectConfig[provider] as ApiProjectType + const { icon } = providerConfig + + // 使用类型守卫进行安全访问 + if ('hostRelated' in providerConfig && providerConfig.hostRelated) { + const hostRelated = providerConfig.hostRelated as Record + + // 安全地检查并访问default属性 + if ('default' in hostRelated && hostRelated.default && 'name' in hostRelated.default) { + deployTypeOptions.push({ + label: hostRelated.default.name, + value: provider, + category: getProviderCategory(provider), + icon, + }) + } + + // 添加其他部署相关选项 + Object.entries(hostRelated).forEach(([relatedKey, relatedValue]) => { + if (relatedKey !== 'default' && relatedValue && typeof relatedValue === 'object' && 'name' in relatedValue) { + // 构建部署类型值,如 'aliyun-cdn' + const deployType = `${provider}-${relatedKey}` + + deployTypeOptions.push({ + label: relatedValue.name, + value: deployType, + category: getProviderCategory(provider), + icon, + }) + } + }) + } + }) + + // 根据ApiProjectConfig中的sort字段进行排序 + return deployTypeOptions.sort((a, b) => { + // 提取主要提供商部分(如 'aliyun-cdn' 返回 'aliyun') + const aProvider = (a.value?.toString() || '').split('-')[0] + const bProvider = (b.value?.toString() || '').split('-')[0] + + // 获取排序值,如果没有定义sort则使用999作为默认值 + const aConfig = aProvider ? ApiProjectConfig[aProvider as keyof typeof ApiProjectConfig] : undefined + const bConfig = bProvider ? ApiProjectConfig[bProvider as keyof typeof ApiProjectConfig] : undefined + const aSort = aConfig && typeof aConfig === 'object' && 'sort' in aConfig ? aConfig.sort || 999 : 999 + const bSort = bConfig && typeof bConfig === 'object' && 'sort' in bConfig ? bConfig.sort || 999 : 999 + + // 首先按sort字段排序 + if (aSort !== bSort) { + return aSort - bSort + } + + // 同一提供商内按标签名称排序 + const aLabel = a.label?.toString() || '' + const bLabel = b.label?.toString() || '' + return aLabel.localeCompare(bLabel) + }) +} + +/** + * 获取部署类型标签名称 - 动态获取 + * @param category 分类标识 + * @returns 适合展示的标签名称 + */ +export function getDeployTabName(category: string): string { + // 处理特殊的全部分类 + if (category === DeployCategories.ALL) { + return $t('t_7_1747271292060') // 全部 + } + + // 从ApiProjectConfig中查找匹配的图标,并返回对应的提供商名称 + for (const [_, config] of Object.entries(ApiProjectConfig)) { + if (config.icon === category) { + if (config.name === '本地部署') return $t('t_0_1747969933657') + return config.name + } + } + return '' +} + +/** + * 获取部署类型标签选项 + * @returns 部署类型标签选项 + */ +export function getDeployTabOptions(): { name: string; tab: string }[] { + // 获取所有用到的分类 + const categories = Array.from( + new Set( + getDeployTypeOptions() + .map((option) => option.category) + .filter(Boolean), + ), + ) as string[] + + // 确保ALL总是第一个 + if (!categories.includes(DeployCategories.ALL)) { + categories.unshift(DeployCategories.ALL) + } else { + const index = categories.indexOf(DeployCategories.ALL) + categories.splice(index, 1) + categories.unshift(DeployCategories.ALL) + } + + // 根据ApiProjectConfig中的sort字段进行分类排序 + const sortedCategories = categories.sort((a, b) => { + // 全部分类始终排在第一位 + if (a === DeployCategories.ALL) return -1 + if (b === DeployCategories.ALL) return 1 + + // 查找对应的提供商配置 + const aProviderEntry = Object.entries(ApiProjectConfig).find(([_, config]) => config.icon === a) + const bProviderEntry = Object.entries(ApiProjectConfig).find(([_, config]) => config.icon === b) + + // 获取排序值,如果没有定义sort则使用999作为默认值 + const aSort = aProviderEntry?.[1]?.sort || 999 + const bSort = bProviderEntry?.[1]?.sort || 999 + + return aSort - bSort + }) + + // 生成标签选项 + return sortedCategories.map((category) => { + return { + name: category, + tab: getDeployTabName(category), + } + }) +} + +/** + * 获取本地部署提供商选项 + * @returns 本地部署提供商选项 + */ +export function getLocalProviderOptions(): FormOption[] { + const localProvider = ApiProjectConfig.localhost + + if (localProvider && Array.isArray(localProvider.type) && localProvider.type.includes('host')) { + return [ + { + label: localProvider.hostRelated?.default?.name || $t('t_6_1747271296994'), + value: 'localhost', + }, + ] + } + + return [{ label: $t('t_6_1747271296994'), value: 'localhost' }] +} + +/** + * 过滤部署类型选项 + * @param options 所有部署类型选项 + * @param category 类别 + * @param keyword 搜索关键词 + * @returns 过滤后的选项 + */ +export function filterDeployTypeOptions(options: FormOption[], category: string, keyword: string): FormOption[] { + let filtered = [...options] + + // 根据标签过滤 + if (category !== DeployCategories.ALL) { + filtered = filtered.filter((item) => item.category === category) + } + + // 根据搜索关键词过滤 + if (keyword) { + const searchTerm = keyword.toLowerCase() + filtered = filtered.filter( + (item) => + (item.label?.toString().toLowerCase() || '').includes(searchTerm) || + (item.value?.toString().toLowerCase() || '').includes(searchTerm), + ) + } + + return filtered +} diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/lib/NodeFormConfig.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/lib/NodeFormConfig.tsx new file mode 100644 index 0000000..5031e8f --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/lib/NodeFormConfig.tsx @@ -0,0 +1,204 @@ +import { NFormItem, NSwitch, NText, SelectOption } from 'naive-ui' +import { $t } from '@locales/index' +import { Ref } from 'vue' +import { useFormHooks } from '@baota/naive-ui/hooks' +import { noSideSpace } from '@lib/utils' + +export interface FormOption extends SelectOption { + category?: string + icon?: string +} + +/** + * 节点表单配置工厂 + * 用于生成各种节点类型的表单配置项 + */ +export function createNodeFormConfig() { + const { useFormInput, useFormTextarea, useFormSelect } = useFormHooks() + + return { + /** + * 基础表单组件 + */ + + /** + * 创建文本输入框 + * @param label 标签 + * @param path 字段路径 + * @param options 选项 + * @param formItemProps 表单项属性 + */ + input(label: string, path: string, options: Record = {}, formItemProps: Record = {}) { + return useFormInput( + label, + path, + { placeholder: options.placeholder || $t('t_0_1747817614953') + label, allowInput: noSideSpace, ...options }, + formItemProps, + ) + }, + + /** + * 创建文本域 + * @param label 标签 + * @param path 字段路径 + * @param options 选项 + * @param formItemProps 表单项属性 + */ + textarea(label: string, path: string, options: Record = {}, formItemProps: Record = {}) { + return useFormTextarea( + label, + path, + { placeholder: options.placeholder || $t('t_0_1747817614953') + label, rows: options.rows || 3, ...options }, + { showRequireMark: false, ...formItemProps }, + ) + }, + + /** + * 创建下拉选择框 + * @param label 标签 + * @param path 字段路径 + * @param options 选项数组 + * @param selectProps 选择框属性 + * @param formItemProps 表单项属性 + */ + select( + label: string, + path: string, + options: FormOption[], + selectProps: Record = {}, + formItemProps: Record = {}, + ) { + return useFormSelect(label, path, options as SelectOption[], selectProps, formItemProps) + }, + + /** + * 创建开关组件 + * @param label 标签 + * @param path 字段路径 + * @param valueRef 值引用 + * @param options 选项 + */ + switch( + label: string, + path: string, + valueRef: Ref, + options: { checkedText?: string; uncheckedText?: string; description?: string } = {}, + ) { + const checkedText = options.checkedText || $t('t_1_1747817639034') + const uncheckedText = options.uncheckedText || $t('t_2_1747817610671') + const description = options.description || '' + + return { + type: 'custom' as const, + render: () => { + return ( + + {description && {description}} + checkedText, unchecked: () => uncheckedText }} + /> + + ) + }, + } + }, + + /** + * 创建自定义组件 + * @param render 渲染函数 + */ + custom(render: () => any) { + return { + type: 'custom' as const, + render, + } + }, + + /** + * 部署相关的表单配置 + */ + + /** + * 创建SSH部署相关字段 + * @param valueRef 值引用 + */ + sshDeploy() { + return [ + this.input($t('t_1_1747280813656'), 'certPath', { placeholder: $t('t_30_1746667591892') }), + this.input($t('t_2_1747280811593'), 'keyPath', { placeholder: $t('t_31_1746667593074') }), + this.textarea($t('t_3_1747280812067'), 'beforeCmd', { placeholder: $t('t_21_1745735769154'), rows: 2 }), + this.textarea($t('t_4_1747280811462'), 'afterCmd', { placeholder: $t('t_22_1745735767366'), rows: 2 }), + ] + }, + + /** + * 创建站点相关字段 + * @param valueRef 值引用 + */ + siteDeploy() { + return [this.input($t('t_0_1747296173751'), 'siteName', { placeholder: $t('t_1_1747296175494') })] + }, + + /** + * 创建1Panel站点相关字段 + * @param valueRef 值引用 + */ + onePanelSiteDeploy() { + return [this.input($t('t_6_1747280809615'), 'site_id', { placeholder: $t('t_24_1745735766826') })] + }, + + /** + * 创建CDN相关字段 + * @param valueRef 值引用 + */ + cdnDeploy() { + return [this.input($t('t_17_1745227838561'), 'domain', { placeholder: $t('t_0_1744958839535') })] + }, + + /** + * 创建WAF相关字段 + */ + wafDeploy() { + const regionOptions = [ + { label: 'cn-hangzhou', value: 'cn-hangzhou' }, + { label: 'ap-southeast-1', value: 'ap-southeast-1' }, + ] + + return [ + this.input($t('t_17_1745227838561'), 'domain', { placeholder: $t('t_0_1744958839535') }), + this.select($t('t_7_1747280808936'), 'region', regionOptions, { + placeholder: $t('t_25_1745735766651'), + defaultValue: 'cn-hangzhou', + }), + ] + }, + + /** + * 创建对象存储相关字段 + * @param valueRef 值引用 + */ + storageDeploy() { + return [ + this.input($t('t_17_1745227838561'), 'domain', { placeholder: $t('t_0_1744958839535') }), + this.input($t('t_7_1747280808936'), 'region', { placeholder: $t('t_25_1745735766651') }), + this.input($t('t_8_1747280809382'), 'bucket', { placeholder: $t('t_26_1745735767144') }), + ] + }, + + /** + * 创建跳过选项字段 + * @param valueRef 值引用 + */ + skipOption(valueRef: Ref) { + return this.switch($t('t_9_1747280810169'), 'skip', valueRef, { + checkedText: $t('t_11_1747280809178'), + uncheckedText: $t('t_12_1747280809893'), + description: $t('t_10_1747280816952'), + }) + }, + } +} diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/lib/NodeHandler.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/lib/NodeHandler.tsx new file mode 100644 index 0000000..921059a --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/lib/NodeHandler.tsx @@ -0,0 +1,38 @@ +import { useModal } from '@baota/naive-ui/hooks' +import { $t } from '@locales/index' +import { Ref } from 'vue' +import { JSX } from 'vue/jsx-runtime' + +/** + * @description 节点点击处理器 + * @template T 节点配置类型 + */ +export function useNodeHandler>() { + /** + * @description 处理节点点击事件 + * @param {Ref<{ id: string; name: string; config: T }>} selectedNode 选中的节点 + * @param {string} title 标题后缀(可选) + * @param {string} area 弹窗宽度(默认:'60rem') + * @param {() => JSX.Element} drawerComponent 抽屉组件渲染函数 + * @param {boolean} showFooter 是否显示底部(默认:true) + */ + const handleNodeClick = ( + selectedNode: Ref<{ id: string; name: string; config: T }>, + drawerComponent: (node: any) => JSX.Element, + title?: string | false, + area: string | string[] = '60rem', + showFooter: boolean = true, + ) => { + useModal({ + title: `${selectedNode.value?.name}${title || $t('t_1_1745490731990')}`, + area, + component: () => drawerComponent(selectedNode.value), + confirmText: $t('t_2_1744861190040'), + footer: showFooter, + }) + } + + return { + handleNodeClick, + } +} diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/lib/NodeValidator.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/lib/NodeValidator.tsx new file mode 100644 index 0000000..ceebbb4 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/lib/NodeValidator.tsx @@ -0,0 +1,110 @@ +import { FormRules, FormItemRule } from 'naive-ui' +import { $t } from '@locales/index' +import { isDomain, isDomainGroup, isEmail } from '@baota/utils/business' + +/** + * 节点验证器工厂函数 + * @param nodeType 节点类型名称(用于显示错误信息时指定节点类型) + */ +export function createNodeValidator(nodeType: string) { + return { + /** + * 创建必填规则 + * @param field 字段名称 + * @param message 错误消息 + * @param trigger 触发方式 + */ + required(field: string, message: string, trigger: string | string[] = 'change'): FormItemRule { + return { + required: true, + message: message || $t('t_3_1747817612697', { nodeName: nodeType, field }), + trigger, + } + }, + + /** + * 创建域名验证规则 + * @param trigger 触发方式 + */ + domain(trigger: string | string[] = 'input'): FormItemRule { + return { + required: true, + trigger, + validator: (rule: FormItemRule, value: string) => { + if (!value) { + return new Error($t('t_0_1744958839535')) + } + if (!isDomain(value)) { + return new Error($t('t_4_1747817613325')) + } + return true + }, + } + }, + + /** + * 创建域名组验证规则 + * @param trigger 触发方式 + */ + domainGroup(trigger: string | string[] = 'input'): FormItemRule { + return { + required: true, + trigger, + validator: (rule: FormItemRule, value: string) => { + if (!value) { + return new Error($t('t_0_1744958839535')) + } + if (!isDomainGroup(value)) { + return new Error($t('t_5_1747817619337')) + } + return true + }, + } + }, + + /** + * 创建邮箱验证规则 + * @param trigger 触发方式 + */ + email(trigger: string | string[] = 'input'): FormItemRule { + return { + required: true, + trigger, + validator: (rule: FormItemRule, value: string) => { + if (!value) { + return new Error($t('t_6_1747817644358')) + } + if (!isEmail(value)) { + return new Error($t('t_7_1747817613773')) + } + return true + }, + } + }, + + /** + * 创建自定义验证器 + * @param validator 验证函数 + * @param trigger 触发方式 + */ + custom( + validator: (rule: FormItemRule, value: any) => boolean | Error | Promise, + trigger: string | string[] = 'change', + ): FormItemRule { + return { + required: true, + trigger, + validator, + } + }, + } +} + +/** + * 创建包含节点类型的错误信息 + * @param nodeType 节点类型名称 + * @param fieldError 原始错误信息 + */ +export function createNodeError(nodeType: string, fieldError: string): string { + return `${nodeType}${$t('t_8_1747817614764')}: ${fieldError}` +} diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/apply/index.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/apply/index.tsx new file mode 100644 index 0000000..d659f68 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/apply/index.tsx @@ -0,0 +1,51 @@ +import { Ref } from 'vue' +import { defineComponent, PropType } from 'vue' +import { useBaseNodeValidator } from '@workflowView/lib/BaseNodeValidator' +import { $t } from '@locales/index' +import rules from './verify' +import Drawer from './model' +import { useNodeHandler } from '@workflowView/lib/NodeHandler' +import type { ApplyNodeConfig } from '@components/FlowChart/types' + +interface NodeProps { + node: { + id: string + config: ApplyNodeConfig + } +} + +export default defineComponent({ + name: 'ApplyNode', + props: { + node: { + type: Object as PropType<{ id: string; config: ApplyNodeConfig }>, + default: () => ({ id: '', config: {} }), + }, + }, + setup(props: NodeProps, { expose }) { + /** + * @description 渲染节点内容 + * @param {boolean} valid 是否有效 + * @param {ApplyNodeConfig} config 节点配置 + * @returns {string} 渲染节点内容 + */ + const renderContent = (valid: boolean, config: ApplyNodeConfig) => { + if (valid) return $t('t_9_1747817611448') + config?.domains + return $t('t_9_1745735765287') + } + + const { renderNode } = useBaseNodeValidator(props, rules, renderContent) + + // 使用通用节点处理器 + const { handleNodeClick } = useNodeHandler() + + // 暴露方法给父组件 + expose({ + handleNodeClick: (selectedNode: Ref<{ id: string; name: string; config: ApplyNodeConfig }>) => + handleNodeClick(selectedNode, (node) => ), + }) + + // 返回渲染函数 + return renderNode + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/apply/model.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/apply/model.tsx new file mode 100644 index 0000000..48eaf25 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/apply/model.tsx @@ -0,0 +1,200 @@ +import { NFormItem, NInputNumber } from 'naive-ui' +import { useForm, useFormHooks, useModalHooks } from '@baota/naive-ui/hooks' +import { useStore } from '@components/FlowChart/useStore' +import { $t } from '@locales/index' +import rules from './verify' +import DnsProviderSelect from '@components/DnsProviderSelect' +import CAProviderSelect from '@components/CAProviderSelect' +import type { ApplyNodeConfig } from '@components/FlowChart/types' +import { deepClone } from '@baota/utils/data' +import { noSideSpace } from '@lib/utils' + +export default defineComponent({ + name: 'ApplyNodeDrawer', + props: { + // 节点配置数据 + node: { + type: Object as PropType<{ id: string; config: ApplyNodeConfig }>, + default: () => ({ + id: '', + config: { + domains: '', + email: '', + eabId: '', + ca: '', + proxy: '', + provider_id: '', + provider: '', + end_day: 30, + name_server: '', + skip_check: 0, + algorithm: 'RSA2048', + }, + }), + }, + }, + setup(props) { + const { updateNodeConfig, advancedOptions, isRefreshNode } = useStore() + // 弹窗辅助 + const { confirm } = useModalHooks() + // 获取表单助手函数 + const { useFormInput, useFormSelect, useFormMore, useFormHelp, useFormSwitch } = useFormHooks() + // 表单参数 + const param = ref(deepClone(props.node.config)) + + // 表单渲染配置 + const config = computed(() => { + // 基本选项 + return [ + useFormInput($t('t_17_1745227838561'), 'domains', { + placeholder: $t('t_0_1745735774005'), + allowInput: noSideSpace, + onInput: (val: string) => { + param.value.domains = val.replace(/,/g, ',').replace(/;/g, ',') // 中文逗号分隔 + }, + }), + { + type: 'custom' as const, + render: () => { + return ( + { + param.value.provider_id = val.value + param.value.provider = val.type + }, + }} + /> + ) + }, + }, + { + type: 'custom' as const, + render: () => { + return ( + { + param.value.eabId = val.value + param.value.ca = val.ca + if (val.value) param.value.email = val.email + }, + }} + /> + ) + }, + }, + useFormInput($t('t_68_1745289354676'), 'email', { + placeholder: $t('t_2_1748052862259'), + allowInput: noSideSpace, + }), + + { + type: 'custom' as const, + render: () => { + return ( + +
+ {$t('t_5_1747990228592')} + + {$t('t_6_1747990228465')} +
+
+ ) + }, + }, + useFormMore(advancedOptions), + ...(advancedOptions.value + ? [ + useFormSelect( + $t('t_0_1747647014927'), + 'algorithm', + [ + { label: 'RSA2048', value: 'RSA2048' }, + { label: 'RSA3072', value: 'RSA3072' }, + { label: 'RSA4096', value: 'RSA4096' }, + { label: 'RSA8192', value: 'RSA8192' }, + { label: 'EC256', value: 'EC256' }, + { label: 'EC384', value: 'EC384' }, + ], + {}, + { showRequireMark: false }, + ), + useFormInput( + $t('t_7_1747990227761'), + 'proxy', + { + placeholder: $t('t_8_1747990235316'), + allowInput: noSideSpace, + }, + { showRequireMark: false }, + ), + 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 }, + ), + useFormSwitch( + $t('t_2_1747106957037'), + 'skip_check', + { + checkedValue: 1, + uncheckedValue: 0, + }, + { showRequireMark: false }, + ), + ] + : []), + useFormHelp([ + { + content: $t('t_0_1747040228657'), + }, + { + content: $t('t_1_1747040226143'), + }, + ]), + ] + }) + + // 创建表单实例 + const { component: Form, data, example } = useForm({ defaultValue: param, config, rules }) + + onMounted(() => { + advancedOptions.value = false + }) + + // 确认事件触发 + confirm(async (close) => { + try { + await example.value?.validate() + updateNodeConfig(props.node.id, data.value) // 更新节点配置 + isRefreshNode.value = props.node.id // 刷新节点 + close() + } catch (error) { + console.log(error) + } + }) + + return () => ( +
+
+
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/apply/verify.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/apply/verify.tsx new file mode 100644 index 0000000..e665c1f --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/apply/verify.tsx @@ -0,0 +1,20 @@ +import { FormRules } from 'naive-ui' +import { $t } from '@locales/index' +import { createNodeValidator } from '@workflowView/lib/NodeValidator' + +// 创建申请节点验证器 +const validator = createNodeValidator($t('t_10_1747817611126')) + +// 导出申请节点验证规则 +export default { + domains: validator.domainGroup(), + email: validator.email(), + provider_id: validator.required('provider_id', $t('t_3_1745490735059')), + end_day: validator.custom((rule, value) => { + // 检查值是否为数字类型且大于0 + if (typeof value !== 'number' || isNaN(value) || value < 1) { + return new Error($t('t_9_1747990229640')) + } + return true + }), +} as FormRules diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/deploy/index.module.css b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/deploy/index.module.css new file mode 100644 index 0000000..0ca8154 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/deploy/index.module.css @@ -0,0 +1,88 @@ +/* Deploy Node Drawer Styles */ + +/* 整体布局容器 */ +.configContainer { + @apply flex mt-[2.4rem] gap-4 ; +} + +/* 表单容器 */ +.formContainer { + @apply mt-[2.4rem]; +} + +/* 左侧面板样式 */ +.leftPanel { + @apply border-r border-solid; + border-color: var(--n-border-color); +} + + +/* 右侧面板样式 */ +.rightPanel { + @apply flex-1 flex flex-col; +} + +/* 搜索栏样式 */ +.searchBar { + @apply w-full; +} + +/* Card container styles */ +.cardContainer { + @apply grid grid-cols-3 gap-4 mt-[0.5rem] overflow-y-auto h-[38rem]; + grid-auto-rows: min-content; +} + +/* Option card styles */ +.optionCard { + @apply flex items-center justify-center rounded-[0.4rem] transition-all border-[1px] border-transparent h-[6.2rem]; + border-color: var(--n-border-color); +} + +.optionCardSelected { + @apply border-[1px] relative overflow-hidden; + border-color: var(--n-primary-color); +} + +/* Add checkmark for selected item */ +.optionCardSelected::after { + content: ''; + @apply absolute bottom-[.1rem] right-[.1rem] w-[1rem] h-[1rem] rounded-full z-10; + @apply bg-[length:14px_14px] bg-center bg-no-repeat; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z'/%3E%3C/svg%3E"); +} + +/* Add triangle in bottom-right corner for selected item */ +.optionCardSelected::before { + content: ''; + @apply absolute -bottom-[.1rem] -right-[.1rem] w-0 h-0 z-10 text-white text-[1.2rem] flex items-center justify-center; + border-style: solid; + border-width: 0 0 20px 20px; + border-color: transparent transparent var(--n-primary-color) transparent; + line-height: 0; + padding-left: 2px; + padding-bottom: 2px; +} + +/* Card content styles */ +.cardContent { + @apply flex flex-col items-center justify-center p-[4px] cursor-pointer pt-[1rem] text-[1.3rem]; +} + +/* Icon styles */ +.icon { + @apply mb-[0.4rem]; +} + +.iconSelected { + color: var(--n-primary-color); +} +/* +Footer styles */ +.footer { + @apply flex justify-end w-full mt-[1rem]; +} + +.footerButton { + @apply mr-[0.8rem]; +} diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/deploy/index.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/deploy/index.tsx new file mode 100644 index 0000000..052d4ec --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/deploy/index.tsx @@ -0,0 +1,54 @@ +import { useBaseNodeValidator } from '@workflowView/lib/BaseNodeValidator' +import { $t } from '@locales/index' +import TypeIcon from '@components/TypeIcon' +import rules from './verify' +import type { DeployNodeConfig, DeployNodeInputsConfig } from '@components/FlowChart/types' +import { useNodeHandler } from '@workflowView/lib/NodeHandler' +import Drawer from './model' +import { Ref } from 'vue' + +interface NodeProps { + node: { + id: string + inputs: DeployNodeInputsConfig + config: DeployNodeConfig + } +} + +export default defineComponent({ + name: 'DeployNode', + props: { + node: { + type: Object as PropType<{ id: string; inputs: DeployNodeInputsConfig; config: DeployNodeConfig }>, + default: () => ({ id: '', inputs: {}, config: {} }), + }, + }, + setup(props: NodeProps, { expose }) { + // 使用通用节点验证 + const renderContent = (valid: boolean, config: DeployNodeConfig) => { + if (config.provider) { + return + } + return $t('t_9_1745735765287') + } + const { renderNode } = useBaseNodeValidator(props, rules, renderContent) + + // 使用通用节点处理器 + const { handleNodeClick } = useNodeHandler() + + /** + * @description 点击节点 + * @param {Ref<{ id: string; name: string; config: DeployNodeConfig }>} selectedNode 选中的节点 + */ + const handleNodeClicked = (selectedNode: Ref<{ id: string; name: string; config: DeployNodeConfig }>) => { + handleNodeClick(selectedNode, (node) => , false, '68rem', false) + } + + // 暴露方法给父组件 + expose({ + handleNodeClick: handleNodeClicked, + }) + + return renderNode + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/deploy/model.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/deploy/model.tsx new file mode 100644 index 0000000..e4696fe --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/deploy/model.tsx @@ -0,0 +1,429 @@ +import { NButton, NCard, NStep, NSteps, NText, NTooltip, NTabs, NTabPane, NInput, NDivider } from 'naive-ui' +import { useForm, useModalClose, useModalOptions, useMessage } from '@baota/naive-ui/hooks' +import { useThemeCssVar } from '@baota/naive-ui/theme' +import { useError } from '@baota/hooks/error' +import { useStore } from '@components/FlowChart/useStore' +import { getSites } from '@api/access' + +import { $t } from '@locales/index' +import { deepClone } from '@baota/utils/data' + +import verifyRules from './verify' +import { createNodeFormConfig, FormOption } from '@workflowView/lib/NodeFormConfig' +import { + DeployCategories, + getDeployTypeOptions, + getDeployTabOptions, + getLocalProviderOptions, + filterDeployTypeOptions, +} from '@workflowView/lib/DeployUtils' + +import SvgIcon from '@components/SvgIcon' +import DnsProviderSelect from '@components/DnsProviderSelect' +import SearchOutlined from '@vicons/antd/es/SearchOutlined' + +import type { DeployNodeConfig, DeployNodeInputsConfig } from '@components/FlowChart/types' +import type { DnsProviderType } from '@components/DnsProviderSelect/types' +import type { VNode } from 'vue' + +import styles from './index.module.css' + +type StepStatus = 'process' | 'wait' | 'finish' | 'error' + +/** + * 部署节点抽屉组件 + */ +export default defineComponent({ + name: 'DeployNodeDrawer', + props: { + // 节点配置数据 + node: { + type: Object as PropType<{ id: string; config: DeployNodeConfig; inputs: DeployNodeInputsConfig[] }>, + default: () => ({ + id: '', + inputs: [], + config: { + provider: '', + provider_id: '', + inputs: { + fromNodeId: '', + name: '', + }, + skip: 1, + }, + }), + }, + }, + setup(props) { + const { updateNode, updateNodeConfig, findApplyUploadNodesUp, isRefreshNode } = useStore() + // 样式支持 + const cssVar = useThemeCssVar(['primaryColor', 'borderColor']) + // 错误处理 + const { handleError } = useError() + // 消息处理 + const message = useMessage() + // 弹窗配置 + const modalOptions = useModalOptions() + // 弹窗关闭 + const closeModal = useModalClose() + // 表单配置工厂 + const formConfig = createNodeFormConfig() + + // 部署类型选项 + const deployTypeOptions = getDeployTypeOptions() + // 部署类型标签选项 + const deployTabOptions = getDeployTabOptions() + // 证书选项 + const certOptions = ref([]) + // 网站选项(用于btpanel-site类型) + const siteOptions = ref([]) + // 网站选项加载状态 + const siteOptionsLoading = ref(false) + // 当前步骤 + const current = ref(1) + // 是否是下一步 + const next = ref(true) + // 当前步骤状态 + const currentStatus = ref('process') + // 当前选中的tab + const currentTab = ref(DeployCategories.ALL) + // 搜索关键字 + const searchKeyword = ref('') + + // 表单参数 + const param = ref(deepClone(props.node.config)) + // 本地提供商 + const localProvider = ref(getLocalProviderOptions()) + // 提供商描述 + const provider = computed((): string => { + return param.value.provider + ? $t('t_4_1746858917773') + ':' + deployTypeOptions.find((item) => item.value === param.value.provider)?.label + : $t('t_19_1745735766810') + }) + + // 过滤后的部署类型选项 + const filteredDeployTypes = computed((): FormOption[] => { + return filterDeployTypeOptions(deployTypeOptions, currentTab.value, searchKeyword.value) + }) + + // 表单配置 + const nodeFormConfig = computed(() => { + const config = [] + // 部署提供商选择 + if (param.value.provider !== 'localhost') { + config.push( + formConfig.custom(() => { + // 创建props对象 + const dnsProviderProps = { + type: param.value.provider as DnsProviderType, + path: 'provider_id', + value: param.value.provider_id, + valueType: 'value' as const, + isAddMode: true, + 'onUpdate:value': (val: { value: number | string; type: string }) => { + if ( + val.value !== '' && + param.value.provider_id !== '' && + param.value.provider_id !== val.value && + param.provider === 'btpanel-site' + ) { + param.value.siteName = [] + } + param.value.provider_id = val.value + param.value.type = val.type + }, + } + return () as VNode + }), + ) + } else { + config.push(formConfig.select($t('t_0_1746754500246'), 'provider', localProvider.value)) + } + + // 证书来源选择 + config.push( + formConfig.select($t('t_1_1745748290291'), 'inputs.fromNodeId', certOptions.value, { + onUpdateValue: (val: string, option: { label: string; value: string }) => { + param.value.inputs.fromNodeId = val + param.value.inputs.name = option?.label + }, + }), + ) + + console.log(param.value.provider) + // 根据不同的部署类型添加不同的表单配置 + switch (param.value.provider) { + case 'localhost': + case 'ssh': + config.push(...formConfig.sshDeploy()) + break + case 'btpanel-site': + // 使用异步加载的网站选择器 + config.push( + formConfig.select($t('t_0_1747296173751'), 'siteName', siteOptions.value, { + placeholder: $t('t_10_1747990232207'), + multiple: true, + filterable: true, + remote: true, + clearable: true, + loading: siteOptionsLoading.value, + onSearch: handleSiteSearch, + }), + ) + break + case 'btwaf-site': + case 'btpanel-dockersite': + case 'safeline-site': + config.push(...formConfig.siteDeploy()) + break + case '1panel-site': + config.push(...formConfig.onePanelSiteDeploy()) + break + case 'tencentcloud-cdn': + case 'tencentcloud-waf': + case 'tencentcloud-teo': + case 'aliyun-cdn': + case 'baidu-cdn': + case 'qiniu-cdn': + case 'qiniu-oss': + case 'huaweicloud-cdn': + config.push(...formConfig.cdnDeploy()) + break + case 'aliyun-waf': + config.push(...formConfig.wafDeploy()) + break + case 'tencentcloud-cos': + case 'aliyun-oss': + config.push(...formConfig.storageDeploy()) + break + } + + // 添加跳过选项 + config.push(formConfig.skipOption(param)) + return config + }) + + watch( + () => param.value.provider_id, + () => { + if (param.value.provider === 'btpanel-site') { + handleSiteSearch('') + } + }, + ) + + /** + * 处理网站搜索 + * @param query 搜索关键字 + */ + const handleSiteSearch = useThrottleFn(async (query: string): Promise => { + if (param.value.provider !== 'btpanel-site') return + if (!param.value.provider_id) return + try { + siteOptionsLoading.value = true + const { data } = await getSites({ + id: param.value.provider_id.toString(), + type: param.value.provider, + search: query, + limit: '100', + }).fetch() + siteOptions.value = data?.map((siteName: string) => ({ + label: siteName, + value: siteName, + })) + // param.value.siteName = [] + } catch (error) { + handleError(error) + siteOptions.value = [] + } finally { + siteOptionsLoading.value = false + } + }, 1000) + + /** + * 下一步 + */ + const nextStep = async (): Promise => { + if (!param.value.provider) return message.error($t('t_0_1746858920894')) + if (param.value.provider === 'localhost') { + delete param.value.provider_id + } + // else { + // param.value.provider_id = props.node.config.provider_id + // } + // 加载证书来源选项 + certOptions.value = findApplyUploadNodesUp(props.node.id).map((item) => { + return { label: item.name, value: item.id } + }) + + if (!certOptions.value.length) { + message.warning($t('t_3_1745748298161')) + } else if (!param.value.inputs?.fromNodeId) { + param.value.inputs = { + name: certOptions.value[0]?.label || '', + fromNodeId: certOptions.value[0]?.value || '', + } as DeployNodeInputsConfig + } + + current.value++ + next.value = false + } + + // 表单组件 + const { component: Form, example } = useForm({ + config: nodeFormConfig, + defaultValue: param, + rules: verifyRules, + }) + + /** + * 上一步 + */ + const prevStep = (): void => { + current.value-- + next.value = true + param.value = {} + param.value.provider_id = '' + param.value.provider = '' + } + + /** + * 提交 + */ + const submit = async (): Promise => { + try { + await example.value?.validate() + const tempData = deepClone(param.value) + + // 处理siteName字段的提交转换:将数组合并为字符串 + if (tempData.provider === 'btpanel-site' && tempData.siteName && Array.isArray(tempData.siteName)) { + tempData.siteName = tempData.siteName.join(',') + } + + const inputs = tempData.inputs + // 将输入值直接传递给updateNodeConfig + updateNodeConfig(props.node.id, { + ...tempData, + }) + // 单独更新inputs + updateNode(props.node.id, { inputs: [inputs] } as any, false) + isRefreshNode.value = props.node.id + closeModal() + } catch (error) { + handleError(error) + } + } + + // 初始化 + onMounted(() => { + // 如果已经选择了部署类型,则跳转到下一步 + if (param.value.provider) { + if (props.node.inputs) param.value.inputs = props.node.inputs[0] + // 处理siteName字段的读取转换:将字符串拆分为数组(针对已有数据) + if (param.value.provider === 'btpanel-site' && param.value.siteName) { + handleSiteSearch('') + param.value.siteName = param.value.siteName.split(',').filter(Boolean) + } + nextStep() + } + }) + + return () => ( +
+ + + + + {current.value === 1 && ( +
+
+ (currentTab.value = val)} + > + {deployTabOptions.map((tab) => ( + + ))} + +
+
+
+ (searchKeyword.value = val)} + placeholder={$t('t_14_1747280811231')} + clearable + > + {{ + suffix: () => ( +
+ +
+ ), + }} +
+
+ +
+ {filteredDeployTypes.value.map((item) => ( +
{ + param.value.provider = item.value + }} + > +
+ + {item.label} +
+
+ ))} +
+
+
+ )} + {current.value === 2 && ( + + + + )} +
+ + {$t('t_4_1744870861589')} + + ( + + {next.value ? $t('t_27_1745735764546') : $t('t_0_1745738961258')} + + ), + }} + > + {next.value ? $t('t_4_1745765868807') : null} + + {!next.value && ( + + {$t('t_1_1745738963744')} + + )} +
+
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/deploy/verify.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/deploy/verify.tsx new file mode 100644 index 0000000..c2f7f29 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/deploy/verify.tsx @@ -0,0 +1,63 @@ +import { FormRules } from 'naive-ui' +import { $t } from '@locales/index' +import { createNodeValidator } from '@workflowView/lib/NodeValidator' +import { isDomain } from '@baota/utils/business' + +// 创建部署节点验证器 +const validator = createNodeValidator($t('t_11_1747817612051')) + +// 导出部署节点验证规则 +export default { + provider: validator.required('provider', $t('t_0_1746858920894')), + + provider_id: validator.custom((rule, value) => { + if (!value) { + return new Error($t('t_0_1746858920894')) + } + return true + }), + + 'inputs.fromNodeId': validator.required('inputs.fromNodeId', $t('t_3_1745748298161')), + + // SSH相关字段验证 + certPath: validator.required('certPath', $t('t_30_1746667591892'), 'input'), + keyPath: validator.required('keyPath', $t('t_31_1746667593074'), 'input'), + + // 站点相关字段验证 + siteName: validator.custom((rule, value) => { + if (!value) { + return new Error($t('t_1_1747296175494')) + } + // 支持字符串和数组两种类型 + if (typeof value === 'string') { + if (!value.trim()) { + return new Error($t('t_1_1747296175494')) + } + } else if (Array.isArray(value)) { + if (value.length === 0) { + return new Error($t('t_1_1747296175494')) + } + } else { + return new Error($t('t_1_1747296175494')) + } + return true + }, 'input'), + + // 1panel相关字段验证 + site_id: validator.required('site_id', $t('t_24_1745735766826'), 'input'), + + // CDN相关字段验证 + domain: validator.custom((rule, value) => { + if (!value) { + return new Error($t('t_0_1744958839535')) + } + if (!isDomain(value)) { + return new Error($t('t_0_1744958839535')) + } + return true + }, 'input'), + + // 存储桶相关字段验证 + region: validator.required('region', $t('t_25_1745735766651'), 'input'), + bucket: validator.required('bucket', $t('t_26_1745735767144'), 'input'), +} as FormRules diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/notify/index.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/notify/index.tsx new file mode 100644 index 0000000..6e7c243 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/notify/index.tsx @@ -0,0 +1,46 @@ +import { NotifyNodeConfig } from '@components/FlowChart/types' +import { useNodeHandler } from '@workflowView/lib/NodeHandler' +import Drawer from './model' +import { Ref } from 'vue' + +import rules from './verify' +import TypeIcon from '@components/TypeIcon' +import { $t } from '@locales/index' +import { useBaseNodeValidator } from '@workflowView/lib/BaseNodeValidator' + +export default defineComponent({ + name: 'NotifyNode', + props: { + node: { + type: Object as PropType<{ id: string; config: NotifyNodeConfig }>, + default: () => ({ id: '', config: {} }), + }, + }, + setup(props, { expose }) { + // 使用通用节点验证 + const renderContent = (valid: boolean, config: NotifyNodeConfig) => { + if (config.provider) { + console.log(config.provider) + return + } + return $t('t_9_1745735765287') + } + + const { renderNode } = useBaseNodeValidator(props, rules, renderContent) + + // 使用通用节点处理器 + const { handleNodeClick } = useNodeHandler() + + /** + * @description 点击节点 + * @param {Ref<{ id: string; name: string; config: NotifyNodeConfig }>} selectedNode 选中的节点 + */ + const handleNodeClicked = (selectedNode: Ref<{ id: string; name: string; config: NotifyNodeConfig }>) => { + handleNodeClick(selectedNode, (node) => ) + } + + // 暴露方法给父组件 + expose({ handleNodeClick: handleNodeClicked }) + return renderNode + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/notify/model.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/notify/model.tsx new file mode 100644 index 0000000..eb8944b --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/notify/model.tsx @@ -0,0 +1,92 @@ +import { useForm, useFormHooks, useModalHooks } from '@baota/naive-ui/hooks' +import { FormConfig } from '@baota/naive-ui/types/form' +import { useStore } from '@components/FlowChart/useStore' +import { useError } from '@baota/hooks/error' +import { $t } from '@locales/index' +// 假设这个类型需要在types文件中定义 + +import NotifyProviderSelect from '@components/NotifyProviderSelect' +import verify from './verify' + +import { NotifyNodeConfig } from '@components/FlowChart/types' +import { deepClone } from '@baota/utils/data' +import { noSideSpace } from '@lib/utils' + +export default defineComponent({ + name: 'NotifyNodeDrawer', + props: { + // 节点配置数据 + node: { + type: Object as PropType<{ id: string; config: NotifyNodeConfig }>, + default: () => ({ + id: '', + config: { + provider: '', + provider_id: '', + subject: '', + body: '', + }, + }), + }, + }, + setup(props) { + const { updateNodeConfig, isRefreshNode } = useStore() + const { useFormInput, useFormTextarea, useFormCustom } = useFormHooks() + const { confirm } = useModalHooks() + const { handleError } = useError() + const param = ref(deepClone(props.node.config)) + + // 表单渲染配置 + const formConfig: FormConfig = [ + useFormInput($t('t_0_1745920566646'), 'subject', { + placeholder: $t('t_3_1745887835089'), + allowInput: noSideSpace, + }), + useFormTextarea($t('t_1_1745920567200'), 'body', { + placeholder: $t('t_4_1745887835265'), + rows: 4, + allowInput: noSideSpace, + }), + useFormCustom(() => ( + { + param.value.provider_id = item.value + param.value.provider = item.type + }} + /> + )), + ] + + // 创建表单实例 + const { + component: Form, + data, + example, + } = useForm({ + defaultValue: param, + config: formConfig, + rules: verify, + }) + + // 确认事件触发 + confirm(async (close) => { + try { + await example.value?.validate() + updateNodeConfig(props.node.id, data.value) + isRefreshNode.value = props.node.id + close() + } catch (error) { + handleError(error) + } + }) + + return () => ( +
+ +
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/notify/verify.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/notify/verify.tsx new file mode 100644 index 0000000..68acafe --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/notify/verify.tsx @@ -0,0 +1,49 @@ +import type { FormItemRule, FormRules } from 'naive-ui' +import { $t } from '@locales/index' + +export default { + subject: { + trigger: 'input', + required: true, + validator: (rule: FormItemRule, value: string) => { + return new Promise((resolve, reject) => { + if (!value) { + reject(new Error($t('t_3_1745887835089'))) + } else if (value.length > 100) { + reject(new Error($t('t_3_1745887835089') + '长度不能超过100个字符')) + } else { + resolve() + } + }) + }, + }, + body: { + trigger: 'input', + required: true, + validator: (rule: FormItemRule, value: string) => { + return new Promise((resolve, reject) => { + if (!value) { + reject(new Error($t('t_4_1745887835265'))) + } else if (value.length > 1000) { + reject(new Error($t('t_4_1745887835265') + '长度不能超过1000个字符')) + } else { + resolve() + } + }) + }, + }, + provider_id: { + trigger: 'change', + type: 'string', + required: true, + validator: (rule: FormItemRule, value: string) => { + return new Promise((resolve, reject) => { + if (!value) { + reject(new Error($t('t_0_1745887835267'))) + } else { + resolve() + } + }) + }, + }, +} as FormRules diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/start/index.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/start/index.tsx new file mode 100644 index 0000000..7fb9bd8 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/start/index.tsx @@ -0,0 +1,53 @@ +import { $t } from '@locales/index' +import rules from './verify' +import type { StartNodeConfig } from '@components/FlowChart/types' +import { useNodeHandler } from '@workflowView/lib/NodeHandler' +import Drawer from './model' +import { Ref } from 'vue' +import { useBaseNodeValidator } from '@workflowView/lib/BaseNodeValidator' + +interface NodeProps { + node: { + id: string + config: StartNodeConfig + } +} + +export default defineComponent({ + name: 'StartNode', + props: { + node: { + type: Object as PropType<{ id: string; config: StartNodeConfig }>, + default: () => ({ id: '', config: {} }), + }, + }, + setup(props: NodeProps, { expose }) { + // 使用通用节点验证 + const renderContent = (valid: boolean, config: StartNodeConfig) => { + if (valid) { + return config.exec_type === 'auto' ? $t('t_4_1744875940750') : $t('t_5_1744875940010') + } + return '未配置' + } + + const { renderNode } = useBaseNodeValidator(props, rules, renderContent) + + // 使用通用节点处理器 + const { handleNodeClick } = useNodeHandler() + + /** + * @description 点击节点 + * @param {Ref<{ id: string; name: string; config: StartNodeConfig }>} selectedNode 选中的节点 + */ + const handleNodeClicked = (selectedNode: Ref<{ id: string; name: string; config: StartNodeConfig }>) => { + handleNodeClick(selectedNode, (node) => ) + } + + // 暴露方法给父组件 + expose({ + handleNodeClick: handleNodeClicked, + }) + + return renderNode + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/start/model.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/start/model.tsx new file mode 100644 index 0000000..f1f5d25 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/start/model.tsx @@ -0,0 +1,219 @@ +import { NFormItemGi, NGrid, NInputGroup, NInputGroupLabel, NInputNumber, NSelect } from 'naive-ui' +import { useForm, useFormHooks, useModalHooks } from '@baota/naive-ui/hooks' +import { $t } from '@locales/index' +import { useStore } from '@components/FlowChart/useStore' +import rules from './verify' + +import type { StartNodeConfig } from '@components/FlowChart/types' +import type { FormConfig } from '@baota/naive-ui/types/form' + +// 类型 +import type { VNode } from 'vue' +import { useError } from '@baota/hooks/error' +import { deepClone } from '@baota/utils/data' + +export default defineComponent({ + name: 'StartNodeDrawer', + props: { + // 节点配置数据 + node: { + type: Object as PropType<{ id: string; config: StartNodeConfig }>, + default: () => ({ + id: '', + config: { + exec_type: 'auto', + }, + }), + }, + }, + setup(props) { + const { updateNodeConfig, isRefreshNode } = useStore() + // 弹窗辅助 + const { confirm } = useModalHooks() + // 错误处理 + const { handleError } = useError() + // 获取表单助手函数 + const { useFormRadio, useFormCustom } = useFormHooks() + // 表单参数 + const param = ref(deepClone(props.node.config)) + + // 周期类型选项 + const cycleTypeOptions = [ + { label: $t('t_2_1744875938555'), value: 'day' }, + { label: $t('t_0_1744942117992'), value: 'week' }, + { label: $t('t_3_1744875938310'), value: 'month' }, + ] + + // 星期选项 + const weekOptions = [ + { label: $t('t_1_1744942116527'), value: 1 }, + { label: $t('t_2_1744942117890'), value: 2 }, + { label: $t('t_3_1744942117885'), value: 3 }, + { label: $t('t_4_1744942117738'), value: 4 }, + { label: $t('t_5_1744942117167'), value: 5 }, + { label: $t('t_6_1744942117815'), value: 6 }, + { label: $t('t_7_1744942117862'), value: 0 }, + ] + + // 定义默认值常量,避免重复 + const DEFAULT_AUTO_SETTINGS: Record = { + day: { exec_type: 'auto', type: 'day', hour: 1, minute: 0 }, + week: { exec_type: 'auto', type: 'week', hour: 1, minute: 0, week: 1 }, + month: { exec_type: 'auto', type: 'month', hour: 1, minute: 0, month: 1 }, + } + + // 创建时间输入input + const createTimeInput = (value: number, updateFn: (val: number) => void, max: number, label: string): VNode => ( + + { + if (val !== null) { + updateFn(val) + } + }} + max={max} + min={0} + showButton={false} + class="w-full" + /> + {label} + + ) + + // 表单渲染 + const formRender = computed(() => { + const formItems: FormConfig = [] + if (param.value.exec_type === 'auto') { + formItems.push( + useFormCustom(() => { + return ( + + + + + + {param.value.type !== 'day' && ( + + {param.value.type === 'week' ? ( + { + if (typeof val === 'number') { + param.value.week = val + } + }} + options={weekOptions} + /> + ) : ( + createTimeInput( + param.value.month || 0, + (val: number) => (param.value.month = val), + 31, + $t('t_29_1744958838904'), + ) + )} + + )} + + + {createTimeInput( + param.value.hour || 0, + (val: number) => (param.value.hour = val), + 23, + $t('t_5_1744879615277'), + )} + + + + {createTimeInput( + param.value.minute || 0, + (val: number) => (param.value.minute = val), + 59, + $t('t_3_1744879615723'), + )} + + + ) + }), + ) + } + return [ + // 运行模式选择 + useFormRadio($t('t_30_1745735764748'), 'exec_type', [ + { label: $t('t_4_1744875940750'), value: 'auto' }, + { label: $t('t_5_1744875940010'), value: 'manual' }, + ]), + ...formItems, + ] + }) + + // 创建表单实例 + const { + component: Form, + data, + example, + } = useForm({ + defaultValue: param, + config: formRender, + rules, + }) + + // 更新参数的函数 + const updateParamValue = (updates: StartNodeConfig) => { + let newParams = { ...updates } + if (newParams.exec_type === 'manual') { + // 小时随机 1-6 + const randomHour = Math.floor(Math.random() * 6) + 1 + // 分钟每5分钟随机,0-55 + const randomMinute = Math.floor(Math.random() * 12) * 5 + newParams = { + ...newParams, + hour: randomHour, + minute: randomMinute, + } + param.value = newParams + } + } + + // 监听执行类型变化 + watch( + () => param.value.exec_type, + (newVal) => { + if (newVal === 'auto') { + updateParamValue(DEFAULT_AUTO_SETTINGS.day as StartNodeConfig) + } else if (newVal === 'manual') { + updateParamValue({ exec_type: 'manual' }) + } + }, + ) + + // 监听类型变化 + watch( + () => param.value.type, + (newVal) => { + if (newVal && param.value.exec_type === 'auto') { + updateParamValue(DEFAULT_AUTO_SETTINGS[newVal] as StartNodeConfig) + } + }, + ) + + // 确认事件触发 + confirm(async (close) => { + try { + await example.value?.validate() + updateNodeConfig(props.node.id, data.value) // 更新节点配置 + isRefreshNode.value = props.node.id // 刷新节点 + close() + } catch (error) { + handleError(error) + } + }) + + return () => ( +
+ +
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/start/verify.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/start/verify.tsx new file mode 100644 index 0000000..6b1217c --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/start/verify.tsx @@ -0,0 +1,11 @@ +import type { FormRules } from 'naive-ui' +import { $t } from '@locales/index' + +export default { + exec_type: { required: true, message: $t('t_31_1745735767891'), trigger: 'change' }, + type: { required: true, message: $t('t_32_1745735767156'), trigger: 'change' }, + week: { required: true, message: $t('t_33_1745735766532'), trigger: 'input', type: 'number' }, + month: { required: true, message: $t('t_33_1745735766532'), trigger: 'input', type: 'number' }, + hour: { required: true, message: $t('t_33_1745735766532'), trigger: 'input', type: 'number' }, + minute: { required: true, message: $t('t_33_1745735766532'), trigger: 'input', type: 'number' }, +} as FormRules diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/upload/index.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/upload/index.tsx new file mode 100644 index 0000000..663e946 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/upload/index.tsx @@ -0,0 +1,41 @@ +import { useBaseNodeValidator } from '@workflowView/lib/BaseNodeValidator' +import rules from './verify' +import { $t } from '@locales/index' +import { UploadNodeConfig } from '@components/FlowChart/types' +import { useNodeHandler } from '@workflowView/lib/NodeHandler' +import Drawer from './model' +import { Ref } from 'vue' + +export default defineComponent({ + name: 'UploadNode', + props: { + node: { + type: Object as PropType<{ id: string; config: UploadNodeConfig }>, + default: () => ({ id: '', config: {} }), + }, + }, + setup(props, { expose }) { + // 使用通用节点验证 + const renderContent = (valid: boolean, config: UploadNodeConfig) => { + return valid ? $t('t_8_1745735765753') : $t('t_9_1745735765287') + } + + const { renderNode } = useBaseNodeValidator(props, rules, renderContent) + + // 使用通用节点处理器 + const { handleNodeClick } = useNodeHandler() + + /** + * @description 点击节点 + * @param {Ref<{ id: string; name: string; config: UploadNodeConfig }>} selectedNode 选中的节点 + */ + const handleNodeClicked = (selectedNode: Ref<{ id: string; name: string; config: UploadNodeConfig }>) => { + handleNodeClick(selectedNode, (node) => ) + } + + // 暴露方法给父组件 + expose({ handleNodeClick: handleNodeClicked }) + + return renderNode + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/upload/model.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/upload/model.tsx new file mode 100644 index 0000000..0e50895 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/upload/model.tsx @@ -0,0 +1,174 @@ +import { useForm, useFormHooks, useModalHooks } from '@baota/naive-ui/hooks' +import { useError } from '@baota/hooks/error' +import { deepClone } from '@baota/utils/data' + +import { $t } from '@locales/index' +import { getCertList, uploadCert } from '@api/cert' +import { useStore } from '@components/FlowChart/useStore' +import verifyRules from './verify' + +import type { FormConfig } from '@baota/naive-ui/types/form' +import type { UploadNodeConfig } from '@components/FlowChart/types' +import type { CertItem } from '@/types/cert' +import { noSideSpace } from '@lib/utils' + +export default defineComponent({ + name: 'UploadNodeDrawer', + props: { + // 节点配置数据 + node: { + type: Object as PropType<{ id: string; config: UploadNodeConfig }>, + default: () => ({ + id: '', + config: { + cert_id: '', + cert: '', + key: '', + }, + }), + }, + }, + setup(props) { + // 获取store + const { updateNodeConfig, isRefreshNode } = useStore() + // 获取表单助手函数 + const { useFormTextarea, useFormSelect, useFormHelp } = useFormHooks() + // 节点配置数据 + const param = ref(deepClone(props.node.config)) + // 弹窗辅助 + const { confirm, options } = useModalHooks() + // 错误处理 + const { handleError } = useError() + // 弹窗配置 + const modalOptions = options() + + // 证书列表 + const certList = ref<{ cert: string; key: string; label: string; value: string }[]>([ + { + cert: '', + key: '', + label: '自定义证书', + value: '', + }, + ]) + + const isReadonly = computed(() => { + return param.value.cert_id === '' ? false : true + }) + + const textAreaProps = computed(() => { + return { + readonly: isReadonly.value, + allowInput: noSideSpace, + rows: 6, + } + }) + + // 表单渲染配置 + const formConfig = computed( + () => + [ + useFormSelect( + $t('t_0_1747110184700'), + 'cert_id', + certList.value, + { + filterable: true, + onUpdateValue: (val: string) => { + param.value.cert_id = val + const item = findCertItem(val) + if (item) { + param.value.cert = item.cert + param.value.key = item.key + } + }, + }, + { showRequireMark: false }, + ), + useFormTextarea($t('t_34_1745735771147'), 'cert', { + placeholder: $t('t_35_1745735781545'), + ...textAreaProps.value, + }), + useFormTextarea($t('t_36_1745735769443'), 'key', { + placeholder: $t('t_37_1745735779980'), + ...textAreaProps.value, + }), + useFormHelp([{ content: $t('t_1_1747110191587') }, { content: $t('t_2_1747110193465') }]), + ] as FormConfig, + ) + + // 创建表单实例 + const { + component: Form, + data, + example, + } = useForm({ + defaultValue: param, + config: formConfig, + rules: verifyRules, + }) + + /** + * 查找证书项 + * @param {string} val 证书值 + * @returns {object} 证书项 + */ + const findCertItem = (val: string) => { + return certList.value.find((item) => item.value === val) + } + + /** + * @description 渲染证书列表 + */ + const renderCertList = async () => { + try { + const { data } = await getCertList({ p: 1, limit: 100 }).fetch() + certList.value = + data?.map((item: CertItem) => ({ + cert: item.cert, + key: item.key, + label: item.domains + ' 【 ' + item.issuer + ' 】', + value: item.sha256, + })) || [] + certList.value.unshift({ + cert: '', + key: '', + label: '自定义证书', + value: '', + }) + } catch (error) { + certList.value = [] + handleError(error) + } + } + onMounted(async () => { + await renderCertList() + }) + + modalOptions.value.confirmText = computed(() => { + return param.value.cert_id === '' ? $t('t_3_1747110185110') : $t('t_2_1744861190040') + }) + + // 确认事件触发 + confirm(async (close) => { + try { + await example.value?.validate() + if (param.value.cert_id === '') { + const { data } = await uploadCert(param.value).fetch() + param.value.cert_id = data + } + updateNodeConfig(props.node.id, data.value) // 更新节点配置 + isRefreshNode.value = props.node.id // 刷新节点 + close() + } catch (error) { + handleError(error) + } + }) + + return () => ( +
+ +
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/upload/verify.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/upload/verify.tsx new file mode 100644 index 0000000..1656e13 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/node/upload/verify.tsx @@ -0,0 +1,12 @@ +import { FormRules } from 'naive-ui' +import { $t } from '@locales/index' +import { createNodeValidator } from '@workflowView/lib/NodeValidator' + +// 创建上传节点验证器 +const validator = createNodeValidator($t('t_12_1747817611391')) + +// 导出上传节点验证规则 +export default { + key: validator.required('key', $t('t_38_1745735769521'), ['input', 'blur', 'focus']), + cert: validator.required('cert', $t('t_40_1745735815317'), ['input', 'blur', 'focus']), +} as FormRules diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/useController.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/useController.tsx new file mode 100644 index 0000000..6cd6ea3 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/useController.tsx @@ -0,0 +1,42 @@ +import { onUnmounted } from 'vue' +import { useRoute, useRouter } from 'vue-router' + +import { useStore } from '@autoDeploy/children/workflowView/useStore' +import { $t } from '@locales/index' + +/** + * @description WorkflowView 的控制器,处理路由、页面生命周期事件和初始化逻辑。 + * @returns 返回包含初始化方法的对象。 + */ +export const useController = () => { + const { workflowType, detectionRefresh } = useStore() + const route = useRoute() + const router = useRouter() + + // 监听页面刷新 + const beforeUnload = (event: BeforeUnloadEvent) => { + event.preventDefault() + event.returnValue = $t('t_16_1747886308182') + return $t('t_16_1747886308182') + } + + // 初始化 + const init = () => { + // 监听页面刷新 + window.addEventListener('beforeunload', beforeUnload) + // 获取路由参数 + const type = route.query.type + if (type) workflowType.value = type as 'quick' | 'advanced' + // 如果检测刷新为false,则跳转至自动部署页面 + if (!detectionRefresh.value && route.path !== '/auto-deploy') router.push('/auto-deploy') + } + + // 卸载 + onUnmounted(() => { + window.removeEventListener('beforeunload', beforeUnload) + }) + + return { + init, + } +} diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/useStore.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/useStore.tsx new file mode 100644 index 0000000..410730e --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/children/workflowView/useStore.tsx @@ -0,0 +1,117 @@ +import { addWorkflow, updateWorkflow } from '@api/workflow' +import { useError } from '@baota/hooks/error' +import { $t } from '@locales/index' + +import type { FlowNode } from '@components/FlowChart/types' +import type { AddWorkflowParams, UpdateWorkflowParams } from '@/types/workflow' + +export const useWorkEditViewStore = defineStore('work-edit-view-store', () => { + const { handleError } = useError() + const isEdit = ref(false) // 是否编辑 + const detectionRefresh = ref(false) // 检测是否刷新, 用于检测是否刷新页面 + // 工作流数据 + const workflowData = ref({ + id: '', + name: '', + content: '', + active: '1', + exec_type: 'manual', + }) + + // 工作流类型 + const workflowType = ref<'quick' | 'advanced'>('quick') + + // 工作流默认节点数据 + const workDefalutNodeData = ref({ + id: '', + name: '', + childNode: { + id: 'start-1', + name: '开始', + type: 'start', + config: { + exec_type: 'manual', + }, + childNode: null, + }, + }) + + /** + * @description 重置工作流数据 + */ + const resetWorkflowData = () => { + workflowData.value = { + id: '', + name: '', + content: '', + active: '1', + exec_type: 'manual', + } + workDefalutNodeData.value = { + id: '', + name: '', + childNode: { + id: 'start-1', + name: '开始', + type: 'start', + config: { + exec_type: 'manual', + }, + childNode: null, + }, + } + workflowType.value = 'quick' + isEdit.value = false + } + /** + * 添加新工作流 + * @description 创建新的工作流配置 + * @param {AddWorkflowParams} params - 工作流参数 + * @returns {Promise} 是否添加成功 + */ + const addNewWorkflow = async (params: AddWorkflowParams) => { + try { + const { message, fetch } = addWorkflow(params) + message.value = true + await fetch() + } catch (error) { + handleError(error).default($t('t_10_1745457486451')) + } + } + + /** + * 设置工作流运行方式 + * @description 设置工作流运行方式 + * @param {number} id - 工作流ID + * @param {number} execType - 运行方式 + */ + const updateWorkflowData = async (param: UpdateWorkflowParams) => { + try { + const { message, fetch } = updateWorkflow(param) + message.value = true + await fetch() + } catch (error) { + handleError(error).default($t('t_11_1745457488256')) + } + } + return { + isEdit, + detectionRefresh, + workflowData, + workflowType, + workDefalutNodeData, + resetWorkflowData, + addNewWorkflow, + updateWorkflowData, + } +}) + +/** + * useStore + * @description 组合式API使用store + * @returns {object} store - 返回store对象 + */ +export const useStore = () => { + const store = useWorkEditViewStore() + return { ...store, ...storeToRefs(store) } +} diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/components/CAManageForm.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/components/CAManageForm.tsx new file mode 100644 index 0000000..e714ae5 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/components/CAManageForm.tsx @@ -0,0 +1,13 @@ +import { defineComponent } from 'vue' +import { useCAFormController } from '../useController' + +/** + * CA授权表单组件 + */ +export default defineComponent({ + name: 'CAManageForm', + setup() { + const { CAForm } = useCAFormController() + return () => + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/components/CAManageModal.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/components/CAManageModal.tsx new file mode 100644 index 0000000..bd665ec --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/components/CAManageModal.tsx @@ -0,0 +1,59 @@ +import { defineComponent } from 'vue' +import { NButton } from 'naive-ui' +import { PlusOutlined } from '@vicons/antd' +import { $t } from '@locales/index' +import { useCAManageController } from '../useController' +import EmptyState from '@components/TableEmptyState' +import BaseComponent from '@components/BaseLayout' + +/** + * CA授权管理模态框组件 + */ +export default defineComponent({ + name: 'CAManageModal', + props: { + type: { + type: String, + default: '', + }, + }, + setup(props) { + const { CATable, CATablePage, handleOpenAddForm, total } = useCAManageController(props) + + return () => ( + ( + + + {$t('t_4_1747903685371')} + + ), + content: () => ( +
+ , + }} + /> +
+ ), + footerRight: () => ( +
+ ( + + {$t('t_15_1745227839354')} {total.value} {$t('t_16_1745227838930')} + + ), + }} + /> +
+ ), + }} + /> + ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/components/HistoryLogsModal.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/components/HistoryLogsModal.tsx new file mode 100644 index 0000000..a4fee46 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/components/HistoryLogsModal.tsx @@ -0,0 +1,44 @@ +import { getWorkflowHistoryDetail } from '@/api/workflow' +import LogViewer from '@components/LogDisplay' + +export default defineComponent({ + name: 'HistoryLogsModal', + props: { + id: { + type: [String] as PropType, + required: true, + }, + }, + setup(props) { + const loading = ref(false) + const logContent = ref('') + + // 获取日志数据 + const fetchLogs = async () => { + loading.value = true + try { + const { data } = await getWorkflowHistoryDetail({ id: props.id }).fetch() + if (data) { + logContent.value = data + } else { + logContent.value = '没有日志数据' + } + return logContent.value + } catch (error) { + console.error('获取日志详情失败:', error) + return '获取日志失败: ' + (error instanceof Error ? error.message : String(error)) + } finally { + loading.value = false + } + } + + return () => ( + + ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/components/HistoryModal.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/components/HistoryModal.tsx new file mode 100644 index 0000000..43a97b5 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/components/HistoryModal.tsx @@ -0,0 +1,40 @@ +import { useHistoryController } from '@autoDeploy/useController' +import BaseComponent from '@components/BaseLayout' +import { $t } from '@locales/index' +import { NButton } from 'naive-ui' + +/** + * 工作流执行历史模态框组件 + */ +export default defineComponent({ + name: 'HistoryModal', + props: { + id: { + type: String, + required: true, + }, + }, + setup(props) { + const { WorkflowHistoryTable, WorkflowHistoryTablePage, fetch } = useHistoryController(props.id) + onMounted(() => { + fetch() + }) + return () => ( +
+ ( +
+ fetch()}> + {$t('t_9_1746667589516')} + +
+ ), + content: () => , + footerRight: () => , + }} + >
+
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/components/WorkflowModal.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/components/WorkflowModal.tsx new file mode 100644 index 0000000..30e295c --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/components/WorkflowModal.tsx @@ -0,0 +1,55 @@ +import { NCard, NSpace, NFormItem, NRadio } from 'naive-ui' +import { useStore } from '@autoDeploy/useStore' +import { useAddWorkflowController } from '@autoDeploy/useController' +import { $t } from '@locales/index' + +/** + * 添加工作流模态框组件 + */ +export default defineComponent({ + name: 'AddWorkflowModal', + setup() { + const { workflowTemplateOptions, workflowFormData } = useStore() + const { AddWorkflowForm } = useAddWorkflowController() + return () => ( + + { + return ( + + + {workflowTemplateOptions.value.map((item) => ( +
{ + workflowFormData.value.templateType = item.value + }} + > + + +
+
{item.label}
+
{item.description}
+
+ +
+
+
+ ))} +
+
+ ) + }, + }} + /> +
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/index.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/index.tsx new file mode 100644 index 0000000..eef4054 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/index.tsx @@ -0,0 +1,115 @@ +import { NInput, NButton, NSpace } from 'naive-ui' +import { $t } from '@/locales' +import { useThemeCssVar } from '@baota/naive-ui/theme' +import { RouterView } from '@baota/router' +import { Search } from '@vicons/carbon' +import { useController } from './useController' +import { useRouter } from 'vue-router' + +import BaseComponent from '@components/BaseLayout' +import EmptyState from '@components/TableEmptyState' + +/** + * 工作流页面组件 + */ +export default defineComponent({ + name: 'WorkflowManager', + setup() { + const { + WorkflowTable, + WorkflowTablePage, + isDetectionAddWorkflow, + isDetectionOpenCAManage, + isDetectionOpenAddCAForm, + handleAddWorkflow, + handleOpenCAManage, + hasChildRoutes, + param, + fetch, + data, + } = useController() + const router = useRouter() + // 获取主题变量 + const cssVar = useThemeCssVar(['contentPadding', 'borderColor', 'headerHeight', 'iconColorHover']) + + watch( + () => router.currentRoute.value.path, + (val) => { + if (val === '/auto-deploy') fetch() + }, + ) + + // 挂载时获取数据 + onMounted(() => { + isDetectionAddWorkflow() + isDetectionOpenCAManage() + isDetectionOpenAddCAForm() + fetch() + }) + + return () => ( +
+
+ {hasChildRoutes.value ? ( + + ) : ( + ( + + + {$t('t_0_1747047213730')} + + + {$t('t_0_1747903670020')} + + + ), + headerRight: () => ( + { + if (e.key === 'Enter') fetch() + }} + onClear={() => useTimeoutFn(fetch, 100)} + placeholder={$t('t_1_1745227838776')} + clearable + size="large" + class="min-w-[300px]" + v-slots={{ + suffix: () => ( +
+ +
+ ), + }} + >
+ ), + content: () => ( +
+ + {{ + empty: () => ( + + ), + }} + +
+ ), + footerRight: () => ( +
+ {$t('t_0_1746773350551', [data.value.total])}, + }} + /> +
+ ), + }} + >
+ )} +
+
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/useController.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/useController.tsx new file mode 100644 index 0000000..ba34057 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/useController.tsx @@ -0,0 +1,796 @@ +import { NSwitch, NTag, NButton, NSpace, NFlex, NText, NFormItem, NSelect } from 'naive-ui' +import { useRoute, useRouter } from 'vue-router' +import { useStore } from '@autoDeploy/useStore' +import { + useDialog, + useTable, + useTablePage, + useModal, + useFormHooks, + useForm, + useModalHooks, + useMessage, +} from '@baota/naive-ui/hooks' +import { useStore as useWorkflowViewStore } from '@autoDeploy/children/workflowView/useStore' +import { useError } from '@baota/hooks/error' +import AddWorkflowModal from './components/WorkflowModal' +import HistoryModal from './components/HistoryModal' +import HistoryLogsModal from './components/HistoryLogsModal' +import CAManageModal from './components/CAManageModal' +import { $t } from '@/locales' +import { router } from '@router/index' +import { CACertificateAuthorization } from '@config/data' +import SvgIcon from '@components/SvgIcon' +import { isEmail } from '@baota/utils/business' + +import type { WorkflowItem, WorkflowListParams, WorkflowHistoryParams, WorkflowHistoryItem } from '@/types/workflow' +import type { DataTableColumn } from 'naive-ui' +import type { TableColumn } from 'naive-ui/es/data-table/src/interface' +import type { EabItem, EabListParams } from '@/types/access' + +const { + refreshTable, + fetchWorkflowList, + fetchWorkflowHistory, + workflowFormData, + deleteExistingWorkflow, + executeExistingWorkflow, + setWorkflowActive, + setWorkflowExecType, + caFormData, + fetchEabList, + addNewEab, + deleteExistingEab, + resetCaForm, +} = useStore() +const { isEdit, workDefalutNodeData, resetWorkflowData, workflowData, detectionRefresh } = useWorkflowViewStore() +const { handleError } = useError() +const { useFormSlot } = useFormHooks() + +/** + * @description 状态列 + * @param {string} key - 状态列的key + * @returns {DataTableColumn[]} 返回状态列配置数组 + */ +const statusCol = >(key: string, title: string): TableColumn => ({ + title, + key, + width: 100, + render: (row: T) => { + const statusMap: Record = { + success: { type: 'success', text: $t('t_0_1747895713179') }, + fail: { type: 'error', text: $t('t_4_1746773348957') }, + running: { type: 'warning', text: $t('t_1_1747895712756') }, + } + const status = statusMap[row[key] as string] || { + type: 'default', + text: $t('t_1_1746773348701'), + } + if (row[key] === 'running') refreshTable.value = true + return ( + + {status.text} + + ) + }, +}) + +/** + * @description 工作流业务逻辑控制器 + * @function useController + */ +export const useController = () => { + // 获取当前路由 + const route = useRoute() + // 获取路由实例 + const router = useRouter() + + // 判断是否为子路由 + const hasChildRoutes = computed(() => route.path !== '/auto-deploy') + + /** + * @description 创建表格列配置 + * @returns {DataTableColumn[]} 返回表格列配置数组 + */ + const createColumns = (): DataTableColumn[] => [ + { + title: $t('t_0_1745215914686'), + key: 'name', + width: 200, + ellipsis: { + tooltip: true, + }, + }, + { + title: $t('t_1_1746590060448'), + key: 'type', + width: 100, + render: (row: WorkflowItem) => ( + + { + handleSetWorkflowExecType(row) + }} + checkedValue={'auto'} + uncheckedValue={'manual'} + /> + {row.exec_type === 'auto' ? $t('t_2_1745215915397') : $t('t_3_1745215914237')} + + ), + }, + { + title: $t('t_7_1745215914189'), + key: 'created_at', + width: 180, + render: (row: WorkflowItem) => row.create_time || '-', + }, + statusCol('last_run_status', $t('t_0_1746677882486')), + { + title: $t('t_8_1745215914610'), + key: 'actions', + fixed: 'right', + align: 'right', + width: 220, + render: (row: WorkflowItem) => ( + + handleViewHistory(row)}> + {$t('t_9_1745215914666')} + + handleExecuteWorkflow(row)}> + {$t('t_10_1745215914342')} + + handleEditWorkflow(row)}> + {$t('t_11_1745215915429')} + + handleDeleteWorkflow(row)}> + {$t('t_12_1745215914312')} + + + ), + }, + ] + + // 表格实例 + const { + component: WorkflowTable, + loading, + param, + data, + total, + fetch, + } = useTable({ + config: createColumns(), + request: fetchWorkflowList, + defaultValue: { + p: 1, + limit: 10, + search: '', + }, + watchValue: ['p', 'limit'], + }) + + // 分页实例 + const { component: WorkflowTablePage } = useTablePage({ + param, + total, + alias: { + page: 'p', + pageSize: 'limit', + }, + }) + + // 节流渲染 + const throttleFn = useThrottleFn(() => { + setTimeout(() => { + fetch() + refreshTable.value = false + }, 1000) + }, 100) + + watch( + () => refreshTable.value, + (val) => { + if (val) throttleFn() + }, + ) + + /** + * @description 打开添加工作流弹窗 + */ + const handleAddWorkflow = () => { + detectionRefresh.value = true + useModal({ + title: $t('t_5_1746667590676'), + component: AddWorkflowModal, + footer: true, + area: 500, + onUpdateShow(show) { + if (!show) fetch() + }, + }) + } + + /** + * @description 查看工作流执行历史 + * @param {number} workflowId - 工作流ID + */ + const handleViewHistory = async (workflow: WorkflowItem) => { + useModal({ + title: workflow ? `【${workflow.name}】 - ${$t('t_9_1745215914666')}` : $t('t_9_1745215914666'), + component: HistoryModal, + area: 800, + componentProps: { id: workflow.id.toString() }, + }) + } + + /** + * @description 执行工作流 + * @param {WorkflowItem} workflow - 工作流对象 + */ + const handleExecuteWorkflow = async ({ name, id }: WorkflowItem) => { + useDialog({ + title: $t('t_13_1745215915455'), + content: $t('t_2_1745227839794', { name }), + onPositiveClick: async () => { + await executeExistingWorkflow(id) + await fetch() + }, + }) + } + + /** + * @description 设置工作流运行方式 + * @param {WorkflowItem} workflow - 工作流对象 + */ + const handleSetWorkflowExecType = ({ id, exec_type }: WorkflowItem) => { + useDialog({ + title: exec_type === 'manual' ? $t('t_2_1745457488661') : $t('t_3_1745457486983'), + content: exec_type === 'manual' ? $t('t_4_1745457497303') : $t('t_5_1745457494695'), + onPositiveClick: () => setWorkflowExecType({ id, exec_type }), + onNegativeClick: fetch, + onClose: fetch, + }) + } + + /** + * @description 切换工作流状态 + * @param active - 工作流状态 + */ + const handleChangeActive = ({ id, active }: WorkflowItem) => { + useDialog({ + title: !active ? $t('t_6_1745457487560') : $t('t_7_1745457487185'), + content: !active ? $t('t_8_1745457496621') : $t('t_9_1745457500045'), + onPositiveClick: () => setWorkflowActive({ id, active }), + onNegativeClick: fetch, + onClose: fetch, + }) + } + + /** + * @description 编辑工作流 + * @param {WorkflowItem} workflow - 工作流对象 + * @todo 实现工作流编辑功能 + */ + const handleEditWorkflow = (workflow: WorkflowItem) => { + const content = JSON.parse(workflow.content) + isEdit.value = true + workflowData.value = { + id: workflow.id, + name: workflow.name, + content: content, + exec_type: workflow.exec_type, + active: workflow.active, + } + workDefalutNodeData.value = { + id: workflow.id, + name: workflow.name, + childNode: content, + } + detectionRefresh.value = true + router.push(`/auto-deploy/workflow-view?isEdit=true`) + } + + /** + * @description 删除工作流 + * @param {WorkflowItem} workflow - 工作流对象 + */ + const handleDeleteWorkflow = (workflow: WorkflowItem) => { + useDialog({ + title: $t('t_16_1745215915209'), + content: $t('t_3_1745227841567', { name: workflow.name }), + onPositiveClick: async () => { + await deleteExistingWorkflow(workflow.id) + await fetch() + }, + }) + } + + /** + * @description 检测是否需要添加工作流 + */ + const isDetectionAddWorkflow = () => { + const { type } = route.query + if (type?.includes('create')) { + handleAddWorkflow() + router.push({ query: {} }) + } + } + + /** + * @description 检测是否需要打开CA授权管理弹窗 + */ + const isDetectionOpenCAManage = () => { + const { type } = route.query + if (type?.includes('caManage')) { + handleOpenCAManage() + router.push({ query: {} }) + } + } + + /** + * @description 检测是否需要打开添加CA授权弹窗 + */ + const isDetectionOpenAddCAForm = () => { + const { type } = route.query + if (type?.includes('addCAForm')) { + handleOpenCAManage({ type: 'addCAForm' }) + router.push({ query: {} }) + } + } + + /** + * @description 打开CA授权管理弹窗 + */ + const handleOpenCAManage = ({ type }: { type: string } = { type: '' }) => { + useModal({ + title: $t('t_0_1747903670020'), + component: CAManageModal, + componentProps: { type }, + area: 780, + }) + } + + return { + WorkflowTable, + WorkflowTablePage, + isDetectionAddWorkflow, // 检测是否需要添加工作流 + isDetectionOpenCAManage, // 检测是否需要打开CA授权管理弹窗 + isDetectionOpenAddCAForm, // 检测是否需要打开添加CA授权弹窗 + handleViewHistory, // 查看工作流执行历史 + handleAddWorkflow, // 打开添加工作流弹窗 + handleChangeActive, // 切换工作流状态 + handleSetWorkflowExecType, // 设置工作流运行方式 + handleExecuteWorkflow, // 执行工作流 + handleEditWorkflow, // 编辑工作流 + handleDeleteWorkflow, // 删除工作流 + handleOpenCAManage, // 打开CA授权管理弹窗 + hasChildRoutes, + fetch, + data, + loading, + param, + } +} + +/** + * @description 添加工作流业务逻辑控制器 + * @returns {Object} 返回添加工作流业务逻辑控制器实例 + */ +export const useAddWorkflowController = () => { + const { confirm } = useModalHooks() + // 表单配置 + const config = computed(() => [useFormSlot('template')]) + + // 表单实例 + const { component: AddWorkflowForm, data } = useForm({ + config, + rules: {}, + defaultValue: workflowFormData, + }) + + // 确认添加工作流 + confirm(async (close) => { + try { + close() + resetWorkflowData() + router.push(`/auto-deploy/workflow-view?type=${data.value.templateType}`) + } catch (error) { + handleError(error) + } + }) + return { AddWorkflowForm } +} + +/** + * @description 工作流历史记录业务逻辑控制器 + * @param {number} workflowId - 工作流ID + * @returns {Object} 返回工作流历史记录业务逻辑控制器实例 + */ +export const useHistoryController = (id: string) => { + /** + * @description 工作流历史详情 + * @param {number} workflowId - 工作流ID + */ + const handleViewHistoryDetail = async (workflowId: string) => { + useModal({ + title: $t('t_0_1746579648713'), + component: HistoryLogsModal, + area: 730, + componentProps: { id: workflowId }, + }) + } + + /** + * @description 创建历史记录表格列配置 + * @returns {DataTableColumn[]} 返回表格列配置数组 + */ + const createColumns = (): DataTableColumn[] => [ + { + title: $t('t_4_1745227838558'), + key: 'create_time', + width: 230, + render: (row: WorkflowHistoryItem) => { + // 处理数字类型的时间戳 + return row.create_time ? row.create_time : '-' + }, + }, + { + title: $t('t_5_1745227839906'), + key: 'end_time', + width: 230, + render: (row: WorkflowHistoryItem) => { + // 处理数字类型的时间戳 + return row.end_time ? row.end_time : '-' + }, + }, + { + title: $t('t_6_1745227838798'), + key: 'exec_type', + width: 110, + render: (row: WorkflowHistoryItem) => ( + + {row.exec_type === 'auto' ? $t('t_2_1745215915397') : $t('t_3_1745215914237')} + + ), + }, + statusCol('status', $t('t_7_1745227838093')), + { + title: $t('t_8_1745215914610'), + key: 'actions', + fixed: 'right', + align: 'right', + width: 80, + render: (row: WorkflowHistoryItem) => ( + + handleViewHistoryDetail(row.id.toString())} + > + {$t('t_12_1745227838814')} + + + ), + }, + ] + + // 表格实例 + const { + component: WorkflowHistoryTable, + loading, + param, + total, + fetch, + } = useTable({ + config: createColumns(), + request: fetchWorkflowHistory, + defaultValue: { + id, + p: 1, + limit: 10, + }, + watchValue: ['p', 'limit'], + }) + + const { component: WorkflowHistoryTablePage } = useTablePage({ + param, + total, + alias: { + page: 'p', + pageSize: 'limit', + }, + }) + + return { + WorkflowHistoryTable, + WorkflowHistoryTablePage, + loading, + fetch, + } +} + +/** + * @description CA授权管理业务逻辑控制器 + * @returns {Object} 返回CA授权管理控制器对象 + */ +export const useCAManageController = (props: { type: string }) => { + const { handleError } = useError() + // 表格配置 + const columns = [ + { + title: $t('t_2_1745289353944'), + key: 'name', + ellipsis: { + tooltip: true, + }, + }, + { + title: $t('t_1_1745735764953'), + key: 'mail', + ellipsis: { + tooltip: true, + }, + }, + { + title: $t('t_9_1747903669360'), + key: 'ca', + width: 120, + render: (row: EabItem) => ( + + + {CACertificateAuthorization[row.ca as keyof typeof CACertificateAuthorization].name} + + ), + }, + { + title: $t('t_7_1745215914189'), + key: 'create_time', + width: 180, + render: (row: EabItem) => (row.create_time ? row.create_time : '--'), + }, + { + title: $t('t_8_1745215914610'), + key: 'actions', + width: 80, + align: 'right' as const, + fixed: 'right' as const, + render: (row: EabItem) => ( + + confirmDelete(row.id.toString())}> + {$t('t_12_1745215914312')} + + + ), + }, + ] + + // 表格实例 + const { + component: CATable, + loading, + param, + total, + fetch, + } = useTable({ + config: columns, + request: fetchEabList, + defaultValue: { + p: 1, + limit: 10, + }, + watchValue: ['p', 'limit'], + }) + + // 分页实例 + const { component: CATablePage } = useTablePage({ + param, + total, + alias: { + page: 'p', + pageSize: 'limit', + }, + }) + + /** + * 确认删除CA授权 + * @param {string} id - CA授权ID + */ + const confirmDelete = (id: string) => { + useDialog({ + title: $t('t_2_1747903672640'), + content: $t('t_3_1747903672833'), + onPositiveClick: async () => { + try { + await deleteExistingEab(id) + await fetch() + } catch (error) { + handleError(error) + } + }, + }) + } + + /** + * 打开添加CA授权表单 + */ + const handleOpenAddForm = () => { + resetCaForm() + useModal({ + title: $t('t_4_1747903685371'), + area: 500, + component: () => import('./components/CAManageForm').then((m) => m.default), + footer: true, + onUpdateShow: (show) => { + if (!show) fetch() + }, + }) + } + + // 挂载时获取数据 + onMounted(() => { + fetch() + if (props.type === 'addCAForm') handleOpenAddForm() + }) + + return { + CATable, + CATablePage, + loading, + param, + total, + fetch, + handleOpenAddForm, + } +} + +/** + * @description CA授权表单控制器 + * @returns {Object} 返回CA授权表单控制器对象 + */ +export const useCAFormController = () => { + const { handleError } = useError() + const message = useMessage() + const { confirm } = useModalHooks() + const { useFormInput, useFormCustom } = useFormHooks() + + // 表单验证规则 + const formRules = { + name: { + required: true, + message: $t('t_25_1746773349596'), + trigger: ['blur', 'input'], + }, + mail: { + required: true, + message: $t('t_6_1747817644358'), + trigger: ['blur', 'input'], + validator: (rule: any, value: string) => { + if (!value) return true + if (!isEmail(value)) { + return new Error($t('t_7_1747817613773')) + } + return true + }, + }, + Kid: { + required: true, + message: $t('t_5_1747903671439'), + trigger: ['blur', 'input'], + }, + HmacEncoded: { + required: true, + message: $t('t_6_1747903672931'), + trigger: ['blur', 'input'], + }, + ca: { + required: true, + message: $t('t_7_1747903678624'), + trigger: 'change', + }, + } + + // 渲染标签函数 + const renderLabel = (option: { value: string; label: string }): VNode => { + return ( + + + {option.label} + + ) + } + + // 渲染单选标签函数 + const renderSingleSelectTag = ({ option }: Record): VNode => { + return ( + + {option.label ? ( + renderLabel(option) + ) : ( + {$t('t_7_1747903678624')} + )} + + ) + } + + // 获取CA提供商选项 + const caOptions = Object.values(CACertificateAuthorization).map((item) => ({ + label: item.name, + value: item.type, + })) + + // 表单配置 + const formConfig = [ + useFormInput($t('t_2_1745289353944'), 'name', { + placeholder: $t('t_8_1747903675532'), + }), + useFormInput($t('t_1_1745735764953'), 'mail', { + placeholder: $t('t_0_1747965909665'), + }), + useFormCustom(() => { + return ( + + { + return {$t('t_7_1747903678624')} + }, + }} + /> + + ) + }), + useFormInput($t('t_10_1747903662994'), 'Kid', { + placeholder: $t('t_11_1747903674802'), + }), + useFormInput($t('t_12_1747903662994'), 'HmacEncoded', { + type: 'textarea', + placeholder: $t('t_13_1747903673007'), + rows: 3, + }), + ] + + // 提交表单 + const submitForm = async (formData: any) => { + try { + await addNewEab(formData) + message.success($t('t_40_1745289355715')) + return true + } catch (error) { + handleError(error) + return false + } + } + + // 表单实例 + const { component: CAForm } = useForm({ + config: formConfig, + rules: formRules, + defaultValue: caFormData, + request: submitForm, + }) + + // 确认提交表单 + confirm(async (close) => { + try { + await submitForm(caFormData.value) + close() + } catch (error) { + handleError(error) + } + }) + + return { + CAForm, + } +} diff --git a/frontend/apps/allin-ssl/src/views/autoDeploy/useStore.tsx b/frontend/apps/allin-ssl/src/views/autoDeploy/useStore.tsx new file mode 100644 index 0000000..d8f361d --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/autoDeploy/useStore.tsx @@ -0,0 +1,237 @@ +import { + getWorkflowList, + deleteWorkflow, + getWorkflowHistory, + executeWorkflow, + updateWorkflowExecType, + enableWorkflow, +} from '@/api/workflow' +import { getEabList, addEab, deleteEab } from '@/api/access' +import { useError } from '@baota/hooks/error' +// import { useMessage } from '@baota/naive-ui/hooks' +import { $t } from '@locales/index' +import type { + WorkflowListParams, + WorkflowHistoryParams, + WorkflowHistoryItem, + WorkflowItem, + UpdateWorkflowExecTypeParams, + EnableWorkflowParams, +} from '@/types/workflow' +import type { EabItem, EabListParams, EabAddParams } from '@/types/access' +import type { TableResponse } from '@baota/naive-ui/types/table' + +const { handleError } = useError() + +/** + * 工作流状态管理 Store + * @description 用于管理工作流相关的状态和操作,包括工作流列表、历史记录、分页等 + */ +export const useWorkflowStore = defineStore('workflow-store', () => { + // 刷新表格 + const refreshTable = ref(false) + const isEditWorkFlow = ref(false) // 是否编辑工作流 + // 表单数据 + const workflowFormData = ref({ + name: '', + templateType: 'quick', + }) + + // 模板选项 + const workflowTemplateOptions = ref([ + { label: '快速部署模板', value: 'quick', description: '快速上线应用,简化流程' }, + { label: '高级自定义模板', value: 'advanced', description: '完全自定义的部署流程' }, + ]) + + // CA授权管理相关数据 + // CA授权表单数据 + const caFormData = ref({ + name: '', + Kid: '', + HmacEncoded: '', + ca: 'zerossl', + }) + + // -------------------- 工具方法 -------------------- + /** + * 获取工作流列表 + * @description 根据分页参数获取工作流列表数据 + * @returns {Promise} + */ + const fetchWorkflowList = async ({ p, limit, search }: WorkflowListParams) => { + try { + const { data, count } = await getWorkflowList({ p, limit, search }).fetch() + return { list: (data || []) as T[], total: count } + } catch (error) { + handleError(error) + return { list: [], total: 0 } + } + } + + /** + * 获取工作流历史记录 + * @description 根据工作流ID获取其历史执行记录 + * @param {number} workflowId - 工作流ID + * @returns {Promise} + */ + const fetchWorkflowHistory = async ({ id, p, limit }: WorkflowHistoryParams) => { + try { + const res = await getWorkflowHistory({ id, p, limit }).fetch() + return { list: (res.data || []) as T[], total: res.count } + } catch (error) { + handleError(error) + return { list: [], total: 0 } + } + } + + /** + * 设置工作流运行方式 + * @description 设置工作流运行方式 + * @param {number} id - 工作流ID + * @param {number} execType - 运行方式 + */ + const setWorkflowExecType = async ({ id, exec_type }: UpdateWorkflowExecTypeParams) => { + try { + const { message, fetch } = updateWorkflowExecType({ id, exec_type }) + message.value = true + await fetch() + } catch (error) { + handleError(error).default($t('t_11_1745457488256')) + } + } + + /** + * 启用或禁用工作流 + * @description 启用或禁用指定工作流 + * @param {number} id - 工作流ID + * @param {boolean} active - 是否启用 + */ + const setWorkflowActive = async ({ id, active }: EnableWorkflowParams) => { + try { + const { message, fetch } = enableWorkflow({ id, active }) + message.value = true + await fetch() + } catch (error) { + handleError(error).default($t('t_12_1745457489076')) + } + } + + /** + * 执行工作流 + * @description 触发指定工作流的执行 + * @param {number} id - 工作流ID + * @returns {Promise} 是否执行成功 + */ + const executeExistingWorkflow = async (id: string) => { + try { + const { message, fetch } = executeWorkflow({ id }) + message.value = true + await fetch() + } catch (error) { + handleError(error).default($t('t_13_1745457487555')) + } + } + + /** + * 删除工作流 + * @description 删除指定的工作流配置 + * @param {number} id - 工作流ID + * @returns {Promise} 是否删除成功 + */ + const deleteExistingWorkflow = async (id: string) => { + try { + const { message, fetch } = deleteWorkflow({ id: id.toString() }) + message.value = true + await fetch() + } catch (error) { + handleError(error).default($t('t_14_1745457488092')) + } + } + + /** + * 获取CA授权列表 + * @param {EabListParams} params - 请求参数 + * @returns {Promise>} 返回表格数据结构 + */ + const fetchEabList = async ({ p, limit }: EabListParams) => { + try { + const { data, count } = await getEabList({ p, limit }).fetch() + return { list: (data || []) as T[], total: count } + } catch (error) { + handleError(error) + return { list: [], total: 0 } + } + } + + /** + * 添加CA授权 + * @param {EabAddParams} params - CA授权数据 + */ + const addNewEab = async (formData: EabAddParams): Promise => { + try { + const { message, fetch } = addEab(formData) + message.value = true + await fetch() + resetCaForm() // 重置表单 + } catch (error) { + handleError(error) + } + } + + /** + * 删除CA授权 + * @param {string} id - CA授权ID + */ + const deleteExistingEab = async (id: string): Promise => { + try { + const { message, fetch } = deleteEab({ id }) + message.value = true + await fetch() + } catch (error) { + handleError(error).default($t('t_40_1745227838872')) + } + } + + /** + * 重置CA表单 + */ + const resetCaForm = () => { + caFormData.value = { + name: '', + Kid: '', + HmacEncoded: '', + ca: 'zerossl', + } + } + + return { + // 状态 + refreshTable, + isEditWorkFlow, + workflowFormData, + workflowTemplateOptions, + caFormData, + + // 方法 + fetchWorkflowList, + fetchWorkflowHistory, + deleteExistingWorkflow, + executeExistingWorkflow, + setWorkflowActive, + setWorkflowExecType, + fetchEabList, + addNewEab, + deleteExistingEab, + resetCaForm, + } +}) + +/** + * 组合式 API 使用 Store + * @description 提供对工作流 Store 的访问,并返回响应式引用 + * @returns {Object} 包含所有 store 状态和方法的对象 + */ +export const useStore = () => { + const store = useWorkflowStore() + return { ...store, ...storeToRefs(store) } +} diff --git a/frontend/apps/allin-ssl/src/views/certApply/certApply.data.ts b/frontend/apps/allin-ssl/src/views/certApply/certApply.data.ts new file mode 100644 index 0000000..9f323b9 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/certApply/certApply.data.ts @@ -0,0 +1,253 @@ +import type { ProductsType, FreeProductItem } from '@/types/cert' + +export const mainTabOptionsData = [ + { key: 'commercial', title: '商业证书', desc: '品牌SSL证书,安全保障,全球兼容' }, + { key: 'free', title: '免费证书', desc: '适用于个人博客、测试环境的免费SSL证书' }, +] + +export const typeOptionsData = { + dv: '域名型(DV)', + ov: '企业型(OV)', + ev: '增强型(EV)', +} + +export const sslTypeListData = [ + { type: 'dv', title: '个人(DV 证书)', explain: '个人博客、个人项目等
可选择DV SSL证书。' }, + { + type: 'ov', + title: '传统行业(OV 证书)', + explain: '企业官网、电商、教育、医疗、公共
部门等,可选择OV SSL证书。', + }, + { + type: 'ev', + title: '金融机构(EV 证书)', + explain: '银行、金融、保险、电子商务、中大型企
业、政府机关等,可选择EV SSL证书。', + }, +] + +export const sslTypeDescriptionsData = { + dv: { + title: '域名型SSL证书 (DV SSL)', + features: [ + '适用场景: 个人网站、博客、论坛等', + '验证方式: 仅验证域名所有权', + '签发时间: 最快5分钟', + '安全级别: 基础级', + ], + advantages: '优势: 价格低廉,签发速度快,适合个人使用', + disadvantages: '劣势: 仅显示锁形图标,不显示企业信息', + recommendation: '推荐指数: ★★★☆☆', + }, + ov: { + title: '企业型SSL证书 (OV SSL)', + features: [ + '适用场景: 企业官网、电商网站、教育医疗网站等', + '验证方式: 验证域名所有权和企业真实性', + '签发时间: 1-3个工作日', + '安全级别: 中级', + ], + advantages: '优势: 兼顾安全和价格,适合一般企业使用', + disadvantages: '劣势: 签发时间较DV长', + recommendation: '推荐指数: ★★★★☆', + }, + ev: { + title: '增强型SSL证书 (EV SSL)', + features: [ + '适用场景: 银行、金融机构、政府网站、大型企业', + '验证方式: 最严格的身份验证流程', + '签发时间: 5-7个工作日', + '安全级别: 最高级', + ], + advantages: '优势: 提供最高级别安全认证,浏览器地址栏显示企业名称', + disadvantages: '劣势: 价格较高,签发时间最长', + recommendation: '推荐指数: ★★★★★', + }, +} + +export const productsData: ProductsType = { + dv: [ + { + pid: 0, + brand: '宝塔证书', + type: '域名型(DV)', + add_price: 0, + other_price: 128.66, + title: '宝塔证书 单域名SSL证书', + code: 'comodo-positivessl', + num: 1, + price: 128.66, + discount: 1, + state: 1, + install_price: 200, + src_price: 128.66, + }, + { + pid: 0, + brand: '宝塔证书', + type: '域名型(DV)', + add_price: 0, + other_price: 1688, + title: '宝塔证书 通配符SSL证书', + code: 'comodo-positivessl-wildcard', + num: 1, + price: 1688, + discount: 1, + state: 1, + install_price: 200, + src_price: 1688, + }, + { + pid: 0, + brand: '宝塔证书', + type: '域名型(DV)', + add_price: 98, + other_price: 180, + title: '宝塔证书 IP-SSL证书', + code: 'comodo-positive-multi-domain', + num: 1, + price: 180, + discount: 1, + ipssl: 1, + state: 1, + install_price: 200, + src_price: 180, + }, + ], + ov: [ + { + pid: 8303, + brand: 'Sectigo', + type: '企业型(OV)', + add_price: 0, + other_price: 1880, + title: 'Sectigo OV SSL证书', + code: 'sectigo-ov', + num: 1, + price: 1388, + discount: 1, + state: 1, + install_price: 500, + src_price: 1388, + }, + { + pid: 8304, + brand: 'Sectigo', + type: '企业型(OV)', + add_price: 880, + other_price: 5640, + title: 'Sectigo OV多域名SSL证书', + code: 'sectigo-ov-multi-san', + num: 3, + price: 3888, + discount: 1, + state: 1, + install_price: 500, + src_price: 3888, + }, + { + pid: 8305, + brand: 'Sectigo', + type: '企业型(OV)', + add_price: 0, + other_price: 6980, + title: 'Sectigo OV通配符SSL证书', + code: 'sectigo-ov-wildcard', + num: 1, + price: 4888, + discount: 1, + state: 1, + install_price: 500, + src_price: 4888, + }, + { + pid: 8307, + brand: 'Sectigo', + type: '企业型(OV)', + add_price: 3680, + other_price: 2094, + title: 'Sectigo OV多域名通配符SSL证书', + code: 'comodo-multi-domain-wildcard-certificate', + num: 3, + price: 15888, + discount: 1, + state: 1, + install_price: 500, + src_price: 15888, + }, + ], + ev: [ + { + pid: 8300, + brand: 'Sectigo', + type: '企业增强型(EV)', + add_price: 0, + other_price: 3400, + title: 'Sectigo EV SSL证书', + code: 'comodo-ev-ssl-certificate', + num: 1, + price: 2788, + discount: 1, + state: 1, + install_price: 500, + src_price: 2788, + }, + { + pid: 8302, + brand: 'Sectigo', + type: '企业增强型(EV)', + add_price: 1488, + other_price: 10200, + title: 'Sectigo EV多域名SSL证书', + code: 'comodo-ev-multi-domin-ssl', + num: 3, + price: 8388, + discount: 1, + state: 1, + install_price: 500, + src_price: 8388, + }, + { + pid: 8520, + brand: '锐安信', + type: '企业增强型(EV)', + add_price: 0, + other_price: 3480, + title: '锐安信EV SSL证书', + code: 'ssltrus-ev-ssl', + num: 1, + price: 2688, + discount: 1, + state: 1, + install_price: 500, + src_price: 2688, + }, + { + pid: 8521, + brand: '锐安信', + type: '企业增强型(EV)', + add_price: 2380, + other_price: 10440, + title: '锐安信EV多域名SSL证书', + code: 'ssltrus-ev-multi', + num: 3, + price: 9096, + discount: 1, + state: 1, + install_price: 500, + src_price: 9096, + }, + ], +} + +export const freeProductsData: FreeProductItem[] = [ + { + pid: 9001, + brand: "Let's Encrypt", + type: '域名型(DV)', + title: "Let's Encrypt 单域名SSL证书", + code: 'letsencrypt-single', + num: 1, + valid_days: 90, + features: ['90天有效期', '自动续期', '单域名', '全球认可'], + }, +] diff --git a/frontend/apps/allin-ssl/src/views/certApply/components/FreeProductCard.tsx b/frontend/apps/allin-ssl/src/views/certApply/components/FreeProductCard.tsx new file mode 100644 index 0000000..a5a4236 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/certApply/components/FreeProductCard.tsx @@ -0,0 +1,140 @@ +import { NButton, NImage, NBadge } from 'naive-ui' +import { $t } from '@locales/index' +interface FreeProductCardProps { + product: { + pid: number + brand: string + type: string + title: string + code: string + num: number + valid_days: number + features: string[] + } + onApply: (id: number) => void +} + +/** + * 免费SSL证书产品卡片组件 + * @param product - 产品信息 + * @param onApply - 申请按钮点击处理函数 + */ +export default defineComponent({ + name: 'FreeProductCard', + props: { + product: { + type: Object as PropType, + required: true, + }, + onApply: { + type: Function as PropType, + required: true, + }, + }, + setup(props) { + // 判断是否为通配符证书 + const isWildcard = computed(() => { + return props.product.title.toLowerCase().includes($t('t_10_1746667589575')) + }) + + // 判断是否为多域名证书 + const isMultiDomain = computed(() => { + return props.product.title.toLowerCase().includes($t('t_11_1746667589598')) + }) + + // 处理申请按钮点击 + const handleApply = () => { + props.onApply(props.product.pid) + } + + // 获取品牌图标 + const getBrandIcon = (brand: string) => { + const brandLower = brand.toLowerCase() + const brandIconMap: Record = { + sectigo: '/static/icons/sectigo-ico.png', + positive: '/static/icons/positive-ico.png', + ssltrus: '/static/icons/ssltrus-ico.png', + "let's encrypt": '/static/icons/letsencrypt-icon.svg', + } + return Object.keys(brandIconMap).find((key) => brandLower.includes(key)) + ? brandIconMap[Object.keys(brandIconMap).find((key) => brandLower.includes(key)) as string] + : undefined + } + + return () => ( +
+ {props.product.brand === "Let's Encrypt" && ( +
+ +
+ )} + +
+
+ +
+
+

{props.product.title}

+

+ {props.product.brand + $t('t_13_1746667599218')} +

+
+
+ +
+
+
+ {$t('t_14_1746667590827')} + {props.product.num + $t('t_15_1746667588493')} +
+
+ {$t('t_16_1746667591069')} + {$t('t_17_1746667588785')} +
+
+ {$t('t_19_1746667589295')} + {props.product.valid_days + $t('t_20_1746667588453')} +
+
+ {$t('t_21_1746667590834')} + {$t('t_17_1746667588785')} +
+
+ {$t('t_22_1746667591024')} + + {isWildcard.value + ? isMultiDomain.value + ? $t('t_23_1746667591989') + : $t('t_24_1746667583520') + : isMultiDomain.value + ? $t('t_25_1746667590147') + : $t('t_26_1746667594662')} + +
+
+ +
+
+
+ {$t('t_27_1746667589350')} +
+
+ + {$t('t_28_1746667590336')} + +
+
+
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/certApply/components/FreeProductModal.tsx b/frontend/apps/allin-ssl/src/views/certApply/components/FreeProductModal.tsx new file mode 100644 index 0000000..0331b7d --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/certApply/components/FreeProductModal.tsx @@ -0,0 +1,12 @@ +import { useCertificateFormController } from '@certApply/useController' + +/** + * 证书申请表单组件 + */ +export default defineComponent({ + name: 'CertificateForm', + setup() { + const { CertificateForm } = useCertificateFormController() + return () => + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/certApply/components/ProductCard.tsx b/frontend/apps/allin-ssl/src/views/certApply/components/ProductCard.tsx new file mode 100644 index 0000000..de49f2c --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/certApply/components/ProductCard.tsx @@ -0,0 +1,160 @@ +import { NButton, NCard, NTag, NImage, NBadge, NList, NListItem, NTooltip } from 'naive-ui' + +interface ProductCardProps { + product: { + pid: number + brand: string + type: string + title: string + add_price: number + other_price: number + num: number + price: number + discount: number + ipssl?: number + state: number + install_price: number + src_price: number + code: string + } + formatPrice: (price: number) => string + onBuy: (id: number) => void +} + +/** + * SSL证书产品卡片组件 + * @param product - 产品信息 + * @param formatPrice - 价格格式化函数 + * @param onBuy - 购买按钮点击处理函数 + */ +export default defineComponent({ + name: 'ProductCard', + props: { + product: { + type: Object as PropType, + required: true, + }, + formatPrice: { + type: Function as PropType, + required: true, + }, + onBuy: { + type: Function as PropType, + required: true, + }, + }, + setup(props) { + // 判断是否为通配符证书 + const isWildcard = computed(() => { + return props.product.title.toLowerCase().includes('通配符') + }) + + // 判断是否为多域名证书 + const isMultiDomain = computed(() => { + return props.product.title.toLowerCase().includes('多域名') + }) + + // 处理购买按钮点击 + const handleBuy = () => { + props.onBuy(props.product.pid) + } + + // 获取品牌图标 + const getBrandIcon = (brand: string) => { + const brandLower = brand.toLowerCase() + if (brandLower.includes('sectigo')) return '/static/icons/sectigo-ico.png' + if (brandLower.includes('positive')) return '/static/icons/positive-ico.png' + if (brandLower.includes('锐安信')) return '/static/icons/ssltrus-ico.png' + if (brandLower.includes("let's encrypt")) return '/static/icons/letsencrypt-icon.svg' + if (brandLower.includes('宝塔证书')) return '/static/icons/btssl.svg' + } + + return () => ( +
+ {props.product.discount < 1 && ( +
+ +
+ )} + +
+
+ +
+
+

{props.product.title}

+

+ {props.product.brand === '宝塔证书' + ? '宝塔证书是新国产证书品牌,支持 ECC、RSA 及我国商用密码 SM2 等标准算法,兼容国密浏览器' + : `${props.product.brand}是知名的证书颁发机构,提供高质量的SSL证书解决方案`} + 。 +

+
+
+ +
+
+
+ 支持域名数: + {props.product.num}个 +
+
+ 支持通配符: + {isWildcard.value ? '支持' : '不支持'} +
+
+ 绿色地址栏: + {props.product.type.includes('EV') ? '显示' : '不显示'} +
+
+ 支持小程序: + 支持 +
+
+ 适用网站: + + {props.product?.ipssl + ? '支持IP SSL证书' + : isWildcard.value + ? isMultiDomain.value + ? '*.bt.cn、*.btnode.cn' + : '*.bt.cn' + : isMultiDomain.value + ? 'bt.cn、btnode.cn' + : 'www.bt.cn、bt.cn'} + +
+
+ +
+
+
+ + {props.formatPrice(props.product.price)} + + 元/年 +
+
+ 原价 {props.formatPrice(props.product.other_price)}元/年 +
+
+ + 立即查看 + +
+
+
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/certApply/components/SslTypeInfo.tsx b/frontend/apps/allin-ssl/src/views/certApply/components/SslTypeInfo.tsx new file mode 100644 index 0000000..c43c93d --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/certApply/components/SslTypeInfo.tsx @@ -0,0 +1,106 @@ +import { NCard, NText, NList, NListItem, NIcon, NDivider, NButton, NSpace } from 'naive-ui' +import { SafetyCertificateOutlined, CheckCircleOutlined, InfoCircleOutlined } from '@vicons/antd' + +interface SSLTypeInfoProps { + certType: string + typeInfo: { + title: string + features: string[] + advantages: string + disadvantages: string + recommendation: string + } +} + +/** + * SSL证书类型说明组件 + */ +export default defineComponent({ + name: 'SSLTypeInfo', + props: { + certType: { + type: String, + required: true, + }, + typeInfo: { + type: Object as PropType, + required: true, + }, + }, + setup(props) { + // 获取图标类型 + const getIcon = (certType: string) => { + switch (certType) { + case 'dv': + return + case 'ov': + return + case 'ev': + return + default: + return + } + } + + // 获取图标颜色 + const getColor = (certType: string) => { + switch (certType) { + case 'dv': + return '#18a058' + case 'ov': + return '#2080f0' + case 'ev': + return '#8a2be2' + default: + return '#999' + } + } + + return () => ( + +
+ + {getIcon(props.certType)} + +

{props.typeInfo.title}

+
+ + + +
+ 证书特点 + + {props.typeInfo.features.map((feature, index) => ( + + + + + + {feature} + + + ))} + + +
+ {props.typeInfo.advantages} +
+ +
+ {props.typeInfo.disadvantages} +
+ +
+ {props.typeInfo.recommendation} +
+
+ +
+ window.open('https://www.bt.cn/new/ssl.html', '_blank')} block> + 了解更多详情 + +
+
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/certApply/index.tsx b/frontend/apps/allin-ssl/src/views/certApply/index.tsx new file mode 100644 index 0000000..dc65a95 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/certApply/index.tsx @@ -0,0 +1,98 @@ +import { useController } from './useController' +import { NTabs, NTabPane, NEmpty, NIcon } from 'naive-ui' +import ProductCard from './components/ProductCard' +import FreeProductCard from './components/FreeProductCard' +import { ShoppingCartOutlined, LockOutlined } from '@vicons/antd' + +interface SSLTypeItem { + type: 'dv' | 'ov' | 'ev' + title: string + explain: string +} + +export default defineComponent({ + setup() { + const { + activeMainTab, + activeTab, + mainTabOptions, + sslTypeList, + freeProducts, + filteredProducts, + handleBuyProduct, + formatPrice, + handleOpenApplyModal, + } = useController() + + return () => ( +
+
+ {/* 主标签页:商业证书/免费证书 */} + + {mainTabOptions.value.map((tab) => ( + + {{ + tab: () => ( +
+ {tab.key === 'commercial' ? : } + {tab.title} +
+ ), + default: () => ( +
+ {/* 商业证书内容 */} + {activeMainTab.value === 'commercial' && ( + + {(sslTypeList.value as SSLTypeItem[]).map((item: SSLTypeItem) => ( + +
+ {/* 证书产品列表 */} + {filteredProducts.value.length > 0 ? ( +
+ {filteredProducts.value.map((product) => ( + + ))} +
+ ) : ( + + )} +
+
+ ))} +
+ )} + {activeMainTab.value === 'free' && ( +
+ {freeProducts.value.map((product) => ( + + ))} +
+ )} +
+ ), + }} +
+ ))} +
+
+
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/certApply/useController.tsx b/frontend/apps/allin-ssl/src/views/certApply/useController.tsx new file mode 100644 index 0000000..6c79c14 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/certApply/useController.tsx @@ -0,0 +1,200 @@ +import { FormRules } from 'naive-ui' +import { useModal, useForm, useFormHooks, useLoadingMask, useModalHooks } from '@baota/naive-ui/hooks' +import { useError } from '@baota/hooks/error' +import { isDomain, isDomainGroup, isWildcardDomain } from '@baota/utils/business' +import { useStore as useWorkflowViewStore } from '@autoDeploy/children/workflowView/useStore' +import { $t } from '@locales/index' +import { useStore } from './useStore' +import CertificateForm from './components/FreeProductModal' +import DnsProviderSelect from '@components/DnsProviderSelect' + +// 错误处理 +const { handleError } = useError() + +/** + * useController + * @description 组合式API使用store + * @returns {object} store - 返回store对象 + */ +export const useController = () => { + const { + test, + handleTest, + activeMainTab, + activeTab, + mainTabOptions, + typeOptions, + sslTypeList, + sslTypeDescriptions, + freeProducts, + filteredProducts, + } = useStore() + + // const dialog = useDialog() + + // -------------------- 业务逻辑 -------------------- + // 处理商业产品购买按钮点击 + const handleBuyProduct = () => { + // 跳转到堡塔官网SSL证书购买页面 + window.open('https://www.bt.cn/new/ssl.html', '_blank') + } + + // 格式化价格显示 + const formatPrice = (price: number) => { + return Math.floor(price) + .toString() + .replace(/\B(?=(\d{3})+(?!\d))/g, ',') + } + + /** + * @description 打开申请弹窗 + */ + const handleOpenApplyModal = () => { + useModal({ + title: $t(`申请免费证书 - Let's Encrypt`), + area: '500px', + component: CertificateForm, + footer: true, + }) + } + + return { + test, + handleTest, + activeMainTab, + activeTab, + mainTabOptions, + typeOptions, + sslTypeList, + sslTypeDescriptions, + freeProducts, + filteredProducts, + handleBuyProduct, + handleOpenApplyModal, + formatPrice, + } +} + +/** + * @description 证书申请表单控制器 + * @returns {object} 返回controller对象 + */ +export const useCertificateFormController = () => { + // 表单hooks + const { useFormInput } = useFormHooks() + const { addNewWorkflow } = useWorkflowViewStore() + + // 加载遮罩 + const { open: openLoad, close: closeLoad } = useLoadingMask({ text: $t('t_6_1746667592831') }) + + // 消息和对话框 + const { confirm } = useModalHooks() + + // 表单数据 + const formData = ref({ + domains: '', + provider_id: '', + provider: '', + }) + + // 表单配置 + const config = computed(() => [ + useFormInput($t('t_17_1745227838561'), 'domains'), + { + type: 'custom' as const, + render: () => { + return ( + { + formData.value.provider_id = val.value + formData.value.provider = val.type + }} + /> + ) + }, + }, + ]) + + /** + * @description 表单验证规则 + */ + const rules = { + domains: { + required: true, + message: $t('t_7_1746667592468'), + trigger: 'input', + validator: (rule: any, value: any, callback: any) => { + if (isDomain(value) || isWildcardDomain(value) || isDomainGroup(value, ',')) { + callback() + } else { + callback(new Error($t('t_7_1746667592468'))) + } + }, + }, + provider_id: { + required: true, + message: $t('t_8_1746667591924'), + trigger: 'change', + type: 'string', + }, + } as FormRules + + /** + * @description 提交表单 + */ + const request = async () => { + try { + await addNewWorkflow({ + name: `申请免费证书-Let's Encrypt(${formData.value.domains})`, + exec_type: 'manual', + active: '1', + content: JSON.stringify({ + id: 'start-1', + name: '开始', + type: 'start', + config: { exec_type: 'manual' }, + childNode: { + id: 'apply-1', + name: '申请证书', + type: 'apply', + config: { + ...formData.value, + email: 'test@test.com', + end_day: 30, + }, + }, + }), + }) + } catch (error) { + handleError(error) + } + } + + // 使用表单hooks + const { component: CertificateForm, fetch } = useForm({ + config, + defaultValue: formData, + request, + rules, + }) + + // 关联确认按钮 + confirm(async (close) => { + try { + openLoad() + await fetch() + close() + } catch (error) { + return handleError(error) + } finally { + closeLoad() + } + }) + + return { + CertificateForm, + } +} diff --git a/frontend/apps/allin-ssl/src/views/certApply/useStore.tsx b/frontend/apps/allin-ssl/src/views/certApply/useStore.tsx new file mode 100644 index 0000000..9e3d189 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/certApply/useStore.tsx @@ -0,0 +1,113 @@ +import { defineStore, storeToRefs } from 'pinia' +import type { ProductsType, FreeProductItem } from '@/types/cert' +import { + mainTabOptionsData, + typeOptionsData, + sslTypeListData, + sslTypeDescriptionsData, + productsData, + freeProductsData, +} from './certApply.data' + +export const useCertApplyStore = defineStore('cert-apply-store', () => { + // -------------------- 状态定义 -------------------- + const test = ref('证书申请') + + // 当前激活的主标签 + const activeMainTab = ref<'commercial' | 'free'>('commercial') + + // 当前激活的子标签 + const activeTab = ref<'dv' | 'ov' | 'ev'>('dv') + + // 主标签选项 + const mainTabOptions = ref(mainTabOptionsData) + + // 证书类型选项 + const typeOptions = ref(typeOptionsData) + + // SSL证书类型列表 + const sslTypeList = ref(sslTypeListData) + + // SSL证书类型详细说明 + const sslTypeDescriptions = ref(sslTypeDescriptionsData) + + // 产品数据类型定义 + // type ProductItem = { + // pid: number + // brand: string + // type: string + // add_price: number + // other_price: number + // title: string + // code: string + // num: number + // price: number + // discount: number + // ipssl?: number + // state: number + // install_price: number + // src_price: number + // } + + // type ProductsType = { + // dv: ProductItem[] + // ov: ProductItem[] + // ev: ProductItem[] + // } + + // 商业证书产品数据 + const products = ref(productsData) + + // 免费证书数据 + // type FreeProductItem = { + // pid: number + // brand: string + // type: string + // title: string + // code: string + // num: number + // valid_days: number + // features: string[] + // } + + const freeProducts = ref(freeProductsData) + + // -------------------- 派生状态 -------------------- + // 根据当前活动标签筛选产品 + const filteredProducts = computed(() => { + if (activeMainTab.value === 'commercial') { + return products.value[activeTab.value] || [] + } else { + return [] // 免费证书不通过这个计算属性获取 + } + }) + + // -------------------- 工具方法 -------------------- + const handleTest = () => { + test.value = '点击了证书申请' + } + + return { + test, + handleTest, + activeMainTab, + activeTab, + mainTabOptions, + typeOptions, + sslTypeList, + sslTypeDescriptions, + products, + freeProducts, + filteredProducts, + } +}) + +/** + * useStore + * @description 组合式API使用store + * @returns {object} store - 返回store对象 + */ +export const useStore = () => { + const store = useCertApplyStore() + return { ...store, ...storeToRefs(store) } +} diff --git a/frontend/apps/allin-ssl/src/views/certManage/components/UploadCertModel.tsx b/frontend/apps/allin-ssl/src/views/certManage/components/UploadCertModel.tsx new file mode 100644 index 0000000..1b0ee38 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/certManage/components/UploadCertModel.tsx @@ -0,0 +1,11 @@ +import { useUploadCertController } from '@certManage/useController' + +/** + * 上传证书组件 + */ +export default defineComponent({ + name: 'UploadCert', + setup() { + + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/certManage/index.tsx b/frontend/apps/allin-ssl/src/views/certManage/index.tsx new file mode 100644 index 0000000..15a81f4 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/certManage/index.tsx @@ -0,0 +1,80 @@ +import { NInput, NButton } from 'naive-ui' +import { useTheme, useThemeCssVar } from '@baota/naive-ui/theme' +import { Search } from '@vicons/carbon' +import { $t } from '@locales/index' +import { useController } from './useController' + +import BaseComponent from '@components/BaseLayout' +import EmptyState from '@components/TableEmptyState' + +/** + * 证书管理组件 + */ +export default defineComponent({ + name: 'CertManage', + setup() { + const { CertTable, CertTablePage, fetch, data, param, openUploadModal, getRowClassName } = useController() + const cssVar = useThemeCssVar(['contentPadding', 'borderColor', 'headerHeight', 'iconColorHover']) + // 挂载时请求数据 + onMounted(() => fetch()) + const { theme, themeOverrides } = useTheme() + console.log(theme.value, themeOverrides.value) + return () => ( +
+
+ ( + + {$t('t_13_1745227838275')} + + ), + headerRight: () => ( + { + if (e.key === 'Enter') fetch() + }} + onClear={() => useThrottleFn(fetch, 100)} + placeholder={$t('t_14_1745227840904')} + clearable + size="large" + class="min-w-[300px]" + v-slots={{ + suffix: () => ( +
+ +
+ ), + }} + >
+ ), + content: () => ( +
+ + {{ + empty: () => , + }} + +
+ ), + footerRight: () => ( +
+ ( + + {$t('t_15_1745227839354')} {data.value.total} {$t('t_16_1745227838930')} + + ), + }} + /> +
+ ), + }} + >
+
+
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/certManage/useController.tsx b/frontend/apps/allin-ssl/src/views/certManage/useController.tsx new file mode 100644 index 0000000..fb1556e --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/certManage/useController.tsx @@ -0,0 +1,340 @@ +import { NButton, NSpace, NTag, useMessage, type DataTableColumns } from 'naive-ui' +import { + useModal, + useTable, + useTablePage, + useDialog, + useFormHooks, + useModalHooks, + useForm, + useLoadingMask, +} from '@baota/naive-ui/hooks' +import { useError } from '@baota/hooks/error' +import { $t } from '@locales/index' + +import { useStore } from './useStore' + +import type { CertItem, CertListParams } from '@/types/cert' + +const { handleError } = useError() +const { useFormTextarea } = useFormHooks() +const { fetchCertList, downloadExistingCert, deleteExistingCert, uploadNewCert, uploadForm, resetUploadForm } = + useStore() +const { confirm } = useModalHooks() +/** + * useController + * @description 证书管理业务逻辑控制器 + * @returns {object} 返回controller对象 + */ +export const useController = () => { + /** + * @description 创建表格列配置 + * @returns {DataTableColumns} 返回表格列配置数组 + */ + const createColumns = (): DataTableColumns => [ + { + title: $t('t_17_1745227838561'), + key: 'domains', + width: 200, + ellipsis: { + tooltip: true, + }, + }, + { + title: $t('t_18_1745227838154'), + key: 'issuer', + width: 200, + ellipsis: { + tooltip: true, + }, + }, + { + title: $t('t_21_1745227837972'), + key: 'source', + width: 100, + render: (row: CertItem) => (row.source !== 'upload' ? $t('t_22_1745227838154') : $t('t_23_1745227838699')), + }, + { + title: $t('t_19_1745227839107'), + key: 'end_day', + width: 100, + render: (row: CertItem) => { + const endDay = Number(row.end_day) + const config = [ + [endDay <= 0, 'error', $t('t_0_1746001199409')], + [endDay < 30, 'warning', $t('t_1_1745999036289', { days: row.end_day })], + [endDay > 30, 'success', $t('t_0_1745999035681', { days: row.end_day })], + ] as [boolean, 'success' | 'error' | 'warning' | 'default' | 'info' | 'primary', string][] + const [_, type, text] = config.find((item) => item[0]) ?? ['default', 'error', '获取失败'] + return ( + + {text} + + ) + }, + }, + { + title: $t('t_20_1745227838813'), + key: 'end_time', + width: 150, + }, + + { + title: $t('t_24_1745227839508'), + key: 'create_time', + width: 150, + }, + { + title: $t('t_8_1745215914610'), + key: 'actions', + fixed: 'right' as const, + align: 'right', + width: 200, + render: (row: CertItem) => ( + + openViewModal(row)}> + 查看 + + downloadExistingCert(row.id.toString())}> + {$t('t_25_1745227838080')} + + handleDeleteCert(row)}> + {$t('t_12_1745215914312')} + + + ), + }, + ] + + /** + * 根据证书的到期天数确定行的 CSS 类名。 + * @param row 当前行的数据对象,类型为 CertItem。 + * @returns 返回一个字符串,表示行的 CSS 类名。 + * - 'bg-red-500/10':如果证书已过期 (endDay <= 0)。 + * - 'bg-orange-500/10':如果证书将在30天内过期 (0 < endDay < 30)。 + * - 空字符串:其他情况。 + */ + const getRowClassName = (row: CertItem): string => { + const endDay = Number(row.end_day) + if (endDay <= 0) { + return 'bg-red-500/10' // Tailwind class for light red background + } + if (endDay < 30) { + return 'bg-orange-500/10' // Tailwind class for light orange background + } + return '' // 默认情况下没有额外的类 + } + + // 表格实例 + const { + component: CertTable, + loading, + param, + data, + total, + fetch, + } = useTable({ + config: createColumns(), + request: fetchCertList, + defaultValue: { + p: 1, + limit: 10, + search: '', + }, + watchValue: ['p', 'limit'], + }) + + // 分页实例 + const { component: CertTablePage } = useTablePage({ + param, + total, + alias: { + page: 'p', + pageSize: 'limit', + }, + }) + + /** + * @description 打开上传证书弹窗 + */ + const openUploadModal = () => { + useModal({ + title: $t('t_13_1745227838275'), + area: 600, + component: () => { + const { UploadCertForm } = useUploadCertController() + return + }, + footer: true, + onUpdateShow: (show) => { + if (!show) fetch() + resetUploadForm() + }, + }) + } + + /** + * @description 删除证书 + * @param {CertItem} cert - 证书对象 + */ + const handleDeleteCert = async ({ id }: CertItem) => { + useDialog({ + title: $t('t_29_1745227838410'), + content: $t('t_30_1745227841739'), + onPositiveClick: async () => { + try { + await deleteExistingCert(id.toString()) + await fetch() + } catch (error) { + handleError(error) + } + }, + }) + } + + /** + * @description 打开查看证书弹窗 + * @param {CertItem} cert - 证书对象 + */ + const openViewModal = (cert: CertItem) => { + useModal({ + title: '查看证书信息', + area: 600, + component: () => { + const { ViewCertForm } = useViewCertController(cert) + return + }, + footer: false, + }) + } + + return { + loading, + fetch, + CertTable, + CertTablePage, + getRowClassName, + param, + data, + openUploadModal, + openViewModal, + } +} + +/** + * @description 上传证书控制器 + */ +export const useUploadCertController = () => { + const { open: openLoad, close: closeLoad } = useLoadingMask({ text: $t('t_0_1746667592819') }) + // 表单实例 + const { example, component, loading, fetch } = useForm({ + config: [ + useFormTextarea($t('t_34_1745227839375'), 'cert', { placeholder: $t('t_35_1745227839208'), rows: 6 }), + useFormTextarea($t('t_36_1745227838958'), 'key', { placeholder: $t('t_37_1745227839669'), rows: 6 }), + ], + request: uploadNewCert, + defaultValue: uploadForm, + rules: { + cert: [{ required: true, message: $t('t_35_1745227839208'), trigger: 'input' }], + key: [{ required: true, message: $t('t_37_1745227839669'), trigger: 'input' }], + }, + }) + + // 关联确认按钮 + confirm(async (close) => { + try { + openLoad() + await fetch() + close() + } catch (error) { + handleError(error) + } finally { + closeLoad() + } + }) + + return { + UploadCertForm: component, + example, + loading, + fetch, + } +} + +/** + * @description 查看证书控制器 + * @param {CertItem} cert - 证书对象 + */ +export const useViewCertController = (cert: CertItem) => { + /** + * @description 复制文本到剪贴板 + * @param {string} text - 要复制的文本 + */ + const copyToClipboard = async (text: string) => { + const message = useMessage() + try { + await navigator.clipboard.writeText(text) + message.success('复制成功') + } catch (error) { + // 降级方案:使用传统的复制方法 + try { + const textArea = document.createElement('textarea') + textArea.value = text + document.body.appendChild(textArea) + textArea.select() + document.execCommand('copy') + document.body.removeChild(textArea) + message.success('复制成功') + } catch (error) { + message.error('复制失败') + } + } + } + + // 合并证书内容(cert + issuer_cert) + const combinedCert = cert.cert + (cert.issuer_cert ? '\n' + cert.issuer_cert : '') + + // 表单实例 + const { component } = useForm({ + config: [ + useFormTextarea( + $t('t_34_1745227839375'), + 'cert', + { placeholder: '', rows: 8, readonly: true }, + {}, + { + suffix: [ + () => ( + copyToClipboard(combinedCert)}> + {$t('t_4_1747984130327')} + + ), + ], + }, + ), + useFormTextarea( + $t('t_36_1745227838958'), + 'key', + { placeholder: '', rows: 8, readonly: true }, + {}, + { + suffix: [ + () => ( + copyToClipboard(cert.key)}> + {$t('t_4_1747984130327')} + + ), + ], + }, + ), + ], + defaultValue: { + cert: combinedCert, + key: cert.key, + }, + }) + + return { + ViewCertForm: component, + } +} diff --git a/frontend/apps/allin-ssl/src/views/certManage/useStore.tsx b/frontend/apps/allin-ssl/src/views/certManage/useStore.tsx new file mode 100644 index 0000000..bd247d4 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/certManage/useStore.tsx @@ -0,0 +1,123 @@ +import { defineStore, storeToRefs } from 'pinia' +import { getCertList, uploadCert, deleteCert } from '@/api/cert' +import { useError } from '@baota/hooks/error' +import { $t } from '@locales/index' +import type { CertItem, UploadCertParams, CertListParams } from '@/types/cert' +import type { TableResponse } from '@baota/naive-ui/types/table' + +// 导入错误处理钩子 +const { handleError } = useError() + +/** + * 证书管理状态 Store + * @description 用于管理证书相关的状态和操作,包括证书列表、上传、下载等 + */ +export const useCertManageStore = defineStore('cert-manage-store', () => { + // -------------------- 状态定义 -------------------- + // 上传证书表单 + const uploadForm = ref({ + cert_id: '', + cert: '', + key: '', + }) + + // -------------------- 工具方法 -------------------- + /** + * 获取证书列表 + * @description 根据分页参数获取证书列表数据 + * @param {CertListParams} params - 查询参数 + * @returns {Promise>} 返回列表数据和总数 + */ + const fetchCertList = async (params: CertListParams): Promise> => { + try { + const { data, count } = await getCertList(params).fetch() + return { + list: (data || []) as T[], + total: count, + } + } catch (error) { + handleError(error) + return { list: [] as T[], total: 0 } + } + } + + /** + * 下载证书 + * @description 下载指定ID的证书文件 + * @param {number} id - 证书ID + * @returns {Promise} 是否下载成功 + */ + const downloadExistingCert = (id: string) => { + try { + const link = document.createElement('a') + link.href = '/v1/cert/download?id=' + id + link.target = '_blank' + link.click() + } catch (error) { + handleError(error).default($t('t_38_1745227838813')) + } + } + + /** + * 上传证书 + * @description 上传新证书 + * @param {UploadCertParams} params - 上传证书参数 + * @returns {Promise} 是否上传成功 + */ + const uploadNewCert = async (params: UploadCertParams) => { + try { + const { message, fetch } = uploadCert(params) + message.value = true + await fetch() + } catch (error) { + handleError(error) + } + } + + /** + * 删除证书 + * @description 删除指定ID的证书 + * @param {number} id - 证书ID + * @returns {Promise} 是否删除成功 + */ + const deleteExistingCert = async (id: string) => { + try { + const { message, fetch } = deleteCert({ id }) + message.value = true + await fetch() + } catch (error) { + handleError(error) + } + } + + /** + * @description 重置上传证书表单 + */ + const resetUploadForm = () => { + uploadForm.value = { + cert: '', + key: '', + } + } + + return { + // 状态 + uploadForm, + // 方法 + fetchCertList, + downloadExistingCert, + uploadNewCert, + deleteExistingCert, + resetUploadForm, + } +}) + +/** + * 组合式 API 使用 Store + * @description 提供对证书管理 Store 的访问,并返回响应式引用 + * @returns {Object} 包含所有 store 状态和方法的对象 + */ +export const useStore = () => { + const store = useCertManageStore() + return { ...store, ...storeToRefs(store) } +} diff --git a/frontend/apps/allin-ssl/src/views/home/index.module.css b/frontend/apps/allin-ssl/src/views/home/index.module.css new file mode 100644 index 0000000..167b4cc --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/home/index.module.css @@ -0,0 +1,119 @@ +/* 状态文本颜色定义 */ +.stateText { + &.success { color: var(--n-success-color); } + &.warning { color: var(--n-warning-color); } + &.error { color: var(--n-error-color); } + &.info { color: var(--n-info-color); } + &.default { color: var(--n-text-color3); } /* 使用 Naive UI 较浅的文本颜色 */ +} + +/* 卡片悬停效果 */ +.cardHover { + transition: all 0.3s ease; + &:hover { + transform: translateY(-2px); + box-shadow: var(--n-box-shadow2); /* 使用 Naive UI 阴影变量 */ + } +} + +/* 快捷入口卡片基础样式 */ +.quickEntryCard { + height: 100% ; + transition: all 0.3s ease; + border-radius: 0.6rem; /* 圆角 */ +} + +.quickEntryCard:hover { + transform: translateY(-4px); /* 悬停时上移效果更明显 */ +} + +/* 工作流快捷入口特定样式 */ +.workflow { + background: var(--color-workflow-bg); /* 使用 variable.css 中定义的变量 */ +} + +.workflow .iconWrapper { + background: var(--color-workflow-icon-wrapper-bg); /* 使用 variable.css 中定义的变量 */ + color: var(--n-success-color); /* 使用 Naive UI 主题色 */ +} + +.workflow .title { + color: var(--n-success-color); /* 使用 Naive UI 主题色 */ +} + +/* 证书快捷入口特定样式 */ +.cert { + background: var(--color-cert-bg); /* 使用 variable.css 中定义的变量 */ +} + +.cert .iconWrapper { + background: var(--color-cert-icon-wrapper-bg); /* 使用 variable.css 中定义的变量 */ + color: var(--n-warning-color); /* 使用 Naive UI 主题色 */ +} + +.cert .title { + color: var(--n-warning-color); /* 使用 Naive UI 主题色 */ +} + +/* 监控快捷入口特定样式 */ +.monitor { + background: var(--color-monitor-bg); /* 使用 variable.css 中定义的变量 */ +} + +.monitor .iconWrapper { + background: var(--color-monitor-icon-wrapper-bg); /* 使用 variable.css 中定义的变量 */ + color: var(--color-monitor-text); /* 使用 variable.css 中定义的变量 */ +} + +.monitor .title { + color: var(--color-monitor-text); /* 使用 variable.css 中定义的变量 */ +} + +/* 图标包装器通用样式 */ +.iconWrapper { + border-radius: 50%; /* 圆形 */ + padding: 1rem; /* 内边距 */ + display: flex; + align-items: center; + justify-content: center; +} + +/* 快捷入口卡片内标题样式 */ +.title { + font-size: 2rem; + font-weight: 500; /* 中等字重 */ + margin-bottom: 0.75rem; +} + +/* 表格内文本、卡片内描述文本等通用文本样式 */ +.tableText { + font-size: 1.4rem; /* 14px */ + color: var(--n-text-color2); /* Naive UI secondary text color */ + line-height: 1.6; /* Improved readability */ +} + + + +/* 概览卡片中的图标区域样式 */ +.workflowIcon, +.certIcon, +.monitorIcon { + /* 若有特定于这些图标容器的样式,可在此定义 */ + /* 例如:padding: 0.5rem; background-color: #f0f0f0; border-radius: 8px; */ +} + +/* Utility classes for background colors from index.tsx */ +.bgUtilSuccess { + background-color: var(--n-success-color); } + +.bgUtilError { + background-color: var(--n-error-color); +} + +.bgUtilWarning { + background-color: var(--n-warning-color); +} + +.bgUtilDecorative { + background-color: var(--n-primary-color); /* 修改:使用 Naive UI 标准变量 (假设 primaryColor 是期望的装饰色) */ +} diff --git a/frontend/apps/allin-ssl/src/views/home/index.tsx b/frontend/apps/allin-ssl/src/views/home/index.tsx new file mode 100644 index 0000000..03b296f --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/home/index.tsx @@ -0,0 +1,258 @@ +import { defineComponent } from 'vue'; // 修改:移除 computed +import { NCard, NSpin, NIcon, NEmpty, NDataTable, NButton } from 'naive-ui'; +import { CloudMonitoring, Flow, ArrowRight } from '@vicons/carbon'; +import { Certificate20Regular } from '@vicons/fluent'; +import { useThemeCssVar } from '@baota/naive-ui/theme'; + +// Absolute Internal Imports - Utilities +import { $t } from '@locales/index'; + +// Relative Internal Imports +import { useController } from './useController'; +import { useStore } from './useStore'; + +// Side-effect Imports +import styles from './index.module.css'; + +/** + * @component HomeView + * @description 首页视图组件。 + * 负责展示应用概览信息、工作流历史以及快捷入口。 + */ +export default defineComponent({ + name: 'HomeView', + setup() { + const { loading } = useStore() + const { overviewData, pushToWorkflow, pushToCert, pushToMonitor, pushToCertManage, createColumns } = useController() + const columns = createColumns() + + // 参考 layout/index.tsx 的用法,直接获取需要的 Naive UI 主题变量 + // useThemeCssVar 会将这些 camelCase 变量名转换为 kebab-case CSS 变量 (e.g., successColor -> --n-success-color) + // 并将它们应用到绑定 style 的元素上。 + const cssVars = useThemeCssVar(['successColor', 'errorColor', 'warningColor', 'primaryColor']); + + return () => ( +
+ +
+ {/* 概览模块 */} +
+ {/* 自动化工作流概览卡片 */} +
pushToWorkflow()} class="cursor-pointer relative"> +
+ +
+
+
{$t('t_2_1746773350970')}
+
+
+ + {overviewData.value.workflow.count} + +

{$t('t_3_1746773348798')}

+
+
+
+ + + {$t('t_0_1746782379424')}: {overviewData.value.workflow.active} + +
+
+ + + {$t('t_4_1746773348957')}: {overviewData.value.workflow.failure} + +
+
+
+
+
+ + + +
+
+
+
+ + {/* 证书管理概览卡片 */} +
pushToCertManage()} class="cursor-pointer relative"> +
+ +
+
+
{$t('t_2_1744258111238')}
+
+
+ + {overviewData.value.cert.count} + +

{$t('t_3_1746773348798')}

+
+
+
+ + + {$t('t_5_1746773349141')}: {overviewData.value.cert.will} + +
+
+ + + {$t('t_0_1746001199409')}: {overviewData.value.cert.end} + +
+
+
+
+
+ + + +
+
+
+
+ + {/* 实时监控概览卡片 */} +
pushToMonitor()} class="cursor-pointer relative"> +
+ +
+
+
{$t('t_6_1746773349980')}
+
+
+ + {overviewData.value.site_monitor.count} + +

{$t('t_3_1746773348798')}

+
+
+
+ + + {$t('t_7_1746773349302')}: {overviewData.value.site_monitor.exception} + +
+
+
+
+
+ + + +
+
+
+
+
+ + {/* 工作流执行列表 */} + +
+
{$t('t_8_1746773351524')}
+ pushToWorkflow()} class={styles.viewAllButton}> + {$t('t_9_1746773348221')} + + + + +
+ {overviewData.value.workflow_history.length > 0 ? ( + 'border-none'} + class="border-none" + style={{ + '--n-border-color': 'transparent', + '--n-border-radius': '0', + }} + /> + ) : ( + + )} +
+ + {/* 快捷入口区域 */} +
+ {/* 工作流构建入口 */} +
pushToWorkflow('create')} class="cursor-pointer"> + +
+
+ + + +
+
+
{$t('t_11_1746773349054')}
+
{$t('t_12_1746773355641')}
+
+
+
+
+ + {/* 申请证书入口 */} +
pushToCert()} class="cursor-pointer"> + +
+
+ + + +
+
+
{$t('t_13_1746773349526')}
+
{$t('t_14_1746773355081')}
+
+
+
+
+ + {/* 添加监控入口 */} +
pushToMonitor('create')} class="cursor-pointer"> + +
+
+ + + +
+
+
{$t('t_11_1745289354516')}
+
{$t('t_1_1747019624067')}
+
+
+
+
+
+
+
+
+ ) + } +}) diff --git a/frontend/apps/allin-ssl/src/views/home/useController.tsx b/frontend/apps/allin-ssl/src/views/home/useController.tsx new file mode 100644 index 0000000..067c0e7 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/home/useController.tsx @@ -0,0 +1,193 @@ +import { useRouter } from 'vue-router'; +import { onMounted } from 'vue'; // 新增:导入 onMounted +import { NTag } from 'naive-ui'; + +// Type Imports +import type { Ref } from 'vue'; // 新增:导入 Ref 类型 +import type { DataTableColumns } from 'naive-ui'; +import type { OverviewData, WorkflowHistoryItem } from '@/types/public'; // 新增:导入 OverviewData 类型 + +// Absolute Internal Imports - Utilities +import { $t } from '@locales/index'; + +// Relative Internal Imports +import { useStore } from './useStore'; + +// Side-effect Imports +import styles from './index.module.css'; + +/** + * @interface HomeControllerExposes + * @description 首页 Controller 暴露给视图的接口定义。 + * @property {Ref} overviewData - 概览数据。 + * @property {(type?: string) => void} pushToWorkflow - 跳转到工作流页面。 + * @property {(type?: string) => void} pushToCert - 跳转到证书申请页面。 + * @property {(type?: string) => void} pushToMonitor - 跳转到监控页面。 + * @property {() => void} pushToCertManage - 跳转到证书管理页面。 + * @property {(state: number) => 'success' | 'error' | 'warning' | 'default'} getWorkflowStateType - 获取工作流状态对应的标签类型。 + * @property {(state: number) => string} getWorkflowStateText - 获取工作流状态对应的文本。 + * @property {(time: string) => string} formatExecTime - 格式化执行时间。 + * @property {() => DataTableColumns} createColumns - 创建表格列配置。 + */ +interface HomeControllerExposes { + overviewData: Ref; + pushToWorkflow: (type?: string) => void; + pushToCert: (type?: string) => void; + pushToMonitor: (type?: string) => void; + pushToCertManage: () => void; + getWorkflowStateType: (state: number) => 'success' | 'error' | 'warning' | 'default'; + getWorkflowStateText: (state: number) => string; + formatExecTime: (time: string) => string; + createColumns: () => DataTableColumns; +} + +/** + * 首页控制器 (Composable Function) + * + * @description 处理首页视图的业务逻辑,包括状态转换、数据格式化、页面导航等功能。 + * @returns {HomeControllerExposes} 返回首页视图所需的状态和方法。 + */ +export function useController(): HomeControllerExposes { + // 路由实例 + const router = useRouter(); + // 从 Store 中获取状态和方法 + const { overviewData, fetchOverviewData } = useStore(); + + // -------------------- 业务逻辑处理 -------------------- + /** + * 获取工作流状态对应的标签类型。 + * @function getWorkflowStateType + * @param {number} state - 工作流状态值。 + * @returns {'success' | 'error' | 'warning' | 'default'} NTag 组件的 type 属性值。 + */ + function getWorkflowStateType(state: number): 'success' | 'error' | 'warning' | 'default' { + switch (state) { + case 1: + return 'success'; // 成功状态 + case 0: + return 'warning'; // 正在运行状态 (根据原代码逻辑,0是warning,-1是error) + case -1: + return 'error'; // 失败状态 + default: + return 'default'; // 未知状态 + } + } + + /** + * 获取工作流状态对应的文本说明。 + * @function getWorkflowStateText + * @param {number} state - 工作流状态值。 + * @returns {string} 状态的中文描述。 + */ + function getWorkflowStateText(state: number): string { + switch (state) { + case 1: + return $t('t_8_1745227838023'); + case 0: + return $t('t_0_1747795605426'); + case -1: + return $t('t_9_1745227838305'); + default: + return $t('t_11_1745227838422'); + } + } + + /** + * 格式化执行时间为本地化的日期时间字符串。 + * @function formatExecTime + * @param {string} time - ISO 格式的时间字符串。 + * @returns {string} 格式化后的本地时间字符串。 + */ + function formatExecTime(time: string): string { + return new Date(time).toLocaleString(); + } + + /** + * 创建工作流历史表格列配置。 + * @function createColumns + * @returns {DataTableColumns} 工作流历史表格列配置。 + */ + function createColumns(): DataTableColumns { + return [ + { + title: $t('t_2_1745289353944'), // 名称 + key: 'name', + }, + { + title: $t('t_0_1746590054456'), // 状态 + key: 'state', + render: (row: WorkflowHistoryItem) => { + const stateType = getWorkflowStateType(row.state); + const stateText = getWorkflowStateText(row.state); + return ( + + {stateText} + + ); + }, + }, + { + title: $t('t_1_1746590060448'), // 模式 + key: 'mode', + render: (row: WorkflowHistoryItem) => { + return {row.mode || $t('t_11_1745227838422')}; + }, + }, + { + title: $t('t_4_1745227838558'), // 执行时间 + key: 'exec_time', + render: (row: WorkflowHistoryItem) => {formatExecTime(row.exec_time)}, + }, + ]; + } + + /** + * 跳转至工作流构建页面。 + * @function pushToWorkflow + * @param {string} [type=''] - 类型参数,可选。 + */ + function pushToWorkflow(type: string = ''): void { + router.push(`/auto-deploy${type ? `?type=${type}` : ''}`); + } + + /** + * 跳转至申请证书页面。 + * @function pushToCert + * @param {string} [type=''] - 类型参数,可选。 + */ + function pushToCert(type: string = ''): void { + router.push(`/cert-apply${type ? `?type=${type}` : ''}`); + } + + /** + * 跳转至证书管理页面。 + * @function pushToCertManage + */ + function pushToCertManage(): void { + router.push(`/cert-manage`); + } + + /** + * 跳转至添加监控页面。 + * @function pushToMonitor + * @param {string} [type=''] - 类型参数,可选。 + */ + function pushToMonitor(type: string = ''): void { + router.push(`/monitor${type ? `?type=${type}` : ''}`); + } + + onMounted(fetchOverviewData); + + // 暴露状态和方法给视图使用 + return { + overviewData, + pushToWorkflow, + pushToCert, + pushToMonitor, + pushToCertManage, + getWorkflowStateType, + getWorkflowStateText, + formatExecTime, + createColumns, + }; +} diff --git a/frontend/apps/allin-ssl/src/views/home/useStore.tsx b/frontend/apps/allin-ssl/src/views/home/useStore.tsx new file mode 100644 index 0000000..5feb0ed --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/home/useStore.tsx @@ -0,0 +1,118 @@ +import { defineStore, storeToRefs } from 'pinia'; +import { ref } from 'vue'; + +// Type Imports +import type { OverviewData } from '@/types/public'; + +// Absolute Internal Imports - API +import { getOverviews } from '@/api/public'; + +// Absolute Internal Imports - Hooks +import { useError } from '@baota/hooks/error'; + +// Absolute Internal Imports - Utilities +import { $t } from '@locales/index'; + +/** + * Home Store 暴露的类型接口 + * @interface HomeStoreExposes + * @property {Ref} loading - 数据加载状态。 + * @property {Ref} overviewData - 首页概览数据。 + * @property {() => Promise} fetchOverviewData - 获取首页概览数据的方法。 + */ +export interface HomeStoreExposes { + loading: Ref; + overviewData: Ref; + fetchOverviewData: () => Promise; +} + +/** + * 首页数据存储 (Pinia Store) + * + * @description 使用Pinia管理首页相关的状态和操作,包括: + * - 概览数据的获取和存储 + * - 加载状态管理 + * @returns {HomeStoreExposes} 包含状态和方法的 Store 实例。 + */ +export const useHomeStore = defineStore('home-store', (): HomeStoreExposes => { + // -------------------- 状态定义 -------------------- + /** + * 数据加载状态 + * @type {Ref} + * @description 用于控制页面加载指示器的显示。 + */ + const loading = ref(false); + + /** + * 首页概览数据 + * @type {Ref} + * @description 包含工作流、证书和监控的统计信息以及工作流历史记录。 + */ + const overviewData = ref({ + workflow: { count: 0, active: 0, failure: 0 }, + cert: { count: 0, will: 0, end: 0 }, + site_monitor: { count: 0, exception: 0 }, + workflow_history: [], + }); + + // 错误处理 + const { handleError } = useError(); + + // -------------------- 请求方法 -------------------- + /** + * 获取首页概览数据 + * @async + * @function fetchOverviewData + * @returns {Promise} 返回Promise对象,在数据获取完成后解析。 + */ + const fetchOverviewData = async (): Promise => { + try { + loading.value = true + const { data, status } = await getOverviews().fetch() + if (status) { + const { workflow, cert, site_monitor, workflow_history } = data + overviewData.value = { + workflow: { + count: workflow?.count || 0, + active: workflow?.active || 0, + failure: workflow?.failure || 0, + }, + cert: { count: cert?.count || 0, will: cert?.will || 0, end: cert?.end || 0 }, + site_monitor: { count: site_monitor?.count || 0, exception: site_monitor?.exception || 0 }, + workflow_history: workflow_history || [], + } + } + } catch (error) { + console.error('获取首页概览数据失败', error) + handleError(error).default($t('t_3_1745833936770')) + } finally { + loading.value = false + } + } + + // 返回状态和方法 + return { + loading, + overviewData, + fetchOverviewData, + }; +}); + +/** + * 首页状态管理钩子 (Composable Function) + * + * @description 将 Store 包装为组合式 API 风格,便于在视图组件中使用。 + * 自动处理响应式引用,简化状态的访问和修改。 + * + * @returns {HomeStoreExposes & ReturnType>} 包含状态和方法的对象,所有状态都已转换为 Ref,支持解构使用。 + */ +export const useStore = () => { + const store = useHomeStore(); + // 结合 storeToRefs 以便从 store 中提取 ref 同时保持响应性 + // 注意:直接扩展 storeToRefs 的返回类型可能比较复杂, + // 实践中通常直接使用 store 和 storeToRefs 返回的对象。 + // 这里为了更精确的类型提示,可以考虑更复杂的类型体操或接受一定的类型宽松。 + // 一个简化的方式是让调用者自行处理类型,或者返回一个结构更清晰的对象。 + // 为简化,此处返回展开后的 store 和 refs。 + return { ...store, ...storeToRefs(store) }; +}; diff --git a/frontend/apps/allin-ssl/src/views/layout/index.module.css b/frontend/apps/allin-ssl/src/views/layout/index.module.css new file mode 100644 index 0000000..772622c --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/layout/index.module.css @@ -0,0 +1,120 @@ +/* 布局容器样式 */ +.layoutContainer { + @apply min-h-screen flex flex-col; + background-color: var(--n-color); /* Naive UI 主题背景色 */ +} + +/* 侧边栏样式 */ +.sider { + @apply h-screen shadow-lg z-10 transition-all duration-300 ease-in-out; + /* 移动端默认行为通过 NLayoutSider 的 collapsed 属性和 TSX 逻辑控制 */ +} + +/* Logo容器样式 */ +.logoContainer { + @apply flex items-center h-[var(--n-sider-login-height)] border-b relative; + @apply px-3 sm:px-4 md:px-6; /* 统一响应式内边距 */ + border-color: var(--n-border-color); +} + +/* Logo容器内文字样式 (用于控制展开时的文字) */ +.logoContainer span.logoText { /* 更具体的选择器 */ + @apply text-nowrap overflow-hidden overflow-ellipsis; + @apply w-full sm:w-auto; /* 在小屏幕上允许文字占据更多空间或换行,大屏幕自适应 */ + color: var(--n-text-color-base); + /* 响应式字体大小 */ + @apply text-base sm:text-lg md:text-xl; /* 例如: 1rem, 1.125rem, 1.25rem */ +} + +/* Logo容器文本整体 (当侧边栏展开时) */ +.logoContainerText { + @apply flex items-center w-full; /* 确保在各种情况下都能正确对齐 */ +} + +/* Logo容器激活(折叠时)样式 */ +.logoContainerActive { + @apply flex items-center justify-center px-0; /* 折叠时无内边距,确保图标居中 */ +} + +/* 折叠/展开图标容器 (原 .collapsedIcon) */ +.menuToggleButton { + @apply h-[3.6rem] absolute rounded-[.4rem] flex items-center justify-center cursor-pointer transition-all duration-300; + @apply right-2 sm:right-3 md:right-4 px-2; /* 统一响应式定位和内边距 */ + color: var(--n-text-color-2); /* Naive UI 次要文字颜色 */ +} +.menuToggleButton:hover { + background-color: var(--n-action-color); /* Naive UI 交互元素背景色 */ + color: var(--n-text-color-1); /* Naive UI 主要文字颜色 */ +} + +/* 新增:头部菜单切换按钮样式 */ +.headerMenuToggleButton { + @apply flex items-center justify-center cursor-pointer rounded-md p-2; /* p-2 提供 8px 内边距 */ + color: var(--n-text-color-2); /* 默认图标颜色 (由 NIcon 继承) */ + transition: background-color 0.3s ease, color 0.3s ease; +} + +.headerMenuToggleButton:hover { + background-color: var(--n-action-color); /* 悬浮背景色 */ + color: var(--n-text-color-1); /* 悬浮图标颜色 (由 NIcon 继承) */ +} + +/* 折叠图标激活(折叠时)样式 (原 .collapsedIconActive) - 如果还需要特殊处理折叠时的按钮样式 */ +/* .menuToggleButtonActive { ... } */ + + +/* 头部样式 */ +.header { + @apply h-[var(--n-header-height)] border-b shadow-sm z-10 transition-all duration-300 ease-in-out flex items-center justify-end; + background-color: var(--n-header-color); + border-color: var(--n-border-color); + @apply px-3 sm:px-4 md:px-6; /* 统一响应式内边距 */ +} + +/* 系统信息样式 */ +.systemInfo { + @apply flex items-center space-x-2 sm:space-x-3 md:space-x-4 text-[1.4rem]; + color: var(--n-text-color-secondary); /* Naive UI 次级文字颜色 */ +} + + +/* 内容区域样式 */ +.content { + @apply flex-1 transition-all duration-300 ease-in-out h-[calc(100vh-var(--n-main-diff-height))] overflow-y-auto p-[var(--n-content-padding)] sm:p-0 md:p-0; + background-color: var(--n-layout-content-background-color); + transition: padding 0s; +} + +/* 移动端视图 */ +.siderMobileOpen { + @apply fixed top-0 left-0 h-full shadow-xl; + z-index: 1050; /* 确保在顶层 */ + background-color: var(--n-sider-color, var(--n-body-color)); /* 匹配侧边栏背景色 */ + transform: translateX(0); + transition: transform 0.3s ease-in-out; + /* 宽度由 NLayoutSider 的 width prop 控制 */ +} + +.siderMobileClosed { + @apply fixed top-0 left-0 h-full; + transform: translateX(-100%); + transition: transform 0.3s ease-in-out; + /* 宽度由 NLayoutSider 的 collapsedWidth prop 控制,但由于移出屏幕,实际宽度不重要 */ +} + +/* Mobile Menu Backdrop */ +.mobileMenuBackdrop { + @apply fixed inset-0 bg-black bg-opacity-50; + z-index: 1040; /* 在侧边栏下方,内容区域上方 */ +} + + +/* 针对 1100px 以下屏幕的样式调整 */ +@media (max-width: 1100px) { + .header { + @apply px-2 sm:px-3; + } + .content { + @apply p-2 sm:p-3; + } +} diff --git a/frontend/apps/allin-ssl/src/views/layout/index.tsx b/frontend/apps/allin-ssl/src/views/layout/index.tsx new file mode 100644 index 0000000..31332ef --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/layout/index.tsx @@ -0,0 +1,208 @@ +// 外部库依赖 +import { Transition, type Component as ComponentType, h, defineComponent, ref, onMounted, computed, watch } from 'vue' // 添加 watch +import { NBadge, NIcon, NLayout, NLayoutContent, NLayoutHeader, NLayoutSider, NMenu, NTooltip } from 'naive-ui' +import { RouterView } from 'vue-router' +import { MenuFoldOutlined, MenuUnfoldOutlined } from '@vicons/antd' +import { useMediaQuery } from '@vueuse/core' // 引入 useMediaQuery + +// 内部模块导入 - Hooks/Composables +import { useThemeCssVar } from '@baota/naive-ui/theme' +import { useController } from './useController' +// 内部模块导入 - 工具函数 +import { $t } from '@locales/index' +// 内部模块导入 - 样式 +import styles from './index.module.css' + + +/** + * @description 基础布局组件,包含侧边栏导航、头部信息和内容区域。 + * @component LayoutView + */ +export default defineComponent({ + name: 'LayoutView', + setup() { + const { menuItems, menuActive, isCollapsed, toggleCollapse, handleExpand, handleCollapse, updateMenuActive } = + useController() + // 确保所有需要的颜色变量都已在 useThemeCssVar 中声明,或直接在 CSS Module 中使用 var(--n-...) + const cssVars = useThemeCssVar([ + 'bodyColor', // --n-color 通常是 bodyColor + 'headerColor', + 'borderColor', + 'textColorBase', + 'textColor1', + 'textColor2', + 'textColorSecondary', + 'actionColor', + 'layoutContentBackgroundColor', + 'siderLoginHeight', // 确保这个变量在 Naive UI 主题中存在或已自定义 + 'contentPadding' + ]) + + const siderWidth = ref(200) + const siderCollapsedWidth = ref(60) + + // 将断点从 768px 调整为 1100px + const isMobile = useMediaQuery('(max-width: 768px)') + const isNarrowScreen = useMediaQuery('(max-width: 1100px)') + + onMounted(() => { + // 初始化时根据屏幕宽度设置菜单状态 + if (isMobile.value || isNarrowScreen.value) { + isCollapsed.value = true + } + }) + + // 监听屏幕宽度变化,自动折叠/展开菜单 + watch(isNarrowScreen, (newValue) => { + if (newValue && !isMobile.value) { // 仅在非移动设备且宽度小于1100px时处理 + isCollapsed.value = true + } else if (!newValue && !isMobile.value) { // 宽度大于1100px且非移动设备时 + isCollapsed.value = false + } + }) + + // 控制 NLayoutSider 组件的 'collapsed' prop + // 在移动端,我们希望 NLayoutSider 始终保持其展开时的宽度, + // 通过 CSS transform 控制其显示/隐藏,以避免 Naive UI 自身的宽度过渡动画。 + const nLayoutSiderCollapsedProp = computed(() => { + if (isMobile.value) { + return false // 在移动端,阻止 NLayoutSider 因 collapsed 变化而产生的宽度动画 + } + return isCollapsed.value // 桌面端按正常逻辑处理 + }) + + // 控制 NLayoutSider 内部 NMenu 组件的 'collapsed' prop + // 这决定了菜单项是显示为图标还是图标加文字。 + // 在移动端,当侧边栏滑出隐藏时 (isCollapsed.value 为 true),菜单也应处于折叠状态。 + // 当侧边栏滑入显示时 (!isCollapsed.value 为 true),菜单应处于展开状态。 + // 桌面端则直接跟随 isCollapsed.value。 + // 因此,此计算属性直接返回 isCollapsed.value 即可。 + const nMenuCollapsedProp = computed(() => { + return isCollapsed.value + }) + + // 动态计算 NLayoutSider 的 class,用于移动端的滑入滑出动画 + const siderDynamicClass = computed(() => { + if (isMobile.value) { + // 当 !isCollapsed.value (菜单逻辑上应为打开状态) 时,应用滑入样式 + // 当 isCollapsed.value (菜单逻辑上应为关闭状态) 时,应用滑出样式 + return !isCollapsed.value ? styles.siderMobileOpen : styles.siderMobileClosed + } + return '' // 桌面端不需要此动态 class + }) + + const showBackdrop = computed(() => isMobile.value && !isCollapsed.value) + + // NMenu 的折叠状态 (此处的 menuCollapsedState 变量名可以替换为 nMenuCollapsedProp) + // const menuCollapsedState = computed(() => { ... }) // 旧的,将被 nMenuCollapsedProp 替代 + + return () => ( + + +
+ {/* Logo 显示逻辑 */} + {(isMobile.value ? false : isCollapsed.value) ? ( + // 折叠时的 Logo (仅桌面端) +
+ logo +
+ ) : ( + // 展开时的 Logo (桌面端展开时,或移动端侧边栏可见时) +
+ logo + {$t('t_1_1744164835667')} +
+ )} + {/* 桌面端展开状态下的内部折叠按钮 */} + {!isCollapsed.value && !isMobile.value && ( + + {{ + trigger: () => ( +
toggleCollapse()} + > + {/* 图标大小调整为 20 */} +
+ ), + default: () => {$t('t_4_1744098802046')}, + }} +
+ )} +
+ { + updateMenuActive(key as any) // 保留原有的菜单激活逻辑 + // 如果是移动端并且菜单当前是展开状态,则关闭菜单 + if (isMobile.value && !isCollapsed.value) { + isCollapsed.value = true // 直接设置 isCollapsed 为 true 来关闭菜单 + } + }} + options={menuItems.value} + class="border-none" + collapsed={nMenuCollapsedProp.value} // NMenu 的折叠状态 + collapsedWidth={siderCollapsedWidth.value} + collapsedIconSize={22} + /> +
+ + + + {/* 移动端或桌面端侧边栏折叠时,在头部左侧显示展开/收起按钮 */} + {(isMobile.value || (!isMobile.value && isCollapsed.value)) && ( +
+ + {{ + trigger: () => ( +
toggleCollapse()}> + + {isCollapsed.value ? : } + +
+ ), + default: () => 展开主菜单, + }} +
+
+ )} +
+ + v1.0.3 + +
+
+ + + {({ Component }: { Component: ComponentType }) => ( + + {Component && h(Component)} + + )} + + +
+ {/* 移动端菜单展开时的背景遮罩 */} + {showBackdrop.value && ( +
toggleCollapse()}>
+ )} +
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/layout/useController.tsx b/frontend/apps/allin-ssl/src/views/layout/useController.tsx new file mode 100644 index 0000000..029a502 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/layout/useController.tsx @@ -0,0 +1,193 @@ +// 外部库依赖 +import { ref, computed, watch, onMounted, h } from 'vue' // 从 vue 导入 +import { NIcon } from 'naive-ui' +import { RouterLink, useRoute, useRouter, type RouteRecordRaw } from 'vue-router' + +// 类型导入 - 从全局类型文件导入 +import type { MenuOption } from 'naive-ui/es/menu/src/interface' +import type { + RouteName, + IconMap, // 导入 IconMap + LayoutControllerExposes, // 导入 LayoutControllerExposes +} from '../../types/layout' // 调整路径 + +// 内部模块导入 - Hooks +import { useMessage, useDialog } from '@baota/naive-ui/hooks' +import { useError } from '@baota/hooks/error' +// 内部模块导入 - API +import { signOut } from '@api/public' +// 内部模块导入 - Store +import { useStore } from './useStore' +// 内部模块导入 - 配置 +import { routes } from '@router/index' // 假设 routes 是 RouteRecordRaw[] +// 内部模块导入 - 工具函数 +import { $t } from '@locales/index' + +// 图标导入 +import { SettingsOutline, LogOutOutline } from '@vicons/ionicons5' +import { CloudMonitoring, Home, Flow } from '@vicons/carbon' +import { Certificate20Regular, AddSquare24Regular } from '@vicons/fluent' +import { ApiOutlined } from '@vicons/antd' + + +/** + * @description 布局控制器 + * @returns 返回布局相关状态和方法 + */ +export const useController = (): LayoutControllerExposes => { // 使用导入的 LayoutControllerExposes + const store = useStore() + const router = useRouter() // 从 vue-router 导入 + const route = useRoute() + const message = useMessage() + const { handleError } = useError() + // 从 store 中解构需要的状态和方法 + const { isCollapsed, menuActive, updateMenuActive, toggleCollapse, handleCollapse, handleExpand, resetDataInfo } = store + + /** + * 当前路由是否为子路由 + */ + const isChildRoute = ref(false) + + /** + * 当前子路由配置 + */ + const childRouteConfig = ref>({}) // 替换 any + + /** + * ==================== 弹窗相关功能 ==================== + */ + // (此处无弹窗相关功能直接定义,而是通过 useDialog hook 使用) + + // ============================== + // 图标渲染方法 + // ============================== + + /** + * @description 渲染导航图标 + * @param name - 路由名称 + * @returns 对应的图标组件 + */ + const renderIcon = (name: RouteName) => { + const iconObj: IconMap = { // IconMap 类型来自导入 + certManage: Certificate20Regular, + autoDeploy: Flow, + home: Home, + certApply: AddSquare24Regular, + monitor: CloudMonitoring, + settings: SettingsOutline, + logout: LogOutOutline, + authApiManage: ApiOutlined, + } + return () => h(NIcon, null, () => h(iconObj[name] || 'div')) + } + + // ============================== + // 菜单相关方法 + // ============================== + const menuItems = computed(() => { // 添加显式返回类型 + const routeMenuItems: MenuOption[] = routes + .filter((r) => r.meta?.title) // 过滤掉没有 title 的路由,避免渲染空标签 + .map((r) => ({ + key: r.name as RouteName, + label: () => {r?.meta?.title as string}, + icon: renderIcon(r.name as RouteName), + })) + return [ + ...routeMenuItems, + { + key: 'logout', + label: () => {$t('t_15_1745457484292')}, + icon: renderIcon('logout'), + }, + ] + }) + + /** + * @description 检查当前路由是否为子路由 + * @returns {void} + */ + const checkIsChildRoute = (): void => { + const currentPath = route.path + isChildRoute.value = currentPath.includes('/children/') + + if (isChildRoute.value) { + const parentRoute = routes.find((r) => r.name === menuActive.value) + if (parentRoute && parentRoute.children) { + const currentChild = parentRoute.children.find((child: RouteRecordRaw) => route.path.includes(child.path)) + childRouteConfig.value = currentChild || {} + } else { + childRouteConfig.value = {} + } + } else { + childRouteConfig.value = {} + } + } + + watch( + () => route.name, + (newName) => { // route.name 可能为 null 或 undefined + if (newName && newName !== menuActive.value) { + updateMenuActive(newName as RouteName) + } + checkIsChildRoute() + }, + { immediate: true }, + ) + + /** + * ==================== 用户操作功能 ==================== + */ + + /** + * @description 退出登录 + * @returns {Promise} + */ + const handleLogout = async (): Promise => { + try { + await useDialog({ + title: $t('t_15_1745457484292'), + content: $t('t_16_1745457491607'), + onPositiveClick: async () => { + try { + message.success($t('t_17_1745457488251')) + await signOut().fetch() + setTimeout(() => { + resetDataInfo() + sessionStorage.clear() + router.push('/login') + }, 1000) + } catch (error) { + handleError(error) + } + }, + }) + } catch (error) { + // useDialog 拒绝时会抛出错误,这里可以捕获不处理,或者记录日志 + // handleError(error) // 如果 useDialog 的拒绝也需要统一处理 + } + } + + /** + * ==================== 初始化逻辑 ==================== + */ + + onMounted(async () => { + checkIsChildRoute() + }) + + return { + // 从 store 暴露 + isCollapsed, + menuActive, + updateMenuActive, + toggleCollapse, + handleCollapse, + handleExpand, + resetDataInfo, + // controller 自身逻辑 + handleLogout, + menuItems, + isChildRoute, + childRouteConfig, + } +} diff --git a/frontend/apps/allin-ssl/src/views/layout/useStore.tsx b/frontend/apps/allin-ssl/src/views/layout/useStore.tsx new file mode 100644 index 0000000..5a03e69 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/layout/useStore.tsx @@ -0,0 +1,182 @@ +// 外部库依赖 +import { defineStore, storeToRefs } from 'pinia' +import { ref, computed } from 'vue' +import { useLocalStorage, useSessionStorage } from '@vueuse/core' + +// 类型导入 - 从全局类型文件导入 +import type { + RouteName, + LayoutStoreInterface, // 替换 LayoutStoreExposes + PushSourceTypeItem, // 导入 PushSourceTypeItem +} from '@/types/layout' // 调整路径 +import type { DnsProviderOption, NotifyProviderOption } from '@/types/setting' + +// 内部模块导入 - Hooks +import { useError } from '@baota/hooks/error' +// 内部模块导入 - API +import { getReportList } from '@api/setting' +import { getAccessAllList } from '@api/index' +// 内部模块导入 - 工具函数 +import { $t } from '@locales/index' + +/** + * @description 布局相关的状态管理 + * @warn 包含部分硬编码的业务数据,需要从API获取 + */ +export const useLayoutStore = defineStore('layout-store', (): LayoutStoreInterface => { + // 使用导入的 LayoutStoreInterface + const { handleError } = useError() + + // ============================== + // 状态定义 + // ============================== + + /** + * @description UI 相关状态 + */ + const isCollapsed = useLocalStorage('layout-collapsed', false) + + /** + * @description 消息通知 + */ + const notifyProvider = ref([]) + + /** + * @description DNS提供商 + */ + const dnsProvider = ref([]) + + /** + * @description 导航状态 + */ + const menuActive = useSessionStorage('menu-active', 'home') + + /** + * @description 布局内边距 + */ + const layoutPadding = computed(() => { + return menuActive.value !== 'home' ? 'var(--n-content-padding)' : '0' + }) + + /** + * @description 语言 + */ + const locales = useLocalStorage('locales-active', 'zhCN') + + /** + * @description 推送消息提供商 (保持 PushSourceTypeItem 和 pushSourceType) + */ + const pushSourceType = ref>({ + mail: { name: $t('t_68_1745289354676') }, + dingtalk: { name: $t('t_32_1746773348993') }, + wecom: { name: $t('t_33_1746773350932') }, + feishu: { name: $t('t_34_1746773350153') }, + webhook: { name: 'WebHook' }, + }) + + // ============================== + // UI 交互方法 + // ============================== + + /** + * @description 切换侧边栏折叠状态 + */ + const toggleCollapse = (): void => { + isCollapsed.value = !isCollapsed.value + } + + const handleCollapse = (): void => { + isCollapsed.value = true + } + + const handleExpand = (): void => { + isCollapsed.value = false + } + + const updateMenuActive = (active: RouteName): void => { + if (active === 'logout') return + menuActive.value = active + } + + const resetDataInfo = (): void => { + menuActive.value = 'home' + sessionStorage.removeItem('menu-active') + } + + // ============================== + // API 请求方法 + // ============================== + + /** + * @description 获取消息通知提供商 + * @returns 消息通知提供商 + */ + const fetchNotifyProvider = async (): Promise => { + try { + notifyProvider.value = [] + const { data } = await getReportList({ p: 1, search: '', limit: 1000 }).fetch() + notifyProvider.value = + data?.map((item) => { + return { + label: item.name, + value: item.id.toString(), + type: item.type, + } + }) || [] // 添加空数组作为备选,以防 data 为 null/undefined + } catch (error) { + handleError(error) + } + } + + /** + * @description 获取DNS提供商 + * @param type - 类型 (简化了联合类型,实际使用时可根据需要定义更精确的类型别名) + * @returns DNS提供商 + */ + const fetchDnsProvider = async (type: string = ''): Promise => { + try { + dnsProvider.value = [] + const { data } = await getAccessAllList({ type }).fetch() + dnsProvider.value = + data?.map((item) => ({ + label: item.name, + value: item.id.toString(), + type: item.type, + })) || [] + } catch (error) { + dnsProvider.value = [] + handleError(error) + } + } + + /** + * @description 重置DNS提供商 + * + */ + const resetDnsProvider = (): void => { + dnsProvider.value = [] + } + + return { + isCollapsed, + notifyProvider, + dnsProvider, + menuActive, + layoutPadding, + locales, + pushSourceType, + toggleCollapse, + handleCollapse, + handleExpand, + updateMenuActive, + resetDataInfo, + fetchNotifyProvider, + fetchDnsProvider, + resetDnsProvider, + } +}) + +export const useStore = () => { + const store = useLayoutStore() + return { ...store, ...storeToRefs(store) } +} diff --git a/frontend/apps/allin-ssl/src/views/login/index.module.css b/frontend/apps/allin-ssl/src/views/login/index.module.css new file mode 100644 index 0000000..6f4150b --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/login/index.module.css @@ -0,0 +1,386 @@ +/* 颜色变量定义 */ + +/* 主容器样式 - 全屏显示并居中内容 */ +.container { + @apply w-screen h-screen flex items-center justify-center relative overflow-hidden; + background: no-repeat center center; + background-size: cover; + animation: fadeIn 1.2s cubic-bezier(0.4, 0, 0.2, 1); + will-change: opacity; +} + +/* 主容器背景遮罩 - 创建渐变暗色效果 */ +.container::before { + @apply content-[''] absolute inset-0; + animation: fadeIn 1.5s cubic-bezier(0.4, 0, 0.2, 1); + will-change: opacity; +} + +/* 登录盒子 - 包含左右两个部分 */ +.loginBox { + @apply w-[90vw] max-w-[100rem] min-h-[60rem] flex relative z-10 justify-center items-center; + animation: scaleIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + will-change: transform, opacity; +} + +.leftImageWrapper { + @apply p-[2rem] w-[33rem]; +} + +.leftImage:hover { + animation: floating 2s ease-in-out infinite; +} + +@keyframes floating { + 0% { + transform: translateY(0px); + } + 50% { + transform: translateY(-10px); + } + 100% { + transform: translateY(0px); + } +} + +.leftImage { + @apply w-full text-[3.2rem] font-bold mb-7 flex items-center transition-transform duration-300 ease-in-out; +} + +/* 左侧内容区域 - 包含标题和描述文本 */ +.leftSection { + @apply flex-1 flex flex-col justify-center; + @apply p-14; + animation: fadeInLeft 1s cubic-bezier(0.4, 0, 0.2, 1) 0.3s both; + will-change: transform, opacity; +} + +/* 左侧标题样式 - 渐变文本效果 */ +.leftTitle { + @apply text-[var(--n-text-color-2)] text-[3.2rem] font-bold mb-7 flex items-center; +} + +.logo { + @apply w-[5.6rem] h-[5.6rem] mr-6; + will-change: transform; +} + +/* 左侧描述文本样式 */ +.leftDesc { + @apply text-[clamp(1.6rem,2vw,1.8rem)] leading-[1.8] opacity-90 max-w-[60rem]; + animation: slideUp 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) 0.5s both; + will-change: transform, opacity; +} + +/* 右侧登录区域 - 白色背景卡片 */ +.rightSection { + @apply w-[40rem] min-h-[38rem] flex flex-col bg-[var(--n-action-color)] rounded-xl shadow-lg p-14 mr-[5rem]; + animation: fadeInRight 0.6s cubic-bezier(0.4, 0, 0.2, 1) 0.3s both; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + will-change: transform, opacity, box-shadow; + backdrop-filter: blur(20px); +} + +.rightSection:hover { + transform: translateY(-2px) scale(1.01); +} + +/* 登录标题样式 */ +.title { + @apply text-4xl font-bold text-[var(--n-text-color-2)] text-left mb-11; + animation: slideDown 0.3s ease-out 0.3s both; +} + +/* 表单容器 - 采用 flex 布局实现自适应高度 */ +.formContainer { + @apply flex-1 flex flex-col; + animation: fadeIn 0.4s cubic-bezier(0.4, 0, 0.2, 1) 0.5s both; +} + +/* 表单包装器 - 确保表单占满剩余空间 */ +.formWrapper { + @apply flex-1 flex flex-col; +} + +/* 表单内容区域 - 使用 flex 布局分配空间 */ +.formContent { + @apply flex-1 flex flex-col; +} + +/* 表单输入区域 - 输入框垂直排列 */ +.formInputs { + @apply flex flex-col; +} + +/* 表单输入框样式 */ +.formInputs :global(.n-input) { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + will-change: transform; +} + +.formInputs :global(.n-input:hover) { + transform: translateY(-1px) scale(1.01); +} + +.formInputs :global(.n-input:focus-within) { + transform: translateY(-2px) scale(1.02); +} + +/* 表单底部操作区域 */ +.formActions { + @apply flex flex-col; +} + +/* 记住密码和忘记密码区域 */ +.rememberSection { + @apply flex justify-between items-center; + @apply mb-6; + animation: fadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1) 0.5s both; +} + +/* 底部按钮区域 */ +.formButton { + @apply mt-6; + animation: slideUp 0.3s ease-out 1.2s both; +} + +/* 社交链接区域 */ +.socialLinks { + @apply mt-14 flex items-center; + animation: fadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1) 1s both; +} + +.socialLinks > :not(:first-child) { + @apply ml-6; +} + +.socialLinks > * { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.socialLinks > *:hover { + transform: scale(1.1); +} + +/* 错误信息显示 */ +.error { + @apply text-[var(--n-error-color)] text-[1.4rem] text-center mt-3; + animation: shake 0.3s cubic-bezier(0.36, 0, 0.66, -0.56); + transform-origin: center; + will-change: transform; +} + +/* 基础淡入动画 */ +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/* 缩放进入动画 */ +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.95) translateY(10px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +/* 向下滑入动画 */ +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-30px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +/* 向上滑入动画 */ +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(30px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +/* 左侧内容淡入动画 */ +@keyframes fadeInLeft { + from { + opacity: 0; + transform: translateX(-50px) scale(0.98); + } + to { + opacity: 1; + transform: translateX(0) scale(1); + } +} + +/* 右侧内容淡入动画 */ +@keyframes fadeInRight { + from { + opacity: 0; + transform: translateX(50px) scale(0.98); + } + to { + opacity: 1; + transform: translateX(0) scale(1); + } +} + +/* 错误信息抖动动画 */ +@keyframes shake { + 0%, + 100% { + transform: translateX(0); + } + 10%, + 30%, + 50%, + 70%, + 90% { + transform: translateX(-2px) rotate(-1deg); + } + 20%, + 40%, + 60%, + 80% { + transform: translateX(2px) rotate(1deg); + } +} + +/* 动画关键帧优化 */ +@keyframes rotate { + from { + transform: rotate(-180deg) scale(0.5); + opacity: 0; + } + to { + transform: rotate(0) scale(1); + opacity: 1; + } +} + +/* 响应式布局适配 */ +@media (max-width: 768px) { + .loginBox { + @apply flex-col p-4 w-[95vw] min-h-[auto]; + } + + .leftSection { + @apply p-4 text-center mb-4; + } + + .leftTitle { + @apply text-[2.4rem] mb-4; + } + + .leftImageWrapper { + @apply hidden; + } + + .leftDesc { + @apply mx-auto text-[1.4rem]; + } + + .rightSection { + @apply w-full mx-auto mr-0 p-8 min-h-[auto]; + } + + .title { + @apply text-[2.8rem] mb-8; + } + + .formContainer { + @apply gap-4; + } +} + +/* 更小屏幕适配 */ +@media (max-width: 480px) { + .container { + @apply p-4; + } + + .loginBox { + @apply w-full p-2 min-h-[auto]; + } + + .leftSection { + @apply p-2 mb-2; + } + + .leftTitle { + @apply text-[2rem] mb-2 flex-col; + } + + .logo { + @apply w-[4rem] h-[4rem] mr-0 mb-2; + } + + .rightSection { + @apply p-6; + } + + .title { + @apply text-[2.4rem] mb-6; + } + + .formInputs :global(.n-form-item) { + @apply mb-4; + } + + .rememberSection { + @apply flex-col items-start gap-2 mb-4; + } + + /* 验证码输入框移动端优化 */ + .formInputs :global(.n-input) { + @apply text-[1.6rem]; + } + + /* 验证码图片容器移动端优化 */ + .codeImageContainer { + @apply w-[8rem] h-[3.5rem] mr-[-1rem] text-[1.4rem]; + } +} + +/* 清理不需要的样式 */ +.todoList, +.todoItem, +.todoCheckbox, +.todoTitle, +.deleteButton { + display: none; +} + +/* 忘记密码链接样式 */ +.forgotPassword { + @apply text-[var(--n-primary-color)] no-underline text-[1.4rem] transition-opacity duration-300 hover:opacity-80; +} + +/* 图标样式 */ +.icon { + color: var(--n-primary-color-suppl); +} + +/* 验证码图片容器样式 */ +.codeImageContainer { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.codeImageContainer:hover { + @apply bg-slate-500; +} diff --git a/frontend/apps/allin-ssl/src/views/login/index.tsx b/frontend/apps/allin-ssl/src/views/login/index.tsx new file mode 100644 index 0000000..34a08be --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/login/index.tsx @@ -0,0 +1,152 @@ +// External Libraries +import { NInput, NButton, NCheckbox, NForm, NFormItem, NIcon, NImage } from 'naive-ui' +import { UserOutlined, LockOutlined, CodeOutlined } from '@vicons/antd' + +// Absolute Internal Imports +import { useTheme, useThemeCssVar } from '@baota/naive-ui/theme' +import { $t } from '@locales/index' + +// Relative Internal Imports +import { useController } from './useController' + +// Side-effect Imports +import styles from './index.module.css' + +export default defineComponent({ + name: 'LoginView', + setup() { + const { loading, error, rememberMe, handleSubmit, handleKeyup, loginData, handleGetCode, codeImg, mustCode } = + useController() + const { isDark } = useTheme() + const cssVar = useThemeCssVar(['textColor2', 'actionColor', 'errorColor', 'primaryColor', 'primaryColorSuppl']) + + return () => ( +
+
+
+
+

+ logo + {$t('t_2_1747047214975')} +

+
+ {$t('t_1_1744164835667')} +
+
+
+
+

{$t('t_2_1744164839713')}

+ +
+
+ + + {{ + prefix: () => , + }} + + + + + {{ + prefix: () => , + }} + + + {mustCode.value ? ( + + + {{ + prefix: () => , + suffix: () => ( + + + + ), + }} + + + ) : null} +
+ +
+
+ {$t('t_5_1744164840468')} + + {$t('t_6_1744164838900')} + +
+ {error.value &&
{error.value}
} + + {loading.value ? $t('t_7_1744164838625') : $t('t_8_1744164839833')} + +
+
+
+
+
+
+
+
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/login/useController.tsx b/frontend/apps/allin-ssl/src/views/login/useController.tsx new file mode 100644 index 0000000..c77b844 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/login/useController.tsx @@ -0,0 +1,172 @@ +// External Libraries +import md5 from 'crypto-js/md5' + +// Type Imports +import type { LoginParams } from '@/types/login' + +// Absolute Internal Imports +import { useError } from '@baota/hooks/error' +import { $t } from '@locales/index' + +// Relative Internal Imports +import { useStore } from './useStore' + +/** + * @file 登录控制器 + * @description 处理登录页面的业务逻辑,包括表单验证、密码加密、记住密码等功能 + */ + +// ==================== 工具函数 ==================== +/** + * @description md5 密码加密 + * @param password - 原始密码 + * @returns 加密后的密码 + */ +const encryptPassword = (password: string): string => { + return md5(`${password}_bt_all_in_ssl`).toString() +} + +/** + * @description 获取记住的登录数据 + * @returns 返回记住的登录数据,如果不存在则返回 null + */ +const getRememberData = (): LoginParams | null => { + const loginDataInfo = localStorage.getItem('loginData') + if (!loginDataInfo) return null + return JSON.parse(loginDataInfo) as LoginParams // 添加类型断言 +} + +/** + * @description 设置记住的登录数据 + * @param username - 用户名 + * @param password - 密码 (明文,存储前加密) + */ +const setRememberData = (username: string, password: string): void => { + localStorage.setItem('loginData', JSON.stringify({ username, password })) +} + +// ==================== Controller 类型定义 ==================== +/** + * Login Controller 返回的公开成员类型 + */ +interface LoginControllerExposes extends ReturnType { + // 继承自 useStore 的返回类型 + handleSubmit: (event: Event) => Promise + handleKeyup: (event: KeyboardEvent) => void + handleLogin: (params: LoginParams) => Promise // 覆盖 store 中的 handleLogin + getRememberData: () => LoginParams | null + setRememberData: (username: string, password: string) => void +} + +// ==================== 控制器逻辑 ==================== +/** + * @description 登录控制器钩子 + * @returns 返回登录相关的状态和方法 + */ +export const useController = (): LoginControllerExposes => { + const store = useStore() + const { handleError } = useError() + // 从 store 中解构需要的状态和方法 + const { error, loginData, handleLogin: storeHandleLogin, rememberMe, checkMustCode, mustCode, handleGetCode } = store + + /** + * @description 处理登录业务逻辑,包括表单验证和密码加密 + * @param params - 登录参数 (用户名、密码等) + */ + const handleLoginBusiness = async (params: LoginParams): Promise => { + // 表单验证 + if (!params.username.trim()) { + error.value = $t('t_3_1744164839524') // 请输入用户名 + return + } + if (!params.password.trim()) { + error.value = $t('t_4_1744164840458') // 请输入密码 + return + } + if (mustCode.value && !params.code?.trim()) { + error.value = $t('t_25_1745289355721') // 请输入验证码 + return + } + + try { + const encryptedPassword = encryptPassword(params.password) + await storeHandleLogin({ ...params, password: encryptedPassword }) // 调用 store 中的登录方法 + + // 处理记住密码逻辑 + if (rememberMe.value && !error.value) { + // 登录成功且勾选了记住密码 + setRememberData(params.username, params.password) // 存储原始密码以便回填 + } else if (error.value) { + // 登录失败 + loginData.value.password = '' // 清空密码框 + if (mustCode.value) handleGetCode() // 刷新验证码 + } else if (!error.value && !rememberMe.value) { + // 登录成功但未勾选记住密码 + localStorage.removeItem('loginData') // 清除已记住的密码 + // resetForm(); // 根据产品需求决定是否在登录成功后重置表单,通常不需要,因为会跳转 + } + } catch (err) { + handleError(err) // 处理未知错误 + if (mustCode.value) handleGetCode() // 发生错误时也刷新验证码 + } + } + + /** + * @description 处理表单提交事件 + * @param event - 表单提交事件对象 + */ + const handleSubmit = async (event: Event): Promise => { + event.preventDefault() + await handleLoginBusiness(loginData.value) + } + + /** + * @description 处理键盘回车事件,触发表单提交 + * @param event - 键盘事件对象 + */ + const handleKeyup = (event: KeyboardEvent): void => { + if (event.key === 'Enter') { + handleSubmit(event as unknown as Event) // 类型转换以匹配 handleSubmit + } + } + + // ==================== 生命周期钩子 ==================== + const scope = effectScope() + + scope.run(() => { + // 监听错误信息,5秒后自动清除 + watch(error, (newValue) => { + if (newValue) { + // 仅当有错误信息时启动计时器 + setTimeout(() => { + error.value = '' + }, 5000) + } + }) + + onScopeDispose(() => { + scope.stop() + }) + }) + + onMounted(() => { + checkMustCode() // 初始化时检测是否必须验证码 + if (rememberMe.value) { + const rememberedData = getRememberData() // 获取记住的登录数据 + if (rememberedData) { + loginData.value.username = rememberedData.username + loginData.value.password = rememberedData.password // 回填原始密码 + } + } + }) + + // ==================== 返回值 ==================== + return { + ...store, // 暴露 store 中的所有属性和方法 + handleSubmit, + handleKeyup, + handleLogin: handleLoginBusiness, // 控制器封装的登录逻辑 + getRememberData, + setRememberData, + } +} diff --git a/frontend/apps/allin-ssl/src/views/login/useStore.tsx b/frontend/apps/allin-ssl/src/views/login/useStore.tsx new file mode 100644 index 0000000..0e43719 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/login/useStore.tsx @@ -0,0 +1,174 @@ +/** + * @file 登录模块状态管理 + * @description 负责处理登录相关的状态管理,包括用户认证、token管理和登录状态维护 + */ +// External Libraries +import { useMessage } from '@baota/naive-ui/hooks' + +// Type Imports +import type { UserInfo, LoginParams } from '@/types/login' // 假设 LoginData 在 types/login 中定义或将在此处定义 + +// Absolute Internal Imports +import { getLoginCode, login } from '@api/public' +import { getCookie } from '@baota/utils/browser' +import { useError } from '@baota/hooks/error' +import { ShallowRef } from 'vue' + +/** 消息提示 */ +const { success } = useMessage() +const { handleError } = useError() + +// ==================== Store 类型定义 ==================== +/** + * Store 返回的公开成员类型 + */ +interface LoginStoreExposes { + loading: Ref + codeImg: Ref + error: ShallowRef + user: Ref + loginData: Ref // 使用 LoginData 接口 + rememberMe: Ref + forgotPasswordRef: Ref + mustCode: Ref + handleLogin: (params: { username: string; password: string; code?: string }) => Promise + handleLogout: () => void + handleGetCode: () => Promise + checkMustCode: () => void + resetForm: () => void + clearToken: () => void +} + +// ==================== Store 定义 ==================== +export const useLoginStore = defineStore('login-store', (): LoginStoreExposes => { + // -------------------- 状态定义 -------------------- + /** 认证相关状态 */ + const user = ref(null) + const codeImg = ref('') + const useToken = useLocalStorage('login-token', '') + const mustCode = ref(false) // 是否必须验证码 + + /** 表单相关状态 */ + const loginData = ref({ + username: '', + password: '', + code: '', + }) + + const rememberMeRef = useLocalStorage('remember-me', false) + const forgotPasswordRef = ref(null) + + // 初始化登录请求 + const { fetch, error, data, message, loading } = login() + // -------------------- 工具方法 -------------------- + + /** + * 重置表单状态 + */ + const resetForm = (): void => { + loginData.value.username = '' + loginData.value.password = '' + rememberMeRef.value = false + error.value = null + } + + /** + * 清除token + */ + const clearToken = (): void => { + useToken.value = null + } + + // -------------------- 核心业务逻辑 -------------------- + /** + * 登录处理 + * @param params - 包含用户名、密码和可选验证码的对象 + * @returns Promise,操作完成时 resolve + */ + const handleLogin = async (params: { username: string; password: string; code?: string }): Promise => { + try { + error.value = null // 清除之前的错误信息 + message.value = true // 触发消息提示 (假设这是控制 NMessage 的开关) + // 发送登录请求 + await fetch(params) + const { status } = data.value + // 处理登录响应 + if (status) { + success('登录成功,正在跳转中...') + // 登录成功,跳转到服务器页面 + setTimeout(() => (location.href = '/'), 1000) + } else { + throw new Error(data.value.message) + } + checkMustCode() + } catch (err: unknown) { + error.value = (err as Error).message + checkMustCode() + } + } + + /** + * 登出处理 + * 清除用户信息和token,并重定向到登录页 + */ + const handleLogout = (): void => { + // 清除所有状态 + user.value = null + useToken.value = null + resetForm() + location.href = '/login' + } + + /** + * 获取登录验证码 + * @returns Promise,操作完成时 resolve + */ + const handleGetCode = async (): Promise => { + try { + const { data: codeData } = await getLoginCode() // 重命名 data 避免与外部 data 冲突 + codeImg.value = codeData.data + } catch (err) { + // 明确捕获的 error 类型 + handleError(err) + } + } + + /** + * 检测是否必须验证码 + */ + const checkMustCode = (): void => { + const res = getCookie('must_code', false) + mustCode.value = Number(res) === 1 + if (mustCode.value) handleGetCode() + } + + // -------------------- Store 导出 -------------------- + return { + // 状态导出 + loading, + codeImg, + error, + user, + loginData, + rememberMe: rememberMeRef, + forgotPasswordRef, + mustCode, + + // 方法导出 + handleLogin, + handleLogout, + handleGetCode, + checkMustCode, + resetForm, + clearToken, + } +}) + +/** + * 登录Store Hook + * @returns 登录Store的响应式状态和方法 + */ +export const useStore = () => { + const store = useLoginStore() + return { ...store, ...storeToRefs(store) } // 确保返回类型与 LoginStoreExposes 兼容 +} diff --git a/frontend/apps/allin-ssl/src/views/monitor/components/AddMonitorModel.tsx b/frontend/apps/allin-ssl/src/views/monitor/components/AddMonitorModel.tsx new file mode 100644 index 0000000..9119356 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/monitor/components/AddMonitorModel.tsx @@ -0,0 +1,35 @@ +import { defineComponent, type PropType } from 'vue' +import { useMonitorFormController } from '../useController' + +import type { UpdateSiteMonitorParams } from '@/types/monitor' + +/** + * 监控表单组件 + * @description 用于添加和编辑证书监控的表单界面 + */ +export default defineComponent({ + name: 'MonitorForm', + props: { + /** + * 是否为编辑模式 + */ + isEdit: { + type: Boolean, + default: false, + }, + /** + * 编辑时的初始数据 + */ + data: { + type: Object as PropType, + default: () => null, + }, + }, + setup(props) { + // 使用表单控制器获取表单组件 + const { component: MonitorForm } = useMonitorFormController(props.data) + + // 返回渲染函数 + return () => + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/monitor/index.tsx b/frontend/apps/allin-ssl/src/views/monitor/index.tsx new file mode 100644 index 0000000..58a099d --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/monitor/index.tsx @@ -0,0 +1,97 @@ +import { defineComponent, onMounted } from 'vue' +import { NButton, NInput } from 'naive-ui' +import { Search } from '@vicons/carbon' + +import { $t } from '@locales/index' +import { useThemeCssVar } from '@baota/naive-ui/theme' + +import { useController } from './useController' + +import BaseComponent from '@components/BaseLayout' +import EmptyState from '@components/TableEmptyState' + +/** + * 监控管理组件 + * @description 提供证书监控的管理界面,包括列表展示、搜索、添加、编辑等功能 + */ +export default defineComponent({ + name: 'MonitorManage', + setup() { + // 使用控制器获取数据和方法 + const { MonitorTable, MonitorTablePage, param, fetch, data, openAddForm, isDetectionAddMonitor } = useController() + + // 获取主题CSS变量 + const cssVar = useThemeCssVar(['contentPadding', 'borderColor', 'headerHeight', 'iconColorHover']) + + // 组件挂载时初始化数据 + onMounted(() => { + // 获取监控列表数据 + fetch() + // 检测是否需要自动打开添加表单(从其他页面跳转而来) + isDetectionAddMonitor() + }) + + // 返回渲染函数 + return () => ( +
+
+ ( + + {$t('t_11_1745289354516')} + + ), + // 头部右侧区域 - 搜索框 + headerRight: () => ( + { + if (e.key === 'Enter') fetch() + }} + onClear={() => fetch()} + placeholder={$t('t_12_1745289356974')} + clearable + size="large" + class="min-w-[300px]" + v-slots={{ + suffix: () => ( +
+ +
+ ), + }} + >
+ ), + // 内容区域 - 监控表格 + content: () => ( +
+ + {{ + empty: () => , + }} + +
+ ), + // 底部右侧区域 - 分页组件 + footerRight: () => ( +
+ ( + + {$t('t_15_1745227839354')} {data.value.total} {$t('t_16_1745227838930')} + + ), + }} + /> +
+ ), + }} + >
+
+
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/monitor/useController.tsx b/frontend/apps/allin-ssl/src/views/monitor/useController.tsx new file mode 100644 index 0000000..a5e7198 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/monitor/useController.tsx @@ -0,0 +1,414 @@ +import { computed, onMounted, onUnmounted } from 'vue' +import { useRoute, useRouter } from 'vue-router' +import { FormRules, NButton, NSpace, NSwitch, type DataTableColumns } from 'naive-ui' + +// 钩子和工具 +import { + useModal, + useTable, + useTablePage, + useDialog, + useModalHooks, + useForm, + useFormHooks, + useLoadingMask, +} from '@baota/naive-ui/hooks' +import { useError } from '@baota/hooks/error' +import { isDomain } from '@baota/utils/business' +import { $t } from '@locales/index' + +// Store和组件 +import { useStore } from './useStore' +import MonitorForm from './components/AddMonitorModel' +import NotifyProviderSelect from '@components/NotifyProviderSelect' +import TypeIcon from '@components/TypeIcon' + +// 类型导入 +import type { Ref } from 'vue' +import type { + AddSiteMonitorParams, + SiteMonitorItem, + SiteMonitorListParams, + UpdateSiteMonitorParams, +} from '@/types/monitor' + +/** + * 监控管理控制器接口定义 + */ +interface MonitorControllerExposes { + // 表格相关 + MonitorTable: ReturnType['component'] + MonitorTablePage: ReturnType['component'] + loading: Ref + param: Ref + data: Ref<{ list: SiteMonitorItem[]; total: number }> + fetch: () => Promise + + // 表单和操作相关 + openAddForm: () => void + isDetectionAddMonitor: () => void +} + +// 从Store中获取方法 +const { + fetchMonitorList, + deleteExistingMonitor, + setMonitorStatus, + monitorForm, + addNewMonitor, + updateExistingMonitor, + resetMonitorForm, + updateMonitorForm, +} = useStore() + +// 错误处理 +const { handleError } = useError() + +/** + * 监控管理业务逻辑控制器 + * @description 处理监控列表页面的业务逻辑,包括表格展示、添加、编辑、删除等操作 + * @returns {MonitorControllerExposes} 返回controller对象 + */ +export const useController = (): MonitorControllerExposes => { + const route = useRoute() + const router = useRouter() + + /** + * 创建表格列配置 + * @description 定义监控表格的列结构和渲染方式 + * @returns {DataTableColumns} 返回表格列配置数组 + */ + const createColumns = (): DataTableColumns => [ + { + title: $t('t_13_1745289354528'), + key: 'name', + width: 150, + }, + { + title: $t('t_17_1745227838561'), + key: 'site_domain', + width: 180, + render: (row: SiteMonitorItem) => { + return ( + + {row.site_domain} + + ) + }, + }, + { + title: $t('t_14_1745289354902'), + key: 'cert_domain', + width: 180, + render: (row: SiteMonitorItem) => { + return row.cert_domain || '-' + }, + }, + { + title: $t('t_15_1745289355714'), + key: 'ca', + width: 180, + }, + { + title: $t('t_16_1745289354902'), + key: 'state', + width: 100, + }, + { + title: $t('t_17_1745289355715'), + key: 'end_time', + width: 150, + render: (row: SiteMonitorItem) => row.end_time + '(' + row.end_day + ')', + }, + { + title: $t('t_18_1745289354598'), + key: 'report_type', + width: 150, + render: (row: SiteMonitorItem) => { + return + }, + }, + { + title: $t('t_4_1745215914951'), + key: 'active', + width: 100, + render: (row: SiteMonitorItem) => { + return toggleStatus(row)} /> + }, + }, + { + title: $t('t_19_1745289354676'), + key: 'update_time', + width: 150, + render: (row: SiteMonitorItem) => row.update_time || '-', + }, + { + title: $t('t_7_1745215914189'), + key: 'create_time', + width: 150, + }, + { + title: $t('t_8_1745215914610'), + key: 'actions', + width: 150, + fixed: 'right' as const, + align: 'right', + render: (row: SiteMonitorItem) => { + return ( + + openEditForm(row)}> + {$t('t_11_1745215915429')} + + confirmDelete(row)}> + {$t('t_12_1745215914312')} + + + ) + }, + }, + ] + + /** + * 表格实例 + * @description 创建表格实例并管理相关状态 + */ + const { + component: MonitorTable, + loading, + param, + data, + total, + fetch, + } = useTable({ + config: createColumns(), + request: fetchMonitorList, + defaultValue: { + p: 1, + limit: 10, + search: '', + }, + watchValue: ['p', 'limit'], + }) + + /** + * 分页实例 + * @description 创建表格分页组件 + */ + const { component: MonitorTablePage } = useTablePage({ + param, + total, + alias: { + page: 'p', + pageSize: 'limit', + }, + }) + + /** + * 打开添加监控弹窗 + * @description 显示添加监控的表单弹窗 + */ + const openAddForm = (): void => { + useModal({ + title: $t('t_11_1745289354516'), + area: 500, + component: MonitorForm, + footer: true, + onUpdateShow(show) { + if (!show) fetch() + }, + }) + } + + /** + * 打开编辑监控弹窗 + * @description 显示编辑监控的表单弹窗 + * @param {SiteMonitorItem} data - 要编辑的监控项数据 + */ + const openEditForm = (data: SiteMonitorItem): void => { + useModal({ + title: $t('t_20_1745289354598'), + area: 500, + component: MonitorForm, + componentProps: { isEdit: data.id, data }, + footer: true, + onUpdateShow(show) { + if (!show) fetch() + }, + }) + } + + /** + * 确认删除监控 + * @description 显示删除确认对话框 + * @param {SiteMonitorItem} row - 要删除的监控项 + */ + const confirmDelete = (row: SiteMonitorItem): void => { + useDialog({ + title: $t('t_0_1745294710530'), + content: $t('t_22_1745289359036'), + confirmText: $t('t_5_1744870862719'), + cancelText: $t('t_4_1744870861589'), + onPositiveClick: async () => { + await deleteExistingMonitor(row) + fetch() + }, + }) + } + + /** + * 切换监控状态 + * @description 启用或禁用监控 + * @param {SiteMonitorItem} row - 监控项 + */ + const toggleStatus = async (row: SiteMonitorItem): Promise => { + await setMonitorStatus({ id: row.id, active: Number(row.active) ? 0 : 1 }) + fetch() + } + + /** + * 检测是否需要添加工作流 + * @description 从URL参数判断是否需要自动打开添加表单 + */ + const isDetectionAddMonitor = (): void => { + const { type } = route.query + if (type?.includes('create')) { + openAddForm() + router.push({ query: {} }) + } + } + + return { + loading, + fetch, + MonitorTable, + MonitorTablePage, + isDetectionAddMonitor, + param, + data, + openAddForm, + } +} + +/** + * 监控表单控制器接口定义 + */ +interface MonitorFormControllerExposes { + component: ReturnType['component'] +} + +/** + * 监控表单控制器 + * @description 处理监控添加/编辑表单的业务逻辑 + * @param {UpdateSiteMonitorParams | null} data - 编辑时的初始数据 + * @returns {MonitorFormControllerExposes} 返回控制器对象 + */ +export const useMonitorFormController = (data: UpdateSiteMonitorParams | null = null): MonitorFormControllerExposes => { + // 表单工具 + const { useFormInput, useFormCustom, useFormInputNumber } = useFormHooks() + + // 加载遮罩 + const { open: openLoad, close: closeLoad } = useLoadingMask({ text: '正在提交信息,请稍后...' }) + + // 消息和对话框 + const { confirm } = useModalHooks() + + /** + * 表单配置 + * @description 定义表单字段和布局 + */ + const config = computed(() => [ + useFormInput('名称', 'name'), + useFormInput('域名', 'domain'), + useFormInputNumber('周期(分钟)', 'cycle', { class: 'w-full' }), + useFormCustom(() => { + return ( + { + monitorForm.value.report_type = item.value + }} + /> + ) + }), + ]) + + /** + * 表单验证规则 + */ + const rules = { + name: { required: true, message: '请输入名称', trigger: 'input' }, + domain: { + required: true, + message: '请输入正确的域名', + trigger: 'input', + validator: (rule: any, value: any, callback: any) => { + if (!isDomain(value)) { + callback(new Error('请输入正确的域名')) + } else { + callback() + } + }, + }, + cycle: { required: true, message: '请输入周期', trigger: 'input', type: 'number', min: 1, max: 365 }, + report_type: { required: true, message: '请选择消息通知类型', trigger: 'change' }, + } as FormRules + + /** + * 表单提交处理 + * @description 根据当前模式处理表单提交 + * @param {AddSiteMonitorParams | UpdateSiteMonitorParams} params - 表单数据 + */ + const request = async (params: AddSiteMonitorParams | UpdateSiteMonitorParams): Promise => { + try { + if (data) { + await updateExistingMonitor({ ...params, id: data.id }) + } else { + const { id, ...rest } = params + await addNewMonitor(rest) + } + } catch (error) { + handleError(error).default('添加失败') + } + } + + /** + * 使用表单hooks创建表单组件 + */ + const { component, fetch } = useForm({ + config, + defaultValue: monitorForm, + request, + rules, + }) + + /** + * 关联确认按钮 + * @description 处理表单提交逻辑 + */ + confirm(async (close) => { + try { + openLoad() + await fetch() + close() + } catch (error) { + return handleError(error) + } finally { + closeLoad() + } + }) + + // 组件挂载时更新表单数据 + onMounted(() => { + updateMonitorForm(data) + }) + + // 组件卸载时重置表单 + onUnmounted(resetMonitorForm) + + return { + component, + } +} diff --git a/frontend/apps/allin-ssl/src/views/monitor/useStore.tsx b/frontend/apps/allin-ssl/src/views/monitor/useStore.tsx new file mode 100644 index 0000000..3100db1 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/monitor/useStore.tsx @@ -0,0 +1,211 @@ +import { defineStore, storeToRefs } from 'pinia' +import { ref, computed } from 'vue' +import { getSiteMonitorList, addSiteMonitor, updateSiteMonitor, deleteSiteMonitor, setSiteMonitor } from '@/api/monitor' +import { useMessage } from '@baota/naive-ui/hooks' +import { useError } from '@baota/hooks/error' +import { $t } from '@locales/index' + +import type { Ref } from 'vue' +import type { + SiteMonitorItem, + SiteMonitorListParams, + AddSiteMonitorParams, + UpdateSiteMonitorParams, + SetSiteMonitorParams, + DeleteSiteMonitorParams, +} from '@/types/monitor' +import type { TableResponse } from '@baota/naive-ui/types/table' + +// 导入错误处理钩子 +const { handleError } = useError() +const message = useMessage() + +/** + * 定义Store暴露的类型 + */ +interface MonitorStoreExposes { + // 状态 + monitorForm: Ref + + // 方法 + fetchMonitorList: (params: SiteMonitorListParams) => Promise> + addNewMonitor: (params: AddSiteMonitorParams) => Promise + updateExistingMonitor: (params: UpdateSiteMonitorParams) => Promise + deleteExistingMonitor: (params: DeleteSiteMonitorParams) => Promise + setMonitorStatus: (params: SetSiteMonitorParams) => Promise + resetMonitorForm: () => void + updateMonitorForm: (params?: UpdateSiteMonitorParams | null) => void + submitForm: () => Promise +} + +/** + * 监控管理状态 Store + * @description 用于管理监控相关的状态和操作,包括监控列表、添加、编辑等 + */ +export const useMonitorStore = defineStore('monitor-store', (): MonitorStoreExposes => { + // -------------------- 状态定义 -------------------- + + /** + * 添加/编辑监控表单状态 + */ + const monitorForm = ref({ + id: 0, + name: '', + domain: '', + cycle: 1, + report_type: '', + }) + + // -------------------- 方法定义 -------------------- + /** + * 获取监控列表 + * @description 根据分页参数获取监控列表数据 + * @param {SiteMonitorListParams} params - 查询参数 + * @returns {Promise>} 返回列表数据和总数 + */ + const fetchMonitorList = async (params: SiteMonitorListParams): Promise> => { + try { + const { data, count } = await getSiteMonitorList(params).fetch() + return { + list: (data || []) as T[], + total: count, + } + } catch (error) { + handleError(error) + return { list: [] as T[], total: 0 } + } + } + + /** + * 添加监控 + * @description 添加新监控 + * @param {AddSiteMonitorParams} params - 添加监控参数 + * @returns {Promise} 是否添加成功 + */ + const addNewMonitor = async (params: AddSiteMonitorParams): Promise => { + try { + const { fetch, message } = addSiteMonitor(params) + message.value = true + await fetch() + return true + } catch (error) { + if (handleError(error)) message.error($t('t_7_1745289355714')) + return false + } + } + + /** + * 更新监控 + * @description 更新指定ID的监控 + * @param {UpdateSiteMonitorParams} params - 更新监控参数 + * @returns {Promise} 是否更新成功 + */ + const updateExistingMonitor = async (params: UpdateSiteMonitorParams): Promise => { + try { + const { fetch, message } = updateSiteMonitor(params) + message.value = true + await fetch() + return true + } catch (error) { + if (handleError(error)) message.error($t('t_23_1745289355716')) + return false + } + } + + /** + * 删除监控 + * @description 删除指定ID的监控 + * @param {DeleteSiteMonitorParams} params - 删除监控参数 + * @returns {Promise} 是否删除成功 + */ + const deleteExistingMonitor = async (params: DeleteSiteMonitorParams): Promise => { + try { + const { fetch, message } = deleteSiteMonitor(params) + message.value = true + await fetch() + return true + } catch (error) { + if (handleError(error)) message.error($t('t_40_1745227838872')) + return false + } + } + + /** + * 设置监控状态 + * @description 设置指定ID的监控状态 + * @param {SetSiteMonitorParams} params - 设置监控状态参数 + * @returns {Promise} 是否设置成功 + */ + const setMonitorStatus = async (params: SetSiteMonitorParams): Promise => { + try { + const { fetch, message } = setSiteMonitor(params) + message.value = true + await fetch() + return true + } catch (error) { + if (handleError(error)) message.error($t('t_24_1745289355715')) + return false + } + } + + /** + * 更新监控表单 + * @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 } + } + + /** + * 重置表单 + * @description 清空表单数据 + */ + const resetMonitorForm = (): void => { + monitorForm.value = { + id: 0, + name: '', + domain: '', + cycle: 1, + report_type: '', + } + } + + /** + * 提交表单 + * @description 根据表单状态自动判断是添加还是更新操作 + * @returns {Promise} 是否提交成功 + */ + const submitForm = async (): Promise => { + const { id, ...params } = monitorForm.value + if (id) { + return updateExistingMonitor({ id, ...params }) // 编辑模式 + } else { + return addNewMonitor(params) // 添加模式 + } + } + + // 返回所有状态和方法 + return { + monitorForm, + fetchMonitorList, + addNewMonitor, + updateExistingMonitor, + deleteExistingMonitor, + setMonitorStatus, + resetMonitorForm, + updateMonitorForm, + submitForm, + } +}) + +/** + * 组合式 API 使用 Store + * @description 提供对监控管理 Store 的访问,并返回响应式引用 + * @returns {MonitorStoreExposes} 包含所有 store 状态和方法的对象 + */ +export const useStore = () => { + const store = useMonitorStore() + return { ...store, ...storeToRefs(store) } +} diff --git a/frontend/apps/allin-ssl/src/views/settings/components/AboutSettings.tsx b/frontend/apps/allin-ssl/src/views/settings/components/AboutSettings.tsx new file mode 100644 index 0000000..2b7242e --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/settings/components/AboutSettings.tsx @@ -0,0 +1,77 @@ +import { NCard, NSpace, NDescriptions, NDescriptionsItem, NIcon, NButton } from 'naive-ui' +import { $t } from '@locales/index' +import { LogoGithub } from '@vicons/ionicons5' +/** + * 关于我们标签页组件 + */ +export default defineComponent({ + name: 'AboutSettings', + setup() { + return () => ( +
+ + + + +
+ v1.0.3 +
+
+ +
+ + + + + https://github.com/allinssl/allinssl + +
+
+
+
+
+ + +
+

+

AllinSSL

+
+

{$t('t_35_1746773362992')}

+ + {$t( + '本工具可帮助用户轻松管理多个网站的SSL证书,提供自动化的证书申请、更新和部署流程,并实时监控证书状态,确保网站安全持续运行。', + )} +
    +
  • + {$t('t_36_1746773348989')} + {$t('t_1_1746773763643')} +
  • +
  • + {$t('t_38_1746773349796')} + {$t('t_39_1746773358932')} +
  • +
  • + {$t('t_40_1746773352188')} + {$t('t_41_1746773364475')} +
  • +
  • + {$t('t_42_1746773348768')} + {$t('t_43_1746773359511')} +
  • +
  • + {$t('t_44_1746773352805')} + {$t('t_45_1746773355717')} +
  • +
  • + {$t('t_46_1746773350579')} + {$t('t_47_1746773360760')} +
  • +
+
+

+
+
+
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/settings/components/GeneralSettings.tsx b/frontend/apps/allin-ssl/src/views/settings/components/GeneralSettings.tsx new file mode 100644 index 0000000..80f5939 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/settings/components/GeneralSettings.tsx @@ -0,0 +1,33 @@ +import { NCard, NButton, NGrid, NGridItem } from 'naive-ui' +import { $t } from '@locales/index' +import { useStore } from '../useStore' +import { useController, useGeneralSettingsController } from '../useController' + +/** + * 常用设置标签页组件 + */ +export default defineComponent({ + name: 'GeneralSettings', + setup() { + const { generalSettings } = useStore() + const { handleSaveGeneralSettings } = useController() + const { GeneralForm } = useGeneralSettingsController() + return () => ( +
+
+ handleSaveGeneralSettings(generalSettings.value)}> + {$t('t_9_1745464078110')} + +
+ + + + + + + + +
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/settings/components/NotificationSettings.tsx b/frontend/apps/allin-ssl/src/views/settings/components/NotificationSettings.tsx new file mode 100644 index 0000000..1eee6a9 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/settings/components/NotificationSettings.tsx @@ -0,0 +1,187 @@ +import { NCard, NButton, NList, NListItem, NTag, NSpace, NGrid, NGridItem, NSwitch } from 'naive-ui' +import { useController } from '@settings/useController' +import { useStore } from '@settings/useStore' +import SvgIcon from '@components/SvgIcon' +import { $t } from '@locales/index' + +/** + * 告警通知标签页组件 + */ +export default defineComponent({ + name: 'NotificationSettings', + setup() { + const { notifyChannels, channelTypes } = useStore() + const { + openAddEmailChannelModal, + openAddFeishuChannelModal, + openAddWebhookChannelModal, + openAddDingtalkChannelModal, + editChannelConfig, + testChannelConfig, + confirmDeleteChannel, + handleEnableChange, + } = useController() + + // 获取已配置的渠道数量 + const getConfiguredCount = (type: string) => { + return notifyChannels.value.filter((item) => item.type === type).length + } + + // 检查渠道是否已配置 + const isChannelConfigured = (type: string) => { + return getConfiguredCount(type) > 0 + } + + // 根据渠道类型和配置状态获取操作按钮 + const getChannelActionButton = (type: string) => { + // 根据类型返回对应的按钮 + if (type === 'mail') { + return ( + openAddEmailChannelModal(getConfiguredCount(type))}> + {$t('t_1_1746676859550')} + + ) + } else if (type === 'feishu') { + return ( + openAddFeishuChannelModal(getConfiguredCount(type))}> + {$t('t_1_1746676859550')} + + ) + } else if (type === 'webhook') { + return ( + openAddWebhookChannelModal(getConfiguredCount(type))}> + {$t('t_1_1746676859550')} + + ) + } else if (type === 'dingtalk') { + return ( + openAddDingtalkChannelModal(getConfiguredCount(type))} + > + {$t('t_1_1746676859550')} + + ) + } + // 其他渠道暂未支持 + return ( + + {$t('t_2_1746676856700')} + + ) + } + + // 渠道配置项数据 + const channelConfigs = [ + { + type: 'mail', + name: $t('t_3_1746676857930'), + description: $t('t_4_1746676861473'), + color: '#2080f0', + }, + { + type: 'feishu', + name: $t('t_9_1746676857164'), + description: $t('t_10_1746676862329'), + color: '#3370ff', + }, + { + type: 'webhook', + name: $t('t_11_1746676859158'), + description: $t('t_12_1746676860503'), + color: '#531dab', + }, + { + type: 'dingtalk', + name: $t('t_5_1746676856974'), + description: $t('t_6_1746676860886'), + color: '#1677ff', + }, + { + type: 'wecom', + name: $t('t_7_1746676857191'), + description: $t('t_8_1746676860457'), + color: '#07c160', + }, + ] + return () => ( +
+ + + {channelConfigs.map((item) => ( + +
+
+ +
+
+ {item.name} + {isChannelConfigured(item.type) && ( + + {$t('t_8_1745735765753')} {getConfiguredCount(item.type)} + + )} +
+
{item.description}
+
+
+
{getChannelActionButton(item.type)}
+
+
+ ))} +
+
+ + {/* 已配置的通知渠道列表 */} + {notifyChannels.value.length > 0 && ( + + + {notifyChannels.value.map((item) => ( + +
+
+ +
{item.name}
+
+ + {(channelTypes.value as Record)[item.type] || item.id} + +
+
+
+ handleEnableChange(item)} + checkedValue={'1'} + uncheckedValue={'0'} + v-slots={{ + checked: () => {$t('t_0_1745457486299')}, + unchecked: () => {$t('t_15_1746676856567')}, + }} + /> +
+
+ + editChannelConfig(item)}> + {$t('t_11_1745215915429')} + + testChannelConfig(item)}> + {$t('t_16_1746676855270')} + + confirmDeleteChannel(item)}> + {$t('t_12_1745215914312')} + + +
+
+
+ ))} +
+
+ )} +
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/settings/components/channel/DingtalkChannelModel.tsx b/frontend/apps/allin-ssl/src/views/settings/components/channel/DingtalkChannelModel.tsx new file mode 100644 index 0000000..6e83754 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/settings/components/channel/DingtalkChannelModel.tsx @@ -0,0 +1,71 @@ +import { useForm, useModalHooks } from '@baota/naive-ui/hooks' +import { useError } from '@baota/hooks/error' +import { useDingtalkChannelFormController } from './useController' +import { useStore } from '@settings/useStore' + +import type { ReportDingtalk, ReportType } from '@/types/setting' + +/** + * 钉钉通知渠道表单组件 + */ +export default defineComponent({ + name: 'DingtalkChannelModel', + props: { + data: { + type: Object as PropType | null>, + default: () => null, + }, + }, + setup(props: { data: ReportType | null }) { + const { handleError } = useError() + const { confirm } = useModalHooks() + const { fetchNotifyChannels } = useStore() + const { config, rules, dingtalkChannelForm, submitForm } = useDingtalkChannelFormController() + + if (props.data) { + const { name, config } = props.data + dingtalkChannelForm.value = { + name, + ...config, + } + } + // 使用表单hooks + const { + component: DingtalkForm, + example, + data, + } = useForm({ + config, + defaultValue: dingtalkChannelForm, + rules, + }) + + // 关联确认按钮 + confirm(async (close) => { + try { + const { name, ...other } = data.value + await example.value?.validate() + const res = await submitForm( + { + type: 'dingtalk', + name: name || '', + config: other, + }, + example, + props.data?.id, + ) + + fetchNotifyChannels() + if (res) close() + } catch (error) { + handleError(error) + } + }) + + return () => ( +
+ +
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/settings/components/channel/EmailChannelModel.tsx b/frontend/apps/allin-ssl/src/views/settings/components/channel/EmailChannelModel.tsx new file mode 100644 index 0000000..47ec4ca --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/settings/components/channel/EmailChannelModel.tsx @@ -0,0 +1,130 @@ +import { NGrid, NFormItemGi, NInput, NSwitch, NTooltip } from 'naive-ui' +import { useForm, useModalHooks } from '@baota/naive-ui/hooks' +import { useError } from '@baota/hooks/error' +import { $t } from '@locales/index' +import { useEmailChannelFormController } from './useController' +import { useStore } from '@settings/useStore' + +import type { ReportMail, ReportType } from '@/types/setting' + +/** + * 邮箱通知渠道表单组件 + */ +export default defineComponent({ + name: 'EmailChannelModel', + props: { + data: { + type: Object as PropType | null>, + default: () => null, + }, + }, + setup(props: { data: ReportType | null }) { + const { handleError } = useError() + const { confirm } = useModalHooks() + const { fetchNotifyChannels } = useStore() + const { config, rules, emailChannelForm, submitForm } = useEmailChannelFormController() + + if (props.data) { + const { name, config } = props.data + emailChannelForm.value = { + name, + ...config, + } + } + // 使用表单hooks + const { + component: EmailForm, + example, + data, + } = useForm({ + config, + defaultValue: emailChannelForm, + rules, + }) + + // 关联确认按钮 + confirm(async (close) => { + try { + const { name, ...other } = data.value + await example.value?.validate() + const res = await submitForm( + { + type: 'mail', + name: name || '', + config: other, + }, + example, + props.data?.id, + ) + + fetchNotifyChannels() + if (res) close() + } catch (error) { + handleError(error) + } + }) + + return () => ( + + ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/settings/components/channel/FeishuChannelModel.tsx b/frontend/apps/allin-ssl/src/views/settings/components/channel/FeishuChannelModel.tsx new file mode 100644 index 0000000..1e9726c --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/settings/components/channel/FeishuChannelModel.tsx @@ -0,0 +1,71 @@ +import { useForm, useModalHooks } from '@baota/naive-ui/hooks' +import { useError } from '@baota/hooks/error' +import { useFeishuChannelFormController } from './useController' +import { useStore } from '@settings/useStore' + +import type { ReportFeishu, ReportType } from '@/types/setting' + +/** + * 飞书通知渠道表单组件 + */ +export default defineComponent({ + name: 'FeishuChannelModel', + props: { + data: { + type: Object as PropType | null>, + default: () => null, + }, + }, + setup(props: { data: ReportType | null }) { + const { handleError } = useError() + const { confirm } = useModalHooks() + const { fetchNotifyChannels } = useStore() + const { config, rules, feishuChannelForm, submitForm } = useFeishuChannelFormController() + + if (props.data) { + const { name, config } = props.data + feishuChannelForm.value = { + name, + ...config, + } + } + // 使用表单hooks + const { + component: FeishuForm, + example, + data, + } = useForm({ + config, + defaultValue: feishuChannelForm, + rules, + }) + + // 关联确认按钮 + confirm(async (close) => { + try { + const { name, ...other } = data.value + await example.value?.validate() + const res = await submitForm( + { + type: 'feishu', + name: name || '', + config: other, + }, + example, + props.data?.id, + ) + + fetchNotifyChannels() + if (res) close() + } catch (error) { + handleError(error) + } + }) + + return () => ( +
+ +
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/settings/components/channel/WebhookChannelModel.tsx b/frontend/apps/allin-ssl/src/views/settings/components/channel/WebhookChannelModel.tsx new file mode 100644 index 0000000..ba85590 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/settings/components/channel/WebhookChannelModel.tsx @@ -0,0 +1,71 @@ +import { useForm, useModalHooks } from '@baota/naive-ui/hooks' +import { useError } from '@baota/hooks/error' +import { useWebhookChannelFormController } from './useController' +import { useStore } from '@settings/useStore' + +import type { ReportWebhook, ReportType } from '@/types/setting' + +/** + * Webhook通知渠道表单组件 + */ +export default defineComponent({ + name: 'WebhookChannelModel', + props: { + data: { + type: Object as PropType | null>, + default: () => null, + }, + }, + setup(props: { data: ReportType | null }) { + const { handleError } = useError() + const { confirm } = useModalHooks() + const { fetchNotifyChannels } = useStore() + const { config, rules, webhookChannelForm, submitForm } = useWebhookChannelFormController() + + if (props.data) { + const { name, config } = props.data + webhookChannelForm.value = { + name, + ...config, + } + } + // 使用表单hooks + const { + component: WebhookForm, + example, + data, + } = useForm({ + config, + defaultValue: webhookChannelForm, + rules, + }) + + // 关联确认按钮 + confirm(async (close) => { + try { + const { name, ...other } = data.value + await example.value?.validate() + const res = await submitForm( + { + type: 'webhook', + name: name || '', + config: other, + }, + example, + props.data?.id, + ) + + fetchNotifyChannels() + if (res) close() + } catch (error) { + handleError(error) + } + }) + + return () => ( +
+ +
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/settings/components/channel/useController.tsx b/frontend/apps/allin-ssl/src/views/settings/components/channel/useController.tsx new file mode 100644 index 0000000..b740449 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/settings/components/channel/useController.tsx @@ -0,0 +1,351 @@ +import { FormInst, FormItemRule, FormRules } from 'naive-ui' +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' + +const { + emailChannelForm, + feishuChannelForm, + webhookChannelForm, + dingtalkChannelForm, + addReportChannel, + updateReportChannel, +} = useStore() + +const { handleError } = useError() +const { useFormInput, useFormSwitch, useFormTextarea, useFormSelect, useFormSlot } = useFormHooks() + +/** + * 邮箱通知渠道表单控制器 + * @function useEmailChannelFormController + * @description 提供邮箱通知渠道表单的配置、规则和提交方法 + * @returns {object} 返回表单相关配置、规则和方法 + */ +export const useEmailChannelFormController = () => { + 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'), + }, + smtpHost: { + required: true, + trigger: ['input', 'blur'], + message: $t('t_15_1745833940280'), + }, + smtpPort: { + required: true, + trigger: 'input', + validator: (rule: FormItemRule, value: string) => { + const port = Number(value) + if (isNaN(port) || port < 1 || port > 65535) { + return new Error($t('t_26_1746773353409')) + } else { + return true + } + }, + }, + password: { + required: true, + trigger: ['input', 'blur'], + message: $t('t_27_1746773352584'), + }, + sender: { + required: true, + trigger: ['input', 'blur'], + type: 'email', + message: $t('t_28_1746773354048'), + }, + receiver: { + required: true, + trigger: ['input', 'blur'], + type: 'email', + message: $t('t_29_1746773351834'), + }, + } + + /** + * 表单配置 + * @type {ComputedRef} + * @description 生成邮箱通知渠道表单的字段配置 + */ + const config = computed(() => [ + useFormInput($t('t_2_1745289353944'), 'name'), + useFormSlot('smtp-template'), + useFormSlot('username-template'), + useFormInput($t('t_30_1746773350013'), 'sender'), + useFormInput($t('t_31_1746773349857'), 'receiver'), + ]) + + /** + * 提交表单 + * @async + * @function submitForm + * @description 验证并提交邮箱通知渠道表单 + * @param {any} params - 表单参数 + * @param {Ref} formRef - 表单实例引用 + * @returns {Promise} 提交成功返回true,失败返回false + */ + const submitForm = async ( + { config, ...other }: AddReportParams, + formRef: Ref, + 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, + emailChannelForm, + submitForm, + } +} + +/** + * 飞书通知渠道表单控制器 + * @function useFeishuChannelFormController + * @description 提供飞书通知渠道表单的配置、规则和提交方法 + * @returns {object} 返回表单相关配置、规则和方法 + */ +export const useFeishuChannelFormController = () => { + 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'), + }, + webhook: { + required: true, + trigger: ['input', 'blur'], + message: '请输入飞书webhook地址', + }, + } + + /** + * 表单配置 + * @type {ComputedRef} + * @description 生成飞书通知渠道表单的字段配置 + */ + const config = computed(() => [ + useFormInput($t('t_2_1745289353944'), 'name'), + useFormInput('飞书WebHook地址', 'webhook'), + useFormInput('飞书WebHook密钥(可选)', 'secret', {}, { showRequireMark: false }), + ]) + + /** + * 提交表单 + * @async + * @function submitForm + * @description 验证并提交飞书通知渠道表单 + * @param {any} params - 表单参数 + * @param {Ref} formRef - 表单实例引用 + * @returns {Promise} 提交成功返回true,失败返回false + */ + const submitForm = async ( + { config, ...other }: AddReportParams, + formRef: Ref, + 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, + feishuChannelForm, + submitForm, + } +} + +/** + * Webhook通知渠道表单控制器 + * @function useWebhookChannelFormController + * @description 提供Webhook通知渠道表单的配置、规则和提交方法 + * @returns {object} 返回表单相关配置、规则和方法 + */ +export const useWebhookChannelFormController = () => { + 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} + * @description 生成Webhook通知渠道表单的字段配置 + */ + const config = computed(() => [ + useFormInput($t('t_2_1745289353944'), 'name'), + useFormInput('WebHook回调地址', 'url'), + useFormTextarea('WebHook推送通知回调数据(可选)', 'data', { rows: 3 }, { showRequireMark: false }), + useFormSelect('请求方式', 'method', [ + { label: 'POST', value: 'post' }, + { label: 'GET', value: 'get' }, + ]), + useFormTextarea('WebHook请求头(可选)', 'headers', { rows: 3 }, { showRequireMark: false }), + useFormSwitch('忽略SSL/TLS证书错误', 'ignore_ssl'), + ]) + + /** + * 提交表单 + * @async + * @function submitForm + * @description 验证并提交Webhook通知渠道表单 + * @param {any} params - 表单参数 + * @param {Ref} formRef - 表单实例引用 + * @returns {Promise} 提交成功返回true,失败返回false + */ + const submitForm = async ( + { config, ...other }: AddReportParams, + formRef: Ref, + 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, + webhookChannelForm, + submitForm, + } +} + +/** + * 钉钉通知渠道表单控制器 + * @function useDingtalkChannelFormController + * @description 提供钉钉通知渠道表单的配置、规则和提交方法 + * @returns {object} 返回表单相关配置、规则和方法 + */ +export const useDingtalkChannelFormController = () => { + 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'), + }, + webhook: { + required: true, + trigger: ['input', 'blur'], + message: '请输入钉钉webhook地址', + }, + } + + /** + * 表单配置 + * @type {ComputedRef} + * @description 生成钉钉通知渠道表单的字段配置 + */ + const config = computed(() => [ + useFormInput($t('t_2_1745289353944'), 'name'), + useFormInput('钉钉WebHook地址', 'webhook'), + useFormInput('钉钉WebHook密钥(可选)', 'secret', {}, { showRequireMark: false }), + ]) + + /** + * 提交表单 + * @async + * @function submitForm + * @description 验证并提交钉钉通知渠道表单 + * @param {any} params - 表单参数 + * @param {Ref} formRef - 表单实例引用 + * @returns {Promise} 提交成功返回true,失败返回false + */ + const submitForm = async ( + { config, ...other }: AddReportParams, + formRef: Ref, + 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, + dingtalkChannelForm, + submitForm, + } +} diff --git a/frontend/apps/allin-ssl/src/views/settings/index.tsx b/frontend/apps/allin-ssl/src/views/settings/index.tsx new file mode 100644 index 0000000..e711961 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/settings/index.tsx @@ -0,0 +1,86 @@ +import { NTabs, NTabPane, NCard, NIcon } from 'naive-ui' +import { SettingOutlined, BellOutlined, InfoCircleOutlined } from '@vicons/antd' + +import { useStore } from './useStore' +import { useController } from './useController' +import BaseComponent from '@components/BaseLayout' +import GeneralSettings from './components/GeneralSettings' +import NotificationSettings from './components/NotificationSettings' +import AboutSettings from './components/AboutSettings' + +/** + * 设置页面组件 + */ +export default defineComponent({ + name: 'Settings', + setup() { + const { activeTab, tabOptions } = useStore() + const { fetchAllSettings, isCutTab } = useController() + + // 渲染图标组件 + const renderIcon = (iconName: string) => { + const icons: Record = { + SettingOutlined: , + BellOutlined: , + InfoCircleOutlined: , + } + return {icons[iconName]} + } + + // 自定义Tab样式已移至全局reset.css + + onMounted(() => { + isCutTab() + fetchAllSettings() + }) + + return () => ( +
+
+ ( +
+ + + {tabOptions.value.map((tab) => ( + + {{ + tab: () => ( +
+ {renderIcon(tab.icon)} + {tab.title} +
+ ), + default: () => ( +
+ {/* 常用设置 */} + {activeTab.value === 'general' && } + + {/* 告警通知 */} + {activeTab.value === 'notification' && } + + {/* 关于我们 */} + {activeTab.value === 'about' && } +
+ ), + }} +
+ ))} +
+
+
+ ), + }} + /> +
+
+ ) + }, +}) diff --git a/frontend/apps/allin-ssl/src/views/settings/useController.tsx b/frontend/apps/allin-ssl/src/views/settings/useController.tsx new file mode 100644 index 0000000..2cef76e --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/settings/useController.tsx @@ -0,0 +1,423 @@ +import { FormRules } from 'naive-ui' +import md5 from 'crypto-js/md5' +import { useFormHooks, useModal, useDialog, useForm, useMessage, useLoadingMask } from '@baota/naive-ui/hooks' +import { clearCookie, clearSession } from '@baota/utils/browser' +import { useError } from '@baota/hooks/error' +import { $t } from '@locales/index' +import { useStore } from './useStore' + +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 type { ReportMail, SaveSettingParams, ReportType } from '@/types/setting' + +const { + // 标签页 + activeTab, + tabOptions, + // 常用设置 + generalSettings, + channelTypes, + aboutInfo, + fetchGeneralSettings, + saveGeneralSettings, + // 通知设置 + fetchNotifyChannels, + notifyChannels, + updateReportChannel, + testReportChannel, + deleteReportChannel, +} = useStore() +const message = useMessage() +const { handleError } = useError() +const { useFormInput, useFormInputNumber, useFormSwitch, useFormTextarea } = useFormHooks() + +/** + * 设置页面业务逻辑控制器 + * @function useController + * @description 提供设置页面所需的全部业务逻辑和状态数据,负责协调不同设置组件的交互行为 + * @returns {Object} 返回设置页面相关的状态数据和处理方法 + */ +export const useController = () => { + const route = useRoute() + const router = useRouter() + + /** + * @description 检测是否需要添加工作流 + */ + const isCutTab = () => { + const { tab } = route.query + if (tab?.includes('notification')) { + activeTab.value = 'notification' + router.push({ query: {} }) + } + } + /** + * 一次性加载所有设置数据 + * @async + * @function fetchAllSettings + * @description 页面初始化时调用,并行加载系统设置、通知设置和通知渠道数据 + * @returns {Promise} 无返回值 + */ + const fetchAllSettings = async () => { + try { + await Promise.all([fetchGeneralSettings(), fetchNotifyChannels()]) + } catch (error) { + handleError(error) + } + } + + /** + * @description md5 密码加密 + * @param {string} password - 原始密码 + * @returns {string} 加密后的密码 + */ + const encryptPassword = (password: string): string => { + return md5(`${password}_bt_all_in_ssl`).toString() + } + + /** + * 保存常用设置 + * @async + * @function handleSaveGeneralSettings + * @description 验证并保存常用设置表单数据 + * @param {SaveSettingParams} params - 保存设置请求参数 + * @param {Ref} formRef - 表单实例引用,用于验证表单数据 + * @returns {Promise} 无返回值 + */ + const handleSaveGeneralSettings = async (params: SaveSettingParams) => { + try { + await saveGeneralSettings({ + ...params, + password: params.password !== '' ? encryptPassword(params.password) : '', + }) + setTimeout(() => { + clearCookie() + clearSession() + window.location.href = `${params.secure}` + }, 2000) + } catch (error) { + handleError(error) + } + } + + /** + * 打开添加邮箱通知渠道弹窗 + * @function openAddEmailChannelModal + * @description 打开添加邮箱通知渠道的模态框,并在关闭后刷新通知渠道列表 + * @returns {void} 无返回值 + */ + const openAddEmailChannelModal = (limit: number = 1) => { + if (limit >= 1) { + message.warning($t('t_16_1746773356568')) + return + } + useModal({ + title: $t('t_18_1745457490931'), + area: 650, + component: EmailChannelModel, + footer: true, + }) + } + + /** + * 打开添加飞书通知渠道弹窗 + * @function openAddFeishuChannelModal + * @description 打开添加飞书通知渠道的模态框,并在关闭后刷新通知渠道列表 + * @returns {void} 无返回值 + */ + const openAddFeishuChannelModal = (limit: number = 1) => { + if (limit >= 1) { + message.warning($t('t_16_1746773356568')) + return + } + useModal({ + title: $t('t_9_1746676857164'), + area: 650, + component: FeishuChannelModel, + footer: true, + }) + } + + /** + * 打开添加Webhook通知渠道弹窗 + * @function openAddWebhookChannelModal + * @description 打开添加Webhook通知渠道的模态框,并在关闭后刷新通知渠道列表 + * @returns {void} 无返回值 + */ + const openAddWebhookChannelModal = (limit: number = 1) => { + if (limit >= 1) { + message.warning($t('t_16_1746773356568')) + return + } + useModal({ + title: $t('t_11_1746676859158'), + area: 650, + component: WebhookChannelModel, + footer: true, + }) + } + + /** + * 打开添加钉钉通知渠道弹窗 + * @function openAddDingtalkChannelModal + * @description 打开添加钉钉通知渠道的模态框,并在关闭后刷新通知渠道列表 + * @returns {void} 无返回值 + */ + const openAddDingtalkChannelModal = (limit: number = 1) => { + if (limit >= 1) { + message.warning($t('t_16_1746773356568')) + return + } + useModal({ + title: '添加钉钉通知', + area: 650, + component: DingtalkChannelModel, + footer: true, + }) + } + + // 处理启用状态切换 + const handleEnableChange = async (item: ReportType) => { + useDialog({ + title: $t('t_17_1746773351220', [ + Number(item.config.enabled) ? $t('t_5_1745215914671') : $t('t_6_1745215914104'), + ]), + content: $t('t_18_1746773355467', [ + Number(item.config.enabled) ? $t('t_5_1745215914671') : $t('t_6_1745215914104'), + ]), + onPositiveClick: async () => { + try { + await updateReportChannel({ + id: Number(item.id), + name: item.name, + type: item.type, + config: JSON.stringify(item.config), + }) + await fetchNotifyChannels() + } catch (error) { + handleError(error) + } + }, + // 取消后刷新通知渠道列表 + onNegativeClick: () => { + fetchNotifyChannels() + }, + onClose: () => { + fetchNotifyChannels() + }, + }) + } + + /** + * 查看通知渠道配置 + * @function viewChannelConfig + * @description 显示特定通知渠道的详细配置信息 + * @param {ReportType} item - 要查看的通知渠道对象 + * @returns {void} 无返回值 + */ + const editChannelConfig = (item: ReportType) => { + console.log(item) + if (item.type === 'mail') { + useModal({ + title: $t('t_0_1745895057404'), + area: 650, + component: EmailChannelModel, + componentProps: { + data: item, + }, + footer: true, + onClose: () => fetchNotifyChannels(), + }) + } else if (item.type === 'feishu') { + useModal({ + title: $t('t_9_1746676857164'), + area: 650, + component: FeishuChannelModel, + componentProps: { + data: item, + }, + footer: true, + onClose: () => fetchNotifyChannels(), + }) + } else if (item.type === 'webhook') { + useModal({ + title: $t('t_11_1746676859158'), + area: 650, + component: WebhookChannelModel, + componentProps: { + data: item, + }, + footer: true, + onClose: () => fetchNotifyChannels(), + }) + } else if (item.type === 'dingtalk') { + useModal({ + title: '编辑钉钉通知', + area: 650, + component: DingtalkChannelModel, + componentProps: { + data: item, + }, + footer: true, + onClose: () => fetchNotifyChannels(), + }) + } + } + + /** + * 测试通知渠道配置 + * @function testChannelConfig + * @description 测试通知渠道配置 + * @param {ReportType} item - 要测试的通知渠道对象 + * @returns {void} 无返回值 + */ + const testChannelConfig = (item: ReportType) => { + if (item.type !== 'mail' && item.type !== 'feishu' && item.type !== 'webhook') { + message.warning($t('t_19_1746773352558')) + return + } + const { open, close } = useLoadingMask({ text: $t('t_20_1746773356060') }) + + useDialog({ + title: $t('t_21_1746773350759'), + content: $t('t_22_1746773360711'), + onPositiveClick: async () => { + try { + open() + await testReportChannel({ id: item.id }) + } catch (error) { + handleError(error) + } finally { + close() + } + }, + }) + } + + /** + * 删除通知渠道 + * @function confirmDeleteChannel + * @description 确认并删除指定的通知渠道 + * @param {ReportType} item - 要删除的通知渠道对象 + * @returns {void} 无返回值 + */ + const confirmDeleteChannel = (item: ReportType) => { + useDialog({ + title: $t('t_23_1746773350040'), + content: $t('t_0_1746773763967', [item.name]), + onPositiveClick: async () => { + try { + await deleteReportChannel({ id: item.id }) + await fetchNotifyChannels() + } catch (error) { + handleError(error) + } + }, + }) + } + + return { + activeTab, + isCutTab, + tabOptions, + generalSettings, + notifyChannels, + channelTypes, + aboutInfo, + fetchAllSettings, + handleSaveGeneralSettings, + openAddEmailChannelModal, + openAddFeishuChannelModal, + openAddWebhookChannelModal, + openAddDingtalkChannelModal, + handleEnableChange, + editChannelConfig, + testChannelConfig, + confirmDeleteChannel, + } +} + +/** + * 常用设置表单控制器 + * @function useGeneralSettingsController + * @description 提供常用设置表单的配置、规则和组件 + * @returns {object} 返回表单相关配置、规则和组件 + */ +export const useGeneralSettingsController = () => { + /** + * 表单验证规则 + * @type {FormRules} + */ + const rules: FormRules = { + timeout: { + required: true, + type: 'number', + trigger: ['input', 'blur'], + message: '请输入超时时间', + }, + secure: { + required: true, + trigger: ['input', 'blur'], + message: '请输入安全入口', + }, + username: { + required: true, + trigger: ['input', 'blur'], + message: '请输入管理员账号', + }, + password: { + trigger: ['input', 'blur'], + message: '请输入管理员密码', + }, + cert: { + required: true, + trigger: 'input', + message: '请输入SSL证书', + }, + key: { + required: true, + trigger: 'input', + message: '请输入SSL密钥', + }, + } + + /** + * 表单配置 + * @type {ComputedRef} + * @description 动态生成表单项配置,根据SSL启用状态显示或隐藏SSL相关字段 + */ + const config = computed(() => { + const options = [ + useFormInputNumber('超时时间 (秒)', 'timeout', { class: 'w-full' }), + useFormInput('安全入口', 'secure'), + useFormInput('管理员账号', 'username'), + useFormInput('管理员密码', 'password', { type: 'password', showPasswordOn: 'click' }), + useFormSwitch('启用SSL', 'https', { + checkedValue: '1', + uncheckedValue: '0', + }), + ] + if (Number(generalSettings.value.https) === 1) { + options.push(useFormTextarea('SSL证书', 'cert', { rows: 3 }), useFormTextarea('SSL密钥', 'key', { rows: 3 })) + } + return options + }) + + /** + * 创建表单组件 + * @type {Object} + */ + const { component: GeneralForm } = useForm({ + config, + defaultValue: generalSettings, + rules, + }) + + return { + GeneralForm, + config, + rules, + } +} diff --git a/frontend/apps/allin-ssl/src/views/settings/useStore.tsx b/frontend/apps/allin-ssl/src/views/settings/useStore.tsx new file mode 100644 index 0000000..9c91836 --- /dev/null +++ b/frontend/apps/allin-ssl/src/views/settings/useStore.tsx @@ -0,0 +1,275 @@ +import { useError } from '@baota/hooks/error' +import { $t } from '@locales/index' +import { + getSystemSetting, + saveSystemSetting, + getReportList, + updateReport, + deleteReport, + addReport, + testReport, +} from '@/api/setting' +import type { + SaveSettingParams, + SystemSetting, + ReportType, + GetReportListParams, + AddReportParams, + UpdateReportParams, + DeleteReportParams, + ReportMail, + TestReportParams, + ReportFeishu, + ReportWebhook, + ReportDingtalk, +} from '@/types/setting' + +const { handleError } = useError() +/** + * 设置页面状态 Store + * @description 用于管理设置相关的状态和操作 + */ +export const useSettingsStore = defineStore('settings-store', () => { + // -------------------- 状态定义 -------------------- + // 当前激活的标签页 + const activeTab = ref<'general' | 'notification' | 'about'>('general') + + // 标签页选项 + const tabOptions = ref([ + { key: 'general', title: '常用设置', icon: 'SettingOutlined' }, + { key: 'notification', title: '告警通知', icon: 'BellOutlined' }, + { key: 'about', title: '关于我们', icon: 'InfoCircleOutlined' }, + ]) + + const generalSettings = ref({ + timeout: 30, + secure: '', + username: 'admin', + password: '', + https: 0, + key: '', + cert: '', + }) + // // 通知设置表单数据 + // const notificationSettings = ref({ + // title: '【AllIn SSL】系统告警通知', // 通知标题 + // content: '尊敬的用户,您的系统出现了以下警告:{{content}},请及时处理。', // 通知内容模板 + // }) + + // 通知渠道列表 + const notifyChannels = ref[]>([]) + + // 通知渠道类型 + const channelTypes = ref>({ + mail: $t('t_68_1745289354676'), + dingtalk: $t('t_32_1746773348993'), + wecom: $t('t_33_1746773350932'), + feishu: $t('t_34_1746773350153'), + webhook: 'WebHook', + }) + + // 邮箱通知渠道表单 + const emailChannelForm = ref({ + name: '', + enabled: '1', + receiver: '', // 接受邮箱 + sender: '', // 发送邮箱 + smtpHost: '', // SMTP服务器 + smtpPort: '465', //SMTP端口 + smtpTLS: false, // TLS协议,加密 + password: '', + }) + + // 飞书通知渠道表单 + const feishuChannelForm = ref({ + name: '', + enabled: '1', + webhook: '', // 飞书webhook地址 + secret: '', // 飞书webhook加密密钥(可选) + }) + + // Webhook通知渠道表单 + const webhookChannelForm = ref({ + name: '', + enabled: '1', + url: '', // WebHook回调地址 + data: '', // WebHook推送通知回调数据(可选) + method: 'post', // 请求方式 + headers: '', // WebHook请求头(可选) + ignore_ssl: false, // 忽略SSL/TLS证书错误 + }) + + // 钉钉通知渠道表单 + const dingtalkChannelForm = ref({ + name: '', + enabled: '1', + webhook: '', // 钉钉webhook地址 + secret: '', // 钉钉webhook加密密钥(可选) + }) + + // 关于页面数据 + const aboutInfo = ref({ + version: '1.0.0', + hasUpdate: false, + latestVersion: '', + updateLog: '', + qrcode: { + service: 'https://example.com/service_qr.png', + wechat: 'https://example.com/wechat_qr.png', + }, + description: $t('t_0_1747904536291'), + }) + + // -------------------- 工具方法 -------------------- + /** + * 获取系统设置 + */ + const fetchGeneralSettings = async () => { + try { + const { data } = await getSystemSetting().fetch() + generalSettings.value = { ...generalSettings.value, ...(data || {}) } + } catch (error) { + handleError(error).default($t('t_0_1745464080226')) + } + } + /** + * 保存系统设置 + */ + const saveGeneralSettings = async (params: SaveSettingParams) => { + try { + const { fetch, message } = saveSystemSetting(params) + message.value = true + await fetch() + } catch (error) { + handleError(error).default($t('t_1_1745464079590')) + } + } + + /** + * 获取通知渠道列表 + */ + const fetchNotifyChannels = async (params: GetReportListParams = { p: 1, search: '', limit: 1000 }) => { + try { + const { data } = await getReportList(params).fetch() + notifyChannels.value = (data || []).map(({ config, ...otherwise }) => { + console.log(config) + return { + config: JSON.parse(config), + ...otherwise, + } + }) + + console.log(notifyChannels.value) + } catch (error) { + notifyChannels.value = [] + handleError(error).default($t('t_4_1745464075382')) + } + } + + /** + * 添加通知渠道 + */ + const addReportChannel = async (params: AddReportParams) => { + try { + const { fetch, message } = addReport(params) + message.value = true + await fetch() + } catch (error) { + handleError(error).default($t('t_5_1745464086047')) + } + } + + /** + * 更新通知渠道 + */ + const updateReportChannel = async (params: UpdateReportParams) => { + try { + const { fetch, message } = updateReport(params) + message.value = true + await fetch() + } catch (error) { + handleError(error).default($t('t_6_1745464075714')) + } + } + + /** + * 测试通知渠道 + */ + const testReportChannel = async (params: TestReportParams) => { + try { + const { fetch, message } = testReport(params) + message.value = true + await fetch() + } catch (error) { + handleError(error).default($t('t_0_1746676862189')) + } + } + /** + * 删除通知渠道 + */ + const deleteReportChannel = async ({ id }: DeleteReportParams) => { + try { + const { fetch, message } = deleteReport({ id }) + message.value = true + await fetch() + await fetchNotifyChannels() + } catch (error) { + handleError(error).default($t('t_7_1745464073330')) + } + } + + // /** + // * 检查版本更新 + // */ + // const checkUpdate = async () => { + // try { + // const res = await systemUpdate().fetch() + // // 实际应用中可能需要修改API或类型定义 + // aboutInfo.value = { + // ...aboutInfo.value, + // ...(res.data || { + // hasUpdate: false, + // latestVersion: '--', + // updateLog: '--', + // }), + // } + // } catch (error) { + // handleError(error).default($t('t_8_1745464081472')) + // return null + // } + // } + + return { + // 状态 + activeTab, + tabOptions, + generalSettings, + notifyChannels, + channelTypes, + emailChannelForm, + feishuChannelForm, + webhookChannelForm, + dingtalkChannelForm, + aboutInfo, + + // 方法 + fetchGeneralSettings, + saveGeneralSettings, + + fetchNotifyChannels, + addReportChannel, + updateReportChannel, + deleteReportChannel, + testReportChannel, + } +}) + +/** + * 组合式 API 使用 Store + * @description 提供对设置页面 Store 的访问,并返回响应式引用 + * @returns {Object} 包含所有 store 状态和方法的对象 + */ +export const useStore = () => { + const store = useSettingsStore() + return { ...store, ...storeToRefs(store) } +} diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml new file mode 100644 index 0000000..13b6338 --- /dev/null +++ b/frontend/pnpm-workspace.yaml @@ -0,0 +1,8 @@ +packages: + - "apps/*" # 应用 + - "packages/*" # 包 + - "packages/vue/*" # 包 + - "packages/svelte/*" # 包 + - "packages/react/*" # 包 + - "environment/*" # 环境 + - "plugin/*" # 插件