auto-save 2026-04-01 09:03 (+7, ~1)

This commit is contained in:
2026-04-01 09:04:03 +08:00
parent 35696488f4
commit cb9d421363
2182 changed files with 6161 additions and 1 deletions

View File

@@ -0,0 +1,157 @@
import SwiftUI
@MainActor
class AppState: ObservableObject {
// API endpoints
let memoryAPI = "http://localhost:3202/api/memories"
let portAPI = "http://localhost:3201/api/ports"
let portStatsAPI = "http://localhost:3201/api/ports/stats"
let asrStatusAPI = "http://localhost:4125/api/asr-status"
// Data
@Published var projects: [Project] = []
@Published var portStats: PortStats = PortStats()
@Published var asrStatus: ASRStatus = ASRStatus()
@Published var isLoading = false
@Published var lastRefresh: Date?
@Published var searchText = ""
// Services status
@Published var memoryOnline = false
@Published var portsOnline = false
@Published var asrOnline = false
private var refreshTask: Task<Void, Never>?
init() {
startAutoRefresh()
}
deinit {
refreshTask?.cancel()
}
private func startAutoRefresh() {
refreshTask = Task { @MainActor [weak self] in
guard let self = self else { return }
// Initial refresh
await self.refresh()
// Auto-refresh every 30 seconds
while !Task.isCancelled {
try? await Task.sleep(nanoseconds: 30_000_000_000)
if !Task.isCancelled {
await self.refresh()
}
}
}
}
func refresh() async {
isLoading = true
async let p: Void = fetchProjects()
async let s: Void = fetchPortStats()
async let a: Void = fetchASRStatus()
_ = await (p, s, a)
isLoading = false
lastRefresh = Date()
}
var filteredProjects: [Project] {
if searchText.isEmpty { return projects }
let q = searchText.lowercased()
return projects.filter {
$0.name.lowercased().contains(q) ||
$0.slug.lowercased().contains(q) ||
($0.what ?? "").lowercased().contains(q)
}
}
var activeProjects: [Project] {
projects.filter { $0.status == "active" || $0.status == "进行中" }
}
// MARK: - Fetch
private func fetchProjects() async {
guard let url = URL(string: memoryAPI) else { return }
do {
let (data, _) = try await URLSession.shared.data(from: url)
let decoded = try JSONDecoder().decode([Project].self, from: data)
projects = decoded
memoryOnline = true
} catch {
memoryOnline = false
}
}
private func fetchPortStats() async {
guard let url = URL(string: portStatsAPI) else { return }
do {
let (data, _) = try await URLSession.shared.data(from: url)
let decoded = try JSONDecoder().decode(PortStats.self, from: data)
portStats = decoded
portsOnline = true
} catch {
portsOnline = false
}
}
private func fetchASRStatus() async {
guard let url = URL(string: asrStatusAPI) else { return }
do {
let (data, _) = try await URLSession.shared.data(from: url)
let decoded = try JSONDecoder().decode(ASRStatus.self, from: data)
asrStatus = decoded
asrOnline = true
} catch {
asrOnline = false
}
}
}
// MARK: - Models
struct Project: Codable, Identifiable {
var id: String { slug }
let slug: String
let name: String
let path: String?
let status: String?
let category: String?
let what: String?
let updatedAt: String?
let eventCount: Int?
enum CodingKeys: String, CodingKey {
case slug, name, path, status, category, what
case updatedAt = "updated_at"
case eventCount = "event_count"
}
}
struct PortStats: Codable {
var total: Int = 0
var project: Int = 0
var adhoc: Int = 0
var legacy: Int = 0
var running: Int = 0
var nextBlock: Int?
enum CodingKeys: String, CodingKey {
case total, project, adhoc, legacy, running
case nextBlock = "next_block"
}
}
struct ASRStatus: Codable {
var total: Int = 0
var done: Int = 0
var failed: Int = 0
var chars: Int = 0
var running: Bool = false
}

View File

