mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-05 01:26:37 +08:00
fix(forward): stabilize selection in non-compact grouped view
Prevent cascading checkbox toggles and scope select-all per tunnel group; update grouped styling to neutral gray.
This commit is contained in:
@@ -11,7 +11,7 @@ function Checkbox({
|
|||||||
return (
|
return (
|
||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
className={cn(
|
className={cn(
|
||||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow transition-transform duration-100 active:scale-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground",
|
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow transition-transform duration-100 active:scale-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground data-[state=checked]:shadow-none data-[state=indeterminate]:shadow-none",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
data-slot="checkbox"
|
data-slot="checkbox"
|
||||||
|
|||||||
+158
-114
@@ -651,7 +651,7 @@ const SortableTunnelGroupContainer = ({
|
|||||||
</Button>
|
</Button>
|
||||||
{/* 倍率 */}
|
{/* 倍率 */}
|
||||||
<span className={titleClassName}>{tunnel.tunnelName}</span>
|
<span className={titleClassName}>{tunnel.tunnelName}</span>
|
||||||
<span className="text-primary font-bold text-[10px] mr-1.5">
|
<span className="text-default-500 font-semibold text-[10px] mr-1.5">
|
||||||
[{formatTunnelTrafficRatio(tunnel.tunnelTrafficRatio)}]
|
[{formatTunnelTrafficRatio(tunnel.tunnelTrafficRatio)}]
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -753,7 +753,7 @@ const SortableTableRow = ({
|
|||||||
<TableCell className={FORWARD_GROUPED_TABLE_COLUMN_CLASS.select}>
|
<TableCell className={FORWARD_GROUPED_TABLE_COLUMN_CLASS.select}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
isSelected={selectedIds.has(forward.id)}
|
isSelected={selectedIds.has(forward.id)}
|
||||||
onValueChange={() => toggleSelect(forward.id)}
|
onValueChange={(checked) => toggleSelect(forward.id, checked)}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
@@ -775,13 +775,13 @@ const SortableTableRow = ({
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
className={`${selectedIds.has(forward.id) ? "bg-primary-50/70 dark:bg-primary-900/40" : ""} ${FORWARD_GROUPED_TABLE_COLUMN_CLASS.name} whitespace-nowrap text-foreground cursor-pointer hover:text-primary transition-colors`}
|
className={`${FORWARD_GROUPED_TABLE_COLUMN_CLASS.name} whitespace-nowrap text-foreground cursor-pointer hover:text-primary transition-colors`}
|
||||||
onClick={() => copyToClipboard(forward.name, "规则名")}
|
onClick={() => copyToClipboard(forward.name, "规则名")}
|
||||||
>
|
>
|
||||||
{forward.name}
|
{forward.name}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
className={`${FORWARD_GROUPED_TABLE_COLUMN_CLASS.inbound} max-w-[280px] ${selectedIds.has(forward.id) ? "bg-primary-50/70 dark:bg-primary-900/40" : ""}`}
|
className={`${FORWARD_GROUPED_TABLE_COLUMN_CLASS.inbound} max-w-[280px]`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="w-full truncate rounded-md bg-default-100/50 px-2.5 py-1.5 text-left font-mono text-xs font-medium text-default-700 transition-all hover:bg-default-200 hover:shadow-sm cursor-pointer"
|
className="w-full truncate rounded-md bg-default-100/50 px-2.5 py-1.5 text-left font-mono text-xs font-medium text-default-700 transition-all hover:bg-default-200 hover:shadow-sm cursor-pointer"
|
||||||
@@ -795,7 +795,7 @@ const SortableTableRow = ({
|
|||||||
</button>
|
</button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
className={`${FORWARD_GROUPED_TABLE_COLUMN_CLASS.target} max-w-[280px] ${selectedIds.has(forward.id) ? "bg-primary-50/70 dark:bg-primary-900/40" : ""}`}
|
className={`${FORWARD_GROUPED_TABLE_COLUMN_CLASS.target} max-w-[280px]`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="w-full truncate rounded-md bg-default-100/50 px-2.5 py-1.5 text-left font-mono text-xs font-medium text-default-700 transition-all hover:bg-default-200 hover:shadow-sm cursor-pointer"
|
className="w-full truncate rounded-md bg-default-100/50 px-2.5 py-1.5 text-left font-mono text-xs font-medium text-default-700 transition-all hover:bg-default-200 hover:shadow-sm cursor-pointer"
|
||||||
@@ -817,7 +817,7 @@ const SortableTableRow = ({
|
|||||||
</Chip>
|
</Chip>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
className={`${FORWARD_GROUPED_TABLE_COLUMN_CLASS.totalFlow} whitespace-nowrap ${selectedIds.has(forward.id) ? "bg-primary-50/70 dark:bg-primary-900/40" : ""}`}
|
className={`${FORWARD_GROUPED_TABLE_COLUMN_CLASS.totalFlow} whitespace-nowrap`}
|
||||||
>
|
>
|
||||||
<span className="text-sm font-medium text-default-600 font-mono">
|
<span className="text-sm font-medium text-default-600 font-mono">
|
||||||
{formatFlow(getForwardDisplayFlow(forward))}
|
{formatFlow(getForwardDisplayFlow(forward))}
|
||||||
@@ -948,12 +948,10 @@ const SortableCompactTableRow = ({
|
|||||||
return (
|
return (
|
||||||
<TableRow key={forward.id} ref={setNodeRef} style={style}>
|
<TableRow key={forward.id} ref={setNodeRef} style={style}>
|
||||||
{true && (
|
{true && (
|
||||||
<TableCell
|
<TableCell>
|
||||||
className={`${selectedIds.has(forward.id) ? "bg-primary-50/70 dark:bg-primary-900/40" : ""}`}
|
|
||||||
>
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
isSelected={selectedIds.has(forward.id)}
|
isSelected={selectedIds.has(forward.id)}
|
||||||
onValueChange={() => toggleSelect(forward.id)}
|
onValueChange={(checked) => toggleSelect(forward.id, checked)}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
@@ -3069,15 +3067,22 @@ export default function ForwardPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const toggleSelect = (id: number) => {
|
const toggleSelect = (id: number, explicitSelected?: boolean) => {
|
||||||
const newSet = new Set(selectedIds);
|
setSelectedIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
const shouldSelect =
|
||||||
|
typeof explicitSelected === "boolean"
|
||||||
|
? explicitSelected
|
||||||
|
: !next.has(id);
|
||||||
|
|
||||||
if (newSet.has(id)) {
|
if (shouldSelect) {
|
||||||
newSet.delete(id);
|
next.add(id);
|
||||||
} else {
|
} else {
|
||||||
newSet.add(id);
|
next.delete(id);
|
||||||
}
|
}
|
||||||
setSelectedIds(newSet);
|
|
||||||
|
return next;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
const deselectAll = () => {
|
const deselectAll = () => {
|
||||||
setSelectedIds(new Set());
|
setSelectedIds(new Set());
|
||||||
@@ -3691,7 +3696,7 @@ export default function ForwardPage() {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
className="mr-2"
|
className="mr-2"
|
||||||
isSelected={selectedIds.has(forward.id)}
|
isSelected={selectedIds.has(forward.id)}
|
||||||
onValueChange={() => toggleSelect(forward.id)}
|
onValueChange={(checked) => toggleSelect(forward.id, checked)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -4358,6 +4363,42 @@ export default function ForwardPage() {
|
|||||||
const tunnelSortableForwardIds = tunnel.items
|
const tunnelSortableForwardIds = tunnel.items
|
||||||
.map((item) => item.id)
|
.map((item) => item.id)
|
||||||
.filter((id) => id > 0);
|
.filter((id) => id > 0);
|
||||||
|
const tunnelSelectedCount =
|
||||||
|
tunnelSortableForwardIds.reduce(
|
||||||
|
(count, id) =>
|
||||||
|
count + (selectedIds.has(id) ? 1 : 0),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const isTunnelAllSelected =
|
||||||
|
tunnelSortableForwardIds.length > 0 &&
|
||||||
|
tunnelSelectedCount ===
|
||||||
|
tunnelSortableForwardIds.length;
|
||||||
|
const isTunnelIndeterminate =
|
||||||
|
tunnelSelectedCount > 0 &&
|
||||||
|
tunnelSelectedCount <
|
||||||
|
tunnelSortableForwardIds.length;
|
||||||
|
|
||||||
|
const handleTunnelSelectAllToggle = (
|
||||||
|
isSelected: boolean,
|
||||||
|
) => {
|
||||||
|
setSelectedIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
tunnelSortableForwardIds.forEach((id) => {
|
||||||
|
next.add(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnelSortableForwardIds.forEach((id) => {
|
||||||
|
next.delete(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
const collapsed =
|
const collapsed =
|
||||||
sanitizedCollapsedTunnelGroups[
|
sanitizedCollapsedTunnelGroups[
|
||||||
buildTunnelGroupCollapseKey(
|
buildTunnelGroupCollapseKey(
|
||||||
@@ -4371,12 +4412,12 @@ export default function ForwardPage() {
|
|||||||
key={`grouped-table-${group.userId}-${tunnel.tunnelKey}`}
|
key={`grouped-table-${group.userId}-${tunnel.tunnelKey}`}
|
||||||
bodyClassName=""
|
bodyClassName=""
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
countClassName="text-xs text-secondary-700"
|
countClassName="text-xs text-default-600"
|
||||||
groupUserId={group.userId}
|
groupUserId={group.userId}
|
||||||
headerClassName="flex items-center justify-between border-b border-secondary/20 bg-secondary/10 px-4 py-2.5"
|
headerClassName="flex items-center justify-between border-b border-divider bg-default-100/60 px-4 py-2.5"
|
||||||
titleClassName="truncate text-sm font-semibold text-secondary-700"
|
titleClassName="truncate text-sm font-semibold text-default-700"
|
||||||
tunnel={tunnel}
|
tunnel={tunnel}
|
||||||
wrapperClassName="overflow-hidden rounded-lg border border-secondary/20 bg-secondary/5"
|
wrapperClassName="overflow-hidden rounded-lg border border-divider bg-default-50/60"
|
||||||
onToggleCollapsed={() =>
|
onToggleCollapsed={() =>
|
||||||
toggleTunnelGroupCollapsed(
|
toggleTunnelGroupCollapsed(
|
||||||
group.userId,
|
group.userId,
|
||||||
@@ -4389,92 +4430,95 @@ export default function ForwardPage() {
|
|||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
<Table
|
<SortableContext
|
||||||
aria-label={`${group.userName}-${tunnel.tunnelName}规则列表`}
|
items={tunnelSortableForwardIds}
|
||||||
className={`table-fixed ${FORWARD_GROUPED_TABLE_MIN_WIDTH_CLASS}`}
|
strategy={verticalListSortingStrategy}
|
||||||
classNames={{
|
|
||||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
|
||||||
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
|
|
||||||
tr: "hover:bg-default-50/50 transition-colors",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<TableHeader>
|
<Table
|
||||||
{true && (
|
aria-label={`${group.userName}-${tunnel.tunnelName}规则列表`}
|
||||||
<TableColumn className="w-14">
|
className={`table-fixed ${FORWARD_GROUPED_TABLE_MIN_WIDTH_CLASS}`}
|
||||||
{/* @ts-ignore */}
|
classNames={{
|
||||||
<Checkbox
|
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
||||||
aria-label="全选"
|
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
|
||||||
isIndeterminate={isIndeterminate}
|
tr: "hover:bg-default-50/50 transition-colors",
|
||||||
isSelected={isAllSelected}
|
}}
|
||||||
onValueChange={handleSelectAllToggle}
|
|
||||||
/>
|
|
||||||
</TableColumn>
|
|
||||||
)}
|
|
||||||
<TableColumn
|
|
||||||
className={
|
|
||||||
FORWARD_GROUPED_TABLE_COLUMN_CLASS.drag
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TableColumn
|
|
||||||
className={
|
|
||||||
FORWARD_GROUPED_TABLE_COLUMN_CLASS.name
|
|
||||||
}
|
|
||||||
>
|
|
||||||
名称
|
|
||||||
</TableColumn>
|
|
||||||
<TableColumn
|
|
||||||
className={
|
|
||||||
FORWARD_GROUPED_TABLE_COLUMN_CLASS.inbound
|
|
||||||
}
|
|
||||||
>
|
|
||||||
入口
|
|
||||||
</TableColumn>
|
|
||||||
<TableColumn
|
|
||||||
className={
|
|
||||||
FORWARD_GROUPED_TABLE_COLUMN_CLASS.target
|
|
||||||
}
|
|
||||||
>
|
|
||||||
目标
|
|
||||||
</TableColumn>
|
|
||||||
<TableColumn
|
|
||||||
className={
|
|
||||||
FORWARD_GROUPED_TABLE_COLUMN_CLASS.strategy
|
|
||||||
}
|
|
||||||
>
|
|
||||||
策略
|
|
||||||
</TableColumn>
|
|
||||||
<TableColumn
|
|
||||||
className={
|
|
||||||
FORWARD_GROUPED_TABLE_COLUMN_CLASS.totalFlow
|
|
||||||
}
|
|
||||||
>
|
|
||||||
总流量
|
|
||||||
</TableColumn>
|
|
||||||
<TableColumn
|
|
||||||
className={
|
|
||||||
FORWARD_GROUPED_TABLE_COLUMN_CLASS.status
|
|
||||||
}
|
|
||||||
>
|
|
||||||
状态
|
|
||||||
</TableColumn>
|
|
||||||
<TableColumn
|
|
||||||
className={
|
|
||||||
FORWARD_GROUPED_TABLE_COLUMN_CLASS.actions
|
|
||||||
}
|
|
||||||
>
|
|
||||||
操作
|
|
||||||
</TableColumn>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody
|
|
||||||
emptyContent="暂无规则配置"
|
|
||||||
items={tunnel.items}
|
|
||||||
>
|
>
|
||||||
{(forward) => (
|
<TableHeader>
|
||||||
<SortableContext
|
{true && (
|
||||||
key={forward.id}
|
<TableColumn className="w-14">
|
||||||
items={tunnelSortableForwardIds}
|
{/* @ts-ignore */}
|
||||||
strategy={verticalListSortingStrategy}
|
<Checkbox
|
||||||
|
aria-label="全选"
|
||||||
|
isIndeterminate={
|
||||||
|
isTunnelIndeterminate
|
||||||
|
}
|
||||||
|
isSelected={isTunnelAllSelected}
|
||||||
|
onValueChange={
|
||||||
|
handleTunnelSelectAllToggle
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableColumn>
|
||||||
|
)}
|
||||||
|
<TableColumn
|
||||||
|
className={
|
||||||
|
FORWARD_GROUPED_TABLE_COLUMN_CLASS.drag
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TableColumn
|
||||||
|
className={
|
||||||
|
FORWARD_GROUPED_TABLE_COLUMN_CLASS.name
|
||||||
|
}
|
||||||
>
|
>
|
||||||
|
名称
|
||||||
|
</TableColumn>
|
||||||
|
<TableColumn
|
||||||
|
className={
|
||||||
|
FORWARD_GROUPED_TABLE_COLUMN_CLASS.inbound
|
||||||
|
}
|
||||||
|
>
|
||||||
|
入口
|
||||||
|
</TableColumn>
|
||||||
|
<TableColumn
|
||||||
|
className={
|
||||||
|
FORWARD_GROUPED_TABLE_COLUMN_CLASS.target
|
||||||
|
}
|
||||||
|
>
|
||||||
|
目标
|
||||||
|
</TableColumn>
|
||||||
|
<TableColumn
|
||||||
|
className={
|
||||||
|
FORWARD_GROUPED_TABLE_COLUMN_CLASS.strategy
|
||||||
|
}
|
||||||
|
>
|
||||||
|
策略
|
||||||
|
</TableColumn>
|
||||||
|
<TableColumn
|
||||||
|
className={
|
||||||
|
FORWARD_GROUPED_TABLE_COLUMN_CLASS.totalFlow
|
||||||
|
}
|
||||||
|
>
|
||||||
|
总流量
|
||||||
|
</TableColumn>
|
||||||
|
<TableColumn
|
||||||
|
className={
|
||||||
|
FORWARD_GROUPED_TABLE_COLUMN_CLASS.status
|
||||||
|
}
|
||||||
|
>
|
||||||
|
状态
|
||||||
|
</TableColumn>
|
||||||
|
<TableColumn
|
||||||
|
className={
|
||||||
|
FORWARD_GROUPED_TABLE_COLUMN_CLASS.actions
|
||||||
|
}
|
||||||
|
>
|
||||||
|
操作
|
||||||
|
</TableColumn>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody
|
||||||
|
emptyContent="暂无规则配置"
|
||||||
|
items={tunnel.items}
|
||||||
|
>
|
||||||
|
{(forward) => (
|
||||||
<SortableTableRow
|
<SortableTableRow
|
||||||
copyToClipboard={copyToClipboard}
|
copyToClipboard={copyToClipboard}
|
||||||
formatFlow={formatFlow}
|
formatFlow={formatFlow}
|
||||||
@@ -4500,10 +4544,10 @@ export default function ForwardPage() {
|
|||||||
showAddressModal={showAddressModal}
|
showAddressModal={showAddressModal}
|
||||||
toggleSelect={toggleSelect}
|
toggleSelect={toggleSelect}
|
||||||
/>
|
/>
|
||||||
</SortableContext>
|
)}
|
||||||
)}
|
</TableBody>
|
||||||
</TableBody>
|
</Table>
|
||||||
</Table>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
</SortableTunnelGroupContainer>
|
</SortableTunnelGroupContainer>
|
||||||
);
|
);
|
||||||
@@ -4590,12 +4634,12 @@ export default function ForwardPage() {
|
|||||||
key={`direct-group-${group.userId}-${tunnel.tunnelKey}`}
|
key={`direct-group-${group.userId}-${tunnel.tunnelKey}`}
|
||||||
bodyClassName="p-3"
|
bodyClassName="p-3"
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
countClassName="text-xs text-secondary-700"
|
countClassName="text-xs text-default-600"
|
||||||
groupUserId={group.userId}
|
groupUserId={group.userId}
|
||||||
headerClassName="flex items-center justify-between rounded-lg bg-secondary/10 px-3 py-2"
|
headerClassName="flex items-center justify-between rounded-lg bg-default-100/60 px-3 py-2"
|
||||||
titleClassName="truncate text-sm font-semibold text-secondary-700"
|
titleClassName="truncate text-sm font-semibold text-default-700"
|
||||||
tunnel={tunnel}
|
tunnel={tunnel}
|
||||||
wrapperClassName="rounded-xl border border-secondary/20 bg-secondary/5 space-y-3"
|
wrapperClassName="rounded-xl border border-divider bg-default-50/60 space-y-3"
|
||||||
onToggleCollapsed={() =>
|
onToggleCollapsed={() =>
|
||||||
toggleTunnelGroupCollapsed(
|
toggleTunnelGroupCollapsed(
|
||||||
group.userId,
|
group.userId,
|
||||||
|
|||||||
Reference in New Issue
Block a user