package webui import ( "bytes" "fmt" "html" "strings" ) // HTML 渲染辅助函数 // renderTablesHTML 渲染表列表 HTML func renderTablesHTML(tables []TableListItem) string { var buf bytes.Buffer for _, table := range tables { buf.WriteString(`
`) buf.WriteString(`
`) // 左侧:展开图标和表名 buf.WriteString(`
`) buf.WriteString(``) buf.WriteString(``) buf.WriteString(html.EscapeString(table.Name)) buf.WriteString(`
`) // 右侧:字段数量 buf.WriteString(``) buf.WriteString(formatCount(int64(len(table.Fields)))) buf.WriteString(` fields`) buf.WriteString(`
`) // Schema 字段列表(默认隐藏) if len(table.Fields) > 0 { buf.WriteString(`
`) for _, field := range table.Fields { buf.WriteString(`
`) buf.WriteString(``) buf.WriteString(html.EscapeString(field.Name)) buf.WriteString(``) buf.WriteString(``) buf.WriteString(html.EscapeString(field.Type)) buf.WriteString(``) if field.Indexed { buf.WriteString(`●indexed`) } buf.WriteString(`
`) } buf.WriteString(`
`) } buf.WriteString(`
`) } return buf.String() } // renderDataViewHTML 渲染数据视图 HTML func renderDataViewHTML(tableName string, schema SchemaInfo, tableData TableDataResponse) string { var buf bytes.Buffer // 标题 buf.WriteString(`

`) buf.WriteString(html.EscapeString(tableName)) buf.WriteString(`

`) // 视图切换标签 buf.WriteString(`
`) buf.WriteString(``) buf.WriteString(``) buf.WriteString(`
`) // Schema 部分 if len(schema.Fields) > 0 { buf.WriteString(`
`) buf.WriteString(`

Schema (点击字段卡片选择要显示的列)

`) buf.WriteString(`
`) for _, field := range schema.Fields { buf.WriteString(`
`) buf.WriteString(`
`) buf.WriteString(``) buf.WriteString(html.EscapeString(field.Name)) buf.WriteString(``) buf.WriteString(``) buf.WriteString(html.EscapeString(field.Type)) buf.WriteString(``) if field.Indexed { buf.WriteString(`●indexed`) } buf.WriteString(`
`) buf.WriteString(`
`) if field.Comment != "" { buf.WriteString(html.EscapeString(field.Comment)) } buf.WriteString(`
`) buf.WriteString(`
`) } buf.WriteString(`
`) buf.WriteString(`
`) } // 数据表格 buf.WriteString(`

Data (`) buf.WriteString(formatCount(tableData.TotalRows)) buf.WriteString(` rows)

`) if len(tableData.Data) == 0 { buf.WriteString(`

No data available

`) return buf.String() } // 获取列并排序:_seq 第1列,_time 倒数第2列 columns := []string{} otherColumns := []string{} hasSeq := false hasTime := false if len(tableData.Data) > 0 { for key := range tableData.Data[0] { if !strings.HasSuffix(key, "_truncated") { if key == "_seq" { hasSeq = true } else if key == "_time" { hasTime = true } else { otherColumns = append(otherColumns, key) } } } } // 按顺序组装:_seq, 其他列, _time if hasSeq { columns = append(columns, "_seq") } columns = append(columns, otherColumns...) if hasTime { columns = append(columns, "_time") } // 表格 buf.WriteString(`
`) buf.WriteString(``) buf.WriteString(``) for _, col := range columns { buf.WriteString(``) } buf.WriteString(``) buf.WriteString(``) buf.WriteString(``) for _, row := range tableData.Data { buf.WriteString(``) for _, col := range columns { value := row[col] buf.WriteString(``) } // Actions 列 buf.WriteString(``) buf.WriteString(``) } buf.WriteString(``) buf.WriteString(`
`) buf.WriteString(html.EscapeString(col)) buf.WriteString(`Actions
`) buf.WriteString(html.EscapeString(fmt.Sprintf("%v", value))) // 检查是否被截断 if truncated, ok := row[col+"_truncated"]; ok && truncated == true { buf.WriteString(`✂️`) } buf.WriteString(``) buf.WriteString(``) buf.WriteString(`
`) buf.WriteString(`
`) // 分页 buf.WriteString(renderPagination(tableData)) return buf.String() } // renderManifestViewHTML 渲染 Manifest 视图 HTML func renderManifestViewHTML(tableName string, manifest ManifestResponse) string { var buf bytes.Buffer // 标题 buf.WriteString(`

