管理后台初始化,登录,团队管理,报价单管理 完成

This commit is contained in:
‘Liammcl’
2024-12-15 17:39:58 +08:00
commit 5882bf9548
91 changed files with 16260 additions and 0 deletions

23
src/pages/Dashboard.jsx Normal file
View File

@@ -0,0 +1,23 @@
import React from 'react';
import { Card } from 'antd';
import StatisticsOverview from '@/components/dashboard/StatisticsOverview';
const Dashboard = () => {
return (
<div className="space-y-6">
<Card
title="Statistics Overview"
bordered={false}
styles={{
body: {
padding: '24px',
}
}}
>
<StatisticsOverview />
</Card>
</div>
);
};
export default Dashboard;

24
src/pages/Profile.jsx Normal file
View File

@@ -0,0 +1,24 @@
import { Card, Avatar, Typography, Descriptions } from 'antd';
import { UserOutlined } from '@ant-design/icons';
const { Title } = Typography;
const Profile = () => {
return (
<Card>
<div style={{ textAlign: 'center', marginBottom: '24px' }}>
<Avatar size={64} icon={<UserOutlined />} />
<Title level={3} style={{ marginTop: '16px' }}>John Doe</Title>
</div>
<Descriptions bordered>
<Descriptions.Item label="Username">johndoe</Descriptions.Item>
<Descriptions.Item label="Email">john@example.com</Descriptions.Item>
<Descriptions.Item label="Role">Administrator</Descriptions.Item>
<Descriptions.Item label="Status">Active</Descriptions.Item>
<Descriptions.Item label="Join Date">2023-10-25</Descriptions.Item>
</Descriptions>
</Card>
);
};
export default Profile;

40
src/pages/Settings.jsx Normal file
View File

@@ -0,0 +1,40 @@
import { Form, Input, Button, Card, message } from 'antd';
const Settings = () => {
const [form] = Form.useForm();
const onFinish = (values) => {
console.log('Success:', values);
message.success('Settings updated successfully');
};
return (
<Card title="Settings">
<Form
form={form}
layout="vertical"
onFinish={onFinish}
initialValues={{
email: 'john@example.com',
notifications: true,
}}
>
<Form.Item
label="Email"
name="email"
rules={[{ required: true, type: 'email' }]}
>
<Input />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Save Changes
</Button>
</Form.Item>
</Form>
</Card>
);
};
export default Settings;

102
src/pages/auth/Login.jsx Normal file
View File

@@ -0,0 +1,102 @@
import React from 'react';
import { Form, Input, Button, Divider, message } from 'antd';
import { GoogleOutlined } from '@ant-design/icons';
import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
const Login = () => {
const navigate = useNavigate();
const { login, signInWithGoogle } = useAuth();
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);
}
};
const handleGoogleLogin = async () => {
try {
await signInWithGoogle();
navigate('/');
} 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"
>
使用 Google 账号登录
</Button>
</Form>
</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" />
</div>
</div>
);
};
export default Login;

120
src/pages/auth/Register.jsx Normal file
View File

@@ -0,0 +1,120 @@
import React from 'react';
import { Form, Input, Button, Divider, message } from 'antd';
import { GoogleOutlined } from '@ant-design/icons';
import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
const Register = () => {
const navigate = useNavigate();
const { register, signInWithGoogle } = useAuth();
const [form] = Form.useForm();
const handleRegister = async (values) => {
try {
await register(values.email, values.password);
message.success('注册成功!请查看您的邮箱进行验证。');
navigate('/login');
} catch (error) {
console.error('Registration error:', error);
}
};
const handleGoogleSignUp = async () => {
try {
await signInWithGoogle();
navigate('/');
} catch (error) {
console.error('Google sign up 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">
{/* Register Form */}
<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">
创建账户
</h1>
<p className="text-gray-500">加入 Uppeta开启您的管理之旅</p>
</div>
<Form
form={form}
name="register"
onFinish={handleRegister}
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: '请输入密码!' },
{ min: 6, message: '密码长度至少为 6 位!' }
]}
>
<Input.Password placeholder="密码" />
</Form.Item>
<Form.Item
name="confirmPassword"
dependencies={['password']}
rules={[
{ required: true, message: '请确认密码!' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('password') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('两次输入的密码不一致!'));
},
}),
]}
>
<Input.Password placeholder="确认密码" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" block>
注册
</Button>
</Form.Item>
<Divider></Divider>
<Button
icon={<GoogleOutlined />}
block
onClick={handleGoogleSignUp}
className="mb-6"
>
使用 Google 账号注册
</Button>
<div className="text-center">
<Link to="/login" className="text-primary-500 hover:text-primary-600">
已有账户立即登录
</Link>
</div>
</Form>
</div>
{/* Register Image */}
<div className="flex-1 hidden md:block rounded-[20px] bg-[url('/auth-bg.png')] bg-center bg-cover" />
</div>
</div>
);
};
export default Register;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { Card, Table, Button } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
const CustomerPage = () => {
const columns = [
{
title: '客户名称',
dataIndex: 'name',
key: 'name',
},
{
title: '联系人',
dataIndex: 'contact',
key: 'contact',
},
{
title: '电话',
dataIndex: 'phone',
key: 'phone',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
},
];
return (
<Card
title="客户管理"
extra={
<Button type="primary" icon={<PlusOutlined />}>
新增客户
</Button>
}
>
<Table columns={columns} dataSource={[]} rowKey="id" />
</Card>
);
};
export default CustomerPage;

View File

