完成前端部署内容
This commit is contained in:
129
.agent/workflows/deploy-workflow.md
Normal file
129
.agent/workflows/deploy-workflow.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# 项目部署流程设计文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档描述了项目管理模块与 1Panel 服务器的集成方案,采用**前后端分离架构**:
|
||||
|
||||
- **前端**:负责 UI 交互,调用后端 API
|
||||
- **后端**:存储项目数据到数据库,调用 1Panel API 执行部署
|
||||
|
||||
## 架构设计
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ 架构示意图 │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ 前端 │──────│ 后端 API │──────│ 1Panel │ │
|
||||
│ │ (Vue.js) │ HTTP │ (Spring) │ HTTP │ 服务器 │ │
|
||||
│ └─────────────┘ └──────────────┘ └──────────────┘ │
|
||||
│ │ │ │
|
||||
│ │ ▼ │
|
||||
│ │ ┌──────────┐ │
|
||||
│ │ │ MySQL │ │
|
||||
│ │ │ 数据库 │ │
|
||||
│ │ └──────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 后端实现
|
||||
|
||||
### 1. 实体类扩展 (`PlatformProject.java`)
|
||||
|
||||
新增字段:
|
||||
|
||||
- `shortName` - 项目简称
|
||||
- `projectGroup` - 项目分组
|
||||
- `logo` - Logo 文字
|
||||
- `color` - 主题色
|
||||
- `domain` - 绑定域名
|
||||
- `enableHttps` - 是否启用 HTTPS
|
||||
- `panelWebsiteId` - 1Panel 网站 ID
|
||||
- `panelSslId` - 1Panel 证书 ID
|
||||
- `lastDeployTime` - 最后部署时间
|
||||
- `lastDeployStatus` - 最后部署状态
|
||||
- `lastDeployMessage` - 最后部署消息
|
||||
|
||||
### 2. DTO 类
|
||||
|
||||
- `DeployRequest.java` - 部署请求参数
|
||||
- `DeployResult.java` - 部署结果
|
||||
|
||||
### 3. 服务类
|
||||
|
||||
- `OnePanelService.java` - 1Panel 服务接口
|
||||
- `OnePanelServiceImpl.java` - 1Panel 服务实现
|
||||
|
||||
### 4. 控制器 (`PlatformProjectController.java`)
|
||||
|
||||
新增接口:
|
||||
| 接口 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/platform/project/deploy` | POST | 部署项目 |
|
||||
| `/platform/project/{serverId}/websites` | GET | 获取网站列表 |
|
||||
| `/platform/project/{serverId}/certificates` | GET | 获取证书列表 |
|
||||
| `/platform/project/{serverId}/acme-accounts` | GET | 获取 Acme 账户 |
|
||||
| `/platform/project/{serverId}/dns-accounts` | GET | 获取 DNS 账户 |
|
||||
|
||||
### 5. 数据库迁移
|
||||
|
||||
执行 `src/main/resources/db/migration_project_deploy.sql`
|
||||
|
||||
## 前端实现
|
||||
|
||||
### 1. API 封装 (`src/api/project.ts`)
|
||||
|
||||
- `deployProject` - 部署项目
|
||||
- `getServerAcmeAccounts` - 获取 Acme 账户
|
||||
- `getServerDnsAccounts` - 获取 DNS 账户
|
||||
|
||||
### 2. 项目管理页面
|
||||
|
||||
- 部署按钮
|
||||
- 部署确认弹窗
|
||||
- 部署进度抽屉
|
||||
|
||||
## 完整部署流程
|
||||
|
||||
1. 前端:用户点击"部署"按钮
|
||||
2. 前端:打开部署确认弹窗,配置 HTTPS 选项
|
||||
3. 前端:调用后端 POST /platform/project/deploy
|
||||
4. 后端:从数据库获取项目和服务器信息
|
||||
5. 后端:检查/创建网站
|
||||
6. 后端:如启用 HTTPS,检查/申请证书
|
||||
7. 后端:配置网站 HTTPS
|
||||
8. 后端:更新项目状态到数据库
|
||||
9. 前端:显示部署结果
|
||||
|
||||
## 1Panel Token 生成算法
|
||||
|
||||
```java
|
||||
// Token = md5('1panel' + API-Key + UnixTimestamp)
|
||||
String rawToken = "1panel" + apiKey + timestamp;
|
||||
String token = DigestUtils.md5DigestAsHex(rawToken.getBytes());
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 服务器配置
|
||||
|
||||
在 `platform_server` 表中配置:
|
||||
|
||||
- `panel_port` - 1Panel 端口(默认 42588)
|
||||
- `panel_api_key` - 1Panel API 密钥
|
||||
|
||||
### 项目配置
|
||||
|
||||
创建项目时配置:
|
||||
|
||||
- `domain` - 绑定域名
|
||||
- `serverId` - 目标服务器
|
||||
- `deployPath` - 部署路径
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
1. API 密钥存储在后端数据库,不暴露给前端
|
||||
2. 每次请求生成新 Token,防止重放攻击
|
||||
3. 建议在 1Panel 中配置 API 白名单
|
||||
2
.env
2
.env
@@ -4,3 +4,5 @@ VITE_APP_TITLE=楠溪屿后台管理系统
|
||||
# API基础URL
|
||||
VITE_API_BASE_URL=/api
|
||||
|
||||
# 1Panel API 配置
|
||||
VITE_1PANEL_API_KEY=KBya4QxxPB3b8eYCSECOfhgWpXE0ZhuI
|
||||
|
||||
@@ -707,6 +707,416 @@ export async function decompressFile(path: string, dst: string): Promise<void> {
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 网站管理 API ====================
|
||||
|
||||
/**
|
||||
* 创建网站参数
|
||||
*/
|
||||
export interface CreateWebsiteParams {
|
||||
primaryDomain: string // 主域名
|
||||
alias: string // 网站别名(唯一标识)
|
||||
type: 'static' | 'proxy' // 网站类型:static=静态站点, proxy=反向代理
|
||||
remark?: string // 备注
|
||||
|
||||
// 静态站点配置
|
||||
siteDir?: string // 站点目录(静态站点时使用)
|
||||
|
||||
// 反向代理配置
|
||||
proxy?: string // 反向代理地址(如 http://127.0.0.1:3000)
|
||||
proxyType?: string // 代理类型
|
||||
|
||||
// 其他配置
|
||||
otherDomains?: string // 其他域名,逗号分隔
|
||||
IPV6?: boolean // 是否启用 IPv6
|
||||
|
||||
// 运行时配置(静态站点)
|
||||
runtimeID?: number // 运行时 ID (OpenResty)
|
||||
runtimeType?: string // 运行时类型
|
||||
|
||||
// 额外配置
|
||||
appType?: string // 应用类型
|
||||
webSiteGroupID?: number // 网站分组ID
|
||||
port?: number // 端口
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建网站
|
||||
* @param params 创建参数
|
||||
*/
|
||||
export async function createWebsite(params: CreateWebsiteParams): Promise<number> {
|
||||
const response = await panelService.post<PanelResponse<{ id: number }>>('/websites', {
|
||||
primaryDomain: params.primaryDomain,
|
||||
alias: params.alias,
|
||||
type: params.type || 'static',
|
||||
remark: params.remark || '',
|
||||
siteDir: params.siteDir || '',
|
||||
proxy: params.proxy || '',
|
||||
proxyType: params.proxyType || 'http',
|
||||
otherDomains: params.otherDomains || '',
|
||||
IPV6: params.IPV6 || false,
|
||||
runtimeID: params.runtimeID || 0,
|
||||
runtimeType: params.runtimeType || '',
|
||||
appType: params.appType || 'new',
|
||||
webSiteGroupID: params.webSiteGroupID || 0,
|
||||
port: params.port || 80
|
||||
})
|
||||
return response.data.data.id
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网站详情
|
||||
* @param id 网站ID
|
||||
*/
|
||||
export async function getWebsiteDetail(id: number): Promise<Website & { domains: any[], path: string }> {
|
||||
const response = await panelService.get<PanelResponse<Website & { domains: any[], path: string }>>(`/websites/${id}`)
|
||||
return response.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除网站
|
||||
* @param id 网站ID
|
||||
*/
|
||||
export async function deleteWebsite(id: number, deleteApp: boolean = false, deleteBackup: boolean = false): Promise<void> {
|
||||
await panelService.post('/websites/del', {
|
||||
id,
|
||||
deleteApp,
|
||||
deleteBackup,
|
||||
forceDelete: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新网站
|
||||
* @param id 网站ID
|
||||
* @param params 更新参数
|
||||
*/
|
||||
export async function updateWebsite(id: number, params: Partial<CreateWebsiteParams>): Promise<void> {
|
||||
await panelService.post(`/websites/update`, {
|
||||
id,
|
||||
...params
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 网站 HTTPS 配置 API ====================
|
||||
|
||||
/**
|
||||
* 网站 HTTPS 配置参数
|
||||
*/
|
||||
export interface WebsiteHTTPSConfig {
|
||||
websiteId: number // 网站ID
|
||||
enable: boolean // 是否启用 HTTPS
|
||||
type: string // 证书类型: 'select'=选择已有证书, 'import'=导入证书
|
||||
websiteSSLId?: number // 已有证书ID(type=select时使用)
|
||||
httpConfig: string // HTTP配置: 'HTTPSOnly'=仅HTTPS, 'HTTPAlso'=同时HTTP, 'HTTPToHTTPS'=HTTP跳转HTTPS
|
||||
SSLProtocol?: string[] // SSL协议版本
|
||||
algorithm?: string // 加密算法
|
||||
}
|
||||
|
||||
/**
|
||||
* 为网站配置 HTTPS
|
||||
* @param config HTTPS配置
|
||||
*/
|
||||
export async function configureWebsiteHTTPS(config: WebsiteHTTPSConfig): Promise<void> {
|
||||
await panelService.post(`/websites/${config.websiteId}/https`, {
|
||||
enable: config.enable,
|
||||
type: config.type,
|
||||
websiteSSLId: config.websiteSSLId || 0,
|
||||
httpConfig: config.httpConfig || 'HTTPToHTTPS',
|
||||
SSLProtocol: config.SSLProtocol || ['TLSv1.2', 'TLSv1.3'],
|
||||
algorithm: config.algorithm || 'ECDHE-ECDSA-AES128-GCM-SHA256'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网站 HTTPS 配置
|
||||
* @param websiteId 网站ID
|
||||
*/
|
||||
export async function getWebsiteHTTPSConfig(websiteId: number): Promise<any> {
|
||||
const response = await panelService.get<PanelResponse<any>>(`/websites/${websiteId}/https`)
|
||||
return response.data.data
|
||||
}
|
||||
|
||||
// ==================== 部署相关 API ====================
|
||||
|
||||
/**
|
||||
* 上传文件到指定目录
|
||||
* @param path 目标路径(目录)
|
||||
* @param file 文件
|
||||
* @param overwrite 是否覆盖
|
||||
*/
|
||||
export async function uploadFileToPath(path: string, file: File, overwrite: boolean = true): Promise<void> {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('path', path)
|
||||
formData.append('overwrite', String(overwrite))
|
||||
|
||||
await panelService.post('/files/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
timeout: 300000 // 上传超时时间 5 分钟
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 大文件分块上传
|
||||
*/
|
||||
export interface ChunkUploadParams {
|
||||
path: string // 目标路径
|
||||
filename: string // 文件名
|
||||
chunk: Blob // 分块数据
|
||||
chunkIndex: number // 当前分块索引
|
||||
chunkCount: number // 总分块数
|
||||
}
|
||||
|
||||
/**
|
||||
* 分块上传文件
|
||||
* @param params 分块上传参数
|
||||
*/
|
||||
export async function uploadChunk(params: ChunkUploadParams): Promise<void> {
|
||||
const formData = new FormData()
|
||||
formData.append('chunk', params.chunk, params.filename)
|
||||
formData.append('path', params.path)
|
||||
formData.append('chunkIndex', String(params.chunkIndex))
|
||||
formData.append('chunkCount', String(params.chunkCount))
|
||||
|
||||
await panelService.post('/files/chunkupload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
timeout: 60000
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并分块文件
|
||||
* @param path 目标路径
|
||||
* @param filename 文件名
|
||||
*/
|
||||
export async function mergeChunks(path: string, filename: string): Promise<void> {
|
||||
await panelService.post('/files/merge', {
|
||||
path,
|
||||
filename
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件或目录
|
||||
* @param path 文件或目录路径
|
||||
*/
|
||||
export async function deleteFile(path: string): Promise<void> {
|
||||
await panelService.post('/files/del', {
|
||||
path,
|
||||
isDir: false,
|
||||
forceDelete: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建目录
|
||||
* @param path 目录路径
|
||||
*/
|
||||
export async function createDirectory(path: string): Promise<void> {
|
||||
await panelService.post('/files/mkdir', {
|
||||
path,
|
||||
mode: 755
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件/目录是否存在
|
||||
* @param path 路径
|
||||
*/
|
||||
export async function checkPathExists(path: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await panelService.post<PanelResponse<FileInfo[]>>('/files/search', {
|
||||
path: path.substring(0, path.lastIndexOf('/')),
|
||||
expand: true,
|
||||
page: 1,
|
||||
pageSize: 100
|
||||
})
|
||||
const files = response.data.data || []
|
||||
const targetName = path.substring(path.lastIndexOf('/') + 1)
|
||||
return files.some(f => f.name === targetName)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 完整部署流程 API ====================
|
||||
|
||||
/**
|
||||
* 项目部署参数
|
||||
*/
|
||||
export interface ProjectDeployParams {
|
||||
// 项目信息
|
||||
projectId: string // 项目ID
|
||||
projectName: string // 项目名称
|
||||
domain: string // 绑定域名
|
||||
|
||||
// 部署配置
|
||||
deployPath: string // 部署路径
|
||||
enableHttps: boolean // 是否启用HTTPS
|
||||
|
||||
// SSL 配置(启用HTTPS时需要)
|
||||
acmeAccountId?: number // Acme账户ID
|
||||
dnsAccountId?: number // DNS账户ID
|
||||
|
||||
// 可选配置
|
||||
otherDomains?: string // 其他域名
|
||||
createIfNotExist?: boolean // 如果网站不存在则创建
|
||||
}
|
||||
|
||||
/**
|
||||
* 部署结果
|
||||
*/
|
||||
export interface DeployResult {
|
||||
success: boolean
|
||||
websiteId?: number
|
||||
sslCertificateId?: number
|
||||
message: string
|
||||
steps: {
|
||||
step: string
|
||||
status: 'success' | 'failed' | 'skipped'
|
||||
message?: string
|
||||
}[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查域名对应的网站是否已存在
|
||||
* @param domain 域名
|
||||
*/
|
||||
export async function findWebsiteByDomain(domain: string): Promise<Website | null> {
|
||||
try {
|
||||
const websites = await getAllWebsites()
|
||||
return websites.find(w => w.primaryDomain === domain) || null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行项目部署
|
||||
* 此函数封装了完整的部署流程:
|
||||
* 1. 检查/创建网站
|
||||
* 2. 检查/申请SSL证书
|
||||
* 3. 配置HTTPS
|
||||
*
|
||||
* @param params 部署参数
|
||||
*/
|
||||
export async function deployProject(params: ProjectDeployParams): Promise<DeployResult> {
|
||||
const result: DeployResult = {
|
||||
success: false,
|
||||
message: '',
|
||||
steps: []
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: 检查网站是否存在
|
||||
console.log(`[部署] 检查网站是否存在: ${params.domain}`)
|
||||
let website = await findWebsiteByDomain(params.domain)
|
||||
|
||||
if (!website) {
|
||||
if (!params.createIfNotExist) {
|
||||
result.steps.push({ step: '检查网站', status: 'failed', message: '网站不存在' })
|
||||
result.message = `域名 ${params.domain} 对应的网站不存在,请先在 1Panel 中创建`
|
||||
return result
|
||||
}
|
||||
|
||||
// 创建静态网站
|
||||
console.log(`[部署] 创建静态网站: ${params.domain}`)
|
||||
const websiteId = await createWebsite({
|
||||
primaryDomain: params.domain,
|
||||
alias: params.domain.replace(/\./g, '_'),
|
||||
type: 'static',
|
||||
siteDir: params.deployPath,
|
||||
otherDomains: params.otherDomains || '',
|
||||
remark: `项目: ${params.projectName}`,
|
||||
port: 80
|
||||
})
|
||||
|
||||
result.websiteId = websiteId
|
||||
result.steps.push({ step: '创建网站', status: 'success', message: `网站ID: ${websiteId}` })
|
||||
|
||||
// 重新获取网站信息
|
||||
const websites = await getAllWebsites()
|
||||
website = websites.find(w => w.id === websiteId) || null
|
||||
} else {
|
||||
result.websiteId = website.id
|
||||
result.steps.push({ step: '检查网站', status: 'success', message: '网站已存在' })
|
||||
}
|
||||
|
||||
// Step 2: 如果启用 HTTPS,检查/申请证书
|
||||
if (params.enableHttps && website) {
|
||||
console.log(`[部署] 检查SSL证书: ${params.domain}`)
|
||||
|
||||
// 查找已有证书
|
||||
let certificate = await findCertificateByDomain(params.domain, params.acmeAccountId || 1)
|
||||
|
||||
if (!certificate) {
|
||||
// 需要申请证书
|
||||
if (params.acmeAccountId && params.dnsAccountId) {
|
||||
console.log(`[部署] 申请SSL证书: ${params.domain}`)
|
||||
await applySSLCertificate({
|
||||
primaryDomain: params.domain,
|
||||
otherDomains: params.otherDomains || '',
|
||||
provider: 'dnsAccount',
|
||||
acmeAccountId: params.acmeAccountId,
|
||||
dnsAccountId: params.dnsAccountId,
|
||||
autoRenew: true,
|
||||
keyType: 'P256',
|
||||
apply: true
|
||||
})
|
||||
|
||||
// 等待证书申请完成(最多等待 120 秒)
|
||||
for (let i = 0; i < 24; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 5000))
|
||||
certificate = await findCertificateByDomain(params.domain, params.acmeAccountId)
|
||||
if (certificate) break
|
||||
}
|
||||
|
||||
if (certificate) {
|
||||
result.sslCertificateId = certificate.id
|
||||
result.steps.push({ step: '申请SSL证书', status: 'success', message: `证书ID: ${certificate.id}` })
|
||||
} else {
|
||||
result.steps.push({ step: '申请SSL证书', status: 'failed', message: '证书申请超时或失败' })
|
||||
}
|
||||
} else {
|
||||
result.steps.push({ step: '申请SSL证书', status: 'skipped', message: '未配置 Acme/DNS 账户' })
|
||||
}
|
||||
} else {
|
||||
result.sslCertificateId = certificate.id
|
||||
result.steps.push({ step: '检查SSL证书', status: 'success', message: '证书已存在' })
|
||||
}
|
||||
|
||||
// Step 3: 配置 HTTPS
|
||||
if (result.sslCertificateId && website) {
|
||||
console.log(`[部署] 配置HTTPS: ${params.domain}`)
|
||||
await configureWebsiteHTTPS({
|
||||
websiteId: website.id,
|
||||
enable: true,
|
||||
type: 'select',
|
||||
websiteSSLId: result.sslCertificateId,
|
||||
httpConfig: 'HTTPToHTTPS'
|
||||
})
|
||||
result.steps.push({ step: '配置HTTPS', status: 'success' })
|
||||
}
|
||||
} else {
|
||||
result.steps.push({ step: '配置HTTPS', status: 'skipped', message: '未启用HTTPS' })
|
||||
}
|
||||
|
||||
result.success = true
|
||||
result.message = '部署流程完成'
|
||||
return result
|
||||
|
||||
} catch (error: any) {
|
||||
result.success = false
|
||||
result.message = error.message || '部署过程中发生错误'
|
||||
result.steps.push({ step: '异常', status: 'failed', message: error.message })
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// 导出 panelService 实例供其他地方使用
|
||||
export default panelService
|
||||
|
||||
|
||||
168
src/api/certificate.ts
Normal file
168
src/api/certificate.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* 证书管理 API
|
||||
* 对接后端接口,管理SSL/TLS证书
|
||||
*/
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
export type CertificateStatus = 'valid' | 'expired' | 'pending' | 'error'
|
||||
|
||||
export interface CertificateInfo {
|
||||
id: number
|
||||
serverId: number
|
||||
serverName?: string
|
||||
panelSslId?: number
|
||||
primaryDomain: string
|
||||
otherDomains?: string
|
||||
cn?: string
|
||||
organization?: string
|
||||
provider?: string
|
||||
acmeAccountId?: number
|
||||
acmeAccountEmail?: string
|
||||
dnsAccountId?: number
|
||||
dnsAccountName?: string
|
||||
dnsAccountType?: string
|
||||
keyType?: string
|
||||
status: CertificateStatus
|
||||
autoRenew?: boolean
|
||||
startDate?: string
|
||||
expireDate?: string
|
||||
certContent?: string
|
||||
keyContent?: string
|
||||
description?: string
|
||||
lastSyncTime?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export interface CertificateQueryParams {
|
||||
page: number
|
||||
pageSize: number
|
||||
serverId?: number
|
||||
keyword?: string
|
||||
}
|
||||
|
||||
export interface CertificateApplyRequest {
|
||||
serverId: number
|
||||
primaryDomain: string
|
||||
otherDomains?: string
|
||||
acmeAccountId: number
|
||||
dnsAccountId: number
|
||||
keyType?: string
|
||||
autoRenew?: boolean
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface CertificateApplyStep {
|
||||
step: string
|
||||
status: 'success' | 'failed' | 'skipped'
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface CertificateApplyResult {
|
||||
success: boolean
|
||||
message: string
|
||||
panelSslId?: number
|
||||
certificateId?: number
|
||||
steps: CertificateApplyStep[]
|
||||
}
|
||||
|
||||
export interface AcmeAccount {
|
||||
id: number
|
||||
email: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
export interface DnsAccount {
|
||||
id: number
|
||||
name: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
export interface WebsiteInfo {
|
||||
id: number
|
||||
primaryDomain: string
|
||||
alias?: string
|
||||
}
|
||||
|
||||
// ==================== 证书管理 API ====================
|
||||
|
||||
/**
|
||||
* 获取证书列表(分页)
|
||||
*/
|
||||
export async function getCertificateList(params: CertificateQueryParams) {
|
||||
const res = await request.get<{ records: CertificateInfo[]; total: number }>('/platform/certificate/list', { params })
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取证书详情(包含证书内容和私钥)
|
||||
*/
|
||||
export async function getCertificateDetail(id: number) {
|
||||
const res = await request.get<CertificateInfo>(`/platform/certificate/${id}`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除证书
|
||||
*/
|
||||
export async function deleteCertificate(id: number) {
|
||||
await request.delete<void>(`/platform/certificate/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新证书设置
|
||||
*/
|
||||
export async function updateCertificateSettings(id: number, autoRenew?: boolean, description?: string) {
|
||||
await request.put<void>(`/platform/certificate/${id}/settings`, null, {
|
||||
params: { autoRenew, description }
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 证书申请 API ====================
|
||||
|
||||
/**
|
||||
* 申请证书
|
||||
* 调用1Panel API申请SSL证书
|
||||
*/
|
||||
export async function applyCertificate(data: CertificateApplyRequest): Promise<CertificateApplyResult> {
|
||||
const res = await request.post<CertificateApplyResult>('/platform/certificate/apply', data)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
// ==================== 证书同步 API ====================
|
||||
|
||||
/**
|
||||
* 从1Panel同步证书列表
|
||||
*/
|
||||
export async function syncCertificates(serverId: number) {
|
||||
const res = await request.post<{ syncCount: number; message: string }>(`/platform/certificate/sync/${serverId}`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
// ==================== 1Panel 账户查询 API ====================
|
||||
|
||||
/**
|
||||
* 获取Acme账户列表
|
||||
*/
|
||||
export async function getAcmeAccounts(serverId: number): Promise<AcmeAccount[]> {
|
||||
const res = await request.get<AcmeAccount[]>(`/platform/certificate/acme-accounts/${serverId}`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取DNS账户列表
|
||||
*/
|
||||
export async function getDnsAccounts(serverId: number): Promise<DnsAccount[]> {
|
||||
const res = await request.get<DnsAccount[]>(`/platform/certificate/dns-accounts/${serverId}`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网站列表
|
||||
*/
|
||||
export async function getWebsites(serverId: number): Promise<WebsiteInfo[]> {
|
||||
const res = await request.get<WebsiteInfo[]>(`/platform/certificate/websites/${serverId}`)
|
||||
return res.data.data
|
||||
}
|
||||
175
src/api/domain.ts
Normal file
175
src/api/domain.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* 域名管理 API
|
||||
* 对接后端接口,管理域名的增删改查及部署
|
||||
*/
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
export type DomainStatus = 'active' | 'pending' | 'expired' | 'error'
|
||||
export type DnsStatus = 'resolved' | 'unresolved' | 'checking'
|
||||
export type SslStatus = 'valid' | 'expiring' | 'expired' | 'none'
|
||||
export type DeployStatus = 'not_deployed' | 'deploying' | 'deployed' | 'failed'
|
||||
|
||||
export interface DomainInfo {
|
||||
id: number
|
||||
domain: string
|
||||
projectId?: number
|
||||
projectName?: string
|
||||
serverId?: number
|
||||
serverName?: string
|
||||
serverIp?: string
|
||||
status: DomainStatus
|
||||
dnsStatus: DnsStatus
|
||||
dnsRecords?: string
|
||||
sslStatus: SslStatus
|
||||
sslExpireDate?: string
|
||||
certificateId?: number
|
||||
certificateName?: string
|
||||
nginxConfigPath?: string
|
||||
proxyPass?: string
|
||||
port?: number
|
||||
enableHttps?: boolean
|
||||
forceHttps?: boolean
|
||||
description?: string
|
||||
// 1Panel 相关字段
|
||||
panelWebsiteId?: number
|
||||
panelSslId?: number
|
||||
sitePath?: string
|
||||
alias?: string
|
||||
deployStatus?: DeployStatus
|
||||
lastDeployTime?: string
|
||||
lastDeployMessage?: string
|
||||
// 时间
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export interface DomainQueryParams {
|
||||
page: number
|
||||
pageSize: number
|
||||
keyword?: string
|
||||
status?: DomainStatus
|
||||
sslStatus?: SslStatus
|
||||
serverId?: number
|
||||
}
|
||||
|
||||
export interface DomainDeployRequest {
|
||||
domainId: number
|
||||
enableHttps?: boolean
|
||||
acmeAccountId?: number
|
||||
dnsAccountId?: number
|
||||
createIfNotExist?: boolean
|
||||
}
|
||||
|
||||
export interface DomainDeployStep {
|
||||
step: string
|
||||
status: 'success' | 'failed' | 'skipped'
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface DomainDeployResult {
|
||||
success: boolean
|
||||
message: string
|
||||
websiteId?: number
|
||||
sslCertificateId?: number
|
||||
steps: DomainDeployStep[]
|
||||
}
|
||||
|
||||
// ==================== 域名管理 API ====================
|
||||
|
||||
/**
|
||||
* 获取域名列表(分页)
|
||||
*/
|
||||
export async function getDomainList(params: DomainQueryParams) {
|
||||
const res = await request.get<{ records: DomainInfo[]; total: number }>('/platform/domain/list', { params })
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有域名
|
||||
*/
|
||||
export async function getAllDomains() {
|
||||
const res = await request.get<DomainInfo[]>('/platform/domain/all')
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取域名详情
|
||||
*/
|
||||
export async function getDomainById(id: number) {
|
||||
const res = await request.get<DomainInfo>(`/platform/domain/${id}`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建域名
|
||||
*/
|
||||
export async function createDomain(data: Partial<DomainInfo>) {
|
||||
const res = await request.post<DomainInfo>('/platform/domain', data)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新域名
|
||||
*/
|
||||
export async function updateDomain(id: number, data: Partial<DomainInfo>) {
|
||||
await request.put<void>(`/platform/domain/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除域名
|
||||
*/
|
||||
export async function deleteDomain(id: number) {
|
||||
await request.delete<void>(`/platform/domain/${id}`)
|
||||
}
|
||||
|
||||
// ==================== 域名部署 API ====================
|
||||
|
||||
/**
|
||||
* 部署域名到 1Panel
|
||||
* 包含:创建网站、申请证书、配置HTTPS
|
||||
*/
|
||||
export async function deployDomain(data: DomainDeployRequest): Promise<DomainDeployResult> {
|
||||
const res = await request.post<DomainDeployResult>('/platform/domain/deploy', data)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查域名 DNS 解析状态
|
||||
*/
|
||||
export async function checkDomainDns(id: number) {
|
||||
const res = await request.post<{ resolved: boolean; records?: string[] }>(`/platform/domain/${id}/check-dns`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步域名信息到 1Panel
|
||||
* 从 1Panel 获取最新的网站和证书信息并更新本地数据
|
||||
*/
|
||||
export async function syncDomainFromPanel(id: number) {
|
||||
const res = await request.post<DomainInfo>(`/platform/domain/${id}/sync`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取域名统计信息
|
||||
*/
|
||||
export async function getDomainStats() {
|
||||
const res = await request.get<{
|
||||
total: number
|
||||
active: number
|
||||
pending: number
|
||||
sslExpiring: number
|
||||
deployed: number
|
||||
}>('/platform/domain/stats')
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 从证书同步域名
|
||||
*/
|
||||
export async function syncDomainsFromCertificates(serverId: number) {
|
||||
const res = await request.post<{ syncCount: number; message: string }>(`/platform/domain/sync-from-certificates/${serverId}`)
|
||||
return res.data.data
|
||||
}
|
||||
239
src/api/project.ts
Normal file
239
src/api/project.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* 项目管理 API
|
||||
*/
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
export interface ProjectInfo {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
shortName?: string
|
||||
type: string
|
||||
projectGroup?: string
|
||||
logo?: string
|
||||
color?: string
|
||||
domain?: string
|
||||
url?: string
|
||||
domainId?: number
|
||||
serverId?: number
|
||||
serverName?: string
|
||||
deployPath?: string
|
||||
version?: string
|
||||
status: string
|
||||
icon?: string
|
||||
description?: string
|
||||
remark?: string
|
||||
sort?: number
|
||||
enableHttps?: boolean
|
||||
panelWebsiteId?: number
|
||||
panelSslId?: number
|
||||
lastDeployTime?: string
|
||||
lastDeployStatus?: string
|
||||
lastDeployMessage?: string
|
||||
createdTime?: string
|
||||
updatedTime?: string
|
||||
}
|
||||
|
||||
export interface DeployRequest {
|
||||
projectId: number
|
||||
enableHttps?: boolean
|
||||
acmeAccountId?: number
|
||||
dnsAccountId?: number
|
||||
createIfNotExist?: boolean
|
||||
}
|
||||
|
||||
export interface DeployStep {
|
||||
step: string
|
||||
status: 'success' | 'failed' | 'skipped'
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface DeployResult {
|
||||
success: boolean
|
||||
message: string
|
||||
websiteId?: number
|
||||
sslCertificateId?: number
|
||||
steps: DeployStep[]
|
||||
}
|
||||
|
||||
export interface AcmeAccount {
|
||||
id: number
|
||||
email: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
export interface DnsAccount {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
}
|
||||
|
||||
// ==================== 项目管理 API ====================
|
||||
|
||||
/**
|
||||
* 获取项目列表
|
||||
*/
|
||||
export async function getProjectList(params: { page: number; pageSize: number; keyword?: string }) {
|
||||
const res = await request.get<{ records: ProjectInfo[]; total: number }>('/platform/project/list', { params })
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有项目
|
||||
*/
|
||||
export async function getAllProjects() {
|
||||
const res = await request.get<ProjectInfo[]>('/platform/project/all')
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目详情
|
||||
*/
|
||||
export async function getProjectById(id: number) {
|
||||
const res = await request.get<ProjectInfo>(`/platform/project/${id}`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建项目
|
||||
*/
|
||||
export async function createProject(data: Partial<ProjectInfo>) {
|
||||
const res = await request.post<ProjectInfo>('/platform/project', data)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新项目
|
||||
*/
|
||||
export async function updateProject(id: number, data: Partial<ProjectInfo>) {
|
||||
await request.put<void>(`/platform/project/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除项目
|
||||
*/
|
||||
export async function deleteProject(id: number) {
|
||||
await request.delete<void>(`/platform/project/${id}`)
|
||||
}
|
||||
|
||||
// ==================== 部署相关 API ====================
|
||||
|
||||
/**
|
||||
* 部署项目到服务器
|
||||
*/
|
||||
export async function deployProject(data: DeployRequest): Promise<DeployResult> {
|
||||
const res = await request.post<DeployResult>('/platform/project/deploy', data)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器网站列表
|
||||
*/
|
||||
export async function getServerWebsites(serverId: number) {
|
||||
const res = await request.get<string>(`/platform/project/${serverId}/websites`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器证书列表
|
||||
*/
|
||||
export async function getServerCertificates(serverId: number) {
|
||||
const res = await request.get<string>(`/platform/project/${serverId}/certificates`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Acme账户列表
|
||||
*/
|
||||
export async function getServerAcmeAccounts(serverId: number): Promise<{ items: AcmeAccount[] }> {
|
||||
const res = await request.get<string>(`/platform/project/${serverId}/acme-accounts`)
|
||||
// 后端返回的是 JSON 字符串,需要解析
|
||||
const jsonStr = res.data.data
|
||||
try {
|
||||
const data = typeof jsonStr === 'string' ? JSON.parse(jsonStr) : jsonStr
|
||||
return { items: data?.data?.items || [] }
|
||||
} catch {
|
||||
return { items: [] }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取DNS账户列表
|
||||
*/
|
||||
export async function getServerDnsAccounts(serverId: number): Promise<{ items: DnsAccount[] }> {
|
||||
const res = await request.get<string>(`/platform/project/${serverId}/dns-accounts`)
|
||||
// 后端返回的是 JSON 字符串,需要解析
|
||||
const jsonStr = res.data.data
|
||||
try {
|
||||
const data = typeof jsonStr === 'string' ? JSON.parse(jsonStr) : jsonStr
|
||||
return { items: data?.data?.items || [] }
|
||||
} catch {
|
||||
return { items: [] }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查项目部署状态
|
||||
*/
|
||||
export interface DeployStatusResult {
|
||||
projectId: number
|
||||
domain: string
|
||||
deployed: boolean
|
||||
websiteId: number | null
|
||||
message: string
|
||||
}
|
||||
|
||||
export async function checkDeployStatus(projectId: number): Promise<DeployStatusResult> {
|
||||
const res = await request.get<DeployStatusResult>(`/platform/project/check-deploy/${projectId}`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否存在
|
||||
*/
|
||||
export async function checkUploadFiles(projectId: number, paths: string[]): Promise<string[]> {
|
||||
const res = await request.post<string[]>('/platform/project/upload/check', {
|
||||
projectId,
|
||||
paths
|
||||
})
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备上传(清理旧文件)
|
||||
*/
|
||||
export async function prepareUploadFile(projectId: number, path: string): Promise<boolean> {
|
||||
const res = await request.post<boolean>('/platform/project/upload/prepare', {
|
||||
projectId,
|
||||
path
|
||||
})
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 分片上传文件
|
||||
*/
|
||||
export async function uploadFileChunk(
|
||||
projectId: number,
|
||||
filename: string,
|
||||
path: string,
|
||||
chunkIndex: number,
|
||||
chunkCount: number,
|
||||
chunk: Blob
|
||||
): Promise<any> {
|
||||
const formData = new FormData()
|
||||
formData.append('projectId', projectId.toString())
|
||||
formData.append('filename', filename)
|
||||
formData.append('path', path)
|
||||
formData.append('chunkIndex', chunkIndex.toString())
|
||||
formData.append('chunkCount', chunkCount.toString())
|
||||
formData.append('chunk', chunk)
|
||||
|
||||
const res = await request.post<any>('/platform/project/upload/chunk', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
155
src/api/server.ts
Normal file
155
src/api/server.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* 服务器管理 API
|
||||
*/
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
export interface CpuInfo {
|
||||
cores: number
|
||||
usage: number
|
||||
}
|
||||
|
||||
export interface MemoryInfo {
|
||||
total: number
|
||||
used: number
|
||||
usage: number
|
||||
}
|
||||
|
||||
export interface DiskInfo {
|
||||
total: number
|
||||
used: number
|
||||
usage: number
|
||||
}
|
||||
|
||||
export type ServerStatus = 'online' | 'offline' | 'warning' | 'maintenance'
|
||||
export type ServerType = 'physical' | 'virtual' | 'cloud'
|
||||
|
||||
/**
|
||||
* 服务器基础信息(数据库中的数据)
|
||||
*/
|
||||
export interface ServerBase {
|
||||
id: number
|
||||
name: string
|
||||
ip: string
|
||||
internalIp?: string
|
||||
port: number
|
||||
type: ServerType
|
||||
status: ServerStatus
|
||||
os?: string
|
||||
cpuCores?: number
|
||||
memoryTotal?: number
|
||||
diskTotal?: number
|
||||
tags?: string // JSON 字符串
|
||||
sshUser?: string
|
||||
sshPort?: number
|
||||
panelUrl?: string
|
||||
panelPort?: number
|
||||
panelApiKey?: string
|
||||
description?: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务器信息(包含实时资源使用情况)
|
||||
*/
|
||||
export interface ServerInfo {
|
||||
id: number
|
||||
name: string
|
||||
ip: string
|
||||
internalIp?: string
|
||||
port: number
|
||||
type: ServerType
|
||||
status: ServerStatus
|
||||
os?: string
|
||||
tags: string[]
|
||||
|
||||
panelUrl?: string
|
||||
panelPort?: number
|
||||
description?: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
// 资源使用情况(从1Panel获取)
|
||||
cpu: CpuInfo
|
||||
memory: MemoryInfo
|
||||
disk: DiskInfo
|
||||
}
|
||||
|
||||
export interface ServerFormData {
|
||||
name: string
|
||||
ip: string
|
||||
internalIp?: string
|
||||
port?: number
|
||||
type: ServerType
|
||||
os?: string
|
||||
tags?: string[]
|
||||
|
||||
panelUrl?: string
|
||||
panelPort?: number
|
||||
panelApiKey?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
// ==================== API 方法 ====================
|
||||
|
||||
/**
|
||||
* 获取所有服务器(基础数据,不含实时状态)
|
||||
*/
|
||||
export async function getServerList(params?: { keyword?: string; status?: string; type?: string }): Promise<ServerBase[]> {
|
||||
const res = await request.get<ServerBase[]>('/platform/server/all', { params })
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器实时状态(从1Panel获取CPU/内存/磁盘使用情况)
|
||||
*/
|
||||
export async function getServerStatus(id: number): Promise<ServerInfo> {
|
||||
const res = await request.get<ServerInfo>(`/platform/server/${id}/status`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新服务器状态(同 getServerStatus)
|
||||
*/
|
||||
export async function refreshServerStatus(id: number): Promise<ServerInfo> {
|
||||
const res = await request.post<ServerInfo>(`/platform/server/${id}/refresh`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器详情(基础数据)
|
||||
*/
|
||||
export async function getServerDetail(id: number): Promise<ServerBase> {
|
||||
const res = await request.get<ServerBase>(`/platform/server/${id}`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建服务器
|
||||
*/
|
||||
export async function createServer(data: ServerFormData) {
|
||||
const res = await request.post<ServerBase>('/platform/server', {
|
||||
...data,
|
||||
tags: data.tags ? JSON.stringify(data.tags) : null
|
||||
})
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新服务器
|
||||
*/
|
||||
export async function updateServer(id: number, data: ServerFormData) {
|
||||
await request.put<void>(`/platform/server/${id}`, {
|
||||
...data,
|
||||
tags: data.tags ? JSON.stringify(data.tags) : null
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除服务器
|
||||
*/
|
||||
export async function deleteServer(id: number) {
|
||||
await request.delete<void>(`/platform/server/${id}`)
|
||||
}
|
||||
|
||||
147
src/api/system/approval.ts
Normal file
147
src/api/system/approval.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { PageResult } from '@/types/api/response'
|
||||
import type { ApprovalTemplate, ApprovalInstance, ApprovalStats, ApprovalScenario, ApprovalInstanceStatus, ApprovalNode } from '@/types'
|
||||
|
||||
// ==================== 统计 ====================
|
||||
|
||||
/**
|
||||
* 获取审批统计数据
|
||||
*/
|
||||
export async function getApprovalStats() {
|
||||
const res = await request.get<ApprovalStats>('/system/approval/stats')
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
// ==================== 模板管理 ====================
|
||||
|
||||
export interface TemplateQueryParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
scenario?: ApprovalScenario
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询模板列表
|
||||
*/
|
||||
export async function getApprovalTemplatePage(params: TemplateQueryParams) {
|
||||
const res = await request.get<PageResult<ApprovalTemplate>>('/system/approval/template/page', { params })
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有模板列表
|
||||
*/
|
||||
export async function getApprovalTemplateList(scenario?: ApprovalScenario) {
|
||||
const res = await request.get<ApprovalTemplate[]>('/system/approval/template/list', {
|
||||
params: { scenario }
|
||||
})
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模板详情
|
||||
*/
|
||||
export async function getApprovalTemplateDetail(id: number) {
|
||||
const res = await request.get<ApprovalTemplate>(`/system/approval/template/${id}`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export interface TemplateCreateRequest {
|
||||
name: string
|
||||
description?: string
|
||||
scenario: ApprovalScenario
|
||||
enabled: boolean
|
||||
nodes: ApprovalNode[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建模板
|
||||
*/
|
||||
export async function createApprovalTemplate(data: TemplateCreateRequest) {
|
||||
const res = await request.post<number>('/system/approval/template', data)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新模板
|
||||
*/
|
||||
export async function updateApprovalTemplate(id: number, data: TemplateCreateRequest) {
|
||||
await request.put<void>(`/system/approval/template/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除模板
|
||||
*/
|
||||
export async function deleteApprovalTemplate(id: number) {
|
||||
await request.delete<void>(`/system/approval/template/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换模板启用状态
|
||||
*/
|
||||
export async function toggleApprovalTemplate(id: number) {
|
||||
await request.post<void>(`/system/approval/template/${id}/toggle`)
|
||||
}
|
||||
|
||||
// ==================== 实例管理 ====================
|
||||
|
||||
export interface InstanceQueryParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
scenario?: ApprovalScenario
|
||||
status?: ApprovalInstanceStatus
|
||||
keyword?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询实例列表
|
||||
*/
|
||||
export async function getApprovalInstancePage(params: InstanceQueryParams) {
|
||||
const res = await request.get<PageResult<ApprovalInstance>>('/system/approval/instance/page', { params })
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实例详情
|
||||
*/
|
||||
export async function getApprovalInstanceDetail(id: number) {
|
||||
const res = await request.get<ApprovalInstance>(`/system/approval/instance/${id}`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交审批
|
||||
*/
|
||||
export async function submitApprovalInstance(id: number) {
|
||||
await request.post<void>(`/system/approval/instance/${id}/submit`)
|
||||
}
|
||||
|
||||
export interface ApproveRequest {
|
||||
nodeId: number
|
||||
approverId: number
|
||||
action: 'approve' | 'reject'
|
||||
comment?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批操作
|
||||
*/
|
||||
export async function approveInstance(id: number, data: ApproveRequest) {
|
||||
await request.post<void>(`/system/approval/instance/${id}/approve`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回审批
|
||||
*/
|
||||
export async function withdrawApprovalInstance(id: number) {
|
||||
await request.post<void>(`/system/approval/instance/${id}/withdraw`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消审批
|
||||
*/
|
||||
export async function cancelApprovalInstance(id: number) {
|
||||
await request.post<void>(`/system/approval/instance/${id}/cancel`)
|
||||
}
|
||||
|
||||
86
src/api/system/dept.ts
Normal file
86
src/api/system/dept.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
// 部门类型定义
|
||||
export interface DeptRecord {
|
||||
id: number
|
||||
parentId: number
|
||||
name: string
|
||||
code?: string
|
||||
leaderId?: number
|
||||
leaderName?: string
|
||||
phone?: string
|
||||
email?: string
|
||||
sort: number
|
||||
status: number
|
||||
remark?: string
|
||||
createdAt?: string
|
||||
children?: DeptRecord[]
|
||||
}
|
||||
|
||||
export interface DeptFormData {
|
||||
id?: number
|
||||
parentId: number
|
||||
name: string
|
||||
code?: string
|
||||
leaderId?: number
|
||||
leaderName?: string
|
||||
phone?: string
|
||||
email?: string
|
||||
sort: number
|
||||
status: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门树
|
||||
*/
|
||||
export async function getDeptTree() {
|
||||
const res = await request.get<DeptRecord[]>('/system/dept/tree')
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门列表(扁平结构)
|
||||
*/
|
||||
export async function getDeptList() {
|
||||
const res = await request.get<DeptRecord[]>('/system/dept/list')
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门详情
|
||||
*/
|
||||
export async function getDeptDetail(id: number) {
|
||||
const res = await request.get<DeptRecord>(`/system/dept/${id}`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建部门
|
||||
*/
|
||||
export async function createDept(data: DeptFormData) {
|
||||
const res = await request.post<number>('/system/dept', data)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新部门
|
||||
*/
|
||||
export async function updateDept(id: number, data: DeptFormData) {
|
||||
await request.put<void>(`/system/dept/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除部门
|
||||
*/
|
||||
export async function deleteDept(id: number) {
|
||||
await request.delete<void>(`/system/dept/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门用户列表
|
||||
*/
|
||||
export async function getDeptUsers(id: number) {
|
||||
const res = await request.get<number[]>(`/system/dept/${id}/users`)
|
||||
return res.data.data
|
||||
}
|
||||
112
src/api/system/dict.ts
Normal file
112
src/api/system/dict.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// ==================== 字典类型 ====================
|
||||
|
||||
/**
|
||||
* 分页查询字典
|
||||
*/
|
||||
export function getDictPage(params: { page: number; pageSize: number; name?: string; code?: string }) {
|
||||
return request.get('/system/dict/page', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有字典
|
||||
*/
|
||||
export function getDictList() {
|
||||
return request.get('/system/dict/list')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典详情
|
||||
*/
|
||||
export function getDictById(id: number) {
|
||||
return request.get(`/system/dict/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据编码获取字典
|
||||
*/
|
||||
export function getDictByCode(code: string) {
|
||||
return request.get(`/system/dict/code/${code}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建字典
|
||||
*/
|
||||
export function createDict(data: { name: string; code: string; remark?: string; status?: number }) {
|
||||
return request.post('/system/dict', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新字典
|
||||
*/
|
||||
export function updateDict(id: number, data: { name: string; code: string; remark?: string; status?: number }) {
|
||||
return request.put(`/system/dict/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除字典
|
||||
*/
|
||||
export function deleteDict(id: number) {
|
||||
return request.delete(`/system/dict/${id}`)
|
||||
}
|
||||
|
||||
// ==================== 字典项 ====================
|
||||
|
||||
/**
|
||||
* 获取字典项列表
|
||||
*/
|
||||
export function getDictItems(dictId: number) {
|
||||
return request.get(`/system/dict/${dictId}/items`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字典编码获取字典项
|
||||
*/
|
||||
export function getDictItemsByCode(code: string) {
|
||||
return request.get(`/system/dict/code/${code}/items`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典项详情
|
||||
*/
|
||||
export function getDictItemById(id: number) {
|
||||
return request.get(`/system/dict/item/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建字典项
|
||||
*/
|
||||
export function createDictItem(data: {
|
||||
dictId: number
|
||||
label: string
|
||||
value: string
|
||||
sort?: number
|
||||
isDefault?: number
|
||||
remark?: string
|
||||
status?: number
|
||||
}) {
|
||||
return request.post('/system/dict/item', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新字典项
|
||||
*/
|
||||
export function updateDictItem(id: number, data: {
|
||||
dictId: number
|
||||
label: string
|
||||
value: string
|
||||
sort?: number
|
||||
isDefault?: number
|
||||
remark?: string
|
||||
status?: number
|
||||
}) {
|
||||
return request.put(`/system/dict/item/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除字典项
|
||||
*/
|
||||
export function deleteDictItem(id: number) {
|
||||
return request.delete(`/system/dict/item/${id}`)
|
||||
}
|
||||
@@ -2,34 +2,41 @@ import { request } from '@/utils/request'
|
||||
import type { RoleRecord, RoleQuery, RoleFormData, RoleOption } from '@/types/system/role'
|
||||
import type { PageResult } from '@/types/api/response'
|
||||
|
||||
export function getRolePage(params: RoleQuery) {
|
||||
return request.get<PageResult<RoleRecord>>('/system/role/page', { params })
|
||||
// 导出类型供外部使用
|
||||
export type { RoleOption, RoleRecord }
|
||||
|
||||
export async function getRolePage(params: RoleQuery) {
|
||||
const res = await request.get<PageResult<RoleRecord>>('/system/role/page', { params })
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export function getRoleList() {
|
||||
return request.get<RoleRecord[]>('/system/role/list')
|
||||
export async function getRoleList() {
|
||||
const res = await request.get<RoleOption[]>('/system/role/list-all')
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export function getAllRoles() {
|
||||
return request.get<RoleOption[]>('/system/role/list-all')
|
||||
} // Check if backend has list-all or if frontend filtered list.
|
||||
|
||||
export function createRole(data: RoleFormData) {
|
||||
return request.post<void>('/system/role', data)
|
||||
export async function getAllRoles() {
|
||||
const res = await request.get<RoleOption[]>('/system/role/list-all')
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export function updateRole(data: RoleFormData) {
|
||||
return request.put<void>(`/system/role/${data.id}`, data)
|
||||
export async function createRole(data: RoleFormData) {
|
||||
await request.post<void>('/system/role', data)
|
||||
}
|
||||
|
||||
export function deleteRole(id: number) {
|
||||
return request.delete<void>(`/system/role/${id}`)
|
||||
export async function updateRole(data: RoleFormData) {
|
||||
await request.put<void>(`/system/role/${data.id}`, data)
|
||||
}
|
||||
|
||||
export function getRoleMenuIds(roleId: number) {
|
||||
return request.get<number[]>(`/system/role/${roleId}/menus`)
|
||||
export async function deleteRole(id: number) {
|
||||
await request.delete<void>(`/system/role/${id}`)
|
||||
}
|
||||
|
||||
export function assignRoleMenus(roleId: number, menuIds: number[]) {
|
||||
return request.post<void>(`/system/role/${roleId}/menus`, { menuIds })
|
||||
export async function getRoleMenuIds(roleId: number) {
|
||||
const res = await request.get<number[]>(`/system/role/${roleId}/menus`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export async function assignRoleMenus(roleId: number, menuIds: number[]) {
|
||||
await request.post<void>(`/system/role/${roleId}/menus`, menuIds)
|
||||
}
|
||||
|
||||
3
src/components.d.ts
vendored
3
src/components.d.ts
vendored
@@ -40,9 +40,6 @@ declare module 'vue' {
|
||||
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
|
||||
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
|
||||
ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
|
||||
AList: typeof import('ant-design-vue/es')['List']
|
||||
AListItem: typeof import('ant-design-vue/es')['ListItem']
|
||||
AListItemMeta: typeof import('ant-design-vue/es')['ListItemMeta']
|
||||
AMenu: typeof import('ant-design-vue/es')['Menu']
|
||||
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
|
||||
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
|
||||
|
||||
@@ -172,10 +172,25 @@
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="审批角色" v-if="selectedNode.data.approverType === 'role'">
|
||||
<a-input
|
||||
<a-select
|
||||
:value="selectedNode.data.approverRole"
|
||||
placeholder="如:财务主管"
|
||||
@input="(e: Event) => { updateNodeData('approverRole', (e.target as HTMLInputElement).value); updateApproverDesc() }"
|
||||
placeholder="选择角色"
|
||||
@change="(val: any) => { updateNodeData('approverRole', val); updateApproverDesc() }"
|
||||
>
|
||||
<a-select-option v-for="role in roleList" :key="role.code" :value="role.code">
|
||||
{{ role.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="选择部门" v-if="selectedNode.data.approverType === 'department'">
|
||||
<a-tree-select
|
||||
:value="selectedNode.data.deptId"
|
||||
:tree-data="deptTree"
|
||||
placeholder="选择部门"
|
||||
tree-default-expand-all
|
||||
:field-names="{ children: 'children', label: 'name', value: 'id' }"
|
||||
@change="(val: any) => { updateNodeData('deptId', val); updateDeptName(val); updateApproverDesc() }"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
@@ -250,6 +265,8 @@ import {
|
||||
import type { ApprovalNode, ApproverInfo } from '@/types'
|
||||
import { ApproverTypeMap } from '@/types'
|
||||
import { mockGetApprovers } from '@/mock'
|
||||
import { getDeptTree, type DeptRecord } from '@/api/system/dept'
|
||||
import { getRoleList, type RoleOption } from '@/api/system/role'
|
||||
|
||||
interface Props {
|
||||
modelValue?: ApprovalNode[]
|
||||
@@ -300,6 +317,8 @@ const edges = ref<Edge[]>([
|
||||
|
||||
const selectedNodeId = ref<string | null>(null)
|
||||
const approverList = ref<ApproverInfo[]>([])
|
||||
const deptTree = ref<DeptRecord[]>([])
|
||||
const roleList = ref<RoleOption[]>([])
|
||||
|
||||
const nodeTypeList = [
|
||||
{ type: 'approval', label: '审批节点', icon: CheckCircleOutlined },
|
||||
@@ -342,7 +361,9 @@ function onDrop(event: DragEvent) {
|
||||
approverType: 'specified',
|
||||
approverIds: [],
|
||||
approvalMode: 'or',
|
||||
timeoutHours: 0
|
||||
timeoutHours: 0,
|
||||
deptId: undefined,
|
||||
deptName: ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,13 +432,33 @@ function updateApproverDesc() {
|
||||
}).filter(Boolean)
|
||||
desc = names.join(', ')
|
||||
} else if (node.data.approverType === 'role' && node.data.approverRole) {
|
||||
desc = `角色: ${node.data.approverRole}`
|
||||
const role = roleList.value.find(r => r.code === node.data.approverRole)
|
||||
desc = `角色: ${role?.name || node.data.approverRole}`
|
||||
} else if (node.data.approverType === 'department' && node.data.deptName) {
|
||||
desc = `部门: ${node.data.deptName}`
|
||||
} else {
|
||||
desc = ApproverTypeMap[node.data.approverType as keyof typeof ApproverTypeMap] || ''
|
||||
}
|
||||
updateNodeData('approverDesc', desc)
|
||||
}
|
||||
|
||||
function updateDeptName(deptId: number) {
|
||||
const findDept = (list: DeptRecord[]): DeptRecord | undefined => {
|
||||
for (const dept of list) {
|
||||
if (dept.id === deptId) return dept
|
||||
if (dept.children) {
|
||||
const found = findDept(dept.children)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
const dept = findDept(deptTree.value)
|
||||
if (dept) {
|
||||
updateNodeData('deptName', dept.name)
|
||||
}
|
||||
}
|
||||
|
||||
function deleteSelectedNode() {
|
||||
if (!selectedNodeId.value) return
|
||||
const nodeId = selectedNodeId.value
|
||||
@@ -441,6 +482,8 @@ function toApprovalNodes(): any[] {
|
||||
approverType: n.data.approverType,
|
||||
approverIds: n.data.approverIds,
|
||||
approverRole: n.data.approverRole,
|
||||
deptId: n.data.deptId,
|
||||
deptName: n.data.deptName,
|
||||
approvalMode: n.data.approvalMode,
|
||||
timeoutHours: n.data.timeoutHours,
|
||||
order: index + 1
|
||||
@@ -466,7 +509,10 @@ function loadFromApprovalNodes(approvalNodes: ApprovalNode[]) {
|
||||
}).filter(Boolean)
|
||||
desc = names.join(', ')
|
||||
} else if (node.approverType === 'role' && node.approverRole) {
|
||||
desc = `角色: ${node.approverRole}`
|
||||
const role = roleList.value.find(r => r.code === node.approverRole)
|
||||
desc = `角色: ${role?.name || node.approverRole}`
|
||||
} else if (node.approverType === 'department' && node.deptName) {
|
||||
desc = `部门: ${node.deptName}`
|
||||
} else {
|
||||
desc = ApproverTypeMap[node.approverType as keyof typeof ApproverTypeMap] || ''
|
||||
}
|
||||
@@ -480,6 +526,8 @@ function loadFromApprovalNodes(approvalNodes: ApprovalNode[]) {
|
||||
approverType: node.approverType,
|
||||
approverIds: node.approverIds || [],
|
||||
approverRole: node.approverRole,
|
||||
deptId: node.deptId,
|
||||
deptName: node.deptName || '',
|
||||
approvalMode: node.approvalMode,
|
||||
timeoutHours: node.timeoutHours,
|
||||
approverDesc: desc
|
||||
@@ -522,11 +570,16 @@ watch(() => props.modelValue, (newVal) => {
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(async () => {
|
||||
approverList.value = await mockGetApprovers()
|
||||
// 加载数据,此时 approverList 已就绪,由于 watch immediate 可能会先于 onMounted 执行(但 mock 没回),
|
||||
// 所以这里需要再次检查并重新渲染描述(如果需要)。
|
||||
// 更好的方式是:mock 回来后,如果已经有节点,刷新描述。
|
||||
// 或者:mock 回来后,再调用一次 loadFromApprovalNodes。
|
||||
// 并行加载所有数据
|
||||
const [approvers, depts, roles] = await Promise.all([
|
||||
mockGetApprovers(),
|
||||
getDeptTree().catch(() => []),
|
||||
getRoleList().catch(() => [])
|
||||
])
|
||||
approverList.value = approvers
|
||||
deptTree.value = depts
|
||||
roleList.value = roles
|
||||
|
||||
if (props.modelValue?.length) {
|
||||
loadFromApprovalNodes(props.modelValue)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
ref="uploadCoreRef"
|
||||
:auto-upload="false"
|
||||
:flex-mode="true"
|
||||
:project-id="projectId"
|
||||
:deploy-path="deployPath"
|
||||
@files-change="handleFilesChange"
|
||||
/>
|
||||
</div>
|
||||
@@ -57,6 +59,8 @@
|
||||
ref="uploadCoreRef"
|
||||
:auto-upload="false"
|
||||
:flex-mode="true"
|
||||
:project-id="projectId"
|
||||
:deploy-path="deployPath"
|
||||
@files-change="handleFilesChange"
|
||||
/>
|
||||
|
||||
@@ -87,6 +91,7 @@ const props = withDefaults(defineProps<{
|
||||
projectId?: string
|
||||
projectName?: string
|
||||
versionId?: string
|
||||
deployPath?: string
|
||||
title?: string
|
||||
mode?: 'drawer' | 'modal'
|
||||
}>(), {
|
||||
@@ -107,6 +112,10 @@ watch(() => props.visible, (newVal) => {
|
||||
if (!newVal) {
|
||||
uploading.value = false
|
||||
fileCount.value = 0
|
||||
// 清空上传列表
|
||||
if (uploadCoreRef.value) {
|
||||
uploadCoreRef.value.reset()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -152,6 +161,10 @@ async function handleConfirm() {
|
||||
try {
|
||||
const files = await uploadCoreRef.value.startUpload()
|
||||
message.success('上传完成!')
|
||||
|
||||
// 上传成功后清空列表
|
||||
uploadCoreRef.value.reset()
|
||||
|
||||
emit('uploaded', files)
|
||||
emit('update:visible', false)
|
||||
} catch (error) {
|
||||
|
||||
@@ -112,11 +112,14 @@ import {
|
||||
} from '@ant-design/icons-vue'
|
||||
import type { UploadFile, DuplicateFile } from '@/types'
|
||||
import DuplicateFileModal from './DuplicateFileModal.vue'
|
||||
import { checkUploadFiles, prepareUploadFile, uploadFileChunk } from '@/api/project'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
autoUpload?: boolean
|
||||
flexMode?: boolean
|
||||
existingFiles?: string[] // 已存在的文件路径列表
|
||||
projectId?: string
|
||||
deployPath?: string
|
||||
}>(), {
|
||||
autoUpload: false,
|
||||
flexMode: false,
|
||||
@@ -217,11 +220,11 @@ function handleFolderSelect(event: Event) {
|
||||
/**
|
||||
* 添加文件到列表
|
||||
*/
|
||||
function addFiles(files: File[]) {
|
||||
async function addFiles(files: File[]) {
|
||||
const newFiles: UploadFile[] = files.map(file => ({
|
||||
uid: generateUid(),
|
||||
name: file.name,
|
||||
path: (file as any).webkitRelativePath || file.name,
|
||||
path: (file as any).relativePath || (file as any).webkitRelativePath || file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
status: 'pending',
|
||||
@@ -229,7 +232,57 @@ function addFiles(files: File[]) {
|
||||
file
|
||||
}))
|
||||
|
||||
// 检测同名文件(与已存在文件和已添加文件对比)
|
||||
// 如果有 projectId 和 deployPath,进行服务端同名检测
|
||||
if (props.projectId && props.deployPath) {
|
||||
const basePath = props.deployPath.replace(/\/$/, '')
|
||||
const pathsToCheck = newFiles.map(f => {
|
||||
const filePath = f.path.startsWith('/') ? f.path.substring(1) : f.path
|
||||
return `${basePath}/${filePath}`
|
||||
})
|
||||
|
||||
try {
|
||||
const existingPaths = await checkUploadFiles(Number(props.projectId), pathsToCheck)
|
||||
const existingSet = new Set(existingPaths)
|
||||
|
||||
const duplicates: DuplicateFile[] = []
|
||||
const nonDuplicates: UploadFile[] = []
|
||||
|
||||
for (let i = 0; i < newFiles.length; i++) {
|
||||
const file = newFiles[i]
|
||||
const fullPath = pathsToCheck[i]
|
||||
|
||||
if (existingSet.has(fullPath)) {
|
||||
duplicates.push({
|
||||
uid: file.uid,
|
||||
name: file.name,
|
||||
path: file.path,
|
||||
size: file.size,
|
||||
file: file.file
|
||||
})
|
||||
} else {
|
||||
nonDuplicates.push(file)
|
||||
}
|
||||
}
|
||||
|
||||
if (nonDuplicates.length > 0) {
|
||||
fileList.value.push(...nonDuplicates)
|
||||
}
|
||||
|
||||
if (duplicates.length > 0) {
|
||||
duplicateFiles.value = duplicates
|
||||
pendingFiles.value = newFiles.filter(f => duplicates.some(d => d.uid === f.uid))
|
||||
duplicateModalVisible.value = true
|
||||
} else if (props.autoUpload) {
|
||||
startUpload()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
message.error('检查同名文件失败')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 原有的本地检测逻辑 (当没有 projectId 时使用)
|
||||
const existingPaths = new Set([
|
||||
...props.existingFiles,
|
||||
...fileList.value.map(f => f.path)
|
||||
@@ -252,12 +305,10 @@ function addFiles(files: File[]) {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加非重复文件
|
||||
if (nonDuplicates.length > 0) {
|
||||
fileList.value.push(...nonDuplicates)
|
||||
}
|
||||
|
||||
// 如果有重复文件,显示提示弹窗
|
||||
if (duplicates.length > 0) {
|
||||
duplicateFiles.value = duplicates
|
||||
pendingFiles.value = newFiles.filter(f => duplicates.some(d => d.uid === f.uid))
|
||||
@@ -340,7 +391,7 @@ async function handleDrop(event: DragEvent) {
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
addFilesWithPath(files)
|
||||
addFiles(files)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,7 +472,62 @@ async function uploadFile(file: UploadFile): Promise<void> {
|
||||
file.status = 'uploading'
|
||||
currentUploadFile.value = file
|
||||
|
||||
// 模拟上传过程
|
||||
// 服务端上传逻辑
|
||||
if (props.projectId && props.deployPath) {
|
||||
try {
|
||||
const projectIdNum = Number(props.projectId)
|
||||
const basePath = props.deployPath.replace(/\/$/, '')
|
||||
const filePath = file.path.startsWith('/') ? file.path.substring(1) : file.path
|
||||
const fullPath = `${basePath}/${filePath}`
|
||||
|
||||
// 1. 准备上传(清理可能存在的旧文件)
|
||||
await prepareUploadFile(projectIdNum, fullPath)
|
||||
|
||||
// 2. 计算目标目录(不包含文件名)
|
||||
// 1Panel 接口需要的是上传到的目录路径
|
||||
const dirPath = fullPath.substring(0, fullPath.lastIndexOf('/') + 1)
|
||||
|
||||
// 3. 分片上传
|
||||
const chunkSize = 2 * 1024 * 1024 // 2MB 分片
|
||||
const totalChunks = Math.ceil(file.size / chunkSize)
|
||||
|
||||
// 如果文件为空,处理为 1 个空分片
|
||||
if (totalChunks === 0) {
|
||||
await uploadFileChunk(projectIdNum, file.name, dirPath, 0, 1, file.file.slice(0, 0))
|
||||
file.percent = 100
|
||||
} else {
|
||||
for (let i = 0; i < totalChunks; i++) {
|
||||
const start = i * chunkSize
|
||||
const end = Math.min(file.size, start + chunkSize)
|
||||
const chunkBlob = file.file.slice(start, end)
|
||||
|
||||
await uploadFileChunk(
|
||||
projectIdNum,
|
||||
file.name,
|
||||
dirPath,
|
||||
i,
|
||||
totalChunks,
|
||||
chunkBlob
|
||||
)
|
||||
|
||||
const percent = Math.floor(((i + 1) / totalChunks) * 100)
|
||||
file.percent = percent
|
||||
}
|
||||
}
|
||||
|
||||
file.status = 'done'
|
||||
currentUploadFile.value = null
|
||||
return
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
file.status = 'error'
|
||||
message.error(`上传失败: ${file.name}`)
|
||||
currentUploadFile.value = null
|
||||
throw error // 抛出错误以便调用者捕获
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟上传过程 (降级逻辑)
|
||||
return new Promise((resolve) => {
|
||||
let progress = 0
|
||||
const interval = setInterval(() => {
|
||||
@@ -437,8 +543,6 @@ async function uploadFile(file: UploadFile): Promise<void> {
|
||||
}
|
||||
}, 100)
|
||||
})
|
||||
|
||||
// TODO: 真实上传逻辑
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
@click="handleSelect(icon)"
|
||||
:title="icon"
|
||||
>
|
||||
<component :is="Icons[icon as keyof typeof Icons]" />
|
||||
<component :is="(Icons as any)[icon]" />
|
||||
</div>
|
||||
<div v-if="filteredIcons.length === 0" class="no-data">
|
||||
未找到图标
|
||||
@@ -34,8 +34,8 @@
|
||||
class="icon-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<span v-if="modelValue && Icons[modelValue as keyof typeof Icons]" class="selected-icon">
|
||||
<component :is="Icons[modelValue as keyof typeof Icons]" />
|
||||
<span v-if="modelValue && (Icons as any)[modelValue]" class="selected-icon">
|
||||
<component :is="(Icons as any)[modelValue]" />
|
||||
</span>
|
||||
</template>
|
||||
<template #suffix>
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import { createDict, updateDict } from '@/api/system/dict'
|
||||
|
||||
// 字典类型定义
|
||||
interface DictRecord {
|
||||
@@ -112,18 +113,26 @@ async function handleSubmit() {
|
||||
await formRef.value?.validate()
|
||||
submitLoading.value = true
|
||||
|
||||
// TODO: 接入真实API
|
||||
// if (isEdit.value) {
|
||||
// await updateDict(formData)
|
||||
// } else {
|
||||
// await createDict(formData)
|
||||
// }
|
||||
const data = {
|
||||
name: formData.name,
|
||||
code: formData.code,
|
||||
remark: formData.remark,
|
||||
status: formData.status
|
||||
}
|
||||
|
||||
if (isEdit.value && formData.id) {
|
||||
await updateDict(formData.id, data)
|
||||
} else {
|
||||
await createDict(data)
|
||||
}
|
||||
|
||||
message.success(isEdit.value ? '编辑成功' : '新增成功')
|
||||
emit('update:visible', false)
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
// 验证失败
|
||||
} catch (error: any) {
|
||||
if (error?.response?.data?.message) {
|
||||
message.error(error.response.data.message)
|
||||
}
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEditItem(record)">编辑</a-button>
|
||||
<a-button type="link" size="small" @click="handleEditItem(record as DictItemRecord)">编辑</a-button>
|
||||
<a-popconfirm
|
||||
title="确定删除此字典项吗?"
|
||||
@confirm="handleDeleteItem(record.id)"
|
||||
@confirm="handleDeleteItem((record as DictItemRecord).id!)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
@@ -94,6 +94,7 @@ import { ref, reactive, watch } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import { getDictItems, createDictItem, updateDictItem, deleteDictItem } from '@/api/system/dict'
|
||||
|
||||
// 类型定义
|
||||
interface DictRecord {
|
||||
@@ -131,7 +132,7 @@ const columns = [
|
||||
{ title: '默认', dataIndex: 'isDefault', width: 70 },
|
||||
{ title: '状态', dataIndex: 'status', width: 70 },
|
||||
{ title: '备注', dataIndex: 'remark', ellipsis: true },
|
||||
{ title: '操作', dataIndex: 'action', width: 120, fixed: 'right' }
|
||||
{ title: '操作', dataIndex: 'action', width: 120, fixed: 'right' as const }
|
||||
]
|
||||
|
||||
// 字典项表单
|
||||
@@ -174,31 +175,11 @@ function handleClose() {
|
||||
async function loadData(dictId: number) {
|
||||
loading.value = true
|
||||
try {
|
||||
// TODO: 接入真实API
|
||||
// const res = await getDictItemList(dictId)
|
||||
// tableData.value = res.data.data
|
||||
|
||||
// 模拟数据
|
||||
if (dictId === 1) {
|
||||
tableData.value = [
|
||||
{ id: 1, dictId: 1, label: '男', value: '1', sort: 1, isDefault: 1, status: 1, remark: '' },
|
||||
{ id: 2, dictId: 1, label: '女', value: '2', sort: 2, isDefault: 0, status: 1, remark: '' },
|
||||
{ id: 3, dictId: 1, label: '未知', value: '0', sort: 3, isDefault: 0, status: 1, remark: '' }
|
||||
]
|
||||
} else if (dictId === 2) {
|
||||
tableData.value = [
|
||||
{ id: 4, dictId: 2, label: '正常', value: '1', sort: 1, isDefault: 1, status: 1, remark: '' },
|
||||
{ id: 5, dictId: 2, label: '禁用', value: '0', sort: 2, isDefault: 0, status: 1, remark: '' }
|
||||
]
|
||||
} else if (dictId === 3) {
|
||||
tableData.value = [
|
||||
{ id: 6, dictId: 3, label: '待审批', value: 'pending', sort: 1, isDefault: 1, status: 1, remark: '' },
|
||||
{ id: 7, dictId: 3, label: '已通过', value: 'approved', sort: 2, isDefault: 0, status: 1, remark: '' },
|
||||
{ id: 8, dictId: 3, label: '已驳回', value: 'rejected', sort: 3, isDefault: 0, status: 1, remark: '' }
|
||||
]
|
||||
} else {
|
||||
tableData.value = []
|
||||
}
|
||||
const res = await getDictItems(dictId)
|
||||
tableData.value = res.data.data || []
|
||||
} catch (error) {
|
||||
console.error('加载字典项失败:', error)
|
||||
tableData.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -231,11 +212,16 @@ function handleEditItem(record: DictItemRecord) {
|
||||
}
|
||||
|
||||
async function handleDeleteItem(id: number) {
|
||||
// TODO: 接入真实API
|
||||
// await deleteDictItem(id)
|
||||
message.success('删除成功')
|
||||
if (props.dictData) {
|
||||
loadData(props.dictData.id)
|
||||
try {
|
||||
await deleteDictItem(id)
|
||||
message.success('删除成功')
|
||||
if (props.dictData) {
|
||||
loadData(props.dictData.id)
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error?.response?.data?.message) {
|
||||
message.error(error.response.data.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,20 +230,31 @@ async function handleSubmitItem() {
|
||||
await formRef.value?.validate()
|
||||
submitLoading.value = true
|
||||
|
||||
// TODO: 接入真实API
|
||||
// if (isEditItem.value) {
|
||||
// await updateDictItem(formData)
|
||||
// } else {
|
||||
// await createDictItem(formData)
|
||||
// }
|
||||
const data = {
|
||||
dictId: formData.dictId!,
|
||||
label: formData.label,
|
||||
value: formData.value,
|
||||
sort: formData.sort,
|
||||
isDefault: formData.isDefault,
|
||||
status: formData.status,
|
||||
remark: formData.remark
|
||||
}
|
||||
|
||||
if (isEditItem.value && formData.id) {
|
||||
await updateDictItem(formData.id, data)
|
||||
} else {
|
||||
await createDictItem(data)
|
||||
}
|
||||
|
||||
message.success(isEditItem.value ? '编辑成功' : '新增成功')
|
||||
itemModalVisible.value = false
|
||||
if (props.dictData) {
|
||||
loadData(props.dictData.id)
|
||||
}
|
||||
} catch (error) {
|
||||
// 验证失败
|
||||
} catch (error: any) {
|
||||
if (error?.response?.data?.message) {
|
||||
message.error(error.response.data.message)
|
||||
}
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
|
||||
@@ -22,6 +22,17 @@
|
||||
<a-form-item label="邮箱" name="email">
|
||||
<a-input v-model:value="formData.email" placeholder="请输入邮箱" />
|
||||
</a-form-item>
|
||||
<a-form-item label="所属部门" name="deptId">
|
||||
<a-tree-select
|
||||
v-model:value="formData.deptId"
|
||||
:tree-data="deptTreeData"
|
||||
placeholder="请选择部门"
|
||||
tree-default-expand-all
|
||||
:field-names="{ children: 'children', label: 'name', value: 'id' }"
|
||||
allow-clear
|
||||
@change="handleDeptChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="角色" name="role">
|
||||
<a-select v-model:value="formData.role" placeholder="请选择角色">
|
||||
<a-select-option v-for="role in roleOptions" :key="role.code" :value="role.code">
|
||||
@@ -43,11 +54,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed } from 'vue'
|
||||
import { ref, reactive, watch, computed, onMounted } from 'vue'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import type { Rule } from 'ant-design-vue/es/form'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { createUser, updateUser } from '@/api/system/user'
|
||||
import { getDeptTree, type DeptRecord } from '@/api/system/dept'
|
||||
import type { UserFormData } from '@/types/system/user'
|
||||
|
||||
interface RoleOption {
|
||||
@@ -65,6 +77,7 @@ const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const loading = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const deptTreeData = ref<DeptRecord[]>([])
|
||||
|
||||
const formData = reactive<UserFormData>({
|
||||
username: '',
|
||||
@@ -73,6 +86,8 @@ const formData = reactive<UserFormData>({
|
||||
phone: '',
|
||||
email: '',
|
||||
role: undefined,
|
||||
deptId: undefined,
|
||||
deptName: undefined,
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
@@ -85,10 +100,44 @@ const formRules: Record<string, Rule[]> = {
|
||||
role: [{ required: true, message: '请选择角色', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 加载部门树
|
||||
async function loadDeptTree() {
|
||||
try {
|
||||
deptTreeData.value = await getDeptTree()
|
||||
} catch (error) {
|
||||
console.error('加载部门列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 部门选择变化时,更新部门名称
|
||||
function handleDeptChange(value: number | undefined) {
|
||||
if (!value) {
|
||||
formData.deptName = undefined
|
||||
return
|
||||
}
|
||||
// 递归查找部门名称
|
||||
const findDeptName = (list: DeptRecord[]): string | undefined => {
|
||||
for (const dept of list) {
|
||||
if (dept.id === value) return dept.name
|
||||
if (dept.children) {
|
||||
const found = findDeptName(dept.children)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
formData.deptName = findDeptName(deptTreeData.value)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
// 确保部门树已加载
|
||||
if (deptTreeData.value.length === 0) {
|
||||
loadDeptTree()
|
||||
}
|
||||
|
||||
if (props.record) {
|
||||
Object.assign(formData, { ...props.record, password: '' })
|
||||
} else {
|
||||
@@ -100,6 +149,8 @@ watch(
|
||||
phone: '',
|
||||
email: '',
|
||||
role: undefined,
|
||||
deptId: undefined,
|
||||
deptName: undefined,
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
@@ -110,13 +161,6 @@ watch(
|
||||
|
||||
async function handleOk() {
|
||||
try {
|
||||
// Skip password validation if editing
|
||||
if (formData.id) {
|
||||
// A quick hack for optional password in edit mode if not handled by dynamic rules
|
||||
// AntDV form validation is powerful.
|
||||
// Ideally we should use dynamic rules.
|
||||
}
|
||||
|
||||
await formRef.value?.validate()
|
||||
loading.value = true
|
||||
|
||||
@@ -141,4 +185,9 @@ function handleCancel() {
|
||||
emit('update:visible', false)
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDeptTree()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
*/
|
||||
|
||||
// 审批人选择方式
|
||||
export type ApproverType = 'specified' | 'role' | 'superior' | 'self_select'
|
||||
export type ApproverType = 'specified' | 'role' | 'department' | 'superior' | 'self_select'
|
||||
|
||||
// 审批人类型映射
|
||||
export const ApproverTypeMap: Record<ApproverType, string> = {
|
||||
specified: '指定人员',
|
||||
role: '按角色',
|
||||
department: '按部门',
|
||||
superior: '上级领导',
|
||||
self_select: '发起人自选'
|
||||
}
|
||||
@@ -29,6 +30,8 @@ export interface ApprovalNode {
|
||||
approverType: ApproverType // 审批人选择方式
|
||||
approverIds?: number[] // 指定审批人ID列表(approverType为specified时使用)
|
||||
approverRole?: string // 审批角色(approverType为role时使用)
|
||||
deptId?: number // 部门ID(approverType为department时使用)
|
||||
deptName?: string // 部门名称
|
||||
approvalMode: ApprovalMode // 审批方式
|
||||
timeoutHours?: number // 超时时间(小时)
|
||||
timeoutAction?: 'skip' | 'reject' // 超时操作
|
||||
|
||||
@@ -10,6 +10,8 @@ export interface UserRecord {
|
||||
email?: string
|
||||
phone?: string
|
||||
role: string
|
||||
deptId?: number
|
||||
deptName?: string
|
||||
status: number
|
||||
createdAt?: string
|
||||
lastLoginTime?: string
|
||||
@@ -21,6 +23,7 @@ export interface UserQuery {
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
role?: string
|
||||
deptId?: number
|
||||
status?: number
|
||||
}
|
||||
|
||||
@@ -32,6 +35,8 @@ export interface UserFormData {
|
||||
phone?: string
|
||||
email?: string
|
||||
role?: string
|
||||
deptId?: number
|
||||
deptName?: string
|
||||
status: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
placeholder="请选择服务器"
|
||||
@change="handleServerChange"
|
||||
>
|
||||
<a-select-option v-for="server in serverList" :key="server.id" :value="server.id">
|
||||
{{ server.name }} ({{ server.address }})
|
||||
<a-select-option v-for="server in serverListData" :key="server.id" :value="server.id">
|
||||
{{ server.name }} ({{ server.ip }})
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
@@ -26,6 +26,9 @@
|
||||
<a-button type="primary" @click="handleApply" :disabled="!selectedServer">
|
||||
<PlusOutlined /> 申请证书
|
||||
</a-button>
|
||||
<a-button @click="handleSync" :loading="syncing" :disabled="!selectedServer" style="margin-left: 12px;">
|
||||
<SyncOutlined /> 同步证书
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 证书列表 -->
|
||||
@@ -137,15 +140,7 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="验证方式" name="verifyType" required>
|
||||
<a-radio-group v-model:value="formData.verifyType">
|
||||
<a-radio value="dns">DNS账号</a-radio>
|
||||
<a-radio value="manual">手动解析</a-radio>
|
||||
<a-radio value="http">HTTP</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="formData.verifyType === 'dns'" label="DNS账号" name="dnsAccount" required>
|
||||
<a-form-item label="DNS账号" name="dnsAccount" required>
|
||||
<a-select v-model:value="formData.dnsAccount" placeholder="请选择 DNS 账号">
|
||||
<a-select-option v-for="account in dnsAccountList" :key="account.value" :value="account.value">
|
||||
{{ account.label }}
|
||||
@@ -273,20 +268,25 @@ import {
|
||||
PlusOutlined,
|
||||
ArrowLeftOutlined,
|
||||
CopyOutlined,
|
||||
DownloadOutlined
|
||||
DownloadOutlined,
|
||||
SyncOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import type { TablePaginationConfig } from 'ant-design-vue'
|
||||
import {
|
||||
getAllWebsites,
|
||||
getCertificateList,
|
||||
getCertificateDetail,
|
||||
deleteCertificate,
|
||||
applyCertificate,
|
||||
syncCertificates,
|
||||
getAcmeAccounts,
|
||||
getDnsAccounts,
|
||||
getSSLCertificates,
|
||||
setPanelServer,
|
||||
type Website,
|
||||
getWebsites,
|
||||
type CertificateInfo,
|
||||
type AcmeAccount,
|
||||
type DnsAccount,
|
||||
type SSLCertificate
|
||||
} from '@/api/1panel'
|
||||
type WebsiteInfo
|
||||
} from '@/api/certificate'
|
||||
import { getServerList, type ServerBase } from '@/api/server'
|
||||
|
||||
// 证书类型定义
|
||||
interface Certificate {
|
||||
@@ -299,7 +299,7 @@ interface Certificate {
|
||||
startDate: string
|
||||
expireDate: string
|
||||
autoRenew: boolean
|
||||
verifyType: 'dns' | 'manual' | 'http'
|
||||
verifyType: 'dns'
|
||||
dnsAccount?: string
|
||||
dnsAccountName?: string
|
||||
dnsAccountType?: string
|
||||
@@ -429,63 +429,48 @@ MIIEpAIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyf8wwwwwwwwwwww
|
||||
}
|
||||
]
|
||||
|
||||
// 服务器列表 mock 数据
|
||||
// 每个服务器对应不同的 1Panel 实例,有各自的地址和 API 密钥
|
||||
// 服务器列表(从后端获取)
|
||||
interface ServerConfig {
|
||||
id: string
|
||||
id: number
|
||||
name: string
|
||||
address: string
|
||||
apiKey: string
|
||||
ip: string
|
||||
}
|
||||
|
||||
const serverList = ref<ServerConfig[]>([
|
||||
{
|
||||
id: 'server1',
|
||||
name: '主服务器',
|
||||
address: '47.109.57.58:42588',
|
||||
apiKey: import.meta.env.VITE_1PANEL_API_KEY || ''
|
||||
},
|
||||
{
|
||||
id: 'server2',
|
||||
name: '测试服务器',
|
||||
address: '192.168.1.100:42588',
|
||||
apiKey: 'test_api_key_123'
|
||||
},
|
||||
{
|
||||
id: 'server3',
|
||||
name: '备份服务器',
|
||||
address: '10.0.0.1:42588',
|
||||
apiKey: 'backup_api_key_456'
|
||||
const serverListData = ref<ServerConfig[]>([])
|
||||
|
||||
// 加载服务器列表
|
||||
async function loadServerList() {
|
||||
try {
|
||||
const servers = await getServerList()
|
||||
serverListData.value = servers.map((s: ServerBase) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
ip: s.ip
|
||||
}))
|
||||
// 默认选择第一个服务器
|
||||
if (serverListData.value.length > 0 && !selectedServer.value) {
|
||||
selectedServer.value = serverListData.value[0].id
|
||||
handleServerChange(serverListData.value[0].id)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取服务器列表失败:', error)
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
// 当前选中的服务器
|
||||
const selectedServer = ref<string>('')
|
||||
const currentServerConfig = ref<ServerConfig | null>(null)
|
||||
const selectedServer = ref<number | undefined>(undefined)
|
||||
|
||||
// 处理服务器切换
|
||||
function handleServerChange(serverId: string) {
|
||||
const server = serverList.value.find(s => s.id === serverId)
|
||||
if (server) {
|
||||
currentServerConfig.value = server
|
||||
function handleServerChange(serverId: number) {
|
||||
selectedServer.value = serverId
|
||||
console.log(`[证书管理] 切换到服务器ID: ${serverId}`)
|
||||
|
||||
// 切换 1Panel API 服务器配置
|
||||
setPanelServer({
|
||||
id: server.id,
|
||||
address: server.address,
|
||||
apiKey: server.apiKey
|
||||
})
|
||||
|
||||
console.log(`[证书管理] 切换到服务器: ${server.name}`)
|
||||
console.log(`[证书管理] 请求地址: http://${server.address}/api/v2`)
|
||||
|
||||
// 重新加载证书列表
|
||||
loadCertificates()
|
||||
// 重新加载下拉数据
|
||||
loadWebsites()
|
||||
loadAcmeAccounts()
|
||||
loadDnsAccounts()
|
||||
}
|
||||
// 重新加载证书列表
|
||||
loadCertificates()
|
||||
// 重新加载下拉数据
|
||||
loadWebsites()
|
||||
loadAcmeAccounts()
|
||||
loadDnsAccounts()
|
||||
}
|
||||
|
||||
// 响应式状态
|
||||
@@ -502,6 +487,9 @@ const pagination = reactive<TablePaginationConfig>({
|
||||
showTotal: (total: number) => `共 ${total} 条`
|
||||
})
|
||||
|
||||
// 同步状态
|
||||
const syncing = ref(false)
|
||||
|
||||
// 申请/编辑抽屉
|
||||
const applyDrawerVisible = ref(false)
|
||||
const isEditing = ref(false)
|
||||
@@ -516,7 +504,6 @@ const formData = reactive({
|
||||
fromWebsite: undefined as string | undefined,
|
||||
acmeAccount: undefined as number | undefined,
|
||||
keyAlgorithm: 'EC 256',
|
||||
verifyType: 'dns' as 'dns' | 'manual' | 'http',
|
||||
dnsAccount: undefined as number | undefined,
|
||||
autoRenew: true
|
||||
})
|
||||
@@ -526,7 +513,6 @@ const formRules = {
|
||||
domain: [{ required: true, message: '请输入主域名' }],
|
||||
acmeAccount: [{ required: true, message: '请选择 Acme 账户' }],
|
||||
keyAlgorithm: [{ required: true, message: '请选择密钥算法' }],
|
||||
verifyType: [{ required: true, message: '请选择验证方式' }],
|
||||
dnsAccount: [{ required: true, message: '请选择 DNS 账号' }]
|
||||
}
|
||||
|
||||
@@ -537,96 +523,90 @@ const dnsAccountList = ref<{ value: number; label: string }[]>([])
|
||||
|
||||
// 加载网站列表
|
||||
async function loadWebsites() {
|
||||
if (!selectedServer.value) return
|
||||
try {
|
||||
const websites = await getAllWebsites()
|
||||
websiteList.value = websites.map((site: Website) => ({
|
||||
const websites = await getWebsites(selectedServer.value)
|
||||
websiteList.value = websites.map((site: WebsiteInfo) => ({
|
||||
value: site.primaryDomain,
|
||||
label: site.primaryDomain
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('获取网站列表失败:', error)
|
||||
// 失败时使用默认数据
|
||||
websiteList.value = [
|
||||
{ value: 'nanxiislet.com', label: 'nanxiislet.com' },
|
||||
{ value: 'www.nanxiislet.com', label: 'www.nanxiislet.com' }
|
||||
]
|
||||
websiteList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 加载Acme账户列表
|
||||
async function loadAcmeAccounts() {
|
||||
if (!selectedServer.value) return
|
||||
try {
|
||||
const result = await getAcmeAccounts()
|
||||
acmeAccountList.value = (result.items || []).map((account: AcmeAccount) => ({
|
||||
const accounts = await getAcmeAccounts(selectedServer.value)
|
||||
acmeAccountList.value = accounts.map((account: AcmeAccount) => ({
|
||||
value: account.id,
|
||||
label: `${account.email} [${account.type || "Let's Encrypt"}]`
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('获取Acme账户列表失败:', error)
|
||||
// 失败时使用默认数据
|
||||
acmeAccountList.value = [
|
||||
{ value: 1, label: "acme@1paneldev.com [Let's Encrypt]" }
|
||||
]
|
||||
acmeAccountList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 加载DNS账户列表
|
||||
async function loadDnsAccounts() {
|
||||
if (!selectedServer.value) return
|
||||
try {
|
||||
const result = await getDnsAccounts()
|
||||
dnsAccountList.value = (result.items || []).map((account: DnsAccount) => ({
|
||||
const accounts = await getDnsAccounts(selectedServer.value)
|
||||
dnsAccountList.value = accounts.map((account: DnsAccount) => ({
|
||||
value: account.id,
|
||||
label: `${account.name} [${account.type}]`
|
||||
label: `${account.name} [${account.type || ''}]`
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('获取DNS账户列表失败:', error)
|
||||
// 失败时使用默认数据
|
||||
dnsAccountList.value = [
|
||||
{ value: 1, label: '阿里云 [阿里云]' },
|
||||
{ value: 2, label: 'Cloudflare [Cloudflare]' }
|
||||
]
|
||||
dnsAccountList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 加载证书列表
|
||||
async function loadCertificates() {
|
||||
if (!selectedServer.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await getSSLCertificates({
|
||||
const result = await getCertificateList({
|
||||
page: pagination.current || 1,
|
||||
pageSize: pagination.pageSize || 20
|
||||
pageSize: pagination.pageSize || 20,
|
||||
serverId: selectedServer.value
|
||||
})
|
||||
|
||||
// 转换API数据为本地格式
|
||||
certificateList.value = (result.items || []).map((cert: SSLCertificate) => {
|
||||
certificateList.value = (result.records || []).map((cert: CertificateInfo) => {
|
||||
// 根据 acmeAccountId 查找对应的 Acme 账户 email
|
||||
const acmeAccount = acmeAccountList.value.find(a => a.value === cert.acmeAccountId)
|
||||
const acmeAccountEmail = acmeAccount?.label.split(' [')[0] || ''
|
||||
const acmeAccountEmail = acmeAccount?.label.split(' [')[0] || cert.acmeAccountEmail || ''
|
||||
|
||||
// 根据 dnsAccountId 查找对应的 DNS 账户
|
||||
const dnsAccount = dnsAccountList.value.find(d => d.value === cert.dnsAccountId)
|
||||
const dnsAccountName = dnsAccount?.label.split(' [')[0] || ''
|
||||
const dnsAccountType = dnsAccount?.label.match(/\[(.+)\]/)?.[1] || ''
|
||||
const dnsAccountName = dnsAccount?.label.split(' [')[0] || cert.dnsAccountName || ''
|
||||
const dnsAccountType = dnsAccount?.label.match(/\[(.+)\]/)?.[1] || cert.dnsAccountType || ''
|
||||
|
||||
return {
|
||||
id: String(cert.id),
|
||||
domain: cert.primaryDomain,
|
||||
otherDomain: cert.domains || '',
|
||||
cn: cert.organization || '',
|
||||
otherDomain: cert.otherDomains || '',
|
||||
cn: cert.cn || '',
|
||||
issuer: cert.organization || "Let's Encrypt",
|
||||
status: getSSLStatus(cert.status, cert.expireDate),
|
||||
status: getSSLStatus(cert.status, cert.expireDate || ''),
|
||||
startDate: cert.startDate,
|
||||
expireDate: cert.expireDate,
|
||||
autoRenew: cert.autoRenew,
|
||||
verifyType: getVerifyType(cert.provider),
|
||||
verifyType: getVerifyType(cert.provider || ''),
|
||||
verifyTypeText: cert.provider || 'DNS账号',
|
||||
dnsAccount: String(cert.dnsAccountId || ''),
|
||||
dnsAccountName: dnsAccountName,
|
||||
dnsAccountType: dnsAccountType,
|
||||
acmeAccount: acmeAccountEmail,
|
||||
remark: cert.description || '',
|
||||
certContent: '',
|
||||
keyContent: ''
|
||||
certContent: cert.certContent || '',
|
||||
keyContent: cert.keyContent || ''
|
||||
}
|
||||
})
|
||||
|
||||
@@ -653,19 +633,13 @@ function getSSLStatus(status: string, expireDate: string): 'valid' | 'expired' |
|
||||
}
|
||||
|
||||
// 获取验证方式
|
||||
function getVerifyType(provider: string): 'dns' | 'manual' | 'http' {
|
||||
if (provider?.toLowerCase().includes('dns')) return 'dns'
|
||||
if (provider?.toLowerCase().includes('http')) return 'http'
|
||||
function getVerifyType(_provider: string): 'dns' {
|
||||
return 'dns'
|
||||
}
|
||||
|
||||
// 页面加载时选择默认服务器
|
||||
onMounted(() => {
|
||||
// 默认选择第一个服务器
|
||||
if (serverList.value.length > 0) {
|
||||
selectedServer.value = serverList.value[0].id
|
||||
handleServerChange(serverList.value[0].id)
|
||||
}
|
||||
loadServerList()
|
||||
})
|
||||
|
||||
// 详情抽屉
|
||||
@@ -701,8 +675,8 @@ function getVerifyTypeTextStatic(type?: string) {
|
||||
return texts[type || ''] || type || '-'
|
||||
}
|
||||
|
||||
function getVerifyTypeText(type?: string) {
|
||||
return getVerifyTypeTextStatic(type)
|
||||
function getVerifyTypeText(_type?: string) {
|
||||
return 'DNS账号'
|
||||
}
|
||||
|
||||
function isExpiringSoon(expireDate: string) {
|
||||
@@ -727,6 +701,27 @@ function handleWebsiteSelect(value: string | undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
// 同步证书
|
||||
async function handleSync() {
|
||||
if (!selectedServer.value) {
|
||||
message.warning('请先选择服务器')
|
||||
return
|
||||
}
|
||||
|
||||
syncing.value = true
|
||||
try {
|
||||
const result = await syncCertificates(selectedServer.value)
|
||||
message.success(`${result.message}`)
|
||||
// 重新加载证书列表
|
||||
await loadCertificates()
|
||||
} catch (error) {
|
||||
message.error('同步失败')
|
||||
console.error('同步证书失败:', error)
|
||||
} finally {
|
||||
syncing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 申请证书
|
||||
function handleApply() {
|
||||
isEditing.value = false
|
||||
@@ -737,7 +732,6 @@ function handleApply() {
|
||||
fromWebsite: undefined,
|
||||
acmeAccount: undefined,
|
||||
keyAlgorithm: 'EC 256',
|
||||
verifyType: 'dns',
|
||||
dnsAccount: undefined,
|
||||
autoRenew: true
|
||||
})
|
||||
@@ -756,7 +750,6 @@ function handleEdit(record: Certificate) {
|
||||
fromWebsite: undefined,
|
||||
acmeAccount: undefined, // 编辑时需要重新选择
|
||||
keyAlgorithm: 'EC 256',
|
||||
verifyType: record.verifyType,
|
||||
dnsAccount: undefined, // 编辑时需要重新选择
|
||||
autoRenew: record.autoRenew
|
||||
})
|
||||
@@ -790,8 +783,8 @@ async function handleFormSubmit() {
|
||||
domain: formData.domain,
|
||||
otherDomain: formData.otherDomain,
|
||||
acmeAccount: acmeAccountStr,
|
||||
verifyType: formData.verifyType,
|
||||
verifyTypeText: getVerifyTypeTextStatic(formData.verifyType),
|
||||
verifyType: 'dns',
|
||||
verifyTypeText: 'DNS账号',
|
||||
dnsAccount: dnsAccountStr,
|
||||
dnsAccountName: dnsAccountStr,
|
||||
dnsAccountType: dnsAccountTypeStr,
|
||||
@@ -800,27 +793,25 @@ async function handleFormSubmit() {
|
||||
}
|
||||
message.success('证书更新成功')
|
||||
} else {
|
||||
const newCert: Certificate & { verifyTypeText: string } = {
|
||||
id: String(certificateList.value.length + 1),
|
||||
domain: formData.domain,
|
||||
otherDomain: formData.otherDomain,
|
||||
cn: 'E7',
|
||||
issuer: "Let's Encrypt",
|
||||
status: 'pending',
|
||||
startDate: new Date().toISOString().split('T')[0],
|
||||
expireDate: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
||||
autoRenew: formData.autoRenew,
|
||||
verifyType: formData.verifyType,
|
||||
verifyTypeText: getVerifyTypeTextStatic(formData.verifyType),
|
||||
dnsAccount: dnsAccountStr,
|
||||
dnsAccountName: dnsAccountStr,
|
||||
dnsAccountType: dnsAccountTypeStr,
|
||||
acmeAccount: acmeAccountStr,
|
||||
certContent: '证书申请中...',
|
||||
keyContent: '证书申请中...'
|
||||
if (!selectedServer.value) {
|
||||
message.warning('请先选择服务器')
|
||||
return
|
||||
}
|
||||
certificateList.value.unshift(newCert)
|
||||
|
||||
const applyData = {
|
||||
serverId: Number(selectedServer.value),
|
||||
primaryDomain: formData.domain,
|
||||
otherDomains: formData.otherDomain,
|
||||
acmeAccountId: Number(formData.acmeAccount),
|
||||
dnsAccountId: Number(formData.dnsAccount),
|
||||
keyType: formData.keyAlgorithm,
|
||||
autoRenew: formData.autoRenew
|
||||
}
|
||||
|
||||
await applyCertificate(applyData)
|
||||
message.success('证书申请已提交')
|
||||
applyDrawerVisible.value = false
|
||||
loadCertificates()
|
||||
}
|
||||
|
||||
applyDrawerVisible.value = false
|
||||
@@ -832,18 +823,41 @@ async function handleFormSubmit() {
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
function handleViewDetail(record: Certificate) {
|
||||
currentCertificate.value = record
|
||||
async function handleViewDetail(record: Certificate) {
|
||||
currentCertificate.value = { ...record }
|
||||
detailActiveTab.value = 'info'
|
||||
detailDrawerVisible.value = true
|
||||
|
||||
// 异步加载证书详情(包含证书内容和私钥)
|
||||
try {
|
||||
const detail = await getCertificateDetail(Number(record.id))
|
||||
if (detail) {
|
||||
// 合并详情数据
|
||||
currentCertificate.value = {
|
||||
...currentCertificate.value,
|
||||
certContent: detail.certContent || '',
|
||||
keyContent: detail.keyContent || '',
|
||||
cn: detail.cn || currentCertificate.value?.cn || ''
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取证书详情失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除证书
|
||||
function handleDelete(record: Certificate) {
|
||||
const index = certificateList.value.findIndex(c => c.id === record.id)
|
||||
if (index > -1) {
|
||||
certificateList.value.splice(index, 1)
|
||||
async function handleDelete(record: Certificate) {
|
||||
try {
|
||||
await deleteCertificate(Number(record.id))
|
||||
// 从列表中移除
|
||||
const index = certificateList.value.findIndex(c => c.id === record.id)
|
||||
if (index > -1) {
|
||||
certificateList.value.splice(index, 1)
|
||||
}
|
||||
message.success('证书已删除')
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
console.error('删除证书失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,9 +41,14 @@
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<PlusOutlined /> 添加域名
|
||||
</a-button>
|
||||
<div class="action-buttons">
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<PlusOutlined /> 添加域名
|
||||
</a-button>
|
||||
<a-button @click="handleSyncFromCert" :loading="syncing" style="margin-left: 12px">
|
||||
<SyncOutlined /> 从证书同步
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
@@ -158,16 +163,16 @@
|
||||
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<a-button type="link" size="small" @click="handleEdit(record as DomainInfo)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleViewNginx(record)">
|
||||
<a-button type="link" size="small" @click="handleViewNginx(record as DomainInfo)">
|
||||
<FileTextOutlined /> Nginx
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleCheckDns(record)">
|
||||
<a-button type="link" size="small" @click="handleCheckDns(record as DomainInfo)">
|
||||
<ReloadOutlined /> 检测
|
||||
</a-button>
|
||||
<a-popconfirm title="确定删除此域名配置?" @confirm="handleDelete(record)">
|
||||
<a-popconfirm title="确定删除此域名配置?" @confirm="handleDelete(record as DomainInfo)">
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
@@ -226,20 +231,30 @@
|
||||
<a-switch v-model:checked="formData.enableHttps" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="formData.enableHttps" label="强制 HTTPS">
|
||||
<a-switch v-model:checked="formData.forceHttps" />
|
||||
<span class="form-help">开启后将自动跳转 HTTPS</span>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="formData.enableHttps" label="SSL 证书" name="certificateId">
|
||||
<a-select v-model:value="formData.certificateId" placeholder="请选择 SSL 证书">
|
||||
<a-select-option v-for="cert in certificateList" :key="cert.id" :value="cert.id">
|
||||
{{ cert.domain }} (过期:{{ cert.expireDate }})
|
||||
<a-select
|
||||
v-model:value="formData.certificateId"
|
||||
placeholder="请选择 SSL 证书"
|
||||
:disabled="filteredCertificates.length === 0"
|
||||
>
|
||||
<a-select-option v-for="cert in filteredCertificates" :key="cert.id" :value="cert.id">
|
||||
{{ cert.primaryDomain }} (过期:{{ cert.expireDate }})
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<div class="form-help">
|
||||
没有合适的证书?
|
||||
<a-button type="link" size="small" @click="goToCertificates">前往申请</a-button>
|
||||
<template v-if="!formData.serverId">
|
||||
<span class="text-warning">请先选择目标服务器</span>
|
||||
</template>
|
||||
<template v-else-if="!formData.domain">
|
||||
请先输入域名以匹配证书
|
||||
</template>
|
||||
<template v-else-if="filteredCertificates.length === 0">
|
||||
<a-tag color="warning">该域名没有匹配的证书</a-tag>
|
||||
<a-button type="link" size="small" @click="goToCertificates">前往申请</a-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
已找到 {{ filteredCertificates.length }} 个匹配的证书
|
||||
</template>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
@@ -306,7 +321,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
@@ -327,9 +342,21 @@ import {
|
||||
DownloadOutlined,
|
||||
CloudUploadOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import type { DomainInfo, DomainStatus, DnsStatus, SslStatus } from '@/types/platform'
|
||||
import { mockDomains, mockServers } from '@/mock/platform'
|
||||
import { mockProjects } from '@/mock/projects'
|
||||
import type { DomainStatus, DnsStatus, SslStatus } from '@/types/platform'
|
||||
import {
|
||||
getDomainList,
|
||||
getAllDomains,
|
||||
createDomain,
|
||||
updateDomain,
|
||||
deleteDomain,
|
||||
checkDomainDns,
|
||||
getDomainStats,
|
||||
syncDomainsFromCertificates,
|
||||
type DomainInfo
|
||||
} from '@/api/domain'
|
||||
import { getServerList } from '@/api/server'
|
||||
import { getAllProjects } from '@/api/project'
|
||||
import { getCertificateList, type CertificateInfo } from '@/api/certificate'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -348,22 +375,27 @@ const filterKeyword = ref('')
|
||||
const domains = ref<DomainInfo[]>([])
|
||||
const currentDomain = ref<DomainInfo | null>(null)
|
||||
|
||||
const projectList = computed(() => mockProjects.map(p => ({ id: p.id, name: p.name })))
|
||||
const serverList = computed(() => mockServers.map(s => ({ id: s.id, name: s.name, ip: s.ip })))
|
||||
const certificateList = ref([
|
||||
{ id: 'cert-001', domain: 'codeport.nanxiislet.com', expireDate: '2025-03-18' },
|
||||
{ id: 'cert-002', domain: '*.nanxiislet.com', expireDate: '2025-03-18' }
|
||||
])
|
||||
const projectList = ref<{ id: number; name: string }[]>([])
|
||||
const serverList = ref<{ id: number; name: string; ip: string }[]>([])
|
||||
const certificateList = ref<CertificateInfo[]>([])
|
||||
|
||||
const formData = reactive({
|
||||
const formData = reactive<{
|
||||
domain: string
|
||||
projectId?: number
|
||||
serverId?: number
|
||||
proxyPass: string
|
||||
port: number
|
||||
enableHttps: boolean
|
||||
certificateId?: number
|
||||
description: string
|
||||
}>({
|
||||
domain: '',
|
||||
projectId: '',
|
||||
serverId: '',
|
||||
projectId: undefined,
|
||||
serverId: undefined,
|
||||
proxyPass: '',
|
||||
port: 80,
|
||||
enableHttps: false,
|
||||
forceHttps: false,
|
||||
certificateId: '',
|
||||
certificateId: undefined,
|
||||
description: ''
|
||||
})
|
||||
|
||||
@@ -382,21 +414,7 @@ const columns = [
|
||||
]
|
||||
|
||||
const filteredDomains = computed(() => {
|
||||
let result = domains.value
|
||||
if (filterStatus.value) {
|
||||
result = result.filter(d => d.status === filterStatus.value)
|
||||
}
|
||||
if (filterSslStatus.value) {
|
||||
result = result.filter(d => d.sslStatus === filterSslStatus.value)
|
||||
}
|
||||
if (filterServer.value) {
|
||||
result = result.filter(d => d.serverId === filterServer.value)
|
||||
}
|
||||
if (filterKeyword.value) {
|
||||
const keyword = filterKeyword.value.toLowerCase()
|
||||
result = result.filter(d => d.domain.toLowerCase().includes(keyword))
|
||||
}
|
||||
return result
|
||||
return domains.value
|
||||
})
|
||||
|
||||
const stats = computed(() => {
|
||||
@@ -409,23 +427,103 @@ const stats = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 根据输入的域名过滤匹配的证书
|
||||
const filteredCertificates = computed(() => {
|
||||
if (!formData.domain || !formData.serverId) return []
|
||||
|
||||
const inputDomain = formData.domain.toLowerCase()
|
||||
|
||||
return certificateList.value.filter(cert => {
|
||||
// 匹配主域名
|
||||
const primaryDomain = cert.primaryDomain?.toLowerCase() || ''
|
||||
if (primaryDomain === inputDomain) return true
|
||||
|
||||
// 匹配通配符证书
|
||||
if (primaryDomain.startsWith('*.')) {
|
||||
const wildcardBase = primaryDomain.slice(2)
|
||||
if (inputDomain.endsWith(wildcardBase)) return true
|
||||
}
|
||||
|
||||
// 匹配其他域名
|
||||
const otherDomains = cert.otherDomains?.toLowerCase() || ''
|
||||
if (otherDomains.includes(inputDomain)) return true
|
||||
|
||||
return false
|
||||
})
|
||||
})
|
||||
|
||||
// 加载证书列表
|
||||
async function loadCertificates(serverId: number) {
|
||||
try {
|
||||
const res = await getCertificateList({ page: 1, pageSize: 500, serverId })
|
||||
certificateList.value = res.records || []
|
||||
} catch (error) {
|
||||
console.error('加载证书列表失败:', error)
|
||||
certificateList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 服务器变化时重新加载证书
|
||||
watch(() => formData.serverId, async (newServerId) => {
|
||||
if (newServerId) {
|
||||
await loadCertificates(Number(newServerId))
|
||||
} else {
|
||||
certificateList.value = []
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (route.query.serverId) {
|
||||
filterServer.value = route.query.serverId as string
|
||||
}
|
||||
loadDomains()
|
||||
await Promise.all([loadDomains(), loadProjects(), loadServers()])
|
||||
})
|
||||
|
||||
function loadDomains() {
|
||||
async function loadDomains() {
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
domains.value = [...mockDomains]
|
||||
try {
|
||||
const params = {
|
||||
page: 1,
|
||||
pageSize: 1000,
|
||||
serverId: filterServer.value ? Number(filterServer.value) : undefined,
|
||||
status: filterStatus.value,
|
||||
sslStatus: filterSslStatus.value,
|
||||
keyword: filterKeyword.value
|
||||
}
|
||||
const res = await getDomainList(params)
|
||||
domains.value = res.records.map((d: any) => ({
|
||||
...d,
|
||||
id: String(d.id),
|
||||
projectId: d.projectId ? String(d.projectId) : undefined,
|
||||
serverId: d.serverId ? String(d.serverId) : undefined
|
||||
}))
|
||||
} catch (error) {
|
||||
message.error('加载域名列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
// 筛选已通过 computed 实现
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const res = await getAllProjects()
|
||||
projectList.value = res.map((p: any) => ({ id: p.id, name: p.name }))
|
||||
} catch (error) {
|
||||
console.error('加载项目列表失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadServers() {
|
||||
try {
|
||||
const res = await getServerList()
|
||||
serverList.value = res.map((s: any) => ({ id: s.id, name: s.name, ip: s.ip }))
|
||||
} catch (error) {
|
||||
console.error('加载服务器列表失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSearch() {
|
||||
await loadDomains()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
@@ -451,20 +549,59 @@ function handleAdd() {
|
||||
drawerVisible.value = true
|
||||
}
|
||||
|
||||
function handleEdit(record: DomainInfo) {
|
||||
// 同步状态
|
||||
const syncing = ref(false)
|
||||
|
||||
// 从证书同步域名
|
||||
async function handleSyncFromCert() {
|
||||
if (!filterServer.value) {
|
||||
message.warning('请先在筛选栏选择服务器')
|
||||
return
|
||||
}
|
||||
|
||||
syncing.value = true
|
||||
try {
|
||||
const res = await syncDomainsFromCertificates(Number(filterServer.value))
|
||||
message.success(res.message)
|
||||
await loadDomains()
|
||||
} catch (error) {
|
||||
message.error('同步失败')
|
||||
console.error(error)
|
||||
} finally {
|
||||
syncing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到证书申请
|
||||
function goToCertificates() {
|
||||
// 如果有选中的服务器,带上服务器ID参数
|
||||
const query = formData.serverId ? { serverId: formData.serverId } : {}
|
||||
router.push({ path: '/platform/certificates', query })
|
||||
}
|
||||
|
||||
async function handleEdit(record: DomainInfo) {
|
||||
currentDomain.value = record
|
||||
Object.assign(formData, {
|
||||
domain: record.domain,
|
||||
projectId: record.projectId || '',
|
||||
serverId: record.serverId || '',
|
||||
proxyPass: record.proxyPass || '',
|
||||
port: record.port || 80,
|
||||
enableHttps: record.enableHttps,
|
||||
forceHttps: record.forceHttps,
|
||||
certificateId: record.certificateId || '',
|
||||
description: record.description || ''
|
||||
})
|
||||
|
||||
// 重置表单并赋值,确保类型为 Number
|
||||
formData.domain = record.domain
|
||||
formData.projectId = record.projectId ? Number(record.projectId) : undefined
|
||||
formData.serverId = record.serverId ? Number(record.serverId) : undefined
|
||||
formData.proxyPass = record.proxyPass || ''
|
||||
formData.port = record.port || 80
|
||||
formData.enableHttps = record.enableHttps
|
||||
formData.certificateId = record.certificateId ? Number(record.certificateId) : undefined
|
||||
formData.description = record.description || ''
|
||||
|
||||
drawerVisible.value = true
|
||||
|
||||
// 如果有服务器ID,加载对应的证书列表
|
||||
if (formData.serverId) {
|
||||
try {
|
||||
await loadCertificates(formData.serverId)
|
||||
} catch (e) {
|
||||
console.error('加载证书失败', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFormSubmit() {
|
||||
@@ -476,70 +613,56 @@ async function handleFormSubmit() {
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
const project = projectList.value.find(p => p.id === Number(formData.projectId))
|
||||
const server = serverList.value.find(s => s.id === Number(formData.serverId))
|
||||
|
||||
const project = projectList.value.find(p => p.id === formData.projectId)
|
||||
const server = serverList.value.find(s => s.id === formData.serverId)
|
||||
const domainData = {
|
||||
domain: formData.domain,
|
||||
projectId: formData.projectId ? Number(formData.projectId) : undefined,
|
||||
projectName: project?.name,
|
||||
serverId: formData.serverId ? Number(formData.serverId) : undefined,
|
||||
serverName: server?.name,
|
||||
serverIp: server?.ip,
|
||||
proxyPass: formData.proxyPass,
|
||||
port: formData.port,
|
||||
enableHttps: formData.enableHttps,
|
||||
certificateId: formData.certificateId || undefined,
|
||||
description: formData.description
|
||||
}
|
||||
|
||||
if (currentDomain.value) {
|
||||
// 编辑
|
||||
const index = domains.value.findIndex(d => d.id === currentDomain.value!.id)
|
||||
if (index !== -1) {
|
||||
domains.value[index] = {
|
||||
...domains.value[index],
|
||||
domain: formData.domain,
|
||||
projectId: formData.projectId || undefined,
|
||||
projectName: project?.name,
|
||||
serverId: formData.serverId || undefined,
|
||||
serverName: server?.name,
|
||||
serverIp: server?.ip,
|
||||
proxyPass: formData.proxyPass,
|
||||
port: formData.port,
|
||||
enableHttps: formData.enableHttps,
|
||||
forceHttps: formData.forceHttps,
|
||||
certificateId: formData.certificateId || undefined,
|
||||
description: formData.description,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
await updateDomain(Number(currentDomain.value.id), domainData)
|
||||
message.success('域名配置已更新')
|
||||
} else {
|
||||
// 新增
|
||||
const newDomain: DomainInfo = {
|
||||
id: `domain-${Date.now()}`,
|
||||
domain: formData.domain,
|
||||
projectId: formData.projectId || undefined,
|
||||
projectName: project?.name,
|
||||
serverId: formData.serverId || undefined,
|
||||
serverName: server?.name,
|
||||
serverIp: server?.ip,
|
||||
await createDomain({
|
||||
...domainData,
|
||||
status: 'pending',
|
||||
dnsStatus: 'checking',
|
||||
sslStatus: formData.enableHttps ? 'none' : 'none',
|
||||
proxyPass: formData.proxyPass,
|
||||
port: formData.port,
|
||||
enableHttps: formData.enableHttps,
|
||||
forceHttps: formData.forceHttps,
|
||||
certificateId: formData.certificateId || undefined,
|
||||
description: formData.description,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
domains.value.unshift(newDomain)
|
||||
sslStatus: 'none',
|
||||
deployStatus: 'not_deployed'
|
||||
})
|
||||
message.success('域名添加成功')
|
||||
}
|
||||
|
||||
drawerVisible.value = false
|
||||
} catch (error) {
|
||||
await loadDomains() // 重新加载列表
|
||||
} catch (error: any) {
|
||||
message.error('操作失败')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete(record: DomainInfo) {
|
||||
domains.value = domains.value.filter(d => d.id !== record.id)
|
||||
message.success('删除成功')
|
||||
async function handleDelete(record: DomainInfo) {
|
||||
try {
|
||||
await deleteDomain(Number(record.id))
|
||||
message.success('删除成功')
|
||||
await loadDomains()
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
function handleViewNginx(record: DomainInfo) {
|
||||
@@ -547,25 +670,31 @@ function handleViewNginx(record: DomainInfo) {
|
||||
nginxDrawerVisible.value = true
|
||||
}
|
||||
|
||||
function handleCheckDns(record: DomainInfo) {
|
||||
async function handleCheckDns(record: DomainInfo) {
|
||||
message.loading(`正在检测 ${record.domain} 的 DNS...`, 2)
|
||||
setTimeout(() => {
|
||||
const index = domains.value.findIndex(d => d.id === record.id)
|
||||
if (index !== -1) {
|
||||
domains.value[index].dnsStatus = 'resolved'
|
||||
try {
|
||||
const result = await checkDomainDns(Number(record.id))
|
||||
if (result.resolved) {
|
||||
const index = domains.value.findIndex(d => d.id === record.id)
|
||||
if (index !== -1) {
|
||||
domains.value[index].dnsStatus = 'resolved'
|
||||
}
|
||||
message.success('DNS 解析正常')
|
||||
} else {
|
||||
message.warning('DNS 未解析')
|
||||
}
|
||||
message.success('DNS 解析正常')
|
||||
}, 2000)
|
||||
} catch (error) {
|
||||
message.error('DNS 检测失败')
|
||||
}
|
||||
}
|
||||
|
||||
function goToCertificates() {
|
||||
router.push('/platform/certificates')
|
||||
}
|
||||
|
||||
|
||||
function generateNginxConfig(domain: DomainInfo): string {
|
||||
let config = `server {\n`
|
||||
|
||||
if (domain.enableHttps && domain.forceHttps) {
|
||||
if (domain.enableHttps) {
|
||||
// HTTP 重定向
|
||||
config += ` listen 80;\n`
|
||||
config += ` server_name ${domain.domain};\n`
|
||||
config += ` return 301 https://$server_name$request_uri;\n`
|
||||
@@ -577,11 +706,6 @@ function generateNginxConfig(domain: DomainInfo): string {
|
||||
config += ` ssl_certificate_key /etc/nginx/ssl/${domain.domain}.key;\n`
|
||||
config += ` ssl_protocols TLSv1.2 TLSv1.3;\n`
|
||||
config += ` ssl_ciphers HIGH:!aNULL:!MD5;\n\n`
|
||||
} else if (domain.enableHttps) {
|
||||
config += ` listen 443 ssl http2;\n`
|
||||
config += ` server_name ${domain.domain};\n\n`
|
||||
config += ` ssl_certificate /etc/nginx/ssl/${domain.domain}.pem;\n`
|
||||
config += ` ssl_certificate_key /etc/nginx/ssl/${domain.domain}.key;\n\n`
|
||||
} else {
|
||||
config += ` listen ${domain.port || 80};\n`
|
||||
config += ` server_name ${domain.domain};\n\n`
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
<div class="var-value">
|
||||
<template v-if="record.isSecret && !showSecrets">
|
||||
<span class="secret-mask">••••••••</span>
|
||||
<a-button type="link" size="small" @click="toggleShowValue(record)">
|
||||
<a-button type="link" size="small" @click="toggleShowValue(record as EnvironmentVariable)">
|
||||
<EyeOutlined />
|
||||
</a-button>
|
||||
</template>
|
||||
@@ -168,10 +168,10 @@
|
||||
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEditVariable(record)">
|
||||
<a-button type="link" size="small" @click="handleEditVariable(record as EnvironmentVariable)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm title="确定删除此变量?" @confirm="handleDeleteVariable(record)">
|
||||
<a-popconfirm title="确定删除此变量?" @confirm="handleDeleteVariable(record as EnvironmentVariable)">
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-button type="link" size="small" @click="handleViewDetail(record)">
|
||||
<a-button type="link" size="small" @click="handleViewDetail(record as LogRecord)">
|
||||
<EyeOutlined /> 详情
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<!-- 项目列表 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="projectStore.projects"
|
||||
:data-source="projectList"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
:pagination="false"
|
||||
@@ -44,8 +44,8 @@
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.enabled ? 'green' : 'default'">
|
||||
{{ record.enabled ? '启用' : '禁用' }}
|
||||
<a-tag :color="record.status === 'active' ? 'green' : 'default'">
|
||||
{{ record.status === 'active' ? '启用' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
@@ -62,12 +62,37 @@
|
||||
<a-button type="primary" size="small" ghost @click="handleEnterProject(record as any)">
|
||||
进入
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleUpload(record as any)">
|
||||
<a-button
|
||||
v-if="getDeployStatus((record as any).id).deployed"
|
||||
type="default"
|
||||
size="small"
|
||||
style="color: #52c41a; border-color: #52c41a;"
|
||||
@click="handleDeploy(record as any)"
|
||||
>
|
||||
<CheckCircleOutlined /> 已部署
|
||||
</a-button>
|
||||
<a-button
|
||||
v-else
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleDeploy(record as any)"
|
||||
:disabled="!(record as any).domain"
|
||||
:loading="getDeployStatus((record as any).id).checking"
|
||||
>
|
||||
<CloudUploadOutlined /> 部署
|
||||
</a-button>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleUpload(record as any)"
|
||||
:disabled="!getDeployStatus((record as any).id).deployed"
|
||||
:title="!getDeployStatus((record as any).id).deployed ? '请先部署项目' : ''"
|
||||
>
|
||||
<UploadOutlined /> 上传
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleEdit(record as any)">编辑</a-button>
|
||||
<a-switch
|
||||
:checked="record.enabled"
|
||||
:checked="record.status === 'active'"
|
||||
size="small"
|
||||
@change="() => handleToggle(record as any)"
|
||||
/>
|
||||
@@ -109,9 +134,9 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="项目标识" name="id">
|
||||
<a-form-item label="项目标识" name="code">
|
||||
<a-input
|
||||
v-model:value="formData.id"
|
||||
v-model:value="formData.code"
|
||||
placeholder="如:codePort(英文标识,唯一)"
|
||||
:disabled="!!currentProject?.id"
|
||||
/>
|
||||
@@ -144,35 +169,6 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 访问配置 -->
|
||||
<a-divider orientation="left">访问配置</a-divider>
|
||||
|
||||
<a-form-item label="绑定域名" name="domain">
|
||||
<a-select
|
||||
v-model:value="formData.domain"
|
||||
placeholder="请选择绑定的域名"
|
||||
allow-clear
|
||||
show-search
|
||||
:filter-option="filterDomain"
|
||||
@change="onDomainChange"
|
||||
>
|
||||
<a-select-option v-for="domain in domainList" :key="domain.id" :value="domain.domain">
|
||||
{{ domain.domain }}
|
||||
<a-tag v-if="domain.enableHttps" color="green" size="small" style="margin-left: 8px">HTTPS</a-tag>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<div class="form-item-help">选择已配置的域名,项目地址将自动生成</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="项目地址">
|
||||
<a-input
|
||||
:value="formData.baseUrl"
|
||||
disabled
|
||||
:placeholder="formData.domain ? '' : '选择域名后自动生成'"
|
||||
/>
|
||||
<div class="form-item-help">根据绑定域名自动生成,用于 iframe 嵌套</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 部署配置 -->
|
||||
<a-divider orientation="left">部署配置</a-divider>
|
||||
|
||||
@@ -181,6 +177,7 @@
|
||||
v-model:value="formData.serverId"
|
||||
placeholder="请选择部署服务器"
|
||||
allow-clear
|
||||
@change="(val: any) => handleServerChange(val)"
|
||||
>
|
||||
<a-select-option v-for="server in serverList" :key="server.id" :value="server.id">
|
||||
{{ server.name }} ({{ server.ip }})
|
||||
@@ -200,6 +197,36 @@
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 访问配置 -->
|
||||
<a-divider orientation="left">访问配置</a-divider>
|
||||
|
||||
<a-form-item label="绑定域名" name="domain">
|
||||
<a-select
|
||||
v-model:value="formData.domain"
|
||||
placeholder="请先选择服务器"
|
||||
allow-clear
|
||||
show-search
|
||||
:filter-option="filterDomain"
|
||||
@change="onDomainChange"
|
||||
:disabled="!formData.serverId"
|
||||
>
|
||||
<a-select-option v-for="domain in domainList" :key="domain.id" :value="domain.domain">
|
||||
{{ domain.domain }}
|
||||
<a-tag v-if="domain.enableHttps" color="green" size="small" style="margin-left: 8px">HTTPS</a-tag>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<div class="form-item-help">选择该服务器下的域名,项目地址将自动生成</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="项目地址">
|
||||
<a-input
|
||||
:value="formData.baseUrl"
|
||||
disabled
|
||||
:placeholder="formData.domain ? '' : '选择域名后自动生成'"
|
||||
/>
|
||||
<div class="form-item-help">根据绑定域名自动生成,用于 iframe 嵌套</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 其他 -->
|
||||
<a-divider orientation="left">其他</a-divider>
|
||||
|
||||
@@ -322,8 +349,91 @@
|
||||
v-model:visible="uploadVisible"
|
||||
:project-id="uploadProjectId"
|
||||
:project-name="uploadProjectName"
|
||||
:deploy-path="uploadProjectDeployPath"
|
||||
@uploaded="handleUploadComplete"
|
||||
/>
|
||||
|
||||
<!-- 部署确认弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="deployModalVisible"
|
||||
title="部署到服务器"
|
||||
:confirm-loading="deploying"
|
||||
@ok="handleConfirmDeploy"
|
||||
width="600px"
|
||||
>
|
||||
<div class="deploy-confirm-content">
|
||||
<a-alert
|
||||
v-if="!deployProject?.domain"
|
||||
type="warning"
|
||||
message="该项目未绑定域名,无法部署"
|
||||
show-icon
|
||||
style="margin-bottom: 16px;"
|
||||
/>
|
||||
|
||||
<a-descriptions :column="1" bordered size="small">
|
||||
<a-descriptions-item label="项目名称">{{ deployProject?.name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="绑定域名">
|
||||
<a-tag :color="deployProject?.domain ? 'green' : 'red'">
|
||||
{{ deployProject?.domain || '未绑定' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="部署路径">{{ deployProject?.deployPath || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="目标服务器">
|
||||
{{ getServerName(deployProject?.serverId) || '-' }}
|
||||
</a-descriptions-item>
|
||||
|
||||
</a-descriptions>
|
||||
|
||||
|
||||
|
||||
<a-alert
|
||||
type="info"
|
||||
style="margin-top: 16px;"
|
||||
>
|
||||
<template #message>
|
||||
点击确定后,系统将检查并创建 1Panel 网站。
|
||||
如果不绑定域名,将无法部署。
|
||||
</template>
|
||||
</a-alert>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 部署进度抽屉 -->
|
||||
<a-drawer
|
||||
v-model:open="deployDrawerVisible"
|
||||
title="部署进度"
|
||||
width="500"
|
||||
placement="right"
|
||||
:closable="!deploying"
|
||||
:mask-closable="!deploying"
|
||||
>
|
||||
<div class="deploy-progress">
|
||||
<a-steps direction="vertical" :current="deployCurrentStep" :status="deployStepStatus">
|
||||
<a-step v-for="(step, index) in deploySteps" :key="index" :title="step.title">
|
||||
<template #description>
|
||||
<span :class="['step-desc', step.status]">
|
||||
{{ step.description }}
|
||||
</span>
|
||||
</template>
|
||||
</a-step>
|
||||
</a-steps>
|
||||
|
||||
<div v-if="deployResult" class="deploy-result">
|
||||
<a-result
|
||||
:status="deployResult.success ? 'success' : 'error'"
|
||||
:title="deployResult.success ? '部署成功' : '部署失败'"
|
||||
:sub-title="deployResult.message"
|
||||
>
|
||||
<template #extra>
|
||||
<a-button v-if="deployResult.success && deployProject?.domain" type="primary" @click="openDeployedSite">
|
||||
访问网站
|
||||
</a-button>
|
||||
<a-button @click="deployDrawerVisible = false">关闭</a-button>
|
||||
</template>
|
||||
</a-result>
|
||||
</div>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -338,35 +448,116 @@ import {
|
||||
RollbackOutlined,
|
||||
DeleteOutlined,
|
||||
UploadOutlined,
|
||||
FileOutlined
|
||||
FileOutlined,
|
||||
CloudUploadOutlined,
|
||||
CheckCircleOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import type { PlatformProject, ProjectMenuItem, ProjectVersion } from '@/mock/projects'
|
||||
import { useProjectStore } from '@/stores'
|
||||
import ProjectUpload from '@/components/ProjectUpload.vue'
|
||||
import { mockServers, mockDomains } from '@/mock/platform'
|
||||
import {
|
||||
type ProjectInfo,
|
||||
getProjectList,
|
||||
createProject,
|
||||
updateProject,
|
||||
deleteProject,
|
||||
getAllProjects,
|
||||
deployProject as deployProjectApi,
|
||||
getServerAcmeAccounts,
|
||||
getServerDnsAccounts,
|
||||
checkDeployStatus,
|
||||
type AcmeAccount,
|
||||
type DnsAccount,
|
||||
type DeployResult
|
||||
} from '@/api/project'
|
||||
import { getServerList } from '@/api/server'
|
||||
import { getDomainList } from '@/api/domain'
|
||||
|
||||
const router = useRouter()
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
// 服务器列表
|
||||
const serverList = computed(() => mockServers.map(s => ({ id: s.id, name: s.name, ip: s.ip })))
|
||||
// 项目列表
|
||||
const projectList = ref<ProjectInfo[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 域名列表
|
||||
const domainList = computed(() => mockDomains.map(d => ({
|
||||
id: d.id,
|
||||
domain: d.domain,
|
||||
enableHttps: d.enableHttps
|
||||
})))
|
||||
// 加载项目列表
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
// 获取所有项目(不分页或分页取全部)
|
||||
const res = await getAllProjects()
|
||||
projectList.value = res || []
|
||||
|
||||
// 加载完成后检查所有项目的部署状态
|
||||
checkAllDeployStatus()
|
||||
|
||||
// 可选:同步更新到 Store (需要适配类型)
|
||||
// projectStore.syncProjects(res)
|
||||
} catch (error) {
|
||||
console.error('加载项目列表失败:', error)
|
||||
message.error('加载项目列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 服务器列表
|
||||
const serverList = ref<any[]>([])
|
||||
const domainList = ref<any[]>([])
|
||||
|
||||
// 加载服务器列表
|
||||
async function loadServers() {
|
||||
try {
|
||||
const res = await getServerList({})
|
||||
serverList.value = res || []
|
||||
} catch (error) {
|
||||
console.error('加载服务器失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 服务器变更处理
|
||||
async function handleServerChange(serverId: number, keepDomain = false) {
|
||||
if (!keepDomain) {
|
||||
formData.domain = undefined
|
||||
formData.baseUrl = ''
|
||||
formData.deployPath = ''
|
||||
formData.domainId = undefined
|
||||
}
|
||||
|
||||
if (serverId) {
|
||||
try {
|
||||
// 加载该服务器下的域名
|
||||
const res = await getDomainList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
serverId: Number(serverId)
|
||||
})
|
||||
domainList.value = res.records || []
|
||||
} catch (error) {
|
||||
console.error('加载域名失败:', error)
|
||||
domainList.value = []
|
||||
}
|
||||
} else {
|
||||
domainList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadServers()
|
||||
loadData()
|
||||
})
|
||||
|
||||
// 域名搜索过滤
|
||||
function filterDomain(input: string, option: any): boolean {
|
||||
return option.value.toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
|
||||
// 域名变更时自动生成项目地址和部署路径
|
||||
// 域名变更时自动生成项目地址和部署路径
|
||||
function onDomainChange(domain: string) {
|
||||
if (domain) {
|
||||
// 查找域名是否启用 HTTPS
|
||||
// 查找域名信息
|
||||
const domainInfo = domainList.value.find(d => d.domain === domain)
|
||||
const protocol = domainInfo?.enableHttps ? 'https://' : 'http://'
|
||||
|
||||
@@ -375,21 +566,25 @@ function onDomainChange(domain: string) {
|
||||
|
||||
// 自动生成部署路径(根据 1Panel 的目录结构)
|
||||
formData.deployPath = `/opt/1panel/www/sites/${domain}/index`
|
||||
|
||||
// 设置 domainId
|
||||
formData.domainId = domainInfo?.id
|
||||
} else {
|
||||
formData.baseUrl = ''
|
||||
formData.deployPath = ''
|
||||
formData.domainId = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const formVisible = ref(false)
|
||||
const formLoading = ref(false)
|
||||
const formRef = ref()
|
||||
const currentProject = ref<PlatformProject | null>(null)
|
||||
const currentProject = ref<any | null>(null)
|
||||
|
||||
// 版本管理
|
||||
const versionDrawerVisible = ref(false)
|
||||
const versionProject = ref<PlatformProject | null>(null)
|
||||
const versionProject = ref<any | null>(null)
|
||||
const newVersionDescription = ref('')
|
||||
|
||||
// 文件上传
|
||||
@@ -398,6 +593,56 @@ const uploadProjectId = ref('')
|
||||
const uploadProjectName = ref('')
|
||||
const uploadVersionId = ref<string | null>(null) // 当前上传针对的版本ID
|
||||
|
||||
// 部署相关状态
|
||||
const deployModalVisible = ref(false)
|
||||
const deployDrawerVisible = ref(false)
|
||||
const deploying = ref(false)
|
||||
const deployProject = ref<(PlatformProject & { domain?: string; deployPath?: string; serverId?: string }) | null>(null)
|
||||
const deployEnableHttps = ref(true)
|
||||
const deployAcmeAccountId = ref<number | undefined>()
|
||||
const deployDnsAccountId = ref<number | undefined>()
|
||||
const acmeAccounts = ref<AcmeAccount[]>([])
|
||||
const dnsAccounts = ref<DnsAccount[]>([])
|
||||
const deployResult = ref<DeployResult | null>(null)
|
||||
const deployCurrentStep = ref(0)
|
||||
const deployStepStatus = ref<'process' | 'error' | 'finish'>('process')
|
||||
const deploySteps = ref<{ title: string; description: string; status: string }[]>([])
|
||||
|
||||
// 部署状态映射 - 存储每个项目的部署状态
|
||||
const deployStatusMap = ref<Record<number, { deployed: boolean; websiteId: number | null; checking: boolean }>>({})
|
||||
|
||||
// 检查单个项目的部署状态
|
||||
async function checkProjectDeployStatus(projectId: number) {
|
||||
if (!projectId) return
|
||||
deployStatusMap.value[projectId] = { deployed: false, websiteId: null, checking: true }
|
||||
try {
|
||||
const result = await checkDeployStatus(projectId)
|
||||
deployStatusMap.value[projectId] = {
|
||||
deployed: result.deployed,
|
||||
websiteId: result.websiteId,
|
||||
checking: false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查部署状态失败:', error)
|
||||
deployStatusMap.value[projectId] = { deployed: false, websiteId: null, checking: false }
|
||||
}
|
||||
}
|
||||
|
||||
// 检查所有项目的部署状态
|
||||
async function checkAllDeployStatus() {
|
||||
for (const project of projectList.value) {
|
||||
if (project.id && project.domain && project.serverId) {
|
||||
checkProjectDeployStatus(project.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取项目部署状态
|
||||
function getDeployStatus(projectId: number) {
|
||||
return deployStatusMap.value[projectId] || { deployed: false, websiteId: null, checking: false }
|
||||
}
|
||||
|
||||
|
||||
const versionList = computed(() => {
|
||||
if (!versionProject.value) return []
|
||||
return [...(versionProject.value.versions || [])].reverse()
|
||||
@@ -428,23 +673,25 @@ const certData = [
|
||||
]
|
||||
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
id: '', // 对应后端 ID (Update用)
|
||||
code: '', // 对应后端 Code (项目标识)
|
||||
name: '',
|
||||
shortName: '',
|
||||
logo: '',
|
||||
color: '#1890ff',
|
||||
domain: '',
|
||||
domainId: undefined as number | undefined,
|
||||
baseUrl: '',
|
||||
description: '',
|
||||
group: 'default',
|
||||
serverId: '',
|
||||
serverId: undefined as number | undefined,
|
||||
deployPath: '',
|
||||
remark: '',
|
||||
fileList: [] as any[]
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
id: [
|
||||
code: [
|
||||
{ required: true, message: '请输入项目标识' },
|
||||
{ pattern: /^[a-zA-Z][a-zA-Z0-9]*$/, message: '只能包含英文字母和数字,且以字母开头' }
|
||||
],
|
||||
@@ -582,15 +829,17 @@ function handleAdd() {
|
||||
currentProject.value = null
|
||||
Object.assign(formData, {
|
||||
id: '',
|
||||
code: '',
|
||||
name: '',
|
||||
shortName: '',
|
||||
logo: '',
|
||||
color: '#1890ff',
|
||||
domain: '',
|
||||
domainId: undefined,
|
||||
baseUrl: '',
|
||||
description: '',
|
||||
group: 'default',
|
||||
serverId: '',
|
||||
serverId: undefined,
|
||||
deployPath: '',
|
||||
remark: '',
|
||||
fileList: []
|
||||
@@ -598,23 +847,31 @@ function handleAdd() {
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
function handleEdit(record: PlatformProject) {
|
||||
function handleEdit(record: any) {
|
||||
currentProject.value = record
|
||||
Object.assign(formData, {
|
||||
id: record.id,
|
||||
code: record.code || '',
|
||||
name: record.name,
|
||||
shortName: record.shortName,
|
||||
logo: record.logo,
|
||||
color: record.color || '#1890ff',
|
||||
domain: (record as any).domain || '',
|
||||
baseUrl: record.baseUrl || '',
|
||||
domain: record.domain || '',
|
||||
domainId: record.domainId,
|
||||
baseUrl: record.url || record.baseUrl || '', // 兼容字段
|
||||
description: record.description || '',
|
||||
group: (record as any).group || 'default',
|
||||
serverId: (record as any).serverId || '',
|
||||
deployPath: (record as any).deployPath || '',
|
||||
remark: (record as any).remark || '',
|
||||
group: record.projectGroup || 'default',
|
||||
serverId: record.serverId,
|
||||
deployPath: record.deployPath || '',
|
||||
remark: record.remark || '',
|
||||
fileList: []
|
||||
})
|
||||
|
||||
// 触发联动逻辑(如果需要加载域名列表),并保留现有域名设置
|
||||
if (record.serverId) {
|
||||
handleServerChange(record.serverId, true)
|
||||
}
|
||||
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
@@ -627,68 +884,88 @@ async function handleFormSubmit() {
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
const projectData = {
|
||||
code: formData.code,
|
||||
name: formData.name,
|
||||
shortName: formData.shortName,
|
||||
logo: formData.logo,
|
||||
color: formData.color,
|
||||
domain: formData.domain,
|
||||
baseUrl: formData.baseUrl,
|
||||
domainId: formData.domainId,
|
||||
url: formData.baseUrl,
|
||||
description: formData.description,
|
||||
group: formData.group,
|
||||
projectGroup: formData.group,
|
||||
serverId: formData.serverId,
|
||||
deployPath: formData.deployPath,
|
||||
remark: formData.remark
|
||||
}
|
||||
|
||||
if (currentProject.value?.id) {
|
||||
projectStore.updateProject(currentProject.value.id, projectData)
|
||||
await updateProject(Number(currentProject.value.id), projectData)
|
||||
message.success('编辑成功')
|
||||
} else {
|
||||
if (projectStore.getProjectById(formData.id)) {
|
||||
message.error('项目标识已存在')
|
||||
return
|
||||
}
|
||||
projectStore.addProject({
|
||||
id: formData.id,
|
||||
await createProject({
|
||||
...projectData,
|
||||
enabled: true,
|
||||
menuCount: 0,
|
||||
menus: [],
|
||||
createdAt: new Date().toISOString()
|
||||
} as any)
|
||||
type: 'web', // 默认类型
|
||||
status: 'active'
|
||||
})
|
||||
message.success('新建成功')
|
||||
}
|
||||
formVisible.value = false
|
||||
loadData() // 刷新列表
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
console.error(error)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggle(record: PlatformProject) {
|
||||
projectStore.toggleProjectEnabled(record.id)
|
||||
message.success(record.enabled ? '已禁用' : '已启用')
|
||||
async function handleToggle(record: any) {
|
||||
const newStatus = record.status === 'active' ? 'inactive' : 'active'
|
||||
try {
|
||||
await updateProject(record.id, { status: newStatus })
|
||||
message.success(newStatus === 'active' ? '已启用' : '已禁用')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete(record: PlatformProject) {
|
||||
projectStore.deleteProject(record.id)
|
||||
message.success('删除成功')
|
||||
function handleDelete(record: any) {
|
||||
try {
|
||||
// 调用删除接口
|
||||
deleteProject(Number(record.id)).then(() => {
|
||||
message.success('删除成功')
|
||||
loadData()
|
||||
})
|
||||
} catch(e) {
|
||||
console.error(e)
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
function goToMenus(record: PlatformProject) {
|
||||
router.push({ path: '/platform/menus', query: { projectId: record.id } })
|
||||
}
|
||||
|
||||
const uploadProjectDeployPath = ref<string>('')
|
||||
|
||||
/**
|
||||
* 打开上传弹窗
|
||||
*/
|
||||
function handleUpload(record: PlatformProject) {
|
||||
uploadProjectId.value = record.id
|
||||
function handleUpload(record: ProjectInfo) {
|
||||
uploadProjectId.value = record.id.toString()
|
||||
uploadProjectName.value = record.name
|
||||
|
||||
// 确保部署路径包含 /index
|
||||
let deployPath = record.deployPath || ''
|
||||
if (deployPath && !deployPath.endsWith('/index')) {
|
||||
deployPath += '/index'
|
||||
}
|
||||
uploadProjectDeployPath.value = deployPath
|
||||
|
||||
uploadVersionId.value = null // 不关联版本
|
||||
uploadVisible.value = true
|
||||
}
|
||||
@@ -738,6 +1015,166 @@ function handleVersionUploadForVersion(version: ProjectVersion) {
|
||||
uploadVersionId.value = version.id
|
||||
uploadVisible.value = true
|
||||
}
|
||||
|
||||
// ==================== 部署相关函数 ====================
|
||||
|
||||
/**
|
||||
* 获取服务器名称
|
||||
*/
|
||||
function getServerName(serverId?: string): string {
|
||||
if (!serverId) return ''
|
||||
const server = serverList.value.find(s => s.id === serverId)
|
||||
return server ? `${server.name} (${server.ip})` : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 1Panel 账户数据
|
||||
*/
|
||||
async function loadPanelAccounts() {
|
||||
try {
|
||||
// 从项目绑定的服务器获取账户数据
|
||||
const serverId = (deployProject.value as any)?.serverId
|
||||
if (!serverId) {
|
||||
console.warn('项目未绑定服务器')
|
||||
return
|
||||
}
|
||||
|
||||
// 通过后端接口获取 Acme 账户列表
|
||||
try {
|
||||
const acmeResult = await getServerAcmeAccounts(Number(serverId))
|
||||
acmeAccounts.value = acmeResult?.items || []
|
||||
} catch (e) {
|
||||
console.error('获取 Acme 账户失败:', e)
|
||||
}
|
||||
|
||||
// 通过后端接口获取 DNS 账户列表
|
||||
try {
|
||||
const dnsResult = await getServerDnsAccounts(Number(serverId))
|
||||
dnsAccounts.value = dnsResult?.items || []
|
||||
} catch (e) {
|
||||
console.error('获取 DNS 账户失败:', e)
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if (acmeAccounts.value.length > 0) {
|
||||
deployAcmeAccountId.value = acmeAccounts.value[0].id
|
||||
}
|
||||
if (dnsAccounts.value.length > 0) {
|
||||
deployDnsAccountId.value = dnsAccounts.value[0].id
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载 1Panel 账户失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开部署弹窗
|
||||
*/
|
||||
async function handleDeploy(record: PlatformProject) {
|
||||
deployProject.value = record as any
|
||||
deployEnableHttps.value = true
|
||||
deployResult.value = null
|
||||
deploySteps.value = []
|
||||
deployCurrentStep.value = 0
|
||||
|
||||
// 加载账户数据 - 已移除,改用后端自动匹配已绑定证书
|
||||
// await loadPanelAccounts()
|
||||
|
||||
deployModalVisible.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认部署
|
||||
*/
|
||||
async function handleConfirmDeploy() {
|
||||
if (!deployProject.value?.domain) {
|
||||
message.error('项目未绑定域名,无法部署')
|
||||
return
|
||||
}
|
||||
|
||||
deploying.value = true
|
||||
deployModalVisible.value = false
|
||||
deployDrawerVisible.value = true
|
||||
deployResult.value = null
|
||||
|
||||
// 初始化部署步骤
|
||||
deploySteps.value = [
|
||||
{ title: '检查/创建网站', description: '准备中...', status: 'wait' },
|
||||
]
|
||||
|
||||
if (deployEnableHttps.value) {
|
||||
deploySteps.value.push(
|
||||
{ title: '检查/申请 SSL 证书', description: '等待中...', status: 'wait' },
|
||||
{ title: '配置 HTTPS', description: '等待中...', status: 'wait' }
|
||||
)
|
||||
}
|
||||
|
||||
deploySteps.value.push({ title: '完成', description: '', status: 'wait' })
|
||||
|
||||
try {
|
||||
// 更新第一步状态
|
||||
deployCurrentStep.value = 0
|
||||
deploySteps.value[0].description = '正在检查网站...'
|
||||
deploySteps.value[0].status = 'process'
|
||||
|
||||
// 调用后端部署 API
|
||||
const result = await deployProjectApi({
|
||||
projectId: Number(deployProject.value.id),
|
||||
enableHttps: deployEnableHttps.value,
|
||||
acmeAccountId: deployAcmeAccountId.value,
|
||||
dnsAccountId: deployDnsAccountId.value,
|
||||
createIfNotExist: true
|
||||
})
|
||||
|
||||
// 更新步骤状态
|
||||
result.steps.forEach((step, index) => {
|
||||
if (index < deploySteps.value.length - 1) {
|
||||
deploySteps.value[index].status = step.status
|
||||
deploySteps.value[index].description = step.message || (step.status === 'success' ? '完成' : step.status === 'skipped' ? '已跳过' : '失败')
|
||||
}
|
||||
})
|
||||
|
||||
// 更新最后一步
|
||||
const lastStepIndex = deploySteps.value.length - 1
|
||||
if (result.success) {
|
||||
deployCurrentStep.value = lastStepIndex
|
||||
deploySteps.value[lastStepIndex].status = 'success'
|
||||
deploySteps.value[lastStepIndex].description = '部署完成'
|
||||
deployStepStatus.value = 'finish'
|
||||
} else {
|
||||
deployStepStatus.value = 'error'
|
||||
deploySteps.value[lastStepIndex].status = 'error'
|
||||
deploySteps.value[lastStepIndex].description = result.message
|
||||
}
|
||||
|
||||
deployResult.value = result
|
||||
|
||||
// 部署完成后刷新项目状态
|
||||
if (deployProject.value?.id) {
|
||||
checkProjectDeployStatus(Number(deployProject.value.id))
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
deployStepStatus.value = 'error'
|
||||
deployResult.value = {
|
||||
success: false,
|
||||
message: error.message || '部署过程中发生错误',
|
||||
steps: []
|
||||
}
|
||||
} finally {
|
||||
deploying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开已部署的网站
|
||||
*/
|
||||
function openDeployedSite() {
|
||||
if (deployProject.value?.domain) {
|
||||
const protocol = deployEnableHttps.value ? 'https' : 'http'
|
||||
window.open(`${protocol}://${deployProject.value.domain}`, '_blank')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -883,4 +1320,43 @@ function handleVersionUploadForVersion(version: ProjectVersion) {
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 部署相关样式 */
|
||||
.deploy-confirm-content {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.deploy-progress {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.deploy-result {
|
||||
margin-top: 24px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.step-desc.success {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.step-desc.failed {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.step-desc.skipped {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.step-desc.process {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.step-desc.wait {
|
||||
color: #d9d9d9;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<!-- 服务器列表 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="filteredServers"
|
||||
:data-source="servers"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
:pagination="{ pageSize: 10, showTotal: (total: number) => `共 ${total} 条` }"
|
||||
@@ -101,32 +101,37 @@
|
||||
|
||||
<template v-if="column.key === 'resources'">
|
||||
<div class="resource-info">
|
||||
<div class="resource-item">
|
||||
<span class="label">CPU:</span>
|
||||
<div class="resource-row">
|
||||
<span class="label">CPU</span>
|
||||
<a-progress
|
||||
:percent="record.cpu.usage"
|
||||
:size="60"
|
||||
:size="[100, 6]"
|
||||
:stroke-color="getUsageColor(record.cpu.usage)"
|
||||
:format="(percent: number) => `${percent}%`"
|
||||
:show-info="false"
|
||||
/>
|
||||
<span class="value">{{ record.cpu.usage }}%</span>
|
||||
</div>
|
||||
<div class="resource-item">
|
||||
<span class="label">内存:</span>
|
||||
<div class="resource-row" :title="`已用: ${record.memory.used}GB / 总计: ${record.memory.total}GB`">
|
||||
<span class="label">
|
||||
内存 <span style="font-size: 11px; color: #8c8c8c">({{ record.memory.total }}G)</span>
|
||||
</span>
|
||||
<a-progress
|
||||
:percent="record.memory.usage"
|
||||
:size="60"
|
||||
:size="[100, 6]"
|
||||
:stroke-color="getUsageColor(record.memory.usage)"
|
||||
:format="(percent: number) => `${percent}%`"
|
||||
:show-info="false"
|
||||
/>
|
||||
<span class="value">{{ record.memory.usage }}%</span>
|
||||
</div>
|
||||
<div class="resource-item">
|
||||
<span class="label">磁盘:</span>
|
||||
<div class="resource-row">
|
||||
<span class="label">磁盘</span>
|
||||
<a-progress
|
||||
:percent="record.disk.usage"
|
||||
:size="60"
|
||||
:size="[100, 6]"
|
||||
:stroke-color="getUsageColor(record.disk.usage)"
|
||||
:format="(percent: number) => `${percent}%`"
|
||||
:show-info="false"
|
||||
/>
|
||||
<span class="value">{{ record.disk.usage }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -139,21 +144,21 @@
|
||||
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleView(record)">
|
||||
<a-button type="link" size="small" @click="handleView(record as unknown as ServerDisplayInfo)">
|
||||
<EyeOutlined /> 详情
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<a-button type="link" size="small" @click="handleEdit(record as unknown as ServerDisplayInfo)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="record.panelUrl"
|
||||
v-if="record.panelPort"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="openPanel(record)"
|
||||
@click="openPanel(record as unknown as ServerDisplayInfo)"
|
||||
>
|
||||
<LinkOutlined /> 面板
|
||||
</a-button>
|
||||
<a-popconfirm title="确定删除此服务器?" @confirm="handleDelete(record)">
|
||||
<a-popconfirm title="确定删除此服务器?" @confirm="handleDelete(record as unknown as ServerDisplayInfo)">
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
@@ -200,15 +205,7 @@
|
||||
<a-input v-model:value="formData.os" placeholder="如:Ubuntu 22.04 LTS" />
|
||||
</a-form-item>
|
||||
|
||||
<a-divider orientation="left">SSH 配置</a-divider>
|
||||
|
||||
<a-form-item label="SSH 用户名" name="sshUser">
|
||||
<a-input v-model:value="formData.sshUser" placeholder="如:root" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="SSH 端口" name="sshPort">
|
||||
<a-input-number v-model:value="formData.sshPort" :min="1" :max="65535" style="width: 100%" />
|
||||
</a-form-item>
|
||||
|
||||
<a-divider orientation="left">1Panel 配置</a-divider>
|
||||
|
||||
@@ -220,6 +217,10 @@
|
||||
<a-input-number v-model:value="formData.panelPort" :min="1" :max="65535" style="width: 100%" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="API 密钥" name="panelApiKey">
|
||||
<a-input-password v-model:value="formData.panelApiKey" placeholder="1Panel API 密钥(用于获取服务器状态)" />
|
||||
</a-form-item>
|
||||
|
||||
<a-divider orientation="left">其他</a-divider>
|
||||
|
||||
<a-form-item label="标签" name="tags">
|
||||
@@ -275,12 +276,7 @@
|
||||
<a-descriptions-item label="操作系统" :span="2">
|
||||
{{ detailServer.os }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="SSH 用户">
|
||||
{{ detailServer.sshUser || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="SSH 端口">
|
||||
{{ detailServer.sshPort || 22 }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="标签" :span="2">
|
||||
<a-tag v-for="tag in detailServer.tags" :key="tag" :color="getTagColor(tag)">
|
||||
{{ tag }}
|
||||
@@ -366,8 +362,18 @@ import {
|
||||
FileTextOutlined,
|
||||
GlobalOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import type { ServerInfo, ServerStatus, ServerType } from '@/types/platform'
|
||||
import { mockServers } from '@/mock/platform'
|
||||
import {
|
||||
getServerList,
|
||||
getServerStatus,
|
||||
createServer,
|
||||
updateServer,
|
||||
deleteServer,
|
||||
refreshServerStatus,
|
||||
type ServerBase,
|
||||
type ServerInfo,
|
||||
type ServerStatus,
|
||||
type ServerType
|
||||
} from '@/api/server'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -381,9 +387,34 @@ const filterStatus = ref<ServerStatus | undefined>()
|
||||
const filterType = ref<ServerType | undefined>()
|
||||
const filterKeyword = ref('')
|
||||
|
||||
const servers = ref<ServerInfo[]>([])
|
||||
const currentServer = ref<ServerInfo | null>(null)
|
||||
const detailServer = ref<ServerInfo | null>(null)
|
||||
// 服务器列表,包含基础信息和实时状态
|
||||
interface ServerDisplayInfo extends Partial<ServerInfo> {
|
||||
id: number
|
||||
name: string
|
||||
ip: string
|
||||
internalIp?: string
|
||||
port?: number
|
||||
type: ServerType
|
||||
status: ServerStatus
|
||||
os?: string
|
||||
tags: string[]
|
||||
panelApiKey?: string
|
||||
panelUrl?: string
|
||||
panelPort?: number
|
||||
description?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
// 资源使用情况
|
||||
cpu: { cores: number; usage: number }
|
||||
memory: { total: number; used: number; usage: number }
|
||||
disk: { total: number; used: number; usage: number }
|
||||
// 状态加载标记
|
||||
statusLoading?: boolean
|
||||
}
|
||||
|
||||
const servers = ref<ServerDisplayInfo[]>([])
|
||||
const currentServer = ref<ServerDisplayInfo | null>(null)
|
||||
const detailServer = ref<ServerDisplayInfo | null>(null)
|
||||
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
@@ -391,10 +422,9 @@ const formData = reactive({
|
||||
internalIp: '',
|
||||
type: 'cloud' as ServerType,
|
||||
os: '',
|
||||
sshUser: 'root',
|
||||
sshPort: 22,
|
||||
panelUrl: '',
|
||||
panelPort: 8888,
|
||||
panelPort: 42588,
|
||||
panelApiKey: '',
|
||||
tags: [] as string[],
|
||||
description: ''
|
||||
})
|
||||
@@ -407,31 +437,15 @@ const formRules = {
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ title: '服务器', key: 'name', width: 200 },
|
||||
{ title: '状态', key: 'status', width: 100 },
|
||||
{ title: '类型', key: 'type', width: 100 },
|
||||
{ title: '服务器', key: 'name', width: 180 },
|
||||
{ title: '状态', key: 'status', width: 80 },
|
||||
{ title: '类型', key: 'type', width: 80 },
|
||||
{ title: '资源使用', key: 'resources', width: 280 },
|
||||
{ title: '标签', key: 'tags', width: 150 },
|
||||
{ title: '操作', key: 'action', width: 220, fixed: 'right' as const }
|
||||
{ title: '操作', key: 'action', width: 200, fixed: 'right' as const }
|
||||
]
|
||||
|
||||
const filteredServers = computed(() => {
|
||||
let result = servers.value
|
||||
if (filterStatus.value) {
|
||||
result = result.filter(s => s.status === filterStatus.value)
|
||||
}
|
||||
if (filterType.value) {
|
||||
result = result.filter(s => s.type === filterType.value)
|
||||
}
|
||||
if (filterKeyword.value) {
|
||||
const keyword = filterKeyword.value.toLowerCase()
|
||||
result = result.filter(s =>
|
||||
s.name.toLowerCase().includes(keyword) ||
|
||||
s.ip.includes(keyword)
|
||||
)
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
|
||||
const stats = computed(() => {
|
||||
const all = servers.value
|
||||
@@ -447,22 +461,87 @@ onMounted(() => {
|
||||
loadServers()
|
||||
})
|
||||
|
||||
function loadServers() {
|
||||
/**
|
||||
* 加载服务器列表
|
||||
*/
|
||||
async function loadServers() {
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
servers.value = [...mockServers]
|
||||
try {
|
||||
const data = await getServerList({
|
||||
status: filterStatus.value,
|
||||
type: filterType.value,
|
||||
keyword: filterKeyword.value
|
||||
})
|
||||
// 转换为显示格式,并设置默认资源值
|
||||
servers.value = data.map(server => {
|
||||
// 解析 tags
|
||||
let tags: string[] = []
|
||||
if (server.tags) {
|
||||
try {
|
||||
tags = JSON.parse(server.tags)
|
||||
} catch {
|
||||
tags = []
|
||||
}
|
||||
}
|
||||
return {
|
||||
...server,
|
||||
tags,
|
||||
cpu: { cores: server.cpuCores || 0, usage: 0 },
|
||||
memory: { total: server.memoryTotal || 0, used: 0, usage: 0 },
|
||||
disk: { total: server.diskTotal || 0, used: 0, usage: 0 },
|
||||
statusLoading: true
|
||||
}
|
||||
})
|
||||
|
||||
// 异步加载每个服务器的实时状态
|
||||
for (const server of servers.value) {
|
||||
loadServerStatus(server.id)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载服务器列表失败:', error)
|
||||
message.error('加载服务器列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载单个服务器的实时状态
|
||||
*/
|
||||
async function loadServerStatus(serverId: number) {
|
||||
try {
|
||||
const status = await getServerStatus(serverId)
|
||||
const index = servers.value.findIndex(s => s.id === serverId)
|
||||
if (index !== -1) {
|
||||
servers.value[index] = {
|
||||
...servers.value[index],
|
||||
status: status.status,
|
||||
os: status.os,
|
||||
cpu: status.cpu,
|
||||
memory: status.memory,
|
||||
disk: status.disk,
|
||||
statusLoading: false
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`加载服务器 ${serverId} 状态失败:`, error)
|
||||
const index = servers.value.findIndex(s => s.id === serverId)
|
||||
if (index !== -1) {
|
||||
servers.value[index].status = 'offline'
|
||||
servers.value[index].statusLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
// 筛选已通过 computed 实现
|
||||
loadServers()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
filterStatus.value = undefined
|
||||
filterType.value = undefined
|
||||
filterKeyword.value = ''
|
||||
loadServers()
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
@@ -473,8 +552,6 @@ function handleAdd() {
|
||||
internalIp: '',
|
||||
type: 'cloud',
|
||||
os: '',
|
||||
sshUser: 'root',
|
||||
sshPort: 22,
|
||||
panelUrl: '',
|
||||
panelPort: 8888,
|
||||
tags: [],
|
||||
@@ -483,7 +560,7 @@ function handleAdd() {
|
||||
drawerVisible.value = true
|
||||
}
|
||||
|
||||
function handleEdit(record: ServerInfo) {
|
||||
function handleEdit(record: ServerDisplayInfo) {
|
||||
currentServer.value = record
|
||||
Object.assign(formData, {
|
||||
name: record.name,
|
||||
@@ -491,17 +568,16 @@ function handleEdit(record: ServerInfo) {
|
||||
internalIp: record.internalIp || '',
|
||||
type: record.type,
|
||||
os: record.os,
|
||||
sshUser: record.sshUser || 'root',
|
||||
sshPort: record.sshPort || 22,
|
||||
panelUrl: record.panelUrl || '',
|
||||
panelPort: record.panelPort || 8888,
|
||||
panelApiKey: record.panelApiKey || '',
|
||||
tags: [...record.tags],
|
||||
description: record.description || ''
|
||||
})
|
||||
drawerVisible.value = true
|
||||
}
|
||||
|
||||
function handleView(record: ServerInfo) {
|
||||
function handleView(record: ServerDisplayInfo) {
|
||||
detailServer.value = record
|
||||
detailVisible.value = true
|
||||
}
|
||||
@@ -515,88 +591,103 @@ async function handleFormSubmit() {
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
const serverData = {
|
||||
name: formData.name,
|
||||
ip: formData.ip,
|
||||
internalIp: formData.internalIp || undefined,
|
||||
type: formData.type,
|
||||
os: formData.os,
|
||||
|
||||
panelUrl: formData.panelUrl || undefined,
|
||||
panelPort: formData.panelPort,
|
||||
panelApiKey: formData.panelApiKey || undefined,
|
||||
tags: formData.tags.length > 0 ? formData.tags : undefined,
|
||||
description: formData.description || undefined
|
||||
}
|
||||
|
||||
if (currentServer.value) {
|
||||
// 编辑
|
||||
const index = servers.value.findIndex(s => s.id === currentServer.value!.id)
|
||||
if (index !== -1) {
|
||||
servers.value[index] = {
|
||||
...servers.value[index],
|
||||
name: formData.name,
|
||||
ip: formData.ip,
|
||||
internalIp: formData.internalIp,
|
||||
type: formData.type,
|
||||
os: formData.os,
|
||||
sshUser: formData.sshUser,
|
||||
sshPort: formData.sshPort,
|
||||
panelUrl: formData.panelUrl,
|
||||
panelPort: formData.panelPort,
|
||||
tags: formData.tags,
|
||||
description: formData.description,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
await updateServer(currentServer.value.id, serverData)
|
||||
message.success('服务器信息已更新')
|
||||
} else {
|
||||
// 新增
|
||||
const newServer: ServerInfo = {
|
||||
id: `server-${Date.now()}`,
|
||||
name: formData.name,
|
||||
ip: formData.ip,
|
||||
internalIp: formData.internalIp,
|
||||
port: 22,
|
||||
type: formData.type,
|
||||
status: 'offline',
|
||||
os: formData.os,
|
||||
cpu: { cores: 4, usage: 0 },
|
||||
memory: { total: 8, used: 0, usage: 0 },
|
||||
disk: { total: 100, used: 0, usage: 0 },
|
||||
sshUser: formData.sshUser,
|
||||
sshPort: formData.sshPort,
|
||||
panelUrl: formData.panelUrl,
|
||||
panelPort: formData.panelPort,
|
||||
tags: formData.tags,
|
||||
description: formData.description,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
servers.value.unshift(newServer)
|
||||
await createServer(serverData)
|
||||
message.success('服务器添加成功')
|
||||
}
|
||||
|
||||
drawerVisible.value = false
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
// 刷新列表
|
||||
await loadServers()
|
||||
} catch (error: any) {
|
||||
console.error('操作失败:', error)
|
||||
message.error(error.message || '操作失败')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete(record: ServerInfo) {
|
||||
servers.value = servers.value.filter(s => s.id !== record.id)
|
||||
message.success('删除成功')
|
||||
}
|
||||
|
||||
function openPanel(record: ServerInfo) {
|
||||
if (record.panelUrl) {
|
||||
window.open(`${record.panelUrl}:${record.panelPort || 8888}`, '_blank')
|
||||
async function handleDelete(record: ServerDisplayInfo) {
|
||||
try {
|
||||
await deleteServer(record.id)
|
||||
message.success('删除成功')
|
||||
await loadServers()
|
||||
} catch (error: any) {
|
||||
console.error('删除失败:', error)
|
||||
message.error(error.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
function handleRefreshStatus(record: ServerInfo) {
|
||||
message.loading('正在刷新服务器状态...', 1)
|
||||
setTimeout(() => {
|
||||
function openPanel(record: ServerDisplayInfo) {
|
||||
if (record.ip && record.panelPort) {
|
||||
window.open(`http://${record.ip}:${record.panelPort}`, '_blank')
|
||||
} else if (record.panelUrl) {
|
||||
window.open(record.panelUrl, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRefreshStatus(record: ServerDisplayInfo) {
|
||||
const hide = message.loading('正在刷新服务器状态...', 0)
|
||||
try {
|
||||
const updatedServer = await refreshServerStatus(record.id)
|
||||
// 更新列表中的服务器数据
|
||||
const index = servers.value.findIndex(s => s.id === record.id)
|
||||
if (index !== -1) {
|
||||
servers.value[index] = {
|
||||
...servers.value[index],
|
||||
status: updatedServer.status,
|
||||
os: updatedServer.os,
|
||||
cpu: updatedServer.cpu,
|
||||
memory: updatedServer.memory,
|
||||
disk: updatedServer.disk,
|
||||
statusLoading: false
|
||||
}
|
||||
}
|
||||
// 如果是详情页面,也更新
|
||||
if (detailServer.value?.id === record.id) {
|
||||
detailServer.value = {
|
||||
...detailServer.value,
|
||||
status: updatedServer.status,
|
||||
os: updatedServer.os,
|
||||
cpu: updatedServer.cpu,
|
||||
memory: updatedServer.memory,
|
||||
disk: updatedServer.disk
|
||||
}
|
||||
}
|
||||
message.success('状态已刷新')
|
||||
}, 1000)
|
||||
} catch (error: any) {
|
||||
console.error('刷新状态失败:', error)
|
||||
message.error(error.message || '刷新状态失败')
|
||||
} finally {
|
||||
hide()
|
||||
}
|
||||
}
|
||||
|
||||
function goToLogs(record: ServerInfo) {
|
||||
router.push({ path: '/platform/log-center', query: { serverId: record.id } })
|
||||
function goToLogs(record: ServerDisplayInfo) {
|
||||
router.push({ path: '/platform/log-center', query: { serverId: String(record.id) } })
|
||||
}
|
||||
|
||||
function goToDomains(record: ServerInfo) {
|
||||
router.push({ path: '/platform/domains', query: { serverId: record.id } })
|
||||
function goToDomains(record: ServerDisplayInfo) {
|
||||
router.push({ path: '/platform/domains', query: { serverId: String(record.id) } })
|
||||
}
|
||||
|
||||
function getStatusColor(status: ServerStatus): string {
|
||||
@@ -719,27 +810,41 @@ function getTagColor(tag: string): string {
|
||||
.resource-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.resource-item {
|
||||
.resource-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
min-height: 22px;
|
||||
}
|
||||
|
||||
.resource-item .label {
|
||||
font-size: 12px;
|
||||
.resource-row .label {
|
||||
font-size: 11px;
|
||||
color: #8c8c8c;
|
||||
width: 36px;
|
||||
width: 90px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.resource-item :deep(.ant-progress) {
|
||||
.resource-row .value {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
width: 40px;
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.resource-row :deep(.ant-progress) {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.resource-item :deep(.ant-progress-text) {
|
||||
font-size: 11px;
|
||||
.resource-row :deep(.ant-progress-inner) {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
|
||||
@@ -221,11 +221,13 @@ import {
|
||||
import type { ApprovalTemplate, ApprovalScenario, ApprovalNode } from '@/types'
|
||||
import { ApprovalScenarioMap } from '@/types'
|
||||
import {
|
||||
mockGetApprovalStats,
|
||||
mockGetApprovalTemplateList,
|
||||
mockToggleApprovalTemplate,
|
||||
mockDeleteApprovalTemplate
|
||||
} from '@/mock'
|
||||
getApprovalStats,
|
||||
getApprovalTemplateList,
|
||||
toggleApprovalTemplate,
|
||||
deleteApprovalTemplate,
|
||||
createApprovalTemplate,
|
||||
updateApprovalTemplate
|
||||
} from '@/api/system/approval'
|
||||
import { formatDateTime } from '@/utils/common'
|
||||
import FlowEditor from '@/components/FlowEditor/index.vue'
|
||||
|
||||
@@ -290,7 +292,7 @@ function getScenarioColor(scenario: ApprovalScenario): string {
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const data = await mockGetApprovalStats()
|
||||
const data = await getApprovalStats()
|
||||
Object.assign(stats, data)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -300,10 +302,8 @@ async function loadStats() {
|
||||
async function loadTemplates() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await mockGetApprovalTemplateList({
|
||||
scenario: filterScenario.value
|
||||
})
|
||||
templateList.value = res.list
|
||||
const list = await getApprovalTemplateList(filterScenario.value)
|
||||
templateList.value = list
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
message.error('加载数据失败')
|
||||
@@ -346,12 +346,27 @@ async function handleSubmit() {
|
||||
formData.nodes = flowEditorRef.value.toApprovalNodes()
|
||||
}
|
||||
|
||||
// 这里应该调用创建/更新API
|
||||
const requestData = {
|
||||
name: formData.name,
|
||||
description: formData.description,
|
||||
scenario: formData.scenario!,
|
||||
enabled: formData.enabled,
|
||||
nodes: formData.nodes
|
||||
}
|
||||
|
||||
if (editingTemplate.value) {
|
||||
await updateApprovalTemplate(editingTemplate.value.id, requestData)
|
||||
} else {
|
||||
await createApprovalTemplate(requestData)
|
||||
}
|
||||
|
||||
message.success(editingTemplate.value ? '更新成功' : '创建成功')
|
||||
drawerVisible.value = false
|
||||
loadTemplates()
|
||||
} catch {
|
||||
// 验证失败
|
||||
loadStats()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
message.error('保存失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
@@ -359,7 +374,7 @@ async function handleSubmit() {
|
||||
|
||||
async function handleToggle(id: number) {
|
||||
try {
|
||||
await mockToggleApprovalTemplate(id)
|
||||
await toggleApprovalTemplate(id)
|
||||
message.success('状态已更新')
|
||||
loadTemplates()
|
||||
loadStats()
|
||||
@@ -371,7 +386,7 @@ async function handleToggle(id: number) {
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
await mockDeleteApprovalTemplate(id)
|
||||
await deleteApprovalTemplate(id)
|
||||
message.success('删除成功')
|
||||
loadTemplates()
|
||||
loadStats()
|
||||
|
||||
@@ -203,7 +203,7 @@ import {
|
||||
} from '@ant-design/icons-vue'
|
||||
import type { ApprovalInstance, ApprovalScenario, ApprovalInstanceStatus } from '@/types'
|
||||
import { ApprovalScenarioMap, ApprovalInstanceStatusMap, ApprovalInstanceStatusBadgeMap } from '@/types'
|
||||
import { mockGetApprovalInstanceList } from '@/mock'
|
||||
import { getApprovalInstancePage } from '@/api/system/approval'
|
||||
import { formatDateTime } from '@/utils/common'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -274,16 +274,14 @@ function getActionText(action: string): string {
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await mockGetApprovalInstanceList({
|
||||
const res = await getApprovalInstancePage({
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: searchKeyword.value || undefined,
|
||||
scenario: filterScenario.value,
|
||||
status: filterStatus.value,
|
||||
startDate: dateRange.value?.[0]?.format('YYYY-MM-DD'),
|
||||
endDate: dateRange.value?.[1]?.format('YYYY-MM-DD')
|
||||
status: filterStatus.value
|
||||
})
|
||||
instanceList.value = res.list
|
||||
instanceList.value = res.records
|
||||
pagination.total = res.total
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
290
src/views/system/dept/index.vue
Normal file
290
src/views/system/dept/index.vue
Normal file
@@ -0,0 +1,290 @@
|
||||
<template>
|
||||
<div class="dept-page">
|
||||
<a-page-header title="部门管理" sub-title="管理组织架构和部门信息">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="showCreateModal()">
|
||||
<PlusOutlined /> 新增部门
|
||||
</a-button>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<a-card class="main-card" :loading="loading">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="deptTree"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
:default-expand-all-rows="true"
|
||||
:indent-size="24"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'name'">
|
||||
<div class="dept-name">
|
||||
<ApartmentOutlined class="dept-icon" />
|
||||
<span>{{ record.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'leader'">
|
||||
<span v-if="record.leaderName">{{ record.leaderName }}</span>
|
||||
<span v-else class="empty-text">-</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 1 ? 'green' : 'red'">
|
||||
{{ record.status === 1 ? '正常' : '停用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="showCreateModal(record.id)">
|
||||
<PlusOutlined /> 新增子部门
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="showEditModal(record as DeptRecord)">
|
||||
<EditOutlined /> 编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定删除此部门?"
|
||||
@confirm="handleDelete(record.id)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>
|
||||
<DeleteOutlined /> 删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="editingDept ? '编辑部门' : '新增部门'"
|
||||
:confirm-loading="submitting"
|
||||
@ok="handleSubmit"
|
||||
@cancel="modalVisible = false"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<a-form-item label="上级部门" name="parentId">
|
||||
<a-tree-select
|
||||
v-model:value="formData.parentId"
|
||||
:tree-data="parentTreeData"
|
||||
placeholder="请选择上级部门"
|
||||
tree-default-expand-all
|
||||
:field-names="{ children: 'children', label: 'name', value: 'id' }"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="部门名称" name="name" :rules="[{ required: true, message: '请输入部门名称' }]">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入部门名称" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="部门编码" name="code">
|
||||
<a-input v-model:value="formData.code" placeholder="请输入部门编码" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="负责人" name="leaderName">
|
||||
<a-input v-model:value="formData.leaderName" placeholder="请输入负责人姓名" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="联系电话" name="phone">
|
||||
<a-input v-model:value="formData.phone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="邮箱" name="email">
|
||||
<a-input v-model:value="formData.email" placeholder="请输入邮箱" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="显示排序" name="sort">
|
||||
<a-input-number v-model:value="formData.sort" :min="0" style="width: 100%" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="formData.status">
|
||||
<a-radio :value="1">正常</a-radio>
|
||||
<a-radio :value="0">停用</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea v-model:value="formData.remark" :rows="3" placeholder="请输入备注" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
ApartmentOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { getDeptTree, createDept, updateDept, deleteDept, type DeptRecord, type DeptFormData } from '@/api/system/dept'
|
||||
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const deptTree = ref<DeptRecord[]>([])
|
||||
const modalVisible = ref(false)
|
||||
const editingDept = ref<DeptRecord | null>(null)
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const formData = reactive<DeptFormData>({
|
||||
parentId: 0,
|
||||
name: '',
|
||||
code: '',
|
||||
leaderName: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ title: '部门名称', key: 'name', width: 280 },
|
||||
{ title: '部门编码', dataIndex: 'code', width: 120 },
|
||||
{ title: '负责人', key: 'leader', width: 100 },
|
||||
{ title: '联系电话', dataIndex: 'phone', width: 120 },
|
||||
{ title: '状态', key: 'status', width: 80 },
|
||||
{ title: '排序', dataIndex: 'sort', width: 80 },
|
||||
{ title: '操作', key: 'action', width: 240 }
|
||||
]
|
||||
|
||||
// 构建上级部门下拉数据(添加顶级选项)
|
||||
const parentTreeData = computed(() => {
|
||||
return [
|
||||
{ id: 0, name: '无(顶级部门)', children: deptTree.value }
|
||||
]
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
deptTree.value = await getDeptTree()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
message.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
Object.assign(formData, {
|
||||
parentId: 0,
|
||||
name: '',
|
||||
code: '',
|
||||
leaderName: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
}
|
||||
|
||||
function showCreateModal(parentId?: number) {
|
||||
editingDept.value = null
|
||||
resetForm()
|
||||
if (parentId) {
|
||||
formData.parentId = parentId
|
||||
}
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
function showEditModal(dept: DeptRecord) {
|
||||
editingDept.value = dept
|
||||
Object.assign(formData, {
|
||||
parentId: dept.parentId,
|
||||
name: dept.name,
|
||||
code: dept.code || '',
|
||||
leaderName: dept.leaderName || '',
|
||||
phone: dept.phone || '',
|
||||
email: dept.email || '',
|
||||
sort: dept.sort,
|
||||
status: dept.status,
|
||||
remark: dept.remark || ''
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
submitting.value = true
|
||||
|
||||
if (editingDept.value) {
|
||||
await updateDept(editingDept.value.id, formData)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await createDept(formData)
|
||||
message.success('创建成功')
|
||||
}
|
||||
|
||||
modalVisible.value = false
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
await deleteDept(id)
|
||||
message.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dept-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.main-card {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.dept-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dept-icon {
|
||||
color: #1890ff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: #bfbfbf;
|
||||
}
|
||||
</style>
|
||||
@@ -49,8 +49,8 @@
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button type="link" size="small" @click="handleViewItems(record)">字典项</a-button>
|
||||
<a-button type="link" size="small" @click="handleEdit(record as DictRecord)">编辑</a-button>
|
||||
<a-button type="link" size="small" @click="handleViewItems(record as DictRecord)">字典项</a-button>
|
||||
<a-popconfirm
|
||||
title="确定删除此字典吗?"
|
||||
@confirm="handleDelete(record.id)"
|
||||
@@ -84,6 +84,7 @@ import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined, SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
||||
import DictFormModal from '@/components/system/dict/DictFormModal.vue'
|
||||
import DictItemDrawer from '@/components/system/dict/DictItemDrawer.vue'
|
||||
import { getDictPage, deleteDict } from '@/api/system/dict'
|
||||
|
||||
// 类型定义
|
||||
interface DictRecord {
|
||||
@@ -119,7 +120,7 @@ const columns = [
|
||||
{ title: '备注', dataIndex: 'remark', ellipsis: true },
|
||||
{ title: '状态', dataIndex: 'status', width: 80 },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', width: 170 },
|
||||
{ title: '操作', dataIndex: 'action', width: 160, fixed: 'right' }
|
||||
{ title: '操作', dataIndex: 'action', width: 160, fixed: 'right' as const }
|
||||
]
|
||||
|
||||
// 字典弹窗
|
||||
@@ -134,18 +135,17 @@ const currentDict = ref<DictRecord | null>(null)
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
// TODO: 接入真实API
|
||||
// const res = await getDictList({ ...searchForm, page: pagination.current, pageSize: pagination.pageSize })
|
||||
// tableData.value = res.data.data.records
|
||||
// pagination.total = res.data.data.total
|
||||
|
||||
// 模拟数据
|
||||
tableData.value = [
|
||||
{ id: 1, name: '性别', code: 'gender', remark: '用户性别', status: 1, createdAt: '2026-01-08 10:00:00' },
|
||||
{ id: 2, name: '状态', code: 'status', remark: '通用状态', status: 1, createdAt: '2026-01-08 10:00:00' },
|
||||
{ id: 3, name: '审批状态', code: 'approval_status', remark: '审批流程状态', status: 1, createdAt: '2026-01-08 10:00:00' }
|
||||
]
|
||||
pagination.total = 3
|
||||
const res = await getDictPage({
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
name: searchForm.name || undefined,
|
||||
code: searchForm.code || undefined
|
||||
})
|
||||
tableData.value = res.data.data.records || []
|
||||
pagination.total = res.data.data.total || 0
|
||||
} catch (error) {
|
||||
console.error('加载字典数据失败:', error)
|
||||
tableData.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -184,10 +184,15 @@ function handleViewItems(record: DictRecord) {
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
// TODO: 接入真实API
|
||||
// await deleteDict(id)
|
||||
message.success('删除成功')
|
||||
loadData()
|
||||
try {
|
||||
await deleteDict(id)
|
||||
message.success('删除成功')
|
||||
loadData()
|
||||
} catch (error: any) {
|
||||
if (error?.response?.data?.message) {
|
||||
message.error(error.response.data.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -119,7 +119,7 @@ async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getRolePage({ pageSize: 100 })
|
||||
tableData.value = res.data.data.records || []
|
||||
tableData.value = res.records || []
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
} finally {
|
||||
@@ -161,7 +161,7 @@ async function handleAssignMenus(record: RoleRecord) {
|
||||
|
||||
// 加载角色已有的菜单
|
||||
const roleMenuRes = await getRoleMenuIds(record.id)
|
||||
checkedMenuIds.value = roleMenuRes.data.data || []
|
||||
checkedMenuIds.value = roleMenuRes || []
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
} finally {
|
||||
|
||||
@@ -6,6 +6,17 @@
|
||||
<a-form-item label="关键词">
|
||||
<a-input v-model:value="searchForm.keyword" placeholder="用户名/昵称/手机号" allow-clear style="width: 200px" />
|
||||
</a-form-item>
|
||||
<a-form-item label="部门">
|
||||
<a-tree-select
|
||||
v-model:value="searchForm.deptId"
|
||||
:tree-data="deptTreeData"
|
||||
placeholder="请选择部门"
|
||||
tree-default-expand-all
|
||||
:field-names="{ children: 'children', label: 'name', value: 'id' }"
|
||||
allow-clear
|
||||
style="width: 180px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="角色">
|
||||
<a-select v-model:value="searchForm.role" placeholder="请选择角色" allow-clear style="width: 150px">
|
||||
<a-select-option v-for="role in roleOptions" :key="role.code" :value="role.code">
|
||||
@@ -68,8 +79,8 @@
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button type="link" size="small" @click="handleResetPassword(record)">重置密码</a-button>
|
||||
<a-button type="link" size="small" @click="handleEdit(record as UserRecord)">编辑</a-button>
|
||||
<a-button type="link" size="small" @click="handleResetPassword(record as UserRecord)">重置密码</a-button>
|
||||
<a-popconfirm title="确定删除该用户吗?" @confirm="handleDelete(record.id)">
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
@@ -106,6 +117,7 @@ import {
|
||||
|
||||
import { getUserList, deleteUser, updateUserStatus } from '@/api/system/user'
|
||||
import { getRoleList } from '@/api/system/role'
|
||||
import { getDeptTree, type DeptRecord } from '@/api/system/dept'
|
||||
import type { UserRecord, UserFormData } from '@/types/system/user'
|
||||
import type { RoleRecord } from '@/types/system/role'
|
||||
|
||||
@@ -116,10 +128,12 @@ import ResetPasswordModal from '@/components/system/user/ResetPasswordModal.vue'
|
||||
const loading = ref(false)
|
||||
const tableData = ref<UserRecord[]>([])
|
||||
const roleOptions = ref<{ code: string; name: string }[]>([]) // Role List for display/select
|
||||
const deptTreeData = ref<DeptRecord[]>([])
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
deptId: undefined as number | undefined,
|
||||
role: undefined as string | undefined,
|
||||
status: undefined as number | undefined
|
||||
})
|
||||
@@ -139,11 +153,11 @@ const columns = [
|
||||
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
||||
{ title: '昵称', dataIndex: 'nickname', key: 'nickname' },
|
||||
{ title: '手机号', dataIndex: 'phone', key: 'phone' },
|
||||
{ title: '邮箱', dataIndex: 'email', key: 'email' },
|
||||
{ title: '部门', dataIndex: 'deptName', key: 'deptName', width: 120 },
|
||||
{ title: '角色', dataIndex: 'role', key: 'role', width: 120 },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', width: 180 },
|
||||
{ title: '操作', key: 'action', width: 200, fixed: 'right' }
|
||||
{ title: '操作', key: 'action', width: 200, fixed: 'right' as const }
|
||||
]
|
||||
|
||||
// Modal State
|
||||
@@ -190,16 +204,22 @@ async function loadData() {
|
||||
// 加载角色列表
|
||||
async function loadRoles() {
|
||||
try {
|
||||
// 这里如果后端没有 list-all,我们可以用 list 且不传参(默认第一页),或者传大 pageSize
|
||||
// 假设后端支持 list 并返回 records
|
||||
const res = await getRoleList()
|
||||
// Mapping RoleRecord to Option { code, name }
|
||||
roleOptions.value = (res.data.data || []).map((r: RoleRecord) => ({ code: r.code, name: r.name }))
|
||||
const roles = await getRoleList()
|
||||
roleOptions.value = (roles || []).map((r: any) => ({ code: r.code, name: r.name }))
|
||||
} catch (error) {
|
||||
console.error('加载角色列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载部门树
|
||||
async function loadDeptTree() {
|
||||
try {
|
||||
deptTreeData.value = await getDeptTree()
|
||||
} catch (error) {
|
||||
console.error('加载部门列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
@@ -207,6 +227,7 @@ function handleSearch() {
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.deptId = undefined
|
||||
searchForm.role = undefined
|
||||
searchForm.status = undefined
|
||||
handleSearch()
|
||||
@@ -258,6 +279,7 @@ function handleResetPassword(record: UserRecord) {
|
||||
|
||||
onMounted(() => {
|
||||
loadRoles()
|
||||
loadDeptTree()
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -63,7 +63,7 @@ export default defineConfig({
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
configure: (proxy) => {
|
||||
proxy.on('proxyReq', (proxyReq, req) => {
|
||||
proxy.on('proxyReq', (_proxyReq, req) => {
|
||||
console.log(`[Backend API Proxy] ${req.method} ${req.url}`)
|
||||
})
|
||||
}
|
||||
@@ -74,7 +74,7 @@ export default defineConfig({
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/1panel-api\/server1/, '/api/v2'),
|
||||
configure: (proxy) => {
|
||||
proxy.on('proxyReq', (proxyReq, req) => {
|
||||
proxy.on('proxyReq', (_proxyReq, req) => {
|
||||
console.log(`[1Panel Proxy - 主服务器] ${req.method} ${req.url}`)
|
||||
})
|
||||
}
|
||||
@@ -85,7 +85,7 @@ export default defineConfig({
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/1panel-api\/server2/, '/api/v2'),
|
||||
configure: (proxy) => {
|
||||
proxy.on('proxyReq', (proxyReq, req) => {
|
||||
proxy.on('proxyReq', (_proxyReq, req) => {
|
||||
console.log(`[1Panel Proxy - 测试服务器] ${req.method} ${req.url}`)
|
||||
})
|
||||
}
|
||||
@@ -96,7 +96,7 @@ export default defineConfig({
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/1panel-api\/server3/, '/api/v2'),
|
||||
configure: (proxy) => {
|
||||
proxy.on('proxyReq', (proxyReq, req) => {
|
||||
proxy.on('proxyReq', (_proxyReq, req) => {
|
||||
console.log(`[1Panel Proxy - 备份服务器] ${req.method} ${req.url}`)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user