package main import ( "bytes" "fmt" "os" "path/filepath" "strings" "text/template" "github.com/fatih/color" "github.com/spf13/cobra" "kra/pkg/utils/autocode" ) // previewCmd represents the preview command var previewCmd = &cobra.Command{ Use: "preview", Short: "预览生成的代码", Long: `预览将要生成的代码,不写入任何文件。 这是 'generate -p' 的快捷方式。 使用示例: # 从配置文件预览 kra-gen preview -c autocode.yaml # 使用命令行参数预览 kra-gen preview --table users --package example --struct User # 预览时显示完整代码内容 kra-gen preview -c autocode.yaml --full # 预览时不使用语法高亮 kra-gen preview -c autocode.yaml --full --no-color`, Run: runPreview, } var ( // Preview specific flags fullPreview bool noColor bool ) func init() { // Preview command specific flags (same as generate) previewCmd.Flags().StringVar(&dbName, "db", "", "数据库连接名") previewCmd.Flags().StringVar(&tableName, "table", "", "表名") previewCmd.Flags().StringVar(&packageName, "package", "", "包名") previewCmd.Flags().StringVar(&structName, "struct", "", "结构体名") previewCmd.Flags().StringVar(&desc, "desc", "", "描述") previewCmd.Flags().BoolVar(&noWeb, "no-web", false, "不生成前端代码") previewCmd.Flags().BoolVar(&noServer, "no-server", false, "不生成后端代码") previewCmd.Flags().BoolVar(&fullPreview, "full", false, "显示完整代码内容") previewCmd.Flags().BoolVar(&noColor, "no-color", false, "不使用语法高亮") } func runPreview(cmd *cobra.Command, args []string) { var config *AutoCodeConfig var err error // Determine configuration source if configFile != "" { config, err = loadConfigFile(configFile) if err != nil { fmt.Fprintf(os.Stderr, "加载配置文件失败: %v\n", err) os.Exit(1) } } else if hasRequiredFlags() { config = buildConfigFromFlags() } else { fmt.Println("请指定配置文件 (-c) 或提供必要参数 (--table, --package, --struct)") fmt.Println("使用 'kra-gen preview --help' 查看帮助") os.Exit(1) } // Apply command line overrides applyFlagOverrides(config) // Validate configuration if err := validateConfig(config); err != nil { fmt.Fprintf(os.Stderr, "配置验证失败: %v\n", err) os.Exit(1) } // Execute preview if fullPreview { if err := executeFullPreview(config); err != nil { fmt.Fprintf(os.Stderr, "预览失败: %v\n", err) os.Exit(1) } } else { if err := executePreview(config); err != nil { fmt.Fprintf(os.Stderr, "预览失败: %v\n", err) os.Exit(1) } } } // TemplateInfo 模板信息 type TemplateInfo struct { Name string // 模板名称 Path string // 模板路径 OutputPath string // 输出路径 Type string // 类型: server/web Layer string // 层级: biz/data/handler/service/router/pages/services/types } // getTemplateList 获取模板列表 func getTemplateList(config *AutoCodeConfig) []TemplateInfo { var templates []TemplateInfo workDir := getWorkingDir() pkg := config.Package structLower := strings.ToLower(config.StructName) if config.Options.GenerateServer { // 后端模板 templates = append(templates, []TemplateInfo{ { Name: "Biz Layer", Path: "server/biz/biz.go.tpl", OutputPath: filepath.Join(workDir, "internal/biz", pkg, structLower+".go"), Type: "server", Layer: "biz", }, { Name: "Biz Enter", Path: "server/biz/enter.go.tpl", OutputPath: filepath.Join(workDir, "internal/biz", pkg, "enter.go"), Type: "server", Layer: "biz", }, { Name: "Data Layer", Path: "server/data/data.go.tpl", OutputPath: filepath.Join(workDir, "internal/data", pkg, structLower+".go"), Type: "server", Layer: "data", }, { Name: "Data Enter", Path: "server/data/enter.go.tpl", OutputPath: filepath.Join(workDir, "internal/data", pkg, "enter.go"), Type: "server", Layer: "data", }, { Name: "Data Model", Path: "server/data/model.go.tpl", OutputPath: filepath.Join(workDir, "internal/data/model", pkg, structLower+".go"), Type: "server", Layer: "model", }, { Name: "Handler Layer", Path: "server/handler/handler.go.tpl", OutputPath: filepath.Join(workDir, "internal/server/handler", pkg, structLower+".go"), Type: "server", Layer: "handler", }, { Name: "Handler Enter", Path: "server/handler/enter.go.tpl", OutputPath: filepath.Join(workDir, "internal/server/handler", pkg, "enter.go"), Type: "server", Layer: "handler", }, { Name: "Service Layer", Path: "server/service/service.go.tpl", OutputPath: filepath.Join(workDir, "internal/service", pkg, structLower+".go"), Type: "server", Layer: "service", }, { Name: "Service Enter", Path: "server/service/enter.go.tpl", OutputPath: filepath.Join(workDir, "internal/service", pkg, "enter.go"), Type: "server", Layer: "service", }, { Name: "Request Types", Path: "server/types/request.go.tpl", OutputPath: filepath.Join(workDir, "internal/service/types", pkg, "request", structLower+".go"), Type: "server", Layer: "types", }, { Name: "Router Layer", Path: "server/router/router.go.tpl", OutputPath: filepath.Join(workDir, "internal/server/router", pkg, structLower+".go"), Type: "server", Layer: "router", }, { Name: "Router Enter", Path: "server/router/enter.go.tpl", OutputPath: filepath.Join(workDir, "internal/server/router", pkg, "enter.go"), Type: "server", Layer: "router", }, }...) } if config.Options.GenerateWeb { // 前端模板 templates = append(templates, []TemplateInfo{ { Name: "React Page", Path: "web/pages/index.tsx.tpl", OutputPath: filepath.Join(workDir, "web/src/pages", pkg, structLower, "index.tsx"), Type: "web", Layer: "pages", }, { Name: "React Form", Path: "web/pages/form.tsx.tpl", OutputPath: filepath.Join(workDir, "web/src/pages", pkg, structLower, "form.tsx"), Type: "web", Layer: "pages", }, { Name: "React Detail", Path: "web/pages/detail.tsx.tpl", OutputPath: filepath.Join(workDir, "web/src/pages", pkg, structLower, "detail.tsx"), Type: "web", Layer: "pages", }, { Name: "API Service", Path: "web/services/api.ts.tpl", OutputPath: filepath.Join(workDir, "web/src/services/kratos", pkg+".ts"), Type: "web", Layer: "services", }, { Name: "TypeScript Types", Path: "web/types/types.d.ts.tpl", OutputPath: filepath.Join(workDir, "web/src/types", pkg, structLower+".d.ts"), Type: "web", Layer: "types", }, }...) } return templates } // buildTemplateData 构建模板数据 func buildTemplateData(config *AutoCodeConfig) map[string]interface{} { // 转换字段 fields := make([]autocode.AutoCodeField, len(config.Fields)) for i, f := range config.Fields { var ds *autocode.DataSource if f.DataSource != nil { ds = &autocode.DataSource{ DBName: f.DataSource.DBName, Table: f.DataSource.Table, Label: f.DataSource.Label, Value: f.DataSource.Value, Association: f.DataSource.Association, HasDeletedAt: f.DataSource.HasDeletedAt, } } fields[i] = autocode.AutoCodeField{ FieldName: f.FieldName, FieldDesc: f.FieldDesc, FieldType: f.FieldType, FieldJson: f.FieldJson, DataTypeLong: f.DataTypeLong, ColumnName: f.ColumnName, FieldSearchType: f.FieldSearchType, FieldSearchHide: f.FieldSearchHide, DictType: f.DictType, Form: f.Form, Table: f.Table, Desc: f.Desc, Excel: f.Excel, Require: f.Require, DefaultValue: f.DefaultValue, ErrorText: f.ErrorText, Clearable: f.Clearable, Sort: f.Sort, PrimaryKey: f.PrimaryKey, DataSource: ds, FieldIndexType: f.FieldIndexType, } } // 查找主键字段 var primaryField autocode.AutoCodeField for _, f := range fields { if f.PrimaryKey { primaryField = f break } } // 如果没有主键,使用默认的 ID if primaryField.FieldName == "" { primaryField = autocode.AutoCodeField{ FieldName: "ID", FieldJson: "ID", FieldType: "uint", } } // 收集字典类型 var dictTypes []string hasDataSource := false for _, f := range fields { if f.DictType != "" { dictTypes = append(dictTypes, f.DictType) } if f.DataSource != nil { hasDataSource = true } } // 检查特殊字段类型 hasTimer := false hasPic := false hasFile := false hasRichText := false hasArray := false needSort := false for _, f := range fields { switch f.FieldType { case "time.Time": hasTimer = true case "picture": hasPic = true case "file", "pictures": hasFile = true case "richtext": hasRichText = true case "array": hasArray = true } if f.Sort { needSort = true } } // 获取模块名 module := "kra" if modFile, err := os.ReadFile("go.mod"); err == nil { lines := strings.Split(string(modFile), "\n") for _, line := range lines { if strings.HasPrefix(line, "module ") { module = strings.TrimSpace(strings.TrimPrefix(line, "module ")) break } } } return map[string]interface{}{ "Package": config.Package, "PackageName": strings.ToLower(config.Package), "PackageT": strings.Title(config.Package), "StructName": config.StructName, "TableName": config.TableName, "Description": config.Description, "BusinessDB": config.BusinessDB, "Abbreviation": config.Abbreviation, "HumpPackageName": strings.ToLower(string(config.Package[0])) + config.Package[1:], "Module": module, "Fields": fields, "PrimaryField": primaryField, "DictTypes": dictTypes, "HasDataSource": hasDataSource, "HasTimer": hasTimer, "HasPic": hasPic, "HasFile": hasFile, "HasRichText": hasRichText, "HasArray": hasArray, "NeedSort": needSort, "GvaModel": config.Options.GvaModel, "AutoMigrate": config.Options.AutoMigrate, "AutoCreateResource": config.Options.AutoCreateResource, "OnlyTemplate": config.Options.OnlyTemplate, "IsTree": config.Options.IsTree, "TreeJson": config.Options.TreeJson, "IsAdd": config.Options.IsAdd, } } // findTemplateDir 查找模板目录 func findTemplateDir() string { // 优先使用命令行指定的模板目录 if templateDir != "" { return templateDir } // 尝试多个可能的路径 possiblePaths := []string{ "resource/package", "../resource/package", "../../resource/package", } // 获取可执行文件所在目录 if execPath, err := os.Executable(); err == nil { execDir := filepath.Dir(execPath) possiblePaths = append(possiblePaths, filepath.Join(execDir, "resource/package"), filepath.Join(execDir, "../resource/package"), ) } for _, p := range possiblePaths { if _, err := os.Stat(p); err == nil { return p } } return "resource/package" } // renderTemplate 渲染单个模板 func renderTemplate(tplPath string, data map[string]interface{}) (string, error) { // 查找模板目录 baseDir := findTemplateDir() resourcePath := filepath.Join(baseDir, tplPath) tplContent, err := os.ReadFile(resourcePath) if err != nil { return "", fmt.Errorf("读取模板文件失败 (%s): %w", resourcePath, err) } // 创建模板 tmpl, err := template.New(filepath.Base(tplPath)). Funcs(autocode.GetTemplateFuncMap()). Parse(string(tplContent)) if err != nil { return "", fmt.Errorf("解析模板失败: %w", err) } // 渲染模板 var buf bytes.Buffer if err := tmpl.Execute(&buf, data); err != nil { return "", fmt.Errorf("渲染模板失败: %w", err) } return buf.String(), nil } // executeFullPreview 执行完整预览(显示所有文件内容) func executeFullPreview(config *AutoCodeConfig) error { // 颜色定义 titleColor := color.New(color.FgCyan, color.Bold) fileColor := color.New(color.FgGreen, color.Bold) pathColor := color.New(color.FgYellow) separatorColor := color.New(color.FgBlue) errorColor := color.New(color.FgRed) if noColor { color.NoColor = true } // 打印头部信息 titleColor.Println("╔════════════════════════════════════════════════════════════════╗") titleColor.Println("║ KRA 代码预览 (完整模式) ║") titleColor.Println("╚════════════════════════════════════════════════════════════════╝") fmt.Println() // 打印配置信息 fmt.Printf("包名: %s\n", config.Package) fmt.Printf("结构体: %s\n", config.StructName) fmt.Printf("表名: %s\n", config.TableName) fmt.Printf("描述: %s\n", config.Description) fmt.Println() // 获取模板列表 templates := getTemplateList(config) // 构建模板数据 data := buildTemplateData(config) // 统计信息 successCount := 0 failCount := 0 var failedTemplates []string // 渲染并显示每个模板 for _, tpl := range templates { separatorColor.Println("────────────────────────────────────────────────────────────────") fileColor.Printf("📄 %s\n", tpl.Name) pathColor.Printf(" 输出路径: %s\n", tpl.OutputPath) fmt.Println() content, err := renderTemplate(tpl.Path, data) if err != nil { errorColor.Printf(" ❌ 渲染失败: %v\n", err) failCount++ failedTemplates = append(failedTemplates, tpl.Name) continue } // 显示代码内容(带语法高亮) printCodeWithHighlight(content, tpl.OutputPath) successCount++ fmt.Println() } // 打印统计信息 separatorColor.Println("════════════════════════════════════════════════════════════════") titleColor.Println("预览统计") fmt.Printf(" 成功: %d 个文件\n", successCount) if failCount > 0 { errorColor.Printf(" 失败: %d 个文件\n", failCount) for _, name := range failedTemplates { errorColor.Printf(" - %s\n", name) } } fmt.Println() titleColor.Println("⚠️ 预览模式 - 未写入任何文件") return nil } // printCodeWithHighlight 打印代码(带简单语法高亮) func printCodeWithHighlight(content, filePath string) { if noColor { fmt.Println(content) return } // 根据文件扩展名确定语言 ext := strings.ToLower(filepath.Ext(filePath)) // 简单的语法高亮 lines := strings.Split(content, "\n") for i, line := range lines { // 行号 lineNum := color.New(color.FgHiBlack).Sprintf("%4d │ ", i+1) fmt.Print(lineNum) // 根据语言类型进行简单高亮 switch ext { case ".go": fmt.Println(highlightGo(line)) case ".tsx", ".ts": fmt.Println(highlightTypeScript(line)) default: fmt.Println(line) } } } // highlightGo 简单的 Go 语法高亮 func highlightGo(line string) string { if noColor { return line } // 注释 if strings.HasPrefix(strings.TrimSpace(line), "//") { return color.HiBlackString(line) } // 关键字 keywords := []string{"package", "import", "func", "type", "struct", "interface", "return", "if", "else", "for", "range", "switch", "case", "default", "var", "const", "map", "chan", "go", "defer", "select", "break", "continue"} result := line for _, kw := range keywords { // 只替换完整单词 result = strings.ReplaceAll(result, " "+kw+" ", " "+color.CyanString(kw)+" ") result = strings.ReplaceAll(result, "\t"+kw+" ", "\t"+color.CyanString(kw)+" ") if strings.HasPrefix(result, kw+" ") { result = color.CyanString(kw) + result[len(kw):] } } return result } // highlightTypeScript 简单的 TypeScript 语法高亮 func highlightTypeScript(line string) string { if noColor { return line } // 注释 trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, "//") || strings.HasPrefix(trimmed, "/*") || strings.HasPrefix(trimmed, "*") { return color.HiBlackString(line) } // 关键字 keywords := []string{"import", "export", "from", "const", "let", "var", "function", "return", "if", "else", "for", "while", "switch", "case", "default", "interface", "type", "class", "extends", "implements", "async", "await", "try", "catch", "finally", "throw", "new", "this", "super"} result := line for _, kw := range keywords { result = strings.ReplaceAll(result, " "+kw+" ", " "+color.CyanString(kw)+" ") result = strings.ReplaceAll(result, "\t"+kw+" ", "\t"+color.CyanString(kw)+" ") if strings.HasPrefix(result, kw+" ") { result = color.CyanString(kw) + result[len(kw):] } } return result }