服务模块

This commit is contained in:
‘Liammcl’
2024-12-28 15:51:26 +08:00
parent 4f0e7be806
commit d021f39b04
8 changed files with 623 additions and 895 deletions

View File

@@ -1,127 +1,127 @@
import React, { useState, useEffect } from 'react';
import { Table, Button, Drawer, Modal, Form, Input, InputNumber, Space, message, Popconfirm, Select } from 'antd';
import { PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { supabase } from '@/config/supabase';
import {
Table,
Button,
Form,
Input,
Space,
message,
Popconfirm,
Select,
Segmented,
InputNumber,
Card,
Typography
} from 'antd';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
import { supabaseService } from '@/hooks/supabaseService';
import { v4 as uuidv4 } from 'uuid';
const SectionManagement = () => {
const { Text } = Typography;
const SectionsManagement = ({ activeType = 'quotation', typeList }) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [drawerVisible, setDrawerVisible] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [editingRecord, setEditingRecord] = useState(null);
const [editingKey, setEditingKey] = useState('');
const [form] = Form.useForm();
const [units, setUnits] = useState([]);
const [filterType, setFilterType] = useState('all');
// 获取子模块数据
const fetchSections = async () => {
const fetchSections = async (type = activeType, filterTypeValue = filterType) => {
setLoading(true);
try {
const { data: sections, error } = await supabase
.from('resources')
.select('*')
.eq('type', 'sections');
let filterCondition;
switch (filterTypeValue) {
case 'current':
filterCondition = { eq: type };
break;
default:
filterCondition = { eq: type };
}
if (error) throw error;
const { data: sections } = await supabaseService.select('resources', {
filter: {
'type': { eq: 'sections' },
'attributes->>template_type': filterCondition
},
order: {
column: 'created_at',
ascending: false
}
});
setData(sections || []);
} catch (error) {
message.error('获取模块数据失败');
message.error('获取模块数据失败');
console.error(error);
} finally {
setLoading(false);
}
};
// 获取单位数据
const fetchUnits = async () => {
try {
const { data: unitsData, error } = await supabase
.from('resources')
.select('*')
.eq('type', 'units')
.order('created_at', { ascending: false });
if (error) throw error;
setUnits(unitsData || []);
} catch (error) {
message.error('获取单位数据失败');
console.error(error);
}
};
useEffect(() => {
if (drawerVisible) {
fetchSections();
}
fetchUnits();
}, [drawerVisible]);
fetchSections(activeType, filterType);
}, [activeType]);
// 打开新增/编辑模态框
const showModal = async (record = null) => {
setModalVisible(true);
setEditingRecord(record);
if (record) {
try {
const { data: section, error } = await supabase
.from('resources')
.select('*')
.eq('id', record.id)
.single();
if (error) throw error;
form.setFieldsValue({
name: section.attributes.name,
items: section.attributes.items
});
} catch (error) {
message.error('获取子模块详情失败');
console.error(error);
}
} else {
form.setFieldsValue({
const handleAdd = () => {
const newData = {
id: Date.now().toString(),
attributes: {
name: '',
items: [{ name: '', description: '', price: 0, quantity: 1, unit: '' }]
});
}
template_type: activeType,
items: [{
key: uuidv4(),
name: '',
description: '',
quantity: 1,
price: 0,
unit: ''
}]
},
isNew: true
};
setData([newData, ...data]);
setEditingKey(newData.id);
form.setFieldsValue(newData.attributes);
};
// 保存子模块数据
const handleSave = async (values) => {
const handleSave = async (record) => {
try {
if (editingRecord) {
// 更新
const { error } = await supabase
.from('resources')
.update({
const values = await form.validateFields();
const items = form.getFieldValue(['items']) || [];
// 验证items数组
if (!items.length || !items.some(item => item.name)) {
message.error('请至少添加一个有效的服务项目');
return;
}
if (record.isNew) {
await supabaseService.insert('resources', {
type: 'sections',
attributes: {
name: values.name,
template_type: activeType,
items: items.filter(item => item.name), // 只保存有名称的项目
},
schema_version: 1
});
} else {
await supabaseService.update('resources',
{ id: record.id },
{
attributes: {
name: values.name,
items: values.items
template_type: activeType,
items: items.filter(item => item.name),
},
updated_at: new Date().toISOString()
})
.eq('id', editingRecord.id);
if (error) throw error;
} else {
// 新增
const { error } = await supabase
.from('resources')
.insert([{
type: 'sections',
attributes: {
name: values.name,
items: values.items
},
schema_version: 1
}]);
if (error) throw error;
}
);
}
message.success('保存成功');
setModalVisible(false);
form.resetFields();
setEditingKey('');
fetchSections();
} catch (error) {
message.error('保存失败');
@@ -129,15 +129,9 @@ const SectionManagement = () => {
}
};
// 删除子模块
const handleDelete = async (id) => {
const handleDelete = async (record) => {
try {
const { error } = await supabase
.from('resources')
.delete()
.eq('id', id);
if (error) throw error;
await supabaseService.delete('resources', { id: record.id });
message.success('删除成功');
fetchSections();
} catch (error) {
@@ -146,290 +140,226 @@ const SectionManagement = () => {
}
};
const drawerColumns = [
const columns = [
{
title: '项目名称',
dataIndex: 'name',
},
{
title: '描述',
dataIndex: 'description',
},
{
title: '单价',
dataIndex: 'price',
render: (price) => `¥${price}`
},
{
title: '数量',
dataIndex: 'quantity',
},
{
title: '单位',
dataIndex: 'unit',
}
];
// 添加模态框内表格列定义
const modalColumns = [
{
title: '项目名称',
dataIndex: 'name',
render: (_, __, index) => (
<Form.Item
name={[index, 'name']}
style={{ margin: 0 }}
>
<Input placeholder="项目名称" />
</Form.Item>
)
},
{
title: '描述',
dataIndex: 'description',
render: (_, __, index) => (
<Form.Item
name={[index, 'description']}
style={{ margin: 0 }}
>
<Input placeholder="描述" />
</Form.Item>
)
},
{
title: '单价',
dataIndex: 'price',
render: (_, __, index) => (
<Form.Item
name={[index, 'price']}
rules={[{ required: true, message: '请输入单价!' }]}
style={{ margin: 0 }}
>
<InputNumber
min={0}
placeholder="单价"
className="w-full"
/>
</Form.Item>
)
},
{
title: '数量',
dataIndex: 'quantity',
render: (_, __, index) => (
<Form.Item
name={[index, 'quantity']}
rules={[{ required: true, message: '请输入数量!' }]}
style={{ margin: 0 }}
>
<InputNumber
min={1}
placeholder="数量"
className="w-full"
/>
</Form.Item>
)
},
{
title: '单位',
dataIndex: 'unit',
render: (_, __, index) => (
<Form.Item
name={[index, 'unit']}
rules={[{ required: true, message: '请选择或输入单位!' }]}
style={{ margin: 0 }}
>
<Select
placeholder="请选择或输入单位"
className="w-full"
showSearch
allowClear
options={units.map(unit => ({
label: unit.attributes.name,
value: unit.attributes.name
}))}
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
/>
</Form.Item>
)
},
{
title: '操作',
render: (_, __, index, { remove }) => (
<Button
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => remove(index)}
/>
)
}
];
return (
<div>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setDrawerVisible(true)}
className="flex items-center"
>
子模块管理
</Button>
<Drawer
title={
<span className="text-lg font-medium text-gray-800 dark:text-gray-200">
子模块管理
</span>
}
placement="right"
width={1000}
onClose={() => setDrawerVisible(false)}
open={drawerVisible}
className="dark:bg-gray-800"
>
<div className="flex flex-col h-full">
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => showModal()}
className="mb-4 w-32 flex items-center justify-center"
>
新增子模块
</Button>
<div className="space-y-6">
{data.map((section) => (
<div
key={section.id}
className="bg-white rounded-lg shadow-sm dark:bg-gray-800 border border-gray-200 dark:border-gray-700"
>
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
<h3 className="text-lg font-medium text-gray-800 dark:text-gray-200">
{section.attributes.name}
</h3>
<Space>
<Button
type="link"
icon={<EditOutlined />}
onClick={() => showModal(section)}
className="text-blue-600 hover:text-blue-500"
>
编辑
</Button>
<Popconfirm
title="确定要删除吗?"
onConfirm={() => handleDelete(section.id)}
okButtonProps={{
className: "bg-red-500 hover:bg-red-600 border-red-500"
}}
>
<Button
type="link"
danger
icon={<DeleteOutlined />}
className="text-red-600 hover:text-red-500"
/>
</Popconfirm>
</Space>
</div>
<div className="p-4">
<Table
scroll={{ x: true }}
dataSource={section.attributes.items}
columns={drawerColumns}
pagination={false}
rowKey={(record, index) => `${section.id}-${index}`}
className="border dark:border-gray-700 rounded-lg"
rowClassName="hover:bg-gray-50 dark:hover:bg-gray-700/50"
size="small"
/>
</div>
</div>
))}
</div>
</div>
</Drawer>
<Modal
title={`${editingRecord ? '编辑' : '新增'}子模块`}
open={modalVisible}
onCancel={() => {
setModalVisible(false);
setEditingRecord(null);
form.resetFields();
}}
footer={null}
width={1200}
destroyOnClose={true}
>
<Form
form={form}
onFinish={handleSave}
layout="vertical"
className="mt-4"
>
title: '模块名称',
dataIndex: ['attributes', 'name'],
width: 200,
render: (text, record) => {
const isEditing = record.id === editingKey;
return isEditing ? (
<Form.Item
name="name"
label="子模块名称"
rules={[{ required: true, message: '请输入模块名称!' }]}
style={{ margin: 0 }}
rules={[{ required: true, message: '请输入模块名称!' }]}
>
<Input placeholder="请输入子模块名称" />
<Input
placeholder="请输入模块名称"
className="rounded-md"
/>
</Form.Item>
<Form.List name="items">
{(fields, { add, remove }) => (
<div className="bg-white rounded-lg border dark:bg-gray-800 dark:border-gray-700">
<Table
dataSource={fields}
columns={modalColumns.map(col => ({
...col,
render: (...args) => col.render(...args, { remove })
}))}
pagination={false}
rowKey="key"
className="mb-4"
/>
<div className="p-4 border-t dark:border-gray-700">
) : (
<span className="text-gray-700 font-medium">{text}</span>
);
},
},
{
title: '服务项目',
dataIndex: ['attributes', 'items'],
render: (items, record) => {
const isEditing = record.id === editingKey;
if (isEditing) {
return (
<Form.List name="items">
{(fields, { add, remove }) => (
<div className="space-y-2">
{fields.map((field, index) => (
<Card key={field.key} size="small" className="bg-gray-50">
<div className="grid grid-cols-6 gap-2">
<Form.Item
{...field}
name={[field.name, 'name']}
className="col-span-2 mb-0"
>
<Input placeholder="项目名称" />
</Form.Item>
<Form.Item
{...field}
name={[field.name, 'unit']}
className="mb-0"
>
<Input placeholder="单位" />
</Form.Item>
<Form.Item
{...field}
name={[field.name, 'quantity']}
className="mb-0"
>
<InputNumber
placeholder="数量"
min={0}
className="w-full"
/>
</Form.Item>
<Form.Item
{...field}
name={[field.name, 'price']}
className="mb-0"
>
<InputNumber
placeholder="单价"
min={0}
className="w-full"
/>
</Form.Item>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => remove(field.name)}
className="flex items-center justify-center"
/>
</div>
</Card>
))}
<Button
type="dashed"
onClick={() => add({
key: uuidv4(),
name: '',
description: '',
price: 0,
unit: '',
quantity: 1,
unit: ''
price: 0
})}
icon={<PlusOutlined />}
className="w-full hover:border-blue-400 hover:text-blue-500
dark:border-gray-600 dark:text-gray-400 dark:hover:text-blue-400"
className="w-full"
>
添加项目
<PlusOutlined /> 添加服务项目
</Button>
</div>
</div>
)}
</Form.List>
)}
</Form.List>
);
}
<div className="flex justify-end gap-4 mt-6">
<Button onClick={() => {
setModalVisible(false);
setEditingRecord(null);
form.resetFields();
}}>
取消
</Button>
<Button type="primary" htmlType="submit">
return (
<div className="space-y-1">
{(items || []).map((item, index) => (
<div key={index} className="flex justify-between text-sm">
<span className="text-gray-600">{item.name}</span>
<span className="text-gray-500">
{item.quantity} {item.unit} × ¥{item.price}
</span>
</div>
))}
</div>
);
},
},
{
title: '操作',
width: 160,
fixed: 'right',
render: (_, record) => {
const isEditing = record.id === editingKey;
return isEditing ? (
<Space>
<Button
type="link"
className="text-green-600 hover:text-green-500 font-medium"
onClick={() => handleSave(record)}
>
保存
</Button>
<Button
type="link"
className="text-gray-600 hover:text-gray-500 font-medium"
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 font-medium"
disabled={editingKey !== ''}
onClick={() => {
setEditingKey(record.id);
form.setFieldsValue({
name: record.attributes.name,
items: record.attributes.items
});
}}
>
编辑
</Button>
<Popconfirm
title="确认删除"
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 font-medium"
disabled={editingKey !== ''}
>
删除
</Button>
</Popconfirm>
</Space>
);
},
},
];
return (
<div className="p-6 bg-gray-50">
<div className="bg-white rounded-lg shadow-sm mb-6 p-4">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div className="flex items-center gap-2">
<Button
type="primary"
onClick={handleAdd}
icon={<PlusOutlined />}
className="bg-blue-600 hover:bg-blue-700 border-0 shadow-sm h-10"
>
新增模块
</Button>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm">
<Form form={form}>
<Table
scroll={{ x: 1200 }}
columns={columns}
dataSource={data}
rowKey="id"
loading={loading}
pagination={{
pageSize: 10,
showTotal: (total) => `${total}`,
className: "px-4"
}}
className="rounded-lg"
/>
</Form>
</Modal>
</div>
</div>
);
};
export default SectionManagement;
export default SectionsManagement;