merge
This commit is contained in:
@@ -25,6 +25,7 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-icons": "^5.4.0",
|
||||||
"react-infinite-scroll-component": "^6.1.0",
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
"react-router-dom": "^6.18.0",
|
"react-router-dom": "^6.18.0",
|
||||||
"recharts": "^2.9.0",
|
"recharts": "^2.9.0",
|
||||||
|
|||||||
@@ -1,79 +1,91 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { Layout, Switch, Button, Dropdown } from 'antd';
|
import { Layout, Switch, Button, Dropdown } from "antd";
|
||||||
import { UserOutlined, LogoutOutlined } from '@ant-design/icons';
|
import { UserOutlined, LogoutOutlined } from "@ant-design/icons";
|
||||||
import { useTheme } from '@/contexts/ThemeContext';
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { MenuTrigger } from '../Layout/MenuTrigger';
|
import { MenuTrigger } from "../Layout/MenuTrigger";
|
||||||
const { Header: AntHeader } = Layout;
|
const { Header: AntHeader } = Layout;
|
||||||
|
import { LuSun } from "react-icons/lu";
|
||||||
|
import { GoMoon } from "react-icons/go";
|
||||||
|
|
||||||
const Header = ({ collapsed, setCollapsed }) => {
|
const Header = ({ collapsed, setCollapsed }) => {
|
||||||
const { isDarkMode, toggleTheme } = useTheme();
|
const { isDarkMode, toggleTheme } = useTheme();
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
await logout();
|
await logout();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Logout error:', error);
|
console.error("Logout error:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const userMenuItems = [
|
const userMenuItems = [
|
||||||
{
|
{
|
||||||
key: 'profile',
|
key: "profile",
|
||||||
icon: <UserOutlined />,
|
icon: <UserOutlined />,
|
||||||
label: '个人信息',
|
label: "个人信息",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'logout',
|
key: "logout",
|
||||||
icon: <LogoutOutlined />,
|
icon: <LogoutOutlined />,
|
||||||
label: '退出登录',
|
label: "退出登录",
|
||||||
onClick: handleLogout,
|
onClick: handleLogout,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AntHeader
|
<AntHeader
|
||||||
style={{
|
style={{
|
||||||
padding: 0,
|
padding: 0,
|
||||||
background: isDarkMode ? '#141414' : '#fff',
|
background: isDarkMode ? "#141414" : "#fff",
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
justifyContent: 'space-between'
|
justifyContent: "space-between",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<MenuTrigger
|
<MenuTrigger
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
setCollapsed={setCollapsed}
|
setCollapsed={setCollapsed}
|
||||||
isDarkMode={isDarkMode}
|
isDarkMode={isDarkMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 mr-6">
|
<div className="flex items-center gap-4 mr-10 h-full select-none">
|
||||||
<Switch
|
<div className="flex items-center justify-center cursor-pointer h-full" onClick={toggleTheme}>
|
||||||
checked={isDarkMode}
|
{isDarkMode ? (
|
||||||
onChange={toggleTheme}
|
<GoMoon className="text-2xl" />
|
||||||
checkedChildren="🌙"
|
) : (
|
||||||
unCheckedChildren="☀️"
|
<LuSun className="text-2xl" />
|
||||||
/>
|
)}
|
||||||
|
</div>
|
||||||
<Dropdown
|
|
||||||
menu={{ items: userMenuItems }}
|
<Dropdown
|
||||||
|
menu={{ items: userMenuItems }}
|
||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
trigger={['click']}
|
trigger={["click"]}
|
||||||
>
|
>
|
||||||
<Button
|
<div className="flex gap-2 items-center justify-center cursor-pointer h-full">
|
||||||
type="text"
|
{user?.user_metadata?.picture ? (
|
||||||
icon={<UserOutlined />}
|
<img
|
||||||
className="flex items-center"
|
src={user?.user_metadata?.picture}
|
||||||
>
|
alt="picture"
|
||||||
{user?.email}
|
className="w-8 h-8 rounded-full"
|
||||||
</Button>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="text-2xl bg-gray-400 dark:bg-gray-600 rounded-full flex justify-center items-center w-8 h-8 text-white pb-1">
|
||||||
|
{user?.email.slice(0, 1)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="max-w-20 truncate">
|
||||||
|
{!user?.user_metadata?.name || user?.email?.split("@")[0]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</AntHeader>
|
</AntHeader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from "react";
|
||||||
import { Layout, Menu } from 'antd';
|
import { Layout, Menu } from "antd";
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
import { useTheme } from '@/contexts/ThemeContext';
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
import { getMenuItems } from '@/utils/menuUtils';
|
import { getMenuItems } from "@/utils/menuUtils";
|
||||||
import { Logo } from '@/components/Layout/Logo';
|
import { Logo } from "@/components/Layout/Logo";
|
||||||
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
|
|
||||||
const { Sider } = Layout;
|
const { Sider } = Layout;
|
||||||
|
|
||||||
@@ -11,13 +12,18 @@ const Sidebar = ({ collapsed }) => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { isDarkMode } = useTheme();
|
const { isDarkMode } = useTheme();
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
const menuItems = useMemo(() => getMenuItems(), []);
|
const menuItems = useMemo(() => {
|
||||||
|
if (!user?.id) return [];
|
||||||
|
const { adminRole: role } = user;
|
||||||
|
return getMenuItems(role);
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
const defaultOpenKeys = useMemo(() => {
|
const defaultOpenKeys = useMemo(() => {
|
||||||
const pathSegments = location.pathname.split('/').filter(Boolean);
|
const pathSegments = location.pathname.split("/").filter(Boolean);
|
||||||
return pathSegments.reduce((acc, _, index) => {
|
return pathSegments.reduce((acc, _, index) => {
|
||||||
const path = `/${pathSegments.slice(0, index + 1).join('/')}`;
|
const path = `/${pathSegments.slice(0, index + 1).join("/")}`;
|
||||||
acc.push(path);
|
acc.push(path);
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
@@ -29,18 +35,18 @@ const Sidebar = ({ collapsed }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sider
|
<Sider
|
||||||
trigger={null}
|
trigger={null}
|
||||||
collapsible
|
collapsible
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
theme={isDarkMode ? 'dark' : 'light'}
|
theme={isDarkMode ? "dark" : "light"}
|
||||||
width={256}
|
width={256}
|
||||||
collapsedWidth={80} // 添加这个属性
|
collapsedWidth={80} // 添加这个属性
|
||||||
className={`app-sidebar ${collapsed ? 'collapsed' : ''}`} // 添加collapsed类名
|
className={`app-sidebar ${collapsed ? "collapsed" : ""}`} // 添加collapsed类名
|
||||||
>
|
>
|
||||||
<Logo collapsed={collapsed} isDarkMode={isDarkMode} />
|
<Logo collapsed={collapsed} isDarkMode={isDarkMode} />
|
||||||
<Menu
|
<Menu
|
||||||
theme={isDarkMode ? 'dark' : 'light'}
|
theme={isDarkMode ? "dark" : "light"}
|
||||||
mode="inline"
|
mode="inline"
|
||||||
selectedKeys={[location.pathname]}
|
selectedKeys={[location.pathname]}
|
||||||
defaultOpenKeys={defaultOpenKeys}
|
defaultOpenKeys={defaultOpenKeys}
|
||||||
@@ -51,4 +57,4 @@ const Sidebar = ({ collapsed }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(Sidebar);
|
export default React.memo(Sidebar);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { Navigate,useLocation } from 'react-router-dom';
|
import { Navigate, useLocation } from "react-router-dom";
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { Spin } from 'antd';
|
import { Spin } from "antd";
|
||||||
|
|
||||||
export const ProtectedRoute = ({ children }) => {
|
export const ProtectedRoute = ({ children }) => {
|
||||||
const { user, loading } = useAuth();
|
const { user, loading } = useAuth();
|
||||||
@@ -14,9 +14,11 @@ export const ProtectedRoute = ({ children }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user?.id) {
|
||||||
return <Navigate to={`/login`} replace />;
|
return (
|
||||||
|
<Navigate to={`/login?redirectTo=${location.pathname || ""}`} replace />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return children;
|
return children;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import React, { createContext, useContext, useState, useEffect } from "react";
|
|||||||
import { supabase } from "@/config/supabase";
|
import { supabase } from "@/config/supabase";
|
||||||
import { message } from "antd";
|
import { message } from "antd";
|
||||||
import { useNavigate, useSearchParams, useLocation } from "react-router-dom";
|
import { useNavigate, useSearchParams, useLocation } from "react-router-dom";
|
||||||
|
import { supabaseService } from "@/hooks/supabaseService";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
const AuthContext = createContext({});
|
const AuthContext = createContext({});
|
||||||
|
|
||||||
@@ -9,7 +11,7 @@ export const AuthProvider = ({ children }) => {
|
|||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState({});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -37,7 +39,9 @@ export const AuthProvider = ({ children }) => {
|
|||||||
data: { session },
|
data: { session },
|
||||||
error,
|
error,
|
||||||
} = await supabase.auth.getSession();
|
} = await supabase.auth.getSession();
|
||||||
setUser(session?.user ?? null);
|
|
||||||
|
const role = await checkInTeam(session?.user ?? null);
|
||||||
|
setUser({ ...session?.user, adminRole: role });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error getting session:", error);
|
console.error("Error getting session:", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -46,17 +50,6 @@ export const AuthProvider = ({ children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
initSession();
|
initSession();
|
||||||
|
|
||||||
// 订阅认证状态变化
|
|
||||||
const {
|
|
||||||
data: { subscription },
|
|
||||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
|
||||||
setUser(session?.user ?? null);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
subscription?.unsubscribe();
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
@@ -66,6 +59,45 @@ export const AuthProvider = ({ children }) => {
|
|||||||
// }
|
// }
|
||||||
// }, [location.pathname]);
|
// }, [location.pathname]);
|
||||||
|
|
||||||
|
//检查时候在管理模块团队中,没有就自动加入
|
||||||
|
const checkInTeam = async (user) => {
|
||||||
|
if (!user) return null;
|
||||||
|
try {
|
||||||
|
const { data: teamData, error: teamError } = await supabase
|
||||||
|
.from("teams")
|
||||||
|
.select("*")
|
||||||
|
.eq("attributes->>type", "uppetaAdmin")
|
||||||
|
.single();
|
||||||
|
if (teamData) {
|
||||||
|
const { data: teamMembers, error: teamMembersError } = await supabase
|
||||||
|
.from("team_membership")
|
||||||
|
.select("*")
|
||||||
|
.eq("user_id", user.id)
|
||||||
|
.eq("team_id", teamData.id)
|
||||||
|
.single();
|
||||||
|
if (!teamMembers) {
|
||||||
|
// 自动加入团队
|
||||||
|
const { data: teamMembershipData, error: teamMembershipError } =
|
||||||
|
await supabaseService.insert("team_membership", {
|
||||||
|
id: uuidv4(),
|
||||||
|
user_id: user.id,
|
||||||
|
team_id: teamData.id,
|
||||||
|
role: "MEMBER",
|
||||||
|
is_creator: false,
|
||||||
|
});
|
||||||
|
return "MEMBER";
|
||||||
|
} else {
|
||||||
|
return teamMembers.role;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "MEMBER";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking in team:", error);
|
||||||
|
return "MEMBER";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 邮箱密码登录
|
// 邮箱密码登录
|
||||||
const login = async (email, password) => {
|
const login = async (email, password) => {
|
||||||
try {
|
try {
|
||||||
@@ -79,7 +111,6 @@ export const AuthProvider = ({ children }) => {
|
|||||||
message.error(error.message || "登录失败,请稍后重试");
|
message.error(error.message || "登录失败,请稍后重试");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setUser(data.user);
|
setUser(data.user);
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -152,7 +183,7 @@ export const AuthProvider = ({ children }) => {
|
|||||||
message.error(error.message || "登出失败,请稍后重试");
|
message.error(error.message || "登出失败,请稍后重试");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setUser(null);
|
setUser({});
|
||||||
message.success("已成功登出");
|
message.success("已成功登出");
|
||||||
navigate(`/login?redirectTo=${location.pathname}`, { replace: true });
|
navigate(`/login?redirectTo=${location.pathname}`, { replace: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,143 +1,194 @@
|
|||||||
import React, { createContext,useContext, useState, useEffect, } from 'react';
|
import React, { createContext, useContext, useState, useEffect } from "react";
|
||||||
import { ConfigProvider, theme } from "antd";
|
import { ConfigProvider, theme } from "antd";
|
||||||
const ThemeContext = createContext();
|
const ThemeContext = createContext();
|
||||||
|
|
||||||
export const ThemeProvider = ({ children }) => {
|
export const ThemeProvider = ({ children }) => {
|
||||||
const [isDarkMode, setIsDarkMode] = useState(() => {
|
const [isDarkMode, setIsDarkMode] = useState(() => {
|
||||||
const savedTheme = localStorage.getItem('theme');
|
const savedTheme = localStorage.getItem("theme");
|
||||||
return savedTheme === 'dark';
|
return savedTheme === "dark";
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
|
localStorage.setItem("theme", isDarkMode ? "dark" : "light");
|
||||||
document.documentElement.classList.toggle('dark', isDarkMode);
|
document.documentElement.classList.toggle("dark", isDarkMode);
|
||||||
}, [isDarkMode]);
|
}, [isDarkMode]);
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const handleTheme = (event) => {
|
||||||
setIsDarkMode(!isDarkMode);
|
const { clientX, clientY } = event;
|
||||||
|
let outIsDark;
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setIsDarkMode((prev) => {
|
||||||
|
const newPrev = !prev;
|
||||||
|
outIsDark = newPrev;
|
||||||
|
return newPrev;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if ("startViewTransition" in document) {
|
||||||
|
const transition = document.startViewTransition(() => {
|
||||||
|
toggleTheme();
|
||||||
|
});
|
||||||
|
|
||||||
|
transition.ready.then(() => {
|
||||||
|
const radius = Math.hypot(
|
||||||
|
Math.max(clientX, innerWidth - clientX),
|
||||||
|
Math.max(clientY, innerHeight - clientY)
|
||||||
|
);
|
||||||
|
|
||||||
|
const clipPath = [
|
||||||
|
`circle(0px at ${clientX}px ${clientY}px)`,
|
||||||
|
`circle(${radius}px at ${clientX}px ${clientY}px)`,
|
||||||
|
];
|
||||||
|
|
||||||
|
document.documentElement.animate(
|
||||||
|
{ clipPath: outIsDark ? clipPath.reverse() : clipPath },
|
||||||
|
{
|
||||||
|
duration: 300,
|
||||||
|
easing: "ease-in",
|
||||||
|
pseudoElement: outIsDark
|
||||||
|
? "::view-transition-old(root)"
|
||||||
|
: "::view-transition-new(root)",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toggleTheme();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={{ isDarkMode, toggleTheme }}>
|
<ThemeContext.Provider value={{ isDarkMode, toggleTheme: handleTheme }}>
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
componentSize="small"
|
componentSize="small"
|
||||||
theme={{
|
theme={{
|
||||||
algorithm: isDarkMode ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
algorithm: isDarkMode ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
||||||
token: {
|
token: {
|
||||||
// 主色调 - Google Blue
|
// 主色调 - Google Blue
|
||||||
colorPrimary: '#1a73e8',
|
colorPrimary: "#1a73e8",
|
||||||
colorPrimaryHover: '#1557b0',
|
colorPrimaryHover: "#1557b0",
|
||||||
colorPrimaryActive: '#174ea6',
|
colorPrimaryActive: "#174ea6",
|
||||||
|
|
||||||
// 成功色 - Google Green
|
// 成功色 - Google Green
|
||||||
colorSuccess: '#1e8e3e',
|
colorSuccess: "#1e8e3e",
|
||||||
colorSuccessHover: '#188130',
|
colorSuccessHover: "#188130",
|
||||||
colorSuccessActive: '#137333',
|
colorSuccessActive: "#137333",
|
||||||
|
|
||||||
// 警告色 - Google Yellow
|
// 警告色 - Google Yellow
|
||||||
colorWarning: '#f9ab00',
|
colorWarning: "#f9ab00",
|
||||||
colorWarningHover: '#f29900',
|
colorWarningHover: "#f29900",
|
||||||
colorWarningActive: '#ea8600',
|
colorWarningActive: "#ea8600",
|
||||||
|
|
||||||
// 错误色 - Google Red
|
// 错误色 - Google Red
|
||||||
colorError: '#d93025',
|
colorError: "#d93025",
|
||||||
colorErrorHover: '#c5221f',
|
colorErrorHover: "#c5221f",
|
||||||
colorErrorActive: '#a50e0e',
|
colorErrorActive: "#a50e0e",
|
||||||
|
|
||||||
// 文字颜色
|
// 文字颜色
|
||||||
colorText: isDarkMode ? 'rgba(255, 255, 255, 0.87)' : 'rgba(0, 0, 0, 0.87)',
|
colorText: isDarkMode
|
||||||
colorTextSecondary: isDarkMode ? 'rgba(255, 255, 255, 0.60)' : 'rgba(0, 0, 0, 0.60)',
|
? "rgba(255, 255, 255, 0.87)"
|
||||||
colorTextTertiary: isDarkMode ? 'rgba(255, 255, 255, 0.38)' : 'rgba(0, 0, 0, 0.38)',
|
: "rgba(0, 0, 0, 0.87)",
|
||||||
|
colorTextSecondary: isDarkMode
|
||||||
// 背景色
|
? "rgba(255, 255, 255, 0.60)"
|
||||||
colorBgContainer: isDarkMode ? '#1f1f1f' : '#ffffff',
|
: "rgba(0, 0, 0, 0.60)",
|
||||||
colorBgElevated: isDarkMode ? '#2d2d2d' : '#ffffff',
|
colorTextTertiary: isDarkMode
|
||||||
colorBgLayout: isDarkMode ? '#121212' : '#f8f9fa',
|
? "rgba(255, 255, 255, 0.38)"
|
||||||
|
: "rgba(0, 0, 0, 0.38)",
|
||||||
// 边框
|
|
||||||
borderRadius: 8,
|
// 背景色
|
||||||
borderRadiusLG: 12,
|
colorBgContainer: isDarkMode ? "#1f1f1f" : "#ffffff",
|
||||||
borderRadiusSM: 4,
|
colorBgElevated: isDarkMode ? "#2d2d2d" : "#ffffff",
|
||||||
colorBorder: isDarkMode ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.12)',
|
colorBgLayout: isDarkMode ? "#121212" : "#f8f9fa",
|
||||||
|
|
||||||
// 阴影
|
// 边框
|
||||||
boxShadow: '0 1px 3px 0 rgba(60,64,67,0.3), 0 4px 8px 3px rgba(60,64,67,0.15)',
|
|
||||||
boxShadowSecondary: '0 1px 2px 0 rgba(60,64,67,0.3), 0 1px 3px 1px rgba(60,64,67,0.15)',
|
|
||||||
|
|
||||||
// 字体
|
|
||||||
fontFamily: 'Google Sans, Roboto, Arial, sans-serif',
|
|
||||||
fontSize: 14,
|
|
||||||
|
|
||||||
// 控件尺寸
|
|
||||||
controlHeight: 36,
|
|
||||||
controlHeightSM: 32,
|
|
||||||
controlHeightLG: 44,
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
Button: {
|
|
||||||
algorithm: true,
|
|
||||||
borderRadius: 20, // Google 风格的圆角按钮
|
|
||||||
controlHeight: 36,
|
|
||||||
controlHeightSM: 32,
|
|
||||||
controlHeightLG: 44,
|
|
||||||
paddingContentHorizontal: 24,
|
|
||||||
},
|
|
||||||
Input: {
|
|
||||||
algorithm: true,
|
|
||||||
controlHeight: 36,
|
|
||||||
controlHeightSM: 32,
|
|
||||||
controlHeightLG: 44,
|
|
||||||
paddingContentHorizontal: 16,
|
|
||||||
borderRadius: 4,
|
|
||||||
controlOutline: 'none',
|
|
||||||
controlOutlineWidth: 0,
|
|
||||||
controlPaddingHorizontal: 16,
|
|
||||||
controlPaddingHorizontalSM: 12,
|
|
||||||
addonBg: 'transparent',
|
|
||||||
},
|
|
||||||
Select: {
|
|
||||||
algorithm: true,
|
|
||||||
controlHeight: 36,
|
|
||||||
controlHeightSM: 32,
|
|
||||||
controlHeightLG: 44,
|
|
||||||
borderRadius: 4,
|
|
||||||
controlOutline: 'none',
|
|
||||||
controlOutlineWidth: 0,
|
|
||||||
selectorBg: 'transparent',
|
|
||||||
},
|
|
||||||
Card: {
|
|
||||||
algorithm: true,
|
|
||||||
borderRadiusLG: 8,
|
|
||||||
boxShadow: '0 1px 2px 0 rgba(60,64,67,0.3), 0 1px 3px 1px rgba(60,64,67,0.15)',
|
|
||||||
},
|
|
||||||
Menu: {
|
|
||||||
algorithm: true,
|
|
||||||
itemBorderRadius: 20, // Google 风格的圆角菜单项
|
|
||||||
itemHeight: 36,
|
|
||||||
itemHeightSM: 32,
|
|
||||||
},
|
|
||||||
Tabs: {
|
|
||||||
algorithm: true,
|
|
||||||
inkBarColor: '#1a73e8',
|
|
||||||
itemSelectedColor: '#1a73e8',
|
|
||||||
itemHoverColor: '#174ea6',
|
|
||||||
},
|
|
||||||
Table: {
|
|
||||||
algorithm: true,
|
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
headerBg: isDarkMode ? '#2d2d2d' : '#f8f9fa',
|
borderRadiusLG: 12,
|
||||||
|
borderRadiusSM: 4,
|
||||||
|
colorBorder: isDarkMode
|
||||||
|
? "rgba(255, 255, 255, 0.12)"
|
||||||
|
: "rgba(0, 0, 0, 0.12)",
|
||||||
|
|
||||||
|
// 阴影
|
||||||
|
boxShadow:
|
||||||
|
"0 1px 3px 0 rgba(60,64,67,0.3), 0 4px 8px 3px rgba(60,64,67,0.15)",
|
||||||
|
boxShadowSecondary:
|
||||||
|
"0 1px 2px 0 rgba(60,64,67,0.3), 0 1px 3px 1px rgba(60,64,67,0.15)",
|
||||||
|
|
||||||
|
// 字体
|
||||||
|
fontFamily: "Google Sans, Roboto, Arial, sans-serif",
|
||||||
|
fontSize: 14,
|
||||||
|
|
||||||
|
// 控件尺寸
|
||||||
|
controlHeight: 36,
|
||||||
|
controlHeightSM: 32,
|
||||||
|
controlHeightLG: 44,
|
||||||
},
|
},
|
||||||
Modal: {
|
components: {
|
||||||
algorithm: true,
|
Button: {
|
||||||
borderRadius: 28, // Google 风格的大圆角对话框
|
algorithm: true,
|
||||||
paddingContentHorizontal: 24,
|
borderRadius: 20, // Google 风格的圆角按钮
|
||||||
paddingContentVertical: 24,
|
controlHeight: 36,
|
||||||
}
|
controlHeightSM: 32,
|
||||||
}
|
controlHeightLG: 44,
|
||||||
}}
|
paddingContentHorizontal: 24,
|
||||||
>
|
},
|
||||||
{ children }
|
Input: {
|
||||||
</ConfigProvider>
|
algorithm: true,
|
||||||
|
controlHeight: 36,
|
||||||
|
controlHeightSM: 32,
|
||||||
|
controlHeightLG: 44,
|
||||||
|
paddingContentHorizontal: 16,
|
||||||
|
borderRadius: 4,
|
||||||
|
controlOutline: "none",
|
||||||
|
controlOutlineWidth: 0,
|
||||||
|
controlPaddingHorizontal: 16,
|
||||||
|
controlPaddingHorizontalSM: 12,
|
||||||
|
addonBg: "transparent",
|
||||||
|
},
|
||||||
|
Select: {
|
||||||
|
algorithm: true,
|
||||||
|
controlHeight: 36,
|
||||||
|
controlHeightSM: 32,
|
||||||
|
controlHeightLG: 44,
|
||||||
|
borderRadius: 4,
|
||||||
|
controlOutline: "none",
|
||||||
|
controlOutlineWidth: 0,
|
||||||
|
selectorBg: "transparent",
|
||||||
|
},
|
||||||
|
Card: {
|
||||||
|
algorithm: true,
|
||||||
|
borderRadiusLG: 8,
|
||||||
|
boxShadow:
|
||||||
|
"0 1px 2px 0 rgba(60,64,67,0.3), 0 1px 3px 1px rgba(60,64,67,0.15)",
|
||||||
|
},
|
||||||
|
Menu: {
|
||||||
|
algorithm: true,
|
||||||
|
itemBorderRadius: 20, // Google 风格的圆角菜单项
|
||||||
|
itemHeight: 36,
|
||||||
|
itemHeightSM: 32,
|
||||||
|
},
|
||||||
|
Tabs: {
|
||||||
|
algorithm: true,
|
||||||
|
inkBarColor: "#1a73e8",
|
||||||
|
itemSelectedColor: "#1a73e8",
|
||||||
|
itemHoverColor: "#174ea6",
|
||||||
|
},
|
||||||
|
Table: {
|
||||||
|
algorithm: true,
|
||||||
|
borderRadius: 8,
|
||||||
|
headerBg: isDarkMode ? "#2d2d2d" : "#f8f9fa",
|
||||||
|
},
|
||||||
|
Modal: {
|
||||||
|
algorithm: true,
|
||||||
|
borderRadius: 28, // Google 风格的大圆角对话框
|
||||||
|
paddingContentHorizontal: 24,
|
||||||
|
paddingContentVertical: 24,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ConfigProvider>
|
||||||
</ThemeContext.Provider>
|
</ThemeContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -145,7 +196,7 @@ export const ThemeProvider = ({ children }) => {
|
|||||||
export const useTheme = () => {
|
export const useTheme = () => {
|
||||||
const context = useContext(ThemeContext);
|
const context = useContext(ThemeContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('useTheme must be used within a ThemeProvider');
|
throw new Error("useTheme must be used within a ThemeProvider");
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,50 +1,55 @@
|
|||||||
import { supabase } from '@/config/supabase'
|
import { supabase } from "@/config/supabase";
|
||||||
|
|
||||||
class SupabaseService {
|
class SupabaseService {
|
||||||
async select(table, options = {}) {
|
async select(table, options = {}) {
|
||||||
try {
|
try {
|
||||||
let query = supabase
|
let query = supabase
|
||||||
.from(table)
|
.from(table)
|
||||||
.select(options.select || '*', { count: 'exact' })
|
.select(options.select || "*", { count: "exact" });
|
||||||
|
|
||||||
// 处理精确匹配条件
|
// 处理精确匹配条件
|
||||||
if (options.match) {
|
if (options.match) {
|
||||||
query = query.match(options.match)
|
query = query.match(options.match);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理过滤条件
|
// 处理过滤条件
|
||||||
if (options.filter) {
|
if (options.filter) {
|
||||||
Object.entries(options.filter).forEach(([key, condition]) => {
|
Object.entries(options.filter).forEach(([key, condition]) => {
|
||||||
Object.entries(condition).forEach(([operator, value]) => {
|
Object.entries(condition).forEach(([operator, value]) => {
|
||||||
query = query.filter(key, operator, value)
|
query = query.filter(key, operator, value);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理排序
|
// 处理排序
|
||||||
if (options.order) {
|
if (options.order) {
|
||||||
query = query.order(options.order.column, {
|
query = query.order(options.order.column, {
|
||||||
ascending: options.order.ascending
|
ascending: options.order.ascending,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理分页
|
// 处理分页
|
||||||
if (options.page && options.pageSize) {
|
if (options.page && options.pageSize) {
|
||||||
const from = (options.page - 1) * options.pageSize
|
const from = (options.page - 1) * options.pageSize;
|
||||||
const to = from + options.pageSize - 1
|
const to = from + options.pageSize - 1;
|
||||||
query = query.range(from, to)
|
query = query.range(from, to);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error, count } = await query
|
// 如果需要单条数据
|
||||||
|
if (options.single) {
|
||||||
|
query = query.single();
|
||||||
|
}
|
||||||
|
|
||||||
if (error) throw error
|
const { data, error, count } = await query;
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
total: count || 0
|
total: count || 0,
|
||||||
}
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching from ${table}:`, error.message)
|
console.error(`Error fetching from ${table}:`, error.message);
|
||||||
throw error
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,29 +59,27 @@ class SupabaseService {
|
|||||||
const { data: result, error } = await supabase
|
const { data: result, error } = await supabase
|
||||||
.from(table)
|
.from(table)
|
||||||
.insert(data)
|
.insert(data)
|
||||||
.select()
|
.select();
|
||||||
|
|
||||||
if (error) throw error
|
if (error) throw error;
|
||||||
return result
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error inserting into ${table}:`, error.message)
|
console.error(`Error inserting into ${table}:`, error.message);
|
||||||
throw error
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优化 UPDATE 请求
|
// 优化 UPDATE 请求
|
||||||
async update(table, match, updates) {
|
async update(table, match, updates) {
|
||||||
try {
|
try {
|
||||||
let query = supabase
|
let query = supabase.from(table).update(updates);
|
||||||
.from(table)
|
|
||||||
.update(updates);
|
|
||||||
|
|
||||||
// 如果是对象,使用 match 方法
|
// 如果是对象,使用 match 方法
|
||||||
if (typeof match === 'object') {
|
if (typeof match === "object") {
|
||||||
query = query.match(match);
|
query = query.match(match);
|
||||||
} else {
|
} else {
|
||||||
// 如果是单个 id,使用 eq 方法
|
// 如果是单个 id,使用 eq 方法
|
||||||
query = query.eq('id', match);
|
query = query.eq("id", match);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error } = await query.select();
|
const { data, error } = await query.select();
|
||||||
@@ -92,38 +95,32 @@ class SupabaseService {
|
|||||||
// 通用 DELETE 请求
|
// 通用 DELETE 请求
|
||||||
async delete(table, match) {
|
async delete(table, match) {
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase
|
const { error } = await supabase.from(table).delete().match(match);
|
||||||
.from(table)
|
|
||||||
.delete()
|
|
||||||
.match(match)
|
|
||||||
|
|
||||||
if (error) throw error
|
if (error) throw error;
|
||||||
return true
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error deleting from ${table}:`, error.message)
|
console.error(`Error deleting from ${table}:`, error.message);
|
||||||
throw error
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 通用 UPSERT 请求
|
// 通用 UPSERT 请求
|
||||||
async upsert(table, data, onConflict) {
|
async upsert(table, data, onConflict) {
|
||||||
try {
|
try {
|
||||||
let query = supabase
|
let query = supabase.from(table).upsert(data).select();
|
||||||
.from(table)
|
|
||||||
.upsert(data)
|
|
||||||
.select()
|
|
||||||
|
|
||||||
if (onConflict) {
|
if (onConflict) {
|
||||||
query = query.onConflict(onConflict)
|
query = query.onConflict(onConflict);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: result, error } = await query
|
const { data: result, error } = await query;
|
||||||
if (error) throw error
|
if (error) throw error;
|
||||||
return result
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error upserting into ${table}:`, error.message)
|
console.error(`Error upserting into ${table}:`, error.message);
|
||||||
throw error
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const supabaseService = new SupabaseService()
|
export const supabaseService = new SupabaseService();
|
||||||
|
|||||||
@@ -14,100 +14,97 @@ const Login = () => {
|
|||||||
login(values.email, values.password);
|
login(values.email, values.password);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-[#f5f8ff] dark:bg-gray-900">
|
<div className="min-h-screen flex items-center justify-center bg-[#f5f8ff] dark:bg-gray-900">
|
||||||
<div className="w-full max-w-[1200px] mx-auto flex p-4 md:p-0">
|
<div className="w-full max-w-[1200px] mx-auto flex p-4 md:p-0">
|
||||||
{/* 左侧登录表单 */}
|
{/* 左侧登录表单 */}
|
||||||
<div className="w-full md:w-1/2 bg-white dark:bg-gray-800 p-8 md:p-12 rounded-3xl shadow-2xl dark:shadow-gray-800/50">
|
<div className="w-full md:w-1/2 bg-white dark:bg-gray-800 p-8 md:p-12 rounded-3xl shadow-2xl dark:shadow-gray-800/50">
|
||||||
<div className="mb-10 text-center">
|
<div className="mb-10 text-center">
|
||||||
<h1 className="text-4xl font-bold mb-3 bg-gradient-to-r from-blue-600 to-cyan-500 bg-clip-text text-transparent">
|
<h1 className="text-4xl font-bold mb-3 bg-gradient-to-r from-blue-600 to-cyan-500 bg-clip-text text-transparent">
|
||||||
Uppeta
|
Uppeta
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 dark:text-gray-400">
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
欢迎回来!请登录您的账户
|
欢迎回来!请登录您的账户
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
name="login"
|
|
||||||
onFinish={handleLogin}
|
|
||||||
layout="vertical"
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
name="email"
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: "请输入邮箱!" },
|
|
||||||
{ type: "email", message: "请输入有效的邮箱地址!" },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
prefix={<MailOutlined className="text-gray-400" />}
|
|
||||||
placeholder="邮箱"
|
|
||||||
className="h-12 rounded-xl dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="password"
|
|
||||||
rules={[{ required: true, message: "请输入密码!" }]}
|
|
||||||
>
|
|
||||||
<Input.Password
|
|
||||||
prefix={<LockOutlined className="text-gray-400" />}
|
|
||||||
placeholder="密码"
|
|
||||||
className="h-12 rounded-xl dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<div className="flex justify-end mb-6">
|
|
||||||
<Link
|
|
||||||
to="/forgot-password"
|
|
||||||
className="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 text-sm"
|
|
||||||
>
|
|
||||||
忘记密码?
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form.Item>
|
<Form
|
||||||
<Button
|
form={form}
|
||||||
type="primary"
|
name="login"
|
||||||
htmlType="submit"
|
onFinish={handleLogin}
|
||||||
block
|
layout="vertical"
|
||||||
loading={emailLoading}
|
size="large"
|
||||||
disabled={googleLoading}
|
|
||||||
className="h-12 rounded-xl text-base font-medium bg-gradient-to-r from-blue-600 to-cyan-500 border-0 hover:from-blue-700 hover:to-cyan-600"
|
|
||||||
>
|
|
||||||
登录
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Divider className="dark:border-gray-700">
|
|
||||||
<span className="text-gray-400">或</span>
|
|
||||||
</Divider>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
icon={<GoogleOutlined />}
|
|
||||||
block
|
|
||||||
onClick={signInWithGoogle}
|
|
||||||
loading={googleLoading}
|
|
||||||
disabled={emailLoading}
|
|
||||||
className="h-12 rounded-xl text-base font-medium mb-6 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 border border-gray-200 dark:border-gray-600"
|
|
||||||
>
|
>
|
||||||
使用 Google 账号登录
|
<Form.Item
|
||||||
</Button>
|
name="email"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: "请输入邮箱!" },
|
||||||
|
{ type: "email", message: "请输入有效的邮箱地址!" },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<MailOutlined className="text-gray-400" />}
|
||||||
|
placeholder="邮箱"
|
||||||
|
className="h-12 rounded-xl dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
</Form>
|
name="password"
|
||||||
</div>
|
rules={[{ required: true, message: "请输入密码!" }]}
|
||||||
|
>
|
||||||
|
<Input.Password
|
||||||
|
prefix={<LockOutlined className="text-gray-400" />}
|
||||||
|
placeholder="密码"
|
||||||
|
className="h-12 rounded-xl dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
{/* 右侧图片 */}
|
<div className="flex justify-end mb-6">
|
||||||
<div className="hidden md:flex md:w-1/2 items-center justify-center p-12">
|
<Link
|
||||||
<div className="w-full h-full rounded-3xl bg-[url('https://uppeta.com/img/svg/main.svg')] bg-center bg-contain bg-no-repeat dark:opacity-90 animate-pulse-slow transform hover:scale-105 transition-all duration-500" />
|
to="/forgot-password"
|
||||||
|
className="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 text-sm"
|
||||||
|
>
|
||||||
|
忘记密码?
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
block
|
||||||
|
loading={emailLoading}
|
||||||
|
disabled={googleLoading}
|
||||||
|
className="h-12 rounded-xl text-base font-medium bg-gradient-to-r from-blue-600 to-cyan-500 border-0 hover:from-blue-700 hover:to-cyan-600"
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Divider className="dark:border-gray-700">
|
||||||
|
<span className="text-gray-400">或</span>
|
||||||
|
</Divider>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
icon={<GoogleOutlined />}
|
||||||
|
block
|
||||||
|
onClick={signInWithGoogle}
|
||||||
|
loading={googleLoading}
|
||||||
|
disabled={emailLoading}
|
||||||
|
className="h-12 rounded-xl text-base font-medium mb-6 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 border border-gray-200 dark:border-gray-600"
|
||||||
|
>
|
||||||
|
使用 Google 账号登录
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右侧图片 */}
|
||||||
|
<div className="hidden md:flex md:w-1/2 items-center justify-center p-12">
|
||||||
|
<div className="w-full h-full rounded-3xl bg-[url('https://uppeta.com/img/svg/main.svg')] bg-center bg-contain bg-no-repeat dark:opacity-90 animate-pulse-slow transform hover:scale-105 transition-all duration-500" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ const ServicePage = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="bg-gray-50 p-4 rounded-lg">
|
<div className="bg-gray-50 p-4 rounded-lg">
|
||||||
{record.attributes.sections.map((section) => (
|
{record.attributes.sections.map((section) => (
|
||||||
<div key={section.key} className="mb-6 bg-white rounded-lg shadow-sm p-4">
|
<div key={section.key} className="mb-6 rounded-lg shadow-sm p-4">
|
||||||
<div className="flex items-center justify-between mb-3 border-b pb-2">
|
<div className="flex items-center justify-between mb-3 border-b pb-2">
|
||||||
<h3 className="text-lg font-medium text-gray-800">{section.sectionName}</h3>
|
<h3 className="text-lg font-medium text-gray-800">{section.sectionName}</h3>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
@@ -586,7 +586,7 @@ const ServicePage = () => {
|
|||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
showTotal: (total) => `共 ${total} 条`,
|
showTotal: (total) => `共 ${total} 条`,
|
||||||
}}
|
}}
|
||||||
className="bg-white rounded-lg"
|
className="rounded-lg"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 添加模板类型选择弹窗 */}
|
{/* 添加模板类型选择弹窗 */}
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ const Classify = ({activeType,typeList}) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow-sm">
|
<div className="rounded-lg shadow-sm">
|
||||||
<Form form={form}>
|
<Form form={form}>
|
||||||
<Table
|
<Table
|
||||||
scroll={{ x: true }}
|
scroll={{ x: true }}
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ const ResourceManagement = () => {
|
|||||||
},[activeType])
|
},[activeType])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 bg-gray-50 min-h-screen">
|
<div className="p-6 min-h-screen">
|
||||||
<Tabs
|
<Tabs
|
||||||
activeKey={activeType}
|
activeKey={activeType}
|
||||||
onChange={setActiveType}
|
onChange={setActiveType}
|
||||||
type="card"
|
type="card"
|
||||||
className="bg-white rounded-lg shadow-sm"
|
className="rounded-lg shadow-sm"
|
||||||
items={TEMPLATE_TYPES.map(type => {
|
items={TEMPLATE_TYPES.map(type => {
|
||||||
return ({
|
return ({
|
||||||
key: type.key,
|
key: type.key,
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ const UnitManagement = ({ activeType, typeList }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 bg-gray-50">
|
<div className="p-6 bg-gray-50">
|
||||||
<div className="bg-white rounded-lg shadow-sm mb-6 p-4">
|
<div className="rounded-lg shadow-sm mb-6 p-4">
|
||||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -261,7 +261,7 @@ const UnitManagement = ({ activeType, typeList }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow-sm">
|
<div className="rounded-lg shadow-sm">
|
||||||
<Form form={form}>
|
<Form form={form}>
|
||||||
<Table
|
<Table
|
||||||
scroll={{ x: true }}
|
scroll={{ x: true }}
|
||||||
|
|||||||
@@ -460,7 +460,7 @@ const StorageManager = () => {
|
|||||||
// 修改文件列表渲染
|
// 修改文件列表渲染
|
||||||
const renderFileList = () => (
|
const renderFileList = () => (
|
||||||
<div
|
<div
|
||||||
className="flex-1 overflow-y-auto bg-white rounded-lg shadow-sm"
|
className="flex-1 overflow-y-auto rounded-lg shadow-sm"
|
||||||
id="scrollableDiv"
|
id="scrollableDiv"
|
||||||
>
|
>
|
||||||
{/* 面包屑导航 */}
|
{/* 面包屑导航 */}
|
||||||
@@ -600,7 +600,7 @@ const StorageManager = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-gray-50">
|
<div className="flex h-screen bg-gray-50">
|
||||||
<div className="w-1/3 p-4 flex flex-col space-y-4">
|
<div className="w-1/3 p-4 flex flex-col space-y-4">
|
||||||
<div className="bg-white rounded-lg shadow-sm overflow-hidden">
|
<div className="rounded-lg shadow-sm overflow-hidden">
|
||||||
<Dragger
|
<Dragger
|
||||||
{...uploadProps}
|
{...uploadProps}
|
||||||
className="px-6 py-8 hover:bg-gray-50 transition-colors group"
|
className="px-6 py-8 hover:bg-gray-50 transition-colors group"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { Suspense } from 'react';
|
|||||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
import MainLayout from '@/components/Layout/MainLayout';
|
import MainLayout from '@/components/Layout/MainLayout';
|
||||||
import { routes } from '@/routes/routes';
|
import { generateRoutes } from '@/routes/routes';
|
||||||
import Login from '@/pages/auth/Login';
|
import Login from '@/pages/auth/Login';
|
||||||
import NotFound from '@/pages/notFound';
|
import NotFound from '@/pages/notFound';
|
||||||
import Dashboard from '@/pages/Dashboard';
|
import Dashboard from '@/pages/Dashboard';
|
||||||
@@ -48,13 +48,14 @@ const renderRoutes = (routes) => {
|
|||||||
|
|
||||||
const AppRoutes = () => {
|
const AppRoutes = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const { adminRole: role } = user;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
path="/login"
|
path="/login"
|
||||||
element={
|
element={
|
||||||
user ? <Navigate to="/company/serviceTemplate" replace /> : <Login />
|
user?.id ? <Navigate to="/company/serviceTemplate" replace /> : <Login />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ const AppRoutes = () => {
|
|||||||
index
|
index
|
||||||
element={<Navigate to="/company/serviceTemplate" replace />}
|
element={<Navigate to="/company/serviceTemplate" replace />}
|
||||||
/>
|
/>
|
||||||
{renderRoutes(routes)}
|
{renderRoutes(generateRoutes(role))}
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { lazy } from "react";
|
import { lazy } from "react";
|
||||||
|
|
||||||
|
|
||||||
// Resource Management routes
|
// Resource Management routes
|
||||||
const resourceRoutes = [
|
const resourceRoutes = [
|
||||||
{
|
{
|
||||||
@@ -8,24 +7,28 @@ const resourceRoutes = [
|
|||||||
component: lazy(() => import("@/pages/resource/team")),
|
component: lazy(() => import("@/pages/resource/team")),
|
||||||
name: "团队管理",
|
name: "团队管理",
|
||||||
icon: "team",
|
icon: "team",
|
||||||
|
roles: ["OWNER"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "bucket",
|
path: "bucket",
|
||||||
component: lazy(() => import("@/pages/resource/bucket")),
|
component: lazy(() => import("@/pages/resource/bucket")),
|
||||||
name: "对象存储",
|
name: "对象存储",
|
||||||
icon: "shop",
|
icon: "shop",
|
||||||
|
roles: ["OWNER"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "task",
|
path: "task",
|
||||||
component: lazy(() => import("@/pages/resource/resourceTask")),
|
component: lazy(() => import("@/pages/resource/resourceTask")),
|
||||||
name: "任务管理",
|
name: "任务管理",
|
||||||
icon: "appstore",
|
icon: "appstore",
|
||||||
|
roles: ["OWNER"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "task/edit/:id?",
|
path: "task/edit/:id?",
|
||||||
component: lazy(() => import("@/pages/resource/resourceTask/edit")),
|
component: lazy(() => import("@/pages/resource/resourceTask/edit")),
|
||||||
hidden: true,
|
hidden: true,
|
||||||
name: "新增/编辑任务",
|
name: "新增/编辑任务",
|
||||||
|
roles: ["OWNER"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -36,6 +39,7 @@ const companyRoutes = [
|
|||||||
component: lazy(() => import("@/pages/company/quotation")),
|
component: lazy(() => import("@/pages/company/quotation")),
|
||||||
name: "报价单",
|
name: "报价单",
|
||||||
icon: "file",
|
icon: "file",
|
||||||
|
roles: ["ADMIN", "OWNER"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "quotaInfo/:id?", // 添加可选的 id 参数
|
path: "quotaInfo/:id?", // 添加可选的 id 参数
|
||||||
@@ -43,18 +47,21 @@ const companyRoutes = [
|
|||||||
component: lazy(() => import("@/pages/company/quotation/detail")),
|
component: lazy(() => import("@/pages/company/quotation/detail")),
|
||||||
name: "报价单详情",
|
name: "报价单详情",
|
||||||
icon: "file",
|
icon: "file",
|
||||||
|
roles: ["ADMIN", "OWNER"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "serviceTemplate",
|
path: "serviceTemplate",
|
||||||
component: lazy(() => import("@/pages/company/service")),
|
component: lazy(() => import("@/pages/company/service")),
|
||||||
name: "服务管理",
|
name: "服务管理",
|
||||||
icon: "container",
|
icon: "container",
|
||||||
|
roles: ["ADMIN", "OWNER"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "templateItemManage",
|
path: "templateItemManage",
|
||||||
component: lazy(() => import("@/pages/company/service/itemsManange")),
|
component: lazy(() => import("@/pages/company/service/itemsManange")),
|
||||||
name: "资源类型",
|
name: "资源类型",
|
||||||
icon: "container",
|
icon: "container",
|
||||||
|
roles: ["ADMIN", "OWNER"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "serviceTemplateInfo/:id?",
|
path: "serviceTemplateInfo/:id?",
|
||||||
@@ -62,6 +69,7 @@ const companyRoutes = [
|
|||||||
component: lazy(() => import("@/pages/company/service/detail")),
|
component: lazy(() => import("@/pages/company/service/detail")),
|
||||||
name: "服务模版详情",
|
name: "服务模版详情",
|
||||||
icon: "container",
|
icon: "container",
|
||||||
|
roles: ["ADMIN", "OWNER"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "quotaInfo/preview/:id?", // 添加可选的 id 参数
|
path: "quotaInfo/preview/:id?", // 添加可选的 id 参数
|
||||||
@@ -69,12 +77,14 @@ const companyRoutes = [
|
|||||||
component: lazy(() => import("@/pages/company/quotation/view")),
|
component: lazy(() => import("@/pages/company/quotation/view")),
|
||||||
name: "报价单预览",
|
name: "报价单预览",
|
||||||
icon: "file",
|
icon: "file",
|
||||||
|
roles: ["ADMIN", "OWNER"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "customer",
|
path: "customer",
|
||||||
component: lazy(() => import("@/pages/company/customer")),
|
component: lazy(() => import("@/pages/company/customer")),
|
||||||
name: "客户管理",
|
name: "客户管理",
|
||||||
icon: "user",
|
icon: "user",
|
||||||
|
roles: ["ADMIN", "OWNER"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "customerInfo/:id?",
|
path: "customerInfo/:id?",
|
||||||
@@ -82,12 +92,14 @@ const companyRoutes = [
|
|||||||
component: lazy(() => import("@/pages/company/customer/detail")),
|
component: lazy(() => import("@/pages/company/customer/detail")),
|
||||||
name: "客户详情",
|
name: "客户详情",
|
||||||
icon: "user",
|
icon: "user",
|
||||||
|
roles: ["ADMIN", "OWNER"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "supplier",
|
path: "supplier",
|
||||||
component: lazy(() => import("@/pages/company/supplier")),
|
component: lazy(() => import("@/pages/company/supplier")),
|
||||||
name: "供应商管理",
|
name: "供应商管理",
|
||||||
icon: "branches",
|
icon: "branches",
|
||||||
|
roles: ["ADMIN", "OWNER"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "supplierInfo/:id?",
|
path: "supplierInfo/:id?",
|
||||||
@@ -95,37 +107,63 @@ const companyRoutes = [
|
|||||||
component: lazy(() => import("@/pages/company/supplier/detail")),
|
component: lazy(() => import("@/pages/company/supplier/detail")),
|
||||||
name: "供应商详情",
|
name: "供应商详情",
|
||||||
icon: "branches",
|
icon: "branches",
|
||||||
|
roles: ["ADMIN", "OWNER"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const marketingRoutes = [];
|
const marketingRoutes = [];
|
||||||
|
|
||||||
export const routes = [
|
const roleRoutes = [
|
||||||
// {
|
|
||||||
// path: "dashboard",
|
|
||||||
// component: lazy(() => import("@/pages/Dashboard")),
|
|
||||||
// name: "仪表盘",
|
|
||||||
// icon: "dashboard",
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
path: "resource",
|
path: "role",
|
||||||
component: lazy(() => import("@/pages/resource")),
|
component: lazy(() => import("@/pages/role")),
|
||||||
name: "资源管理",
|
name: "角色管理",
|
||||||
icon: "appstore",
|
icon: "setting",
|
||||||
children: resourceRoutes,
|
roles: ["ADMIN", "OWNER"],
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "company",
|
|
||||||
component: lazy(() => import("@/pages/company")),
|
|
||||||
name: "公司管理",
|
|
||||||
icon: "bank",
|
|
||||||
children: companyRoutes,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "marketing",
|
|
||||||
component: lazy(() => import("@/pages/marketing")),
|
|
||||||
name: "行销中心",
|
|
||||||
icon: "shopping",
|
|
||||||
children: marketingRoutes,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const generateRoutes = (role) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
path: "dashboard",
|
||||||
|
component: lazy(() => import("@/pages/Dashboard")),
|
||||||
|
name: "仪表盘",
|
||||||
|
icon: "dashboard",
|
||||||
|
roles: ["ADMIN", "OWNER", "MEMBER"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "resource",
|
||||||
|
component: lazy(() => import("@/pages/resource")),
|
||||||
|
name: "资源管理",
|
||||||
|
icon: "appstore",
|
||||||
|
children: resourceRoutes.filter((route) => route.roles.includes(role)),
|
||||||
|
roles: ["OWNER"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "company",
|
||||||
|
component: lazy(() => import("@/pages/company")),
|
||||||
|
name: "公司管理",
|
||||||
|
icon: "bank",
|
||||||
|
children: companyRoutes.filter((route) => route.roles.includes(role)),
|
||||||
|
roles: ["ADMIN", "OWNER"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "marketing",
|
||||||
|
component: lazy(() => import("@/pages/marketing")),
|
||||||
|
name: "行销中心",
|
||||||
|
icon: "shopping",
|
||||||
|
children: marketingRoutes.filter((route) => route.roles.includes(role)),
|
||||||
|
roles: ["ADMIN", "OWNER"],
|
||||||
|
},
|
||||||
|
|
||||||
|
// {
|
||||||
|
// path: "role",
|
||||||
|
// component: lazy(() => import("@/pages/role")),
|
||||||
|
// name: "权限管理",
|
||||||
|
// icon: "setting",
|
||||||
|
// children: roleRoutes.filter((route) => route.roles.includes(role)),
|
||||||
|
// roles: ["ADMIN", "OWNER"],
|
||||||
|
// },
|
||||||
|
].filter((route) => route.roles.includes(role));
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&display=swap');
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--primary-color: #1677ff;
|
--primary-color: #1677ff;
|
||||||
--primary-gradient: linear-gradient(45deg, #1677ff, #36cff0);
|
--primary-gradient: linear-gradient(45deg, #1677ff, #36cff0);
|
||||||
@@ -40,7 +41,7 @@ body {
|
|||||||
line-height: 44px;
|
line-height: 44px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
|
||||||
.ant-menu-item-icon + span {
|
.ant-menu-item-icon+span {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
@@ -56,7 +57,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.ant-menu-inline-collapsed {
|
&.ant-menu-inline-collapsed {
|
||||||
.ant-menu-item,
|
|
||||||
|
.ant-menu-item,
|
||||||
.ant-menu-submenu .ant-menu-submenu-title {
|
.ant-menu-submenu .ant-menu-submenu-title {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -75,17 +77,17 @@ body {
|
|||||||
|
|
||||||
.ant-menu-submenu {
|
.ant-menu-submenu {
|
||||||
.ant-menu-submenu-title {
|
.ant-menu-submenu-title {
|
||||||
@apply rounded-lg;
|
@apply rounded-lg;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
line-height: 44px;
|
line-height: 44px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply bg-gray-100 dark:bg-gray-700;
|
@apply bg-gray-100 dark:bg-gray-700;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.ant-menu-submenu-open {
|
&.ant-menu-submenu-open {
|
||||||
> .ant-menu-submenu-title {
|
>.ant-menu-submenu-title {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,7 +98,7 @@ body {
|
|||||||
// Logo styles
|
// Logo styles
|
||||||
.logo {
|
.logo {
|
||||||
@apply flex items-center justify-center h-16 border-b border-gray-200 dark:border-gray-700;
|
@apply flex items-center justify-center h-16 border-b border-gray-200 dark:border-gray-700;
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
@apply text-lg font-semibold m-0;
|
@apply text-lg font-semibold m-0;
|
||||||
background: var(--primary-gradient);
|
background: var(--primary-gradient);
|
||||||
@@ -108,25 +110,25 @@ body {
|
|||||||
// Dark mode styles
|
// Dark mode styles
|
||||||
.dark {
|
.dark {
|
||||||
@apply bg-gray-900;
|
@apply bg-gray-900;
|
||||||
|
|
||||||
.ant-card {
|
.ant-card {
|
||||||
background: #1f1f1f;
|
background: #1f1f1f;
|
||||||
|
|
||||||
.ant-card-head {
|
.ant-card-head {
|
||||||
color: rgba(255, 255, 255, 0.85);
|
color: rgba(255, 255, 255, 0.85);
|
||||||
border-bottom-color: #303030;
|
border-bottom-color: #303030;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-table {
|
.ant-table {
|
||||||
background: #1f1f1f;
|
background: #1f1f1f;
|
||||||
|
|
||||||
.ant-table-thead > tr > th {
|
.ant-table-thead>tr>th {
|
||||||
background: #141414;
|
background: #141414;
|
||||||
color: rgba(255, 255, 255, 0.85);
|
color: rgba(255, 255, 255, 0.85);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-table-tbody > tr > td {
|
.ant-table-tbody>tr>td {
|
||||||
border-bottom: 1px solid #303030;
|
border-bottom: 1px solid #303030;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,42 +137,49 @@ body {
|
|||||||
// Statistics card styles
|
// Statistics card styles
|
||||||
.stat-card {
|
.stat-card {
|
||||||
@apply rounded-lg border border-gray-100 dark:border-gray-800 p-6;
|
@apply rounded-lg border border-gray-100 dark:border-gray-800 p-6;
|
||||||
|
|
||||||
.stat-icon {
|
.stat-icon {
|
||||||
@apply w-12 h-12 rounded-full flex items-center justify-center mb-4;
|
@apply w-12 h-12 rounded-full flex items-center justify-center mb-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-title {
|
.stat-title {
|
||||||
@apply text-gray-600 dark:text-gray-400 text-sm font-medium;
|
@apply text-gray-600 dark:text-gray-400 text-sm font-medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
@apply text-2xl font-semibold mt-2;
|
@apply text-2xl font-semibold mt-2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 滚动条基础样式 */
|
/* 滚动条基础样式 */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px; /* 垂直滚动条宽度 */
|
width: 8px;
|
||||||
height: 8px; /* 水平滚动条高度 */
|
/* 垂直滚动条宽度 */
|
||||||
|
height: 8px;
|
||||||
|
/* 水平滚动条高度 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 亮色模式滚动条样式 */
|
/* 亮色模式滚动条样式 */
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
@apply bg-gray-100; /* 轨道背景色 */
|
@apply bg-gray-100;
|
||||||
|
/* 轨道背景色 */
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
@apply bg-gray-300 rounded-full hover:bg-gray-400 transition-colors; /* 滑块样式 */
|
@apply bg-gray-300 rounded-full hover:bg-gray-400 transition-colors;
|
||||||
|
/* 滑块样式 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 暗色模式滚动条样式 */
|
/* 暗色模式滚动条样式 */
|
||||||
.dark {
|
.dark {
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
@apply bg-gray-800; /* 暗色模式轨道背景 */
|
@apply bg-gray-800;
|
||||||
|
/* 暗色模式轨道背景 */
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
@apply bg-gray-600 hover:bg-gray-500; /* 暗色模式滑块样式 */
|
@apply bg-gray-600 hover:bg-gray-500;
|
||||||
|
/* 暗色模式滑块样式 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,3 +193,17 @@ body {
|
|||||||
scrollbar-color: theme('colors.gray.600') theme('colors.gray.800');
|
scrollbar-color: theme('colors.gray.600') theme('colors.gray.800');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
::view-transition-new(root),
|
||||||
|
::view-transition-old(root) {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark::view-transition-old(root) {
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { routes } from '@/routes/routes';
|
import { generateRoutes } from "@/routes/routes";
|
||||||
import * as AntIcons from '@ant-design/icons';
|
import * as AntIcons from "@ant-design/icons";
|
||||||
import { ColorIcon } from '@/components/Layout/ColorIcon';
|
import { ColorIcon } from "@/components/Layout/ColorIcon";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const getAntIcon = (iconName) => {
|
const getAntIcon = (iconName) => {
|
||||||
const iconKey = `${iconName.charAt(0).toUpperCase()}${iconName.slice(
|
const iconKey = `${iconName.charAt(0).toUpperCase()}${iconName.slice(
|
||||||
@@ -12,23 +10,25 @@ const getAntIcon = (iconName) => {
|
|||||||
return AntIcons[iconKey] ? React.createElement(AntIcons[iconKey]) : null;
|
return AntIcons[iconKey] ? React.createElement(AntIcons[iconKey]) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateMenuItems = (routes, parentPath = '') => {
|
const generateMenuItems = (routes, parentPath = "") => {
|
||||||
return routes.filter(route => !route.hidden).map((route) => {
|
return routes
|
||||||
const fullPath = `${parentPath}/${route.path}`.replace(/\/+/g, '/');
|
.filter((route) => !route.hidden)
|
||||||
const icon = route.icon && <ColorIcon icon={getAntIcon(route.icon)} />;
|
.map((route) => {
|
||||||
|
const fullPath = `${parentPath}/${route.path}`.replace(/\/+/g, "/");
|
||||||
|
const icon = route.icon && <ColorIcon icon={getAntIcon(route.icon)} />;
|
||||||
|
|
||||||
const menuItem = {
|
const menuItem = {
|
||||||
key: fullPath,
|
key: fullPath,
|
||||||
icon,
|
icon,
|
||||||
label: route.name,
|
label: route.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (route.children) {
|
if (route.children) {
|
||||||
menuItem.children = generateMenuItems(route.children, fullPath);
|
menuItem.children = generateMenuItems(route.children, fullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return menuItem;
|
return menuItem;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMenuItems = () => generateMenuItems(routes);
|
export const getMenuItems = (role) => generateMenuItems(generateRoutes(role));
|
||||||
|
|||||||
Reference in New Issue
Block a user