任务模块

This commit is contained in:
‘Liammcl’
2024-12-28 19:23:54 +08:00
parent b7802abb2d
commit 4c9601b949
9 changed files with 1109 additions and 181 deletions

View File

@@ -18,7 +18,6 @@ const Header = ({ collapsed, setCollapsed }) => {
console.error("Logout error:", error);
}
};
console.log(user);
const userMenuItems = [
{

View File

@@ -572,7 +572,7 @@ const SectionList = ({
))}
</div>
<div className="flex items-center justify-center mt-6">
<div className="flex items-center justify-center mt-6 flex-col ">
{!isView && (
<div className="w-full flex justify-center">
<Button
@@ -589,8 +589,8 @@ const SectionList = ({
</div>
)}
<div className="flex w-fit flex-shrink-0 flex-col gap-1">
<span className="text-gray-500 flex items-center">
<div className="flex items-end w-full flex-shrink-0 flex-col gap-1">
<span className="text-gray-500 flex items-center ">
税前总额
<span className="text-blue-500 font-medium ml-2">
{formatExchangeRate(

View File

@@ -0,0 +1,583 @@
import React, { useState, useEffect, useMemo } from "react";
import {
Form,
Input,
DatePicker,
Button,
Card,
Typography,
Modal,
message,
Divider,
Select,
} from "antd";
import {
PlusOutlined,
DeleteOutlined,
EditOutlined,
CheckOutlined,
CloseOutlined,
} from "@ant-design/icons";
import { v4 as uuidv4 } from "uuid";
import { supabase } from "@/config/supabase";
import { supabaseService } from "@/hooks/supabaseService";
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import 'dayjs/locale/zh-cn';
dayjs.extend(duration);
dayjs.locale('zh-cn');
const { Text } = Typography;
const SectionList = ({ form, isView, formValues, type }) => {
const [editingSectionIndex, setEditingSectionIndex] = useState(null);
const [editingSectionName, setEditingSectionName] = useState("");
const [templateModalVisible, setTemplateModalVisible] = useState(false);
const [availableSections, setAvailableSections] = useState([]);
const [loading, setLoading] = useState(false);
const [units, setUnits] = useState([]);
const [loadingUnits, setLoadingUnits] = useState(false);
const fetchAvailableSections = async () => {
try {
setLoading(true);
const { data: sections, error } = await supabase
.from("resources")
.select("*")
.eq("type", "sections")
.eq("attributes->>template_type", [type])
.order("created_at", { ascending: false });
if (error) throw error;
setAvailableSections(sections || []);
} catch (error) {
message.error("获取小节模板失败");
console.error(error);
} finally {
setLoading(false);
}
};
const handleSectionNameEdit = (sectionIndex, initialValue) => {
setEditingSectionIndex(sectionIndex);
setEditingSectionName(initialValue || "");
};
const handleSectionNameSave = () => {
if (!editingSectionName.trim()) {
message.error("请输入名称");
return;
}
const sections = form.getFieldValue("sections");
const newSections = [...sections];
newSections[editingSectionIndex] = {
...newSections[editingSectionIndex],
sectionName: editingSectionName.trim(),
};
form.setFieldValue("sections", newSections);
setEditingSectionIndex(null);
setEditingSectionName("");
};
const handleAddItem = (add) => {
add({
key: uuidv4(),
name: "",
description: "",
timeRange: null,
unit: "",
});
};
const handleUseTemplate = (template, add) => {
const newSection = {
key: uuidv4(),
sectionName: template.attributes.name,
items: (template.attributes.items || []).map((item) => ({
key: uuidv4(),
name: item.name || "",
description: item.description || "",
timeRange: item.timeRange || null,
unit: item.unit || "",
})),
};
add(newSection);
setTemplateModalVisible(false);
message.success("套用模版成功");
};
const handleCreateCustom = (add, fieldsLength) => {
add({
key: uuidv4(),
sectionName: `任务类型 ${fieldsLength + 1}`,
items: [
{
key: uuidv4(),
name: "",
description: "",
timeRange: '',
unit: "",
},
],
});
setTemplateModalVisible(false);
};
const fetchUnits = async () => {
setLoadingUnits(true);
try {
const { data: units } = await supabaseService.select("resources", {
filter: {
type: { eq: "units" },
"attributes->>template_type": { in: `(${type})` },
},
order: {
column: "created_at",
ascending: false,
},
});
setUnits(units || []);
} catch (error) {
message.error("获取单位列表失败");
console.error(error);
} finally {
setLoadingUnits(false);
}
};
useEffect(() => {
fetchUnits();
}, []);
const handleAddUnit = async (unitName) => {
try {
const { error } = await supabase.from("resources").insert([
{
type: "units",
attributes: {
name: unitName,
template_type: type,
},
schema_version: 1,
},
]);
if (error) throw error;
message.success("新增单位成功");
fetchUnits();
return true;
} catch (error) {
message.error("新增单位失败");
console.error(error);
return false;
}
};
const calculateDuration = useMemo(() => (timeRange) => {
if (!timeRange || !timeRange[0] || !timeRange[1]) return '';
const start = dayjs(timeRange[0]);
const end = dayjs(timeRange[1]);
const durationObj = dayjs.duration(end.diff(start));
const days = Math.floor(durationObj.asDays());
const hours = durationObj.hours();
const minutes = durationObj.minutes();
let duration = '';
if (days > 0) duration += `${days}`;
if (hours > 0) duration += `${hours}小时`;
if (minutes > 0) duration += `${minutes}分钟`;
return duration || '小于1分钟';
}, []);
const renderTemplateModalContent = (add, fieldsLength) => (
<div className="space-y-6">
{availableSections.length > 0 ? (
<div className="flex flex-col">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{availableSections.map((section) => (
<div
key={section.id}
className="group relative bg-white rounded-lg shadow-sm border border-gray-200 hover:shadow-lg transition-all duration-300 cursor-pointer"
onClick={() => handleUseTemplate(section, add)}
>
<div className="p-6">
<div className="text-center mb-4">
<h3 className="text-lg font-medium group-hover:text-blue-500 transition-colors">
{section.attributes.name}
</h3>
<div className="text-sm text-gray-500 mt-1">
{section.attributes.items?.length || 0} 个项目
</div>
</div>
<div className="space-y-2 mt-4 border-t pt-4">
{(section.attributes.items || [])
.slice(0, 3)
.map((item, index) => (
<div
key={index}
className="flex justify-between items-center"
>
<span className="text-sm text-gray-600 truncate flex-1">
{item.name}
</span>
<span className="text-sm text-gray-500 ml-2">
</span>
</div>
))}
{(section.attributes.items || []).length > 3 && (
<div className="text-sm text-gray-500 text-center">
还有 {section.attributes.items.length - 3} 个项目...
</div>
)}
</div>
</div>
</div>
))}
</div>
<div className="flex justify-center">
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => handleCreateCustom(add, fieldsLength)}
className="bg-blue-600 hover:bg-blue-700 border-0 shadow-md hover:shadow-lg transition-all duration-200 h-10 px-6 rounded-lg flex items-center gap-2"
>
<span className="font-medium">自定义模块</span>
</Button>
</div>
</div>
) : (
<div className="flex flex-col items-center justify-center py-16 px-4">
<div className="w-48 h-48 mb-8">
<svg
className="w-full h-full text-gray-200"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1}
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg>
</div>
<h3 className="text-xl font-medium text-gray-900 mb-2">
暂无可用模板
</h3>
<p className="text-gray-500 text-center max-w-sm mb-8">
您可以选择创建一个自定义模块开始使用
</p>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => handleCreateCustom(add, fieldsLength)}
size="large"
className="shadow-md hover:shadow-lg transition-shadow"
>
创建自定义模块
</Button>
</div>
)}
</div>
);
return (
<>
<Form.List name="sections">
{(fields, { add, remove }) => (
<>
<div className="space-y-4 overflow-auto">
{fields.map((field, sectionIndex) => (
<Card
key={field.key}
className="shadow-sm rounded-lg min-w-[1200px]"
type="inner"
title={
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{editingSectionIndex === sectionIndex ? (
<div className="flex items-center gap-2">
<Input
value={editingSectionName}
onChange={(e) =>
setEditingSectionName(e.target.value)
}
onPressEnter={handleSectionNameSave}
autoFocus
className="w-48"
/>
<Button
type="link"
icon={<CheckOutlined />}
onClick={handleSectionNameSave}
className="text-green-500"
/>
<Button
type="link"
icon={<CloseOutlined />}
onClick={() => {
setEditingSectionIndex(null);
setEditingSectionName("");
}}
className="text-red-500"
/>
</div>
) : (
<div className="flex items-center gap-2">
<span className="w-1 h-4 bg-purple-500 rounded-full" />
<Text strong className="text-lg">
{form.getFieldValue([
"sections",
sectionIndex,
"sectionName",
]) || `任务类型 ${sectionIndex + 1}`}
</Text>
{!isView && (
<Button
type="link"
icon={<EditOutlined />}
onClick={() =>
handleSectionNameEdit(
sectionIndex,
form.getFieldValue([
"sections",
sectionIndex,
"sectionName",
])
)
}
className="text-gray-400 hover:text-blue-500"
/>
)}
</div>
)}
</div>
{!isView && (
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => remove(field.name)}
/>
)}
</div>
}
>
<Form.List name={[field.name, "items"]}>
{(itemFields, { add: addItem, remove: removeItem }) => (
<>
<div className="grid grid-cols-[2fr_2fr_3fr_1.5fr_1fr_1fr] gap-4 mb-2 text-gray-500 px-2">
<div>任务名称</div>
<div>描述/备注</div>
<div>开始/结束时间</div>
<div className="text-center">时间范围</div>
<div className="text-center">状态</div>
<div className="text-center">操作</div>
</div>
{itemFields.map((itemField, itemIndex) => {
const { key, ...restItemField } = itemField;
return (
<div
key={key}
className="grid grid-cols-[2fr_2fr_3fr_1.5fr_1fr_1fr] gap-4 mb-4 items-start"
>
<Form.Item
{...restItemField}
name={[itemField.name, "name"]}
className="!mb-0"
>
<Input placeholder="任务名称" />
</Form.Item>
<Form.Item
{...restItemField}
name={[itemField.name, "description"]}
className="!mb-0"
>
<Input placeholder="请输入描述/备注" />
</Form.Item>
<div className="flex items-center gap-2">
<Form.Item
{...restItemField}
name={[itemField.name, "timeRange"]}
className="!mb-0 flex-1"
getValueFromEvent={(dates) => {
if (!dates) return null;
return [
dates[0].format('YYYY-MM-DD HH:mm'),
dates[1].format('YYYY-MM-DD HH:mm')
];
}}
getValueProps={(value) => {
if (!value) return {};
return {
value: [
dayjs(value[0]),
dayjs(value[1])
]
};
}}
>
<DatePicker.RangePicker
showTime
format="YYYY-MM-DD HH:mm"
placeholder={["开始时间", "结束时间"]}
className="w-full"
disabled={isView}
showNow
showToday
allowClear
disabledDate={(current) => {
return current && current < dayjs().startOf('day');
}}
/>
</Form.Item>
</div>
<span className="text-gray-500 text-center whitespace-nowrap">
{calculateDuration(
form.getFieldValue([
"sections",
field.name,
"items",
itemField.name,
"timeRange",
])
)}
</span>
<Form.Item
{...restItemField}
name={[itemField.name, "unit"]}
className="!mb-0"
>
<Select
placeholder="选择状态"
loading={loadingUnits}
showSearch
allowClear
style={{ minWidth: "120px" }}
options={units.map((unit) => ({
label: unit.attributes.name,
value: unit.attributes.name,
}))}
onDropdownVisibleChange={(open) => {
if (open) fetchUnits();
}}
dropdownRender={(menu) => (
<>
{menu}
<Divider style={{ margin: "12px 0" }} />
<div style={{ padding: "4px" }}>
<Input.Search
placeholder="输入新状态"
enterButton={<PlusOutlined />}
onSearch={async (value) => {
if (!value.trim()) return;
if (
await handleAddUnit(value.trim())
) {
const currentItems =
form.getFieldValue([
"sections",
field.name,
"items",
]);
currentItems[
itemField.name
].unit = value.trim();
form.setFieldValue(
[
"sections",
field.name,
"items",
],
currentItems
);
}
}}
/>
</div>
</>
)}
/>
</Form.Item>
{!isView && itemFields.length > 1 && (
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => removeItem(itemField.name)}
className="flex items-center justify-center"
/>
)}
</div>
);
})}
{!isView && (
<Button
type="dashed"
onClick={() => handleAddItem(addItem)}
icon={<PlusOutlined />}
className="w-full hover:border-blue-400 hover:text-blue-500 mb-4"
>
添加子任务
</Button>
)}
</>
)}
</Form.List>
</Card>
))}
</div>
<div className="flex items-center justify-center mt-6 flex-col ">
{!isView && (
<div className="w-full flex justify-center">
<Button
type="dashed"
onClick={() => {
setTemplateModalVisible(true);
fetchAvailableSections();
}}
icon={<PlusOutlined />}
className="w-1/3 border-2 hover:border-blue-400 hover:text-blue-500"
>
新建任务
</Button>
</div>
)}
</div>
<Modal
title={<h3 className="text-lg font-medium">选择模版</h3>}
open={templateModalVisible}
onCancel={() => setTemplateModalVisible(false)}
footer={null}
width={800}
closeIcon={<CloseOutlined className="text-gray-500" />}
>
{renderTemplateModalContent(add, fields.length)}
</Modal>
</>
)}
</Form.List>
</>
);
};
export default SectionList;