@@ -0,0 +1,8 @@
import React from 'react';
import { Outlet } from 'react-router-dom';
const CompanyManagement = () => {
return <Outlet />;
};
export default CompanyManagement;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { Card, Table, Button } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
const ProjectPage = () => {
const columns = [
{
title: '项目名称',
dataIndex: 'name',
key: 'name',
},
{
title: '负责人',
dataIndex: 'manager',
key: 'manager',
},
{
title: '开始日期',
dataIndex: 'startDate',
key: 'startDate',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
},
];
return (
<Card
title="专案管理"
extra={
<Button type="primary" icon={<PlusOutlined />}>
新增专案
</Button>
}
>
<Table columns={columns} dataSource={[]} rowKey="id" />
</Card>
);
};
export default ProjectPage;

View File

@@ -0,0 +1,208 @@
import React, { useEffect, useState } from 'react';
import { Card, Table, Button, Modal, Form, Input, message, Popconfirm } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import { useResources } from '@/hooks/resource/useResource';
const QuotationPage = () => {
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const [editingId, setEditingId] = useState(null);
const [pagination, setPagination] = useState({ current: 1, pageSize: 10 });
const [sorter, setSorter] = useState({ field: 'created_at', order: 'descend' });
const {
resources: quotations,
loading,
total,
fetchResources: fetchQuotations,
createResource: createQuotation,
updateResource: updateQuotation,
deleteResource: deleteQuotation
} = useResources(pagination, sorter);
useEffect(() => {
fetchQuotations();
}, []);
const handleTableChange = (pagination, filters, sorter) => {
setPagination(pagination);
setSorter(sorter);
fetchQuotations({
current: pagination.current,
pageSize: pagination.pageSize,
field: sorter.field,
order: sorter.order,
});
};
const handleSubmit = async (values) => {
try {
const quotationData = {
external_id: values.id,
attributes: {
customerName: values.customerName,
status: values.status || '新建',
},
type: 'quota'
};
if (editingId) {
await updateQuotation(editingId, quotationData);
} else {
await createQuotation(quotationData);
}
setVisible(false);
form.resetFields();
fetchQuotations();
} catch (error) {
console.error('提交失败:', error);
}
};
const handleDelete = async (id) => {
try {
await deleteQuotation(id);
fetchQuotations();
} catch (error) {
console.error('删除失败:', error);
}
};
const columns = [
{
title: '报价单号',
dataIndex: ['external_id'],
key: 'external_id',
sorter: true,
},
{
title: '客户名称',
dataIndex: ['attributes', 'customerName'],
key: 'customerName',
},
{
title: '创建日期',
dataIndex: 'created_at',
key: 'created_at',
sorter: true,
render: (text) => new Date(text).toLocaleString(),
},
{
title: '状态',
dataIndex: ['attributes', 'status'],
key: 'status',
},
{
title: '操作',
key: 'action',
render: (_, record) => (
<span>
<Button
type="link"
onClick={() => {
setEditingId(record.id);
form.setFieldsValue({
id: record.external_id,
customerName: record.attributes?.customerName,
status: record.attributes?.status,
});
setVisible(true);
}}
>
编辑
</Button>
<Popconfirm
title="确定要删除这个报价单吗?"
onConfirm={() => handleDelete(record.id)}
okText="确定"
cancelText="取消"
>
<Button type="link" danger>
删除
</Button>
</Popconfirm>
</span>
),
},
];
return (
<Card
title="报价单管理"
extra={
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
setEditingId(null);
form.resetFields();
setVisible(true);
}}
>
新增报价单
</Button>
}
>
<Table
columns={columns}
dataSource={quotations}
rowKey="id"
loading={loading}
onChange={handleTableChange}
pagination={{
...pagination,
total,
}}
/>
<Modal
title={editingId ? '编辑报价单' : '新增报价单'}
open={visible}
onCancel={() => {
setVisible(false);
setEditingId(null);
form.resetFields();
}}
footer={null}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
>
<Form.Item
name="id"
label="报价单号"
rules={[{ required: true, message: '请输入报价单号' }]}
>
<Input placeholder="请输入报价单号" />
</Form.Item>
<Form.Item
name="customerName"
label="客户名称"
rules={[{ required: true, message: '请输入客户名称' }]}
>
<Input placeholder="请输入客户名称" />
</Form.Item>
<Form.Item
name="status"
label="状态"
initialValue="新建"
>
<Input placeholder="请输入状态" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" block>
提交
</Button>
</Form.Item>
</Form>
</Modal>
</Card>
);
};
export default QuotationPage;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { Card, Table, Button } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
const ServicePage = () => {
const columns = [
{
title: '服务名称',
dataIndex: 'name',
key: 'name',
},
{
title: '服务类型',
dataIndex: 'type',
key: 'type',
},
{
title: '价格',
dataIndex: 'price',
key: 'price',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
},
];
return (
<Card
title="服务管理"
extra={
<Button type="primary" icon={<PlusOutlined />}>
新增服务
</Button>
}
>
<Table columns={columns} dataSource={[]} rowKey="id" />
</Card>
);
};
export default ServicePage;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { Card, Table, Button } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
const SupplierPage = () => {
const columns = [
{
title: '供应商名称',
dataIndex: 'name',
key: 'name',
},
{
title: '联系人',
dataIndex: 'contact',
key: 'contact',
},
{
title: '电话',
dataIndex: 'phone',
key: 'phone',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
},
];
return (
<Card
title="供应商管理"
extra={
<Button type="primary" icon={<PlusOutlined />}>
新增供应商
</Button>
}
>
<Table columns={columns} dataSource={[]} rowKey="id" />
</Card>
);
};
export default SupplierPage;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { Card, Table, Button } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
const TaskPage = () => {
const columns = [
{
title: '任务名称',
dataIndex: 'name',
key: 'name',
},
{
title: '负责人',
dataIndex: 'assignee',
key: 'assignee',
},
{
title: '截止日期',
dataIndex: 'dueDate',
key: 'dueDate',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
},
];
return (
<Card
title="任务管理"
extra={
<Button type="primary" icon={<PlusOutlined />}>
新增任务
</Button>
}
>
<Table columns={columns} dataSource={[]} rowKey="id" />
</Card>
);
};
export default TaskPage;

