【初始化】前端工程项目

pull/79/head^2
chudong 2025-05-09 15:11:21 +08:00
parent c012704c9a
commit d7c556c3b0
524 changed files with 55595 additions and 112 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
backend/.DS_Store vendored

Binary file not shown.

BIN
frontend/.DS_Store vendored

Binary file not shown.

169
frontend/README.md Normal file
View File

@ -0,0 +1,169 @@
# ALLinSSL 前端项目
## 项目简介
ALLinSSL是一个基于Monorepo架构的前端项目使用Turborepo进行工作区管理专注于提供SSL证书的申请、管理、部署和监控等功能。项目采用模块化设计将通用功能抽象为独立包提高代码复用性和可维护性。
## 技术栈
- **框架**Vue 3.5.x
- **开发语言**TypeScript
- **包管理工具**pnpm 9.0.0
- **Monorepo方案**Turborepo
- **构建工具**Vite 6.x
- **状态管理**Pinia 2.x
- **路由**Vue Router 4.x
- **UI组件库**Naive UI 2.x
- **CSS框架**TailwindCSS 3.x
- **国际化**Vue I18n 11.x
- **测试工具**Vitest
## 项目结构
```
frontend/
├── apps/ # 应用目录
│ └── allin-ssl/ # SSL证书管理应用
├── packages/ # 共享包目录
│ ├── hooks/ # 可复用的Vue Hooks
│ ├── i18n/ # 国际化相关功能
│ ├── naive-ui/ # UI组件库封装
│ ├── pinia/ # 状态管理相关功能
│ ├── router/ # 路由相关功能
│ ├── utils/ # 通用工具函数
│ └── vite/ # Vite相关配置和插件
├── environment/ # 环境配置
│ ├── eslint/ # ESLint配置
│ ├── prettier/ # Prettier配置
│ ├── stylelint/ # Stylelint配置
│ └── typescript/ # TypeScript配置
├── plugin/ # 项目插件
│ ├── plugin-i18n/ # 国际化插件
│ ├── project-ftp-sync/ # FTP同步插件
│ └── project-sync-git/ # Git同步插件
├── scripts/ # 脚本目录
├── types/ # 全局类型定义
└── turbo.json # Turborepo配置
```
## 应用
### allin-ssl
SSL证书管理平台提供证书申请、管理、部署、监控等功能帮助用户轻松管理和部署SSL证书保障网站安全。
## 共享包
- **hooks**: 提供常用的Vue Hooks简化组件逻辑
- **i18n**: 国际化解决方案,支持多语言切换
- **naive-ui**: 封装和扩展Naive UI组件库
- **pinia**: 封装Pinia状态管理相关功能
- **router**: 封装路由相关功能
- **utils**: 提供常用工具函数和助手方法
- **vite**: Vite相关配置和插件
## 环境与规范
- **eslint**: 代码质量检查配置
- **prettier**: 代码格式化配置
- **stylelint**: 样式质量检查配置
- **typescript**: TypeScript配置
## 插件
- **plugin-i18n**: 国际化插件,支持自动提取和生成国际化资源
- **project-ftp-sync**: FTP同步插件用于部署前端资源
- **project-sync-git**: Git同步插件用于代码同步
## 安装
### 环境要求
- Node.js >= 18.x
- pnpm >= 9.0.0
### 安装依赖
```bash
# 安装所有依赖
pnpm install
```
## 开发命令
```bash
# 启动所有应用的开发服务
pnpm dev
# 只启动ALLinSSL应用
pnpm dev --filter allin-ssl
# 构建所有应用
pnpm build
# 只构建ALLinSSL应用
pnpm build --filter allin-ssl
# 运行代码检查
pnpm lint
# 运行类型检查
pnpm check-types
# 运行测试
pnpm test
# 清理构建缓存
pnpm clear
```
## 开发指南
### 新建应用
1. 在 `apps` 目录下创建新的应用目录
2. 初始化应用配置文件和依赖
3. 在应用的 `package.json` 中添加对共享包的依赖
### 使用共享包
在应用或其他包的 `package.json` 中添加依赖,例如:
```json
"dependencies": {
"@baota/utils": "workspace:*",
"@baota/hooks": "workspace:*"
}
```
### 开发规范
1. 遵循项目的代码风格和组织方式
2. 共享功能应提取到 `packages` 目录下的相应包中
3. 应用特定功能应保留在各自的应用目录中
4. 使用TypeScript进行类型定义
5. 编写测试用例确保功能正确性
## 部署
### 构建生产版本
```bash
pnpm build
```
### FTP部署
使用内置的FTP同步插件进行部署
```bash
pnpm build
# 自动部署到配置的FTP服务器
```
## 浏览器兼容性
- Chrome >= 60
- Firefox >= 55
- Safari >= 11
- Edge >= 79

BIN
frontend/apps/.DS_Store vendored Normal file

Binary file not shown.

BIN
frontend/apps/allin-ssl/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,117 @@
# ALLinSSL
## 项目简介
ALLinSSL是一个SSL证书管理平台提供证书申请、管理、部署、监控等功能帮助用户轻松管理和部署SSL证书保障网站安全。
## 技术栈
- 框架Vue 3.5.x
- 开发语言TypeScript
- 构建工具Vite 6.x
- 状态管理Pinia 2.x
- 路由Vue Router 4.x
- UI组件库Naive UI 2.x
- CSS框架TailwindCSS 3.x
- 国际化Vue I18n 11.x
- HTTP客户端Axios
- 工具库VueUse、UUID、CryptoJS等
## 项目结构
```
src/
├── api/ # API接口定义
├── assets/ # 静态资源
├── components/ # 公共组件
├── config/ # 全局配置
├── locales/ # 国际化资源
├── router/ # 路由配置
├── styles/ # 全局样式
├── types/ # 类型定义
├── views/ # 页面组件
│ ├── autoDeploy/ # 自动部署
│ ├── authApiManage/ # API管理
│ ├── certApply/ # 证书申请
│ ├── certManage/ # 证书管理
│ ├── home/ # 首页
│ ├── layout/ # 布局组件
│ ├── login/ # 登录页面
│ ├── monitor/ # 监控页面
│ └── settings/ # 设置页面
├── App.tsx # 应用根组件
└── main.ts # 入口文件
```
## 功能模块
- **证书管理**集中管理所有SSL证书包括查看、更新、删除等操作
- **证书申请**:提供证书申请流程
- **自动部署**自动部署SSL证书到目标服务器
- **监控系统**:监控证书状态,到期提醒等
- **系统设置**:系统全局配置管理
## 安装与运行
### 环境要求
- Node.js 16.x 或更高版本
- pnpm 7.x 或更高版本(推荐使用)
### 安装依赖
```bash
pnpm install
```
### 开发模式
```bash
pnpm dev
```
### 构建生产版本
```bash
pnpm build
```
### 预览构建结果
```bash
pnpm preview
```
### 运行测试
```bash
pnpm test
```
### 代码检查
```bash
pnpm lint
```
## 开发指南
本项目采用模块化和组件化开发方式,开发新功能时请遵循以下原则:
1. 遵循项目已有的代码风格和组织方式
2. 新增API请在api目录下对应文件中添加
3. 公共组件放在components目录
4. 页面组件放在views目录下对应模块文件夹中
5. 路由配置自动导入
6. 使用CSS变量和TailwindCSS进行样式管理
## 浏览器兼容性
- Chrome >= 60
- Firefox >= 55
- Safari >= 11
- Edge >= 79
## 许可证
私有项目,未经授权不得使用

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
import vueConfig from '@baota/eslint/vue'
import baseConfig from '@baota/eslint'
/** @type {import("eslint").Linter.Config[]} */
const config = [
// Vue 相关配置,包含 TypeScript 支持
...vueConfig,
// 基础配置,用于通用的 JavaScript/TypeScript 规则
...baseConfig,
// 项目特定的配置覆盖
{
files: ['**/*.{js,ts,tsx,jsx,vue}'],
rules: {
// 在此处添加项目特定的规则覆盖
'vue/multi-word-component-names': 'off', // 关闭组件名称必须由多个单词组成的规则
},
},
]
export default config

View File

@ -2,13 +2,12 @@
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="./favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ALLinSSL</title> <title>ALLinSSL</title>
<script type="module" crossorigin src="./static/js/main-D8Z-8iPZ.js"></script>
<link rel="stylesheet" crossorigin href="./static/css/style-CZclU6lr.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,28 @@
分别生成以下相关的类型文件,在 types/ 目录下,以及 api 文件,在 api/目录下
文件名称如下:
home
autoDeploy
certManage
certApply
authAPIManage
monitor
settings
1、types文件命名方式{视图名称}.d.ts
2、api文件命名方式{视图名称}.ts
3、api文件结构如下
```typescript
import { useApi } from './index'
import type { loginParams, loginResponse } from '@/types/public'
/**
* 登录
* @param params 登录参数
* @returns 登录
*/
export const loginCloudControl = (params?: loginParams) => useApi<loginResponse, loginParams>('/v1/user/login', params)
```

View File

@ -0,0 +1,273 @@
目前接口返回格式
```ts
export interface AxiosResponseData<T = unknown> {
code: number
count: number
data: T
message: string
status: boolean
}
```
后端请求的协议方式formdata 还是 json
登录的方式使用那种方案jwt以及加密方式
接口文档需要包含一下内容
接口类型、接口返回值、以及其他需要处理的内容需要说清楚,例如登录失效,以及状态的判断
基础要求
1、基于vue3 的 tsx文件编写并将业务和数据、视图分离成 mvc 模式,如果代码中没有包含业务和数据,可以移除该文件,确保结构单一和可维护性。
2、视图文件文件入口为 index.tsx
3、业务代码文件入口为 useController.tsx
4、数据代码文件入口为 useStore.tsx
5、使用hooks编写如果存在表单、表格使用配置生成。参考如下
视图文件内容要求
1、所有的视图style使用css module 编写基于tailwind 的@apply 实现如果行内元素少可以省略创建css module。
2、如果视图使用naiveui tsx 开发内容,如果没有相应的组件,则使用原生样式实现
业务代码内容要求
1、包含 hooks 配置的生成方法,需要导出
2、事件逻辑处理、dom 操作、生命周期、监听函数等,
3、数据代码转发支持将 sotre 数据直接转发
数据代码内容要求
1、包含所有的数据内容例如请求方法、表单数据处理
2、响应式数据存储、持久化数据存储
国际化内容要求
1、将文件中的中文文本未使用 $t 包裹的内容,使用 $t('中文内容') 包裹起来,如果存在则引入 $timport { $t } from "@locales/index"
格式要求
1、注释采用jsdoc格式使用中文作为注释语言
2、注释内容清晰对同类的函数位置整理和功能使用注释分割线分
3、方法名称规范化根据类型和作用定义。
基础视图参考:/Users/chudong/project/tools-monorepo/apps/allin-ssl/src/components/baseComponent/index.tsx
首页模块设计
1、界面设计
包含三个模块,采用上下结构
* 概览模块包含工作流、证书、监控模块,每个模块,都具备所有数量、启用/到期/异常数量、失败/到期数量,以及对应产品图标
* 最近工作流执行列表,包含字段:
*   名称
*   执行状态
*   执行时间
* 快捷入口模块,提供常用的三个模块入口,工作流构建、申请证书、添加监控
2、接口设计包含
* 概览数据接口
* 最近工作流列表
工作流列表模块设计
1、界面设计
采用基础视图组件设计该页面,传入相关内容,
* 添加按钮文本:新增工作流
* 搜索框提示内容:请输入工作流名称
* 表格包含字段:
* 工作流名称
* 触发方式(自动执行或手动可切换)
* 启用状态(启用/停用)
* 最后一次执行时间
* 创建时间
* 操作(包含日志、执行、编辑、删除)
* 添加工作流按钮触发弹窗内容
* 弹窗界面包含多个具备概览图的单选项
* 常用SSL部署工作流
* 自定义工作流
2、接口设计包含
* 工作流列表
* 获取指定工作流程数据
* 新建自定义工作流程数据
* 获取指定工作流执行日志
* 执行指定工作流
* 保存指定工作流程数据
* 删除指定工作流程数据
工作流设计模块设计
1、界面设计
采用流程图构建组建,设计页面,需要设计一下节点设计,所有节点采用
* 申请SSL表单设计
* 域名信息input、必填
* 邮箱信息input、非必填
* DNS 提供商select支持添加添加调用 API 授权页面的添加表单)
* 高级选项折叠
* 数字证书算法select支持字段RSA2048\RSA3072\RSA4096\RSA8192\EC256\EC384默认使用RSA2048\
* 指定DNS解析服务器input非必填提示申请证书过程中优先使用它
* DNS超时时间(input、非必填)
* DNS解析TTL时间(input、非必填)
* 关闭CNAME跟随switch默认关闭
* 关闭ARI续期switch默认关闭
* 续签间隔input当上次签发的证书距离到期时间超过xx天后跳过续签使用
* 部署SSL到指定位置表单设计需要手动开发使用 AI 生成消耗时间,而且基本上可用性低)
*
* 通知告警
* 通知主题input、必填
* 通知内容textarea、必填
* 选择通知渠道select右侧支持添加渠道
*
* 并行分支
* 触发并行添加节点的功能
* 执行结果分支只有节点是申请SSL、部署SSL到指定位置、通知告警节点后面才出现此分支提供选择
* 触发条件判断分支,仅事件触发的节点支持添加该节点
* 节点执行成功后
* 节点执行失败后
2、接口设计包含
* 获取API服务商列表
* 获取证书列表
* 获取网站列表(宝塔面板)
* 获取 CDN 加速域名(仅腾讯、阿里相关密钥获取)
* 获取 WAF 包含域名(仅腾讯、阿里相关密钥获取)
证书管理列表模块设计
1、界面设计
采用基础视图组件设计该页面,传入相关内容,
* 添加按钮文本:上传证书
* 搜索框提示内容:请输入证书名称/类型
* 表格包含字段:
* 域名
* 品牌
* 剩余天数
* 到期时间
* 来源
* 添加时间
* 操作(下载、删除)
* 上传证书按钮触发弹窗内容
* 弹窗界面包含表单(支持证书压缩上传,识别腾讯、阿里下的证书压缩包文件)
* 证书名称
* 证书内容 (PEM 格式)
* 私钥内容 (KEY 格式)
2、接口设计包含
* 获取证书列表
* 下载指定证书
* 上传证书信息
* 删除指定证书信息
申请证书模块模块设计
1、界面设计
采用 tab 页面切换,分为商业证书页面和免费证书页面
* 商业证书采用iframe 内嵌官网页面,官网页面需要新增一个界面作为兼容页面。
* 免费证书采用,卡片式设计界面,以便于后期支持其他渠道
* 证书申请表单
* 手动申请和自动部署的选项卡片(包含 icon 和文件描述)
* 手动申请,采用 DNS 验证自动认证的方式,参考面板的统一设计
* 自动化部署,直接调用工作流的界面。
2、接口设计包含
* 申请证书信息
* 证书验证接口
授权API管理列表模块设计
1、界面设计
采用基础视图组件设计该页面,传入相关内容,
* 添加按钮文本:上传授权 API 管理
* 搜索框提示内容请输入授权api 名称或类型
* 表格包含字段:
*   名称
*   授权API类型提供商如腾讯云宝塔面板
*   创建时间
*   操作(编辑、删除)
* 上传证书按钮触发弹窗内容
* 弹窗界面包含表单
* 授权 API 类型select支持类型 SSH、宝塔、1panel、腾讯、阿里
* 不同的类型显示不同的东西
* SSH
* 服务器IPinput
* SSH端口input默认填充22
* 用户名input默认填充root
* 验证类型radio-button
1. 密码
2. SSH密钥
1. SSH密钥口令
* 宝塔
* 地址input包含协议和端口
* api 密钥input
* 1panel
* 地址input包含协议和端口
* api 密钥input
* 阿里云
* AccessKeyIdinput
* AccessKeySecretinput
* 腾讯云
* SecretIdinput
* SecretKeyinput
2、接口设计包含
* 获取授权 api 列表
* 新建授权 api 信息
* 编辑指定授权 aai 信息
* 删除指定授权 api 信息
监控管理列表模块设计
1、界面设计
采用基础视图组件设计该页面,传入相关内容,
* 添加按钮文本:添加监控
* 搜索框提示内容:请输入监控名称和域名
* 表格包含字段:
*   名称
*   授权API类型提供商如腾讯云宝塔面板
*   创建时间
*   操作(编辑、删除)
* 上传证书按钮触发弹窗内容
* 弹窗界面包含表单
* 监控名称input
* 域名/IPinput
* 检查周期select5/10/15/30/60分钟
* 告警渠道(多选,右侧支持添加渠道)
2、接口设计包含
* 获取监控管理列表
* 设置指定监控启动状态
* 添加监控信息
* 编辑指定监控信息
* 删除指定监控信息
设置管理模块设置
1、界面设置
采用 tab 页面切换,分为三个界面常用设置、告警通知、更新版本,内容部分采用卡片设计
* 常用设置采用1:1 分栏设计,支持表单元素行显示),包含一下具体模块,采用统一保存的方式,保存按钮固定在顶部,避免设置过长的导致问题
* 超时时间input
* 管理员账号input
* 管理员密码input
* SSL 配置switch关闭提供粘贴 keypem 文本域输入,默认折叠隐藏)
* 告警通知(采用上下结构),包含全局通知模块和通知渠道配置两个模块
* 全局通知模块(表单)
* 通知主题input默认文字
* 通知内容textarea默认文字
* 通知渠道(卡片列表,参考面板的设计)
* 左侧图标名称(包含已配置数量,点击可查看)+右侧操作
* 关于我们
* 版本信息,支持更新版本
* 客服二维码
* 微信公众好
* 关于产品介绍
2、接口设计包含
* 获取基础设置信息
* 保存基础设置信息
* 获取告警通用模板配置
* 设置告警通用模板配置
* 获取告警渠道列表
* 设置告警渠道
* 获取更新信息
* 更新版本版本

