609 lines
17 KiB
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
|
|
}
|