fix(frontend): stabilize multiselect behavior and UI spacing (#154)

* fix(frontend): left-align announcement logo in homepage banner

* fix(frontend): keep multiselect panels floating and preserve summaries

* fix(frontend): rebalance config card header spacing

* fix(frontend): normalize formatting and fix multiselect modal behavior

* fix(frontend): restore config divider spacing on desktop
This commit is contained in:
sagit
2026-02-19 20:50:12 +08:00
committed by GitHub
parent 18c2da7c7e
commit 66de566a00
52 changed files with 743 additions and 301 deletions
+3 -3
View File
@@ -1,10 +1,10 @@
import { Button } from "@/shadcn-bridge/heroui/button";
import { Input } from "@/shadcn-bridge/heroui/input";
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import toast from "react-hot-toast";
import { Button } from "@/shadcn-bridge/heroui/button";
import { Input } from "@/shadcn-bridge/heroui/input";
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { title } from "@/components/primitives";
import { updatePassword } from "@/api";
import DefaultLayout from "@/layouts/default";
+6 -6
View File
@@ -585,7 +585,7 @@ export default function ConfigPage() {
</div>
<Card className="shadow-md">
<CardHeader className="pb-4">
<CardHeader className="pb-6">
<div className="flex justify-between items-center w-full">
<div>
<h2 className="text-xl font-semibold">基本设置</h2>
@@ -609,7 +609,7 @@ export default function ConfigPage() {
<Divider />
<CardBody className="space-y-6 pt-6">
<CardBody className="space-y-6 pt-8 md:pt-8">
{CONFIG_ITEMS.map((item, index) => {
// 检查配置项是否应该显示
if (!shouldShowItem(item)) {
@@ -660,7 +660,7 @@ export default function ConfigPage() {
)}
<Card className="mt-6 shadow-md">
<CardHeader className="pb-4">
<CardHeader className="pb-6">
<div className="flex justify-between items-center w-full">
<div>
<h2 className="text-xl font-semibold">公告管理</h2>
@@ -673,7 +673,7 @@ export default function ConfigPage() {
<Divider />
<CardBody className="space-y-4 pt-6">
<CardBody className="space-y-4 pt-8 md:pt-8">
{announcementLoading ? (
<div className="flex justify-center py-8">
<Spinner size="lg" />
@@ -727,7 +727,7 @@ export default function ConfigPage() {
{/* 备份与恢复 */}
<Card className="mt-6 shadow-md">
<CardHeader className="pb-4">
<CardHeader className="pb-6">
<div className="flex justify-between items-center w-full">
<div>
<h2 className="text-xl font-semibold">数据备份与恢复</h2>
@@ -740,7 +740,7 @@ export default function ConfigPage() {
<Divider />
<CardBody className="space-y-6 pt-6">
<CardBody className="space-y-6 pt-8 md:pt-8">
{/* 导出部分 */}
<div className="space-y-4">
<h3 className="text-lg font-medium">导出数据</h3>
+8 -3
View File
@@ -1,9 +1,14 @@
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { Button } from "@/shadcn-bridge/heroui/button";
import { Modal, ModalContent, ModalHeader, ModalBody } from "@/shadcn-bridge/heroui/modal";
import { useState } from "react";
import toast from "react-hot-toast";
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { Button } from "@/shadcn-bridge/heroui/button";
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
} from "@/shadcn-bridge/heroui/modal";
import { PageEmptyState, PageLoadingState } from "@/components/page-state";
import { AnnouncementBanner } from "@/pages/dashboard/components/announcement-banner";
import { FlowChartCard } from "@/pages/dashboard/components/flow-chart-card";
@@ -16,7 +16,7 @@ export const AnnouncementBanner = ({
return (
<Card className="mb-4 lg:mb-6 border border-blue-200 dark:border-blue-500/30 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-500/10 dark:to-purple-500/10">
<CardBody className="p-4">
<div className="flex items-start gap-3">
<div className="flex items-center justify-start gap-3">
<div className="p-2 bg-blue-100 dark:bg-blue-500/20 rounded-lg flex-shrink-0">
<svg
aria-hidden="true"
@@ -1,4 +1,3 @@
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import {
CartesianGrid,
Line,
@@ -9,6 +8,7 @@ import {
YAxis,
} from "recharts";
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { PageEmptyState } from "@/components/page-state";
interface FlowChartPoint {
@@ -1,3 +1,5 @@
import type { ForwardApiItem } from "@/api/types";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
@@ -6,7 +8,6 @@ import {
getUserPackageInfo,
type AnnouncementData,
} from "@/api";
import type { ForwardApiItem } from "@/api/types";
import { getAdminFlag } from "@/utils/session";
export interface DashboardUserInfo {
@@ -180,9 +181,13 @@ export const useDashboardData = (): DashboardDataState => {
);
const [userTunnels, setUserTunnels] = useState<DashboardUserTunnel[]>([]);
const [forwardList, setForwardList] = useState<DashboardForward[]>([]);
const [statisticsFlows, setStatisticsFlows] = useState<DashboardStatisticsFlow[]>([]);
const [statisticsFlows, setStatisticsFlows] = useState<
DashboardStatisticsFlow[]
>([]);
const [isAdmin, setIsAdmin] = useState(false);
const [announcement, setAnnouncement] = useState<AnnouncementData | null>(null);
const [announcement, setAnnouncement] = useState<AnnouncementData | null>(
null,
);
useEffect(() => {
const loadAnnouncement = async () => {
@@ -214,7 +219,10 @@ export const useDashboardData = (): DashboardDataState => {
setForwardList(normalizedForwards);
setStatisticsFlows(data.statisticsFlows || []);
checkExpirationNotifications(data.userInfo, normalizedTunnelPermissions);
checkExpirationNotifications(
data.userInfo,
normalizedTunnelPermissions,
);
} else {
toast.error(res.msg || "获取套餐信息失败");
}
+30 -20
View File
@@ -1,22 +1,4 @@
import { useState, useEffect, useMemo } from "react";
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { Button } from "@/shadcn-bridge/heroui/button";
import { Input } from "@/shadcn-bridge/heroui/input";
import { Textarea } from "@/shadcn-bridge/heroui/input";
import { Select, SelectItem } from "@/shadcn-bridge/heroui/select";
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
} from "@/shadcn-bridge/heroui/modal";
import { Chip } from "@/shadcn-bridge/heroui/chip";
import { Spinner } from "@/shadcn-bridge/heroui/spinner";
import { Switch } from "@/shadcn-bridge/heroui/switch";
import { Alert } from "@/shadcn-bridge/heroui/alert";
import { Accordion, AccordionItem } from "@/shadcn-bridge/heroui/accordion";
import { Checkbox } from "@/shadcn-bridge/heroui/checkbox";
import toast from "react-hot-toast";
import {
DndContext,
@@ -37,6 +19,24 @@ import {
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { Button } from "@/shadcn-bridge/heroui/button";
import { Input } from "@/shadcn-bridge/heroui/input";
import { Textarea } from "@/shadcn-bridge/heroui/input";
import { Select, SelectItem } from "@/shadcn-bridge/heroui/select";
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
} from "@/shadcn-bridge/heroui/modal";
import { Chip } from "@/shadcn-bridge/heroui/chip";
import { Spinner } from "@/shadcn-bridge/heroui/spinner";
import { Switch } from "@/shadcn-bridge/heroui/switch";
import { Alert } from "@/shadcn-bridge/heroui/alert";
import { Accordion, AccordionItem } from "@/shadcn-bridge/heroui/accordion";
import { Checkbox } from "@/shadcn-bridge/heroui/checkbox";
import {
createForward,
getForwardList,
@@ -1604,10 +1604,20 @@ export default function ForwardPage() {
<span className="text-sm text-default-600 shrink-0">
已选择 {selectedIds.size} 项
</span>
<Button color="primary" size="sm" variant="flat" onPress={selectAll}>
<Button
color="primary"
size="sm"
variant="flat"
onPress={selectAll}
>
全选
</Button>
<Button color="secondary" size="sm" variant="flat" onPress={deselectAll}>
<Button
color="secondary"
size="sm"
variant="flat"
onPress={deselectAll}
>
清空
</Button>
<Button
+34 -14
View File
@@ -1,4 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import toast from "react-hot-toast";
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { Button } from "@/shadcn-bridge/heroui/button";
import { Input } from "@/shadcn-bridge/heroui/input";
@@ -21,8 +23,6 @@ import {
} from "@/shadcn-bridge/heroui/table";
import { Chip } from "@/shadcn-bridge/heroui/chip";
import { Spinner } from "@/shadcn-bridge/heroui/spinner";
import toast from "react-hot-toast";
import {
assignGroupPermission,
assignTunnelsToGroup,
@@ -173,6 +173,22 @@ export default function GroupPage() {
return map;
}, [users]);
const selectedTunnelSummary = useMemo(() => {
const value = Array.from(selectedTunnelKeys)
.map((id) => tunnelNameMap.get(Number(id)) || id)
.join("、");
return value || "无";
}, [selectedTunnelKeys, tunnelNameMap]);
const selectedUserSummary = useMemo(() => {
const value = Array.from(selectedUserKeys)
.map((id) => userNameMap.get(Number(id)) || id)
.join("、");
return value || "无";
}, [selectedUserKeys, userNameMap]);
const loadData = useCallback(async () => {
setLoading(true);
try {
@@ -765,8 +781,10 @@ export default function GroupPage() {
>
<ModalContent>
<ModalHeader>分配隧道 - {assignTunnelGroup?.name}</ModalHeader>
<ModalBody>
<ModalBody className="min-w-0">
<Select
className="min-w-0"
classNames={{ trigger: "max-w-full" }}
items={tunnels}
label="选择隧道"
selectedKeys={selectedTunnelKeys}
@@ -779,11 +797,11 @@ export default function GroupPage() {
>
{(item) => <SelectItem key={item.id}>{item.name}</SelectItem>}
</Select>
<p className="text-xs text-default-500 break-all whitespace-normal">
当前已选:
{Array.from(selectedTunnelKeys)
.map((id) => tunnelNameMap.get(Number(id)) || id)
.join("、") || "无"}
<p
className="w-full min-w-0 max-w-full text-xs text-default-500 truncate"
title={`当前已选:${selectedTunnelSummary}`}
>
当前已选:{selectedTunnelSummary}
</p>
<p className="text-xs text-default-500">
不选择任何隧道并保存将清空该分组成员。
@@ -810,8 +828,10 @@ export default function GroupPage() {
>
<ModalContent>
<ModalHeader>分配用户 - {assignUserGroup?.name}</ModalHeader>
<ModalBody>
<ModalBody className="min-w-0">
<Select
className="min-w-0"
classNames={{ trigger: "max-w-full" }}
items={users}
label="选择用户"
selectedKeys={selectedUserKeys}
@@ -824,11 +844,11 @@ export default function GroupPage() {
>
{(item) => <SelectItem key={item.id}>{item.user}</SelectItem>}
</Select>
<p className="text-xs text-default-500 break-all whitespace-normal">
当前已选:
{Array.from(selectedUserKeys)
.map((id) => userNameMap.get(Number(id)) || id)
.join("、") || "无"}
<p
className="w-full min-w-0 max-w-full text-xs text-default-500 truncate"
title={`当前已选:${selectedUserSummary}`}
>
当前已选:{selectedUserSummary}
</p>
<p className="text-xs text-default-500">
不选择任何用户并保存将清空该分组成员。
+3 -3
View File
@@ -1,11 +1,11 @@
import { Button } from "@/shadcn-bridge/heroui/button";
import { Input } from "@/shadcn-bridge/heroui/input";
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import toast from "react-hot-toast";
import { Turnstile } from "@marsidev/react-turnstile";
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { Input } from "@/shadcn-bridge/heroui/input";
import { Button } from "@/shadcn-bridge/heroui/button";
import { siteConfig } from "@/config/site";
import { title } from "@/components/primitives";
import DefaultLayout from "@/layouts/default";
+2 -2
View File
@@ -1,4 +1,6 @@
import { useState, useEffect } from "react";
import toast from "react-hot-toast";
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { Button } from "@/shadcn-bridge/heroui/button";
import { Input } from "@/shadcn-bridge/heroui/input";
@@ -11,8 +13,6 @@ import {
ModalFooter,
} from "@/shadcn-bridge/heroui/modal";
import { Chip } from "@/shadcn-bridge/heroui/chip";
import toast from "react-hot-toast";
import {
createSpeedLimit,
getSpeedLimitList,
+31 -21
View File
@@ -1,23 +1,4 @@
import { useState, useEffect, useMemo, useCallback } from "react";
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { Button } from "@/shadcn-bridge/heroui/button";
import { Input } from "@/shadcn-bridge/heroui/input";
import { Textarea } from "@/shadcn-bridge/heroui/input";
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
} from "@/shadcn-bridge/heroui/modal";
import { Chip } from "@/shadcn-bridge/heroui/chip";
import { Switch } from "@/shadcn-bridge/heroui/switch";
import { Spinner } from "@/shadcn-bridge/heroui/spinner";
import { Alert } from "@/shadcn-bridge/heroui/alert";
import { Progress } from "@/shadcn-bridge/heroui/progress";
import { Accordion, AccordionItem } from "@/shadcn-bridge/heroui/accordion";
import { Select, SelectItem } from "@/shadcn-bridge/heroui/select";
import { Checkbox } from "@/shadcn-bridge/heroui/checkbox";
import toast from "react-hot-toast";
import {
DndContext,
@@ -37,6 +18,25 @@ import {
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { Button } from "@/shadcn-bridge/heroui/button";
import { Input } from "@/shadcn-bridge/heroui/input";
import { Textarea } from "@/shadcn-bridge/heroui/input";
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
} from "@/shadcn-bridge/heroui/modal";
import { Chip } from "@/shadcn-bridge/heroui/chip";
import { Switch } from "@/shadcn-bridge/heroui/switch";
import { Spinner } from "@/shadcn-bridge/heroui/spinner";
import { Alert } from "@/shadcn-bridge/heroui/alert";
import { Progress } from "@/shadcn-bridge/heroui/progress";
import { Accordion, AccordionItem } from "@/shadcn-bridge/heroui/accordion";
import { Select, SelectItem } from "@/shadcn-bridge/heroui/select";
import { Checkbox } from "@/shadcn-bridge/heroui/checkbox";
import {
createNode,
getNodeList,
@@ -1020,10 +1020,20 @@ export default function NodePage() {
<span className="text-sm font-medium shrink-0">
已选 {selectedIds.size} 项
</span>
<Button color="primary" size="sm" variant="flat" onPress={selectAll}>
<Button
color="primary"
size="sm"
variant="flat"
onPress={selectAll}
>
全选
</Button>
<Button color="secondary" size="sm" variant="flat" onPress={deselectAll}>
<Button
color="secondary"
size="sm"
variant="flat"
onPress={deselectAll}
>
清空
</Button>
<Button
+2 -2
View File
@@ -1,4 +1,6 @@
import { useState, useEffect, useCallback } from "react";
import { toast } from "react-hot-toast";
import { Button } from "@/shadcn-bridge/heroui/button";
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { Tabs, Tab } from "@/shadcn-bridge/heroui/tabs";
@@ -11,8 +13,6 @@ import {
ModalFooter,
} from "@/shadcn-bridge/heroui/modal";
import { Select, SelectItem } from "@/shadcn-bridge/heroui/select";
import { toast } from "react-hot-toast";
import {
getNodeList,
createPeerShare,
+3 -3
View File
@@ -1,4 +1,7 @@
import React, { useState, useEffect } from "react";
import { toast } from "react-hot-toast";
import { useNavigate } from "react-router-dom";
import { Card, CardBody } from "@/shadcn-bridge/heroui/card";
import { Button } from "@/shadcn-bridge/heroui/button";
import {
@@ -10,9 +13,6 @@ import {
useDisclosure,
} from "@/shadcn-bridge/heroui/modal";
import { Input } from "@/shadcn-bridge/heroui/input";
import { toast } from "react-hot-toast";
import { useNavigate } from "react-router-dom";
import { isWebViewFunc } from "@/utils/panel";
import { siteConfig } from "@/config/site";
import { updatePassword } from "@/api";
+3 -3
View File
@@ -1,10 +1,10 @@
import { useState, useEffect } from "react";
import { Input } from "@/shadcn-bridge/heroui/input";
import { Button } from "@/shadcn-bridge/heroui/button";
import { Card, CardBody } from "@/shadcn-bridge/heroui/card";
import { useNavigate } from "react-router-dom";
import toast from "react-hot-toast";
import { Input } from "@/shadcn-bridge/heroui/input";
import { Button } from "@/shadcn-bridge/heroui/button";
import { Card, CardBody } from "@/shadcn-bridge/heroui/card";
import { reinitializeBaseURL } from "@/api/network";
import {
getPanelAddresses,
+28 -18
View File
@@ -1,20 +1,4 @@
import { useState, useEffect, useMemo } from "react";
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { Button } from "@/shadcn-bridge/heroui/button";
import { Input, Textarea } from "@/shadcn-bridge/heroui/input";
import { Select, SelectItem } from "@/shadcn-bridge/heroui/select";
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
} from "@/shadcn-bridge/heroui/modal";
import { Chip } from "@/shadcn-bridge/heroui/chip";
import { Spinner } from "@/shadcn-bridge/heroui/spinner";
import { Divider } from "@/shadcn-bridge/heroui/divider";
import { Alert } from "@/shadcn-bridge/heroui/alert";
import { Checkbox } from "@/shadcn-bridge/heroui/checkbox";
import toast from "react-hot-toast";
import {
DndContext,
@@ -34,6 +18,22 @@ import {
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { Button } from "@/shadcn-bridge/heroui/button";
import { Input, Textarea } from "@/shadcn-bridge/heroui/input";
import { Select, SelectItem } from "@/shadcn-bridge/heroui/select";
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
} from "@/shadcn-bridge/heroui/modal";
import { Chip } from "@/shadcn-bridge/heroui/chip";
import { Spinner } from "@/shadcn-bridge/heroui/spinner";
import { Divider } from "@/shadcn-bridge/heroui/divider";
import { Alert } from "@/shadcn-bridge/heroui/alert";
import { Checkbox } from "@/shadcn-bridge/heroui/checkbox";
import {
createTunnel,
getTunnelList,
@@ -697,10 +697,20 @@ export default function TunnelPage() {
<span className="text-sm text-default-600 shrink-0">
已选择 {selectedIds.size} 项
</span>
<Button color="primary" size="sm" variant="flat" onPress={selectAll}>
<Button
color="primary"
size="sm"
variant="flat"
onPress={selectAll}
>
全选
</Button>
<Button color="secondary" size="sm" variant="flat" onPress={deselectAll}>
<Button
color="secondary"
size="sm"
variant="flat"
onPress={deselectAll}
>
清空
</Button>
<Button
+3 -3
View File
@@ -1,4 +1,7 @@
import { useState, useEffect } from "react";
import toast from "react-hot-toast";
import { parseDate } from "@internationalized/date";
import { Button } from "@/shadcn-bridge/heroui/button";
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { Input } from "@/shadcn-bridge/heroui/input";
@@ -25,9 +28,6 @@ import { Checkbox } from "@/shadcn-bridge/heroui/checkbox";
import { DatePicker } from "@/shadcn-bridge/heroui/date-picker";
import { Spinner } from "@/shadcn-bridge/heroui/spinner";
import { Progress } from "@/shadcn-bridge/heroui/progress";
import toast from "react-hot-toast";
import { parseDate } from "@internationalized/date";
import {
User,
UserForm,