This commit is contained in:
cruldra committed 2026-06-24 02:32:22 +08:00
1 parent e6f1776d4f
commit 495e961e53
6 files changed
+966 -40

No files matched your search

+193 -14
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"path/filepath"
"strings"
"time"
"charm.land/lipgloss/v2"
"github.com/charmbracelet/x/ansi"
@@ -117,7 +118,7 @@ var (
// 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 {
func (m Model) detailBody(iss issue.Issue, width int) string {
box := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(detailBorderColor).
@@ -144,9 +145,8 @@ func detailBody(iss issue.Issue, width int) string {
}
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),
cell(hint.Render("↑↓/jk 选择 ←→/hl 列 H/L 移动 c 关闭 d 删除 p 前置 P 清前置 n 新建"), false),
cell(m.actionBarView(), false),
)
headerBox := box.Render(header)
@@ -178,22 +178,201 @@ func detailBody(iss issue.Issue, width int) string {
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.
// detailView renders the board above a bottom issue property panel, matching
// the VSCode extension's kanban + attributes split.
func (m Model) detailView() string {
bg := m.boardView()
if m.detailIss == nil {
return bg
iss, ok := m.currentDetailIssue()
if !ok {
return m.boardView()
}
width := 84
if m.width > 0 && m.width-8 < width {
width = m.width - 8
return m.boardWithDetailView(iss)
}
func (m Model) boardWithDetailView(iss issue.Issue) string {
width := m.width - 2
if m.width <= 0 {
width = 100
}
if width < 40 {
width = 40
}
modal := detailBody(*m.detailIss, width)
return placeOverlayCenter(bg, modal)
panel := m.bottomTabsPanel(iss, width)
panelH := lipgloss.Height(panel)
statusLine := ""
indicator := ""
if m.hasChanges {
indicator = " " + confirmStyle.Render("● 未提交")
}
if status := m.statusBar(); status != "" {
statusLine = status + indicator
} else if indicator != "" {
statusLine = indicator
}
reserved := panelH + 1
if statusLine != "" {
reserved++
}
boardH := m.height - reserved
if m.height == 0 {
boardH = 12
}
if boardH < 5 {
boardH = 5
}
parts := []string{m.boardOnlyView(boardH)}
if statusLine != "" {
parts = append(parts, statusLine)
}
parts = append(parts, panel)
return lipgloss.JoinVertical(lipgloss.Left, parts...)
}
func (m Model) bottomTabsPanel(iss issue.Issue, width int) string {
tabs := m.bottomTabsHeader(width)
content := ""
switch m.bottomTab {
case bottomTabProfile:
content = m.profileTabBody(width)
case bottomTabSessions:
content = m.sessionsTabBody(width)
case bottomTabCommits:
content = m.commitsTabBody(width)
default:
content = m.detailBody(iss, width)
}
return lipgloss.JoinVertical(lipgloss.Left, tabs, content)
}
func (m Model) bottomTabsHeader(width int) string {
labels := []struct {
tab bottomTab
label string
}{
{bottomTabIssue, "1 工单"},
{bottomTabProfile, "2 Profile"},
{bottomTabSessions, "3 会话"},
{bottomTabCommits, "4 提交"},
}
var parts []string
for _, item := range labels {
prefix := " "
if m.focusPane == focusPanel && m.bottomTab == item.tab {
prefix = "▸"
}
text := prefix + item.label + " "
if m.bottomTab == item.tab {
parts = append(parts, selectedCardStyle.Render(text))
} else {
parts = append(parts, helpStyle.Render(text))
}
}
line := strings.Join(parts, " ")
hint := helpStyle.Render(" Tab 切焦点;面板聚焦后 ↑↓/←→ 切 tab;点击区域也可切焦点")
if lipgloss.Width(line)+lipgloss.Width(hint) < width {
line += hint
}
return lipgloss.NewStyle().Width(width).Render(line)
}
func (m Model) tabBox(title string, lines []string, width int) string {
box := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(detailBorderColor).
Width(width)
titleLine := lipgloss.NewStyle().Bold(true).Render(title)
body := append([]string{titleLine, ""}, lines...)
return box.Render(lipgloss.JoinVertical(lipgloss.Left, body...))
}
func (m Model) profileTabBody(width int) string {
lines := []string{
helpStyle.Render("只读摘要;按 G 打开 Profile 表格编辑器"),
}
if len(m.profileGridData.Profiles) == 0 {
lines = append(lines, "未加载 Profile 数据,按 2 重新加载")
return m.tabBox("Profile", lines, width)
}
lines = append(lines, "Profiles: "+strings.Join(m.profileGridData.Profiles, ", "))
if len(m.profileGridData.Rows) == 0 {
lines = append(lines, "暂无配置行")
return m.tabBox("Profile", lines, width)
}
maxRows := 8
for i, row := range m.profileGridData.Rows {
if i >= maxRows {
lines = append(lines, fmt.Sprintf("... 还有 %d 行", len(m.profileGridData.Rows)-maxRows))
break
}
var vals []string
for _, prof := range m.profileGridData.Profiles {
vals = append(vals, fmt.Sprintf("%s=%s", prof, orDash(row.Values[prof])))
}
lines = append(lines, truncate(row.Key+" "+strings.Join(vals, " "), width-6))
}
return m.tabBox("Profile", lines, width)
}
func (m Model) sessionsTabBody(width int) string {
lines := []string{
helpStyle.Render("只读摘要;按 m 打开会话管理器"),
}
if len(m.managedSessions.Sessions) == 0 {
lines = append(lines, "暂无托管会话")
return m.tabBox("会话", lines, width)
}
maxRows := 8
for i, sess := range m.managedSessions.Sessions {
if i >= maxRows {
lines = append(lines, fmt.Sprintf("... 还有 %d 个会话", len(m.managedSessions.Sessions)-maxRows))
break
}
ts := time.Unix(sess.CreatedAt, 0).Format("2006-01-02 15:04")
id := sess.ID
if len(id) > 12 {
id = id[:12]
}
lines = append(lines, truncate(fmt.Sprintf("%s %s %s %s", sess.Name, id, profileDisplay(sess.ProfilePath), ts), width-6))
}
return m.tabBox("会话", lines, width)
}
func (m Model) commitsTabBody(width int) string {
lines := []string{
helpStyle.Render("只读摘要;按 R 打开 PR 提交确认面板"),
}
if m.prCommitsIss == nil || m.prCommitsIss.PR == "" {
lines = append(lines, "当前工单无关联 PR")
return m.tabBox("提交", lines, width)
}
if m.prCommitsErr != nil {
lines = append(lines, errorStyle.Render(m.prCommitsErr.Error()))
return m.tabBox(fmt.Sprintf("PR #%s 提交", m.prCommitsIss.PR), lines, width)
}
if !m.prCommitsLoaded {
lines = append(lines, "加载中...")
return m.tabBox(fmt.Sprintf("PR #%s 提交", m.prCommitsIss.PR), lines, width)
}
maxCommits := 5
lines = append(lines, fmt.Sprintf("Commits (%d):", len(m.prCommits)))
for i, c := range m.prCommits {
if i >= maxCommits {
lines = append(lines, fmt.Sprintf("... 还有 %d 个 commit", len(m.prCommits)-maxCommits))
break
}
mark := " "
if m.prConfirmedSHAs[c.SHA] {
mark = "x"
}
lines = append(lines, truncate(fmt.Sprintf("[%s] %s %s", mark, shortSHA(c.SHA), firstLine(c.Commit.Message)), width-6))
}
lines = append(lines, fmt.Sprintf("Files: %d", len(m.prFiles)))
if allFilesConfirmed(m.prFiles, m.prConfirmed) && len(m.prFiles) > 0 {
lines = append(lines, confirmStyle.Render("所有文件已确认"))
}
return m.tabBox(fmt.Sprintf("PR #%s 提交", m.prCommitsIss.PR), lines, width)
}