View File

@@ -0,0 +1,8 @@
import React from 'react';
import { Outlet } from 'react-router-dom';
const MarketingCampaign = () => {
return <Outlet />;
};
export default MarketingCampaign;

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { Card, DatePicker } from 'antd';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
const CampaignPerformance = () => {
const data = [
{ name: '1月', 点击率: 400, 转化率: 240 },
{ name: '2月', 点击率: 300, 转化率: 139 },
{ name: '3月', 点击率: 200, 转化率: 980 },
];
return (
<Card title="活动成效分析">
<div className="mb-4">
<DatePicker.RangePicker />
</div>
<div style={{ width: '100%', height: 400 }}>
<ResponsiveContainer>
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="点击率" stroke="#8884d8" />
<Line type="monotone" dataKey="转化率" stroke="#82ca9d" />
</LineChart>
</ResponsiveContainer>
</div>
</Card>
);
};
export default CampaignPerformance;

View File

@@ -0,0 +1,48 @@
import React from 'react';
import { Card, Table, Button, Tag } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
const CampaignPlan = () => {
const columns = [
{
title: '计划名称',
dataIndex: 'name',
key: 'name',
},
{
title: '开始时间',
dataIndex: 'startTime',
key: 'startTime',
},
{
title: '结束时间',
dataIndex: 'endTime',
key: 'endTime',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: status => (
<Tag color={status === '进行中' ? 'green' : 'default'}>
{status}
</Tag>
),
},
];
return (
<Card
title="活动计划"
extra={
<Button type="primary" icon={<PlusOutlined />}>
新增计划
</Button>
}
>
<Table columns={columns} dataSource={[]} rowKey="id" />
</Card>
);
};
export default CampaignPlan;

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { Form, Input, DatePicker, Select } from 'antd';
import { STATUS } from '@/constants/status';
export const ProjectForm = ({ form }) => (
<Form form={form} layout="vertical">
<Form.Item
name="name"
label="专案名称"
rules={[{ required: true, message: '请输入专案名称' }]}
>
<Input />
</Form.Item>
<Form.Item
name="manager"
label="负责人"
rules={[{ required: true, message: '请选择负责人' }]}
>
<Select placeholder="请选择负责人" />
</Form.Item>
<Form.Item
name="startDate"
label="开始日期"
rules={[{ required: true, message: '请选择开始日期' }]}
>
<DatePicker />
</Form.Item>
<Form.Item
name="status"
label="状态"
rules={[{ required: true, message: '请选择状态' }]}
>
<Select>
{Object.values(STATUS).map(status => (
<Select.Option key={status} value={status}>
{status}
</Select.Option>
))}
</Select>
</Form.Item>
</Form>
);

View File

@@ -0,0 +1,71 @@
import React, { useState } from 'react';
import { Card, Modal, Form } from 'antd';
import { PageHeader } from '@/components/common/PageHeader';
import { BaseTable } from '@/components/common/BaseTable';
import { ProjectForm } from './components/ProjectForm';
import { useProjectData } from './hooks/useProjectData';
import { getStatusColumn, getDateColumn } from '@/utils/tableColumns';
const CampaignProject = () => {
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const { loading, data, pagination, loadData } = useProjectData();
const columns = [
{
title: '专案名称',
dataIndex: 'name',
key: 'name',
},
{
title: '负责人',
dataIndex: 'manager',
key: 'manager',
},
getDateColumn('开始日期', 'startDate'),
getStatusColumn(),
];
const handleAdd = () => {
setVisible(true);
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
console.log('Success:', values);
setVisible(false);
form.resetFields();
loadData();
} catch (error) {
console.error('Failed:', error);
}
};
return (
<Card>
<PageHeader
title="行销活动专案"
onAdd={handleAdd}
addButtonText="新增专案"
/>
<BaseTable
columns={columns}
dataSource={data}
loading={loading}
pagination={pagination}
onChange={loadData}
/>
<Modal
title="新增专案"
open={visible}
onOk={handleSubmit}
onCancel={() => setVisible(false)}
>
<ProjectForm form={form} />
</Modal>
</Card>
);
};
export default CampaignProject;

View File

@@ -0,0 +1,8 @@
import React from 'react';
import { Outlet } from 'react-router-dom';
const Communication = () => {
return <Outlet />;
};
export default Communication;

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { Card, Timeline, Button } from 'antd';
import { ClockCircleOutlined } from '@ant-design/icons';
const CommunicationJourney = () => {
return (
<Card
title="沟通历程"
extra={
<Button type="primary">导出记录</Button>
}
>
<Timeline
mode="alternate"
items={[
{
children: '创建营销活动 2023-10-25 10:00:00',
},
{
children: '发送邮件通知 2023-10-25 10:30:00',
color: 'green',
},
{
dot: <ClockCircleOutlined style={{ fontSize: '16px' }} />,
children: '客户查看邮件 2023-10-25 11:00:00',
},
{
color: 'red',
children: '系统提醒跟进 2023-10-25 14:00:00',
},
{
children: '完成跟进 2023-10-25 16:00:00',
},
]}
/>
</Card>
);
};
export default CommunicationJourney;

View File

