docs: 授权延长在 README 里单独成节,主界面与动图带上延长申请
- 「申请延长」单独成节:同事端 ④⑤⑥ 与 App 的连接与授权截图放在一起, 写明卡片提示、点「批准延长」选时长、最近结束的设备也可直接延长 - 演示数据:MySQL 通道带一条延长申请和一台最近结束的设备,主界面、动图、 授权管理与网页面板展示同一组数据,卡片上能看到「申请延长 1」
No files matched your search
@@ -82,7 +82,7 @@ PortRelay 把这件事变成三步:
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="docs/images/live-traffic.gif" alt="通道卡片与实时流量" width="100%">
|
<img src="docs/images/live-traffic.gif" alt="通道卡片与实时流量" width="100%">
|
||||||
<br><sub>通道卡片:访问地址、授权状态、实际连接时长,以及近 10 分钟流量(动图为加速演示)</sub>
|
<br><sub>通道卡片:访问地址、待处理的访问与延长申请、实际连接时长,以及近 10 分钟流量(动图为加速演示)</sub>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<picture>
|
<picture>
|
||||||
@@ -104,7 +104,7 @@ PortRelay 把这件事变成三步:
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
授权快到期或刚结束时,同事再打开同一个链接,就能看到自己的授权状态并申请延长。你选好时长批准后,页面会显示新的到期时间。
|
**申请延长**:授权快到期或刚结束时,同事再打开同一个链接,就能看到自己的授权状态并申请延长,不用你重新发邀请。
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -114,18 +114,18 @@ PortRelay 把这件事变成三步:
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
你这边,通道卡片上会出现「申请延长」。打开「连接与授权」,延长申请排在最前面,点「批准延长」选好时长即可,同事的页面随即显示新的到期时间。这里还列出待审批的访问申请、已授权设备(在线状态与剩余时间)、最近结束的授权和仍然有效的邀请链接,刚结束的设备也可以直接延长。
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/images/app-access.png" alt="连接与授权:延长申请、待审批、已授权设备、最近结束" width="80%">
|
||||||
|
</p>
|
||||||
|
|
||||||
**通道组**:组卡片汇总整组的状态与流量,下面逐行列出每个访问地址,可以单独开关、单独邀请或移出组。
|
**通道组**:组卡片汇总整组的状态与流量,下面逐行列出每个访问地址,可以单独开关、单独邀请或移出组。
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="docs/images/app-group.png" alt="通道组" width="100%">
|
<img src="docs/images/app-group.png" alt="通道组" width="100%">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**连接与授权管理**:访问申请和延长申请、已授权设备(在线状态与剩余时间)、最近结束的授权,以及仍然有效的邀请链接,都在这里处理。给设备延长只需选一个时长,不用重新发链接。
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="docs/images/app-access.png" alt="授权管理" width="80%">
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>网页面板与更多截图</b></summary>
|
<summary><b>网页面板与更多截图</b></summary>
|
||||||
<br>
|
<br>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ PortRelay turns that into three steps:
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="docs/images/live-traffic.gif" alt="Tunnel card with live traffic" width="100%">
|
<img src="docs/images/live-traffic.gif" alt="Tunnel card with live traffic" width="100%">
|
||||||
<br><sub>A tunnel card: access address, grants, connected time and the last 10 minutes of traffic (sped up)</sub>
|
<br><sub>A tunnel card: access address, pending access and extension requests, connected time and the last 10 minutes of traffic (sped up)</sub>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<picture>
|
<picture>
|
||||||
@@ -107,7 +107,7 @@ PortRelay turns that into three steps:
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
When access is about to run out or has just ended, the teammate opens the same link again, sees where their access stands and asks for more time. Once you pick a duration, the page shows the new expiry.
|
**Extensions.** When access is about to run out or has just ended, the teammate opens the same link again, sees where their access stands and asks for more time. You don't need to send a new invite.
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -117,18 +117,18 @@ When access is about to run out or has just ended, the teammate opens the same l
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
On your side, the tunnel card shows 申请延长 (Extension requested). Open 连接与授权 (Connections & access): extension requests are listed first, and you approve one by clicking 批准延长 (Approve) and picking a duration. The teammate's page then shows the new expiry. The same view lists pending access requests, authorized devices (with online state and remaining time), recently ended grants and active invite links, and a device whose access just ended can be extended from there too.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/images/app-access.png" alt="Access management: extension requests, pending requests, authorized devices, recently ended" width="80%">
|
||||||
|
</p>
|
||||||
|
|
||||||
**Tunnel groups.** A group card sums up the state and traffic of the whole group and lists every access address. Each address can be switched, invited or moved out on its own.
|
**Tunnel groups.** A group card sums up the state and traffic of the whole group and lists every access address. Each address can be switched, invited or moved out on its own.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="docs/images/app-group.png" alt="Tunnel group" width="100%">
|
<img src="docs/images/app-group.png" alt="Tunnel group" width="100%">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**Access management.** Access and extension requests, authorized devices (with online state and remaining time), recently ended grants and active invite links all live here. Extending a device takes one pick of a duration, with no new link to send.
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="docs/images/app-access.png" alt="Access management" width="80%">
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>Web panel and more screenshots</b></summary>
|
<summary><b>Web panel and more screenshots</b></summary>
|
||||||
<br>
|
<br>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 653 KiB After Width: | Height: | Size: 657 KiB |
|
Before Width: | Height: | Size: 744 KiB After Width: | Height: | Size: 749 KiB |
|
Before Width: | Height: | Size: 510 KiB After Width: | Height: | Size: 510 KiB |
|
Before Width: | Height: | Size: 1006 KiB After Width: | Height: | Size: 1010 KiB |
@@ -163,6 +163,12 @@ func makeDemoVM(tick: Int = 0) -> PortRelayViewModel {
|
|||||||
{"code":"gx2","note":"测试组","created_at":"\(t(-100))","expires_at":"\(t(86000))","max_uses":3,"lease_duration":"today","require_approval":true}]}}]
|
{"code":"gx2","note":"测试组","created_at":"\(t(-100))","expires_at":"\(t(86000))","max_uses":3,"lease_duration":"today","require_approval":true}]}}]
|
||||||
""")
|
""")
|
||||||
vm.groups[0].rate_history = RateSeriesItem(end: ga.end, step: 5, up: zip(ga.up, gb.up).map { $0 + $1 }, down: zip(ga.down, gb.down).map { $0 + $1 })
|
vm.groups[0].rate_history = RateSeriesItem(end: ga.end, step: 5, up: zip(ga.up, gb.up).map { $0 + $1 }, down: zip(ga.down, gb.down).map { $0 + $1 })
|
||||||
|
// 授权延长:赵六的访问 20 分钟前结束,刚申请延长;王五结束不到 2 小时,还可以延长
|
||||||
|
vm.tunnels[0].renewals = decode([RenewalItem].self, """
|
||||||
|
[{"tunnel_id":"t1","ip":"192.168.1.47","name":"赵六","invite":"ab1","ended_at":"\(t(-1200))","renew_before":"\(t(6000))",
|
||||||
|
"request":{"id":"rn1","tunnel_id":"t1","name":"赵六","ip":"192.168.1.47","invite":"ab1","at":"\(t(-60))","status":"pending","renewal":true}},
|
||||||
|
{"tunnel_id":"t1","ip":"192.168.1.36","name":"王五","invite":"ab2","ended_at":"\(t(-3000))","renew_before":"\(t(4200))"}]
|
||||||
|
""")
|
||||||
// 卡片与顶部统计的实时速率取曲线最后一个点
|
// 卡片与顶部统计的实时速率取曲线最后一个点
|
||||||
var totalIn: Int64 = 0, totalOut: Int64 = 0
|
var totalIn: Int64 = 0, totalOut: Int64 = 0
|
||||||
for i in [0, 1, 3, 4] {
|
for i in [0, 1, 3, 4] {
|
||||||
@@ -283,15 +289,9 @@ MainActor.assumeIsolated {
|
|||||||
render("group-edit-member", CGSize(width: 520, height: 800), TunnelEditSheet(vm: demo, tunnel: demo.tunnels[3], isPresented: yes))
|
render("group-edit-member", CGSize(width: 520, height: 800), TunnelEditSheet(vm: demo, tunnel: demo.tunnels[3], isPresented: yes))
|
||||||
|
|
||||||
// 授权延长:申请延长、最近结束、已授权设备的「延长」
|
// 授权延长:申请延长、最近结束、已授权设备的「延长」
|
||||||
let rvm = makeDemoVM()
|
render("readme-access", CGSize(width: 760, height: 800), ClientDetailsSheet(vm: demo, tunnel: demo.tunnels[0], initialTab: 2, isPresented: yes))
|
||||||
rvm.tunnels[0].renewals = decode([RenewalItem].self, """
|
render("renew-card", CGSize(width: 980, height: 220), TunnelCardView(tunnel: demo.tunnels[0], vm: demo, onEdit: {}, onViewClients: {}, onViewLeases: {}, onInvite: {}).padding(20))
|
||||||
[{"tunnel_id":"\(rvm.tunnels[0].id)","ip":"192.168.1.47","name":"赵六","invite":"ab1","ended_at":"\(t(-1200))","renew_before":"\(t(6000))",
|
render("renew-invite-form", CGSize(width: 600, height: 400), InviteSheet(vm: demo, tunnel: demo.tunnels[0], autoGenerate: false, isPresented: yes))
|
||||||
"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("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 })
|
render("renew-custom", CGSize(width: 300, height: 90), CustomMinutesForm(actionTitle: "延长") { _ in })
|
||||||
|
|
||||||
// README 动图逐帧:卡片随时间推移(每帧 = 5 秒)
|
// README 动图逐帧:卡片随时间推移(每帧 = 5 秒)
|
||||||
|
|||||||
@@ -96,8 +96,11 @@ func main() {
|
|||||||
"metrics":{"tunnel_id":"t1","status":"online","active_conns":3,"total_conns":1287,"bytes_in":245039104,"bytes_out":8703180,"blocked_attempts":12,
|
"metrics":{"tunnel_id":"t1","status":"online","active_conns":3,"total_conns":1287,"bytes_in":245039104,"bytes_out":8703180,"blocked_attempts":12,
|
||||||
"dynamic_leases":[{"ip":"192.168.1.21","expires_at":%q,"name":"张三","invite":"a1","duration":"2h","started_at":%q},
|
"dynamic_leases":[{"ip":"192.168.1.21","expires_at":%q,"name":"张三","invite":"a1","duration":"2h","started_at":%q},
|
||||||
{"ip":"192.168.1.35","expires_at":%q,"name":"王小明","invite":"a2","duration":"8h","awaiting_connect":true}],
|
{"ip":"192.168.1.35","expires_at":%q,"name":"王小明","invite":"a2","duration":"8h","awaiting_connect":true}],
|
||||||
"pending":[{"id":"r1","tunnel_id":"t1","name":"李四","ip":"192.168.1.88","invite":"a3","at":%q,"status":"pending"}]}}`,
|
"pending":[{"id":"r1","tunnel_id":"t1","name":"李四","ip":"192.168.1.88","invite":"a3","at":%q,"status":"pending"}]},
|
||||||
at(-3725), at(10675), at(-3300), at(7000), at(-200), at(80000), at(-60)),
|
"renewals":[{"tunnel_id":"t1","ip":"192.168.1.47","name":"赵六","invite":"ab1","ended_at":%q,"renew_before":%q,
|
||||||
|
"request":{"id":"rn1","tunnel_id":"t1","name":"赵六","ip":"192.168.1.47","invite":"ab1","at":%q,"status":"pending","renewal":true}},
|
||||||
|
{"tunnel_id":"t1","ip":"192.168.1.36","name":"王五","invite":"ab2","ended_at":%q,"renew_before":%q}]}`,
|
||||||
|
at(-3725), at(10675), at(-3300), at(7000), at(-200), at(80000), at(-60), at(-1200), at(6000), at(-60), at(-3000), at(4200)),
|
||||||
member("g1a", "10.0.3.11", 33670, 1, -1500, 1320, 3203180, 18039104),
|
member("g1a", "10.0.3.11", 33670, 1, -1500, 1320, 3203180, 18039104),
|
||||||
member("g1b", "10.0.3.12", 33671, 2, -1400, 1100, 2203180, 12039104),
|
member("g1b", "10.0.3.12", 33671, 2, -1400, 1100, 2203180, 12039104),
|
||||||
member("g1c", "10.0.3.13", 33672, 0, 0, 0, 0, 0),
|
member("g1c", "10.0.3.13", 33672, 0, 0, 0, 0, 0),
|
||||||
|
|||||||