管理后台初始化,登录,团队管理,报价单管理 完成
This commit is contained in:
23
src/pages/Dashboard.jsx
Normal file
23
src/pages/Dashboard.jsx
Normal 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
24
src/pages/Profile.jsx
Normal 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
40
src/pages/Settings.jsx
Normal 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
102
src/pages/auth/Login.jsx
Normal 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
120
src/pages/auth/Register.jsx
Normal 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;
|
||||
43
src/pages/company/customer/index.jsx
Normal file
43
src/pages/company/customer/index.jsx
Normal 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;
|
||||
8
src/pages/company/index.jsx
Normal file
8
src/pages/company/index.jsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
const CompanyManagement = () => {
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
export default CompanyManagement;
|
||||
43
src/pages/company/project/index.jsx
Normal file
43
src/pages/company/project/index.jsx
Normal 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;
|
||||
208
src/pages/company/quotation/index.jsx
Normal file
208
src/pages/company/quotation/index.jsx
Normal 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;
|
||||
43
src/pages/company/service/index.jsx
Normal file
43
src/pages/company/service/index.jsx
Normal 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;
|
||||
43
src/pages/company/supplier/index.jsx
Normal file
43
src/pages/company/supplier/index.jsx
Normal 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;
|
||||
43
src/pages/company/task/index.jsx
Normal file
43
src/pages/company/task/index.jsx
Normal 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;
|
||||
8
src/pages/marketing/campaign/index.jsx
Normal file
8
src/pages/marketing/campaign/index.jsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
const MarketingCampaign = () => {
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
export default MarketingCampaign;
|
||||
34
src/pages/marketing/campaign/performance/index.jsx
Normal file
34
src/pages/marketing/campaign/performance/index.jsx
Normal 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;
|
||||
48
src/pages/marketing/campaign/plan/index.jsx
Normal file
48
src/pages/marketing/campaign/plan/index.jsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
71
src/pages/marketing/campaign/project/index.jsx
Normal file
71
src/pages/marketing/campaign/project/index.jsx
Normal 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;
|
||||
8
src/pages/marketing/communication/index.jsx
Normal file
8
src/pages/marketing/communication/index.jsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
const Communication = () => {
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
export default Communication;
|
||||
40
src/pages/marketing/communication/journey/index.jsx
Normal file
40
src/pages/marketing/communication/journey/index.jsx
Normal 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;
|
||||
48
src/pages/marketing/communication/list/index.jsx
Normal file
48
src/pages/marketing/communication/list/index.jsx
Normal 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;
|
||||
42
src/pages/marketing/communication/preview/index.jsx
Normal file
42
src/pages/marketing/communication/preview/index.jsx
Normal 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;
|
||||
49
src/pages/marketing/communication/send/index.jsx
Normal file
49
src/pages/marketing/communication/send/index.jsx
Normal 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;
|
||||
48
src/pages/marketing/communication/tasks/index.jsx
Normal file
48
src/pages/marketing/communication/tasks/index.jsx
Normal 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;
|
||||
8
src/pages/marketing/index.jsx
Normal file
8
src/pages/marketing/index.jsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
const MarketingCenter = () => {
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
export default MarketingCenter;
|
||||
43
src/pages/marketing/template/index.jsx
Normal file
43
src/pages/marketing/template/index.jsx
Normal 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;
|
||||
8
src/pages/resource/index.jsx
Normal file
8
src/pages/resource/index.jsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
const ResourceManagement = () => {
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
export default ResourceManagement;
|
||||
34
src/pages/resource/team/components/AddMemberModal.jsx
Normal file
34
src/pages/resource/team/components/AddMemberModal.jsx
Normal 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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
32
src/pages/resource/team/components/CreateTeamModal/index.jsx
Normal file
32
src/pages/resource/team/components/CreateTeamModal/index.jsx
Normal 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;
|
||||
36
src/pages/resource/team/components/EditableCell.jsx
Normal file
36
src/pages/resource/team/components/EditableCell.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
98
src/pages/resource/team/components/ExpandedMemberships.jsx
Normal file
98
src/pages/resource/team/components/ExpandedMemberships.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
160
src/pages/resource/team/components/MembershipTable.jsx
Normal file
160
src/pages/resource/team/components/MembershipTable.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
13
src/pages/resource/team/components/RoleSelect.jsx
Normal file
13
src/pages/resource/team/components/RoleSelect.jsx
Normal 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>
|
||||
);
|
||||
59
src/pages/resource/team/components/TeamActions.jsx
Normal file
59
src/pages/resource/team/components/TeamActions.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
82
src/pages/resource/team/components/TeamForm.jsx
Normal file
82
src/pages/resource/team/components/TeamForm.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
17
src/pages/resource/team/components/TeamHeader.jsx
Normal file
17
src/pages/resource/team/components/TeamHeader.jsx
Normal 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>
|
||||
);
|
||||
197
src/pages/resource/team/components/TeamTable.jsx
Normal file
197
src/pages/resource/team/components/TeamTable.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
24
src/pages/resource/team/components/UserForm.jsx
Normal file
24
src/pages/resource/team/components/UserForm.jsx
Normal 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>
|
||||
);
|
||||
63
src/pages/resource/team/constants/mockData.js
Normal file
63
src/pages/resource/team/constants/mockData.js
Normal 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,
|
||||
}
|
||||
],
|
||||
}
|
||||
];
|
||||
11
src/pages/resource/team/constants/teamConstants.js
Normal file
11
src/pages/resource/team/constants/teamConstants.js
Normal 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' },
|
||||
];
|
||||
33
src/pages/resource/team/hooks/useMemberActions.js
Normal file
33
src/pages/resource/team/hooks/useMemberActions.js
Normal 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,
|
||||
};
|
||||
};
|
||||
31
src/pages/resource/team/hooks/useTeamActions.js
Normal file
31
src/pages/resource/team/hooks/useTeamActions.js
Normal 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,
|
||||
};
|
||||
};
|
||||
104
src/pages/resource/team/hooks/useTeamData.js
Normal file
104
src/pages/resource/team/hooks/useTeamData.js
Normal 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,
|
||||
};
|
||||
};
|
||||
73
src/pages/resource/team/hooks/useTeamMembership.js
Normal file
73
src/pages/resource/team/hooks/useTeamMembership.js
Normal 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,
|
||||
};
|
||||
};
|
||||
114
src/pages/resource/team/index.jsx
Normal file
114
src/pages/resource/team/index.jsx
Normal 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;
|
||||
Reference in New Issue
Block a user