feat:分类模块,单位模块完成

This commit is contained in:
liamzi
2024-12-27 18:10:20 +08:00
parent 8d2383a8a9
commit fb188374b2
15 changed files with 668 additions and 779 deletions

17
src/constants/typeEnum.js Normal file
View File

@@ -0,0 +1,17 @@
export const TEMPLATE_TYPES = [
{
value: 'quotation',
label: '报价单模板',
},
{
value: 'project',
label: '专案模板',
},
{
value: 'task',
label: '任务模板',
},{
value: 'common',
label: '通用',
}
];

View File

@@ -1,336 +0,0 @@
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;

View File

@@ -58,7 +58,7 @@ const QuotationTemplate = ({ id, isView, onCancel,isEdit }) => {
const { data } = await supabaseService.select('resources', { const { data } = await supabaseService.select('resources', {
filter: { filter: {
type: { eq: 'categories' }, type: { eq: 'categories' },
'attributes->>type': { eq: 'quotation' } 'attributes->>template_type': { eq: 'quotation' }
}, },
order: { order: {
column: 'created_at', column: 'created_at',

View File

@@ -61,7 +61,7 @@ const ServiceForm = () => {
<TemplateComponent <TemplateComponent
id={id} id={id}
isView={isView} isView={isView}
onCancel={() => navigate("/company/serviceTeamplate")} onCancel={() => navigate("/company/serviceTemplate")}
/> />
</Card> </Card>
</div> </div>

View File

@@ -66,7 +66,7 @@ const ServicePage = () => {
filter: { filter: {
type: { eq: "serviceTemplate" }, type: { eq: "serviceTemplate" },
...(selectedType ...(selectedType
? { "attributes->>type": { eq: selectedType } } ? { "attributes->>template_type": { eq: selectedType } }
: {}), : {}),
}, },
order: { order: {

View File

@@ -0,0 +1,288 @@
import React, { useState, useEffect } from 'react';
import { Table, Button, Form, Input, Space, message, Popconfirm, Drawer, Select, Segmented, Badge } from 'antd';
import { PlusOutlined, FileTextOutlined, ProjectOutlined, CheckSquareOutlined } from '@ant-design/icons';
import { supabaseService } from '@/hooks/supabaseService';
const Classify = ({activeType,typeList}) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [editingKey, setEditingKey] = useState('');
const [form] = Form.useForm();
const [filterType, setFilterType] = useState('all'); // 'all', 'common', 'current'
const fetchCategories = async (type = activeType, filterTypeValue = filterType) => {
setLoading(true);
try {
let filterCondition;
switch (filterTypeValue) {
case 'common':
filterCondition = { eq: 'common' };
break;
case 'current':
filterCondition = { eq: type };
break;
default:
filterCondition = { in: `(${type},common)` };
}
const { data: categories } = await supabaseService.select('resources', {
filter: {
'type': { eq: 'categories' },
'attributes->>template_type': filterCondition
},
order: {
column: 'created_at',
ascending: false
}
});
setData(categories || []);
} catch (error) {
message.error('获取分类数据失败');
console.error(error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchCategories(activeType, filterType);
}, [activeType]);
// 新增分类
const handleAdd = (type) => {
const newData = {
id: Date.now().toString(),
attributes: {
name: '',
template_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,
template_type: values.template_type
},
schema_version: 1
});
} else {
await supabaseService.update('resources',
{ id: record.id },
{
attributes: {
name: values.name,
template_type: values.template_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="请输入分类名称"
className="rounded-md"
/>
</Form.Item>
) : (
<span className="text-gray-700">{text}</span>
);
},
},
{
title: '模板类型',
dataIndex: ['attributes', 'template_type'],
render: (text, record) => {
const isEditing = record.id === editingKey;
return isEditing ? (
<Form.Item
name="template_type"
style={{ margin: 0 }}
rules={[{ required: true, message: '请选择模板类型!' }]}
>
<Select
options={typeList}
placeholder="请选择模板类型"
className="w-full rounded-md"
/>
</Form.Item>
) : (
<span className="text-gray-700 px-2 py-1 bg-gray-100 rounded-full text-sm">
{typeList.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 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,
template_type: record.attributes.template_type
});
}}
>
编辑
</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(activeType)}
icon={<PlusOutlined />}
className="bg-blue-600 hover:bg-blue-700 border-0 shadow-sm h-10"
>
新增分类
</Button>
</div>
<Segmented
value={filterType}
onChange={(value) => {
setFilterType(value);
fetchCategories(activeType, value);
}}
options={[
{
label: '全部',
value: 'all',
},
{
label: '仅当前类型',
value: 'current',
},
{
label: '仅通用类型',
value: 'common',
}
]}
className="bg-gray-50 p-1"
/>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm">
<Form form={form}>
<Table
scroll={{ x: true }}
columns={columns}
dataSource={data}
rowKey="id"
loading={loading}
pagination={{
pageSize: 10,
showTotal: (total) => `${total}`,
className: "px-4"
}}
className="rounded-lg"
/>
</Form>
</div>
</div>
);
};
export default Classify;

View File

@@ -0,0 +1,69 @@
import React, { useState, useMemo } from 'react';
import { Tabs, Card, Button, Table, Modal, Form, Input, Space, message, InputNumber, Select } from 'antd';
import { FileTextOutlined, ProjectOutlined, CheckSquareOutlined, PlusOutlined } from '@ant-design/icons';
import { supabase } from '@/config/supabase';
import Classify from './classify'
import Sections from './sections'
import Unit from './unit'
import {TEMPLATE_TYPES as typelist}from '@/constants/typeEnum'
const ResourceManagement = () => {
const [activeType, setActiveType] = useState('quotation');
// 模板类型配置
const TEMPLATE_TYPES = [
{
key: 'quotation',
label: '报价单模板',
icon: <FileTextOutlined className="text-blue-500" />,
},
{
key: 'project',
label: '专案模板',
icon: <ProjectOutlined className="text-green-500" />,
},
{
key: 'task',
label: '任务模板',
icon: <CheckSquareOutlined className="text-purple-500" />,
}
];
const filterOption =useMemo(()=>{
return typelist.filter(v=>v.value===activeType||v.value==='common')
},[activeType])
return (
<div className="p-6 bg-gray-50 min-h-screen">
<Tabs
activeKey={activeType}
onChange={setActiveType}
type="card"
className="bg-white rounded-lg shadow-sm"
items={TEMPLATE_TYPES.map(type => ({
key: type.key,
label: (
<span className="flex items-center gap-2">
{type.icon}
<span>{type.label}</span>
</span>
),
children: (
<div className="p-6">
<Card>
<Classify typeList={filterOption} activeType={activeType} setActiveType={setActiveType} />
<Unit typeList={filterOption} activeType={activeType} />
</Card>
</div>
)
}))}
/>
</div>
);
};
export default ResourceManagement;

View File

@@ -146,7 +146,6 @@ const SectionManagement = () => {
} }
}; };
// Drawer
const drawerColumns = [ const drawerColumns = [
{ {
title: '项目名称', title: '项目名称',

View File

@@ -0,0 +1,285 @@
import React, { useState, useEffect } from 'react';
import { Table, Button, Form, Input, Space, message, Popconfirm, Select, Segmented } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import { supabaseService } from '@/hooks/supabaseService';
const UnitManagement = ({ activeType, typeList }) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [editingKey, setEditingKey] = useState('');
const [form] = Form.useForm();
const [filterType, setFilterType] = useState('all');
const fetchUnits = async (type = activeType, filterTypeValue = filterType) => {
setLoading(true);
try {
let filterCondition;
switch (filterTypeValue) {
case 'common':
filterCondition = { eq: 'common' };
break;
case 'current':
filterCondition = { eq: type };
break;
default:
filterCondition = { in: `(${type},common)` };
}
const { data: units } = await supabaseService.select('resources', {
filter: {
'type': { eq: 'units' },
'attributes->>template_type': filterCondition
},
order: {
column: 'created_at',
ascending: false
}
});
setData(units || []);
} catch (error) {
message.error('获取单位数据失败');
console.error(error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchUnits(activeType, filterType);
}, [activeType]);
const handleAdd = () => {
const newData = {
id: Date.now().toString(),
attributes: {
name: '',
template_type: activeType
},
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: 'units',
attributes: {
name: values.name,
template_type: values.template_type
},
schema_version: 1
});
} else {
await supabaseService.update('resources',
{ id: record.id },
{
attributes: {
name: values.name,
template_type: values.template_type
},
updated_at: new Date().toISOString()
}
);
}
message.success('保存成功');
setEditingKey('');
fetchUnits();
} catch (error) {
message.error('保存失败');
console.error(error);
}
};
const handleDelete = async (record) => {
try {
await supabaseService.delete('resources', { id: record.id });
message.success('删除成功');
fetchUnits();
} 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="请输入单位名称"
className="rounded-md"
/>
</Form.Item>
) : (
<span className="text-gray-700">{text}</span>
);
},
},
{
title: '模板类型',
dataIndex: ['attributes', 'template_type'],
render: (text, record) => {
const isEditing = record.id === editingKey;
return isEditing ? (
<Form.Item
name="template_type"
style={{ margin: 0 }}
rules={[{ required: true, message: '请选择模板类型!' }]}
>
<Select
options={typeList}
placeholder="请选择模板类型"
className="w-full rounded-md"
/>
</Form.Item>
) : (
<span className="text-gray-700 px-2 py-1 bg-gray-100 rounded-full text-sm">
{typeList.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 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,
template_type: record.attributes.template_type
});
}}
>
编辑
</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>
<Segmented
value={filterType}
onChange={(value) => {
setFilterType(value);
fetchUnits(activeType, value);
}}
options={[
{
label: '全部',
value: 'all',
},
{
label: '仅当前类型',
value: 'current',
},
{
label: '仅通用类型',
value: 'common',
}
]}
className="bg-gray-50 p-1"
/>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm">
<Form form={form}>
<Table
scroll={{ x: true }}
columns={columns}
dataSource={data}
rowKey="id"
loading={loading}
pagination={{
pageSize: 10,
showTotal: (total) => `${total}`,
className: "px-4"
}}
className="rounded-lg"
/>
</Form>
</div>
</div>
);
};
export default UnitManagement;

View File

@@ -1,169 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Table, Button, Modal, Form, Input, Space, message } from 'antd';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
const ServiceType = () => {
const [data, setData] = useState([]);
const [visible, setVisible] = useState(false);
const [form] = Form.useForm();
const [editingId, setEditingId] = useState(null);
// 表格列定义
const columns = [
{
title: '服务类型',
dataIndex: 'serviceType',
key: 'serviceType',
},
{
title: '操作',
key: 'action',
render: (_, record) => (
<Space>
<Button type="link" onClick={() => handleEdit(record)}>
编辑
</Button>
<Button type="link" danger onClick={() => handleDelete(record.id)}>
删除
</Button>
</Space>
),
},
];
// 处理添加/编辑表单提交
const handleSubmit = async (values) => {
try {
if (editingId) {
// 编辑操作
// await updateServiceType({ ...values, id: editingId });
message.success('更新成功');
} else {
// 添加操作
// await addServiceType(values);
message.success('添加成功');
}
setVisible(false);
form.resetFields();
// 重新加载数据
// loadData();
} catch (error) {
message.error('操作失败');
}
};
// 编辑操作
const handleEdit = (record) => {
setEditingId(record.id);
form.setFieldsValue({
serviceType: record.serviceType,
details: record.details || [],
});
setVisible(true);
};
// 删除操作
const handleDelete = async (id) => {
try {
// await deleteServiceType(id);
message.success('删除成功');
// 重新加载数据
// loadData();
} catch (error) {
message.error('删除失败');
}
};
return (
<div>
<Button
type="primary"
onClick={() => {
setEditingId(null);
form.resetFields();
setVisible(true);
}}
style={{ marginBottom: 16 }}
>
<PlusOutlined /> 新增服务类型
</Button>
<Table scroll={{ x: true }} columns={columns} dataSource={data} rowKey="id" />
<Modal
title={editingId ? '编辑服务类型' : '新增服务类型'}
open={visible}
onOk={() => form.submit()}
onCancel={() => {
setVisible(false);
form.resetFields();
}}
width={800}
>
<Form form={form} onFinish={handleSubmit}>
<Form.Item
label="服务类型"
name="serviceType"
rules={[{ required: true, message: '请输入服务类型' }]}
>
<Input />
</Form.Item>
<Form.List name="details">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Space key={key} align="baseline">
<Form.Item
{...restField}
name={[name, 'projectDetail']}
rules={[{ required: true, message: '请输入项目明细' }]}
>
<Input placeholder="项目明细" />
</Form.Item>
<Form.Item
{...restField}
name={[name, 'description']}
rules={[{ required: true, message: '请输入描述' }]}
>
<Input placeholder="描述" />
</Form.Item>
<Form.Item
{...restField}
name={[name, 'quantity']}
rules={[{ required: true, message: '请输入数量' }]}
>
<Input placeholder="数量" type="number" />
</Form.Item>
<Form.Item
{...restField}
name={[name, 'unit']}
rules={[{ required: true, message: '请输入单位' }]}
>
<Input placeholder="单位" />
</Form.Item>
<Form.Item
{...restField}
name={[name, 'price']}
rules={[{ required: true, message: '请输入单价' }]}
>
<Input placeholder="单价" type="number" />
</Form.Item>
<DeleteOutlined onClick={() => remove(name)} />
</Space>
))}
<Form.Item>
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
添加项目明细
</Button>
</Form.Item>
</>
)}
</Form.List>
</Form>
</Modal>
</div>
);
};
export default ServiceType;

