前端项目关键重构规范
This commit is contained in:
3
.env
3
.env
@@ -2,4 +2,5 @@
|
||||
VITE_APP_TITLE=楠溪屿后台管理系统
|
||||
|
||||
# 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=楠溪屿后台管理系统
|
||||
|
||||
# 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 {
|
||||
DatabaseType,
|
||||
DatabaseInfo,
|
||||
DatabaseItem,
|
||||
CreateDatabaseRequest,
|
||||
UpdateDatabaseRequest,
|
||||
DatabaseConnectionInfo,
|
||||
DatabaseListParams,
|
||||
DatabaseListResponse
|
||||
} from '@/types/database'
|
||||
/**
|
||||
* 数据库管理 API
|
||||
* 通过1Panel管理数据库
|
||||
*/
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
// 获取数据库信息
|
||||
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
|
||||
}
|
||||
|
||||
// 获取连接信息
|
||||
export function getDatabaseConnectionInfo(type: DatabaseType, id: number): Promise<DatabaseConnectionInfo> {
|
||||
return request.get(`/api/database/${type}/${id}/connection`)
|
||||
// ==================== API 方法 ====================
|
||||
|
||||
/**
|
||||
* 检查应用安装状态(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,
|
||||
DatabaseItem,
|
||||
CreateDatabaseRequest,
|
||||
UpdateDatabaseRequest,
|
||||
DatabaseConnectionInfo,
|
||||
DatabaseListParams,
|
||||
DatabaseListResponse
|
||||
/**
|
||||
* 查询数据库列表
|
||||
*/
|
||||
export async function searchDatabases(
|
||||
serverId: number,
|
||||
database: string,
|
||||
page: number = 1,
|
||||
pageSize: number = 20
|
||||
): Promise<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
|
||||
lastDeployTime?: string
|
||||
lastDeployMessage?: string
|
||||
// 运行环境关联
|
||||
runtimeId?: number
|
||||
runtimeServerId?: number
|
||||
runtimeName?: string
|
||||
runtimeType?: string
|
||||
runtimeDeployStatus?: DeployStatus
|
||||
// 时间
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
@@ -135,6 +141,13 @@ export async function deployDomain(data: DomainDeployRequest): Promise<DomainDep
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除部署(从 1Panel 删除网站)
|
||||
*/
|
||||
export async function undeployDomain(id: number) {
|
||||
await request.post<void>(`/platform/domain/undeploy/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查域名 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}`)
|
||||
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
|
||||
}
|
||||
|
||||
// ==================== 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'
|
||||
|
||||
// 部门类型定义
|
||||
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
|
||||
}
|
||||
import type { DeptRecord, DeptFormData } from '@/types/system/dept'
|
||||
export type { DeptRecord, DeptFormData }
|
||||
|
||||
/**
|
||||
* 获取部门树
|
||||
|
||||
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']
|
||||
AFormItemRest: typeof import('ant-design-vue/es')['FormItemRest']
|
||||
AInput: typeof import('ant-design-vue/es')['Input']
|
||||
AInputGroup: typeof import('ant-design-vue/es')['InputGroup']
|
||||
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
|
||||
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
||||
AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
|
||||
@@ -40,6 +41,9 @@ declare module 'vue' {
|
||||
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
|
||||
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
|
||||
ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
|
||||
AList: typeof import('ant-design-vue/es')['List']
|
||||
AListItem: typeof import('ant-design-vue/es')['ListItem']
|
||||
AListItemMeta: typeof import('ant-design-vue/es')['ListItemMeta']
|
||||
AMenu: typeof import('ant-design-vue/es')['Menu']
|
||||
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
|
||||
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
|
||||
@@ -47,9 +51,11 @@ declare module 'vue' {
|
||||
AModal: typeof import('ant-design-vue/es')['Modal']
|
||||
APageHeader: typeof import('ant-design-vue/es')['PageHeader']
|
||||
APagination: typeof import('ant-design-vue/es')['Pagination']
|
||||
ApartmentOutlined: typeof import('@ant-design/icons-vue')['ApartmentOutlined']
|
||||
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
||||
APopover: typeof import('ant-design-vue/es')['Popover']
|
||||
ApprovalDrawer: typeof import('./components/ApprovalDrawer/index.vue')['default']
|
||||
AppstoreOutlined: typeof import('@ant-design/icons-vue')['AppstoreOutlined']
|
||||
AProgress: typeof import('ant-design-vue/es')['Progress']
|
||||
ARadio: typeof import('ant-design-vue/es')['Radio']
|
||||
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
||||
@@ -57,7 +63,10 @@ declare module 'vue' {
|
||||
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
|
||||
AResult: typeof import('ant-design-vue/es')['Result']
|
||||
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']
|
||||
ASelectOptGroup: typeof import('ant-design-vue/es')['SelectOptGroup']
|
||||
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
||||
ASpace: typeof import('ant-design-vue/es')['Space']
|
||||
ASpin: typeof import('ant-design-vue/es')['Spin']
|
||||
@@ -76,23 +85,73 @@ declare module 'vue' {
|
||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||
ATree: typeof import('ant-design-vue/es')['Tree']
|
||||
ATreeSelect: typeof import('ant-design-vue/es')['TreeSelect']
|
||||
AUpload: typeof import('ant-design-vue/es')['Upload']
|
||||
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
|
||||
BudgetDetailModal: typeof import('./components/finance/budget/BudgetDetailModal.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']
|
||||
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']
|
||||
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']
|
||||
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']
|
||||
HistoryOutlined: typeof import('@ant-design/icons-vue')['HistoryOutlined']
|
||||
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']
|
||||
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']
|
||||
ReloadOutlined: typeof import('@ant-design/icons-vue')['ReloadOutlined']
|
||||
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']
|
||||
RollbackOutlined: typeof import('@ant-design/icons-vue')['RollbackOutlined']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
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']
|
||||
UploadOutlined: typeof import('@ant-design/icons-vue')['UploadOutlined']
|
||||
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)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
for (const [menuKey, menuPath] of Object.entries(subMenuRouteMap)) {
|
||||
const relativePath = menuPath.replace(`/app/${projectId}`, '')
|
||||
if (subPath === relativePath || subPath.startsWith(relativePath + '/')) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
const service: AxiosInstance = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||
timeout: 30000,
|
||||
timeout: 5 * 60 * 1000, // 5分钟超时,适合大文件上传
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
@@ -262,15 +262,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from '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 {
|
||||
getCertificateList,
|
||||
@@ -287,27 +279,8 @@ import {
|
||||
type WebsiteInfo
|
||||
} from '@/api/certificate'
|
||||
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 = [
|
||||
|
||||
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>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from '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 { mockEnvironments, mockServers } from '@/mock/platform'
|
||||
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>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
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 { mockLogs } from '@/mock/platform'
|
||||
import { mockProjects } from '@/mock/projects'
|
||||
|
||||
@@ -148,26 +148,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import type { MenuItem } from '@/config'
|
||||
import type { PlatformProject, MenuNode } from '@/types/platform/menus'
|
||||
|
||||
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 selectedMenuKeys = ref<string[]>([])
|
||||
const isEditing = ref(false)
|
||||
|
||||
@@ -56,7 +56,13 @@
|
||||
show-zero
|
||||
: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-badge>
|
||||
@@ -71,8 +77,7 @@
|
||||
v-if="getDeployStatus((record as any).id).deployed"
|
||||
type="default"
|
||||
size="small"
|
||||
style="color: #52c41a; border-color: #52c41a;"
|
||||
@click="handleDeploy(record as any)"
|
||||
style="color: #52c41a; border-color: #52c41a; cursor: default;"
|
||||
>
|
||||
<CheckCircleOutlined /> 已部署
|
||||
</a-button>
|
||||
@@ -462,20 +467,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
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 { useProjectStore } from '@/stores'
|
||||
import ProjectUpload from '@/components/ProjectUpload.vue'
|
||||
@@ -491,9 +483,13 @@ import {
|
||||
getServerAcmeAccounts,
|
||||
getServerDnsAccounts,
|
||||
checkDeployStatus,
|
||||
getNginxConfig,
|
||||
saveNginxConfig,
|
||||
reloadNginx,
|
||||
type AcmeAccount,
|
||||
type DnsAccount,
|
||||
type DeployResult
|
||||
type DeployResult,
|
||||
type NginxConfigResult
|
||||
} from '@/api/project'
|
||||
import { getServerList } from '@/api/server'
|
||||
import { getDomainList } from '@/api/domain'
|
||||
@@ -672,30 +668,6 @@ const versionList = computed(() => {
|
||||
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({
|
||||
id: '', // 对应后端 ID (Update用)
|
||||
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) {
|
||||
if (!record.enabled) {
|
||||
function handleEnterProject(record: any) {
|
||||
if (record.status !== 'active') {
|
||||
message.warning('项目已禁用,无法进入')
|
||||
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('项目未配置访问地址,无法进入')
|
||||
return
|
||||
}
|
||||
projectStore.switchProject(record.id)
|
||||
router.push(`/app/${record.id}/dashboard`)
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
@@ -1077,45 +1045,6 @@ function getServerName(serverId?: string): string {
|
||||
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
|
||||
deploySteps.value = []
|
||||
deployCurrentStep.value = 0
|
||||
|
||||
// 加载账户数据 - 已移除,改用后端自动匹配已绑定证书
|
||||
// await loadPanelAccounts()
|
||||
|
||||
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-item label="状态">
|
||||
<a-select v-model:value="filterStatus" placeholder="全部" style="width: 120px" allow-clear>
|
||||
<a-select-option value="online">在线</a-select-option>
|
||||
<a-select-option value="offline">离线</a-select-option>
|
||||
<a-select-option value="warning">告警</a-select-option>
|
||||
<a-select-option value="maintenance">维护中</a-select-option>
|
||||
<a-select-option v-for="item in serverStatusDict" :key="item.value" :value="item.value">
|
||||
{{ item.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="类型">
|
||||
<a-select v-model:value="filterType" placeholder="全部" style="width: 120px" allow-clear>
|
||||
<a-select-option value="cloud">云服务器</a-select-option>
|
||||
<a-select-option value="physical">物理机</a-select-option>
|
||||
<a-select-option value="virtual">虚拟机</a-select-option>
|
||||
<a-select-option v-for="item in serverTypeDict" :key="item.value" :value="item.value">
|
||||
{{ item.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="关键字">
|
||||
@@ -195,9 +194,9 @@
|
||||
|
||||
<a-form-item label="服务器类型" name="type">
|
||||
<a-select v-model:value="formData.type" placeholder="请选择">
|
||||
<a-select-option value="cloud">云服务器</a-select-option>
|
||||
<a-select-option value="physical">物理机</a-select-option>
|
||||
<a-select-option value="virtual">虚拟机</a-select-option>
|
||||
<a-select-option v-for="item in serverTypeDict" :key="item.value" :value="item.value">
|
||||
{{ item.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
@@ -225,10 +224,9 @@
|
||||
|
||||
<a-form-item label="标签" name="tags">
|
||||
<a-select v-model:value="formData.tags" mode="tags" placeholder="输入标签后回车">
|
||||
<a-select-option value="生产">生产</a-select-option>
|
||||
<a-select-option value="测试">测试</a-select-option>
|
||||
<a-select-option value="开发">开发</a-select-option>
|
||||
<a-select-option value="备份">备份</a-select-option>
|
||||
<a-select-option v-for="item in serverTagDict" :key="item.value" :value="item.value">
|
||||
{{ item.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
@@ -347,21 +345,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
EyeOutlined,
|
||||
LinkOutlined,
|
||||
CloudServerOutlined,
|
||||
DesktopOutlined,
|
||||
ClusterOutlined,
|
||||
ReloadOutlined,
|
||||
FileTextOutlined,
|
||||
GlobalOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import {
|
||||
getServerList,
|
||||
getServerStatus,
|
||||
@@ -369,11 +353,10 @@ import {
|
||||
updateServer,
|
||||
deleteServer,
|
||||
refreshServerStatus,
|
||||
type ServerBase,
|
||||
type ServerInfo,
|
||||
type ServerStatus,
|
||||
type ServerType
|
||||
} from '@/api/server'
|
||||
import type { MonitorData, ServerStatus, ServerType, ServerInfo } from '@/types/platform/servers'
|
||||
|
||||
import { getDictItemsByCode } from '@/api/system/dict'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -387,8 +370,13 @@ const filterStatus = ref<ServerStatus | undefined>()
|
||||
const filterType = ref<ServerType | undefined>()
|
||||
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
|
||||
name: string
|
||||
ip: string
|
||||
@@ -410,6 +398,7 @@ interface ServerDisplayInfo extends Partial<ServerInfo> {
|
||||
disk: { total: number; used: number; usage: number }
|
||||
// 状态加载标记
|
||||
statusLoading?: boolean
|
||||
monitor?: MonitorData
|
||||
}
|
||||
|
||||
const servers = ref<ServerDisplayInfo[]>([])
|
||||
@@ -458,9 +447,25 @@ const stats = computed(() => {
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadDictionaries()
|
||||
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) } })
|
||||
}
|
||||
|
||||
// 字典辅助函数
|
||||
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 {
|
||||
const colors: Record<ServerStatus, string> = {
|
||||
online: 'green',
|
||||
offline: 'default',
|
||||
warning: 'orange',
|
||||
maintenance: 'blue'
|
||||
}
|
||||
return colors[status]
|
||||
return getDictColor(serverStatusDict.value, status)
|
||||
}
|
||||
|
||||
function getStatusText(status: ServerStatus): string {
|
||||
const texts: Record<ServerStatus, string> = {
|
||||
online: '在线',
|
||||
offline: '离线',
|
||||
warning: '告警',
|
||||
maintenance: '维护中'
|
||||
}
|
||||
return texts[status]
|
||||
return getDictLabel(serverStatusDict.value, status)
|
||||
}
|
||||
|
||||
function getTypeText(type: ServerType): string {
|
||||
const texts: Record<ServerType, string> = {
|
||||
cloud: '云服务器',
|
||||
physical: '物理机',
|
||||
virtual: '虚拟机'
|
||||
}
|
||||
return texts[type]
|
||||
return getDictLabel(serverTypeDict.value, type)
|
||||
}
|
||||
|
||||
function getUsageColor(usage: number): string {
|
||||
@@ -726,16 +725,7 @@ function getUsageColor(usage: number): string {
|
||||
}
|
||||
|
||||
function getTagColor(tag: string): string {
|
||||
const colors: Record<string, string> = {
|
||||
'生产': 'red',
|
||||
'测试': 'blue',
|
||||
'开发': 'green',
|
||||
'备份': 'purple',
|
||||
'主节点': 'gold',
|
||||
'从节点': 'cyan',
|
||||
'离线': 'default'
|
||||
}
|
||||
return colors[tag] || 'default'
|
||||
return getDictColor(serverTagDict.value, tag)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -105,18 +105,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
CloseOutlined,
|
||||
UploadOutlined,
|
||||
FolderOpenOutlined,
|
||||
CloudUploadOutlined,
|
||||
FileOutlined,
|
||||
CheckOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -128,17 +117,7 @@ const folderInputRef = ref<HTMLInputElement | null>(null)
|
||||
// 拖拽状态
|
||||
const isDragover = ref(false)
|
||||
|
||||
// 文件列表
|
||||
interface UploadFile {
|
||||
uid: string
|
||||
name: string
|
||||
path: string // 相对路径
|
||||
size: number
|
||||
type: string
|
||||
status: 'pending' | 'uploading' | 'done' | 'error'
|
||||
percent: number
|
||||
file: File
|
||||
}
|
||||
import type { UploadFile } from '@/types/platform/upload-file'
|
||||
|
||||
const fileList = ref<UploadFile[]>([])
|
||||
|
||||
|
||||
@@ -203,21 +203,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } 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 { ApprovalScenarioMap } from '@/types'
|
||||
import {
|
||||
|
||||
@@ -191,16 +191,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
EyeOutlined,
|
||||
ClockCircleOutlined,
|
||||
MessageOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import type { ApprovalInstance, ApprovalScenario, ApprovalInstanceStatus } from '@/types'
|
||||
import { ApprovalScenarioMap, ApprovalInstanceStatusMap, ApprovalInstanceStatusBadgeMap } from '@/types'
|
||||
import { getApprovalInstancePage } from '@/api/system/approval'
|
||||
|
||||
@@ -123,16 +123,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
ApartmentOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { getDeptTree, createDept, updateDept, deleteDept, type DeptRecord, type DeptFormData } from '@/api/system/dept'
|
||||
import { getDeptTree, createDept, updateDept, deleteDept } from '@/api/system/dept'
|
||||
import type { DeptRecord, DeptFormData } from '@/types/system/dept'
|
||||
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
|
||||
@@ -79,22 +79,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from '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 DictItemDrawer from '@/components/system/dict/DictItemDrawer.vue'
|
||||
import { getDictPage, deleteDict } from '@/api/system/dict'
|
||||
|
||||
// 类型定义
|
||||
interface DictRecord {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
remark?: string
|
||||
status: number
|
||||
createdAt?: string
|
||||
}
|
||||
import type { DictRecord } from '@/types/system/dict'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
|
||||
@@ -72,9 +72,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import type { MenuRecord, MenuFormData } from '@/types/system/menu'
|
||||
import { getMenuList, deleteMenu } from '@/api/system/menu'
|
||||
import { getAllProjects, type ProjectInfo } from '@/api/project'
|
||||
|
||||
@@ -75,9 +75,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
import { buildMenuTree } from '@/utils/route'
|
||||
|
||||
|
||||
@@ -106,14 +106,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from '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 { getRoleList } from '@/api/system/role'
|
||||
|
||||
@@ -17,5 +17,5 @@
|
||||
"noFallthroughCasesInSwitch": 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: [
|
||||
// Ant Design Vue组件自动导入
|
||||
AntDesignVueResolver({
|
||||
importStyle: false
|
||||
importStyle: false,
|
||||
resolveIcons: true
|
||||
}),
|
||||
// Ant Design Vue图标自动导入
|
||||
IconsResolver({
|
||||
|
||||
Reference in New Issue
Block a user