mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-09-20 17:07:57 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f9f18ae13c | |||
| f935cad33f | |||
| f9c2e9476a | |||
| 15b1dcce8f | |||
| 6ee901dc64 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -33,3 +33,6 @@ gui-test-screenshots/
|
||||
# vendored deps (regenerable; keep out of git)
|
||||
vendor/
|
||||
.pi-glla/
|
||||
|
||||
# 发布产物(由 scripts/release.sh 生成)
|
||||
dist/
|
||||
|
||||
66
README.md
66
README.md
@ -72,8 +72,74 @@ rm -rf internal/httpapi/dist && cp -r web/dist internal/httpapi/dist
|
||||
cd web && npm run build && rm -rf internal/httpapi/dist && cp -r web/dist internal/httpapi/dist && go build -o webui4frpc ./cmd/webui4frpc
|
||||
```
|
||||
|
||||
## 安装
|
||||
|
||||
从 [Release 页](https://gitcode.com/JianFeeeee/webui4frpc/releases/latest) 下载对应平台的安装包。
|
||||
|
||||
### Linux (Debian / Ubuntu)
|
||||
|
||||
```bash
|
||||
sudo dpkg -i webui4frpc-0.1.1-linux-amd64.deb # 或 linux-arm64.deb
|
||||
sudo nano /etc/webui4frpc/webui4frpc.env # 改掉默认密码
|
||||
sudo systemctl restart webui4frpc
|
||||
```
|
||||
|
||||
### Linux (RHEL / Fedora / openEuler / 默认 / 龙蜥)
|
||||
|
||||
```bash
|
||||
sudo rpm -i webui4frpc-0.1.1-linux-x86_64.rpm
|
||||
sudo vi /etc/webui4frpc/webui4frpc.env
|
||||
sudo systemctl enable --now webui4frpc
|
||||
```
|
||||
|
||||
安装后自动建 `webui4frpc` 系统账号、注册 systemd 单元,数据目录在 `/var/lib/webui4frpc`。
|
||||
服务以非 root 运行,并启用 `ProtectSystem=full` / `NoNewPrivileges` 等加固项。
|
||||
|
||||
### Windows
|
||||
|
||||
双击 `webui4frpc-0.1.1-windows-amd64-setup.exe`(ARM 设备用 `windows-arm64-setup.exe`)。
|
||||
安装向导会询问监听地址与管理员凭据,完成后:
|
||||
|
||||
- 程序装到 `C:\Program Files\webui4frpc`
|
||||
- 开始菜单生成启动快捷方式与卸载项
|
||||
- 自动添加防火墙入站规则
|
||||
|
||||
### macOS
|
||||
|
||||
```bash
|
||||
sudo installer -pkg webui4frpc-0.1.1-darwin-arm64.pkg -target / # Apple Silicon
|
||||
sudo installer -pkg webui4frpc-0.1.1-darwin-amd64.pkg -target / # Intel
|
||||
```
|
||||
|
||||
或直接双击 `.pkg` 走图形安装器。安装后 launchd 开机自启,配置在
|
||||
`/usr/local/etc/webui4frpc/webui4frpc.env`,改完执行:
|
||||
|
||||
```bash
|
||||
sudo launchctl kickstart -k system/com.jianfeeeee.webui4frpc
|
||||
```
|
||||
|
||||
> 该 `.pkg` 未经 Apple 公证(notarization)。首次打开若被 Gatekeeper 拦下,
|
||||
> 在「系统设置 → 隐私与安全」选择仍要打开。
|
||||
|
||||
### 免安装(tar.gz / zip)
|
||||
|
||||
不想装服务只想跑一下,用对应的 `.tar.gz` / `.zip`,解压即用:
|
||||
|
||||
```bash
|
||||
tar xzf webui4frpc-0.1.1-linux-amd64.tar.gz
|
||||
./webui4frpc -addr 127.0.0.1:7500 -user admin -password 你的密码 -workdir ./data
|
||||
```
|
||||
|
||||
下载后可用 `SHA256SUMS` 校验完整性:
|
||||
|
||||
```bash
|
||||
sha256sum -c SHA256SUMS --ignore-missing
|
||||
```
|
||||
|
||||
## 运行
|
||||
|
||||
从源码构建后直接跑:
|
||||
|
||||
```bash
|
||||
./webui4frpc -addr 127.0.0.1:7500 -user admin -password admin123 -workdir ./data
|
||||
```
|
||||
|
||||
@ -25,6 +25,7 @@ import (
|
||||
"webui4frpc/internal/process"
|
||||
"webui4frpc/internal/render"
|
||||
"webui4frpc/internal/store"
|
||||
"webui4frpc/internal/version"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -132,7 +133,7 @@ func main() {
|
||||
}
|
||||
var ring *cluster.Engine
|
||||
ring = cluster.NewEngine(
|
||||
selfID, selfID, *user, *pass, "0.1.0", nil,
|
||||
selfID, selfID, *user, *pass, version.Version, nil,
|
||||
&cluster.AppHandler{
|
||||
LoadFn: func() (float64, float64) {
|
||||
// NetPct: real NIC saturation from /proc/net/dev deltas vs
|
||||
@ -533,7 +534,7 @@ func main() {
|
||||
// CreateCluster so the node is at least functional as standalone.
|
||||
func retryJoinCluster(ctx context.Context, ring *cluster.Engine, targetAddr, joinKey string) {
|
||||
selfNode := ring.State().Nodes[0]
|
||||
ji := cluster.JoinInfo{ID: selfNode.ID, Addr: selfNode.Addr, Version: "0.1.0", JoinKey: joinKey}
|
||||
ji := cluster.JoinInfo{ID: selfNode.ID, Addr: selfNode.Addr, Version: version.Version, JoinKey: joinKey}
|
||||
deadline := time.NewTimer(5 * time.Minute)
|
||||
defer deadline.Stop()
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
@ -593,7 +594,7 @@ func retryRejoinCached(ctx context.Context, ring *cluster.Engine, peersJSON stri
|
||||
return
|
||||
}
|
||||
for _, p := range peers {
|
||||
ji := cluster.JoinInfo{ID: selfNode.ID, Addr: selfNode.Addr, Version: "0.1.0", JoinKey: p.Key}
|
||||
ji := cluster.JoinInfo{ID: selfNode.ID, Addr: selfNode.Addr, Version: version.Version, JoinKey: p.Key}
|
||||
jc, cancel := context.WithTimeout(ctx, 8*time.Second)
|
||||
err := ring.JoinRing(jc, p.Addr, ji)
|
||||
cancel()
|
||||
|
||||
@ -155,7 +155,9 @@ func (e *Engine) forwardToNext(ctx context.Context, tk *Token) error {
|
||||
}
|
||||
// Non-leader neighbor death: continue to the next recipient.
|
||||
}
|
||||
e.inflight.clear()
|
||||
// 全环遍历完毕,所有后继都不可达(或已全部尝试过)。
|
||||
// 保持 inflight(不清空),让 WatchTokenLoss 超时后重发。
|
||||
// 重发时 AliveSuccessor 重新计算,可能已被心跳复活。
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -18,6 +20,7 @@ import (
|
||||
"webui4frpc/internal/cluster"
|
||||
"webui4frpc/internal/process"
|
||||
"webui4frpc/internal/store"
|
||||
"webui4frpc/internal/version"
|
||||
)
|
||||
|
||||
//go:embed all:dist
|
||||
@ -411,7 +414,7 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
resp := map[string]any{
|
||||
"version": "0.1.0",
|
||||
"version": version.Version,
|
||||
"workDir": h.WorkDir,
|
||||
"settings": settings,
|
||||
"services": locals,
|
||||
@ -538,11 +541,31 @@ func (h *Handler) handleClusterToken(w http.ResponseWriter, r *http.Request) {
|
||||
// predecessor's WatchLeader heartbeat fail → MarkOffline → becomeLeader
|
||||
// → StartRing, healing the ring. Per design: "心跳拒绝应当发生在leader
|
||||
// 退出节点时,让leader上邻居意识到当前环已经没有节点了".
|
||||
//
|
||||
// 同时,心跳到达说明前驱还活着,leader 应在地自己的拓扑中把它标记为
|
||||
// alive。否则 forwardToNext 一次超时把前驱 MarkOffline 后就再没有
|
||||
// 途径恢复它——前驱明明活着,leader 却认为它 offline → AliveSuccessor
|
||||
// 找不到后继,令牌冻结在 leader 手上。心跳是 leader 侧纠正误判的唯一
|
||||
// 入口(因为心跳只有前驱发给 leader 这一条边)。
|
||||
if r.Body == nil {
|
||||
if s := h.Ring.State(); len(s.Nodes) <= 1 {
|
||||
http.Error(w, "standalone node", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
// 心跳到达 = 前驱活着,标记 alive(纠正 forwardToNext 的误判)
|
||||
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||
s := h.Ring.State()
|
||||
for i := range s.Nodes {
|
||||
if h, _, e := net.SplitHostPort(s.Nodes[i].Addr); e == nil && h == host {
|
||||
if !s.Nodes[i].Alive {
|
||||
log.Printf("heartbeat from %s revives it (was offline)", s.Nodes[i].ID)
|
||||
}
|
||||
s.Nodes[i].Alive = true
|
||||
s.Nodes[i].LastSeen = time.Now().Unix()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
@ -552,6 +575,20 @@ func (h *Handler) handleClusterToken(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "standalone node", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
// 同上:心跳到达 = 前驱活着
|
||||
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||
s := h.Ring.State()
|
||||
for i := range s.Nodes {
|
||||
if h, _, e := net.SplitHostPort(s.Nodes[i].Addr); e == nil && h == host {
|
||||
if !s.Nodes[i].Alive {
|
||||
log.Printf("heartbeat from %s revives it (was offline)", s.Nodes[i].ID)
|
||||
}
|
||||
s.Nodes[i].Alive = true
|
||||
s.Nodes[i].LastSeen = time.Now().Unix()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
12
internal/version/version.go
Normal file
12
internal/version/version.go
Normal file
@ -0,0 +1,12 @@
|
||||
// Package version holds the single source of truth for the build version.
|
||||
//
|
||||
// Value is overridable at build time so release artifacts carry the real tag:
|
||||
//
|
||||
// go build -ldflags="-X webui4frpc/internal/version.Version=0.1.1" ./cmd/webui4frpc
|
||||
//
|
||||
// Kept in its own package because both cmd/ and internal/httpapi report it
|
||||
// (main can't be imported, and httpapi must not depend on cmd).
|
||||
package version
|
||||
|
||||
// Version is the running build's semantic version, without a leading "v".
|
||||
var Version = "0.1.1"
|
||||
21
packaging/common/LICENSE.txt
Normal file
21
packaging/common/LICENSE.txt
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 JianFeeeee
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
8
packaging/common/webui4frpc.env
Normal file
8
packaging/common/webui4frpc.env
Normal file
@ -0,0 +1,8 @@
|
||||
# webui4frpc runtime configuration
|
||||
# 默认监听全部接口的 7500 端口;请修改密码后再投入生产。
|
||||
W4F_ADDR=0.0.0.0:7500
|
||||
W4F_USER=admin
|
||||
W4F_PASSWORD=admin
|
||||
|
||||
# 可选:首次提供的 frpc 二进制路径(留空则从 Web UI 手动/自动安装)
|
||||
# W4F_FRPC=/usr/local/bin/frpc
|
||||
25
packaging/common/webui4frpc.service
Normal file
25
packaging/common/webui4frpc.service
Normal file
@ -0,0 +1,25 @@
|
||||
[Unit]
|
||||
Description=webui4frpc - visual frpc controller (cluster node)
|
||||
Documentation=https://gitcode.com/JianFeeeee/webui4frpc
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=webui4frpc
|
||||
Group=webui4frpc
|
||||
EnvironmentFile=/etc/webui4frpc/webui4frpc.env
|
||||
ExecStart=/usr/bin/webui4frpc -addr ${W4F_ADDR} -user ${W4F_USER} -password ${W4F_PASSWORD} -workdir /var/lib/webui4frpc
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
LimitNOFILE=65535
|
||||
|
||||
# hardening
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/var/lib/webui4frpc
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
42
packaging/deb/postinst
Executable file
42
packaging/deb/postinst
Executable file
@ -0,0 +1,42 @@
|
||||
#!/bin/sh
|
||||
# webui4frpc deb/rpm 安装后脚本:建用户、建目录、装服务
|
||||
set -e
|
||||
|
||||
if ! getent group webui4frpc >/dev/null 2>&1; then
|
||||
groupadd --system webui4frpc
|
||||
fi
|
||||
if ! getent passwd webui4frpc >/dev/null 2>&1; then
|
||||
useradd --system --gid webui4frpc \
|
||||
--home-dir /var/lib/webui4frpc --no-create-home \
|
||||
--shell /usr/sbin/nologin \
|
||||
--comment "webui4frpc service account" webui4frpc
|
||||
fi
|
||||
|
||||
mkdir -p /var/lib/webui4frpc
|
||||
chown -R webui4frpc:webui4frpc /var/lib/webui4frpc
|
||||
chmod 750 /var/lib/webui4frpc
|
||||
chown root:webui4frpc /etc/webui4frpc/webui4frpc.env 2>/dev/null || true
|
||||
chmod 640 /etc/webui4frpc/webui4frpc.env 2>/dev/null || true
|
||||
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl daemon-reload || true
|
||||
systemctl enable webui4frpc.service || true
|
||||
if [ -z "${DPKG_ROOT:-}" ]; then
|
||||
systemctl restart webui4frpc.service || true
|
||||
fi
|
||||
fi
|
||||
|
||||
cat <<'EOF'
|
||||
|
||||
webui4frpc 已安装。
|
||||
|
||||
配置: /etc/webui4frpc/webui4frpc.env (请立即修改 W4F_USER / W4F_PASSWORD)
|
||||
数据: /var/lib/webui4frpc
|
||||
服务: systemctl status webui4frpc
|
||||
界面: http://<本机地址>:7500
|
||||
|
||||
改完配置后执行: systemctl restart webui4frpc
|
||||
|
||||
EOF
|
||||
|
||||
exit 0
|
||||
20
packaging/deb/postrm
Executable file
20
packaging/deb/postrm
Executable file
@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
# webui4frpc deb 卸载后脚本:purge 时清数据与账号
|
||||
set -e
|
||||
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl daemon-reload || true
|
||||
fi
|
||||
|
||||
if [ "$1" = "purge" ]; then
|
||||
rm -rf /var/lib/webui4frpc
|
||||
rm -rf /etc/webui4frpc
|
||||
if getent passwd webui4frpc >/dev/null 2>&1; then
|
||||
userdel webui4frpc || true
|
||||
fi
|
||||
if getent group webui4frpc >/dev/null 2>&1; then
|
||||
groupdel webui4frpc || true
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
||||
10
packaging/deb/prerm
Executable file
10
packaging/deb/prerm
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
# webui4frpc deb 卸载前脚本:停服务、撤 enable
|
||||
set -e
|
||||
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl stop webui4frpc.service || true
|
||||
systemctl disable webui4frpc.service || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
187
packaging/macos/build_pkg.sh
Executable file
187
packaging/macos/build_pkg.sh
Executable file
@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 在 Linux 上手工组装 macOS .pkg (flat package)。
|
||||
# macOS 的 pkgbuild/productbuild 不可用,因此用 xar + mkbom 手搓 flat package:
|
||||
#
|
||||
# archive.xar
|
||||
# ├── Distribution 安装器界面描述 (choices / title)
|
||||
# └── webui4frpc.pkg/
|
||||
# ├── PackageInfo 组件元数据 + postinstall 声明
|
||||
# ├── Bom 文件清单 (mkbom 生成)
|
||||
# ├── Payload 根文件系统 cpio.gz
|
||||
# └── Scripts 安装脚本 cpio.gz
|
||||
#
|
||||
# 用法: build_pkg.sh <version> <arch:amd64|arm64> <binary> <outfile>
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="$1"; ARCH="$2"
|
||||
# xar 与 cpio 都在临时目录里执行,输入输出必须先转成绝对路径
|
||||
BINARY="$(cd "$(dirname "$3")" && pwd)/$(basename "$3")"
|
||||
mkdir -p "$(dirname "$4")"
|
||||
OUTFILE="$(cd "$(dirname "$4")" && pwd)/$(basename "$4")"
|
||||
|
||||
case "$ARCH" in
|
||||
amd64) PKG_ARCH="x86_64" ;;
|
||||
arm64) PKG_ARCH="arm64" ;;
|
||||
*) echo "未知架构: $ARCH" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
ROOT="$WORK/root"
|
||||
SCRIPTS="$WORK/scripts"
|
||||
FLAT="$WORK/flat"
|
||||
PKGDIR="$FLAT/webui4frpc.pkg"
|
||||
mkdir -p "$ROOT/usr/local/bin" "$ROOT/usr/local/etc/webui4frpc" "$SCRIPTS" "$PKGDIR"
|
||||
|
||||
# ---- payload 根文件系统 ----
|
||||
install -m 0755 "$BINARY" "$ROOT/usr/local/bin/webui4frpc"
|
||||
|
||||
cat > "$ROOT/usr/local/etc/webui4frpc/webui4frpc.env" <<'ENVEOF'
|
||||
# webui4frpc runtime configuration (macOS)
|
||||
# 请修改密码后再投入使用;改完执行:
|
||||
# sudo launchctl kickstart -k system/com.jianfeeeee.webui4frpc
|
||||
W4F_ADDR=0.0.0.0:7500
|
||||
W4F_USER=admin
|
||||
W4F_PASSWORD=admin
|
||||
ENVEOF
|
||||
chmod 0644 "$ROOT/usr/local/etc/webui4frpc/webui4frpc.env"
|
||||
|
||||
# ---- launchd plist ----
|
||||
mkdir -p "$ROOT/Library/LaunchDaemons"
|
||||
cat > "$ROOT/Library/LaunchDaemons/com.jianfeeeee.webui4frpc.plist" <<'PLISTEOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.jianfeeeee.webui4frpc</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/local/bin/webui4frpc-launch</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/usr/local/var/log/webui4frpc.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/usr/local/var/log/webui4frpc.err.log</string>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/usr/local/var/lib/webui4frpc</string>
|
||||
</dict>
|
||||
</plist>
|
||||
PLISTEOF
|
||||
chmod 0644 "$ROOT/Library/LaunchDaemons/com.jianfeeeee.webui4frpc.plist"
|
||||
|
||||
# launchd 不读 EnvironmentFile,用小包装脚本 source 配置再 exec
|
||||
cat > "$ROOT/usr/local/bin/webui4frpc-launch" <<'LAUNCHEOF'
|
||||
#!/bin/sh
|
||||
# 读取 /usr/local/etc/webui4frpc/webui4frpc.env 后启动 webui4frpc
|
||||
set -e
|
||||
ENV_FILE=/usr/local/etc/webui4frpc/webui4frpc.env
|
||||
W4F_ADDR=0.0.0.0:7500
|
||||
W4F_USER=admin
|
||||
W4F_PASSWORD=admin
|
||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||
exec /usr/local/bin/webui4frpc \
|
||||
-addr "$W4F_ADDR" \
|
||||
-user "$W4F_USER" \
|
||||
-password "$W4F_PASSWORD" \
|
||||
-workdir /usr/local/var/lib/webui4frpc
|
||||
LAUNCHEOF
|
||||
chmod 0755 "$ROOT/usr/local/bin/webui4frpc-launch"
|
||||
|
||||
# ---- postinstall ----
|
||||
cat > "$SCRIPTS/postinstall" <<'POSTEOF'
|
||||
#!/bin/sh
|
||||
set -e
|
||||
mkdir -p /usr/local/var/lib/webui4frpc /usr/local/var/log
|
||||
chmod 750 /usr/local/var/lib/webui4frpc
|
||||
|
||||
PLIST=/Library/LaunchDaemons/com.jianfeeeee.webui4frpc.plist
|
||||
chown root:wheel "$PLIST"
|
||||
chmod 644 "$PLIST"
|
||||
|
||||
# 覆盖安装时先卸载旧的
|
||||
launchctl bootout system "$PLIST" 2>/dev/null || true
|
||||
launchctl bootstrap system "$PLIST" 2>/dev/null || \
|
||||
launchctl load -w "$PLIST" 2>/dev/null || true
|
||||
|
||||
exit 0
|
||||
POSTEOF
|
||||
chmod 0755 "$SCRIPTS/postinstall"
|
||||
|
||||
# ---- Payload / Scripts (cpio.gz) ----
|
||||
( cd "$ROOT" && find . -print | cpio -o --format odc --owner 0:0 2>/dev/null ) \
|
||||
| gzip -9 > "$PKGDIR/Payload"
|
||||
( cd "$SCRIPTS" && find . -print | cpio -o --format odc --owner 0:0 2>/dev/null ) \
|
||||
| gzip -9 > "$PKGDIR/Scripts"
|
||||
|
||||
# ---- Bom ----
|
||||
mkbom -u 0 -g 80 "$ROOT" "$PKGDIR/Bom"
|
||||
|
||||
NFILES=$(find "$ROOT" | wc -l)
|
||||
INSTALL_KB=$(du -sk "$ROOT" | cut -f1)
|
||||
|
||||
# ---- PackageInfo ----
|
||||
cat > "$PKGDIR/PackageInfo" <<INFOEOF
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<pkg-info format-version="2"
|
||||
identifier="com.jianfeeeee.webui4frpc"
|
||||
version="$VERSION"
|
||||
install-location="/"
|
||||
auth="root"
|
||||
relocatable="false"
|
||||
overwrite-permissions="true">
|
||||
<payload installKBytes="$INSTALL_KB" numberOfFiles="$NFILES"/>
|
||||
<scripts>
|
||||
<postinstall file="./postinstall"/>
|
||||
</scripts>
|
||||
<bundle-version/>
|
||||
</pkg-info>
|
||||
INFOEOF
|
||||
|
||||
# ---- Distribution ----
|
||||
cat > "$FLAT/Distribution" <<DISTEOF
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<installer-gui-script minSpecVersion="1">
|
||||
<title>webui4frpc $VERSION</title>
|
||||
<organization>com.jianfeeeee</organization>
|
||||
<options customize="never" require-scripts="false" hostArchitectures="$PKG_ARCH"/>
|
||||
<domains enable_localSystem="true" enable_anywhere="false" enable_currentUserHome="false"/>
|
||||
<volume-check>
|
||||
<allowed-os-versions><os-version min="11.0"/></allowed-os-versions>
|
||||
</volume-check>
|
||||
<welcome language="zh_CN" mime-type="text/plain">webui4frpc — 可视化 frpc 控制器
|
||||
|
||||
安装内容:
|
||||
/usr/local/bin/webui4frpc 主程序(内嵌 Web 界面)
|
||||
/usr/local/etc/webui4frpc/webui4frpc.env 配置文件
|
||||
/Library/LaunchDaemons/…webui4frpc.plist 开机自启服务
|
||||
|
||||
安装完成后服务自动启动,访问 http://127.0.0.1:7500
|
||||
默认凭据 admin/admin —— 请立即修改 webui4frpc.env 中的密码。</welcome>
|
||||
<choices-outline>
|
||||
<line choice="default">
|
||||
<line choice="com.jianfeeeee.webui4frpc"/>
|
||||
</line>
|
||||
</choices-outline>
|
||||
<choice id="default"/>
|
||||
<choice id="com.jianfeeeee.webui4frpc" visible="false">
|
||||
<pkg-ref id="com.jianfeeeee.webui4frpc"/>
|
||||
</choice>
|
||||
<pkg-ref id="com.jianfeeeee.webui4frpc"
|
||||
version="$VERSION"
|
||||
onConclusion="none">#webui4frpc.pkg</pkg-ref>
|
||||
</installer-gui-script>
|
||||
DISTEOF
|
||||
|
||||
# ---- 打成 xar (Distribution 必须是第一个成员) ----
|
||||
rm -f "$OUTFILE"
|
||||
( cd "$FLAT" && xar --compression none -cf "$OUTFILE" Distribution webui4frpc.pkg )
|
||||
|
||||
echo "已生成: $OUTFILE ($(du -h "$OUTFILE" | cut -f1))"
|
||||
77
packaging/rpm/webui4frpc.spec
Normal file
77
packaging/rpm/webui4frpc.spec
Normal file
@ -0,0 +1,77 @@
|
||||
Name: webui4frpc
|
||||
Version: %{?_w4f_version}%{!?_w4f_version:0.1.1}
|
||||
Release: 1%{?dist}
|
||||
Summary: Visual frpc controller with token-ring cluster
|
||||
License: MIT
|
||||
URL: https://gitcode.com/JianFeeeee/webui4frpc
|
||||
BuildArch: %{?_w4f_arch}%{!?_w4f_arch:x86_64}
|
||||
|
||||
Requires(pre): shadow-utils
|
||||
%{?systemd_requires}
|
||||
BuildRequires: systemd-rpm-macros
|
||||
|
||||
%description
|
||||
A web UI to manage FRP forwards across a cluster of nodes. Ships as a single
|
||||
statically-linked binary with an embedded Vue frontend. Includes a token-ring
|
||||
based cluster for distributed forward management, NIC network load sampling,
|
||||
and audit-log CSV export.
|
||||
|
||||
%install
|
||||
install -D -m 0755 %{_w4f_bin} %{buildroot}%{_bindir}/webui4frpc
|
||||
install -D -m 0644 %{_w4f_service} %{buildroot}%{_unitdir}/webui4frpc.service
|
||||
install -D -m 0640 %{_w4f_env} %{buildroot}%{_sysconfdir}/webui4frpc/webui4frpc.env
|
||||
install -D -m 0644 %{_w4f_readme} %{buildroot}%{_docdir}/webui4frpc/README.md
|
||||
install -d -m 0750 %{buildroot}%{_sharedstatedir}/webui4frpc
|
||||
|
||||
%pre
|
||||
getent group webui4frpc >/dev/null || groupadd --system webui4frpc
|
||||
getent passwd webui4frpc >/dev/null || \
|
||||
useradd --system --gid webui4frpc \
|
||||
--home-dir %{_sharedstatedir}/webui4frpc --no-create-home \
|
||||
--shell /sbin/nologin \
|
||||
--comment "webui4frpc service account" webui4frpc
|
||||
exit 0
|
||||
|
||||
%post
|
||||
%systemd_post webui4frpc.service
|
||||
chown -R webui4frpc:webui4frpc %{_sharedstatedir}/webui4frpc || :
|
||||
chown root:webui4frpc %{_sysconfdir}/webui4frpc/webui4frpc.env || :
|
||||
cat <<'EOF'
|
||||
|
||||
webui4frpc 已安装。
|
||||
|
||||
配置: /etc/webui4frpc/webui4frpc.env (请立即修改 W4F_USER / W4F_PASSWORD)
|
||||
数据: /var/lib/webui4frpc
|
||||
服务: systemctl enable --now webui4frpc
|
||||
界面: http://<本机地址>:7500
|
||||
|
||||
EOF
|
||||
|
||||
%preun
|
||||
%systemd_preun webui4frpc.service
|
||||
|
||||
%postun
|
||||
%systemd_postun_with_restart webui4frpc.service
|
||||
if [ $1 -eq 0 ]; then
|
||||
rm -rf %{_sharedstatedir}/webui4frpc
|
||||
getent passwd webui4frpc >/dev/null && userdel webui4frpc || :
|
||||
getent group webui4frpc >/dev/null && groupdel webui4frpc || :
|
||||
fi
|
||||
|
||||
%files
|
||||
%{_bindir}/webui4frpc
|
||||
%{_unitdir}/webui4frpc.service
|
||||
%dir %{_sysconfdir}/webui4frpc
|
||||
%config(noreplace) %attr(0640,root,webui4frpc) %{_sysconfdir}/webui4frpc/webui4frpc.env
|
||||
%dir %attr(0750,webui4frpc,webui4frpc) %{_sharedstatedir}/webui4frpc
|
||||
%doc %{_docdir}/webui4frpc/README.md
|
||||
|
||||
%changelog
|
||||
* Wed Sep 02 2026 JianFeeeee <jianfeeeee@gitcode> - 0.1.1-1
|
||||
- 修复令牌环冻结:心跳到达时复活被误判 offline 的前驱;
|
||||
全环无可达后继时保留 inflight,让 leader 超时重发令牌。
|
||||
- WebUI 窄屏适配:侧栅转底部 tab bar,各页响应式重排。
|
||||
|
||||
* Fri Aug 28 2026 JianFeeeee <jianfeeeee@gitcode> - 0.1.0-1
|
||||
- 首次发布:per-forward worker 模型、NIC 负载采样、审计 CSV 导出、
|
||||
session 登录与三级角色。
|
||||
21
packaging/windows/LICENSE.txt
Normal file
21
packaging/windows/LICENSE.txt
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 JianFeeeee
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
268
packaging/windows/README.md
Normal file
268
packaging/windows/README.md
Normal file
@ -0,0 +1,268 @@
|
||||
# webui4frpc
|
||||
|
||||
> **AI-assisted**: 本项目使用 AI 辅助编程(代码与文档由 AI 协作完成,经人工复核)。
|
||||
一个**独立于 frp 源码**的可视化 frpc 控制器。用画布方式把「本地转发项」连到「远程服务器」,自动为每台远程服务器生成 frpc 配置并拉起独立 worker 进程,免去运维反复手写 frpc 配置的麻烦。支持多节点组成令牌环集群,协同分发转发任务。
|
||||
|
||||
> 本仓库**不捆绑、不包含任何 frp 源码**。frpc 可执行文件由用户一键从官方 GitHub Releases 下载,或手动指定路径。
|
||||
|
||||
## 特性
|
||||
|
||||
### 画布配置
|
||||
|
||||
- **Scratch 风格画布**:本地转发项(local)与远程服务器(remote)可视化连线,一个 local 可连多个 remote,一个 remote 可连多个 local
|
||||
- **曲线连线 + 端口标签**:每条连线独立曲线,标签为远程端口,可拖拽调整曲线位置,端口可点开编辑
|
||||
- **画布冲突检查**:保存时自动检测远程端口/域名重复,避免配置冲突
|
||||
- **边开关与分组**:画布边上可一键禁用/启用单条转发,可打分组标签(状态页按分组管理、一键启停整组)
|
||||
- **Del 键/× 按钮删除**:选中连线按 Delete 键或点击端口标签红 × 删除,确认后标记脏
|
||||
- **一键生成配置**:保存画布即渲染每台 remote 的 frpc JSON 配置,自动拉起/重启对应 worker
|
||||
- **画布导入导出**:支持 JSON 信封备份/恢复(含 provenance 元数据)
|
||||
|
||||
### frpc 能力
|
||||
|
||||
- **代理类型**:tcp(完整)、http/https(customDomains / subdomain / locations / basicAuth / headerRewrite)
|
||||
- **传输参数**:useEncryption / useCompression / bandwidthLimit / poolCount / transport.protocol(tcp/quic/kcp/websocket)/ TLS
|
||||
- **负载均衡与健康检查**:lbGroup + healthCheck 渲染输出;状态页通过 frpc admin API 拉取 per-proxy 真实状态(running / check failed / wait start …)
|
||||
- **worker 自愈**:崩溃自动重启(指数退避),随 webui 启停
|
||||
- **frpc 一键安装**:从官方 Releases 下载任意版本 frpc,或手动指定二进制路径
|
||||
|
||||
### 集群模式(令牌环)
|
||||
|
||||
- **令牌环协议**:若干 webui4frpc 节点组成协作网络;令牌按固定顺序传递,每节点持完整集群信息,单轮即全网收敛
|
||||
- **负载最低者摘取**:新转发任务附加在令牌中,由负载最低的节点自行摘取并创建 frpc worker
|
||||
- 主负载信号:当前节点拥有的转发数(权重 ×100)
|
||||
- 次级信号:**NIC 网络接口利用率**(从 `/proc/net/dev` 增量 + `/sys/class/net/*/speed` 计算,排除了 lo/docker/veth 等虚拟接口)
|
||||
- 同转发数节点间由内存占用 + 网络饱和度打破平局
|
||||
- **per-forward 独立进程**:每条转发拥有独立的 frpc 配置文件和 frpc 进程(worker 键 = `local~remote~port`),单条转发故障不影响兄弟,消除旧模型的多节点 `proxy already exists` 冲突
|
||||
- **容错自愈**:leader / 非 leader 节点宕机 → 自动检测 → 环跳过死亡节点、任务重挂;leader 宕机 → 上邻居晋升为新 leader
|
||||
- **宕机自动重联**:节点宕机重启后通过缓存的集群拓扑(对端地址 + 准入密钥)自动重联,显式脱离集群的节点不自动重联
|
||||
- **准入密钥**:每节点有 nodeKey,新节点加入需提供对端密钥;密钥随令牌环交换,全节点互通
|
||||
- **集群二进制分发**:节点间优先互传 frpc 二进制,外部 URL 兜底
|
||||
|
||||
### 认证与权限
|
||||
|
||||
- **Session-cookie 登录 + Basic Auth**:UI 使用 session-cookie(支持登出),API 保持 Basic Auth + Bearer API Key 兼容
|
||||
- **三级角色**:superadmin(账号管理)、admin(全功能除账号)、viewer(只读状态/集群/审计导出)
|
||||
- **用户管理**:superadmin 角色创建/管理用户,bcrypt 存储,flag 内置账号为 system 只读
|
||||
- **API Key**:read / write / admin 三级 scope,`w4f_` 前缀,sha256 存储,创建时明文仅展示一次
|
||||
- **审计导出**:read 级 view 即可导出 CSV(账号登录记录 / API Key 使用记录 / 集群操作日志 / worker 运行日志)
|
||||
|
||||
## 构建
|
||||
|
||||
### 后端(Go 1.25+)
|
||||
|
||||
```bash
|
||||
go build -o webui4frpc ./cmd/webui4frpc
|
||||
```
|
||||
|
||||
### 前端(Node 20+)
|
||||
|
||||
```bash
|
||||
cd web && npm install && npm run build
|
||||
```
|
||||
|
||||
前端产物在 `web/dist`,需拷贝到 `internal/httpapi/dist` 以便 Go `embed` 打包进单一二进制:
|
||||
|
||||
```bash
|
||||
rm -rf internal/httpapi/dist && cp -r web/dist internal/httpapi/dist
|
||||
```
|
||||
|
||||
一键全量构建(dist 不进入 git,编译前必须构建前端):
|
||||
|
||||
```bash
|
||||
cd web && npm run build && rm -rf internal/httpapi/dist && cp -r web/dist internal/httpapi/dist && go build -o webui4frpc ./cmd/webui4frpc
|
||||
```
|
||||
|
||||
## 安装
|
||||
|
||||
从 [Release 页](https://gitcode.com/JianFeeeee/webui4frpc/releases/latest) 下载对应平台的安装包。
|
||||
|
||||
### Linux (Debian / Ubuntu)
|
||||
|
||||
```bash
|
||||
sudo dpkg -i webui4frpc-0.1.1-linux-amd64.deb # 或 linux-arm64.deb
|
||||
sudo nano /etc/webui4frpc/webui4frpc.env # 改掉默认密码
|
||||
sudo systemctl restart webui4frpc
|
||||
```
|
||||
|
||||
### Linux (RHEL / Fedora / openEuler / 默认 / 龙蜥)
|
||||
|
||||
```bash
|
||||
sudo rpm -i webui4frpc-0.1.1-linux-x86_64.rpm
|
||||
sudo vi /etc/webui4frpc/webui4frpc.env
|
||||
sudo systemctl enable --now webui4frpc
|
||||
```
|
||||
|
||||
安装后自动建 `webui4frpc` 系统账号、注册 systemd 单元,数据目录在 `/var/lib/webui4frpc`。
|
||||
服务以非 root 运行,并启用 `ProtectSystem=full` / `NoNewPrivileges` 等加固项。
|
||||
|
||||
### Windows
|
||||
|
||||
双击 `webui4frpc-0.1.1-windows-amd64-setup.exe`(ARM 设备用 `windows-arm64-setup.exe`)。
|
||||
安装向导会询问监听地址与管理员凭据,完成后:
|
||||
|
||||
- 程序装到 `C:\Program Files\webui4frpc`
|
||||
- 开始菜单生成启动快捷方式与卸载项
|
||||
- 自动添加防火墙入站规则
|
||||
|
||||
### macOS
|
||||
|
||||
```bash
|
||||
sudo installer -pkg webui4frpc-0.1.1-darwin-arm64.pkg -target / # Apple Silicon
|
||||
sudo installer -pkg webui4frpc-0.1.1-darwin-amd64.pkg -target / # Intel
|
||||
```
|
||||
|
||||
或直接双击 `.pkg` 走图形安装器。安装后 launchd 开机自启,配置在
|
||||
`/usr/local/etc/webui4frpc/webui4frpc.env`,改完执行:
|
||||
|
||||
```bash
|
||||
sudo launchctl kickstart -k system/com.jianfeeeee.webui4frpc
|
||||
```
|
||||
|
||||
> 该 `.pkg` 未经 Apple 公证(notarization)。首次打开若被 Gatekeeper 拦下,
|
||||
> 在「系统设置 → 隐私与安全」选择仍要打开。
|
||||
|
||||
### 免安装(tar.gz / zip)
|
||||
|
||||
不想装服务只想跑一下,用对应的 `.tar.gz` / `.zip`,解压即用:
|
||||
|
||||
```bash
|
||||
tar xzf webui4frpc-0.1.1-linux-amd64.tar.gz
|
||||
./webui4frpc -addr 127.0.0.1:7500 -user admin -password 你的密码 -workdir ./data
|
||||
```
|
||||
|
||||
下载后可用 `SHA256SUMS` 校验完整性:
|
||||
|
||||
```bash
|
||||
sha256sum -c SHA256SUMS --ignore-missing
|
||||
```
|
||||
|
||||
## 运行
|
||||
|
||||
从源码构建后直接跑:
|
||||
|
||||
```bash
|
||||
./webui4frpc -addr 127.0.0.1:7500 -user admin -password admin123 -workdir ./data
|
||||
```
|
||||
|
||||
打开 <http://127.0.0.1:7500/> ,输入账号密码进入画布。首次使用可到「设置」页一键安装 frpc(或手动指定路径),然后回到画布创建 local / remote 并连线、保存。
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数 | 默认 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `-addr` | `127.0.0.1:7500` | 监听地址 |
|
||||
| `-user` / `-password` | `admin` / `admin` | Basic 认证(自动同步为 system 管理员账号) |
|
||||
| `-workdir` | `./webui-frpc` | 数据目录(db / 配置 / 日志 / bin) |
|
||||
| `-bin` | 空 | 初始 frpc 二进制路径(可选,首次启动写入 settings) |
|
||||
| `-frpc` | 空 | worker 默认 frpc 路径(settings 无值时的回退) |
|
||||
| `-peer` | 空 | 集群对端地址 `host:port`(可重复,仅首次引导加入用) |
|
||||
| `-join-key` | `$W4F_JOIN_KEY` | 集群准入密钥 = 对端节点的 nodeKey(与 `-peer` 配合使用) |
|
||||
|
||||
环境变量 `W4F_HOST` 可覆盖节点对外可达的主机名(Docker DNS / 通配监听场景用)。
|
||||
|
||||
### 启动行为
|
||||
|
||||
节点启动时的集群引导优先级:
|
||||
|
||||
1. **缓存拓扑优先**:读取持久化的 `ClusterPeers`(宕机前最后令牌周期保存的对端列表),逐个尝试 rejoin
|
||||
2. **`-peer` 引导**:无缓存拓扑时,用 `-peer` + `-join-key` 加入指定对端(10s 重试、5min 超时)
|
||||
3. **新建集群**:无缓存无 `-peer` → `CreateCluster` 成为独立 leader
|
||||
|
||||
显式脱离集群(detachAsStandalone)会清除 `ClusterPeers` → 重启后不自动重联。
|
||||
|
||||
## 数据目录
|
||||
|
||||
```
|
||||
<workdir>
|
||||
├── manager.db # SQLite(locals / remotes / links / settings / users / api_keys)
|
||||
├── configs/<local>~<remote>~<port>.json # 每条转发的独立 frpc 配置(per-forward 模型)
|
||||
├── logs/<local>~<remote>~<port>.log # 每条转发的独立 worker 日志(按大小轮转)
|
||||
└── bin/frpc-<v>/frpc # 一键安装的 frpc
|
||||
```
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
webui4frpc(单二进制,无 frp 依赖,前端 go:embed)
|
||||
├── cmd/webui4frpc 入口:flag 解析 → store/process/cluster 装配 → HTTP 服务 + 生命周期
|
||||
├── internal/
|
||||
│ ├── store SQLite 持久化(locals / remotes / links / settings / users / api_keys / sessions)
|
||||
│ ├── canvas 画布模型(local / remote / link 多对多)
|
||||
│ ├── render 渲染 frpc JSON 配置(per-forward 单 proxy + 高级参数 + LB/健康检查)
|
||||
│ ├── process worker 进程管理(spawn/stop/restart/自愈/日志轮转,worker 键 = local~remote~port)
|
||||
│ ├── install 一键下载官方 frpc(GitHub Releases,含解压安全防护)
|
||||
│ ├── cluster 令牌环协议(ring engine / leader 选举 / 容错 / 宕机重联 / 二进制分发 / NIC 负载采样)
|
||||
│ └── httpapi REST API + 静态资源(auth/session / canvas / forwards / cluster / users / audit 导出)
|
||||
└── web/ Vue 3 SPA(Element Plus + VueFlow 画布 + Pinia)
|
||||
└── src/views/ 状态总览 / 连接配置(画布)/ 集群 / 设置 / 账号与密钥
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
所有接口前缀 `/api/manager`。UI 使用 session-cookie 登录(`/login` `/logout`),API 兼容 Basic Auth 或 Bearer API Key。权限分 read / write / admin 三级,角色 superadmin / admin / viewer 映射到不同 UI 可见性与操作能力。
|
||||
|
||||
| 类别 | 方法 | 路径 | 权限 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **状态** | GET | `/status` | read | 总览(版本/服务/节点/转发/profiles) |
|
||||
| **画布** | GET/PUT | `/canvas` | read/write | 读写完整画布 |
|
||||
| | GET | `/canvas/export` | read | 导出画布备份 |
|
||||
| | POST | `/canvas/import` | write | 导入画布 |
|
||||
| **转发** | POST | `/forwards/start\|stop` | write | 启停单条转发 |
|
||||
| | POST | `/forwards/group/start\|stop` | write | 启停整组 |
|
||||
| | POST | `/forwards/assign` | write | 修改转发分组 |
|
||||
| | POST | `/forwards/group/delete` | write | 删除分组 |
|
||||
| **单资源** | PUT/DELETE | `/locals[/{name}]` | write | 增删 local |
|
||||
| | PUT/DELETE | `/remotes[/{name}]` | write | 增删 remote |
|
||||
| | POST/DELETE | `/links[/{id}]` | write | 增删 link |
|
||||
| **Profile** | POST | `/profiles/{name}/start\|stop\|restart` | write | worker 启停重启 |
|
||||
| | GET | `/profiles/{name}/config\|logs` | read | 配置/日志 |
|
||||
| **设置** | GET/PUT | `/settings` | read/write | 运行策略 |
|
||||
| | GET | `/binary/status` | read | frpc 路径 |
|
||||
| | POST | `/binary/install` | write | 安装 frpc |
|
||||
| **集群** | GET | `/cluster/ring` | read | 令牌环快照 |
|
||||
| | POST | `/cluster/create` | write | 创建独立集群 |
|
||||
| | POST | `/cluster/join-ring` | write | 加入集群(本节点发起) |
|
||||
| | POST | `/cluster/join` | write | 接收新节点加入 |
|
||||
| | POST | `/cluster/task` | write | 提交转发任务到环 |
|
||||
| | POST | `/cluster/node-remove` | write | 移除节点 |
|
||||
| | POST | `/cluster/token` | write | 令牌中继 + 心跳 |
|
||||
| | GET | `/cluster/nodes` | read | 集群节点 + 缓存 |
|
||||
| | GET/POST | `/cluster/cache` | read/write | 二进制缓存管理 |
|
||||
| | GET | `/node/logs` | read | 本节点 worker 日志 |
|
||||
| | GET | `/cluster/logs/export` | read | 导出全节点 worker 日志(JSON bundle) |
|
||||
| **审计导出** | GET | `/audit/users.csv` | read | 账号清单 CSV(含最后登录) |
|
||||
| | GET | `/audit/apikeys.csv` | read | API Key 清单 CSV(含最后使用/过期) |
|
||||
| | GET | `/audit/cluster-log.csv` | read | 集群操作日志 CSV(时间线 + 原始载荷) |
|
||||
| | GET | `/audit/worker-logs.csv` | read | 全节点 worker 运行日志 CSV(逐行展开) |
|
||||
| | GET | `/frpc/{version}` | read | 节点间 frpc 二进制分发 |
|
||||
| **认证** | POST | `/login` | 无 | 登录(表单凭证 → session cookie) |
|
||||
| | POST | `/logout` | 无 | 登出(清除 session) |
|
||||
| | GET | `/me` | read | 当前身份 |
|
||||
| | GET/POST | `/users` | admin | 用户列表/创建 |
|
||||
| | PUT/DELETE | `/users/{name}` | admin | 修改/删除用户 |
|
||||
| | GET/POST | `/apikeys` | admin | API Key 列表/创建 |
|
||||
| | DELETE | `/apikeys/{id}` | admin | 吊销 API Key |
|
||||
|
||||
> 集群管理 API 的详细说明(请求体、响应格式、错误码、示例)见 [docs/cluster-api.md](docs/cluster-api.md)。
|
||||
|
||||
## 集群模式
|
||||
|
||||
多个 webui4frpc 节点可组成令牌环集群协同工作:
|
||||
|
||||
- **创建集群**:首个节点 `CreateCluster` 成为 leader,生成准入密钥(nodeKey)
|
||||
- **加入集群**:在集群页复制对端的 nodeKey,填入「加入集群」对话框(对端地址 + 密钥)
|
||||
- **任务分发**:在任意节点画布上配置 `localOnly=false` 的转发并保存 → 画布差异判断生成命令 → 令牌携带 → 负载最低节点摘取 → 拉起 frpc worker
|
||||
- **容错**:节点宕机 → 自动检测 → 环自愈(跳过死亡节点 / leader failover);重启 → 自动重联
|
||||
- **退出集群**:在集群页「退出集群」→ 本节点脱离为 standalone,不自动重联
|
||||
|
||||
集群页展示:环拓扑(self/leader 标记)、令牌轮次、节点负载、待办任务、活跃转发拓扑(含 owner 归属)、增量事件日志。
|
||||
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
# 后端
|
||||
go test ./...
|
||||
|
||||
# 前端类型检查
|
||||
cd web && npm run type-check
|
||||
```
|
||||
162
packaging/windows/webui4frpc.nsi
Normal file
162
packaging/windows/webui4frpc.nsi
Normal file
@ -0,0 +1,162 @@
|
||||
; webui4frpc Windows 安装程序 (NSIS)
|
||||
;
|
||||
; 由 scripts/build_installers.sh 调用,需在命令行传入:
|
||||
; -DVERSION=0.1.0 -DARCH=amd64 -DBINSRC=<webui4frpc.exe 路径> -DOUTFILE=<输出 exe>
|
||||
|
||||
!include "MUI2.nsh"
|
||||
!include "LogicLib.nsh"
|
||||
|
||||
!ifndef VERSION
|
||||
!define VERSION "0.0.0"
|
||||
!endif
|
||||
!ifndef ARCH
|
||||
!define ARCH "amd64"
|
||||
!endif
|
||||
|
||||
Name "webui4frpc ${VERSION}"
|
||||
OutFile "${OUTFILE}"
|
||||
Unicode true
|
||||
RequestExecutionLevel admin
|
||||
InstallDir "$PROGRAMFILES64\webui4frpc"
|
||||
InstallDirRegKey HKLM "Software\webui4frpc" "InstallDir"
|
||||
|
||||
VIProductVersion "${VERSION}.0"
|
||||
VIAddVersionKey "ProductName" "webui4frpc"
|
||||
VIAddVersionKey "FileDescription" "Visual frpc controller with token-ring cluster"
|
||||
VIAddVersionKey "FileVersion" "${VERSION}"
|
||||
VIAddVersionKey "ProductVersion" "${VERSION}"
|
||||
VIAddVersionKey "LegalCopyright" "JianFeeeee"
|
||||
|
||||
!define MUI_ABORTWARNING
|
||||
!insertmacro MUI_PAGE_LICENSE "LICENSE.txt"
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
Page custom ConfigPage ConfigPageLeave
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
!insertmacro MUI_LANGUAGE "SimpChinese"
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
|
||||
Var Dialog
|
||||
Var LblAddr
|
||||
Var TxtAddr
|
||||
Var LblUser
|
||||
Var TxtUser
|
||||
Var LblPass
|
||||
Var TxtPass
|
||||
Var CfgAddr
|
||||
Var CfgUser
|
||||
Var CfgPass
|
||||
|
||||
Function .onInit
|
||||
StrCpy $CfgAddr "0.0.0.0:7500"
|
||||
StrCpy $CfgUser "admin"
|
||||
StrCpy $CfgPass "admin"
|
||||
FunctionEnd
|
||||
|
||||
Function ConfigPage
|
||||
!insertmacro MUI_HEADER_TEXT "服务配置" "设置监听地址与登录凭据"
|
||||
nsDialogs::Create 1018
|
||||
Pop $Dialog
|
||||
${If} $Dialog == error
|
||||
Abort
|
||||
${EndIf}
|
||||
|
||||
${NSD_CreateLabel} 0 0 100% 12u "监听地址 (host:port):"
|
||||
Pop $LblAddr
|
||||
${NSD_CreateText} 0 14u 100% 12u "$CfgAddr"
|
||||
Pop $TxtAddr
|
||||
|
||||
${NSD_CreateLabel} 0 34u 100% 12u "管理员用户名:"
|
||||
Pop $LblUser
|
||||
${NSD_CreateText} 0 48u 100% 12u "$CfgUser"
|
||||
Pop $TxtUser
|
||||
|
||||
${NSD_CreateLabel} 0 68u 100% 12u "管理员密码(请勿沿用默认值):"
|
||||
Pop $LblPass
|
||||
${NSD_CreatePassword} 0 82u 100% 12u "$CfgPass"
|
||||
Pop $TxtPass
|
||||
|
||||
nsDialogs::Show
|
||||
FunctionEnd
|
||||
|
||||
Function ConfigPageLeave
|
||||
${NSD_GetText} $TxtAddr $CfgAddr
|
||||
${NSD_GetText} $TxtUser $CfgUser
|
||||
${NSD_GetText} $TxtPass $CfgPass
|
||||
FunctionEnd
|
||||
|
||||
Section "webui4frpc" SecMain
|
||||
SetOutPath "$INSTDIR"
|
||||
File "/oname=webui4frpc.exe" "${BINSRC}"
|
||||
File "README.md"
|
||||
File "LICENSE.txt"
|
||||
|
||||
CreateDirectory "$INSTDIR\data"
|
||||
|
||||
; 启动脚本,承载安装时选择的配置
|
||||
FileOpen $0 "$INSTDIR\start-webui4frpc.cmd" w
|
||||
FileWrite $0 "@echo off$\r$\n"
|
||||
FileWrite $0 "rem webui4frpc 启动脚本(由安装程序生成)$\r$\n"
|
||||
FileWrite $0 "cd /d %~dp0$\r$\n"
|
||||
FileWrite $0 'webui4frpc.exe -addr $CfgAddr -user $CfgUser -password $CfgPass -workdir "%~dp0data"$\r$\n'
|
||||
FileClose $0
|
||||
|
||||
WriteRegStr HKLM "Software\webui4frpc" "InstallDir" "$INSTDIR"
|
||||
WriteRegStr HKLM "Software\webui4frpc" "Version" "${VERSION}"
|
||||
|
||||
; 添加/删除程序
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\webui4frpc" \
|
||||
"DisplayName" "webui4frpc ${VERSION}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\webui4frpc" \
|
||||
"UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\webui4frpc" \
|
||||
"DisplayVersion" "${VERSION}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\webui4frpc" \
|
||||
"Publisher" "JianFeeeee"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\webui4frpc" \
|
||||
"URLInfoAbout" "https://gitcode.com/JianFeeeee/webui4frpc"
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\webui4frpc" \
|
||||
"NoModify" 1
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\webui4frpc" \
|
||||
"NoRepair" 1
|
||||
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
|
||||
; 开始菜单
|
||||
CreateDirectory "$SMPROGRAMS\webui4frpc"
|
||||
CreateShortcut "$SMPROGRAMS\webui4frpc\webui4frpc.lnk" \
|
||||
"$INSTDIR\start-webui4frpc.cmd" "" "$INSTDIR\webui4frpc.exe" 0
|
||||
CreateShortcut "$SMPROGRAMS\webui4frpc\打开管理界面.lnk" \
|
||||
"http://127.0.0.1:7500"
|
||||
CreateShortcut "$SMPROGRAMS\webui4frpc\卸载 webui4frpc.lnk" "$INSTDIR\uninstall.exe"
|
||||
|
||||
; 防火墙放行(失败不阻断安装)
|
||||
nsExec::ExecToLog 'netsh advfirewall firewall add rule name="webui4frpc" \
|
||||
dir=in action=allow program="$INSTDIR\webui4frpc.exe" enable=yes profile=any'
|
||||
SectionEnd
|
||||
|
||||
Section "Uninstall"
|
||||
nsExec::ExecToLog 'netsh advfirewall firewall delete rule name="webui4frpc"'
|
||||
|
||||
Delete "$INSTDIR\webui4frpc.exe"
|
||||
Delete "$INSTDIR\start-webui4frpc.cmd"
|
||||
Delete "$INSTDIR\README.md"
|
||||
Delete "$INSTDIR\LICENSE.txt"
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
|
||||
; data 目录保留(含数据库与日志),由用户手动删除
|
||||
MessageBox MB_YESNO|MB_ICONQUESTION \
|
||||
"是否同时删除数据目录 $INSTDIR\data(数据库、转发配置、日志)?" \
|
||||
IDNO KeepData
|
||||
RMDir /r "$INSTDIR\data"
|
||||
KeepData:
|
||||
RMDir "$INSTDIR"
|
||||
|
||||
Delete "$SMPROGRAMS\webui4frpc\*.lnk"
|
||||
RMDir "$SMPROGRAMS\webui4frpc"
|
||||
|
||||
DeleteRegKey HKLM "Software\webui4frpc"
|
||||
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\webui4frpc"
|
||||
SectionEnd
|
||||
131
scripts/build_installers.sh
Executable file
131
scripts/build_installers.sh
Executable file
@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 从 release.sh 的交叉编译产物构建原生安装包:
|
||||
# - linux amd64/arm64 → .deb
|
||||
# - linux amd64/arm64 → .rpm
|
||||
# - windows amd64/arm64 → NSIS .exe 安装器
|
||||
# - darwin amd64/arm64 → macOS .pkg
|
||||
#
|
||||
# 用法: build_installers.sh <version> [arch...]
|
||||
# arch 默认: linux amd64 arm64
|
||||
#
|
||||
# 在 release.sh 流程内被调用;依赖 dist/artifacts/<version>/bin/<os>-<arch>/ 下的二进制。
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${1:-0.1.1}"
|
||||
VERSION="${VERSION#v}"
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
OUT="$ROOT/dist/artifacts/$VERSION"
|
||||
PKG="$ROOT/packaging"
|
||||
|
||||
# 工具检测(缺工具则给出明确提示后跳过该平台)
|
||||
have() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
build_deb() {
|
||||
local arch="$1" key="$2" bindir
|
||||
bindir="$OUT/bin/linux-$arch"
|
||||
[ -x "$bindir/webui4frpc" ] || { echo " ! 缺二进制 linux-$arch,跳过 deb"; return; }
|
||||
local deb=/tmp/w4f-deb-build; rm -rf "$deb"
|
||||
mkdir -p "$deb/DEBIAN" "$deb/usr/bin" "$deb/etc/webui4frpc" \
|
||||
"$deb/lib/systemd/system" "$deb/usr/share/doc/webui4frpc"
|
||||
# control
|
||||
cat > "$deb/DEBIAN/control" <<EOF
|
||||
Package: webui4frpc
|
||||
Version: $VERSION
|
||||
Section: net
|
||||
Priority: optional
|
||||
Architecture: $arch
|
||||
Maintainer: JianFeeeee <jianfeeeee@gitcode>
|
||||
Description: visual frpc controller with token-ring cluster
|
||||
A web UI to manage FRP forwards across a cluster of nodes. Single
|
||||
statically-linked binary with an embedded Vue frontend. Includes a
|
||||
token-ring based cluster for distributed forward management, NIC network
|
||||
load sampling, and audit-log CSV export.
|
||||
Homepage: https://gitcode.com/JianFeeeee/webui4frpc
|
||||
EOF
|
||||
install -m 0755 "$bindir/webui4frpc" "$deb/usr/bin/webui4frpc"
|
||||
install -m 0644 "$PKG/common/webui4frpc.service" "$deb/lib/systemd/system/webui4frpc.service"
|
||||
install -m 0640 "$PKG/common/webui4frpc.env" "$deb/etc/webui4frpc/webui4frpc.env"
|
||||
install -m 0644 "$ROOT/README.md" "$deb/usr/share/doc/webui4frpc/README.md"
|
||||
cat > "$deb/usr/share/doc/webui4frpc/copyright" <<'EOF'
|
||||
webui4frpc - visual frpc controller
|
||||
Copyright (C) 2026 JianFeeeee
|
||||
SPDX-License-Identifier: MIT
|
||||
EOF
|
||||
install -m 0755 "$PKG/deb/postinst" "$deb/DEBIAN/postinst"
|
||||
install -m 0755 "$PKG/deb/prerm" "$deb/DEBIAN/prerm"
|
||||
install -m 0755 "$PKG/deb/postrm" "$deb/DEBIAN/postrm"
|
||||
echo "/etc/webui4frpc/webui4frpc.env" > "$deb/DEBIAN/conffiles"
|
||||
( cd "$deb" && find usr etc lib -type f -exec md5sum {} \; > DEBIAN/md5sums )
|
||||
dpkg-deb -b --root-owner-group "$deb" "$OUT/webui4frpc-${VERSION}-linux-${arch}.deb" >/dev/null
|
||||
echo " ✔ .deb linux/$arch"
|
||||
}
|
||||
|
||||
build_rpm() {
|
||||
local arch="$1" rarch key="$2" bindir
|
||||
case "$arch" in amd64) rarch=x86_64;; arm64) rarch=aarch64;; *) return;; esac
|
||||
bindir="$OUT/bin/linux-$arch"
|
||||
[ -x "$bindir/webui4frpc" ] || { echo " ! 缺二进制 linux-$arch,跳过 rpm"; return; }
|
||||
local R=/tmp/w4f-rpmbuild; rm -rf "$R"; mkdir -p "$R"/{BUILD,RPMS,SPECS,SOURCES}
|
||||
rpmbuild -bb "$PKG/rpm/webui4frpc.spec" --nodeps --target "$rarch" \
|
||||
--define "_topdir $R" \
|
||||
--define "_w4f_version $VERSION" \
|
||||
--define "_w4f_arch $rarch" \
|
||||
--define "_w4f_bin $bindir/webui4frpc" \
|
||||
--define "_w4f_service $PKG/common/webui4frpc.service" \
|
||||
--define "_w4f_env $PKG/common/webui4frpc.env" \
|
||||
--define "_w4f_readme $ROOT/README.md" \
|
||||
--define "_unitdir /usr/lib/systemd/system" \
|
||||
--define "_sharedstatedir /var/lib" >/tmp/w4f-rpm.log 2>&1 || true
|
||||
cp "$R"/RPMS/$rarch/*.rpm "$OUT/webui4frpc-${VERSION}-linux-${rarch}.rpm" 2>/dev/null \
|
||||
&& echo " ✔ .rpm linux/$rarch" || { echo " ! rpm 失败: $(tail -3 /tmp/w4f-rpm.log)"; }
|
||||
}
|
||||
|
||||
build_nsis() {
|
||||
local arch="$1" key="$2" bindir
|
||||
bindir="$OUT/bin/windows-$arch"
|
||||
[ -f "$bindir/webui4frpc.exe" ] || { echo " ! 缺二进制 windows-$arch,跳过 NSIS"; return; }
|
||||
[ -f "$PKG/windows/LICENSE.txt" ] || { echo " ! 缺 $PKG/windows/LICENSE.txt"; return; }
|
||||
cp -f "$ROOT/README.md" "$PKG/windows/README.md"
|
||||
makensis "-DVERSION=$VERSION" "-DARCH=$arch" \
|
||||
"-DBINSRC=$bindir/webui4frpc.exe" \
|
||||
"-DOUTFILE=$OUT/webui4frpc-${VERSION}-windows-${arch}-setup.exe" \
|
||||
"$PKG/windows/webui4frpc.nsi" >/tmp/w4f-nsis.log 2>&1 \
|
||||
&& echo " ✔ NSIS setup.exe windows/$arch" \
|
||||
|| { echo " ! NSIS 失败: $(tail -4 /tmp/w4f-nsis.log)"; }
|
||||
}
|
||||
|
||||
build_pkg() {
|
||||
local arch="$1" key="$2" bindir
|
||||
bindir="$OUT/bin/darwin-$arch"
|
||||
[ -x "$bindir/webui4frpc" ] || { echo " ! 缺二进制 darwin-$arch,跳过 pkg"; return; }
|
||||
"$PKG/macos/build_pkg.sh" "$VERSION" "$arch" \
|
||||
"$bindir/webui4frpc" \
|
||||
"$OUT/webui4frpc-${VERSION}-darwin-${arch}.pkg" >/dev/null \
|
||||
&& echo " ✔ .pkg darwin/$arch" \
|
||||
|| echo " ! macOS pkg 失败 darwin/$arch"
|
||||
}
|
||||
|
||||
ARCHES=("${@:2}")
|
||||
[ ${#ARCHES[@]} -eq 0 ] && ARCHES=(amd64 arm64)
|
||||
|
||||
echo "==> 原生安装包 (v$VERSION):"
|
||||
for arch in "${ARCHES[@]}"; do
|
||||
key="linux-$arch"
|
||||
if have dpkg-deb; then build_deb "$arch" "$key"; else echo " . dpkg-deb 不可用,跳过 deb"; fi
|
||||
if have rpmbuild; then build_rpm "$arch" "$key"; else echo " . rpmbuild 不可用,跳过 rpm"; fi
|
||||
done
|
||||
if have makensis; then
|
||||
for arch in "${ARCHES[@]}"; do build_nsis "$arch" "windows-$arch"; done
|
||||
else
|
||||
echo " . makensis 不可用,跳过 windows setup.exe"
|
||||
fi
|
||||
if have xar && have mkbom && have cpio; then
|
||||
for arch in "${ARCHES[@]}"; do build_pkg "$arch" "darwin-$arch"; done
|
||||
else
|
||||
echo " . xar/mkbom 不可用,跳过 macOS pkg"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "==> 安装包产物:"
|
||||
ls -lh "$OUT"/*.deb "$OUT"/*.rpm "$OUT"/*-setup.exe "$OUT"/*.pkg 2>/dev/null || true
|
||||
150
scripts/release.sh
Executable file
150
scripts/release.sh
Executable file
@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# webui4frpc 多平台发布脚本
|
||||
#
|
||||
# 用法:
|
||||
# ./scripts/release.sh [version] # 默认 version=0.1.1
|
||||
#
|
||||
# 行为:
|
||||
# 1. 构建前端 (web/dist) 并同步到 internal/httpapi/dist (go:embed 用)
|
||||
# 2. 交叉编译全平台单二进制 (linux/windows/darwin × amd64/arm64)
|
||||
# 3. 打包各平台安装包:
|
||||
# - linux/darwin: tar.gz (二进制 + README + systemd 模板)
|
||||
# - windows: zip (exe + README)
|
||||
# 4. 生成 SHA256SUMS
|
||||
# 5. 输出到 dist/artifacts/<version>/
|
||||
#
|
||||
# 环境变量:
|
||||
# GOPROXY_OVERRIDE 覆盖 go module 代理 (默认 goproxy.cn,direct, 走本地 7890 代理)
|
||||
# NO_BUILD_FRONTEND 设为 1 跳过前端构建 (用现有 dist)
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${1:-0.1.1}"
|
||||
VERSION="${VERSION#v}" # 去掉可能的前导 v
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
OUT="$ROOT/dist/artifacts/$VERSION"
|
||||
GOFLAGS_ENV=""
|
||||
|
||||
# ---- 0. Go module 代理 (内网需走本地代理访问 goproxy.cn) ----
|
||||
if [ -n "${GOPROXY_OVERRIDE:-}" ]; then
|
||||
GOFLAGS_ENV="GOPROXY=${GOPROXY_OVERRIDE}"
|
||||
else
|
||||
GOFLAGS_ENV="GOPROXY=https://goproxy.cn,direct"
|
||||
fi
|
||||
# 若本地 7890 代理存在则导出 (模块下载走代理)
|
||||
if curl -s -o /dev/null -m 2 -x http://127.0.0.1:7890 https://goproxy.cn >/dev/null 2>&1; then
|
||||
export HTTPS_PROXY=http://127.0.0.1:7890 HTTP_PROXY=http://127.0.0.1:7890
|
||||
fi
|
||||
|
||||
echo "==> 版本: $VERSION"
|
||||
mkdir -p "$OUT"
|
||||
|
||||
# ---- 1. 构建前端 ----
|
||||
if [ "${NO_BUILD_FRONTEND:-0}" != "1" ]; then
|
||||
echo "==> 构建前端"
|
||||
( cd "$ROOT/web" && npm run build )
|
||||
rm -rf "$ROOT/internal/httpapi/dist"
|
||||
cp -r "$ROOT/web/dist" "$ROOT/internal/httpapi/dist"
|
||||
else
|
||||
echo "==> 跳过前端构建 (用现有 dist)"
|
||||
fi
|
||||
|
||||
# ---- 2. 交叉编译全平台 ----
|
||||
# target 格式: os arch 扩展名 [tar|zip]
|
||||
TARGETS=(
|
||||
"linux amd64" "linux arm64"
|
||||
"windows amd64" "windows arm64"
|
||||
"darwin amd64" "darwin arm64"
|
||||
)
|
||||
|
||||
declare -A PLATFORM_EXT=(
|
||||
[linux-amd64]=tgz [linux-arm64]=tgz
|
||||
[darwin-amd64]=tgz [darwin-arm64]=tgz
|
||||
[windows-amd64]=zip [windows-arm64]=zip
|
||||
)
|
||||
declare -A PLATFORM_BIN=(
|
||||
[linux-amd64]=webui4frpc [linux-arm64]=webui4frpc
|
||||
[darwin-amd64]=webui4frpc [darwin-arm64]=webui4frpc
|
||||
[windows-amd64]=webui4frpc.exe [windows-arm64]=webui4frpc.exe
|
||||
)
|
||||
|
||||
for target in "${TARGETS[@]}"; do
|
||||
os="${target%% *}"; arch="${target##* }"
|
||||
key="$os-$arch"
|
||||
bin="${PLATFORM_BIN[$key]}"
|
||||
# 每个平台编译到独立子目录,避免 darwin/linux 同名二进制互相覆盖
|
||||
bindir="$OUT/bin/$key"
|
||||
mkdir -p "$bindir"
|
||||
echo "==> 编译 $os/$arch"
|
||||
env $GOFLAGS_ENV CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" \
|
||||
go build -trimpath \
|
||||
-ldflags="-s -w -X webui4frpc/internal/version.Version=$VERSION" \
|
||||
-o "$bindir/$bin" "$ROOT/cmd/webui4frpc"
|
||||
done
|
||||
|
||||
# ---- 3. 打包各平台 ----
|
||||
# 整理 systemd 模板
|
||||
cat > "$OUT/webui4frpc.service.tpl" <<'TPL'
|
||||
[Unit]
|
||||
Description=webui4frpc - visual frpc controller (cluster node)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/opt/webui4frpc/webui4frpc -addr 0.0.0.0:7500 -user admin -password admin -workdir /opt/webui4frpc/data
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
LimitNOFILE=65535
|
||||
NoNewPrivileges=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
TPL
|
||||
|
||||
for target in "${TARGETS[@]}"; do
|
||||
os="${target%% *}"; arch="${target##* }"
|
||||
key="$os-$arch"
|
||||
bin="${PLATFORM_BIN[$key]}"
|
||||
ext="${PLATFORM_EXT[$key]}"
|
||||
base="webui4frpc-${VERSION}-${os}-${arch}"
|
||||
|
||||
echo "==> 打包 $key -> $base.$ext"
|
||||
cp "$ROOT/README.md" "$OUT/README.md"
|
||||
bindir="$OUT/bin/$key"
|
||||
# staging 目录: 把二进制 + README (+systemd) 放一起再一次性打包
|
||||
stage="$OUT/.stage/$key"
|
||||
rm -rf "$stage"; mkdir -p "$stage"
|
||||
cp "$bindir/$bin" "$stage/$bin"
|
||||
cp "$OUT/README.md" "$stage/README.md"
|
||||
if [ "$ext" = "zip" ]; then
|
||||
( cd "$stage" && zip -q "$OUT/$base.zip" "$bin" README.md )
|
||||
else
|
||||
cp "$OUT/webui4frpc.service.tpl" "$stage/"
|
||||
( cd "$stage" && tar czf "$OUT/$base.tar.gz" "$bin" README.md webui4frpc.service.tpl )
|
||||
fi
|
||||
done
|
||||
rm -rf "$OUT/.stage"
|
||||
|
||||
# ---- 4. 校验和 ----
|
||||
# 先生成免安装包的校验和;原生安装包构建完后在 4.6 统一重算,
|
||||
# 保证 SHA256SUMS 覆盖 deb/rpm/setup.exe/pkg(否则用户校验会 "no file was verified")。
|
||||
|
||||
# ---- 4.5 原生安装包 (deb/rpm/setup.exe/pkg) ----
|
||||
if [ "${NO_INSTALLERS:-0}" != "1" ]; then
|
||||
echo ""
|
||||
echo "==> 构建原生安装包"
|
||||
"$ROOT/scripts/build_installers.sh" "$VERSION"
|
||||
fi
|
||||
|
||||
# ---- 4.6 校验和(覆盖全部产物)----
|
||||
echo ""
|
||||
echo "==> 生成 SHA256SUMS"
|
||||
( cd "$OUT" && sha256sum *.tar.gz *.zip *.deb *.rpm *-setup.exe *.pkg 2>/dev/null | tee SHA256SUMS )
|
||||
|
||||
# ---- 5. 汇总 ----
|
||||
echo ""
|
||||
echo "==> 发布产物位于: $OUT"
|
||||
ls -lh "$OUT"
|
||||
echo ""
|
||||
echo "==> 完成"
|
||||
77
scripts/upload_assets.py
Executable file
77
scripts/upload_assets.py
Executable file
@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""上传 release 资产到 gitcode(两步:取签名 URL → PUT 到 OBS)。
|
||||
|
||||
用法: upload_assets.py <version> <token> [file...]
|
||||
不传 file 时上传该版本目录下所有 tar.gz/zip 与 SHA256SUMS。
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
REPO = "JianFeeeee/webui4frpc"
|
||||
API = "https://gitcode.com/api/v5/repos"
|
||||
|
||||
|
||||
def get_upload_url(version: str, token: str, filename: str) -> tuple[str, dict]:
|
||||
q = urllib.parse.urlencode({"file_name": filename})
|
||||
url = f"{API}/{REPO}/releases/v{version}/upload_url?{q}"
|
||||
req = urllib.request.Request(url, headers={"private-token": token})
|
||||
with urllib.request.urlopen(req, timeout=20) as r:
|
||||
data = json.loads(r.read())
|
||||
return data["url"], data.get("headers", {})
|
||||
|
||||
|
||||
def put_file(url: str, headers: dict, path: str) -> tuple[int, str]:
|
||||
size = os.path.getsize(path)
|
||||
with open(path, "rb") as f:
|
||||
body = f.read()
|
||||
req = urllib.request.Request(url, data=body, method="PUT")
|
||||
for k, v in headers.items():
|
||||
req.add_header(k, v)
|
||||
req.add_header("Content-Length", str(size))
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=300) as r:
|
||||
return r.status, r.read().decode("utf-8", "replace")[:300]
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, e.read().decode("utf-8", "replace")[:300]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 3:
|
||||
print(__doc__)
|
||||
return 2
|
||||
version, token = sys.argv[1], sys.argv[2]
|
||||
outdir = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
"dist", "artifacts", version,
|
||||
)
|
||||
files = sys.argv[3:] or sorted(
|
||||
f for f in os.listdir(outdir)
|
||||
if f.endswith((".tar.gz", ".zip")) or f == "SHA256SUMS"
|
||||
)
|
||||
failed = 0
|
||||
for name in files:
|
||||
path = os.path.join(outdir, name)
|
||||
if not os.path.isfile(path):
|
||||
print(f"skip (missing): {name}")
|
||||
continue
|
||||
print(f"==> {name} ({os.path.getsize(path)/1048576:.1f} MiB)")
|
||||
try:
|
||||
url, headers = get_upload_url(version, token, name)
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f" upload_url FAILED: {e}")
|
||||
failed += 1
|
||||
continue
|
||||
status, body = put_file(url, headers, path)
|
||||
ok = 200 <= status < 300
|
||||
print(f" PUT -> {status} {'OK' if ok else body}")
|
||||
if not ok:
|
||||
failed += 1
|
||||
print(f"\n{'ALL OK' if failed == 0 else f'{failed} FAILED'}")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -2,7 +2,7 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<title>webui4frpc</title>
|
||||
</head>
|
||||
|
||||
115
web/src/App.vue
115
web/src/App.vue
@ -363,4 +363,119 @@ const currentLabel = computed(() => nav.value.find((n) => n.key === view.value)?
|
||||
.breadcrumb { padding: 12px 16px 6px; }
|
||||
.content { padding: 8px 14px 32px; }
|
||||
}
|
||||
|
||||
/* ---------- narrow screens: sidebar becomes a bottom tab bar ----------
|
||||
窄屏(≤ 720px)下 210px 侧栅会吃掉手机一半宽度,改为:
|
||||
- shell 竖向排列,侧栅沉到底部变成固定 tab bar(图标在上文字在下)
|
||||
- 品牌区/身份区/主题切换折叠进顶部窄条,不占垂直空间
|
||||
- 内容区预留底部安全区 + tab bar 高度,避开 iOS 手势条
|
||||
- 每个 tab 至少 44px 高(触控目标下限) */
|
||||
@media (max-width: 720px) {
|
||||
.app-shell {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 顶部窄条:logo + 身份 + 主题,横向排一行 */
|
||||
.sidebar {
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
order: -1;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
overflow: visible;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--w4f-line);
|
||||
padding-top: max(10px, env(safe-area-inset-top));
|
||||
}
|
||||
.sb-brand {
|
||||
padding: 0;
|
||||
gap: 8px;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.sb-logo { width: 30px; height: 30px; border-radius: 10px; }
|
||||
.sb-brand-text b { font-size: 14.5px; }
|
||||
.sb-brand-text small { display: none; }
|
||||
|
||||
/* 导航从侧栅里抽出来,固定到底部 */
|
||||
.sb-nav {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 40;
|
||||
flex: none;
|
||||
flex-direction: row;
|
||||
gap: 0;
|
||||
padding: 4px 4px max(4px, env(safe-area-inset-bottom));
|
||||
background: var(--w4f-card);
|
||||
border-top: 1px solid var(--w4f-line);
|
||||
backdrop-filter: blur(calc(var(--w4f-glass) * 0.9)) saturate(1.3);
|
||||
-webkit-backdrop-filter: blur(calc(var(--w4f-glass) * 0.9)) saturate(1.3);
|
||||
box-shadow: 0 -4px 20px rgb(0 0 0 / 6%);
|
||||
}
|
||||
.sb-i {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
min-height: 44px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
padding: 5px 2px;
|
||||
border-radius: 11px;
|
||||
font-size: 10.5px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
.sb-i .label {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sb-i.active {
|
||||
/* 底部 tab 不用渐变底,只高亮图标与文字(避免拥挤) */
|
||||
background: var(--w4f-primary-50);
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 身份与主题折进顶条右侧 */
|
||||
.sb-foot {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.sb-identity {
|
||||
margin-bottom: 0;
|
||||
padding: 5px 8px;
|
||||
font-size: 11.5px;
|
||||
max-width: 42vw;
|
||||
}
|
||||
.sb-identity .id-name { max-width: 14vw; }
|
||||
.sb-theme { margin-bottom: 0; }
|
||||
.sb-theme .sw { width: 22px; height: 22px; border-radius: 7px; }
|
||||
|
||||
.main { height: auto; flex: 1; min-height: 0; }
|
||||
.breadcrumb { padding: 10px 14px 4px; font-size: 14px; }
|
||||
.content {
|
||||
padding: 6px 12px 24px;
|
||||
/* 底部 tab bar(44px + padding)+ 安全区 */
|
||||
padding-bottom: calc(64px + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
|
||||
/* 极窄(≤ 380px,iPhone SE / 小屏安卓):进一步压缩顶条 */
|
||||
@media (max-width: 380px) {
|
||||
.sb-brand-text b { font-size: 13px; }
|
||||
.sb-identity { max-width: 38vw; padding: 4px 7px; }
|
||||
.sb-identity .id-level { display: none; }
|
||||
.sb-i { font-size: 9.5px; }
|
||||
.content { padding-left: 10px; padding-right: 10px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1053,4 +1053,47 @@ onBeforeUnmount(() => {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- narrow screens ----------
|
||||
画布本质上需要宽屏拖拽,窄屏下只做到“可用”:
|
||||
- toolbar 七个按钮在 375px 下会溢出,改成标题占行 + 按钮换行平铺
|
||||
- 画布高度给个下限,否则在短屏上只剩一条缝
|
||||
- handle 抬到 16px(手指命中 12px 圆点几乎不可能) */
|
||||
@media (max-width: 720px) {
|
||||
.canvas-editor { gap: 10px; }
|
||||
|
||||
.toolbar {
|
||||
padding: 11px 12px;
|
||||
row-gap: 8px;
|
||||
}
|
||||
.toolbar-left,
|
||||
.toolbar-right {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.toolbar-title {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
font-size: 15.5px;
|
||||
}
|
||||
.toolbar .btn {
|
||||
flex: 1 1 auto;
|
||||
min-height: 38px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
.save-hint { width: 100%; }
|
||||
|
||||
.flow-wrap { min-height: 62vh; }
|
||||
|
||||
/* 触控:连接点与边标签放大,手指才点得到 */
|
||||
:deep(.vue-flow__handle) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
:deep(.vue-flow__edge-label) {
|
||||
padding: 3px 9px;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -560,4 +560,78 @@ onBeforeUnmount(() => {
|
||||
/* dialog form */
|
||||
.form-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; label { width: 72px; font-size: 13px; color: $color-text-secondary; flex-shrink: 0; } }
|
||||
.form-hint { font-size: 12px; color: $color-text-muted; }
|
||||
|
||||
/* ---------- narrow screens ----------
|
||||
窄屏下的主要问题:
|
||||
- ring-node 的 min-width:168px 与环形箭头在 375px 下会横向溢出,且节点横排无意义
|
||||
- nodeKey 是 32 位 hex,单行放不下会撑宽容器
|
||||
- log-row 的 lg-detail max-width:60% + 不换行,窄屏几乎全被省略号吃掉
|
||||
- toolbar / 对话框表单同 StatusView 的问题 */
|
||||
@media (max-width: 720px) {
|
||||
.cluster-page { gap: 10px; }
|
||||
.section { padding: 14px 14px; }
|
||||
|
||||
.hero { padding: 14px 15px; gap: 12px; }
|
||||
.page-title { font-size: 16.5px; }
|
||||
|
||||
/* nodeKey 允许折行,不再撑宽容器 */
|
||||
.nk-value {
|
||||
font-size: 11px;
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
max-width: 100%;
|
||||
}
|
||||
.nk-label { white-space: normal; }
|
||||
|
||||
.kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
||||
|
||||
/* 工具栏按钮平分整行 */
|
||||
.toolbar { padding: 11px 13px; gap: 8px; }
|
||||
.toolbar > .w4f-btn { flex: 1 1 auto; min-height: 40px; }
|
||||
.act-hint { width: 100%; }
|
||||
|
||||
/* 环拓扑改竖向:节点卡占整宽,箭头旋转 90° 指下 */
|
||||
.ring-line { flex-direction: column; align-items: stretch; gap: 6px; }
|
||||
.ring-node { min-width: 0; width: 100%; padding: 11px 12px; }
|
||||
.rn-id { font-size: 12.5px; word-break: break-all; }
|
||||
.ring-arrow {
|
||||
align-self: center;
|
||||
transform: rotate(90deg);
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 日志:详情另起一行完整显示,不再被省略号截掉 */
|
||||
.log-row { row-gap: 3px; font-size: 11.5px; }
|
||||
.lg-detail {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
text-align: left;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
.log-list { max-height: 260px; }
|
||||
|
||||
/* 任务/拓扑行:允许折行,归属另起一行 */
|
||||
.task-row, .topo-row { flex-wrap: wrap; row-gap: 4px; font-size: 12.5px; }
|
||||
.tp-owner { margin-left: 0; width: 100%; }
|
||||
|
||||
.title-actions { margin-left: 0; width: 100%; }
|
||||
.title-actions > .w4f-btn { flex: 1 1 0; min-height: 36px; }
|
||||
.section-title { flex-wrap: wrap; row-gap: 8px; }
|
||||
|
||||
/* 对话框表单:标签在上、输入在下 */
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 5px;
|
||||
label { width: auto; }
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 380px) {
|
||||
.kpis { grid-template-columns: minmax(0, 1fr); }
|
||||
.page-title { font-size: 15.5px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -321,4 +321,21 @@ onMounted(load)
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
/* ---------- narrow screens ----------
|
||||
binary-row 把输入框与安装按钮横排,窄屏下路径输入框被挤到只剩几十像素;
|
||||
setting-row 的 space-between 在窄屏也会把说明文字与开关挤在一起。 */
|
||||
@media (max-width: 720px) {
|
||||
.binary-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.binary-row .btn { min-height: 40px; justify-content: center; }
|
||||
|
||||
.setting-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -638,4 +638,91 @@ onBeforeUnmount(() => {
|
||||
.form-row { display: flex; align-items: center; gap: 10px; margin-bottom: 12px;
|
||||
label { width: 72px; font-size: 13px; color: $color-text-secondary; flex-shrink: 0; }
|
||||
}
|
||||
|
||||
/* ---------- narrow screens ----------
|
||||
窄屏下的主要问题:
|
||||
- card-grid 的 minmax(320px) 在 375px 屏上会溢出(加上内边距超宽)
|
||||
- topbar 的 space-between 把标题和按钮挤在一行
|
||||
- fwd-head 一行塞下 7~8 个元素(类型/点/路由/协议/分组/归属/按钮)
|
||||
- 分组标题的三个按钮 margin-left:auto 后被挤成细条
|
||||
- 触控目标(small 按钮)低于 44px */
|
||||
@media (max-width: 720px) {
|
||||
.status-page { gap: 10px; }
|
||||
|
||||
/* 标题与工具栏换行,按钮占整行平分 */
|
||||
.topbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.tb-right { width: 100%; }
|
||||
.tb-right > .w4f-btn { flex: 1 1 0; min-height: 40px; }
|
||||
|
||||
/* KPI 固定两列(auto-fit 在窄屏会变单列,四张卡拉得太长) */
|
||||
.kpis {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 卡片网格改单列:消除 320px 最小宽导致的横向溢出 */
|
||||
.card-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 分组标题:标题一行,操作按钮另起一行平分 */
|
||||
.section-title {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 8px;
|
||||
}
|
||||
.grp-actions {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
gap: 6px;
|
||||
}
|
||||
.grp-actions > .w4f-btn { flex: 1 1 0; min-height: 38px; font-size: 12px; }
|
||||
|
||||
/* 转发卡:路由占整行,其余属性换行,按钮另起一行 */
|
||||
.fwd-card { padding: 11px 12px; }
|
||||
.fwd-head { row-gap: 7px; }
|
||||
.fwd-route {
|
||||
order: -1;
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 2px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.fwd-owner { max-width: 100%; }
|
||||
.fwd-actions {
|
||||
width: 100%;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.fwd-actions > .w4f-btn { flex: 1 1 0; min-height: 38px; }
|
||||
|
||||
/* 远程节点卡:名称占行,操作按钮平分 */
|
||||
.node-card { padding: 14px 15px; }
|
||||
.nc-name { width: 100%; margin-right: 0; }
|
||||
.nc-actions { width: 100%; }
|
||||
.nc-actions > .w4f-btn { flex: 1 1 0; min-height: 38px; }
|
||||
.nc-cluster-note { max-width: 100%; }
|
||||
.meta-pill { max-width: 100%; }
|
||||
.np-err { max-width: 45%; }
|
||||
|
||||
/* 对话框表单:标签在上、输入在下(窄屏下 72px 标签挤死输入框) */
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 5px;
|
||||
label { width: auto; }
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 380px) {
|
||||
/* 极窄:KPI 也降为单列,否则数字会被压断 */
|
||||
.kpis { grid-template-columns: minmax(0, 1fr); }
|
||||
.fwd-route { font-size: 12.5px; }
|
||||
.page-title { font-size: 16px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -362,4 +362,37 @@ const onDeleteKey = async (k: ApiKey) => {
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ---------- narrow screens ----------
|
||||
两张 el-table 列实宽加起来 ~900px,窄屏必然需要横向滚动;
|
||||
Element Plus 自己会处理表内滚动,但卡片内边距与标题行需要压缩,
|
||||
否则“导出 CSV / 新建”两个按钮会把标题挤出容器。 */
|
||||
@media (max-width: 720px) {
|
||||
.users-view { gap: 14px; }
|
||||
.card { padding: 13px 13px; border-radius: 14px; }
|
||||
|
||||
.card-h {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 9px;
|
||||
}
|
||||
.card-h .h-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
/* Element 按钮在窄屏平分整行,并抬到 40px 触控高度 */
|
||||
.card-h .h-actions :deep(.el-button) {
|
||||
flex: 1 1 0;
|
||||
min-height: 40px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* 表格横向滚动时给个视觉提示,并保证不把卡片撑宽 */
|
||||
:deep(.el-table) {
|
||||
font-size: 12.5px;
|
||||
}
|
||||
:deep(.el-table__body-wrapper) {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user