release(ios): 发布 0.2.1 并重构图标与界面细节
- 应用图标改用与 Web favicon 同源的扁平标记,金色 SIM 卡体加三格递增信号,29pt 下仍可读 - 标签统一为胶囊形状,提高深色主题下的描边权重,避免标签在暗色下消失 - 消息卡片与详情分区卡片收紧行距,单屏信息密度明显提升 - 详情头部指标精简为 4 行,数据模块改为单条读数 - 批量选择勾选框移出名称区域,工具栏控件统一为等高 34pt - 分组编辑器改为点选即生效,移除说明文字与常驻禁用按钮
This commit is contained in:
@@ -4,7 +4,6 @@ import SwiftUI
|
||||
enum FleetGroupMode: String, CaseIterable, Identifiable {
|
||||
case none
|
||||
case status
|
||||
case tags
|
||||
case groups
|
||||
|
||||
var id: String { rawValue }
|
||||
@@ -13,7 +12,6 @@ enum FleetGroupMode: String, CaseIterable, Identifiable {
|
||||
switch self {
|
||||
case .none: return "平铺"
|
||||
case .status: return "状态"
|
||||
case .tags: return "标签"
|
||||
case .groups: return "分组"
|
||||
}
|
||||
}
|
||||
@@ -184,12 +182,15 @@ final class FleetViewModel: ObservableObject {
|
||||
@Published var rows: [Row] = []
|
||||
@Published var searchText = ""
|
||||
@Published var statusFilter: ConnectionStatus?
|
||||
@Published var groupMode: FleetGroupMode = {
|
||||
let stored = UserDefaults.standard.string(forKey: "fleet.groupMode")
|
||||
return FleetGroupMode(rawValue: stored ?? "") ?? .groups
|
||||
}() {
|
||||
@Published var groupMode: FleetGroupMode = .groups {
|
||||
didSet {
|
||||
UserDefaults.standard.set(groupMode.rawValue, forKey: "fleet.groupMode")
|
||||
preferences.set(groupMode.rawValue, forKey: "fleet.groupMode")
|
||||
}
|
||||
}
|
||||
@Published var selectedTagFilters: Set<String> = [] {
|
||||
didSet {
|
||||
let values = Array(selectedTagFilters).sorted()
|
||||
preferences.set(values, forKey: "fleet.selectedTagFilters")
|
||||
}
|
||||
}
|
||||
@Published var selectedIDs: Set<UUID> = []
|
||||
@@ -206,19 +207,35 @@ final class FleetViewModel: ObservableObject {
|
||||
private let credentials: any CredentialStore
|
||||
private let deviceService: (any DeviceSummaryService)?
|
||||
private let groupStore: (any InstanceGroupStore)?
|
||||
private let preferences: UserDefaults
|
||||
|
||||
init(
|
||||
repository: any InstanceRepository,
|
||||
probe: ConnectionProbe,
|
||||
credentials: any CredentialStore,
|
||||
deviceService: (any DeviceSummaryService)? = nil,
|
||||
groupStore: (any InstanceGroupStore)? = nil
|
||||
groupStore: (any InstanceGroupStore)? = nil,
|
||||
preferences: UserDefaults = .standard
|
||||
) {
|
||||
self.repository = repository
|
||||
self.probe = probe
|
||||
self.credentials = credentials
|
||||
self.deviceService = deviceService
|
||||
self.groupStore = groupStore
|
||||
self.preferences = preferences
|
||||
// 旧版本的「按标签分组」已废弃,标签现在只作为筛选条件存在。
|
||||
let storedMode = preferences.string(forKey: "fleet.groupMode")
|
||||
groupMode = FleetGroupMode(rawValue: storedMode ?? "") ?? .groups
|
||||
selectedTagFilters = Set(preferences.stringArray(forKey: "fleet.selectedTagFilters") ?? [])
|
||||
}
|
||||
|
||||
/// 节点列表变化后,丢弃已经不存在任何实例的标签筛选,避免列表被隐藏条件清空。
|
||||
func pruneTagFilters() {
|
||||
guard selectedTagFilters.isEmpty == false else { return }
|
||||
let live = selectedTagFilters.filter { countForTagFilter($0) > 0 }
|
||||
if live != selectedTagFilters {
|
||||
selectedTagFilters = live
|
||||
}
|
||||
}
|
||||
|
||||
func refresh() async {
|
||||
@@ -243,6 +260,7 @@ final class FleetViewModel: ObservableObject {
|
||||
} catch {
|
||||
errorMessage = "无法刷新节点列表"
|
||||
}
|
||||
pruneTagFilters()
|
||||
lastRefresh = Date()
|
||||
isLoading = false
|
||||
}
|
||||
@@ -400,6 +418,13 @@ final class FleetViewModel: ObservableObject {
|
||||
if let statusFilter, row.status != statusFilter {
|
||||
return false
|
||||
}
|
||||
if selectedTagFilters.isEmpty == false {
|
||||
let tags = Set(row.instance.tags.map { Self.normalizedTag($0) })
|
||||
let needles = Set(selectedTagFilters.map { Self.normalizedTag($0) })
|
||||
guard needles.isSubset(of: tags) else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
guard !searchText.isEmpty else { return true }
|
||||
let haystacks: [String] = [
|
||||
row.instance.name,
|
||||
@@ -417,6 +442,47 @@ final class FleetViewModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
var allTags: [String] {
|
||||
Array(tagCounts.keys).sorted { $0.localizedCompare($1) == .orderedAscending }
|
||||
}
|
||||
|
||||
var tagCounts: [String: Int] {
|
||||
var counts: [String: Int] = [:]
|
||||
for row in rows {
|
||||
for tag in Set(row.instance.tags.map(Self.normalizedTag)) where tag.isEmpty == false {
|
||||
counts[tag, default: 0] += 1
|
||||
}
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
func countForTagFilter(_ tag: String) -> Int {
|
||||
tagCounts[Self.normalizedTag(tag)] ?? 0
|
||||
}
|
||||
|
||||
func isTagFilterActive(_ tag: String) -> Bool {
|
||||
selectedTagFilters.contains(Self.normalizedTag(tag))
|
||||
}
|
||||
|
||||
func toggleTagFilter(_ tag: String) {
|
||||
let normalized = Self.normalizedTag(tag)
|
||||
guard normalized.isEmpty == false else { return }
|
||||
if selectedTagFilters.contains(normalized) {
|
||||
selectedTagFilters.remove(normalized)
|
||||
} else {
|
||||
selectedTagFilters.insert(normalized)
|
||||
}
|
||||
}
|
||||
|
||||
func clearTagFilters() {
|
||||
guard selectedTagFilters.isEmpty == false else { return }
|
||||
selectedTagFilters.removeAll()
|
||||
}
|
||||
|
||||
private static func normalizedTag(_ tag: String) -> String {
|
||||
tag.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
}
|
||||
|
||||
var groupedRows: [FleetGroup] {
|
||||
let rows = visibleRows.sorted {
|
||||
$0.instance.name.localizedCompare($1.instance.name) == .orderedAscending
|
||||
@@ -431,19 +497,6 @@ final class FleetViewModel: ObservableObject {
|
||||
guard !matched.isEmpty else { return nil }
|
||||
return FleetGroup(id: status.rawValue, title: status.label, rows: matched)
|
||||
}
|
||||
case .tags:
|
||||
let tags = Array(Set(rows.flatMap(\.instance.tags))).sorted()
|
||||
let grouped = tags.map { tag in
|
||||
FleetGroup(
|
||||
id: tag,
|
||||
title: tag,
|
||||
rows: rows.filter { $0.instance.tags.contains(tag) }
|
||||
)
|
||||
}
|
||||
let untagged = rows.filter { $0.instance.tags.isEmpty }
|
||||
return grouped + (untagged.isEmpty
|
||||
? []
|
||||
: [FleetGroup(id: "__untagged__", title: "未打标签", rows: untagged)])
|
||||
case .groups:
|
||||
var groupedRows: [UUID: [Row]] = [:]
|
||||
var ungroupedRows: [Row] = []
|
||||
|
||||
Reference in New Issue
Block a user