feat: 重构

This commit is contained in:
chenxiangtong
2026-05-28 17:37:57 +08:00
parent 88a0c45f5b
commit 3ff2454c63
26 changed files with 1248 additions and 2055 deletions

332
pages.go Normal file
View File

@@ -0,0 +1,332 @@
package main
import (
"fmt"
"strings"
"time"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/progress"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
)
// --- Version Select Page ---
func updateVersionSelect(m model, msg tea.Msg) (model, tea.Cmd) {
switch msg := msg.(type) {
case versionsFoundMsg:
items := make([]list.Item, len(msg))
for i, v := range msg {
items[i] = versionItem{name: v.name, path: v.path}
}
m.logLines = nil // Bug3: 清除旧的扫描错误,否则 view 永远显示错误
m.versionList.SetItems(items)
return m, nil
case scanErrorMsg:
m.logLines = []string{errorStyle.Render("扫描版本目录失败: " + msg.err.Error())}
return m, nil
case tea.KeyMsg:
if msg.String() == "enter" {
selected, ok := m.versionList.SelectedItem().(versionItem)
if ok {
m.versionDir = selected.path
m.versionName = selected.name
m.currentPage = pageMainMenu
m.logLines = nil // Bug1: 清除版本选择页的 logLines
m.menuList.SetItems([]list.Item{
menuItem{title: "输入数字码", desc: "输入工具码以执行操作"},
menuItem{title: "切换版本", desc: "选择其他游戏版本"},
})
return m, nil
}
}
}
var cmd tea.Cmd
m.versionList, cmd = m.versionList.Update(msg)
return m, cmd
}
func viewVersionSelect(m model) string {
if len(m.logLines) > 0 {
return "\n" + strings.Join(m.logLines, "\n") + "\n\n" + subtleStyle.Render("按 ctrl+c 退出")
}
return m.versionList.View()
}
// --- Main Menu Page ---
func updateMainMenu(m model, msg tea.Msg) (model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "enter":
idx := m.menuList.Index()
switch idx {
case 0:
m.currentPage = pageCodeInput
m.codeInput.SetValue("")
return m, m.codeInput.Focus()
case 1:
m.currentPage = pageVersionSelect
m.logLines = nil // Bug2: 清除残留 logLines否则版本列表被错误文本遮挡
m.versionList.SetItems(nil) // Bug10: 清除旧列表,避免短暂显示过时数据
return m, scanVersions(m.exeDir)
}
case "esc", "q":
return m, tea.Quit
}
}
var cmd tea.Cmd
m.menuList, cmd = m.menuList.Update(msg)
return m, cmd
}
func viewMainMenu(m model) string {
header := subtleStyle.Render(fmt.Sprintf("当前版本: %s", m.versionName))
return header + "\n" + m.menuList.View()
}
// --- Code Input Page ---
func updateCodeInput(m model, msg tea.Msg) (model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "enter":
code := m.codeInput.Value()
if len(code) == 4 {
m.currentPage = pageExecuting
m = resetExecState(m)
return m, tea.Batch(m.spinner.Tick, fetchActions(code))
}
case "esc":
m.currentPage = pageMainMenu
return m, nil
}
}
var cmd tea.Cmd
m.codeInput, cmd = m.codeInput.Update(msg)
return m, cmd
}
func viewCodeInput(m model) string {
return fmt.Sprintf(
"%s\n\n 输入4位数字码:\n\n %s\n\n %s\n",
appTitle,
m.codeInput.View(),
subtleStyle.Render("enter 确认 | esc 返回"),
)
}
// --- Executing Page ---
type actionProgressMsg struct {
percent float64
message string
}
type actionCompleteMsg struct{ index int }
type actionErrorMsg struct {
index int
err error
}
type mirrorChoiceMsg struct {
index int
mirrors []string
action Action
}
func updateExecuting(m model, msg tea.Msg) (model, tea.Cmd) {
if m.choosingMirror {
return updateMirrorChoice(m, msg)
}
switch msg := msg.(type) {
case actionsReceivedMsg:
m.actions = msg.actions
m.actionIdx = 0
if len(m.actions) == 0 {
m.execDone = true
m.logLines = append(m.logLines, successStyle.Render("没有需要执行的操作"))
return m, nil
}
m.backupDir = fmt.Sprintf("amt/backup/%s", time.Now().Format("20060102_150405"))
m.logLines = append(m.logLines, boldStyle.Render(fmt.Sprintf("共 %d 个操作", len(m.actions))))
m.logLines = append(m.logLines, describeAction(m.actions[0], 0, len(m.actions)))
return m, tea.Batch(m.spinner.Tick, executeAction(m.versionDir, m.actions[0], 0, m.backupDir))
case apiErrorMsg:
m.execErr = msg.err
m.logLines = append(m.logLines, crossMark+" "+errorStyle.Render("API 请求失败: "+msg.err.Error()))
return m, nil
case actionProgressMsg:
if msg.message != "" {
m.logLines = append(m.logLines, msg.message)
}
cmd := m.progress.SetPercent(msg.percent)
var cmds []tea.Cmd
cmds = append(cmds, cmd)
if m.progressCh != nil {
cmds = append(cmds, waitForProgress(m.progressCh, m.actionIdx))
}
return m, tea.Batch(cmds...)
case actionCompleteMsg:
m.progressCh = nil
total := len(m.actions)
m.logLines = append(m.logLines, fmt.Sprintf("%s [%d/%d] 完成", checkMark, msg.index+1, total))
if msg.index+1 >= total {
m.execDone = true
m.logLines = append(m.logLines, "\n"+successStyle.Render("全部操作完成!"))
return m, m.progress.SetPercent(1.0)
}
m.actionIdx = msg.index + 1
m.logLines = append(m.logLines, describeAction(m.actions[m.actionIdx], m.actionIdx, total))
progressCmd := m.progress.SetPercent(float64(m.actionIdx) / float64(total))
return m, tea.Batch(
progressCmd,
executeAction(m.versionDir, m.actions[m.actionIdx], m.actionIdx, m.backupDir),
)
case actionErrorMsg:
m.progressCh = nil
m.execErr = msg.err
m.logLines = append(m.logLines, fmt.Sprintf("%s [%d/%d] 失败: %s", crossMark, msg.index+1, len(m.actions), msg.err.Error()))
return m, nil
case mirrorChoiceMsg:
m.progressCh = nil
m.choosingMirror = true
m.pendingAction = &msg.action
items := make([]list.Item, len(msg.mirrors))
for i, u := range msg.mirrors {
items[i] = mirrorItem{url: u}
}
delegate := list.NewDefaultDelegate()
l := list.New(items, delegate, m.width, m.height-6)
l.Title = "主 URL 下载失败,请选择镜像源"
l.SetFilteringEnabled(false)
l.SetShowStatusBar(false)
m.mirrorList = l
return m, nil
case tea.KeyMsg:
if m.execDone || m.execErr != nil {
m.currentPage = pageMainMenu
m = resetExecState(m)
return m, nil
}
case spinner.TickMsg:
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
case progress.FrameMsg:
p, cmd := m.progress.Update(msg)
m.progress = p.(progress.Model)
return m, cmd
}
return m, nil
}
func updateMirrorChoice(m model, msg tea.Msg) (model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.String() == "enter" {
selected, ok := m.mirrorList.SelectedItem().(mirrorItem)
if ok && m.pendingAction != nil {
m.choosingMirror = false
action := *m.pendingAction
action.URL = selected.url
m.pendingAction = nil
m.logLines = append(m.logLines, subtleStyle.Render("使用镜像: "+selected.url))
return m, executeAction(m.versionDir, action, m.actionIdx, m.backupDir)
}
}
if msg.String() == "esc" {
m.choosingMirror = false
m.pendingAction = nil
m.execErr = fmt.Errorf("用户取消镜像选择")
m.logLines = append(m.logLines, crossMark+" "+errorStyle.Render("已取消"))
return m, nil
}
}
var cmd tea.Cmd
m.mirrorList, cmd = m.mirrorList.Update(msg)
return m, cmd
}
func viewExecuting(m model) string {
if m.choosingMirror {
return m.mirrorList.View()
}
var sb strings.Builder
sb.WriteString(appTitle)
sb.WriteString("\n\n")
for _, line := range m.logLines {
sb.WriteString(" ")
sb.WriteString(line)
sb.WriteString("\n")
}
if !m.execDone && m.execErr == nil && len(m.actions) > 0 {
sb.WriteString("\n")
sb.WriteString(fmt.Sprintf(" %s 正在执行 [%d/%d]...\n", m.spinner.View(), m.actionIdx+1, len(m.actions)))
sb.WriteString("\n ")
sb.WriteString(m.progress.View())
sb.WriteString("\n")
}
if m.execDone || m.execErr != nil {
sb.WriteString("\n ")
sb.WriteString(subtleStyle.Render("按任意键返回主菜单"))
sb.WriteString("\n")
}
return sb.String()
}
func resetExecState(m model) model {
m.actions = nil
m.actionIdx = 0
m.logLines = nil
m.execErr = nil
m.execDone = false
m.choosingMirror = false
m.pendingAction = nil
m.backupDir = ""
m.progressCh = nil
return m
}
func describeAction(a Action, idx, total int) string {
prefix := fmt.Sprintf("[%d/%d]", idx+1, total)
switch a.Type {
case "add":
if a.Unzip {
return subtleStyle.Render(fmt.Sprintf("%s 下载并解压 %s ...", prefix, a.Path))
}
return subtleStyle.Render(fmt.Sprintf("%s 下载 %s ...", prefix, a.Path))
case "delete":
return subtleStyle.Render(fmt.Sprintf("%s 删除 %s ...", prefix, a.Path))
case "copy":
return subtleStyle.Render(fmt.Sprintf("%s 复制 %s -> %s ...", prefix, a.Path, a.NewPath))
case "move":
return subtleStyle.Render(fmt.Sprintf("%s 移动 %s -> %s ...", prefix, a.Path, a.NewPath))
default:
return subtleStyle.Render(fmt.Sprintf("%s %s %s ...", prefix, a.Type, a.Path))
}
}