@@ -0,0 +1,154 @@
import SwiftUI
import SwiftUI
@MainActor
class AppState: ObservableObject {
// API endpoints
let memoryAPI = "http://localhost:3202/api/memories"
let portAPI = "http://localhost:3201/api/ports"
let portStatsAPI = "http://localhost:3201/api/ports/stats"
let asrStatusAPI = "http://localhost:4125/api/asr-status"
// Data
@Published var projects: [Project] = []
@Published var portStats: PortStats = PortStats()
@Published var asrStatus: ASRStatus = ASRStatus()
@Published var isLoading = false
@Published var lastRefresh: Date?
@Published var searchText = ""
// Services status
@Published var memoryOnline = false
@Published var portsOnline = false
@Published var asrOnline = false
private var refreshTask: Task<Void, Never>?
init() {
startAutoRefresh()
}
deinit {
refreshTask?.cancel()
}
private func startAutoRefresh() {
refreshTask = Task { @MainActor in
// Initial refresh
await refresh()
// Auto-refresh every 30 seconds
while !Task.isCancelled {
try? await Task.sleep(nanoseconds: 30_000_000_000)
if !Task.isCancelled {
await refresh()
}
}
}
}
func refresh() async {
isLoading = true
async let p: Void = fetchProjects()
async let s: Void = fetchPortStats()
async let a: Void = fetchASRStatus()
_ = await (p, s, a)
isLoading = false
lastRefresh = Date()
}
var filteredProjects: [Project] {
if searchText.isEmpty { return projects }
let q = searchText.lowercased()
return projects.filter {
$0.name.lowercased().contains(q) ||
$0.slug.lowercased().contains(q) ||
($0.what ?? "").lowercased().contains(q)
}
}
var activeProjects: [Project] {
projects.filter { $0.status == "active" || $0.status == "进行中" }
}
// MARK: - Fetch
private func fetchProjects() async {
guard let url = URL(string: memoryAPI) else { return }
do {
let (data, _) = try await URLSession.shared.data(from: url)
let decoded = try JSONDecoder().decode([Project].self, from: data)
projects = decoded
memoryOnline = true
} catch {
memoryOnline = false
}
}
private func fetchPortStats() async {
guard let url = URL(string: portStatsAPI) else { return }
do {
let (data, _) = try await URLSession.shared.data(from: url)
let decoded = try JSONDecoder().decode(PortStats.self, from: data)
portStats = decoded
portsOnline = true
} catch {
portsOnline = false
}
}
private func fetchASRStatus() async {
guard let url = URL(string: asrStatusAPI) else { return }
do {
let (data, _) = try await URLSession.shared.data(from: url)
let decoded = try JSONDecoder().decode(ASRStatus.self, from: data)
asrStatus = decoded
asrOnline = true
} catch {
asrOnline = false
}
}
}
// MARK: - Models
struct Project: Codable, Identifiable {
var id: String { slug }
let slug: String
let name: String
let path: String?
let status: String?
let category: String?
let what: String?
let updatedAt: String?
let eventCount: Int?
enum CodingKeys: String, CodingKey {
case slug, name, path, status, category, what
case updatedAt = "updated_at"
case eventCount = "event_count"
}
}
struct PortStats: Codable {
var total: Int = 0
var project: Int = 0
var adhoc: Int = 0
var legacy: Int = 0
var running: Int = 0
var nextBlock: Int?
enum CodingKeys: String, CodingKey {
case total, project, adhoc, legacy, running
case nextBlock = "next_block"
}
}
struct ASRStatus: Codable {
var total: Int = 0
var done: Int = 0
var failed: Int = 0
var chars: Int = 0
var running: Bool = false
}

View File