View File

@@ -1,261 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Table, Button, Drawer, Form, Input, Space, message, Popconfirm } from 'antd';
import { PlusOutlined, DeleteOutlined, SaveOutlined, CloseOutlined } from '@ant-design/icons';
import { supabase } from '@/config/supabase';
const UnitManagement = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [drawerVisible, setDrawerVisible] = useState(false);
const [editingKey, setEditingKey] = useState('');
const [form] = Form.useForm();
// 获取单位数据
const fetchUnits = async () => {
setLoading(true);
try {
const { data: units, error } = await supabase
.from('resources')
.select('*')
.eq('type', 'units')
.order('created_at', { ascending: false });
if (error) throw error;
setData(units || []);
} catch (error) {
message.error('获取单位数据失败');
console.error(error);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (drawerVisible) {
fetchUnits();
}
}, [drawerVisible]);
// 新增<E696B0><E5A29E><EFBFBD>
const handleAdd = () => {
const newData = {
id: Date.now().toString(),
attributes: { name: '' },
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) {
// 新增
const { error } = await supabase
.from('resources')
.insert([{
type: 'units',
attributes: {
name: values.name
},
schema_version: 1
}]);
if (error) throw error;
} else {
// 更新
const { error } = await supabase
.from('resources')
.update({
attributes: {
name: values.name
},
updated_at: new Date().toISOString()
})
.eq('id', record.id);
if (error) throw error;
}
message.success('保存成功');
setEditingKey('');
fetchUnits();
} catch (error) {
message.error('保存失败');
console.error(error);
}
};
// 删除单位
const handleDelete = async (id) => {
try {
const { error } = await supabase
.from('resources')
.delete()
.eq('id', id);
if (error) throw error;
message.success('删除成功');
fetchUnits();
} catch (error) {
message.error('删除失败');
console.error(error);
}
};
// 修改取消编辑的处理方法
const handleCancel = (record) => {
setEditingKey('');
if (record.isNew) {
// 如果是新增的记录,直接从数据中移除
setData(data.filter(item => item.id !== record.id));
}
form.resetFields();
};
const columns = [
{
title: '单位名称',
dataIndex: ['attributes', 'name'],
render: (text, record) => {
const isEditing = record.id === editingKey;
return isEditing ? (
<Form.Item
name="name"
rules={[{ required: true, message: '请输入单位名称!' }]}
style={{ margin: 0 }}
initialValue={text}
>
<Input placeholder="请输入单位名称" />
</Form.Item>
) : (
text
);
}
},
{
title: '操作',
render: (_, record) => {
const isEditing = record.id === editingKey;
return isEditing ? (
<Space>
<Button
type="link"
icon={<SaveOutlined />}
onClick={() => handleSave(record)}
className="text-green-600 hover:text-green-500"
>
保存
</Button>
<Button
type="link"
icon={<CloseOutlined />}
onClick={() => handleCancel(record)}
className="text-gray-600 hover:text-gray-500"
>
取消
</Button>
</Space>
) : (
<Space>
<Button
type="link"
disabled={editingKey !== ''}
onClick={() => {
setEditingKey(record.id);
form.setFieldsValue(record.attributes);
}}
className="text-blue-600 hover:text-blue-500 disabled:text-gray-400"
>
编辑
</Button>
<Popconfirm
title="确定要删除吗?"
onConfirm={() => handleDelete(record.id)}
okButtonProps={{
className: "bg-red-500 hover:bg-red-600 border-red-500"
}}
>
<Button
type="link"
danger
disabled={editingKey !== ''}
icon={<DeleteOutlined />}
className="text-red-600 hover:text-red-500 disabled:text-gray-400"
/>
</Popconfirm>
</Space>
);
}
}
];
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={600}
onClose={() => {
setDrawerVisible(false);
setEditingKey('');
form.resetFields();
}}
open={drawerVisible}
className="dark:bg-gray-800"
>
<div className="flex flex-col h-full">
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAdd}
disabled={editingKey !== ''}
className="mb-4 w-32 flex items-center justify-center"
>
新增单位
</Button>
<Form form={form} className="flex-1">
<Table
scroll={{ x: true }}
dataSource={data}
columns={columns}
rowKey="id"
loading={loading}
pagination={{
pageSize: 10,
className: "dark:text-gray-300"
}}
className="bg-white dark:bg-gray-800 rounded-lg shadow-sm"
rowClassName={(record) =>
`transition-colors ${
record.id === editingKey
? 'bg-blue-50 dark:bg-blue-900/20'
: 'hover:bg-gray-50 dark:hover:bg-gray-700/50'
}`
}
/>
</Form>
</div>
</Drawer>
</div>
);
};
export default UnitManagement;

