feat(ios): refine native interface and app icon

Unify bottom actions, selection controls, navigation structure, spacing, and theme colors across iOS pages. Replace the app icon with a higher-contrast native-style asset and include its source SVG.
This commit is contained in:
chick
2026-09-12 14:09:02 +08:00
parent 31b2f33d15
commit 0f2866a853
16 changed files with 1293 additions and 1035 deletions
@@ -15,9 +15,7 @@ struct NotificationsView: View {
}
var body: some View {
ZStack {
AITheme.canvas.ignoresSafeArea()
ScrollView {
ScrollView {
LazyVStack(spacing: 12) {
if let actionMessage = model.actionMessage {
AIStatusBanner(
@@ -44,18 +42,29 @@ struct NotificationsView: View {
}
}
.padding(16)
}
.padding(.bottom, 8)
}
.navigationTitle("通知中心")
.aiBottomActionBar {
Button {
Task { await model.retryAll() }
} label: {
Label("立即投递", systemImage: "paperplane")
}
.buttonStyle(AIButtonStyle(kind: .primary, size: .regular, fillsWidth: true))
.disabled(model.isLoading)
Button {
Task { await model.retryAll() }
} label: {
HStack(spacing: 8) {
if model.isRetryingAll {
ProgressView()
.controlSize(.small)
}
Text(
model.isRetryingAll
? "投递中"
: (model.overview?.queue.total == 0 ? "暂无待投递" : "立即投递")
)
}
}
.buttonStyle(AIButtonStyle(kind: .primary, size: .regular, fillsWidth: true))
.disabled(model.isLoading || model.isRetryingAll || model.overview?.queue.total == 0)
}
.background(AITheme.canvas.ignoresSafeArea())
.navigationTitle("通知中心")
.confirmationDialog(
"删除这条通知队列记录?操作不可恢复。",
isPresented: Binding(
@@ -176,13 +185,12 @@ struct NotificationsView: View {
}
} else {
ForEach(overview.queue.items) { item in
AICard {
AIListCard(
title: eventTypeLabel(item.eventType),
subtitle: "\(item.instanceName) · \(timestampText(item.createdAt))",
status: statusLabel(item.status),
statusTint: statusTint(item.status)
) {
AIListCard(
title: eventTypeLabel(item.eventType),
subtitle: "\(item.instanceName) · \(timestampText(item.createdAt))",
status: statusLabel(item.status),
statusTint: statusTint(item.status)
) {
HStack(spacing: 8) {
Button {
Task { await model.retryQueue(item) }
@@ -190,27 +198,17 @@ struct NotificationsView: View {
Label("重试", systemImage: "arrow.clockwise")
}
.buttonStyle(
AIButtonStyle(
kind: .secondary,
size: .compact,
fixedWidth: AITheme.listActionButtonWidth
)
AIButtonStyle(kind: .secondary, size: .compact, fillsWidth: true)
)
.disabled(model.busyQueueID == item.id)
.disabled(model.busyQueueID != nil)
Button {
pendingDeleteQueueID = item.id
} label: {
Label("删除", systemImage: "trash")
}
.buttonStyle(
AIButtonStyle(
kind: .danger,
size: .compact,
fixedWidth: AITheme.listActionButtonWidth
)
)
}
.buttonStyle(AIButtonStyle(kind: .danger, size: .compact, fillsWidth: true))
.disabled(model.busyQueueID != nil)
}
}
}
@@ -2,79 +2,83 @@ import Foundation
@MainActor
final class NotificationsViewModel: ObservableObject {
@Published private(set) var overview: NotificationOverview?
@Published private(set) var instances: [Instance] = []
@Published private(set) var isLoading = false
@Published var errorMessage: String?
@Published private(set) var actionMessage: String?
@Published private(set) var actionSucceeded = true
@Published private(set) var busyQueueID: String?
@Published private(set) var overview: NotificationOverview?
@Published private(set) var instances: [Instance] = []
@Published private(set) var isLoading = false
@Published var errorMessage: String?
@Published private(set) var actionMessage: String?
@Published private(set) var actionSucceeded = true
@Published private(set) var busyQueueID: String?
@Published private(set) var isRetryingAll = false
private let repository: any InstanceRepository
private let service: NotificationService
private let repository: any InstanceRepository
private let service: NotificationService
init(repository: any InstanceRepository, service: NotificationService) {
self.repository = repository
self.service = service
init(repository: any InstanceRepository, service: NotificationService) {
self.repository = repository
self.service = service
}
func refresh() async {
isLoading = true
errorMessage = nil
do {
let loaded = try await repository.list(InstanceQuery())
instances = loaded
overview = try await service.overview(loaded)
} catch {
errorMessage = "无法加载通知中心。"
}
isLoading = false
}
func refresh() async {
isLoading = true
errorMessage = nil
do {
let loaded = try await repository.list(InstanceQuery())
instances = loaded
overview = try await service.overview(loaded)
} catch {
errorMessage = "无法加载通知中心。"
}
isLoading = false
func retryAll() async {
guard !isRetryingAll && !isLoading else { return }
isRetryingAll = true
let result = await service.retryAll(instances)
actionSucceeded = result.succeeded > 0
if result.succeeded > 0 {
actionMessage = "已提交全部队列重试,成功 \(result.succeeded) 个实例。"
} else if result.skipped > 0 {
actionMessage = "没有可重试的队列接口。"
} else {
actionMessage = "队列重试失败。"
}
await refresh()
isRetryingAll = false
}
func retryAll() async {
let result = await service.retryAll(instances)
actionSucceeded = result.succeeded > 0
if result.succeeded > 0 {
actionMessage = "已提交全部队列重试,成功 \(result.succeeded) 个实例。"
} else if result.skipped > 0 {
actionMessage = "没有可重试的队列接口。"
} else {
actionMessage = "队列重试失败。"
}
await refresh()
func retryQueue(_ item: NotificationQueueEntry) async {
await queueAction(item) { instance, id in
try await self.service.retryQueue(instance, id: id)
}
}
func retryQueue(_ item: NotificationQueueEntry) async {
await queueAction(item) { instance, id in
try await self.service.retryQueue(instance, id: id)
}
func deleteQueue(_ item: NotificationQueueEntry) async {
await queueAction(item) { instance, id in
try await self.service.deleteQueueItem(instance, id: id)
}
await refresh()
}
func deleteQueue(_ item: NotificationQueueEntry) async {
await queueAction(item) { instance, id in
try await self.service.deleteQueueItem(instance, id: id)
}
await refresh()
private func queueAction(
_ item: NotificationQueueEntry,
action: (Instance, String) async throws -> Void
) async {
guard let instance = instances.first(where: { $0.id == item.instanceID }) else {
errorMessage = "找不到队列所属实例。"
return
}
private func queueAction(
_ item: NotificationQueueEntry,
action: (Instance, String) async throws -> Void
) async {
guard let instance = instances.first(where: { $0.id == item.instanceID }) else {
errorMessage = "找不到队列所属实例。"
return
}
busyQueueID = item.id
do {
try await action(instance, item.id)
actionMessage = "队列操作已完成。"
actionSucceeded = true
errorMessage = nil
} catch {
errorMessage = "队列操作失败。"
actionSucceeded = false
}
busyQueueID = nil
busyQueueID = item.id
do {
try await action(instance, item.id)
actionMessage = "队列操作已完成。"
actionSucceeded = true
errorMessage = nil
} catch {
errorMessage = "队列操作失败。"
actionSucceeded = false
}
busyQueueID = nil
}
}