mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-08 18:56:37 +08:00
feat(frontend): support markdown in dashboard announcements (#176)
## Summary - render dashboard announcements with Markdown using `react-markdown` + `remark-gfm` while sanitizing output with `rehype-sanitize` - add styled Markdown element mappings in the announcement banner for links, lists, code blocks, and blockquotes - update announcement management hint text to communicate Markdown support in the config page ## Verification - npm run build (vite-frontend)
This commit is contained in:
@@ -268,3 +268,6 @@ sql/
|
|||||||
!go-backend/internal/store/postgres/sql/
|
!go-backend/internal/store/postgres/sql/
|
||||||
!go-backend/internal/store/postgres/sql/schema.sql
|
!go-backend/internal/store/postgres/sql/schema.sql
|
||||||
!go-backend/internal/store/postgres/sql/data.sql
|
!go-backend/internal/store/postgres/sql/data.sql
|
||||||
|
go-backend/gost.db-shm
|
||||||
|
.gitignore
|
||||||
|
go-backend/gost.db-wal
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"@internationalized/date": "^3.10.0",
|
||||||
|
"@marsidev/react-turnstile": "^1.1.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.0",
|
"@radix-ui/react-accordion": "^1.2.0",
|
||||||
"@radix-ui/react-checkbox": "^1.1.0",
|
"@radix-ui/react-checkbox": "^1.1.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.0",
|
"@radix-ui/react-dialog": "^1.1.0",
|
||||||
@@ -25,8 +27,6 @@
|
|||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@radix-ui/react-switch": "^1.1.0",
|
"@radix-ui/react-switch": "^1.1.0",
|
||||||
"@radix-ui/react-tabs": "^1.1.0",
|
"@radix-ui/react-tabs": "^1.1.0",
|
||||||
"@marsidev/react-turnstile": "^1.1.0",
|
|
||||||
"@internationalized/date": "^3.10.0",
|
|
||||||
"@react-aria/i18n": "^3.12.12",
|
"@react-aria/i18n": "^3.12.12",
|
||||||
"@react-aria/visually-hidden": "3.8.25",
|
"@react-aria/visually-hidden": "3.8.25",
|
||||||
"@react-types/shared": "3.30.0",
|
"@react-types/shared": "3.30.0",
|
||||||
@@ -43,8 +43,11 @@
|
|||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-hot-toast": "^2.5.2",
|
"react-hot-toast": "^2.5.2",
|
||||||
"react-is": "^19.2.4",
|
"react-is": "^19.2.4",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
"react-router-dom": "6.30.3",
|
"react-router-dom": "6.30.3",
|
||||||
"recharts": "^3.1.1",
|
"recharts": "^3.1.1",
|
||||||
|
"rehype-sanitize": "^6.0.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"sonner": "^2.0.6",
|
"sonner": "^2.0.6",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
"tailwind-variants": "1.0.0",
|
"tailwind-variants": "1.0.0",
|
||||||
|
|||||||
@@ -700,13 +700,16 @@ export default function ConfigPage() {
|
|||||||
<Textarea
|
<Textarea
|
||||||
label="公告内容"
|
label="公告内容"
|
||||||
minRows={4}
|
minRows={4}
|
||||||
placeholder="请输入公告内容"
|
placeholder="支持 Markdown,例如:**加粗**、[链接](https://example.com)、- 列表"
|
||||||
value={announcement.content}
|
value={announcement.content}
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setAnnouncement({ ...announcement, content: e.target.value })
|
setAnnouncement({ ...announcement, content: e.target.value })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
公告支持 Markdown 语法,链接会在新标签页打开
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import type { AnnouncementData } from "@/api";
|
import type { AnnouncementData } from "@/api";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import rehypeSanitize from "rehype-sanitize";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
|
||||||
import { Card, CardBody } from "@/shadcn-bridge/heroui/card";
|
import { Card, CardBody } from "@/shadcn-bridge/heroui/card";
|
||||||
|
|
||||||
@@ -14,13 +17,13 @@ export const AnnouncementBanner = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<Card className="mb-4 lg:mb-6 border border-blue-100 dark:border-blue-500/20 bg-gradient-to-br from-blue-50/80 to-indigo-50/80 dark:from-blue-500/5 dark:to-indigo-500/5 shadow-sm">
|
||||||
<CardBody className="p-4">
|
<CardBody className="p-4 sm:p-5">
|
||||||
<div className="flex items-start justify-start gap-3.5">
|
<div className="flex items-center gap-3.5 sm:gap-4">
|
||||||
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-500/20 rounded-lg flex-shrink-0 flex items-center justify-center mt-0.5">
|
<div className="w-10 h-10 sm:w-11 sm:h-11 bg-white dark:bg-blue-500/10 rounded-full flex-shrink-0 flex items-center justify-center shadow-sm border border-blue-100 dark:border-blue-500/20">
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="w-5 h-5 text-blue-600 dark:text-blue-400"
|
className="w-5 h-5 sm:w-5 sm:h-5 text-blue-500 dark:text-blue-400"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
>
|
>
|
||||||
@@ -31,13 +34,52 @@ export const AnnouncementBanner = ({
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0 pt-0.5">
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="text-base font-semibold leading-none text-blue-900 dark:text-blue-100 mb-1.5">
|
<h3 className="text-[15px] font-semibold text-blue-900 dark:text-blue-100 tracking-wide mb-1 sm:mb-1.5 mt-1.5">
|
||||||
公告
|
公告
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-blue-800 dark:text-blue-200 whitespace-pre-wrap break-words leading-relaxed">
|
<div className="text-sm text-blue-800/90 dark:text-blue-100/90 break-words leading-relaxed">
|
||||||
{announcement.content}
|
<ReactMarkdown
|
||||||
</p>
|
rehypePlugins={[rehypeSanitize]}
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
components={{
|
||||||
|
p: ({ children }) => <p className="mb-2 last:mb-0">{children}</p>,
|
||||||
|
a: ({ children, href }) => (
|
||||||
|
<a
|
||||||
|
className="underline decoration-blue-500/70 underline-offset-2 hover:text-blue-700 dark:hover:text-blue-100"
|
||||||
|
href={href}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
ul: ({ children }) => (
|
||||||
|
<ul className="list-disc pl-5 space-y-1 mb-2 last:mb-0">{children}</ul>
|
||||||
|
),
|
||||||
|
ol: ({ children }) => (
|
||||||
|
<ol className="list-decimal pl-5 space-y-1 mb-2 last:mb-0">{children}</ol>
|
||||||
|
),
|
||||||
|
code: ({ children }) => (
|
||||||
|
<code className="rounded bg-blue-100/80 dark:bg-blue-900/40 px-1 py-0.5 text-[0.92em]">
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
),
|
||||||
|
pre: ({ children }) => (
|
||||||
|
<pre className="mb-2 overflow-x-auto rounded-md bg-blue-100/70 dark:bg-blue-900/40 p-2.5 text-xs leading-relaxed">
|
||||||
|
{children}
|
||||||
|
</pre>
|
||||||
|
),
|
||||||
|
blockquote: ({ children }) => (
|
||||||
|
<blockquote className="mb-2 border-l-2 border-blue-300/80 dark:border-blue-500/60 pl-3 italic">
|
||||||
|
{children}
|
||||||
|
</blockquote>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{announcement.content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|||||||
@@ -1934,34 +1934,14 @@ export default function ForwardPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* 空状态 */
|
/* 空状态 */
|
||||||
<Card className="shadow-sm border border-gray-200 dark:border-gray-700">
|
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
|
||||||
<CardBody className="text-center py-16">
|
<CardBody className="text-center py-20 flex flex-col items-center justify-center min-h-[240px]">
|
||||||
<div className="flex flex-col items-center gap-4">
|
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
|
||||||
<div className="w-16 h-16 bg-default-100 rounded-full flex items-center justify-center">
|
暂无转发配置
|
||||||
<svg
|
</h3>
|
||||||
aria-hidden="true"
|
<p className="text-default-500 text-sm max-w-xs mx-auto leading-relaxed">
|
||||||
className="w-8 h-8 text-default-400"
|
还没有创建任何转发配置,点击上方按钮开始创建
|
||||||
fill="none"
|
</p>
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M8 9l4-4 4 4m0 6l-4 4-4-4"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-foreground">
|
|
||||||
暂无转发配置
|
|
||||||
</h3>
|
|
||||||
<p className="text-default-500 text-sm mt-1">
|
|
||||||
还没有创建任何转发配置,点击上方按钮开始创建
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
@@ -1988,34 +1968,14 @@ export default function ForwardPage() {
|
|||||||
</DndContext>
|
</DndContext>
|
||||||
) : (
|
) : (
|
||||||
/* 空状态 */
|
/* 空状态 */
|
||||||
<Card className="shadow-sm border border-gray-200 dark:border-gray-700">
|
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
|
||||||
<CardBody className="text-center py-16">
|
<CardBody className="text-center py-20 flex flex-col items-center justify-center min-h-[240px]">
|
||||||
<div className="flex flex-col items-center gap-4">
|
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
|
||||||
<div className="w-16 h-16 bg-default-100 rounded-full flex items-center justify-center">
|
暂无转发配置
|
||||||
<svg
|
</h3>
|
||||||
aria-hidden="true"
|
<p className="text-default-500 text-sm max-w-xs mx-auto leading-relaxed">
|
||||||
className="w-8 h-8 text-default-400"
|
还没有创建任何转发配置,点击上方按钮开始创建
|
||||||
fill="none"
|
</p>
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M8 9l4-4 4 4m0 6l-4 4-4-4"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-foreground">
|
|
||||||
暂无转发配置
|
|
||||||
</h3>
|
|
||||||
<p className="text-default-500 text-sm mt-1">
|
|
||||||
还没有创建任何转发配置,点击上方按钮开始创建
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -334,34 +334,14 @@ export default function LimitPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* 空状态 */
|
/* 空状态 */
|
||||||
<Card className="shadow-sm border border-gray-200 dark:border-gray-700">
|
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
|
||||||
<CardBody className="text-center py-16">
|
<CardBody className="text-center py-20 flex flex-col items-center justify-center min-h-[240px]">
|
||||||
<div className="flex flex-col items-center gap-4">
|
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
|
||||||
<div className="w-16 h-16 bg-default-100 rounded-full flex items-center justify-center">
|
暂无限速规则
|
||||||
<svg
|
</h3>
|
||||||
aria-hidden="true"
|
<p className="text-default-500 text-sm max-w-xs mx-auto leading-relaxed">
|
||||||
className="w-8 h-8 text-default-400"
|
还没有创建任何限速规则,点击上方按钮开始创建
|
||||||
fill="none"
|
</p>
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M12 6v6l4 2m6-6a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-foreground">
|
|
||||||
暂无限速规则
|
|
||||||
</h3>
|
|
||||||
<p className="text-default-500 text-sm mt-1">
|
|
||||||
还没有创建任何限速规则,点击上方按钮开始创建
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -410,7 +410,7 @@ export default function PanelSharingPage() {
|
|||||||
>
|
>
|
||||||
<Card>
|
<Card>
|
||||||
<CardBody className="space-y-5">
|
<CardBody className="space-y-5">
|
||||||
<div className="flex flex-col gap-4 rounded-lg border border-divider bg-default-50/60 dark:bg-default-100/20 p-4 md:flex-row md:items-center md:justify-between">
|
<div className="mt-4 flex flex-col gap-4 rounded-lg border border-divider bg-default-50/60 dark:bg-default-100/20 p-4 md:flex-row md:items-center md:justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h2 className="text-lg font-semibold text-foreground">
|
<h2 className="text-lg font-semibold text-foreground">
|
||||||
Provider 共享
|
Provider 共享
|
||||||
@@ -555,7 +555,7 @@ export default function PanelSharingPage() {
|
|||||||
>
|
>
|
||||||
<Card>
|
<Card>
|
||||||
<CardBody className="space-y-5">
|
<CardBody className="space-y-5">
|
||||||
<div className="flex flex-col gap-4 rounded-lg border border-divider bg-default-50/60 dark:bg-default-100/20 p-4 md:flex-row md:items-center md:justify-between">
|
<div className="mt-4 flex flex-col gap-4 rounded-lg border border-divider bg-default-50/60 dark:bg-default-100/20 p-4 md:flex-row md:items-center md:justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h2 className="text-lg font-semibold text-foreground">
|
<h2 className="text-lg font-semibold text-foreground">
|
||||||
Consumer 接入
|
Consumer 接入
|
||||||
|
|||||||
@@ -1014,34 +1014,14 @@ export default function TunnelPage() {
|
|||||||
</DndContext>
|
</DndContext>
|
||||||
) : (
|
) : (
|
||||||
/* 空状态 */
|
/* 空状态 */
|
||||||
<Card className="shadow-sm border border-gray-200 dark:border-gray-700">
|
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
|
||||||
<CardBody className="text-center py-16">
|
<CardBody className="text-center py-20 flex flex-col items-center justify-center min-h-[240px]">
|
||||||
<div className="flex flex-col items-center gap-4">
|
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
|
||||||
<div className="w-16 h-16 bg-default-100 rounded-full flex items-center justify-center">
|
暂无隧道配置
|
||||||
<svg
|
</h3>
|
||||||
aria-hidden="true"
|
<p className="text-default-500 text-sm max-w-xs mx-auto leading-relaxed">
|
||||||
className="w-8 h-8 text-default-400"
|
还没有创建任何隧道配置,点击上方按钮开始创建
|
||||||
fill="none"
|
</p>
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-foreground">
|
|
||||||
暂无隧道配置
|
|
||||||
</h3>
|
|
||||||
<p className="text-default-500 text-sm mt-1">
|
|
||||||
还没有创建任何隧道配置,点击上方按钮开始创建
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ import {
|
|||||||
SearchIcon,
|
SearchIcon,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
DeleteIcon,
|
DeleteIcon,
|
||||||
UserIcon,
|
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
} from "@/components/icons";
|
} from "@/components/icons";
|
||||||
import { PageLoadingState } from "@/components/page-state";
|
import { PageLoadingState } from "@/components/page-state";
|
||||||
@@ -247,7 +246,7 @@ export default function UserPage() {
|
|||||||
if (response.code === 0) {
|
if (response.code === 0) {
|
||||||
setTunnels(Array.isArray(response.data) ? response.data : []);
|
setTunnels(Array.isArray(response.data) ? response.data : []);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadSpeedLimits = async () => {
|
const loadSpeedLimits = async () => {
|
||||||
@@ -257,15 +256,15 @@ export default function UserPage() {
|
|||||||
if (response.code === 0) {
|
if (response.code === 0) {
|
||||||
const speedLimitList = Array.isArray(response.data)
|
const speedLimitList = Array.isArray(response.data)
|
||||||
? response.data.map((item) => ({
|
? response.data.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
uploadSpeed: item.uploadSpeed ?? item.speed ?? 0,
|
uploadSpeed: item.uploadSpeed ?? item.speed ?? 0,
|
||||||
downloadSpeed: item.downloadSpeed ?? item.speed ?? 0,
|
downloadSpeed: item.downloadSpeed ?? item.speed ?? 0,
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
setSpeedLimits(speedLimitList);
|
setSpeedLimits(speedLimitList);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadUserGroups = async () => {
|
const loadUserGroups = async () => {
|
||||||
@@ -275,7 +274,7 @@ export default function UserPage() {
|
|||||||
if (response.code === 0) {
|
if (response.code === 0) {
|
||||||
setUserGroups(Array.isArray(response.data) ? response.data : []);
|
setUserGroups(Array.isArray(response.data) ? response.data : []);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadUserTunnels = async (userId: number) => {
|
const loadUserTunnels = async (userId: number) => {
|
||||||
@@ -326,7 +325,7 @@ export default function UserPage() {
|
|||||||
if (groupRes.code === 0) {
|
if (groupRes.code === 0) {
|
||||||
currentGroupIds = groupRes.data || [];
|
currentGroupIds = groupRes.data || [];
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
|
|
||||||
setUserForm({
|
setUserForm({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@@ -652,21 +651,14 @@ export default function UserPage() {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<PageLoadingState message="正在加载..." />
|
<PageLoadingState message="正在加载..." />
|
||||||
) : users.length === 0 ? (
|
) : users.length === 0 ? (
|
||||||
<Card className="shadow-sm border border-gray-200 dark:border-gray-700">
|
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
|
||||||
<CardBody className="text-center py-16">
|
<CardBody className="text-center py-20 flex flex-col items-center justify-center min-h-[240px]">
|
||||||
<div className="flex flex-col items-center gap-4">
|
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
|
||||||
<div className="w-16 h-16 bg-default-100 rounded-full flex items-center justify-center">
|
暂无用户数据
|
||||||
<UserIcon className="w-8 h-8 text-default-400" />
|
</h3>
|
||||||
</div>
|
<p className="text-default-500 text-sm max-w-xs mx-auto leading-relaxed">
|
||||||
<div>
|
还没有创建任何用户,点击上方按钮开始创建
|
||||||
<h3 className="text-lg font-semibold text-foreground">
|
</p>
|
||||||
暂无用户数据
|
|
||||||
</h3>
|
|
||||||
<p className="text-default-500 text-sm mt-1">
|
|
||||||
还没有创建任何用户,点击上方按钮开始创建
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
@@ -680,9 +672,9 @@ export default function UserPage() {
|
|||||||
const flowPercent =
|
const flowPercent =
|
||||||
user.flow > 0
|
user.flow > 0
|
||||||
? Math.min(
|
? Math.min(
|
||||||
(usedFlow / (user.flow * 1024 * 1024 * 1024)) * 100,
|
(usedFlow / (user.flow * 1024 * 1024 * 1024)) * 100,
|
||||||
100,
|
100,
|
||||||
)
|
)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -947,8 +939,8 @@ export default function UserPage() {
|
|||||||
value={
|
value={
|
||||||
userForm.expTime
|
userForm.expTime
|
||||||
? (parseDate(
|
? (parseDate(
|
||||||
userForm.expTime.toISOString().split("T")[0],
|
userForm.expTime.toISOString().split("T")[0],
|
||||||
) as any)
|
) as any)
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
onChange={(date) => {
|
onChange={(date) => {
|
||||||
@@ -1054,12 +1046,11 @@ export default function UserPage() {
|
|||||||
aria-disabled={isAssigned}
|
aria-disabled={isAssigned}
|
||||||
className={`
|
className={`
|
||||||
px-4 py-3 rounded-lg border transition-all duration-200 cursor-pointer
|
px-4 py-3 rounded-lg border transition-all duration-200 cursor-pointer
|
||||||
${
|
${isAssigned
|
||||||
isAssigned
|
? "bg-default-100/50 dark:bg-default-50/50 border-default-200/50 dark:border-default-100/20 opacity-60 cursor-not-allowed"
|
||||||
? "bg-default-100/50 dark:bg-default-50/50 border-default-200/50 dark:border-default-100/20 opacity-60 cursor-not-allowed"
|
: isSelected
|
||||||
: isSelected
|
? "bg-primary-50 dark:bg-primary-900/20 border-primary-300 dark:border-primary-500/50 shadow-sm"
|
||||||
? "bg-primary-50 dark:bg-primary-900/20 border-primary-300 dark:border-primary-500/50 shadow-sm"
|
: "bg-white dark:bg-default-50 border-default-200 dark:border-default-100/30 hover:border-primary-200 dark:hover:border-primary-500/30 hover:shadow-sm"
|
||||||
: "bg-white dark:bg-default-50 border-default-200 dark:border-default-100/30 hover:border-primary-200 dark:hover:border-primary-500/30 hover:shadow-sm"
|
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
role="button"
|
role="button"
|
||||||
@@ -1119,13 +1110,13 @@ export default function UserPage() {
|
|||||||
selectedKeys={
|
selectedKeys={
|
||||||
batchTunnelSelections.get(tunnel.id) !==
|
batchTunnelSelections.get(tunnel.id) !==
|
||||||
null &&
|
null &&
|
||||||
batchTunnelSelections.get(tunnel.id) !==
|
batchTunnelSelections.get(tunnel.id) !==
|
||||||
undefined
|
undefined
|
||||||
? [
|
? [
|
||||||
batchTunnelSelections
|
batchTunnelSelections
|
||||||
.get(tunnel.id)!
|
.get(tunnel.id)!
|
||||||
.toString(),
|
.toString(),
|
||||||
]
|
]
|
||||||
: ["null"]
|
: ["null"]
|
||||||
}
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -1385,9 +1376,9 @@ export default function UserPage() {
|
|||||||
setEditTunnelForm((prev) =>
|
setEditTunnelForm((prev) =>
|
||||||
prev
|
prev
|
||||||
? {
|
? {
|
||||||
...prev,
|
...prev,
|
||||||
speedId: value === "null" ? null : Number(value),
|
speedId: value === "null" ? null : Number(value),
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -1442,10 +1433,10 @@ export default function UserPage() {
|
|||||||
value={
|
value={
|
||||||
editTunnelForm.expTime
|
editTunnelForm.expTime
|
||||||
? (parseDate(
|
? (parseDate(
|
||||||
new Date(editTunnelForm.expTime)
|
new Date(editTunnelForm.expTime)
|
||||||
.toISOString()
|
.toISOString()
|
||||||
.split("T")[0],
|
.split("T")[0],
|
||||||
) as any)
|
) as any)
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
onChange={(date) => {
|
onChange={(date) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user