@@ -0,0 +1,48 @@
import React from 'react';
import { Card, Table, Button, Input } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
const { Search } = Input;
const CommunicationList = () => {
const columns = [
{
title: '名单名称',
dataIndex: 'name',
key: 'name',
},
{
title: '联系人数量',
dataIndex: 'contactCount',
key: 'contactCount',
},
{
title: '创建日期',
dataIndex: 'createdAt',
key: 'createdAt',
},
{
title: '更新日期',
dataIndex: 'updatedAt',
key: 'updatedAt',
},
];
return (
<Card
title="名单管理"
extra={
<Button type="primary" icon={<PlusOutlined />}>
新增名单
</Button>
}
>
<div className="mb-4">
<Search placeholder="搜索名单" allowClear enterButton />
</div>
<Table columns={columns} dataSource={[]} rowKey="id" />
</Card>
);
};
export default CommunicationList;

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { Card, Tabs, Button } from 'antd';
import { SendOutlined } from '@ant-design/icons';
const CommunicationPreview = () => {
const items = [
{
key: '1',
label: '邮件预览',
children: (
<div className="p-4 border rounded">
<h2>邮件主题</h2>
<div className="mt-4">邮件内容预览区域</div>
</div>
),
},
{
key: '2',
label: '短信预览',
children: (
<div className="p-4 border rounded">
<div className="mt-4">短信内容预览区域</div>
</div>
),
},
];
return (
<Card
title="消息预览"
extra={
<Button type="primary" icon={<SendOutlined />}>
发送测试
</Button>
}
>
<Tabs items={items} />
</Card>
);
};
export default CommunicationPreview;

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { Card, Steps, Form, Button, Select, DatePicker, Input } from 'antd';
const { Step } = Steps;
const { Option } = Select;
const CommunicationSend = () => {
const [form] = Form.useForm();
return (
<Card title="发送管理">
<Steps
current={0}
items={[
{ title: '选择模板' },
{ title: '设置参数' },
{ title: '确认发送' },
]}
className="mb-8"
/>
<Form form={form} layout="vertical">
<Form.Item name="template" label="选择模板">
<Select placeholder="请选择模板">
<Option value="template1">模板一</Option>
<Option value="template2">模板二</Option>
</Select>
</Form.Item>
<Form.Item name="sendTime" label="发送时间">
<DatePicker showTime />
</Form.Item>
<Form.Item name="targetList" label="目标名单">
<Select placeholder="请选择目标名单">
<Option value="list1">名单一</Option>
<Option value="list2">名单二</Option>
</Select>
</Form.Item>
<Form.Item>
<Button type="primary">下一步</Button>
</Form.Item>
</Form>
</Card>
);
};
export default CommunicationSend;

View File

@@ -0,0 +1,48 @@
import React from 'react';
import { Card, Table, Button, Tag } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
const CommunicationTasks = () => {
const columns = [
{
title: '任务名称',
dataIndex: 'name',
key: 'name',
},
{
title: '负责人',
dataIndex: 'assignee',
key: 'assignee',
},
{
title: '截止日期',
dataIndex: 'dueDate',
key: 'dueDate',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: status => (
<Tag color={status === '进行中' ? 'green' : 'default'}>
{status}
</Tag>
),
},
];
return (
<Card
title="沟通任务"
extra={
<Button type="primary" icon={<PlusOutlined />}>
新增任务
</Button>
}
>
<Table columns={columns} dataSource={[]} rowKey="id" />
</Card>
);
};
export default CommunicationTasks;

View File

@@ -0,0 +1,8 @@
import React from 'react';
import { Outlet } from 'react-router-dom';
const MarketingCenter = () => {
return <Outlet />;
};
export default MarketingCenter;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { Card, Table, Button } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
const TemplatePage = () => {
const columns = [
{
title: '模版名称',
dataIndex: 'name',
key: 'name',
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
},
{
title: '创建日期',
dataIndex: 'createdAt',
key: 'createdAt',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
},
];
return (
<Card
title="模版管理"
extra={
<Button type="primary" icon={<PlusOutlined />}>
新增模版
</Button>
}
>
<Table columns={columns} dataSource={[]} rowKey="id" />
</Card>
);
};
export default TemplatePage;

View File

@@ -0,0 +1,8 @@
import React from 'react';
import { Outlet } from 'react-router-dom';
const ResourceManagement = () => {
return <Outlet />;
};
export default ResourceManagement;

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { Modal, Form } from 'antd';
import { UserForm } from './UserForm';
import { RoleSelect } from './RoleSelect';
export const AddMemberModal = ({
visible,
onOk,
onCancel,
form
}) => (
<Modal
title="添加成员"
open={visible}
onOk={onOk}
onCancel={onCancel}
>
<Form form={form} layout="vertical">
<Form.Item label="成员信息">
<UserForm
nameProps={{ name: ['user', 'name'] }}
emailProps={{ name: ['user', 'email'] }}
/>
</Form.Item>
<Form.Item
name="role"
label="角色"
rules={[{ required: true, message: '请选择角色!' }]}
>
<RoleSelect />
</Form.Item>
</Form>
</Modal>
);

View File

@@ -0,0 +1,88 @@
import React from 'react';
import { Form, Input, Select, Upload } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import { supabase } from '@/config/supabase';
export const TeamForm = ({ form }) => {
const handleUpload = async ({ file, onSuccess, onError }) => {
try {
const fileExt = file.name.split('.').pop();
const fileName = `${Math.random()}.${fileExt}`;
const filePath = `team-avatars/${fileName}`;
const { error: uploadError } = await supabase.storage
.from('avatars')
.upload(filePath, file);
if (uploadError) throw uploadError;
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(filePath);
form.setFieldValue('avatarUrl', publicUrl);
onSuccess(publicUrl);
} catch (error) {
console.error('Upload error:', error);
onError(error);
}
};
return (
<Form
form={form}
layout="vertical"
requiredMark={false}
>
<Form.Item
name="name"
label="团队名称"
rules={[{ required: true, message: '请输入团队名称' }]}
>
<Input placeholder="请输入团队名称" />
</Form.Item>
<Form.Item
name="avatarUrl"
label="团队头像"
>
<Upload
customRequest={handleUpload}
showUploadList={false}
maxCount={1}
listType="picture-card"
>
{form.getFieldValue('avatarUrl') ? (
<img
src={form.getFieldValue('avatarUrl')}
alt="avatar"
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
) : (
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}>上传头像</div>
</div>
)}
</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>
);
};

