前端项目关键重构规范
This commit is contained in:
3
.env
3
.env
@@ -2,4 +2,5 @@
|
|||||||
VITE_APP_TITLE=楠溪屿后台管理系统
|
VITE_APP_TITLE=楠溪屿后台管理系统
|
||||||
|
|
||||||
# API基础URL
|
# API基础URL
|
||||||
VITE_API_BASE_URL=https://api.superwax.cn:4433/api
|
VITE_API_BASE_URL=https://apis.codeport.online/api
|
||||||
|
# VITE_API_BASE_URL=https://api.superwax.cn:4433/api
|
||||||
|
|||||||
@@ -2,5 +2,6 @@
|
|||||||
VITE_APP_TITLE=楠溪屿后台管理系统
|
VITE_APP_TITLE=楠溪屿后台管理系统
|
||||||
|
|
||||||
# API基础URL - 生产环境
|
# API基础URL - 生产环境
|
||||||
VITE_API_BASE_URL=https://api.superwax.cn:4433/api
|
VITE_API_BASE_URL=https://apis.codeport.online/api
|
||||||
|
# VITE_API_BASE_URL=https://api.superwax.cn:4433/api
|
||||||
|
|
||||||
|
|||||||
@@ -1,68 +1,364 @@
|
|||||||
import request from '@/utils/request'
|
/**
|
||||||
import type {
|
* 数据库管理 API
|
||||||
DatabaseType,
|
* 通过1Panel管理数据库
|
||||||
DatabaseInfo,
|
*/
|
||||||
DatabaseItem,
|
import { request } from '@/utils/request'
|
||||||
CreateDatabaseRequest,
|
|
||||||
UpdateDatabaseRequest,
|
|
||||||
DatabaseConnectionInfo,
|
|
||||||
DatabaseListParams,
|
|
||||||
DatabaseListResponse
|
|
||||||
} from '@/types/database'
|
|
||||||
|
|
||||||
// 获取数据库信息
|
// ==================== 类型定义 ====================
|
||||||
export function getDatabaseInfo(type: DatabaseType): Promise<DatabaseInfo> {
|
|
||||||
return request.get(`/api/database/${type}/info`)
|
/**
|
||||||
|
* 应用安装状态
|
||||||
|
*/
|
||||||
|
export interface AppInstallStatus {
|
||||||
|
isExist: boolean
|
||||||
|
name: string
|
||||||
|
app: string
|
||||||
|
version: string
|
||||||
|
status: string
|
||||||
|
createdAt: string
|
||||||
|
lastBackupAt: string
|
||||||
|
appInstallId: number
|
||||||
|
containerName: string
|
||||||
|
installPath: string
|
||||||
|
httpPort: number
|
||||||
|
httpsPort: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动数据库
|
/**
|
||||||
export function startDatabase(type: DatabaseType): Promise<void> {
|
* 数据库记录
|
||||||
return request.post(`/api/database/${type}/start`)
|
*/
|
||||||
|
export interface DatabaseRecord {
|
||||||
|
id: number
|
||||||
|
createdAt: string
|
||||||
|
name: string
|
||||||
|
from: string
|
||||||
|
mysqlName: string
|
||||||
|
format: string
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
permission: string
|
||||||
|
isDelete: boolean
|
||||||
|
description: string
|
||||||
|
showPassword?: boolean // 前端控制是否显示密码
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止数据库
|
/**
|
||||||
export function stopDatabase(type: DatabaseType): Promise<void> {
|
* 数据库列表响应
|
||||||
return request.post(`/api/database/${type}/stop`)
|
*/
|
||||||
|
export interface DatabaseListResponse {
|
||||||
|
total: number
|
||||||
|
items: DatabaseRecord[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重启数据库
|
/**
|
||||||
export function restartDatabase(type: DatabaseType): Promise<void> {
|
* 创建数据库参数
|
||||||
return request.post(`/api/database/${type}/restart`)
|
*/
|
||||||
|
export interface CreateDatabaseParams {
|
||||||
|
serverId: number
|
||||||
|
name: string
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
format?: string // 字符集 format
|
||||||
|
collation?: string // 排序规则 collation (可选,后端接口没明确提到但通常MySQL需要)
|
||||||
|
permission?: string
|
||||||
|
permissionIPs?: string
|
||||||
|
type: string // 数据库类型,如mysql
|
||||||
|
database: string // 数据库名称,如mysql
|
||||||
|
from?: string // 固定 'local'
|
||||||
|
description?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取数据库列表
|
/**
|
||||||
export function getDatabaseList(type: DatabaseType, params?: DatabaseListParams): Promise<DatabaseListResponse> {
|
* 删除数据库参数
|
||||||
return request.get(`/api/database/${type}/list`, { params })
|
*/
|
||||||
|
export interface DeleteDatabaseParams {
|
||||||
|
serverId: number
|
||||||
|
id: number
|
||||||
|
type: string // 数据库类型,如mysql
|
||||||
|
database: string // 数据库名称/应用类型
|
||||||
|
deleteBackup?: boolean
|
||||||
|
forceDelete?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建数据库
|
/**
|
||||||
export function createDatabase(type: DatabaseType, data: CreateDatabaseRequest): Promise<DatabaseItem> {
|
* 修改密码参数
|
||||||
return request.post(`/api/database/${type}`, data)
|
*/
|
||||||
|
export interface ChangePasswordParams {
|
||||||
|
serverId: number
|
||||||
|
id: number
|
||||||
|
value: string // 新密码 (Base64)
|
||||||
|
type: string // 数据库类型
|
||||||
|
database: string // 数据库名称
|
||||||
|
from: string // 固定 'local'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新数据库
|
/**
|
||||||
export function updateDatabase(type: DatabaseType, id: number, data: UpdateDatabaseRequest): Promise<DatabaseItem> {
|
* 更新描述参数
|
||||||
return request.put(`/api/database/${type}/${id}`, data)
|
*/
|
||||||
|
export interface UpdateDescriptionParams {
|
||||||
|
serverId: number
|
||||||
|
id: number
|
||||||
|
description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除数据库
|
/**
|
||||||
export function deleteDatabase(type: DatabaseType, id: number): Promise<void> {
|
* 操作应用参数
|
||||||
return request.delete(`/api/database/${type}/${id}`)
|
*/
|
||||||
|
export interface OperateAppParams {
|
||||||
|
serverId: number
|
||||||
|
operate: 'start' | 'stop' | 'restart'
|
||||||
|
installId: number
|
||||||
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取连接信息
|
// ==================== API 方法 ====================
|
||||||
export function getDatabaseConnectionInfo(type: DatabaseType, id: number): Promise<DatabaseConnectionInfo> {
|
|
||||||
return request.get(`/api/database/${type}/${id}/connection`)
|
/**
|
||||||
|
* 检查应用安装状态(MySQL/PostgreSQL/Redis等)
|
||||||
|
*/
|
||||||
|
export async function checkAppInstalled(
|
||||||
|
serverId: number,
|
||||||
|
key: string,
|
||||||
|
name: string
|
||||||
|
): Promise<AppInstallStatus> {
|
||||||
|
const res = await request.post<AppInstallStatus>('/platform/database/app/check', {
|
||||||
|
serverId,
|
||||||
|
key,
|
||||||
|
name
|
||||||
|
})
|
||||||
|
return res.data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出类型
|
/**
|
||||||
export type {
|
* 查询数据库列表
|
||||||
DatabaseType,
|
*/
|
||||||
DatabaseInfo,
|
export async function searchDatabases(
|
||||||
DatabaseItem,
|
serverId: number,
|
||||||
CreateDatabaseRequest,
|
database: string,
|
||||||
UpdateDatabaseRequest,
|
page: number = 1,
|
||||||
DatabaseConnectionInfo,
|
pageSize: number = 20
|
||||||
DatabaseListParams,
|
): Promise<DatabaseListResponse> {
|
||||||
DatabaseListResponse
|
const res = await request.post<DatabaseListResponse>('/platform/database/search', {
|
||||||
|
serverId,
|
||||||
|
database,
|
||||||
|
page,
|
||||||
|
pageSize
|
||||||
|
})
|
||||||
|
return res.data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建数据库
|
||||||
|
*/
|
||||||
|
export async function createDatabase(params: CreateDatabaseParams): Promise<void> {
|
||||||
|
await request.post('/platform/database/create', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除数据库
|
||||||
|
*/
|
||||||
|
export async function deleteDatabase(params: DeleteDatabaseParams): Promise<void> {
|
||||||
|
await request.post('/platform/database/delete', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新数据库描述
|
||||||
|
*/
|
||||||
|
export async function updateDatabaseDescription(params: UpdateDescriptionParams): Promise<void> {
|
||||||
|
await request.post('/platform/database/description/update', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改数据库密码
|
||||||
|
*/
|
||||||
|
export async function changeDatabasePassword(params: ChangePasswordParams): Promise<void> {
|
||||||
|
await request.post('/platform/database/password/change', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作应用(启动/停止/重启)
|
||||||
|
*/
|
||||||
|
export async function operateApp(params: OperateAppParams): Promise<void> {
|
||||||
|
await request.post('/platform/database/app/operate', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据库字符集排序规则选项
|
||||||
|
*/
|
||||||
|
export async function getFormatOptions(
|
||||||
|
serverId: number,
|
||||||
|
type: string,
|
||||||
|
database: string,
|
||||||
|
format?: string
|
||||||
|
): Promise<string[]> {
|
||||||
|
const res = await request.post<string[]>('/platform/database/format/options', {
|
||||||
|
serverId,
|
||||||
|
type,
|
||||||
|
database,
|
||||||
|
format
|
||||||
|
})
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用信息
|
||||||
|
*/
|
||||||
|
export interface AppInfo {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
key: string
|
||||||
|
shortDescZh: string
|
||||||
|
shortDescEn: string
|
||||||
|
description: string
|
||||||
|
icon: string
|
||||||
|
type: string
|
||||||
|
status: string
|
||||||
|
website: string
|
||||||
|
github: string
|
||||||
|
document: string
|
||||||
|
versions: string[]
|
||||||
|
installed: boolean
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用版本详情
|
||||||
|
*/
|
||||||
|
export interface AppDetail {
|
||||||
|
id: number
|
||||||
|
appId: number
|
||||||
|
version: string
|
||||||
|
dockerCompose: string
|
||||||
|
status: string
|
||||||
|
enable: boolean
|
||||||
|
params: {
|
||||||
|
formFields: Array<{
|
||||||
|
default: any
|
||||||
|
envKey: string
|
||||||
|
label: Record<string, string>
|
||||||
|
labelEn: string
|
||||||
|
labelZh: string
|
||||||
|
required: boolean
|
||||||
|
type: string
|
||||||
|
random?: boolean
|
||||||
|
rule?: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安装应用参数
|
||||||
|
*/
|
||||||
|
export interface InstallAppParams {
|
||||||
|
serverId: number
|
||||||
|
appDetailId: number
|
||||||
|
name: string
|
||||||
|
version: string
|
||||||
|
params: Record<string, any>
|
||||||
|
dockerCompose: string
|
||||||
|
taskID: string // 任务ID,用于查询安装日志
|
||||||
|
// 高级设置
|
||||||
|
advanced?: boolean
|
||||||
|
containerName?: string
|
||||||
|
allowPort?: boolean
|
||||||
|
specifyIP?: string
|
||||||
|
restartPolicy?: string
|
||||||
|
cpuQuota?: number
|
||||||
|
memoryLimit?: number
|
||||||
|
memoryUnit?: string
|
||||||
|
pullImage?: boolean
|
||||||
|
editCompose?: boolean
|
||||||
|
gpuConfig?: boolean
|
||||||
|
appID?: string
|
||||||
|
format?: string
|
||||||
|
collation?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安装应用返回结果
|
||||||
|
*/
|
||||||
|
export interface InstallAppResult {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
appId: number
|
||||||
|
appDetailId: number
|
||||||
|
version: string
|
||||||
|
status: string
|
||||||
|
containerName: string
|
||||||
|
httpPort: number
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务日志响应
|
||||||
|
*/
|
||||||
|
export interface TaskLogResponse {
|
||||||
|
end: boolean
|
||||||
|
path: string
|
||||||
|
total: number
|
||||||
|
taskStatus: string
|
||||||
|
lines: string[]
|
||||||
|
totalLines: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取应用信息(如Redis)
|
||||||
|
*/
|
||||||
|
export async function getAppInfo(
|
||||||
|
serverId: number,
|
||||||
|
appKey: string
|
||||||
|
): Promise<AppInfo> {
|
||||||
|
const res = await request.post<AppInfo>('/platform/database/app/info', {
|
||||||
|
serverId,
|
||||||
|
appKey
|
||||||
|
})
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取应用版本详情
|
||||||
|
*/
|
||||||
|
export async function getAppDetail(
|
||||||
|
serverId: number,
|
||||||
|
appId: number,
|
||||||
|
version: string
|
||||||
|
): Promise<AppDetail> {
|
||||||
|
const res = await request.post<AppDetail>('/platform/database/app/detail', {
|
||||||
|
serverId,
|
||||||
|
appId,
|
||||||
|
version
|
||||||
|
})
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安装应用
|
||||||
|
*/
|
||||||
|
export async function installApp(params: InstallAppParams): Promise<InstallAppResult> {
|
||||||
|
const { serverId, ...restParams } = params
|
||||||
|
const res = await request.post<InstallAppResult>('/platform/database/app/install', {
|
||||||
|
serverId,
|
||||||
|
...restParams
|
||||||
|
})
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取任务日志
|
||||||
|
*/
|
||||||
|
export async function readTaskLog(
|
||||||
|
serverId: number,
|
||||||
|
taskId: string,
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 500
|
||||||
|
): Promise<TaskLogResponse> {
|
||||||
|
const res = await request.post<TaskLogResponse>('/platform/database/task/log', {
|
||||||
|
serverId,
|
||||||
|
taskId,
|
||||||
|
page,
|
||||||
|
pageSize
|
||||||
|
})
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ export interface DomainInfo {
|
|||||||
deployStatus?: DeployStatus
|
deployStatus?: DeployStatus
|
||||||
lastDeployTime?: string
|
lastDeployTime?: string
|
||||||
lastDeployMessage?: string
|
lastDeployMessage?: string
|
||||||
|
// 运行环境关联
|
||||||
|
runtimeId?: number
|
||||||
|
runtimeServerId?: number
|
||||||
|
runtimeName?: string
|
||||||
|
runtimeType?: string
|
||||||
|
runtimeDeployStatus?: DeployStatus
|
||||||
// 时间
|
// 时间
|
||||||
createdAt?: string
|
createdAt?: string
|
||||||
updatedAt?: string
|
updatedAt?: string
|
||||||
@@ -135,6 +141,13 @@ export async function deployDomain(data: DomainDeployRequest): Promise<DomainDep
|
|||||||
return res.data.data
|
return res.data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除部署(从 1Panel 删除网站)
|
||||||
|
*/
|
||||||
|
export async function undeployDomain(id: number) {
|
||||||
|
await request.post<void>(`/platform/domain/undeploy/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查域名 DNS 解析状态
|
* 检查域名 DNS 解析状态
|
||||||
*/
|
*/
|
||||||
@@ -173,3 +186,48 @@ export async function syncDomainsFromCertificates(serverId: number) {
|
|||||||
const res = await request.post<{ syncCount: number; message: string }>(`/platform/domain/sync-from-certificates/${serverId}`)
|
const res = await request.post<{ syncCount: number; message: string }>(`/platform/domain/sync-from-certificates/${serverId}`)
|
||||||
return res.data.data
|
return res.data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部署运行环境到1Panel
|
||||||
|
*/
|
||||||
|
export async function deployRuntime(domainId: number) {
|
||||||
|
const res = await request.post<DomainDeployResult>(`/platform/domain/${domainId}/deploy-runtime`)
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Nginx配置相关 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nginx配置信息
|
||||||
|
*/
|
||||||
|
export interface NginxConfigResult {
|
||||||
|
domain: string
|
||||||
|
path: string
|
||||||
|
content: string
|
||||||
|
name: string
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取域名Nginx配置
|
||||||
|
*/
|
||||||
|
export async function getNginxConfig(domainId: number): Promise<NginxConfigResult> {
|
||||||
|
const res = await request.get<NginxConfigResult>(`/platform/domain/${domainId}/nginx-config`)
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存域名Nginx配置
|
||||||
|
*/
|
||||||
|
export async function saveNginxConfig(domainId: number, content: string): Promise<boolean> {
|
||||||
|
const res = await request.put<boolean>(`/platform/domain/${domainId}/nginx-config`, { content })
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重载Nginx
|
||||||
|
*/
|
||||||
|
export async function reloadNginx(domainId: number): Promise<boolean> {
|
||||||
|
const res = await request.post<boolean>(`/platform/domain/${domainId}/nginx-reload`)
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
|||||||
148
src/api/file.ts
Normal file
148
src/api/file.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { request } from '@/utils/request'
|
||||||
|
|
||||||
|
export interface CheckFileResult {
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量检查文件是否存在
|
||||||
|
* @param serverId 服务器ID
|
||||||
|
* @param paths 文件路径列表
|
||||||
|
*/
|
||||||
|
export async function checkFileBatch(serverId: number, paths: string[]) {
|
||||||
|
const res = await request.post<string[]>('/platform/files/check', {
|
||||||
|
serverId,
|
||||||
|
paths
|
||||||
|
})
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分片上传文件(注意:1Panel 不支持真正的分片合并,此方法每次只上传一个分片作为完整文件)
|
||||||
|
* @deprecated 使用 uploadFile 代替
|
||||||
|
*/
|
||||||
|
export async function uploadFileChunk(
|
||||||
|
serverId: number,
|
||||||
|
filename: string,
|
||||||
|
path: string,
|
||||||
|
chunkIndex: number,
|
||||||
|
chunkCount: number,
|
||||||
|
chunk: Blob,
|
||||||
|
onUploadProgress?: (progressEvent: { loaded: number; total?: number }) => void
|
||||||
|
) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('serverId', serverId.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('/platform/files/upload/chunk', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
},
|
||||||
|
timeout: 30 * 60 * 1000, // 30分钟超时,适合大文件分片
|
||||||
|
onUploadProgress
|
||||||
|
})
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接上传完整文件
|
||||||
|
* @param serverId 服务器ID
|
||||||
|
* @param path 目标目录路径
|
||||||
|
* @param file 文件对象
|
||||||
|
* @param onUploadProgress 上传进度回调
|
||||||
|
*/
|
||||||
|
export async function uploadFile(
|
||||||
|
serverId: number,
|
||||||
|
path: string,
|
||||||
|
file: File,
|
||||||
|
onUploadProgress?: (progressEvent: { loaded: number; total?: number }) => void
|
||||||
|
) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('serverId', serverId.toString())
|
||||||
|
formData.append('path', path)
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
const res = await request.post('/platform/files/upload', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
},
|
||||||
|
timeout: 30 * 60 * 1000, // 30分钟超时,适合大文件
|
||||||
|
onUploadProgress
|
||||||
|
})
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分片上传 v2(支持真正的分片合并)
|
||||||
|
* @param serverId 服务器ID
|
||||||
|
* @param path 目标目录路径
|
||||||
|
* @param filename 文件名
|
||||||
|
* @param chunkIndex 分片索引(从0开始)
|
||||||
|
* @param chunkCount 分片总数
|
||||||
|
* @param fileSize 文件总大小
|
||||||
|
* @param uploadId 上传ID
|
||||||
|
* @param chunk 分片数据
|
||||||
|
* @param onUploadProgress 上传进度回调
|
||||||
|
*/
|
||||||
|
export async function uploadChunkV2(
|
||||||
|
serverId: number,
|
||||||
|
path: string,
|
||||||
|
filename: string,
|
||||||
|
chunkIndex: number,
|
||||||
|
chunkCount: number,
|
||||||
|
fileSize: number,
|
||||||
|
uploadId: string,
|
||||||
|
chunk: Blob,
|
||||||
|
onUploadProgress?: (progressEvent: { loaded: number; total?: number }) => void
|
||||||
|
) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('serverId', serverId.toString())
|
||||||
|
formData.append('path', path)
|
||||||
|
formData.append('filename', filename)
|
||||||
|
formData.append('chunkIndex', chunkIndex.toString())
|
||||||
|
formData.append('chunkCount', chunkCount.toString())
|
||||||
|
formData.append('fileSize', fileSize.toString())
|
||||||
|
formData.append('uploadId', uploadId)
|
||||||
|
formData.append('chunk', chunk)
|
||||||
|
|
||||||
|
const res = await request.post('/platform/files/upload/chunk/v2', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
},
|
||||||
|
timeout: 10 * 60 * 1000, // 10分钟超时
|
||||||
|
onUploadProgress
|
||||||
|
})
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并已上传的分片
|
||||||
|
* @param serverId 服务器ID
|
||||||
|
* @param path 目标目录路径
|
||||||
|
* @param filename 文件名
|
||||||
|
* @param chunkCount 分片总数
|
||||||
|
* @param fileSize 文件总大小
|
||||||
|
* @param uploadId 上传ID
|
||||||
|
*/
|
||||||
|
export async function mergeChunks(
|
||||||
|
serverId: number,
|
||||||
|
path: string,
|
||||||
|
filename: string,
|
||||||
|
chunkCount: number,
|
||||||
|
fileSize: number,
|
||||||
|
uploadId: string
|
||||||
|
) {
|
||||||
|
const res = await request.post('/platform/files/upload/merge', {
|
||||||
|
serverId,
|
||||||
|
path,
|
||||||
|
filename,
|
||||||
|
chunkCount,
|
||||||
|
fileSize,
|
||||||
|
uploadId
|
||||||
|
})
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
@@ -248,3 +248,40 @@ export async function uploadFileChunk(
|
|||||||
return res.data.data
|
return res.data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Nginx配置相关 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nginx配置信息
|
||||||
|
*/
|
||||||
|
export interface NginxConfigResult {
|
||||||
|
domain: string
|
||||||
|
path: string
|
||||||
|
content: string
|
||||||
|
name: string
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取项目Nginx配置
|
||||||
|
*/
|
||||||
|
export async function getNginxConfig(projectId: number): Promise<NginxConfigResult> {
|
||||||
|
const res = await request.get<NginxConfigResult>(`/platform/project/${projectId}/nginx-config`)
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存项目Nginx配置
|
||||||
|
*/
|
||||||
|
export async function saveNginxConfig(projectId: number, content: string): Promise<boolean> {
|
||||||
|
const res = await request.put<boolean>(`/platform/project/${projectId}/nginx-config`, { content })
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重载Nginx
|
||||||
|
*/
|
||||||
|
export async function reloadNginx(projectId: number): Promise<boolean> {
|
||||||
|
const res = await request.post<boolean>(`/platform/project/${projectId}/nginx-reload`)
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
310
src/api/runtime.ts
Normal file
310
src/api/runtime.ts
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行时类型
|
||||||
|
*/
|
||||||
|
export interface RuntimeType {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 端口映射
|
||||||
|
*/
|
||||||
|
export interface ExposedPort {
|
||||||
|
hostPort: number
|
||||||
|
containerPort: number
|
||||||
|
hostIP: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行时参数
|
||||||
|
*/
|
||||||
|
export interface RuntimeParams {
|
||||||
|
CODE_DIR?: string
|
||||||
|
CONTAINER_NAME?: string
|
||||||
|
CONTAINER_PACKAGE_URL?: string
|
||||||
|
CONTAINER_PORT_0?: string
|
||||||
|
CUSTOM_SCRIPT?: string
|
||||||
|
EXEC_SCRIPT?: string
|
||||||
|
HOST_IP?: string
|
||||||
|
HOST_IP_0?: string
|
||||||
|
HOST_PORT_0?: string
|
||||||
|
NODE_VERSION?: string
|
||||||
|
PACKAGE_MANAGER?: string
|
||||||
|
RUN_INSTALL?: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行时记录
|
||||||
|
*/
|
||||||
|
export interface RuntimeRecord {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
resource: string
|
||||||
|
appDetailID: number
|
||||||
|
appID: number
|
||||||
|
source: string
|
||||||
|
status: string
|
||||||
|
type: string
|
||||||
|
image: string
|
||||||
|
params: RuntimeParams
|
||||||
|
message: string
|
||||||
|
version: string
|
||||||
|
createdAt: string
|
||||||
|
codeDir: string
|
||||||
|
appParams: any
|
||||||
|
port: string
|
||||||
|
path: string
|
||||||
|
exposedPorts: ExposedPort[]
|
||||||
|
taskId?: string // 任务ID
|
||||||
|
environments: any
|
||||||
|
volumes: any
|
||||||
|
extraHosts: any
|
||||||
|
containerStatus: string
|
||||||
|
container: string
|
||||||
|
remark: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行时列表响应
|
||||||
|
*/
|
||||||
|
export interface RuntimeListResponse {
|
||||||
|
total: number
|
||||||
|
items: RuntimeRecord[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索运行时列表
|
||||||
|
*/
|
||||||
|
export async function searchRuntimes(
|
||||||
|
serverId: number,
|
||||||
|
type: string,
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 20
|
||||||
|
): Promise<RuntimeListResponse> {
|
||||||
|
const res = await request.post<any>('/platform/runtime/search', {
|
||||||
|
serverId,
|
||||||
|
type,
|
||||||
|
page,
|
||||||
|
pageSize
|
||||||
|
})
|
||||||
|
return res.data.data as RuntimeListResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索正在运行的运行时列表
|
||||||
|
* @param serverId 服务器ID
|
||||||
|
* @param type 运行时类型(java/node)
|
||||||
|
* @param page 页码
|
||||||
|
* @param pageSize 每页数量
|
||||||
|
*/
|
||||||
|
export async function searchRunningRuntimes(
|
||||||
|
serverId: number,
|
||||||
|
type: string,
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 100
|
||||||
|
): Promise<RuntimeListResponse> {
|
||||||
|
const res = await request.post<any>('/platform/runtime/search', {
|
||||||
|
serverId,
|
||||||
|
type,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
status: 'Running'
|
||||||
|
})
|
||||||
|
return res.data.data as RuntimeListResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步运行时状态
|
||||||
|
*/
|
||||||
|
export async function syncRuntimes(serverId: number): Promise<void> {
|
||||||
|
await request.post('/platform/runtime/sync', {
|
||||||
|
serverId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作运行时(启动/停止/重启)
|
||||||
|
*/
|
||||||
|
export async function operateRuntime(params: {
|
||||||
|
serverId: number
|
||||||
|
id: number
|
||||||
|
operate: 'start' | 'stop' | 'restart'
|
||||||
|
}): Promise<void> {
|
||||||
|
await request.post('/platform/runtime/operate', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除运行时
|
||||||
|
*/
|
||||||
|
export async function deleteRuntime(params: {
|
||||||
|
serverId: number
|
||||||
|
id: number
|
||||||
|
forceDelete?: boolean
|
||||||
|
deleteFolder?: boolean
|
||||||
|
codeDir?: string
|
||||||
|
}): Promise<void> {
|
||||||
|
await request.post('/platform/runtime/delete', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行时应用信息
|
||||||
|
*/
|
||||||
|
export interface RuntimeApp {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
key: string
|
||||||
|
description: string
|
||||||
|
status: string
|
||||||
|
installed: boolean
|
||||||
|
type: string
|
||||||
|
versions?: string[]
|
||||||
|
tags?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行时应用列表响应
|
||||||
|
*/
|
||||||
|
export interface RuntimeAppListResponse {
|
||||||
|
total: number
|
||||||
|
items: RuntimeApp[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索运行时应用列表
|
||||||
|
*/
|
||||||
|
export async function searchRuntimeApps(
|
||||||
|
serverId: number,
|
||||||
|
type: string,
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 20
|
||||||
|
): Promise<RuntimeAppListResponse> {
|
||||||
|
const res = await request.post<any>('/platform/runtime/apps/search', {
|
||||||
|
serverId,
|
||||||
|
type,
|
||||||
|
page,
|
||||||
|
pageSize
|
||||||
|
})
|
||||||
|
return res.data.data as RuntimeAppListResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取应用信息(含版本列表)
|
||||||
|
*/
|
||||||
|
export async function getRuntimeAppInfo(
|
||||||
|
serverId: number,
|
||||||
|
appKey: string
|
||||||
|
): Promise<RuntimeApp> {
|
||||||
|
const res = await request.post<any>('/platform/runtime/app/info', {
|
||||||
|
serverId,
|
||||||
|
appKey
|
||||||
|
})
|
||||||
|
return res.data.data as RuntimeApp
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行时版本详情
|
||||||
|
*/
|
||||||
|
export interface RuntimeVersionDetail {
|
||||||
|
id: number
|
||||||
|
appId: number
|
||||||
|
version: string
|
||||||
|
dockerCompose: string
|
||||||
|
status: string
|
||||||
|
enable: boolean
|
||||||
|
params: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取运行时版本详情
|
||||||
|
*/
|
||||||
|
export async function getRuntimeDetail(
|
||||||
|
serverId: number,
|
||||||
|
appId: number,
|
||||||
|
version: string
|
||||||
|
): Promise<RuntimeVersionDetail> {
|
||||||
|
const res = await request.post<any>('/platform/runtime/detail', {
|
||||||
|
serverId,
|
||||||
|
appId,
|
||||||
|
version
|
||||||
|
})
|
||||||
|
return res.data.data as RuntimeVersionDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建运行时参数
|
||||||
|
*/
|
||||||
|
export interface CreateRuntimeParams {
|
||||||
|
serverId: number
|
||||||
|
appDetailId: number
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
image: string
|
||||||
|
version: string
|
||||||
|
source: string
|
||||||
|
codeDir: string
|
||||||
|
params: Record<string, any>
|
||||||
|
exposedPorts?: ExposedPort[]
|
||||||
|
remark?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建运行时
|
||||||
|
*/
|
||||||
|
export async function createRuntime(params: CreateRuntimeParams): Promise<void> {
|
||||||
|
await request.post('/platform/runtime/create', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取容器日志
|
||||||
|
*/
|
||||||
|
export async function getContainerLog(
|
||||||
|
serverId: number,
|
||||||
|
containerName: string,
|
||||||
|
composePath?: string
|
||||||
|
): Promise<{ log: string }> {
|
||||||
|
// 注意:这里由于后端返回的是 Map {log: "..."},所以类型匹配 { log: string }
|
||||||
|
const res = await request.post<any>('/platform/runtime/container/log', {
|
||||||
|
serverId,
|
||||||
|
containerName,
|
||||||
|
composePath
|
||||||
|
})
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新运行时参数
|
||||||
|
*/
|
||||||
|
export interface UpdateRuntimeParams {
|
||||||
|
serverId: number
|
||||||
|
id: number
|
||||||
|
remark?: string
|
||||||
|
exposedPorts?: ExposedPort[]
|
||||||
|
params?: Record<string, any>
|
||||||
|
codeDir?: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新运行时
|
||||||
|
*/
|
||||||
|
export async function updateRuntime(params: UpdateRuntimeParams): Promise<void> {
|
||||||
|
await request.post('/platform/runtime/update', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeScript {
|
||||||
|
name: string
|
||||||
|
script: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取Node脚本
|
||||||
|
*/
|
||||||
|
export async function getNodeScripts(serverId: number, codeDir: string): Promise<NodeScript[]> {
|
||||||
|
const res = await request.post<NodeScript[]>('/platform/runtime/node/scripts', { serverId, codeDir })
|
||||||
|
// @ts-ignore
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,35 +1,7 @@
|
|||||||
import { request } from '@/utils/request'
|
import { request } from '@/utils/request'
|
||||||
|
|
||||||
// 部门类型定义
|
import type { DeptRecord, DeptFormData } from '@/types/system/dept'
|
||||||
export interface DeptRecord {
|
export type { DeptRecord, DeptFormData }
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取部门树
|
* 获取部门树
|
||||||
|
|||||||
59
src/components.d.ts
vendored
59
src/components.d.ts
vendored
@@ -33,6 +33,7 @@ declare module 'vue' {
|
|||||||
AFormItem: typeof import('ant-design-vue/es')['FormItem']
|
AFormItem: typeof import('ant-design-vue/es')['FormItem']
|
||||||
AFormItemRest: typeof import('ant-design-vue/es')['FormItemRest']
|
AFormItemRest: typeof import('ant-design-vue/es')['FormItemRest']
|
||||||
AInput: typeof import('ant-design-vue/es')['Input']
|
AInput: typeof import('ant-design-vue/es')['Input']
|
||||||
|
AInputGroup: typeof import('ant-design-vue/es')['InputGroup']
|
||||||
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
|
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
|
||||||
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
||||||
AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
|
AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
|
||||||
@@ -40,6 +41,9 @@ declare module 'vue' {
|
|||||||
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
|
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
|
||||||
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
|
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
|
||||||
ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
|
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']
|
AMenu: typeof import('ant-design-vue/es')['Menu']
|
||||||
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
|
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
|
||||||
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
|
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
|
||||||
@@ -47,9 +51,11 @@ declare module 'vue' {
|
|||||||
AModal: typeof import('ant-design-vue/es')['Modal']
|
AModal: typeof import('ant-design-vue/es')['Modal']
|
||||||
APageHeader: typeof import('ant-design-vue/es')['PageHeader']
|
APageHeader: typeof import('ant-design-vue/es')['PageHeader']
|
||||||
APagination: typeof import('ant-design-vue/es')['Pagination']
|
APagination: typeof import('ant-design-vue/es')['Pagination']
|
||||||
|
ApartmentOutlined: typeof import('@ant-design/icons-vue')['ApartmentOutlined']
|
||||||
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
||||||
APopover: typeof import('ant-design-vue/es')['Popover']
|
APopover: typeof import('ant-design-vue/es')['Popover']
|
||||||
ApprovalDrawer: typeof import('./components/ApprovalDrawer/index.vue')['default']
|
ApprovalDrawer: typeof import('./components/ApprovalDrawer/index.vue')['default']
|
||||||
|
AppstoreOutlined: typeof import('@ant-design/icons-vue')['AppstoreOutlined']
|
||||||
AProgress: typeof import('ant-design-vue/es')['Progress']
|
AProgress: typeof import('ant-design-vue/es')['Progress']
|
||||||
ARadio: typeof import('ant-design-vue/es')['Radio']
|
ARadio: typeof import('ant-design-vue/es')['Radio']
|
||||||
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
||||||
@@ -57,7 +63,10 @@ declare module 'vue' {
|
|||||||
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
|
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
|
||||||
AResult: typeof import('ant-design-vue/es')['Result']
|
AResult: typeof import('ant-design-vue/es')['Result']
|
||||||
ARow: typeof import('ant-design-vue/es')['Row']
|
ARow: typeof import('ant-design-vue/es')['Row']
|
||||||
|
ArrowLeftOutlined: typeof import('@ant-design/icons-vue')['ArrowLeftOutlined']
|
||||||
|
ArrowUpOutlined: typeof import('@ant-design/icons-vue')['ArrowUpOutlined']
|
||||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||||
|
ASelectOptGroup: typeof import('ant-design-vue/es')['SelectOptGroup']
|
||||||
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
||||||
ASpace: typeof import('ant-design-vue/es')['Space']
|
ASpace: typeof import('ant-design-vue/es')['Space']
|
||||||
ASpin: typeof import('ant-design-vue/es')['Spin']
|
ASpin: typeof import('ant-design-vue/es')['Spin']
|
||||||
@@ -76,23 +85,73 @@ declare module 'vue' {
|
|||||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||||
ATree: typeof import('ant-design-vue/es')['Tree']
|
ATree: typeof import('ant-design-vue/es')['Tree']
|
||||||
ATreeSelect: typeof import('ant-design-vue/es')['TreeSelect']
|
ATreeSelect: typeof import('ant-design-vue/es')['TreeSelect']
|
||||||
|
AUpload: typeof import('ant-design-vue/es')['Upload']
|
||||||
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
|
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
|
||||||
BudgetDetailModal: typeof import('./components/finance/budget/BudgetDetailModal.vue')['default']
|
BudgetDetailModal: typeof import('./components/finance/budget/BudgetDetailModal.vue')['default']
|
||||||
BudgetFormModal: typeof import('./components/finance/budget/BudgetFormModal.vue')['default']
|
BudgetFormModal: typeof import('./components/finance/budget/BudgetFormModal.vue')['default']
|
||||||
|
CheckCircleOutlined: typeof import('@ant-design/icons-vue')['CheckCircleOutlined']
|
||||||
|
CheckOutlined: typeof import('@ant-design/icons-vue')['CheckOutlined']
|
||||||
|
ClockCircleOutlined: typeof import('@ant-design/icons-vue')['ClockCircleOutlined']
|
||||||
|
CloseCircleFilled: typeof import('@ant-design/icons-vue')['CloseCircleFilled']
|
||||||
|
CloseCircleOutlined: typeof import('@ant-design/icons-vue')['CloseCircleOutlined']
|
||||||
|
CloseOutlined: typeof import('@ant-design/icons-vue')['CloseOutlined']
|
||||||
|
CloudServerOutlined: typeof import('@ant-design/icons-vue')['CloudServerOutlined']
|
||||||
|
CloudUploadOutlined: typeof import('@ant-design/icons-vue')['CloudUploadOutlined']
|
||||||
|
ClusterOutlined: typeof import('@ant-design/icons-vue')['ClusterOutlined']
|
||||||
|
CopyOutlined: typeof import('@ant-design/icons-vue')['CopyOutlined']
|
||||||
|
DatabaseOutlined: typeof import('@ant-design/icons-vue')['DatabaseOutlined']
|
||||||
|
DeleteOutlined: typeof import('@ant-design/icons-vue')['DeleteOutlined']
|
||||||
|
DesktopOutlined: typeof import('@ant-design/icons-vue')['DesktopOutlined']
|
||||||
DictFormModal: typeof import('./components/system/dict/DictFormModal.vue')['default']
|
DictFormModal: typeof import('./components/system/dict/DictFormModal.vue')['default']
|
||||||
DictItemDrawer: typeof import('./components/system/dict/DictItemDrawer.vue')['default']
|
DictItemDrawer: typeof import('./components/system/dict/DictItemDrawer.vue')['default']
|
||||||
|
DislikeOutlined: typeof import('@ant-design/icons-vue')['DislikeOutlined']
|
||||||
|
DownloadOutlined: typeof import('@ant-design/icons-vue')['DownloadOutlined']
|
||||||
DuplicateFileModal: typeof import('./components/DuplicateFileModal.vue')['default']
|
DuplicateFileModal: typeof import('./components/DuplicateFileModal.vue')['default']
|
||||||
DynamicMenu: typeof import('./components/DynamicMenu/index.vue')['default']
|
DynamicMenu: typeof import('./components/DynamicMenu/index.vue')['default']
|
||||||
|
EditOutlined: typeof import('@ant-design/icons-vue')['EditOutlined']
|
||||||
|
EyeInvisibleOutlined: typeof import('@ant-design/icons-vue')['EyeInvisibleOutlined']
|
||||||
|
EyeOutlined: typeof import('@ant-design/icons-vue')['EyeOutlined']
|
||||||
|
FileAddOutlined: typeof import('@ant-design/icons-vue')['FileAddOutlined']
|
||||||
|
FileOutlined: typeof import('@ant-design/icons-vue')['FileOutlined']
|
||||||
|
FileTextOutlined: typeof import('@ant-design/icons-vue')['FileTextOutlined']
|
||||||
|
FileUploader: typeof import('./components/FileUploader.vue')['default']
|
||||||
FlowEditor: typeof import('./components/FlowEditor/index.vue')['default']
|
FlowEditor: typeof import('./components/FlowEditor/index.vue')['default']
|
||||||
|
FolderAddOutlined: typeof import('@ant-design/icons-vue')['FolderAddOutlined']
|
||||||
|
FolderFilled: typeof import('@ant-design/icons-vue')['FolderFilled']
|
||||||
|
FolderOpenOutlined: typeof import('@ant-design/icons-vue')['FolderOpenOutlined']
|
||||||
|
FolderOutlined: typeof import('@ant-design/icons-vue')['FolderOutlined']
|
||||||
|
FolderSelector: typeof import('./components/FolderSelector.vue')['default']
|
||||||
|
GlobalOutlined: typeof import('@ant-design/icons-vue')['GlobalOutlined']
|
||||||
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
|
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
|
||||||
|
HistoryOutlined: typeof import('@ant-design/icons-vue')['HistoryOutlined']
|
||||||
IconPicker: typeof import('./components/common/IconPicker.vue')['default']
|
IconPicker: typeof import('./components/common/IconPicker.vue')['default']
|
||||||
|
LikeOutlined: typeof import('@ant-design/icons-vue')['LikeOutlined']
|
||||||
|
LinkOutlined: typeof import('@ant-design/icons-vue')['LinkOutlined']
|
||||||
|
LockOutlined: typeof import('@ant-design/icons-vue')['LockOutlined']
|
||||||
MenuFormModal: typeof import('./components/system/menu/MenuFormModal.vue')['default']
|
MenuFormModal: typeof import('./components/system/menu/MenuFormModal.vue')['default']
|
||||||
|
MessageOutlined: typeof import('@ant-design/icons-vue')['MessageOutlined']
|
||||||
|
MoreOutlined: typeof import('@ant-design/icons-vue')['MoreOutlined']
|
||||||
|
PlusOutlined: typeof import('@ant-design/icons-vue')['PlusOutlined']
|
||||||
ProjectUpload: typeof import('./components/ProjectUpload.vue')['default']
|
ProjectUpload: typeof import('./components/ProjectUpload.vue')['default']
|
||||||
|
ReloadOutlined: typeof import('@ant-design/icons-vue')['ReloadOutlined']
|
||||||
ResetPasswordModal: typeof import('./components/system/user/ResetPasswordModal.vue')['default']
|
ResetPasswordModal: typeof import('./components/system/user/ResetPasswordModal.vue')['default']
|
||||||
|
RightOutlined: typeof import('@ant-design/icons-vue')['RightOutlined']
|
||||||
RoleFormModal: typeof import('./components/system/role/RoleFormModal.vue')['default']
|
RoleFormModal: typeof import('./components/system/role/RoleFormModal.vue')['default']
|
||||||
|
RollbackOutlined: typeof import('@ant-design/icons-vue')['RollbackOutlined']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
SafetyCertificateOutlined: typeof import('@ant-design/icons-vue')['SafetyCertificateOutlined']
|
||||||
|
SaveOutlined: typeof import('@ant-design/icons-vue')['SaveOutlined']
|
||||||
|
SearchOutlined: typeof import('@ant-design/icons-vue')['SearchOutlined']
|
||||||
|
SwapOutlined: typeof import('@ant-design/icons-vue')['SwapOutlined']
|
||||||
|
SyncOutlined: typeof import('@ant-design/icons-vue')['SyncOutlined']
|
||||||
|
TagOutlined: typeof import('@ant-design/icons-vue')['TagOutlined']
|
||||||
|
TaskLogViewer: typeof import('./components/TaskLogViewer.vue')['default']
|
||||||
|
ThunderboltOutlined: typeof import('@ant-design/icons-vue')['ThunderboltOutlined']
|
||||||
UploadCore: typeof import('./components/UploadCore.vue')['default']
|
UploadCore: typeof import('./components/UploadCore.vue')['default']
|
||||||
|
UploadOutlined: typeof import('@ant-design/icons-vue')['UploadOutlined']
|
||||||
UserFormModal: typeof import('./components/system/user/UserFormModal.vue')['default']
|
UserFormModal: typeof import('./components/system/user/UserFormModal.vue')['default']
|
||||||
|
UserOutlined: typeof import('@ant-design/icons-vue')['UserOutlined']
|
||||||
|
WarningOutlined: typeof import('@ant-design/icons-vue')['WarningOutlined']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
457
src/components/FileUploader.vue
Normal file
457
src/components/FileUploader.vue
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
<template>
|
||||||
|
<a-drawer
|
||||||
|
v-model:open="visible"
|
||||||
|
title="上传文件"
|
||||||
|
placement="right"
|
||||||
|
:width="600"
|
||||||
|
:maskClosable="false"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<div class="uploader-container">
|
||||||
|
<!-- 顶部操作栏 -->
|
||||||
|
<div class="action-bar">
|
||||||
|
<a-space>
|
||||||
|
<a-upload
|
||||||
|
:file-list="fileList"
|
||||||
|
:multiple="true"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:show-upload-list="false"
|
||||||
|
>
|
||||||
|
<a-button type="primary">
|
||||||
|
<template #icon><UploadOutlined /></template>
|
||||||
|
上传文件
|
||||||
|
</a-button>
|
||||||
|
</a-upload>
|
||||||
|
|
||||||
|
<!-- webkitdirectory 属性用于选择文件夹,但在标准 a-upload 中可能需要自定义属性 -->
|
||||||
|
<a-upload
|
||||||
|
:file-list="fileList"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:show-upload-list="false"
|
||||||
|
directory
|
||||||
|
>
|
||||||
|
<a-button type="primary">
|
||||||
|
<template #icon><FolderOpenOutlined /></template>
|
||||||
|
上传文件夹
|
||||||
|
</a-button>
|
||||||
|
</a-upload>
|
||||||
|
|
||||||
|
<a-button @click="clearFiles" :disabled="fileList.length === 0">清空列表</a-button>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 拖拽区域 -->
|
||||||
|
<div class="upload-area">
|
||||||
|
<a-upload-dragger
|
||||||
|
:file-list="fileList"
|
||||||
|
:multiple="true"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:show-upload-list="false"
|
||||||
|
:customRequest="customRequest"
|
||||||
|
class="upload-dragger"
|
||||||
|
>
|
||||||
|
<div class="dragger-content">
|
||||||
|
<p class="ant-upload-drag-icon">
|
||||||
|
<CloudUploadOutlined />
|
||||||
|
</p>
|
||||||
|
<p class="ant-upload-text">将需要上传的文件拖拽到此处</p>
|
||||||
|
</div>
|
||||||
|
</a-upload-dragger>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文件列表 -->
|
||||||
|
<div class="file-list-header" v-if="fileList.length > 0">
|
||||||
|
<span>待上传列表 ({{ fileList.length }})</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="upload-list" v-if="fileList.length > 0">
|
||||||
|
<a-list :data-source="fileList" size="small">
|
||||||
|
<template #renderItem="{ item, index }">
|
||||||
|
<a-list-item>
|
||||||
|
<template #actions>
|
||||||
|
<a-button type="text" danger size="small" @click="removeFile(index)">
|
||||||
|
<DeleteOutlined />
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
<a-list-item-meta>
|
||||||
|
<template #title>
|
||||||
|
<span :class="{ 'error-text': item.status === 'error', 'success-text': item.status === 'done' }">
|
||||||
|
{{ item.name }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
<a-progress
|
||||||
|
v-if="item.status === 'uploading'"
|
||||||
|
:percent="item.percent"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<span v-else-if="item.status === 'error'" class="error-text">上传失败</span>
|
||||||
|
<span v-else-if="item.status === 'done'" class="success-text">上传成功</span>
|
||||||
|
<span v-else>{{ formatSize(item.size) }}</span>
|
||||||
|
</template>
|
||||||
|
<template #avatar>
|
||||||
|
<FileOutlined />
|
||||||
|
</template>
|
||||||
|
</a-list-item-meta>
|
||||||
|
</a-list-item>
|
||||||
|
</template>
|
||||||
|
</a-list>
|
||||||
|
</div>
|
||||||
|
<div v-else class="empty-state">
|
||||||
|
<InboxOutlined style="font-size: 48px; color: #d9d9d9;" />
|
||||||
|
<p style="color: #999; margin-top: 10px;">暂无文件</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="footer-actions">
|
||||||
|
<a-button @click="handleClose">取消</a-button>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
:loading="uploading"
|
||||||
|
@click="checkAndUpload"
|
||||||
|
:disabled="fileList.length === 0 || uploading"
|
||||||
|
>
|
||||||
|
{{ uploading ? '上传中...' : '确定上传' }} ({{ fileList.length }})
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 同名文件提示 -->
|
||||||
|
<DuplicateFileModal
|
||||||
|
v-model:visible="duplicateModalVisible"
|
||||||
|
:duplicate-files="duplicateFiles"
|
||||||
|
@skip-all="handleSkipDuplicates"
|
||||||
|
@overwrite-all="handleOverwriteDuplicates"
|
||||||
|
/>
|
||||||
|
</a-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { message, Modal } from 'ant-design-vue'
|
||||||
|
import type { UploadProps, UploadFile } from 'ant-design-vue'
|
||||||
|
import {
|
||||||
|
UploadOutlined,
|
||||||
|
FolderOpenOutlined,
|
||||||
|
CloudUploadOutlined,
|
||||||
|
FileOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
InboxOutlined
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
import axios from 'axios'
|
||||||
|
import DuplicateFileModal from './DuplicateFileModal.vue'
|
||||||
|
import { checkFileBatch, uploadFile } from '@/api/file'
|
||||||
|
import type { DuplicateFile } from '@/types'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
serverId: number
|
||||||
|
path: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const fileList = ref<UploadFile[]>([])
|
||||||
|
const uploading = ref(false)
|
||||||
|
const duplicateModalVisible = ref(false)
|
||||||
|
const duplicateFiles = ref<DuplicateFile[]>([])
|
||||||
|
|
||||||
|
// 辅助函数:更新文件进度并强制触发 Vue 响应式更新
|
||||||
|
const updateFileProgress = (file: UploadFile, percent: number, status?: UploadFile['status']) => {
|
||||||
|
const index = fileList.value.findIndex(f => f.uid === file.uid)
|
||||||
|
if (index !== -1) {
|
||||||
|
const updated = { ...fileList.value[index], percent: Math.floor(percent) }
|
||||||
|
if (status !== undefined) {
|
||||||
|
updated.status = status
|
||||||
|
}
|
||||||
|
fileList.value[index] = updated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露打开方法给父组件
|
||||||
|
const open = () => {
|
||||||
|
visible.value = true
|
||||||
|
fileList.value = []
|
||||||
|
uploading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭处理
|
||||||
|
const handleClose = () => {
|
||||||
|
if (uploading.value) {
|
||||||
|
message.warning('正在上传中,请稍后')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拦截自动上传
|
||||||
|
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||||
|
// Add to list manually to control state better
|
||||||
|
fileList.value = [...fileList.value, file]
|
||||||
|
return false // Prevent auto upload
|
||||||
|
}
|
||||||
|
|
||||||
|
// 也就是阻止默认的XHR请求,虽然beforeUpload返回false已经阻止了,
|
||||||
|
// 但UploadDragger有时候行为不一致,保留这个空实现更安全
|
||||||
|
const customRequest = ({ onSuccess }: any) => {
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除文件
|
||||||
|
const removeFile = (index: number) => {
|
||||||
|
const newFileList = [...fileList.value]
|
||||||
|
newFileList.splice(index, 1)
|
||||||
|
fileList.value = newFileList
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空列表
|
||||||
|
const clearFiles = () => {
|
||||||
|
fileList.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化大小
|
||||||
|
const formatSize = (bytes: number | undefined) => {
|
||||||
|
if (bytes === undefined) return '0 B'
|
||||||
|
if (bytes === 0) return '0 B'
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件并上传
|
||||||
|
const checkAndUpload = async () => {
|
||||||
|
if (fileList.value.length === 0) return
|
||||||
|
|
||||||
|
// 1. 构建检查路径
|
||||||
|
const basePath = props.path.replace(/\/$/, '')
|
||||||
|
// 过滤掉已经上传成功的
|
||||||
|
const pendingFiles = fileList.value.filter(f => f.status !== 'done')
|
||||||
|
if (pendingFiles.length === 0) {
|
||||||
|
if (fileList.value.some(f => f.status === 'done')) {
|
||||||
|
message.success('文件已全部上传')
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathsToCheck = pendingFiles.map(f => {
|
||||||
|
// 这里的 UploadFile 可能是 a-upload 生成的,没有 path 属性,使用 name
|
||||||
|
const relativePath = (f as any).webkitRelativePath || f.name
|
||||||
|
return `${basePath}/${relativePath}`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. 调用检查接口
|
||||||
|
try {
|
||||||
|
uploading.value = true
|
||||||
|
const existingPaths = await checkFileBatch(props.serverId, pathsToCheck)
|
||||||
|
uploading.value = false
|
||||||
|
|
||||||
|
if (existingPaths && existingPaths.length > 0) {
|
||||||
|
// 发现重复文件
|
||||||
|
const duplicates: DuplicateFile[] = []
|
||||||
|
const existingSet = new Set(existingPaths)
|
||||||
|
|
||||||
|
for (let i = 0; i < pendingFiles.length; i++) {
|
||||||
|
const file = pendingFiles[i]
|
||||||
|
const relativePath = (file as any).webkitRelativePath || file.name
|
||||||
|
const fullPath = `${basePath}/${relativePath}`
|
||||||
|
|
||||||
|
if (existingSet.has(fullPath)) {
|
||||||
|
duplicates.push({
|
||||||
|
uid: file.uid,
|
||||||
|
name: file.name,
|
||||||
|
path: relativePath,
|
||||||
|
size: file.size,
|
||||||
|
file: file.originFileObj as File
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duplicates.length > 0) {
|
||||||
|
duplicateFiles.value = duplicates
|
||||||
|
duplicateModalVisible.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有重复,直接上传
|
||||||
|
startUpload()
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
uploading.value = false
|
||||||
|
|
||||||
|
// 显示确认框,让用户选择是否跳过检查直接上传
|
||||||
|
Modal.confirm({
|
||||||
|
title: '检查文件失败',
|
||||||
|
content: '无法检查服务器上是否存在同名文件(可能是网络问题)。是否跳过检查直接上传?\n\n注意:如果服务器上存在同名文件,将会被覆盖。',
|
||||||
|
okText: '直接上传',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: () => {
|
||||||
|
startUpload()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳过重复
|
||||||
|
const handleSkipDuplicates = () => {
|
||||||
|
// 从 fileList 中移除重复的
|
||||||
|
const dupeUids = new Set(duplicateFiles.value.map(d => d.uid))
|
||||||
|
fileList.value = fileList.value.filter(f => !dupeUids.has(f.uid))
|
||||||
|
duplicateFiles.value = []
|
||||||
|
|
||||||
|
if (fileList.value.length > 0) {
|
||||||
|
startUpload()
|
||||||
|
} else {
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 覆盖重复
|
||||||
|
const handleOverwriteDuplicates = () => {
|
||||||
|
// 不做处理,直接上传(因为上传接口支持 overwrite)
|
||||||
|
duplicateFiles.value = []
|
||||||
|
startUpload()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始上传
|
||||||
|
const startUpload = async () => {
|
||||||
|
if (fileList.value.length === 0) return
|
||||||
|
|
||||||
|
uploading.value = true
|
||||||
|
|
||||||
|
for (const file of fileList.value) {
|
||||||
|
if (file.status === 'done') continue // Skip already uploaded
|
||||||
|
|
||||||
|
// 使用辅助函数更新状态
|
||||||
|
updateFileProgress(file, 0, 'uploading')
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取实际的 File 对象,可能在 originFileObj 中或文件本身就是 File
|
||||||
|
const realFile = (file.originFileObj || file) as File
|
||||||
|
|
||||||
|
// 检查文件对象是否有效
|
||||||
|
if (!realFile || typeof realFile.size !== 'number') {
|
||||||
|
throw new Error('无效的文件对象')
|
||||||
|
}
|
||||||
|
|
||||||
|
const basePath = props.path.replace(/\/$/, '')
|
||||||
|
const relativePath = (file as any).webkitRelativePath || file.name
|
||||||
|
// 1Panel 的 upload 接口 expects "path" to be the DIRECTORY where file is saved.
|
||||||
|
// 所以我们传 props.path (如果 relativePath 只有文件名)
|
||||||
|
// 如果 relativePath 包含子目录(如 'folder/file.txt'),我们需要传 props.path + '/folder'
|
||||||
|
|
||||||
|
let targetDir = basePath
|
||||||
|
const lastSlashIndex = relativePath.lastIndexOf('/')
|
||||||
|
if (lastSlashIndex > 0) {
|
||||||
|
targetDir = `${basePath}/${relativePath.substring(0, lastSlashIndex)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用直接上传(1Panel 分片上传 API 参数不明确,暂时禁用分片上传)
|
||||||
|
await uploadFile(
|
||||||
|
props.serverId,
|
||||||
|
targetDir,
|
||||||
|
realFile,
|
||||||
|
(progressEvent) => {
|
||||||
|
if (progressEvent.total) {
|
||||||
|
const percent = (progressEvent.loaded / progressEvent.total) * 100
|
||||||
|
updateFileProgress(file, percent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 上传成功
|
||||||
|
updateFileProgress(file, 100, 'done')
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Upload failed', error)
|
||||||
|
updateFileProgress(file, 0, 'error')
|
||||||
|
|
||||||
|
// 显示更详细的错误信息
|
||||||
|
const errorMsg = error?.response?.data?.message || error?.message || '未知错误'
|
||||||
|
message.error(`文件 ${file.name} 上传失败: ${errorMsg}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uploading.value = false
|
||||||
|
|
||||||
|
// Check if all success
|
||||||
|
const allSuccess = fileList.value.every(f => f.status === 'done')
|
||||||
|
if (allSuccess) {
|
||||||
|
message.success('所有文件上传成功')
|
||||||
|
setTimeout(() => {
|
||||||
|
visible.value = false
|
||||||
|
// Emit event to refresh parent list
|
||||||
|
emit('upload-success')
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
message.warn('部分文件上传失败,请重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'upload-success'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.uploader-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragger-content {
|
||||||
|
padding: 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-header {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px dashed #d9d9d9;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-actions {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-text {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
412
src/components/FolderSelector.vue
Normal file
412
src/components/FolderSelector.vue
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
<template>
|
||||||
|
<div class="folder-selector">
|
||||||
|
<a-input
|
||||||
|
:value="modelValue"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:disabled="disabled"
|
||||||
|
readonly
|
||||||
|
@click="!disabled && openModal()"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FolderOutlined
|
||||||
|
class="folder-icon"
|
||||||
|
:style="{ cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.5 : 1 }"
|
||||||
|
@click.stop="!disabled && openModal()"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
|
||||||
|
<!-- 文件列表弹窗 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="modalVisible"
|
||||||
|
title="文件列表"
|
||||||
|
:width="600"
|
||||||
|
:maskClosable="false"
|
||||||
|
@ok="handleConfirm"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
>
|
||||||
|
<div class="folder-browser">
|
||||||
|
<!-- 当前路径 -->
|
||||||
|
<div class="path-bar">
|
||||||
|
<a-input v-model:value="currentPath" readonly>
|
||||||
|
<template #prefix>
|
||||||
|
<HomeOutlined />
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作栏 -->
|
||||||
|
<div class="action-bar">
|
||||||
|
<a-button type="link" @click="goBack" :disabled="currentPath === '/'">
|
||||||
|
返回上一级
|
||||||
|
</a-button>
|
||||||
|
<div class="right-actions">
|
||||||
|
<a-button type="link" @click="handleNewFolder">新建文件夹</a-button>
|
||||||
|
<a-button type="link" @click="handleNewFile">新建文件</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文件/文件夹列表 -->
|
||||||
|
<div class="file-list">
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div
|
||||||
|
v-for="item in fileList"
|
||||||
|
:key="item.name"
|
||||||
|
class="file-item"
|
||||||
|
:class="{ selected: selectedPath === getFullPath(item.name) }"
|
||||||
|
@click="handleSelect(item)"
|
||||||
|
@dblclick="handleDoubleClick(item)"
|
||||||
|
>
|
||||||
|
<FolderFilled v-if="item.isDir" class="icon folder" />
|
||||||
|
<FileOutlined v-else class="icon file" />
|
||||||
|
<span class="name">{{ item.name }}</span>
|
||||||
|
<a-popconfirm
|
||||||
|
title="确定删除吗?"
|
||||||
|
@confirm="handleDelete(item)"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<a-button type="text" danger size="small" class="delete-btn">
|
||||||
|
<template #icon><DeleteOutlined /></template>
|
||||||
|
</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
</div>
|
||||||
|
<div v-if="!loading && fileList.length === 0" class="empty">
|
||||||
|
当前目录为空
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 当前选中 -->
|
||||||
|
<div class="selected-path">
|
||||||
|
<span>当前选中:</span>
|
||||||
|
<a-tag v-if="selectedPath" color="blue">{{ selectedPath }}</a-tag>
|
||||||
|
<span v-else class="no-select">未选择</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新建文件夹对话框 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="newFolderVisible"
|
||||||
|
title="新建文件夹"
|
||||||
|
:width="400"
|
||||||
|
@ok="confirmNewFolder"
|
||||||
|
>
|
||||||
|
<a-input v-model:value="newFolderName" placeholder="请输入文件夹名称" />
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<a-modal
|
||||||
|
v-model:open="newFileVisible"
|
||||||
|
title="新建文件"
|
||||||
|
:width="400"
|
||||||
|
@ok="confirmNewFile"
|
||||||
|
>
|
||||||
|
<a-input v-model:value="newFileName" placeholder="请输入文件名称" />
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import {
|
||||||
|
FolderOutlined,
|
||||||
|
FolderFilled,
|
||||||
|
FileOutlined,
|
||||||
|
HomeOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
interface FileItem {
|
||||||
|
name: string
|
||||||
|
isDir: boolean
|
||||||
|
size?: number
|
||||||
|
modTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: string
|
||||||
|
serverId: number
|
||||||
|
placeholder?: string
|
||||||
|
disabled?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 弹窗状态
|
||||||
|
const modalVisible = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const currentPath = ref('/')
|
||||||
|
const selectedPath = ref('')
|
||||||
|
const fileList = ref<FileItem[]>([])
|
||||||
|
|
||||||
|
// 新建文件夹
|
||||||
|
const newFolderVisible = ref(false)
|
||||||
|
const newFolderName = ref('')
|
||||||
|
const newFileVisible = ref(false)
|
||||||
|
const newFileName = ref('')
|
||||||
|
|
||||||
|
// 打开弹窗
|
||||||
|
function openModal() {
|
||||||
|
modalVisible.value = true
|
||||||
|
currentPath.value = props.modelValue || '/'
|
||||||
|
selectedPath.value = props.modelValue || ''
|
||||||
|
loadFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载文件列表
|
||||||
|
async function loadFiles() {
|
||||||
|
if (!props.serverId) {
|
||||||
|
message.warning('请先选择服务器')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await request.post<any>('/platform/files/list', {
|
||||||
|
serverId: props.serverId,
|
||||||
|
path: currentPath.value
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
fileList.value = (res.data.data.items || [])
|
||||||
|
// .filter((item: FileItem) => item.isDir) // 移除过滤,显示所有文件
|
||||||
|
.sort((a: FileItem, b: FileItem) => {
|
||||||
|
// 文件夹排在前面
|
||||||
|
if (a.isDir && !b.isDir) return -1;
|
||||||
|
if (!a.isDir && b.isDir) return 1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
message.error(res.data.message || '加载失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('加载文件列表失败:', error)
|
||||||
|
message.error('加载文件列表失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回上一级
|
||||||
|
function goBack() {
|
||||||
|
if (currentPath.value === '/') return
|
||||||
|
|
||||||
|
const parts = currentPath.value.split('/').filter(Boolean)
|
||||||
|
parts.pop()
|
||||||
|
currentPath.value = '/' + parts.join('/')
|
||||||
|
if (!currentPath.value) currentPath.value = '/'
|
||||||
|
loadFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取完整路径
|
||||||
|
function getFullPath(name: string): string {
|
||||||
|
if (currentPath.value === '/') {
|
||||||
|
return '/' + name
|
||||||
|
}
|
||||||
|
return currentPath.value + '/' + name
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择项目
|
||||||
|
function handleSelect(item: FileItem) {
|
||||||
|
if (item.isDir) {
|
||||||
|
selectedPath.value = getFullPath(item.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 双击进入目录
|
||||||
|
function handleDoubleClick(item: FileItem) {
|
||||||
|
if (item.isDir) {
|
||||||
|
currentPath.value = getFullPath(item.name)
|
||||||
|
loadFiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新建文件夹
|
||||||
|
function handleNewFolder() {
|
||||||
|
newFolderName.value = ''
|
||||||
|
newFolderVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认新建文件夹
|
||||||
|
async function confirmNewFolder() {
|
||||||
|
if (!newFolderName.value.trim()) {
|
||||||
|
message.warning('请输入文件夹名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newPath = getFullPath(newFolderName.value)
|
||||||
|
await request.post('/platform/files/mkdir', {
|
||||||
|
serverId: props.serverId,
|
||||||
|
path: newPath
|
||||||
|
})
|
||||||
|
message.success('创建成功')
|
||||||
|
newFolderVisible.value = false
|
||||||
|
loadFiles()
|
||||||
|
} catch (error: any) {
|
||||||
|
// 显示具体错误信息
|
||||||
|
const msg = error.response?.data?.message || error.message || '创建失败'
|
||||||
|
message.error(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除文件/文件夹
|
||||||
|
async function handleDelete(item: FileItem) {
|
||||||
|
try {
|
||||||
|
const path = getFullPath(item.name)
|
||||||
|
await request.post('/platform/files/delete', {
|
||||||
|
serverId: props.serverId,
|
||||||
|
path,
|
||||||
|
isDir: item.isDir
|
||||||
|
})
|
||||||
|
message.success('删除成功')
|
||||||
|
loadFiles()
|
||||||
|
} catch (error: any) {
|
||||||
|
const msg = error.response?.data?.message || error.message || '删除失败'
|
||||||
|
message.error(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新建文件
|
||||||
|
function handleNewFile() {
|
||||||
|
newFileName.value = ''
|
||||||
|
newFileVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认新建文件
|
||||||
|
async function confirmNewFile() {
|
||||||
|
if (!newFileName.value.trim()) {
|
||||||
|
message.warning('请输入文件名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newPath = getFullPath(newFileName.value)
|
||||||
|
await request.post('/platform/files/create', {
|
||||||
|
serverId: props.serverId,
|
||||||
|
path: newPath
|
||||||
|
})
|
||||||
|
message.success('创建成功')
|
||||||
|
newFileVisible.value = false
|
||||||
|
loadFiles()
|
||||||
|
} catch (error: any) {
|
||||||
|
const msg = error.response?.data?.message || error.message || '创建失败'
|
||||||
|
message.error(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认选择
|
||||||
|
function handleConfirm() {
|
||||||
|
if (selectedPath.value) {
|
||||||
|
emit('update:modelValue', selectedPath.value)
|
||||||
|
}
|
||||||
|
modalVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消
|
||||||
|
function handleCancel() {
|
||||||
|
modalVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听modelValue变化
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
if (val && !modalVisible.value) {
|
||||||
|
selectedPath.value = val
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.folder-selector :deep(.ant-input) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-browser {
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-bar {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list {
|
||||||
|
min-height: 300px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item.selected {
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item .icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item .icon.folder {
|
||||||
|
color: #faad14;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item .icon.file {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item .name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-path {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-path .no-select {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
197
src/components/TaskLogViewer.vue
Normal file
197
src/components/TaskLogViewer.vue
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
:open="open"
|
||||||
|
title="日志详情"
|
||||||
|
:footer="null"
|
||||||
|
width="800px"
|
||||||
|
@update:open="val => emit('update:open', val)"
|
||||||
|
@cancel="handleClose"
|
||||||
|
>
|
||||||
|
<div class="log-container">
|
||||||
|
<div class="log-status">
|
||||||
|
<span>状态:</span>
|
||||||
|
<a-tag :color="getStatusColor(status)">
|
||||||
|
{{ getStatusText(status) }}
|
||||||
|
</a-tag>
|
||||||
|
<a-spin v-if="isActive(status)" size="small" style="margin-left: 8px" />
|
||||||
|
<a-button v-if="containerName || composePath" type="link" size="small" @click="fetchLogs">刷新</a-button>
|
||||||
|
</div>
|
||||||
|
<div class="log-content" ref="logContentRef">
|
||||||
|
<pre><code>{{ logs.join('\n') }}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, onUnmounted, nextTick } from 'vue'
|
||||||
|
import { readTaskLog } from '@/api/database'
|
||||||
|
import { getContainerLog } from '@/api/runtime'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
open: boolean
|
||||||
|
serverId: number | undefined
|
||||||
|
taskId?: string
|
||||||
|
containerName?: string
|
||||||
|
composePath?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:open', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const logs = ref<string[]>([])
|
||||||
|
const status = ref('')
|
||||||
|
const logContentRef = ref<HTMLElement>()
|
||||||
|
let timer: any = null
|
||||||
|
|
||||||
|
function isActive(s: string) {
|
||||||
|
return s === 'Executing' || s === 'Waiting'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusColor(s: string) {
|
||||||
|
switch (s) {
|
||||||
|
case 'Success': return 'green'
|
||||||
|
case 'Failed': return 'red'
|
||||||
|
case 'Executing': return 'blue'
|
||||||
|
case 'Waiting': return 'orange'
|
||||||
|
case 'Fetched': return 'green'
|
||||||
|
default: return 'default'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusText(s: string) {
|
||||||
|
switch (s) {
|
||||||
|
case 'Success': return '成功'
|
||||||
|
case 'Failed': return '失败'
|
||||||
|
case 'Executing': return '执行中'
|
||||||
|
case 'Waiting': return '等待中'
|
||||||
|
case 'Fetched': return '已获取'
|
||||||
|
default: return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Remove "data: container |" prefix
|
||||||
|
function cleanLogLine(line: string) {
|
||||||
|
if (!line) return ''
|
||||||
|
let content = line
|
||||||
|
// Handle "data: " prefix
|
||||||
|
if (content.startsWith('data:')) {
|
||||||
|
// Find the pipe separator
|
||||||
|
const pipeIdx = content.indexOf('|')
|
||||||
|
if (pipeIdx > -1) {
|
||||||
|
// "data: name | log..."
|
||||||
|
content = content.substring(pipeIdx + 1)
|
||||||
|
} else {
|
||||||
|
// "data: log..."
|
||||||
|
content = content.substring(5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Remove ANSI Color Codes
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
content = content.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '')
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchLogs() {
|
||||||
|
if (!props.serverId) return
|
||||||
|
|
||||||
|
if (props.taskId) {
|
||||||
|
// ... (task log)
|
||||||
|
try {
|
||||||
|
const res = await readTaskLog(props.serverId, props.taskId)
|
||||||
|
logs.value = res.lines || []
|
||||||
|
status.value = res.taskStatus
|
||||||
|
|
||||||
|
// Check if active
|
||||||
|
if (isActive(res.taskStatus)) {
|
||||||
|
if (!timer) {
|
||||||
|
timer = setInterval(fetchLogs, 2000)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stopPolling()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
stopPolling()
|
||||||
|
}
|
||||||
|
} else if (props.containerName || props.composePath) {
|
||||||
|
status.value = 'Fetching'
|
||||||
|
try {
|
||||||
|
const res = await getContainerLog(props.serverId, props.containerName || '', props.composePath)
|
||||||
|
logs.value = res.log ? res.log.split('\n').map(cleanLogLine) : ['无日志内容']
|
||||||
|
status.value = 'Fetched'
|
||||||
|
} catch (e: any) {
|
||||||
|
logs.value = ['获取日志失败: ' + (e.message || '未知错误')]
|
||||||
|
status.value = 'Failed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// Auto scroll to bottom
|
||||||
|
nextTick(() => {
|
||||||
|
if (logContentRef.value) {
|
||||||
|
logContentRef.value.scrollTop = logContentRef.value.scrollHeight
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPolling() {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
stopPolling()
|
||||||
|
emit('update:open', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.open, (val) => {
|
||||||
|
if (val) {
|
||||||
|
logs.value = []
|
||||||
|
status.value = ''
|
||||||
|
fetchLogs()
|
||||||
|
} else {
|
||||||
|
stopPolling()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopPolling()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.log-container .log-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-container .log-content {
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-container .log-content pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-container .log-content code {
|
||||||
|
color: #d4d4d4;
|
||||||
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -418,14 +418,34 @@ function updateMenuState() {
|
|||||||
projectStore.switchProject(projectId)
|
projectStore.switchProject(projectId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const subPath = path.replace(`/app/${projectId}`, '')
|
// 同步子项目 iframe URL
|
||||||
|
const subPath = path.replace(`/app/${projectId}`, '') || '/dashboard'
|
||||||
|
const project = projectStore.getProjectById(projectId)
|
||||||
|
// 只有当项目已加载且 url 有效时才更新
|
||||||
|
if (project?.baseUrl) {
|
||||||
|
const targetUrl = `${project.baseUrl}${subPath}?__embedded=true`
|
||||||
|
// 避免重复刷新
|
||||||
|
if (subProjectUrl.value !== targetUrl) {
|
||||||
|
iframeLoading.value = true
|
||||||
|
subProjectUrl.value = targetUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const subMenuRouteMap = projectStore.getMenuRouteMap()
|
const subMenuRouteMap = projectStore.getMenuRouteMap()
|
||||||
|
|
||||||
for (const [menuKey, menuPath] of Object.entries(subMenuRouteMap)) {
|
for (const [menuKey, menuPath] of Object.entries(subMenuRouteMap)) {
|
||||||
const relativePath = menuPath.replace(`/app/${projectId}`, '')
|
const relativePath = menuPath.replace(`/app/${projectId}`, '')
|
||||||
if (subPath === relativePath || subPath.startsWith(relativePath + '/')) {
|
if (subPath === relativePath || subPath.startsWith(relativePath + '/')) {
|
||||||
selectedKeys.value = [menuKey]
|
selectedKeys.value = [menuKey]
|
||||||
// 展开逻辑略
|
// 展开父级菜单
|
||||||
|
const pathParts = menuKey.split('/').filter(Boolean)
|
||||||
|
if (pathParts.length > 1) {
|
||||||
|
const parentPath = `/${pathParts[0]}`
|
||||||
|
const openKey = `sub-${parentPath}`
|
||||||
|
if (!openKeys.value.includes(openKey)) {
|
||||||
|
openKeys.value.push(openKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
// 数据库类型
|
|
||||||
export type DatabaseType = 'mysql' | 'postgresql' | 'redis'
|
|
||||||
|
|
||||||
// 数据库状态
|
|
||||||
export type DatabaseStatus = 'running' | 'stopped' | 'error'
|
|
||||||
|
|
||||||
// 数据库信息
|
|
||||||
export interface DatabaseInfo {
|
|
||||||
type: DatabaseType
|
|
||||||
status: DatabaseStatus
|
|
||||||
version: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 数据库实例
|
|
||||||
export interface DatabaseItem {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
description?: string
|
|
||||||
createdAt: string
|
|
||||||
updatedAt?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建数据库请求
|
|
||||||
export interface CreateDatabaseRequest {
|
|
||||||
name: string
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
description?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新数据库请求
|
|
||||||
export interface UpdateDatabaseRequest {
|
|
||||||
name?: string
|
|
||||||
username?: string
|
|
||||||
password?: string
|
|
||||||
description?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 数据库连接信息
|
|
||||||
export interface DatabaseConnectionInfo {
|
|
||||||
host: string
|
|
||||||
port: number
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
database: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分页请求
|
|
||||||
export interface DatabaseListParams {
|
|
||||||
keyword?: string
|
|
||||||
page?: number
|
|
||||||
pageSize?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分页响应
|
|
||||||
export interface DatabaseListResponse {
|
|
||||||
list: DatabaseItem[]
|
|
||||||
total: number
|
|
||||||
page: number
|
|
||||||
pageSize: number
|
|
||||||
}
|
|
||||||
23
src/types/platform/certificates.ts
Normal file
23
src/types/platform/certificates.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* 证书管理类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Certificate {
|
||||||
|
id: string
|
||||||
|
domain: string
|
||||||
|
otherDomain?: string
|
||||||
|
cn?: string
|
||||||
|
issuer: string
|
||||||
|
status: 'valid' | 'expired' | 'pending'
|
||||||
|
startDate: string
|
||||||
|
expireDate: string
|
||||||
|
autoRenew: boolean
|
||||||
|
verifyType: 'dns'
|
||||||
|
dnsAccount?: string
|
||||||
|
dnsAccountName?: string
|
||||||
|
dnsAccountType?: string
|
||||||
|
acmeAccount: string
|
||||||
|
remark?: string
|
||||||
|
certContent?: string
|
||||||
|
keyContent?: string
|
||||||
|
}
|
||||||
9
src/types/platform/databases.ts
Normal file
9
src/types/platform/databases.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* 数据库管理类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface DatabaseType {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
defaultPort: number
|
||||||
|
}
|
||||||
11
src/types/platform/files.ts
Normal file
11
src/types/platform/files.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* 文件管理类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface FileItem {
|
||||||
|
name: string
|
||||||
|
isDir: boolean
|
||||||
|
size: number
|
||||||
|
modTime: string
|
||||||
|
mode?: string
|
||||||
|
}
|
||||||
17
src/types/platform/menus.ts
Normal file
17
src/types/platform/menus.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* 平台菜单管理类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { MenuItem } from '@/config'
|
||||||
|
|
||||||
|
export interface PlatformProject {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
shortName: string
|
||||||
|
logo: string
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuNode extends MenuItem {
|
||||||
|
order?: number
|
||||||
|
}
|
||||||
36
src/types/platform/servers.ts
Normal file
36
src/types/platform/servers.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* 服务器管理类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface MonitorData {
|
||||||
|
cpu: number
|
||||||
|
memory: number
|
||||||
|
disk: number
|
||||||
|
networkIn: number
|
||||||
|
networkOut: number
|
||||||
|
time: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerStatus = 'online' | 'offline' | 'warning' | 'maintenance'
|
||||||
|
export type ServerType = 'cloud' | 'physical' | 'virtual'
|
||||||
|
|
||||||
|
export interface ServerInfo {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
ip: string
|
||||||
|
internalIp?: string
|
||||||
|
port?: number
|
||||||
|
user?: string
|
||||||
|
status: ServerStatus
|
||||||
|
type: ServerType
|
||||||
|
os?: string
|
||||||
|
cpu?: number
|
||||||
|
memory?: number
|
||||||
|
disk?: number
|
||||||
|
region?: string
|
||||||
|
expiredAt?: string
|
||||||
|
remark?: string
|
||||||
|
// 监控数据
|
||||||
|
monitor?: MonitorData
|
||||||
|
tags?: string[]
|
||||||
|
}
|
||||||
14
src/types/platform/upload-file.ts
Normal file
14
src/types/platform/upload-file.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* 上传文件类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface UploadFile {
|
||||||
|
uid: string
|
||||||
|
name: string
|
||||||
|
path: string // 相对路径
|
||||||
|
size: number
|
||||||
|
type: string
|
||||||
|
status: 'pending' | 'uploading' | 'done' | 'error'
|
||||||
|
percent: number
|
||||||
|
file: File
|
||||||
|
}
|
||||||
9
src/types/platform/upload.ts
Normal file
9
src/types/platform/upload.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* 文件上传类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface UploadOptions {
|
||||||
|
path: string
|
||||||
|
overwrite: boolean
|
||||||
|
createDirs: boolean
|
||||||
|
}
|
||||||
33
src/types/system/dept.ts
Normal file
33
src/types/system/dept.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* 部门管理类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
12
src/types/system/dict.ts
Normal file
12
src/types/system/dict.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* 字典管理类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface DictRecord {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
remark?: string
|
||||||
|
status: number
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import type { ApiResponse } from '@/types/api/response'
|
|||||||
console.log('API Base URL:', import.meta.env.VITE_API_BASE_URL)
|
console.log('API Base URL:', import.meta.env.VITE_API_BASE_URL)
|
||||||
const service: AxiosInstance = axios.create({
|
const service: AxiosInstance = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||||
timeout: 30000,
|
timeout: 5 * 60 * 1000, // 5分钟超时,适合大文件上传
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,15 +262,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import {
|
|
||||||
PlusOutlined,
|
|
||||||
ArrowLeftOutlined,
|
|
||||||
CopyOutlined,
|
|
||||||
DownloadOutlined,
|
|
||||||
SyncOutlined
|
|
||||||
} from '@ant-design/icons-vue'
|
|
||||||
import type { TablePaginationConfig } from 'ant-design-vue'
|
import type { TablePaginationConfig } from 'ant-design-vue'
|
||||||
import {
|
import {
|
||||||
getCertificateList,
|
getCertificateList,
|
||||||
@@ -287,27 +279,8 @@ import {
|
|||||||
type WebsiteInfo
|
type WebsiteInfo
|
||||||
} from '@/api/certificate'
|
} from '@/api/certificate'
|
||||||
import { getServerList, type ServerBase } from '@/api/server'
|
import { getServerList, type ServerBase } from '@/api/server'
|
||||||
|
import type { Certificate } from '@/types/platform/certificates'
|
||||||
|
|
||||||
// 证书类型定义
|
|
||||||
interface Certificate {
|
|
||||||
id: string
|
|
||||||
domain: string
|
|
||||||
otherDomain?: string
|
|
||||||
cn?: string
|
|
||||||
issuer: string
|
|
||||||
status: 'valid' | 'expired' | 'pending'
|
|
||||||
startDate: string
|
|
||||||
expireDate: string
|
|
||||||
autoRenew: boolean
|
|
||||||
verifyType: 'dns'
|
|
||||||
dnsAccount?: string
|
|
||||||
dnsAccountName?: string
|
|
||||||
dnsAccountType?: string
|
|
||||||
acmeAccount: string
|
|
||||||
remark?: string
|
|
||||||
certContent?: string
|
|
||||||
keyContent?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 表格列配置
|
// 表格列配置
|
||||||
const columns = [
|
const columns = [
|
||||||
|
|||||||
1483
src/views/platform/databases/index.vue
Normal file
1483
src/views/platform/databases/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -361,22 +361,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
|
||||||
import { message, Modal } from 'ant-design-vue'
|
import { message, Modal } from 'ant-design-vue'
|
||||||
import {
|
|
||||||
PlusOutlined,
|
|
||||||
MoreOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
CopyOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
TagOutlined,
|
|
||||||
CloudServerOutlined,
|
|
||||||
SwapOutlined,
|
|
||||||
DownloadOutlined,
|
|
||||||
UploadOutlined,
|
|
||||||
EyeOutlined,
|
|
||||||
LockOutlined
|
|
||||||
} from '@ant-design/icons-vue'
|
|
||||||
import type { EnvironmentInfo, EnvironmentType, EnvironmentVariable } from '@/types/platform'
|
import type { EnvironmentInfo, EnvironmentType, EnvironmentVariable } from '@/types/platform'
|
||||||
import { mockEnvironments, mockServers } from '@/mock/platform'
|
import { mockEnvironments, mockServers } from '@/mock/platform'
|
||||||
import { mockProjects } from '@/mock/projects'
|
import { mockProjects } from '@/mock/projects'
|
||||||
|
|||||||
267
src/views/platform/files/index.vue
Normal file
267
src/views/platform/files/index.vue
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
<template>
|
||||||
|
<div class="file-manager-page">
|
||||||
|
<a-page-header title="文件管理" sub-title="管理服务器文件">
|
||||||
|
<template #extra>
|
||||||
|
<a-select
|
||||||
|
v-model:value="currentServerId"
|
||||||
|
placeholder="请选择服务器"
|
||||||
|
style="width: 200px"
|
||||||
|
@change="handleServerChange"
|
||||||
|
>
|
||||||
|
<a-select-option v-for="server in serverList" :key="server.id" :value="server.id">
|
||||||
|
{{ server.name }} ({{ server.ip }})
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</template>
|
||||||
|
</a-page-header>
|
||||||
|
|
||||||
|
<a-card :bordered="false">
|
||||||
|
<!-- 操作栏 -->
|
||||||
|
<div class="action-bar">
|
||||||
|
<div class="left-actions">
|
||||||
|
<!-- 路径导航 -->
|
||||||
|
<a-input-group compact style="display: flex; width: 400px">
|
||||||
|
<a-button @click="goUp" :disabled="currentPath === '/'"><ArrowUpOutlined /></a-button>
|
||||||
|
<a-input v-model:value="currentPath" @pressEnter="loadFiles" style="flex: 1" />
|
||||||
|
<a-button type="primary" @click="loadFiles"><ReloadOutlined /></a-button>
|
||||||
|
</a-input-group>
|
||||||
|
</div>
|
||||||
|
<div class="right-actions">
|
||||||
|
<a-space>
|
||||||
|
<a-button @click="handleNewFolder"><FolderAddOutlined /> 新建文件夹</a-button>
|
||||||
|
<a-button @click="handleNewFile"><FileAddOutlined /> 新建文件</a-button>
|
||||||
|
<FileUploader
|
||||||
|
v-if="currentServerId"
|
||||||
|
:server-id="currentServerId"
|
||||||
|
:path="currentPath"
|
||||||
|
@success="loadFiles"
|
||||||
|
>
|
||||||
|
<a-button type="primary"><CloudUploadOutlined /> 上传文件</a-button>
|
||||||
|
</FileUploader>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文件列表 -->
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="fileList"
|
||||||
|
:loading="loading"
|
||||||
|
row-key="name"
|
||||||
|
:pagination="false"
|
||||||
|
:scroll="{ y: 600 }"
|
||||||
|
size="middle"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'name'">
|
||||||
|
<div class="file-name" @click="handleItemClick(record as FileItem)" style="cursor: pointer; display: flex; align-items: center;">
|
||||||
|
<FolderFilled v-if="record.isDir" style="color: #faad14; font-size: 18px; margin-right: 8px;" />
|
||||||
|
<FileOutlined v-else style="color: #1890ff; font-size: 18px; margin-right: 8px;" />
|
||||||
|
<span :style="{ fontWeight: record.isDir ? '500' : 'normal' }">{{ record.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'size'">
|
||||||
|
<span v-if="!record.isDir">{{ formatSize(record.size) }}</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'action'">
|
||||||
|
<a-space>
|
||||||
|
<a-button type="link" size="small" @click="handleDownload(record as FileItem)" v-if="!record.isDir">下载</a-button>
|
||||||
|
<a-popconfirm title="确定删除吗?" @confirm="handleDelete(record as FileItem)">
|
||||||
|
<a-button type="link" danger size="small">删除</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<!-- 新建文件夹弹窗 -->
|
||||||
|
<a-modal v-model:open="newFolderVisible" title="新建文件夹" @ok="confirmNewFolder">
|
||||||
|
<a-input v-model:value="newFolderName" placeholder="请输入文件夹名称" ref="newFolderInput" />
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 新建文件弹窗 -->
|
||||||
|
<a-modal v-model:open="newFileVisible" title="新建文件" @ok="confirmNewFile">
|
||||||
|
<a-input v-model:value="newFileName" placeholder="请输入文件名称" />
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
import { getServerList } from '@/api/server'
|
||||||
|
import FileUploader from '@/components/FileUploader.vue'
|
||||||
|
import type { FileItem } from '@/types/platform/files'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const serverList = ref<any[]>([])
|
||||||
|
const currentServerId = ref<number | undefined>(undefined)
|
||||||
|
const currentPath = ref('/')
|
||||||
|
const fileList = ref<FileItem[]>([])
|
||||||
|
|
||||||
|
const newFolderVisible = ref(false)
|
||||||
|
const newFolderName = ref('')
|
||||||
|
const newFileVisible = ref(false)
|
||||||
|
const newFileName = ref('')
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ title: '名称', key: 'name', sorter: (a: FileItem, b: FileItem) => a.name.localeCompare(b.name) },
|
||||||
|
{ title: '大小', key: 'size', width: 120, align: 'right' as const },
|
||||||
|
{ title: '修改时间', dataIndex: 'modTime', key: 'modTime', width: 200 },
|
||||||
|
{ title: '权限', dataIndex: 'mode', key: 'mode', width: 100 },
|
||||||
|
{ title: '操作', key: 'action', width: 150, align: 'center' as const }
|
||||||
|
]
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadServers()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadServers() {
|
||||||
|
try {
|
||||||
|
const res = await getServerList()
|
||||||
|
serverList.value = res || []
|
||||||
|
if (serverList.value.length > 0) {
|
||||||
|
currentServerId.value = serverList.value[0].id
|
||||||
|
loadFiles()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载服务器列表失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleServerChange() {
|
||||||
|
currentPath.value = '/'
|
||||||
|
loadFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadFiles() {
|
||||||
|
if (!currentServerId.value) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await request.post<any>('/platform/files/list', {
|
||||||
|
serverId: currentServerId.value,
|
||||||
|
path: currentPath.value
|
||||||
|
})
|
||||||
|
|
||||||
|
// 兼容不同的后端返回结构
|
||||||
|
const data = res.data.data
|
||||||
|
const items = Array.isArray(data) ? data : (data.items || [])
|
||||||
|
|
||||||
|
fileList.value = items.sort((a: FileItem, b: FileItem) => {
|
||||||
|
if (a.isDir && !b.isDir) return -1
|
||||||
|
if (!a.isDir && b.isDir) return 1
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载文件列表失败', error)
|
||||||
|
fileList.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleItemClick(item: FileItem) {
|
||||||
|
if (item.isDir) {
|
||||||
|
currentPath.value = getFullPath(item.name)
|
||||||
|
loadFiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goUp() {
|
||||||
|
if (currentPath.value === '/') return
|
||||||
|
const parts = currentPath.value.split('/').filter(Boolean)
|
||||||
|
parts.pop()
|
||||||
|
currentPath.value = '/' + parts.join('/')
|
||||||
|
loadFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFullPath(name: string) {
|
||||||
|
return currentPath.value === '/' ? `/${name}` : `${currentPath.value}/${name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSize(bytes: number) {
|
||||||
|
if (bytes === 0) return '0 B'
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新建文件夹
|
||||||
|
function handleNewFolder() {
|
||||||
|
newFolderName.value = ''
|
||||||
|
newFolderVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmNewFolder() {
|
||||||
|
if (!newFolderName.value) return
|
||||||
|
try {
|
||||||
|
await request.post('/platform/files/mkdir', {
|
||||||
|
serverId: currentServerId.value,
|
||||||
|
path: getFullPath(newFolderName.value)
|
||||||
|
})
|
||||||
|
message.success('创建成功')
|
||||||
|
newFolderVisible.value = false
|
||||||
|
loadFiles()
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.response?.data?.message || '创建失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新建文件
|
||||||
|
function handleNewFile() {
|
||||||
|
newFileName.value = ''
|
||||||
|
newFileVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmNewFile() {
|
||||||
|
if (!newFileName.value) return
|
||||||
|
try {
|
||||||
|
await request.post('/platform/files/create', {
|
||||||
|
serverId: currentServerId.value,
|
||||||
|
path: getFullPath(newFileName.value)
|
||||||
|
})
|
||||||
|
message.success('创建成功')
|
||||||
|
newFileVisible.value = false
|
||||||
|
loadFiles()
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.response?.data?.message || '创建失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
async function handleDelete(item: FileItem) {
|
||||||
|
try {
|
||||||
|
await request.post('/platform/files/delete', {
|
||||||
|
serverId: currentServerId.value,
|
||||||
|
path: getFullPath(item.name),
|
||||||
|
isDir: item.isDir
|
||||||
|
})
|
||||||
|
message.success('删除成功')
|
||||||
|
loadFiles()
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.response?.data?.message || '删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载
|
||||||
|
function handleDownload(item: FileItem) {
|
||||||
|
// 简易下载实现,实际上可能需要一个 dedicated download API that handles streams or signed URLs
|
||||||
|
// Assuming backend has a simple download endpoint or we reuse 1Panel approach
|
||||||
|
// 这里暂时不做完整实现,或者提示用户
|
||||||
|
message.info('下载功能开发中')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.file-manager-page {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.action-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -242,17 +242,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import type { Dayjs } from 'dayjs'
|
import type { Dayjs } from 'dayjs'
|
||||||
import {
|
|
||||||
SearchOutlined,
|
|
||||||
DownloadOutlined,
|
|
||||||
EyeOutlined,
|
|
||||||
TagOutlined,
|
|
||||||
CloudServerOutlined
|
|
||||||
} from '@ant-design/icons-vue'
|
|
||||||
import type { LogRecord, LogType, LogLevel } from '@/types/platform'
|
import type { LogRecord, LogType, LogLevel } from '@/types/platform'
|
||||||
import { mockLogs } from '@/mock/platform'
|
import { mockLogs } from '@/mock/platform'
|
||||||
import { mockProjects } from '@/mock/projects'
|
import { mockProjects } from '@/mock/projects'
|
||||||
|
|||||||
@@ -148,26 +148,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
import type { PlatformProject, MenuNode } from '@/types/platform/menus'
|
||||||
import type { MenuItem } from '@/config'
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
interface PlatformProject {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
shortName: string
|
|
||||||
logo: string
|
|
||||||
color?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MenuNode extends MenuItem {
|
|
||||||
order?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedProjectId = ref<string>('')
|
const selectedProjectId = ref<string>('')
|
||||||
const selectedMenuKeys = ref<string[]>([])
|
const selectedMenuKeys = ref<string[]>([])
|
||||||
const isEditing = ref(false)
|
const isEditing = ref(false)
|
||||||
|
|||||||
@@ -56,7 +56,13 @@
|
|||||||
show-zero
|
show-zero
|
||||||
:number-style="{ backgroundColor: record.color || '#1890ff' }"
|
:number-style="{ backgroundColor: record.color || '#1890ff' }"
|
||||||
>
|
>
|
||||||
<a-button type="link" size="small" @click="goToMenus(record as any)">
|
<a-button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
@click="goToMenus(record as any)"
|
||||||
|
:disabled="(record as any).systemType === 'portal'"
|
||||||
|
:title="(record as any).systemType === 'portal' ? '门户类项目无菜单' : ''"
|
||||||
|
>
|
||||||
查看菜单
|
查看菜单
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-badge>
|
</a-badge>
|
||||||
@@ -71,8 +77,7 @@
|
|||||||
v-if="getDeployStatus((record as any).id).deployed"
|
v-if="getDeployStatus((record as any).id).deployed"
|
||||||
type="default"
|
type="default"
|
||||||
size="small"
|
size="small"
|
||||||
style="color: #52c41a; border-color: #52c41a;"
|
style="color: #52c41a; border-color: #52c41a; cursor: default;"
|
||||||
@click="handleDeploy(record as any)"
|
|
||||||
>
|
>
|
||||||
<CheckCircleOutlined /> 已部署
|
<CheckCircleOutlined /> 已部署
|
||||||
</a-button>
|
</a-button>
|
||||||
@@ -462,20 +467,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { message, Modal } from 'ant-design-vue'
|
import { message, Modal } from 'ant-design-vue'
|
||||||
import {
|
|
||||||
PlusOutlined,
|
|
||||||
HistoryOutlined,
|
|
||||||
SaveOutlined,
|
|
||||||
RollbackOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
UploadOutlined,
|
|
||||||
FileOutlined,
|
|
||||||
CloudUploadOutlined,
|
|
||||||
CheckCircleOutlined
|
|
||||||
} from '@ant-design/icons-vue'
|
|
||||||
import type { PlatformProject, ProjectMenuItem, ProjectVersion } from '@/mock/projects'
|
import type { PlatformProject, ProjectMenuItem, ProjectVersion } from '@/mock/projects'
|
||||||
import { useProjectStore } from '@/stores'
|
import { useProjectStore } from '@/stores'
|
||||||
import ProjectUpload from '@/components/ProjectUpload.vue'
|
import ProjectUpload from '@/components/ProjectUpload.vue'
|
||||||
@@ -491,9 +483,13 @@ import {
|
|||||||
getServerAcmeAccounts,
|
getServerAcmeAccounts,
|
||||||
getServerDnsAccounts,
|
getServerDnsAccounts,
|
||||||
checkDeployStatus,
|
checkDeployStatus,
|
||||||
|
getNginxConfig,
|
||||||
|
saveNginxConfig,
|
||||||
|
reloadNginx,
|
||||||
type AcmeAccount,
|
type AcmeAccount,
|
||||||
type DnsAccount,
|
type DnsAccount,
|
||||||
type DeployResult
|
type DeployResult,
|
||||||
|
type NginxConfigResult
|
||||||
} from '@/api/project'
|
} from '@/api/project'
|
||||||
import { getServerList } from '@/api/server'
|
import { getServerList } from '@/api/server'
|
||||||
import { getDomainList } from '@/api/domain'
|
import { getDomainList } from '@/api/domain'
|
||||||
@@ -672,30 +668,6 @@ const versionList = computed(() => {
|
|||||||
return [...(versionProject.value.versions || [])].reverse()
|
return [...(versionProject.value.versions || [])].reverse()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 证书信息表格列
|
|
||||||
const certColumns = [
|
|
||||||
{ title: '主域名', dataIndex: 'domain', key: 'domain' },
|
|
||||||
{ title: '其他域名', dataIndex: 'otherDomain', key: 'otherDomain' },
|
|
||||||
{ title: '签发组织', dataIndex: 'issuer', key: 'issuer' },
|
|
||||||
{ title: '类型', dataIndex: 'type', key: 'type' },
|
|
||||||
{ title: 'Acme 账号', dataIndex: 'acmeAccount', key: 'acmeAccount' },
|
|
||||||
{ title: '过期时间', dataIndex: 'expireTime', key: 'expireTime' },
|
|
||||||
{ title: '备注', dataIndex: 'remark', key: 'remark' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 证书mock数据
|
|
||||||
const certData = [
|
|
||||||
{
|
|
||||||
domain: 'key.nanxiislet.com',
|
|
||||||
otherDomain: '',
|
|
||||||
issuer: "Let's Encrypt",
|
|
||||||
type: 'DNS账号',
|
|
||||||
acmeAccount: 'acme@1paneldev.com',
|
|
||||||
expireTime: '2026-03-18',
|
|
||||||
remark: ''
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
id: '', // 对应后端 ID (Update用)
|
id: '', // 对应后端 ID (Update用)
|
||||||
code: '', // 对应后端 Code (项目标识)
|
code: '', // 对应后端 Code (项目标识)
|
||||||
@@ -753,21 +725,6 @@ function formatTime(isoString: string): string {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算菜单项总数
|
|
||||||
*/
|
|
||||||
function countMenuItems(menus: ProjectMenuItem[]): number {
|
|
||||||
if (!menus) return 0
|
|
||||||
let count = 0
|
|
||||||
for (const menu of menus) {
|
|
||||||
if (menu.children) {
|
|
||||||
count += menu.children.length
|
|
||||||
} else {
|
|
||||||
count += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开版本管理抽屉
|
* 打开版本管理抽屉
|
||||||
@@ -839,17 +796,28 @@ function handleDeleteVersion(version: ProjectVersion) {
|
|||||||
/**
|
/**
|
||||||
* 进入项目
|
* 进入项目
|
||||||
*/
|
*/
|
||||||
function handleEnterProject(record: PlatformProject) {
|
function handleEnterProject(record: any) {
|
||||||
if (!record.enabled) {
|
if (record.status !== 'active') {
|
||||||
message.warning('项目已禁用,无法进入')
|
message.warning('项目已禁用,无法进入')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!record.baseUrl) {
|
|
||||||
|
// 1. 集成到框架内的后台:切换项目上下文并跳转
|
||||||
|
if (record.integrateToFramework) {
|
||||||
|
projectStore.switchProject(record.id)
|
||||||
|
message.success(`已切换到项目:${record.name}`)
|
||||||
|
// 跳转到业务项目路由
|
||||||
|
router.push(`/app/${record.id}/dashboard`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 未集成:打开新页面
|
||||||
|
const url = record.url || record.baseUrl
|
||||||
|
if (!url) {
|
||||||
message.warning('项目未配置访问地址,无法进入')
|
message.warning('项目未配置访问地址,无法进入')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
projectStore.switchProject(record.id)
|
window.open(url, '_blank')
|
||||||
router.push(`/app/${record.id}/dashboard`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAdd() {
|
function handleAdd() {
|
||||||
@@ -1077,45 +1045,6 @@ function getServerName(serverId?: string): string {
|
|||||||
return server ? `${server.name} (${server.ip})` : ''
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开部署弹窗
|
* 打开部署弹窗
|
||||||
@@ -1126,10 +1055,6 @@ async function handleDeploy(record: PlatformProject) {
|
|||||||
deployResult.value = null
|
deployResult.value = null
|
||||||
deploySteps.value = []
|
deploySteps.value = []
|
||||||
deployCurrentStep.value = 0
|
deployCurrentStep.value = 0
|
||||||
|
|
||||||
// 加载账户数据 - 已移除,改用后端自动匹配已绑定证书
|
|
||||||
// await loadPanelAccounts()
|
|
||||||
|
|
||||||
deployModalVisible.value = true
|
deployModalVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1110
src/views/platform/runtimes/index.vue
Normal file
1110
src/views/platform/runtimes/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,17 +8,16 @@
|
|||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label="状态">
|
<a-form-item label="状态">
|
||||||
<a-select v-model:value="filterStatus" placeholder="全部" style="width: 120px" allow-clear>
|
<a-select v-model:value="filterStatus" placeholder="全部" style="width: 120px" allow-clear>
|
||||||
<a-select-option value="online">在线</a-select-option>
|
<a-select-option v-for="item in serverStatusDict" :key="item.value" :value="item.value">
|
||||||
<a-select-option value="offline">离线</a-select-option>
|
{{ item.label }}
|
||||||
<a-select-option value="warning">告警</a-select-option>
|
</a-select-option>
|
||||||
<a-select-option value="maintenance">维护中</a-select-option>
|
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="类型">
|
<a-form-item label="类型">
|
||||||
<a-select v-model:value="filterType" placeholder="全部" style="width: 120px" allow-clear>
|
<a-select v-model:value="filterType" placeholder="全部" style="width: 120px" allow-clear>
|
||||||
<a-select-option value="cloud">云服务器</a-select-option>
|
<a-select-option v-for="item in serverTypeDict" :key="item.value" :value="item.value">
|
||||||
<a-select-option value="physical">物理机</a-select-option>
|
{{ item.label }}
|
||||||
<a-select-option value="virtual">虚拟机</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="关键字">
|
<a-form-item label="关键字">
|
||||||
@@ -195,9 +194,9 @@
|
|||||||
|
|
||||||
<a-form-item label="服务器类型" name="type">
|
<a-form-item label="服务器类型" name="type">
|
||||||
<a-select v-model:value="formData.type" placeholder="请选择">
|
<a-select v-model:value="formData.type" placeholder="请选择">
|
||||||
<a-select-option value="cloud">云服务器</a-select-option>
|
<a-select-option v-for="item in serverTypeDict" :key="item.value" :value="item.value">
|
||||||
<a-select-option value="physical">物理机</a-select-option>
|
{{ item.label }}
|
||||||
<a-select-option value="virtual">虚拟机</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
@@ -225,10 +224,9 @@
|
|||||||
|
|
||||||
<a-form-item label="标签" name="tags">
|
<a-form-item label="标签" name="tags">
|
||||||
<a-select v-model:value="formData.tags" mode="tags" placeholder="输入标签后回车">
|
<a-select v-model:value="formData.tags" mode="tags" placeholder="输入标签后回车">
|
||||||
<a-select-option value="生产">生产</a-select-option>
|
<a-select-option v-for="item in serverTagDict" :key="item.value" :value="item.value">
|
||||||
<a-select-option value="测试">测试</a-select-option>
|
{{ item.label }}
|
||||||
<a-select-option value="开发">开发</a-select-option>
|
</a-select-option>
|
||||||
<a-select-option value="备份">备份</a-select-option>
|
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
@@ -347,21 +345,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import {
|
|
||||||
PlusOutlined,
|
|
||||||
SearchOutlined,
|
|
||||||
EyeOutlined,
|
|
||||||
LinkOutlined,
|
|
||||||
CloudServerOutlined,
|
|
||||||
DesktopOutlined,
|
|
||||||
ClusterOutlined,
|
|
||||||
ReloadOutlined,
|
|
||||||
FileTextOutlined,
|
|
||||||
GlobalOutlined
|
|
||||||
} from '@ant-design/icons-vue'
|
|
||||||
import {
|
import {
|
||||||
getServerList,
|
getServerList,
|
||||||
getServerStatus,
|
getServerStatus,
|
||||||
@@ -369,11 +353,10 @@ import {
|
|||||||
updateServer,
|
updateServer,
|
||||||
deleteServer,
|
deleteServer,
|
||||||
refreshServerStatus,
|
refreshServerStatus,
|
||||||
type ServerBase,
|
|
||||||
type ServerInfo,
|
|
||||||
type ServerStatus,
|
|
||||||
type ServerType
|
|
||||||
} from '@/api/server'
|
} from '@/api/server'
|
||||||
|
import type { MonitorData, ServerStatus, ServerType, ServerInfo } from '@/types/platform/servers'
|
||||||
|
|
||||||
|
import { getDictItemsByCode } from '@/api/system/dict'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@@ -387,8 +370,13 @@ const filterStatus = ref<ServerStatus | undefined>()
|
|||||||
const filterType = ref<ServerType | undefined>()
|
const filterType = ref<ServerType | undefined>()
|
||||||
const filterKeyword = ref('')
|
const filterKeyword = ref('')
|
||||||
|
|
||||||
|
// Dictionaries
|
||||||
|
const serverStatusDict = ref<any[]>([])
|
||||||
|
const serverTypeDict = ref<any[]>([])
|
||||||
|
const serverTagDict = ref<any[]>([])
|
||||||
|
|
||||||
// 服务器列表,包含基础信息和实时状态
|
// 服务器列表,包含基础信息和实时状态
|
||||||
interface ServerDisplayInfo extends Partial<ServerInfo> {
|
interface ServerDisplayInfo extends Omit<Partial<ServerInfo>, 'cpu' | 'memory' | 'disk'> {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
ip: string
|
ip: string
|
||||||
@@ -410,6 +398,7 @@ interface ServerDisplayInfo extends Partial<ServerInfo> {
|
|||||||
disk: { total: number; used: number; usage: number }
|
disk: { total: number; used: number; usage: number }
|
||||||
// 状态加载标记
|
// 状态加载标记
|
||||||
statusLoading?: boolean
|
statusLoading?: boolean
|
||||||
|
monitor?: MonitorData
|
||||||
}
|
}
|
||||||
|
|
||||||
const servers = ref<ServerDisplayInfo[]>([])
|
const servers = ref<ServerDisplayInfo[]>([])
|
||||||
@@ -458,9 +447,25 @@ const stats = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
loadDictionaries()
|
||||||
loadServers()
|
loadServers()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function loadDictionaries() {
|
||||||
|
try {
|
||||||
|
const [statusRes, typeRes, tagRes] = await Promise.all([
|
||||||
|
getDictItemsByCode('server_status'),
|
||||||
|
getDictItemsByCode('server_type'),
|
||||||
|
getDictItemsByCode('server_tag')
|
||||||
|
])
|
||||||
|
serverStatusDict.value = statusRes.data.data || []
|
||||||
|
serverTypeDict.value = typeRes.data.data || []
|
||||||
|
serverTagDict.value = tagRes.data.data || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载字典失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载服务器列表
|
* 加载服务器列表
|
||||||
*/
|
*/
|
||||||
@@ -690,33 +695,27 @@ function goToDomains(record: ServerDisplayInfo) {
|
|||||||
router.push({ path: '/platform/domains', query: { serverId: String(record.id) } })
|
router.push({ path: '/platform/domains', query: { serverId: String(record.id) } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 字典辅助函数
|
||||||
|
function getDictColor(dict: any[], value: string, defaultColor: string = 'default'): string {
|
||||||
|
const item = dict.find(item => item.value === value)
|
||||||
|
return item?.remark || defaultColor
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDictLabel(dict: any[], value: string, defaultText: string = '-'): string {
|
||||||
|
const item = dict.find(item => item.value === value)
|
||||||
|
return item?.label || defaultText
|
||||||
|
}
|
||||||
|
|
||||||
function getStatusColor(status: ServerStatus): string {
|
function getStatusColor(status: ServerStatus): string {
|
||||||
const colors: Record<ServerStatus, string> = {
|
return getDictColor(serverStatusDict.value, status)
|
||||||
online: 'green',
|
|
||||||
offline: 'default',
|
|
||||||
warning: 'orange',
|
|
||||||
maintenance: 'blue'
|
|
||||||
}
|
|
||||||
return colors[status]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusText(status: ServerStatus): string {
|
function getStatusText(status: ServerStatus): string {
|
||||||
const texts: Record<ServerStatus, string> = {
|
return getDictLabel(serverStatusDict.value, status)
|
||||||
online: '在线',
|
|
||||||
offline: '离线',
|
|
||||||
warning: '告警',
|
|
||||||
maintenance: '维护中'
|
|
||||||
}
|
|
||||||
return texts[status]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTypeText(type: ServerType): string {
|
function getTypeText(type: ServerType): string {
|
||||||
const texts: Record<ServerType, string> = {
|
return getDictLabel(serverTypeDict.value, type)
|
||||||
cloud: '云服务器',
|
|
||||||
physical: '物理机',
|
|
||||||
virtual: '虚拟机'
|
|
||||||
}
|
|
||||||
return texts[type]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUsageColor(usage: number): string {
|
function getUsageColor(usage: number): string {
|
||||||
@@ -726,16 +725,7 @@ function getUsageColor(usage: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTagColor(tag: string): string {
|
function getTagColor(tag: string): string {
|
||||||
const colors: Record<string, string> = {
|
return getDictColor(serverTagDict.value, tag)
|
||||||
'生产': 'red',
|
|
||||||
'测试': 'blue',
|
|
||||||
'开发': 'green',
|
|
||||||
'备份': 'purple',
|
|
||||||
'主节点': 'gold',
|
|
||||||
'从节点': 'cyan',
|
|
||||||
'离线': 'default'
|
|
||||||
}
|
|
||||||
return colors[tag] || 'default'
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -105,18 +105,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
|
||||||
import { message, Modal } from 'ant-design-vue'
|
import { message, Modal } from 'ant-design-vue'
|
||||||
import {
|
|
||||||
ArrowLeftOutlined,
|
|
||||||
CloseOutlined,
|
|
||||||
UploadOutlined,
|
|
||||||
FolderOpenOutlined,
|
|
||||||
CloudUploadOutlined,
|
|
||||||
FileOutlined,
|
|
||||||
CheckOutlined
|
|
||||||
} from '@ant-design/icons-vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -128,17 +117,7 @@ const folderInputRef = ref<HTMLInputElement | null>(null)
|
|||||||
// 拖拽状态
|
// 拖拽状态
|
||||||
const isDragover = ref(false)
|
const isDragover = ref(false)
|
||||||
|
|
||||||
// 文件列表
|
import type { UploadFile } from '@/types/platform/upload-file'
|
||||||
interface UploadFile {
|
|
||||||
uid: string
|
|
||||||
name: string
|
|
||||||
path: string // 相对路径
|
|
||||||
size: number
|
|
||||||
type: string
|
|
||||||
status: 'pending' | 'uploading' | 'done' | 'error'
|
|
||||||
percent: number
|
|
||||||
file: File
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileList = ref<UploadFile[]>([])
|
const fileList = ref<UploadFile[]>([])
|
||||||
|
|
||||||
|
|||||||
@@ -203,21 +203,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import type { FormInstance } from 'ant-design-vue'
|
import type { FormInstance } from 'ant-design-vue'
|
||||||
import {
|
|
||||||
PlusOutlined,
|
|
||||||
ApartmentOutlined,
|
|
||||||
CheckCircleOutlined,
|
|
||||||
FileTextOutlined,
|
|
||||||
ClockCircleOutlined,
|
|
||||||
LikeOutlined,
|
|
||||||
DislikeOutlined,
|
|
||||||
RightOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
DeleteOutlined
|
|
||||||
} from '@ant-design/icons-vue'
|
|
||||||
import type { ApprovalTemplate, ApprovalScenario, ApprovalNode } from '@/types'
|
import type { ApprovalTemplate, ApprovalScenario, ApprovalNode } from '@/types'
|
||||||
import { ApprovalScenarioMap } from '@/types'
|
import { ApprovalScenarioMap } from '@/types'
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -191,16 +191,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import type { Dayjs } from 'dayjs'
|
import type { Dayjs } from 'dayjs'
|
||||||
import {
|
|
||||||
SearchOutlined,
|
|
||||||
ReloadOutlined,
|
|
||||||
EyeOutlined,
|
|
||||||
ClockCircleOutlined,
|
|
||||||
MessageOutlined
|
|
||||||
} from '@ant-design/icons-vue'
|
|
||||||
import type { ApprovalInstance, ApprovalScenario, ApprovalInstanceStatus } from '@/types'
|
import type { ApprovalInstance, ApprovalScenario, ApprovalInstanceStatus } from '@/types'
|
||||||
import { ApprovalScenarioMap, ApprovalInstanceStatusMap, ApprovalInstanceStatusBadgeMap } from '@/types'
|
import { ApprovalScenarioMap, ApprovalInstanceStatusMap, ApprovalInstanceStatusBadgeMap } from '@/types'
|
||||||
import { getApprovalInstancePage } from '@/api/system/approval'
|
import { getApprovalInstancePage } from '@/api/system/approval'
|
||||||
|
|||||||
@@ -123,16 +123,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import type { FormInstance } from 'ant-design-vue'
|
import type { FormInstance } from 'ant-design-vue'
|
||||||
import {
|
import { getDeptTree, createDept, updateDept, deleteDept } from '@/api/system/dept'
|
||||||
PlusOutlined,
|
import type { DeptRecord, DeptFormData } from '@/types/system/dept'
|
||||||
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 loading = ref(false)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
|
|||||||
@@ -79,22 +79,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { PlusOutlined, SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
|
||||||
import DictFormModal from '@/components/system/dict/DictFormModal.vue'
|
import DictFormModal from '@/components/system/dict/DictFormModal.vue'
|
||||||
import DictItemDrawer from '@/components/system/dict/DictItemDrawer.vue'
|
import DictItemDrawer from '@/components/system/dict/DictItemDrawer.vue'
|
||||||
import { getDictPage, deleteDict } from '@/api/system/dict'
|
import { getDictPage, deleteDict } from '@/api/system/dict'
|
||||||
|
import type { DictRecord } from '@/types/system/dict'
|
||||||
// 类型定义
|
|
||||||
interface DictRecord {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
code: string
|
|
||||||
remark?: string
|
|
||||||
status: number
|
|
||||||
createdAt?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索表单
|
// 搜索表单
|
||||||
const searchForm = reactive({
|
const searchForm = reactive({
|
||||||
|
|||||||
@@ -72,9 +72,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
|
||||||
import type { MenuRecord, MenuFormData } from '@/types/system/menu'
|
import type { MenuRecord, MenuFormData } from '@/types/system/menu'
|
||||||
import { getMenuList, deleteMenu } from '@/api/system/menu'
|
import { getMenuList, deleteMenu } from '@/api/system/menu'
|
||||||
import { getAllProjects, type ProjectInfo } from '@/api/project'
|
import { getAllProjects, type ProjectInfo } from '@/api/project'
|
||||||
|
|||||||
@@ -75,9 +75,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
|
||||||
|
|
||||||
import { buildMenuTree } from '@/utils/route'
|
import { buildMenuTree } from '@/utils/route'
|
||||||
|
|
||||||
|
|||||||
@@ -106,14 +106,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import {
|
|
||||||
SearchOutlined,
|
|
||||||
ReloadOutlined,
|
|
||||||
PlusOutlined,
|
|
||||||
UserOutlined
|
|
||||||
} from '@ant-design/icons-vue'
|
|
||||||
|
|
||||||
import { getUserList, deleteUser, updateUserStatus } from '@/api/system/user'
|
import { getUserList, deleteUser, updateUserStatus } from '@/api/system/user'
|
||||||
import { getRoleList } from '@/api/system/role'
|
import { getRoleList } from '@/api/system/role'
|
||||||
|
|||||||
@@ -17,5 +17,5 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "src/auto-imports.d.ts", "src/components.d.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ export default defineConfig({
|
|||||||
resolvers: [
|
resolvers: [
|
||||||
// Ant Design Vue组件自动导入
|
// Ant Design Vue组件自动导入
|
||||||
AntDesignVueResolver({
|
AntDesignVueResolver({
|
||||||
importStyle: false
|
importStyle: false,
|
||||||
|
resolveIcons: true
|
||||||
}),
|
}),
|
||||||
// Ant Design Vue图标自动导入
|
// Ant Design Vue图标自动导入
|
||||||
IconsResolver({
|
IconsResolver({
|
||||||
|
|||||||
Reference in New Issue
Block a user