This commit is contained in:
‘Liammcl’
2024-12-28 15:53:26 +08:00
18 changed files with 5018 additions and 421 deletions

View File

@@ -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",

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;
}; };

View File

@@ -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) {

View File

@@ -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;
}; };

View File

@@ -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();

View File

@@ -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>
); );
}; };

View File

@@ -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"
/> />
{/* 添加模板类型选择弹窗 */} {/* 添加模板类型选择弹窗 */}

View File

@@ -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 }}

View File

@@ -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,

View File

@@ -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 }}

View File

@@ -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"

View File

@@ -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>

View File

@@ -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));
};

View File

@@ -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%;
}

View File

@@ -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));

4438
yarn.lock Normal file

File diff suppressed because it is too large Load Diff