View File

@@ -15,7 +15,7 @@ const NotFound = () => {
<Button <Button
type="primary" type="primary"
key="home" key="home"
onClick={() => navigate('/company/serviceTeamplate')} onClick={() => navigate('/company/serviceTemplate')}
> >
返回首页 返回首页
</Button>, </Button>,

View File

@@ -599,9 +599,7 @@ const StorageManager = () => {
return ( return (
<div className="flex h-screen bg-gray-50"> <div className="flex h-screen bg-gray-50">
{/* 左侧文件列表 */}
<div className="w-1/3 p-4 flex flex-col space-y-4"> <div className="w-1/3 p-4 flex flex-col space-y-4">
{/* 上传区域 */}
<div className="bg-white rounded-lg shadow-sm overflow-hidden"> <div className="bg-white rounded-lg shadow-sm overflow-hidden">
<Dragger <Dragger
{...uploadProps} {...uploadProps}

View File

@@ -54,7 +54,7 @@ const AppRoutes = () => {
<Route <Route
path="/login" path="/login"
element={ element={
user ? <Navigate to="/company/serviceTeamplate" replace /> : <Login /> user ? <Navigate to="/company/serviceTemplate" replace /> : <Login />
} }
/> />

View File

@@ -45,16 +45,15 @@ const companyRoutes = [
icon: "file", icon: "file",
}, },
{ {
path: "serviceTeamplate", path: "serviceTemplate",
component: lazy(() => import("@/pages/company/service")), component: lazy(() => import("@/pages/company/service")),
name: "服务管理", name: "服务管理",
icon: "container", icon: "container",
}, },
{ {
path: "serviceType", path: "templateItemManage",
hidden: true, component: lazy(() => import("@/pages/company/service/itemsManange")),
component: lazy(() => import("@/pages/company/service/serviceType")), name: "资源类型",
name: "类型管理",
icon: "container", icon: "container",
}, },
{ {