View File

@ -0,0 +1,70 @@
流程/工作流图拆封设计
- 基础节点
- 初始化节点(不支持上传)
- 并行节点
- 执行结果节点(删除整个条件判断)
- 任务节点
- 申请节点(支持执行结果判断)
- 上传节点(不支持执行结果判断)
- 部署节点(支持执行结果判断)
- 通知节点(支持执行结果判断)
- 节点操作
- 重命名
- 删除
- 节点下一步配置
- 申请
- 上传
- 部署
- 通知
- 执行结果判断(上传节点不支持)
- 并行
- 节点辅助功能
- 拖拽
- 放大、缩小、还原
- 节点验证
- 验证任务节点
结构规划
- 状态存储(包含节点默认配置数据)
- 基础节点
- 任务节点(可以根据外部的机构自由的构建任务节点,主要有节点条件,节点操作方法)
- 节点渲染器
- 工具方法
- 入口文件
工作流图组件
├─ 状态存储
│ └─ 节点默认配置数据
├─ 基础节点
│ ├─ 初始化节点
│ ├─ 并行节点
│ └─ 执行结果节点
├─ 任务节点
│ ├─ 申请节点
│ ├─ 上传节点
│ ├─ 部署节点
│ └─ 通知节点
├─ 节点渲染器
│ └─ 渲染节点到界面
├─ 工具方法
│ ├─ 创建节点
│ ├─ 重命名节点
│ ├─ 删除节点
│ ├─ 配置节点下一步
│ ├─ 视图缩放
│ └─ 流程验证
└─ 入口文件
└─ 初始化工作流图组件

View File

@ -0,0 +1,95 @@
以下是当前项目的视图结构,参考/src/view/test 的目录结构和内容,生成下述的文件和内容
src/
├── views/
│ ├── home/
│ │ ├── index.tsx
│ │ ├── index.model.css
│ │ ├── useController.tsx
│ │ ├── useStore.tsx
│ ├── autoDeploy/
│ │ ├── index.tsx
│ │ ├── index.model.css
│ │ ├── useController.tsx
│ │ ├── useStore.tsx
│ │ └── children/
│ │ └── pipeDesign/
│ │ ├── index.tsx
│ │ ├── index.model.css
│ │ ├── useController.tsx
│ │ └── useStore.tsx
│ ├── certManage/
│ │ ├── index.tsx
│ │ ├── index.model.css
│ │ ├── useController.tsx
│ │ ├── useStore.tsx
│ │ └── components/
│ │ └── uploadCert/
│ │ ├── index.tsx
│ │ ├── index.model.css
│ │ ├── useController.tsx
│ │ └── useStore.tsx
│ ├── certApply/
│ │ ├── index.tsx
│ │ ├── index.model.css
│ │ ├── useController.tsx
│ │ ├── useStore.tsx
│ │ ├── components/
│ │ │ └── certApplyForm/
│ │ │ ├── index.tsx
│ │ │ ├── index.model.css
│ │ │ ├── useController.tsx
│ │ │ └── useStore.tsx
│ │ └── children/
│ │ ├── commCert/
│ │ │ ├── index.tsx
│ │ │ ├── index.model.css
│ │ │ ├── useController.tsx
│ │ │ └── useStore.tsx
│ │ └── freeSslCert/
│ │ ├── index.tsx
│ │ ├── index.model.css
│ │ ├── useController.tsx
│ │ └── useStore.tsx
│ ├── authApiManage/
│ │ ├── index.tsx
│ │ ├── index.model.css
│ │ ├── useController.tsx
│ │ ├── useStore.tsx
│ │ └── components/
│ │ └── addAuthApi/
│ │ ├── index.tsx
│ │ ├── index.model.css
│ │ ├── useController.tsx
│ │ └── useStore.tsx
│ ├── monitor/
│ │ ├── index.tsx
│ │ ├── index.model.css
│ │ ├── useController.tsx
│ │ ├── useStore.tsx
│ │ └── components/
│ │ └── addMonitor/
│ │ ├── index.tsx
│ │ ├── index.model.css
│ │ ├── useController.tsx
│ │ └── useStore.tsx
│ └── settings/
│ ├── index.tsx
│ ├── index.model.css
│ ├── useController.tsx
│ ├── useStore.tsx
│ └── components/
│ ├── commSettings/
│ │ ├── index.tsx
│ │ ├── index.model.css
│ │ ├── useController.tsx
│ │ └── useStore.tsx
│ ├── notifySettings/
│ │ ├── index.tsx
│ │ ├── index.model.css
│ │ ├── useController.tsx
│ │ └── useStore.tsx
│ └── aboutUs/
│ ├── index.tsx
│ ├── index.model.css
│ ├── useController.tsx
│ └── useStore.tsx

View File

@ -0,0 +1,96 @@
import Mock from 'mockjs'
import { listTemplate, messageTemplate } from './template'
// 授权列表
export const getAccessList = Mock.mock(/\/access\/get_list/, 'post', () => {
const list = []
for (let i = 0; i < 10; i++) {
list.push({
id: Mock.Random.id(),
name: `授权-${Mock.Random.ctitle(3, 5)}`,
type: Mock.Random.pick(['ssh', 'btpanel', '1panel', 'aliyun', 'tencent']),
status: Mock.Random.integer(0, 1),
created_at: Mock.Random.datetime('yyyy-MM-dd HH:mm:ss'),
})
}
return {
code: 0,
count: list.length,
data: {
list,
total: 18,
},
message: '获取成功',
status: true,
}
})
// 授权类型列表
export const getAccessTypes = Mock.mock(/\/access\/get_access_types/, 'post', () => ({
code: 0,
count: 3,
data: [
{ key: 'ssh', name: 'SSH验证' },
{ key: 'btpanel', name: '宝塔验证' },
{ key: '1panel', name: '1Panel验证' },
{ key: 'aliyun', name: '阿里云验证' },
{ key: 'tencentcloud', name: '腾讯云验证' },
],
message: '获取成功',
status: true,
}))
// 新增授权
export const addAccess = Mock.mock(/\/access\/add_access/, 'post', () => ({
code: 0,
count: 0,
data: {
id: Mock.Random.id(),
},
message: '添加成功',
status: true,
}))
// 修改授权
export const updateAccess = Mock.mock(/\/access\/upd_access/, 'post', () => ({
code: 0,
count: 0,
data: {
id: Mock.Random.id(),
},
message: '修改成功',
status: true,
}))
// 删除授权
export const deleteAccess = Mock.mock(/\/access\/del_access/, 'post', () => ({
code: 0,
count: 0,
data: null,
message: '删除成功',
status: true,
}))
// 获取工作流 dns 配置
export const getAccessAllList = Mock.mock(/\/access\/get_all/, 'post', () => {
const list: Array<{ id: string; name: string; type: string }> = []
for (let i = 0; i < 3; i++) {
const group = Mock.Random.pick([
{ name: '阿里云', type: 'aliyun' },
{ name: '腾讯云', type: 'tencentcloud' },
])
list.push({
id: Mock.Random.id(),
name: `${group.name} DNS 配置`,
type: group.type,
})
}
return {
code: 0,
count: list.length,
data: list,
message: '获取成功',
status: true,
}
})

View File

@ -0,0 +1,70 @@
import Mock from 'mockjs'
import { listTemplate, messageTemplate } from './template'
// 证书列表
export const getCertList = Mock.mock(/\/cert\/get_list/, 'post', () => {
const list = []
for (let i = 0; i < 15; i++) {
list.push({
id: Mock.Random.id(),
name: `${Mock.Random.domain()}证书`,
domain: Mock.Random.domain(),
expire_time: Mock.Random.datetime('yyyy-MM-dd'),
status: Mock.Random.integer(0, 2),
created_at: Mock.Random.datetime('yyyy-MM-dd HH:mm:ss'),
})
}
return {
code: 0,
count: list.length,
data: {
list,
total: 32,
},
message: '获取成功',
status: true,
}
})
// 申请证书
export const applyCert = Mock.mock(/\/cert\/apply_cert/, 'post', () => ({
code: 0,
count: 0,
data: {
id: Mock.Random.id(),
},
message: '申请成功',
status: true,
}))
// 上传证书
export const uploadCert = Mock.mock(/\/cert\/upload_cert/, 'post', () => ({
code: 0,
count: 0,
data: {
id: Mock.Random.id(),
},
message: '上传成功',
status: true,
}))
// 删除证书
export const deleteCert = Mock.mock(/\/cert\/del_cert/, 'post', () => ({
code: 0,
count: 0,
data: null,
message: '删除成功',
status: true,
}))
// 下载证书
export const downloadCert = Mock.mock(/\/cert\/download_cert/, 'get', () => {
// 二进制文件流模拟,实际上应该是从服务器获取的二进制数据
return {
code: 0,
count: 0,
data: 'certificate-file-content',
message: '下载成功',
status: true,
}
})

View File

@ -0,0 +1,9 @@
// 导出所有模块
export * from './userInfo' // 示例接口
export * from './overview'
export * from './workflow'
export * from './cert'
export * from './access'
export * from './siteMonitor'
export * from './setting'
export * from './login'

View File

@ -0,0 +1,14 @@
import Mock from 'mockjs'
import { listTemplate, messageTemplate } from './template'
// 登录
export const login = Mock.mock(/\/login\/login/, 'post', () => ({
code: 0,
count: 0,
data: {
token:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNjE0NjE5MDQ2LCJleHAiOjE2MTQ3MDU0NDZ9._QS2nQa2FRpqH7zJSnjYVBXCOp7-QR-zrXsHl6dTHaU',
},
message: '登录成功',
status: true,
}))

View File

@ -0,0 +1,58 @@
import Mock from 'mockjs'
import { listTemplate, messageTemplate } from './template'
// 获取首页概览
export const getOverviews = Mock.mock(/\/overview\/get_overviews/, 'post', () => ({
code: 0,
count: 0,
data: {
workfolw: {
count: Mock.Random.integer(150, 250),
active: Mock.Random.integer(120, 200),
failure: Mock.Random.integer(0, 10),
},
cert: {
count: Mock.Random.integer(30, 80),
will: Mock.Random.integer(5, 15),
end: Mock.Random.integer(0, 5),
},
site_monitor: {
count: Mock.Random.integer(80, 150),
exception: Mock.Random.integer(0, 8),
},
workflow_history: [
{
name: '服务A部署流水线',
state: Mock.Random.integer(-1, 1),
mode: Mock.Random.pick(['定时触发', '手动触发']),
exec_time: Mock.Random.datetime('yyyy-MM-dd HH:mm'),
},
{
name: '1panel 面板证书部署流水线',
state: Mock.Random.integer(-1, 1),
mode: Mock.Random.pick(['定时触发', '手动触发']),
exec_time: Mock.Random.datetime('yyyy-MM-dd HH:mm'),
},
{
name: '网站证书申请流水线',
state: Mock.Random.integer(-1, 1),
mode: Mock.Random.pick(['定时触发', '手动触发']),
exec_time: Mock.Random.datetime('yyyy-MM-dd HH:mm'),
},
{
name: '网站证书申请流水线',
state: Mock.Random.integer(-1, 1),
mode: Mock.Random.pick(['定时触发', '手动触发']),
exec_time: Mock.Random.datetime('yyyy-MM-dd HH:mm'),
},
{
name: '网站证书申请流水线',
state: Mock.Random.integer(-1, 1),
mode: Mock.Random.pick(['定时触发', '手动触发']),
exec_time: Mock.Random.datetime('yyyy-MM-dd HH:mm'),
},
],
},
message: '获取成功',
status: true,
}))

View File

@ -0,0 +1,87 @@
import Mock from 'mockjs'
import { listTemplate, messageTemplate } from './template'
// 获取系统设置
export const getSystemSetting = Mock.mock(/\/setting\/get_setting/, 'post', () => ({
code: 0,
count: 0,
data: {
auto_renew: true,
renew_days: 30,
notify_enable: true,
notify_days: 15,
},
message: '获取成功',
status: true,
}))
// 保存系统设置
export const saveSystemSetting = Mock.mock(/\/setting\/save_setting/, 'post', () => ({
code: 0,
count: 0,
data: null,
message: '保存成功',
status: true,
}))
// 获取告警类型列表
export const getReportTypes = Mock.mock(/\/setting\/get_report_types/, 'post', () => ({
code: 0,
count: 4,
data: [
{ key: 'email', name: '邮件通知' },
{ key: 'sms', name: '短信通知' },
{ key: 'webhook', name: 'Webhook' },
{ key: 'dingtalk', name: '钉钉通知' },
],
message: '获取成功',
status: true,
}))
// 配置告警
export const setReport = Mock.mock(/\/setting\/set_report/, 'post', () => ({
code: 0,
count: 0,
data: null,
message: '配置成功',
status: true,
}))
// 删除告警
export const deleteReport = Mock.mock(/\/setting\/del_report/, 'post', () => ({
code: 0,
count: 0,
data: null,
message: '删除成功',
status: true,
}))
// 获取证书过期通知模板
export const getCertEndNoticeTemplate = Mock.mock(/\/setting\/get_certend_notice_temp/, 'post', () => ({
code: 0,
count: 0,
data: {
title: '证书即将过期通知',
content: '您的证书 {{domain}} 将在 {{days}} 天后过期,请及时更新。',
},
message: '获取成功',
status: true,
}))
// 保存证书过期通知模板
export const saveCertEndNoticeTemplate = Mock.mock(/\/setting\/save_certend_notice_temp/, 'post', () => ({
code: 0,
count: 0,
data: null,
message: '保存成功',
status: true,
}))
// 系统更新
export const systemUpdate = Mock.mock(/\/setting\/update/, 'post', () => ({
code: 0,
count: 0,
data: null,
message: '更新成功',
status: true,
}))

View File

@ -0,0 +1,68 @@
import Mock from 'mockjs'
import { listTemplate, messageTemplate } from './template'
// 站点监控列表
export const getSiteMonitorList = Mock.mock(/\/siteMonitor\/get_list/, 'post', () => {
const list = []
for (let i = 0; i < 12; i++) {
list.push({
id: Mock.Random.id(),
name: `${Mock.Random.ctitle(2, 5)}网站监控`,
url: `https://${Mock.Random.domain()}/api/${Mock.Random.word(3, 8)}`,
type: Mock.Random.pick(['HTTP', 'HTTPS', 'TCP', 'PING']),
status: Mock.Random.integer(0, 1),
check_result: Mock.Random.pick(['连接成功', '超时', '证书有效', '证书已过期']),
created_at: Mock.Random.datetime('yyyy-MM-dd HH:mm:ss'),
})
}
return {
code: 0,
count: list.length,
data: {
list,
total: 25,
},
message: '获取成功',
status: true,
}
})
// 新增站点监控
export const addSiteMonitor = Mock.mock(/\/siteMonitor\/add_site_monitor/, 'post', () => ({
code: 0,
count: 0,
data: {
id: Mock.Random.id(),
},
message: '添加成功',
status: true,
}))
// 修改站点监控
export const updateSiteMonitor = Mock.mock(/\/siteMonitor\/upd_site_monitor/, 'post', () => ({
code: 0,
count: 0,
data: {
id: Mock.Random.id(),
},
message: '修改成功',
status: true,
}))
// 删除站点监控
export const deleteSiteMonitor = Mock.mock(/\/siteMonitor\/del_site_monitor/, 'post', () => ({
code: 0,
count: 0,
data: null,
message: '删除成功',
status: true,
}))
// 启用/禁用站点监控
export const setSiteMonitor = Mock.mock(/\/siteMonitor\/set_site_monitor/, 'post', () => ({
code: 0,
count: 0,
data: null,
message: '设置成功',
status: true,
}))

View File

@ -0,0 +1,24 @@
/**
* @description
* @param {string} msg
* @returns
*/
export const listTemplate = <T>(data: T, count: number) => ({
code: 200, // 状态码
count, // 总数仅data 为数组时有效
data, // 数据
message: '', // 消息
status: true, // 消息状态true 为成功false 为失败
})
/**
* @description
* @param {object} data
* @returns
*/
export const messageTemplate = (message: string, status: boolean) => ({
code: 200, // 状态码
count: 0, // 总数仅data 为数组时有效
message, // 消息
status, // 消息状态true 为成功false 为失败
})

View File

@ -0,0 +1,26 @@
import Mock from 'mockjs'
import { listTemplate, messageTemplate } from './template'
// 用户列表 (案例接口)
export const listUser = Mock.mock(
/\/api\/user\/list/,
'post',
listTemplate(
{
id: '@id',
name: '@cname',
avatar: '@image',
'age|18-60': 18,
'gender|1': ['男', '女'],
phone: /^1[385][1-9]\d{8}/,
email: '@EMAIL',
address: '@county(true)',
'role|1': ['admin', 'user'],
},
100,
),
)
// 用户详情 (案例接口)
export const addUser = Mock.mock(/\/api\/user\/add/, 'post', messageTemplate('添加成功', true))

View File

