mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-10 11:36:36 +08:00
refactor: Standardize empty state UI for tunnel and user pages, update announcement banner styling, and add new entries to .gitignore.
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
|
||||||
|
|||||||
@@ -17,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"
|
||||||
>
|
>
|
||||||
@@ -34,11 +34,11 @@ 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>
|
||||||
<div className="text-sm text-blue-800 dark:text-blue-200 break-words leading-relaxed">
|
<div className="text-sm text-blue-800/90 dark:text-blue-100/90 break-words leading-relaxed">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
rehypePlugins={[rehypeSanitize]}
|
rehypePlugins={[rehypeSanitize]}
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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