From f7a7d44ebe88726f6c168bfb9091ac15266f6727 Mon Sep 17 00:00:00 2001 From: Admin Date: Tue, 21 Jul 2026 17:07:41 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=EF=BC=8C=E6=94=AF=E6=8C=81=20SQLite=20?= =?UTF-8?q?=E5=92=8C=20Redis=20=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增安装控制器 (install_controller.go) - 新增安装服务 (install_service.go) - 添加 Redis 配置结构到配置服务 - 更新路由注册,添加安装 API - 创建前端安装页面 (Install.tsx) - 支持 SQLite 和 MySQL 数据库选择 - Redis 为可选配置 - 安装完成后创建管理员账号 API 端点: - GET /api/v1/install/status - 获取安装状态 - POST /api/v1/install - 执行安装 --- docker-compose.yml | 38 +- internal/controllers/install_controller.go | 110 ++++ internal/router/api_routes.go | 7 + internal/router/register.go | 1 + internal/router/router.go | 1 + internal/services/config_service.go | 9 + internal/services/install_service.go | 263 +++++++++ internal/utils/encoding.go | 30 + web/src/api/hooks.ts | 15 + web/src/api/index.ts | 29 + web/src/pages/Install.tsx | 632 +++++++++++++++++++++ web/src/routeTree.gen.ts | 8 + 12 files changed, 1134 insertions(+), 9 deletions(-) create mode 100644 internal/controllers/install_controller.go create mode 100644 internal/services/install_service.go create mode 100644 web/src/pages/Install.tsx diff --git a/docker-compose.yml b/docker-compose.yml index 3478260..9906a0b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,5 @@ -version: '3.8' - services: + # Frontend - React Web UI frontend: image: git.viaeon.com/admin/taskpool-react:latest ports: @@ -9,13 +8,34 @@ services: - backend restart: unless-stopped + # Backend - Go API Server backend: image: git.viaeon.com/admin/taskpool:latest - environment: - - DB_TYPE=sqlite - - DB_PATH=/data/taskpool.db - volumes: - - ./data:/data ports: - - "8080:8080" - restart: unless-stopped \ No newline at end of file + - "8052:8052" + volumes: + - ./data:/app/data + - ./configs:/app/configs + - ./envs:/app/envs + environment: + - TZ=Asia/Shanghai + - BH_SERVER_PORT=8052 + - BH_SERVER_HOST=0.0.0.0 + - BH_DB_TYPE=sqlite + - BH_DB_PATH=/app/data/taskpool.db + restart: unless-stopped + + # Redis (可选 - 用于缓存和队列) + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + command: redis-server --appendonly yes + restart: unless-stopped + profiles: + - redis # 使用 --profile redis 启用 + +volumes: + redis_data: \ No newline at end of file diff --git a/internal/controllers/install_controller.go b/internal/controllers/install_controller.go new file mode 100644 index 0000000..5aa4646 --- /dev/null +++ b/internal/controllers/install_controller.go @@ -0,0 +1,110 @@ +package controllers + +import ( + "net/http" + + "github.com/engigu/baihu-panel/internal/services" + "github.com/gin-gonic/gin" +) + +type InstallController struct { + installService *services.InstallService +} + +func NewInstallController() *InstallController { + return &InstallController{ + installService: services.NewInstallService(), + } +} + +// GetInstallStatus 获取安装状态 +// @Summary 获取安装状态 +// @Description 检查系统是否已完成安装 +// @Tags 安装 +// @Produce json +// @Success 200 {object} services.InstallStatus +// @Router /api/v1/install/status [get] +func (c *InstallController) GetInstallStatus(ctx *gin.Context) { + status, err := c.installService.CheckInstallStatus() + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + ctx.JSON(http.StatusOK, status) +} + +// Install 执行安装 +// @Summary 执行安装 +// @Description 初始化系统配置和管理员账号 +// @Tags 安装 +// @Accept json +// @Produce json +// @Param request body services.InstallRequest true "安装请求" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /api/v1/install [post] +func (c *InstallController) Install(ctx *gin.Context) { + // 先检查是否已安装 + status, err := c.installService.CheckInstallStatus() + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "检查安装状态失败"}) + return + } + + if status.Installed { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "系统已安装,无法重复安装"}) + return + } + + var req services.InstallRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()}) + return + } + + // 验证必填字段 + if req.AdminUsername == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "管理员用户名不能为空"}) + return + } + if req.AdminPassword == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "管理员密码不能为空"}) + return + } + if len(req.AdminPassword) < 6 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "管理员密码至少6位"}) + return + } + + // MySQL 必填验证 + if req.DBType == "mysql" { + if req.DBHost == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "MySQL 主机不能为空"}) + return + } + if req.DBName == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "MySQL 数据库名不能为空"}) + return + } + } + + // Redis 启用时的验证 + if req.RedisEnabled { + if req.RedisHost == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "Redis 主机不能为空"}) + return + } + } + + // 执行安装 + if err := c.installService.Install(&req); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "安装失败: " + err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{ + "message": "安装成功", + "admin_username": req.AdminUsername, + }) +} diff --git a/internal/router/api_routes.go b/internal/router/api_routes.go index fdbf4ba..b4836a7 100644 --- a/internal/router/api_routes.go +++ b/internal/router/api_routes.go @@ -11,6 +11,13 @@ func initPublicAPIRoutes(api *gin.RouterGroup, c *Controllers) { ctx.JSON(200, gin.H{"message": "pong"}) }) + // Install routes (无需认证,仅在未安装时可用) + install := api.Group("/install") + { + install.GET("/status", c.Install.GetInstallStatus) + install.POST("", c.Install.Install) + } + // api.GET("/debug/goroutines", func(ctx *gin.Context) { // buf := make([]byte, 1024*1024) // n := runtime.Stack(buf, true) diff --git a/internal/router/register.go b/internal/router/register.go index 7d3dbee..27b88cd 100644 --- a/internal/router/register.go +++ b/internal/router/register.go @@ -71,6 +71,7 @@ func RegisterControllers() *Controllers { Monitor: controllers.NewMonitorController(executorService), Interconnect: controllers.NewInterconnectController(interconnectService), Data: controllers.NewDataController(taskController, envController), + Install: controllers.NewInstallController(), } } diff --git a/internal/router/router.go b/internal/router/router.go index 3e40837..9f52209 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -34,6 +34,7 @@ type Controllers struct { Monitor *controllers.MonitorController Interconnect *controllers.InterconnectController Data *controllers.DataController + Install *controllers.InstallController } func Setup(c *Controllers) *gin.Engine { diff --git a/internal/services/config_service.go b/internal/services/config_service.go index 603da24..700a567 100644 --- a/internal/services/config_service.go +++ b/internal/services/config_service.go @@ -36,10 +36,19 @@ type SecurityConfig struct { Secret string `ini:"secret"` } +type RedisConfig struct { + Enabled bool `ini:"enabled"` + Host string `ini:"host"` + Port int `ini:"port"` + Password string `ini:"password"` + DB int `ini:"db"` +} + type AppConfig struct { Server ServerConfig `ini:"server"` Database DatabaseConfig `ini:"database"` Security SecurityConfig `ini:"security"` + Redis RedisConfig `ini:"redis"` } var Config *AppConfig diff --git a/internal/services/install_service.go b/internal/services/install_service.go new file mode 100644 index 0000000..2f78012 --- /dev/null +++ b/internal/services/install_service.go @@ -0,0 +1,263 @@ +package services + +import ( + "os" + "path/filepath" + + "github.com/engigu/baihu-panel/internal/constant" + "github.com/engigu/baihu-panel/internal/database" + "github.com/engigu/baihu-panel/internal/logger" + "github.com/engigu/baihu-panel/internal/utils" + + "gopkg.in/ini.v1" +) + +// InstallRequest 安装请求 +type InstallRequest struct { + // 数据库配置 + DBType string `json:"db_type" binding:"required"` // sqlite 或 mysql + DBHost string `json:"db_host"` // MySQL 主机 + DBPort int `json:"db_port"` // MySQL 端口 + DBUser string `json:"db_user"` // MySQL 用户名 + DBPassword string `json:"db_password"` // MySQL 密码 + DBName string `json:"db_name"` // MySQL 数据库名 + DBPath string `json:"db_path"` // SQLite 数据库路径 + DBSSLMode string `json:"db_ssl_mode"` // SSL 模式 + + // Redis 配置(可选) + RedisEnabled bool `json:"redis_enabled"` // 是否启用 Redis + RedisHost string `json:"redis_host"` // Redis 主机 + RedisPort int `json:"redis_port"` // Redis 端口 + RedisPassword string `json:"redis_password"` // Redis 密码 + RedisDB int `json:"redis_db"` // Redis 数据库索引 + + // 管理员账号 + AdminUsername string `json:"admin_username" binding:"required"` // 管理员用户名 + AdminPassword string `json:"admin_password" binding:"required"` // 管理员密码 + AdminEmail string `json:"admin_email"` // 管理员邮箱 + + // 站点设置 + SiteTitle string `json:"site_title"` // 站点标题 + SiteSubtitle string `json:"site_subtitle"` // 站点副标题 +} + +// InstallStatus 安装状态 +type InstallStatus struct { + Installed bool `json:"installed"` + ConfigPath string `json:"config_path"` + DBType string `json:"db_type"` +} + +// InstallService 安装服务 +type InstallService struct { + settingsService *SettingsService + userService *UserService +} + +// NewInstallService 创建安装服务 +func NewInstallService() *InstallService { + return &InstallService{ + settingsService: NewSettingsService(), + userService: NewUserService(), + } +} + +// CheckInstallStatus 检查安装状态 +func (s *InstallService) CheckInstallStatus() (*InstallStatus, error) { + status := &InstallStatus{ + ConfigPath: constant.ConfigPath, + } + + // 检查配置文件是否存在 + configExists := false + if _, err := os.Stat(constant.ConfigPath); err == nil { + configExists = true + } + + // 检查是否已初始化(数据库中有管理员用户) + initialized := false + if configExists { + // 尝试检查是否有管理员用户 + admin := s.userService.GetUserByUsername("admin") + if admin != nil { + initialized = true + } + } + + // 同时检查数据库中的初始化标志 + dbInitialized := s.settingsService.GetSettingValue(constant.SectionSystem, constant.KeyInitialized) + if dbInitialized == "true" { + initialized = true + } + + status.Installed = initialized + status.DBType = Config.Database.Type + + return status, nil +} + +// Install 执行安装 +func (s *InstallService) Install(req *InstallRequest) error { + // 1. 创建配置文件 + if err := s.createConfigFile(req); err != nil { + return err + } + + // 2. 重新加载数据库配置 + if err := s.reloadDatabase(req); err != nil { + return err + } + + // 3. 初始化数据库 + if err := database.Migrate(); err != nil { + return err + } + + // 4. 初始化设置 + if err := s.settingsService.InitSettings(); err != nil { + logger.Warnf("[Install] 初始化设置失败: %v", err) + } + + // 5. 创建管理员账号 + if err := s.createAdmin(req); err != nil { + return err + } + + // 6. 保存站点设置 + if req.SiteTitle != "" || req.SiteSubtitle != "" { + siteSettings := make(map[string]string) + if req.SiteTitle != "" { + siteSettings[constant.KeyTitle] = req.SiteTitle + } + if req.SiteSubtitle != "" { + siteSettings[constant.KeySubtitle] = req.SiteSubtitle + } + if err := s.settingsService.UpdateSectionSettings(constant.SectionSite, siteSettings); err != nil { + logger.Warnf("[Install] 保存站点设置失败: %v", err) + } + } + + // 7. 标记已初始化 + if err := s.settingsService.SaveSetting(constant.SectionSystem, constant.KeyInitialized, "true"); err != nil { + return err + } + + logger.Info("[Install] 安装完成") + return nil +} + +// createConfigFile 创建配置文件 +func (s *InstallService) createConfigFile(req *InstallRequest) error { + // 确保配置目录存在 + configDir := filepath.Dir(constant.ConfigPath) + if err := os.MkdirAll(configDir, 0755); err != nil { + return err + } + + // 创建配置文件 + cfg := ini.Empty() + + // [server] 配置 + serverSection, _ := cfg.NewSection("server") + serverSection.NewKey("port", "8052") + serverSection.NewKey("host", "0.0.0.0") + serverSection.NewKey("cookie_name", "BHToken") + + // [database] 配置 + dbSection, _ := cfg.NewSection("database") + dbSection.NewKey("type", req.DBType) + + if req.DBType == "sqlite" { + dbPath := req.DBPath + if dbPath == "" { + dbPath = constant.DefaultDBPath + } + dbSection.NewKey("path", dbPath) + } else if req.DBType == "mysql" { + dbSection.NewKey("host", req.DBHost) + dbSection.NewKey("port", intToStr(req.DBPort)) + dbSection.NewKey("user", req.DBUser) + dbSection.NewKey("password", req.DBPassword) + dbSection.NewKey("dbname", req.DBName) + if req.DBSSLMode != "" { + dbSection.NewKey("ssl_mode", req.DBSSLMode) + } + } + dbSection.NewKey("table_prefix", "baihu_") + + // [redis] 配置(如果启用) + if req.RedisEnabled { + redisSection, _ := cfg.NewSection("redis") + redisSection.NewKey("enabled", "true") + redisSection.NewKey("host", req.RedisHost) + redisSection.NewKey("port", intToStr(req.RedisPort)) + if req.RedisPassword != "" { + redisSection.NewKey("password", req.RedisPassword) + } + redisSection.NewKey("db", intToStr(req.RedisDB)) + } + + // [security] 配置 + securitySection, _ := cfg.NewSection("security") + secret := utils.RandomString(32) + securitySection.NewKey("secret", secret) + + // 保存配置文件 + if err := cfg.SaveTo(constant.ConfigPath); err != nil { + return err + } + + logger.Infof("[Install] 配置文件已保存到: %s", constant.ConfigPath) + return nil +} + +// reloadDatabase 重新加载数据库 +func (s *InstallService) reloadDatabase(req *InstallRequest) error { + dbCfg := &database.Config{ + Type: req.DBType, + Host: req.DBHost, + Port: req.DBPort, + User: req.DBUser, + Password: req.DBPassword, + DBName: req.DBName, + Path: req.DBPath, + SSLMode: req.DBSSLMode, + } + + if req.DBType == "sqlite" && req.DBPath == "" { + dbCfg.Path = constant.DefaultDBPath + } + + if err := database.Init(dbCfg); err != nil { + return err + } + + return nil +} + +// createAdmin 创建管理员账号 +func (s *InstallService) createAdmin(req *InstallRequest) error { + // 检查用户是否已存在 + existingUser := s.userService.GetUserByUsername(req.AdminUsername) + if existingUser != nil { + logger.Info("[Install] 管理员账号已存在,跳过创建") + return nil + } + + email := req.AdminEmail + if email == "" { + email = "admin@local" + } + + s.userService.CreateUser(req.AdminUsername, req.AdminPassword, email, "admin") + logger.Infof("[Install] 管理员账号创建成功: %s", req.AdminUsername) + return nil +} + +// intToStr 整数转字符串 +func intToStr(n int) string { + if n == 0 { + return "" + } + return utils.IntToStr(n) +} diff --git a/internal/utils/encoding.go b/internal/utils/encoding.go index 4aa9b5d..1a76761 100644 --- a/internal/utils/encoding.go +++ b/internal/utils/encoding.go @@ -60,3 +60,33 @@ func TrimLastRunes(s string, maxRunes int) string { } return s } + +// IntToStr 将整数转换为字符串 +func IntToStr(n int) string { + if n == 0 { + return "0" + } + + var negative bool + if n < 0 { + negative = true + n = -n + } + + var digits []byte + for n > 0 { + digits = append(digits, byte('0'+n%10)) + n /= 10 + } + + if negative { + digits = append(digits, '-') + } + + // Reverse + for i, j := 0, len(digits)-1; i < j; i, j = i+1, j-1 { + digits[i], digits[j] = digits[j], digits[i] + } + + return string(digits) +} diff --git a/web/src/api/hooks.ts b/web/src/api/hooks.ts index ed0c5ae..191e6b3 100644 --- a/web/src/api/hooks.ts +++ b/web/src/api/hooks.ts @@ -218,4 +218,19 @@ export function useUpdateSettings() { mutationFn: api.updateSettings, onSuccess: () => qc.invalidateQueries({ queryKey: ['settings'] }), }) +} + +// Install hooks +export function useInstallStatus() { + return useQuery({ + queryKey: ['installStatus'], + queryFn: api.getInstallStatus, + retry: false, + }) +} + +export function useInstall() { + return useMutation({ + mutationFn: api.install, + }) } \ No newline at end of file diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 746d903..d23eb6a 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -171,4 +171,33 @@ export const updateSettings = (data: any) => request('/settings', { body: JSON.stringify(data), }) +// Install +export const getInstallStatus = () => request<{ installed: boolean; config_path: string; db_type: string }>('/install/status') + +export const install = async (data: { + db_type: string + db_host?: string + db_port?: number + db_user?: string + db_password?: string + db_name?: string + db_path?: string + db_ssl_mode?: string + redis_enabled?: boolean + redis_host?: string + redis_port?: number + redis_password?: string + redis_db?: number + admin_username: string + admin_password: string + admin_email?: string + site_title?: string + site_subtitle?: string +}) => { + return request<{ message: string; admin_username: string }>('/install', { + method: 'POST', + body: JSON.stringify(data), + }) +} + export { request } \ No newline at end of file diff --git a/web/src/pages/Install.tsx b/web/src/pages/Install.tsx new file mode 100644 index 0000000..b873769 --- /dev/null +++ b/web/src/pages/Install.tsx @@ -0,0 +1,632 @@ +import { useState } from 'react' +import { useNavigate } from '@tanstack/react-router' +import { useInstallStatus, useInstall } from '@/api/hooks' +import { Database, Server, User, Lock, AlertCircle, CheckCircle, ChevronDown, ChevronRight } from 'lucide-react' + +export default function Install() { + const navigate = useNavigate() + const { data: statusData, isLoading: statusLoading } = useInstallStatus() + const installMutation = useInstall() + + const [step, setStep] = useState(1) // 1: Database, 2: Redis, 3: Admin + const [dbType, setDbType] = useState<'sqlite' | 'mysql'>('sqlite') + const [showRedis, setShowRedis] = useState(false) + + // Database config + const [dbHost, setDbHost] = useState('localhost') + const [dbPort, setDbPort] = useState('3306') + const [dbUser, setDbUser] = useState('root') + const [dbPassword, setDbPassword] = useState('') + const [dbName, setDbName] = useState('taskpool') + const [dbPath, setDbPath] = useState('') + + // Redis config + const [redisEnabled, setRedisEnabled] = useState(false) + const [redisHost, setRedisHost] = useState('localhost') + const [redisPort, setRedisPort] = useState('6379') + const [redisPassword, setRedisPassword] = useState('') + const [redisDB, setRedisDB] = useState('0') + + // Admin config + const [adminUsername, setAdminUsername] = useState('admin') + const [adminPassword, setAdminPassword] = useState('') + const [adminEmail, setAdminEmail] = useState('') + + // Site config + const [siteTitle, setSiteTitle] = useState('TaskPool') + const [siteSubtitle, setSiteSubtitle] = useState('自动化任务调度平台') + + const [error, setError] = useState('') + const [success, setSuccess] = useState(false) + + // If already installed, redirect to login + if (statusData?.installed && !success) { + navigate({ to: '/login' }) + return null + } + + if (statusLoading) { + return ( +
+
Loading...
+
+ ) + } + + const handleInstall = async () => { + setError('') + + if (adminPassword.length < 6) { + setError('密码至少需要6位') + return + } + + try { + await installMutation.mutateAsync({ + db_type: dbType, + db_host: dbType === 'mysql' ? dbHost : undefined, + db_port: dbType === 'mysql' ? parseInt(dbPort) : undefined, + db_user: dbType === 'mysql' ? dbUser : undefined, + db_password: dbType === 'mysql' ? dbPassword : undefined, + db_name: dbType === 'mysql' ? dbName : undefined, + db_path: dbType === 'sqlite' ? dbPath : undefined, + redis_enabled: redisEnabled, + redis_host: redisEnabled ? redisHost : undefined, + redis_port: redisEnabled ? parseInt(redisPort) : undefined, + redis_password: redisEnabled ? redisPassword : undefined, + redis_db: redisEnabled ? parseInt(redisDB) : undefined, + admin_username: adminUsername, + admin_password: adminPassword, + admin_email: adminEmail || undefined, + site_title: siteTitle, + site_subtitle: siteSubtitle, + }) + setSuccess(true) + } catch (err: any) { + setError(err.message || '安装失败') + } + } + + const renderStepIndicator = () => ( +
+ {[1, 2, 3].map((s) => ( +
+ ))} +
+ ) + + const renderDatabaseStep = () => ( +
+

数据库配置

+ + {/* Database type selector */} +
+ + +
+ + {/* SQLite config */} + {dbType === 'sqlite' && ( +
+ + setDbPath(e.target.value)} + placeholder="默认: data/taskpool.db" + style={{ + width: '100%', + padding: '10px 12px', + background: 'var(--bg-primary)', + border: '1px solid var(--border)', + borderRadius: 6, + color: 'var(--text-primary)', + fontSize: 14, + }} + /> +
+ )} + + {/* MySQL config */} + {dbType === 'mysql' && ( +
+
+
+ + setDbHost(e.target.value)} + placeholder="localhost" + required + style={{ + width: '100%', + padding: '10px 12px', + background: 'var(--bg-primary)', + border: '1px solid var(--border)', + borderRadius: 6, + color: 'var(--text-primary)', + fontSize: 14, + }} + /> +
+
+ + setDbPort(e.target.value)} + placeholder="3306" + required + style={{ + width: '100%', + padding: '10px 12px', + background: 'var(--bg-primary)', + border: '1px solid var(--border)', + borderRadius: 6, + color: 'var(--text-primary)', + fontSize: 14, + }} + /> +
+
+
+ + setDbUser(e.target.value)} + placeholder="root" + required + style={{ + width: '100%', + padding: '10px 12px', + background: 'var(--bg-primary)', + border: '1px solid var(--border)', + borderRadius: 6, + color: 'var(--text-primary)', + fontSize: 14, + }} + /> +
+
+ + setDbPassword(e.target.value)} + placeholder="数据库密码" + style={{ + width: '100%', + padding: '10px 12px', + background: 'var(--bg-primary)', + border: '1px solid var(--border)', + borderRadius: 6, + color: 'var(--text-primary)', + fontSize: 14, + }} + /> +
+
+ + setDbName(e.target.value)} + placeholder="taskpool" + required + style={{ + width: '100%', + padding: '10px 12px', + background: 'var(--bg-primary)', + border: '1px solid var(--border)', + borderRadius: 6, + color: 'var(--text-primary)', + fontSize: 14, + }} + /> +
+
+ )} + + {/* Redis config (optional) */} +
+ + + {showRedis && ( +
+ + + {redisEnabled && ( +
+
+
+ + setRedisHost(e.target.value)} + placeholder="localhost" + style={{ + width: '100%', + padding: '10px 12px', + background: 'var(--bg-secondary)', + border: '1px solid var(--border)', + borderRadius: 6, + color: 'var(--text-primary)', + fontSize: 14, + }} + /> +
+
+ + setRedisPort(e.target.value)} + placeholder="6379" + style={{ + width: '100%', + padding: '10px 12px', + background: 'var(--bg-secondary)', + border: '1px solid var(--border)', + borderRadius: 6, + color: 'var(--text-primary)', + fontSize: 14, + }} + /> +
+
+
+ + setRedisPassword(e.target.value)} + placeholder="Redis 密码 (可选)" + style={{ + width: '100%', + padding: '10px 12px', + background: 'var(--bg-secondary)', + border: '1px solid var(--border)', + borderRadius: 6, + color: 'var(--text-primary)', + fontSize: 14, + }} + /> +
+
+ )} +
+ )} +
+
+ ) + + const renderAdminStep = () => ( +
+

管理员账号

+ +
+
+ +
+ + setAdminUsername(e.target.value)} + placeholder="admin" + required + style={{ + width: '100%', + padding: '10px 12px 10px 40px', + background: 'var(--bg-primary)', + border: '1px solid var(--border)', + borderRadius: 6, + color: 'var(--text-primary)', + fontSize: 14, + }} + /> +
+
+ +
+ +
+ + setAdminPassword(e.target.value)} + placeholder="至少6位密码" + required + style={{ + width: '100%', + padding: '10px 12px 10px 40px', + background: 'var(--bg-primary)', + border: '1px solid var(--border)', + borderRadius: 6, + color: 'var(--text-primary)', + fontSize: 14, + }} + /> +
+
+ +
+ + setAdminEmail(e.target.value)} + placeholder="admin@example.com" + style={{ + width: '100%', + padding: '10px 12px', + background: 'var(--bg-primary)', + border: '1px solid var(--border)', + borderRadius: 6, + color: 'var(--text-primary)', + fontSize: 14, + }} + /> +
+ +
+

