任务模块
This commit is contained in:
@@ -1,18 +1,217 @@
|
||||
import React from 'react';
|
||||
import { Form, Card } from 'antd';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Form, Card, Input, Select, message,Button } from 'antd';
|
||||
import { supabaseService } from '@/hooks/supabaseService';
|
||||
import {ArrowLeftOutlined}from '@ant-design/icons'
|
||||
import TaskList from '@/components/TaskList';
|
||||
const TYPE = 'task'
|
||||
const TaskTemplate = ({ id, isView, onCancel,isEdit }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [formValues, setFormValues] = useState({
|
||||
sections: [{ items: [{}] }],
|
||||
});
|
||||
const [categories, setCategories] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (id) {
|
||||
fetchServiceTemplate();
|
||||
}
|
||||
fetchCategories();
|
||||
}, [id]);
|
||||
|
||||
const fetchServiceTemplate = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await supabaseService.select('resources', {
|
||||
filter: {
|
||||
id: { eq: id },
|
||||
type: { eq: 'serviceTemplate' },
|
||||
'attributes->>template_type': { eq: TYPE }
|
||||
}
|
||||
});
|
||||
|
||||
if (data?.[0]) {
|
||||
const formData = {
|
||||
templateName: data[0].attributes.templateName,
|
||||
description: data[0].attributes.description,
|
||||
category: data[0].attributes.category?.map(v => v.id) || [],
|
||||
sections: data[0].attributes.sections || [{ items: [{}] }],
|
||||
currency: data[0].attributes.currency || "CNY",
|
||||
};
|
||||
form.setFieldsValue(formData);
|
||||
setFormValues(formData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取服务模版失败:", error);
|
||||
message.error("获取服务模版失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const { data } = await supabaseService.select('resources', {
|
||||
filter: {
|
||||
type: { eq: 'categories' },
|
||||
'attributes->>template_type': { in: `(${TYPE},common)` }
|
||||
},
|
||||
order: {
|
||||
column: 'created_at',
|
||||
ascending: false
|
||||
}
|
||||
});
|
||||
|
||||
const formattedCategories = (data || []).map(category => ({
|
||||
value: category.id,
|
||||
label: category.attributes.name
|
||||
}));
|
||||
|
||||
setCategories(formattedCategories);
|
||||
} catch (error) {
|
||||
message.error('获取分类数据失败');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleValuesChange = (changedValues, allValues) => {
|
||||
setFormValues(allValues);
|
||||
};
|
||||
|
||||
const onFinish = async (values) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const categoryData = values.category.map(categoryId => {
|
||||
const category = categories.find(c => c.value === categoryId);
|
||||
return {
|
||||
id: categoryId,
|
||||
name: category.label
|
||||
};
|
||||
});
|
||||
const serviceData = {
|
||||
type: "serviceTemplate",
|
||||
attributes: {
|
||||
template_type: TYPE,
|
||||
templateName: values.templateName,
|
||||
description: values.description,
|
||||
sections: values.sections,
|
||||
category: categoryData,
|
||||
},
|
||||
};
|
||||
|
||||
if (id) {
|
||||
await supabaseService.update('resources',
|
||||
{ id },
|
||||
serviceData
|
||||
);
|
||||
} else {
|
||||
// 新增
|
||||
await supabaseService.insert('resources', serviceData);
|
||||
}
|
||||
|
||||
message.success("保存成功");
|
||||
onCancel();
|
||||
} catch (error) {
|
||||
console.error("保存失败:", error);
|
||||
message.error("保存失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const TaskTemplate = ({ form, id, isView }) => {
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
disabled={isView}
|
||||
>
|
||||
{/* 任务模板特有的字段和组件 */}
|
||||
<Card>
|
||||
{/* 任务步骤、检查项等内容 */}
|
||||
</Card>
|
||||
</Form>
|
||||
<>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
onValuesChange={handleValuesChange}
|
||||
layout="vertical"
|
||||
variant="filled"
|
||||
disabled={isView}
|
||||
>
|
||||
<Card
|
||||
className="shadow-sm rounded-lg mb-6"
|
||||
type="inner"
|
||||
title={
|
||||
<span className="flex items-center space-x-2 text-gray-700">
|
||||
<span className="w-1 h-4 bg-blue-500 rounded-full" />
|
||||
<span>基本信息</span>
|
||||
</span>
|
||||
}
|
||||
bordered={false}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<Form.Item
|
||||
label="模版名称"
|
||||
name="templateName"
|
||||
rules={[{ required: true, message: "请输入模版名称" }]}
|
||||
>
|
||||
<Input placeholder="请输入模版名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="模版分类"
|
||||
name="category"
|
||||
rules={[{ required: true, message: "请选择或输入分类" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择或输入分类"
|
||||
showSearch
|
||||
allowClear
|
||||
mode="tags"
|
||||
options={categories}
|
||||
loading={loading}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="模版描述"
|
||||
name="description"
|
||||
className="col-span-2"
|
||||
>
|
||||
<Input.TextArea rows={4} placeholder="请输入模版描述" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
className="shadow-sm rounded-lg"
|
||||
type="inner"
|
||||
title={
|
||||
<span className="flex items-center space-x-2 text-gray-700">
|
||||
<span className="w-1 h-4 bg-blue-500 rounded-full" />
|
||||
<span>服务明细</span>
|
||||
</span>
|
||||
}
|
||||
bordered={false}
|
||||
>
|
||||
<TaskList
|
||||
type={TYPE}
|
||||
form={form}
|
||||
isView={isView}
|
||||
formValues={formValues}
|
||||
onValuesChange={handleValuesChange}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
</Form>
|
||||
<div className="flex justify-end pt-4 space-x-4">
|
||||
<Button
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={onCancel}
|
||||
>
|
||||
返回
|
||||
</Button>
|
||||
{!isView && (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => form.submit()}
|
||||
loading={loading}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -224,8 +224,295 @@ const ServicePage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 子表格列定义
|
||||
const table_warp=(record,section)=>{
|
||||
|
||||
switch(record.attributes.template_type){
|
||||
case 'quotation':
|
||||
return [
|
||||
{
|
||||
title: "名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "15%",
|
||||
render: (text, item, index) => {
|
||||
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
|
||||
return isEditing ? (
|
||||
<Input
|
||||
defaultValue={text}
|
||||
onChange={(e) => {
|
||||
setEditingItem((prev) => ({
|
||||
...prev || item,
|
||||
name: e.target.value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span>{text}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "unit",
|
||||
key: "unit",
|
||||
width: "10%",
|
||||
render: (text, item, index) => {
|
||||
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
|
||||
return isEditing ? (
|
||||
<Input
|
||||
defaultValue={text}
|
||||
onChange={(e) => {
|
||||
setEditingItem((prev) => ({
|
||||
...prev || item,
|
||||
unit: e.target.value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span>{text}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "单价",
|
||||
dataIndex: "price",
|
||||
key: "price",
|
||||
width: "10%",
|
||||
render: (text, item, index) => {
|
||||
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
|
||||
return isEditing ? (
|
||||
<InputNumber
|
||||
defaultValue={text}
|
||||
onChange={(value) => {
|
||||
setEditingItem((prev) => ({
|
||||
...prev || item,
|
||||
price: value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span>{text}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "数量",
|
||||
dataIndex: "quantity",
|
||||
key: "quantity",
|
||||
width: "10%",
|
||||
render: (text, item, index) => {
|
||||
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
|
||||
return isEditing ? (
|
||||
<InputNumber
|
||||
defaultValue={text}
|
||||
onChange={(value) => {
|
||||
setEditingItem((prev) => ({
|
||||
...prev || item,
|
||||
quantity: value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span>{text}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
render: (text, item, index) => {
|
||||
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
|
||||
return isEditing ? (
|
||||
<Input
|
||||
defaultValue={text}
|
||||
onChange={(e) => {
|
||||
setEditingItem((prev) => ({
|
||||
...prev || item,
|
||||
description: e.target.value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span>{text}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 150,
|
||||
render: (_, item, index) => {
|
||||
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
|
||||
return (
|
||||
<Space>
|
||||
{isEditing ? (
|
||||
<>
|
||||
<SaveOutlined
|
||||
className="text-green-600 cursor-pointer"
|
||||
onClick={() => handleSave(record, section.key, index)}
|
||||
/>
|
||||
<CloseOutlined
|
||||
className="text-gray-600 cursor-pointer"
|
||||
onClick={() => {
|
||||
setEditingKey("");
|
||||
setEditingItem(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<EditOutlined
|
||||
className="text-blue-600 cursor-pointer"
|
||||
onClick={() => {
|
||||
setEditingKey(`${record.id}-${section.key}-${index}`);
|
||||
setEditingItem(item); // 初始化编辑项
|
||||
}}
|
||||
/>
|
||||
<Popconfirm
|
||||
title="确定要删除这一项吗?"
|
||||
onConfirm={() => handleDeleteItem(record, section.key, index)}
|
||||
>
|
||||
<DeleteOutlined className="text-red-500 cursor-pointer" />
|
||||
</Popconfirm>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
case 'task':
|
||||
return [
|
||||
{
|
||||
title: "名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "15%",
|
||||
render: (text, item, index) => {
|
||||
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
|
||||
return isEditing ? (
|
||||
<Input
|
||||
defaultValue={text}
|
||||
onChange={(e) => {
|
||||
setEditingItem((prev) => ({
|
||||
...prev || item,
|
||||
name: e.target.value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span>{text}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "执行状态",
|
||||
dataIndex: "unit",
|
||||
key: "unit",
|
||||
render: (text, item, index) => {
|
||||
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
|
||||
return isEditing ? (
|
||||
<Input
|
||||
defaultValue={text}
|
||||
onChange={(e) => {
|
||||
setEditingItem((prev) => ({
|
||||
...prev || item,
|
||||
unit: e.target.value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span>{text}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title: "开始时间",
|
||||
dataIndex: "timeRange",
|
||||
key: "startTime",
|
||||
render: (timeRange) => timeRange?.[0] || '-',
|
||||
},
|
||||
{
|
||||
title: "结束时间",
|
||||
dataIndex: "timeRange",
|
||||
key: "endTime",
|
||||
render: (timeRange) => timeRange?.[1] || '-',
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
render: (text, item, index) => {
|
||||
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
|
||||
return isEditing ? (
|
||||
<Input
|
||||
defaultValue={text}
|
||||
onChange={(e) => {
|
||||
setEditingItem((prev) => ({
|
||||
...prev || item,
|
||||
description: e.target.value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span>{text}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
render: (_, item, index) => {
|
||||
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
|
||||
return (
|
||||
<Space>
|
||||
{isEditing ? (
|
||||
<>
|
||||
<SaveOutlined
|
||||
className="text-green-600 cursor-pointer"
|
||||
onClick={() => handleSave(record, section.key, index)}
|
||||
/>
|
||||
<CloseOutlined
|
||||
className="text-gray-600 cursor-pointer"
|
||||
onClick={() => {
|
||||
setEditingKey("");
|
||||
setEditingItem(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* <EditOutlined
|
||||
className="text-blue-600 cursor-pointer"
|
||||
onClick={() => {
|
||||
setEditingKey(`${record.id}-${section.key}-${index}`);
|
||||
setEditingItem(item); // 初始化编辑项
|
||||
}}
|
||||
/> */}
|
||||
<Popconfirm
|
||||
title="确定要删除这一项吗?"
|
||||
onConfirm={() => handleDeleteItem(record, section.key, index)}
|
||||
>
|
||||
<DeleteOutlined className="text-red-500 cursor-pointer" />
|
||||
</Popconfirm>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
case "project":
|
||||
default:[]
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
const expandedRowRender = (record) => {
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50 dark:bg-gray-600 p-4 rounded-lg">
|
||||
{record.attributes.sections.map((section) => (
|
||||
@@ -240,164 +527,12 @@ const ServicePage = () => {
|
||||
</Popconfirm>
|
||||
</div>
|
||||
<Table
|
||||
scroll={{x:true}}
|
||||
dataSource={section.items}
|
||||
pagination={false}
|
||||
size="small"
|
||||
className="nested-items-table border-t border-gray-100"
|
||||
columns={[
|
||||
{
|
||||
title: "名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "15%",
|
||||
render: (text, item, index) => {
|
||||
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
|
||||
return isEditing ? (
|
||||
<Input
|
||||
defaultValue={text}
|
||||
onChange={(e) => {
|
||||
setEditingItem((prev) => ({
|
||||
...prev || item,
|
||||
name: e.target.value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span>{text}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "单位",
|
||||
dataIndex: "unit",
|
||||
key: "unit",
|
||||
width: "10%",
|
||||
render: (text, item, index) => {
|
||||
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
|
||||
return isEditing ? (
|
||||
<Input
|
||||
defaultValue={text}
|
||||
onChange={(e) => {
|
||||
setEditingItem((prev) => ({
|
||||
...prev || item,
|
||||
unit: e.target.value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span>{text}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "单价",
|
||||
dataIndex: "price",
|
||||
key: "price",
|
||||
width: "10%",
|
||||
render: (text, item, index) => {
|
||||
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
|
||||
return isEditing ? (
|
||||
<InputNumber
|
||||
defaultValue={text}
|
||||
onChange={(value) => {
|
||||
setEditingItem((prev) => ({
|
||||
...prev || item,
|
||||
price: value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span>{text}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "数量",
|
||||
dataIndex: "quantity",
|
||||
key: "quantity",
|
||||
width: "10%",
|
||||
render: (text, item, index) => {
|
||||
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
|
||||
return isEditing ? (
|
||||
<InputNumber
|
||||
defaultValue={text}
|
||||
onChange={(value) => {
|
||||
setEditingItem((prev) => ({
|
||||
...prev || item,
|
||||
quantity: value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span>{text}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
render: (text, item, index) => {
|
||||
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
|
||||
return isEditing ? (
|
||||
<Input
|
||||
defaultValue={text}
|
||||
onChange={(e) => {
|
||||
setEditingItem((prev) => ({
|
||||
...prev || item,
|
||||
description: e.target.value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span>{text}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 150,
|
||||
render: (_, item, index) => {
|
||||
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
|
||||
return (
|
||||
<Space>
|
||||
{isEditing ? (
|
||||
<>
|
||||
<SaveOutlined
|
||||
className="text-green-600 cursor-pointer"
|
||||
onClick={() => handleSave(record, section.key, index)}
|
||||
/>
|
||||
<CloseOutlined
|
||||
className="text-gray-600 cursor-pointer"
|
||||
onClick={() => {
|
||||
setEditingKey("");
|
||||
setEditingItem(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<EditOutlined
|
||||
className="text-blue-600 cursor-pointer"
|
||||
onClick={() => {
|
||||
setEditingKey(`${record.id}-${section.key}-${index}`);
|
||||
setEditingItem(item); // 初始化编辑项
|
||||
}}
|
||||
/>
|
||||
<Popconfirm
|
||||
title="确定要删除这一项吗?"
|
||||
onConfirm={() => handleDeleteItem(record, section.key, index)}
|
||||
>
|
||||
<DeleteOutlined className="text-red-500 cursor-pointer" />
|
||||
</Popconfirm>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
columns={table_warp(record,section)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@@ -411,7 +546,7 @@ const ServicePage = () => {
|
||||
|
||||
const path = id ? `${basePath}/${id}` : basePath;
|
||||
if (mode === "create") {
|
||||
navigate(`${basePath}`);
|
||||
navigate(`${basePath}?type=${type}`);
|
||||
} else {
|
||||
navigate(`${basePath}/${id}?type=${type}&${mode}=true`);
|
||||
}
|
||||
@@ -589,7 +724,6 @@ const ServicePage = () => {
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
{/* 添加模板类型选择弹窗 */}
|
||||
<TemplateTypeModal
|
||||
open={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
|
||||
Reference in New Issue
Block a user