This commit is contained in:
cruldra committed 2026-06-23 05:02:15 +08:00
commit e6f1776d4f
264 files changed
+54215

No files matched your search

+199
View File
@@ -0,0 +1,199 @@
package tui
import (
"fmt"
"path/filepath"
"strings"
"charm.land/lipgloss/v2"
"github.com/charmbracelet/x/ansi"
"superwork-tui/internal/issue"
)
var columnZhLabel = map[issue.Column]string{
issue.ColumnTodo: "待办",
issue.ColumnInProgress: "进行中",
issue.ColumnReview: "审查",
issue.ColumnDone: "完成",
}
func columnDisplay(col issue.Column) string {
if zh, ok := columnZhLabel[col]; ok {
return fmt.Sprintf("%s(%s)", col, zh)
}
return string(col)
}
func autoReviewDisplay(v *bool) string {
if v == nil {
return "(默认)"
}
if *v {
return "✓ True"
}
return "✗ False"
}
// colorDisplay shows a colored dot plus the short color name (the palette key
// with its "terminal.ansi" prefix stripped), matching the VSCode panel.
func colorDisplay(key string) string {
if key == "" {
return "未分配"
}
dot := "●"
if c, ok := keyToColor[key]; ok {
dot = lipgloss.NewStyle().Foreground(c).Render("●")
}
return dot + " " + strings.TrimPrefix(key, "terminal.ansi")
}
func profileDisplay(path string) string {
if path == "" {
return "(默认)"
}
base := filepath.Base(path)
return strings.TrimSuffix(base, filepath.Ext(base))
}
func prDisplay(iss issue.Issue) string {
if iss.PR == "" {
return "未关联 PR"
}
s := "#" + iss.PR
if iss.PrMerged {
s += "(已合并)"
}
return s
}
func orDash(s string) string {
if s == "" {
return "—"
}
return s
}
func orNotLoaded(s string) string {
if s == "" {
return "未加载"
}
return s
}
// detailRows is the ordered field list of the STATE JSON section, mirroring the
// VSCode IssueDetailPanel schema (label → formatted value).
func detailRows(iss issue.Issue) [][2]string {
return [][2]string{
{"状态", columnDisplay(iss.Column)},
{"头脑风暴会话id", orDash(iss.SessionID)},
{"实施会话id", orDash(iss.ImplementSessionID)},
{"审查会话id", orDash(iss.ReviewSessionID)},
{"测试会话id", orDash(iss.TestSessionID)},
{"自动审查", autoReviewDisplay(iss.AutoReview)},
{"颜色", colorDisplay(iss.Color)},
{"实施配置文件", profileDisplay(iss.ProfilePath)},
{"测试配置文件", profileDisplay(iss.TestProfilePath)},
{"规格文件", orNotLoaded(iss.SpecFile)},
{"计划文件", orNotLoaded(iss.PlanFile)},
{"PR 变更摘要", orNotLoaded(iss.PrDiffFile)},
{"合并请求", prDisplay(iss)},
{"分支", orDash(iss.Branch)},
{"工作树", orNotLoaded(iss.WorktreePath)},
}
}
// padCells right-pads s with spaces to a target display width.
func padCells(s string, w int) string {
if gap := w - lipgloss.Width(s); gap > 0 {
return s + strings.Repeat(" ", gap)
}
return s
}
var (
detailBorderColor = lipgloss.Color("12")
detailZebraBG = lipgloss.Color("236")
)
// detailBody builds the two stacked blocks (header + STATE JSON table) shown for
// an issue, sized to the given outer width.
func detailBody(iss issue.Issue, width int) string {
box := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(detailBorderColor).
Width(width)
innerW := width - 2 // border-to-border content width
// cell renders one full-width content line with a 1-space left inset,
// optionally with a zebra background, so nothing re-wraps inside the box.
cell := func(s string, zebra bool) string {
st := lipgloss.NewStyle().Width(innerW)
if zebra {
st = st.Background(detailZebraBG)
}
return st.Render(" " + s)
}
hint := lipgloss.NewStyle().Faint(true)
// ── header block ──
titleStyle := lipgloss.NewStyle().Bold(true).Foreground(ColorFor(iss))
title := titleStyle.Render(fmt.Sprintf("#%d %s", iss.Number, iss.Title))
if iss.Prerequisite > 0 {
title += " " + blockedStyle.Render(fmt.Sprintf("等待 #%d 完成", iss.Prerequisite))
}
header := lipgloss.JoinVertical(lipgloss.Left,
cell(title, false),
cell("", false),
cell(hint.Render("[b] 头脑风暴 [i] 实施 [v] 审查 [t] 测试 [p] PR提交 [g] PR摘要"), false),
cell(hint.Render("[f] 配置 [F] 测试配置 [o] 打开文件 [c] 关闭 [d] 删除 esc 返回"), false),
)
headerBox := box.Render(header)
// ── STATE JSON table block ──
rows := detailRows(iss)
labelW := 16
sep := " │ "
valueW := innerW - 1 - labelW - lipgloss.Width(sep) // 1 = left inset
if valueW < 8 {
valueW = 8
}
labelStyle := lipgloss.NewStyle().Faint(true)
countStr := fmt.Sprintf("%d 项", len(rows))
gap := innerW - 1 - lipgloss.Width("STATE JSON") - lipgloss.Width(countStr) - 1
if gap < 1 {
gap = 1
}
lines := []string{cell(
lipgloss.NewStyle().Bold(true).Render("STATE JSON")+strings.Repeat(" ", gap)+hint.Render(countStr),
false,
)}
for i, r := range rows {
line := labelStyle.Render(padCells(r[0], labelW)) + sep + ansi.Truncate(r[1], valueW, "…")
lines = append(lines, cell(line, i%2 == 1))
}
tableBox := box.Render(lipgloss.JoinVertical(lipgloss.Left, lines...))
return lipgloss.JoinVertical(lipgloss.Left, headerBox, tableBox)
}
// detailView renders the issue detail as a centered modal (two stacked blocks)
// floating over the board via lipgloss v2 layer compositing.
func (m Model) detailView() string {
bg := m.boardView()
if m.detailIss == nil {
return bg
}
width := 84
if m.width > 0 && m.width-8 < width {
width = m.width - 8
}
if width < 40 {
width = 40
}
modal := detailBody(*m.detailIss, width)
return placeOverlayCenter(bg, modal)
}