`) buf.WriteString(html.EscapeString(tableName)) buf.WriteString(`

`) // 视图切换标签 buf.WriteString(`
`) buf.WriteString(``) buf.WriteString(``) buf.WriteString(`
`) // 标题和控制按钮 buf.WriteString(`
`) buf.WriteString(`

LSM-Tree Structure

`) buf.WriteString(`
`) buf.WriteString(``) buf.WriteString(``) buf.WriteString(`
`) buf.WriteString(`
`) // 统计卡片 totalLevels := len(manifest.Levels) totalFiles := 0 totalSize := int64(0) for _, level := range manifest.Levels { totalFiles += level.FileCount totalSize += level.TotalSize } buf.WriteString(`
`) // Active Levels buf.WriteString(`
`) buf.WriteString(`
Active Levels
`) buf.WriteString(`
`) buf.WriteString(fmt.Sprintf("%d", totalLevels)) buf.WriteString(`
`) // Total Files buf.WriteString(`
`) buf.WriteString(`
Total Files
`) buf.WriteString(`
`) buf.WriteString(fmt.Sprintf("%d", totalFiles)) buf.WriteString(`
`) // Total Size buf.WriteString(`
`) buf.WriteString(`
Total Size
`) buf.WriteString(`
`) buf.WriteString(formatBytes(totalSize)) buf.WriteString(`
`) // Next File Number buf.WriteString(`
`) buf.WriteString(`
Next File Number
`) buf.WriteString(`
`) buf.WriteString(fmt.Sprintf("%d", manifest.NextFileNumber)) buf.WriteString(`
`) // Last Sequence buf.WriteString(`
`) buf.WriteString(`
Last Sequence
`) buf.WriteString(`
`) buf.WriteString(fmt.Sprintf("%d", manifest.LastSequence)) buf.WriteString(`
`) // Total Compactions buf.WriteString(`
`) buf.WriteString(`
Total Compactions
`) buf.WriteString(`
`) totalCompactions := 0 if manifest.CompactionStats != nil { if tc, ok := manifest.CompactionStats["total_compactions"]; ok { if tcInt, ok := tc.(float64); ok { totalCompactions = int(tcInt) } } } buf.WriteString(fmt.Sprintf("%d", totalCompactions)) buf.WriteString(`
`) buf.WriteString(`
`) // 渲染所有层级(L0-L6) for i := 0; i <= 6; i++ { var level *LevelInfo for j := range manifest.Levels { if manifest.Levels[j].Level == i { level = &manifest.Levels[j] break } } if level == nil { // 创建空层级 level = &LevelInfo{ Level: i, FileCount: 0, TotalSize: 0, Score: 0, Files: []FileInfo{}, } } buf.WriteString(renderLevelCard(*level)) } return buf.String() } // renderLevelCard 渲染层级卡片 func renderLevelCard(level LevelInfo) string { var buf bytes.Buffer scoreClass := "normal" if level.Score >= 1.0 { scoreClass = "critical" } else if level.Score >= 0.8 { scoreClass = "warning" } buf.WriteString(`
`) buf.WriteString(`
`) // 左侧:展开图标和标题 buf.WriteString(`
`) buf.WriteString(``) buf.WriteString(`
Level `) buf.WriteString(fmt.Sprintf("%d", level.Level)) buf.WriteString(`
`) // 右侧:统计信息 buf.WriteString(`
`) buf.WriteString(``) buf.WriteString(fmt.Sprintf("%d", level.FileCount)) buf.WriteString(` files`) buf.WriteString(``) buf.WriteString(formatBytes(level.TotalSize)) buf.WriteString(``) buf.WriteString(`Score: `) buf.WriteString(fmt.Sprintf("%.2f", level.Score)) buf.WriteString(``) buf.WriteString(`
`) buf.WriteString(`
`) // 文件列表(默认隐藏) buf.WriteString(`
`) if len(level.Files) == 0 { buf.WriteString(`
No files in this level
`) } else { for _, file := range level.Files { buf.WriteString(`
`) buf.WriteString(`
`) buf.WriteString(`File #`) buf.WriteString(fmt.Sprintf("%d", file.FileNumber)) buf.WriteString(``) buf.WriteString(``) buf.WriteString(formatBytes(file.FileSize)) buf.WriteString(``) buf.WriteString(`
`) buf.WriteString(`
`) buf.WriteString(`Key Range:`) buf.WriteString(``) buf.WriteString(fmt.Sprintf("%d - %d", file.MinKey, file.MaxKey)) buf.WriteString(`
`) buf.WriteString(`
`) buf.WriteString(`Rows:`) buf.WriteString(``) buf.WriteString(formatCount(file.RowCount)) buf.WriteString(`
`) buf.WriteString(`
`) } } buf.WriteString(`
`) buf.WriteString(`
`) return buf.String() } // renderPagination 渲染分页 HTML func renderPagination(data TableDataResponse) string { var buf bytes.Buffer buf.WriteString(``) return buf.String() } // formatBytes 格式化字节数 func formatBytes(bytes int64) string { if bytes == 0 { return "0 B" } const k = 1024 sizes := []string{"B", "KB", "MB", "GB", "TB"} i := 0 size := float64(bytes) for size >= k && i < len(sizes)-1 { size /= k i++ } return fmt.Sprintf("%.2f %s", size, sizes[i]) } // formatCount 格式化数量(K/M) func formatCount(count int64) string { if count >= 1000000 { return fmt.Sprintf("%.1fM", float64(count)/1000000) } if count >= 1000 { return fmt.Sprintf("%.1fK", float64(count)/1000) } return fmt.Sprintf("%d", count) } // escapeJSString 转义 JavaScript 字符串 func escapeJSString(s string) string { s = strings.ReplaceAll(s, `\`, `\\`) s = strings.ReplaceAll(s, `'`, `\'`) s = strings.ReplaceAll(s, `"`, `\"`) s = strings.ReplaceAll(s, "\n", `\n`) s = strings.ReplaceAll(s, "\r", `\r`) s = strings.ReplaceAll(s, "\t", `\t`) return s } // 数据结构定义 type TableListItem struct { Name string `json:"name"` CreatedAt int64 `json:"created_at"` Fields []FieldInfo `json:"fields"` } type FieldInfo struct { Name string `json:"name"` Type string `json:"type"` Indexed bool `json:"indexed"` Comment string `json:"comment"` } type SchemaInfo struct { Name string `json:"name"` Fields []FieldInfo `json:"fields"` } type TableDataResponse struct { Data []map[string]any `json:"data"` Page int64 `json:"page"` PageSize int64 `json:"pageSize"` TotalRows int64 `json:"totalRows"` TotalPages int64 `json:"totalPages"` } type ManifestResponse struct { Levels []LevelInfo `json:"levels"` NextFileNumber int64 `json:"next_file_number"` LastSequence int64 `json:"last_sequence"` CompactionStats map[string]any `json:"compaction_stats"` } type LevelInfo struct { Level int `json:"level"` FileCount int `json:"file_count"` TotalSize int64 `json:"total_size"` Score float64 `json:"score"` Files []FileInfo `json:"files"` } type FileInfo struct { FileNumber int64 `json:"file_number"` Level int `json:"level"` FileSize int64 `json:"file_size"` MinKey int64 `json:"min_key"` MaxKey int64 `json:"max_key"` RowCount int64 `json:"row_count"` }