页面登录优化,team优化
This commit is contained in:
@@ -21,7 +21,7 @@ export const createSupabase = () => {
|
|||||||
detectSessionInUrl: false,
|
detectSessionInUrl: false,
|
||||||
},
|
},
|
||||||
db: {
|
db: {
|
||||||
schema: 'limq_dev'
|
schema: 'limq'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
import React, { createContext,useContext, useState, useEffect, } from 'react';
|
||||||
|
import { ConfigProvider, theme } from "antd";
|
||||||
const ThemeContext = createContext();
|
const ThemeContext = createContext();
|
||||||
|
|
||||||
export const ThemeProvider = ({ children }) => {
|
export const ThemeProvider = ({ children }) => {
|
||||||
@@ -19,7 +19,52 @@ export const ThemeProvider = ({ children }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={{ isDarkMode, toggleTheme }}>
|
<ThemeContext.Provider value={{ isDarkMode, toggleTheme }}>
|
||||||
{children}
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
algorithm: isDarkMode ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
||||||
|
token: {
|
||||||
|
colorPrimary: "#1677ff",
|
||||||
|
borderRadius: 4,
|
||||||
|
colorBgContainer: isDarkMode ? "#141414" : "#ffffff",
|
||||||
|
colorBgElevated: isDarkMode ? "#1f1f1f" : "#ffffff",
|
||||||
|
colorText: isDarkMode
|
||||||
|
? "rgba(255, 255, 255, 0.85)"
|
||||||
|
: "rgba(0, 0, 0, 0.85)",
|
||||||
|
colorTextSecondary: isDarkMode
|
||||||
|
? "rgba(255, 255, 255, 0.45)"
|
||||||
|
: "rgba(0, 0, 0, 0.45)",
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
// 为所有支持 variant 的组件设置 filled 模式
|
||||||
|
Form: {
|
||||||
|
variant: 'filled',
|
||||||
|
},
|
||||||
|
Input: {
|
||||||
|
variant: 'filled',
|
||||||
|
},
|
||||||
|
Select: {
|
||||||
|
variant: 'filled',
|
||||||
|
},
|
||||||
|
TreeSelect: {
|
||||||
|
variant: 'filled',
|
||||||
|
},
|
||||||
|
DatePicker: {
|
||||||
|
variant: 'filled',
|
||||||
|
},
|
||||||
|
TimePicker: {
|
||||||
|
variant: 'filled',
|
||||||
|
},
|
||||||
|
Cascader: {
|
||||||
|
variant: 'filled',
|
||||||
|
},
|
||||||
|
AutoComplete: {
|
||||||
|
variant: 'filled',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
</ConfigProvider>
|
||||||
</ThemeContext.Provider>
|
</ThemeContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
101
src/hooks/supabaseService.js
Normal file
101
src/hooks/supabaseService.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { supabase } from '@/config/supabase'
|
||||||
|
|
||||||
|
class SupabaseService {
|
||||||
|
async get(table, options = {}) {
|
||||||
|
try {
|
||||||
|
let query = supabase
|
||||||
|
.from(table)
|
||||||
|
.select(options.select || '*', { count: 'exact' })
|
||||||
|
|
||||||
|
// 处理精确匹配条件
|
||||||
|
if (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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理排序
|
||||||
|
if (options.order) {
|
||||||
|
query = query.order(options.order.column, {
|
||||||
|
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 { data, error, count } = await query
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
total: count || 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching from ${table}:`, error.message)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用 INSERT 请求
|
||||||
|
async insert(table, data) {
|
||||||
|
try {
|
||||||
|
const { data: result, error } = await supabase
|
||||||
|
.from(table)
|
||||||
|
.insert(data)
|
||||||
|
.select()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error inserting into ${table}:`, error.message)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用 UPDATE 请求
|
||||||
|
async update(table, match, updates) {
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from(table)
|
||||||
|
.update(updates)
|
||||||
|
.match(match)
|
||||||
|
.select()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error updating ${table}:`, error.message)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用 DELETE 请求
|
||||||
|
async delete(table, match) {
|
||||||
|
try {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from(table)
|
||||||
|
.delete()
|
||||||
|
.match(match)
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error deleting from ${table}:`, error.message)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const supabaseService = new SupabaseService()
|
||||||
@@ -1,71 +1,58 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { message } from 'antd';
|
import { supabaseService } from '@/hooks/supabaseService';
|
||||||
import { teamMembershipService } from '@/services/supabase/teamMembership';
|
|
||||||
|
|
||||||
export const useTeamMemberships = (teamId) => {
|
export const useTeamMembership = (teamId) => {
|
||||||
const [memberships, setMemberships] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [memberships, setMemberships] = useState([]);
|
||||||
|
|
||||||
const fetchMemberships = useCallback(async () => {
|
const loadMemberships = useCallback(async () => {
|
||||||
if (!teamId) return;
|
if (!teamId) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
const result = await supabaseService.get('team_memberships', {
|
||||||
const data = await teamMembershipService.getMemberships(teamId);
|
select: '*',
|
||||||
setMemberships(data);
|
relations: {
|
||||||
|
user: 'id, email, name'
|
||||||
|
},
|
||||||
|
match: { teamId }
|
||||||
|
});
|
||||||
|
setMemberships(result.data || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('Failed to fetch team memberships');
|
console.error('获取成员列表失败:', error);
|
||||||
console.error(error);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [teamId]);
|
}, [teamId]);
|
||||||
|
|
||||||
const createMembership = async (values) => {
|
const addMembership = async (values) => {
|
||||||
try {
|
const result = await supabaseService.insert('team_memberships', {
|
||||||
const newMembership = await teamMembershipService.createMembership({
|
...values,
|
||||||
...values,
|
teamId
|
||||||
teamId,
|
});
|
||||||
});
|
await loadMemberships();
|
||||||
setMemberships(prev => [...prev, newMembership]);
|
return result[0];
|
||||||
message.success('Member added successfully');
|
|
||||||
return newMembership;
|
|
||||||
} catch (error) {
|
|
||||||
message.error('Failed to add member');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateMembership = async (id, values) => {
|
const updateMembership = async (id, values) => {
|
||||||
try {
|
const result = await supabaseService.update('team_memberships',
|
||||||
const updatedMembership = await teamMembershipService.updateMembership(id, values);
|
{ id },
|
||||||
setMemberships(prev => prev.map(membership =>
|
values
|
||||||
membership.id === id ? updatedMembership : membership
|
);
|
||||||
));
|
await loadMemberships();
|
||||||
message.success('Member updated successfully');
|
return result[0];
|
||||||
return updatedMembership;
|
|
||||||
} catch (error) {
|
|
||||||
message.error('Failed to update member');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteMembership = async (id) => {
|
const deleteMembership = async (id) => {
|
||||||
try {
|
await supabaseService.delete('team_memberships', { id });
|
||||||
await teamMembershipService.deleteMembership(id);
|
await loadMemberships();
|
||||||
setMemberships(prev => prev.filter(membership => membership.id !== id));
|
|
||||||
message.success('Member removed successfully');
|
|
||||||
} catch (error) {
|
|
||||||
message.error('Failed to remove member');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
memberships,
|
|
||||||
loading,
|
loading,
|
||||||
fetchMemberships,
|
memberships,
|
||||||
createMembership,
|
loadMemberships,
|
||||||
|
addMembership,
|
||||||
updateMembership,
|
updateMembership,
|
||||||
deleteMembership,
|
deleteMembership,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,68 +1,96 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { supabaseService } from '@/hooks/supabaseService';
|
||||||
import { message } from 'antd';
|
|
||||||
import { teamService } from '@/services/supabase/team';
|
|
||||||
|
|
||||||
export const useTeams = (pagination, sorter) => {
|
export const useTeams = () => {
|
||||||
const [teams, setTeams] = useState([]);
|
// 获取团队列表
|
||||||
const [loading, setLoading] = useState(false);
|
const fetchTeams = async (params = {}) => {
|
||||||
|
|
||||||
const fetchTeams = useCallback(async (params = {}) => {
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
const result = await supabaseService.get('teams', {
|
||||||
const { data, total } = await teamService.getTeams({
|
select: `
|
||||||
page: params.current || pagination.current,
|
id,
|
||||||
pageSize: params.pageSize || pagination.pageSize,
|
name,
|
||||||
orderBy: params.field || sorter.field,
|
description,
|
||||||
ascending: params.order === 'ascend',
|
attributes,
|
||||||
...(params?.search!==''?{searchQuery:params.search}:{})
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
avatar_url,
|
||||||
|
team_membership!inner (
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
role,
|
||||||
|
is_creator,
|
||||||
|
users (
|
||||||
|
id,
|
||||||
|
email
|
||||||
|
)
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
filter: {
|
||||||
|
deleted_at: { is: null },
|
||||||
|
...(params.search ? { name: { ilike: `%${params.search}%` } } : {})
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
column: params.field || 'created_at',
|
||||||
|
ascending: params.order === 'ascend'
|
||||||
|
},
|
||||||
|
page: params.current || 1,
|
||||||
|
pageSize: params.pageSize || 10
|
||||||
});
|
});
|
||||||
setTeams(data);
|
return result;
|
||||||
return { data, total };
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error('获取团队列表失败:', error);
|
||||||
message.error('获取团队列表失败');
|
throw error;
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
}, [pagination.current, pagination.pageSize, sorter.field, sorter.order]);
|
};
|
||||||
|
|
||||||
|
// 创建团队
|
||||||
const createTeam = async (values) => {
|
const createTeam = async (values) => {
|
||||||
try {
|
try {
|
||||||
const newTeam = await teamService.createTeam(values);
|
const newTeam = await supabaseService.insert('teams', {
|
||||||
setTeams(prev => [...prev, newTeam]);
|
name: values.name,
|
||||||
return newTeam;
|
description: values.description
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建团队成员关系
|
||||||
|
await supabaseService.insert('team_membership', {
|
||||||
|
team_id: newTeam[0].id,
|
||||||
|
user_id: values.userId,
|
||||||
|
role: 'OWNER',
|
||||||
|
is_creator: true,
|
||||||
|
id: newTeam[0].id
|
||||||
|
});
|
||||||
|
|
||||||
|
return newTeam[0];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('Failed to create team');
|
console.error('创建团队失败:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 更新团队
|
||||||
const updateTeam = async (id, values) => {
|
const updateTeam = async (id, values) => {
|
||||||
try {
|
try {
|
||||||
const updatedTeam = await teamService.updateTeam(id, values);
|
const result = await supabaseService.update('teams',
|
||||||
setTeams(prev => prev.map(team =>
|
{ id },
|
||||||
team.id === id ? updatedTeam : team
|
values
|
||||||
));
|
);
|
||||||
return updatedTeam;
|
return result[0];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('Failed to update team');
|
console.error('更新团队失败:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 删除团队
|
||||||
const deleteTeam = async (id) => {
|
const deleteTeam = async (id) => {
|
||||||
try {
|
try {
|
||||||
await teamService.deleteTeam(id);
|
await supabaseService.delete('teams', { id });
|
||||||
setTeams(prev => prev.filter(team => team.id !== id));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('删除团队失败:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
teams,
|
|
||||||
loading,
|
|
||||||
fetchTeams,
|
fetchTeams,
|
||||||
createTeam,
|
createTeam,
|
||||||
updateTeam,
|
updateTeam,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Form, Input, Button, Divider, message } from "antd";
|
import { Form, Input, Button, Divider, message } from "antd";
|
||||||
import { GoogleOutlined } from "@ant-design/icons";
|
import { GoogleOutlined, MailOutlined, LockOutlined } from "@ant-design/icons";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
|
import { supabaseService } from "@/hooks/supabaseService";
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -10,92 +11,100 @@ const Login = () => {
|
|||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const handleLogin = async (values) => {
|
const handleLogin = async (values) => {
|
||||||
try {
|
login(values.email, values.password);
|
||||||
await login(values.email, values.password);
|
|
||||||
message.success("登录成功!");
|
|
||||||
navigate("/");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Login error:", error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGoogleLogin = async () => {
|
|
||||||
try {
|
|
||||||
await signInWithGoogle();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Google login error:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex bg-gradient-to-br from-[#f5f7fa] to-[#c3cfe2]">
|
<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-8 gap-16">
|
<div className="w-full max-w-[1200px] mx-auto flex p-4 md:p-0">
|
||||||
<div className="flex-1 bg-white p-12 rounded-[20px] shadow-[0_10px_30px_rgba(0,0,0,0.1)]">
|
{/* 左侧登录表单 */}
|
||||||
<div className="mb-8 text-center">
|
<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">
|
||||||
<h1 className="text-3xl font-bold mb-2 bg-gradient-to-r from-primary-500 to-[#36cff0] bg-clip-text text-transparent">
|
<div className="mb-10 text-center">
|
||||||
Uppeta
|
<h1 className="text-4xl font-bold mb-3 bg-gradient-to-r from-blue-600 to-cyan-500 bg-clip-text text-transparent">
|
||||||
</h1>
|
Uppeta
|
||||||
<p className="text-gray-500">欢迎回来!请登录您的账户。</p>
|
</h1>
|
||||||
</div>
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
|
欢迎回来!请登录您的账户
|
||||||
<Form
|
</p>
|
||||||
form={form}
|
|
||||||
name="login"
|
|
||||||
onFinish={handleLogin}
|
|
||||||
layout="vertical"
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
name="email"
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: "请输入邮箱!" },
|
|
||||||
{ type: "email", message: "请输入有效的邮箱地址!" },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input placeholder="邮箱" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="password"
|
|
||||||
rules={[{ required: true, message: "请输入密码!" }]}
|
|
||||||
>
|
|
||||||
<Input.Password placeholder="密码" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center mb-6">
|
|
||||||
<Form.Item name="remember" valuePropName="checked" noStyle>
|
|
||||||
<Link
|
|
||||||
to="/forgot-password"
|
|
||||||
className="text-primary-500 hover:text-primary-600"
|
|
||||||
>
|
|
||||||
忘记密码?
|
|
||||||
</Link>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Form.Item>
|
|
||||||
<Button type="primary" htmlType="submit" block>
|
|
||||||
登录
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Divider>或</Divider>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
icon={<GoogleOutlined />}
|
|
||||||
block
|
|
||||||
onClick={handleGoogleLogin}
|
|
||||||
className="mb-6"
|
|
||||||
loading={loading}
|
|
||||||
>
|
|
||||||
使用 Google 账号登录
|
|
||||||
</Button>
|
|
||||||
</Form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 hidden md:block rounded-[20px] bg-[url('https://uppeta.com/img/svg/main.svg')] bg-center bg-contain bg-no-repeat" />
|
<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>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
block
|
||||||
|
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={loading}
|
||||||
|
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 transform hover:scale-105 transition-all duration-500" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -67,22 +67,7 @@ export const TeamForm = ({ form }) => {
|
|||||||
</Upload>
|
</Upload>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="tags"
|
|
||||||
label="标签"
|
|
||||||
rules={[{ required: true, message: '请选择至少一个标签' }]}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
mode="tags"
|
|
||||||
placeholder="请输入或选择标签"
|
|
||||||
options={[
|
|
||||||
{ label: '研发', value: 'development' },
|
|
||||||
{ label: '设计', value: 'design' },
|
|
||||||
{ label: '运营', value: 'operation' },
|
|
||||||
{ label: '市场', value: 'marketing' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,24 +1,72 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Modal, Form, Input, Select, message } from 'antd';
|
import { Modal,Button, Form, Input, Select, message } from 'antd';
|
||||||
import { MembershipTable } from './MembershipTable';
|
import { MembershipTable } from './MembershipTable';
|
||||||
|
import { supabaseService } from '@/hooks/supabaseService';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
export const ExpandedMemberships = ({ teamId, memberships: initialMemberships }) => {
|
export const ExpandedMemberships = ({ teamId }) => {
|
||||||
const [memberships, setMemberships] = useState(initialMemberships);
|
const [memberships, setMemberships] = useState([]);
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleUpdate = (id, values) => {
|
// 加载团队成员
|
||||||
setMemberships(prev =>
|
const loadMemberships = async () => {
|
||||||
prev.map(item => item.id === id ? { ...item, ...values } : item)
|
try {
|
||||||
);
|
setLoading(true);
|
||||||
message.success('成员信息已更新');
|
const { data } = await supabaseService.get('team_membership', {
|
||||||
|
select: `
|
||||||
|
id,
|
||||||
|
role,
|
||||||
|
is_creator,
|
||||||
|
user:users (
|
||||||
|
id,
|
||||||
|
email
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
match: { team_id: teamId }
|
||||||
|
});
|
||||||
|
setMemberships(data || []);
|
||||||
|
} catch (error) {
|
||||||
|
message.error('获取团队成员失败');
|
||||||
|
console.error('Failed to fetch memberships:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (id) => {
|
useEffect(() => {
|
||||||
setMemberships(prev => prev.filter(item => item.id !== id));
|
if (teamId) {
|
||||||
message.success('成员已删除');
|
loadMemberships();
|
||||||
|
}
|
||||||
|
}, [teamId]);
|
||||||
|
|
||||||
|
const handleUpdate = async (id, values) => {
|
||||||
|
try {
|
||||||
|
await supabaseService.update('team_membership',
|
||||||
|
{ id },
|
||||||
|
{
|
||||||
|
role: values.role
|
||||||
|
}
|
||||||
|
);
|
||||||
|
message.success('成员角色已更新');
|
||||||
|
await loadMemberships();
|
||||||
|
} catch (error) {
|
||||||
|
message.error('更新成员角色失败');
|
||||||
|
console.error('Update failed:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id) => {
|
||||||
|
try {
|
||||||
|
await supabaseService.delete('team_membership', { id });
|
||||||
|
message.success('成员已删除');
|
||||||
|
await loadMemberships();
|
||||||
|
} catch (error) {
|
||||||
|
message.error('删除成员失败');
|
||||||
|
console.error('Delete failed:', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
@@ -29,16 +77,18 @@ export const ExpandedMemberships = ({ teamId, memberships: initialMemberships })
|
|||||||
const handleModalOk = async () => {
|
const handleModalOk = async () => {
|
||||||
try {
|
try {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
const newMember = {
|
await supabaseService.insert('team_membership', {
|
||||||
id: `${Date.now()}`,
|
team_id: teamId,
|
||||||
teamId,
|
user_id: values.user_id,
|
||||||
isCreator: false,
|
role: values.role,
|
||||||
...values,
|
is_creator: false
|
||||||
};
|
});
|
||||||
setMemberships(prev => [...prev, newMember]);
|
|
||||||
setIsModalVisible(false);
|
setIsModalVisible(false);
|
||||||
message.success('成员已添加');
|
message.success('成员已添加');
|
||||||
|
await loadMemberships();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
message.error('添加成员失败');
|
||||||
console.error('Add failed:', error);
|
console.error('Add failed:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -46,53 +96,14 @@ export const ExpandedMemberships = ({ teamId, memberships: initialMemberships })
|
|||||||
return (
|
return (
|
||||||
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
|
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
|
||||||
<h3 className="text-lg font-medium mb-4">团队成员</h3>
|
<h3 className="text-lg font-medium mb-4">团队成员</h3>
|
||||||
|
|
||||||
<MembershipTable
|
<MembershipTable
|
||||||
|
loading={loading}
|
||||||
memberships={memberships}
|
memberships={memberships}
|
||||||
onUpdate={handleUpdate}
|
onUpdate={handleUpdate}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onAdd={handleAdd}
|
onAdd={handleAdd}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
|
||||||
title="添加成员"
|
|
||||||
open={isModalVisible}
|
|
||||||
onOk={handleModalOk}
|
|
||||||
onCancel={() => setIsModalVisible(false)}
|
|
||||||
>
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
layout="vertical"
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
name={['user', 'name']}
|
|
||||||
label="姓名"
|
|
||||||
rules={[{ required: true, message: '请输入姓名!' }]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={['user', 'email']}
|
|
||||||
label="邮箱"
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: '请输入邮箱!' },
|
|
||||||
{ type: 'email', message: '请输入有效的邮箱!' }
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name="role"
|
|
||||||
label="角色"
|
|
||||||
rules={[{ required: true, message: '请选择角色!' }]}
|
|
||||||
>
|
|
||||||
<Select>
|
|
||||||
<Option value="ADMIN">Admin</Option>
|
|
||||||
<Option value="MEMBER">Member</Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -32,13 +32,13 @@ export const MembershipTable = ({ memberships, onUpdate, onDelete, onAdd }) => {
|
|||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: '成员',
|
title: '成员',
|
||||||
dataIndex: 'users',
|
dataIndex: 'user',
|
||||||
key: 'users',
|
key: 'user',
|
||||||
editable: true,
|
editable: true,
|
||||||
render: (users) => (
|
render: (user) => (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-medium">{users.email}</span>
|
<span className="font-medium">{user?.email}</span>
|
||||||
<span className="text-gray-500 text-sm">{users.email}</span>
|
<span className="text-gray-500 text-sm">{user?.email}</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -139,14 +139,14 @@ export const MembershipTable = ({ memberships, onUpdate, onDelete, onAdd }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Button
|
{/* <Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={onAdd}
|
onClick={onAdd}
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
>
|
>
|
||||||
添加成员
|
添加成员
|
||||||
</Button>
|
</Button> */}
|
||||||
<Form form={form} component={false}>
|
<Form form={form} component={false}>
|
||||||
<Table
|
<Table
|
||||||
components={{
|
components={{
|
||||||
|
|||||||
@@ -3,13 +3,6 @@ import { Form, Input, Space } from 'antd';
|
|||||||
|
|
||||||
export const UserForm = ({ nameProps, emailProps }) => (
|
export const UserForm = ({ nameProps, emailProps }) => (
|
||||||
<Space.Compact block>
|
<Space.Compact block>
|
||||||
<Form.Item
|
|
||||||
{...nameProps}
|
|
||||||
style={{ margin: 0, width: '50%' }}
|
|
||||||
rules={[{ required: true, message: '请输入姓名!' }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="姓名" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
{...emailProps}
|
{...emailProps}
|
||||||
style={{ margin: 0, width: '50%' }}
|
style={{ margin: 0, width: '50%' }}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Card, App } from 'antd';
|
import { Card, App, message } from 'antd';
|
||||||
import { TeamHeader } from './components/TeamHeader';
|
import { TeamHeader } from './components/TeamHeader';
|
||||||
import { TeamTable } from './components/TeamTable';
|
import { TeamTable } from './components/TeamTable';
|
||||||
import CreateTeamModal from './components/CreateTeamModal';
|
import CreateTeamModal from './components/CreateTeamModal';
|
||||||
import { useTeams } from '@/hooks/team/useTeams';
|
import { useTeams } from '@/hooks/team/useTeams';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
|
import { supabaseService } from '@/hooks/supabaseService';
|
||||||
|
|
||||||
const TeamManagement = () => {
|
const TeamManagement = () => {
|
||||||
|
const [teams, setTeams] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState({
|
||||||
current: 1,
|
current: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
@@ -19,25 +22,96 @@ const TeamManagement = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
teams,
|
|
||||||
loading,
|
|
||||||
fetchTeams,
|
fetchTeams,
|
||||||
createTeam,
|
createTeam,
|
||||||
updateTeam,
|
updateTeam,
|
||||||
deleteTeam
|
deleteTeam
|
||||||
} = useTeams(pagination, sorter);
|
} = useTeams();
|
||||||
|
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
useEffect(() => {
|
// 加载团队列表
|
||||||
const loadTeams = async () => {
|
const loadTeams = async (params = {}) => {
|
||||||
const { total } = await fetchTeams();
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const {data,total} = await fetchTeams({
|
||||||
|
current: params.current || pagination.current,
|
||||||
|
pageSize: params.pageSize || pagination.pageSize,
|
||||||
|
field: params.field || sorter.field,
|
||||||
|
order: params.order || sorter.order,
|
||||||
|
search: params.search
|
||||||
|
});
|
||||||
|
console.log(data,'data');
|
||||||
|
|
||||||
|
setTeams(data);
|
||||||
setPagination(prev => ({ ...prev, total }));
|
setPagination(prev => ({ ...prev, total }));
|
||||||
};
|
} catch (error) {
|
||||||
|
message.error('获取团队列表失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取团队成员
|
||||||
|
const getTeamMembers = async (teamId) => {
|
||||||
|
try {
|
||||||
|
const result = await supabaseService.get('team_memberships', {
|
||||||
|
select: '*',
|
||||||
|
relations: {
|
||||||
|
user: 'id, email, name'
|
||||||
|
},
|
||||||
|
match: { teamId }
|
||||||
|
});
|
||||||
|
return result.data;
|
||||||
|
} catch (error) {
|
||||||
|
message.error('获取团队成员失败');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加团队成员
|
||||||
|
const addTeamMember = async (teamId, userData) => {
|
||||||
|
try {
|
||||||
|
const result = await supabaseService.insert('team_memberships', {
|
||||||
|
teamId,
|
||||||
|
...userData
|
||||||
|
});
|
||||||
|
return result[0];
|
||||||
|
} catch (error) {
|
||||||
|
message.error('添加团队成员失败');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新团队成员
|
||||||
|
const updateTeamMember = async (id, values) => {
|
||||||
|
try {
|
||||||
|
const result = await supabaseService.update('team_memberships',
|
||||||
|
{ id },
|
||||||
|
values
|
||||||
|
);
|
||||||
|
return result[0];
|
||||||
|
} catch (error) {
|
||||||
|
message.error('更新团队成员失败');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除团队成员
|
||||||
|
const deleteTeamMember = async (id) => {
|
||||||
|
try {
|
||||||
|
await supabaseService.delete('team_memberships', { id });
|
||||||
|
} catch (error) {
|
||||||
|
message.error('删除团队成员失败');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
loadTeams();
|
loadTeams();
|
||||||
}, [fetchTeams]);
|
}, []);
|
||||||
|
|
||||||
const handleTableChange = (newPagination, filters, newSorter) => {
|
const handleTableChange = (newPagination, filters, newSorter) => {
|
||||||
const params = {
|
const params = {
|
||||||
@@ -58,9 +132,7 @@ const TeamManagement = () => {
|
|||||||
order: params.order || sorter.order,
|
order: params.order || sorter.order,
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchTeams(params).then(({ total }) => {
|
loadTeams(params);
|
||||||
setPagination(prev => ({ ...prev, total }));
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
@@ -77,28 +149,54 @@ const TeamManagement = () => {
|
|||||||
await createTeam({ ...values, userId: user.id });
|
await createTeam({ ...values, userId: user.id });
|
||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
setPagination(prev => ({ ...prev, current: 1 }));
|
setPagination(prev => ({ ...prev, current: 1 }));
|
||||||
const { total } = await fetchTeams({ current: 1 });
|
await loadTeams({ current: 1 });
|
||||||
setPagination(prev => ({ ...prev, total }));
|
message.success('创建团队成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create team:', error);
|
message.error('创建团队失败');
|
||||||
} finally {
|
} finally {
|
||||||
setConfirmLoading(false);
|
setConfirmLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onSearch=(value)=>{
|
|
||||||
fetchTeams({search:value})
|
const handleUpdateTeam = async (id, values) => {
|
||||||
}
|
try {
|
||||||
|
await updateTeam(id, values);
|
||||||
|
await loadTeams();
|
||||||
|
message.success('更新团队成功');
|
||||||
|
} catch (error) {
|
||||||
|
message.error('更新团队失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteTeam = async (id) => {
|
||||||
|
try {
|
||||||
|
await deleteTeam(id);
|
||||||
|
await loadTeams();
|
||||||
|
message.success('删除团队成功');
|
||||||
|
} catch (error) {
|
||||||
|
message.error('删除团队失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = (value) => {
|
||||||
|
loadTeams({ search: value, current: 1 });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<App>
|
<App>
|
||||||
<Card title="团队管理" bordered={false}>
|
<Card title="团队管理" bordered={false}>
|
||||||
<TeamHeader onSearch={onSearch} onAdd={handleAdd} />
|
<TeamHeader onSearch={handleSearch} onAdd={handleAdd} />
|
||||||
<TeamTable
|
<TeamTable
|
||||||
tableLoading={loading}
|
loading={loading}
|
||||||
dataSource={teams}
|
dataSource={teams}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onTableChange={handleTableChange}
|
onTableChange={handleTableChange}
|
||||||
onUpdate={updateTeam}
|
onUpdate={handleUpdateTeam}
|
||||||
onDelete={deleteTeam}
|
onDelete={handleDeleteTeam}
|
||||||
|
onGetMembers={getTeamMembers}
|
||||||
|
onAddMember={addTeamMember}
|
||||||
|
onUpdateMember={updateTeamMember}
|
||||||
|
onDeleteMember={deleteTeamMember}
|
||||||
/>
|
/>
|
||||||
<CreateTeamModal
|
<CreateTeamModal
|
||||||
open={modalVisible}
|
open={modalVisible}
|
||||||
|
|||||||
@@ -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 '@/config/routes';
|
import { routes } 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';
|
||||||
|
|||||||
@@ -1,169 +0,0 @@
|
|||||||
import { supabase } from '@/config/supabase';
|
|
||||||
|
|
||||||
export const teamService = {
|
|
||||||
async getTeams({ page = 1, pageSize = 10, orderBy = 'created_at', ascending = false , searchQuery = ''
|
|
||||||
} = {}) {
|
|
||||||
const from = (page - 1) * pageSize;
|
|
||||||
const to = from + pageSize - 1;
|
|
||||||
|
|
||||||
let query = supabase
|
|
||||||
.from('teams')
|
|
||||||
.select(`
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
attributes,
|
|
||||||
created_at,
|
|
||||||
updated_at,
|
|
||||||
deleted_at,
|
|
||||||
schema_version,
|
|
||||||
avatar_url,
|
|
||||||
team_membership(
|
|
||||||
id,
|
|
||||||
user_id,
|
|
||||||
role,
|
|
||||||
is_creator,
|
|
||||||
users(
|
|
||||||
id,
|
|
||||||
email
|
|
||||||
)
|
|
||||||
)
|
|
||||||
`, { count: 'exact' })
|
|
||||||
.is('deleted_at', null)
|
|
||||||
.order(orderBy, { ascending })
|
|
||||||
.range(from, to);
|
|
||||||
|
|
||||||
if (searchQuery) {
|
|
||||||
query = query.ilike('name', `%${searchQuery}%`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data, error, count } = await query
|
|
||||||
.order(orderBy, { ascending })
|
|
||||||
.range(from, to);
|
|
||||||
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error fetching teams:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
total: count || 0
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
// 创建团队
|
|
||||||
async createTeam({ name, description, userId }) {
|
|
||||||
const { data: team, error: teamError } = await supabase
|
|
||||||
.from('teams')
|
|
||||||
.insert([
|
|
||||||
{
|
|
||||||
name,
|
|
||||||
description
|
|
||||||
}
|
|
||||||
])
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (teamError) throw teamError;
|
|
||||||
|
|
||||||
// 创建团队成员关系(创建者)
|
|
||||||
const { error: membershipError } = await supabase
|
|
||||||
.from('team_membership')
|
|
||||||
.insert([
|
|
||||||
{
|
|
||||||
id:team.id,
|
|
||||||
team_id: team.id,
|
|
||||||
user_id: userId,
|
|
||||||
role: 'OWNER',
|
|
||||||
is_creator: true
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (membershipError) throw membershipError;
|
|
||||||
|
|
||||||
return team;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 获取单个团队详情
|
|
||||||
async getTeamById(teamId) {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('teams')
|
|
||||||
.select(`
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
attributes,
|
|
||||||
created_at,
|
|
||||||
updated_at,
|
|
||||||
avatar_url,
|
|
||||||
team_membership(
|
|
||||||
id,
|
|
||||||
user_id,
|
|
||||||
role,
|
|
||||||
is_creator,
|
|
||||||
users(
|
|
||||||
id,
|
|
||||||
email
|
|
||||||
)
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
.eq('id', teamId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 更新团队信息
|
|
||||||
async updateTeam(teamId, updates) {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('teams')
|
|
||||||
.update(updates)
|
|
||||||
.eq('id', teamId)
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 添加团队成员
|
|
||||||
async addTeamMember(teamId, userId, role = 'MEMBER') {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('team_membership')
|
|
||||||
.insert([
|
|
||||||
{
|
|
||||||
team_id: teamId,
|
|
||||||
user_id: userId,
|
|
||||||
role,
|
|
||||||
is_creator: false
|
|
||||||
}
|
|
||||||
])
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 更新成员角色
|
|
||||||
async updateMemberRole(teamId, userId, role) {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('team_membership')
|
|
||||||
.update({ role })
|
|
||||||
.match({ team_id: teamId, user_id: userId })
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
},
|
|
||||||
async deleteTeam(teamId) {
|
|
||||||
const { error: teamError } = await supabase
|
|
||||||
.from('teams')
|
|
||||||
.delete()
|
|
||||||
.eq('id', teamId)
|
|
||||||
.select()
|
|
||||||
if (teamError) throw teamError;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { supabase } from '@/config/supabase';
|
|
||||||
|
|
||||||
export const teamMembershipService = {
|
|
||||||
async getMemberships(teamId) {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('team_memberships')
|
|
||||||
.select(`
|
|
||||||
*,
|
|
||||||
user:users(id, email, name)
|
|
||||||
`)
|
|
||||||
.eq('teamId', teamId);
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
|
|
||||||
async createMembership(membershipData) {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('team_memberships')
|
|
||||||
.insert([membershipData])
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
|
|
||||||
async updateMembership(id, membershipData) {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('team_memberships')
|
|
||||||
.update(membershipData)
|
|
||||||
.eq('id', id)
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
|
|
||||||
async deleteMembership(id) {
|
|
||||||
const { error } = await supabase
|
|
||||||
.from('team_memberships')
|
|
||||||
.delete()
|
|
||||||
.eq('id', id);
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { routes } from '@/config/routes';
|
import { routes } from '@/routes/routes';
|
||||||
import * as AntIcons from '@ant-design/icons';
|
import * as AntIcons from '@ant-design/icons';
|
||||||
import { ColorIcon } from '@/components/common/ColorIcon';
|
import { ColorIcon } from '@/components/common/ColorIcon';
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es2015",
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["src/*"],
|
|
||||||
"@components/*": ["src/components/*"],
|
|
||||||
"@pages/*": ["src/pages/*"],
|
|
||||||
"@utils/*": ["src/utils/*"],
|
|
||||||
"@config/*": ["src/config/*"],
|
|
||||||
"@contexts/*": ["src/contexts/*"],
|
|
||||||
"@assets/*": ["src/assets/*"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user