页面登录优化,team优化
This commit is contained in:
@@ -21,7 +21,7 @@ export const createSupabase = () => {
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
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();
|
||||
|
||||
export const ThemeProvider = ({ children }) => {
|
||||
@@ -19,7 +19,52 @@ export const ThemeProvider = ({ children }) => {
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
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 { message } from 'antd';
|
||||
import { teamMembershipService } from '@/services/supabase/teamMembership';
|
||||
import { supabaseService } from '@/hooks/supabaseService';
|
||||
|
||||
export const useTeamMemberships = (teamId) => {
|
||||
const [memberships, setMemberships] = useState([]);
|
||||
export const useTeamMembership = (teamId) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [memberships, setMemberships] = useState([]);
|
||||
|
||||
const fetchMemberships = useCallback(async () => {
|
||||
const loadMemberships = useCallback(async () => {
|
||||
if (!teamId) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await teamMembershipService.getMemberships(teamId);
|
||||
setMemberships(data);
|
||||
const result = await supabaseService.get('team_memberships', {
|
||||
select: '*',
|
||||
relations: {
|
||||
user: 'id, email, name'
|
||||
},
|
||||
match: { teamId }
|
||||
});
|
||||
setMemberships(result.data || []);
|
||||
} catch (error) {
|
||||
message.error('Failed to fetch team memberships');
|
||||
console.error(error);
|
||||
console.error('获取成员列表失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [teamId]);
|
||||
|
||||
const createMembership = async (values) => {
|
||||
try {
|
||||
const newMembership = await teamMembershipService.createMembership({
|
||||
...values,
|
||||
teamId,
|
||||
});
|
||||
setMemberships(prev => [...prev, newMembership]);
|
||||
message.success('Member added successfully');
|
||||
return newMembership;
|
||||
} catch (error) {
|
||||
message.error('Failed to add member');
|
||||
throw error;
|
||||
}
|
||||
const addMembership = async (values) => {
|
||||
const result = await supabaseService.insert('team_memberships', {
|
||||
...values,
|
||||
teamId
|
||||
});
|
||||
await loadMemberships();
|
||||
return result[0];
|
||||
};
|
||||
|
||||
const updateMembership = async (id, values) => {
|
||||
try {
|
||||
const updatedMembership = await teamMembershipService.updateMembership(id, values);
|
||||
setMemberships(prev => prev.map(membership =>
|
||||
membership.id === id ? updatedMembership : membership
|
||||
));
|
||||
message.success('Member updated successfully');
|
||||
return updatedMembership;
|
||||
} catch (error) {
|
||||
message.error('Failed to update member');
|
||||
throw error;
|
||||
}
|
||||
const result = await supabaseService.update('team_memberships',
|
||||
{ id },
|
||||
values
|
||||
);
|
||||
await loadMemberships();
|
||||
return result[0];
|
||||
};
|
||||
|
||||
const deleteMembership = async (id) => {
|
||||
try {
|
||||
await teamMembershipService.deleteMembership(id);
|
||||
setMemberships(prev => prev.filter(membership => membership.id !== id));
|
||||
message.success('Member removed successfully');
|
||||
} catch (error) {
|
||||
message.error('Failed to remove member');
|
||||
throw error;
|
||||
}
|
||||
await supabaseService.delete('team_memberships', { id });
|
||||
await loadMemberships();
|
||||
};
|
||||
|
||||
return {
|
||||
memberships,
|
||||
loading,
|
||||
fetchMemberships,
|
||||
createMembership,
|
||||
memberships,
|
||||
loadMemberships,
|
||||
addMembership,
|
||||
updateMembership,
|
||||
deleteMembership,
|
||||
};
|
||||
|
||||
@@ -1,68 +1,96 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { message } from 'antd';
|
||||
import { teamService } from '@/services/supabase/team';
|
||||
import { supabaseService } from '@/hooks/supabaseService';
|
||||
|
||||
export const useTeams = (pagination, sorter) => {
|
||||
const [teams, setTeams] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchTeams = useCallback(async (params = {}) => {
|
||||
export const useTeams = () => {
|
||||
// 获取团队列表
|
||||
const fetchTeams = async (params = {}) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data, total } = await teamService.getTeams({
|
||||
page: params.current || pagination.current,
|
||||
pageSize: params.pageSize || pagination.pageSize,
|
||||
orderBy: params.field || sorter.field,
|
||||
ascending: params.order === 'ascend',
|
||||
...(params?.search!==''?{searchQuery:params.search}:{})
|
||||
const result = await supabaseService.get('teams', {
|
||||
select: `
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
attributes,
|
||||
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 { data, total };
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error('获取团队列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
console.error('获取团队列表失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}, [pagination.current, pagination.pageSize, sorter.field, sorter.order]);
|
||||
};
|
||||
|
||||
// 创建团队
|
||||
const createTeam = async (values) => {
|
||||
try {
|
||||
const newTeam = await teamService.createTeam(values);
|
||||
setTeams(prev => [...prev, newTeam]);
|
||||
return newTeam;
|
||||
const newTeam = await supabaseService.insert('teams', {
|
||||
name: values.name,
|
||||
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) {
|
||||
message.error('Failed to create team');
|
||||
console.error('创建团队失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 更新团队
|
||||
const updateTeam = async (id, values) => {
|
||||
try {
|
||||
const updatedTeam = await teamService.updateTeam(id, values);
|
||||
setTeams(prev => prev.map(team =>
|
||||
team.id === id ? updatedTeam : team
|
||||
));
|
||||
return updatedTeam;
|
||||
const result = await supabaseService.update('teams',
|
||||
{ id },
|
||||
values
|
||||
);
|
||||
return result[0];
|
||||
} catch (error) {
|
||||
message.error('Failed to update team');
|
||||
console.error('更新团队失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 删除团队
|
||||
const deleteTeam = async (id) => {
|
||||
try {
|
||||
await teamService.deleteTeam(id);
|
||||
setTeams(prev => prev.filter(team => team.id !== id));
|
||||
await supabaseService.delete('teams', { id });
|
||||
} catch (error) {
|
||||
console.error('删除团队失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
teams,
|
||||
loading,
|
||||
fetchTeams,
|
||||
createTeam,
|
||||
updateTeam,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from "react";
|
||||
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 { useAuth } from "@/contexts/AuthContext";
|
||||
import { supabaseService } from "@/hooks/supabaseService";
|
||||
|
||||
const Login = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -10,92 +11,100 @@ const Login = () => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleLogin = async (values) => {
|
||||
try {
|
||||
await login(values.email, values.password);
|
||||
message.success("登录成功!");
|
||||
navigate("/");
|
||||
} catch (error) {
|
||||
console.error("Login error:", error);
|
||||
}
|
||||
login(values.email, values.password);
|
||||
};
|
||||
|
||||
const handleGoogleLogin = async () => {
|
||||
try {
|
||||
await signInWithGoogle();
|
||||
} catch (error) {
|
||||
console.error("Google login error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex bg-gradient-to-br from-[#f5f7fa] to-[#c3cfe2]">
|
||||
<div className="w-full max-w-[1200px] mx-auto flex p-8 gap-16">
|
||||
<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">
|
||||
<h1 className="text-3xl font-bold mb-2 bg-gradient-to-r from-primary-500 to-[#36cff0] bg-clip-text text-transparent">
|
||||
Uppeta
|
||||
</h1>
|
||||
<p className="text-gray-500">欢迎回来!请登录您的账户。</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 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 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 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">
|
||||
<h1 className="text-4xl font-bold mb-3 bg-gradient-to-r from-blue-600 to-cyan-500 bg-clip-text text-transparent">
|
||||
Uppeta
|
||||
</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
欢迎回来!请登录您的账户
|
||||
</p>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -67,22 +67,7 @@ export const TeamForm = ({ form }) => {
|
||||
</Upload>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -1,24 +1,72 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Modal, Form, Input, Select, message } from 'antd';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal,Button, Form, Input, Select, message } from 'antd';
|
||||
import { MembershipTable } from './MembershipTable';
|
||||
import { supabaseService } from '@/hooks/supabaseService';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
export const ExpandedMemberships = ({ teamId, memberships: initialMemberships }) => {
|
||||
const [memberships, setMemberships] = useState(initialMemberships);
|
||||
export const ExpandedMemberships = ({ teamId }) => {
|
||||
const [memberships, setMemberships] = useState([]);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleUpdate = (id, values) => {
|
||||
setMemberships(prev =>
|
||||
prev.map(item => item.id === id ? { ...item, ...values } : item)
|
||||
);
|
||||
message.success('成员信息已更新');
|
||||
// 加载团队成员
|
||||
const loadMemberships = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
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) => {
|
||||
setMemberships(prev => prev.filter(item => item.id !== id));
|
||||
message.success('成员已删除');
|
||||
useEffect(() => {
|
||||
if (teamId) {
|
||||
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 = () => {
|
||||
@@ -29,16 +77,18 @@ export const ExpandedMemberships = ({ teamId, memberships: initialMemberships })
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const newMember = {
|
||||
id: `${Date.now()}`,
|
||||
teamId,
|
||||
isCreator: false,
|
||||
...values,
|
||||
};
|
||||
setMemberships(prev => [...prev, newMember]);
|
||||
await supabaseService.insert('team_membership', {
|
||||
team_id: teamId,
|
||||
user_id: values.user_id,
|
||||
role: values.role,
|
||||
is_creator: false
|
||||
});
|
||||
|
||||
setIsModalVisible(false);
|
||||
message.success('成员已添加');
|
||||
await loadMemberships();
|
||||
} catch (error) {
|
||||
message.error('添加成员失败');
|
||||
console.error('Add failed:', error);
|
||||
}
|
||||
};
|
||||
@@ -46,53 +96,14 @@ export const ExpandedMemberships = ({ teamId, memberships: initialMemberships })
|
||||
return (
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
|
||||
<h3 className="text-lg font-medium mb-4">团队成员</h3>
|
||||
|
||||
<MembershipTable
|
||||
loading={loading}
|
||||
memberships={memberships}
|
||||
onUpdate={handleUpdate}
|
||||
onDelete={handleDelete}
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -32,13 +32,13 @@ export const MembershipTable = ({ memberships, onUpdate, onDelete, onAdd }) => {
|
||||
const columns = [
|
||||
{
|
||||
title: '成员',
|
||||
dataIndex: 'users',
|
||||
key: 'users',
|
||||
dataIndex: 'user',
|
||||
key: 'user',
|
||||
editable: true,
|
||||
render: (users) => (
|
||||
render: (user) => (
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{users.email}</span>
|
||||
<span className="text-gray-500 text-sm">{users.email}</span>
|
||||
<span className="font-medium">{user?.email}</span>
|
||||
<span className="text-gray-500 text-sm">{user?.email}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -139,14 +139,14 @@ export const MembershipTable = ({ memberships, onUpdate, onDelete, onAdd }) => {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Button
|
||||
{/* <Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={onAdd}
|
||||
className="mb-4"
|
||||
>
|
||||
添加成员
|
||||
</Button>
|
||||
</Button> */}
|
||||
<Form form={form} component={false}>
|
||||
<Table
|
||||
components={{
|
||||
|
||||
@@ -3,13 +3,6 @@ import { Form, Input, Space } from 'antd';
|
||||
|
||||
export const UserForm = ({ nameProps, emailProps }) => (
|
||||
<Space.Compact block>
|
||||
<Form.Item
|
||||
{...nameProps}
|
||||
style={{ margin: 0, width: '50%' }}
|
||||
rules={[{ required: true, message: '请输入姓名!' }]}
|
||||
>
|
||||
<Input placeholder="姓名" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...emailProps}
|
||||
style={{ margin: 0, width: '50%' }}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, App } from 'antd';
|
||||
import { Card, App, message } from 'antd';
|
||||
import { TeamHeader } from './components/TeamHeader';
|
||||
import { TeamTable } from './components/TeamTable';
|
||||
import CreateTeamModal from './components/CreateTeamModal';
|
||||
import { useTeams } from '@/hooks/team/useTeams';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { supabaseService } from '@/hooks/supabaseService';
|
||||
|
||||
const TeamManagement = () => {
|
||||
const [teams, setTeams] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [pagination, setPagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
@@ -19,25 +22,96 @@ const TeamManagement = () => {
|
||||
});
|
||||
|
||||
const {
|
||||
teams,
|
||||
loading,
|
||||
fetchTeams,
|
||||
createTeam,
|
||||
updateTeam,
|
||||
deleteTeam
|
||||
} = useTeams(pagination, sorter);
|
||||
} = useTeams();
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||
const { user } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
const loadTeams = async () => {
|
||||
const { total } = await fetchTeams();
|
||||
// 加载团队列表
|
||||
const loadTeams = async (params = {}) => {
|
||||
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 }));
|
||||
};
|
||||
} 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();
|
||||
}, [fetchTeams]);
|
||||
}, []);
|
||||
|
||||
const handleTableChange = (newPagination, filters, newSorter) => {
|
||||
const params = {
|
||||
@@ -58,9 +132,7 @@ const TeamManagement = () => {
|
||||
order: params.order || sorter.order,
|
||||
});
|
||||
|
||||
fetchTeams(params).then(({ total }) => {
|
||||
setPagination(prev => ({ ...prev, total }));
|
||||
});
|
||||
loadTeams(params);
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
@@ -77,28 +149,54 @@ const TeamManagement = () => {
|
||||
await createTeam({ ...values, userId: user.id });
|
||||
setModalVisible(false);
|
||||
setPagination(prev => ({ ...prev, current: 1 }));
|
||||
const { total } = await fetchTeams({ current: 1 });
|
||||
setPagination(prev => ({ ...prev, total }));
|
||||
await loadTeams({ current: 1 });
|
||||
message.success('创建团队成功');
|
||||
} catch (error) {
|
||||
console.error('Failed to create team:', error);
|
||||
message.error('创建团队失败');
|
||||
} finally {
|
||||
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 (
|
||||
<App>
|
||||
<Card title="团队管理" bordered={false}>
|
||||
<TeamHeader onSearch={onSearch} onAdd={handleAdd} />
|
||||
<TeamHeader onSearch={handleSearch} onAdd={handleAdd} />
|
||||
<TeamTable
|
||||
tableLoading={loading}
|
||||
loading={loading}
|
||||
dataSource={teams}
|
||||
pagination={pagination}
|
||||
onTableChange={handleTableChange}
|
||||
onUpdate={updateTeam}
|
||||
onDelete={deleteTeam}
|
||||
onUpdate={handleUpdateTeam}
|
||||
onDelete={handleDeleteTeam}
|
||||
onGetMembers={getTeamMembers}
|
||||
onAddMember={addTeamMember}
|
||||
onUpdateMember={updateTeamMember}
|
||||
onDeleteMember={deleteTeamMember}
|
||||
/>
|
||||
<CreateTeamModal
|
||||
open={modalVisible}
|
||||
|
||||
@@ -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 '@/config/routes';
|
||||
import { routes } from '@/routes/routes';
|
||||
import Login from '@/pages/auth/Login';
|
||||
import NotFound from '@/pages/notFound';
|
||||
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 { routes } from '@/config/routes';
|
||||
import { routes } from '@/routes/routes';
|
||||
import * as AntIcons from '@ant-design/icons';
|
||||
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