View File

@@ -0,0 +1,32 @@
import React from 'react';
import { Modal, Form } from 'antd';
import { TeamForm } from './TeamForm';
const CreateTeamModal = ({ open, onCancel, onSubmit, confirmLoading }) => {
const [form] = Form.useForm();
const handleSubmit = async () => {
try {
const values = await form.validateFields();
await onSubmit(values);
form.resetFields();
} catch (error) {
console.error('Validation failed:', error);
}
};
return (
<Modal
title="新增团队"
open={open}
onCancel={onCancel}
onOk={handleSubmit}
confirmLoading={confirmLoading}
width={600}
>
<TeamForm form={form} />
</Modal>
);
};
export default CreateTeamModal;

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { Form, Input } from 'antd';
export const EditableCell = ({
editing,
dataIndex,
title,
inputType,
record,
index,
children,
...restProps
}) => {
const inputNode = <Input />;
return (
<td {...restProps}>
{editing ? (
<Form.Item
name={dataIndex}
style={{ margin: 0 }}
rules={[
{
required: true,
message: `请输入${title}!`,
},
]}
>
{inputNode}
</Form.Item>
) : (
children
)}
</td>
);
};

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { Form } from 'antd';
import { UserForm } from './UserForm';
import { RoleSelect } from './RoleSelect';
export const EditableMembershipCell = ({
editing,
dataIndex,
title,
record,
children,
...restProps
}) => {
let inputNode;
if (dataIndex === 'role') {
inputNode = <RoleSelect />;
} else if (dataIndex === 'user') {
inputNode = (
<UserForm
nameProps={{ name: ['user', 'name'] }}
emailProps={{ name: ['user', 'email'] }}
/>
);
}
return (
<td {...restProps}>
{editing ? (
<Form.Item
name={dataIndex}
style={{ margin: 0 }}
rules={[{ required: true, message: `请输入${title}!` }]}
>
{inputNode}
</Form.Item>
) : (
children
)}
</td>
);
};

View File

@@ -0,0 +1,98 @@
import React, { useState } from 'react';
import { Modal, Form, Input, Select, message } from 'antd';
import { MembershipTable } from './MembershipTable';
const { Option } = Select;
export const ExpandedMemberships = ({ teamId, memberships: initialMemberships }) => {
const [memberships, setMemberships] = useState(initialMemberships);
const [isModalVisible, setIsModalVisible] = useState(false);
const [form] = Form.useForm();
const handleUpdate = (id, values) => {
setMemberships(prev =>
prev.map(item => item.id === id ? { ...item, ...values } : item)
);
message.success('成员信息已更新');
};
const handleDelete = (id) => {
setMemberships(prev => prev.filter(item => item.id !== id));
message.success('成员已删除');
};
const handleAdd = () => {
setIsModalVisible(true);
form.resetFields();
};
const handleModalOk = async () => {
try {
const values = await form.validateFields();
const newMember = {
id: `${Date.now()}`,
teamId,
isCreator: false,
...values,
};
setMemberships(prev => [...prev, newMember]);
setIsModalVisible(false);
message.success('成员已添加');
} catch (error) {
console.error('Add failed:', error);
}
};
return (
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
<h3 className="text-lg font-medium mb-4">团队成员</h3>
<MembershipTable
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>
);
};

View File

@@ -0,0 +1,160 @@
import React, { useState } from 'react';
import { Table, Button, Space, Popconfirm, Tag, Form } from 'antd';
import { EditOutlined, DeleteOutlined, SaveOutlined, CloseOutlined, PlusOutlined } from '@ant-design/icons';
import { EditableMembershipCell } from './EditableMembershipCell';
import { roleColors } from '../constants/teamConstants';
export const MembershipTable = ({ memberships, onUpdate, onDelete, onAdd }) => {
const [form] = Form.useForm();
const [editingKey, setEditingKey] = useState('');
const isEditing = (record) => record.id === editingKey;
const edit = (record) => {
form.setFieldsValue({ ...record });
setEditingKey(record.id);
};
const cancel = () => {
setEditingKey('');
};
const save = async (key) => {
try {
const row = await form.validateFields();
setEditingKey('');
onUpdate(key, row);
} catch (error) {
console.error('Save failed:', error);
}
};
const columns = [
{
title: '成员',
dataIndex: 'users',
key: 'users',
editable: true,
render: (users) => (
<div className="flex flex-col">
<span className="font-medium">{users.email}</span>
<span className="text-gray-500 text-sm">{users.email}</span>
</div>
),
},
{
title: '角色',
dataIndex: 'role',
key: 'role',
editable: true,
render: (role) => (
<Tag color={roleColors[role]}>
{role.toLowerCase()}
</Tag>
),
},
{
title: '创建者',
dataIndex: 'is_creator',
key: 'is_creator',
render: (is_creator) => (
is_creator ? (
<Tag color="green"></Tag>
) : (
<Tag color="default"></Tag>
)
),
},
{
title: '操作',
key: 'action',
render: (_, record) => {
const editable = isEditing(record);
return editable ? (
<Space>
<Button
icon={<SaveOutlined />}
onClick={() => save(record.id)}
type="link"
>
保存
</Button>
<Button
icon={<CloseOutlined />}
onClick={cancel}
type="link"
>
取消
</Button>
</Space>
) : (
<Space>
<Button
disabled={editingKey !== '' || record.isCreator}
icon={<EditOutlined />}
onClick={() => edit(record)}
type="link"
>
编辑
</Button>
{!record.isCreator && (
<Popconfirm
title="确定要删除该成员吗?"
onConfirm={() => onDelete(record.id)}
>
<Button
icon={<DeleteOutlined />}
type="link"
danger
>
删除
</Button>
</Popconfirm>
)}
</Space>
);
},
},
];
const mergedColumns = columns.map((col) => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: (record) => ({
record,
dataIndex: col.dataIndex,
title: col.title,
editing: isEditing(record),
}),
};
});
return (
<div className="space-y-4">
<Button
type="primary"
icon={<PlusOutlined />}
onClick={onAdd}
className="mb-4"
>
添加成员
</Button>
<Form form={form} component={false}>
<Table
components={{
body: {
cell: EditableMembershipCell,
},
}}
columns={mergedColumns}
dataSource={memberships}
rowKey="id"
pagination={false}
/>
</Form>
</div>
);
};

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { Select } from 'antd';
import { roles } from '../constants/teamConstants';
export const RoleSelect = ({ value, onChange, disabled }) => (
<Select value={value} onChange={onChange} disabled={disabled}>
{roles.map(({ label, value }) => (
<Select.Option key={value} value={value}>
{label}
</Select.Option>
))}
</Select>
);