@ -0,0 +1,90 @@
import Mock from 'mockjs'
import { listTemplate, messageTemplate } from './template'
// 工作流列表
export const getWorkflowList = Mock.mock(/\/workflow\/get_list/, 'post', () => {
const list = []
for (let i = 0; i < 10; i++) {
list.push({
id: Mock.Random.id(),
name: `${Mock.Random.ctitle(3, 8)}部署流水线`,
type: Mock.Random.pick(['auto', 'manual']),
status: Mock.Random.integer(0, 1),
created_at: Mock.Random.datetime('yyyy-MM-dd HH:mm:ss'),
updated_at: Mock.Random.datetime('yyyy-MM-dd HH:mm:ss'),
})
}
return {
code: 0,
count: list.length,
data: {
list,
total: 28,
},
message: '获取成功',
status: true,
}
})
// 新增工作流
export const addWorkflow = Mock.mock(/\/workflow\/add_workflow/, 'post', () => ({
code: 0,
count: 0,
data: {
id: Mock.Random.id(),
},
message: '添加成功',
status: true,
}))
// 修改工作流
export const updateWorkflow = Mock.mock(/\/workflow\/upd_workflow/, 'post', () => ({
code: 0,
count: 0,
data: {
id: Mock.Random.id(),
},
message: '修改成功',
status: true,
}))
// 删除工作流
export const deleteWorkflow = Mock.mock(/\/workflow\/del_workflow/, 'post', () => ({
code: 0,
count: 0,
data: null,
message: '删除成功',
status: true,
}))
// 获取工作流执行历史
export const getWorkflowHistory = Mock.mock(/\/workflow\/get_workflow_history/, 'post', () => {
const list = []
for (let i = 0; i < 10; i++) {
list.push({
id: Mock.Random.id(),
workflow_id: Mock.Random.id(),
workflow_name: `${Mock.Random.ctitle(3, 8)}部署流水线`,
state: Mock.Random.integer(-1, 1),
mode: Mock.Random.pick(['定时触发', '手动触发']),
exec_time: Mock.Random.datetime('yyyy-MM-dd HH:mm:ss'),
})
}
return {
code: 0,
count: list.length,
data: list,
message: '获取成功',
status: true,
}
})
// 手动执行工作流
export const executeWorkflow = Mock.mock(/\/workflow\/execute_workflow/, 'post', () => ({
code: 0,
count: 0,
data: null,
message: '执行成功',
status: true,
}))

View File

