fix
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
"react-router-dom": "^6.18.0",
|
||||
"recharts": "^2.9.0",
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
import React from 'react';
|
||||
import { Layout, Switch, Button, Dropdown } from 'antd';
|
||||
import { UserOutlined, LogoutOutlined } from '@ant-design/icons';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { MenuTrigger } from '../Layout/MenuTrigger';
|
||||
import React from "react";
|
||||
import { Layout, Switch, Button, Dropdown } from "antd";
|
||||
import { UserOutlined, LogoutOutlined } from "@ant-design/icons";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { MenuTrigger } from "../Layout/MenuTrigger";
|
||||
const { Header: AntHeader } = Layout;
|
||||
import { LuSun } from "react-icons/lu";
|
||||
import { GoMoon } from "react-icons/go";
|
||||
|
||||
const Header = ({ collapsed, setCollapsed }) => {
|
||||
const { isDarkMode, toggleTheme } = useTheme();
|
||||
const { user, logout } = useAuth();
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await logout();
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
console.error("Logout error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const userMenuItems = [
|
||||
{
|
||||
key: 'profile',
|
||||
key: "profile",
|
||||
icon: <UserOutlined />,
|
||||
label: '个人信息',
|
||||
label: "个人信息",
|
||||
},
|
||||
{
|
||||
key: 'logout',
|
||||
key: "logout",
|
||||
icon: <LogoutOutlined />,
|
||||
label: '退出登录',
|
||||
label: "退出登录",
|
||||
onClick: handleLogout,
|
||||
},
|
||||
];
|
||||
@@ -36,10 +37,10 @@ const Header = ({ collapsed, setCollapsed }) => {
|
||||
<AntHeader
|
||||
style={{
|
||||
padding: 0,
|
||||
background: isDarkMode ? '#141414' : '#fff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
background: isDarkMode ? "#141414" : "#fff",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
@@ -50,26 +51,37 @@ const Header = ({ collapsed, setCollapsed }) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 mr-6">
|
||||
<Switch
|
||||
checked={isDarkMode}
|
||||
onChange={toggleTheme}
|
||||
checkedChildren="🌙"
|
||||
unCheckedChildren="☀️"
|
||||
/>
|
||||
<div className="flex items-center gap-4 mr-10 h-full select-none">
|
||||
<div className="flex items-center justify-center cursor-pointer h-full" onClick={toggleTheme}>
|
||||
{isDarkMode ? (
|
||||
<GoMoon className="text-2xl" />
|
||||
) : (
|
||||
<LuSun className="text-2xl" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Dropdown
|
||||
menu={{ items: userMenuItems }}
|
||||
placement="bottomRight"
|
||||
trigger={['click']}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<UserOutlined />}
|
||||
className="flex items-center"
|
||||
>
|
||||
{user?.email}
|
||||
</Button>
|
||||
<div className="flex gap-2 items-center justify-center cursor-pointer h-full">
|
||||
{user?.user_metadata?.picture ? (
|
||||
<img
|
||||
src={user?.user_metadata?.picture}
|
||||
alt="picture"
|
||||
className="w-8 h-8 rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<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>
|
||||
</div>
|
||||
</AntHeader>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Layout, Menu } from 'antd';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import { getMenuItems } from '@/utils/menuUtils';
|
||||
import { Logo } from '@/components/Layout/Logo';
|
||||
import React, { useMemo } from "react";
|
||||
import { Layout, Menu } from "antd";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { getMenuItems } from "@/utils/menuUtils";
|
||||
import { Logo } from "@/components/Layout/Logo";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
|
||||
const { Sider } = Layout;
|
||||
|
||||
@@ -11,13 +12,18 @@ const Sidebar = ({ collapsed }) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
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 pathSegments = location.pathname.split('/').filter(Boolean);
|
||||
const pathSegments = location.pathname.split("/").filter(Boolean);
|
||||
return pathSegments.reduce((acc, _, index) => {
|
||||
const path = `/${pathSegments.slice(0, index + 1).join('/')}`;
|
||||
const path = `/${pathSegments.slice(0, index + 1).join("/")}`;
|
||||
acc.push(path);
|
||||
return acc;
|
||||
}, []);
|
||||
@@ -33,14 +39,14 @@ const Sidebar = ({ collapsed }) => {
|
||||
trigger={null}
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
theme={isDarkMode ? 'dark' : 'light'}
|
||||
theme={isDarkMode ? "dark" : "light"}
|
||||
width={256}
|
||||
collapsedWidth={80} // 添加这个属性
|
||||
className={`app-sidebar ${collapsed ? 'collapsed' : ''}`} // 添加collapsed类名
|
||||
className={`app-sidebar ${collapsed ? "collapsed" : ""}`} // 添加collapsed类名
|
||||
>
|
||||
<Logo collapsed={collapsed} isDarkMode={isDarkMode} />
|
||||
<Menu
|
||||
theme={isDarkMode ? 'dark' : 'light'}
|
||||
theme={isDarkMode ? "dark" : "light"}
|
||||
mode="inline"
|
||||
selectedKeys={[location.pathname]}
|
||||
defaultOpenKeys={defaultOpenKeys}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Navigate,useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { Spin } from 'antd';
|
||||
import React from "react";
|
||||
import { Navigate, useLocation } from "react-router-dom";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { Spin } from "antd";
|
||||
|
||||
export const ProtectedRoute = ({ children }) => {
|
||||
const { user, loading } = useAuth();
|
||||
@@ -14,8 +14,10 @@ export const ProtectedRoute = ({ children }) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <Navigate to={`/login`} replace />;
|
||||
if (!user?.id) {
|
||||
return (
|
||||
<Navigate to={`/login?redirectTo=${location.pathname || ""}`} replace />
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
|
||||
@@ -2,6 +2,8 @@ import React, { createContext, useContext, useState, useEffect } from "react";
|
||||
import { supabase } from "@/config/supabase";
|
||||
import { message } from "antd";
|
||||
import { useNavigate, useSearchParams, useLocation } from "react-router-dom";
|
||||
import { supabaseService } from "@/hooks/supabaseService";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
const AuthContext = createContext({});
|
||||
|
||||
@@ -9,7 +11,7 @@ export const AuthProvider = ({ children }) => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [user, setUser] = useState(null);
|
||||
const [user, setUser] = useState({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -37,7 +39,9 @@ export const AuthProvider = ({ children }) => {
|
||||
data: { session },
|
||||
error,
|
||||
} = await supabase.auth.getSession();
|
||||
setUser(session?.user ?? null);
|
||||
|
||||
const role = await checkInTeam(session?.user ?? null);
|
||||
setUser({ ...session?.user, adminRole: role });
|
||||
} catch (error) {
|
||||
console.error("Error getting session:", error);
|
||||
} finally {
|
||||
@@ -46,17 +50,6 @@ export const AuthProvider = ({ children }) => {
|
||||
};
|
||||
|
||||
initSession();
|
||||
|
||||
// 订阅认证状态变化
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setUser(session?.user ?? null);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription?.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// useEffect(() => {
|
||||
@@ -66,6 +59,45 @@ export const AuthProvider = ({ children }) => {
|
||||
// }
|
||||
// }, [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) => {
|
||||
try {
|
||||
@@ -79,7 +111,6 @@ export const AuthProvider = ({ children }) => {
|
||||
message.error(error.message || "登录失败,请稍后重试");
|
||||
return;
|
||||
}
|
||||
|
||||
setUser(data.user);
|
||||
return data;
|
||||
} catch (error) {
|
||||
@@ -152,7 +183,7 @@ export const AuthProvider = ({ children }) => {
|
||||
message.error(error.message || "登出失败,请稍后重试");
|
||||
return;
|
||||
}
|
||||
setUser(null);
|
||||
setUser({});
|
||||
message.success("已成功登出");
|
||||
navigate(`/login?redirectTo=${location.pathname}`, { replace: true });
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,71 +1,121 @@
|
||||
import React, { createContext,useContext, useState, useEffect, } from 'react';
|
||||
import React, { createContext, useContext, useState, useEffect } from "react";
|
||||
import { ConfigProvider, theme } from "antd";
|
||||
const ThemeContext = createContext();
|
||||
|
||||
export const ThemeProvider = ({ children }) => {
|
||||
const [isDarkMode, setIsDarkMode] = useState(() => {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
return savedTheme === 'dark';
|
||||
const savedTheme = localStorage.getItem("theme");
|
||||
return savedTheme === "dark";
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
|
||||
document.documentElement.classList.toggle('dark', isDarkMode);
|
||||
localStorage.setItem("theme", isDarkMode ? "dark" : "light");
|
||||
document.documentElement.classList.toggle("dark", isDarkMode);
|
||||
}, [isDarkMode]);
|
||||
|
||||
const handleTheme = (event) => {
|
||||
const { clientX, clientY } = event;
|
||||
let outIsDark;
|
||||
|
||||
const toggleTheme = () => {
|
||||
setIsDarkMode(!isDarkMode);
|
||||
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 (
|
||||
<ThemeContext.Provider value={{ isDarkMode, toggleTheme }}>
|
||||
<ThemeContext.Provider value={{ isDarkMode, toggleTheme: handleTheme }}>
|
||||
<ConfigProvider
|
||||
componentSize="small"
|
||||
theme={{
|
||||
algorithm: isDarkMode ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
||||
token: {
|
||||
// 主色调 - Google Blue
|
||||
colorPrimary: '#1a73e8',
|
||||
colorPrimaryHover: '#1557b0',
|
||||
colorPrimaryActive: '#174ea6',
|
||||
colorPrimary: "#1a73e8",
|
||||
colorPrimaryHover: "#1557b0",
|
||||
colorPrimaryActive: "#174ea6",
|
||||
|
||||
// 成功色 - Google Green
|
||||
colorSuccess: '#1e8e3e',
|
||||
colorSuccessHover: '#188130',
|
||||
colorSuccessActive: '#137333',
|
||||
colorSuccess: "#1e8e3e",
|
||||
colorSuccessHover: "#188130",
|
||||
colorSuccessActive: "#137333",
|
||||
|
||||
// 警告色 - Google Yellow
|
||||
colorWarning: '#f9ab00',
|
||||
colorWarningHover: '#f29900',
|
||||
colorWarningActive: '#ea8600',
|
||||
colorWarning: "#f9ab00",
|
||||
colorWarningHover: "#f29900",
|
||||
colorWarningActive: "#ea8600",
|
||||
|
||||
// 错误色 - Google Red
|
||||
colorError: '#d93025',
|
||||
colorErrorHover: '#c5221f',
|
||||
colorErrorActive: '#a50e0e',
|
||||
colorError: "#d93025",
|
||||
colorErrorHover: "#c5221f",
|
||||
colorErrorActive: "#a50e0e",
|
||||
|
||||
// 文字颜色
|
||||
colorText: isDarkMode ? 'rgba(255, 255, 255, 0.87)' : 'rgba(0, 0, 0, 0.87)',
|
||||
colorTextSecondary: isDarkMode ? 'rgba(255, 255, 255, 0.60)' : 'rgba(0, 0, 0, 0.60)',
|
||||
colorTextTertiary: isDarkMode ? 'rgba(255, 255, 255, 0.38)' : 'rgba(0, 0, 0, 0.38)',
|
||||
colorText: isDarkMode
|
||||
? "rgba(255, 255, 255, 0.87)"
|
||||
: "rgba(0, 0, 0, 0.87)",
|
||||
colorTextSecondary: isDarkMode
|
||||
? "rgba(255, 255, 255, 0.60)"
|
||||
: "rgba(0, 0, 0, 0.60)",
|
||||
colorTextTertiary: isDarkMode
|
||||
? "rgba(255, 255, 255, 0.38)"
|
||||
: "rgba(0, 0, 0, 0.38)",
|
||||
|
||||
// 背景色
|
||||
colorBgContainer: isDarkMode ? '#1f1f1f' : '#ffffff',
|
||||
colorBgElevated: isDarkMode ? '#2d2d2d' : '#ffffff',
|
||||
colorBgLayout: isDarkMode ? '#121212' : '#f8f9fa',
|
||||
colorBgContainer: isDarkMode ? "#1f1f1f" : "#ffffff",
|
||||
colorBgElevated: isDarkMode ? "#2d2d2d" : "#ffffff",
|
||||
colorBgLayout: isDarkMode ? "#121212" : "#f8f9fa",
|
||||
|
||||
// 边框
|
||||
borderRadius: 8,
|
||||
borderRadiusLG: 12,
|
||||
borderRadiusSM: 4,
|
||||
colorBorder: isDarkMode ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.12)',
|
||||
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)',
|
||||
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',
|
||||
fontFamily: "Google Sans, Roboto, Arial, sans-serif",
|
||||
fontSize: 14,
|
||||
|
||||
// 控件尺寸
|
||||
@@ -89,11 +139,11 @@ export const ThemeProvider = ({ children }) => {
|
||||
controlHeightLG: 44,
|
||||
paddingContentHorizontal: 16,
|
||||
borderRadius: 4,
|
||||
controlOutline: 'none',
|
||||
controlOutline: "none",
|
||||
controlOutlineWidth: 0,
|
||||
controlPaddingHorizontal: 16,
|
||||
controlPaddingHorizontalSM: 12,
|
||||
addonBg: 'transparent',
|
||||
addonBg: "transparent",
|
||||
},
|
||||
Select: {
|
||||
algorithm: true,
|
||||
@@ -101,14 +151,15 @@ export const ThemeProvider = ({ children }) => {
|
||||
controlHeightSM: 32,
|
||||
controlHeightLG: 44,
|
||||
borderRadius: 4,
|
||||
controlOutline: 'none',
|
||||
controlOutline: "none",
|
||||
controlOutlineWidth: 0,
|
||||
selectorBg: 'transparent',
|
||||
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)',
|
||||
boxShadow:
|
||||
"0 1px 2px 0 rgba(60,64,67,0.3), 0 1px 3px 1px rgba(60,64,67,0.15)",
|
||||
},
|
||||
Menu: {
|
||||
algorithm: true,
|
||||
@@ -118,25 +169,25 @@ export const ThemeProvider = ({ children }) => {
|
||||
},
|
||||
Tabs: {
|
||||
algorithm: true,
|
||||
inkBarColor: '#1a73e8',
|
||||
itemSelectedColor: '#1a73e8',
|
||||
itemHoverColor: '#174ea6',
|
||||
inkBarColor: "#1a73e8",
|
||||
itemSelectedColor: "#1a73e8",
|
||||
itemHoverColor: "#174ea6",
|
||||
},
|
||||
Table: {
|
||||
algorithm: true,
|
||||
borderRadius: 8,
|
||||
headerBg: isDarkMode ? '#2d2d2d' : '#f8f9fa',
|
||||
headerBg: isDarkMode ? "#2d2d2d" : "#f8f9fa",
|
||||
},
|
||||
Modal: {
|
||||
algorithm: true,
|
||||
borderRadius: 28, // Google 风格的大圆角对话框
|
||||
paddingContentHorizontal: 24,
|
||||
paddingContentVertical: 24,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{ children }
|
||||
{children}
|
||||
</ConfigProvider>
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
@@ -145,7 +196,7 @@ export const ThemeProvider = ({ children }) => {
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -1,50 +1,55 @@
|
||||
import { supabase } from '@/config/supabase'
|
||||
import { supabase } from "@/config/supabase";
|
||||
|
||||
class SupabaseService {
|
||||
async select(table, options = {}) {
|
||||
try {
|
||||
let query = supabase
|
||||
.from(table)
|
||||
.select(options.select || '*', { count: 'exact' })
|
||||
.select(options.select || "*", { count: "exact" });
|
||||
|
||||
// 处理精确匹配条件
|
||||
if (options.match) {
|
||||
query = query.match(options.match)
|
||||
query = query.match(options.match);
|
||||
}
|
||||
|
||||
// 处理过滤条件
|
||||
if (options.filter) {
|
||||
Object.entries(options.filter).forEach(([key, condition]) => {
|
||||
Object.entries(condition).forEach(([operator, value]) => {
|
||||
query = query.filter(key, operator, value)
|
||||
})
|
||||
})
|
||||
query = query.filter(key, operator, value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 处理排序
|
||||
if (options.order) {
|
||||
query = query.order(options.order.column, {
|
||||
ascending: options.order.ascending
|
||||
})
|
||||
ascending: options.order.ascending,
|
||||
});
|
||||
}
|
||||
|
||||
// 处理分页
|
||||
if (options.page && options.pageSize) {
|
||||
const from = (options.page - 1) * options.pageSize
|
||||
const to = from + options.pageSize - 1
|
||||
query = query.range(from, to)
|
||||
const from = (options.page - 1) * options.pageSize;
|
||||
const to = from + options.pageSize - 1;
|
||||
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 {
|
||||
data,
|
||||
total: count || 0
|
||||
}
|
||||
total: count || 0,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error fetching from ${table}:`, error.message)
|
||||
throw error
|
||||
console.error(`Error fetching from ${table}:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,29 +59,27 @@ class SupabaseService {
|
||||
const { data: result, error } = await supabase
|
||||
.from(table)
|
||||
.insert(data)
|
||||
.select()
|
||||
.select();
|
||||
|
||||
if (error) throw error
|
||||
return result
|
||||
if (error) throw error;
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`Error inserting into ${table}:`, error.message)
|
||||
throw error
|
||||
console.error(`Error inserting into ${table}:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 优化 UPDATE 请求
|
||||
async update(table, match, updates) {
|
||||
try {
|
||||
let query = supabase
|
||||
.from(table)
|
||||
.update(updates);
|
||||
let query = supabase.from(table).update(updates);
|
||||
|
||||
// 如果是对象,使用 match 方法
|
||||
if (typeof match === 'object') {
|
||||
if (typeof match === "object") {
|
||||
query = query.match(match);
|
||||
} else {
|
||||
// 如果是单个 id,使用 eq 方法
|
||||
query = query.eq('id', match);
|
||||
query = query.eq("id", match);
|
||||
}
|
||||
|
||||
const { data, error } = await query.select();
|
||||
@@ -92,38 +95,32 @@ class SupabaseService {
|
||||
// 通用 DELETE 请求
|
||||
async delete(table, match) {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from(table)
|
||||
.delete()
|
||||
.match(match)
|
||||
const { error } = await supabase.from(table).delete().match(match);
|
||||
|
||||
if (error) throw error
|
||||
return true
|
||||
if (error) throw error;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Error deleting from ${table}:`, error.message)
|
||||
throw error
|
||||
console.error(`Error deleting from ${table}:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// 通用 UPSERT 请求
|
||||
async upsert(table, data, onConflict) {
|
||||
try {
|
||||
let query = supabase
|
||||
.from(table)
|
||||
.upsert(data)
|
||||
.select()
|
||||
let query = supabase.from(table).upsert(data).select();
|
||||
|
||||
if (onConflict) {
|
||||
query = query.onConflict(onConflict)
|
||||
query = query.onConflict(onConflict);
|
||||
}
|
||||
|
||||
const { data: result, error } = await query
|
||||
if (error) throw error
|
||||
return result
|
||||
const { data: result, error } = await query;
|
||||
if (error) throw error;
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`Error upserting into ${table}:`, error.message)
|
||||
throw error
|
||||
console.error(`Error upserting into ${table}:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const supabaseService = new SupabaseService()
|
||||
export const supabaseService = new SupabaseService();
|
||||
|
||||
@@ -14,7 +14,6 @@ const Login = () => {
|
||||
login(values.email, values.password);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<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">
|
||||
@@ -97,8 +96,6 @@ const Login = () => {
|
||||
>
|
||||
使用 Google 账号登录
|
||||
</Button>
|
||||
|
||||
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ const ServicePage = () => {
|
||||
return (
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
{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">
|
||||
<h3 className="text-lg font-medium text-gray-800">{section.sectionName}</h3>
|
||||
<Popconfirm
|
||||
@@ -586,7 +586,7 @@ const ServicePage = () => {
|
||||
pageSize: 10,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}}
|
||||
className="bg-white rounded-lg"
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
{/* 添加模板类型选择弹窗 */}
|
||||
|
||||
@@ -264,7 +264,7 @@ const Classify = ({activeType,typeList}) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-sm">
|
||||
<div className="rounded-lg shadow-sm">
|
||||
<Form form={form}>
|
||||
<Table
|
||||
scroll={{ x: true }}
|
||||
|
||||
@@ -41,7 +41,7 @@ const ResourceManagement = () => {
|
||||
activeKey={activeType}
|
||||
onChange={setActiveType}
|
||||
type="card"
|
||||
className="bg-white rounded-lg shadow-sm"
|
||||
className="rounded-lg shadow-sm"
|
||||
items={TEMPLATE_TYPES.map(type => ({
|
||||
key: type.key,
|
||||
label: (
|
||||
|
||||
@@ -223,7 +223,7 @@ const UnitManagement = ({ activeType, typeList }) => {
|
||||
|
||||
return (
|
||||
<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 items-center gap-2">
|
||||
<Button
|
||||
@@ -261,7 +261,7 @@ const UnitManagement = ({ activeType, typeList }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-sm">
|
||||
<div className="rounded-lg shadow-sm">
|
||||
<Form form={form}>
|
||||
<Table
|
||||
scroll={{ x: true }}
|
||||
|
||||
@@ -460,7 +460,7 @@ const StorageManager = () => {
|
||||
// 修改文件列表渲染
|
||||
const renderFileList = () => (
|
||||
<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"
|
||||
>
|
||||
{/* 面包屑导航 */}
|
||||
@@ -600,7 +600,7 @@ const StorageManager = () => {
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-50">
|
||||
<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
|
||||
{...uploadProps}
|
||||
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 { Spin } from 'antd';
|
||||
import MainLayout from '@/components/Layout/MainLayout';
|
||||
import { routes } from '@/routes/routes';
|
||||
import { generateRoutes } from '@/routes/routes';
|
||||
import Login from '@/pages/auth/Login';
|
||||
import NotFound from '@/pages/notFound';
|
||||
import Dashboard from '@/pages/Dashboard';
|
||||
@@ -48,13 +48,14 @@ const renderRoutes = (routes) => {
|
||||
|
||||
const AppRoutes = () => {
|
||||
const { user } = useAuth();
|
||||
const { adminRole: role } = user;
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
path="/login"
|
||||
element={
|
||||
user ? <Navigate to="/company/serviceTemplate" replace /> : <Login />
|
||||
user?.id ? <Navigate to="/company/serviceTemplate" replace /> : <Login />
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -70,7 +71,7 @@ const AppRoutes = () => {
|
||||
index
|
||||
element={<Navigate to="/company/serviceTemplate" replace />}
|
||||
/>
|
||||
{renderRoutes(routes)}
|
||||
{renderRoutes(generateRoutes(role))}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { lazy } from "react";
|
||||
|
||||
|
||||
// Resource Management routes
|
||||
const resourceRoutes = [
|
||||
{
|
||||
@@ -8,24 +7,28 @@ const resourceRoutes = [
|
||||
component: lazy(() => import("@/pages/resource/team")),
|
||||
name: "团队管理",
|
||||
icon: "team",
|
||||
roles: ["OWNER"],
|
||||
},
|
||||
{
|
||||
path: "bucket",
|
||||
component: lazy(() => import("@/pages/resource/bucket")),
|
||||
name: "对象存储",
|
||||
icon: "shop",
|
||||
roles: ["OWNER"],
|
||||
},
|
||||
{
|
||||
path: "task",
|
||||
component: lazy(() => import("@/pages/resource/resourceTask")),
|
||||
name: "任务管理",
|
||||
icon: "appstore",
|
||||
roles: ["OWNER"],
|
||||
},
|
||||
{
|
||||
path: "task/edit/:id?",
|
||||
component: lazy(() => import("@/pages/resource/resourceTask/edit")),
|
||||
hidden: true,
|
||||
name: "新增/编辑任务",
|
||||
roles: ["OWNER"],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -36,6 +39,7 @@ const companyRoutes = [
|
||||
component: lazy(() => import("@/pages/company/quotation")),
|
||||
name: "报价单",
|
||||
icon: "file",
|
||||
roles: ["ADMIN", "OWNER"],
|
||||
},
|
||||
{
|
||||
path: "quotaInfo/:id?", // 添加可选的 id 参数
|
||||
@@ -43,18 +47,21 @@ const companyRoutes = [
|
||||
component: lazy(() => import("@/pages/company/quotation/detail")),
|
||||
name: "报价单详情",
|
||||
icon: "file",
|
||||
roles: ["ADMIN", "OWNER"],
|
||||
},
|
||||
{
|
||||
path: "serviceTemplate",
|
||||
component: lazy(() => import("@/pages/company/service")),
|
||||
name: "服务管理",
|
||||
icon: "container",
|
||||
roles: ["ADMIN", "OWNER"],
|
||||
},
|
||||
{
|
||||
path: "templateItemManage",
|
||||
component: lazy(() => import("@/pages/company/service/itemsManange")),
|
||||
name: "资源类型",
|
||||
icon: "container",
|
||||
roles: ["ADMIN", "OWNER"],
|
||||
},
|
||||
{
|
||||
path: "serviceTemplateInfo/:id?",
|
||||
@@ -62,6 +69,7 @@ const companyRoutes = [
|
||||
component: lazy(() => import("@/pages/company/service/detail")),
|
||||
name: "服务模版详情",
|
||||
icon: "container",
|
||||
roles: ["ADMIN", "OWNER"],
|
||||
},
|
||||
{
|
||||
path: "quotaInfo/preview/:id?", // 添加可选的 id 参数
|
||||
@@ -69,12 +77,14 @@ const companyRoutes = [
|
||||
component: lazy(() => import("@/pages/company/quotation/view")),
|
||||
name: "报价单预览",
|
||||
icon: "file",
|
||||
roles: ["ADMIN", "OWNER"],
|
||||
},
|
||||
{
|
||||
path: "customer",
|
||||
component: lazy(() => import("@/pages/company/customer")),
|
||||
name: "客户管理",
|
||||
icon: "user",
|
||||
roles: ["ADMIN", "OWNER"],
|
||||
},
|
||||
{
|
||||
path: "customerInfo/:id?",
|
||||
@@ -82,12 +92,14 @@ const companyRoutes = [
|
||||
component: lazy(() => import("@/pages/company/customer/detail")),
|
||||
name: "客户详情",
|
||||
icon: "user",
|
||||
roles: ["ADMIN", "OWNER"],
|
||||
},
|
||||
{
|
||||
path: "supplier",
|
||||
component: lazy(() => import("@/pages/company/supplier")),
|
||||
name: "供应商管理",
|
||||
icon: "branches",
|
||||
roles: ["ADMIN", "OWNER"],
|
||||
},
|
||||
{
|
||||
path: "supplierInfo/:id?",
|
||||
@@ -95,37 +107,63 @@ const companyRoutes = [
|
||||
component: lazy(() => import("@/pages/company/supplier/detail")),
|
||||
name: "供应商详情",
|
||||
icon: "branches",
|
||||
roles: ["ADMIN", "OWNER"],
|
||||
},
|
||||
];
|
||||
|
||||
const marketingRoutes = [];
|
||||
|
||||
export const routes = [
|
||||
// {
|
||||
// path: "dashboard",
|
||||
// component: lazy(() => import("@/pages/Dashboard")),
|
||||
// name: "仪表盘",
|
||||
// icon: "dashboard",
|
||||
// },
|
||||
const roleRoutes = [
|
||||
{
|
||||
path: "role",
|
||||
component: lazy(() => import("@/pages/role")),
|
||||
name: "角色管理",
|
||||
icon: "setting",
|
||||
roles: ["ADMIN", "OWNER"],
|
||||
},
|
||||
];
|
||||
|
||||
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,
|
||||
children: resourceRoutes.filter((route) => route.roles.includes(role)),
|
||||
roles: ["OWNER"],
|
||||
},
|
||||
{
|
||||
path: "company",
|
||||
component: lazy(() => import("@/pages/company")),
|
||||
name: "公司管理",
|
||||
icon: "bank",
|
||||
children: companyRoutes,
|
||||
children: companyRoutes.filter((route) => route.roles.includes(role)),
|
||||
roles: ["ADMIN", "OWNER"],
|
||||
},
|
||||
{
|
||||
path: "marketing",
|
||||
component: lazy(() => import("@/pages/marketing")),
|
||||
name: "行销中心",
|
||||
icon: "shopping",
|
||||
children: marketingRoutes,
|
||||
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 utilities;
|
||||
@import url('https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&display=swap');
|
||||
|
||||
:root {
|
||||
--primary-color: #1677ff;
|
||||
--primary-gradient: linear-gradient(45deg, #1677ff, #36cff0);
|
||||
@@ -40,7 +41,7 @@ body {
|
||||
line-height: 44px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.ant-menu-item-icon + span {
|
||||
.ant-menu-item-icon+span {
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
@@ -56,6 +57,7 @@ body {
|
||||
}
|
||||
|
||||
&.ant-menu-inline-collapsed {
|
||||
|
||||
.ant-menu-item,
|
||||
.ant-menu-submenu .ant-menu-submenu-title {
|
||||
padding: 0 !important;
|
||||
@@ -85,7 +87,7 @@ body {
|
||||
}
|
||||
|
||||
&.ant-menu-submenu-open {
|
||||
> .ant-menu-submenu-title {
|
||||
>.ant-menu-submenu-title {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
@@ -121,12 +123,12 @@ body {
|
||||
.ant-table {
|
||||
background: #1f1f1f;
|
||||
|
||||
.ant-table-thead > tr > th {
|
||||
.ant-table-thead>tr>th {
|
||||
background: #141414;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
.ant-table-tbody>tr>td {
|
||||
border-bottom: 1px solid #303030;
|
||||
}
|
||||
}
|
||||
@@ -148,29 +150,36 @@ body {
|
||||
@apply text-2xl font-semibold mt-2;
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条基础样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px; /* 垂直滚动条宽度 */
|
||||
height: 8px; /* 水平滚动条高度 */
|
||||
width: 8px;
|
||||
/* 垂直滚动条宽度 */
|
||||
height: 8px;
|
||||
/* 水平滚动条高度 */
|
||||
}
|
||||
|
||||
/* 亮色模式滚动条样式 */
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-gray-100; /* 轨道背景色 */
|
||||
@apply bg-gray-100;
|
||||
/* 轨道背景色 */
|
||||
}
|
||||
|
||||
::-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 {
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-gray-800; /* 暗色模式轨道背景 */
|
||||
@apply bg-gray-800;
|
||||
/* 暗色模式轨道背景 */
|
||||
}
|
||||
|
||||
::-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');
|
||||
}
|
||||
|
||||
|
||||
|
||||
::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 { routes } from '@/routes/routes';
|
||||
import * as AntIcons from '@ant-design/icons';
|
||||
import { ColorIcon } from '@/components/Layout/ColorIcon';
|
||||
|
||||
|
||||
import React from "react";
|
||||
import { generateRoutes } from "@/routes/routes";
|
||||
import * as AntIcons from "@ant-design/icons";
|
||||
import { ColorIcon } from "@/components/Layout/ColorIcon";
|
||||
|
||||
const getAntIcon = (iconName) => {
|
||||
const iconKey = `${iconName.charAt(0).toUpperCase()}${iconName.slice(
|
||||
@@ -12,9 +10,11 @@ const getAntIcon = (iconName) => {
|
||||
return AntIcons[iconKey] ? React.createElement(AntIcons[iconKey]) : null;
|
||||
};
|
||||
|
||||
const generateMenuItems = (routes, parentPath = '') => {
|
||||
return routes.filter(route => !route.hidden).map((route) => {
|
||||
const fullPath = `${parentPath}/${route.path}`.replace(/\/+/g, '/');
|
||||
const generateMenuItems = (routes, parentPath = "") => {
|
||||
return routes
|
||||
.filter((route) => !route.hidden)
|
||||
.map((route) => {
|
||||
const fullPath = `${parentPath}/${route.path}`.replace(/\/+/g, "/");
|
||||
const icon = route.icon && <ColorIcon icon={getAntIcon(route.icon)} />;
|
||||
|
||||
const menuItem = {
|
||||
@@ -31,4 +31,4 @@ const generateMenuItems = (routes, parentPath = '') => {
|
||||
});
|
||||
};
|
||||
|
||||
export const getMenuItems = () => generateMenuItems(routes);
|
||||
export const getMenuItems = (role) => generateMenuItems(generateRoutes(role));
|
||||
|
||||
Reference in New Issue
Block a user