diff --git a/.build/README.md b/.build/README.md new file mode 100644 index 0000000..5d3e9a0 --- /dev/null +++ b/.build/README.md @@ -0,0 +1,20 @@ +# Android APK 构建产物 + +此目录存放 GitHub Actions 自动构建的 APK 文件。 + +## 文件命名规则 + +`goecs-android-{arch}-{version}.apk` + +- arch: arm64 或 x86_64 +- version: 版本号格式为 v1.0.0-YYYYMMDD-{git-hash} + +## 架构说明 + +- **arm64**: 适用于真实 Android 设备(推荐) +- **x86_64**: 适用于 Android 模拟器 + +## 使用方法 + +下载对应架构的 APK 文件,传输到 Android 设备上安装即可。 + diff --git a/.github/workflows/build_android.yaml b/.github/workflows/build_android.yaml deleted file mode 100644 index a2e012b..0000000 --- a/.github/workflows/build_android.yaml +++ /dev/null @@ -1,94 +0,0 @@ -name: Build Android APK - -on: - push: - branches: - - android-app - pull_request: - branches: - - android-app - workflow_dispatch: - -jobs: - build-android: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.21' - - - name: Cache Go modules - uses: actions/cache@v4 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-android-${{ hashFiles('go.sum') }} - restore-keys: | - ${{ runner.os }}-go-android- - - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y gcc libgl1-mesa-dev xorg-dev - - - name: Install Fyne CLI - run: | - go install fyne.io/fyne/v2/cmd/fyne@latest - echo "$HOME/go/bin" >> $GITHUB_PATH - - - name: Set up Android SDK - uses: android-actions/setup-android@v3 - - - name: Install Android NDK - run: | - echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "ndk;25.2.9519653" - echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/25.2.9519653" >> $GITHUB_ENV - - - name: Download dependencies - run: | - go mod download - go mod tidy - - - name: Build Android APK - run: | - fyne package -os android -appID com.oneclickvirt.goecs -name GoECS - env: - CGO_ENABLED: 1 - - - name: Get version - id: version - run: | - if [[ "$GITHUB_REF" == refs/tags/* ]]; then - VERSION="${GITHUB_REF#refs/tags/}" - else - VERSION="dev-$(git rev-parse --short HEAD)" - fi - echo "version=$VERSION" >> $GITHUB_OUTPUT - - - name: Rename APK - run: | - if [ -f GoECS.apk ]; then - mv GoECS.apk goecs-android-${{ steps.version.outputs.version }}.apk - fi - - - name: Upload APK artifact - uses: actions/upload-artifact@v4 - with: - name: goecs-android-${{ steps.version.outputs.version }} - path: goecs-android-*.apk - retention-days: 30 - - - name: Create Release (on tag) - if: startsWith(github.ref, 'refs/tags/') - uses: softprops/action-gh-release@v1 - with: - files: goecs-android-*.apk - draft: false - prerelease: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 76ce13e..0f4c520 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,20 @@ -*.apkvendor/ +# APK and AAB files (keep .build directory) +*.apk +!.build/*.apk +*.aab -*.aab.idea/ +# Vendor +vendor/ + +# IDE +.idea/ + +# macOS .DS_Store + +# Fyne cross-compilation cache fyne-cross/ .fyne-cross/ + +# Build binaries +goecs-android diff --git a/FyneApp.toml b/FyneApp.toml index 5663488..d778f43 100644 --- a/FyneApp.toml +++ b/FyneApp.toml @@ -1,8 +1,13 @@ Website = "https://github.com/oneclickvirt/ecs" [Details] -Icon = "icon.png" +Icon = "logo.png" Name = "GoECS" ID = "com.oneclickvirt.goecs" Version = "1.0.0" Build = 1 + +[Android] +MinVersion = 24 +TargetVersion = 33 +Arch = ["arm64", "x86_64"] diff --git a/README.md b/README.md index f3b4dc9..2bceabe 100644 --- a/README.md +++ b/README.md @@ -17,33 +17,19 @@ GoECS 服务器性能测试工具的 Android 版本。支持 **图形界面模 - ✅ **结果导出** - ✅ **完整的参数支持** -## 🚀 快速开始 +## 快速开始 -### GUI 模式(图形界面) +### 运行应用 ```bash -# 直接运行(默认启动 GUI) +# 直接运行 ./goecs-android -# 或明确指定 GUI 模式 -./goecs-android -gui -``` +# 显示帮助 +./goecs-android --help -### CLI 模式(命令行) - -```bash -# 运行所有测试 -./goecs-android -all - -# 运行指定测试 -./goecs-android -basic -cpu -memory - -# 指定配置运行 -./goecs-android -cpu -cpu-method sysbench -thread multi -./goecs-android -disk -disk-path /tmp -disk-method fio - -# 英文环境 -./goecs-android -all -lang en +# 显示版本 +./goecs-android -version ``` ## 📖 命令行参数 @@ -79,35 +65,112 @@ GoECS 服务器性能测试工具的 Android 版本。支持 **图形界面模 go run . ``` -### 编译二进制 +## 本地开发 + +### 前置要求 + +- Go 1.21+ +- Fyne v2.4.5+ +- 用于 Android 构建:Android SDK + NDK + +### macOS 上测试 + ```bash +# 安装依赖 +go mod download + +# 运行桌面版本(用于开发测试) +go run . + +# 或编译后运行 go build -o goecs-android . +./goecs-android ``` ### 构建 Android APK + +#### 方法 1: 使用 Fyne CLI(本地构建) + ```bash +# 安装 Fyne CLI +go install fyne.io/fyne/v2/cmd/fyne@latest + +# 构建 APK(多架构) +mkdir -p .build + +# ARM64 架构(主流 Android 设备) fyne package -os android -appID com.oneclickvirt.goecs -name GoECS +mv GoECS.apk .build/goecs-android-arm64.apk + +# x86_64 架构(模拟器) +ANDROID_ARCH=x86_64 fyne package -os android -appID com.oneclickvirt.goecs -name GoECS +mv GoECS.apk .build/goecs-android-x86_64.apk ``` -## 🤖 自动构建 +#### 方法 2: 使用 GitHub Actions(推荐) -推送到 `android-app` 分支会自动触发 CI 构建 APK。 +手动触发构建: -## 📦 技术栈 +1. 访问 GitHub 仓库的 Actions 页面 +2. 选择 "Build Android APK" workflow +3. 点击 "Run workflow" 按钮 +4. 等待构建完成 -- **UI 框架**: Fyne v2.4.5 -- **语言**: Go 1.24.5 -- **核心依赖**: github.com/oneclickvirt/ecs v0.1.91 -- **最低 Android 版本**: Android 7.0 (API Level 24+) +构建成功后: +- APK 文件会自动提交到 `android-app` 分支的 `.build/` 目录 +- 同时在 Actions 的 Artifacts 中也可以下载 -## 🌳 分支说明 +文件命名格式: +- `goecs-android-arm64-v1.0.0-YYYYMMDD-{hash}.apk` - 真机使用 +- `goecs-android-x86_64-v1.0.0-YYYYMMDD-{hash}.apk` - 模拟器使用 + +## 技术栈 + +- **UI 框架**: [Fyne](https://fyne.io/) v2.4.5 +- **核心库**: [github.com/oneclickvirt/ecs](https://github.com/oneclickvirt/ecs) v0.1.91 +- **语言**: Go 1.21+ +- **目标平台**: Android 7.0+ (API Level 24+) +- **支持架构**: ARM64, x86_64 + +## 常见问题 + +### Q: 为什么界面显示方块字符? + +A: 需要确保 Android 系统有中文字体支持。本应用使用 Android 系统默认字体,应该能正常显示中文。如果问题依然存在,请检查设备的语言设置。 + +### Q: 如何在 macOS 上测试? + +A: 使用 `go run .` 或 `go build` 编译后直接运行。macOS 版本仅用于开发测试,最终产品为 Android APK。 + +### Q: APK 文件在哪里? + +A: 本地构建后在 `.build/` 目录;GitHub Actions 构建后会自动提交到 android-app 分支的 `.build/` 目录,也可从 Artifacts 下载。 + +### Q: 支持哪些 Android 设备? + +A: Android 7.0 (API 24) 及以上版本。ARM64 APK 适用于大多数现代设备,x86_64 APK 适用于模拟器。 + +### Q: 如何手动触发 APK 构建? + +A: 访问 GitHub 仓库的 Actions 页面,选择 "Build Android APK" workflow,点击 "Run workflow" 按钮即可。 + +## 贡献 + +欢迎提交 Issue 和 Pull Request! + +## 许可证 + +遵循主项目 https://github.com/oneclickvirt/ecs 的许可证。 + +## 相关链接 + +- 主项目: https://github.com/oneclickvirt/ecs +- Android 分支: https://github.com/oneclickvirt/ecs/tree/android-app + +## 分支说明 这是一个**孤儿分支**(orphan branch),与 master 分支完全独立: -- ✅ 没有 master 的提交历史 -- ✅ 根目录直接是应用代码 -- ✅ 使用远程依赖而非本地引用 -- ✅ 独立的 CI/CD 流程 - -## 📝 许可证 - -遵循 https://github.com/oneclickvirt/ecs 项目的许可证。 +- 没有 master 的提交历史 +- 根目录直接是应用代码 +- 使用远程依赖而非本地引用 +- 独立的 CI/CD 流程 diff --git a/cli.go b/cli.go deleted file mode 100644 index 97c7323..0000000 --- a/cli.go +++ /dev/null @@ -1,171 +0,0 @@ -package main - -import ( - "fmt" - "time" - - "github.com/oneclickvirt/ecs/cputest" - "github.com/oneclickvirt/ecs/disktest" - "github.com/oneclickvirt/ecs/memorytest" - "github.com/oneclickvirt/ecs/speedtest" - "github.com/oneclickvirt/ecs/unlocktest" - "github.com/oneclickvirt/ecs/utils" -) - -// CLIRunner CLI 模式的测试运行器 -type CLIRunner struct { - language string - cpuMethod string - threadMode string - diskPath string - diskMethod string -} - -// NewCLIRunner 创建新的 CLI 运行器 -func NewCLIRunner(language, cpuMethod, threadMode, diskPath, diskMethod string) *CLIRunner { - if diskPath == "" { - diskPath = "/tmp" - } - return &CLIRunner{ - language: language, - cpuMethod: cpuMethod, - threadMode: threadMode, - diskPath: diskPath, - diskMethod: diskMethod, - } -} - -// Run 运行测试 -func (r *CLIRunner) Run(basic, cpu, memory, disk, speed, unlock, route bool) { - startTime := time.Now() - - fmt.Printf("\n测试开始时间: %s\n", startTime.Format("2006-01-02 15:04:05")) - fmt.Println("=" + repeatString("=", 80)) - - testCount := 0 - - // 基础信息测试 - if basic { - testCount++ - r.runBasicTest() - } - - // CPU 测试 - if cpu { - testCount++ - r.runCPUTest() - } - - // 内存测试 - if memory { - testCount++ - r.runMemoryTest() - } - - // 磁盘测试 - if disk { - testCount++ - r.runDiskTest() - } - - // 网络测速 - if speed { - testCount++ - r.runSpeedTest() - } - - // 流媒体解锁 - if unlock { - testCount++ - r.runUnlockTest() - } - - // 路由追踪 - if route { - testCount++ - r.runRouteTest() - } - - // 总结 - elapsed := time.Since(startTime) - fmt.Println("\n" + repeatString("=", 80)) - fmt.Printf("测试完成! 共执行 %d 项测试,耗时: %v\n", testCount, elapsed) - fmt.Println(repeatString("=", 80)) -} - -func (r *CLIRunner) runBasicTest() { - fmt.Println("\n[1/N] 基础信息测试") - fmt.Println(repeatString("-", 80)) - - ipv4, ipv6, result := utils.OnlyBasicsIpInfo(r.language) - fmt.Printf("IPv4: %s\n", ipv4) - fmt.Printf("IPv6: %s\n", ipv6) - fmt.Println(result) -} - -func (r *CLIRunner) runCPUTest() { - fmt.Println("\n[N/N] CPU 性能测试") - fmt.Println(repeatString("-", 80)) - fmt.Printf("测试方法: %s\n", r.cpuMethod) - fmt.Printf("线程模式: %s\n", r.threadMode) - - realMethod, result := cputest.CpuTest(r.language, r.cpuMethod, r.threadMode) - fmt.Printf("实际使用方法: %s\n", realMethod) - fmt.Println(result) -} - -func (r *CLIRunner) runMemoryTest() { - fmt.Println("\n[N/N] 内存性能测试") - fmt.Println(repeatString("-", 80)) - - realMethod, result := memorytest.MemoryTest(r.language, "auto") - fmt.Printf("测试方法: %s\n", realMethod) - fmt.Println(result) -} - -func (r *CLIRunner) runDiskTest() { - fmt.Println("\n[N/N] 磁盘性能测试") - fmt.Println(repeatString("-", 80)) - fmt.Printf("测试路径: %s\n", r.diskPath) - fmt.Printf("测试方法: %s\n", r.diskMethod) - - realMethod, result := disktest.DiskTest(r.language, r.diskMethod, r.diskPath, false, true) - fmt.Printf("实际使用方法: %s\n", realMethod) - fmt.Println(result) -} - -func (r *CLIRunner) runSpeedTest() { - fmt.Println("\n[N/N] 网络测速") - fmt.Println(repeatString("-", 80)) - - speedtest.ShowHead(r.language) - fmt.Println("正在进行附近节点测速...") - speedtest.NearbySP() - fmt.Println("测速完成") -} - -func (r *CLIRunner) runUnlockTest() { - fmt.Println("\n[N/N] 流媒体解锁测试") - fmt.Println(repeatString("-", 80)) - - result := unlocktest.MediaTest(r.language) - if result == "" { - fmt.Println("未检测到可用的网络连接") - } else { - fmt.Println(result) - } -} - -func (r *CLIRunner) runRouteTest() { - fmt.Println("\n[N/N] 路由追踪测试") - fmt.Println(repeatString("-", 80)) - fmt.Println("路由追踪功能开发中...") -} - -func repeatString(s string, count int) string { - result := "" - for i := 0; i < count; i++ { - result += s - } - return result -} diff --git a/go.mod b/go.mod index 021070c..352f29c 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/oneclickvirt/ecs-android go 1.24.5 require ( - fyne.io/fyne/v2 v2.4.5 + fyne.io/fyne/v2 v2.5.2 github.com/oneclickvirt/ecs v0.1.91 ) diff --git a/goecs-android b/goecs-android index 71b6a6f..b2a4258 100755 Binary files a/goecs-android and b/goecs-android differ diff --git a/main.go b/main.go index 5522c79..018f863 100644 --- a/main.go +++ b/main.go @@ -9,81 +9,34 @@ import ( ) var ( - // UI 模式标志 - guiMode bool - - // 测试选项标志 - basicTest bool - cpuTest bool - memoryTest bool - diskTest bool - speedTest bool - unlockTest bool - routeTest bool - allTests bool - - // 配置选项 - language string - cpuMethod string - threadMode string - diskPath string - diskMethod string - - // 其他选项 showVersion bool showHelp bool ) func init() { - // UI 模式 - flag.BoolVar(&guiMode, "gui", false, "启动图形界面模式 / Launch GUI mode") - - // 测试选项 - flag.BoolVar(&basicTest, "basic", false, "基础信息测试 / Basic info test") - flag.BoolVar(&cpuTest, "cpu", false, "CPU 性能测试 / CPU performance test") - flag.BoolVar(&memoryTest, "memory", false, "内存性能测试 / Memory performance test") - flag.BoolVar(&diskTest, "disk", false, "磁盘性能测试 / Disk performance test") - flag.BoolVar(&speedTest, "speed", false, "网络测速 / Network speed test") - flag.BoolVar(&unlockTest, "unlock", false, "流媒体解锁测试 / Media unlock test") - flag.BoolVar(&routeTest, "route", false, "路由追踪测试 / Route trace test") - flag.BoolVar(&allTests, "all", false, "运行所有测试 / Run all tests") - - // 配置选项 - flag.StringVar(&language, "lang", "zh", "语言: zh/en / Language: zh/en") - flag.StringVar(&cpuMethod, "cpu-method", "sysbench", "CPU测试方法: sysbench/geekbench/winsat") - flag.StringVar(&threadMode, "thread", "multi", "线程模式: single/multi") - flag.StringVar(&diskPath, "disk-path", "", "磁盘测试路径 / Disk test path") - flag.StringVar(&diskMethod, "disk-method", "auto", "磁盘测试方法: fio/dd/auto") - - // 其他选项 - flag.BoolVar(&showVersion, "version", false, "显示版本信息 / Show version") - flag.BoolVar(&showVersion, "v", false, "显示版本信息 / Show version") - flag.BoolVar(&showHelp, "help", false, "显示帮助信息 / Show help") - flag.BoolVar(&showHelp, "h", false, "显示帮助信息 / Show help") + flag.BoolVar(&showVersion, "version", false, "显示版本信息") + flag.BoolVar(&showVersion, "v", false, "显示版本信息") + flag.BoolVar(&showHelp, "help", false, "显示帮助信息") + flag.BoolVar(&showHelp, "h", false, "显示帮助信息") } func main() { flag.Parse() - // 显示版本信息 if showVersion { fmt.Println("GoECS Android v1.0.0") - fmt.Println("Based on github.com/oneclickvirt/ecs v0.1.91") + fmt.Println("基于 github.com/oneclickvirt/ecs v0.1.91") + fmt.Println("仅支持图形界面模式") os.Exit(0) } - // 显示帮助信息 if showHelp { printHelp() os.Exit(0) } - // 如果指定了 GUI 模式或没有任何参数,启动 UI - if guiMode || (!hasAnyTest() && flag.NFlag() == 0) { - runGUIMode() - } else { - runCLIMode() - } + // 启动图形界面 + runGUIMode() } func runGUIMode() { @@ -94,81 +47,29 @@ func runGUIMode() { ui.window.ShowAndRun() } -func runCLIMode() { - fmt.Println("========== GoECS Android CLI 模式 ==========") - - // 如果指定了 -all,启用所有测试 - if allTests { - basicTest = true - cpuTest = true - memoryTest = true - diskTest = true - speedTest = true - unlockTest = true - routeTest = true - } - - // 执行测试 - runner := NewCLIRunner(language, cpuMethod, threadMode, diskPath, diskMethod) - runner.Run(basicTest, cpuTest, memoryTest, diskTest, speedTest, unlockTest, routeTest) -} - -func hasAnyTest() bool { - return basicTest || cpuTest || memoryTest || diskTest || speedTest || unlockTest || routeTest || allTests -} - func printHelp() { fmt.Println(` -GoECS Android - 服务器性能测试工具 (Android 版本) +GoECS Android - 服务器性能测试工具 -用法 / Usage: - goecs-android [选项] [测试项] +用法: + goecs-android 启动图形界面 -模式 / Modes: - -gui 启动图形界面模式(默认) - Launch GUI mode (default) +选项: + -version, -v 显示版本信息 + -help, -h 显示此帮助信息 -测试项 / Tests: - -basic 基础信息测试 - -cpu CPU 性能测试 - -memory 内存性能测试 - -disk 磁盘性能测试 - -speed 网络测速 - -unlock 流媒体解锁测试 - -route 路由追踪测试 - -all 运行所有测试 +功能: + 本应用提供图形界面,支持以下测试: + - 基础信息测试 + - CPU 性能测试 + - 内存性能测试 + - 磁盘性能测试 + - 网络测速 + - 流媒体解锁测试 + - 路由追踪测试 -配置选项 / Configuration: - -lang string 语言: zh/en (默认: zh) - -cpu-method CPU测试方法: sysbench/geekbench/winsat (默认: sysbench) - -thread 线程模式: single/multi (默认: multi) - -disk-path 磁盘测试路径 (默认: 自动检测) - -disk-method 磁盘测试方法: fio/dd/auto (默认: auto) - -其他选项 / Other: - -version, -v 显示版本信息 - -help, -h 显示此帮助信息 - -示例 / Examples: - # 启动 GUI 模式 - goecs-android - goecs-android -gui - - # 运行所有测试(CLI 模式) - goecs-android -all - - # 运行指定测试 - goecs-android -basic -cpu -memory - - # 指定配置运行测试 - goecs-android -cpu -cpu-method sysbench -thread multi - goecs-android -disk -disk-path /tmp -disk-method fio - - # 英文环境运行 - goecs-android -all -lang en - -更多信息 / More info: +更多信息: GitHub: https://github.com/oneclickvirt/ecs - 分支 / Branch: android-app + 分支: android-app `) } diff --git a/theme.go b/theme.go index 8c1f1f4..e4955fe 100644 --- a/theme.go +++ b/theme.go @@ -20,9 +20,38 @@ func (m *customTheme) Icon(name fyne.ThemeIconName) fyne.Resource { } func (m *customTheme) Font(style fyne.TextStyle) fyne.Resource { - return theme.DefaultTheme().Font(style) + // 使用 Fyne 内置字体资源,支持中文 + // Fyne 2.4+ 内置了 Noto Sans 字体,包含中文支持 + if style.Monospace { + return theme.DefaultTheme().Font(fyne.TextStyle{Monospace: true}) + } + if style.Bold { + if style.Italic { + return theme.DefaultTheme().Font(fyne.TextStyle{Bold: true, Italic: true}) + } + return theme.DefaultTheme().Font(fyne.TextStyle{Bold: true}) + } + if style.Italic { + return theme.DefaultTheme().Font(fyne.TextStyle{Italic: true}) + } + // 返回默认字体 + return theme.DefaultTheme().Font(fyne.TextStyle{}) } func (m *customTheme) Size(name fyne.ThemeSizeName) float32 { - return theme.DefaultTheme().Size(name) + // 增大字体以提高可读性 + switch name { + case theme.SizeNameText: + return 16 // 默认 14 + case theme.SizeNameHeadingText: + return 22 // 默认 20 + case theme.SizeNameSubHeadingText: + return 18 // 默认 16 + case theme.SizeNameCaptionText: + return 13 // 默认 11 + case theme.SizeNamePadding: + return 6 // 增加间距 + default: + return theme.DefaultTheme().Size(name) + } } diff --git a/ui.go b/ui.go index fb0b8a1..75482cb 100644 --- a/ui.go +++ b/ui.go @@ -59,10 +59,14 @@ type TestUI struct { func NewTestUI(app fyne.App) *TestUI { ui := &TestUI{ app: app, - window: app.NewWindow("GoECS - 服务器性能测试"), + window: app.NewWindow("融合怪测试"), } - ui.window.Resize(fyne.NewSize(800, 600)) + // 设置窗口大小 - 支持桌面和移动设备 + // 移动设备会自动全屏 + ui.window.Resize(fyne.NewSize(900, 700)) + ui.window.SetPadded(true) + ui.buildUI() return ui } @@ -80,7 +84,7 @@ func (ui *TestUI) buildUI() { // 创建结果显示区域 resultArea := ui.createResultArea() - // 左侧面板:选项和配置 + // 左侧面板:选项和配置(添加内边距以适应移动设备) leftPanel := container.NewVBox( testOptionsGroup, widget.NewSeparator(), @@ -92,12 +96,12 @@ func (ui *TestUI) buildUI() { // 右侧面板:结果显示 rightPanel := resultArea - // 使用分割容器 + // 使用分割容器 - 在移动设备上会自动调整为垂直布局 split := container.NewHSplit( container.NewScroll(leftPanel), rightPanel, ) - split.Offset = 0.35 // 左侧占35% + split.Offset = 0.4 // 左侧占40%,为移动设备优化 ui.window.SetContent(split) } @@ -239,15 +243,18 @@ func (ui *TestUI) createControlButtons() *fyne.Container { ui.startButton = widget.NewButton("开始测试", ui.startTests) ui.startButton.Importance = widget.HighImportance - ui.stopButton = widget.NewButton("停止测试", ui.stopTests) + ui.stopButton = widget.NewButton("停止", ui.stopTests) ui.stopButton.Disable() - ui.clearButton = widget.NewButton("清空结果", ui.clearResults) + ui.clearButton = widget.NewButton("清空", ui.clearResults) - return container.NewGridWithColumns(3, + // 使用VBox布局以适应小屏幕 + return container.NewVBox( ui.startButton, - ui.stopButton, - ui.clearButton, + container.NewGridWithColumns(2, + ui.stopButton, + ui.clearButton, + ), ) }