style: 美化用户隧道权限分配表单UI并统一代码格式

- 使用HeroUI Checkbox组件替换原生checkbox

- 重构隧道列表为card-based tile风格

- 添加选中/未选中/已分配状态视觉区分

- 应用eslint --fix代码格式统一

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
root
2026-02-04 02:22:28 +00:00
parent 72c2e28667
commit 6f6fececa8
31 changed files with 8080 additions and 5740 deletions
+8 -8
View File
@@ -84,11 +84,11 @@ export const SearchIcon = ({
>
<path
d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
fill="none"
/>
<path
d="M22 22L20.5 20.5"
@@ -142,19 +142,19 @@ export const EditIcon = ({
>
<path
d="M11 4H4C3.46957 4 2.96086 4.21071 2.58579 4.58579C2.21071 4.96086 2 5.46957 2 6V20C2 20.5304 2.21071 21.0391 2.58579 21.4142C2.96086 21.7893 3.46957 22 4 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V13"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
fill="none"
/>
<path
d="M18.5 2.49998C18.8978 2.10216 19.4374 1.87866 20 1.87866C20.5626 1.87866 21.1022 2.10216 21.5 2.49998C21.8978 2.89781 22.1213 3.43737 22.1213 3.99998C22.1213 4.56259 21.8978 5.10216 21.5 5.49998L12 15L8 16L9 12L18.5 2.49998Z"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
fill="none"
/>
</svg>
);
@@ -183,11 +183,11 @@ export const DeleteIcon = ({
/>
<path
d="M8 6V4C8 3.46957 8.21071 2.96086 8.58579 2.58579C8.96086 2.21071 9.46957 2 10 2H14C14.5304 2 15.0391 2.21071 15.4142 2.58579C15.7893 2.96086 16 3.46957 16 4V6M19 6V20C19 20.5304 18.7893 21.0391 18.4142 21.4142C18.0391 21.7893 17.5304 22 17 22H7C6.46957 22 5.96086 21.7893 5.58579 21.4142C5.21071 21.0391 5 20.5304 5 20V6H19Z"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
fill="none"
/>
</svg>
);
@@ -209,19 +209,19 @@ export const UserIcon = ({
>
<path
d="M20 21V19C20 17.9391 19.5786 16.9217 18.8284 16.1716C18.0783 15.4214 17.0609 15 16 15H8C6.93913 15 5.92172 15.4214 5.17157 16.1716C4.42143 16.9217 4 17.9391 4 19V21"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
fill="none"
/>
<path
d="M12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11Z"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
fill="none"
/>
</svg>
);
@@ -243,19 +243,19 @@ export const SettingsIcon = ({
>
<path
d="M12.22 2H11.78C11.2496 2 10.7409 2.21071 10.3658 2.58579C9.99072 2.96086 9.78 3.46957 9.78 4C9.78 4.53043 9.99072 5.03914 10.3658 5.41421C10.7409 5.78929 11.2496 6 11.78 6H12.22C12.7504 6 13.2591 5.78929 13.6342 5.41421C14.0093 5.03914 14.22 4.53043 14.22 4C14.22 3.46957 14.0093 2.96086 13.6342 2.58579C13.2591 2.21071 12.7504 2 12.22 2V2Z"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
fill="none"
/>
<path
d="M19.5 8.5C19.8978 8.10218 20.2374 7.63877 20.5 7.12132C20.7626 6.60387 20.8478 6.03677 20.8478 5.464C20.8478 4.89123 20.7626 4.32413 20.5 3.80668C20.2374 3.28923 19.8978 2.82582 19.5 2.428C19.1022 2.03018 18.6388 1.69063 18.1213 1.42801C17.6039 1.16539 17.0368 1.08023 16.464 1.08023C15.8912 1.08023 15.3241 1.16539 14.8067 1.42801C14.2892 1.69063 13.8258 2.03018 13.428 2.428L12 3.856L10.572 2.428C9.78889 1.64486 8.73968 1.21408 7.64800 1.21408C6.55632 1.21408 5.50711 1.64486 4.72400 2.428C3.94086 3.21111 3.51008 4.26032 3.51008 5.352C3.51008 6.44368 3.94086 7.49289 4.72400 8.276L6.15200 9.704L12 15.552L17.848 9.704L19.276 8.276C19.5 8.052 19.5 8.276 19.5 8.5Z"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
fill="none"
/>
</svg>
);
+35 -14
View File
@@ -5,9 +5,9 @@ import {
NavbarBrand,
NavbarContent,
} from "@heroui/navbar";
import { isWebViewFunc } from '@/utils/panel';
import { useNavigate } from "react-router-dom";
import { isWebViewFunc } from "@/utils/panel";
import { Logo } from "@/components/icons";
import { siteConfig, getCachedConfig } from "@/config/site";
@@ -20,21 +20,21 @@ export const Navbar = () => {
// 检测是否在WebView中运行
useEffect(() => {
setIsWebView(isWebViewFunc());
}, []);
useEffect(() => {
// 异步检查是否有更新的配置
const checkForUpdates = async () => {
try {
const cachedAppName = await getCachedConfig('app_name');
const cachedAppName = await getCachedConfig("app_name");
if (cachedAppName && cachedAppName !== appName) {
setAppName(cachedAppName);
// 同步更新siteConfig
siteConfig.name = cachedAppName;
}
} catch (error) {
console.warn('检查配置更新失败:', error);
console.warn("检查配置更新失败:", error);
}
};
@@ -44,27 +44,33 @@ export const Navbar = () => {
// 监听配置更新事件
const handleConfigUpdate = async () => {
try {
const cachedAppName = await getCachedConfig('app_name');
const cachedAppName = await getCachedConfig("app_name");
if (cachedAppName) {
setAppName(cachedAppName);
siteConfig.name = cachedAppName;
}
} catch (error) {
console.warn('更新配置失败:', error);
console.warn("更新配置失败:", error);
}
};
window.addEventListener('configUpdated', handleConfigUpdate);
window.addEventListener("configUpdated", handleConfigUpdate);
return () => {
clearTimeout(timer);
window.removeEventListener('configUpdated', handleConfigUpdate);
window.removeEventListener("configUpdated", handleConfigUpdate);
};
}, [appName]);
return (
<>
<HeroUINavbar maxWidth="xl" position="sticky" height="60px" className="shrink-0">
<HeroUINavbar
className="shrink-0"
height="60px"
maxWidth="xl"
position="sticky"
>
<NavbarContent className="basis-1/5 sm:basis-full" justify="start">
<NavbarBrand className="gap-2 max-w-fit">
<Link
@@ -83,12 +89,27 @@ export const Navbar = () => {
{isWebView && (
<button
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
onClick={() => navigate('/settings')}
title="面板设置"
onClick={() => navigate("/settings")}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
/>
<path
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
/>
</svg>
</button>
)}
+12 -13
View File
@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import AdminLayout from '@/layouts/admin';
import React, { useState, useEffect } from "react";
import AdminLayout from "@/layouts/admin";
interface PageWrapperProps {
children: React.ReactNode;
@@ -8,11 +9,11 @@ interface PageWrapperProps {
className?: string;
}
export default function PageWrapper({
children,
title,
description,
className = "container mx-auto max-w-7xl px-3 lg:px-6 py-8"
export default function PageWrapper({
children,
title,
description,
className = "container mx-auto max-w-7xl px-3 lg:px-6 py-8",
}: PageWrapperProps) {
const [isReady, setIsReady] = useState(false);
@@ -31,8 +32,8 @@ export default function PageWrapper({
<div className="container mx-auto max-w-7xl px-3 lg:px-6 py-8">
<div className="flex items-center justify-center h-64">
<div className="flex items-center gap-3">
<div className="animate-spin h-5 w-5 border-2 border-gray-200 dark:border-gray-700 border-t-gray-600 dark:border-t-gray-300 rounded-full"></div>
<span className="text-default-600"></span>
<div className="animate-spin h-5 w-5 border-2 border-gray-200 dark:border-gray-700 border-t-gray-600 dark:border-t-gray-300 rounded-full" />
<span className="text-default-600" />
</div>
</div>
</div>
@@ -45,12 +46,10 @@ export default function PageWrapper({
<div className={className}>
<div className="mb-6">
<h1 className="text-2xl font-bold mb-2 text-foreground">{title}</h1>
{description && (
<p className="text-default-600">{description}</p>
)}
{description && <p className="text-default-600">{description}</p>}
</div>
{children}
</div>
</AdminLayout>
);
}
}
+19 -13
View File
@@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import { useTheme } from '@heroui/use-theme';
import React, { useEffect } from "react";
import { useTheme } from "@heroui/use-theme";
interface ThemeProviderProps {
children: React.ReactNode;
@@ -11,17 +11,21 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
useEffect(() => {
// 确保主题与HTML class同步
const updateThemeClass = (currentTheme: string) => {
if (currentTheme === 'dark') {
document.documentElement.classList.add('dark');
document.documentElement.style.colorScheme = 'dark';
if (currentTheme === "dark") {
document.documentElement.classList.add("dark");
document.documentElement.style.colorScheme = "dark";
} else {
document.documentElement.classList.remove('dark');
document.documentElement.style.colorScheme = 'light';
document.documentElement.classList.remove("dark");
document.documentElement.style.colorScheme = "light";
}
};
// 始终跟随系统主题
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
if (systemTheme !== theme) {
setTheme(systemTheme);
}
@@ -30,15 +34,17 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
updateThemeClass(theme);
// 监听系统主题变化
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleThemeChange = (e: MediaQueryListEvent) => {
const newTheme = e.matches ? 'dark' : 'light';
const newTheme = e.matches ? "dark" : "light";
setTheme(newTheme);
};
mediaQuery.addEventListener('change', handleThemeChange);
return () => mediaQuery.removeEventListener('change', handleThemeChange);
mediaQuery.addEventListener("change", handleThemeChange);
return () => mediaQuery.removeEventListener("change", handleThemeChange);
}, [theme, setTheme]);
return <>{children}</>;
};
};