View File

@@ -0,0 +1,59 @@
import React from 'react';
import { Button, Space, Popconfirm } from 'antd';
import { EditOutlined, DeleteOutlined, SaveOutlined, CloseOutlined } from '@ant-design/icons';
export const TeamActions = ({
record,
editable,
editingKey,
onEdit,
onSave,
onCancel,
onDelete,
}) => {
if (editable) {
return (
<Space>
<Button
icon={<SaveOutlined />}
onClick={() => onSave(record.id)}
type="link"
>
保存
</Button>
<Button
icon={<CloseOutlined />}
onClick={onCancel}
type="link"
>
取消
</Button>
</Space>
);
}
return (
<Space>
<Button
disabled={editingKey !== ''}
icon={<EditOutlined />}
onClick={() => onEdit(record)}
type="link"
>
编辑
</Button>
<Popconfirm
title="确定要删除该团队吗?"
onConfirm={() => onDelete(record.id)}
>
<Button
icon={<DeleteOutlined />}
type="link"
danger
>
删除
</Button>
</Popconfirm>
</Space>
);
};

View File

@@ -0,0 +1,82 @@
import React from 'react';
import { Form, Input, Upload, Select } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import { supabase } from '@/config/supabase';
export const TeamForm = ({ form }) => {
const normFile = (e) => {
if (Array.isArray(e)) {
return e;
}
return e?.fileList;
};
const handleUpload = async ({ file, onSuccess, onError }) => {
try {
const fileExt = file.name.split('.').pop();
const fileName = `${Math.random()}.${fileExt}`;
const filePath = `team-avatars/${fileName}`;
const { error: uploadError } = await supabase.storage
.from('file')
.upload(filePath, file);
if (uploadError) throw uploadError;
const { data: { publicUrl } } = supabase.storage
.from('file')
.getPublicUrl(filePath);
form.setFieldsValue({ avatarUrl: publicUrl });
onSuccess(publicUrl);
} catch (error) {
console.error('Upload error:', error);
onError(error);
}
};
return (
<Form
form={form}
layout="vertical"
requiredMark={false}
>
<Form.Item
name="name"
label="团队名称"
rules={[{ required: true, message: '请输入团队名称' }]}
>
<Input placeholder="请输入团队名称" />
</Form.Item>
<Form.Item
name="avatarUrl"
label="团队头像"
valuePropName="fileList"
getValueFromEvent={normFile}
>
<Upload
customRequest={handleUpload}
showUploadList={false}
maxCount={1}
listType="picture-card"
>
{form.getFieldValue('avatarUrl') ? (
<img
src={form.getFieldValue('avatarUrl')}
alt="avatar"
className="w-full h-full object-cover"
/>
) : (
<div>
<PlusOutlined />
<div className="mt-2">上传头像</div>
</div>
)}
</Upload>
</Form.Item>
</Form>
);
};

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { Input, Button } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
export const TeamHeader = ({ onSearch, onAdd }) => (
<div className="flex justify-between mb-4">
<Input.Search
placeholder="搜索团队"
allowClear
onSearch={onSearch}
style={{ width: 200 }}
/>
<Button type="primary" icon={<PlusOutlined />} onClick={onAdd}>
新增团队
</Button>
</div>
);

View File

