release(ios): 0.2.2 移除分组改用标签归类并完成石墨配色重构

- 删除分组实体:分组视图/编辑器/仓储与 groupId 全部移除,升级首启清理
  instance-groups.json 与 fleet.groupMode
- 新增批量标签表:三态覆盖度(未贴 / N/M / 全部),整批加减与新建标签
- 配色重构,全应用零蓝色:石墨墨色承担主操作与选中态,颜色只留给状态语义;
  浅色改中性冷灰,深色改低明度石墨三层
- 选中态改实心主色 + onPrimary,替换不可读的低透明度色雾(主题块、图例胶囊)
- 应用图标重做为扁平石墨底 + 黄铜 SIM 面板,参数化光栅化脚本与参考稿
- 运行概览:检测进度并入副标题,读数栅格固定八格两行并保留不可用占位
- 实例详情:仪表改两列发丝线栅格,缺数据留轨道,修掉头部高度回弹;
  补齐设备名称/存储总量/可用存储/使用率四个字段名
- 版本号显示去掉括号,设置页只显示 0.2.2
- 文档:新增 docs/ios-native-0.2.2.md,同步 ios-native.md 与两处 README
This commit is contained in:
chick
2026-09-15 02:23:24 +08:00
parent ec3ffb9db3
commit eb880e0d14
28 changed files with 757 additions and 1524 deletions
+47 -89
View File
@@ -1,11 +1,10 @@
// Renders the 1024x1024 MultiSimAdmin app icon with CoreGraphics only.
// Usage: swift Design/generate_app_icon.swift <output.png>
//
// The mark is the same geometry as the web favicon: a gold SIM plate carrying
// three ascending signal bars. One flat silhouette, one light source, and no
// photographic detail, so it still reads at the 29pt settings size. The plate
// keeps the classic SIM chamfer, moved to the top-left so the tallest bar can
// sit under the intact top-right corner.
// The mark is the web favicon grown up: a flat brass SIM plate with three
// ascending slots cut out of it, on a graphite field. Two colours, one light
// source from the upper left, no metal texture, so it still reads at the 29pt
// settings size. The plate keeps the classic single SIM cut, top-left.
//
// Every coordinate is in visual space (origin top-left, y grows down).
import AppKit
@@ -87,15 +86,15 @@ func roundedPath(_ rect: CGRect, _ radius: CGFloat) -> CGPath {
CGPath(roundedRect: rect, cornerWidth: radius, cornerHeight: radius, transform: nil)
}
/// SIM plate outline: rounded square with one cut top corner.
func platePath(_ rect: CGRect, corner: CGFloat, chamfer: CGFloat) -> CGPath {
let bevel = corner * 0.28
/// SIM plate outline: rounded square with the top-left corner cut at 45 degrees.
func simPath(_ rect: CGRect, corner: CGFloat, chamfer: CGFloat) -> CGPath {
let bevel = corner * 0.26
let vertices: [(point: CGPoint, radius: CGFloat)] = [
(CGPoint(x: rect.minX, y: rect.minY + chamfer), bevel),
(CGPoint(x: rect.minX + chamfer, y: rect.minY), bevel),
(CGPoint(x: rect.maxX, y: rect.minY), corner),
(CGPoint(x: rect.maxX, y: rect.maxY), corner),
(CGPoint(x: rect.minX, y: rect.maxY), corner),
(CGPoint(x: rect.minX, y: rect.minY + chamfer), bevel),
]
let path = CGMutablePath()
path.move(to: vertices[0].point)
@@ -110,7 +109,7 @@ func platePath(_ rect: CGRect, corner: CGFloat, chamfer: CGFloat) -> CGPath {
return path
}
/// Bright along the upper-left edges, dead by the lower-right: an even outline
/// Bright along the upper-left edges, gone by the lower-right: an even outline
/// reads like a sticker cut rather than a plate lit from above.
func directionalRim(_ path: CGPath, _ rect: CGRect, _ width: CGFloat, _ stops: [Stop]) {
ctx.saveGState()
@@ -130,38 +129,34 @@ func directionalRim(_ path: CGPath, _ rect: CGRect, _ width: CGFloat, _ stops: [
fillLinear(
rectPath(CGRect(x: 0, y: 0, width: side, height: side)),
[
(0.00, (28, 66, 128, 255)),
(0.46, (13, 36, 80, 255)),
(1.00, (4, 11, 27, 255)),
(0.00, (26, 31, 30, 255)),
(0.55, (17, 21, 20, 255)),
(1.00, (11, 14, 13, 255)),
] as [Stop],
CGPoint(x: side * 0.10, y: 0),
CGPoint(x: side * 0.92, y: side)
CGPoint(x: side * 0.06, y: 0),
CGPoint(x: side * 0.94, y: side)
)
fillRadial(
CGPoint(x: side * 0.26, y: -side * 0.04), CGFloat(side * 0.92),
[(0.0, (150, 196, 255, 64)), (0.55, (112, 162, 230, 20)), (1.0, (112, 162, 230, 0))] as [Stop]
)
fillRadial(
CGPoint(x: side * 0.5, y: side * 0.52), CGFloat(side * 0.78),
[(0.55, (2, 7, 18, 0)), (1.0, (2, 7, 18, 150))] as [Stop]
CGPoint(x: side * 0.22, y: -side * 0.10), CGFloat(side * 0.86),
[(0.0, (214, 226, 218, 26)), (0.60, (214, 226, 218, 7)), (1.0, (214, 226, 218, 0))] as [Stop]
)
// MARK: - Plate
let plateSide = envDouble("ICON_PLATE_SIDE", 620)
let plateCenterY = envDouble("ICON_PLATE_Y", 528)
let plateCorner = envDouble("ICON_PLATE_CORNER", 134)
let plateChamfer = envDouble("ICON_PLATE_CHAMFER", 202)
let plateSide = envDouble("ICON_PLATE_SIDE", 592)
let plateCenterY = envDouble("ICON_PLATE_Y", 500)
let plateCorner = envDouble("ICON_PLATE_CORNER", 122)
let plateChamfer = envDouble("ICON_PLATE_CHAMFER", 168)
let plateRect = CGRect(
x: (side - plateSide) / 2,
y: plateCenterY - plateSide / 2,
width: plateSide,
height: plateSide
)
let plate = platePath(plateRect, corner: plateCorner, chamfer: plateChamfer)
let plate = simPath(plateRect, corner: plateCorner, chamfer: plateChamfer)
ctx.saveGState()
ctx.setShadow(offset: CGSize(width: 0, height: -30), blur: 62, color: rgb(2, 8, 20, 168))
ctx.setShadow(offset: CGSize(width: 0, height: -22), blur: 46, color: rgb(0, 3, 2, 150))
ctx.addPath(plate)
ctx.setFillColor(rgb(0, 0, 0))
ctx.fillPath()
@@ -170,98 +165,61 @@ ctx.restoreGState()
fillLinear(
plate,
[
(0.00, (255, 244, 205, 255)),
(0.30, (250, 206, 106, 255)),
(0.62, (214, 150, 40, 255)),
(1.00, (146, 92, 16, 255)),
(0.00, (248, 213, 124, 255)),
(0.50, (236, 189, 86, 255)),
(1.00, (212, 158, 54, 255)),
] as [Stop],
CGPoint(x: plateRect.minX, y: plateRect.minY),
CGPoint(x: plateRect.maxX, y: plateRect.maxY)
)
directionalRim(
plate, plateRect, plateSide * 0.016,
plate, plateRect, plateSide * 0.007,
[
(0.00, (255, 251, 232, 230)),
(0.40, (255, 240, 190, 66)),
(0.74, (255, 232, 170, 0)),
(0.00, (255, 244, 214, 132)),
(0.36, (255, 238, 198, 30)),
(0.68, (255, 234, 186, 0)),
] as [Stop]
)
ctx.saveGState()
ctx.addPath(plate)
ctx.clip()
ctx.move(to: CGPoint(x: plateRect.minX - 40, y: plateRect.minY + plateSide * 0.52))
ctx.addLine(to: CGPoint(x: plateRect.maxX + 40, y: plateRect.minY + plateSide * 0.10))
ctx.addLine(to: CGPoint(x: plateRect.maxX + 40, y: plateRect.minY + plateSide * 0.22))
ctx.addLine(to: CGPoint(x: plateRect.minX - 40, y: plateRect.minY + plateSide * 0.64))
ctx.closePath()
ctx.setFillColor(rgb(255, 252, 236, 5))
ctx.fillPath()
ctx.restoreGState()
// MARK: - Slots
// MARK: - Signal bars
let slotWidth = envDouble("ICON_SLOT_W", 92)
let slotGap = envDouble("ICON_SLOT_GAP", 54)
let slotBottom = envDouble("ICON_SLOT_BOTTOM", 640)
let slotHeights: [CGFloat] = [136, 232, 328]
let slotsWidth = CGFloat(slotHeights.count) * slotWidth + CGFloat(slotHeights.count - 1) * slotGap
let slotsStartX = (side - slotsWidth) / 2
let barWidth = envDouble("ICON_BAR_W", 88)
let barGap = envDouble("ICON_BAR_GAP", 62)
let barBottom = envDouble("ICON_BAR_BOTTOM", 688)
let barHeights: [CGFloat] = [142, 242, 342]
let barsWidth = CGFloat(barHeights.count) * barWidth + CGFloat(barHeights.count - 1) * barGap
let barsStartX = (side - barsWidth) / 2
for (index, height) in barHeights.enumerated() {
for (index, height) in slotHeights.enumerated() {
let rect = CGRect(
x: barsStartX + CGFloat(index) * (barWidth + barGap),
y: barBottom - height,
width: barWidth,
x: slotsStartX + CGFloat(index) * (slotWidth + slotGap),
y: slotBottom - height,
width: slotWidth,
height: height
)
let radius = barWidth * 0.30
let radius = slotWidth * 0.30
let path = roundedPath(rect, radius)
ctx.saveGState()
ctx.setShadow(offset: CGSize(width: 0, height: 5), blur: 16, color: rgb(255, 236, 176, 104))
ctx.addPath(path)
ctx.setFillColor(rgb(0, 0, 0))
ctx.fillPath()
ctx.restoreGState()
fillLinear(
path,
[
(0.00, (5, 13, 30, 255)),
(0.58, (9, 23, 50, 255)),
(1.00, (17, 38, 74, 255)),
(0.00, (15, 19, 18, 255)),
(1.00, (29, 35, 33, 255)),
] as [Stop],
CGPoint(x: rect.minX, y: rect.minY),
CGPoint(x: rect.minX, y: rect.maxY)
)
directionalRim(
path, rect, radius * 0.34,
path, rect, radius * 0.30,
[
(0.00, (0, 0, 0, 120)),
(0.48, (0, 0, 0, 26)),
(0.84, (255, 226, 152, 70)),
(1.00, (255, 236, 176, 118)),
(0.00, (120, 84, 18, 96)),
(0.52, (120, 84, 18, 18)),
(1.00, (255, 232, 176, 58)),
] as [Stop]
)
}
// MARK: - Surface sheen
ctx.saveGState()
ctx.addPath(rectPath(CGRect(x: 0, y: 0, width: side, height: side)))
ctx.clip()
ctx.move(to: CGPoint(x: -120, y: 430))
ctx.addLine(to: CGPoint(x: 1140, y: 40))
ctx.addLine(to: CGPoint(x: 1140, y: 214))
ctx.addLine(to: CGPoint(x: -120, y: 604))
ctx.closePath()
ctx.setFillColor(rgb(255, 255, 255, 4))
ctx.fillPath()
ctx.restoreGState()
guard let image = ctx.makeImage() else {
FileHandle.standardError.write("cannot finalize image\n".data(using: .utf8)!)
exit(1)