release(ios): 0.2.2 构建 9 修复消息发送遮挡并中性化石墨灰阶
- 消息中心发送入口从被标签栏盖住的底部操作条移到导航栏右侧,发信恢复可用 - 停用态主按钮保留主色淡底与同色描边,与次级按钮拉开层级 - 消息行删除改安静线性图标,去掉一屏红块 - 缺数据读数统一为短横,卡片延迟补空格单位 - 中性灰阶改为严格 R=G=B,浅色主题不再读出冷调;图标石墨底同步去蓝 - 详情页延迟量表改用与卡片同一算法,身份信息值改为右对齐 - 标签加号按钮与输入框同高同圆角
This commit is contained in:
@@ -14,3 +14,6 @@ data/
|
|||||||
|
|
||||||
# 误在工作副本里跑构建时留下的缓存目录
|
# 误在工作副本里跑构建时留下的缓存目录
|
||||||
apps/ios/apps/
|
apps/ios/apps/
|
||||||
|
|
||||||
|
# 图标由 Design/generate_app_icon.swift 重绘,产物不入库(正式资源在 Assets.xcassets)
|
||||||
|
apps/ios/Design/AppIcon.png
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## iOS Native 应用
|
## iOS Native 应用
|
||||||
|
|
||||||
`apps/ios` 提供不依赖 8788 控制台的原生 iPhone 应用,当前版本 **0.2.2**,构建号 8,运行在 iOS 16 及以上,产物为未签名 IPA,供 TrollStore 或自有签名流程安装。iOS 开发在 `ios-native-ipa` 分支进行。
|
`apps/ios` 提供不依赖 8788 控制台的原生 iPhone 应用,当前版本 **0.2.2**,构建号 9,运行在 iOS 16 及以上,产物为未签名 IPA,供 TrollStore 或自有签名流程安装。iOS 开发在 `ios-native-ipa` 分支进行。
|
||||||
|
|
||||||
底部为节点、消息、设置三个入口:
|
底部为节点、消息、设置三个入口:
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,18 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="field" x1="0.06" y1="0" x2="0.94" y2="1">
|
<linearGradient id="field" x1="0.06" y1="0" x2="0.94" y2="1">
|
||||||
<stop offset="0" stop-color="#202021"/>
|
<stop offset="0" stop-color="#202020"/>
|
||||||
<stop offset="0.55" stop-color="#131314"/>
|
<stop offset="0.55" stop-color="#131313"/>
|
||||||
<stop offset="1" stop-color="#0A0A0B"/>
|
<stop offset="1" stop-color="#0A0A0A"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient id="brass" x1="0" y1="0" x2="1" y2="1">
|
<linearGradient id="brass" x1="0" y1="0" x2="1" y2="1">
|
||||||
<stop offset="0" stop-color="#FADD98"/>
|
<stop offset="0" stop-color="#FCE5AA"/>
|
||||||
<stop offset="0.46" stop-color="#EEBF64"/>
|
<stop offset="0.46" stop-color="#F0C76F"/>
|
||||||
<stop offset="1" stop-color="#C78D2F"/>
|
<stop offset="1" stop-color="#D39B3D"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient id="slot" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="slot" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="0" stop-color="#101011"/>
|
<stop offset="0" stop-color="#101010"/>
|
||||||
<stop offset="1" stop-color="#202021"/>
|
<stop offset="1" stop-color="#202020"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -133,9 +133,10 @@ func directionalRim(_ path: CGPath, _ rect: CGRect, _ width: CGFloat, _ stops: [
|
|||||||
fillLinear(
|
fillLinear(
|
||||||
rectPath(CGRect(x: 0, y: 0, width: side, height: side)),
|
rectPath(CGRect(x: 0, y: 0, width: side, height: side)),
|
||||||
[
|
[
|
||||||
(0.00, (32, 32, 33, 255)),
|
// 石墨底严格等色。原先 B 比 R 高 1 级,缩到 60pt 后边缘像素仍会落在蓝色波段。
|
||||||
(0.55, (19, 19, 20, 255)),
|
(0.00, (32, 32, 32, 255)),
|
||||||
(1.00, (10, 10, 11, 255)),
|
(0.55, (19, 19, 19, 255)),
|
||||||
|
(1.00, (10, 10, 10, 255)),
|
||||||
] as [Stop],
|
] as [Stop],
|
||||||
CGPoint(x: side * 0.06, y: 0),
|
CGPoint(x: side * 0.06, y: 0),
|
||||||
CGPoint(x: side * 0.94, y: side)
|
CGPoint(x: side * 0.94, y: side)
|
||||||
@@ -169,9 +170,10 @@ ctx.restoreGState()
|
|||||||
fillLinear(
|
fillLinear(
|
||||||
plate,
|
plate,
|
||||||
[
|
[
|
||||||
(0.00, (250, 221, 152, 255)),
|
// 金色往香槟靠,收掉底部的芥末/棕调:小尺寸下这段最容易被读成"脏黄"。
|
||||||
(0.46, (238, 191, 100, 255)),
|
(0.00, (252, 229, 170, 255)),
|
||||||
(1.00, (199, 141, 47, 255)),
|
(0.46, (240, 199, 111, 255)),
|
||||||
|
(1.00, (211, 155, 61, 255)),
|
||||||
] as [Stop],
|
] as [Stop],
|
||||||
CGPoint(x: plateRect.minX, y: plateRect.minY),
|
CGPoint(x: plateRect.minX, y: plateRect.minY),
|
||||||
CGPoint(x: plateRect.maxX, y: plateRect.maxY)
|
CGPoint(x: plateRect.maxX, y: plateRect.maxY)
|
||||||
@@ -191,6 +193,8 @@ let slotWidth = envDouble("ICON_SLOT_W", 92)
|
|||||||
let slotGap = envDouble("ICON_SLOT_GAP", 54)
|
let slotGap = envDouble("ICON_SLOT_GAP", 54)
|
||||||
let slotBottom = envDouble("ICON_SLOT_BOTTOM", 640)
|
let slotBottom = envDouble("ICON_SLOT_BOTTOM", 640)
|
||||||
let slotHeights: [CGFloat] = [136, 232, 328]
|
let slotHeights: [CGFloat] = [136, 232, 328]
|
||||||
|
// 说明:以上默认值等价于 plate 相对比例(宽 .155 / 间距 .091 / 底边内缩 .265),
|
||||||
|
// 改 plate 尺寸时按同一比例缩放才不会让槽和卡片脱节。
|
||||||
let slotsWidth = CGFloat(slotHeights.count) * slotWidth + CGFloat(slotHeights.count - 1) * slotGap
|
let slotsWidth = CGFloat(slotHeights.count) * slotWidth + CGFloat(slotHeights.count - 1) * slotGap
|
||||||
let slotsStartX = (side - slotsWidth) / 2
|
let slotsStartX = (side - slotsWidth) / 2
|
||||||
|
|
||||||
@@ -207,8 +211,8 @@ for (index, height) in slotHeights.enumerated() {
|
|||||||
fillLinear(
|
fillLinear(
|
||||||
path,
|
path,
|
||||||
[
|
[
|
||||||
(0.00, (16, 16, 17, 255)),
|
(0.00, (16, 16, 16, 255)),
|
||||||
(1.00, (32, 32, 33, 255)),
|
(1.00, (32, 32, 32, 255)),
|
||||||
] as [Stop],
|
] as [Stop],
|
||||||
CGPoint(x: rect.minX, y: rect.minY),
|
CGPoint(x: rect.minX, y: rect.minY),
|
||||||
CGPoint(x: rect.minX, y: rect.maxY)
|
CGPoint(x: rect.minX, y: rect.maxY)
|
||||||
@@ -217,9 +221,9 @@ for (index, height) in slotHeights.enumerated() {
|
|||||||
directionalRim(
|
directionalRim(
|
||||||
path, rect, radius * 0.30,
|
path, rect, radius * 0.30,
|
||||||
[
|
[
|
||||||
(0.00, (112, 74, 16, 100)),
|
(0.00, (120, 82, 24, 96)),
|
||||||
(0.52, (112, 74, 16, 18)),
|
(0.52, (120, 82, 24, 16)),
|
||||||
(1.00, (255, 236, 190, 60)),
|
(1.00, (255, 240, 202, 64)),
|
||||||
] as [Stop]
|
] as [Stop]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ public enum AppMetadata {
|
|||||||
public static let name = "MultiSimAdmin"
|
public static let name = "MultiSimAdmin"
|
||||||
public static let bundleIdentifier = "fun.chick.multi-simadmin"
|
public static let bundleIdentifier = "fun.chick.multi-simadmin"
|
||||||
public static let marketingVersion = "0.2.2"
|
public static let marketingVersion = "0.2.2"
|
||||||
public static let buildVersion = "8"
|
public static let buildVersion = "9"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -237,7 +237,9 @@ struct InstanceDetailView: View {
|
|||||||
section: "connectivity",
|
section: "connectivity",
|
||||||
keys: ["latency_ms", "latency"],
|
keys: ["latency_ms", "latency"],
|
||||||
format: loading ? nil : { "\(Int($0.rounded())) ms" },
|
format: loading ? nil : { "\(Int($0.rounded())) ms" },
|
||||||
progress: loading ? nil : { _ in 1 },
|
// 进度条读的是"链路质量",和节点卡片同一把尺子(1000ms 归零),
|
||||||
|
// 之前恒为 1,满格黑条压在 38ms 上会被读成"延迟占满了什么"。
|
||||||
|
progress: loading ? nil : { max(0.04, min(1, 1 - $0 / 1000)) },
|
||||||
tint: loading ? nil : { latencyTint($0) }
|
tint: loading ? nil : { latencyTint($0) }
|
||||||
)
|
)
|
||||||
let cpu = summaryValue(
|
let cpu = summaryValue(
|
||||||
@@ -562,18 +564,22 @@ struct InstanceDetailView: View {
|
|||||||
} else if items.isEmpty == false {
|
} else if items.isEmpty == false {
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
ForEach(items, id: \.label) { item in
|
ForEach(items, id: \.label) { item in
|
||||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
// 值一律右对齐,和下方分区卡片的键值行同一节奏;
|
||||||
|
// 之前左边缩进 52pt、值顶在中间,同屏两种排法读起来像两张不同的表。
|
||||||
|
HStack(alignment: .firstTextBaseline, spacing: 12) {
|
||||||
Text(item.label)
|
Text(item.label)
|
||||||
.font(.caption2.weight(.semibold))
|
.font(.caption2.weight(.semibold))
|
||||||
.foregroundStyle(AITheme.muted)
|
.foregroundStyle(AITheme.muted)
|
||||||
.frame(width: 52, alignment: .leading)
|
.fixedSize(horizontal: true, vertical: false)
|
||||||
|
Spacer(minLength: 12)
|
||||||
Text(item.value)
|
Text(item.value)
|
||||||
.font(.caption.weight(.medium))
|
.font(.caption.weight(.medium))
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
.foregroundStyle(AITheme.ink)
|
.foregroundStyle(AITheme.ink)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.truncationMode(.middle)
|
.truncationMode(.middle)
|
||||||
Spacer(minLength: 0)
|
.multilineTextAlignment(.trailing)
|
||||||
|
.layoutPriority(1)
|
||||||
}
|
}
|
||||||
.accessibilityElement(children: .combine)
|
.accessibilityElement(children: .combine)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,6 +180,9 @@ struct FleetView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private extension FleetView {
|
private extension FleetView {
|
||||||
|
/// 概览读数缺数据时只画一根短横:「不可用」在四列窄格里既占位又容易被读成故障结论。
|
||||||
|
static let noDataText = "—"
|
||||||
|
|
||||||
private static let relativeFormatter: RelativeDateTimeFormatter = {
|
private static let relativeFormatter: RelativeDateTimeFormatter = {
|
||||||
let formatter = RelativeDateTimeFormatter()
|
let formatter = RelativeDateTimeFormatter()
|
||||||
formatter.unitsStyle = .abbreviated
|
formatter.unitsStyle = .abbreviated
|
||||||
@@ -361,7 +364,7 @@ private extension FleetView {
|
|||||||
)
|
)
|
||||||
FleetHeadlineStat(
|
FleetHeadlineStat(
|
||||||
title: "异常节点",
|
title: "异常节点",
|
||||||
value: settledCount == 0 ? "--" : "\(abnormalCount)",
|
value: settledCount == 0 ? Self.noDataText : "\(abnormalCount)",
|
||||||
detail: abnormalDetail,
|
detail: abnormalDetail,
|
||||||
tint: abnormalTint
|
tint: abnormalTint
|
||||||
)
|
)
|
||||||
@@ -666,7 +669,7 @@ private extension FleetView {
|
|||||||
private var hasSettledResults: Bool { summaryRows.isEmpty == false && settledCount > 0 }
|
private var hasSettledResults: Bool { summaryRows.isEmpty == false && settledCount > 0 }
|
||||||
|
|
||||||
private var onlineRateValue: String {
|
private var onlineRateValue: String {
|
||||||
guard hasSettledResults else { return "--" }
|
guard hasSettledResults else { return Self.noDataText }
|
||||||
return "\(Int((Double(onlineCount) / Double(summaryRows.count) * 100).rounded()))%"
|
return "\(Int((Double(onlineCount) / Double(summaryRows.count) * 100).rounded()))%"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -749,10 +752,10 @@ private extension FleetView {
|
|||||||
|
|
||||||
private var storageSummary: String {
|
private var storageSummary: String {
|
||||||
let pairs = storagePairs
|
let pairs = storagePairs
|
||||||
guard pairs.isEmpty == false else { return "不可用" }
|
guard pairs.isEmpty == false else { return Self.noDataText }
|
||||||
let used = pairs.map { max(0, $0.total - $0.available) }.reduce(0, +)
|
let used = pairs.map { max(0, $0.total - $0.available) }.reduce(0, +)
|
||||||
let total = pairs.map(\.total).reduce(0, +)
|
let total = pairs.map(\.total).reduce(0, +)
|
||||||
guard total > 0 else { return "不可用" }
|
guard total > 0 else { return Self.noDataText }
|
||||||
return "\(Int((Double(used) / Double(total) * 100).rounded()))% 已用"
|
return "\(Int((Double(used) / Double(total) * 100).rounded()))% 已用"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -777,7 +780,7 @@ private extension FleetView {
|
|||||||
|
|
||||||
private var trafficSummary: String {
|
private var trafficSummary: String {
|
||||||
let values = summaryRows.compactMap(\.downloadRateBytesPerSecond) + summaryRows.compactMap(\.uploadRateBytesPerSecond)
|
let values = summaryRows.compactMap(\.downloadRateBytesPerSecond) + summaryRows.compactMap(\.uploadRateBytesPerSecond)
|
||||||
guard values.isEmpty == false else { return "不可用" }
|
guard values.isEmpty == false else { return Self.noDataText }
|
||||||
return ByteCountFormatter.string(fromByteCount: Int64(values.reduce(0, +)), countStyle: .decimal) + "/s"
|
return ByteCountFormatter.string(fromByteCount: Int64(values.reduce(0, +)), countStyle: .decimal) + "/s"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -793,7 +796,7 @@ private extension FleetView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func formatAverage(_ values: [Double], transform: (Double) -> String) -> String {
|
private func formatAverage(_ values: [Double], transform: (Double) -> String) -> String {
|
||||||
guard values.isEmpty == false else { return "不可用" }
|
guard values.isEmpty == false else { return Self.noDataText }
|
||||||
let average = values.reduce(0, +) / Double(values.count)
|
let average = values.reduce(0, +) / Double(values.count)
|
||||||
return transform(average)
|
return transform(average)
|
||||||
}
|
}
|
||||||
@@ -927,25 +930,25 @@ private extension FleetView {
|
|||||||
[
|
[
|
||||||
FleetGauge(
|
FleetGauge(
|
||||||
label: "信号",
|
label: "信号",
|
||||||
value: row.signalPercent.map { "\($0)%" } ?? "--",
|
value: row.signalPercent.map { "\($0)%" } ?? Self.noDataText,
|
||||||
ratio: row.signalPercent.map { Double($0) / 100 },
|
ratio: row.signalPercent.map { Double($0) / 100 },
|
||||||
tint: row.signalPercent.flatMap(signalColor) ?? AITheme.muted
|
tint: row.signalPercent.flatMap(signalColor) ?? AITheme.muted
|
||||||
),
|
),
|
||||||
FleetGauge(
|
FleetGauge(
|
||||||
label: "延迟",
|
label: "延迟",
|
||||||
value: row.latencyMilliseconds.map { "\($0)ms" } ?? "--",
|
value: row.latencyMilliseconds.map { "\($0) ms" } ?? Self.noDataText,
|
||||||
ratio: row.latencyMilliseconds.map { max(0, 1 - Double($0) / 1000) },
|
ratio: row.latencyMilliseconds.map { max(0, 1 - Double($0) / 1000) },
|
||||||
tint: row.latencyMilliseconds.flatMap(latencyColor) ?? AITheme.muted
|
tint: row.latencyMilliseconds.flatMap(latencyColor) ?? AITheme.muted
|
||||||
),
|
),
|
||||||
FleetGauge(
|
FleetGauge(
|
||||||
label: "CPU",
|
label: "CPU",
|
||||||
value: row.cpuPercent.map(Self.percentText) ?? "--",
|
value: row.cpuPercent.map(Self.percentText) ?? Self.noDataText,
|
||||||
ratio: row.cpuPercent.map { $0 / 100 },
|
ratio: row.cpuPercent.map { $0 / 100 },
|
||||||
tint: row.cpuPercent.flatMap(resourceColor) ?? AITheme.muted
|
tint: row.cpuPercent.flatMap(resourceColor) ?? AITheme.muted
|
||||||
),
|
),
|
||||||
FleetGauge(
|
FleetGauge(
|
||||||
label: "内存",
|
label: "内存",
|
||||||
value: row.memoryPercent.map(Self.percentText) ?? "--",
|
value: row.memoryPercent.map(Self.percentText) ?? Self.noDataText,
|
||||||
ratio: row.memoryPercent.map { $0 / 100 },
|
ratio: row.memoryPercent.map { $0 / 100 },
|
||||||
tint: row.memoryPercent.flatMap(resourceColor) ?? AITheme.muted
|
tint: row.memoryPercent.flatMap(resourceColor) ?? AITheme.muted
|
||||||
),
|
),
|
||||||
@@ -993,7 +996,7 @@ private extension FleetView {
|
|||||||
|
|
||||||
private static func cpuValue(percent: Double?, cores: Int?) -> String? {
|
private static func cpuValue(percent: Double?, cores: Int?) -> String? {
|
||||||
guard percent != nil || cores != nil else { return nil }
|
guard percent != nil || cores != nil else { return nil }
|
||||||
let usage = percent.map(Self.percentText) ?? "--"
|
let usage = percent.map(Self.percentText) ?? FleetView.noDataText
|
||||||
guard let cores, cores > 0 else { return usage }
|
guard let cores, cores > 0 else { return usage }
|
||||||
return "\(usage) · \(cores) 核"
|
return "\(usage) · \(cores) 核"
|
||||||
}
|
}
|
||||||
@@ -1296,7 +1299,7 @@ extension FleetView {
|
|||||||
|
|
||||||
// 没有数据时用弱色占位,读起来不会像"0%"那样的假结论。
|
// 没有数据时用弱色占位,读起来不会像"0%"那样的假结论。
|
||||||
var isPlaceholder: Bool {
|
var isPlaceholder: Bool {
|
||||||
value == "--" || value == "不可用" || value == "暂无"
|
value == "--" || value == "—" || value == "不可用" || value == "暂无"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1546,7 +1549,7 @@ extension FleetView {
|
|||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundStyle(AITheme.faint)
|
.foregroundStyle(AITheme.faint)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
Text(value ?? "--")
|
Text(value ?? FleetView.noDataText)
|
||||||
.font(.footnote.weight(.semibold))
|
.font(.footnote.weight(.semibold))
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
.foregroundStyle(isAlert ? tint : AITheme.ink)
|
.foregroundStyle(isAlert ? tint : AITheme.ink)
|
||||||
|
|||||||
@@ -22,16 +22,17 @@ struct TagEditorView: View {
|
|||||||
commit(draft)
|
commit(draft)
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "plus")
|
Image(systemName: "plus")
|
||||||
.font(.system(size: 15, weight: .semibold))
|
// 加号按钮和左边的输入框同一高度、同一圆角,两个控件才读得出是一组。
|
||||||
.frame(width: 44, height: 44)
|
.font(.system(size: 16, weight: .semibold))
|
||||||
|
.frame(width: AITheme.inputHeight, height: AITheme.inputHeight)
|
||||||
.foregroundStyle(canAdd ? AITheme.primary : AITheme.muted)
|
.foregroundStyle(canAdd ? AITheme.primary : AITheme.muted)
|
||||||
.background(canAdd ? AITheme.primary.opacity(0.10) : AITheme.control)
|
.background(canAdd ? AITheme.primary.opacity(0.10) : AITheme.control)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
.clipShape(RoundedRectangle(cornerRadius: AITheme.inputRadius, style: .continuous))
|
||||||
.overlay {
|
.overlay {
|
||||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
RoundedRectangle(cornerRadius: AITheme.inputRadius, style: .continuous)
|
||||||
.strokeBorder(canAdd ? AITheme.primary.opacity(0.40) : AITheme.line)
|
.strokeBorder(canAdd ? AITheme.primary.opacity(0.40) : AITheme.line)
|
||||||
}
|
}
|
||||||
.contentShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
.contentShape(RoundedRectangle(cornerRadius: AITheme.inputRadius, style: .continuous))
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.disabled(canAdd == false)
|
.disabled(canAdd == false)
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ struct LogsView: View {
|
|||||||
status: entry.outcome.label,
|
status: entry.outcome.label,
|
||||||
statusTint: outcomeTint(entry.outcome),
|
statusTint: outcomeTint(entry.outcome),
|
||||||
details: [
|
details: [
|
||||||
"HTTP \(entry.httpStatus.map(String.init) ?? "--") · \(entry.durationMs) ms",
|
"HTTP \(entry.httpStatus.map(String.init) ?? "—") · \(entry.durationMs) ms",
|
||||||
entry.errorCode ?? "无错误码",
|
entry.errorCode ?? "无错误码",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -46,22 +46,21 @@ struct MessagesView: View {
|
|||||||
.padding(16)
|
.padding(16)
|
||||||
.padding(.bottom, 8)
|
.padding(.bottom, 8)
|
||||||
}
|
}
|
||||||
.aiBottomActionBar {
|
|
||||||
Button {
|
|
||||||
model.showingSendForm = true
|
|
||||||
} label: {
|
|
||||||
Label("发送消息", systemImage: "paperplane")
|
|
||||||
}
|
|
||||||
.buttonStyle(AIButtonStyle(kind: .primary, size: .regular, fillsWidth: true))
|
|
||||||
}
|
|
||||||
.background(AITheme.canvas.ignoresSafeArea())
|
.background(AITheme.canvas.ignoresSafeArea())
|
||||||
.navigationTitle("消息中心")
|
.navigationTitle("消息中心")
|
||||||
.toolbar {
|
.toolbar {
|
||||||
|
// 发送入口和节点页的「添加实例」保持一致:放在导航栏右侧,
|
||||||
|
// 底部固定条在 TabView 里会被自定义标签栏盖住,之前整条 CTA 是不可达的。
|
||||||
ToolbarItem(placement: .primaryAction) {
|
ToolbarItem(placement: .primaryAction) {
|
||||||
AIIconButton("刷新", systemImage: "arrow.clockwise") {
|
HStack(spacing: 8) {
|
||||||
Task { await model.refresh() }
|
AIIconButton("刷新", systemImage: "arrow.clockwise") {
|
||||||
|
Task { await model.refresh() }
|
||||||
|
}
|
||||||
|
.disabled(model.isLoading)
|
||||||
|
AIIconButton("发送消息", systemImage: "paperplane") {
|
||||||
|
model.showingSendForm = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.disabled(model.isLoading)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.confirmationDialog(
|
.confirmationDialog(
|
||||||
@@ -164,7 +163,7 @@ struct MessagesView: View {
|
|||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
Spacer(minLength: 8)
|
Spacer(minLength: 8)
|
||||||
AISymbolAction("删除这条消息", systemImage: "trash", tint: AITheme.danger) {
|
AISymbolAction("删除这条消息", systemImage: "trash", tint: AITheme.danger, quiet: true) {
|
||||||
pendingDeleteMessage = message
|
pendingDeleteMessage = message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ struct SettingsRuntime: Equatable {
|
|||||||
final class SettingsViewModel: ObservableObject {
|
final class SettingsViewModel: ObservableObject {
|
||||||
@Published private(set) var runtime: SettingsRuntime?
|
@Published private(set) var runtime: SettingsRuntime?
|
||||||
@Published private(set) var isLoading = false
|
@Published private(set) var isLoading = false
|
||||||
@Published var storageLabel = "--"
|
@Published var storageLabel = "—"
|
||||||
@Published var notice: String?
|
@Published var notice: String?
|
||||||
@Published var errorMessage: String?
|
@Published var errorMessage: String?
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ final class SettingsViewModel: ObservableObject {
|
|||||||
let connectionCount = try await store.connectionLogs().count
|
let connectionCount = try await store.connectionLogs().count
|
||||||
storageLabel = "\(runtimeCount) 条运行日志 · \(connectionCount) 条连接日志"
|
storageLabel = "\(runtimeCount) 条运行日志 · \(connectionCount) 条连接日志"
|
||||||
} catch {
|
} catch {
|
||||||
storageLabel = "--"
|
storageLabel = "—"
|
||||||
errorMessage = "无法读取本地数据状态。"
|
errorMessage = "无法读取本地数据状态。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 812 KiB After Width: | Height: | Size: 809 KiB |
@@ -10,7 +10,7 @@
|
|||||||
<key>CFBundleName</key><string>MultiSimAdmin</string>
|
<key>CFBundleName</key><string>MultiSimAdmin</string>
|
||||||
<key>CFBundlePackageType</key><string>APPL</string>
|
<key>CFBundlePackageType</key><string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key><string>0.2.2</string>
|
<key>CFBundleShortVersionString</key><string>0.2.2</string>
|
||||||
<key>CFBundleVersion</key><string>8</string>
|
<key>CFBundleVersion</key><string>9</string>
|
||||||
<key>LSRequiresIPhoneOS</key><true/>
|
<key>LSRequiresIPhoneOS</key><true/>
|
||||||
<key>MinimumOSVersion</key><string>16.0</string>
|
<key>MinimumOSVersion</key><string>16.0</string>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
|
|||||||
@@ -21,10 +21,11 @@ public struct AIButtonStyle: ButtonStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func backgroundColor(isEnabled: Bool) -> Color {
|
func backgroundColor(isEnabled: Bool) -> Color {
|
||||||
switch self {
|
if isEnabled { return kindBackground }
|
||||||
case .primary, .secondary, .danger:
|
// 禁用态:实心主色按钮保留主色淡版,其余退回中性灰。
|
||||||
return isEnabled ? kindBackground : AITheme.control
|
return self == .primary
|
||||||
}
|
? AITheme.primary.opacity(AITheme.disabledFillOpacity)
|
||||||
|
: AITheme.control
|
||||||
}
|
}
|
||||||
|
|
||||||
private var kindBackground: Color {
|
private var kindBackground: Color {
|
||||||
@@ -34,6 +35,13 @@ public struct AIButtonStyle: ButtonStyle {
|
|||||||
case .danger: return AITheme.danger
|
case .danger: return AITheme.danger
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 禁用态下主按钮必须还能看出"这是主行动"。
|
||||||
|
/// 之前三种 kind 禁用后都塌成同一块 control 灰,表单里"取消"和"保存"长得一模一样,
|
||||||
|
/// 用户只能靠文字猜哪个是提交。这里让实心主色按钮改用同色相淡填充 + 描边,灰底次按钮不套。
|
||||||
|
var usesTintedDisabledTreatment: Bool {
|
||||||
|
self == .primary
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Size: Hashable {
|
public enum Size: Hashable {
|
||||||
@@ -74,6 +82,8 @@ public struct AIButtonStyle: ButtonStyle {
|
|||||||
case .toolbarInline: AITheme.toolbarButtonHeight
|
case .toolbarInline: AITheme.toolbarButtonHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isTintedDisabled = kind.usesTintedDisabledTreatment && !isEnabled
|
||||||
|
|
||||||
configuration.label
|
configuration.label
|
||||||
.labelStyle(.titleAndIcon)
|
.labelStyle(.titleAndIcon)
|
||||||
.font(
|
.font(
|
||||||
@@ -94,7 +104,7 @@ public struct AIButtonStyle: ButtonStyle {
|
|||||||
AnyShapeStyle(kind.backgroundColor(isEnabled: isEnabled))
|
AnyShapeStyle(kind.backgroundColor(isEnabled: isEnabled))
|
||||||
)
|
)
|
||||||
.shadow(
|
.shadow(
|
||||||
color: (kind == .secondary || !isEnabled) ? .clear : .black.opacity(0.10),
|
color: (kind == .secondary || isTintedDisabled) ? .clear : .black.opacity(0.10),
|
||||||
radius: 12,
|
radius: 12,
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 5
|
y: 5
|
||||||
@@ -111,9 +121,11 @@ public struct AIButtonStyle: ButtonStyle {
|
|||||||
style: .continuous
|
style: .continuous
|
||||||
)
|
)
|
||||||
.strokeBorder(
|
.strokeBorder(
|
||||||
size == .toolbarInline && kind == .primary
|
isTintedDisabled
|
||||||
? AITheme.primary.opacity(0.28)
|
? AITheme.primary.opacity(AITheme.disabledStrokeOpacity)
|
||||||
: (kind == .secondary || !isEnabled ? AITheme.line : .clear)
|
: size == .toolbarInline && kind == .primary
|
||||||
|
? AITheme.primary.opacity(0.28)
|
||||||
|
: (kind == .secondary || !isEnabled ? AITheme.line : .clear)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.scaleEffect(configuration.isPressed ? Self.pressedScale : 1)
|
.scaleEffect(configuration.isPressed ? Self.pressedScale : 1)
|
||||||
|
|||||||
@@ -820,17 +820,22 @@ public struct AISymbolAction: View {
|
|||||||
private let title: String
|
private let title: String
|
||||||
private let systemImage: String
|
private let systemImage: String
|
||||||
private let tint: Color
|
private let tint: Color
|
||||||
|
/// 安静样式:只留着色图标与点击区,不再铺色块。
|
||||||
|
/// 卡片列表里每行都放一块彩色方块会把整页染成"警告",删除这类低频动作更适合安静样式。
|
||||||
|
private let isQuiet: Bool
|
||||||
private let action: () -> Void
|
private let action: () -> Void
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
_ title: String,
|
_ title: String,
|
||||||
systemImage: String,
|
systemImage: String,
|
||||||
tint: Color = AITheme.muted,
|
tint: Color = AITheme.muted,
|
||||||
|
quiet: Bool = false,
|
||||||
action: @escaping () -> Void
|
action: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
self.title = title
|
self.title = title
|
||||||
self.systemImage = systemImage
|
self.systemImage = systemImage
|
||||||
self.tint = tint
|
self.tint = tint
|
||||||
|
self.isQuiet = quiet
|
||||||
self.action = action
|
self.action = action
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -842,11 +847,13 @@ public struct AISymbolAction: View {
|
|||||||
.font(.system(size: 14, weight: .semibold))
|
.font(.system(size: 14, weight: .semibold))
|
||||||
.foregroundStyle(tint)
|
.foregroundStyle(tint)
|
||||||
.frame(width: AITheme.iconButtonSize, height: AITheme.iconButtonSize)
|
.frame(width: AITheme.iconButtonSize, height: AITheme.iconButtonSize)
|
||||||
.background(tint.opacity(0.10))
|
.background(isQuiet ? Color.clear : tint.opacity(0.10))
|
||||||
.clipShape(RoundedRectangle(cornerRadius: AITheme.iconButtonRadius, style: .continuous))
|
.clipShape(RoundedRectangle(cornerRadius: AITheme.iconButtonRadius, style: .continuous))
|
||||||
.overlay {
|
.overlay {
|
||||||
RoundedRectangle(cornerRadius: AITheme.iconButtonRadius, style: .continuous)
|
if isQuiet == false {
|
||||||
.strokeBorder(tint.opacity(0.18))
|
RoundedRectangle(cornerRadius: AITheme.iconButtonRadius, style: .continuous)
|
||||||
|
.strokeBorder(tint.opacity(0.18))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.frame(width: 44, height: 44)
|
.frame(width: 44, height: 44)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
|
|||||||
@@ -28,76 +28,77 @@ private enum AIPalette {
|
|||||||
dynamicColor(light: light, dark: dark)
|
dynamicColor(light: light, dark: dark)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 石墨灰阶:所有中性色 R≈G≈B,不带蓝调;主色让位给黑/白,颜色只用来表达状态。
|
// 中性石墨灰阶:所有中性色严格 R=G=B,既不冷到读出蓝,也不暖到读出米色;主色让位给
|
||||||
static let canvas = color(light: rgba(244, 245, 245, 1), dark: rgba(8, 9, 9, 1))
|
// 黑/白,颜色只用来表达状态。原先 B 比 R 高 3~10 级,浅色下大面积文字与描边会蒙一层冷调。
|
||||||
static let surface = color(light: rgba(255, 255, 255, 1), dark: rgba(19, 21, 21, 1))
|
static let canvas = color(light: rgba(245, 245, 245, 1), dark: rgba(10, 10, 10, 1))
|
||||||
static let surfaceRaised = color(light: rgba(255, 255, 255, 1), dark: rgba(27, 29, 30, 1))
|
static let surface = color(light: rgba(255, 255, 255, 1), dark: rgba(21, 21, 21, 1))
|
||||||
static let control = color(light: rgba(238, 239, 239, 1), dark: rgba(36, 39, 40, 1))
|
static let surfaceRaised = color(light: rgba(255, 255, 255, 1), dark: rgba(29, 29, 29, 1))
|
||||||
static let tile = color(light: rgba(246, 247, 247, 1), dark: rgba(255, 255, 255, 0.055))
|
static let control = color(light: rgba(239, 239, 239, 1), dark: rgba(39, 39, 39, 1))
|
||||||
static let tileLine = color(light: rgba(231, 233, 233, 1), dark: rgba(255, 255, 255, 0.09))
|
static let tile = color(light: rgba(247, 247, 247, 1), dark: rgba(255, 255, 255, 0.055))
|
||||||
static let chrome = color(light: rgba(252, 252, 252, 0.92), dark: rgba(11, 12, 12, 0.88))
|
static let tileLine = color(light: rgba(232, 232, 232, 1), dark: rgba(255, 255, 255, 0.09))
|
||||||
static let ink = color(light: rgba(17, 19, 20, 1), dark: rgba(240, 242, 242, 1))
|
static let chrome = color(light: rgba(252, 252, 252, 0.92), dark: rgba(13, 13, 13, 0.88))
|
||||||
static let muted = color(light: rgba(88, 94, 97, 1), dark: rgba(158, 165, 168, 1))
|
static let ink = color(light: rgba(18, 18, 18, 1), dark: rgba(242, 242, 242, 1))
|
||||||
static let faint = color(light: rgba(126, 132, 135, 1), dark: rgba(112, 118, 121, 1))
|
static let muted = color(light: rgba(92, 92, 92, 1), dark: rgba(164, 164, 164, 1))
|
||||||
static let line = color(light: rgba(223, 226, 226, 1), dark: rgba(255, 255, 255, 0.11))
|
static let faint = color(light: rgba(130, 130, 130, 1), dark: rgba(118, 118, 118, 1))
|
||||||
static let brand = color(light: rgba(18, 20, 21, 1), dark: rgba(8, 9, 9, 1))
|
static let line = color(light: rgba(226, 226, 226, 1), dark: rgba(255, 255, 255, 0.11))
|
||||||
static let primary = color(light: rgba(21, 24, 25, 1), dark: rgba(238, 241, 241, 1))
|
static let brand = color(light: rgba(18, 18, 18, 1), dark: rgba(9, 9, 9, 1))
|
||||||
static let onPrimary = color(light: rgba(255, 255, 255, 1), dark: rgba(12, 14, 14, 1))
|
static let primary = color(light: rgba(23, 23, 23, 1), dark: rgba(240, 240, 240, 1))
|
||||||
|
static let onPrimary = color(light: rgba(255, 255, 255, 1), dark: rgba(13, 13, 13, 1))
|
||||||
static let danger = color(light: rgba(193, 46, 40, 1), dark: rgba(247, 113, 105, 1))
|
static let danger = color(light: rgba(193, 46, 40, 1), dark: rgba(247, 113, 105, 1))
|
||||||
static let onDanger = color(light: rgba(255, 255, 255, 1), dark: rgba(24, 8, 7, 1))
|
static let onDanger = color(light: rgba(255, 255, 255, 1), dark: rgba(24, 8, 7, 1))
|
||||||
static let warning = color(light: rgba(170, 92, 15, 1), dark: rgba(233, 166, 60, 1))
|
static let warning = color(light: rgba(170, 92, 15, 1), dark: rgba(233, 166, 60, 1))
|
||||||
static let success = color(light: rgba(16, 122, 86, 1), dark: rgba(66, 199, 132, 1))
|
static let success = color(light: rgba(16, 122, 86, 1), dark: rgba(66, 199, 132, 1))
|
||||||
static let segmentTrack = color(light: rgba(234, 236, 236, 1), dark: rgba(26, 29, 29, 1))
|
static let segmentTrack = color(light: rgba(235, 235, 235, 1), dark: rgba(28, 28, 28, 1))
|
||||||
static let segmentSelected = color(light: rgba(255, 255, 255, 1), dark: rgba(54, 58, 59, 1))
|
static let segmentSelected = color(light: rgba(255, 255, 255, 1), dark: rgba(58, 58, 58, 1))
|
||||||
static let cardShadow = color(light: rgba(17, 22, 25, 0.06), dark: rgba(0, 0, 0, 0.60))
|
static let cardShadow = color(light: rgba(18, 18, 18, 0.06), dark: rgba(0, 0, 0, 0.60))
|
||||||
#elseif canImport(AppKit)
|
#elseif canImport(AppKit)
|
||||||
static let canvas = Color(red: 0.957, green: 0.961, blue: 0.961)
|
static let canvas = Color(red: 0.961, green: 0.961, blue: 0.961)
|
||||||
static let surface = Color.white
|
static let surface = Color.white
|
||||||
static let control = Color(red: 0.933, green: 0.937, blue: 0.937)
|
static let control = Color(red: 0.937, green: 0.937, blue: 0.937)
|
||||||
static let ink = Color(red: 0.067, green: 0.075, blue: 0.078)
|
static let ink = Color(red: 0.071, green: 0.071, blue: 0.071)
|
||||||
static let muted = Color(red: 0.345, green: 0.369, blue: 0.380)
|
static let muted = Color(red: 0.361, green: 0.361, blue: 0.361)
|
||||||
static let line = Color(red: 0.875, green: 0.886, blue: 0.886)
|
static let line = Color(red: 0.886, green: 0.886, blue: 0.886)
|
||||||
static let brand = Color(red: 0.071, green: 0.078, blue: 0.082)
|
static let brand = Color(red: 0.071, green: 0.071, blue: 0.071)
|
||||||
static let surfaceRaised = Color.white
|
static let surfaceRaised = Color.white
|
||||||
static let chrome = Color.white.opacity(0.86)
|
static let chrome = Color.white.opacity(0.86)
|
||||||
static let onPrimary = Color.white
|
static let onPrimary = Color.white
|
||||||
static let onDanger = Color.white
|
static let onDanger = Color.white
|
||||||
static let primary = Color(red: 0.082, green: 0.094, blue: 0.098)
|
static let primary = Color(red: 0.090, green: 0.090, blue: 0.090)
|
||||||
static let danger = Color(red: 0.757, green: 0.180, blue: 0.157)
|
static let danger = Color(red: 0.757, green: 0.180, blue: 0.157)
|
||||||
static let warning = Color(red: 0.667, green: 0.361, blue: 0.059)
|
static let warning = Color(red: 0.667, green: 0.361, blue: 0.059)
|
||||||
static let success = Color(red: 0.063, green: 0.478, blue: 0.337)
|
static let success = Color(red: 0.063, green: 0.478, blue: 0.337)
|
||||||
static let tile = Color(red: 0.965, green: 0.969, blue: 0.969)
|
static let tile = Color(red: 0.969, green: 0.969, blue: 0.969)
|
||||||
static let tileLine = Color(red: 0.906, green: 0.914, blue: 0.914)
|
static let tileLine = Color(red: 0.910, green: 0.910, blue: 0.910)
|
||||||
static let faint = Color(red: 0.494, green: 0.518, blue: 0.529)
|
static let faint = Color(red: 0.510, green: 0.510, blue: 0.510)
|
||||||
static let segmentTrack = Color(red: 0.918, green: 0.925, blue: 0.925)
|
static let segmentTrack = Color(red: 0.922, green: 0.922, blue: 0.922)
|
||||||
static let segmentSelected = Color.white
|
static let segmentSelected = Color.white
|
||||||
static let cardShadow = Color(red: 0.067, green: 0.086, blue: 0.098).opacity(0.06)
|
static let cardShadow = Color(red: 0.071, green: 0.071, blue: 0.071).opacity(0.06)
|
||||||
#else
|
#else
|
||||||
private static func color(_ red: CGFloat, _ green: CGFloat, _ blue: CGFloat) -> Color {
|
private static func color(_ red: CGFloat, _ green: CGFloat, _ blue: CGFloat) -> Color {
|
||||||
Color(red: red / 255, green: green / 255, blue: blue / 255)
|
Color(red: red / 255, green: green / 255, blue: blue / 255)
|
||||||
}
|
}
|
||||||
|
|
||||||
static let canvas = color(243, 245, 248)
|
static let canvas = color(245, 245, 245)
|
||||||
static let surface = color(255, 255, 255)
|
static let surface = color(255, 255, 255)
|
||||||
static let control = color(247, 249, 251)
|
static let control = color(239, 239, 239)
|
||||||
static let ink = color(28, 39, 56)
|
static let ink = color(18, 18, 18)
|
||||||
static let muted = color(111, 124, 143)
|
static let muted = color(92, 92, 92)
|
||||||
static let line = color(224, 229, 235)
|
static let line = color(226, 226, 226)
|
||||||
static let brand = color(19, 30, 48)
|
static let brand = color(18, 18, 18)
|
||||||
static let surfaceRaised = color(251, 252, 253)
|
static let surfaceRaised = color(255, 255, 255)
|
||||||
static let chrome = color(255, 255, 255, 0.82)
|
static let chrome = color(255, 255, 255, 0.82)
|
||||||
static let onPrimary = color(255, 255, 255)
|
static let onPrimary = color(255, 255, 255)
|
||||||
static let onDanger = color(255, 255, 255)
|
static let onDanger = color(255, 255, 255)
|
||||||
static let primary = color(17, 20, 25)
|
static let primary = color(23, 23, 23)
|
||||||
static let danger = color(190, 65, 57)
|
static let danger = color(190, 65, 57)
|
||||||
static let warning = color(170, 111, 23)
|
static let warning = color(170, 111, 23)
|
||||||
static let success = color(50, 150, 101)
|
static let success = color(50, 150, 101)
|
||||||
static let tile = color(247, 249, 252)
|
static let tile = color(247, 247, 247)
|
||||||
static let tileLine = color(233, 239, 247)
|
static let tileLine = color(232, 232, 232)
|
||||||
static let faint = color(141, 152, 170)
|
static let faint = color(130, 130, 130)
|
||||||
static let segmentTrack = color(236, 240, 246)
|
static let segmentTrack = color(235, 235, 235)
|
||||||
static let segmentSelected = color(255, 255, 255)
|
static let segmentSelected = color(255, 255, 255)
|
||||||
static let cardShadow = color(15, 30, 60)
|
static let cardShadow = color(18, 18, 18)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +126,9 @@ public enum AITheme {
|
|||||||
public static let cardShadow = AIPalette.cardShadow
|
public static let cardShadow = AIPalette.cardShadow
|
||||||
|
|
||||||
public static let buttonHeight: CGFloat = 48
|
public static let buttonHeight: CGFloat = 48
|
||||||
|
// 停用态主按钮保留一层主色淡底,避免和次级按钮长得一模一样,层级读不出来。
|
||||||
|
public static let disabledFillOpacity: Double = 0.14
|
||||||
|
public static let disabledStrokeOpacity: Double = 0.22
|
||||||
public static let compactButtonHeight: CGFloat = 44
|
public static let compactButtonHeight: CGFloat = 44
|
||||||
public static let largeButtonHeight: CGFloat = 56
|
public static let largeButtonHeight: CGFloat = 56
|
||||||
public static let toolbarButtonHeight: CGFloat = 44
|
public static let toolbarButtonHeight: CGFloat = 44
|
||||||
|
|||||||
@@ -77,7 +77,20 @@ final class AIThemeTests: XCTestCase {
|
|||||||
]
|
]
|
||||||
let disabledColors = disabledKinds.map { $0.backgroundColor(isEnabled: false) }
|
let disabledColors = disabledKinds.map { $0.backgroundColor(isEnabled: false) }
|
||||||
|
|
||||||
XCTAssertEqual(Set(disabledColors), [AITheme.control])
|
// 停用态仍然要保住按钮层级:主按钮留一层主色淡底,次级/危险退回中性 control。
|
||||||
|
XCTAssertEqual(disabledColors.count, 3)
|
||||||
|
XCTAssertEqual(disabledColors[1], AITheme.control)
|
||||||
|
XCTAssertEqual(disabledColors[2], AITheme.control)
|
||||||
|
XCTAssertTrue(disabledColors[0] != AITheme.control)
|
||||||
|
// 只有实心主色按钮需要"同色相淡填充"处理,灰底次按钮不能套,否则两者又会长一样。
|
||||||
|
XCTAssertEqual(
|
||||||
|
[
|
||||||
|
AIButtonStyle.Kind.primary,
|
||||||
|
.secondary,
|
||||||
|
.danger,
|
||||||
|
].map(\.usesTintedDisabledTreatment),
|
||||||
|
[true, false, false]
|
||||||
|
)
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
disabledKinds.map { $0.foregroundColor(isEnabled: false) },
|
disabledKinds.map { $0.foregroundColor(isEnabled: false) },
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ final class AppMetadataTests: XCTestCase {
|
|||||||
XCTAssertEqual(AppMetadata.name, "MultiSimAdmin")
|
XCTAssertEqual(AppMetadata.name, "MultiSimAdmin")
|
||||||
XCTAssertEqual(AppMetadata.bundleIdentifier, "fun.chick.multi-simadmin")
|
XCTAssertEqual(AppMetadata.bundleIdentifier, "fun.chick.multi-simadmin")
|
||||||
XCTAssertEqual(AppMetadata.marketingVersion, "0.2.2")
|
XCTAssertEqual(AppMetadata.marketingVersion, "0.2.2")
|
||||||
XCTAssertEqual(AppMetadata.buildVersion, "8")
|
XCTAssertEqual(AppMetadata.buildVersion, "9")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ final class SettingsViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertEqual(model.runtime?.appName, "MultiSimAdmin")
|
XCTAssertEqual(model.runtime?.appName, "MultiSimAdmin")
|
||||||
XCTAssertEqual(model.runtime?.marketingVersion, "0.2.2")
|
XCTAssertEqual(model.runtime?.marketingVersion, "0.2.2")
|
||||||
XCTAssertEqual(model.runtime?.buildVersion, "8")
|
XCTAssertEqual(model.runtime?.buildVersion, "9")
|
||||||
XCTAssertEqual(model.runtime?.platform, "iOS")
|
XCTAssertEqual(model.runtime?.platform, "iOS")
|
||||||
XCTAssertTrue(model.runtime?.systemVersion.hasPrefix("iOS") ?? false)
|
XCTAssertTrue(model.runtime?.systemVersion.hasPrefix("iOS") ?? false)
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,36 @@
|
|||||||
# iOS Native 0.2.2
|
# iOS Native 0.2.2
|
||||||
|
|
||||||
0.2.2 构建 8 把「分组」这个实体从应用里彻底删掉,归类只靠标签;同时完成第二轮配色与图标重构,
|
0.2.2 把「分组」这个实体从应用里彻底删掉,归类只靠标签;同时完成第二轮配色与图标重构,界面上
|
||||||
界面上不再出现蓝色。页面能力、上游白名单与本地数据布局见现行说明 [docs/ios-native.md](ios-native.md),
|
不再出现蓝色。构建 9 是一轮收尾打磨:修掉消息中心发不出去信的按钮遮挡,补齐按钮层级,统一缺数据
|
||||||
|
读数的写法,并把中性灰阶压成严格等色。页面能力、上游白名单与本地数据布局见现行说明
|
||||||
|
[docs/ios-native.md](ios-native.md),
|
||||||
本文只记录本次发布的变化。
|
本文只记录本次发布的变化。
|
||||||
|
|
||||||
## 本次变更
|
## 本次变更
|
||||||
|
|
||||||
|
- 中性灰阶改为严格 R=G=B:此前所有灰底的蓝通道比红通道高 3~10 级,浅色主题下大面积文字与描边
|
||||||
|
会蒙一层冷调,截图取色仍落在蓝色波段。现在画布、卡片、控件、描边、文字全部等色,画面里
|
||||||
|
只剩红(危险)、橙(告警)、绿(正常)三类状态色。
|
||||||
|
- 延迟量表改为可读量:详情页延迟进度条此前恒为满格,38ms 和 900ms 画出来一模一样。现在与节点
|
||||||
|
卡片同一算法(越短越满,1s 归零),一根条真的在说延迟好坏。
|
||||||
|
- 单位与占位写法统一:卡片里的延迟从 `218ms` 改成 `218 ms`,与概览、详情同一写法;设置页日志
|
||||||
|
计数与日志中心的 HTTP 状态占位也从 `--` 收敛到 `—`。
|
||||||
|
- 表单里成对的控件同尺寸:标签输入框高 52,右侧加号按钮只有 44,两个控件并排时明显一高一矮。
|
||||||
|
现在加号与输入框同高同圆角。
|
||||||
|
- 详情页身份信息对齐方式统一:头部摘要的值此前左对齐成一列,下方分区卡片的值右对齐,同一屏两种
|
||||||
|
排法读起来像两张不同的表。现在两处都靠右。
|
||||||
|
- 修复消息中心无法发信:发送入口原先挂在页面底部的操作条上,而底部标签栏是用 `safeAreaInset`
|
||||||
|
贴在根视图上的,两者叠在同一位置,操作条整条被标签栏盖住,「发送消息」在真机上根本点不到。
|
||||||
|
现在发送与刷新一起放进导航栏右侧,节点总览、设置页同一区域同一套做法。
|
||||||
|
- 主按钮禁用态重新分级:禁用前的主要按钮与普通按钮像素一致,表单里「取消」和「保存实例」分不出
|
||||||
|
主次。现在禁用的主要按钮改用主色低透明度底加同色描边,并去掉投影,看得见但明显弱于可用状态。
|
||||||
|
- 消息行的删除按钮降噪:原先每条消息都带一枚红色实心方块按钮,一屏十几个红块。现在改成一个
|
||||||
|
红色线性图标,保留 44pt 点击区域,只在需要时提示危险语义。
|
||||||
|
- 缺数据读数统一为一根短横:概览与卡片读数此前混用「不可用」和 `--`,四列窄格里既容易被读成
|
||||||
|
故障结论,字号还会被压缩。现在一律用 `—`,并按占位弱色渲染。
|
||||||
|
- 应用图标金色再收一档:底部由深古铜提亮到香槟金,整块金属在高亮壁纸上不再往下沉成棕黄。
|
||||||
|
石墨底同步去掉 1 级的蓝通道偏差,1024 与 120 两档取色都不再落在蓝色波段。
|
||||||
|
|
||||||
- 分组概念移除:删除分组视图、分组编辑器与分组仓储,实例模型不再有 `groupId`。升级后首次启动
|
- 分组概念移除:删除分组视图、分组编辑器与分组仓储,实例模型不再有 `groupId`。升级后首次启动
|
||||||
一次性删除 `Documents/instance-groups.json` 与偏好键 `fleet.groupMode`,本机不留死数据。
|
一次性删除 `Documents/instance-groups.json` 与偏好键 `fleet.groupMode`,本机不留死数据。
|
||||||
- 标签接管归类:新增「批量标签」表,批量选择后一次给多台节点贴同一个标签;每个标签显示
|
- 标签接管归类:新增「批量标签」表,批量选择后一次给多台节点贴同一个标签;每个标签显示
|
||||||
@@ -65,10 +90,10 @@
|
|||||||
- `build/ipa/MultiSimAdmin.ipa`
|
- `build/ipa/MultiSimAdmin.ipa`
|
||||||
- `build/ipa/MultiSimAdmin.ipa.sha256`
|
- `build/ipa/MultiSimAdmin.ipa.sha256`
|
||||||
|
|
||||||
0.2.2 构建 8 校验值:
|
0.2.2 构建 9 校验值:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
a40e8e308f667edfe69902d5665a9ad85bdb52f72b5a248a671b6eef3591d847 MultiSimAdmin.ipa
|
a0b90a3754bd5cb8d9b95906eb711b7d140d603b6f821ee838edb273e62e3d75 MultiSimAdmin.ipa
|
||||||
```
|
```
|
||||||
|
|
||||||
IPA 目标为 iOS 16.0 及以上,`arm64`,Bundle ID 为 `fun.chick.multi-simadmin`,未签名,
|
IPA 目标为 iOS 16.0 及以上,`arm64`,Bundle ID 为 `fun.chick.multi-simadmin`,未签名,
|
||||||
|
|||||||
+6
-5
@@ -3,7 +3,7 @@
|
|||||||
本文是 iPhone 应用的唯一现行说明,内容与仓库代码一致。历史设计稿和执行计划保留在
|
本文是 iPhone 应用的唯一现行说明,内容与仓库代码一致。历史设计稿和执行计划保留在
|
||||||
`docs/superpowers/` 下,其中已被实现推翻的决策在本文第 9 节逐条列出。
|
`docs/superpowers/` 下,其中已被实现推翻的决策在本文第 9 节逐条列出。
|
||||||
|
|
||||||
- 当前版本:`0.2.2`,构建号 8
|
- 当前版本:`0.2.2`,构建号 9
|
||||||
- 承载分支:`ios-native-ipa`
|
- 承载分支:`ios-native-ipa`
|
||||||
- 运行要求:iOS 16.0 及以上,`arm64`,Bundle ID `fun.chick.multi-simadmin`
|
- 运行要求:iOS 16.0 及以上,`arm64`,Bundle ID `fun.chick.multi-simadmin`
|
||||||
- 产物:未签名 `MultiSimAdmin.ipa`,由 TrollStore 或用户自有签名流程安装
|
- 产物:未签名 `MultiSimAdmin.ipa`,由 TrollStore 或用户自有签名流程安装
|
||||||
@@ -34,8 +34,8 @@ Node 控制面仍然独立存在,两者只共享上游接口契约。
|
|||||||
|
|
||||||
- 运行概览:在线率与异常节点两枚大字结论、状态分布条与图例、八格两行的群集读数(信号、延迟、
|
- 运行概览:在线率与异常节点两枚大字结论、状态分布条与图例、八格两行的群集读数(信号、延迟、
|
||||||
存储、流量、CPU、内存、温度、电量),可折叠,折叠时保留前四格。检测进度与更新时间写在卡片
|
存储、流量、CPU、内存、温度、电量),可折叠,折叠时保留前四格。检测进度与更新时间写在卡片
|
||||||
副标题里(`刚刚更新 · 已检测 6/6 台`),不占读数格;整群节点没有上报的读数保留「不可用」占位,
|
副标题里(`刚刚更新 · 已检测 6/6 台`),不占读数格;整群节点没有上报的读数保留占位,统一写成
|
||||||
栅格不会在七格与九格之间跳动。
|
一根短横 `—`,避免在四列窄格里被读成故障结论;栅格不会在七格与九格之间跳动。
|
||||||
- 标签归类:标签是唯一归类手段,分组概念已在 0.2.2 移除。标签条支持多选叠加,胶囊上直接显示
|
- 标签归类:标签是唯一归类手段,分组概念已在 0.2.2 移除。标签条支持多选叠加,胶囊上直接显示
|
||||||
命中台数,可单个取消或全部清除;卡片内的标签同样可点,点一下即加入筛选。
|
命中台数,可单个取消或全部清除;卡片内的标签同样可点,点一下即加入筛选。
|
||||||
- 批量标签:批量选择后一次给多台节点贴同一个标签,每个标签显示「未贴 / N/M / 全部」三态覆盖度,
|
- 批量标签:批量选择后一次给多台节点贴同一个标签,每个标签显示「未贴 / N/M / 全部」三态覆盖度,
|
||||||
@@ -64,7 +64,8 @@ Node 控制面仍然独立存在,两者只共享上游接口契约。
|
|||||||
|
|
||||||
- 列表按时间排序,显示来源实例、方向、号码、正文与相对时间。
|
- 列表按时间排序,显示来源实例、方向、号码、正文与相对时间。
|
||||||
- 支持按实例筛选、按上行/下行筛选、按号码或内容搜索。
|
- 支持按实例筛选、按上行/下行筛选、按号码或内容搜索。
|
||||||
- 发送写入目标实例;失败可重试;单条删除走该实例的上游接口。
|
- 发送与刷新一起放在导航栏右侧,页脚不再另挂操作条;发送写入目标实例,失败可重试;
|
||||||
|
单条删除走该实例的上游接口,行内只留一枚红色线性图标。
|
||||||
|
|
||||||
### 日志中心与设置
|
### 日志中心与设置
|
||||||
|
|
||||||
@@ -197,7 +198,7 @@ xcodebuild -scheme MultiSimAdmin \
|
|||||||
## 11. 文档索引
|
## 11. 文档索引
|
||||||
|
|
||||||
- [docs/ios-native.md](ios-native.md):本文,现行说明
|
- [docs/ios-native.md](ios-native.md):本文,现行说明
|
||||||
- [docs/ios-native-0.2.2.md](ios-native-0.2.2.md):0.2.2 构建 8 发布说明与校验值
|
- [docs/ios-native-0.2.2.md](ios-native-0.2.2.md):0.2.2 构建 9 发布说明与校验值
|
||||||
- [docs/ios-native-0.2.1.md](ios-native-0.2.1.md):0.2.1 构建 6 发布说明与校验值
|
- [docs/ios-native-0.2.1.md](ios-native-0.2.1.md):0.2.1 构建 6 发布说明与校验值
|
||||||
- [docs/ios-native-0.2.0.md](ios-native-0.2.0.md):0.2.0 发布说明
|
- [docs/ios-native-0.2.0.md](ios-native-0.2.0.md):0.2.0 发布说明
|
||||||
- [docs/superpowers/specs/2026-09-08-ios-native-ipa-design.md](superpowers/specs/2026-09-08-ios-native-ipa-design.md):历史设计稿
|
- [docs/superpowers/specs/2026-09-08-ios-native-ipa-design.md](superpowers/specs/2026-09-08-ios-native-ipa-design.md):历史设计稿
|
||||||
|
|||||||
Reference in New Issue
Block a user