- 账号与同步:GitHub / Google / Apple 登录后把菜单栏项目与风格等偏好同步到 getopenstats.com,换一台 Mac 登录即恢复(AccountSync 模块、SyncController、SettingsDocument、账号设置页,openstats:// 回调) - 磁盘页成为工具页:已用 / 可清除 / 可用分段容量条、空间分析、文件系统检查、本地快照清理(辅助工具协议 v4)、其他磁盘推出 - 公网 IP:归属地数据源可选 CleanIP.io / ipapi.is / DB-IP / ipinfo.io / 本地 GeoLite2,每台 Mac 自己查、本地缓存 7 天;IPv4 / IPv6 双栈各自查询与切换;弹窗显示 ASN、运营方、原生 / 住宅等徽章与纯净度评分色带;接口区块可重置上传 / 下载统计 - 弹窗:右上角按钮改为该指标自己的图标 + 总设置;主窗口页面标题栏的开关点不动已修复;滚动条改为悬浮式,内容不再偏左;网络弹窗卡片被标题行撑宽的问题已修复 - 设置:⌘ 拖动排序说明;每个菜单栏项目单独选风格并带实时预览;网络与磁盘的进程榜单固定 5 行并平滑排序;CPU 持续高负载通知 - 设计系统:分段条、评分色带、标签徽章、胶囊切换、刷新按钮等组件与徽章配色 token - 更新日志、架构说明、README 与译文同步
221 lines
8.6 KiB
Swift
221 lines
8.6 KiB
Swift
import AppKit
|
||
import SwiftUI
|
||
|
||
/// 设计 token。界面里的颜色、字号、间距、圆角一律从这里取值。
|
||
public enum DS {}
|
||
|
||
// MARK: - 颜色
|
||
|
||
extension DS {
|
||
enum Palette {
|
||
// 品牌色(单个视图内不超过 3 种)
|
||
static let primary = Color.dynamic(light: 0x2563EB, dark: 0x3B82F6)
|
||
static let primaryHover = Color.dynamic(light: 0x1D4ED8, dark: 0x2563EB) // 加深约 10%
|
||
static let secondary = Color.dynamic(light: 0x0D9488, dark: 0x2DD4BF)
|
||
|
||
// 语义色
|
||
static let success = Color.dynamic(light: 0x16A34A, dark: 0x22C55E)
|
||
static let warning = Color.dynamic(light: 0xD97706, dark: 0xF59E0B)
|
||
static let error = Color.dynamic(light: 0xDC2626, dark: 0xF87171)
|
||
|
||
// 文字
|
||
static let textPrimary = Color.dynamic(light: 0x111827, dark: 0xF3F4F6)
|
||
static let textSecondary = Color.dynamic(light: 0x6B7280, dark: 0x9CA3AF)
|
||
static let textTertiary = Color.dynamic(light: 0x9CA3AF, dark: 0x6B7280)
|
||
static let onPrimary = Color.dynamic(light: 0xFFFFFF, dark: 0xFFFFFF)
|
||
|
||
// OpenStats 的统一配色:浅色为白底蓝色,深色为黑底蓝色。
|
||
// 窗口、侧边栏、标题栏与弹窗共用同一底色,卡片用浅灰 / 深灰实色区分层级,不加边框
|
||
static let background = Color(nsColor: NSColor.dsBackground)
|
||
static let surface = Color.dynamic(light: 0xF3F4F6, dark: 0x17181B)
|
||
/// 卡片上的控件底色(分段选中块、次要按钮、输入框)
|
||
static let elevated = Color.dynamic(light: 0xFFFFFF, dark: 0x26282D)
|
||
static let surfaceHover = Color.dynamic(light: 0x000000, lightAlpha: 0.05, dark: 0xFFFFFF, darkAlpha: 0.07)
|
||
static let border = Color.dynamic(light: 0x000000, lightAlpha: 0.07, dark: 0xFFFFFF, darkAlpha: 0.09)
|
||
static let track = Color.dynamic(light: 0x000000, lightAlpha: 0.06, dark: 0xFFFFFF, darkAlpha: 0.10)
|
||
static let neutral300 = Color.dynamic(light: 0xD1D5DB, dark: 0x3F434A)
|
||
/// 分段条里表示“剩余 / 空闲”的中性段,深到能衬白字
|
||
static let neutral500 = Color.dynamic(light: 0x6B7280, dark: 0x4B5563)
|
||
/// 浅一档的品牌蓝,用来区分同一图表里的第二类数据(如性能核)
|
||
static let primarySoft = Color.dynamic(light: 0x93C5FD, dark: 0x60A5FA)
|
||
|
||
/// 核心类型配色:最高性能档为品牌蓝,其次浅蓝,能效核灰色
|
||
static func cluster(_ id: Int) -> Color {
|
||
switch id {
|
||
case 0: primary
|
||
case 1: primarySoft
|
||
default: neutral300
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
extension DS {
|
||
/// 网速配色:上传绿、下载蓝,面板与菜单栏保持一致
|
||
enum NetworkPalette {
|
||
static let upload = NSColor.dynamic(light: 0x16A34A, dark: 0x22C55E)
|
||
static let download = NSColor.dynamic(light: 0x2563EB, dark: 0x3B82F6)
|
||
}
|
||
}
|
||
|
||
extension Color {
|
||
static func dynamic(light: UInt32, lightAlpha: CGFloat = 1, dark: UInt32, darkAlpha: CGFloat = 1) -> Color {
|
||
Color(nsColor: .dynamic(light: light, lightAlpha: lightAlpha, dark: dark, darkAlpha: darkAlpha))
|
||
}
|
||
}
|
||
|
||
extension NSColor {
|
||
/// 窗口底色,AppKit 窗口背景与 SwiftUI 页面共用
|
||
static let dsBackground = NSColor.dynamic(light: 0xFFFFFF, dark: 0x0B0B0D)
|
||
|
||
static func dynamic(light: UInt32, lightAlpha: CGFloat = 1, dark: UInt32, darkAlpha: CGFloat = 1) -> NSColor {
|
||
NSColor(name: nil) { appearance in
|
||
let isDark = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua
|
||
return NSColor(hex: isDark ? dark : light, alpha: isDark ? darkAlpha : lightAlpha)
|
||
}
|
||
}
|
||
}
|
||
|
||
extension NSColor {
|
||
convenience init(hex: UInt32, alpha: CGFloat = 1) {
|
||
self.init(srgbRed: CGFloat((hex >> 16) & 0xFF) / 255,
|
||
green: CGFloat((hex >> 8) & 0xFF) / 255,
|
||
blue: CGFloat(hex & 0xFF) / 255,
|
||
alpha: alpha)
|
||
}
|
||
}
|
||
|
||
// MARK: - 字号 / 间距 / 圆角 / 尺寸
|
||
|
||
extension DS {
|
||
enum TextSize: CGFloat {
|
||
case xs = 12, sm = 14, base = 16, lg = 20, xl = 24, xxl = 32
|
||
}
|
||
|
||
/// 徽章配色:浅底、细边、深字,与 cleanip.io 的徽章一致
|
||
enum Badge {
|
||
static let successText = Color.dynamic(light: 0x15803D, dark: 0x75D99B)
|
||
static let successFill = Color.dynamic(light: 0xF0FDF4, dark: 0x142F24)
|
||
static let successBorder = Color.dynamic(light: 0xBBF7D0, dark: 0x29543C)
|
||
static let warningText = Color.dynamic(light: 0xB45309, dark: 0xF3C66C)
|
||
static let warningFill = Color.dynamic(light: 0xFFFBEB, dark: 0x342B1B)
|
||
static let warningBorder = Color.dynamic(light: 0xFDE68A, dark: 0x625030)
|
||
static let neutralText = Palette.textSecondary
|
||
static let neutralFill = Palette.elevated
|
||
static let neutralBorder = Palette.border
|
||
}
|
||
|
||
/// 0–100 分的六档色阶(F / D / C / B / A / A+),只用于评分类图表,不是界面配色
|
||
enum Grade {
|
||
static let f = Palette.error
|
||
static let d = Palette.warning
|
||
static let c = Color.dynamic(light: 0xEAB308, dark: 0xFACC15)
|
||
static let b = Color.dynamic(light: 0x84CC16, dark: 0xA3E635)
|
||
static let a = Palette.success
|
||
static let aPlus = Color.dynamic(light: 0x15803D, dark: 0x16A34A)
|
||
|
||
/// 分段上限与颜色,从低到高
|
||
static let bands: [(grade: String, upper: Int, color: Color)] = [
|
||
("F", 25, f), ("D", 50, d), ("C", 70, c), ("B", 85, b), ("A", 95, a), ("A+", 100, aPlus),
|
||
]
|
||
|
||
static func color(for score: Int) -> Color {
|
||
bands.first { score < $0.upper }?.color ?? aPlus
|
||
}
|
||
}
|
||
|
||
enum Space {
|
||
static let s1: CGFloat = 4
|
||
static let s2: CGFloat = 8
|
||
static let s3: CGFloat = 12
|
||
static let s4: CGFloat = 16
|
||
static let s6: CGFloat = 24
|
||
static let s8: CGFloat = 32
|
||
static let s12: CGFloat = 48
|
||
static let s16: CGFloat = 64
|
||
}
|
||
|
||
/// 层级:控件 < 卡片 < 面板/窗口
|
||
enum Radius {
|
||
static let sm: CGFloat = 4
|
||
static let md: CGFloat = 8
|
||
static let lg: CGFloat = 12
|
||
static let xl: CGFloat = 16 // 弹窗
|
||
}
|
||
|
||
enum Size {
|
||
static let iconInline: CGFloat = 16
|
||
static let iconStandalone: CGFloat = 20
|
||
static let controlHeight: CGFloat = 32
|
||
static let segmentHeight: CGFloat = 24
|
||
static let switchWidth: CGFloat = 40
|
||
static let switchHeight: CGFloat = 24
|
||
static let switchKnob: CGFloat = 16
|
||
static let barHeight: CGFloat = 8
|
||
static let coreBarHeight: CGFloat = 32
|
||
static let chartHeight: CGFloat = 48
|
||
static let tileChart: CGFloat = 40
|
||
static let stroke: CGFloat = 1
|
||
static let chartLine: CGFloat = 1.5
|
||
static let panelWidth: CGFloat = 640
|
||
/// 菜单栏单项详情弹窗的宽度
|
||
static let popoverWidth: CGFloat = 320
|
||
static let panelMinHeight: CGFloat = 320
|
||
static let panelGap: CGFloat = 4
|
||
/// 窗口顶栏高度,与紧凑工具栏的标题栏同高,红绿灯按钮与页面标题在同一水平线上
|
||
static let windowHeader: CGFloat = 40
|
||
/// 主窗口首次打开的最小高度(屏幕放得下时)
|
||
static let windowDefaultHeight: CGFloat = 720
|
||
/// 主窗口默认内容区宽度(侧边栏另计)
|
||
static let windowDefaultContentWidth: CGFloat = 880
|
||
static let sidebarWidth: CGFloat = 192
|
||
/// 升级提示窗口
|
||
static let updateWindowWidth: CGFloat = 480
|
||
static let updateNotesHeight: CGFloat = 192
|
||
static let valueColumn: CGFloat = 64
|
||
static let labelColumn: CGFloat = 48
|
||
}
|
||
|
||
enum Shadow {
|
||
struct Level {
|
||
let color: Color
|
||
let radius: CGFloat
|
||
let y: CGFloat
|
||
}
|
||
|
||
static let level1 = Level(color: .black.opacity(0.08), radius: 3, y: 1)
|
||
}
|
||
|
||
enum Motion {
|
||
static let quick = Animation.easeOut(duration: 0.15)
|
||
}
|
||
|
||
/// 温度阈值(°C),用于着色
|
||
enum Thermal {
|
||
static let warm: Double = 80
|
||
static let hot: Double = 95
|
||
static let scaleMax: Double = 110
|
||
}
|
||
}
|
||
|
||
// MARK: - 修饰器
|
||
|
||
extension View {
|
||
func dsFont(_ size: DS.TextSize, weight: Font.Weight = .regular) -> some View {
|
||
font(.system(size: size.rawValue, weight: weight))
|
||
}
|
||
|
||
}
|
||
|
||
extension Text {
|
||
func dsFont(_ size: DS.TextSize, weight: Font.Weight = .regular) -> Text {
|
||
font(.system(size: size.rawValue, weight: weight))
|
||
}
|
||
}
|
||
|
||
extension View {
|
||
func dsShadow(_ level: DS.Shadow.Level) -> some View {
|
||
shadow(color: level.color, radius: level.radius, x: 0, y: level.y)
|
||
}
|
||
}
|