@ -0,0 +1,87 @@
{
"name": "allin-ssl",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --host",
"build": "vite build --mode build",
"preview": "vite preview",
"test": "vitest",
"tsc": "vue-tsc -b --noEmit",
"lint:ts": "eslint --ext .ts,.js src --fix",
"lint:style": "stylelint --config .stylelintrc.cjs --fix",
"lint": "npm run lint:ts && npm run lint:style"
},
"dependencies": {
"@baota/hooks": "workspace:*",
"@baota/i18n": "workspace:*",
"@baota/naive-ui": "workspace:*",
"@baota/pinia": "workspace:*",
"@baota/router": "workspace:*",
"@baota/utils": "workspace:*",
"@vicons/antd": "^0.13.0",
"@vicons/carbon": "^0.13.0",
"@vicons/fa": "^0.13.0",
"@vicons/fluent": "^0.13.0",
"@vicons/ionicons5": "^0.13.0",
"@vicons/tabler": "^0.13.0",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.42.5",
"@vue-flow/minimap": "^1.5.3",
"@vue-flow/node-toolbar": "^1.1.1",
"@vueuse/core": "^12.4.0",
"@vueuse/integrations": "^12.4.0",
"axios": "^1.7.9",
"crypto-js": "^4.1.1",
"echarts": "^5.6.0",
"jsoneditor": "^10.2.0",
"naive-ui": "^2.41.0",
"normalize.css": "^8.0.1",
"nprogress": "^0.2.0",
"particlesjs": "^2.2.3",
"pinia": "^2.3.0",
"pinia-plugin-persistedstate": "^4.2.0",
"uuid": "^11.1.0",
"vue": "^3.5.13",
"vue-i18n": "^11.1.2",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@baota/eslint": "workspace:*",
"@baota/plugin-i18n": "workspace:*",
"@baota/prettier": "workspace:*",
"@baota/stylelint": "workspace:*",
"@baota/typescript": "workspace:*",
"@baota/project-ftp-sync": "workspace:*",
"@types/crypto-js": "^4.1.1",
"@types/humps": "^2.0.6",
"@types/md5": "^2.3.5",
"@types/minimist": "^1.2.5",
"@types/mockjs": "^1.0.10",
"@types/node": "^22.10.7",
"@types/ramda": "^0.30.2",
"@vitejs/plugin-basic-ssl": "^1.2.0",
"@vitejs/plugin-legacy": "^6.0.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"@vue/tsconfig": "^0.7.0",
"autoprefixer": "^10.4.20",
"humps": "^2.0.1",
"minimist": "^1.2.8",
"mockjs": "^1.1.0",
"postcss": "^8.5.1",
"tailwindcss": "^3.4.17",
"typescript-plugin-css-modules": "^5.1.0",
"unplugin-auto-import": "^19.0.0",
"unplugin-vue-components": "^28.0.0",
"vite": "^6.0.5",
"vite-plugin-compression2": "^1.3.3",
"vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-devtools": "^7.7.0",
"vite-plugin-vue-mcp": "^0.3.2",
"vitest": "^3.0.2",
"vue-tsc": "^2.2.0"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -0,0 +1,3 @@
import prettierConfig from '@baota/prettier'
export default prettierConfig

BIN
frontend/apps/allin-ssl/public/.DS_Store vendored Normal file

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 364 KiB

After

Width:  |  Height:  |  Size: 364 KiB

View File

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View File

@ -0,0 +1,145 @@
#!/bin/bash
# 获取脚本所在目录的绝对路径
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 项目根目录
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# src 目录
SRC_DIR="$PROJECT_ROOT/src"
# tmp 目录
TMP_DIR="$PROJECT_ROOT/.temp"
# 临时文件
TEMP_PATHS_FILE="$TMP_DIR/tsconfig_paths.json"
TEMP_ALIAS_FILE="$TMP_DIR/vite_alias.js"
# 清理函数
cleanup() {
echo "清理临时文件..."
rm -rf "$TMP_DIR"
}
# 错误处理
handle_error() {
echo "错误: $1"
cleanup
exit 1
}
# 注册清理函数
trap cleanup EXIT
# 检查并创建 tmp 目录
if [ ! -d "$TMP_DIR" ]; then
echo "创建临时目录: $TMP_DIR"
mkdir -p "$TMP_DIR" || handle_error "无法创建临时目录"
fi
# 初始化临时文件
echo "{" > "$TEMP_PATHS_FILE"
echo "import path from 'path'" > "$TEMP_ALIAS_FILE"
echo "export default {" >> "$TEMP_ALIAS_FILE"
# 处理 views 目录下的第一层目录
if [ -d "$SRC_DIR/views" ]; then
echo "处理 views 目录..."
# 确保没有尾随逗号的最后一个条目
view_dirs=()
while IFS= read -r dir; do
if [ -d "$dir" ]; then
dir_name=$(basename "$dir")
view_dirs+=("$dir_name")
fi
done < <(find "$SRC_DIR/views" -mindepth 1 -maxdepth 1 -type d)
# 处理 views 子目录
total=${#view_dirs[@]}
for ((i=0; i<total; i++)); do
dir_name=${view_dirs[$i]}
echo " \"@$dir_name/*\": [\"./src/views/$dir_name/*\"]" >> "$TEMP_PATHS_FILE"
echo " '@$dir_name': path.resolve(__dirname, 'src/views/$dir_name')," >> "$TEMP_ALIAS_FILE"
# 如果不是最后一个元素,添加逗号
if [ $i -lt $((total-1)) ]; then
echo "," >> "$TEMP_PATHS_FILE"
fi
done
fi
# 处理 src 目录下的所有目录
echo "处理 src 目录下的其他目录..."
src_dirs=()
while IFS= read -r dir; do
if [ -d "$dir" ] && [ "$(basename "$dir")" != "views" ]; then
dir_name=$(basename "$dir")
src_dirs+=("$dir_name")
fi
done < <(find "$SRC_DIR" -mindepth 1 -maxdepth 1 -type d)
# 如果之前有 views 目录的条目,添加逗号
if [ ${#view_dirs[@]} -gt 0 ] && [ ${#src_dirs[@]} -gt 0 ]; then
echo "," >> "$TEMP_PATHS_FILE"
fi
# 处理其他目录
total=${#src_dirs[@]}
for ((i=0; i<total; i++)); do
dir_name=${src_dirs[$i]}
echo " \"@$dir_name/*\": [\"./src/$dir_name/*\"]" >> "$TEMP_PATHS_FILE"
echo " '@$dir_name': path.resolve(__dirname, 'src/$dir_name')," >> "$TEMP_ALIAS_FILE"
# 如果不是最后一个元素,添加逗号
if [ $i -lt $((total-1)) ]; then
echo "," >> "$TEMP_PATHS_FILE"
fi
done
# 添加根路径(确保添加逗号如果之前有其他条目)
if [ ${#view_dirs[@]} -gt 0 ] || [ ${#src_dirs[@]} -gt 0 ]; then
echo "," >> "$TEMP_PATHS_FILE"
fi
echo " \"@/*\": [\"./src/*\"]" >> "$TEMP_PATHS_FILE"
echo "}" >> "$TEMP_PATHS_FILE"
# 添加根路径到 alias 配置
echo " '@': path.resolve(__dirname, 'src')" >> "$TEMP_ALIAS_FILE"
echo "}" >> "$TEMP_ALIAS_FILE"
# 更新 tsconfig.app.json
echo "更新 tsconfig.app.json..."
TSCONFIG="$PROJECT_ROOT/tsconfig.app.json"
if [ -f "$TSCONFIG" ]; then
# 创建临时文件
TSCONFIG_TMP="${TSCONFIG}.tmp"
# 使用 jq 处理 JSON如果可用
if command -v jq >/dev/null 2>&1; then
jq --arg paths "$(cat "$TEMP_PATHS_FILE")" '.compilerOptions.paths = $paths' "$TSCONFIG" > "$TSCONFIG_TMP" \
&& mv "$TSCONFIG_TMP" "$TSCONFIG" \
|| handle_error "更新 tsconfig.app.json 失败"
else
# 回退到 sed 方案
sed -e '/"paths":/,/}/c\ "paths": '"$(cat "$TEMP_PATHS_FILE")"',' "$TSCONFIG" > "$TSCONFIG_TMP" \
&& mv "$TSCONFIG_TMP" "$TSCONFIG" \
|| handle_error "更新 tsconfig.app.json 失败"
fi
echo "tsconfig.app.json 更新成功"
else
handle_error "找不到 tsconfig.app.json 文件"
fi
# 更新 vite.config.ts
echo "更新 vite.config.ts..."
VITE_CONFIG="$PROJECT_ROOT/vite.config.ts"
if [ -f "$VITE_CONFIG" ]; then
VITE_CONFIG_TMP="${VITE_CONFIG}.tmp"
# 使用 sed 更新 alias 配置
sed -e '/resolve: {/,/}/c\ resolve: {\n alias: '"$(cat "$TEMP_ALIAS_FILE")"'\n },' "$VITE_CONFIG" > "$VITE_CONFIG_TMP" \
&& mv "$VITE_CONFIG_TMP" "$VITE_CONFIG" \
|| handle_error "更新 vite.config.ts 失败"
echo "vite.config.ts 更新成功"
else
handle_error "找不到 vite.config.ts 文件"
fi
echo "路径别名配置更新完成!"

View File

@ -0,0 +1,891 @@
#!/bin/bash
# 遇到错误时退出
set -e
# 显示帮助信息
show_help() {
echo "使用方法: ./create-roles.sh [选项]"
echo "选项:"
echo " -h, --help 显示帮助信息"
echo
echo "此脚本在 src/views 目录下创建角色管理相关的 Vue3 TSX 路由视图结构"
echo "将生成以下结构:"
echo "src/views/<角色名称>"
echo "├── index.tsx # 入口文件"
echo "├── useController.ts # 控制器"
echo "├── useStore.ts # 状态管理"
echo "├── index.module.css # 样式文件"
echo "├── types.d.ts # 类型定义"
echo "├── children/ # 子路由"
echo "│ └── permissions # 权限管理子路由"
echo "│ ├── index.tsx # 视图"
echo "│ ├── index.module.css # 样式"
echo "│ ├── useController.ts # 控制器"
echo "│ ├── useStore.ts # 状态管理"
echo "│ └── types.d.ts # 类型定义"
echo "└── components/ # 组件"
echo " └── role-form # 角色表单组件"
echo " ├── index.tsx # 视图"
echo " ├── index.module.css # 样式"
echo " ├── useController.ts # 控制器"
echo " ├── useStore.ts # 状态管理"
echo " └── types.d.ts # 类型定义"
echo
echo "同时会创建:"
echo "src/api/<角色名称>.ts # API 文件"
echo "src/types/<角色名称>.d.ts # 类型定义文件"
}
# 解析命令行参数
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
show_help
exit 0
;;
*)
echo "错误: 未知参数 $1"
show_help
exit 1
;;
esac
done
# 交互式选择函数
select_option() {
local prompt="$1"
local options=("是" "否")
local selected
echo "$prompt"
select choice in "${options[@]}"; do
case $REPLY in
1|2)
selected=$choice
break
;;
*)
echo "请选择有效的选项 [1-2]"
;;
esac
done
[[ "$selected" == "是" ]] && return 0 || return 1
}
# 交互式输入路由名称
read -p "请输入路由名称 (routerName): " ROUTER_NAME
if [ -z "$ROUTER_NAME" ]; then
echo "错误: 路由名称不能为空"
exit 1
fi
# 询问是否创建子路由
if select_option "是否创建子路由?"; then
read -p "请输入子路由名称 [默认: list]: " CHILD_ROUTER_NAME
CHILD_ROUTER_NAME=${CHILD_ROUTER_NAME:-"list"}
echo "将创建子路由: $CHILD_ROUTER_NAME"
else
CHILD_ROUTER_NAME=""
echo "不创建子路由"
fi
# 询问是否创建组件
if select_option "是否创建组件?"; then
read -p "请输入组件名称 [默认: todo-form]: " COMPONENT_NAME
COMPONENT_NAME=${COMPONENT_NAME:-"todo-form"}
echo "将创建组件: $COMPONENT_NAME"
else
COMPONENT_NAME=""
echo "不创建组件"
fi
# 获取脚本所在目录的绝对路径
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# 获取项目根目录
PROJECT_ROOT="$SCRIPT_DIR/../"
# 创建目录函数
create_dir() {
local dir="$1"
mkdir -p "$PROJECT_ROOT/$dir"
}
# 确保必需的目录存在
create_dir "src/views"
create_dir "src/api"
create_dir "src/types"
# 创建主目录结构
create_dir "src/views/$ROUTER_NAME/children/$CHILD_ROUTER_NAME"
create_dir "src/views/$ROUTER_NAME/components/$COMPONENT_NAME"
# 创建 API 文件
cat > "$PROJECT_ROOT/src/api/${ROUTER_NAME}.ts" << ''
# 创建类型定义文件
cat > "$PROJECT_ROOT/src/types/${ROUTER_NAME}.d.ts" << ''
# 创建主路由类型文件
cat > "$PROJECT_ROOT/src/views/$ROUTER_NAME/types.d.ts" << EOL
export interface Todo {
id: string
title: string
completed: boolean
createdAt: string
}
export interface TodoState {
todos: Todo[]
loading: boolean
error: string | null
}
EOL
# 创建主路由状态管理文件
cat > "$PROJECT_ROOT/src/views/$ROUTER_NAME/useStore.ts" << EOL
import { defineStore } from '@baota/pinia'
import { ref } from 'vue'
import type { Todo, TodoState } from './types'
const store = defineStore('todo-store', () => {
const todos = ref<Todo[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const addTodo = (title: string) => {
const newTodo: Todo = {
id: Date.now().toString(),
title,
completed: false,
createdAt: new Date().toISOString()
}
todos.value.push(newTodo)
}
const toggleTodo = (id: string) => {
const todo = todos.value.find(t => t.id === id)
if (todo) {
todo.completed = !todo.completed
}
}
const removeTodo = (id: string) => {
todos.value = todos.value.filter(t => t.id !== id)
}
return {
todos,
loading,
error,
addTodo,
toggleTodo,
removeTodo
}
})
export const useStore = () => store()
EOL
# 创建主路由控制器文件
cat > "$PROJECT_ROOT/src/views/$ROUTER_NAME/useController.ts" << EOL
import { onMounted } from 'vue'
import { storeToRefs } from '@baota/pinia'
import { useStore } from './useStore'
export const useController = () => {
const store = useStore()
const { todos, loading, error } = storeToRefs(store)
const handleAddTodo = (title: string) => {
if (title.trim()) {
store.addTodo(title.trim())
}
}
const handleToggleTodo = (id: string) => {
store.toggleTodo(id)
}
const handleRemoveTodo = (id: string) => {
store.removeTodo(id)
}
onMounted(() => {
// 可以在这里加载初始数据
console.log('Todo List Component Mounted')
})
return {
todos,
loading,
error,
handleAddTodo,
handleToggleTodo,
handleRemoveTodo
}
}
EOL
# 创建主路由样式文件
cat > "$PROJECT_ROOT/src/views/$ROUTER_NAME/index.module.css" << EOL
.container {
max-width: 600px;
margin: 0 auto;
padding: 24px;
}
.header {
margin-bottom: 24px;
text-align: center;
}
.title {
font-size: 32px;
color: #2c3e50;
}
.form {
display: flex;
gap: 8px;
margin-bottom: 24px;
}
.input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.button {
padding: 8px 16px;
background: #42b883;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.button:hover {
background: #3aa876;
}
.todoList {
list-style: none;
padding: 0;
}
.todoItem {
display: flex;
align-items: center;
padding: 12px;
background: white;
border-radius: 4px;
margin-bottom: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.todoCheckbox {
margin-right: 12px;
}
.todoTitle {
flex: 1;
}
.todoTitle.completed {
text-decoration: line-through;
color: #999;
}
.deleteButton {
padding: 4px 8px;
background: #ff4757;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.deleteButton:hover {
background: #ff3748;
}
EOL
# 创建主路由入口文件
cat > "$PROJECT_ROOT/src/views/$ROUTER_NAME/index.tsx" << EOL
import { defineComponent, ref } from 'vue'
import { useController } from './useController'
import styles from './index.module.css'
export default defineComponent({
name: 'TodoList',
setup() {
const { todos, handleAddTodo, handleToggleTodo, handleRemoveTodo } = useController()
const newTodo = ref('')
const onSubmit = (e: Event) => {
e.preventDefault()
handleAddTodo(newTodo.value)
newTodo.value = ''
}
return () => (
<div class={styles.container}>
<header class={styles.header}>
<h1 class={styles.title}>Todo List</h1>
</header>
<form class={styles.form} onSubmit={onSubmit}>
<input
class={styles.input}
type="text"
v-model={newTodo.value}
placeholder="添加新任务..."
/>
<button class={styles.button} type="submit">
添加
</button>
</form>
<ul class={styles.todoList}>
{todos.value.map(todo => (
<li key={todo.id} class={styles.todoItem}>
<input
type="checkbox"
class={styles.todoCheckbox}
checked={todo.completed}
onChange={() => handleToggleTodo(todo.id)}
/>
<span class={[
styles.todoTitle,
todo.completed && styles.completed
]}>
{todo.title}
</span>
<button
class={styles.deleteButton}
onClick={() => handleRemoveTodo(todo.id)}
>
删除
</button>
</li>
))}
</ul>
</div>
)
}
})
EOL
# 在脚本开头添加大写转换函数
to_upper_first() {
local str="$1"
local first_char=$(echo "${str:0:1}" | tr '[:lower:]' '[:upper:]')
echo "$first_char${str:1}"
}
# 存储转换后的变量
ROUTER_NAME_PASCAL=$(to_upper_first "$ROUTER_NAME")
# 创建表单组件类型文件
cat > "$PROJECT_ROOT/src/views/$ROUTER_NAME/components/$COMPONENT_NAME/types.d.ts" << EOL
import type { ${ROUTER_NAME_PASCAL}Data } from '@/types/${ROUTER_NAME}'
export interface FormProps {
data?: ${ROUTER_NAME_PASCAL}Data | null
}
export interface FormEmits {
(e: 'submit', data: ${ROUTER_NAME_PASCAL}Data): void
(e: 'cancel'): void
}
EOL
# 创建表单组件状态管理文件
cat > "$PROJECT_ROOT/src/views/$ROUTER_NAME/components/$COMPONENT_NAME/useStore.ts" << EOL
import { defineStore } from '@baota/pinia'
import { ref } from 'vue'
import type { ${ROUTER_NAME_PASCAL}Data } from '@/types/${ROUTER_NAME}'
// 定义 store
const store = defineStore('${ROUTER_NAME}-form-store', () => {
const loading = ref(false)
const formData = ref<${ROUTER_NAME_PASCAL}Data>({
id: '',
name: '',
code: '',
description: '',
permissions: [],
createdAt: '',
updatedAt: ''
})
return {
loading,
formData
}
})
export const useStore = () => store()
EOL
# 创建表单组件控制器文件
cat > "$PROJECT_ROOT/src/views/$ROUTER_NAME/components/$COMPONENT_NAME/useController.ts" << EOL
import { onMounted } from 'vue'
import { storeToRefs } from '@baota/pinia'
import { useStore } from './useStore'
import type { ${ROUTER_NAME_PASCAL}Data } from '@/types/${ROUTER_NAME}'
export const useController = (initialData?: ${ROUTER_NAME_PASCAL}Data | null) => {
const store = useStore()
const storeRef = storeToRefs(store)
onMounted(() => {
if (initialData) {
store.formData = { ...initialData }
}
})
return {
...storeRef
}
}
EOL
# 创建表单组件样式文件
cat > "$PROJECT_ROOT/src/views/$ROUTER_NAME/components/$COMPONENT_NAME/index.module.css" << EOL
.form {
max-width: 600px;
}
.formTitle {
font-size: 18px;
font-weight: bold;
margin-bottom: 24px;
}
.formItem {
margin-bottom: 16px;
}
.label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
.input {
width: 100%;
padding: 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: all 0.3s;
}
.input:hover {
border-color: #40a9ff;
}
.input:focus {
border-color: #1890ff;
outline: none;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.textarea {
composes: input;
min-height: 100px;
resize: vertical;
}
.actions {
margin-top: 24px;
display: flex;
gap: 8px;
justify-content: flex-end;
}
.button {
padding: 8px 16px;
border: 1px solid #d9d9d9;
border-radius: 4px;
cursor: pointer;
background: #fff;
transition: all 0.3s;
}
.button:hover {
background: #f5f5f5;
}
.primaryButton {
composes: button;
background: #1890ff;
color: #fff;
border-color: #1890ff;
}
.primaryButton:hover {
background: #40a9ff;
}
EOL
# 创建表单组件入口文件
cat > "$PROJECT_ROOT/src/views/$ROUTER_NAME/components/$COMPONENT_NAME/index.tsx" << EOL
import { defineComponent } from 'vue'
import { useController } from './useController'
import type { FormProps, FormEmits } from './types'
import styles from './index.module.css'
export default defineComponent({
name: 'RoleForm',
props: {
data: {
type: Object as PropType<FormProps['data']>,
default: null
}
},
emits: ['submit', 'cancel'],
setup(props, { emit }) {
const { formData } = useController(props.data)
const handleSubmit = (e: Event) => {
e.preventDefault()
emit('submit', formData.value)
}
return () => (
<form class={styles.form} onSubmit={handleSubmit}>
<h3 class={styles.formTitle}>
{props.data ? '编辑角色' : '创建角色'}
</h3>
<div class={styles.formItem}>
<label class={styles.label}>角色名称</label>
<input
class={styles.input}
type="text"
v-model={formData.value.name}
placeholder="请输入角色名称"
required
/>
</div>
<div class={styles.formItem}>
<label class={styles.label}>角色代码</label>
<input
class={styles.input}
type="text"
v-model={formData.value.code}
placeholder="请输入角色代码"
required
/>
</div>
<div class={styles.formItem}>
<label class={styles.label}>描述</label>
<textarea
class={styles.textarea}
v-model={formData.value.description}
placeholder="请输入角色描述"
/>
</div>
<div class={styles.actions}>
<button
type="button"
class={styles.button}
onClick={() => emit('cancel')}
>
取消
</button>
<button type="submit" class={styles.primaryButton}>
确定
</button>
</div>
</form>
)
}
})
EOL
# 创建子路由类型文件
cat > "$PROJECT_ROOT/src/views/$ROUTER_NAME/children/$CHILD_ROUTER_NAME/types.d.ts" << EOL
import type { ${ROUTER_NAME_PASCAL}Data } from '@/types/${ROUTER_NAME}'
export interface Permission {
code: string
name: string
description?: string
}
export interface PermissionState {
loading: boolean
data: ${ROUTER_NAME_PASCAL}Data | null
permissions: Permission[]
selectedPermissions: string[]
}
EOL
# 创建子路由状态管理文件
cat > "$PROJECT_ROOT/src/views/$ROUTER_NAME/children/$CHILD_ROUTER_NAME/useStore.ts" << EOL
import { defineStore } from '@baota/pinia'
import { ref } from 'vue'
import type { ${ROUTER_NAME_PASCAL}Data } from '@/types/${ROUTER_NAME}'
import type { Permission } from './types'
// 定义 store
const store = defineStore('${ROUTER_NAME}-permissions-store', () => {
const loading = ref(false)
const data = ref<${ROUTER_NAME_PASCAL}Data | null>(null)
const permissions = ref<Permission[]>([])
const selectedPermissions = ref<string[]>([])
return {
loading,
data,
permissions,
selectedPermissions
}
})
// 导出 store
export const useStore = () => store()
EOL
# 创建子路由控制器文件
cat > "$PROJECT_ROOT/src/views/$ROUTER_NAME/children/$CHILD_ROUTER_NAME/useController.ts" << EOL
import { onMounted } from 'vue'
import { storeToRefs } from '@baota/pinia'
import { useStore } from './useStore'
import { get${ROUTER_NAME_PASCAL}Data } from '@/api/${ROUTER_NAME}'
export const useController = (roleId: string) => {
const store = useStore()
const storeRef = storeToRefs(store)
const fetchData = async () => {
try {
store.loading = true
const data = await get${ROUTER_NAME_PASCAL}Data(roleId)
store.data = data
store.selectedPermissions = data.permissions
} catch (error) {
console.error('获取数据失败:', error)
} finally {
store.loading = false
}
}
const handleSave = async () => {
try {
store.loading = true
// 调用保存API
store.loading = false
} catch (error) {
console.error('保存失败:', error)
}
}
onMounted(() => {
fetchData()
})
return {
...storeRef,
handleSave
}
}
EOL
# 创建子路由样式文件
cat > "$PROJECT_ROOT/src/views/$ROUTER_NAME/children/$CHILD_ROUTER_NAME/index.module.css" << EOL
.container {
padding: 24px;
}
.header {
margin-bottom: 24px;
}
.title {
font-size: 24px;
font-weight: bold;
}
.content {
background: #fff;
padding: 24px;
border-radius: 8px;
}
.loading {
text-align: center;
padding: 24px;
}
.permissionList {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
margin: 24px 0;
}
.permissionItem {
display: flex;
align-items: center;
gap: 8px;
}
.actions {
margin-top: 24px;
display: flex;
justify-content: flex-end;
gap: 8px;
}
.button {
padding: 8px 16px;
border: 1px solid #d9d9d9;
border-radius: 4px;
cursor: pointer;
background: #fff;
transition: all 0.3s;
}
.button:hover {
background: #f5f5f5;
}
.primaryButton {
composes: button;
background: #1890ff;
color: #fff;
border-color: #1890ff;
}
.primaryButton:hover {
background: #40a9ff;
}
EOL
# 创建子路由入口文件
cat > "$PROJECT_ROOT/src/views/$ROUTER_NAME/children/$CHILD_ROUTER_NAME/index.tsx" << EOL
import { defineComponent } from 'vue'
import { useRoute, useRouter } from '@baota/router'
import { useController } from './useController'
import styles from './index.module.css'
export default defineComponent({
name: 'RolePermissions',
setup() {
const route = useRoute()
const router = useRouter()
const roleId = route.params.id as string
const {
loading,
data,
permissions,
selectedPermissions,
handleSave
} = useController(roleId)
const handleCancel = () => {
router.push('/${ROUTER_NAME}')
}
return () => (
<div class={styles.container}>
<div class={styles.header}>
<h1 class={styles.title}>权限设置</h1>
</div>
<div class={styles.content}>
{loading.value ? (
<div class={styles.loading}>加载中...</div>
) : (
<>
<h2>{data.value?.name} - 权限配置</h2>
<div class={styles.permissionList}>
{permissions.value.map(permission => (
<label
key={permission.code}
class={styles.permissionItem}
>
<input
type="checkbox"
value={permission.code}
v-model={selectedPermissions.value}
/>
<span>{permission.name}</span>
</label>
))}
</div>
<div class={styles.actions}>
<button
class={styles.button}
onClick={handleCancel}
>
取消
</button>
<button
class={styles.primaryButton}
onClick={handleSave}
>
保存
</button>
</div>
</>
)}
</div>
</div>
)
}
})
EOL
echo "✨ 文件结构生成成功!"
echo "📁 主路由: $PROJECT_ROOT/src/views/$ROUTER_NAME"
echo "📁 子路由: $PROJECT_ROOT/src/views/$ROUTER_NAME/children/$CHILD_ROUTER_NAME"
echo "📁 组件: $PROJECT_ROOT/src/views/$ROUTER_NAME/components/$COMPONENT_NAME"
echo "📄 API文件: $PROJECT_ROOT/src/api/${ROUTER_NAME}.ts"
echo "📄 类型文件: $PROJECT_ROOT/src/types/${ROUTER_NAME}.d.ts"
echo
echo "目录结构:"
echo "├── src/views/$ROUTER_NAME"
echo "│ ├── index.tsx"
echo "│ ├── useController.ts"
echo "│ ├── useStore.ts"
echo "│ ├── index.module.css"
echo "│ ├── types.d.ts"
echo "│ ├── children"
echo "│ │ └── $CHILD_ROUTER_NAME"
echo "│ │ ├── index.tsx"
echo "│ │ ├── index.module.css"
echo "│ │ ├── useController.ts"
echo "│ │ ├── useStore.ts"
echo "│ │ └── types.d.ts"
echo "│ └── components"
echo "│ └── $COMPONENT_NAME"
echo "│ ├── index.tsx"
echo "│ ├── index.module.css"
echo "│ ├── useController.ts"
echo "│ ├── useStore.ts"
echo "│ └── types.d.ts"
echo "├── src/api"
echo "│ └── ${ROUTER_NAME}.ts"
echo "└── src/types"
echo " └── ${ROUTER_NAME}.d.ts"

BIN
frontend/apps/allin-ssl/src/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,21 @@
import { Transition, type Component as ComponentType, h } from 'vue'
import { RouterView } from 'vue-router'
import CustomProvider from '@baota/naive-ui/components/customProvider'
export default defineComponent({
name: 'App',
setup() {
return () => (
<CustomProvider>
<RouterView>
{({ Component }: { Component: ComponentType }) => (
<Transition name="route-slide" mode="out-in">
{Component && h(Component)}
</Transition>
)}
</RouterView>
</CustomProvider>
)
},
})

View File

@ -0,0 +1,51 @@
import { useApi } from './index'
import type {
AccessListParams,
AccessListResponse,
AddAccessParams,
UpdateAccessParams,
DeleteAccessParams,
GetAccessAllListParams,
GetAccessAllListResponse,
} from '../types/access'
import type { AxiosResponseData } from '../types/public'
/**
* @description
* @param {AccessListParams} [params]
* @returns {Promise<AxiosResponse<AccessListResponse>>}
*/
export const getAccessList = (params?: AccessListParams) =>
useApi<AccessListResponse, AccessListParams>('/v1/access/get_list', params)
/**
* @description
* @param {AddAccessParams<string>} [params]
* @returns {Promise<AxiosResponse<AxiosResponseData>>}
*/
export const addAccess = (params?: AddAccessParams<string>) =>
useApi<AxiosResponseData, AddAccessParams<string>>('/v1/access/add_access', params)
/**
* @description
* @param {UpdateAccessParams<string>} [params]
* @returns {Promise<AxiosResponse<AxiosResponseData>>}
*/
export const updateAccess = (params?: UpdateAccessParams<string>) =>
useApi<AxiosResponseData, UpdateAccessParams<string>>('/v1/access/upd_access', params)
/**
* @description
* @param {DeleteAccessParams} [params]
* @returns {Promise<AxiosResponse<AxiosResponseData>>}
*/
export const deleteAccess = (params?: DeleteAccessParams) =>
useApi<AxiosResponseData, DeleteAccessParams>('/v1/access/del_access', params)
/**
* @description DNS
* @param {GetAccessAllListParams} [params]
* @returns {Promise<AxiosResponse<GetAccessAllListResponse>>} dns
*/
export const getAccessAllList = (params?: GetAccessAllListParams) =>
useApi<GetAccessAllListResponse, GetAccessAllListParams>('/v1/access/get_all', params)

View File

@ -0,0 +1,56 @@
import { useApi } from './index'
import type {
CertListParams,
CertListResponse,
ApplyCertParams,
ApplyCertResponse,
UploadCertParams,
UploadCertResponse,
DeleteCertParams,
DeleteCertResponse,
DownloadCertParams,
} from '../types/cert'
import axios from 'axios'
/**
* @description
* @param {CertListParams} [params]
* @returns {Promise<AxiosResponse<CertListResponse>>}
*/
export const getCertList = (params?: CertListParams) =>
useApi<CertListResponse, CertListParams>('/v1/cert/get_list', params)
/**
* @description
* @param {ApplyCertParams} [params]
* @returns {Promise<AxiosResponse<ApplyCertResponse>>}
*/
export const applyCert = (params?: ApplyCertParams) =>
useApi<ApplyCertResponse, ApplyCertParams>('/v1/cert/apply_cert', params)
/**
* @description
* @param {UploadCertParams} [params]
* @returns {Promise<AxiosResponse<UploadCertResponse>>}
*/
export const uploadCert = (params?: UploadCertParams) =>
useApi<UploadCertResponse, UploadCertParams>('/v1/cert/upload_cert', params)
/**
* @description
* @param {DeleteCertParams} [params]
* @returns {Promise<AxiosResponse<DeleteCertResponse>>}
*/
export const deleteCert = (params?: DeleteCertParams) =>
useApi<DeleteCertResponse, DeleteCertParams>('/v1/cert/del_cert', params)
/**
* @description
* @param {DownloadCertParams} [params]
* @returns {Promise<AxiosResponse<DownloadCertResponse>>}
*/
export const downloadCert = (params?: DownloadCertParams) => {
return axios.get('/v1/cert/download', {
params,
})
}

View File

@ -0,0 +1,86 @@
import { AxiosError } from 'axios'
import MD5 from 'crypto-js/md5'
import { isDev } from '@baota/utils/browser'
import { HttpClient, useAxios, useAxiosReturn } from '@baota/hooks/axios'
import { errorMiddleware } from '@baota/hooks/axios/model'
import { router } from '@router/index'
import type { AxiosResponseData } from '@/types/public'
/**
* @description 401
* @param {AxiosResponseData<T>} response
* @returns {AxiosResponseData<T>}
*/
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<T>}
*/
export const useApiReturn = <T>(data: T, message?: string): AxiosResponseData<T> => {
return {
code: 200,
count: 0,
data,
message: message || '请求返回值错误,请检查',
status: false,
} as AxiosResponseData<T>
}
/**
* @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
* @returns {string} api token
*/
export const createApiToken = () => {
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<T, Z>}
*/
export const useApi = <T, Z = Record<string, unknown>>(url: string, params?: Z) => {
const { urlRef, paramsRef, ...other } = useAxios<T>(instance)
const apiParams = createApiToken()
urlRef.value = url
paramsRef.value = isDev() ? { ...(params || {}), ...apiParams } : params || {}
return { urlRef, paramsRef: paramsRef as Ref<Z>, ...other } as useAxiosReturn<T, Z>
}
// 导出所有模块
export * from './public'
export * from './workflow'
export * from './cert'
export * from './access'
export * from './monitor'
export * from './setting'

View File

@ -0,0 +1,50 @@
import { useApi } from './index'
import type { AxiosResponseData } from '@/types/public'
import type {
SiteMonitorListParams,
SiteMonitorListResponse,
AddSiteMonitorParams,
UpdateSiteMonitorParams,
DeleteSiteMonitorParams,
SetSiteMonitorParams,
} from '../types/monitor'
/**
* @description
* @param {SiteMonitorListParams} [params]
* @returns {Promise<AxiosResponse<SiteMonitorListResponse>>}
*/
export const getSiteMonitorList = (params?: SiteMonitorListParams) =>
useApi<SiteMonitorListResponse, SiteMonitorListParams>('/v1/siteMonitor/get_list', params)
/**
* @description
* @param {AddSiteMonitorParams} [params]
* @returns {Promise<AxiosResponse<AxiosResponseData>>}
*/
export const addSiteMonitor = (params?: AddSiteMonitorParams) =>
useApi<AxiosResponseData, AddSiteMonitorParams>('/v1/siteMonitor/add_site_monitor', params)
/**
* @description
* @param {UpdateSiteMonitorParams} [params]
* @returns {Promise<AxiosResponse<AxiosResponseData>>}
*/
export const updateSiteMonitor = (params?: UpdateSiteMonitorParams) =>
useApi<AxiosResponseData, UpdateSiteMonitorParams>('/v1/siteMonitor/upd_site_monitor', params)
/**
* @description
* @param {DeleteSiteMonitorParams} [params]
* @returns {Promise<AxiosResponse<AxiosResponseData>>}
*/
export const deleteSiteMonitor = (params?: DeleteSiteMonitorParams) =>
useApi<AxiosResponseData, DeleteSiteMonitorParams>('/v1/siteMonitor/del_site_monitor', params)
/**
* @description /
* @param {SetSiteMonitorParams} [params]
* @returns {Promise<AxiosResponse<AxiosResponseData>>}
*/
export const setSiteMonitor = (params?: SetSiteMonitorParams) =>
useApi<AxiosResponseData, SetSiteMonitorParams>('/v1/siteMonitor/set_site_monitor', params)

View File

@ -0,0 +1,38 @@
import axios from 'axios'
import { useApi } from './index'
import type {
loginParams,
loginResponse,
GetOverviewsParams,
GetOverviewsResponse,
AxiosResponseData,
} from '@/types/public'
/**
* @description
* @param {loginParams} [params]
* @returns {Promise<AxiosResponse<loginResponse>>}
*/
export const login = (params?: loginParams) => useApi<loginResponse, loginParams>('/v1/login/sign', params)
/**
* @description
* @returns {Promise<AxiosResponse<loginCodeResponse>>}
*/
export const getLoginCode = () => {
return axios.get('/v1/login/get_code')
}
/**
* @description
* @returns {Promise<AxiosResponse<AxiosResponseData>>}
*/
export const signOut = () => useApi<AxiosResponseData>('/v1/login/sign-out')
/**
* @description
* @param {GetOverviewsParams} [params]
* @returns {Promise<AxiosResponse<GetOverviewsResponse>>}
*/
export const getOverviews = (params?: GetOverviewsParams) =>
useApi<GetOverviewsResponse, GetOverviewsParams>('/v1/overview/get_overviews', params)

View File

@ -0,0 +1,69 @@
import { useApi } from './index'
import type { AxiosResponseData } from '../types/public'
import type {
GetSettingParams,
GetSettingResponse,
SaveSettingParams,
GetReportListParams,
GetReportListResponse,
AddReportParams,
UpdateReportParams,
DeleteReportParams,
TestReportParams,
} from '../types/setting'
/**
* @description
* @param {GetSettingParams} [params]
* @returns {Promise<AxiosResponse<GetSettingResponse>>}
*/
export const getSystemSetting = (params?: GetSettingParams) =>
useApi<GetSettingResponse, GetSettingParams>('/v1/setting/get_setting', params)
/**
* @description
* @param {SaveSettingParams} [params]
* @returns {Promise<AxiosResponse<AxiosResponseData>>}
*/
export const saveSystemSetting = (params?: SaveSettingParams) =>
useApi<AxiosResponseData, SaveSettingParams>('/v1/setting/save_setting', params)
/**
* @description
* @param {AddReportParams} [params]
* @returns {Promise<AxiosResponse<AxiosResponseData>>}
*/
export const addReport = (params?: AddReportParams) =>
useApi<AxiosResponseData, AddReportParams>('/v1/report/add_report', params)
/**
* @description
* @param {UpdateReportParams} [params]
* @returns {Promise<AxiosResponse<AxiosResponseData>>}
*/
export const updateReport = (params?: UpdateReportParams) =>
useApi<AxiosResponseData, UpdateReportParams>('/v1/report/upd_report', params)
/**
* @description
* @param {DeleteReportParams} [params]
* @returns {Promise<AxiosResponse<AxiosResponseData>>}
*/
export const deleteReport = (params?: DeleteReportParams) =>
useApi<AxiosResponseData, DeleteReportParams>('/v1/report/del_report', params)
/**
* @description
* @param {TestReportParams} [params]
* @returns {Promise<AxiosResponse<AxiosResponseData>>}
*/
export const testReport = (params?: TestReportParams) =>
useApi<AxiosResponseData, TestReportParams>('/v1/report/notify_test', params)
/**
* @description
* @param {GetReportListParams} [params]
* @returns {Promise<AxiosResponse<GetReportListResponse>>}
*/
export const getReportList = (params?: GetReportListParams) =>
useApi<GetReportListResponse, GetReportListParams>('/v1/report/get_list', params)

View File

@ -0,0 +1,90 @@
import { useApi } from './index'
import type {
WorkflowListParams,
WorkflowListResponse,
AddWorkflowParams,
UpdateWorkflowParams,
DeleteWorkflowParams,
WorkflowHistoryParams,
WorkflowHistoryResponse,
ExecuteWorkflowParams,
UpdateWorkflowExecTypeParams,
EnableWorkflowParams,
WorkflowHistoryDetailParams,
} from '../types/workflow'
import { AxiosResponseData } from '@/types/public'
/**
* @description
* @param {WorkflowListParams} [params]
* @returns {Promise<AxiosResponse<WorkflowListResponse>>}
*/
export const getWorkflowList = (params?: WorkflowListParams) =>
useApi<WorkflowListResponse, WorkflowListParams>('/v1/workflow/get_list', params)
/**
* @description
* @param {AddWorkflowParams} [params]
* @returns {Promise<AxiosResponse<AxiosResponseData>>}
*/
export const addWorkflow = (params?: AddWorkflowParams) =>
useApi<AxiosResponseData, AddWorkflowParams>('/v1/workflow/add_workflow', params)
/**
* @description
* @param {UpdateWorkflowParams} [params]
* @returns {Promise<AxiosResponse<AxiosResponseData>>}
*/
export const updateWorkflow = (params?: UpdateWorkflowParams) =>
useApi<AxiosResponseData, UpdateWorkflowParams>('/v1/workflow/upd_workflow', params)
/**
* @description
* @param {DeleteWorkflowParams} [params]
* @returns {Promise<AxiosResponse<AxiosResponseData>>}
*/
export const deleteWorkflow = (params?: DeleteWorkflowParams) =>
useApi<AxiosResponseData, DeleteWorkflowParams>('/v1/workflow/del_workflow', params)
/**
* @description
* @param {WorkflowHistoryParams} [params]
* @returns {Promise<AxiosResponse<WorkflowHistoryResponse>>}
*/
export const getWorkflowHistory = (params?: WorkflowHistoryParams) =>
useApi<WorkflowHistoryResponse, WorkflowHistoryParams>('/v1/workflow/get_workflow_history', params)
/**
* @description
* @param {WorkflowHistoryDetailParams} [params]
* @returns {Promise<AxiosResponse<AxiosResponseData>>}
*/
export const getWorkflowHistoryDetail = (params?: WorkflowHistoryDetailParams) =>
useApi<AxiosResponseData, WorkflowHistoryDetailParams>('/v1/workflow/get_exec_log', params)
/**
* @description
* @param {ExecuteWorkflowParams} [params]
* @returns {Promise<AxiosResponse<AxiosResponseData>>}
*/
export const executeWorkflow = (params?: ExecuteWorkflowParams) =>
useApi<AxiosResponseData, ExecuteWorkflowParams>('/v1/workflow/execute_workflow', params)
/**
* @description
* @param {UpdateWorkflowExecTypeParams} [params]
* @returns {Promise<AxiosResponse<AxiosResponseData>>}
*/
export const updateWorkflowExecType = (params?: UpdateWorkflowExecTypeParams) =>
useApi<AxiosResponseData, UpdateWorkflowExecTypeParams>('/v1/workflow/exec_type', params)
/**
* @description
* @param {EnableWorkflowParams} [params]
* @returns {Promise<AxiosResponse<AxiosResponseData>>}
*/
export const enableWorkflow = (params?: EnableWorkflowParams) =>
useApi<AxiosResponseData, EnableWorkflowParams>('/v1/workflow/active', params)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="m576 512 277.333 277.333-64 64L512 576 234.667 853.333l-64-64L448 512 170.667 234.667l64-64L512 448l277.333-277.333 64 64L576 512z"/></svg>

After

Width:  |  Height:  |  Size: 250 B

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" data-icon="solution" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M688 264c0-4.4-3.6-8-8-8H296c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h384c4.4 0 8-3.6 8-8v-48zm-8 136H296c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h384c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zM480 544H296c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zm-48 308H208V148h560v344c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V108c0-17.7-14.3-32-32-32H168c-17.7 0-32 14.3-32 32v784c0 17.7 14.3 32 32 32h264c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm356.8-74.4c29-26.3 47.2-64.3 47.2-106.6 0-79.5-64.5-144-144-144s-144 64.5-144 144c0 42.3 18.2 80.3 47.2 106.6-57 32.5-96.2 92.7-99.2 162.1-.2 4.5 3.5 8.3 8 8.3h48.1c4.2 0 7.7-3.3 8-7.6C564 871.2 621.7 816 692 816s128 55.2 131.9 124.4c.2 4.2 3.7 7.6 8 7.6H880c4.6 0 8.2-3.8 8-8.3-2.9-69.5-42.2-129.6-99.2-162.1zM692 591c44.2 0 80 35.8 80 80s-35.8 80-80 80-80-35.8-80-80 35.8-80 80-80z"/></svg>

After

Width:  |  Height:  |  Size: 958 B

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" data-icon="sisternode" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M672 432c-120.3 0-219.9 88.5-237.3 204H320c-15.5 0-28-12.5-28-28V244h291c14.2 35.2 48.7 60 89 60 53 0 96-43 96-96s-43-96-96-96c-40.3 0-74.8 24.8-89 60H112v72h108v364c0 55.2 44.8 100 100 100h114.7c17.4 115.5 117 204 237.3 204 132.5 0 240-107.5 240-240S804.5 432 672 432zm128 266c0 4.4-3.6 8-8 8h-86v86c0 4.4-3.6 8-8 8h-52c-4.4 0-8-3.6-8-8v-86h-86c-4.4 0-8-3.6-8-8v-52c0-4.4 3.6-8 8-8h86v-86c0-4.4 3.6-8 8-8h52c4.4 0 8 3.6 8 8v86h86c4.4 0 8 3.6 8 8v52z"/></svg>

After

Width:  |  Height:  |  Size: 584 B

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" data-icon="deployment-unit" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M888.3 693.2c-42.5-24.6-94.3-18-129.2 12.8l-53-30.7V523.6c0-15.7-8.4-30.3-22-38.1l-136-78.3v-67.1c44.2-15 76-56.8 76-106.1 0-61.9-50.1-112-112-112s-112 50.1-112 112c0 49.3 31.8 91.1 76 106.1v67.1l-136 78.3c-13.6 7.8-22 22.4-22 38.1v151.6l-53 30.7c-34.9-30.8-86.8-37.4-129.2-12.8-53.5 31-71.7 99.4-41 152.9 30.8 53.5 98.9 71.9 152.2 41 42.5-24.6 62.7-73 53.6-118.8l48.7-28.3 140.6 81c6.8 3.9 14.4 5.9 22 5.9s15.2-2 22-5.9L674.5 740l48.7 28.3c-9.1 45.7 11.2 94.2 53.6 118.8 53.3 30.9 121.5 12.6 152.2-41 30.8-53.6 12.6-122-40.7-152.9zm-673 138.4a47.6 47.6 0 0 1-65.2-17.6c-13.2-22.9-5.4-52.3 17.5-65.5a47.6 47.6 0 0 1 65.2 17.6c13.2 22.9 5.4 52.3-17.5 65.5zM522 463.8zM464 234a48.01 48.01 0 0 1 96 0 48.01 48.01 0 0 1-96 0zm170 446.2-122 70.3-122-70.3V539.8l122-70.3 122 70.3v140.4zm239.9 133.9c-13.2 22.9-42.4 30.8-65.2 17.6-22.8-13.2-30.7-42.6-17.5-65.5s42.4-30.8 65.2-17.6c22.9 13.2 30.7 42.5 17.5 65.5z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M64 512a448 448 0 1 0 896 0 448 448 0 1 0-896 0z" fill="#FA5151"/><path d="m557.3 512 113.1-113.1c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L512 466.7 398.9 353.6c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L466.7 512 353.6 625.1c-12.5 12.5-12.5 32.8 0 45.3 6.2 6.2 14.4 9.4 22.6 9.4s16.4-3.1 22.6-9.4L512 557.3l113.1 113.1c6.2 6.2 14.4 9.4 22.6 9.4s16.4-3.1 22.6-9.4c12.5-12.5 12.5-32.8 0-45.3L557.3 512z" fill="#FFF"/></svg>

After

Width:  |  Height:  |  Size: 538 B

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" data-icon="send" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M931.4 498.9 94.9 79.5c-3.4-1.7-7.3-2.1-11-1.2a15.99 15.99 0 0 0-11.7 19.3l86.2 352.2c1.3 5.3 5.2 9.6 10.4 11.3l147.7 50.7-147.6 50.7c-5.2 1.8-9.1 6-10.3 11.3L72.2 926.5c-.9 3.7-.5 7.6 1.2 10.9 3.9 7.9 13.5 11.1 21.5 7.2l836.5-417c3.1-1.5 5.6-4.1 7.2-7.1 3.9-8 .7-17.6-7.2-21.6zM170.8 826.3l50.3-205.6 295.2-101.3c2.3-.8 4.2-2.6 5-5 1.4-4.2-.8-8.7-5-10.2L221.1 403 171 198.2l628 314.9-628.2 313.2z"/></svg>

After

Width:  |  Height:  |  Size: 525 B

View File

@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M64 512a448 448 0 1 0 896 0 448 448 0 1 0-896 0z" fill="#07C160"/><path d="M466.7 679.8c-8.5 0-16.6-3.4-22.6-9.4l-181-181.1c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l158.4 158.5 249-249c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3L489.3 670.4c-6 6-14.1 9.4-22.6 9.4z" fill="#FFF"/></svg>

After

Width:  |  Height:  |  Size: 398 B

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" data-icon="cloud-upload" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M518.3 459a8 8 0 0 0-12.6 0l-112 141.7a7.98 7.98 0 0 0 6.3 12.9h73.9V856c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V613.7H624c6.7 0 10.4-7.7 6.3-12.9L518.3 459z"/><path d="M811.4 366.7C765.6 245.9 648.9 160 512.2 160S258.8 245.8 213 366.6C127.3 389.1 64 467.2 64 560c0 110.5 89.5 200 199.9 200H304c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8h-40.1c-33.7 0-65.4-13.4-89-37.7-23.5-24.2-36-56.8-34.9-90.6.9-26.4 9.9-51.2 26.2-72.1 16.7-21.3 40.1-36.8 66.1-43.7l37.9-9.9 13.9-36.6c8.6-22.8 20.6-44.1 35.7-63.4a245.6 245.6 0 0 1 52.4-49.9c41.1-28.9 89.5-44.2 140-44.2s98.9 15.3 140 44.2c19.9 14 37.5 30.8 52.4 49.9 15.1 19.3 27.1 40.7 35.7 63.4l13.8 36.5 37.8 10C846.1 454.5 884 503.8 884 560c0 33.1-12.9 64.3-36.3 87.7a123.07 123.07 0 0 1-87.6 36.3H720c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h40.1C870.5 760 960 670.5 960 560c0-92.7-63.1-170.7-148.6-193.3z"/></svg>

After

Width:  |  Height:  |  Size: 968 B

View File

@ -0,0 +1 @@
<svg class="icon" width="30" height="30" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M717.62 682.138h116.582l-212.02 296.396-5.12-1.792 46.08-194.816h-91.136c10.24-47.257 20.07-91.648 30.72-140.185-25.907 7.782-48.537 12.288-69.17 21.299-44.698 19.456-84.89 11.162-121.14-17.613a455.68 455.68 0 0 1-66.97-63.232c-20.48-24.473-13.977-38.451 17.05-43.673 65.69-11.06 131.584-20.788 197.58-33.076h-41.42c-56.78-.563-113.613-1.484-170.394-1.69-34.867 0-59.443-18.175-80.486-43.468a272.589 272.589 0 0 1-54.989-110.848c-6.451-26.01.666-33.178 27.495-26.931 67.84 15.77 135.577 32.102 203.417 47.974a1030.195 1030.195 0 0 0 105.165 20.173c-40.96-13.62-82.74-26.112-123.34-40.96-61.953-22.938-122.88-47.923-184.833-71.68a66.56 66.56 0 0 1-38.963-37.376c-24.115-54.989-42.138-111.565-42.547-172.288 0-22.118 7.219-27.546 26.214-18.33C378.88 143.36 578.918 222.31 779.571 299.315a288.666 288.666 0 0 1 54.989 29.747c33.331 22.17 44.186 50.125 26.931 85.248-35.43 72.09-74.393 142.439-112.23 213.35-9.472 17.46-20.02 34.407-31.642 54.478z" fill="#59ADF8"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg class="icon" width="30" height="30" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M512 0q512 0 512 512t-512 512Q0 1024 0 512T512 0z" fill="#FFF"/><path d="M832.032 367.205c4.123.024 8.243.265 12.34.723a345.91 345.91 0 0 1 91.865 25.368c8.523 3.818 10.629 6.91 3.291 14.608a296.456 296.456 0 0 0-51.46 75.942c-14.15 29.776-29.614 58.928-44.09 88.576a190.048 190.048 0 0 1-43.992 58.567c-45.276 40.963-98.052 58.238-158.264 49.88-69.096-9.575-134.541-32.903-196.365-63.667-3.85-1.908-6.581-3.29-8.851-4.672a4.483 4.483 0 0 1-2.096-3.943 4.483 4.483 0 0 1 2.358-3.79l4.278-2.304c50.079-26.749 91.866-64.027 132.138-103.216 17.011-16.451 33.331-33.725 50.507-50.045a291.126 291.126 0 0 1 135.2-72.387c11.12-2.666 22.34-4.837 33.527-7.239h.528l23.82-2.204" fill="#133C9A"/><path d="M348.029 850.682c-7.6-.428-26.322-2.994-28.56-3.29a452.614 452.614 0 0 1-139.312-40.734c-25.466-11.91-49.98-25.96-74.559-39.648-16.155-9.016-23.492-23.032-23.328-42.05.528-70.347.528-140.704 0-211.074-.262-45.275-1.579-90.548-2.27-135.792a36.602 36.602 0 0 1 1.875-11.744c2.731-8.16 8.359-8.656 13.92-3.29 6.416 6.185 11.515 13.686 17.866 19.74 56.856 56 117.1 107.296 184.651 149.546a1017.56 1017.56 0 0 0 118.451 65.246c65.642 29.81 132.928 56.1 203.44 72.78 62.285 14.742 122.861 5.463 173.333-34.086 15.4-13.161 23.034-22.803 41.294-47.118a303.662 303.662 0 0 1-31.555 61.464c-11.745 18.491-38.2 43.168-58.368 62.515-30.633 29.613-70.677 53.632-108.253 73.901-40.963 22.078-83.54 39.714-129.044 49.354-23.328 5.824-57.023 12.504-68.637 13.161-2.04-.165-8.983 1.415-12.536 1.12-29.975 2.269-48.466 3.125-78.408 0z" fill="#3370FF"/><path d="M219.28 172.912a44.256 44.256 0 0 1 6.283 0c128.848 0 256.645 2.072 385.328 2.072.224 0 .443.069.626.198a303.498 303.498 0 0 1 33.133 33.856c29.054 28.89 50.704 78.968 65.51 109.503 7.371 21.091 18.491 41.26 23.757 64.752v.429a281.552 281.552 0 0 0-38.3 15.596c-37.214 18.887-54.126 32.672-85.022 63.108-16.813 16.45-31.192 31.29-53.533 52.348-7.008 6.581-24.841 23.264-25.137 22.736-5.923-10.464-106.08-206.4-307.283-360.552" fill="#00D6B9"/></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1 @@
<svg class="icon" width="30" height="30" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M906.667 266.667v640h-832v-640h832zM585.259 623.872l-94.592 94.592-94.614-94.613-218.837 218.816h626.88L585.259 623.85zm257.408-257.43-212.139 212.14 212.139 212.16v-424.32zm-704 0V790.72l212.138-212.117-212.138-212.139zm649.28-35.775H193.365l297.302 297.301 297.28-297.301z" fill="#1677FF"/></svg>

After

Width:  |  Height:  |  Size: 407 B

View File

@ -0,0 +1 @@
<svg class="icon" width="30" height="30" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M446.293 810.667C384 898.987 262.4 921.173 174.507 859.733 87.04 798.293 66.56 675.84 128 586.667a195.627 195.627 0 0 1 152.747-84.48l2.133 61.013c-38.827 2.987-76.373 23.04-100.693 58.027-42.667 61.44-29.44 144.213 29.013 185.6 58.88 40.96 141.227 25.173 183.893-35.84 13.227-19.2 20.907-40.107 23.894-61.44v-43.094l238.08-1.706 2.986-4.694c22.614-39.253 71.68-52.906 110.08-30.72a81.067 81.067 0 0 1 29.014 110.934c-22.614 38.826-72.107 52.48-110.507 30.293-17.493-9.813-29.867-25.6-35.413-43.52l-173.654.853a211.627 211.627 0 0 1-33.28 82.774m310.614-304.64c107.946 13.226 184.746 110.08 171.52 216.32-13.227 106.666-111.36 182.186-219.307 168.96a197.504 197.504 0 0 1-146.347-94.294l52.907-30.72a137.387 137.387 0 0 0 98.987 61.867c74.666 8.96 140.8-41.813 149.76-113.067 8.96-71.253-43.947-136.533-117.76-145.493-23.04-2.56-45.227.427-65.28 7.68l-36.267 18.773-110.08-203.52h-9.387a81.323 81.323 0 0 1-78.933-83.2C448 264.96 486.4 230.4 531.2 232.107c44.8 2.56 80.213 38.826 78.933 83.2-.853 18.773-8.106 35.84-19.626 49.066l81.066 149.76c26.454-8.533 55.467-11.52 85.334-8.106M352 389.973c-42.667-100.266 2.56-215.04 101.12-256.853 98.987-41.813 213.333 5.547 256 105.813 25.173 58.454 20.053 122.454-8.533 173.654l-52.907-30.72c17.92-34.56 20.907-76.8 3.84-116.48C622.507 197.12 545.28 164.267 479.147 192c-66.56 28.16-96.427 106.667-67.414 174.933 11.947 28.16 32 49.92 56.32 64.427l16.64 8.96-130.986 212.907c1.28 2.133 2.986 4.693 4.266 8.106 20.907 38.827 6.4 87.894-32.853 108.8-38.827 20.907-87.893 5.547-109.227-34.56-20.906-39.68-6.4-88.746 32.854-109.653 16.64-8.96 34.986-11.093 52.48-7.253l98.56-160.854c-20.054-18.346-37.12-41.386-47.787-67.84z" fill="#44A3F3"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1 @@
<svg class="icon" width="30" height="30" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M803.6 859.2c0 26.6-20.4 49.2-48.4 51.8-21.2 2-48.6-11-54.8-39.8-5.6-26-13.2-50.8-29-72.4-6-8.2-12.8-16-20-23.4-7.6-8-9.4-14-4.4-19.8 5-5.8 12.8-5 20.8 3.2 20.8 21 45.4 35.2 73.6 43.6 7.2 2.2 14.8 3.4 22.2 5.2 24.6 6.2 40 26.2 40 51.6z" fill="#FC6401"/><path d="M698.2 549.8c.2-28.4 20.8-50.2 49.6-52.6 25.6-2.2 50.6 17.6 55 45.2 6 36.2 22.8 66.2 48.4 92 3.2 3.2 5.6 9.2 5.2 13.8-.4 7.2-9.8 10.6-16.2 6.4-3.4-2.2-6.2-5-9-7.8-25.4-24.6-55.6-39.4-90.4-45.4-24.8-4.6-42.6-26.2-42.6-51.6z" fill="#2DBD00"/><path d="M595.4 765.2c-26.6 0-49.2-20.4-51.8-48.4-2-21.2 11-48.6 39.8-54.8 26-5.6 50.8-13.2 72.4-29 8.2-6 16-12.8 23.4-20 8-7.6 14-9.4 19.8-4.4 5.8 5 5 12.8-3.2 20.8-21 20.8-35.2 45.4-43.6 73.6-2.2 7.2-3.4 14.8-5.2 22.2-6.2 24.6-26.2 40-51.6 40z" fill="#FFCD00"/><path d="M898.8 650c28.4.2 50.2 20.8 52.6 49.6 2.2 25.6-17.6 50.6-45.2 55-36.2 6-66.2 22.8-92 48.4-3.2 3.2-9.2 5.6-13.8 5.2-7.2-.4-10.6-9.8-6.4-16.2 2.2-3.4 5-6.2 7.8-9 24.6-25.4 39.4-55.6 45.4-90.4 4.6-25 26.2-42.8 51.6-42.6z" fill="#0084F0"/><path d="M734 208.6c-110.4-108.4-244.8-139.8-392.4-100-260 70.2-340.4 340.6-209 517 6.4 8.6 7.6 24.4 5.2 35.4-7 32.4-17.4 64.2-26 96.2-4.6 17.2-7.4 34.6 8 48.4 16.6 14.8 34.2 11.8 52.2 2.6 29.6-15 59.8-29.2 89-45 19-10.4 36.2-10.8 57.6-4.8 42.8 11.8 87.2 18.4 109.6 23 43.8-.8 83.6-5.2 120.2-13.6-13.8-12-23-29.2-24.8-49-.4-5.4-.2-10.8.6-16.2C466.4 715 405 710 341 688.6c-42.2-14.2-76.8-17.8-113.4 7-3.4 2.2-7.8 2.8-24.6 8.2 33.8-58.4 8.8-95-19.6-136.6-63.4-92-50.4-210.8 24.6-296.4C330.8 131 571 131 693.8 271c52.8 60.2 73.6 135.2 61.8 206.2 28 1.6 52.8 20.6 63 47.2 32-108.8 4-228.8-84.6-315.8z" fill="#0083EF"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M939.94 459.073h-377.6V83.519H462.055v375.554h-377.6v100.284h377.6v379.646H562.34V559.357h377.6z"/></svg>

After

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

View File

@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M69.53 0h884.94v1024H69.53z" fill="none"/><path d="M79.56 259.856v503.572l435.095 250.732 434.041-250.732-1.054-502.519L514.655 10.177 79.56 259.856z" fill="#005EFD"/><path d="M93.256 268.284V755l422.452 241.25L935 753.947V268.284L513.601 25.979 93.256 268.284z" fill="#FFF"/><path d="m139.61 294.621 95.868 56.89 371.885-218.075L513.6 79.708 139.61 294.62z" fill="#005EFD"/><path d="M139.61 293.568v435.094l243.358 139.062 92.707-55.835-241.25-140.116 1.053-320.263-95.868-57.942zm651.66 56.528v321.78l95.27 55.733V294.364L645.75 154.506l-94.223 55.732L791.27 350.096z" fill="#0854C1"/><path d="m420.893 886.47 93.34 56.052L886.54 727.83l-94.388-57.11-371.259 215.75zm-35.818-469.643h49.88v280.23l93.395 55.836V278.819l-143.275 81.12v56.888z" fill="#005EFD"/><path d="m528.35 752.893 31.605-16.856V295.675l-31.605-16.856v474.074z" fill="#0854C1"/></svg>

After

Width:  |  Height:  |  Size: 963 B

View File

@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M1020.587 361.813c0-92.16-75.094-167.253-167.254-167.253h-266.24l23.894 95.573 228.693 51.2c20.48 3.414 37.547 23.894 37.547 44.374v245.76c0 20.48-17.067 40.96-37.547 44.373l-228.693 51.2-27.307 92.16h266.24c92.16 0 167.253-75.093 167.253-167.253 3.414 0 3.414-290.134 3.414-290.134zM187.733 672.427c-20.48-3.414-37.546-23.894-37.546-44.374v-245.76c0-20.48 17.066-40.96 37.546-44.373l228.694-51.2 23.893-95.573H174.08C81.92 191.147 6.827 266.24 6.827 358.4v290.133c0 92.16 75.093 167.254 167.253 167.254h266.24l-23.893-95.574c0 3.414-228.694-47.786-228.694-47.786zM402.773 460.8h218.454v88.747H402.773V460.8z" fill="#ff6b01"/></svg>

After

Width:  |  Height:  |  Size: 741 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M607.934 417.857c-6.18-6.178-12.766-11.747-19.554-16.91l-.012.011c-6.986-6.72-16.472-10.857-26.93-10.857-21.465 0-38.865 17.4-38.865 38.864a38.697 38.697 0 0 0 9.072 24.947h-.001a39.02 39.02 0 0 0 9.59 8.256c3.665 3.022 7.262 5.998 10.625 9.361l3.204 3.205c40.28 40.23 28.255 109.54-12.025 149.82l-171.88 171.83c-40.279 40.23-105.762 40.23-146.042 0l-3.23-3.232c-40.281-40.278-40.281-105.81 0-145.99l75.936-75.91c9.742-7.734 15.997-19.67 15.997-33.073 0-23.313-18.899-42.211-42.212-42.211a42.01 42.01 0 0 0-23.725 7.297c-.021-.045-.044-.088-.066-.134l-.81.757a42.455 42.455 0 0 0-8.026 7.51l-78.913 73.842c-74.178 74.23-74.178 195.633 0 269.759l3.204 3.203c74.179 74.127 195.53 74.127 269.708 0l171.83-171.88c74.075-74.175 80.356-191.185 6.281-265.312l-3.156-3.153z" fill="#333"/><path d="m855.62 165.804-3.204-3.204c-74.178-74.178-195.529-74.178-269.707 0l-171.83 171.88c-74.178 74.178-78.263 181.296-4.085 255.523l3.153 3.104c3.369 3.368 6.866 6.543 10.435 9.589a36.872 36.872 0 0 0 8.993 7.31l.077.062.012-.01a36.508 36.508 0 0 0 18.258 4.87c20.263 0 36.69-16.428 36.69-36.69 0-5.719-1.31-11.132-3.646-15.958-4.85-10.89-13.93-17.52-20.21-23.802l-3.154-3.103c-40.278-40.278-24.983-98.796 15.295-139.074l171.931-171.83c40.18-40.281 105.685-40.281 145.966 0l3.206 3.152c40.279 40.282 40.279 105.839 0 146.068l-75.687 75.738c-10.297 7.629-16.974 19.865-16.974 33.663 0 23.123 18.746 41.87 41.87 41.87a41.668 41.668 0 0 0 21.946-6.211c.048.082.093.157.14.24l1.175-1.083a42.09 42.09 0 0 0 9.529-8.793l79.766-73.603c74.233-74.177 74.233-195.53.055-269.708z" fill="#333"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M512 170.667c130.475 0 240.939 83.797 277.93 199.296a198.827 198.827 0 0 0-41.557-.598 222.293 222.293 0 0 0-49.706 10.624C668.203 309.333 596.096 259.755 512 259.755c-100.267 0-183.467 70.528-199.381 163.029a279.04 279.04 0 0 0-89.43-3.84C241.28 278.954 363.691 170.667 512 170.667z" fill="#006DFE"/><path d="M258.475 417.323c54.442 0 104.192 20.181 142.165 53.418 16.085 14.08 45.227 39.68 87.381 76.8l-7.381-6.528-61.568 60.886-54.4-54.4c-34.219-34.262-66.09-47.958-106.197-47.958a133.59 133.59 0 0 0 0 267.222c10.666 0 29.312.768 56.064 2.346l-90.454 77.142a215.893 215.893 0 0 1 34.347-428.971z" fill="#00CDD8"/><path d="M674.347 434.475a215.808 215.808 0 0 1 168.618 397.354c-15.36 6.486-38.186 15.958-63.146 16.214-72.107.597-244.182.896-516.352.938H220.8a206248.107 206248.107 0 0 0 397.013-380.714c18.262-17.579 41.131-27.264 56.534-33.792zm41.856 80.554c-9.259 3.926-23.04 9.771-34.048 20.352-30.166 29.099-109.952 105.643-239.446 229.632h53.419c148.181 0 242.773-.213 283.733-.554 15.062-.128 28.843-5.846 38.102-9.814a130.133 130.133 0 0 0-101.76-239.616z" fill="#00A2FF"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M128 479.2h768v65.6H128z"/></svg>

After

Width:  |  Height:  |  Size: 144 B

View File

@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M512 .445C229.477.445.445 229.477.445 512.003c0 282.521 229.032 511.552 511.555 511.552s511.555-229.032 511.555-511.552C1023.555 229.477 794.523.445 512 .445zm0 900.338c-47.087 0-85.26-39.416-85.26-88.035S464.913 724.71 512 724.71c47.085 0 85.257 39.417 85.257 88.038s-38.17 88.035-85.257 88.035zm66.202-316.928c0 48.621-26.946 88.035-60.183 88.035h-12.036c-33.24 0-60.183-39.414-60.183-88.035l-36.11-352.14c0-48.62 26.944-88.031 60.181-88.031h84.258c33.239 0 60.183 39.412 60.183 88.032l-36.11 352.14z"/></svg>

After

Width:  |  Height:  |  Size: 622 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

View File

@ -0,0 +1,60 @@
/**
* @description
* @example
* ```tsx
* <BaseComponent>
* <template #header-left></template>
* <template #header-right></template>
* <template #content></template>
* <template #footer-left></template>
* <template #footer-right></template>
* <template #popup></template>
* </BaseComponent>
* ```
*/
export default defineComponent({
name: 'BaseComponent',
setup(_, { slots }) {
// 获取插槽内容,支持驼峰和短横线两种命名方式
const slotHL = slots['header-left'] || slots['headerLeft']
const slotHR = slots['header-right'] || slots['headerRight']
const slotHeader = slots['header'] || slots['header']
const slotFL = slots['footer-left'] || slots['footerLeft']
const slotFR = slots['footer-right'] || slots['footerRight']
const slotFooter = slots['footer'] || slots['footer']
return () => (
<div class="flex flex-col">
{/* 头部区域 */}
{(slotHL || slotHR) && (
<div class="flex justify-between flex-wrap" style={{ rowGap: '0.8rem' }}>
<div class="flex flex-shrink-0">{slotHL && slotHL()}</div>
<div class="flex flex-shrink-0">{slotHR && slotHR()}</div>
</div>
)}
{/* 头部区域 */}
{slotHeader && <div class="flex justify-between flex-wrap w-full">{slotHeader && slotHeader()}</div>}
{/* 内容区域 */}
<div class={`w-full content ${slotHL || slotHR ? 'mt-[1.2rem]' : ''} ${slotFL || slotFR ? 'mb-[1.2rem]' : ''}`}>
{slots.content && slots.content()}
</div>
{/* 底部区域 */}
{(slotFL || slotFR) && (
<div class="flex justify-between">
<div class="flex flex-shrink-0">{slotFL && slotFL()}</div>
<div class="flex flex-shrink-0">{slotFR && slotFR()}</div>
</div>
)}
{/* 底部区域 */}
{slotFooter && <div class="flex justify-between w-full">{slotFooter()}</div>}
{/* 弹窗区域 */}
{slots.popup && slots.popup()}
</div>
)
},
})

View File

@ -0,0 +1,300 @@
import { NButton, NFormItemGi, NGrid, NSelect, NText, NSpin, NFlex } from 'naive-ui'
import { useError } from '@baota/hooks/error'
import { $t } from '@locales/index'
import { useStore } from '@layout/useStore'
import SvgIcon from '@components/svgIcon'
interface DnsProviderOption {
label: string
value: string
type: string
}
type DnsProviderType = 'btpanel' | 'aliyun' | 'ssh' | 'tencentcloud' | '1panel' | 'dns' | ''
interface DnsProviderSelectProps {
// 表单类型,用于获取不同的下拉列表
type: DnsProviderType
// 表单,用于绑定表单的值
path: string
// 表单的值
value: string
// 表单的值类型
valueType: 'value' | 'type'
// 是否为添加模式
isAddMode: boolean
// 是否禁用
disabled?: boolean
// 自定义样式
customClass?: string
}
/**
* @component DnsProviderSelect
* @description DNSDNS
*
* @example 使
* <DnsProviderSelect
* type="dns"
* path="form.dnsProvider"
* v-model:value="formValue.dnsProvider"
* valueType="value"
* :isAddMode="true"
* />
*
* @example
* <DnsProviderSelect
* type="aliyun"
* path="form.dnsProvider"
* v-model:value="formValue.dnsProvider"
* valueType="value"
* :isAddMode="false"
* />
*
* @example
* <DnsProviderSelect
* type="dns"
* path="form.dnsProvider"
* v-model:value="formValue.dnsProvider"
* valueType="value"
* :isAddMode="true"
* :disabled="true"
* />
*
* @property {string} type - DNS 'btpanel'|'aliyun'|'ssh'|'tencentcloud'|'1panel'|'dns'|''
* @property {string} path -
* @property {string} value - v-model:value
* @property {string} valueType - 'value'() 'type'
* @property {boolean} isAddMode - true
* @property {boolean} disabled - false
* @property {string} customClass - CSS
*
* @emits update:value - DNS
*/
export default defineComponent({
name: 'DnsProviderSelect',
props: {
// 表单类型,用于获取不同的下拉列表
type: {
type: String as PropType<DnsProviderType>,
default: '',
},
// 表单,用于绑定表单的值
path: {
type: String,
default: '',
},
// 表单的值
value: {
type: String,
default: '',
},
// 表单的值类型
valueType: {
type: String,
default: 'value',
},
// 是否为添加模式
isAddMode: {
type: Boolean,
default: true,
},
// 是否禁用
disabled: {
type: Boolean,
default: false,
},
// 自定义样式
customClass: {
type: String,
default: '',
},
},
emits: ['update:value'],
setup(props: DnsProviderSelectProps, { emit }) {
// 错误处理
const { handleError } = useError()
// 获取DNS提供商
const { fetchDnsProvider, dnsProvider } = useStore()
// 表单的值
const param = ref<DnsProviderOption>({
label: '',
value: '',
type: '',
})
const dnsProviderRef = ref<DnsProviderOption[]>([])
// 加载状态
const isLoading = ref(false)
// 错误信息
const errorMessage = ref('')
/**
* @description DNS
*/
const goToAddDnsProvider = () => {
window.open('/auth-api-manage', '_blank')
}
/**
*
* @param option -
* @returns VNode
*/
const renderSingleSelectTag = ({ option }: Record<string, any>): VNode => {
return (
<div class="flex items-center">
{option.label ? (
<NFlex>
<SvgIcon icon={`resources-${option.type}`} size="2rem" />
<NText>{option.label}</NText>
</NFlex>
) : (
<NText>{props.type === 'dns' ? $t('t_3_1745490735059') : $t('t_19_1745735766810')}</NText>
)}
</div>
)
}
/**
*
* @param option -
* @returns VNode
*/
const renderLabel = (option: { type: string; label: string }): VNode => {
return (
<NFlex>
<SvgIcon icon={`resources-${option.type}`} size="2rem" />
<NText>{option.label}</NText>
</NFlex>
)
}
/**
* @description
*/
const handleUpdateType = async () => {
const items = dnsProvider.value.find((item) => {
return item.value === param.value.value
})
if (items) {
param.value = {
label: items.label,
value: items.value,
type: items.type,
}
}
if (dnsProvider.value.length > 0 && param.value.value === '') {
param.value = {
label: dnsProvider.value[0]?.label || '',
value: dnsProvider.value[0]?.value || '',
type: dnsProvider.value[0]?.type || '',
}
}
emit('update:value', param.value)
}
/**
*
* @param value -
*/
const handleUpdateValue = (value: string) => {
param.value.value = value
handleUpdateType()
}
/**
* @description DNS
*/
const loadDnsProviders = async (type: DnsProviderType = '') => {
isLoading.value = true
errorMessage.value = ''
try {
await fetchDnsProvider(type)
} catch (error) {
errorMessage.value = typeof error === 'string' ? error : $t('t_0_1746760933542')
handleError(error)
} finally {
isLoading.value = false
}
}
/**
* @description
* @param pattern -
* @param option -
*/
const handleFilter = (pattern: string, option: any) => {
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,
})) || []
handleUpdateType()
},
)
// 监听父组件的值
watch(
() => props.value,
() => {
loadDnsProviders(props.type)
handleUpdateValue(props.value)
},
{ immediate: true },
)
return () => (
<NSpin show={isLoading.value}>
<NGrid cols={24} class={props.customClass}>
<NFormItemGi
span={props.isAddMode ? 13 : 24}
label={props.type === 'dns' ? $t('t_3_1745735765112') : $t('t_0_1745744902975')}
path={props.path}
>
<NSelect
class="flex-1 w-full"
options={dnsProviderRef.value}
renderLabel={renderLabel}
renderTag={renderSingleSelectTag}
filterable
filter={handleFilter}
placeholder={props.type === 'dns' ? $t('t_3_1745490735059') : $t('t_1_1745744905566')}
v-model:value={param.value.value}
onUpdateValue={handleUpdateValue}
disabled={props.disabled}
v-slots={{
empty: () => {
return (
<span class="text-[1.4rem]">
{errorMessage.value || (props.type === 'dns' ? $t('t_3_1745490735059') : $t('t_1_1745744905566'))}
</span>
)
},
}}
/>
</NFormItemGi>
{props.isAddMode && (
<NFormItemGi span={11}>
<NButton class="mx-[8px]" onClick={goToAddDnsProvider} disabled={props.disabled}>
{props.type === 'dns' ? $t('t_1_1746004861166') : $t('t_0_1745748292337')}
</NButton>
<NButton onClick={() => loadDnsProviders(props.type)} loading={isLoading.value} disabled={props.disabled}>
{$t('t_0_1746497662220')}
</NButton>
</NFormItemGi>
)}
</NGrid>
</NSpin>
)
},
})

Binary file not shown.

View File

@ -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] bg-white 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;
}

