kra/cmd/kra-gen/preview.go

609 lines
17 KiB
Go

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
}