diff --git a/.gitignore b/.gitignore index 7d16e648..5fa4b754 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ __pycache__/ # Swagger 文档由 swag init 在构建阶段自动生成(代码即文档,不入 git) /swagger-ui/swagger.json /docs/ +重构.md diff --git a/docker/Dockerfile b/docker/Dockerfile index a4582ba1..65751651 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -52,7 +52,8 @@ COPY --from=backend /out/dockerCopilot ./dockerCopilot COPY etc/. ./etc COPY start.sh ./start.sh -RUN apk add --no-cache tzdata \ +# 安装时区数据、Docker CLI 和 Compose 插件(用于执行 docker compose 命令) +RUN apk add --no-cache tzdata docker-cli docker-cli-compose \ && chmod +x start.sh \ && chmod +x dockerCopilot diff --git a/dockercopilot.go b/dockercopilot.go index 3bff6996..e364c39b 100644 --- a/dockercopilot.go +++ b/dockercopilot.go @@ -96,25 +96,17 @@ func main() { sched.Start() defer sched.Stop() - // Ensure data directory and config exist (Auto-init) - dataDir := "/data/config/image" - if err := os.MkdirAll(dataDir, 0755); err != nil { + // Ensure data directory and config exist (Auto-init),路径统一引用 config 常量 + if err := os.MkdirAll(config.LegacyImageDir, 0755); err != nil { logx.Errorf("Failed to create data directory: %v", err) } // 持久化图标目录:favicon 自动抓取下载与手动上传统一存放 - if err := os.MkdirAll("/data/images", 0755); err != nil { + if err := os.MkdirAll(config.ImagesDir, 0755); err != nil { logx.Errorf("Failed to create images directory: %v", err) } - - imageLogosPath := "/data/config/imageLogos.js" - if _, err := os.Stat(imageLogosPath); os.IsNotExist(err) { - defaultConfig := []byte(`// 自定义镜像logo配置 -export const customImageLogos = { -}; -`) - if err := os.WriteFile(imageLogosPath, defaultConfig, 0644); err != nil { - logx.Errorf("Failed to create default imageLogos.js: %v", err) - } + // 图标配置目录(用于 icons.json) + if err := os.MkdirAll(config.ConfigDir, 0755); err != nil { + logx.Errorf("Failed to create config directory: %v", err) } // 首轮镜像更新检查放到后台执行:镜像列表获取和加速器域名探测都可能较慢, @@ -178,6 +170,22 @@ export const customImageLogos = { logx.Info("程序版本" + config.Version) server.Start() } + +// serveEmbeddedFavicon 返回 DockerCopilot 自身图标。 +// 复用前端 logo.png:embed 目录为 front/(见 //go:embed front/*), +// Dockerfile 将前端 dist/ 内容拷入 front/,故 logo.png 的 embed 路径为 front/logo.png。 +func serveEmbeddedFavicon(w http.ResponseWriter, r *http.Request) { + data, err := embeddedFront.ReadFile("front/logo.png") + if err != nil { + logx.Errorf("无法读取嵌入的 favicon(front/logo.png): %v", err) + http.Error(w, "Icon not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "image/png") + w.Header().Set("Cache-Control", "public, max-age=86400") + _, _ = w.Write(data) +} + func RegisterHandlers(engine *rest.Server) { frontFS, err := fs.Sub(embeddedFront, "front") if err != nil { @@ -189,9 +197,9 @@ func RegisterHandlers(engine *rest.Server) { assetsHandler := http.FileServer(http.FS(frontFS)) // Serve custom icons(历史目录,保持兼容) - iconFileServer := http.StripPrefix("/src/config/image/", http.FileServer(http.Dir("/data/config/image"))) + iconFileServer := http.StripPrefix("/src/config/image/", http.FileServer(http.Dir(config.LegacyImageDir))) // 持久化图标目录 /data/images(favicon 自动抓取下载 + 手动上传统一存放) - imagesFileServer := http.StripPrefix("/images/", http.FileServer(http.Dir("/data/images"))) + imagesFileServer := http.StripPrefix("/images/", http.FileServer(http.Dir(config.ImagesDir))) engine.AddRoutes( []rest.Route{ { @@ -209,6 +217,19 @@ func RegisterHandlers(engine *rest.Server) { imagesFileServer.ServeHTTP(w, r) }, }, + { + // DockerCopilot 自身的 favicon(浏览器标签页图标)。 + // 复用前端 logo.png(embed 路径 front/logo.png,由 Dockerfile 从 dist/ 拷入)。 + Method: http.MethodGet, + Path: "/favicon.png", + Handler: serveEmbeddedFavicon, + }, + { + // 兼容 /favicon.ico 请求,同样返回 logo.png。 + Method: http.MethodGet, + Path: "/favicon.ico", + Handler: serveEmbeddedFavicon, + }, }, ) diff --git a/frontend-react/index.html b/frontend-react/index.html index 6d56d5d0..14145256 100644 --- a/frontend-react/index.html +++ b/frontend-react/index.html @@ -1,4 +1,4 @@ - + @@ -7,13 +7,12 @@ - Docker Copilot - +
diff --git a/frontend-react/public/manifest.json b/frontend-react/public/manifest.json index 76505a74..fd9de7b5 100644 --- a/frontend-react/public/manifest.json +++ b/frontend-react/public/manifest.json @@ -1,7 +1,7 @@ { "name": "Docker Copilot", - "short_name": "Docker", - "description": "一个简洁、优雅且强大的 Docker 容器管理工具", + "short_name": "DC", + "description": "Docker容器管理平台 - 一键更新、定时任务、Compose编排", "start_url": "/", "scope": "/", "display": "standalone", @@ -19,14 +19,24 @@ "src": "/logo.png", "sizes": "512x512", "type": "image/png", - "purpose": "any" + "purpose": "any maskable" + } + ], + "categories": ["utilities", "productivity"], + "shortcuts": [ + { + "name": "容器列表", + "short_name": "容器", + "description": "查看所有容器", + "url": "/", + "icons": [{ "src": "/logo.png", "sizes": "192x192" }] }, { - "src": "/logo.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" + "name": "任务中心", + "short_name": "任务", + "description": "查看后台任务", + "url": "/?open_tasks=1", + "icons": [{ "src": "/logo.png", "sizes": "192x192" }] } - ], - "categories": ["utilities", "productivity"] + ] } diff --git a/frontend-react/src/api/client.js b/frontend-react/src/api/client.js index 83d33e18..b5415169 100644 --- a/frontend-react/src/api/client.js +++ b/frontend-react/src/api/client.js @@ -199,17 +199,19 @@ export const imageAPI = { pruneImages: (ids, force = false, hostId = '') => apiClient.post('/api/images/prune', { ids, force, hostId }), // 手动触发检测所有镜像更新(异步执行,立即返回) checkUpdate: () => apiClient.post('/api/images/check-update'), - // 通过 URL 绑定图标到镜像名(无需上传图片) - setIconUrl: (imageName, url) => apiClient.post('/api/icons/url', { imageName, url }), + // 以下图标方法统一为「对象入参」,字段与后端 struct tag 严格对齐(imageName/url/targetType)。 + // 通过 URL 绑定图标(无需上传图片) + setIconUrl: ({ imageName, url, targetType = 'image' }) => + apiClient.post('/api/icons/url', { imageName, url, targetType }), // 自动抓取站点 favicon 并下载持久化到 /data/images,url 为容器访问地址 - fetchIcon: (imageName, url) => apiClient.post('/api/icons/fetch', { imageName, url }), - uploadIcon: (file, imageName, containerName) => { + fetchIcon: ({ imageName, url, targetType = 'image' }) => + apiClient.post('/api/icons/fetch', { imageName, url, targetType }), + // 上传图标文件:后端读取 formData 字段名固定为 file / imageName / targetType + uploadIcon: ({ file, imageName, targetType = 'image' }) => { const formData = new FormData() formData.append('file', file) formData.append('imageName', imageName) - if (containerName) { - formData.append('containerName', containerName) - } + formData.append('targetType', targetType) return apiClient.post('/api/icons', formData, { headers: { 'Content-Type': 'multipart/form-data' } }) @@ -264,6 +266,12 @@ export const composeAPI = { saveConfig: (cfg) => apiClient.put('/api/compose/config', cfg), // 浏览 DC 自身文件系统目录(目录选择器用);path 为空返回起始目录 browse: (path = '') => apiClient.get('/api/compose/browse', { params: { path } }), + // 创建文件夹 + createFolder: (parentPath, folderName) => + apiClient.post('/api/compose/folder', { parentPath, folderName }), + // 创建 Compose 配置文件 + createFile: (parentPath, fileName) => + apiClient.post('/api/compose/file', { parentPath, fileName }), } // 阶段8:favicon 抓取API(按容器暴露端口解析站点图标) @@ -352,4 +360,9 @@ export const githubAPI = { }, } +// 说明:原重复的 iconAPI 已删除,图标相关接口统一收敛到上方 imageAPI +// (getIcons / setIconUrl / fetchIcon / uploadIcon),字段与后端严格对齐。 + +// 导出 apiClient 供其他组件使用 +export { apiClient } export default apiClient diff --git a/frontend-react/src/assets/logos/asynqmon.png b/frontend-react/src/assets/logos/asynqmon.png deleted file mode 100644 index 594ac8a5..00000000 Binary files a/frontend-react/src/assets/logos/asynqmon.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/audiobookshelf.png b/frontend-react/src/assets/logos/audiobookshelf.png deleted file mode 100644 index 11ae3008..00000000 Binary files a/frontend-react/src/assets/logos/audiobookshelf.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/byte-muse.jpg b/frontend-react/src/assets/logos/byte-muse.jpg deleted file mode 100644 index 2d542a49..00000000 Binary files a/frontend-react/src/assets/logos/byte-muse.jpg and /dev/null differ diff --git a/frontend-react/src/assets/logos/code-server.png b/frontend-react/src/assets/logos/code-server.png deleted file mode 100644 index 24694067..00000000 Binary files a/frontend-react/src/assets/logos/code-server.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/cookiecloud.png b/frontend-react/src/assets/logos/cookiecloud.png deleted file mode 100644 index bd1546e6..00000000 Binary files a/frontend-react/src/assets/logos/cookiecloud.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/cpa.jpg b/frontend-react/src/assets/logos/cpa.jpg deleted file mode 100644 index 58b58a2f..00000000 Binary files a/frontend-react/src/assets/logos/cpa.jpg and /dev/null differ diff --git a/frontend-react/src/assets/logos/docker-copilot.png b/frontend-react/src/assets/logos/docker-copilot.png deleted file mode 100644 index 5777a871..00000000 Binary files a/frontend-react/src/assets/logos/docker-copilot.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/embyserver.png b/frontend-react/src/assets/logos/embyserver.png deleted file mode 100644 index aaa3cce7..00000000 Binary files a/frontend-react/src/assets/logos/embyserver.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/flaresolverr.png b/frontend-react/src/assets/logos/flaresolverr.png deleted file mode 100644 index 23a616fd..00000000 Binary files a/frontend-react/src/assets/logos/flaresolverr.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/fndesk.png b/frontend-react/src/assets/logos/fndesk.png deleted file mode 100644 index cd799b28..00000000 Binary files a/frontend-react/src/assets/logos/fndesk.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/fntv.png b/frontend-react/src/assets/logos/fntv.png deleted file mode 100644 index cd799b28..00000000 Binary files a/frontend-react/src/assets/logos/fntv.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/hermes-agent.png b/frontend-react/src/assets/logos/hermes-agent.png deleted file mode 100644 index 26281d4c..00000000 Binary files a/frontend-react/src/assets/logos/hermes-agent.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/immortal.png b/frontend-react/src/assets/logos/immortal.png deleted file mode 100644 index c00d3aaa..00000000 Binary files a/frontend-react/src/assets/logos/immortal.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/it-tools.webp b/frontend-react/src/assets/logos/it-tools.webp deleted file mode 100644 index c01335d1..00000000 Binary files a/frontend-react/src/assets/logos/it-tools.webp and /dev/null differ diff --git a/frontend-react/src/assets/logos/iyuu.png b/frontend-react/src/assets/logos/iyuu.png deleted file mode 100644 index 3f87fb09..00000000 Binary files a/frontend-react/src/assets/logos/iyuu.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/jellyfin.png b/frontend-react/src/assets/logos/jellyfin.png deleted file mode 100644 index d869e6ca..00000000 Binary files a/frontend-react/src/assets/logos/jellyfin.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/lobechat.png b/frontend-react/src/assets/logos/lobechat.png deleted file mode 100644 index cc29879c..00000000 Binary files a/frontend-react/src/assets/logos/lobechat.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/lucky.png b/frontend-react/src/assets/logos/lucky.png deleted file mode 100644 index bbeef8d3..00000000 Binary files a/frontend-react/src/assets/logos/lucky.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/mdc-ng.png b/frontend-react/src/assets/logos/mdc-ng.png deleted file mode 100644 index f0ff6415..00000000 Binary files a/frontend-react/src/assets/logos/mdc-ng.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/media-saber.png b/frontend-react/src/assets/logos/media-saber.png deleted file mode 100644 index d3b088c2..00000000 Binary files a/frontend-react/src/assets/logos/media-saber.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/momo.png b/frontend-react/src/assets/logos/momo.png deleted file mode 100644 index 30786fe5..00000000 Binary files a/frontend-react/src/assets/logos/momo.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/moviepilot.png b/frontend-react/src/assets/logos/moviepilot.png deleted file mode 100644 index fd97124e..00000000 Binary files a/frontend-react/src/assets/logos/moviepilot.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/ms_tmdb.png b/frontend-react/src/assets/logos/ms_tmdb.png deleted file mode 100644 index a65d31c1..00000000 Binary files a/frontend-react/src/assets/logos/ms_tmdb.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/mt-photos.png b/frontend-react/src/assets/logos/mt-photos.png deleted file mode 100644 index b0cce0ce..00000000 Binary files a/frontend-react/src/assets/logos/mt-photos.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/mysql.png b/frontend-react/src/assets/logos/mysql.png deleted file mode 100644 index 05c3efb7..00000000 Binary files a/frontend-react/src/assets/logos/mysql.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/next-chat.png b/frontend-react/src/assets/logos/next-chat.png deleted file mode 100644 index 79bac3cd..00000000 Binary files a/frontend-react/src/assets/logos/next-chat.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/octopus.svg b/frontend-react/src/assets/logos/octopus.svg deleted file mode 100644 index b2d2b90e..00000000 --- a/frontend-react/src/assets/logos/octopus.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend-react/src/assets/logos/ollama.png b/frontend-react/src/assets/logos/ollama.png deleted file mode 100644 index 59e949e7..00000000 Binary files a/frontend-react/src/assets/logos/ollama.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/one-api.png b/frontend-react/src/assets/logos/one-api.png deleted file mode 100644 index 0f237a22..00000000 Binary files a/frontend-react/src/assets/logos/one-api.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/one-hub.png b/frontend-react/src/assets/logos/one-hub.png deleted file mode 100644 index f036039a..00000000 Binary files a/frontend-react/src/assets/logos/one-hub.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/openwebui.png b/frontend-react/src/assets/logos/openwebui.png deleted file mode 100644 index 2b207478..00000000 Binary files a/frontend-react/src/assets/logos/openwebui.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/pansou.jpg b/frontend-react/src/assets/logos/pansou.jpg deleted file mode 100644 index c39898c8..00000000 Binary files a/frontend-react/src/assets/logos/pansou.jpg and /dev/null differ diff --git a/frontend-react/src/assets/logos/postgres.png b/frontend-react/src/assets/logos/postgres.png deleted file mode 100644 index 8b452714..00000000 Binary files a/frontend-react/src/assets/logos/postgres.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/qbittorrent.webp b/frontend-react/src/assets/logos/qbittorrent.webp deleted file mode 100644 index 840d372c..00000000 Binary files a/frontend-react/src/assets/logos/qbittorrent.webp and /dev/null differ diff --git a/frontend-react/src/assets/logos/qd.png b/frontend-react/src/assets/logos/qd.png deleted file mode 100644 index 3bd9cd62..00000000 Binary files a/frontend-react/src/assets/logos/qd.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/qinglong.svg b/frontend-react/src/assets/logos/qinglong.svg deleted file mode 100644 index 383291b0..00000000 --- a/frontend-react/src/assets/logos/qinglong.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend-react/src/assets/logos/redis.png b/frontend-react/src/assets/logos/redis.png deleted file mode 100644 index d750494f..00000000 Binary files a/frontend-react/src/assets/logos/redis.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/rich-dog.svg b/frontend-react/src/assets/logos/rich-dog.svg deleted file mode 100644 index e8552523..00000000 --- a/frontend-react/src/assets/logos/rich-dog.svg +++ /dev/null @@ -1,370 +0,0 @@ - - - - diff --git a/frontend-react/src/assets/logos/sub-store.webp b/frontend-react/src/assets/logos/sub-store.webp deleted file mode 100644 index 4477075c..00000000 Binary files a/frontend-react/src/assets/logos/sub-store.webp and /dev/null differ diff --git a/frontend-react/src/assets/logos/sun-panel.png b/frontend-react/src/assets/logos/sun-panel.png deleted file mode 100644 index 18843dc8..00000000 Binary files a/frontend-react/src/assets/logos/sun-panel.png and /dev/null differ diff --git a/frontend-react/src/assets/logos/transmission.png b/frontend-react/src/assets/logos/transmission.png deleted file mode 100644 index 0e9c8c70..00000000 Binary files a/frontend-react/src/assets/logos/transmission.png and /dev/null differ diff --git a/frontend-react/src/components/Compose.jsx b/frontend-react/src/components/Compose.jsx index 03111a91..87bd6ea7 100644 --- a/frontend-react/src/components/Compose.jsx +++ b/frontend-react/src/components/Compose.jsx @@ -1,9 +1,8 @@ -import React, { useState, useEffect, useCallback } from 'react' -import { Layers, Play, Square, RotateCw, Download, FileEdit, RefreshCw } from 'lucide-react' +import React, { useState, useEffect, useCallback, useMemo } from 'react' +import { Layers, Play, Square, RotateCw, Download, FileEdit, RefreshCw, FolderOpen, Search } from 'lucide-react' import { composeAPI } from '../api/client.js' import { ComposeEditor } from './ComposeEditor.jsx' -import { ComposeConfigCard } from './ComposeConfigCard.jsx' -import { HostPathMapperCard } from './HostPathMapperCard.jsx' +import { ComposeFileManager } from './ComposeFileManager.jsx' // Compose 项目管理页面 export function Compose() { @@ -12,6 +11,8 @@ export function Compose() { const [error, setError] = useState('') const [editing, setEditing] = useState(null) // { project, filename } const [busyId, setBusyId] = useState('') + const [showFileManager, setShowFileManager] = useState(false) + const [searchQuery, setSearchQuery] = useState('') // 搜索关键词 const load = useCallback(async () => { setLoading(true); setError('') @@ -26,6 +27,17 @@ export function Compose() { useEffect(() => { load() }, [load]) + // 根据搜索关键词过滤项目 + const filteredProjects = useMemo(() => { + if (!searchQuery.trim()) return projects + const query = searchQuery.toLowerCase() + return projects.filter(p => + p.name.toLowerCase().includes(query) || + p.dir.toLowerCase().includes(query) || + p.composeFile.toLowerCase().includes(query) + ) + }, [projects, searchQuery]) + // 执行部署动作,处理高风险 409 二次确认 const doAction = async (project, action) => { setBusyId(project.id) @@ -51,55 +63,87 @@ export function Compose() { return (
{/* 页面头部 */} -
+

Compose 项目

- +
+ + +
+
+ + {/* 搜索框 */} +
+ + setSearchQuery(e.target.value)} + placeholder="搜索项目名称、路径或配置文件..." + className="w-full pl-10 pr-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500" + /> + {searchQuery && ( + + )}
{/* 内容区域 */}
- {/* 两张配置卡片:大屏(lg)并排两列,小屏堆叠;items-stretch 让两卡片等高 */} -
- {/* 扫描配置卡片:保存后自动刷新项目列表 */} - - - {/* 宿主机路径映射配置:供挂载路径转换使用 */} - -
- {error &&
{error}
} {loading &&
加载中...
}
- {projects.map((p) => ( + {filteredProjects.map((p) => (
-
-
-
{p.name}
-
{p.dir}
-
{p.composeFile}
+
+ {/* 左侧:项目信息区 */} +
+
+ {p.name} +
+
+ {p.dir} +
+
+ {p.composeFile} +
+ + {/* 右侧:操作按钮区 */}
- doAction(p, 'up')} icon={Play} title="up" color="emerald" /> - doAction(p, 'down')} icon={Square} title="down" color="red" /> - doAction(p, 'restart')} icon={RotateCw} title="restart" color="blue" /> - doAction(p, 'pull')} icon={Download} title="pull" color="gray" /> + doAction(p, 'up')} icon={Play} title="启动" color="emerald" /> + doAction(p, 'down')} icon={Square} title="停止" color="red" /> + doAction(p, 'restart')} icon={RotateCw} title="重启" color="blue" /> + doAction(p, 'pull')} icon={Download} title="拉取" color="gray" />
))} + {!loading && filteredProjects.length === 0 && projects.length > 0 && ( +
+ 未找到匹配 "{searchQuery}" 的项目 +
+ )} {!loading && projects.length === 0 && ( -
未发现 Compose 项目。请在上方「Compose 扫描配置」中填写已挂载进容器的项目目录并保存。
+
未发现 Compose 项目。请在「设置」页面的「Compose 目录配置」中填写已挂载进容器的项目目录并保存。
)}
@@ -108,6 +152,16 @@ export function Compose() { setEditing(null)} /> )} + + {showFileManager && ( + setShowFileManager(false)} + onFileCreated={(filePath) => { + setShowFileManager(false) + load() // 刷新项目列表 + }} + /> + )}
) } diff --git a/frontend-react/src/components/ComposeFileManager.jsx b/frontend-react/src/components/ComposeFileManager.jsx new file mode 100644 index 00000000..36170a69 --- /dev/null +++ b/frontend-react/src/components/ComposeFileManager.jsx @@ -0,0 +1,439 @@ +import React, { useState, useEffect, useCallback } from 'react' +import { X, Folder, File, Plus, FolderPlus, Edit2, Trash2, ArrowUp, RefreshCw, AlertCircle, FileText, Save } from 'lucide-react' +import { composeAPI, apiClient } from '../api/client.js' + +// Compose 文件管理器弹窗:浏览 /compose 目录,创建文件夹和 docker-compose.yml 文件 +export function ComposeFileManager({ onClose, onFileCreated }) { + const [current, setCurrent] = useState('/compose') + const [parent, setParent] = useState('') + const [dirs, setDirs] = useState([]) + const [files, setFiles] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + const [showCreateFolder, setShowCreateFolder] = useState(false) + const [showCreateFile, setShowCreateFile] = useState(false) + const [newFolderName, setNewFolderName] = useState('') + const [newFileName, setNewFileName] = useState('docker-compose.yml') + const [creating, setCreating] = useState(false) + + // 文件编辑相关 + const [editingFile, setEditingFile] = useState(null) // { name, content } + const [fileContent, setFileContent] = useState('') + const [loadingFile, setLoadingFile] = useState(false) + const [savingFile, setSavingFile] = useState(false) + const [validating, setValidating] = useState(false) + const [validationMsg, setValidationMsg] = useState('') + const [warnings, setWarnings] = useState([]) + + // 加载目录内容 + const load = useCallback(async (path) => { + setLoading(true) + setError('') + try { + const r = await composeAPI.browse(path || '/compose') + const d = r.data?.data || {} + if (r.data?.code !== 200) { + setError(r.data?.msg || '加载失败') + return + } + setCurrent(d.path || '/compose') + setParent(d.parent || '') + setDirs(Array.isArray(d.dirs) ? d.dirs : []) + setFiles(Array.isArray(d.files) ? d.files : []) + } catch (e) { + setError('读取目录失败:' + (e.response?.data?.msg || e.message)) + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { load('/compose') }, [load]) + + // 创建文件夹 + const handleCreateFolder = async () => { + if (!newFolderName.trim()) { + alert('请输入文件夹名称') + return + } + setCreating(true) + try { + const r = await composeAPI.createFolder(current, newFolderName) + if (r.data?.code === 200) { + setShowCreateFolder(false) + setNewFolderName('') + load(current) // 刷新当前目录 + } else { + alert('创建失败:' + (r.data?.msg || '未知错误')) + } + } catch (e) { + alert('创建失败:' + (e.response?.data?.msg || e.message)) + } finally { + setCreating(false) + } + } + + // 创建 Compose 文件 + const handleCreateFile = async () => { + if (!newFileName.trim()) { + alert('请输入文件名') + return + } + setCreating(true) + try { + const r = await composeAPI.createFile(current, newFileName) + if (r.data?.code === 200) { + setShowCreateFile(false) + setNewFileName('docker-compose.yml') + load(current) // 刷新当前目录 + if (onFileCreated) { + const filePath = r.data?.data?.path || `${current}/${newFileName}` + onFileCreated(filePath) + } + } else { + alert('创建失败:' + (r.data?.msg || '未知错误')) + } + } catch (e) { + alert('创建失败:' + (e.response?.data?.msg || e.message)) + } finally { + setCreating(false) + } + } + + // 编辑器部分功能 + const handleEditFile = async (fileName) => { + setLoadingFile(true) + setValidationMsg('') + setWarnings([]) + try { + const filePath = `${current}/${fileName}` + // 使用新的通用文件读取 API + const r = await apiClient.get('/api/files', { params: { path: filePath } }) + + if (r.data?.code === 200) { + setFileContent(r.data.data?.content || '') + setEditingFile({ name: fileName, path: filePath }) + } else { + alert('读取文件失败:' + (r.data?.msg || '未知错误')) + } + } catch (e) { + alert('读取文件失败:' + (e.response?.data?.msg || e.message)) + } finally { + setLoadingFile(false) + } + } + + // 校验 Compose 文件格式 + const handleValidate = async () => { + if (!fileContent.trim()) { + setValidationMsg('内容为空') + return + } + setValidating(true) + setValidationMsg('') + setWarnings([]) + try { + const r = await composeAPI.validate(fileContent) + const d = r.data?.data + if (d?.valid) { + setWarnings(d.warnings || []) + setValidationMsg(d.warnings?.length ? '✅ 语法正确,但有风险提示' : '✅ 校验通过') + } else { + setValidationMsg('❌ 校验失败:' + (d?.error || '未知')) + } + } catch (e) { + setValidationMsg('❌ 校验失败:' + (e.response?.data?.msg || e.message)) + } finally { + setValidating(false) + } + } + + // 保存文件 + const handleSaveFile = async () => { + if (!editingFile) return + setSavingFile(true) + setValidationMsg('') + try { + // 使用新的通用文件保存 API + const r = await apiClient.put('/api/files', { + path: editingFile.path, + content: fileContent + }) + + if (r.data?.code === 200) { + setValidationMsg('✅ 保存成功') + // 刷新目录列表 + load(current) + } else { + alert('保存失败:' + (r.data?.msg || '未知错误')) + } + } catch (e) { + alert('保存失败:' + (e.response?.data?.msg || e.message)) + } finally { + setSavingFile(false) + } + } + + // 关闭编辑器 + const handleCloseEditor = () => { + setEditingFile(null) + setFileContent('') + setValidationMsg('') + setWarnings([]) + } + + return ( +
+
+ {/* 头部 */} +
+
+

+ Compose 文件管理器 +

+

浏览 /compose 目录,创建项目文件

+
+ +
+ + {/* 工具栏 */} +
+ {/* 路径导航栏 */} +
+ + + {current} + + +
+ + {/* 操作按钮栏 */} +
+ + +
+
+ + {/* 文件列表 */} +
+ {error && ( +
+ {error} +
+ )} + + {loading && dirs.length === 0 && files.length === 0 && ( +
+ 加载中... +
+ )} + + {!loading && !error && dirs.length === 0 && files.length === 0 && ( +
该目录下没有文件或文件夹
+ )} + + {/* 文件夹列表 */} +
+ {dirs.map((dir) => ( + + ))} + + {/* 文件列表 */} + {files.map((file) => ( +
+ + {file} + +
+ ))} +
+
+ + {/* 新建文件夹对话框 */} + {showCreateFolder && ( +
+
+

新建文件夹

+ setNewFolderName(e.target.value)} + placeholder="请输入文件夹名称" + className="input w-full mb-4" + autoFocus + /> +
+ + +
+
+
+ )} + + {/* 新建 Compose 文件对话框 */} + {showCreateFile && ( +
+
+

新建 Compose 文件

+
+ + setNewFileName(e.target.value)} + placeholder="docker-compose.yml" + className="input w-full" + autoFocus + /> +

推荐使用 docker-compose.yml 或 compose.yaml

+
+
+ + +
+
+
+ )} +
+ + {/* 文件编辑器弹窗 */} + {editingFile && ( +
+
+ {/* 编辑器头部 */} +
+

+ + 编辑文件:{editingFile.name} +

+ +
+ + {/* 文件内容编辑区 */} +
+ {loadingFile ? ( +
+ 加载中... +
+ ) : ( + <> +