View File

@ -0,0 +1,230 @@
import { v4 as uuidv4 } from 'uuid'
import { useStore } from '@components/flowChart/useStore'
import { useController } from '@components/flowChart/useController'
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'
export default defineComponent({
name: 'BaseNode',
props: {
// 节点数据
node: {
type: Object as PropType<BaseNodeData>,
required: true, // 自读
},
},
setup(props: BaseNodeProps) {
// ====================== 基础状态数据 ======================
const { validator, validate } = useNodeValidator() // 验证器
const tempNodeId = ref(props.node.id || uuidv4()) // 节点id
const config = ref<BaseRenderNodeOptions<BaseNodeData>>(nodeOptions[props.node.type]() || {}) // 节点配置
const nodeNameRef = ref<HTMLInputElement | null>(null) // 节点名称输入框
const isShowEditNodeName = ref(false) // 是否显示编辑节点名称
const inputValue = ref(props.node.name) // 输入框值
const renderNodeContent = shallowRef() // 节点组件
const { removeNode, updateNode } = useStore()
const { handleSelectNode } = useController()
// ====================== 节点状态数据 ======================
// 错误状态
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<string> = 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<string> = computed(() => {
if (props.node.type === EXECUTE_RESULT_CONDITION) return (props.node.config?.type || '') as string
return '#FFFFFF'
})
const nodeComponents = import.meta.glob('../../task/**/index.tsx')
// ====================== 数据监听与副作用 ======================
// 监听节点数据,更新节点配置
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() // 验证器验证
const NodeComp =
nodeComponents[`../../task/${props.node.type}Node/index.tsx`] ||
import('@components/flowChart/components/base/errorNode')
renderNodeContent.value = defineAsyncComponent({
loader: NodeComp as Promise<Component>,
loadingComponent: () => <div>Loading...</div>,
errorComponent: () => <ErrorNode />,
})
},
{ immediate: true },
)
// ====================== 渲染节点内容 ======================
// // 渲染节点内容
// const renderNodeContent = defineAsyncComponent({
// loader: () =>
// (nodeComp ? nodeComp : import('@components/flowChart/components/base/errorNode')) as Promise<Component>,
// loadingComponent: () => <div>Loading...</div>,
// errorComponent: () => <ErrorNode />,
// })
// ====================== 节点操作方法 ======================
// 显示错误提示
const showErrorTips = (flag: boolean) => {
errorState.value.showTips = flag
}
// 删除节点
const removeFindNode = (ev: MouseEvent, id: string, node: BaseNodeData) => {
const validator = validate(id)
console.log(validator)
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()
}
// 点击节点
const handleNodeClick = () => {
handleSelectNode(props.node.id || '', props.node.type)
}
// ====================== 事件处理函数 ======================
// 回车保存
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 () => (
<div class={[styles.node, !isStart.value && styles.nodeArrows]}>
<div class={[styles.nodeContent, isCondition.value && styles.nodeCondition]} onClick={handleNodeClick}>
{/* 节点头部 */}
<div
class={[
styles.nodeHeader,
isCondition.value && styles.nodeConditionHeader,
!typeIcon.value ? styles.nodeHeaderBranch : '',
]}
style={{
color: config.value?.title?.color,
backgroundColor: config.value?.title?.bgColor,
}}
>
{/* 节点图标 */}
{typeIcon.value ? (
<SvgIcon
icon={typeIcon.value ? typeIcon.value : config.value?.icon?.name || ''}
class={[styles.nodeIcon, '!absolute top-[50%] left-[1rem] -mt-[.8rem]']}
color={typeIconColor.value}
/>
) : null}
{/* 节点标题 */}
<div class={styles.nodeHeaderTitle} title="点击编辑">
<div class={styles.nodeHeaderTitleInput}>
<input
ref={nodeNameRef}
value={inputValue.value}
onClick={(e) => e.stopPropagation()}
onInput={saveNodeName}
onBlur={() => (isShowEditNodeName.value = false)}
onKeyup={keyupSaveNodeName}
/>
</div>
</div>
{/* 删除按钮 */}
{isRemoved.value && (
<span
onClick={(ev) => removeFindNode(ev, tempNodeId.value, props.node)}
class="flex items-center justify-center absolute top-[50%] right-[1rem] -mt-[.9rem]"
>
<SvgIcon class={styles.nodeClose} icon="close" color={isCondition.value ? '#333' : '#FFFFFF'} />
</span>
)}
</div>
{/* 节点主体 */}
{!isCondition.value ? (
<div class={[styles.nodeBody]}>
{renderNodeContent.value &&
h(renderNodeContent.value, {
id: props.node.id,
node: props.node || {},
class: 'text-center',
})}
</div>
) : null}
{/* 错误提示 */}
{errorState.value.showTips && (
<div class={styles.nodeErrorMsg}>
<div class={styles.nodeErrorMsgBox}>
<span onMouseenter={() => showErrorTips(true)} onMouseleave={() => showErrorTips(false)}>
<SvgIcon class={styles.nodeErrorIcon} icon="tips" color="red" />
</span>
{errorState.value.message && <div class={styles.nodeErrorTips}>{errorState.value.message}</div>}
</div>
</div>
)}
</div>
{/* 添加节点组件 */}
<AddNode node={props.node} />
</div>
)
},
})

