diff --git a/go-backend/internal/http/handler/handler.go b/go-backend/internal/http/handler/handler.go
index 3bd4170..03530d5 100644
--- a/go-backend/internal/http/handler/handler.go
+++ b/go-backend/internal/http/handler/handler.go
@@ -1185,6 +1185,7 @@ func (h *Handler) backupExport(w http.ResponseWriter, r *http.Request) {
type backupImportRequest struct {
Types []string `json:"types"`
+ sqlite.BackupData
}
func (h *Handler) backupImport(w http.ResponseWriter, r *http.Request) {
@@ -1210,13 +1211,12 @@ func (h *Handler) backupImport(w http.ResponseWriter, r *http.Request) {
return
}
- var backup sqlite.BackupData
- if err := decodeJSON(r.Body, &backup); err != nil {
+ if req.BackupData.Version == "" {
response.WriteJSON(w, response.Err(500, "备份数据格式错误"))
return
}
- result, err := h.repo.Import(&backup, req.Types)
+ result, err := h.repo.Import(&req.BackupData, req.Types)
if err != nil {
response.WriteJSON(w, response.Err(-2, fmt.Sprintf("导入失败: %v", err)))
return
diff --git a/vite-frontend/package.json b/vite-frontend/package.json
index b0b2eaf..aea3441 100644
--- a/vite-frontend/package.json
+++ b/vite-frontend/package.json
@@ -40,7 +40,7 @@
"@heroui/system": "2.4.19",
"@heroui/table": "^2.2.24",
"@heroui/tabs": "^2.2.27",
- "@heroui/theme": "2.4.19",
+ "@heroui/theme": "2.4.24",
"@heroui/use-theme": "2.1.10",
"@marsidev/react-turnstile": "^1.1.0",
"@nextui-org/system": "^2.4.6",
diff --git a/vite-frontend/src/api/index.ts b/vite-frontend/src/api/index.ts
index 265cc00..7abb7f7 100644
--- a/vite-frontend/src/api/index.ts
+++ b/vite-frontend/src/api/index.ts
@@ -1,3 +1,5 @@
+import axios from "axios";
+
import Network from "./network";
// 登陆相关接口
@@ -42,9 +44,17 @@ export const checkNodeStatus = (nodeId?: number) => {
};
export const upgradeNode = (id: number, version?: string) =>
- Network.post("/node/upgrade", { id, version: version || "" }, { timeout: 5 * 60 * 1000 });
+ Network.post(
+ "/node/upgrade",
+ { id, version: version || "" },
+ { timeout: 5 * 60 * 1000 },
+ );
export const batchUpgradeNodes = (ids: number[], version?: string) =>
- Network.post("/node/batch-upgrade", { ids, version: version || "" }, { timeout: 15 * 60 * 1000 });
+ Network.post(
+ "/node/batch-upgrade",
+ { ids, version: version || "" },
+ { timeout: 15 * 60 * 1000 },
+ );
export const getNodeReleases = () => Network.post("/node/releases");
export const rollbackNode = (id: number) =>
Network.post("/node/rollback", { id });
@@ -224,12 +234,8 @@ export const resetPeerShareFlow = (id: number) =>
Network.post("/federation/share/reset-flow", { id });
export const getPeerRemoteUsageList = () =>
Network.post("/federation/share/remote-usage/list");
-export const importRemoteNode = (data: {
- remoteUrl: string;
- token: string;
-}) => Network.post("/federation/node/import", data);
-
-import axios from "axios";
+export const importRemoteNode = (data: { remoteUrl: string; token: string }) =>
+ Network.post("/federation/node/import", data);
export interface BackupTypes {
users?: boolean;
@@ -247,19 +253,25 @@ export interface BackupTypes {
export const exportBackup = async (types: string[] = []) => {
const token = window.localStorage.getItem("token");
const baseURL = axios.defaults.baseURL || "/api/v1/";
-
- const response = await axios.post(`${baseURL}/backup/export`, { types }, {
- headers: {
- Authorization: token,
- "Content-Type": "application/json",
+
+ const response = await axios.post(
+ `${baseURL}/backup/export`,
+ { types },
+ {
+ headers: {
+ Authorization: token,
+ "Content-Type": "application/json",
+ },
+ responseType: "blob",
},
- responseType: "blob",
- });
+ );
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement("a");
+
link.href = url;
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:-]/g, "");
+
link.setAttribute("download", `backup_${timestamp}.json`);
document.body.appendChild(link);
link.click();
@@ -267,7 +279,5 @@ export const exportBackup = async (types: string[] = []) => {
window.URL.revokeObjectURL(url);
};
-export const importBackup = (data: {
- types: string[];
- [key: string]: any;
-}) => Network.post("/backup/import", data);
+export const importBackup = (data: { types: string[]; [key: string]: any }) =>
+ Network.post("/backup/import", data);
diff --git a/vite-frontend/src/pages/config.tsx b/vite-frontend/src/pages/config.tsx
index 71bc198..163931a 100644
--- a/vite-frontend/src/pages/config.tsx
+++ b/vite-frontend/src/pages/config.tsx
@@ -342,6 +342,7 @@ export default function ConfigPage() {
const handleExport = async () => {
if (exportTypes.length === 0) {
toast.error("请至少选择一种数据类型");
+
return;
}
setExporting(true);
@@ -357,10 +358,12 @@ export default function ConfigPage() {
const handleFileChange = async (e: React.ChangeEvent) => {
const file = e.target.files?.[0];
+
if (!file) return;
if (importTypes.length === 0) {
toast.error("请先选择要导入的数据类型");
+
return;
}
@@ -512,13 +515,13 @@ export default function ConfigPage() {
选择要导出的数据类型,导出为 JSON 格式文件
-
+
setExportTypes(values as string[])}
>
@@ -561,10 +564,7 @@ export default function ConfigPage() {
>
全选
-
setImportTypes(values as string[])}
>
@@ -601,18 +601,18 @@ export default function ConfigPage() {
fileInputRef.current?.click()}
>
{importing ? "导入中..." : "选择文件导入"}
diff --git a/vite-frontend/src/pages/index.tsx b/vite-frontend/src/pages/index.tsx
index 8839d2c..f7f02f5 100644
--- a/vite-frontend/src/pages/index.tsx
+++ b/vite-frontend/src/pages/index.tsx
@@ -83,8 +83,9 @@ export default function IndexPage() {
if (response.code !== 0) {
toast.error(response.msg || "登录失败");
if (showCaptcha) {
- setForm((prev) => ({ ...prev, captchaId: "" }));
+ setForm((prev) => ({ ...prev, captchaId: "" }));
}
+
return;
}
@@ -138,11 +139,7 @@ export default function IndexPage() {
} else {
const configResp = await getConfigByName("cloudflare_site_key");
- if (
- configResp.code === 0 &&
- configResp.data &&
- configResp.data.value
- ) {
+ if (configResp.code === 0 && configResp.data && configResp.data.value) {
setSiteKey(configResp.data.value);
setShowCaptcha(true);
} else {
@@ -241,8 +238,10 @@ export default function IndexPage() {
{showCaptcha && siteKey && (
{/* 背景遮罩层 - 模糊效果,暗黑模式下更深 */}
-
{
setShowCaptcha(false);
setLoading(false);
@@ -253,32 +252,35 @@ export default function IndexPage() {
setLoading(false);
}
}}
- role="button"
- tabIndex={0}
/>
{/* 验证码容器 */}
-
请完成安全验证
+
+ 请完成安全验证
+
{
- setForm((prev) => ({ ...prev, captchaId: token }));
- void performLogin(token);
+ options={{
+ theme: (document.documentElement.classList.contains(
+ "dark",
+ ) ||
+ document.documentElement.getAttribute("data-theme") ===
+ "dark" ||
+ window.matchMedia("(prefers-color-scheme: dark)").matches
+ ? "dark"
+ : "light") as "light" | "dark" | "auto",
}}
+ siteKey={siteKey}
onError={() => {
toast.error("验证失败,请刷新重试");
setLoading(false);
}}
onExpire={() => {
- setForm((prev) => ({ ...prev, captchaId: "" }));
+ setForm((prev) => ({ ...prev, captchaId: "" }));
}}
- options={{
- theme: (document.documentElement.classList.contains("dark") ||
- document.documentElement.getAttribute("data-theme") === "dark" ||
- window.matchMedia("(prefers-color-scheme: dark)").matches
- ? "dark"
- : "light") as "light" | "dark" | "auto"
+ onSuccess={(token) => {
+ setForm((prev) => ({ ...prev, captchaId: token }));
+ void performLogin(token);
}}
/>
diff --git a/vite-frontend/src/pages/node.tsx b/vite-frontend/src/pages/node.tsx
index 14b6f97..fe28bda 100644
--- a/vite-frontend/src/pages/node.tsx
+++ b/vite-frontend/src/pages/node.tsx
@@ -125,7 +125,12 @@ const SortableItem = ({
};
return (
-
+
{children(listeners)}
);
@@ -174,13 +179,26 @@ export default function NodePage() {
// 升级相关状态
const [upgradeModalOpen, setUpgradeModalOpen] = useState(false);
- const [upgradeTarget, setUpgradeTarget] = useState<"single" | "batch">("single");
- const [upgradeTargetNodeId, setUpgradeTargetNodeId] = useState
(null);
- const [releases, setReleases] = useState>([]);
+ const [upgradeTarget, setUpgradeTarget] = useState<"single" | "batch">(
+ "single",
+ );
+ const [upgradeTargetNodeId, setUpgradeTargetNodeId] = useState(
+ null,
+ );
+ const [releases, setReleases] = useState<
+ Array<{
+ version: string;
+ name: string;
+ publishedAt: string;
+ prerelease: boolean;
+ }>
+ >([]);
const [releasesLoading, setReleasesLoading] = useState(false);
const [selectedVersion, setSelectedVersion] = useState("");
const [batchUpgradeLoading, setBatchUpgradeLoading] = useState(false);
- const [upgradeProgress, setUpgradeProgress] = useState>({});
+ const [upgradeProgress, setUpgradeProgress] = useState<
+ Record
+ >({});
const websocketRef = useRef(null);
const reconnectTimerRef = useRef(null);
@@ -237,7 +255,11 @@ export default function NodePage() {
const nodesData: Node[] = (res.data || []).map((node: any) => ({
...node,
inx: node.inx ?? 0,
- connectionStatus: node.syncError ? "offline" : node.status === 1 ? "online" : "offline",
+ connectionStatus: node.syncError
+ ? "offline"
+ : node.status === 1
+ ? "online"
+ : "offline",
syncError: node.syncError || undefined,
systemInfo: null,
copyLoading: false,
@@ -444,7 +466,11 @@ export default function NodePage() {
);
} else if (type === "upgrade_progress") {
try {
- const progressData = typeof messageData === "string" ? JSON.parse(messageData) : messageData;
+ const progressData =
+ typeof messageData === "string"
+ ? JSON.parse(messageData)
+ : messageData;
+
if (progressData?.data) {
setUpgradeProgress((prev) => ({
...prev,
@@ -803,10 +829,11 @@ export default function NodePage() {
}
};
-
-
// 打开版本选择弹窗
- const openUpgradeModal = async (target: "single" | "batch", nodeId?: number) => {
+ const openUpgradeModal = async (
+ target: "single" | "batch",
+ nodeId?: number,
+ ) => {
setUpgradeTarget(target);
setUpgradeTargetNodeId(nodeId || null);
setSelectedVersion("");
@@ -814,6 +841,7 @@ export default function NodePage() {
setReleasesLoading(true);
try {
const res = await getNodeReleases();
+
if (res.code === 0 && Array.isArray(res.data)) {
setReleases(res.data);
} else {
@@ -829,16 +857,21 @@ export default function NodePage() {
// 确认升级(从版本弹窗)
const handleConfirmUpgrade = async () => {
const version = selectedVersion || undefined;
+
if (upgradeTarget === "single" && upgradeTargetNodeId) {
setUpgradeModalOpen(false);
// Find the node
const node = nodeList.find((n) => n.id === upgradeTargetNodeId);
+
if (!node) return;
setNodeList((prev) =>
- prev.map((n) => (n.id === upgradeTargetNodeId ? { ...n, upgradeLoading: true } : n)),
+ prev.map((n) =>
+ n.id === upgradeTargetNodeId ? { ...n, upgradeLoading: true } : n,
+ ),
);
try {
const res = await upgradeNode(upgradeTargetNodeId, version);
+
if (res.code === 0) {
toast.success(`节点升级命令已发送,节点将自动重启`);
} else {
@@ -848,7 +881,9 @@ export default function NodePage() {
toast.error("网络错误,请重试");
} finally {
setNodeList((prev) =>
- prev.map((n) => (n.id === upgradeTargetNodeId ? { ...n, upgradeLoading: false } : n)),
+ prev.map((n) =>
+ n.id === upgradeTargetNodeId ? { ...n, upgradeLoading: false } : n,
+ ),
);
}
} else if (upgradeTarget === "batch") {
@@ -856,6 +891,7 @@ export default function NodePage() {
setUpgradeModalOpen(false);
try {
const res = await batchUpgradeNodes(Array.from(selectedIds), version);
+
if (res.code === 0) {
toast.success(`批量升级命令已发送到 ${selectedIds.size} 个节点`);
} else {
@@ -876,6 +912,7 @@ export default function NodePage() {
);
try {
const res = await rollbackNode(node.id);
+
if (res.code === 0) {
toast.success(`节点 ${node.name} 回退命令已发送,节点将自动重启`);
} else {
@@ -885,7 +922,9 @@ export default function NodePage() {
toast.error("网络错误,请重试");
} finally {
setNodeList((prev) =>
- prev.map((n) => (n.id === node.id ? { ...n, rollbackLoading: false } : n)),
+ prev.map((n) =>
+ n.id === node.id ? { ...n, rollbackLoading: false } : n,
+ ),
);
}
};
@@ -1251,332 +1290,349 @@ export default function NodePage() {
return (
{(listeners) => (
-
-
-
-
- {selectMode && (
- toggleSelect(node.id)}
- />
- )}
-
- {node.name}
-
-
-
-
-
+
+
+
+
+ {selectMode && (
+ toggleSelect(node.id)}
+ />
+ )}
+
+ {node.name}
+
- {isRemoteNode && (
+
+
+ {isRemoteNode && (
+
+ 远程
+
+ )}
- 远程
+ {node.connectionStatus === "online"
+ ? "在线"
+ : "离线"}
- )}
-
- {node.connectionStatus === "online"
- ? "在线"
- : "离线"}
-
-
-
-
-
-
- {isRemoteNode && node.syncError && (
-
- {node.syncError === "provider_share_deleted"
- ? "提供方已删除该分享"
- : node.syncError === "provider_share_disabled"
- ? "提供方已禁用该分享"
- : node.syncError === "provider_share_expired"
- ? "提供方分享已过期"
- : `远程同步失败: ${node.syncError}`}
-
- )}
- {/* 基础信息 */}
-
-
-
- IP
-
-
- {node.serverIpV4?.trim() ||
- node.serverIpV6?.trim() ? (
-
- {node.serverIpV4?.trim() && (
-
- {node.serverIpV4.trim()}
-
- )}
- {node.serverIpV6?.trim() && (
-
- {node.serverIpV6.trim()}
-
- )}
-
- ) : (
-
- {node.serverIp.trim()}
-
- )}
+
+
+
+ {isRemoteNode && node.syncError && (
+
+ {node.syncError === "provider_share_deleted"
+ ? "提供方已删除该分享"
+ : node.syncError === "provider_share_disabled"
+ ? "提供方已禁用该分享"
+ : node.syncError === "provider_share_expired"
+ ? "提供方分享已过期"
+ : `远程同步失败: ${node.syncError}`}
+
+ )}
+ {/* 基础信息 */}
+
+
+
+ IP
+
+
+ {node.serverIpV4?.trim() ||
+ node.serverIpV6?.trim() ? (
+
+ {node.serverIpV4?.trim() && (
+
+ {node.serverIpV4.trim()}
+
+ )}
+ {node.serverIpV6?.trim() && (
+
+ {node.serverIpV6.trim()}
+
+ )}
+
+ ) : (
+
+ {node.serverIp.trim()}
+
+ )}
+
+
+ {!isRemoteNode && (
+ <>
+
+ 版本
+
+ {node.version || "未知"}
+
+
+ {upgradeProgress[node.id] &&
+ upgradeProgress[node.id].percent < 100 && (
+
+ )}
+
+
+ 开机时间
+
+
+ {node.connectionStatus === "online" &&
+ node.systemInfo
+ ? formatUptime(node.systemInfo.uptime)
+ : "-"}
+
+
+ >
+ )}
+
+
{!isRemoteNode && (
<>
-
- 版本
-
- {node.version || "未知"}
-
-
- {upgradeProgress[node.id] && upgradeProgress[node.id].percent < 100 && (
-
-
+ {/* 系统监控 */}
+
+
+
+
+ CPU
+
+ {node.connectionStatus === "online" &&
+ node.systemInfo
+ ? `${node.systemInfo.cpuUsage.toFixed(1)}%`
+ : "-"}
+
+
+
+
+
+
+ 内存
+
+ {node.connectionStatus === "online" &&
+ node.systemInfo
+ ? `${node.systemInfo.memoryUsage.toFixed(1)}%`
+ : "-"}
+
+
+
+
+
+
+
+
+
+ 上传
+
+
+ {node.connectionStatus === "online" &&
+ node.systemInfo
+ ? formatSpeed(
+ node.systemInfo.uploadSpeed,
+ )
+ : "-"}
+
+
+
+
+ 下载
+
+
+ {node.connectionStatus === "online" &&
+ node.systemInfo
+ ? formatSpeed(
+ node.systemInfo.downloadSpeed,
+ )
+ : "-"}
+
+
+
+
+ {/* 流量统计 */}
+
+
+
+ ↑ 上行流量
+
+
+ {node.connectionStatus === "online" &&
+ node.systemInfo
+ ? formatTraffic(
+ node.systemInfo.uploadTraffic,
+ )
+ : "-"}
+
+
+
+
+ ↓ 下行流量
+
+
+ {node.connectionStatus === "online" &&
+ node.systemInfo
+ ? formatTraffic(
+ node.systemInfo.downloadTraffic,
+ )
+ : "-"}
+
+
- )}
-
- 开机时间
-
- {node.connectionStatus === "online" &&
- node.systemInfo
- ? formatUptime(node.systemInfo.uptime)
- : "-"}
-
>
)}
-
- {!isRemoteNode && (
- <>
- {/* 系统监控 */}
-
-
-
-
- CPU
-
- {node.connectionStatus === "online" &&
- node.systemInfo
- ? `${node.systemInfo.cpuUsage.toFixed(1)}%`
- : "-"}
-
-
-
-
-
-
- 内存
-
- {node.connectionStatus === "online" &&
- node.systemInfo
- ? `${node.systemInfo.memoryUsage.toFixed(1)}%`
- : "-"}
-
-
-
-
-
-
-
-
-
- 上传
-
-
- {node.connectionStatus === "online" &&
- node.systemInfo
- ? formatSpeed(node.systemInfo.uploadSpeed)
- : "-"}
-
-
-
-
- 下载
-
-
- {node.connectionStatus === "online" &&
- node.systemInfo
- ? formatSpeed(node.systemInfo.downloadSpeed)
- : "-"}
-
-
-
-
- {/* 流量统计 */}
-
-
-
- ↑ 上行流量
-
-
- {node.connectionStatus === "online" &&
- node.systemInfo
- ? formatTraffic(node.systemInfo.uploadTraffic)
- : "-"}
-
-
-
-
- ↓ 下行流量
-
-
- {node.connectionStatus === "online" &&
- node.systemInfo
- ? formatTraffic(
- node.systemInfo.downloadTraffic,
- )
- : "-"}
-
-
-
-
- >
- )}
-
- {/* 操作按钮 */}
-
- {!isRemoteNode && (
-
- handleCopyInstallCommand(node)}
- >
- 安装
-
- openUpgradeModal("single", node.id)}
- >
- 升级
-
- handleRollbackNode(node)}
- >
- 回退
-
-
- )}
-
+ {/* 操作按钮 */}
+
{!isRemoteNode && (
+
+ handleCopyInstallCommand(node)}
+ >
+ 安装
+
+
+ openUpgradeModal("single", node.id)
+ }
+ >
+ 升级
+
+ handleRollbackNode(node)}
+ >
+ 回退
+
+
+ )}
+
+ {!isRemoteNode && (
+ handleEdit(node)}
+ >
+ 编辑
+
+ )}
handleEdit(node)}
+ onPress={() => handleDelete(node)}
>
- 编辑
+ 删除
- )}
- handleDelete(node)}
- >
- 删除
-
+
-
-
-
- )}
-
+
+
+ )}
+
);
})}
@@ -2041,6 +2097,7 @@ export default function NodePage() {
selectedKeys={selectedVersion ? [selectedVersion] : []}
onSelectionChange={(keys) => {
const selected = Array.from(keys)[0] as string;
+
setSelectedVersion(selected || "");
}}
>
@@ -2053,7 +2110,12 @@ export default function NodePage() {
? new Date(r.publishedAt).toLocaleDateString()
: ""}
{r.prerelease && (
-
+
预览
)}
diff --git a/vite-frontend/src/pages/panel-sharing.tsx b/vite-frontend/src/pages/panel-sharing.tsx
index df05e73..b216042 100644
--- a/vite-frontend/src/pages/panel-sharing.tsx
+++ b/vite-frontend/src/pages/panel-sharing.tsx
@@ -12,6 +12,7 @@ import {
} from "@heroui/modal";
import { Select, SelectItem } from "@heroui/select";
import { toast } from "react-hot-toast";
+
import {
getNodeList,
createPeerShare,
@@ -128,6 +129,7 @@ export default function PanelSharingPage() {
setLoading(true);
try {
const res = await getPeerShareList();
+
if (res.code === 0) {
setShares(res.data || []);
} else {
@@ -141,10 +143,12 @@ export default function PanelSharingPage() {
const loadNodes = useCallback(async () => {
try {
const res = await getNodeList();
+
if (res.code === 0) {
const localNodes: Node[] = (res.data || []).filter(
(node: Node) => (node?.isRemote ?? 0) !== 1,
);
+
setNodes(localNodes);
setShareForm((prev) => {
if (!prev.nodeId) {
@@ -153,6 +157,7 @@ export default function PanelSharingPage() {
const hasSelectedNode = localNodes.some(
(node: Node) => String(node.id) === prev.nodeId,
);
+
return hasSelectedNode ? prev : { ...prev, nodeId: "" };
});
}
@@ -165,6 +170,7 @@ export default function PanelSharingPage() {
setRemoteUsageLoading(true);
try {
const res = await getPeerRemoteUsageList();
+
if (res.code === 0) {
setRemoteUsageNodes(res.data || []);
} else {
@@ -179,6 +185,7 @@ export default function PanelSharingPage() {
if (selectedTab === "my-shares") {
loadShares();
loadNodes();
+
return;
}
if (selectedTab === "remote-nodes") {
@@ -189,15 +196,19 @@ export default function PanelSharingPage() {
const handleCreateShare = async () => {
if (!shareForm.name || !shareForm.nodeId) {
toast.error("请填写必要信息");
+
return;
}
const nodeId = parseInt(shareForm.nodeId, 10);
+
if (Number.isNaN(nodeId) || !nodes.some((node) => node.id === nodeId)) {
toast.error("仅可选择本地节点");
+
return;
}
if (shareForm.maxBandwidth < 0) {
toast.error("流量上限不能为负数");
+
return;
}
try {
@@ -213,6 +224,7 @@ export default function PanelSharingPage() {
allowedDomains: shareForm.allowedDomains,
allowedIps: shareForm.allowedIps,
});
+
if (res.code === 0) {
toast.success("创建成功");
setCreateShareOpen(false);
@@ -228,6 +240,7 @@ export default function PanelSharingPage() {
const handleDeleteShare = async (id: number) => {
try {
const res = await deletePeerShare(id);
+
if (res.code === 0) {
toast.success("删除成功");
loadShares();
@@ -242,6 +255,7 @@ export default function PanelSharingPage() {
const handleResetShareFlow = async (id: number) => {
try {
const res = await resetPeerShareFlow(id);
+
if (res.code === 0) {
toast.success("共享流量已重置");
loadShares();
@@ -257,7 +271,10 @@ export default function PanelSharingPage() {
setEditForm({
id: share.id,
name: share.name,
- maxBandwidth: share.maxBandwidth > 0 ? Math.round(share.maxBandwidth / (1024 * 1024 * 1024)) : 0,
+ maxBandwidth:
+ share.maxBandwidth > 0
+ ? Math.round(share.maxBandwidth / (1024 * 1024 * 1024))
+ : 0,
expiryTime: share.expiryTime,
portRangeStart: share.portRangeStart,
portRangeEnd: share.portRangeEnd,
@@ -270,10 +287,12 @@ export default function PanelSharingPage() {
const handleEditShare = async () => {
if (!editForm.name) {
toast.error("名称不能为空");
+
return;
}
if (editForm.maxBandwidth < 0) {
toast.error("流量上限不能为负数");
+
return;
}
try {
@@ -287,6 +306,7 @@ export default function PanelSharingPage() {
allowedDomains: editForm.allowedDomains,
allowedIps: editForm.allowedIps,
});
+
if (res.code === 0) {
toast.success("编辑成功");
setEditShareOpen(false);
@@ -302,19 +322,22 @@ export default function PanelSharingPage() {
const handleImportNode = async () => {
if (!importForm.remoteUrl || !importForm.token) {
toast.error("请填写完整信息");
+
return;
}
try {
// Automatically add http/https if missing
let url = importForm.remoteUrl.trim();
+
if (!url.startsWith("http")) {
url = "http://" + url;
}
-
+
const res = await importRemoteNode({
remoteUrl: url,
token: importForm.token.trim(),
});
+
if (res.code === 0) {
toast.success("导入成功,请前往节点列表查看");
setImportNodeOpen(false);
@@ -341,6 +364,7 @@ export default function PanelSharingPage() {
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB";
if (bytes < 1024 * 1024 * 1024)
return (bytes / (1024 * 1024)).toFixed(2) + " MB";
+
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB";
};
@@ -351,6 +375,7 @@ export default function PanelSharingPage() {
if (chainType === 3) {
return "出口节点";
}
+
return "未知链路";
};
@@ -370,11 +395,14 @@ export default function PanelSharingPage() {
- setCreateShareOpen(true)}>
+ setCreateShareOpen(true)}
+ >
创建分享
-
+
{loading ? (
加载中...
) : shares.length === 0 ? (
@@ -382,7 +410,10 @@ export default function PanelSharingPage() {
) : (
{shares.map((share) => (
-
+
{share.name}
@@ -400,29 +431,67 @@ export default function PanelSharingPage() {
>
重置流量
- handleDeleteShare(share.id)}>删除
+ handleDeleteShare(share.id)}
+ >
+ 删除
+
- 端口范围: {share.portRangeStart} - {share.portRangeEnd}
- 流量上限: {share.maxBandwidth > 0 ? formatFlowGB(share.maxBandwidth) : "不限制"}
+
+ 端口范围: {share.portRangeStart} -{" "}
+ {share.portRangeEnd}
+
+
+ 流量上限:{" "}
+ {share.maxBandwidth > 0
+ ? formatFlowGB(share.maxBandwidth)
+ : "不限制"}
+
当前流量: {formatFlowGB(share.currentFlow || 0)}
- 远程占用端口: {share.usedPorts && share.usedPorts.length > 0 ? share.usedPorts.join(", ") : "暂无"}
- {share.usedPortDetails && share.usedPortDetails.length > 0 && (
-
- {share.usedPortDetails.map((item) => (
-
- {item.port} / {item.role || "reserved"}
-
- ))}
-
+
+ 远程占用端口:{" "}
+ {share.usedPorts && share.usedPorts.length > 0
+ ? share.usedPorts.join(", ")
+ : "暂无"}
+
+ {share.usedPortDetails &&
+ share.usedPortDetails.length > 0 && (
+
+ {share.usedPortDetails.map((item) => (
+
+ {item.port} / {item.role || "reserved"}
+
+ ))}
+
+ )}
+ {share.allowedDomains && (
+ 允许域名: {share.allowedDomains}
)}
- {share.allowedDomains && 允许域名: {share.allowedDomains}
}
- {share.allowedIps && 允许API IP: {share.allowedIps}
}
- 过期时间: {share.expiryTime === 0 ? "永久" : new Date(share.expiryTime).toLocaleDateString()}
+ {share.allowedIps && (
+ 允许API IP: {share.allowedIps}
+ )}
+
+ 过期时间:{" "}
+ {share.expiryTime === 0
+ ? "永久"
+ : new Date(share.expiryTime).toLocaleDateString()}
+
- copyToken(share.token)}>复制
+ copyToken(share.token)}
+ >
+ 复制
+
@@ -436,7 +505,10 @@ export default function PanelSharingPage() {
- setImportNodeOpen(true)}>
+ setImportNodeOpen(true)}
+ >
导入远程节点
@@ -446,15 +518,22 @@ export default function PanelSharingPage() {
) : remoteUsageNodes.length === 0 ? (
暂无远程节点占用记录。
-
导入远程节点并创建隧道后,这里会显示远端端口占用情况。
+
+ 导入远程节点并创建隧道后,这里会显示远端端口占用情况。
+
) : (
{remoteUsageNodes.map((node) => (
-
+
{node.nodeName}
- 绑定 {node.activeBindingNum || 0}
+
+ 绑定 {node.activeBindingNum || 0}
+
{node.syncError && (
@@ -470,18 +549,40 @@ export default function PanelSharingPage() {
)}
{node.remoteUrl && 远程地址: {node.remoteUrl}
}
共享ID: {node.shareId || "-"}
- 端口范围: {node.portRangeStart > 0 && node.portRangeEnd > 0 ? `${node.portRangeStart} - ${node.portRangeEnd}` : "-"}
- 共享流量: {node.maxBandwidth > 0 ? `${formatFlowGB(node.currentFlow || 0)} / ${formatFlowGB(node.maxBandwidth)}` : `${formatFlowGB(node.currentFlow || 0)} / 不限制`}
- 远端占用端口: {node.usedPorts && node.usedPorts.length > 0 ? node.usedPorts.join(", ") : "暂无"}
+
+ 端口范围:{" "}
+ {node.portRangeStart > 0 && node.portRangeEnd > 0
+ ? `${node.portRangeStart} - ${node.portRangeEnd}`
+ : "-"}
+
+
+ 共享流量:{" "}
+ {node.maxBandwidth > 0
+ ? `${formatFlowGB(node.currentFlow || 0)} / ${formatFlowGB(node.maxBandwidth)}`
+ : `${formatFlowGB(node.currentFlow || 0)} / 不限制`}
+
+
+ 远端占用端口:{" "}
+ {node.usedPorts && node.usedPorts.length > 0
+ ? node.usedPorts.join(", ")
+ : "暂无"}
+
{node.bindings && node.bindings.length > 0 && (
{node.bindings.map((binding) => (
-
- 隧道 {binding.tunnelName || `#${binding.tunnelId}`}
+
+ 隧道{" "}
+ {binding.tunnelName || `#${binding.tunnelId}`}
{" · "}
端口 {binding.allocatedPort}
{" · "}
- {formatChainType(binding.chainType, binding.hopInx)}
+ {formatChainType(
+ binding.chainType,
+ binding.hopInx,
+ )}
))}
@@ -505,13 +606,17 @@ export default function PanelSharingPage() {
label="名称"
placeholder="备注名称"
value={shareForm.name}
- onChange={(e) => setShareForm({ ...shareForm, name: e.target.value })}
+ onChange={(e) =>
+ setShareForm({ ...shareForm, name: e.target.value })
+ }
/>
setShareForm({ ...shareForm, expiryDays: parseInt(e.target.value) })}
+ onChange={(e) =>
+ setShareForm({
+ ...shareForm,
+ expiryDays: parseInt(e.target.value),
+ })
+ }
/>
setShareForm({ ...shareForm, maxBandwidth: parseInt(e.target.value, 10) || 0 })}
+ onChange={(e) =>
+ setShareForm({
+ ...shareForm,
+ maxBandwidth: parseInt(e.target.value, 10) || 0,
+ })
+ }
/>
setShareForm({ ...shareForm, allowedDomains: e.target.value })}
+ onChange={(e) =>
+ setShareForm({ ...shareForm, allowedDomains: e.target.value })
+ }
/>
setShareForm({ ...shareForm, allowedIps: e.target.value })}
+ onChange={(e) =>
+ setShareForm({ ...shareForm, allowedIps: e.target.value })
+ }
/>
setCreateShareOpen(false)}>取消
- 创建
+
+ 创建
+
@@ -578,54 +709,88 @@ export default function PanelSharingPage() {
label="名称"
placeholder="备注名称"
value={editForm.name}
- onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
+ onChange={(e) =>
+ setEditForm({ ...editForm, name: e.target.value })
+ }
/>
setEditForm({ ...editForm, portRangeStart: parseInt(e.target.value) || 0 })}
+ onChange={(e) =>
+ setEditForm({
+ ...editForm,
+ portRangeStart: parseInt(e.target.value) || 0,
+ })
+ }
/>
setEditForm({ ...editForm, portRangeEnd: parseInt(e.target.value) || 0 })}
+ onChange={(e) =>
+ setEditForm({
+ ...editForm,
+ portRangeEnd: parseInt(e.target.value) || 0,
+ })
+ }
/>
setEditForm({ ...editForm, maxBandwidth: parseInt(e.target.value, 10) || 0 })}
+ onChange={(e) =>
+ setEditForm({
+ ...editForm,
+ maxBandwidth: parseInt(e.target.value, 10) || 0,
+ })
+ }
/>
0 ? new Date(editForm.expiryTime).toISOString().slice(0, 16) : ""}
- onChange={(e) => setEditForm({ ...editForm, expiryTime: e.target.value ? new Date(e.target.value).getTime() : 0 })}
+ value={
+ editForm.expiryTime > 0
+ ? new Date(editForm.expiryTime).toISOString().slice(0, 16)
+ : ""
+ }
+ onChange={(e) =>
+ setEditForm({
+ ...editForm,
+ expiryTime: e.target.value
+ ? new Date(e.target.value).getTime()
+ : 0,
+ })
+ }
/>
setEditForm({ ...editForm, allowedDomains: e.target.value })}
+ onChange={(e) =>
+ setEditForm({ ...editForm, allowedDomains: e.target.value })
+ }
/>
setEditForm({ ...editForm, allowedIps: e.target.value })}
+ onChange={(e) =>
+ setEditForm({ ...editForm, allowedIps: e.target.value })
+ }
/>
setEditShareOpen(false)}>取消
- 保存
+
+ 保存
+
@@ -639,18 +804,24 @@ export default function PanelSharingPage() {
label="远程面板地址"
placeholder="http://panel.example.com:8088"
value={importForm.remoteUrl}
- onChange={(e) => setImportForm({ ...importForm, remoteUrl: e.target.value })}
+ onChange={(e) =>
+ setImportForm({ ...importForm, remoteUrl: e.target.value })
+ }
/>
setImportForm({ ...importForm, token: e.target.value })}
+ onChange={(e) =>
+ setImportForm({ ...importForm, token: e.target.value })
+ }
/>
setImportNodeOpen(false)}>取消
- 导入
+
+ 导入
+