Compare commits

...

2 Commits

Author SHA1 Message Date
3a5f5ddd5d 优化数据库,日志,命令模块 2026-02-23 18:54:54 +08:00
47a2dfeda1 optimize tui interface and interaction 2026-02-22 20:18:12 +08:00
12 changed files with 619 additions and 401 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
sunhpc sunhpc
testgui

66
cmd/test/main.go Normal file
View File

@@ -0,0 +1,66 @@
package main
import (
"fmt"
tea "github.com/charmbracelet/bubbletea"
)
// model 定义应用的状态
type model struct {
items []string // 列表数据
selectedIdx int // 当前选中的索引
}
// Init 初始化模型,返回初始命令(这里不需要,返回 nil
func (m model) Init() tea.Cmd {
return nil
}
// Update 处理用户输入和状态更新
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
// 处理键盘输入
case tea.KeyMsg:
switch msg.String() {
// Tab 键:切换选中项(循环切换)
case "tab":
m.selectedIdx = (m.selectedIdx + 1) % len(m.items)
// Ctrl+C 或 q 键:退出程序
case "ctrl+c", "q":
return m, tea.Quit
}
}
return m, nil
}
// View 渲染界面
func (m model) View() string {
s := "网络接口列表(按 Tab 切换选择,按 q 退出)\n\n"
// 遍历列表项,渲染每一项
for i, item := range m.items {
// 标记当前选中的项
if i == m.selectedIdx {
s += fmt.Sprintf("→ %s (选中)\n", item)
} else {
s += fmt.Sprintf(" %s\n", item)
}
}
return s
}
func main() {
// 初始化模型,设置列表数据
initialModel := model{
items: []string{"eth0", "eth1", "eth2", "eth3"},
selectedIdx: 0, // 默认选中第一个项
}
// 启动 Bubble Tea 程序
p := tea.NewProgram(initialModel)
if _, err := p.Run(); err != nil {
fmt.Printf("程序运行出错: %v\n", err)
}
}

View File

@@ -3,7 +3,6 @@ package initcmd
import ( import (
"fmt" "fmt"
"sunhpc/internal/middler/auth" "sunhpc/internal/middler/auth"
"sunhpc/pkg/config"
"sunhpc/pkg/database" "sunhpc/pkg/database"
"sunhpc/pkg/logger" "sunhpc/pkg/logger"
@@ -28,26 +27,29 @@ func NewInitDBCmd() *cobra.Command {
logger.Debug("执行数据库初始化...") logger.Debug("执行数据库初始化...")
cfg, err := config.LoadConfig()
if err != nil {
return fmt.Errorf("加载配置失败: %w", err)
}
// 初始化数据库 // 初始化数据库
db, err := database.GetInstance(&cfg.Database, nil) db, err := database.GetDB()
if err != nil { if err != nil {
return fmt.Errorf("数据库连接失败: %w", err) return fmt.Errorf("数据库连接失败: %w", err)
} }
defer db.Close() defer db.Close()
if err := db.InitTables(force); err != nil { if err := database.InitTables(db, force); err != nil {
return fmt.Errorf("数据库初始化失败: %w", err) return fmt.Errorf("数据库初始化失败: %w", err)
} }
// 测试数据库连接
if err := database.TestNodeInsert(db); err != nil {
return fmt.Errorf("数据库测试失败: %w", err)
}
return nil return nil
}, },
} }
cmd.Flags().BoolVarP(&force, "force", "f", false, "强制重新初始化") cmd.Flags().BoolVarP(
&force, "force", "f", false,
"强制重新初始化")
return cmd return cmd
} }

View File