View File

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

View File

@ -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<BaseRenderNodeOptions<BranchNodeData>>(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 () => (
<div class={getContainerClass()}>
{config.value.operateNode?.addBranch && (
<div class={styles.flowConditionNodeAdd} onClick={addCondition}>
{config.value.operateNode?.addBranchTitle || '添加分支'}
</div>
)}
<div class={getBoxClass()}>
{props.node.conditionNodes?.map((condition, index: number) => (
<div
class={styles.flowNodeBranchCol}
key={index}
data-branch-index={index}
data-branches-count={props.node.conditionNodes?.length}
>
{/* 条件节点 */}
<NodeWrap node={condition} />
{/* 用来遮挡最左列的线 */}
{index === 0 && (
<div>
<div class={`${styles.coverLine} ${styles.topLeftCoverLine}`} />
<div class={`${styles.coverLine} ${styles.bottomLeftCoverLine}`} />
<div class={`${styles.rightCoverLine}`} />
</div>
)}
{/* 用来遮挡最右列的线 */}
{index === (props.node.conditionNodes?.length || 0) - 1 && (
<div>
<div class={`${styles.coverLine} ${styles.topRightCoverLine}`} />
<div class={`${styles.coverLine} ${styles.bottomRightCoverLine}`} />
<div class={`${styles.leftCoverLine}`} />
</div>
)}
</div>
))}
</div>
<AddNode node={props.node} />
</div>
)
},
})

