diff --git a/.env b/.env index a7c3355..c56e29b 100644 --- a/.env +++ b/.env @@ -2,7 +2,4 @@ VITE_APP_TITLE=楠溪屿后台管理系统 # API基础URL -VITE_API_BASE_URL=/api - -# 1Panel API 配置 -VITE_1PANEL_API_KEY=KBya4QxxPB3b8eYCSECOfhgWpXE0ZhuI +VITE_API_BASE_URL=https://api.superwax.cn:4433/api diff --git a/.env.production b/.env.production index b8cb314..725a2c1 100644 --- a/.env.production +++ b/.env.production @@ -2,5 +2,5 @@ VITE_APP_TITLE=楠溪屿后台管理系统 # API基础URL - 生产环境 -VITE_API_BASE_URL=/api +VITE_API_BASE_URL=https://api.superwax.cn:4433/api diff --git a/src/api/1panel.ts b/src/api/1panel.ts deleted file mode 100644 index ab08904..0000000 --- a/src/api/1panel.ts +++ /dev/null @@ -1,1122 +0,0 @@ -/** - * 1Panel API 接口 - * - * 1Panel API 认证说明: - * - Token = md5('1panel' + API-Key + UnixTimestamp) - * - 请求头需要携带: - * - 1Panel-Token: 生成的Token值 - * - 1Panel-Timestamp: 当前时间戳(秒级) - */ -import axios, { type AxiosInstance } from 'axios' -import { message } from 'ant-design-vue' -import SparkMD5 from 'spark-md5' - -// 1Panel API 配置(可动态修改) -const PANEL_CONFIG = { - // 服务器ID(用于代理路径) - serverId: 'server1', - // 服务器地址(不含协议和路径) - serverAddress: '47.109.57.58:42588', - // 完整的 baseURL(开发环境使用代理路径,生产环境直接请求) - baseURL: import.meta.env.DEV ? '/1panel-api/server1' : 'http://47.109.57.58:42588/api/v2', - // API密钥 - 可动态切换 - apiKey: import.meta.env.VITE_1PANEL_API_KEY || '' -} - -/** - * 服务器配置接口 - */ -export interface PanelServerConfig { - id: string // 服务器ID,用于代理路径 - address: string // 服务器地址 - apiKey: string // API密钥 -} - -/** - * 切换 1Panel 服务器配置 - * @param config 服务器配置 - */ -export function setPanelServer(config: PanelServerConfig): void { - PANEL_CONFIG.serverId = config.id - PANEL_CONFIG.serverAddress = config.address - PANEL_CONFIG.apiKey = config.apiKey - - // 开发环境使用代理路径,生产环境直接请求 - if (import.meta.env.DEV) { - PANEL_CONFIG.baseURL = `/1panel-api/${config.id}` - } else { - PANEL_CONFIG.baseURL = `http://${config.address}/api/v2` - } - - // 更新 axios 实例的 baseURL - panelService.defaults.baseURL = PANEL_CONFIG.baseURL - - console.log('[1Panel API] 服务器配置已更新:', { - serverId: PANEL_CONFIG.serverId, - serverAddress: PANEL_CONFIG.serverAddress, - baseURL: PANEL_CONFIG.baseURL, - apiKeyConfigured: !!PANEL_CONFIG.apiKey, - isDev: import.meta.env.DEV - }) -} - -/** - * 获取当前 1Panel 服务器配置 - */ -export function getPanelConfig() { - return { - serverId: PANEL_CONFIG.serverId, - serverAddress: PANEL_CONFIG.serverAddress, - baseURL: PANEL_CONFIG.baseURL, - apiKeyConfigured: !!PANEL_CONFIG.apiKey - } -} - -/** - * 生成 MD5 哈希值 - * @param str 要加密的字符串 - * @returns MD5哈希值 - */ -function md5(str: string): string { - return SparkMD5.hash(str) -} - -/** - * 生成 1Panel Token - * @param timestamp 时间戳(秒级) - * @returns Token字符串 - */ -function generateToken(timestamp: number): string { - const tokenString = `1panel${PANEL_CONFIG.apiKey}${timestamp}` - return md5(tokenString) -} - -/** - * 获取当前时间戳(秒级) - * @returns 时间戳 - */ -function getTimestamp(): number { - return Math.floor(Date.now() / 1000) -} - -// 创建 1Panel 专用的 axios 实例 -const panelService: AxiosInstance = axios.create({ - baseURL: PANEL_CONFIG.baseURL, - timeout: 30000, - headers: { - 'Content-Type': 'application/json' - } -}) - -// 请求拦截器 - 添加 1Panel 认证头 -panelService.interceptors.request.use( - (config) => { - const timestamp = getTimestamp() - const token = generateToken(timestamp) - - // 调试日志 - console.log('[1Panel API] 请求调试信息:', { - url: config.url, - apiKeyConfigured: !!PANEL_CONFIG.apiKey, - apiKeyLength: PANEL_CONFIG.apiKey?.length || 0, - timestamp, - tokenPreview: token.substring(0, 8) + '...' - }) - - // 添加 1Panel 认证头 - config.headers['1Panel-Token'] = token - config.headers['1Panel-Timestamp'] = String(timestamp) - - return config - }, - (error) => { - console.error('1Panel 请求错误:', error) - return Promise.reject(error) - } -) - -// 响应拦截器 -panelService.interceptors.response.use( - (response) => { - return response - }, - (error) => { - console.error('1Panel 响应错误:', error) - - let errorMessage = '1Panel API 请求失败' - if (error.response) { - const { status, data } = error.response - switch (status) { - case 401: - errorMessage = 'API 接口密钥错误或已过期' - break - case 403: - errorMessage = '拒绝访问,请检查权限配置' - break - case 404: - errorMessage = '接口不存在' - break - case 500: - errorMessage = '1Panel 服务器错误' - break - default: - errorMessage = data?.message || `请求失败(${status})` - } - } else if (error.code === 'ECONNABORTED') { - errorMessage = '请求超时' - } else if (error.code === 'ERR_NETWORK') { - errorMessage = '网络连接失败,请检查服务器地址' - } - - message.error(errorMessage) - return Promise.reject(error) - } -) - -// ==================== API 接口定义 ==================== - -/** - * 网站信息类型 - */ -export interface Website { - id: number - primaryDomain: string - alias: string - type: string - remark: string - status: string - expireDate: string - sitePath: string - appName: string - runtimeName: string - sslExpireDate: string - sslStatus: string - protocol: string - createdAt: string - updatedAt: string -} - -/** - * 网站列表查询参数 - */ -export interface WebsiteSearchParams { - page: number - pageSize: number - name?: string - websiteGroupId?: number - orderBy?: string - order?: string -} - -/** - * 分页响应 - */ -export interface PageResponse { - items: T[] - total: number -} - -/** - * 1Panel 标准响应 - */ -export interface PanelResponse { - code: number - message: string - data: T -} - -/** - * 获取网站列表(搜索接口) - * @param params 查询参数 - */ -export async function searchWebsites(params: WebsiteSearchParams): Promise> { - const response = await panelService.post>>('/websites/search', { - ...params, - name: params.name || '', - orderBy: params.orderBy || 'created_at', - order: params.order || 'descending' - }) - return response.data.data -} - -/** - * 获取所有网站列表(简化调用,用于下拉选择) - * 使用 /websites/list 接口,无需参数 - */ -export async function getAllWebsites(): Promise { - const response = await panelService.get>('/websites/list') - return response.data.data || [] -} - -// ==================== 证书相关 API ==================== - -/** - * SSL证书信息类型 - */ -export interface SSLCertificate { - id: number - primaryDomain: string - domains: string - type: string - provider: string - organization: string - status: string - startDate: string - expireDate: string - autoRenew: boolean - acmeAccountId: number - dnsAccountId: number - description: string - createdAt: string - updatedAt: string -} - -/** - * 证书列表查询参数 - */ -export interface SSLSearchParams { - page: number - pageSize: number - info?: string -} - -/** - * 获取证书列表 - * @param params 查询参数 - */ -export async function getSSLCertificates(params: SSLSearchParams): Promise> { - const response = await panelService.post>>('/websites/ssl/search', { - ...params, - orderBy: 'created_at', - order: 'descending' - }) - return response.data.data -} - -/** - * 根据 Acme 账户ID 获取证书列表 - * @param acmeAccountID Acme账户ID - */ -export async function getSSLCertificatesByAcmeAccount(acmeAccountID: number | string): Promise { - const response = await panelService.post>>('/websites/ssl/search', { - page: 1, - pageSize: 100, - acmeAccountID: String(acmeAccountID), - orderBy: 'created_at', - order: 'descending' - }) - return response.data.data?.items || [] -} - -/** - * 根据域名查找匹配的证书 - * @param domain 域名 - * @param acmeAccountID Acme账户ID - * @returns 匹配的证书或 null - */ -export async function findCertificateByDomain(domain: string, acmeAccountID: number | string = 1): Promise { - const certificates = await getSSLCertificatesByAcmeAccount(acmeAccountID) - - // 查找主域名或其他域名匹配的证书 - const matchedCert = certificates.find(cert => { - // 主域名匹配 - if (cert.primaryDomain === domain) return true - // 检查其他域名 - if (cert.domains) { - const otherDomains = cert.domains.split(',').map(d => d.trim()) - if (otherDomains.includes(domain)) return true - } - return false - }) - - return matchedCert || null -} - -/** - * Acme账户类型 - */ -export interface AcmeAccount { - id: number - email: string - url: string - type: string - createdAt: string -} - -/** - * 获取 ACME 账户列表 - */ -export async function getAcmeAccounts(): Promise> { - const response = await panelService.post>>('/websites/acme/search', { - page: 1, - pageSize: 100, - orderBy: 'created_at', - order: 'descending' - }) - return response.data.data -} - -/** - * DNS账户类型 - */ -export interface DnsAccount { - id: number - name: string - type: string - authorization: string - createdAt: string -} - -/** - * 获取 DNS 账户列表 - */ -export async function getDnsAccounts(): Promise> { - const response = await panelService.post>>('/websites/dns/search', { - page: 1, - pageSize: 100, - orderBy: 'created_at', - order: 'descending' - }) - return response.data.data -} - -/** - * 申请证书参数 - */ -export interface ApplySSLParams { - primaryDomain: string - otherDomains: string - provider: string - acmeAccountId: number - dnsAccountId?: number - autoRenew: boolean - keyType: string - apply: boolean - pushDir?: boolean - dir?: string - id?: number - websiteId?: number - execShell?: boolean - shell?: string - skipDNS?: boolean - disableCNAME?: boolean - nameserverId?: number -} - -/** - * 申请SSL证书 - * @param params 申请参数 - */ -export async function applySSLCertificate(params: ApplySSLParams): Promise { - await panelService.post('/websites/ssl', params) -} - -/** - * 删除SSL证书 - * @param id 证书ID - */ -export async function deleteSSLCertificate(id: number): Promise { - await panelService.post('/websites/ssl/del', { id }) -} - -/** - * 获取证书详情 - * @param id 证书ID - */ -export async function getSSLCertificateDetail(id: number): Promise { - const response = await panelService.get>(`/websites/ssl/${id}`) - return response.data.data -} - -/** - * 更新证书设置 - */ -export interface UpdateSSLParams { - id: number - autoRenew: boolean - description?: string - execShell?: boolean - shell?: string -} - -/** - * 更新证书设置 - * @param params 更新参数 - */ -export async function updateSSLCertificate(params: UpdateSSLParams): Promise { - await panelService.post('/websites/ssl/update', params) -} - -// ==================== 服务器监控 API ==================== - -/** - * 服务器基础信息 - */ -export interface ServerBaseInfo { - hostname: string - os: string - platform: string - platformFamily: string - platformVersion: string - kernelArch: string - kernelVersion: string - virtualizationSystem: string -} - -/** - * CPU 信息 - */ -export interface CPUInfo { - cpuModelName: string - cpuCores: number - cpuLogicalCores: number - cpuPercent: number[] -} - -/** - * 内存信息 - */ -export interface MemoryInfo { - total: number - available: number - used: number - usedPercent: number -} - -/** - * 磁盘信息 - */ -export interface DiskInfo { - path: string - device: string - fstype: string - total: number - free: number - used: number - usedPercent: number - inodesTotal: number - inodesUsed: number - inodesFree: number - inodesUsedPercent: number -} - -/** - * 系统资源使用状态 - */ -export interface SystemResourceInfo { - cpu: number - cpuTotal: number - memory: number - memoryTotal: number - memoryUsed: number - load1: number - load5: number - load15: number - uptime: number - timeSinceUptime: string - procs: number - disk: DiskInfo[] - diskTotal: number - diskUsed: number - net: { - name: string - bytesRecv: number - bytesSent: number - }[] - shotTime: string -} - -/** - * 获取服务器基础信息 - */ -export async function getServerBaseInfo(): Promise { - const response = await panelService.get>('/hosts/info') - return response.data.data -} - -/** - * 获取系统资源使用状态 - */ -export async function getSystemResourceInfo(): Promise { - const response = await panelService.get>('/dashboard/current') - return response.data.data -} - -/** - * 获取系统实时状态 - */ -export async function getRealtimeStatus(): Promise { - const response = await panelService.post>('/dashboard/current', { - ioOption: 'all', - netOption: 'all' - }) - return response.data.data -} - -// ==================== Nginx 配置 API ==================== - -/** - * Nginx 配置信息 - */ -export interface NginxConfig { - id: number - websiteId: number - operate: string - params?: Record -} - -/** - * 网站配置详情 - */ -export interface WebsiteConfig { - config: string -} - -/** - * 获取网站 Nginx 配置 - * @param websiteId 网站ID - */ -export async function getWebsiteNginxConfig(websiteId: number): Promise { - const response = await panelService.get>(`/websites/${websiteId}/config/nginx`) - return response.data.data.config -} - -/** - * 更新网站 Nginx 配置 - * @param websiteId 网站ID - * @param config 配置内容 - */ -export async function updateWebsiteNginxConfig(websiteId: number, config: string): Promise { - await panelService.post(`/websites/${websiteId}/config/nginx`, { config }) -} - -/** - * 重载 Nginx - */ -export async function reloadNginx(): Promise { - await panelService.post('/websites/nginx/update', { operate: 'reload' }) -} - -// ==================== 应用管理 API ==================== - -/** - * 已安装应用信息 - */ -export interface InstalledApp { - id: number - name: string - appKey: string - version: string - status: string - message: string - httpPort: number - httpsPort: number - path: string - canUpdate: boolean - createdAt: string - updatedAt: string -} - -/** - * 获取已安装应用列表 - */ -export async function getInstalledApps(): Promise { - const response = await panelService.post>('/apps/installed/search', { - page: 1, - pageSize: 100, - all: true - }) - return response.data.data.items || [] -} - -/** - * 操作应用 - * @param installId 安装ID - * @param operate 操作类型:start, stop, restart - */ -export async function operateApp(installId: number, operate: 'start' | 'stop' | 'restart'): Promise { - await panelService.post('/apps/installed/operate', { - installId, - operate - }) -} - -// ==================== 文件管理 API ==================== - -/** - * 文件信息 - */ -export interface FileInfo { - name: string - size: number - mode: string - modeNum: string - isDir: boolean - isSymlink: boolean - linkPath: string - path: string - extension: string - user: string - group: string - uid: string - gid: string - modTime: string -} - -/** - * 获取目录文件列表 - * @param path 目录路径 - */ -export async function getFileList(path: string): Promise { - const response = await panelService.post>('/files/search', { - path, - expand: true, - page: 1, - pageSize: 100 - }) - return response.data.data || [] -} - -/** - * 上传文件 - * @param path 目标路径 - * @param file 文件 - */ -export async function uploadFile(path: string, file: File): Promise { - const formData = new FormData() - formData.append('file', file) - formData.append('path', path) - - await panelService.post('/files/upload', formData, { - headers: { - 'Content-Type': 'multipart/form-data' - } - }) -} - -/** - * 解压文件 - * @param path 文件路径 - * @param dst 解压目标路径 - */ -export async function decompressFile(path: string, dst: string): Promise { - await panelService.post('/files/decompress', { - path, - dst, - type: path.endsWith('.tar.gz') ? 'tar.gz' : 'zip' - }) -} - -// ==================== 网站管理 API ==================== - -/** - * 创建网站参数 - */ -export interface CreateWebsiteParams { - primaryDomain: string // 主域名 - alias: string // 网站别名(唯一标识) - type: 'static' | 'proxy' // 网站类型:static=静态站点, proxy=反向代理 - remark?: string // 备注 - - // 静态站点配置 - siteDir?: string // 站点目录(静态站点时使用) - - // 反向代理配置 - proxy?: string // 反向代理地址(如 http://127.0.0.1:3000) - proxyType?: string // 代理类型 - - // 其他配置 - otherDomains?: string // 其他域名,逗号分隔 - IPV6?: boolean // 是否启用 IPv6 - - // 运行时配置(静态站点) - runtimeID?: number // 运行时 ID (OpenResty) - runtimeType?: string // 运行时类型 - - // 额外配置 - appType?: string // 应用类型 - webSiteGroupID?: number // 网站分组ID - port?: number // 端口 -} - -/** - * 创建网站 - * @param params 创建参数 - */ -export async function createWebsite(params: CreateWebsiteParams): Promise { - const response = await panelService.post>('/websites', { - primaryDomain: params.primaryDomain, - alias: params.alias, - type: params.type || 'static', - remark: params.remark || '', - siteDir: params.siteDir || '', - proxy: params.proxy || '', - proxyType: params.proxyType || 'http', - otherDomains: params.otherDomains || '', - IPV6: params.IPV6 || false, - runtimeID: params.runtimeID || 0, - runtimeType: params.runtimeType || '', - appType: params.appType || 'new', - webSiteGroupID: params.webSiteGroupID || 0, - port: params.port || 80 - }) - return response.data.data.id -} - -/** - * 获取网站详情 - * @param id 网站ID - */ -export async function getWebsiteDetail(id: number): Promise { - const response = await panelService.get>(`/websites/${id}`) - return response.data.data -} - -/** - * 删除网站 - * @param id 网站ID - */ -export async function deleteWebsite(id: number, deleteApp: boolean = false, deleteBackup: boolean = false): Promise { - await panelService.post('/websites/del', { - id, - deleteApp, - deleteBackup, - forceDelete: false - }) -} - -/** - * 更新网站 - * @param id 网站ID - * @param params 更新参数 - */ -export async function updateWebsite(id: number, params: Partial): Promise { - await panelService.post(`/websites/update`, { - id, - ...params - }) -} - -// ==================== 网站 HTTPS 配置 API ==================== - -/** - * 网站 HTTPS 配置参数 - */ -export interface WebsiteHTTPSConfig { - websiteId: number // 网站ID - enable: boolean // 是否启用 HTTPS - type: string // 证书类型: 'select'=选择已有证书, 'import'=导入证书 - websiteSSLId?: number // 已有证书ID(type=select时使用) - httpConfig: string // HTTP配置: 'HTTPSOnly'=仅HTTPS, 'HTTPAlso'=同时HTTP, 'HTTPToHTTPS'=HTTP跳转HTTPS - SSLProtocol?: string[] // SSL协议版本 - algorithm?: string // 加密算法 -} - -/** - * 为网站配置 HTTPS - * @param config HTTPS配置 - */ -export async function configureWebsiteHTTPS(config: WebsiteHTTPSConfig): Promise { - await panelService.post(`/websites/${config.websiteId}/https`, { - enable: config.enable, - type: config.type, - websiteSSLId: config.websiteSSLId || 0, - httpConfig: config.httpConfig || 'HTTPToHTTPS', - SSLProtocol: config.SSLProtocol || ['TLSv1.2', 'TLSv1.3'], - algorithm: config.algorithm || 'ECDHE-ECDSA-AES128-GCM-SHA256' - }) -} - -/** - * 获取网站 HTTPS 配置 - * @param websiteId 网站ID - */ -export async function getWebsiteHTTPSConfig(websiteId: number): Promise { - const response = await panelService.get>(`/websites/${websiteId}/https`) - return response.data.data -} - -// ==================== 部署相关 API ==================== - -/** - * 上传文件到指定目录 - * @param path 目标路径(目录) - * @param file 文件 - * @param overwrite 是否覆盖 - */ -export async function uploadFileToPath(path: string, file: File, overwrite: boolean = true): Promise { - const formData = new FormData() - formData.append('file', file) - formData.append('path', path) - formData.append('overwrite', String(overwrite)) - - await panelService.post('/files/upload', formData, { - headers: { - 'Content-Type': 'multipart/form-data' - }, - timeout: 300000 // 上传超时时间 5 分钟 - }) -} - -/** - * 大文件分块上传 - */ -export interface ChunkUploadParams { - path: string // 目标路径 - filename: string // 文件名 - chunk: Blob // 分块数据 - chunkIndex: number // 当前分块索引 - chunkCount: number // 总分块数 -} - -/** - * 分块上传文件 - * @param params 分块上传参数 - */ -export async function uploadChunk(params: ChunkUploadParams): Promise { - const formData = new FormData() - formData.append('chunk', params.chunk, params.filename) - formData.append('path', params.path) - formData.append('chunkIndex', String(params.chunkIndex)) - formData.append('chunkCount', String(params.chunkCount)) - - await panelService.post('/files/chunkupload', formData, { - headers: { - 'Content-Type': 'multipart/form-data' - }, - timeout: 60000 - }) -} - -/** - * 合并分块文件 - * @param path 目标路径 - * @param filename 文件名 - */ -export async function mergeChunks(path: string, filename: string): Promise { - await panelService.post('/files/merge', { - path, - filename - }) -} - -/** - * 删除文件或目录 - * @param path 文件或目录路径 - */ -export async function deleteFile(path: string): Promise { - await panelService.post('/files/del', { - path, - isDir: false, - forceDelete: false - }) -} - -/** - * 创建目录 - * @param path 目录路径 - */ -export async function createDirectory(path: string): Promise { - await panelService.post('/files/mkdir', { - path, - mode: 755 - }) -} - -/** - * 检查文件/目录是否存在 - * @param path 路径 - */ -export async function checkPathExists(path: string): Promise { - try { - const response = await panelService.post>('/files/search', { - path: path.substring(0, path.lastIndexOf('/')), - expand: true, - page: 1, - pageSize: 100 - }) - const files = response.data.data || [] - const targetName = path.substring(path.lastIndexOf('/') + 1) - return files.some(f => f.name === targetName) - } catch { - return false - } -} - -// ==================== 完整部署流程 API ==================== - -/** - * 项目部署参数 - */ -export interface ProjectDeployParams { - // 项目信息 - projectId: string // 项目ID - projectName: string // 项目名称 - domain: string // 绑定域名 - - // 部署配置 - deployPath: string // 部署路径 - enableHttps: boolean // 是否启用HTTPS - - // SSL 配置(启用HTTPS时需要) - acmeAccountId?: number // Acme账户ID - dnsAccountId?: number // DNS账户ID - - // 可选配置 - otherDomains?: string // 其他域名 - createIfNotExist?: boolean // 如果网站不存在则创建 -} - -/** - * 部署结果 - */ -export interface DeployResult { - success: boolean - websiteId?: number - sslCertificateId?: number - message: string - steps: { - step: string - status: 'success' | 'failed' | 'skipped' - message?: string - }[] -} - -/** - * 检查域名对应的网站是否已存在 - * @param domain 域名 - */ -export async function findWebsiteByDomain(domain: string): Promise { - try { - const websites = await getAllWebsites() - return websites.find(w => w.primaryDomain === domain) || null - } catch { - return null - } -} - -/** - * 执行项目部署 - * 此函数封装了完整的部署流程: - * 1. 检查/创建网站 - * 2. 检查/申请SSL证书 - * 3. 配置HTTPS - * - * @param params 部署参数 - */ -export async function deployProject(params: ProjectDeployParams): Promise { - const result: DeployResult = { - success: false, - message: '', - steps: [] - } - - try { - // Step 1: 检查网站是否存在 - console.log(`[部署] 检查网站是否存在: ${params.domain}`) - let website = await findWebsiteByDomain(params.domain) - - if (!website) { - if (!params.createIfNotExist) { - result.steps.push({ step: '检查网站', status: 'failed', message: '网站不存在' }) - result.message = `域名 ${params.domain} 对应的网站不存在,请先在 1Panel 中创建` - return result - } - - // 创建静态网站 - console.log(`[部署] 创建静态网站: ${params.domain}`) - const websiteId = await createWebsite({ - primaryDomain: params.domain, - alias: params.domain.replace(/\./g, '_'), - type: 'static', - siteDir: params.deployPath, - otherDomains: params.otherDomains || '', - remark: `项目: ${params.projectName}`, - port: 80 - }) - - result.websiteId = websiteId - result.steps.push({ step: '创建网站', status: 'success', message: `网站ID: ${websiteId}` }) - - // 重新获取网站信息 - const websites = await getAllWebsites() - website = websites.find(w => w.id === websiteId) || null - } else { - result.websiteId = website.id - result.steps.push({ step: '检查网站', status: 'success', message: '网站已存在' }) - } - - // Step 2: 如果启用 HTTPS,检查/申请证书 - if (params.enableHttps && website) { - console.log(`[部署] 检查SSL证书: ${params.domain}`) - - // 查找已有证书 - let certificate = await findCertificateByDomain(params.domain, params.acmeAccountId || 1) - - if (!certificate) { - // 需要申请证书 - if (params.acmeAccountId && params.dnsAccountId) { - console.log(`[部署] 申请SSL证书: ${params.domain}`) - await applySSLCertificate({ - primaryDomain: params.domain, - otherDomains: params.otherDomains || '', - provider: 'dnsAccount', - acmeAccountId: params.acmeAccountId, - dnsAccountId: params.dnsAccountId, - autoRenew: true, - keyType: 'P256', - apply: true - }) - - // 等待证书申请完成(最多等待 120 秒) - for (let i = 0; i < 24; i++) { - await new Promise(resolve => setTimeout(resolve, 5000)) - certificate = await findCertificateByDomain(params.domain, params.acmeAccountId) - if (certificate) break - } - - if (certificate) { - result.sslCertificateId = certificate.id - result.steps.push({ step: '申请SSL证书', status: 'success', message: `证书ID: ${certificate.id}` }) - } else { - result.steps.push({ step: '申请SSL证书', status: 'failed', message: '证书申请超时或失败' }) - } - } else { - result.steps.push({ step: '申请SSL证书', status: 'skipped', message: '未配置 Acme/DNS 账户' }) - } - } else { - result.sslCertificateId = certificate.id - result.steps.push({ step: '检查SSL证书', status: 'success', message: '证书已存在' }) - } - - // Step 3: 配置 HTTPS - if (result.sslCertificateId && website) { - console.log(`[部署] 配置HTTPS: ${params.domain}`) - await configureWebsiteHTTPS({ - websiteId: website.id, - enable: true, - type: 'select', - websiteSSLId: result.sslCertificateId, - httpConfig: 'HTTPToHTTPS' - }) - result.steps.push({ step: '配置HTTPS', status: 'success' }) - } - } else { - result.steps.push({ step: '配置HTTPS', status: 'skipped', message: '未启用HTTPS' }) - } - - result.success = true - result.message = '部署流程完成' - return result - - } catch (error: any) { - result.success = false - result.message = error.message || '部署过程中发生错误' - result.steps.push({ step: '异常', status: 'failed', message: error.message }) - return result - } -} - -// 导出 panelService 实例供其他地方使用 -export default panelService - diff --git a/src/api/index.ts b/src/api/index.ts index 04bb3ea..9e3493e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,6 +1,3 @@ /** * API 模块导出 */ - -// 1Panel API -export * from './1panel' diff --git a/src/api/project.ts b/src/api/project.ts index a1a72fd..df7cb41 100644 --- a/src/api/project.ts +++ b/src/api/project.ts @@ -32,6 +32,10 @@ export interface ProjectInfo { lastDeployMessage?: string createdTime?: string updatedTime?: string + // 新增字段 + systemType?: 'admin' | 'portal' // 系统类型 + integrateToFramework?: boolean // 是否集成到框架 + menuCount?: number // 菜单数量 } export interface DeployRequest { @@ -86,6 +90,15 @@ export async function getAllProjects() { return res.data.data } +/** + * 获取集成到框架的业务项目 + * 返回 systemType=admin 且 integrateToFramework=true 的项目 + */ +export async function getIntegratedProjects(): Promise { + const res = await request.get('/platform/project/integrated') + return res.data.data +} + /** * 获取项目详情 */ @@ -200,14 +213,12 @@ export async function checkUploadFiles(projectId: number, paths: string[]): Prom } /** - * 准备上传(清理旧文件) + * 准备上传(已废弃) + * @deprecated upload 接口支持 overwrite 参数,不再需要此函数 */ export async function prepareUploadFile(projectId: number, path: string): Promise { - const res = await request.post('/platform/project/upload/prepare', { - projectId, - path - }) - return res.data.data + // 保留函数签名以向后兼容,但不再调用后端 + return true } /** diff --git a/src/api/system/menu.ts b/src/api/system/menu.ts index a163f1d..08adbb0 100644 --- a/src/api/system/menu.ts +++ b/src/api/system/menu.ts @@ -1,8 +1,8 @@ import { request } from '@/utils/request' import type { MenuRecord, MenuFormData } from '@/types/system/menu' -export function getMenuList() { - return request.get('/system/menu/list') +export function getMenuList(params?: { projectId?: number }) { + return request.get('/system/menu/list', { params }) } export function createMenu(data: MenuFormData) { diff --git a/src/components/UploadCore.vue b/src/components/UploadCore.vue index cb2851a..f60785f 100644 --- a/src/components/UploadCore.vue +++ b/src/components/UploadCore.vue @@ -112,7 +112,7 @@ import { } from '@ant-design/icons-vue' import type { UploadFile, DuplicateFile } from '@/types' import DuplicateFileModal from './DuplicateFileModal.vue' -import { checkUploadFiles, prepareUploadFile, uploadFileChunk } from '@/api/project' +import { checkUploadFiles, uploadFileChunk } from '@/api/project' const props = withDefaults(defineProps<{ autoUpload?: boolean @@ -375,21 +375,33 @@ async function handleDrop(event: DragEvent) { const files: File[] = [] + console.log('[Upload] 拖入项目数量:', items.length) + + // 先收集所有的 entry,避免在异步操作中 items 被清空 + const entries: { entry: FileSystemEntry | null, file: File | null }[] = [] + for (let i = 0; i < items.length; i++) { const item = items[i] if (item.kind === 'file') { const entry = item.webkitGetAsEntry?.() - if (entry) { - await traverseFileTree(entry, '', files) - } else { - const file = item.getAsFile() - if (file) { - files.push(file) - } - } + const file = item.getAsFile() + entries.push({ entry, file }) + console.log(`[Upload] 项目 ${i}: entry=${entry?.name || 'null'}, isFile=${entry?.isFile}, isDir=${entry?.isDirectory}`) } } + // 处理收集到的 entries + for (const { entry, file } of entries) { + if (entry) { + await traverseFileTree(entry, '', files) + } else if (file) { + // 如果没有 entry,直接使用 file + files.push(file) + } + } + + console.log('[Upload] 解析后文件数量:', files.length) + if (files.length > 0) { addFiles(files) } @@ -405,23 +417,36 @@ async function traverseFileTree( ): Promise { if (entry.isFile) { const fileEntry = entry as FileSystemFileEntry - const file = await new Promise((resolve, reject) => { - fileEntry.file(resolve, reject) - }) - Object.defineProperty(file, 'relativePath', { - value: path + entry.name, - writable: false - }) - files.push(file) + try { + const file = await new Promise((resolve, reject) => { + fileEntry.file(resolve, reject) + }) + // 为文件设置相对路径 + const relativePath = path + entry.name + Object.defineProperty(file, 'relativePath', { + value: relativePath, + writable: false + }) + files.push(file) + } catch (e) { + console.error('读取文件失败:', entry.name, e) + } } else if (entry.isDirectory) { const dirEntry = entry as FileSystemDirectoryEntry const reader = dirEntry.createReader() - const entries = await new Promise((resolve, reject) => { - reader.readEntries(resolve, reject) - }) + // readEntries 可能不会一次返回所有条目,需要循环调用直到返回空数组 + let allEntries: FileSystemEntry[] = [] + let readBatch: FileSystemEntry[] = [] - for (const childEntry of entries) { + do { + readBatch = await new Promise((resolve, reject) => { + reader.readEntries(resolve, reject) + }) + allEntries = allEntries.concat(readBatch) + } while (readBatch.length > 0) + + for (const childEntry of allEntries) { await traverseFileTree(childEntry, path + entry.name + '/', files) } } @@ -480,10 +505,7 @@ async function uploadFile(file: UploadFile): Promise { const filePath = file.path.startsWith('/') ? file.path.substring(1) : file.path const fullPath = `${basePath}/${filePath}` - // 1. 准备上传(清理可能存在的旧文件) - await prepareUploadFile(projectIdNum, fullPath) - - // 2. 计算目标目录(不包含文件名) + // 计算目标目录(不包含文件名) // 1Panel 接口需要的是上传到的目录路径 const dirPath = fullPath.substring(0, fullPath.lastIndexOf('/') + 1) diff --git a/src/components/system/menu/MenuFormModal.vue b/src/components/system/menu/MenuFormModal.vue index ad2ff2c..a87a7ea 100644 --- a/src/components/system/menu/MenuFormModal.vue +++ b/src/components/system/menu/MenuFormModal.vue @@ -88,6 +88,8 @@ const loading = ref(false) const formRef = ref() const formData = reactive({ + id: undefined, + projectId: undefined, // Add this parentId: undefined, name: '', code: '', @@ -121,7 +123,8 @@ watch( // Reset to defaults Object.assign(formData, { id: undefined, - parentId: props.record?.parentId || 0, // Use record parentId if exists, else 0 + projectId: props.record?.projectId, // Copy projectId + parentId: props.record?.parentId || 0, name: '', code: '', type: 'menu', diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 6d1b801..0bbea7b 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -180,14 +180,30 @@ - + +