docs: README 跟上授权延长与通道组,截图和动图改为一条命令重新生成

README
- 申请延长写清规则:授权有效期间也可申请,有效的从原到期时间顺延、已结束的从批准时起算,
  被撤销的不能延长;批准延长时通道会开到授权结束(与最长开启时长的关系)
- 修正通道组「一起开关」与「可以单独开关」的矛盾:闲置按整组计算,每个地址仍可单独开关
- 时长选项用界面上的「通道开启期间」;快速开始按实际流程写(保存后自动弹出邀请设置)
- 新增同事端申请延长的三张截图;流程图补上 30 分钟、自定义与 2 小时内可申请延长

截图与动图
- 授权管理、编辑通道、邀请文案、授权成功页、网页面板(含通道组)更新为当前界面
- 离屏渲染时控件按激活状态绘制,开关不再显示成灰色
- 新增 make readme-images:App 离屏渲染、动图合成、授权页与网页面板的无头 Chrome 截图一次完成,
  写入 docs/images;授权页 HTML 由服务端测试用模拟状态生成,网页面板用本机模拟 API

界面
- 计时文字第二个单位补零(27分05秒),卡片上的徽标不再随秒数左右跳动;App 与网页面板一致
- 待审批与延长申请的时间格式统一为不带秒;App 预览的分享文案补上延长提示
This commit is contained in:
DeepFal committed 2026-10-14 14:51:07 +08:00
1 parent 6f2a1472b6
commit 3272e66247
26 files changed
+392 -37

No files matched your search

+8 -7
View File
@@ -205,6 +205,8 @@ func makeDemoVM(tick: Int = 0) -> PortRelayViewModel {
final class KeyWindow: NSWindow {
override var isKeyWindow: Bool { true }
override var isMainWindow: Bool { true }
/// 渲染进程不会被激活,AppKit 控件(开关、分段控件)按这个私有属性决定是否画成灰色
@objc(hasKeyAppearance) func alwaysHasKeyAppearance() -> Bool { true }
}
@MainActor
@@ -256,12 +258,11 @@ MainActor.assumeIsolated {
let demo = makeDemoVM()
render("readme-main-light", CGSize(width: 980, height: 1180), ContentView(vm: demo))
render("readme-main-dark", CGSize(width: 980, height: 1180), dark: true, ContentView(vm: demo))
render("readme-invite", CGSize(width: 560, height: 250), InviteSheet(vm: demo, tunnel: demo.tunnels[0], autoGenerate: true, isPresented: yes))
render("readme-invite", CGSize(width: 600, height: 270), InviteSheet(vm: demo, tunnel: demo.tunnels[0], autoGenerate: true, isPresented: yes))
render("readme-clients", CGSize(width: 760, height: 560), ClientDetailsSheet(vm: demo, tunnel: demo.tunnels[0], initialTab: 0, isPresented: yes))
render("readme-access", CGSize(width: 760, height: 490), ClientDetailsSheet(vm: demo, tunnel: demo.tunnels[0], initialTab: 2, isPresented: yes))
render("readme-edit", CGSize(width: 520, height: 760), TunnelEditSheet(vm: demo, tunnel: demo.tunnels[0], isPresented: yes))
render("readme-edit", CGSize(width: 520, height: 570), TunnelEditSheet(vm: demo, tunnel: demo.tunnels[0], isPresented: yes))
let g = demo.groups[0]
render("group-card", CGSize(width: 980, height: 330), GroupCardView(group: g, members: demo.members(of: g), visible: demo.members(of: g), vm: demo,
render("readme-group", CGSize(width: 980, height: 330), GroupCardView(group: g, members: demo.members(of: g), visible: demo.members(of: g), vm: demo,
onEdit: {}, onAddMember: {}, onInvite: {}, onViewClients: { _ in }, onEditMember: { _ in }, onInviteMember: { _ in }, onMemberClients: { _ in }).padding(20))
render("group-card-dark", CGSize(width: 980, height: 330), dark: true, GroupCardView(group: g, members: demo.members(of: g), visible: demo.members(of: g), vm: demo,
onEdit: {}, onAddMember: {}, onInvite: {}, onViewClients: { _ in }, onEditMember: { _ in }, onInviteMember: { _ in }, onMemberClients: { _ in }).padding(20))
@@ -284,11 +285,11 @@ MainActor.assumeIsolated {
// 授权延长:申请延长、最近结束、已授权设备的「延长」
let rvm = makeDemoVM()
rvm.tunnels[0].renewals = decode([RenewalItem].self, """
[{"tunnel_id":"\(rvm.tunnels[0].id)","ip":"192.168.1.35","name":"李四","invite":"ab1","ended_at":"\(t(-1200))","renew_before":"\(t(6000))",
"request":{"id":"rn1","tunnel_id":"\(rvm.tunnels[0].id)","name":"李四","ip":"192.168.1.35","invite":"ab1","at":"\(t(-60))","status":"pending","renewal":true}},
[{"tunnel_id":"\(rvm.tunnels[0].id)","ip":"192.168.1.47","name":"赵六","invite":"ab1","ended_at":"\(t(-1200))","renew_before":"\(t(6000))",
"request":{"id":"rn1","tunnel_id":"\(rvm.tunnels[0].id)","name":"赵六","ip":"192.168.1.47","invite":"ab1","at":"\(t(-60))","status":"pending","renewal":true}},
{"tunnel_id":"\(rvm.tunnels[0].id)","ip":"192.168.1.36","name":"王五","invite":"ab2","ended_at":"\(t(-3000))","renew_before":"\(t(4200))"}]
""")
render("renew-access", CGSize(width: 760, height: 620), ClientDetailsSheet(vm: rvm, tunnel: rvm.tunnels[0], initialTab: 2, isPresented: yes))
render("readme-access", CGSize(width: 760, height: 800), ClientDetailsSheet(vm: rvm, tunnel: rvm.tunnels[0], initialTab: 2, isPresented: yes))
render("renew-card", CGSize(width: 980, height: 220), TunnelCardView(tunnel: rvm.tunnels[0], vm: rvm, onEdit: {}, onViewClients: {}, onViewLeases: {}, onInvite: {}).padding(20))
render("renew-invite-form", CGSize(width: 600, height: 400), InviteSheet(vm: rvm, tunnel: rvm.tunnels[0], autoGenerate: false, isPresented: yes))
render("renew-custom", CGSize(width: 300, height: 90), CustomMinutesForm(actionTitle: "延长") { _ in })