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() { > 全选 - @@ -580,11 +580,11 @@ export default function ConfigPage() {

setImportTypes(values as string[])} > @@ -601,18 +601,18 @@ export default function ConfigPage() {
- - -
- )} -
+ {/* 操作按钮 */} +
{!isRemoteNode && ( +
+ + + +
+ )} +
+ {!isRemoteNode && ( + + )} - )} - +
-
- - - )} - + + + )} + ); })} @@ -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() {
-
- + {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() { > 重置流量 - +
-

端口范围: {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()} +

- +
@@ -436,7 +505,10 @@ export default function PanelSharingPage() {
-
@@ -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, portRangeEnd: parseInt(e.target.value) })} + onChange={(e) => + setShareForm({ + ...shareForm, + portRangeEnd: parseInt(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 }) + } /> - + @@ -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 }) + } /> - + @@ -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 }) + } /> - +