@@ -0,0 +1,197 @@
import React, { useState } from 'react';
import { Table, Form, Button, Space, Popconfirm, Tag, message } from 'antd';
import { EditOutlined, DeleteOutlined, SaveOutlined, CloseOutlined } from '@ant-design/icons';
import { EditableCell } from './EditableCell';
import { ExpandedMemberships } from './ExpandedMemberships';
export const TeamTable = ({ tableLoading,pagination,dataSource, onTableChange,onDelete ,onUpdate}) => {
const [form] = Form.useForm();
const [editingKey, setEditingKey] = useState('');
const [loading, setLoading] = useState(false);
const isEditing = (record) => record.id === editingKey;
const edit = (record) => {
form.setFieldsValue({
name: record.name,
description: record.description
});
setEditingKey(record.id);
};
const cancel = () => {
setEditingKey('');
form.resetFields();
};
// 保存编辑
const save = async (key) => {
try {
setLoading(true);
const row = await form.validateFields();
await onUpdate(key, {
name: row.name,
description: row.description,
updated_at: new Date().toISOString()
});
setEditingKey('');
message.success('更新成功');
onTableChange?.(); // 刷新数据
} catch (error) {
console.error('Save failed:', error);
message.error('更新失败: ' + error.message);
} finally {
setLoading(false);
}
};
// 删除团队
const handleDelete = async (id) => {
try {
setLoading(true);
await onDelete(id)
message.success('删除成功');
onTableChange?.(); // 刷新数据
} catch (error) {
console.error('Delete failed:', error);
message.error('删除失败: ' + error.message);
} finally {
setLoading(false);
}
};
const columns = [
{
title: '团队名称',
dataIndex: 'name',
key: 'name',
editable: true,
render: (text, record) => (
<div className="flex items-center gap-2">
{record.avatar_url && (
<img
src={record.avatar_url}
alt={text}
className="w-8 h-8 rounded-full object-cover"
/>
)}
<span className="font-medium">{text}</span>
</div>
),
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
editable: true,
},
{
title: '成员数量',
dataIndex: 'team_membership',
key: 'team_membership',
render: (team_membership) => team_membership?.length || 0,
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
sorter:true,
defaultSortOrder: 'descend',
render: (text) => new Date(text).toLocaleString(),
},
{
title: '操作',
key: 'action',
render: (_, record) => {
const editable = isEditing(record);
return editable ? (
<Space>
<Button
icon={<SaveOutlined />}
onClick={() => save(record.id)}
type="link"
loading={loading}
>
保存
</Button>
<Button
icon={<CloseOutlined />}
onClick={cancel}
type="link"
>
取消
</Button>
</Space>
) : (
<Space>
<Button
disabled={editingKey !== ''}
icon={<EditOutlined />}
onClick={() => edit(record)}
type="link"
>
编辑
</Button>
<Popconfirm
title="确定要删除该团队吗?"
onConfirm={() => handleDelete(record.id)}
>
<Button
icon={<DeleteOutlined />}
type="link"
danger
loading={loading}
>
删除
</Button>
</Popconfirm>
</Space>
);
},
},
];
const mergedColumns = columns.map((col) => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: (record) => ({
record,
dataIndex: col.dataIndex,
title: col.title,
editing: isEditing(record),
}),
};
});
const handleTableChange = (pagination, filters, sorter, extra) => {
onTableChange?.(pagination, filters, sorter, extra);
}
return (
<Form form={form} component={false}>
<Table
className='w-full'
pagination={pagination}
loading={loading||tableLoading}
components={{
body: {
cell: EditableCell,
},
}}
onChange={handleTableChange}
dataSource={dataSource}
columns={mergedColumns}
rowKey="id"
expandable={{
expandedRowRender: (record) => (
<ExpandedMemberships
memberships={record.team_membership}
teamId={record.id}
onMembershipChange={onTableChange}
/>
),
}}
/>
</Form>
);
};

View File

@@ -0,0 +1,24 @@
import React from 'react';
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%' }}
rules={[
{ required: true, message: '请输入邮箱!' },
{ type: 'email', message: '请输入有效的邮箱!' }
]}
>
<Input placeholder="邮箱" />
</Form.Item>
</Space.Compact>
);

View File

@@ -0,0 +1,63 @@
export const mockTeams = [
{
id: '1',
name: '研发团队',
avatarUrl: 'https://api.dicebear.com/7.x/avatars/svg?seed=1',
tags: ['前端', '后端', '设计'],
memberships: [
{
id: '1',
user: {
email: 'john@example.com',
name: 'John Doe'
},
role: 'OWNER',
isCreator: true,
},
{
id: '2',
user: {
email: 'jane@example.com',
name: 'Jane Smith'
},
role: 'ADMIN',
isCreator: false,
},
{
id: '3',
user: {
email: 'bob@example.com',
name: 'Bob Wilson'
},
role: 'MEMBER',
isCreator: false,
}
],
},
{
id: '2',
name: '设计团队',
avatarUrl: 'https://api.dicebear.com/7.x/avatars/svg?seed=2',
tags: ['UI', 'UX', '平面设计'],
memberships: [
{
id: '4',
user: {
email: 'alice@example.com',
name: 'Alice Johnson'
},
role: 'OWNER',
isCreator: true,
},
{
id: '5',
user: {
email: 'charlie@example.com',
name: 'Charlie Brown'
},
role: 'MEMBER',
isCreator: false,
}
],
}
];

View File

@@ -0,0 +1,11 @@
export const roleColors = {
OWNER: 'gold',
ADMIN: 'purple',
MEMBER: 'blue',
};
export const roles = [
{ label: 'Owner', value: 'OWNER' },
{ label: 'Admin', value: 'ADMIN' },
{ label: 'Member', value: 'MEMBER' },
];

View File

@@ -0,0 +1,33 @@
import { message } from 'antd';
export const useMemberActions = (teamId) => {
const handleUpdate = (id, values) => {
// 模拟API调用
console.log('Update member:', id, values);
message.success('成员信息已更新');
};
const handleDelete = (id) => {
// 模拟API调用
console.log('Delete member:', id);
message.success('成员已删除');
};
const handleAdd = (values) => {
// 模拟API调用
console.log('Add member:', values);
message.success('成员已添加');
return {
id: Date.now().toString(),
teamId,
isCreator: false,
...values,
};
};
return {
handleUpdate,
handleDelete,
handleAdd,
};
};

View File

@@ -0,0 +1,31 @@
import { message } from 'antd';
export const useTeamActions = () => {
const handleUpdate = (id, values) => {
// 模拟API调用
console.log('Update team:', id, values);
message.success('团队信息已更新');
};
const handleDelete = (id) => {
// 模拟API调用
console.log('Delete team:', id);
message.success('团队已删除');
};
const handleAdd = (values) => {
// 模拟API调用
console.log('Add team:', values);
message.success('团队已创建');
return {
id: Date.now().toString(),
...values,
};
};
return {
handleUpdate,
handleDelete,
handleAdd,
};
};