View File

@ -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<BaseRenderNodeOptions<ExecuteResultBranchNodeData>>(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 () => (
<div class={getContainerClass()}>
{config.value.operateNode?.addBranch && (
<div class={styles.flowConditionNodeAdd} onClick={addCondition}>
{config.value.operateNode?.addBranchTitle || '添加分支'}
</div>
)}
<div class={getBoxClass()}>
{props.node.conditionNodes?.map((condition, index: number) => (
<div
class={styles.flowNodeBranchCol}
key={index}
data-branch-index={index}
data-branches-count={props.node.conditionNodes?.length}
>
{/* 条件节点 */}
<NodeWrap node={condition} />
{/* 用来遮挡最左列的线 */}
{index === 0 && (
<div>
<div class={`${styles.coverLine} ${styles.topLeftCoverLine}`} />
<div class={`${styles.coverLine} ${styles.bottomLeftCoverLine}`} />
<div class={`${styles.rightCoverLine}`} />
</div>
)}
{/* 用来遮挡最右列的线 */}
{index === (props.node.conditionNodes?.length || 0) - 1 && (
<div>
<div class={`${styles.coverLine} ${styles.topRightCoverLine}`} />
<div class={`${styles.coverLine} ${styles.bottomRightCoverLine}`} />
<div class={`${styles.leftCoverLine}`} />
</div>
)}
</div>
))}
</div>
<AddNode node={props.node} />
</div>
)
},
})

View File

@ -0,0 +1,11 @@
export default defineComponent({
name: 'EndNode',
setup() {
return () => (
<div class="flex flex-col items-center justify-center">
<div class="w-[1.5rem] h-[1.5rem] rounded-[1rem] bg-[#cacaca]"></div>
<div class="text-[#5a5e66] mb-[10rem]"></div>
</div>
)
},
})

View File

@ -0,0 +1,14 @@
import { BaseNodeData } from '@components/flowChart/types'
export default defineComponent({
name: 'BranchNode',
props: {
node: {
type: Object as () => BaseNodeData,
default: () => ({}),
},
},
setup() {
return () => <div></div>
},
})

View File

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

View File

