import SwiftUI struct NotificationsView: View { @StateObject private var model: NotificationsViewModel init(model: NotificationsViewModel? = nil) { _model = StateObject(wrappedValue: model ?? DefaultContainer.makeNotificationsModel()) } var body: some View { ZStack { AITheme.canvas.ignoresSafeArea() ScrollView { LazyVStack(spacing: 12) { if let actionMessage = model.actionMessage { AICard { Text(actionMessage) .font(.subheadline) .foregroundStyle(AITheme.success) } } if let errorMessage = model.errorMessage { AICard { Text(errorMessage) .font(.subheadline) .foregroundStyle(AITheme.warning) } } if let overview = model.overview { stats(overview) devices(overview) channels(overview) queue(overview) logs(overview) } else if model.isLoading { AICard { ProgressView("正在加载通知中心...") .frame(maxWidth: .infinity) .padding(.vertical, 22) } } else { empty } } .padding(16) } } .navigationTitle("通知中心") .toolbar { ToolbarItemGroup(placement: .primaryAction) { Button { Task { await model.retryAll() } } label: { Label("立即投递", systemImage: "paperplane") } .buttonStyle(AIButtonStyle(kind: .primary, size: .regular)) .disabled(model.isLoading) } } .refreshable { await model.refresh() } .task { await model.refresh() } } private var empty: some View { AICard { VStack(spacing: 10) { Image(systemName: "bell") .font(.system(size: 28)) .foregroundStyle(AITheme.muted) Text("暂无通知数据") .font(.headline) Text("下拉刷新后从已配置实例同步。") .font(.subheadline) .foregroundStyle(AITheme.muted) } .frame(maxWidth: .infinity) .padding(.vertical, 18) } } private func stats(_ overview: NotificationOverview) -> some View { LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) { statCard("通道", "\(overview.config.channelEnabled)/\(overview.config.channelCount)", "已启用/总数", AITheme.primary) statCard("规则", "\(overview.config.ruleEnabled)/\(overview.config.ruleCount)", "已启用/总数", AITheme.primary) statCard("队列", "\(overview.queue.pending + overview.queue.retrying)", "等待与重试", AITheme.warning) statCard("失败", "\(overview.queue.failed + overview.logs.failed)", "队列与投递", AITheme.danger) } } private func statCard(_ title: String, _ value: String, _ detail: String, _ tint: Color) -> some View { AICard { VStack(alignment: .leading, spacing: 6) { Text(title) .font(.caption) .foregroundStyle(AITheme.muted) Text(value) .font(.title2.weight(.bold)) .foregroundStyle(tint) Text(detail) .font(.caption) .foregroundStyle(AITheme.muted) } .frame(maxWidth: .infinity, alignment: .leading) } } private func devices(_ overview: NotificationOverview) -> some View { AICard { VStack(alignment: .leading, spacing: 10) { title("实例状态") if overview.devices.isEmpty { Text("尚未添加实例。") .font(.subheadline) .foregroundStyle(AITheme.muted) } else { ForEach(overview.devices) { device in HStack(spacing: 8) { Text(device.name) .font(.subheadline) Spacer() AITag(deviceState(device.state), tint: deviceTint(device.state)) } } } } } } private func channels(_ overview: NotificationOverview) -> some View { AICard { VStack(alignment: .leading, spacing: 10) { title("通道类型") if overview.config.channelTypes.isEmpty { Text("实例未配置通知通道。") .font(.subheadline) .foregroundStyle(AITheme.muted) } else { ForEach(overview.config.channelTypes) { channel in HStack(spacing: 8) { Text(channelTypeLabel(channel.type)) .font(.subheadline) Spacer() AITag("\(channel.enabled)/\(channel.total)", tint: channel.enabled > 0 ? AITheme.success : AITheme.muted) } } } } } } private func queue(_ overview: NotificationOverview) -> some View { AICard { VStack(alignment: .leading, spacing: 10) { HStack { title("最近队列") Spacer() Text("\(overview.queue.total) 项") .font(.caption) .foregroundStyle(AITheme.muted) } if overview.queue.items.isEmpty { Text("队列为空。") .font(.subheadline) .foregroundStyle(AITheme.muted) } else { ForEach(overview.queue.items) { item in VStack(alignment: .leading, spacing: 8) { HStack(spacing: 8) { Text(eventTypeLabel(item.eventType)) .font(.subheadline.weight(.semibold)) Spacer() AITag(statusLabel(item.status), tint: statusTint(item.status)) } Text("\(item.instanceName) · \(item.createdAt)") .font(.caption) .foregroundStyle(AITheme.muted) HStack(spacing: 8) { Button { Task { await model.retryQueue(item) } } label: { Label("重试", systemImage: "arrow.clockwise") } .buttonStyle(AIButtonStyle(kind: .secondary, size: .compact)) .disabled(model.busyQueueID == item.id) Button { Task { await model.deleteQueue(item) } } label: { Label("删除", systemImage: "trash") } .buttonStyle(AIButtonStyle(kind: .danger, size: .compact)) } } .padding(.bottom, 4) } } } } } private func logs(_ overview: NotificationOverview) -> some View { AICard { VStack(alignment: .leading, spacing: 10) { HStack { title("投递日志") Spacer() Text("\(overview.logs.total) 条") .font(.caption) .foregroundStyle(AITheme.muted) } if overview.logs.entries.isEmpty { Text("暂无投递日志。") .font(.subheadline) .foregroundStyle(AITheme.muted) } else { ForEach(overview.logs.entries) { entry in VStack(alignment: .leading, spacing: 6) { HStack(spacing: 8) { Text(eventTypeLabel(entry.eventType)) .font(.subheadline.weight(.semibold)) Spacer() AITag(statusLabel(entry.status), tint: statusTint(entry.status)) } Text("\(entry.channelName ?? "未指定通道") · \(entry.createdAt)") .font(.caption) .foregroundStyle(AITheme.muted) } .padding(.bottom, 4) } } } } } private func title(_ text: String) -> some View { Text(text) .font(.headline) .foregroundStyle(AITheme.ink) } private func deviceState(_ state: NotificationDeviceState) -> String { switch state { case .ready: return "在线" case .unauthorized: return "需登录" case .unavailable: return "离线" case .failed: return "异常" } } private func deviceTint(_ state: NotificationDeviceState) -> Color { switch state { case .ready: return AITheme.success case .unauthorized: return AITheme.warning case .unavailable: return AITheme.muted case .failed: return AITheme.danger } } private func channelTypeLabel(_ type: String) -> String { switch type { case "webhook": return "Webhook" case "bark": return "Bark" case "pushplus": return "PushPlus" case "wecom_app": return "企业微信应用" case "wecom_robot": return "企业微信机器人" case "dingtalk_robot": return "钉钉机器人" case "dingtalk_app": return "钉钉应用" case "feishu_robot": return "飞书机器人" case "telegram": return "Telegram" case "email": return "邮件" case "serverchan": return "Server 酱" default: return type } } private func eventTypeLabel(_ value: String) -> String { switch value { case "sms": return "短信" case "ddns": return "DDNS" case "version": return "版本" case "system": return "系统" case "device": return "设备" case "automation": return "自动化" default: return value } } private func statusLabel(_ value: String) -> String { switch value { case "success", "succeeded": return "成功" case "failed": return "失败" case "pending": return "等待" case "sending": return "发送中" case "retrying": return "重试中" case "cancelled": return "已取消" case "unmatched": return "未匹配" case "no_available_channel": return "无可用通道" case "quiet_hours": return "免打扰" default: return value } } private func statusTint(_ value: String) -> Color { switch value { case "success", "succeeded": return AITheme.success case "pending", "sending", "retrying": return AITheme.warning case "failed": return AITheme.danger default: return AITheme.muted } } }