站点设置

+ +
+
+ + setSiteTitle(e.target.value)} + placeholder="TaskPool" + style={{ + width: '100%', + padding: '10px 12px', + background: 'var(--bg-primary)', + border: '1px solid var(--border)', + borderRadius: 6, + color: 'var(--text-primary)', + fontSize: 14, + }} + /> +
+
+ + setSiteSubtitle(e.target.value)} + placeholder="自动化任务调度平台" + style={{ + width: '100%', + padding: '10px 12px', + background: 'var(--bg-primary)', + border: '1px solid var(--border)', + borderRadius: 6, + color: 'var(--text-primary)', + fontSize: 14, + }} + /> +
+
+
+
+
+ ) + + return ( +
+
+ {/* Header */} +
+
+ T +
+

安装向导

+

配置您的 TaskPool 实例

+
+ + {success ? ( +
+ +

安装成功

+

+ 您可以使用管理员账号登录系统 +

+ +
+ ) : ( + <> + {renderStepIndicator()} + + {/* Error */} + {error && ( +
+ + {error} +
+ )} + + {step === 1 && renderDatabaseStep()} + {step === 3 && renderAdminStep()} + + {/* Navigation */} +
+ {step > 1 && ( + + )} + + {step < 3 ? ( + + ) : ( + + )} +
+ + )} +
+
+ ) +} diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index 2f27197..b244176 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -8,6 +8,7 @@ import Interconnect from '@/pages/Interconnect' import Terminal from '@/pages/Terminal' import Settings from '@/pages/Settings' import Login from '@/pages/Login' +import Install from '@/pages/Install' const rootRoute = createRootRoute({ component: () => , @@ -19,6 +20,12 @@ const loginRoute = createRoute({ component: Login, }) +const installRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/install', + component: Install, +}) + const layoutRoute = createRoute({ getParentRoute: () => rootRoute, path: '/', @@ -69,6 +76,7 @@ const settingsRoute = createRoute({ const routeTree = rootRoute.addChildren([ loginRoute, + installRoute, layoutRoute.addChildren([ dashboardRoute, tasksRoute,