release(ios): 发布 0.2.1 并重构图标与界面细节
- 应用图标改用与 Web favicon 同源的扁平标记,金色 SIM 卡体加三格递增信号,29pt 下仍可读 - 标签统一为胶囊形状,提高深色主题下的描边权重,避免标签在暗色下消失 - 消息卡片与详情分区卡片收紧行距,单屏信息密度明显提升 - 详情头部指标精简为 4 行,数据模块改为单条读数 - 批量选择勾选框移出名称区域,工具栏控件统一为等高 34pt - 分组编辑器改为点选即生效,移除说明文字与常驻禁用按钮
This commit is contained in:
@@ -18,17 +18,38 @@ struct LogsView: View {
|
||||
self.autoRefreshOnAppear = resolvedModel.autoRefreshOnAppear
|
||||
}
|
||||
|
||||
private var isUnavailable: Bool {
|
||||
model.snapshot == nil && model.errorMessage != nil && !model.isLoading
|
||||
}
|
||||
|
||||
/// 空列表配一排筛选器只是噪音,有内容才给筛选入口。
|
||||
private var hasRowsForTab: Bool {
|
||||
switch tab {
|
||||
case .runtime: return model.filteredRuntime.isEmpty == false
|
||||
case .connections: return model.filteredConnections.isEmpty == false
|
||||
case .diagnostics: return model.filteredDiagnostics.isEmpty == false
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 12) {
|
||||
if let errorMessage = model.errorMessage {
|
||||
AIStatusBanner(
|
||||
errorMessage, tint: AITheme.danger, systemImage: "exclamationmark.triangle")
|
||||
}
|
||||
if model.snapshot == nil && model.isLoading {
|
||||
if isUnavailable {
|
||||
AICard {
|
||||
AIEmptyState(
|
||||
systemImage: "arrow.triangle.2.circlepath",
|
||||
title: "未能读取日志",
|
||||
message: model.errorMessage ?? "所有节点都没有返回日志数据。",
|
||||
actionTitle: "重试"
|
||||
) {
|
||||
Task { await model.refresh() }
|
||||
}
|
||||
}
|
||||
} else if model.snapshot == nil && model.isLoading {
|
||||
AILoadingCard("正在同步日志数据")
|
||||
} else {
|
||||
stats
|
||||
|
||||
AISegmentedControl(
|
||||
"日志视图",
|
||||
selection: $tab,
|
||||
@@ -39,7 +60,14 @@ struct LogsView: View {
|
||||
]
|
||||
)
|
||||
|
||||
filterBar
|
||||
if hasRowsForTab {
|
||||
filterBar
|
||||
}
|
||||
|
||||
if let errorMessage = model.errorMessage {
|
||||
AIStatusBanner(
|
||||
errorMessage, tint: AITheme.warning, systemImage: "exclamationmark.triangle")
|
||||
}
|
||||
|
||||
switch tab {
|
||||
case .runtime:
|
||||
@@ -58,11 +86,7 @@ struct LogsView: View {
|
||||
.navigationTitle("日志中心")
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .primaryAction) {
|
||||
AIToolbarButton(
|
||||
"刷新",
|
||||
systemImage: "arrow.clockwise",
|
||||
kind: .primary
|
||||
) {
|
||||
AIIconButton("刷新", systemImage: "arrow.clockwise") {
|
||||
Task { await model.refresh() }
|
||||
}
|
||||
.disabled(model.isLoading)
|
||||
@@ -90,67 +114,74 @@ struct LogsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// 三块全 0 的读数和下方的空态说的是同一件事,没有记录时整段不出现。
|
||||
private var stats: some View {
|
||||
LazyVGrid(
|
||||
columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 12
|
||||
) {
|
||||
AIStatCard(title: "日志", value: "\(model.snapshot?.runtime.count ?? 0)", tint: AITheme.primary)
|
||||
AIStatCard(title: "在线", value: "\(onlineCount)", tint: AITheme.success)
|
||||
AIStatCard(title: "离线", value: "\(model.offlineCount)", tint: AITheme.danger)
|
||||
let runtime = model.snapshot?.runtime.count ?? 0
|
||||
let connections = model.snapshot?.connections.count ?? 0
|
||||
let diagnostics = model.snapshot?.diagnostics.count ?? 0
|
||||
|
||||
return Group {
|
||||
if runtime + connections + diagnostics > 0 {
|
||||
LazyVGrid(
|
||||
columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 12
|
||||
) {
|
||||
AIStatCard(
|
||||
title: "运行日志", value: "\(runtime)", systemImage: "list.bullet", tint: AITheme.primary)
|
||||
AIStatCard(
|
||||
title: "连接探测", value: "\(connections)", systemImage: "dot.radiowaves.left.and.right",
|
||||
tint: model.offlineCount == 0 ? AITheme.success : AITheme.warning)
|
||||
AIStatCard(
|
||||
title: "设备诊断", value: "\(diagnostics)", systemImage: "iphone.gen3", tint: AITheme.ink)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var filterBar: some View {
|
||||
AICard {
|
||||
VStack(spacing: 10) {
|
||||
AISearchField("搜索日志、错误码或实例", text: $model.searchText)
|
||||
VStack(spacing: 8) {
|
||||
AISearchField("搜索日志、错误码或实例", text: $model.searchText, minHeight: AITheme.compactControlHeight)
|
||||
|
||||
Picker("实例", selection: $model.selectedInstanceID) {
|
||||
Text("全部实例").tag(Optional<UUID>.none)
|
||||
ForEach(model.instances) { instance in
|
||||
Text(instance.name).tag(Optional(instance.id))
|
||||
Picker("实例", selection: $model.selectedInstanceID) {
|
||||
Text("全部实例").tag(Optional<UUID>.none)
|
||||
ForEach(model.instances) { instance in
|
||||
Text(instance.name).tag(Optional(instance.id))
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.aiSelectSurface(compact: true)
|
||||
|
||||
if tab == .runtime {
|
||||
HStack(spacing: 8) {
|
||||
Picker("来源", selection: $model.selectedSource) {
|
||||
Text("全部来源").tag(Optional<RuntimeLogSource>.none)
|
||||
ForEach(RuntimeLogSource.allCases, id: \.self) { source in
|
||||
Text(source.label).tag(Optional(source))
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.aiSelectSurface(compact: true)
|
||||
|
||||
Picker("级别", selection: $model.selectedLevel) {
|
||||
Text("全部级别").tag(Optional<RuntimeLogLevel>.none)
|
||||
ForEach(RuntimeLogLevel.allCases, id: \.self) { level in
|
||||
Text(level.label).tag(Optional(level))
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.aiSelectSurface(compact: true)
|
||||
}
|
||||
} else if tab == .connections {
|
||||
Picker("结果", selection: $model.selectedOutcome) {
|
||||
Text("全部结果").tag(Optional<ConnectionOutcome>.none)
|
||||
ForEach(ConnectionOutcome.allCases, id: \.self) { outcome in
|
||||
Text(outcome.label).tag(Optional(outcome))
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.aiSelectSurface()
|
||||
|
||||
Group {
|
||||
if tab == .runtime {
|
||||
HStack(spacing: 10) {
|
||||
Picker("来源", selection: $model.selectedSource) {
|
||||
Text("全部来源").tag(Optional<RuntimeLogSource>.none)
|
||||
ForEach(RuntimeLogSource.allCases, id: \.self) { source in
|
||||
Text(source.label).tag(Optional(source))
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.aiSelectSurface()
|
||||
|
||||
Picker("级别", selection: $model.selectedLevel) {
|
||||
Text("全部级别").tag(Optional<RuntimeLogLevel>.none)
|
||||
ForEach(RuntimeLogLevel.allCases, id: \.self) { level in
|
||||
Text(level.label).tag(Optional(level))
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.aiSelectSurface()
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
} else if tab == .connections {
|
||||
Picker("结果", selection: $model.selectedOutcome) {
|
||||
Text("全部结果").tag(Optional<ConnectionOutcome>.none)
|
||||
ForEach(ConnectionOutcome.allCases, id: \.self) { outcome in
|
||||
Text(outcome.label).tag(Optional(outcome))
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.aiSelectSurface()
|
||||
}
|
||||
}
|
||||
.transition(.opacity)
|
||||
.aiSelectSurface(compact: true)
|
||||
}
|
||||
.animation(.easeInOut(duration: 0.18), value: tab)
|
||||
}
|
||||
.animation(.easeInOut(duration: 0.18), value: tab)
|
||||
}
|
||||
|
||||
private var runtimeSection: some View {
|
||||
@@ -158,7 +189,7 @@ struct LogsView: View {
|
||||
AISectionTitle("统一时间线", systemImage: "clock", detail: "\(model.filteredRuntime.count) 条")
|
||||
if model.filteredRuntime.isEmpty {
|
||||
AICard {
|
||||
AIEmptyState(systemImage: "clock", title: "暂无运行日志", message: "下拉刷新后会同步实例运行记录。")
|
||||
AIEmptyState(systemImage: "clock", title: "暂无运行日志", message: "所有节点都没有返回运行记录。")
|
||||
}
|
||||
} else {
|
||||
ForEach(model.filteredRuntime) { entry in
|
||||
@@ -255,10 +286,6 @@ struct LogsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var onlineCount: Int {
|
||||
(model.snapshot?.diagnostics.filter { $0.state == .online }.count) ?? 0
|
||||
}
|
||||
|
||||
private func levelTint(_ level: RuntimeLogLevel) -> Color {
|
||||
switch level {
|
||||
case .info: return AITheme.success
|
||||
|
||||
Reference in New Issue
Block a user