mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-07 18:26:37 +08:00
Merge pull request #81 from Sagit-chu/opencode/shiny-squid
feat(panel-sharing): add edit support for peer shares
This commit is contained in:
@@ -85,6 +85,14 @@ func NewFederationClient() *FederationClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewFederationClientWithTimeout(timeout time.Duration) *FederationClient {
|
||||||
|
return &FederationClient{
|
||||||
|
client: &http.Client{
|
||||||
|
Timeout: timeout,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *FederationClient) Connect(url, token, localDomain string) (*RemoteNodeInfo, error) {
|
func (c *FederationClient) Connect(url, token, localDomain string) (*RemoteNodeInfo, error) {
|
||||||
url = strings.TrimSuffix(url, "/")
|
url = strings.TrimSuffix(url, "/")
|
||||||
req, err := http.NewRequest("POST", url+"/api/v1/federation/connect", nil)
|
req, err := http.NewRequest("POST", url+"/api/v1/federation/connect", nil)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go-backend/internal/http/client"
|
"go-backend/internal/http/client"
|
||||||
@@ -40,6 +41,17 @@ type resetPeerShareFlowRequest struct {
|
|||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type updatePeerShareRequest struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
MaxBandwidth int64 `json:"maxBandwidth"`
|
||||||
|
ExpiryTime int64 `json:"expiryTime"`
|
||||||
|
PortRangeStart int `json:"portRangeStart"`
|
||||||
|
PortRangeEnd int `json:"portRangeEnd"`
|
||||||
|
AllowedDomains string `json:"allowedDomains"`
|
||||||
|
AllowedIPs string `json:"allowedIps"`
|
||||||
|
}
|
||||||
|
|
||||||
type nodeImportRequest struct {
|
type nodeImportRequest struct {
|
||||||
RemoteURL string `json:"remoteUrl"`
|
RemoteURL string `json:"remoteUrl"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
@@ -325,6 +337,80 @@ func (h *Handler) federationShareResetFlow(w http.ResponseWriter, r *http.Reques
|
|||||||
response.WriteJSON(w, response.OKEmpty())
|
response.WriteJSON(w, response.OKEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) federationShareUpdate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
response.WriteJSON(w, response.ErrDefault("Invalid method"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req updatePeerShareRequest
|
||||||
|
if err := decodeJSON(r.Body, &req); err != nil {
|
||||||
|
response.WriteJSON(w, response.ErrDefault("Invalid JSON"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.ID <= 0 {
|
||||||
|
response.WriteJSON(w, response.ErrDefault("Share ID is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
share, err := h.repo.GetPeerShare(req.ID)
|
||||||
|
if err != nil {
|
||||||
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if share == nil {
|
||||||
|
response.WriteJSON(w, response.ErrDefault("Share not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name == "" {
|
||||||
|
response.WriteJSON(w, response.ErrDefault("Name is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.MaxBandwidth < 0 {
|
||||||
|
response.WriteJSON(w, response.ErrDefault("Max bandwidth cannot be negative"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ExpiryTime < 0 {
|
||||||
|
response.WriteJSON(w, response.ErrDefault("Expiry time cannot be negative"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.PortRangeStart < 0 || req.PortRangeStart > 65535 || req.PortRangeEnd < 0 || req.PortRangeEnd > 65535 {
|
||||||
|
response.WriteJSON(w, response.ErrDefault("Invalid port range"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.PortRangeStart > req.PortRangeEnd {
|
||||||
|
response.WriteJSON(w, response.ErrDefault("Port range start cannot be greater than end"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedIPs, err := normalizePeerShareAllowedIPs(req.AllowedIPs)
|
||||||
|
if err != nil {
|
||||||
|
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
share.Name = req.Name
|
||||||
|
share.MaxBandwidth = req.MaxBandwidth
|
||||||
|
share.ExpiryTime = req.ExpiryTime
|
||||||
|
share.PortRangeStart = req.PortRangeStart
|
||||||
|
share.PortRangeEnd = req.PortRangeEnd
|
||||||
|
share.AllowedDomains = req.AllowedDomains
|
||||||
|
share.AllowedIPs = allowedIPs
|
||||||
|
share.UpdatedTime = time.Now().UnixMilli()
|
||||||
|
|
||||||
|
if err := h.repo.UpdatePeerShare(share); err != nil {
|
||||||
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.WriteJSON(w, response.OKEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) federationRemoteUsageList(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) federationRemoteUsageList(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
response.WriteJSON(w, response.ErrDefault("Invalid method"))
|
response.WriteJSON(w, response.ErrDefault("Invalid method"))
|
||||||
@@ -1317,6 +1403,74 @@ func isPeerIPAllowed(clientIP net.IP, whitelist string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) syncRemoteNodeStatuses(items []map[string]interface{}) {
|
||||||
|
type remoteEntry struct {
|
||||||
|
index int
|
||||||
|
remoteURL string
|
||||||
|
remoteToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
var remotes []remoteEntry
|
||||||
|
for i, item := range items {
|
||||||
|
isRemote, _ := item["isRemote"].(int)
|
||||||
|
if isRemote != 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
url, _ := item["remoteUrl"].(string)
|
||||||
|
token, _ := item["remoteToken"].(string)
|
||||||
|
url = strings.TrimSpace(url)
|
||||||
|
token = strings.TrimSpace(token)
|
||||||
|
if url == "" || token == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
remotes = append(remotes, remoteEntry{index: i, remoteURL: url, remoteToken: token})
|
||||||
|
}
|
||||||
|
if len(remotes) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
localDomain := h.federationLocalDomain()
|
||||||
|
fc := client.NewFederationClientWithTimeout(5 * time.Second)
|
||||||
|
|
||||||
|
type syncResult struct {
|
||||||
|
index int
|
||||||
|
status int
|
||||||
|
syncError string
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]syncResult, len(remotes))
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for i, entry := range remotes {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(idx int, e remoteEntry) {
|
||||||
|
defer wg.Done()
|
||||||
|
info, err := fc.Connect(e.remoteURL, e.remoteToken, localDomain)
|
||||||
|
if err != nil {
|
||||||
|
errMsg := err.Error()
|
||||||
|
if strings.Contains(errMsg, "401") || strings.Contains(errMsg, "Invalid token") || strings.Contains(errMsg, "Unauthorized") {
|
||||||
|
results[idx] = syncResult{index: e.index, status: 0, syncError: "provider_share_deleted"}
|
||||||
|
} else if strings.Contains(errMsg, "403") || strings.Contains(errMsg, "Share is disabled") {
|
||||||
|
results[idx] = syncResult{index: e.index, status: 0, syncError: "provider_share_disabled"}
|
||||||
|
} else if strings.Contains(errMsg, "Share expired") {
|
||||||
|
results[idx] = syncResult{index: e.index, status: 0, syncError: "provider_share_expired"}
|
||||||
|
} else {
|
||||||
|
results[idx] = syncResult{index: e.index, status: 0, syncError: errMsg}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
results[idx] = syncResult{index: e.index, status: info.Status, syncError: ""}
|
||||||
|
}
|
||||||
|
}(i, entry)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
for _, r := range results {
|
||||||
|
items[r.index]["status"] = r.status
|
||||||
|
if r.syncError != "" {
|
||||||
|
items[r.index]["syncError"] = r.syncError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) cleanupPeerShareRuntimes(shareID int64) {
|
func (h *Handler) cleanupPeerShareRuntimes(shareID int64) {
|
||||||
if h == nil || h.repo == nil || shareID <= 0 {
|
if h == nil || h.repo == nil || shareID <= 0 {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ func (h *Handler) Register(mux *http.ServeMux) {
|
|||||||
mux.HandleFunc("/api/v1/open_api/sub_store", h.openAPISubStore)
|
mux.HandleFunc("/api/v1/open_api/sub_store", h.openAPISubStore)
|
||||||
mux.HandleFunc("/api/v1/federation/share/list", h.federationShareList)
|
mux.HandleFunc("/api/v1/federation/share/list", h.federationShareList)
|
||||||
mux.HandleFunc("/api/v1/federation/share/create", h.federationShareCreate)
|
mux.HandleFunc("/api/v1/federation/share/create", h.federationShareCreate)
|
||||||
|
mux.HandleFunc("/api/v1/federation/share/update", h.federationShareUpdate)
|
||||||
mux.HandleFunc("/api/v1/federation/share/delete", h.federationShareDelete)
|
mux.HandleFunc("/api/v1/federation/share/delete", h.federationShareDelete)
|
||||||
mux.HandleFunc("/api/v1/federation/share/reset-flow", h.federationShareResetFlow)
|
mux.HandleFunc("/api/v1/federation/share/reset-flow", h.federationShareResetFlow)
|
||||||
mux.HandleFunc("/api/v1/federation/share/remote-usage/list", h.federationRemoteUsageList)
|
mux.HandleFunc("/api/v1/federation/share/remote-usage/list", h.federationRemoteUsageList)
|
||||||
@@ -320,6 +321,9 @@ func (h *Handler) nodeList(w http.ResponseWriter, r *http.Request) {
|
|||||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.syncRemoteNodeStatuses(items)
|
||||||
|
|
||||||
response.WriteJSON(w, response.OK(items))
|
response.WriteJSON(w, response.OK(items))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -200,6 +200,16 @@ export const createPeerShare = (data: {
|
|||||||
allowedDomains?: string;
|
allowedDomains?: string;
|
||||||
allowedIps?: string;
|
allowedIps?: string;
|
||||||
}) => Network.post("/federation/share/create", data);
|
}) => Network.post("/federation/share/create", data);
|
||||||
|
export const updatePeerShare = (data: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
maxBandwidth: number;
|
||||||
|
expiryTime: number;
|
||||||
|
portRangeStart: number;
|
||||||
|
portRangeEnd: number;
|
||||||
|
allowedDomains: string;
|
||||||
|
allowedIps: string;
|
||||||
|
}) => Network.post("/federation/share/update", data);
|
||||||
export const deletePeerShare = (id: number) =>
|
export const deletePeerShare = (id: number) =>
|
||||||
Network.post("/federation/share/delete", { id });
|
Network.post("/federation/share/delete", { id });
|
||||||
export const resetPeerShareFlow = (id: number) =>
|
export const resetPeerShareFlow = (id: number) =>
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ interface Node {
|
|||||||
status: number;
|
status: number;
|
||||||
isRemote?: number;
|
isRemote?: number;
|
||||||
remoteUrl?: string;
|
remoteUrl?: string;
|
||||||
|
syncError?: string;
|
||||||
connectionStatus: "online" | "offline";
|
connectionStatus: "online" | "offline";
|
||||||
systemInfo?: {
|
systemInfo?: {
|
||||||
cpuUsage: number;
|
cpuUsage: number;
|
||||||
@@ -219,7 +220,8 @@ export default function NodePage() {
|
|||||||
const nodesData: Node[] = (res.data || []).map((node: any) => ({
|
const nodesData: Node[] = (res.data || []).map((node: any) => ({
|
||||||
...node,
|
...node,
|
||||||
inx: node.inx ?? 0,
|
inx: node.inx ?? 0,
|
||||||
connectionStatus: node.status === 1 ? "online" : "offline",
|
connectionStatus: node.syncError ? "offline" : node.status === 1 ? "online" : "offline",
|
||||||
|
syncError: node.syncError || undefined,
|
||||||
systemInfo: null,
|
systemInfo: null,
|
||||||
copyLoading: false,
|
copyLoading: false,
|
||||||
}));
|
}));
|
||||||
@@ -1181,6 +1183,17 @@ export default function NodePage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardBody className="pt-0 pb-3">
|
<CardBody className="pt-0 pb-3">
|
||||||
|
{isRemoteNode && node.syncError && (
|
||||||
|
<div className="mb-3 px-2 py-1.5 rounded-md bg-warning-50 dark:bg-warning-100/10 text-warning-700 dark:text-warning-400 text-xs">
|
||||||
|
{node.syncError === "provider_share_deleted"
|
||||||
|
? "提供方已删除该分享"
|
||||||
|
: node.syncError === "provider_share_disabled"
|
||||||
|
? "提供方已禁用该分享"
|
||||||
|
: node.syncError === "provider_share_expired"
|
||||||
|
? "提供方分享已过期"
|
||||||
|
: `远程同步失败: ${node.syncError}`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* 基础信息 */}
|
{/* 基础信息 */}
|
||||||
<div className="space-y-2 mb-4">
|
<div className="space-y-2 mb-4">
|
||||||
<div className="flex justify-between items-center text-sm min-w-0">
|
<div className="flex justify-between items-center text-sm min-w-0">
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
resetPeerShareFlow,
|
resetPeerShareFlow,
|
||||||
getPeerRemoteUsageList,
|
getPeerRemoteUsageList,
|
||||||
importRemoteNode,
|
importRemoteNode,
|
||||||
|
updatePeerShare,
|
||||||
} from "@/api";
|
} from "@/api";
|
||||||
|
|
||||||
interface Node {
|
interface Node {
|
||||||
@@ -77,6 +78,7 @@ interface RemoteUsageNode {
|
|||||||
usedPorts: number[];
|
usedPorts: number[];
|
||||||
bindings: RemoteUsageBinding[];
|
bindings: RemoteUsageBinding[];
|
||||||
activeBindingNum: number;
|
activeBindingNum: number;
|
||||||
|
syncError?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PanelSharingPage() {
|
export default function PanelSharingPage() {
|
||||||
@@ -91,6 +93,7 @@ export default function PanelSharingPage() {
|
|||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
const [createShareOpen, setCreateShareOpen] = useState(false);
|
const [createShareOpen, setCreateShareOpen] = useState(false);
|
||||||
|
const [editShareOpen, setEditShareOpen] = useState(false);
|
||||||
const [importNodeOpen, setImportNodeOpen] = useState(false);
|
const [importNodeOpen, setImportNodeOpen] = useState(false);
|
||||||
|
|
||||||
// Forms
|
// Forms
|
||||||
@@ -110,6 +113,17 @@ export default function PanelSharingPage() {
|
|||||||
token: "",
|
token: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [editForm, setEditForm] = useState({
|
||||||
|
id: 0,
|
||||||
|
name: "",
|
||||||
|
maxBandwidth: 0,
|
||||||
|
expiryTime: 0,
|
||||||
|
portRangeStart: 10000,
|
||||||
|
portRangeEnd: 20000,
|
||||||
|
allowedDomains: "",
|
||||||
|
allowedIps: "",
|
||||||
|
});
|
||||||
|
|
||||||
const loadShares = useCallback(async () => {
|
const loadShares = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -239,6 +253,52 @@ export default function PanelSharingPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openEditShare = (share: PeerShare) => {
|
||||||
|
setEditForm({
|
||||||
|
id: share.id,
|
||||||
|
name: share.name,
|
||||||
|
maxBandwidth: share.maxBandwidth > 0 ? Math.round(share.maxBandwidth / (1024 * 1024 * 1024)) : 0,
|
||||||
|
expiryTime: share.expiryTime,
|
||||||
|
portRangeStart: share.portRangeStart,
|
||||||
|
portRangeEnd: share.portRangeEnd,
|
||||||
|
allowedDomains: share.allowedDomains || "",
|
||||||
|
allowedIps: share.allowedIps || "",
|
||||||
|
});
|
||||||
|
setEditShareOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditShare = async () => {
|
||||||
|
if (!editForm.name) {
|
||||||
|
toast.error("名称不能为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (editForm.maxBandwidth < 0) {
|
||||||
|
toast.error("流量上限不能为负数");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await updatePeerShare({
|
||||||
|
id: editForm.id,
|
||||||
|
name: editForm.name,
|
||||||
|
maxBandwidth: Math.max(0, editForm.maxBandwidth) * 1024 * 1024 * 1024,
|
||||||
|
expiryTime: editForm.expiryTime,
|
||||||
|
portRangeStart: editForm.portRangeStart,
|
||||||
|
portRangeEnd: editForm.portRangeEnd,
|
||||||
|
allowedDomains: editForm.allowedDomains,
|
||||||
|
allowedIps: editForm.allowedIps,
|
||||||
|
});
|
||||||
|
if (res.code === 0) {
|
||||||
|
toast.success("编辑成功");
|
||||||
|
setEditShareOpen(false);
|
||||||
|
loadShares();
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg || "编辑失败");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error("网络错误");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleImportNode = async () => {
|
const handleImportNode = async () => {
|
||||||
if (!importForm.remoteUrl || !importForm.token) {
|
if (!importForm.remoteUrl || !importForm.token) {
|
||||||
toast.error("请填写完整信息");
|
toast.error("请填写完整信息");
|
||||||
@@ -326,6 +386,13 @@ export default function PanelSharingPage() {
|
|||||||
<CardHeader className="flex justify-between">
|
<CardHeader className="flex justify-between">
|
||||||
<h3 className="font-bold">{share.name}</h3>
|
<h3 className="font-bold">{share.name}</h3>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
onPress={() => openEditShare(share)}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
@@ -390,6 +457,17 @@ export default function PanelSharingPage() {
|
|||||||
<span className="text-xs text-default-500">绑定 {node.activeBindingNum || 0}</span>
|
<span className="text-xs text-default-500">绑定 {node.activeBindingNum || 0}</span>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody className="text-sm space-y-2">
|
<CardBody className="text-sm space-y-2">
|
||||||
|
{node.syncError && (
|
||||||
|
<div className="px-2 py-1.5 rounded-md bg-warning-50 dark:bg-warning-100/10 text-warning-700 dark:text-warning-400 text-xs">
|
||||||
|
{node.syncError === "provider_share_deleted"
|
||||||
|
? "提供方已删除该分享"
|
||||||
|
: node.syncError === "provider_share_disabled"
|
||||||
|
? "提供方已禁用该分享"
|
||||||
|
: node.syncError === "provider_share_expired"
|
||||||
|
? "提供方分享已过期"
|
||||||
|
: `远程同步失败: ${node.syncError}`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{node.remoteUrl && <p>远程地址: {node.remoteUrl}</p>}
|
{node.remoteUrl && <p>远程地址: {node.remoteUrl}</p>}
|
||||||
<p>共享ID: {node.shareId || "-"}</p>
|
<p>共享ID: {node.shareId || "-"}</p>
|
||||||
<p>端口范围: {node.portRangeStart > 0 && node.portRangeEnd > 0 ? `${node.portRangeStart} - ${node.portRangeEnd}` : "-"}</p>
|
<p>端口范围: {node.portRangeStart > 0 && node.portRangeEnd > 0 ? `${node.portRangeStart} - ${node.portRangeEnd}` : "-"}</p>
|
||||||
@@ -491,6 +569,67 @@ export default function PanelSharingPage() {
|
|||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* Edit Share Modal */}
|
||||||
|
<Modal isOpen={editShareOpen} onClose={() => setEditShareOpen(false)}>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>编辑分享</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<Input
|
||||||
|
label="名称"
|
||||||
|
placeholder="备注名称"
|
||||||
|
value={editForm.name}
|
||||||
|
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Input
|
||||||
|
label="起始端口"
|
||||||
|
type="number"
|
||||||
|
value={editForm.portRangeStart.toString()}
|
||||||
|
onChange={(e) => setEditForm({ ...editForm, portRangeStart: parseInt(e.target.value) || 0 })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="结束端口"
|
||||||
|
type="number"
|
||||||
|
value={editForm.portRangeEnd.toString()}
|
||||||
|
onChange={(e) => setEditForm({ ...editForm, portRangeEnd: parseInt(e.target.value) || 0 })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
label="流量上限 (GB)"
|
||||||
|
type="number"
|
||||||
|
description="0 表示不限流量"
|
||||||
|
value={editForm.maxBandwidth.toString()}
|
||||||
|
onChange={(e) => setEditForm({ ...editForm, maxBandwidth: parseInt(e.target.value, 10) || 0 })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="过期时间"
|
||||||
|
type="datetime-local"
|
||||||
|
description="留空或清除表示永久有效"
|
||||||
|
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 })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="允许的域名 (可选)"
|
||||||
|
placeholder="example.com, panel.test.com"
|
||||||
|
description="限制使用此Token的来源面板域名,多个域名用逗号分隔,留空不限制"
|
||||||
|
value={editForm.allowedDomains}
|
||||||
|
onChange={(e) => setEditForm({ ...editForm, allowedDomains: e.target.value })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="允许的API IP (可选)"
|
||||||
|
placeholder="203.0.113.10, 2001:db8::10, 198.51.100.0/24"
|
||||||
|
description="仅白名单IP可导入此分享,支持IPv4/IPv6/CIDR,多个用逗号分隔"
|
||||||
|
value={editForm.allowedIps}
|
||||||
|
onChange={(e) => setEditForm({ ...editForm, allowedIps: e.target.value })}
|
||||||
|
/>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={() => setEditShareOpen(false)}>取消</Button>
|
||||||
|
<Button color="primary" onPress={handleEditShare}>保存</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{/* Import Node Modal */}
|
{/* Import Node Modal */}
|
||||||
<Modal isOpen={importNodeOpen} onClose={() => setImportNodeOpen(false)}>
|
<Modal isOpen={importNodeOpen} onClose={() => setImportNodeOpen(false)}>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user