View File

@@ -0,0 +1,104 @@
import { useState, useCallback } from 'react';
import { supabase } from '@/config/supabase';
export const useTeamData = () => {
const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
});
const loadData = useCallback(async ({
pagination = {},
filters = {},
sorter = {},
search = ''
} = {}) => {
setLoading(true);
try {
let query = supabase
.from('teams')
.select(`
*,
memberships:team_memberships(
id,
role,
user:users(id, email, name)
),
tags(id, name)
`);
// Add search condition
if (search) {
query = query.ilike('name', `%${search}%`);
}
// Add sorting
if (sorter.field) {
const order = sorter.order === 'descend' ? 'desc' : 'asc';
query = query.order(sorter.field, { ascending: order === 'asc' });
}
// Add pagination
const from = ((pagination.current || 1) - 1) * (pagination.pageSize || 10);
const to = from + (pagination.pageSize || 10) - 1;
query = query.range(from, to);
const { data: teams, count } = await query;
setData(teams || []);
setPagination({
...pagination,
total: count || 0,
});
} catch (error) {
console.error('Failed to fetch teams:', error);
} finally {
setLoading(false);
}
}, []);
const createTeam = async (values) => {
const { data, error } = await supabase
.from('teams')
.insert([values])
.select()
.single();
if (error) throw error;
return data;
};
const updateTeam = async (id, values) => {
const { data, error } = await supabase
.from('teams')
.update(values)
.eq('id', id)
.select()
.single();
if (error) throw error;
return data;
};
const deleteTeam = async (id) => {
const { error } = await supabase
.from('teams')
.delete()
.eq('id', id);
if (error) throw error;
};
return {
loading,
data,
pagination,
loadData,
createTeam,
updateTeam,
deleteTeam,
};
};

View File

@@ -0,0 +1,73 @@
import { useState, useCallback } from 'react';
import { supabase } from '@/config/supabase';
export const useTeamMembership = (teamId) => {
const [loading, setLoading] = useState(false);
const [memberships, setMemberships] = useState([]);
const loadMemberships = useCallback(async () => {
if (!teamId) return;
setLoading(true);
try {
const { data, error } = await supabase
.from('team_memberships')
.select(`
*,
user:users(id, email, name)
`)
.eq('teamId', teamId);
if (error) throw error;
setMemberships(data || []);
} catch (error) {
console.error('Failed to fetch memberships:', error);
} finally {
setLoading(false);
}
}, [teamId]);
const addMembership = async (values) => {
const { data, error } = await supabase
.from('team_memberships')
.insert([{ ...values, teamId }])
.select()
.single();
if (error) throw error;
await loadMemberships();
return data;
};
const updateMembership = async (id, values) => {
const { data, error } = await supabase
.from('team_memberships')
.update(values)
.eq('id', id)
.select()
.single();
if (error) throw error;
await loadMemberships();
return data;
};
const deleteMembership = async (id) => {
const { error } = await supabase
.from('team_memberships')
.delete()
.eq('id', id);
if (error) throw error;
await loadMemberships();
};
return {
loading,
memberships,
loadMemberships,
addMembership,
updateMembership,
deleteMembership,
};
};

View File

@@ -0,0 +1,114 @@
import React, { useEffect, useState } from 'react';
import { Card, App } 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';
const TeamManagement = () => {
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
});
const [sorter, setSorter] = useState({
field: 'created_at',
order: 'descend',
});
const {
teams,
loading,
fetchTeams,
createTeam,
updateTeam,
deleteTeam
} = useTeams(pagination, sorter);
const [modalVisible, setModalVisible] = useState(false);
const [confirmLoading, setConfirmLoading] = useState(false);
const { user } = useAuth();
useEffect(() => {
const loadTeams = async () => {
const { total } = await fetchTeams();
setPagination(prev => ({ ...prev, total }));
};
loadTeams();
}, [fetchTeams]);
const handleTableChange = (newPagination, filters, newSorter) => {
const params = {
current: newPagination.current,
pageSize: newPagination.pageSize,
field: newSorter.field,
order: newSorter.order,
};
setPagination(prev => ({
...prev,
current: params.current,
pageSize: params.pageSize,
}));
setSorter({
field: params.field || sorter.field,
order: params.order || sorter.order,
});
fetchTeams(params).then(({ total }) => {
setPagination(prev => ({ ...prev, total }));
});
};
const handleAdd = () => {
setModalVisible(true);
};
const handleModalCancel = () => {
setModalVisible(false);
};
const handleModalSubmit = async (values) => {
try {
setConfirmLoading(true);
await createTeam({ ...values, userId: user.id });
setModalVisible(false);
setPagination(prev => ({ ...prev, current: 1 }));
const { total } = await fetchTeams({ current: 1 });
setPagination(prev => ({ ...prev, total }));
} catch (error) {
console.error('Failed to create team:', error);
} finally {
setConfirmLoading(false);
}
};
const onSearch=(value)=>{
fetchTeams({search:value})
}
return (
<App>
<Card title="团队管理" bordered={false}>
<TeamHeader onSearch={onSearch} onAdd={handleAdd} />
<TeamTable
tableLoading={loading}
dataSource={teams}
pagination={pagination}
onTableChange={handleTableChange}
onUpdate={updateTeam}
onDelete={deleteTeam}
/>
<CreateTeamModal
open={modalVisible}
onCancel={handleModalCancel}
onSubmit={handleModalSubmit}
confirmLoading={confirmLoading}
/>
</Card>
</App>
);
};
export default TeamManagement;