336 lines
9.6 KiB
JavaScript
336 lines
9.6 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
||
import { Table, Button, Form, Input, Space, message, Popconfirm, Drawer, Select, Tabs, Badge, Radio } from 'antd';
|
||
import { PlusOutlined, FileTextOutlined, ProjectOutlined, CheckSquareOutlined } from '@ant-design/icons';
|
||
import { supabaseService } from '@/hooks/supabaseService';
|
||
|
||
const CategoryDrawer = () => {
|
||
const [data, setData] = useState([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [editingKey, setEditingKey] = useState('');
|
||
const [drawerVisible, setDrawerVisible] = useState(false);
|
||
const [activeType, setActiveType] = useState('quotation'); // 当前选中的类型
|
||
const [form] = Form.useForm();
|
||
|
||
// 模板类型配置
|
||
const TEMPLATE_TYPES = [
|
||
{
|
||
value: 'quotation',
|
||
label: '报价单模板',
|
||
icon: <FileTextOutlined className="text-blue-500" />,
|
||
color: 'blue',
|
||
description: '用于创建标准化的报价单'
|
||
},
|
||
{
|
||
value: 'project',
|
||
label: '专案模板',
|
||
icon: <ProjectOutlined className="text-green-500" />,
|
||
color: 'green',
|
||
description: '用于创建项目流程模板'
|
||
},
|
||
{
|
||
value: 'task',
|
||
label: '任务模板',
|
||
icon: <CheckSquareOutlined className="text-purple-500" />,
|
||
color: 'purple',
|
||
description: '用于创建标准任务模板'
|
||
}
|
||
];
|
||
|
||
// 获取分类数据
|
||
const fetchCategories = async (type = activeType) => {
|
||
setLoading(true);
|
||
try {
|
||
const { data: categories } = await supabaseService.select('resources', {
|
||
filter: {
|
||
type: { eq: 'categories' },
|
||
'attributes->>type': { eq: type }
|
||
},
|
||
order: {
|
||
column: 'created_at',
|
||
ascending: false
|
||
}
|
||
});
|
||
|
||
setData(categories || []);
|
||
} catch (error) {
|
||
message.error('获取分类数据失败');
|
||
console.error(error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (drawerVisible) {
|
||
fetchCategories();
|
||
}
|
||
}, [drawerVisible, activeType]);
|
||
|
||
// 切换模板类型
|
||
const handleTypeChange = (type) => {
|
||
setActiveType(type);
|
||
setEditingKey(''); // 清除编辑状态
|
||
form.resetFields(); // 重置表单
|
||
};
|
||
|
||
// 新增分类
|
||
const handleAdd = (type) => {
|
||
const newData = {
|
||
id: Date.now().toString(),
|
||
attributes: {
|
||
name: '',
|
||
type: type // 默认类型
|
||
},
|
||
isNew: true
|
||
};
|
||
setData([newData, ...data]);
|
||
setEditingKey(newData.id);
|
||
form.setFieldsValue(newData.attributes);
|
||
};
|
||
|
||
// 保存分类数据
|
||
const handleSave = async (record) => {
|
||
try {
|
||
const values = await form.validateFields();
|
||
if (record.isNew) {
|
||
await supabaseService.insert('resources', {
|
||
type: 'categories',
|
||
attributes: {
|
||
name: values.name,
|
||
type: values.type
|
||
},
|
||
schema_version: 1
|
||
});
|
||
} else {
|
||
// 更新
|
||
await supabaseService.update('resources',
|
||
{ id: record.id },
|
||
{
|
||
attributes: {
|
||
name: values.name,
|
||
type: values.type
|
||
},
|
||
updated_at: new Date().toISOString()
|
||
}
|
||
);
|
||
}
|
||
|
||
message.success('保存成功');
|
||
setEditingKey('');
|
||
fetchCategories();
|
||
} catch (error) {
|
||
message.error('保存失败');
|
||
console.error(error);
|
||
}
|
||
};
|
||
|
||
// 删除分类
|
||
const handleDelete = async (record) => {
|
||
try {
|
||
await supabaseService.delete('resources', { id: record.id });
|
||
message.success('删除成功');
|
||
fetchCategories();
|
||
} catch (error) {
|
||
message.error('删除失败');
|
||
console.error(error);
|
||
}
|
||
};
|
||
|
||
const columns = [
|
||
{
|
||
title: '分类名称',
|
||
dataIndex: ['attributes', 'name'],
|
||
render: (text, record) => {
|
||
const isEditing = record.id === editingKey;
|
||
return isEditing ? (
|
||
<Form.Item
|
||
name="name"
|
||
style={{ margin: 0 }}
|
||
rules={[{ required: true, message: '请输入分类名称!' }]}
|
||
>
|
||
<Input placeholder="请输入分类名称" />
|
||
</Form.Item>
|
||
) : (
|
||
<span className="text-gray-600">{text}</span>
|
||
);
|
||
},
|
||
},
|
||
{
|
||
title: '模板类型',
|
||
dataIndex: ['attributes', 'type'],
|
||
render: (text, record) => {
|
||
const isEditing = record.id === editingKey;
|
||
return isEditing ? (
|
||
<Form.Item
|
||
name="type"
|
||
style={{ margin: 0 }}
|
||
rules={[{ required: true, message: '请选择模板类型!' }]}
|
||
>
|
||
<Select
|
||
options={TEMPLATE_TYPES}
|
||
placeholder="请选择模板类型"
|
||
/>
|
||
</Form.Item>
|
||
) : (
|
||
<span className="text-gray-600">
|
||
{TEMPLATE_TYPES.find(t => t.value === text)?.label || text}
|
||
</span>
|
||
);
|
||
},
|
||
},
|
||
{
|
||
title: '操作',
|
||
render: (_, record) => {
|
||
const isEditing = record.id === editingKey;
|
||
return isEditing ? (
|
||
<Space>
|
||
<Button
|
||
type="link"
|
||
className="text-green-600 hover:text-green-500"
|
||
onClick={() => handleSave(record)}
|
||
>
|
||
保存
|
||
</Button>
|
||
<Button
|
||
type="link"
|
||
className="text-gray-600 hover:text-gray-500"
|
||
onClick={() => {
|
||
setEditingKey('');
|
||
if (record.isNew) {
|
||
setData(data.filter(item => item.id !== record.id));
|
||
}
|
||
}}
|
||
>
|
||
取消
|
||
</Button>
|
||
</Space>
|
||
) : (
|
||
<Space>
|
||
<Button
|
||
type="link"
|
||
className="text-blue-600 hover:text-blue-500"
|
||
disabled={editingKey !== ''}
|
||
onClick={() => {
|
||
setEditingKey(record.id);
|
||
form.setFieldsValue({
|
||
name: record.attributes.name,
|
||
type: record.attributes.type
|
||
});
|
||
}}
|
||
>
|
||
编辑
|
||
</Button>
|
||
<Popconfirm
|
||
title="确认<E7A1AE><E8AEA4><EFBFBD>除"
|
||
description="确定要删除这个分类吗?"
|
||
onConfirm={() => handleDelete(record)}
|
||
okText="确定"
|
||
cancelText="取消"
|
||
okButtonProps={{
|
||
className: "bg-red-500 hover:bg-red-600 border-red-500"
|
||
}}
|
||
>
|
||
<Button
|
||
type="link"
|
||
className="text-red-600 hover:text-red-500"
|
||
disabled={editingKey !== ''}
|
||
>
|
||
删除
|
||
</Button>
|
||
</Popconfirm>
|
||
</Space>
|
||
);
|
||
},
|
||
},
|
||
];
|
||
|
||
return (
|
||
<>
|
||
<Button
|
||
type="primary"
|
||
onClick={() => setDrawerVisible(true)}
|
||
icon={<PlusOutlined />}
|
||
>
|
||
分类管理
|
||
</Button>
|
||
|
||
<Drawer
|
||
title="分类管理"
|
||
placement="right"
|
||
width={800}
|
||
onClose={() => setDrawerVisible(false)}
|
||
open={drawerVisible}
|
||
className="category-drawer"
|
||
>
|
||
<div className="p-6">
|
||
{/* 类型选择器 */}
|
||
<div className="mb-6">
|
||
<Radio.Group
|
||
value={activeType}
|
||
onChange={(e) => handleTypeChange(e.target.value)}
|
||
className="flex space-x-4"
|
||
>
|
||
{TEMPLATE_TYPES.map(type => (
|
||
<Radio.Button
|
||
key={type.value}
|
||
value={type.value}
|
||
className={`flex items-center px-4 py-2 ${
|
||
activeType === type.value ? `bg-${type.color}-50 border-${type.color}-500` : ''
|
||
}`}
|
||
>
|
||
<span className="flex items-center space-x-2">
|
||
{type.icon}
|
||
<span>{type.label}</span>
|
||
<Badge
|
||
count={data.filter(item => item.attributes.type === type.value).length}
|
||
className={`bg-${type.color}-100 text-${type.color}-600`}
|
||
/>
|
||
</span>
|
||
</Radio.Button>
|
||
))}
|
||
</Radio.Group>
|
||
</div>
|
||
|
||
{/* 当前类型说明 */}
|
||
<div className="mb-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||
<h3 className={`text-${TEMPLATE_TYPES.find(t => t.value === activeType)?.color}-500 text-lg font-medium mb-2 flex items-center`}>
|
||
{TEMPLATE_TYPES.find(t => t.value === activeType)?.icon}
|
||
<span className="ml-2">{TEMPLATE_TYPES.find(t => t.value === activeType)?.label}</span>
|
||
</h3>
|
||
<p className="text-gray-500 dark:text-gray-400">
|
||
{TEMPLATE_TYPES.find(t => t.value === activeType)?.description}
|
||
</p>
|
||
</div>
|
||
|
||
{/* 新增按钮 */}
|
||
<Button
|
||
type="primary"
|
||
onClick={() => handleAdd(activeType)}
|
||
className="mb-4"
|
||
icon={<PlusOutlined />}
|
||
>
|
||
新增分类
|
||
</Button>
|
||
|
||
{/* 分类列表 */}
|
||
<Form form={form}>
|
||
<Table
|
||
scroll={{ x: true }}
|
||
columns={columns}
|
||
dataSource={data}
|
||
rowKey="id"
|
||
loading={loading}
|
||
pagination={{
|
||
pageSize: 10,
|
||
showTotal: (total) => `共 ${total} 条`,
|
||
}}
|
||
className="bg-white dark:bg-gray-800 rounded-lg shadow"
|
||
/>
|
||
</Form>
|
||
</div>
|
||
</Drawer>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default CategoryDrawer; |