feat: 发布 v1.0.0-beta 版本
- 添加 ASCII 艺术 Logo 带渐变效果 - 改造输入框使用 ::: 紫色分隔符 - 改造信息栏合并显示语言/模型/记录数 - 添加 Spinner 翻译状态动画 - 优化翻译卡片样式 - 版本号三方同步规则
This commit is contained in:
29
changelog.md
29
changelog.md
@@ -441,4 +441,31 @@ yoyo onboard --force
|
||||
- View() 方法返回 `tea.View` 类型
|
||||
- KeyMsg 改为 KeyPressMsg,使用 `msg.String()` 判断键位
|
||||
|
||||
**讨论记录**: [taolun.md#版本-0.8.1-翻译结果卡片组件设计](taolun.md#版本-081---翻译结果卡片组件设计)
|
||||
**讨论记录**: [taolun.md#版本-0.8.1-翻译结果卡片组件设计](taolun.md#版本-081---翻译结果卡片组件设计)
|
||||
|
||||
---
|
||||
|
||||
## v1.0.0-beta (2026-04-07)
|
||||
|
||||
### 新功能
|
||||
- ASCII艺术Logo标题,带紫色→粉色渐变效果
|
||||
- 输入框改造:
|
||||
- 使用 `:::` 紫色分隔符替代上下边框
|
||||
- Ctrl+J 换行功能
|
||||
- 信息栏改造:
|
||||
- 合并显示:语言、模型名、缓存记录数
|
||||
- 添加翻译状态 Spinner 动画 (MiniDot)
|
||||
- 翻译结果卡片优化:
|
||||
- 底部 `▣` 图标左侧边距从3减少到2
|
||||
- `▣` 与文字间距从3减少到2
|
||||
|
||||
### 技术细节
|
||||
- 使用 lipgloss 实现 True Color 渐变效果
|
||||
- 使用 charmbracelet/bubbles spinner 组件实现加载动画
|
||||
- 版本号显示在Logo右侧 [v1.0.0-beta]
|
||||
- 动态调整 viewport 高度适应终端
|
||||
|
||||
### 版本号规则
|
||||
- 版本号需与 git 标签、changelog.md 中的版本号保持三方同步
|
||||
|
||||
**讨论记录**: [taolun.md#版本-100-beta-Logo和信息栏改造](taolun.md#版本-100-beta-logo和信息栏改造)
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
Version = "1.0.0"
|
||||
Version = "1.0.0-beta"
|
||||
)
|
||||
|
||||
func DetectLanguage(text string) string {
|
||||
|
||||
@@ -20,6 +20,10 @@ type Translator struct {
|
||||
cache cache.Cache
|
||||
}
|
||||
|
||||
func (t *Translator) GetCache() cache.Cache {
|
||||
return t.cache
|
||||
}
|
||||
|
||||
// NewTranslator 创建翻译器实例
|
||||
func NewTranslator(config *config.Config, provider provider.Provider) *Translator {
|
||||
translator := &Translator{
|
||||
|
||||
@@ -3,19 +3,27 @@ package tui
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"charm.land/bubbles/v2/spinner"
|
||||
"charm.land/bubbles/v2/textarea"
|
||||
"charm.land/bubbles/v2/viewport"
|
||||
"charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/titor/fanyi/internal/config"
|
||||
"github.com/titor/fanyi/internal/content"
|
||||
"github.com/titor/fanyi/internal/translator"
|
||||
)
|
||||
|
||||
var supportedLangs = []string{"zh-CN", "en-US", "ja", "ko", "zh-TW", "es", "fr", "de"}
|
||||
|
||||
var logoPattern = "l_ _ _____ _____ " + "\n" +
|
||||
"( \\/ ( _ ( _ ) " + "\n" +
|
||||
" \\ / )(_)( )(_)( " + "\n" +
|
||||
" (__)(_____(_____((() [v" + content.Version + "]"
|
||||
|
||||
type translateMsg struct {
|
||||
result string
|
||||
tokens int
|
||||
@@ -29,6 +37,7 @@ type model struct {
|
||||
messages []ChatMessage
|
||||
input textarea.Model
|
||||
viewport viewport.Model
|
||||
spinner spinner.Model
|
||||
keys KeyMap
|
||||
|
||||
targetLang string
|
||||
@@ -45,25 +54,31 @@ func NewApp(cfg *config.Config, t *translator.Translator) *tea.Program {
|
||||
keys := NewKeyMap()
|
||||
|
||||
ta := textarea.New()
|
||||
ta.Placeholder = "输入要翻译的文本... (Ctrl+J 换行)"
|
||||
ta.Placeholder = "在这里输入你要翻译的内容。(Enter 翻译,Ctrl+J 启用换行)"
|
||||
ta.Focus()
|
||||
ta.Prompt = ""
|
||||
ta.ShowLineNumbers = false
|
||||
ta.SetWidth(60)
|
||||
ta.SetHeight(5)
|
||||
ta.SetStyles(textarea.DefaultStyles(false))
|
||||
ta.SetStyles(textarea.DefaultStyles(true))
|
||||
|
||||
ta.KeyMap.InsertNewline.SetKeys("ctrl+j")
|
||||
ta.KeyMap.InsertNewline.SetEnabled(true)
|
||||
|
||||
vp := viewport.New(viewport.WithWidth(50), viewport.WithHeight(20))
|
||||
vp.SetContent("")
|
||||
|
||||
sp := spinner.New()
|
||||
sp.Spinner = spinner.MiniDot
|
||||
sp.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#8B5CF6"))
|
||||
|
||||
return tea.NewProgram(model{
|
||||
config: cfg,
|
||||
translator: t,
|
||||
messages: make([]ChatMessage, 0),
|
||||
input: ta,
|
||||
viewport: vp,
|
||||
spinner: sp,
|
||||
keys: keys,
|
||||
targetLang: getDefaultLang(cfg),
|
||||
})
|
||||
@@ -77,7 +92,7 @@ func getDefaultLang(cfg *config.Config) string {
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return nil
|
||||
return m.spinner.Tick
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
@@ -135,6 +150,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.viewport, cmd = m.viewport.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
m.spinner, cmd = m.spinner.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
@@ -213,34 +231,24 @@ func (m *model) updateViewportContent() {
|
||||
}
|
||||
|
||||
func (m *model) renderTranslationCard(msg ChatMessage) string {
|
||||
metaContent := lipgloss.JoinHorizontal(
|
||||
lipgloss.Left,
|
||||
CardMetaStyle.Render(fmt.Sprintf("Tokens: %d", msg.Tokens)),
|
||||
CardMetaSeparatorStyle,
|
||||
CardMetaStyle.Render(fmt.Sprintf("耗时: %s", msg.Timestamp.Format("15:04:05"))),
|
||||
CardMetaSeparatorStyle,
|
||||
CardMetaStyle.Render(fmt.Sprintf("模型: %s", msg.Model)),
|
||||
)
|
||||
contentWidth := m.viewport.Width() - 2
|
||||
|
||||
metaBlock := lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.Border{
|
||||
Top: "─",
|
||||
Bottom: "─",
|
||||
Left: "│",
|
||||
Right: "│",
|
||||
}).
|
||||
BorderForeground(lipgloss.Color("#374151")).
|
||||
Width(m.viewport.Width() - 2).
|
||||
Render(metaContent)
|
||||
timeStr := msg.Timestamp.Format("15:04")
|
||||
timeLabel := CardTimeStyle.Render("# " + timeStr)
|
||||
|
||||
inputContent := lipgloss.NewStyle().
|
||||
Width(m.viewport.Width() - 2).
|
||||
inputText := lipgloss.NewStyle().
|
||||
Width(contentWidth).
|
||||
Render(msg.Input)
|
||||
|
||||
inputBlock := lipgloss.NewStyle().
|
||||
Background(lipgloss.Color("#1A1A1A")).
|
||||
Padding(1, 3).
|
||||
Width(m.viewport.Width()).
|
||||
Render(inputContent)
|
||||
Render(lipgloss.JoinVertical(
|
||||
lipgloss.Top,
|
||||
timeLabel,
|
||||
inputText,
|
||||
))
|
||||
|
||||
var outputBlock string
|
||||
if msg.Error != "" {
|
||||
@@ -249,6 +257,7 @@ func (m *model) renderTranslationCard(msg ChatMessage) string {
|
||||
Render(msg.Error)
|
||||
outputBlock = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#F87171")).
|
||||
Padding(0, 3, 1, 3).
|
||||
Width(m.viewport.Width()).
|
||||
Render(outputContent)
|
||||
} else {
|
||||
@@ -256,16 +265,25 @@ func (m *model) renderTranslationCard(msg ChatMessage) string {
|
||||
Width(m.viewport.Width() - 2).
|
||||
Render(msg.Output)
|
||||
outputBlock = lipgloss.NewStyle().
|
||||
Padding(0, 3, 1, 3).
|
||||
Width(m.viewport.Width()).
|
||||
Render(outputContent)
|
||||
}
|
||||
|
||||
footerContent := lipgloss.NewStyle().
|
||||
MarginLeft(3).
|
||||
Render(lipgloss.JoinHorizontal(
|
||||
lipgloss.Left,
|
||||
CardFooterIconStyle.Render("▣"),
|
||||
CardFooterTextStyle.Render(msg.Model+" · "+strconv.Itoa(msg.Tokens)),
|
||||
))
|
||||
|
||||
return CardStyle.Render(
|
||||
lipgloss.JoinVertical(
|
||||
lipgloss.Top,
|
||||
metaBlock,
|
||||
inputBlock,
|
||||
outputBlock,
|
||||
footerContent,
|
||||
),
|
||||
) + "\n"
|
||||
}
|
||||
@@ -285,19 +303,55 @@ func (m model) View() tea.View {
|
||||
header := m.renderHeader()
|
||||
messages := m.viewport.View()
|
||||
inputArea := m.renderInputArea()
|
||||
statusBar := m.renderStatusBar()
|
||||
infoBar := m.renderInfoBar()
|
||||
spinnerView := m.renderSpinner()
|
||||
|
||||
content := header + "\n" + messages + inputArea + statusBar
|
||||
content := header + "\n" + messages + inputArea + infoBar + spinnerView
|
||||
v := tea.NewView(content)
|
||||
v.AltScreen = true
|
||||
return v
|
||||
}
|
||||
|
||||
func gradientText(text string, startColor, endColor string) string {
|
||||
startR, startG, startB := parseHexColor(startColor)
|
||||
endR, endG, endB := parseHexColor(endColor)
|
||||
|
||||
lines := strings.Split(text, "\n")
|
||||
if len(lines) == 0 {
|
||||
return text
|
||||
}
|
||||
|
||||
var result []string
|
||||
for _, line := range lines {
|
||||
if len(line) == 0 {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
|
||||
var coloredLine string
|
||||
for i, char := range line {
|
||||
ratio := float64(i) / float64(len(line)-1)
|
||||
r := int(float64(startR) + float64(endR-startR)*ratio)
|
||||
g := int(float64(startG) + float64(endG-startG)*ratio)
|
||||
b := int(float64(startB) + float64(endB-startB)*ratio)
|
||||
coloredLine += fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[0m", r, g, b, string(char))
|
||||
}
|
||||
result = append(result, coloredLine)
|
||||
}
|
||||
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
func parseHexColor(hex string) (int, int, int) {
|
||||
hex = strings.TrimPrefix(hex, "#")
|
||||
r, _ := strconv.ParseInt(hex[0:2], 16, 64)
|
||||
g, _ := strconv.ParseInt(hex[2:4], 16, 64)
|
||||
b, _ := strconv.ParseInt(hex[4:6], 16, 64)
|
||||
return int(r), int(g), int(b)
|
||||
}
|
||||
|
||||
func (m model) renderHeader() string {
|
||||
title := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#8B5CF6")).
|
||||
Bold(true).
|
||||
Render("✦ YOYO 翻译")
|
||||
title := gradientText(logoPattern, "#8B5CF6", "#EC4899")
|
||||
|
||||
width := m.width - 4
|
||||
if width < 20 {
|
||||
@@ -310,57 +364,50 @@ func (m model) renderHeader() string {
|
||||
|
||||
return lipgloss.NewStyle().
|
||||
Width(width).
|
||||
Render(title + strings.Repeat(" ", width-len(title)-len(right)-1) + right)
|
||||
Render(title + strings.Repeat(" ", width-29-len(right)-1) + right)
|
||||
}
|
||||
|
||||
func (m model) renderInputArea() string {
|
||||
inputView := m.input.View()
|
||||
|
||||
container := lipgloss.NewStyle().
|
||||
Width(m.input.Width() + 1).
|
||||
BorderStyle(lipgloss.Border{
|
||||
Top: "─",
|
||||
Bottom: "─",
|
||||
Left: "│",
|
||||
Right: "│",
|
||||
}).
|
||||
BorderForeground(lipgloss.Color("#60A5FA"))
|
||||
|
||||
return "\n" + container.Render(inputView) + "\n"
|
||||
separator := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#8B5CF6")).
|
||||
Render(":::")
|
||||
return "\n" + separator + "\n" + inputView + "\n"
|
||||
}
|
||||
|
||||
func (m model) renderStatusBar() string {
|
||||
langInfo := "目标: " + m.targetLang
|
||||
modelInfo := "模型: " + getModelName(m.config)
|
||||
tokensInfo := "Tokens: -"
|
||||
if len(m.messages) > 0 {
|
||||
lastMsg := m.messages[len(m.messages)-1]
|
||||
if lastMsg.Tokens > 0 {
|
||||
tokensInfo = fmt.Sprintf("Tokens: %d", lastMsg.Tokens)
|
||||
func (m model) renderInfoBar() string {
|
||||
var recordCount int
|
||||
if m.translator != nil && m.translator.GetCache() != nil {
|
||||
stats, _ := m.translator.GetCache().Stats(context.Background())
|
||||
if stats != nil {
|
||||
recordCount = stats.TotalRecords
|
||||
}
|
||||
}
|
||||
|
||||
statusDot := StatusDotStyle.Render("●")
|
||||
var countInfo string
|
||||
if recordCount == 0 {
|
||||
countInfo = "暂无记录"
|
||||
} else {
|
||||
countInfo = fmt.Sprintf("%d条", recordCount)
|
||||
}
|
||||
|
||||
sep := lipgloss.NewStyle().Foreground(lipgloss.Color("#8B5CF6")).Render(" ")
|
||||
separator := lipgloss.NewStyle().Foreground(lipgloss.Color("#8B5CF6")).Render(":::")
|
||||
lang := lipgloss.NewStyle().Foreground(lipgloss.Color("#F87171")).Render(m.targetLang)
|
||||
model := lipgloss.NewStyle().Foreground(lipgloss.Color("#FAFAFA")).Render(getModelName(m.config))
|
||||
count := lipgloss.NewStyle().Foreground(lipgloss.Color("#6B7280")).Render(countInfo)
|
||||
|
||||
result := separator + sep + lang + sep + model + sep + count
|
||||
|
||||
if m.loading {
|
||||
statusDot = LoadingStyle.Render("○")
|
||||
result += sep + m.spinner.View() + " 翻译中..."
|
||||
}
|
||||
|
||||
sep := StatusItemStyle.Render(" │ ")
|
||||
return result + "\n"
|
||||
}
|
||||
|
||||
width := m.width - 4
|
||||
if width < 30 {
|
||||
width = 60
|
||||
}
|
||||
|
||||
status := StatusItemStyle.Render(langInfo) +
|
||||
sep + StatusItemStyle.Render(modelInfo) +
|
||||
sep + StatusItemStyle.Render(tokensInfo) +
|
||||
sep + statusDot + " " + StatusValueStyle.Render(m.getStatusText())
|
||||
|
||||
return lipgloss.NewStyle().
|
||||
Width(width).
|
||||
Background(lipgloss.Color("#1F2937")).
|
||||
Render(" " + status)
|
||||
func (m model) renderSpinner() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m model) getStatusText() string {
|
||||
|
||||
@@ -51,7 +51,7 @@ var (
|
||||
Foreground(lipgloss.Color("#34D399"))
|
||||
|
||||
CardStyle = lipgloss.NewStyle().
|
||||
MarginBottom(5)
|
||||
MarginBottom(1)
|
||||
|
||||
CardMetaStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#6B7280")).
|
||||
@@ -68,4 +68,17 @@ var (
|
||||
|
||||
CardOutputStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
CardTimeStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#6B7280")).
|
||||
MarginBottom(1)
|
||||
|
||||
CardFooterIconStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#8B5CF6")).
|
||||
AlignVertical(lipgloss.Center)
|
||||
|
||||
CardFooterTextStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#6B7280")).
|
||||
MarginLeft(2).
|
||||
AlignVertical(lipgloss.Center)
|
||||
)
|
||||
|
||||
38
taolun.md
38
taolun.md
@@ -758,4 +758,40 @@ ta.SetHeight(5) // 固定高度,不动态调整
|
||||
**下一步**: 实现组件代码
|
||||
|
||||
**关联文档**:
|
||||
- [changelog.md#0.8.1](changelog.md#081)
|
||||
- [changelog.md#0.8.1](changelog.md#081)
|
||||
|
||||
---
|
||||
|
||||
### [2026-04-07] 版本 1.0.0-beta - Logo和信息栏改造
|
||||
|
||||
**原因**: 用户希望改进TUI界面的视觉效果,使标题更独特,输入框和信息栏更美观
|
||||
|
||||
**分析**:
|
||||
- 原标题 "✦ YOYO 翻译" 过于简单
|
||||
- 输入框需要更好的视觉分隔
|
||||
- 需要添加翻译状态动画
|
||||
|
||||
**解决方案**:
|
||||
1. **标题Logo**:
|
||||
- 使用ASCII艺术 "l_ _ _____ _____"
|
||||
- 实现紫色→粉色渐变效果 (ANSI True Color)
|
||||
- 右侧显示版本号 [v1.0.0-beta]
|
||||
|
||||
2. **输入框改造**:
|
||||
- 去掉边框
|
||||
- 上下使用紫色 `:::` 分隔符
|
||||
- Ctrl+J 启用换行
|
||||
|
||||
3. **信息栏改造**:
|
||||
- 合并显示:语言(红色) + 模型名(白色) + 缓存记录(碳黑)
|
||||
- 翻译时显示 Spinner 动画 (MiniDot)
|
||||
|
||||
4. **翻译卡片优化**:
|
||||
- `▣` 图标边距调整
|
||||
|
||||
**版本号规则**:
|
||||
- 版本号需与 git 标签、changelog.md 中的版本号保持三方同步
|
||||
- 遵循语义化版本:主版本.次版本.修订版本
|
||||
- beta版使用 `-beta` 后缀
|
||||
|
||||
**关联版本**: [changelog.md#1.0.0-beta](changelog.md#100-beta-2026-04-07)
|
||||
Reference in New Issue
Block a user