@@ -0,0 +1,304 @@
import SwiftUI
import AppKit
struct MenuBarView: View {
@EnvironmentObject var state: AppState
@State private var rotationAngle: Double = 0
var body: some View {
VStack(spacing: 0) {
// Header
HStack {
Text("项目管家")
.font(.system(size: 14, weight: .bold))
Spacer()
ServiceDots(
memory: state.memoryOnline,
ports: state.portsOnline,
asr: state.asrOnline
)
Button {
Task {
await state.refresh()
}
} label: {
Image(systemName: "arrow.clockwise")
.font(.system(size: 11))
.rotationEffect(.degrees(rotationAngle))
}
.buttonStyle(.plain)
.foregroundStyle(.secondary)
.onChange(of: state.isLoading) { _, isLoading in
if isLoading {
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
rotationAngle = 360
}
} else {
withAnimation {
rotationAngle = 0
}
}
}
}
.padding(.horizontal, 16)
.padding(.vertical, 10)
Divider()
// Stats row
HStack(spacing: 12) {
StatBadge(
label: "项目",
value: "\(state.projects.count)",
color: .blue
)
StatBadge(
label: "端口",
value: "\(state.portStats.running)/\(state.portStats.total)",
color: .green
)
if state.asrOnline {
StatBadge(
label: "字幕",
value: state.asrStatus.running ? "运行中" : "\(state.asrStatus.done)/\(state.asrStatus.total)",
color: state.asrStatus.running ? .orange : .purple
)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
Divider()
// ASR Progress (if running)
if state.asrOnline && state.asrStatus.total > 0 {
ASRProgressBar(status: state.asrStatus)
.padding(.horizontal, 16)
.padding(.vertical, 6)
Divider()
}
// Search
HStack {
Image(systemName: "magnifyingglass")
.foregroundStyle(.tertiary)
.font(.system(size: 11))
TextField("搜索项目...", text: $state.searchText)
.textFieldStyle(.plain)
.font(.system(size: 12))
}
.padding(.horizontal, 16)
.padding(.vertical, 6)
Divider()
// Project list
ScrollView {
LazyVStack(spacing: 2) {
ForEach(state.filteredProjects) { project in
ProjectRow(project: project)
}
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
}
.frame(maxHeight: 320)
Divider()
// Footer
HStack {
if let last = state.lastRefresh {
Text("更新: \(last, formatter: timeFormatter)")
.font(.system(size: 10))
.foregroundStyle(.tertiary)
}
Spacer()
Button("退出") {
NSApplication.shared.terminate(nil)
}
.font(.system(size: 11))
.buttonStyle(.plain)
.foregroundStyle(.secondary)
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
}
.frame(width: 360)
}
private var timeFormatter: DateFormatter {
let f = DateFormatter()
f.dateFormat = "HH:mm:ss"
return f
}
}
// MARK: - Components
struct ServiceDots: View {
let memory: Bool
let ports: Bool
let asr: Bool
var body: some View {
HStack(spacing: 4) {
Circle()
.fill(memory ? .green : .red.opacity(0.5))
.frame(width: 6, height: 6)
.help(memory ? "记忆中心: 在线" : "记忆中心: 离线")
Circle()
.fill(ports ? .green : .red.opacity(0.5))
.frame(width: 6, height: 6)
.help(ports ? "端口注册: 在线" : "端口注册: 离线")
Circle()
.fill(asr ? .green : .gray.opacity(0.3))
.frame(width: 6, height: 6)
.help(asr ? "ASR: 在线" : "ASR: 离线")
}
}
}
struct StatBadge: View {
let label: String
let value: String
let color: Color
var body: some View {
VStack(spacing: 2) {
Text(value)
.font(.system(size: 16, weight: .bold, design: .rounded))
.foregroundStyle(color)
Text(label)
.font(.system(size: 9, weight: .medium))
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 4)
.background(color.opacity(0.08), in: RoundedRectangle(cornerRadius: 8))
}
}
struct ASRProgressBar: View {
let status: ASRStatus
var progress: Double {
guard status.total > 0 else { return 0 }
return Double(status.done) / Double(status.total)
}
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
Image(systemName: status.running ? "waveform" : "waveform.slash")
.font(.system(size: 10))
.foregroundStyle(status.running ? .orange : .secondary)
Text("字幕提取")
.font(.system(size: 11, weight: .medium))
Spacer()
Text("\(Int(progress * 100))%")
.font(.system(size: 11, weight: .bold, design: .monospaced))
.foregroundStyle(status.running ? .orange : .secondary)
}
ProgressView(value: progress)
.tint(status.running ? .orange : .blue)
HStack {
Text("\(status.done)/\(status.total) 视频")
Spacer()
if status.chars > 0 {
Text("\(status.chars / 10000)万字")
}
}
.font(.system(size: 9))
.foregroundStyle(.tertiary)
}
}
}
struct ProjectRow: View {
let project: Project
var statusIcon: String {
switch project.status {
case "active", "进行中":
return "circle.fill"
case "done", "完成", "completed":
return "checkmark.circle.fill"
case "paused", "暂停":
return "pause.circle.fill"
default:
return "circle"
}
}
var statusColor: Color {
switch project.status {
case "active", "进行中":
return .green
case "done", "完成", "completed":
return .blue
case "paused", "暂停":
return .orange
default:
return .gray
}
}
var categoryBadge: String {
switch project.category {
case "business":
return "B"
case "research":
return "R"
case "code":
return "C"
default:
return "?"
}
}
var body: some View {
HStack(spacing: 8) {
// Category badge
Text(categoryBadge)
.font(.system(size: 9, weight: .bold, design: .monospaced))
.foregroundStyle(.white)
.frame(width: 18, height: 18)
.background(
project.category == "business" ? Color.purple :
project.category == "research" ? Color.teal : Color.blue,
in: RoundedRectangle(cornerRadius: 4)
)
// Info
VStack(alignment: .leading, spacing: 1) {
Text(project.name)
.font(.system(size: 12, weight: .medium))
.lineLimit(1)
if let what = project.what, !what.isEmpty {
Text(what)
.font(.system(size: 10))
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
Spacer()
// Status
Image(systemName: statusIcon)
.font(.system(size: 9))
.foregroundStyle(statusColor)
}
.padding(.horizontal, 8)
.padding(.vertical, 5)
.background(Color.primary.opacity(0.03), in: RoundedRectangle(cornerRadius: 6))
.contentShape(Rectangle())
.onTapGesture {
if let path = project.path {
let expanded = (path as NSString).expandingTildeInPath
NSWorkspace.shared.open(URL(fileURLWithPath: expanded))
}
}
}
}

View File

@@ -0,0 +1,269 @@
import SwiftUI
import SwiftUI
import AppKit
struct MenuBarView: View {
@EnvironmentObject var state: AppState
@State private var isRotating = false
var body: some View {
VStack(spacing: 0) {
// Header
HStack {
Text("项目管家")
.font(.system(size: 14, weight: .bold))
Spacer()
ServiceDots(memory: state.memoryOnline, ports: state.portsOnline, asr: state.asrOnline)
Button(action: {
Task {
await state.refresh()
}
}) {
Image(systemName: state.isLoading ? "arrow.triangle.2.circlepath" : "arrow.clockwise")
.font(.system(size: 11))
.rotationEffect(.degrees(isRotating ? 360 : 0))
}
.onChange(of: state.isLoading) { _, newValue in
withAnimation(newValue ? .linear(duration: 1).repeatForever(autoreverses: false) : .default) {
isRotating = newValue
}
}
.buttonStyle(.plain)
.foregroundStyle(.secondary)
}
.padding(.horizontal, 16)
.padding(.vertical, 10)
Divider()
// Stats row
HStack(spacing: 12) {
StatBadge(label: "项目", value: "\(state.projects.count)", color: .blue)
StatBadge(label: "端口", value: "\(state.portStats.running)/\(state.portStats.total)", color: .green)
if state.asrOnline {
StatBadge(
label: "字幕",
value: state.asrStatus.running ? "运行中" : "\(state.asrStatus.done)/\(state.asrStatus.total)",
color: state.asrStatus.running ? .orange : .purple
)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
Divider()
// ASR Progress (if running)
if state.asrOnline && state.asrStatus.total > 0 {
ASRProgressBar(status: state.asrStatus)
.padding(.horizontal, 16)
.padding(.vertical, 6)
Divider()
}
// Search
HStack {
Image(systemName: "magnifyingglass")
.foregroundStyle(.tertiary)
.font(.system(size: 11))
TextField("搜索项目...", text: $state.searchText)
.textFieldStyle(.plain)
.font(.system(size: 12))
}
.padding(.horizontal, 16)
.padding(.vertical, 6)
Divider()
// Project list
ScrollView {
LazyVStack(spacing: 2) {
ForEach(state.filteredProjects) { project in
ProjectRow(project: project)
}
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
}
.frame(maxHeight: 320)
Divider()
// Footer
HStack {
if let last = state.lastRefresh {
Text("更新: \(last, formatter: timeFormatter)")
.font(.system(size: 10))
.foregroundStyle(.tertiary)
}
Spacer()
Button("退出") {
NSApplication.shared.terminate(nil)
}
.font(.system(size: 11))
.buttonStyle(.plain)
.foregroundStyle(.secondary)
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
}
.frame(width: 360)
}
private var timeFormatter: DateFormatter {
let f = DateFormatter()
f.dateFormat = "HH:mm:ss"
return f
}
}
// MARK: - Components
struct ServiceDots: View {
let memory: Bool
let ports: Bool
let asr: Bool
var body: some View {
HStack(spacing: 4) {
Circle().fill(memory ? .green : .red.opacity(0.5)).frame(width: 6, height: 6)
.help(memory ? "记忆中心: 在线" : "记忆中心: 离线")
Circle().fill(ports ? .green : .red.opacity(0.5)).frame(width: 6, height: 6)
.help(ports ? "端口注册: 在线" : "端口注册: 离线")
Circle().fill(asr ? .green : .gray.opacity(0.3)).frame(width: 6, height: 6)
.help(asr ? "ASR: 在线" : "ASR: 离线")
}
}
}
struct StatBadge: View {
let label: String
let value: String
let color: Color
var body: some View {
VStack(spacing: 2) {
Text(value)
.font(.system(size: 16, weight: .bold, design: .rounded))
.foregroundStyle(color)
Text(label)
.font(.system(size: 9, weight: .medium))
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 4)
.background(color.opacity(0.08), in: RoundedRectangle(cornerRadius: 8))
}
}
struct ASRProgressBar: View {
let status: ASRStatus
var progress: Double {
guard status.total > 0 else { return 0 }
return Double(status.done) / Double(status.total)
}
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
Image(systemName: status.running ? "waveform" : "waveform.slash")
.font(.system(size: 10))
.foregroundStyle(status.running ? .orange : .secondary)
Text("字幕提取")
.font(.system(size: 11, weight: .medium))
Spacer()
Text("\(Int(progress * 100))%")
.font(.system(size: 11, weight: .bold, design: .monospaced))
.foregroundStyle(status.running ? .orange : .secondary)
}
ProgressView(value: progress)
.tint(status.running ? .orange : .blue)
HStack {
Text("\(status.done)/\(status.total) 视频")
Spacer()
if status.chars > 0 {
Text("\(status.chars / 10000)万字")
}
}
.font(.system(size: 9))
.foregroundStyle(.tertiary)
}
}
}
struct ProjectRow: View {
let project: Project
var statusIcon: String {
switch project.status {
case "active", "进行中": return "circle.fill"
case "done", "完成", "completed": return "checkmark.circle.fill"
case "paused", "暂停": return "pause.circle.fill"
default: return "circle"
}
}
var statusColor: Color {
switch project.status {
case "active", "进行中": return .green
case "done", "完成", "completed": return .blue
case "paused", "暂停": return .orange
default: return .gray
}
}
var categoryBadge: String {
switch project.category {
case "business": return "B"
case "research": return "R"
case "code": return "C"
default: return "?"
}
}
var body: some View {
HStack(spacing: 8) {
// Category badge
Text(categoryBadge)
.font(.system(size: 9, weight: .bold, design: .monospaced))
.foregroundStyle(.white)
.frame(width: 18, height: 18)
.background(
project.category == "business" ? Color.purple :
project.category == "research" ? Color.teal : Color.blue,
in: RoundedRectangle(cornerRadius: 4)
)
// Info
VStack(alignment: .leading, spacing: 1) {
Text(project.name)
.font(.system(size: 12, weight: .medium))
.lineLimit(1)
if let what = project.what, !what.isEmpty {
Text(what)
.font(.system(size: 10))
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
Spacer()
// Status
Image(systemName: statusIcon)
.font(.system(size: 9))
.foregroundStyle(statusColor)
}
.padding(.horizontal, 8)
.padding(.vertical, 5)
.background(Color.primary.opacity(0.03), in: RoundedRectangle(cornerRadius: 6))
.contentShape(Rectangle())
.onTapGesture {
if let path = project.path {
let expanded = (path as NSString).expandingTildeInPath
NSWorkspace.shared.open(URL(fileURLWithPath: expanded))
}
}
}
}

View File

@@ -0,0 +1,19 @@
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "ProjectButler",
platforms: [.macOS(.v13)],
products: [
.executable(
name: "ProjectButler",
targets: ["ProjectButler"]
)
],
targets: [
.executableTarget(
name: "ProjectButler",
path: "Sources"
)
]
)

View File

@@ -0,0 +1,19 @@
import SwiftUI
@main
struct ProjectButlerApp: App {
@StateObject private var appState = AppState()
var body: some Scene {
MenuBarExtra("项目管家", systemImage: "tray.full.fill") {
MenuBarView()
.environmentObject(appState)
}
.menuBarExtraStyle(.window)
Settings {
SettingsView()
.environmentObject(appState)
}
}
}

View File

@@ -0,0 +1,19 @@
import SwiftUI
@main
struct ProjectButlerApp: App {
@StateObject private var appState = AppState()
var body: some Scene {
MenuBarExtra("项目管家", systemImage: "tray.full.fill") {
MenuBarView()
.environmentObject(appState)
}
.menuBarExtraStyle(.window)
Settings {
SettingsView()
.environmentObject(appState)
}
}
}

View File

@@ -0,0 +1,63 @@
import SwiftUI
struct SettingsView: View {
@EnvironmentObject var state: AppState
var body: some View {
TabView {
GeneralSettings()
.tabItem {
Label("通用", systemImage: "gear")
}
AboutView()
.tabItem {
Label("关于", systemImage: "info.circle")
}
}
.frame(width: 400, height: 200)
}
}
struct GeneralSettings: View {
var body: some View {
Form {
Section("API 地址") {
LabeledContent("记忆中心") {
Text("localhost:3202").foregroundStyle(.secondary)
}
LabeledContent("端口注册") {
Text("localhost:3201").foregroundStyle(.secondary)
}
LabeledContent("ASR 状态") {
Text("localhost:4125").foregroundStyle(.secondary)
}
}
Section("刷新") {
LabeledContent("自动刷新") {
Text("每 30 秒").foregroundStyle(.secondary)
}
}
}
.padding()
}
}
struct AboutView: View {
var body: some View {
VStack(spacing: 12) {
Image(systemName: "tray.full.fill")
.font(.system(size: 48))
.foregroundStyle(.blue)
Text("项目管家")
.font(.title2.bold())
Text("macOS 菜单栏应用\n统管项目记忆 / 端口 / ASR 进度")
.font(.caption)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
Text("v1.0 · 2026")
.font(.caption2)
.foregroundStyle(.tertiary)
}
.padding()
}
}

View File

@@ -0,0 +1,51 @@
import SwiftUI
struct SettingsView: View {
@EnvironmentObject var state: AppState
var body: some View {
TabView {
GeneralSettings()
.tabItem { Label("通用", systemImage: "gear") }
AboutView()
.tabItem { Label("关于", systemImage: "info.circle") }
}
.frame(width: 400, height: 200)
}
}
struct GeneralSettings: View {
var body: some View {
Form {
Section("API 地址") {
LabeledContent("记忆中心") { Text("localhost:3202").foregroundStyle(.secondary) }
LabeledContent("端口注册") { Text("localhost:3201").foregroundStyle(.secondary) }
LabeledContent("ASR 状态") { Text("localhost:4125").foregroundStyle(.secondary) }
}
Section("刷新") {
LabeledContent("自动刷新") { Text("每 30 秒").foregroundStyle(.secondary) }
}
}
.padding()
}
}
struct AboutView: View {
var body: some View {
VStack(spacing: 12) {
Image(systemName: "tray.full.fill")
.font(.system(size: 48))
.foregroundStyle(.blue)
Text("项目管家")
.font(.title2.bold())
Text("macOS 菜单栏应用\n统管项目记忆 / 端口 / ASR 进度")
.font(.caption)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
Text("v1.0 · 2026")
.font(.caption2)
.foregroundStyle(.tertiary)
}
.padding()
}
}