@ -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<BaseNodeData>,
default: () => ({}),
},
},
setup(props) {
const {
isShowAddNodeSelect,
nodeSelectList,
addNodeBtnRef,
addNodeSelectRef,
addNodeSelectPostion,
showNodeSelect,
addNodeData,
itemNodeSelected,
excludeNodeSelectList,
} = useAddNodeController()
const config = ref<BaseRenderNodeOptions<BaseNodeData | BranchNodeData>>() // 节点配置
watch(
() => props.node.type,
(newVal) => {
config.value = nodeOptions[newVal]() || {}
},
)
return () => (
<div class={styles.add}>
<div
ref={addNodeBtnRef}
class={styles.addBtn}
onMouseenter={() => showNodeSelect(true, props.node.type as NodeNum)}
onMouseleave={() => showNodeSelect(false)}
>
<SvgIcon icon="plus" class={styles.addBtnIcon} color="#FFFFFF" />
{isShowAddNodeSelect.value && (
<ul
ref={addNodeSelectRef}
class={[styles.addSelectBox, addNodeSelectPostion.value === 1 ? styles.addLeft : styles.addRight]}
>
{nodeSelectList.value.map((item: NodeSelect) => {
// 判断类型是否支持添加
if (!excludeNodeSelectList.value?.includes(item.type)) {
return (
<li
key={item.type}
class={[styles.addSelectItem, item.selected && styles.addSelected]}
onClick={() => addNodeData(props.node, item.type)}
onMouseenter={itemNodeSelected}
>
<SvgIcon
icon={'flow-' + item.icon.name}
class={styles.addSelectItemIcon}
color={item.selected ? '#FFFFFF' : item.icon.color}
/>
<div class={styles.addSelectItemTitle}>{item.title.name}</div>
</li>
)
}
return null
})}
</ul>
)}
</div>
</div>
)
},
})

View File

@ -0,0 +1,82 @@
import { NEmpty } from 'naive-ui'
import type { BaseNodeData } from '@/components/flowChart/types'
import { $t } from '@locales/index'
type AsyncComponentLoader = () => Promise<Component>
/**
*
*
*/
export default defineComponent({
name: 'FlowChartDrawer',
props: {
/**
*
*/
node: {
type: Object as PropType<BaseNodeData | null>,
default: null,
},
},
setup(props) {
/**
* Map
*/
const nodeConfigComponents = shallowRef<Record<string, any>>({})
/**
*
* 使import.meta.globdrawer
* 1.
* 2.
*/
const taskDrawers: Record<string, AsyncComponentLoader> = 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 () => (
<div class=" h-full w-full bg-white transform transition-transform duration-300 flex flex-col p-[1.5rem]">
{renderConfigComponent.value}
</div>
)
},
})

View File

@ -0,0 +1,80 @@
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 } from '@components/flowChart/types'
interface NodeWrapProps {
node?: BaseNodeData | BranchNodeData
depth?: number
}
// 自定义样式
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<BaseNodeData | BranchNodeData | ExecuteResultBranchNodeData>,
default: () => ({}),
},
// 嵌套深度
depth: {
type: Number,
default: 0,
},
},
emits: ['select'],
setup(props: NodeWrapProps, { emit }) {
// 计算当前节点的嵌套深度样式类
const getDepthClass = () => {
if (props.depth && props.depth > 1) {
return props.depth > 2 ? styles.flowNodeWrapDeep : styles.flowNodeWrapNested
}
return styles.flowNodeWrap
}
// 选中节点
const handleSelect = (node: BaseNodeData | BranchNodeData | ExecuteResultBranchNodeData) => {
if (node.id) emit('select', node.id)
}
return {
getDepthClass,
handleSelect,
}
},
render() {
if (!this.node) return null
const currentDepth = this.depth || 0
const nextDepth = currentDepth + 1
return (
<div class={this.getDepthClass()}>
{/* 判断是否为分支节点或普通节点 */}
{this.node.type === BRANCH ? <BranchNode node={this.node as BranchNodeData} /> : null}
{/* 判断是否为条件节点 */}
{this.node.type === EXECUTE_RESULT_BRANCH ? (
<ConditionNode node={this.node as ExecuteResultBranchNodeData} />
) : null}
{/* 判断是否为普通节点 */}
{![BRANCH, EXECUTE_RESULT_BRANCH].includes(this.node.type) ? <BaseNode node={this.node} /> : null}
{/* 判断是否存在子节点 */}
{this.node.childNode?.type && (
<NodeWrap node={this.node.childNode} depth={nextDepth} onSelect={(nodeId) => this.$emit('select', nodeId)} />
)}
</div>
)
},
})

View File

@ -0,0 +1,103 @@
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 type { ApplyNodeConfig } from '@components/flowChart/types'
export default defineComponent({
name: 'ApplyNodeDrawer',
props: {
// 节点配置数据
node: {
type: Object as PropType<{ id: string; config: ApplyNodeConfig }>,
default: () => ({
id: '',
config: {},
}),
},
},
setup(props) {
const { updateNodeConfig, isRefreshNode } = useStore()
// 弹窗辅助
const { confirm } = useModalHooks()
// 获取表单助手函数
const { useFormInput } = useFormHooks()
// 表单参数
const param = ref<ApplyNodeConfig>(
Object.keys(props.node.config).length > 0
? props.node.config
: { domains: '', email: '', provider_id: '', provider: '', end_day: 30 },
)
// 表单渲染配置
const config = computed(() => {
// 基本选项
return [
useFormInput($t('t_17_1745227838561'), 'domains', {
placeholder: $t('t_0_1745735774005'),
}),
useFormInput($t('t_1_1745735764953'), 'email', {
placeholder: $t('t_2_1745735773668'),
}),
{
type: 'custom' as const,
render: () => {
return (
<DnsProviderSelect
type="dns"
path="provider_id"
value={param.value.provider_id}
onUpdate:value={(val: { value: string; type: string }) => {
param.value.provider_id = val.value
param.value.provider = val.type
}}
/>
)
},
},
{
type: 'custom' as const,
render: () => {
return (
<NFormItem label={$t('t_5_1745735769112')} path="end_day">
<NInputNumber
v-model:value={param.value.end_day}
showButton={false}
min={1}
class="w-[180px]"
placeholder={$t('t_6_1745735765205')}
/>
<span class="text-[1.4rem] ml-[1.2rem]">{$t('t_7_1745735768326')}</span>
</NFormItem>
)
},
},
]
})
// 创建表单实例
const { component: Form, data, example } = useForm<ApplyNodeConfig>({ defaultValue: param, config, rules })
// 确认事件触发
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 () => (
<div class="apply-node-drawer">
<Form labelPlacement="top" />
</div>
)
},
})

View File

@ -0,0 +1,61 @@
import { useNodeValidator } from '@components/flowChart/lib/verify'
import { useStore } from '@components/flowChart/useStore'
import { useThemeCssVar } from '@baota/naive-ui/theme'
import { $t } from '@locales/index'
import rules from './verify'
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) {
// 注册验证器
const { isRefreshNode } = useStore()
// 初始化节点状态
const { registerCompatValidator, validate, validationResult, unregisterValidator } = useNodeValidator()
// 主题色
const cssVar = useThemeCssVar(['warningColor', 'primaryColor'])
// 是否有效
const validColor = computed(() => {
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))
// 渲染节点状态
return () => (
<div style={cssVar.value} class="text-[12px]">
<div style={{ color: validColor.value }}>
{validationResult.value.valid ? '域名:' + props.node.config?.domains : $t('t_9_1745735765287')}
</div>
</div>
)
},
})

View File

@ -0,0 +1,62 @@
import type { FormRules, FormItemRule } from 'naive-ui'
import { isDomainGroup, isEmail } from '@baota/utils/business'
import { $t } from '@locales/index'
export default {
domains: {
required: true,
trigger: 'input',
validator: (rule: FormItemRule, value: string) => {
return new Promise<void>((resolve, reject) => {
if (!isDomainGroup(value)) {
reject(new Error($t('t_0_1745553910661')))
} else if (!value) {
reject(new Error($t('t_0_1746697487119')))
} else {
resolve()
}
})
},
},
email: {
required: true,
trigger: 'input',
validator: (rule: FormItemRule, value: string) => {
return new Promise<void>((resolve, reject) => {
if (!isEmail(value)) {
reject(new Error($t('t_1_1745553909483')))
} else if (!value) {
reject(new Error($t('t_1_1746697485188')))
} else {
resolve()
}
})
},
},
provider_id: {
required: true,
trigger: 'change',
validator: (rule: FormItemRule, value: string) => {
return new Promise<void>((resolve, reject) => {
if (!value) {
reject(new Error($t('t_3_1745490735059')))
} else {
resolve()
}
})
},
},
end_day: {
required: true,
trigger: 'input',
validator: (rule: FormItemRule, value: number) => {
return new Promise<void>((resolve, reject) => {
if (!value) {
reject(new Error($t('t_2_1745553907423')))
} else {
resolve()
}
})
},
},
} as FormRules

View File

@ -0,0 +1,293 @@
import { NButton, NCard, NStep, NSteps, NText, NTooltip } from 'naive-ui'
import { useForm, useFormHooks, 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 { DeployNodeConfig, DeployNodeInputsConfig } from '@components/flowChart/types'
import { $t } from '@locales/index'
import SvgIcon from '@components/svgIcon'
import DnsProviderSelect from '@/components/dnsProviderSelect'
import styles from './index.module.css'
import verifyRules from './verify'
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: '',
},
}),
},
},
setup(props) {
const { updateNode, updateNodeConfig, findApplyUploadNodesUp, isRefreshNode } = useStore()
// 获取表单助手函数
const { useFormInput, useFormTextarea, useFormSelect } = useFormHooks()
// 样式支持
const cssVar = useThemeCssVar(['primaryColor', 'borderColor'])
// 错误处理
const { handleError } = useError()
// 消息处理
const message = useMessage()
// 弹窗配置
const modalOptions = useModalOptions()
// 弹窗关闭
const closeModal = useModalClose()
// 部署类型选项
const deployTypeOptions = [
{ label: $t('t_5_1744958839222'), value: 'ssh' },
{ label: $t('t_10_1745735765165'), value: 'btpanel' },
{ label: $t('t_11_1745735766456'), value: 'btpanel-site' },
{ label: $t('t_12_1745735765571'), value: '1panel' },
{ label: $t('t_13_1745735766084'), value: '1panel-site' },
{ label: $t('t_14_1745735766121'), value: 'tencentcloud-cdn' },
{ label: $t('t_15_1745735768976'), value: 'tencentcloud-cos' },
{ label: $t('t_16_1745735766712'), value: 'aliyun-cdn' },
{ label: $t('t_2_1746697487164'), value: 'aliyun-oss' },
]
const certOptions = ref<{ label: string; value: string }[]>([]) // 证书选项
const current = ref(1) // 当前步骤
const next = ref(true) // 是否是下一步
const currentStatus = ref<StepStatus>('process') // 当前步骤状态
// 表单参数
const param = ref(
Object.keys(props.node.config).length > 0
? {
...props.node.config,
inputs: Array.isArray(props.node.inputs) ? props.node.inputs[0] : { fromNodeId: '', name: '' },
}
: {
provider: '',
provider_id: '',
inputs: {
fromNodeId: '',
name: '',
},
},
) as Ref<DeployNodeConfig & { inputs: DeployNodeInputsConfig }>
// 表单配置
const formConfig = computed(() => {
const config = []
config.push(
...[
{
type: 'custom' as const,
render: () => {
return (
<DnsProviderSelect
type={param.value.provider}
path="provider_id"
value={param.value.provider_id}
onUpdate:value={(val: { value: number; type: string }) => {
param.value.provider_id = val.value
}}
/>
)
},
},
],
useFormSelect($t('t_1_1745748290291'), 'inputs.fromNodeId', certOptions.value, {
onUpdateValue: (val, option: { label: string; value: string }) => {
param.value.inputs.fromNodeId = val
param.value.inputs.name = option?.label
},
}),
)
switch (param.value.provider) {
case 'ssh':
config.push(
...[
useFormInput('证书文件路径仅支持PEM格式', 'certPath', { placeholder: $t('t_30_1746667591892') }),
useFormInput('私钥文件路径', 'keyPath', { placeholder: $t('t_31_1746667593074') }),
useFormTextarea(
'前置命令',
'beforeCmd',
{ placeholder: $t('t_21_1745735769154') },
{ showRequireMark: false },
),
useFormTextarea(
'后置命令',
'afterCmd',
{ placeholder: $t('t_22_1745735767366') },
{ showRequireMark: false },
),
],
)
break
case 'btpanel-site':
config.push(...[useFormInput('站点名称', 'siteName', { placeholder: $t('t_23_1745735766455') })])
break
case '1panel-site':
config.push(...[useFormInput('站点ID', 'site_id', { placeholder: $t('t_24_1745735766826') })])
break
case 'tencentcloud-cdn':
case 'aliyun-cdn':
config.push(...[useFormInput('域名', 'domain', { placeholder: $t('t_0_1744958839535') })])
break
case 'tencentcloud-cos':
case 'aliyun-oss':
config.push(...[useFormInput('域名', 'domain', { placeholder: $t('t_0_1744958839535') })])
config.push(...[useFormInput('区域', 'region', { placeholder: $t('t_25_1745735766651') })])
config.push(...[useFormInput('存储桶', 'bucket', { placeholder: $t('t_26_1745735767144') })])
break
}
return config
})
/**
* @description
* @returns
*/
const nextStep = async () => {
if (!param.value.provider) return message.error($t('t_19_1745735766810'))
// 加载证书来源选项
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 && param.value.inputs.fromNodeId)) {
param.value.inputs = {} as DeployNodeInputsConfig
param.value.inputs.name = certOptions.value[0]?.label || ''
param.value.inputs.fromNodeId = certOptions.value[0]?.value || ''
}
current.value++
next.value = false
}
/**
* @description
* @returns
*/
const prevStep = () => {
current.value--
next.value = true
param.value.provider_id = ''
param.value.provider = ''
}
// 表单组件
const { component: Form, example } = useForm<DeployNodeConfig>({
config: formConfig,
defaultValue: param,
rules: verifyRules,
})
/**
* @description
* @returns
*/
const submit = async () => {
try {
await example.value?.validate()
const tempData = param.value
const inputs = tempData.inputs
console.log(inputs, 'inputs', props.node)
updateNode(
props.node.id,
{
inputs: [inputs],
config: {},
},
false,
)
delete tempData.inputs
updateNodeConfig(props.node.id, {
...tempData,
})
isRefreshNode.value = props.node.id
closeModal()
} catch (error) {
handleError(error)
}
}
// 初始化
onMounted(() => {
// 隐藏底部按钮
modalOptions.value.footer = false
// 如果已经选择了部署类型,则跳转到下一步
if (param.value.provider) {
if (props.node.inputs) param.value.inputs = props.node.inputs
nextStep()
}
})
return () => (
<div class={styles.container} style={cssVar.value}>
<NSteps size="small" current={current.value} status={currentStatus.value}>
<NStep title={$t('t_28_1745735766626')} description={$t('t_19_1745735766810')}></NStep>
<NStep title={$t('t_29_1745735768933')} description={$t('t_2_1745738969878')}></NStep>
</NSteps>
{current.value === 1 && (
<div class={styles.cardContainer}>
{deployTypeOptions.map((item) => (
<div
key={item.value}
class={`${styles.optionCard} ${param.value.provider === item.value ? styles.optionCardSelected : ''}`}
onClick={() => {
param.value.provider = item.value
}}
>
<NCard contentClass={styles.cardContent} hoverable bordered={false}>
<SvgIcon
icon={`resources-${item.value.replace(/-[a-z]+$/, '')}`}
size="2rem"
class={`${styles.icon} ${param.value.provider === item.value ? styles.iconSelected : ''}`}
/>
<NText type={param.value.provider === item.value ? 'primary' : 'default'}>{item.label}</NText>
</NCard>
</div>
))}
</div>
)}
{current.value === 2 && (
<NCard class={styles.formContainer}>
<Form labelPlacement="top" />
</NCard>
)}
<div class={styles.footer}>
<NButton class={styles.footerButton} onClick={closeModal}>
{$t('t_4_1744870861589')}
</NButton>
<NTooltip
trigger="hover"
disabled={!!param.value.provider}
v-slots={{
trigger: () => (
<NButton
type={next.value ? 'primary' : 'default'}
class={styles.footerButton}
disabled={!param.value.provider}
onClick={next.value ? nextStep : prevStep}
>
{next.value ? $t('t_27_1745735764546') : $t('t_0_1745738961258')}
</NButton>
),
}}
>
{next.value ? $t('t_4_1745765868807') : null}
</NTooltip>
{!next.value && (
<NButton type="primary" onClick={submit}>
{$t('t_1_1745738963744')}
</NButton>
)}
</div>
</div>
)
},
})

View File

@ -0,0 +1,70 @@
/* Deploy Node Drawer Styles */
/* Card container styles */
.cardContainer {
@apply grid grid-cols-3 gap-4 mt-[2.4rem];
}
/* Option card styles */
.optionCard {
@apply flex items-center justify-center rounded-[0.4rem] transition-all border-[1px] border-transparent;
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-xs 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;
}
/* Icon styles */
.icon {
@apply mb-[0.4rem];
}
.iconSelected {
color: var(--n-primary-color);
}
/* Footer styles */
.footer {
@apply flex justify-end absolute right-[1.2rem] -bottom-[1.2rem];
}
.footerButton {
@apply mr-[0.8rem];
}
/* Main container */
.container {
@apply pb-[3.2rem];
}
/* Form container */
.formContainer {
@apply mt-[2.4rem];
}

Some files were not shown because too many files have changed in this diff Show More