mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-03 17:06:36 +08:00
Keep UI improvements: Modal styles, expiryReminderDismissed, BatchActionResultModal
This commit is contained in:
@@ -574,8 +574,11 @@ export default function AdminLayout({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 修改密码弹窗 */}
|
{/* 修改密码弹窗 */}
|
||||||
<Modal classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }}
|
<Modal
|
||||||
backdrop="blur"
|
backdrop="blur"
|
||||||
|
classNames={{
|
||||||
|
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
|
||||||
|
}}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
placement="center"
|
placement="center"
|
||||||
scrollBehavior="outside"
|
scrollBehavior="outside"
|
||||||
|
|||||||
@@ -479,10 +479,10 @@ export default function GroupPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center gap-3 pb-2">
|
<CardHeader className="flex flex-row items-center gap-3 pb-2">
|
||||||
<h3 className="text-lg font-semibold">隧道分组</h3>
|
<h3 className="text-lg font-semibold">隧道分组</h3>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
className="h-7 px-3 text-xs font-medium min-w-0 shadow-sm"
|
||||||
size="sm"
|
color="primary"
|
||||||
className="h-7 px-3 text-xs font-medium min-w-0 shadow-sm"
|
size="sm"
|
||||||
onPress={openCreateTunnelGroup}
|
onPress={openCreateTunnelGroup}
|
||||||
>
|
>
|
||||||
新建
|
新建
|
||||||
@@ -551,10 +551,10 @@ export default function GroupPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center gap-3 pb-2">
|
<CardHeader className="flex flex-row items-center gap-3 pb-2">
|
||||||
<h3 className="text-lg font-semibold">用户分组</h3>
|
<h3 className="text-lg font-semibold">用户分组</h3>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
className="h-7 px-3 text-xs font-medium min-w-0 shadow-sm"
|
||||||
size="sm"
|
color="primary"
|
||||||
className="h-7 px-3 text-xs font-medium min-w-0 shadow-sm"
|
size="sm"
|
||||||
onPress={openCreateUserGroup}
|
onPress={openCreateUserGroup}
|
||||||
>
|
>
|
||||||
新建
|
新建
|
||||||
@@ -701,7 +701,11 @@ export default function GroupPage() {
|
|||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Modal backdrop="blur" classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }}
|
<Modal
|
||||||
|
backdrop="blur"
|
||||||
|
classNames={{
|
||||||
|
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
|
||||||
|
}}
|
||||||
isOpen={tunnelGroupModalOpen}
|
isOpen={tunnelGroupModalOpen}
|
||||||
onOpenChange={onTunnelGroupModalChange}
|
onOpenChange={onTunnelGroupModalChange}
|
||||||
>
|
>
|
||||||
@@ -745,7 +749,14 @@ export default function GroupPage() {
|
|||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal backdrop="blur" classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }} isOpen={userGroupModalOpen} onOpenChange={onUserGroupModalChange}>
|
<Modal
|
||||||
|
backdrop="blur"
|
||||||
|
classNames={{
|
||||||
|
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
|
||||||
|
}}
|
||||||
|
isOpen={userGroupModalOpen}
|
||||||
|
onOpenChange={onUserGroupModalChange}
|
||||||
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{editingUserGroup ? "编辑用户分组" : "新建用户分组"}
|
{editingUserGroup ? "编辑用户分组" : "新建用户分组"}
|
||||||
@@ -786,7 +797,11 @@ export default function GroupPage() {
|
|||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal backdrop="blur" classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }}
|
<Modal
|
||||||
|
backdrop="blur"
|
||||||
|
classNames={{
|
||||||
|
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
|
||||||
|
}}
|
||||||
isOpen={tunnelAssignModalOpen}
|
isOpen={tunnelAssignModalOpen}
|
||||||
onOpenChange={onTunnelAssignModalChange}
|
onOpenChange={onTunnelAssignModalChange}
|
||||||
>
|
>
|
||||||
@@ -833,7 +848,11 @@ export default function GroupPage() {
|
|||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal backdrop="blur" classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }}
|
<Modal
|
||||||
|
backdrop="blur"
|
||||||
|
classNames={{
|
||||||
|
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
|
||||||
|
}}
|
||||||
isOpen={userAssignModalOpen}
|
isOpen={userAssignModalOpen}
|
||||||
onOpenChange={onUserAssignModalChange}
|
onOpenChange={onUserAssignModalChange}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -339,8 +339,11 @@ export default function LimitPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 新增/编辑模态框 */}
|
{/* 新增/编辑模态框 */}
|
||||||
<Modal classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }}
|
<Modal
|
||||||
backdrop="blur"
|
backdrop="blur"
|
||||||
|
classNames={{
|
||||||
|
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
|
||||||
|
}}
|
||||||
isOpen={modalOpen}
|
isOpen={modalOpen}
|
||||||
placement="center"
|
placement="center"
|
||||||
scrollBehavior="outside"
|
scrollBehavior="outside"
|
||||||
@@ -414,8 +417,11 @@ export default function LimitPage() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* 删除确认模态框 */}
|
{/* 删除确认模态框 */}
|
||||||
<Modal classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }}
|
<Modal
|
||||||
backdrop="blur"
|
backdrop="blur"
|
||||||
|
classNames={{
|
||||||
|
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
|
||||||
|
}}
|
||||||
isOpen={deleteModalOpen}
|
isOpen={deleteModalOpen}
|
||||||
placement="center"
|
placement="center"
|
||||||
scrollBehavior="outside"
|
scrollBehavior="outside"
|
||||||
|
|||||||
+232
-100
@@ -1,6 +1,5 @@
|
|||||||
import { useState, useEffect, useMemo, useCallback } from "react";
|
import { useState, useEffect, useMemo, useCallback } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
KeyboardSensor,
|
KeyboardSensor,
|
||||||
@@ -53,6 +52,7 @@ import {
|
|||||||
getNodeReleases,
|
getNodeReleases,
|
||||||
rollbackNode,
|
rollbackNode,
|
||||||
getPeerRemoteUsageList,
|
getPeerRemoteUsageList,
|
||||||
|
dismissNodeExpiryReminder,
|
||||||
type ReleaseChannel,
|
type ReleaseChannel,
|
||||||
} from "@/api";
|
} from "@/api";
|
||||||
import { PageEmptyState, PageLoadingState } from "@/components/page-state";
|
import { PageEmptyState, PageLoadingState } from "@/components/page-state";
|
||||||
@@ -82,6 +82,7 @@ interface Node {
|
|||||||
remark?: string;
|
remark?: string;
|
||||||
expiryTime?: number;
|
expiryTime?: number;
|
||||||
renewalCycle?: NodeRenewalCycle;
|
renewalCycle?: NodeRenewalCycle;
|
||||||
|
expiryReminderDismissed?: number;
|
||||||
ip: string;
|
ip: string;
|
||||||
serverIp: string;
|
serverIp: string;
|
||||||
serverIpV4?: string;
|
serverIpV4?: string;
|
||||||
@@ -274,11 +275,7 @@ const SortableItem = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div ref={setNodeRef} className="overflow-visible h-full" style={style}>
|
||||||
ref={setNodeRef}
|
|
||||||
style={style}
|
|
||||||
className="overflow-visible h-full"
|
|
||||||
>
|
|
||||||
{children(listeners, attributes)}
|
{children(listeners, attributes)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -908,6 +905,24 @@ export default function NodePage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDismissExpiryReminder = async (nodeId: number) => {
|
||||||
|
try {
|
||||||
|
const res = await dismissNodeExpiryReminder(nodeId);
|
||||||
|
|
||||||
|
if (res.code === 0) {
|
||||||
|
setNodeList((prev) =>
|
||||||
|
prev.map((n) =>
|
||||||
|
n.id === nodeId ? { ...n, expiryReminderDismissed: 1 } : n,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg || "操作失败");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error("网络错误,请重试");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const openInstallSelector = (node: Node) => {
|
const openInstallSelector = (node: Node) => {
|
||||||
setInstallTargetNode(node);
|
setInstallTargetNode(node);
|
||||||
setInstallChannel("stable");
|
setInstallChannel("stable");
|
||||||
@@ -1060,7 +1075,9 @@ export default function NodePage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
toast.success(`批量升级命令已发送到 ${selectedLocalIds.length} 个节点`);
|
toast.success(
|
||||||
|
`批量升级命令已发送到 ${selectedLocalIds.length} 个节点`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg || "批量升级失败");
|
toast.error(res.msg || "批量升级失败");
|
||||||
}
|
}
|
||||||
@@ -1081,12 +1098,14 @@ export default function NodePage() {
|
|||||||
const confirmRollback = async () => {
|
const confirmRollback = async () => {
|
||||||
if (!nodeToRollback) return;
|
if (!nodeToRollback) return;
|
||||||
const node = nodeToRollback;
|
const node = nodeToRollback;
|
||||||
|
|
||||||
setRollbackModalOpen(false);
|
setRollbackModalOpen(false);
|
||||||
setNodeList((prev) =>
|
setNodeList((prev) =>
|
||||||
prev.map((n) => (n.id === node.id ? { ...n, rollbackLoading: true } : n)),
|
prev.map((n) => (n.id === node.id ? { ...n, rollbackLoading: true } : n)),
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
const res = await rollbackNode(node.id);
|
const res = await rollbackNode(node.id);
|
||||||
|
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
toast.success(`节点 ${node.name} 回退命令已发送,节点将自动重启`);
|
toast.success(`节点 ${node.name} 回退命令已发送,节点将自动重启`);
|
||||||
} else {
|
} else {
|
||||||
@@ -1224,6 +1243,7 @@ export default function NodePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nextId = reorderedDisplayIds[reorderedDisplayIndex];
|
const nextId = reorderedDisplayIds[reorderedDisplayIndex];
|
||||||
|
|
||||||
reorderedDisplayIndex += 1;
|
reorderedDisplayIndex += 1;
|
||||||
|
|
||||||
return nextId;
|
return nextId;
|
||||||
@@ -1403,7 +1423,8 @@ export default function NodePage() {
|
|||||||
return nodes.filter(
|
return nodes.filter(
|
||||||
(node) =>
|
(node) =>
|
||||||
(node.name && node.name.toLowerCase().includes(normalizedKeyword)) ||
|
(node.name && node.name.toLowerCase().includes(normalizedKeyword)) ||
|
||||||
(node.remark && node.remark.toLowerCase().includes(normalizedKeyword)) ||
|
(node.remark &&
|
||||||
|
node.remark.toLowerCase().includes(normalizedKeyword)) ||
|
||||||
(node.serverIp &&
|
(node.serverIp &&
|
||||||
node.serverIp.toLowerCase().includes(normalizedKeyword)) ||
|
node.serverIp.toLowerCase().includes(normalizedKeyword)) ||
|
||||||
(node.serverIpV4 &&
|
(node.serverIpV4 &&
|
||||||
@@ -1423,34 +1444,31 @@ export default function NodePage() {
|
|||||||
[sortedNodes],
|
[sortedNodes],
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredLocalNodes = useMemo(
|
const filteredLocalNodes = useMemo(() => {
|
||||||
() => {
|
const keywordFiltered = filterNodesByKeyword(
|
||||||
const keywordFiltered = filterNodesByKeyword(localNodes, localSearchKeyword);
|
localNodes,
|
||||||
|
localSearchKeyword,
|
||||||
|
);
|
||||||
|
|
||||||
if (nodeFilterMode === "all") {
|
if (nodeFilterMode === "all") {
|
||||||
return keywordFiltered;
|
return keywordFiltered;
|
||||||
|
}
|
||||||
|
|
||||||
|
return keywordFiltered.filter((node) => {
|
||||||
|
const expiryMeta = getNodeExpiryMeta(node.expiryTime, node.renewalCycle);
|
||||||
|
|
||||||
|
switch (nodeFilterMode) {
|
||||||
|
case "expiringSoon":
|
||||||
|
return expiryMeta.state === "expiringSoon";
|
||||||
|
case "expired":
|
||||||
|
return expiryMeta.state === "expired";
|
||||||
|
case "withExpiry":
|
||||||
|
return getNodeReminderEnabled(node);
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
return keywordFiltered.filter((node) => {
|
}, [filterNodesByKeyword, localNodes, localSearchKeyword, nodeFilterMode]);
|
||||||
const expiryMeta = getNodeExpiryMeta(
|
|
||||||
node.expiryTime,
|
|
||||||
node.renewalCycle,
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (nodeFilterMode) {
|
|
||||||
case "expiringSoon":
|
|
||||||
return expiryMeta.state === "expiringSoon";
|
|
||||||
case "expired":
|
|
||||||
return expiryMeta.state === "expired";
|
|
||||||
case "withExpiry":
|
|
||||||
return getNodeReminderEnabled(node);
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[filterNodesByKeyword, localNodes, localSearchKeyword, nodeFilterMode],
|
|
||||||
);
|
|
||||||
|
|
||||||
const filteredRemoteNodes = useMemo(
|
const filteredRemoteNodes = useMemo(
|
||||||
() => filterNodesByKeyword(remoteNodes, remoteSearchKeyword),
|
() => filterNodesByKeyword(remoteNodes, remoteSearchKeyword),
|
||||||
@@ -1472,7 +1490,8 @@ export default function NodePage() {
|
|||||||
const canUseExpiryFilter = activeTab === "local";
|
const canUseExpiryFilter = activeTab === "local";
|
||||||
const hasKeywordSearch = currentSearchKeyword.trim().length > 0;
|
const hasKeywordSearch = currentSearchKeyword.trim().length > 0;
|
||||||
const hasActiveFilters = nodeFilterMode !== "all";
|
const hasActiveFilters = nodeFilterMode !== "all";
|
||||||
const isDisplayFiltered = hasKeywordSearch || (canUseExpiryFilter && hasActiveFilters);
|
const isDisplayFiltered =
|
||||||
|
hasKeywordSearch || (canUseExpiryFilter && hasActiveFilters);
|
||||||
|
|
||||||
const sortableNodeIds = useMemo(
|
const sortableNodeIds = useMemo(
|
||||||
() => displayNodes.map((n) => n.id),
|
() => displayNodes.map((n) => n.id),
|
||||||
@@ -1491,7 +1510,11 @@ export default function NodePage() {
|
|||||||
onPress={() => setActiveTab("local")}
|
onPress={() => setActiveTab("local")}
|
||||||
>
|
>
|
||||||
本地节点
|
本地节点
|
||||||
<Chip className="ml-1 shrink-0 whitespace-nowrap" size="sm" variant="flat">
|
<Chip
|
||||||
|
className="ml-1 shrink-0 whitespace-nowrap"
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
{localNodes.length}
|
{localNodes.length}
|
||||||
</Chip>
|
</Chip>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1503,7 +1526,11 @@ export default function NodePage() {
|
|||||||
onPress={() => setActiveTab("remote")}
|
onPress={() => setActiveTab("remote")}
|
||||||
>
|
>
|
||||||
远程节点
|
远程节点
|
||||||
<Chip className="ml-1 shrink-0 whitespace-nowrap" size="sm" variant="flat">
|
<Chip
|
||||||
|
className="ml-1 shrink-0 whitespace-nowrap"
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
{remoteNodes.length}
|
{remoteNodes.length}
|
||||||
</Chip>
|
</Chip>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1526,13 +1553,16 @@ export default function NodePage() {
|
|||||||
onChange={setCurrentSearchKeyword}
|
onChange={setCurrentSearchKeyword}
|
||||||
onClose={() => setIsSearchVisible(false)}
|
onClose={() => setIsSearchVisible(false)}
|
||||||
onOpen={() => {
|
onOpen={() => {
|
||||||
setIsSearchVisible(true);
|
setIsSearchVisible(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 精准抓取带有“搜索”字样的输入框并强制聚焦
|
// 精准抓取带有“搜索”字样的输入框并强制聚焦
|
||||||
const searchInput = document.querySelector('input[placeholder*="搜索"]');
|
const searchInput = document.querySelector(
|
||||||
if (searchInput) (searchInput as HTMLElement).focus();
|
'input[placeholder*="搜索"]',
|
||||||
}, 150); // 150ms 刚好是 CSS 展开动画的时间
|
);
|
||||||
}}
|
|
||||||
|
if (searchInput) (searchInput as HTMLElement).focus();
|
||||||
|
}, 150); // 150ms 刚好是 CSS 展开动画的时间
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1592,7 +1622,6 @@ export default function NodePage() {
|
|||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
aria-label="筛选条件"
|
aria-label="筛选条件"
|
||||||
isDisabled={!canUseExpiryFilter}
|
|
||||||
className={
|
className={
|
||||||
canUseExpiryFilter && nodeFilterMode !== "all"
|
canUseExpiryFilter && nodeFilterMode !== "all"
|
||||||
? "bg-primary/20 text-primary relative"
|
? "bg-primary/20 text-primary relative"
|
||||||
@@ -1603,8 +1632,11 @@ export default function NodePage() {
|
|||||||
? "primary"
|
? "primary"
|
||||||
: "default"
|
: "default"
|
||||||
}
|
}
|
||||||
|
isDisabled={!canUseExpiryFilter}
|
||||||
size="sm"
|
size="sm"
|
||||||
title={canUseExpiryFilter ? "筛选条件" : "远程节点不支持到期筛选"}
|
title={
|
||||||
|
canUseExpiryFilter ? "筛选条件" : "远程节点不支持到期筛选"
|
||||||
|
}
|
||||||
variant="flat"
|
variant="flat"
|
||||||
onPress={() => setIsFilterModalOpen(true)}
|
onPress={() => setIsFilterModalOpen(true)}
|
||||||
>
|
>
|
||||||
@@ -1694,7 +1726,9 @@ export default function NodePage() {
|
|||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||||
{displayNodes.map((node) => {
|
{displayNodes.map((node) => {
|
||||||
const isRemoteNode = node.isRemote === 1;
|
const isRemoteNode = node.isRemote === 1;
|
||||||
const remoteUsage = isRemoteNode ? remoteUsageMap[node.id] : null;
|
const remoteUsage = isRemoteNode
|
||||||
|
? remoteUsageMap[node.id]
|
||||||
|
: null;
|
||||||
const expiryMeta = getNodeExpiryMeta(
|
const expiryMeta = getNodeExpiryMeta(
|
||||||
node.expiryTime,
|
node.expiryTime,
|
||||||
node.renewalCycle,
|
node.renewalCycle,
|
||||||
@@ -1704,7 +1738,10 @@ export default function NodePage() {
|
|||||||
);
|
);
|
||||||
const hasRemark = Boolean(node.remark?.trim());
|
const hasRemark = Boolean(node.remark?.trim());
|
||||||
const hasExpiryInfo = Boolean(
|
const hasExpiryInfo = Boolean(
|
||||||
node.expiryTime && node.expiryTime > 0 && node.renewalCycle,
|
node.expiryTime &&
|
||||||
|
node.expiryTime > 0 &&
|
||||||
|
node.renewalCycle &&
|
||||||
|
!node.expiryReminderDismissed,
|
||||||
);
|
);
|
||||||
const hasInfoTrigger = hasRemark || hasExpiryInfo;
|
const hasInfoTrigger = hasRemark || hasExpiryInfo;
|
||||||
const infoCount = Number(hasExpiryInfo) + Number(hasRemark);
|
const infoCount = Number(hasExpiryInfo) + Number(hasRemark);
|
||||||
@@ -1714,14 +1751,13 @@ export default function NodePage() {
|
|||||||
<SortableItem key={node.id} id={node.id}>
|
<SortableItem key={node.id} id={node.id}>
|
||||||
{(listeners, attributes) => (
|
{(listeners, attributes) => (
|
||||||
<Card
|
<Card
|
||||||
data-node-card="true"
|
|
||||||
key={node.id}
|
key={node.id}
|
||||||
className={`group relative overflow-visible shadow-sm border border-divider hover:shadow-md transition-shadow duration-200 h-full flex flex-col ${expiryMeta.accentClassName}`}
|
className={`group relative overflow-visible shadow-sm border border-divider hover:shadow-md transition-shadow duration-200 h-full flex flex-col ${expiryMeta.accentClassName}`}
|
||||||
|
data-node-card="true"
|
||||||
>
|
>
|
||||||
<CardHeader className="pb-3 md:pb-3">
|
<CardHeader className="pb-3 md:pb-3">
|
||||||
<div className="flex justify-between items-start w-full gap-3">
|
<div className="flex justify-between items-start w-full gap-3">
|
||||||
<div className="flex items-start gap-2 flex-1 min-w-0">
|
<div className="flex items-start gap-2 flex-1 min-w-0">
|
||||||
|
|
||||||
{selectMode && (
|
{selectMode && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
className="mt-0.5"
|
className="mt-0.5"
|
||||||
@@ -1742,9 +1778,23 @@ export default function NodePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-2 flex-shrink-0 self-start flex items-center gap-1.5">
|
<div className="ml-2 flex-shrink-0 self-start flex items-center gap-1.5">
|
||||||
|
<div
|
||||||
<div className="cursor-grab active:cursor-grabbing p-1 ml-1 shrink-0 text-default-400 hover:text-default-600 transition-colors touch-manipulation flex-shrink-0" {...attributes} {...listeners} style={{ touchAction: "none" }} title="拖拽排序"><svg aria-hidden="true" className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M7 2a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 2zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 8zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 14zm6-8a2 2 0 1 1-.001-4.001A2 2 0 0 1 13 6zm0 2a2 2 0 1 1 .001 4.001A2 2 0 0 1 13 8zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 13 14z" /></svg></div>
|
className="cursor-grab active:cursor-grabbing p-1 ml-1 shrink-0 text-default-400 hover:text-default-600 transition-colors touch-manipulation flex-shrink-0"
|
||||||
</div>
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
style={{ touchAction: "none" }}
|
||||||
|
title="拖拽排序"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="w-4 h-4"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path d="M7 2a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 2zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 8zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 14zm6-8a2 2 0 1 1-.001-4.001A2 2 0 0 1 13 6zm0 2a2 2 0 1 1 .001 4.001A2 2 0 0 1 13 8zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 13 14z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
@@ -1836,29 +1886,47 @@ export default function NodePage() {
|
|||||||
<>
|
<>
|
||||||
<div className="text-xs rounded-md border border-default-200 dark:border-default-100/30 bg-default-50 dark:bg-default-100/20 p-2.5 space-y-2">
|
<div className="text-xs rounded-md border border-default-200 dark:border-default-100/30 bg-default-50 dark:bg-default-100/20 p-2.5 space-y-2">
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<span className="text-default-500">远程地址</span>
|
<span className="text-default-500">
|
||||||
|
远程地址
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
className="font-mono text-right truncate"
|
className="font-mono text-right truncate"
|
||||||
title={remoteUsage.remoteUrl || node.remoteUrl || "-"}
|
title={
|
||||||
|
remoteUsage.remoteUrl ||
|
||||||
|
node.remoteUrl ||
|
||||||
|
"-"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{remoteUsage.remoteUrl || node.remoteUrl || "-"}
|
{remoteUsage.remoteUrl ||
|
||||||
|
node.remoteUrl ||
|
||||||
|
"-"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<span className="text-default-500">共享ID</span>
|
<span className="text-default-500">
|
||||||
<span className="font-mono">#{remoteUsage.shareId}</span>
|
共享ID
|
||||||
|
</span>
|
||||||
|
<span className="font-mono">
|
||||||
|
#{remoteUsage.shareId}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<span className="text-default-500">流量</span>
|
<span className="text-default-500">
|
||||||
|
流量
|
||||||
|
</span>
|
||||||
<span className="font-mono">
|
<span className="font-mono">
|
||||||
{formatFlow(remoteUsage.currentFlow)}
|
{formatFlow(remoteUsage.currentFlow)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<span className="text-default-500">带宽上限</span>
|
<span className="text-default-500">
|
||||||
|
带宽上限
|
||||||
|
</span>
|
||||||
<span className="font-mono">
|
<span className="font-mono">
|
||||||
{remoteUsage.maxBandwidth > 0
|
{remoteUsage.maxBandwidth > 0
|
||||||
? formatSpeed(remoteUsage.maxBandwidth)
|
? formatSpeed(
|
||||||
|
remoteUsage.maxBandwidth,
|
||||||
|
)
|
||||||
: "不限"}
|
: "不限"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1866,9 +1934,12 @@ export default function NodePage() {
|
|||||||
|
|
||||||
<div className="text-xs rounded-md border border-default-200 dark:border-default-100/30 bg-default-50 dark:bg-default-100/20 p-2.5">
|
<div className="text-xs rounded-md border border-default-200 dark:border-default-100/30 bg-default-50 dark:bg-default-100/20 p-2.5">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-default-500">占用端口</span>
|
<span className="text-default-500">
|
||||||
|
占用端口
|
||||||
|
</span>
|
||||||
<span className="font-mono text-default-700 dark:text-default-300">
|
<span className="font-mono text-default-700 dark:text-default-300">
|
||||||
{remoteUsage.usedPorts.length}/{Math.max(
|
{remoteUsage.usedPorts.length}/
|
||||||
|
{Math.max(
|
||||||
remoteUsage.portRangeEnd -
|
remoteUsage.portRangeEnd -
|
||||||
remoteUsage.portRangeStart +
|
remoteUsage.portRangeStart +
|
||||||
1,
|
1,
|
||||||
@@ -1891,14 +1962,18 @@ export default function NodePage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-default-400">暂无占用端口</div>
|
<div className="text-default-400">
|
||||||
|
暂无占用端口
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs rounded-md border border-default-200 dark:border-default-100/30 bg-default-50 dark:bg-default-100/20 p-2.5">
|
<div className="text-xs rounded-md border border-default-200 dark:border-default-100/30 bg-default-50 dark:bg-default-100/20 p-2.5">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-default-500">绑定明细</span>
|
<span className="text-default-500">
|
||||||
|
绑定明细
|
||||||
|
</span>
|
||||||
<span className="font-mono text-default-700 dark:text-default-300">
|
<span className="font-mono text-default-700 dark:text-default-300">
|
||||||
{remoteUsage.activeBindingNum}
|
{remoteUsage.activeBindingNum}
|
||||||
</span>
|
</span>
|
||||||
@@ -1922,13 +1997,22 @@ export default function NodePage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-[11px] text-default-500 flex items-center justify-between gap-2">
|
<div className="mt-1 text-[11px] text-default-500 flex items-center justify-between gap-2">
|
||||||
<span>{formatChainType(binding.chainType, binding.hopInx)}</span>
|
<span>
|
||||||
<span className="font-mono">端口 {binding.allocatedPort}</span>
|
{formatChainType(
|
||||||
|
binding.chainType,
|
||||||
|
binding.hopInx,
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="font-mono">
|
||||||
|
端口 {binding.allocatedPort}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="text-default-400">暂无绑定明细</div>
|
<div className="text-default-400">
|
||||||
|
暂无绑定明细
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2066,33 +2150,55 @@ export default function NodePage() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{hasInfoTrigger && (
|
{hasInfoTrigger && (
|
||||||
<div className="mt-2 mb-3 space-y-1.5 rounded-lg border border-divider/50 bg-default-50/50 p-2.5">
|
<div className="mt-2 mb-3 space-y-2 p-2.5">
|
||||||
{hasExpiryInfo && (
|
{hasExpiryInfo && (
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
<div className="space-y-2">
|
||||||
<span className="text-[11px] font-medium text-default-500 flex-shrink-0">
|
<div className="flex items-center justify-between">
|
||||||
提醒:
|
<div className="text-[11px] font-medium text-default-500">
|
||||||
</span>
|
到期提醒
|
||||||
<Chip
|
</div>
|
||||||
className="text-[10px] h-5 px-1 max-w-full flex-shrink-0"
|
<button
|
||||||
color={expiryMeta.tone}
|
className="text-[10px] text-default-400 hover:text-default-600 transition-colors"
|
||||||
size="sm"
|
type="button"
|
||||||
variant="flat"
|
onClick={(e) => {
|
||||||
>
|
e.stopPropagation();
|
||||||
<span className="truncate max-w-[120px] inline-block align-bottom">{expiryMeta.label}</span>
|
handleDismissExpiryReminder(node.id);
|
||||||
</Chip>
|
}}
|
||||||
<span className="text-[11px] text-default-600 truncate flex-1 min-w-0">
|
>
|
||||||
{formatNodeRenewalTime(expiryMeta.nextDueTime).split(' ')[0]}
|
关闭提醒
|
||||||
</span>
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
<Chip
|
||||||
|
className="text-[10px] h-5 px-1 flex-shrink-0"
|
||||||
|
color={expiryMeta.tone}
|
||||||
|
size="sm"
|
||||||
|
title={`${formatNodeRenewalTime(expiryMeta.nextDueTime)} (${getNodeRenewalCycleLabel(node.renewalCycle)})`}
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
|
{expiryMeta.label}
|
||||||
|
</Chip>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border border-divider/80 bg-default-50/80 px-3 py-2 text-xs leading-5 text-default-700">
|
||||||
|
{formatNodeRenewalTime(
|
||||||
|
expiryMeta.nextDueTime,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasRemark && (
|
{hasRemark && (
|
||||||
<div className="flex items-start gap-1.5">
|
<div className="space-y-2">
|
||||||
<span className="text-[11px] font-medium text-default-500 flex-shrink-0">
|
<div className="text-[11px] font-medium text-default-500">
|
||||||
备注:{node.remark?.trim()}
|
备注
|
||||||
</span>
|
</div>
|
||||||
|
<div
|
||||||
|
className="max-h-32 overflow-y-auto rounded-lg border border-divider/80 bg-default-50/80 px-3 py-2 text-xs leading-5 text-default-700 break-all [scrollbar-width:thin]"
|
||||||
|
title={node.remark?.trim()}
|
||||||
|
>
|
||||||
|
{node.remark?.trim()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -2181,8 +2287,11 @@ export default function NodePage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 新增/编辑节点对话框 */}
|
{/* 新增/编辑节点对话框 */}
|
||||||
<Modal classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }}
|
<Modal
|
||||||
backdrop="blur"
|
backdrop="blur"
|
||||||
|
classNames={{
|
||||||
|
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
|
||||||
|
}}
|
||||||
isOpen={dialogVisible}
|
isOpen={dialogVisible}
|
||||||
placement="center"
|
placement="center"
|
||||||
scrollBehavior="outside"
|
scrollBehavior="outside"
|
||||||
@@ -2252,7 +2361,8 @@ export default function NodePage() {
|
|||||||
errorMessage={errors.expiryTime}
|
errorMessage={errors.expiryTime}
|
||||||
isInvalid={!!errors.expiryTime}
|
isInvalid={!!errors.expiryTime}
|
||||||
label="续费基准时间"
|
label="续费基准时间"
|
||||||
type="date" max="9999-12-31"
|
max="9999-12-31"
|
||||||
|
type="date"
|
||||||
value={
|
value={
|
||||||
form.expiryTime > 0
|
form.expiryTime > 0
|
||||||
? new Date(form.expiryTime).toISOString().slice(0, 10)
|
? new Date(form.expiryTime).toISOString().slice(0, 10)
|
||||||
@@ -2599,8 +2709,10 @@ export default function NodePage() {
|
|||||||
|
|
||||||
{/* 回退确认模态框 */}
|
{/* 回退确认模态框 */}
|
||||||
<Modal
|
<Modal
|
||||||
classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }}
|
|
||||||
backdrop="blur"
|
backdrop="blur"
|
||||||
|
classNames={{
|
||||||
|
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
|
||||||
|
}}
|
||||||
isOpen={rollbackModalOpen}
|
isOpen={rollbackModalOpen}
|
||||||
placement="center"
|
placement="center"
|
||||||
scrollBehavior="outside"
|
scrollBehavior="outside"
|
||||||
@@ -2615,7 +2727,9 @@ export default function NodePage() {
|
|||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<p>
|
<p>
|
||||||
确定要将节点 <strong>"{nodeToRollback?.name}"</strong> 回退到上一个版本吗?
|
确定要将节点{" "}
|
||||||
|
<strong>"{nodeToRollback?.name}"</strong>{" "}
|
||||||
|
回退到上一个版本吗?
|
||||||
</p>
|
</p>
|
||||||
<p className="text-small text-default-500">
|
<p className="text-small text-default-500">
|
||||||
节点将执行版本回退并自动重启,期间会导致节点短暂离线。
|
节点将执行版本回退并自动重启,期间会导致节点短暂离线。
|
||||||
@@ -2635,8 +2749,11 @@ export default function NodePage() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* 删除确认模态框 */}
|
{/* 删除确认模态框 */}
|
||||||
<Modal classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }}
|
<Modal
|
||||||
backdrop="blur"
|
backdrop="blur"
|
||||||
|
classNames={{
|
||||||
|
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
|
||||||
|
}}
|
||||||
isOpen={deleteModalOpen}
|
isOpen={deleteModalOpen}
|
||||||
placement="center"
|
placement="center"
|
||||||
scrollBehavior="outside"
|
scrollBehavior="outside"
|
||||||
@@ -2675,8 +2792,11 @@ export default function NodePage() {
|
|||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }}
|
<Modal
|
||||||
backdrop="blur"
|
backdrop="blur"
|
||||||
|
classNames={{
|
||||||
|
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
|
||||||
|
}}
|
||||||
isOpen={installSelectorOpen}
|
isOpen={installSelectorOpen}
|
||||||
placement="center"
|
placement="center"
|
||||||
size="md"
|
size="md"
|
||||||
@@ -2725,8 +2845,11 @@ export default function NodePage() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* 安装命令模态框 */}
|
{/* 安装命令模态框 */}
|
||||||
<Modal classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }}
|
<Modal
|
||||||
backdrop="blur"
|
backdrop="blur"
|
||||||
|
classNames={{
|
||||||
|
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
|
||||||
|
}}
|
||||||
isOpen={installCommandModal}
|
isOpen={installCommandModal}
|
||||||
placement="center"
|
placement="center"
|
||||||
scrollBehavior="outside"
|
scrollBehavior="outside"
|
||||||
@@ -2779,8 +2902,11 @@ export default function NodePage() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* 版本选择升级模态框 */}
|
{/* 版本选择升级模态框 */}
|
||||||
<Modal classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }}
|
<Modal
|
||||||
backdrop="blur"
|
backdrop="blur"
|
||||||
|
classNames={{
|
||||||
|
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
|
||||||
|
}}
|
||||||
isOpen={upgradeModalOpen}
|
isOpen={upgradeModalOpen}
|
||||||
placement="center"
|
placement="center"
|
||||||
scrollBehavior="outside"
|
scrollBehavior="outside"
|
||||||
@@ -2882,8 +3008,11 @@ export default function NodePage() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* 批量删除确认模态框 */}
|
{/* 批量删除确认模态框 */}
|
||||||
<Modal classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }}
|
<Modal
|
||||||
backdrop="blur"
|
backdrop="blur"
|
||||||
|
classNames={{
|
||||||
|
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
|
||||||
|
}}
|
||||||
isOpen={batchDeleteModalOpen}
|
isOpen={batchDeleteModalOpen}
|
||||||
placement="center"
|
placement="center"
|
||||||
scrollBehavior="outside"
|
scrollBehavior="outside"
|
||||||
@@ -2922,7 +3051,10 @@ export default function NodePage() {
|
|||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }}
|
<Modal
|
||||||
|
classNames={{
|
||||||
|
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
|
||||||
|
}}
|
||||||
isOpen={isFilterModalOpen}
|
isOpen={isFilterModalOpen}
|
||||||
placement="center"
|
placement="center"
|
||||||
size="md"
|
size="md"
|
||||||
|
|||||||
@@ -677,7 +677,11 @@ export default function PanelSharingPage() {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
{/* Create Share Modal */}
|
{/* Create Share Modal */}
|
||||||
<Modal backdrop="blur" classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }}
|
<Modal
|
||||||
|
backdrop="blur"
|
||||||
|
classNames={{
|
||||||
|
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
|
||||||
|
}}
|
||||||
isOpen={createShareOpen}
|
isOpen={createShareOpen}
|
||||||
scrollBehavior="inside"
|
scrollBehavior="inside"
|
||||||
onClose={() => setCreateShareOpen(false)}
|
onClose={() => setCreateShareOpen(false)}
|
||||||
@@ -784,7 +788,11 @@ export default function PanelSharingPage() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* Edit Share Modal */}
|
{/* Edit Share Modal */}
|
||||||
<Modal backdrop="blur" classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }}
|
<Modal
|
||||||
|
backdrop="blur"
|
||||||
|
classNames={{
|
||||||
|
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
|
||||||
|
}}
|
||||||
isOpen={editShareOpen}
|
isOpen={editShareOpen}
|
||||||
scrollBehavior="inside"
|
scrollBehavior="inside"
|
||||||
onClose={() => setEditShareOpen(false)}
|
onClose={() => setEditShareOpen(false)}
|
||||||
@@ -883,7 +891,14 @@ export default function PanelSharingPage() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* Import Node Modal */}
|
{/* Import Node Modal */}
|
||||||
<Modal backdrop="blur" classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }} isOpen={importNodeOpen} onClose={() => setImportNodeOpen(false)}>
|
<Modal
|
||||||
|
backdrop="blur"
|
||||||
|
classNames={{
|
||||||
|
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
|
||||||
|
}}
|
||||||
|
isOpen={importNodeOpen}
|
||||||
|
onClose={() => setImportNodeOpen(false)}
|
||||||
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>导入远程节点</ModalHeader>
|
<ModalHeader>导入远程节点</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
|
|||||||
@@ -325,8 +325,11 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 修改密码弹窗 */}
|
{/* 修改密码弹窗 */}
|
||||||
<Modal classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }}
|
<Modal
|
||||||
backdrop="blur"
|
backdrop="blur"
|
||||||
|
classNames={{
|
||||||
|
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
|
||||||
|
}}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
placement="center"
|
placement="center"
|
||||||
scrollBehavior="outside"
|
scrollBehavior="outside"
|
||||||
|
|||||||
@@ -3122,6 +3122,7 @@ export default function TunnelPage() {
|
|||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
setBatchResultModal((prev) => ({ ...prev, open: true }));
|
setBatchResultModal((prev) => ({ ...prev, open: true }));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setBatchResultModal(EMPTY_BATCH_RESULT_MODAL_STATE);
|
setBatchResultModal(EMPTY_BATCH_RESULT_MODAL_STATE);
|
||||||
|
|||||||
@@ -59,7 +59,12 @@ import {
|
|||||||
getUserGroupList,
|
getUserGroupList,
|
||||||
getUserGroups,
|
getUserGroups,
|
||||||
} from "@/api";
|
} from "@/api";
|
||||||
import { EditIcon, DeleteIcon, SettingsIcon, SearchIcon } from "@/components/icons";
|
import {
|
||||||
|
EditIcon,
|
||||||
|
DeleteIcon,
|
||||||
|
SettingsIcon,
|
||||||
|
SearchIcon,
|
||||||
|
} from "@/components/icons";
|
||||||
import { PageLoadingState } from "@/components/page-state";
|
import { PageLoadingState } from "@/components/page-state";
|
||||||
import { useLocalStorageState } from "@/hooks/use-local-storage-state";
|
import { useLocalStorageState } from "@/hooks/use-local-storage-state";
|
||||||
import { removeItemsById, replaceItemById } from "@/utils/list-state";
|
import { removeItemsById, replaceItemById } from "@/utils/list-state";
|
||||||
|
|||||||
Reference in New Issue
Block a user