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)) } } } }