@@ -1,6 +1,9 @@
package cli package cli
import ( import (
"fmt"
"os"
"path/filepath"
initcmd "sunhpc/internal/cli/init" initcmd "sunhpc/internal/cli/init"
"sunhpc/pkg/config" "sunhpc/pkg/config"
"sunhpc/pkg/logger" "sunhpc/pkg/logger"
@@ -20,21 +23,49 @@ func NewRootCmd() *cobra.Command {
Use: "sunhpc", Use: "sunhpc",
Short: "SunHPC - HPC集群一体化运维工具", Short: "SunHPC - HPC集群一体化运维工具",
PersistentPreRun: func(cmd *cobra.Command, args []string) { PersistentPreRun: func(cmd *cobra.Command, args []string) {
// 加载全局配置(只加载一次) // 设置 CLI 参数
config.CLIParams.Verbose = verbose
config.CLIParams.NoColor = noColor
config.CLIParams.Config = cfgFile
// 初始化配置目录和默认文件
if err := config.InitConfigs(); err != nil {
fmt.Fprintf(os.Stderr, "初始化配置目录失败: %v\n", err)
os.Exit(1)
}
// 加载配置(后续调用直接返回缓存)
cfg, err := config.LoadConfig() cfg, err := config.LoadConfig()
if err != nil { if err != nil {
// 配置加载失败,使用默认日志配置初始化 fmt.Fprintf(os.Stderr, "加载配置失败: %v\n", err)
logger.Warnf("加载配置失败,使用默认日志配置: %v", err) os.Exit(1)
logger.Init(logger.LogConfig{}) }
// 初始化日志
logger.Init(cfg.Log)
// 记录启动信息
logger.Debugf("SunHPC 启动中...")
logger.Debugf("配置文件: %s", viper.ConfigFileUsed())
logger.Debugf("日志级别: %s", cfg.Log.Level)
logger.Debugf("日志格式: %s", cfg.Log.Format)
logger.Debugf("日志输出: %s", cfg.Log.Output)
logger.Debugf("日志文件: %s", cfg.Log.LogFile)
logger.Debugf("显示颜色: %v", cfg.Log.ShowColor)
// 跳过数据库检查,仅在 init db 时检查
if isInitDbCommand(cmd) {
return return
} }
// 3. 初始化全局日志(全局只执行一次) // 检查数据库文件是否存在
logger.Init(logger.LogConfig{ dbPath := filepath.Join(cfg.Database.Path, cfg.Database.Name)
Verbose: cfg.Log.Verbose, if _, err := os.Stat(dbPath); os.IsNotExist(err) {
ShowColor: !cfg.Log.ShowColor, logger.Warnf("数据库文件不存在: %s", dbPath)
LogFile: cfg.Log.LogFile, logger.Errorf("请先运行 sunhpc init db 初始化数据库")
}) os.Exit(1)
}
logger.Debugf("数据库文件存在: %s", dbPath)
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
@@ -42,23 +73,17 @@ func NewRootCmd() *cobra.Command {
}, },
} }
cmd.PersistentFlags().StringVarP( cmd.PersistentFlags().BoolVar(
&config.CLIParams.Config, &verbose, "verbose", false,
"config", "c", "启用详细日志输出")
"", "配置文件路径 (默认:/etc/sunhpc/config.yaml)")
cmd.PersistentFlags().BoolVarP(
&config.CLIParams.Verbose,
"verbose", "v", false, "启用详细日志输出")
cmd.PersistentFlags().BoolVar( cmd.PersistentFlags().BoolVar(
&config.CLIParams.NoColor, &noColor, "no-color", false,
"no-color", false, "禁用彩色输出") "禁用彩色输出")
// 如果指定了 --config 参数,优先使用该配置文件 cmd.PersistentFlags().StringVar(
if config.CLIParams.Config != "" { &cfgFile, "config", "",
viper.SetConfigFile(config.CLIParams.Config) "配置文件路径 (默认:/etc/sunhpc/config.yaml)")
}
cmd.AddCommand(initcmd.NewInitCmd()) cmd.AddCommand(initcmd.NewInitCmd())
return cmd return cmd
@@ -67,3 +92,14 @@ func NewRootCmd() *cobra.Command {
func Execute() error { func Execute() error {
return NewRootCmd().Execute() return NewRootCmd().Execute()
} }
func isInitDbCommand(cmd *cobra.Command) bool {
// 检查当前命令是否是 db 子命令
if cmd.Name() == "db" {
// 检查父命令是否是 init
if parent := cmd.Parent(); parent != nil {
return parent.Name() == "init"
}
}
return false
}

View File

@@ -4,70 +4,145 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"sunhpc/pkg/logger"
"sunhpc/pkg/utils"
"sync"
"github.com/spf13/viper" "github.com/spf13/viper"
"go.yaml.in/yaml/v3" "go.yaml.in/yaml/v3"
) )
// ==============================================================
// 全局变量
// ==============================================================
var (
GlobalConfig *Config
configOnce sync.Once // 确保配置只加载一次
configMutex sync.RWMutex // 读写锁保护 GlobalConfig
)
// ==============================================================
// 目录常量 (可在 main.go 中通过 flag 覆盖默认值)
// ==============================================================
var (
BaseDir string = utils.DefaultBaseDir
TmplDir string = utils.DefaultTmplDir
LogDir string = utils.DefaultLogDir
)
// ==============================================================
// 配置结构体
// ==============================================================
type Config struct { type Config struct {
Database DatabaseConfig `yaml:"database"` Log logger.LogConfig `mapstructure:"log" yaml:"log"`
Log LogConfig `yaml:"log"` Database DatabaseConfig `mapstructure:"database" yaml:"database"`
} }
type DatabaseConfig struct { type DatabaseConfig struct {
DSN string `yaml:"dsn"` // 数据库连接字符串 DSN string `mapstructure:"dsn" yaml:"dsn"` // 数据库连接字符串
Path string `yaml:"path"` // SQLite: 目录路径 Path string `mapstructure:"path" yaml:"path"` // SQLite: 目录路径
Name string `yaml:"name"` // SQLite: 文件名 Name string `mapstructure:"name" yaml:"name"` // SQLite: 文件名
Args string `mapstructure:"args" yaml:"args"` // 数据库连接参数
} }
type LogConfig struct { type CLIParamsType struct {
Level string `yaml:"level"` Verbose bool // -v/--verbose
Format string `yaml:"format"` NoColor bool // --no-color
Output string `yaml:"output"` Config string // -c/--config
Verbose bool `yaml:"verbose"`
LogFile string `yaml:"log_file"`
ShowColor bool `yaml:"show_color"`
} }
// --------------------------------- 全局单例配置(核心) --------------------------------- var CLIParams CLIParamsType // 命令行参数
var (
// GlobalConfig 全局配置单例实例 // InitConfigs 初始化所有配置目录等数据
GlobalConfig *Config func InitConfigs() error {
// 命令行参数配置(全局、由root命令绑定) dirs := []string{
CLIParams = struct { BaseDir,
Verbose bool // -v/--verbose TmplDir,
NoColor bool // --no-color LogDir,
Config string // -c/--config }
}{} for _, d := range dirs {
BaseDir string = "/etc/sunhpc" if err := os.MkdirAll(d, 0755); err != nil {
LogDir string = "/var/log/sunhpc" return fmt.Errorf("创建目录 %s 失败: %w", d, err)
TmplDir string = BaseDir + "/tmpl.d" }
appName string = "sunhpc" }
defaultDBPath string = "/var/lib/sunhpc"
defaultDBName string = "sunhpc.db" // 检查配置文件是否存在,不存在则创建默认配置
) configPath := filepath.Join(BaseDir, "sunhpc.yaml")
if _, err := os.Stat(configPath); os.IsNotExist(err) {
if err := createDefaultConfig(configPath); err != nil {
return fmt.Errorf("创建默认配置文件失败: %w", err)
}
}
return nil
}
func createDefaultConfig(configPath string) error {
defaultConfig := &Config{
Database: DatabaseConfig{
Path: utils.DefaultDBPath,
Name: utils.DefaultDBName,
Args: utils.DefaultDBArgs,
},
Log: logger.LogConfig{
Level: "info",
Format: "text",
Output: "stdout",
Verbose: false,
LogFile: utils.DefaultLogFile,
ShowColor: true,
},
}
// 确保数据库目录存在
if err := os.MkdirAll(defaultConfig.Database.Path, 0755); err != nil {
return fmt.Errorf("创建数据库目录失败: %w", err)
}
// 序列号并写入
data, err := yaml.Marshal(defaultConfig)
if err != nil {
return fmt.Errorf("序列化配置失败: %w", err)
}
return os.WriteFile(configPath, data, 0644)
}
// ----------------------------------- 配置加载(只加载一次) ----------------------------------- // ----------------------------------- 配置加载(只加载一次) -----------------------------------
func LoadConfig() (*Config, error) { func LoadConfig() (*Config, error) {
// 如果已经加载过,直接返回 configMutex.RLock()
if GlobalConfig != nil { if GlobalConfig != nil {
// 如果已经加载过,直接返回
configMutex.RUnlock()
return GlobalConfig, nil
}
configMutex.RUnlock()
configMutex.Lock()
defer configMutex.Unlock()
// 双重检查
if GlobalConfig != nil {
// 如果已经加载过,直接返回
return GlobalConfig, nil return GlobalConfig, nil
} }
viper.SetConfigName("sunhpc") // 配置文件路径
viper.SetConfigType("yaml") if CLIParams.Config != "" {
viper.AddConfigPath(BaseDir) viper.SetConfigFile(CLIParams.Config)
viper.AddConfigPath(".") } else {
viper.AddConfigPath(filepath.Join(os.Getenv("HOME"), ".")) viper.SetConfigName("sunhpc")
viper.SetConfigType("yaml")
viper.AddConfigPath(BaseDir)
viper.AddConfigPath(".")
viper.AddConfigPath(filepath.Join(os.Getenv("HOME"), "."))
}
// Step 1: 设置默认值(最低优先级) // Step 1: 设置默认值(最低优先级)
viper.SetDefault("log.level", "info") viper.SetDefault("log.level", "info")
viper.SetDefault("log.format", "text") viper.SetDefault("log.format", "text")
viper.SetDefault("log.output", "stdout") viper.SetDefault("log.output", "stdout")
viper.SetDefault("log.verbose", false) viper.SetDefault("log.verbose", false)
viper.SetDefault("log.log_file", filepath.Join(LogDir, "sunhpc.log")) viper.SetDefault("log.log_file", utils.DefaultLogFile)
viper.SetDefault("database.name", "sunhpc.db") viper.SetDefault("database.name", utils.DefaultDBName)
viper.SetDefault("database.path", "/var/lib/sunhpc") viper.SetDefault("database.path", utils.DefaultDBPath)
if err := viper.ReadInConfig(); err != nil { if err := viper.ReadInConfig(); err != nil {
// 配置文件不存在时,使用默认值 // 配置文件不存在时,使用默认值
@@ -87,10 +162,12 @@ func LoadConfig() (*Config, error) {
viper.Set("log.show_color", false) viper.Set("log.show_color", false)
} }
fullPath := filepath.Join( // 计算派生配置 (如数据库 DSN)
viper.GetString("database.path"), viper.GetString("database.name")) dbPath := viper.GetString("database.path")
dsn := fmt.Sprintf( dbName := viper.GetString("database.name")
"%s?_foreign_keys=on&_journal_mode=WAL&_timeout=5000", fullPath) dbArgs := viper.GetString("database.args")
fullPath := filepath.Join(dbPath, dbName)
dsn := fmt.Sprintf("%s?%s", fullPath, dbArgs)
viper.Set("database.dsn", dsn) viper.Set("database.dsn", dsn)
// 解码到结构体 // 解码到结构体
@@ -104,57 +181,23 @@ func LoadConfig() (*Config, error) {
return GlobalConfig, nil return GlobalConfig, nil
} }
// InitDirs 创建所有必需目录 // ==============================================================
func InitDirs() error { // SaveConfig - 保存全局配置到文件、运行时配置
dirs := []string{ // ==============================================================
BaseDir, func SaveConfig() error {
TmplDir, configMutex.RLock()
LogDir, defer configMutex.RUnlock()
}
for _, d := range dirs {
if err := os.MkdirAll(d, 0755); err != nil {
return fmt.Errorf("创建目录 %s 失败: %w", d, err)
}
}
return nil
}
func (c *Config) WriteDefaultConfig(path string) error { if GlobalConfig == nil {
// 确保目录存在 return fmt.Errorf("全局配置为空")
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("创建目录失败: %w", err)
} }
// 生成默认配置 configPath := filepath.Join(BaseDir, "config.yaml")
cfg := DefaultConfig(path) data, err := yaml.Marshal(GlobalConfig)
// 序列化为 YAML
data, err := yaml.Marshal(cfg)
if err != nil { if err != nil {
return fmt.Errorf("序列化配置失败: %w", err) return err
}
// 写入文件0644 权限)
return os.WriteFile(path, data, 0644)
}
func DefaultConfig(path string) *Config {
return &Config{
Database: DatabaseConfig{
DSN: fmt.Sprintf("%s?_foreign_keys=on&_journal_mode=WAL&_timeout=5000",
filepath.Join(filepath.Dir(path), defaultDBName)),
Path: filepath.Dir(path),
Name: defaultDBName,
},
Log: LogConfig{
Level: "info",
Format: "text",
Output: "stdout",
LogFile: filepath.Join(filepath.Dir(path), "sunhpc.log"),
Verbose: false,
},
} }
return os.WriteFile(configPath, data, 0644)
} }
// ResetConfig 重置全局配置为默认值 // ResetConfig 重置全局配置为默认值

View File

@@ -5,7 +5,6 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"os" "os"
"path/filepath"
"strings" "strings"
"sync" "sync"
@@ -16,65 +15,57 @@ import (
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
) )
type DB struct { // =========================================================
db *sql.DB // 全局变量
logger logger.Logger // =========================================================
}
var ( var (
dbInstance *DB dbInstance *sql.DB
dbOnce sync.Once dbOnce sync.Once
dbMutex sync.RWMutex
dbErr error dbErr error
) )
func GetInstance(dbConfig *config.DatabaseConfig, log logger.Logger) (*DB, error) { // =========================================================
// GetDB - 获取数据库连接(单例模式)
// =========================================================
func GetDB() (*sql.DB, error) {
dbOnce.Do(func() { dbOnce.Do(func() {
// 兜底: 未注入则使用全局默认日志实例 if dbInstance != nil {
if log == nil {
log = logger.DefaultLogger
}
log.Debugf("开始初始化数据库,路径: %s", dbConfig.Path)
// 确认数据库目录存在
if err := os.MkdirAll(dbConfig.Path, 0755); err != nil {
log.Errorf("创建数据库目录失败: %v", err)
dbErr = err
return return
} }
fullPath := filepath.Join(dbConfig.Path, dbConfig.Name) // 确保配置已加载
log.Debugf("数据库路径: %s", fullPath) cfg, err := config.LoadConfig()
if err != nil {
dbErr = fmt.Errorf("加载配置失败: %w", err)
return
}
// 构建DSN // 构建DSN
dsn := fmt.Sprintf("%s?_foreign_keys=on&_journal_mode=WAL&_timeout=5000&cache=shared", logger.Debugf("DSN: %s", cfg.Database.DSN)
fullPath)
log.Debugf("DSN: %s", dsn)
// 打开SQLite 连接 // 打开SQLite 连接
sqlDB, err := sql.Open("sqlite3", dsn) sqlDB, err := sql.Open("sqlite3", cfg.Database.DSN)
if err != nil { if err != nil {
log.Errorf("数据库打开失败: %v", err) dbErr = fmt.Errorf("数据库打开失败: %w", err)
dbErr = err
return return
} }
// 设置连接池参数 // 设置连接池参数
sqlDB.SetMaxOpenConns(1) // SQLite 只支持单连接 sqlDB.SetMaxOpenConns(10) // 最大打开连接
sqlDB.SetMaxIdleConns(1) // 保持一个空闲连接 sqlDB.SetMaxIdleConns(5) // 保持空闲连接
sqlDB.SetConnMaxLifetime(0) // 禁用连接生命周期超时 sqlDB.SetConnMaxLifetime(0) // 禁用连接生命周期超时
sqlDB.SetConnMaxIdleTime(0) // 禁用空闲连接超时 sqlDB.SetConnMaxIdleTime(0) // 禁用空闲连接超时
// 测试数据库连接 // 测试数据库连接
if err := sqlDB.Ping(); err != nil { if err := sqlDB.Ping(); err != nil {
sqlDB.Close() sqlDB.Close()
log.Errorf("数据库连接失败: %v", err) dbErr = fmt.Errorf("数据库连接失败: %w", err)
dbErr = err
return return
} }
// 赋值 *DB 类型的单例(而非直接复制 *sql.DB) logger.Debug("数据库连接成功")
log.Info("数据库连接成功") dbInstance = sqlDB
dbInstance = &DB{sqlDB, log}
}) })
if dbErr != nil { if dbErr != nil {
@@ -84,21 +75,6 @@ func GetInstance(dbConfig *config.DatabaseConfig, log logger.Logger) (*DB, error
return dbInstance, nil return dbInstance, nil
} }
// Close 关闭数据库连接
func (d *DB) Close() error {
if d.db != nil {
d.logger.Debug("关闭数据库连接")
err := d.db.Close()
if err != nil {
d.logger.Errorf("数据库连接关闭失败: %v", err)
return err
}
d.logger.Info("数据库连接关闭成功")
return nil
}
return nil
}
func confirmAction(prompt string) bool { func confirmAction(prompt string) bool {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
@@ -112,38 +88,38 @@ func confirmAction(prompt string) bool {
return response == "y" || response == "yes" return response == "y" || response == "yes"
} }
func (d *DB) InitTables(force bool) error { func InitTables(db *sql.DB, force bool) error {
d.logger.Info("开始初始化数据库表...")
if force { if force {
// 确认是否强制删除 // 确认是否强制删除
if !confirmAction("确认强制删除所有表和触发器?") { if !confirmAction("确认强制删除所有表和触发器?") {
d.logger.Info("操作已取消") logger.Info("操作已取消")
db.Close()
os.Exit(0) os.Exit(0)
return nil return nil
} }
// 强制删除所有表和触发器 // 强制删除所有表和触发器
d.logger.Debug("强制删除所有表和触发器...") logger.Debug("强制删除所有表和触发器...")
if err := dropTables(d.db); err != nil { if err := dropTables(db); err != nil {
return fmt.Errorf("删除表失败: %w", err) return fmt.Errorf("删除表失败: %w", err)
} }
d.logger.Debug("删除所有表和触发器成功") logger.Debug("删除所有表和触发器成功")
if err := dropTriggers(d.db); err != nil { if err := dropTriggers(db); err != nil {
return fmt.Errorf("删除触发器失败: %w", err) return fmt.Errorf("删除触发器失败: %w", err)
} }
d.logger.Debug("删除所有触发器成功") logger.Debug("删除所有触发器成功")
} }
// ✅ 调用 schema.go 中的函数 // ✅ 调用 schema.go 中的函数
for _, ddl := range CreateTableStatements() { for _, ddl := range CreateTableStatements() {
d.logger.Debugf("执行: %s", ddl) logger.Debugf("执行: %s", ddl)
if _, err := d.db.Exec(ddl); err != nil { if _, err := db.Exec(ddl); err != nil {
return fmt.Errorf("数据表创建失败: %w", err) return fmt.Errorf("数据表创建失败: %w", err)
} }
} }
d.logger.Info("数据库表创建成功") logger.Info("数据库表创建成功")
/* /*
使用sqlite3命令 测试数据库是否存在表 使用sqlite3命令 测试数据库是否存在表
✅ 查询所有表 ✅ 查询所有表
@@ -174,3 +150,65 @@ func dropTriggers(db *sql.DB) error {
} }
return nil return nil
} }
func CloseDB() error {
dbMutex.Lock()
defer dbMutex.Unlock()
if dbInstance == nil {
if err := dbInstance.Close(); err != nil {
return err
}
dbInstance = nil
}
return nil
}
// 使用事务回滚测试
func RunTestWithRollback(db *sql.DB, testFunc func(*sql.Tx) error) error {
tx, err := db.Begin()
if err != nil {
return err
}
// 执行测试
if err := testFunc(tx); err != nil {
tx.Rollback()
return err
}
// 回滚事务,所有更改(包括 ID 递增)都会撤销
return tx.Rollback()
}
// 使用示例
func TestNodeInsert(db *sql.DB) error {
logger.Debug("测试数据插入...")
return RunTestWithRollback(db, func(tx *sql.Tx) error {
// 插入测试数据
logger.Debug("执行插入测试数据...")
_, err := tx.Exec(`
INSERT INTO nodes (name, cpus, rack, rank)
VALUES (?, ?, ?, ?)
`, "test-node", 64, 1, 1)
if err != nil {
return err
}
// 验证插入
var count int
logger.Debug("执行查询测试数据...")
err = tx.QueryRow(`
SELECT COUNT(*) FROM nodes WHERE name = ?
`, "test-node").Scan(&count)
if err != nil {
return err
}
logger.Infof("测试数据插入成功,共 %d 条", count)
// 不需要手动删除,回滚会自动撤销
return nil
})
}

View File

@@ -67,21 +67,25 @@ var (
// DefaultLogger 全局默认日志实例(所有模块可直接用,也可注入自定义实现) // DefaultLogger 全局默认日志实例(所有模块可直接用,也可注入自定义实现)
DefaultLogger Logger DefaultLogger Logger
// once 保证日志只初始化一次 // once 保证日志只初始化一次
once sync.Once initOnce sync.Once
) )
// LogConfig 日志配置结构体和项目的config包对齐 // LogConfig 日志配置结构体和项目的config包对齐
type LogConfig struct { type LogConfig struct {
Verbose bool // 是否开启详细模式Debug级别 Level string `mapstructure:"level" yaml:"level"`
Level string // 日志级别debug/info/warn/error Format string `mapstructure:"format" yaml:"format"`
ShowColor bool // 是否显示彩色输出 Output string `mapstructure:"output" yaml:"output"`
LogFile string // 日志文件路径(可选,空则只输出到控制台) Verbose bool `mapstructure:"verbose" yaml:"verbose"`
LogFile string `mapstructure:"log_file" yaml:"log_file"`
ShowColor bool `mapstructure:"show_color" yaml:"show_color"`
} }
// 默认配置 // 默认配置
var defaultConfig = LogConfig{ var defaultConfig = LogConfig{
Verbose: false,
Level: "info", Level: "info",
Format: "text",
Output: "stdout",
Verbose: false,
ShowColor: true, ShowColor: true,
LogFile: "/var/log/sunhpc/sunhpc.log", LogFile: "/var/log/sunhpc/sunhpc.log",
} }
@@ -113,6 +117,9 @@ func (f *CustomFormatter) Format(entry *logrus.Entry) ([]byte, error) {
colorReset = "" colorReset = ""
} }
// Debug,打印原始字节,用于调试
// fmt.Printf("%q\n", entry.Message)
// 拼接格式: // 拼接格式:
// 灰色日期 + 空格 + 带颜色的[级别] + 空格 + 日志内容 + 空格 + 灰色文件行号 + 重置 // 灰色日期 + 空格 + 带颜色的[级别] + 空格 + 日志内容 + 空格 + 灰色文件行号 + 重置
fmt.Fprintf(&buf, "%s%s%s %s[%s]%s %s %s%s:%d%s\n", fmt.Fprintf(&buf, "%s%s%s %s[%s]%s %s %s%s:%d%s\n",
@@ -150,27 +157,6 @@ func getLevelInfo(level logrus.Level) (string, string) {
} }
} }
// getCallerInfo 获取调用日志的文件和行号跳过logrus内部调用
func _getCallerInfo() (string, int) {
// 跳过的调用栈深度根据实际情况调整这里跳过logrus和logger包的调用
skip := 6
pc, file, line, ok := runtime.Caller(skip)
if !ok {
return "unknown.go", 0
}
// 只保留文件名(如 db.go去掉完整路径
fileName := filepath.Base(file)
// 过滤logrus内部调用可选
funcName := runtime.FuncForPC(pc).Name()
if funcName == "" || filepath.Base(funcName) == "logrus" {
return getCallerInfoWithSkip(skip + 1)
}
return fileName, line
}
func getCallerInfo() (string, int) { func getCallerInfo() (string, int) {
// 从当前调用开始,逐层向上查找 // 从当前调用开始,逐层向上查找
for i := 2; i < 15; i++ { // i从2开始跳过getCallerInfo自身 for i := 2; i < 15; i++ { // i从2开始跳过getCallerInfo自身
@@ -203,8 +189,7 @@ func shouldSkipPackage(funcName, file string) bool {
} }
// 跳过logger包你自己的包装包 // 跳过logger包你自己的包装包
if strings.Contains(funcName, "your/package/logger") || if strings.Contains(file, "/logger/") {
strings.Contains(file, "logger") {
return true return true
} }
@@ -227,21 +212,14 @@ func getCallerInfoWithSkip(skip int) (string, int) {
// Init 初始化全局默认日志实例(全局只执行一次) // Init 初始化全局默认日志实例(全局只执行一次)
func Init(cfg LogConfig) { func Init(cfg LogConfig) {
once.Do(func() { initOnce.Do(func() {
// 合并配置:传入的配置为空则用默认值
if cfg.Level == "" {
cfg.Level = defaultConfig.Level
}
if cfg.LogFile == "" {
cfg.LogFile = defaultConfig.LogFile
}
// 1. 创建logrus实例 // 1. 创建logrus实例
logrusInst := logrus.New() logrusInst := logrus.New()
// 2. 配置输出(控制台 + 文件,可选) // 2. 配置输出(控制台 + 文件,可选)
var outputs []io.Writer var outputs []io.Writer
outputs = append(outputs, os.Stdout) // 控制台输出 outputs = append(outputs, os.Stdout) // 控制台输出
// 如果配置了日志文件,添加文件输出 // 如果配置了日志文件,添加文件输出
if cfg.LogFile != "" { if cfg.LogFile != "" {
// 确保日志目录存在 // 确保日志目录存在

View File

@@ -31,3 +31,23 @@ func GenerateID() (string, error) {
func GetTimestamp() string { func GetTimestamp() string {
return time.Now().Format("2006-01-02 15:04:05") return time.Now().Format("2006-01-02 15:04:05")
} }
// 定义短语
const (
NoAvailableNetworkInterfaces = "No available network interfaces"
)
// 定义目录
const (
DefaultBaseDir string = "/etc/sunhpc"
DefaultTmplDir string = DefaultBaseDir + "/tmpl.d"
DefaultLogDir string = "/var/log/sunhpc"
DefaultLogFile string = DefaultLogDir + "/sunhpc.log"
)
// 定义数据库
const (
DefaultDBName string = "sunhpc.db"
DefaultDBPath string = "/var/lib/sunhpc"
DefaultDBArgs string = "_foreign_keys=on&_journal_mode=WAL&_timeout=5000"
)

View File

@@ -3,8 +3,10 @@ package wizard
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net"
"os" "os"
"path/filepath" "path/filepath"
"sunhpc/pkg/utils"
"github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/textinput"
) )
@@ -66,31 +68,30 @@ func (m *model) saveCurrentPage() {
func (m *model) saveDataPage() { func (m *model) saveDataPage() {
if len(m.textInputs) >= 8 { if len(m.textInputs) >= 8 {
m.config.Hostname = m.textInputs[0].Value() m.config.HomePage = m.textInputs[0].Value()
m.config.Country = m.textInputs[1].Value() m.config.Hostname = m.textInputs[1].Value()
m.config.Region = m.textInputs[2].Value() m.config.Country = m.textInputs[2].Value()
m.config.Timezone = m.textInputs[3].Value() m.config.Region = m.textInputs[3].Value()
m.config.HomePage = m.textInputs[4].Value() m.config.Timezone = m.textInputs[4].Value()
m.config.DBAddress = m.textInputs[5].Value() m.config.DBAddress = m.textInputs[5].Value()
m.config.DBName = m.textInputs[6].Value() m.config.DataAddress = m.textInputs[6].Value()
m.config.DataAddress = m.textInputs[7].Value()
} }
} }
func (m *model) savePublicNetworkPage() { func (m *model) savePublicNetworkPage() {
if len(m.textInputs) >= 4 { if len(m.textInputs) >= 4 {
m.config.PublicInterface = m.textInputs[0].Value() m.config.PublicInterface = m.textInputs[0].Value()
m.config.IPAddress = m.textInputs[1].Value() m.config.PublicIPAddress = m.textInputs[1].Value()
m.config.Netmask = m.textInputs[2].Value() m.config.PublicNetmask = m.textInputs[2].Value()
m.config.Gateway = m.textInputs[3].Value() m.config.PublicGateway = m.textInputs[3].Value()
} }
} }
func (m *model) saveInternalNetworkPage() { func (m *model) saveInternalNetworkPage() {
if len(m.textInputs) >= 3 { if len(m.textInputs) >= 3 {
m.config.InternalInterface = m.textInputs[0].Value() m.config.InternalInterface = m.textInputs[0].Value()
m.config.InternalIP = m.textInputs[1].Value() m.config.InternalIPAddress = m.textInputs[1].Value()
m.config.InternalMask = m.textInputs[2].Value() m.config.InternalNetmask = m.textInputs[2].Value()
} }
} }
@@ -108,20 +109,18 @@ func (m *model) initPageInputs() {
switch m.currentPage { switch m.currentPage {
case PageData: case PageData:
fields := []struct{ label, value string }{ fields := []struct{ label, value string }{
{"主机名:", m.config.Hostname}, {"Homepage", m.config.HomePage},
{"国家:", m.config.Country}, {"Hostname", m.config.Hostname},
{"地区:", m.config.Region}, {"Country", m.config.Country},
{"时区:", m.config.Timezone}, {"Region", m.config.Region},
{"主页:", m.config.HomePage}, {"Timezone", m.config.Timezone},
{"数据库地址:", m.config.DBAddress}, {"DB Path", m.config.DBAddress},
{"数据库名称:", m.config.DBName}, {"Software", m.config.DataAddress},
{"Data 地址:", m.config.DataAddress},
} }
for _, f := range fields { for _, f := range fields {
ti := textinput.New() ti := textinput.New()
ti.Placeholder = "" ti.Placeholder = ""
ti.Placeholder = f.label ti.Placeholder = f.label
//ti.Placeholder = "请输入" + f.label[:len(f.label)-1]
ti.SetValue(f.value) ti.SetValue(f.value)
ti.Width = 50 ti.Width = 50
m.textInputs = append(m.textInputs, ti) m.textInputs = append(m.textInputs, ti)
@@ -130,18 +129,19 @@ func (m *model) initPageInputs() {
if len(m.textInputs) > 0 { if len(m.textInputs) > 0 {
m.textInputs[0].Focus() m.textInputs[0].Focus()
} }
m.inputLabels = []string{"Hostname", "Country", "Region", "Timezone", "Homepage", "DBPath", "DBName", "Software"} m.inputLabels = []string{"Homepage", "Hostname", "Country", "Region", "Timezone", "DBPath", "Software"}
case PagePublicNetwork: case PagePublicNetwork:
fields := []struct{ label, value string }{ fields := []struct{ label, value string }{
{"公网接口:", m.config.PublicInterface}, {"Public Interface", m.config.PublicInterface},
{"IP 地址:", m.config.IPAddress}, {"Public IP Address", m.config.PublicIPAddress},
{"子网掩码:", m.config.Netmask}, {"Public Netmask", m.config.PublicNetmask},
{"网关:", m.config.Gateway}, {"Public Gateway", m.config.PublicGateway},
} }
for _, f := range fields { for _, f := range fields {
ti := textinput.New() ti := textinput.New()
ti.Placeholder = "" ti.Placeholder = ""
ti.Placeholder = f.label
ti.SetValue(f.value) ti.SetValue(f.value)
ti.Width = 50 ti.Width = 50
m.textInputs = append(m.textInputs, ti) m.textInputs = append(m.textInputs, ti)
@@ -150,17 +150,17 @@ func (m *model) initPageInputs() {
if len(m.textInputs) > 0 { if len(m.textInputs) > 0 {
m.textInputs[0].Focus() m.textInputs[0].Focus()
} }
m.inputLabels = []string{"Wan iface", "IPAddress", "Netmask", "Gateway"} m.inputLabels = []string{"Public Interface", "Public IP Address", "Public Netmask", "Public Gateway"}
case PageInternalNetwork: case PageInternalNetwork:
fields := []struct{ label, value string }{ fields := []struct{ label, value string }{
{"内网接口:", m.config.InternalInterface}, {"Internal Interface", m.config.InternalInterface},
{"内网 IP:", m.config.InternalIP}, {"Internal IP Address", m.config.InternalIPAddress},
{"内网掩码:", m.config.InternalMask}, {"Internal Netmask", m.config.InternalNetmask},
} }
for _, f := range fields { for _, f := range fields {
ti := textinput.New() ti := textinput.New()
ti.Placeholder = "" ti.Placeholder = f.label
ti.SetValue(f.value) ti.SetValue(f.value)
ti.Width = 50 ti.Width = 50
m.textInputs = append(m.textInputs, ti) m.textInputs = append(m.textInputs, ti)
@@ -169,16 +169,16 @@ func (m *model) initPageInputs() {
if len(m.textInputs) > 0 { if len(m.textInputs) > 0 {
m.textInputs[0].Focus() m.textInputs[0].Focus()
} }
m.inputLabels = []string{"Lan iface", "IPAddress", "Netmask"} m.inputLabels = []string{"Internal Interface", "Internal IP", "Internal Mask"}
case PageDNS: case PageDNS:
fields := []struct{ label, value string }{ fields := []struct{ label, value string }{
{" DNS:", m.config.DNSPrimary}, {"Primary DNS", m.config.DNSPrimary},
{" DNS:", m.config.DNSSecondary}, {"Secondary DNS", m.config.DNSSecondary},
} }
for _, f := range fields { for _, f := range fields {
ti := textinput.New() ti := textinput.New()
ti.Placeholder = "" ti.Placeholder = f.label
ti.SetValue(f.value) ti.SetValue(f.value)
ti.Width = 50 ti.Width = 50
m.textInputs = append(m.textInputs, ti) m.textInputs = append(m.textInputs, ti)
@@ -187,6 +187,31 @@ func (m *model) initPageInputs() {
if len(m.textInputs) > 0 { if len(m.textInputs) > 0 {
m.textInputs[0].Focus() m.textInputs[0].Focus()
} }
m.inputLabels = []string{"DNSPrimary", "DNSSecondary"} m.inputLabels = []string{"Pri DNS", "Sec DNS"}
} }
} }
// 获取系统网络接口
func getNetworkInterfaces() []string {
// 实现获取系统网络接口的逻辑
// 例如:使用 net.Interface() 函数获取系统网络接口
// 返回一个字符串切片,包含系统网络接口的名称
interfaces, err := net.Interfaces()
if err != nil {
return []string{utils.NoAvailableNetworkInterfaces}
}
var result []string
for _, iface := range interfaces {
// 跳过 loopback 接口
if iface.Flags&net.FlagLoopback != 0 {
continue
}
result = append(result, iface.Name)
}
if len(result) == 0 {
return []string{utils.NoAvailableNetworkInterfaces}
}
return result
}

View File

@@ -1,6 +1,8 @@
package wizard package wizard
import ( import (
"fmt"
"github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
@@ -17,19 +19,18 @@ type Config struct {
Timezone string `json:"timezone"` Timezone string `json:"timezone"`
HomePage string `json:"homepage"` HomePage string `json:"homepage"`
DBAddress string `json:"db_address"` DBAddress string `json:"db_address"`
DBName string `json:"db_name"`
DataAddress string `json:"data_address"` DataAddress string `json:"data_address"`
// 公网设置 // 公网设置
PublicInterface string `json:"public_interface"` PublicInterface string `json:"public_interface"`
IPAddress string `json:"ip_address"` PublicIPAddress string `json:"ip_address"`
Netmask string `json:"netmask"` PublicNetmask string `json:"netmask"`
Gateway string `json:"gateway"` PublicGateway string `json:"gateway"`
// 内网配置 // 内网配置
InternalInterface string `json:"internal_interface"` InternalInterface string `json:"internal_interface"`
InternalIP string `json:"internal_ip"` InternalIPAddress string `json:"internal_ip"`
InternalMask string `json:"internal_mask"` InternalNetmask string `json:"internal_mask"`
// DNS 配置 // DNS 配置
DNSPrimary string `json:"dns_primary"` DNSPrimary string `json:"dns_primary"`
@@ -56,40 +57,60 @@ const (
// model TUI 主模型 // model TUI 主模型
type model struct { type model struct {
config Config config Config
currentPage PageType currentPage PageType
totalPages int totalPages int
textInputs []textinput.Model networkInterfaces []string // 所有系统网络接口
inputLabels []string // 存储标签 textInputs []textinput.Model
focusIndex int inputLabels []string // 存储标签
focusType int // 0=输入框, 1=上一步按钮, 2=下一步按钮 focusIndex int
agreementIdx int // 0=拒绝1=接受 focusType int // 0=输入框, 1=上一步按钮, 2=下一步按钮
width int agreementIdx int // 0=拒绝1=接受
height int width int
err error height int
quitting bool err error
done bool quitting bool
force bool done bool
force bool
} }
// defaultConfig 返回默认配置 // defaultConfig 返回默认配置
func defaultConfig() Config { func defaultConfig() Config {
var (
defaultPublicInterface string
defaultInternalInterface string
)
interfaces := getNetworkInterfaces()
switch len(interfaces) {
case 0:
defaultPublicInterface = ""
defaultInternalInterface = ""
case 1:
defaultPublicInterface = interfaces[0]
defaultInternalInterface = fmt.Sprintf("%s:0", interfaces[0])
case 2:
defaultPublicInterface = interfaces[0]
defaultInternalInterface = interfaces[1]
default:
defaultPublicInterface = interfaces[0]
defaultInternalInterface = interfaces[1]
}
return Config{ return Config{
Hostname: "sunhpc01", Hostname: "cluster.hpc.org",
Country: "China", Country: "China",
Region: "Beijing", Region: "Beijing",
Timezone: "Asia/Shanghai", Timezone: "Asia/Shanghai",
HomePage: "https://sunhpc.example.com", HomePage: "www.sunhpc.com",
DBAddress: "127.0.0.1", DBAddress: "/var/lib/sunhpc/sunhpc.db",
DBName: "sunhpc_db", DataAddress: "/export/sunhpc",
DataAddress: "/data/sunhpc", PublicInterface: defaultPublicInterface,
PublicInterface: "eth0", PublicIPAddress: "",
InternalInterface: "eth1", PublicNetmask: "",
IPAddress: "192.168.1.100", PublicGateway: "",
Netmask: "255.255.255.0", InternalInterface: defaultInternalInterface,
Gateway: "192.168.1.1", InternalIPAddress: "172.16.9.254",
InternalIP: "10.0.0.100", InternalNetmask: "255.255.255.0",
InternalMask: "255.255.255.0",
DNSPrimary: "8.8.8.8", DNSPrimary: "8.8.8.8",
DNSSecondary: "8.8.4.4", DNSSecondary: "8.8.4.4",
} }

View File

@@ -45,43 +45,39 @@ func (m model) View() string {
// agreementView 协议页面 // agreementView 协议页面
func (m model) agreementView() string { func (m model) agreementView() string {
title := titleStyle.Render("SunHPC 系统初始化向导") title := titleStyle.Render("SunHPC Software License Agreement")
subtitle := subTitleStyle.Render("请先阅读并同意以下协议")
agreement := agreementBox.Render(` agreement := agreementBox.Render(`
┌─────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────┐
│ SunHPC 软件许可协议 │ SunHPC License Agreement
└─────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────┘
1. License Grant
1. 许可授予 This software grants you a non-exclusive, non-transferable
本软件授予您非独占、不可转让的使用许可。 license to use it.
2. Disclaimer of Warranties
2. 使用限制 This software is provided "as is" without any warranties,
- 不得用于非法目的 whether express or implied.
- 不得反向工程或反编译 3. Limitation of Liability
- 不得移除版权标识 This software is provided without warranty of any kind,
either express or implied.
3. 免责声明 In no event shall the author be liable for any damages
本软件按"原样"提供,不提供任何明示或暗示的保证。 arising out of the use of this software.
4. Termination of Agreement
4. 责任限制 If you violate any of the terms of this agreement,
在任何情况下,作者不对因使用本软件造成的任何损失负责。 the license will automatically terminate.
5. 协议终止
如违反本协议条款,许可将自动终止。
─────────────────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────
请仔细阅读以上条款,点击"接受"表示您同意并遵守本协议。 PLEASE READ THE ABOVE TERMS CAREFULLY AND CLICK "ACCEPT"
TO AGREE AND FOLLOW THIS AGREEMENT.
─────────────────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────
`) `)
var acceptBtn, rejectBtn string var acceptBtn, rejectBtn string
if m.agreementIdx == 0 { if m.agreementIdx == 0 {
rejectBtn = selectedButton.Render(">> 拒绝 <<") rejectBtn = selectedButton.Render(">> Reject <<")
acceptBtn = selectedButton.Render(" 同意 ") acceptBtn = " Accept "
} else { } else {
rejectBtn = selectedButton.Render(" 拒绝 ") rejectBtn = " Reject "
acceptBtn = selectedButton.Render(">> 同意 <<") acceptBtn = selectedButton.Render(">> Accept <<")
} }
buttonGroup := lipgloss.JoinHorizontal( buttonGroup := lipgloss.JoinHorizontal(
@@ -92,11 +88,9 @@ func (m model) agreementView() string {
// debugInfo := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")). // debugInfo := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).
// Render(fmt.Sprintf("[DEBUG: idx=%d]", m.agreementIdx),) // Render(fmt.Sprintf("[DEBUG: idx=%d]", m.agreementIdx),)
hint := hintStyle.Render("使用 <- -> 或 Tab 选择,Enter 确认") hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
return lipgloss.JoinVertical(lipgloss.Center, return lipgloss.JoinVertical(lipgloss.Center,
title, "", title, "",
subtitle, "",
agreement, "", agreement, "",
buttonGroup, "", buttonGroup, "",
// debugInfo, "", // ✅ 显示调试信息 // debugInfo, "", // ✅ 显示调试信息
@@ -106,8 +100,7 @@ func (m model) agreementView() string {
// dataView 数据接收页面 // dataView 数据接收页面
func (m model) dataView() string { func (m model) dataView() string {
title := titleStyle.Render("集群基础配置") title := titleStyle.Render("Cluster Information")
subtitle := subTitleStyle.Render("请填写系统基本信息")
var inputs strings.Builder var inputs strings.Builder
for i, ti := range m.textInputs { for i, ti := range m.textInputs {
@@ -117,11 +110,9 @@ func (m model) dataView() string {
} }
buttons := m.renderNavButtons() buttons := m.renderNavButtons()
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
hint := hintStyle.Render("使用 Up/Down 或 Tab 切换、Enter 确认")
return lipgloss.JoinVertical(lipgloss.Center, return lipgloss.JoinVertical(lipgloss.Center,
title, "", title, "",
subtitle, "",
inputs.String(), "", inputs.String(), "",
buttons, "", buttons, "",
hint, hint,
@@ -130,25 +121,24 @@ func (m model) dataView() string {
// publicNetworkView 公网设置页面 // publicNetworkView 公网设置页面
func (m model) publicNetworkView() string { func (m model) publicNetworkView() string {
title := titleStyle.Render("公网配置") title := titleStyle.Render("Public Network Configuration")
subtitle := subTitleStyle.Render("请配置网络接口信息")
autoDetect := infoStyle.Render("[*] 自动检测网络接口: eth0, eth1, ens33") networkInterfaces := getNetworkInterfaces()
autoDetect := infoStyle.Render(
"[*] Auto Detect Network Interfaces: " + strings.Join(networkInterfaces, ", "))
var inputs strings.Builder var inputs strings.Builder
for i, ti := range m.textInputs { for i, ti := range m.textInputs {
info := fmt.Sprintf("%-10s|", m.inputLabels[i]) info := fmt.Sprintf("%-20s|", m.inputLabels[i])
input := inputBox.Render(info + ti.View()) input := inputBox.Render(info + ti.View())
inputs.WriteString(input + "\n") inputs.WriteString(input + "\n")
} }
buttons := m.renderNavButtons() buttons := m.renderNavButtons()
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
hint := hintStyle.Render("使用 Up/Down 或 Tab 切换、Enter 确认")
return lipgloss.JoinVertical(lipgloss.Center, return lipgloss.JoinVertical(lipgloss.Center,
title, "", title, "",
subtitle, "",
autoDetect, "", autoDetect, "",
inputs.String(), "", inputs.String(), "",
buttons, "", buttons, "",
@@ -158,22 +148,25 @@ func (m model) publicNetworkView() string {
// internalNetworkView 内网配置页面 // internalNetworkView 内网配置页面
func (m model) internalNetworkView() string { func (m model) internalNetworkView() string {
title := titleStyle.Render("内网配置") title := titleStyle.Render("Internal Network Configuration")
subtitle := subTitleStyle.Render("请配置内网信息")
networkInterfaces := getNetworkInterfaces()
autoDetect := infoStyle.Render(
"[*] Auto Detect Network Interfaces: " + strings.Join(networkInterfaces, ", "))
var inputs strings.Builder var inputs strings.Builder
for i, ti := range m.textInputs { for i, ti := range m.textInputs {
info := fmt.Sprintf("%-10s|", m.inputLabels[i]) info := fmt.Sprintf("%-20s|", m.inputLabels[i])
input := inputBox.Render(info + ti.View()) input := inputBox.Render(info + ti.View())
inputs.WriteString(input + "\n") inputs.WriteString(input + "\n")
} }
buttons := m.renderNavButtons() buttons := m.renderNavButtons()
hint := hintStyle.Render("使用 Up/Down Tab 切换、Enter 确认") hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
return lipgloss.JoinVertical(lipgloss.Center, return lipgloss.JoinVertical(lipgloss.Center,
title, "", title, "",
subtitle, "", autoDetect, "",
inputs.String(), "", inputs.String(), "",
buttons, "", buttons, "",
hint, hint,
@@ -182,8 +175,7 @@ func (m model) internalNetworkView() string {
// dnsView DNS 配置页面 // dnsView DNS 配置页面
func (m model) dnsView() string { func (m model) dnsView() string {
title := titleStyle.Render("DNS 配置") title := titleStyle.Render("DNS Configuration")
subtitle := subTitleStyle.Render("请配置 DNS 服务器")
var inputs strings.Builder var inputs strings.Builder
for i, ti := range m.textInputs { for i, ti := range m.textInputs {
@@ -193,12 +185,10 @@ func (m model) dnsView() string {
} }
buttons := m.renderNavButtons() buttons := m.renderNavButtons()
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
hint := hintStyle.Render("使用 Up/Down 或 Tab 切换、Enter 确认")
return lipgloss.JoinVertical(lipgloss.Center, return lipgloss.JoinVertical(lipgloss.Center,
title, "", title, "",
subtitle, "",
inputs.String(), "", inputs.String(), "",
buttons, "", buttons, "",
hint, hint,
@@ -207,71 +197,71 @@ func (m model) dnsView() string {
// summaryView 总结页面 // summaryView 总结页面
func (m model) summaryView() string { func (m model) summaryView() string {
title := titleStyle.Render("配置总结") title := titleStyle.Render("Summary")
subtitle := subTitleStyle.Render("请确认以下配置信息") subtitle := subTitleStyle.Render("Please confirm the following configuration information")
summary := summaryBox.Render(fmt.Sprintf(` summary := summaryBox.Render(fmt.Sprintf(`
+---------------------------------------------+ +----------------------------------------------------+
基本信息 Basic Information
+---------------------------------------------+ +----------------------------------------------------+
主机名:%-35s Homepage : %-38s
国 家: %-31s Hostname : %-35s
地 区:%-31s Country : %-31s
时 区:%-38s Region : %-31s
主 页:%-38s Timezone : %-38s
+---------------------------------------------+ Homepage : %-38s
数据库 +----------------------------------------------------+
+---------------------------------------------+ Database Configuration
地 址:%-38s +----------------------------------------------------+
名 称:%-38s Database Path : %-38s
软 件:%-33s Software : %-33s
+---------------------------------------------+ +----------------------------------------------------+
公网配置 Public Network Configuration
+---------------------------------------------+ +----------------------------------------------------+
接 口:%-38s Public Interface : %-38s
地 址: %-41s Public IP : %-41s
掩 码:%-38s Public Netmask : %-38s
网 关:%-38s Public Gateway : %-38s
+---------------------------------------------+ +----------------------------------------------------+
内网配置 Internal Network Configuration
+---------------------------------------------+ +----------------------------------------------------+
接 口:%-38s Internal Interface: %-38s
地 址: %-41s Internal IP : %-41s
掩 码:%-38s Internal Netmask : %-38s
+---------------------------------------------+ +----------------------------------------------------+
DNS DNS Configuration
+---------------------------------------------+ +----------------------------------------------------+
主 DNS: %-37s Primary DNS : %-37s
备 DNS: %-37s Secondary DNS : %-37s
+---------------------------------------------+ +----------------------------------------------------+
`, `,
m.config.HomePage,
m.config.Hostname, m.config.Hostname,
m.config.Country, m.config.Country,
m.config.Region, m.config.Region,
m.config.Timezone, m.config.Timezone,
m.config.HomePage, m.config.HomePage,
m.config.DBAddress, m.config.DBAddress,
m.config.DBName,
m.config.DataAddress, m.config.DataAddress,
m.config.PublicInterface, m.config.PublicInterface,
m.config.IPAddress, m.config.PublicIPAddress,
m.config.Netmask, m.config.PublicNetmask,
m.config.Gateway, m.config.PublicGateway,
m.config.InternalInterface, m.config.InternalInterface,
m.config.InternalIP, m.config.InternalIPAddress,
m.config.InternalMask, m.config.InternalNetmask,
m.config.DNSPrimary, m.config.DNSPrimary,
m.config.DNSSecondary, m.config.DNSSecondary,
)) ))
var buttons string var buttons string
if m.focusIndex == 0 { if m.focusIndex == 0 {
buttons = selectedButton.Render("[>] 执行初始化") + " " + normalButton.Render("[ ] 取消") buttons = selectedButton.Render("[>] Start Initialization") + " " + normalButton.Render("[ ] Cancel")
} else { } else {
buttons = normalButton.Render("[>] 执行初始化") + " " + selectedButton.Render("[ ] 取消") buttons = normalButton.Render("[>] Start Initialization") + " " + selectedButton.Render("[ ] Cancel")
} }
hint := hintStyle.Render("使用 <- -> 或 Tab 选择Enter 确认") hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
return lipgloss.JoinVertical(lipgloss.Center, return lipgloss.JoinVertical(lipgloss.Center,
title, "", title, "",
@@ -297,7 +287,7 @@ func progressView(current PageType, total int) string {
progress += " " progress += " "
} }
} }
labels := []string{"协议", "数据", "公网", "内网", "DNS", "总结"} labels := []string{"License", "Data", "Network", "Network", "DNS", "Summary"}
label := labelStyle.Render(labels[current]) label := labelStyle.Render(labels[current])
return progressStyle.Render(progress) + " " + label return progressStyle.Render(progress) + " " + label
} }
@@ -305,26 +295,26 @@ func progressView(current PageType, total int) string {
// successView 成功视图 // successView 成功视图
func successView() string { func successView() string {
return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center, return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center,
successTitle.Render("初始化完成!"), "", successTitle.Render("Initialization Completed!"), "",
successMsg.Render("系统配置已保存,正在初始化..."), "", successMsg.Render("System configuration has been saved, and the system is initializing..."), "",
hintStyle.Render("按任意键退出"), hintStyle.Render("Press any key to exit"),
)) ))
} }
// quitView 退出视图 // quitView 退出视图
func quitView() string { func quitView() string {
return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center, return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center,
errorTitle.Render("已取消"), "", errorTitle.Render("Canceled"), "",
errorMsg.Render("初始化已取消,未保存任何配置"), errorMsg.Render("Initialization canceled, no configuration saved"),
)) ))
} }
// errorView 错误视图 // errorView 错误视图
func errorView(err error) string { func errorView(err error) string {
return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center, return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center,
errorTitle.Render("错误"), "", errorTitle.Render("Error"), "",
errorMsg.Render(err.Error()), "", errorMsg.Render(err.Error()), "",
hintStyle.Render(" Ctrl+C 退出"), hintStyle.Render("Press Ctrl+C to exit"),
)) ))
} }
@@ -332,9 +322,9 @@ func errorView(err error) string {
func navButtons(m model, prev, next string) string { func navButtons(m model, prev, next string) string {
var btns string var btns string
if m.currentPage == 0 { if m.currentPage == 0 {
btns = normalButton.Render(prev) + " " + selectedButton.Render(next) btns = normalButton.Render(next) + " " + selectedButton.Render(prev)
} else { } else {
btns = selectedButton.Render(prev) + " " + normalButton.Render(next) btns = selectedButton.Render(next) + " " + normalButton.Render(prev)
} }
return btns return btns
} }
@@ -345,22 +335,22 @@ func (m model) renderNavButtons() string {
switch m.focusType { switch m.focusType {
case FocusTypePrev: case FocusTypePrev:
// 焦点在"上一步" // 焦点在"上一步"
prevBtn = selectedButton.Render("<< 上一步 >>") nextBtn = normalButton.Render(" Next ")
nextBtn = normalButton.Render("下一步 >>") prevBtn = selectedButton.Render(" << Prev >>")
case FocusTypeNext: case FocusTypeNext:
// 焦点在"下一步" // 焦点在"下一步"
prevBtn = normalButton.Render("<< 上一步") nextBtn = selectedButton.Render(" << Next >>")
nextBtn = selectedButton.Render("<< 下一步 >>") prevBtn = normalButton.Render(" Prev ")
default: default:
// 焦点在输入框 // 焦点在输入框
prevBtn = normalButton.Render("<< 上一步") nextBtn = normalButton.Render(" Next ")
nextBtn = normalButton.Render("下一步 >>") prevBtn = normalButton.Render(" Prev ")
} }
return lipgloss.JoinHorizontal( return lipgloss.JoinHorizontal(
lipgloss.Center, lipgloss.Center,
prevBtn,
" ",
nextBtn, nextBtn,
" ",
prevBtn,
) )
} }

View File

@@ -41,8 +41,6 @@ var normalButton = lipgloss.NewStyle().
Foreground(lipgloss.Color("#666666")) // 深灰色,更暗 Foreground(lipgloss.Color("#666666")) // 深灰色,更暗
var selectedButton = lipgloss.NewStyle(). var selectedButton = lipgloss.NewStyle().
Padding(0, 2).
Foreground(lipgloss.Color("#3d4747ff")). // 亮绿色
Bold(true) Bold(true)
// 输入框样式 // 输入框样式