-
+
+
税前总额:
{formatExchangeRate(
diff --git a/src/components/TaskList/index.jsx b/src/components/TaskList/index.jsx
new file mode 100644
index 0000000..4c2d6d2
--- /dev/null
+++ b/src/components/TaskList/index.jsx
@@ -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) => (
+
+ {availableSections.length > 0 ? (
+
+
+ {availableSections.map((section) => (
+
handleUseTemplate(section, add)}
+ >
+
+
+
+ {section.attributes.name}
+
+
+ {section.attributes.items?.length || 0} 个项目
+
+
+
+
+ {(section.attributes.items || [])
+ .slice(0, 3)
+ .map((item, index) => (
+
+
+ {item.name}
+
+
+
+
+
+ ))}
+ {(section.attributes.items || []).length > 3 && (
+
+ 还有 {section.attributes.items.length - 3} 个项目...
+
+ )}
+
+
+
+
+
+ ))}
+
+
+
+ }
+ 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"
+ >
+ 自定义模块
+
+
+
+ ) : (
+
+
+
+ 暂无可用模板
+
+
+ 您可以选择创建一个自定义模块开始使用
+
+
}
+ onClick={() => handleCreateCustom(add, fieldsLength)}
+ size="large"
+ className="shadow-md hover:shadow-lg transition-shadow"
+ >
+ 创建自定义模块
+
+
+ )}
+
+ );
+
+ return (
+ <>
+
+ {(fields, { add, remove }) => (
+ <>
+
+ {fields.map((field, sectionIndex) => (
+
+
+ {editingSectionIndex === sectionIndex ? (
+
+
+ setEditingSectionName(e.target.value)
+ }
+ onPressEnter={handleSectionNameSave}
+ autoFocus
+ className="w-48"
+ />
+ }
+ onClick={handleSectionNameSave}
+ className="text-green-500"
+ />
+ }
+ onClick={() => {
+ setEditingSectionIndex(null);
+ setEditingSectionName("");
+ }}
+ className="text-red-500"
+ />
+
+ ) : (
+
+
+
+ {form.getFieldValue([
+ "sections",
+ sectionIndex,
+ "sectionName",
+ ]) || `任务类型 ${sectionIndex + 1}`}
+
+ {!isView && (
+ }
+ onClick={() =>
+ handleSectionNameEdit(
+ sectionIndex,
+ form.getFieldValue([
+ "sections",
+ sectionIndex,
+ "sectionName",
+ ])
+ )
+ }
+ className="text-gray-400 hover:text-blue-500"
+ />
+ )}
+
+ )}
+
+ {!isView && (
+ }
+ onClick={() => remove(field.name)}
+ />
+ )}
+
+ }
+ >
+
+ {(itemFields, { add: addItem, remove: removeItem }) => (
+ <>
+
+
任务名称
+
描述/备注
+
开始/结束时间
+
时间范围
+
状态
+
操作
+
+
+ {itemFields.map((itemField, itemIndex) => {
+ const { key, ...restItemField } = itemField;
+ return (
+
+
+
+
+
+
+
+
+
{
+ 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])
+ ]
+ };
+ }}
+ >
+ {
+ return current && current < dayjs().startOf('day');
+ }}
+
+ />
+
+
+
+
+
+ {calculateDuration(
+ form.getFieldValue([
+ "sections",
+ field.name,
+ "items",
+ itemField.name,
+ "timeRange",
+ ])
+ )}
+
+
+
+
+
+
+ {!isView && itemFields.length > 1 && (
+
}
+ onClick={() => removeItem(itemField.name)}
+ className="flex items-center justify-center"
+ />
+ )}
+
+ );
+ })}
+
+ {!isView && (
+
+ )}
+ >
+ )}
+
+
+ ))}
+
+
+
+ {!isView && (
+
+
+
+ )}
+
+
+ 选择模版}
+ open={templateModalVisible}
+ onCancel={() => setTemplateModalVisible(false)}
+ footer={null}
+ width={800}
+ closeIcon={}
+ >
+ {renderTemplateModalContent(add, fields.length)}
+
+ >
+ )}
+
+ >
+ );
+};
+
+export default SectionList;
diff --git a/src/pages/company/service/detail/components/TaskTemplate.jsx b/src/pages/company/service/detail/components/TaskTemplate.jsx
index d1a11da..f3b5578 100644
--- a/src/pages/company/service/detail/components/TaskTemplate.jsx
+++ b/src/pages/company/service/detail/components/TaskTemplate.jsx
@@ -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 (
-
+ <>
+
+ }
+ bordered={false}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 服务明细
+
+ }
+ bordered={false}
+ >
+
+
+
+
+
+ }
+ onClick={onCancel}
+ >
+ 返回
+
+ {!isView && (
+
+ )}
+
+ >
);
};
diff --git a/src/pages/company/service/index.jsx b/src/pages/company/service/index.jsx
index f24c346..edd8bb5 100644
--- a/src/pages/company/service/index.jsx
+++ b/src/pages/company/service/index.jsx
@@ -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 ? (
+
{
+ setEditingItem((prev) => ({
+ ...prev || item,
+ name: e.target.value
+ }));
+ }}
+ />
+ ) : (
+
{text}
+ );
+ },
+ },
+ {
+ title: "状态",
+ dataIndex: "unit",
+ key: "unit",
+ width: "10%",
+ render: (text, item, index) => {
+ const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
+ return isEditing ? (
+
{
+ setEditingItem((prev) => ({
+ ...prev || item,
+ unit: e.target.value
+ }));
+ }}
+ />
+ ) : (
+
{text}
+ );
+ },
+ },
+ {
+ title: "单价",
+ dataIndex: "price",
+ key: "price",
+ width: "10%",
+ render: (text, item, index) => {
+ const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
+ return isEditing ? (
+
{
+ setEditingItem((prev) => ({
+ ...prev || item,
+ price: value
+ }));
+ }}
+ />
+ ) : (
+ {text}
+ );
+ },
+ },
+ {
+ title: "数量",
+ dataIndex: "quantity",
+ key: "quantity",
+ width: "10%",
+ render: (text, item, index) => {
+ const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
+ return isEditing ? (
+ {
+ setEditingItem((prev) => ({
+ ...prev || item,
+ quantity: value
+ }));
+ }}
+ />
+ ) : (
+ {text}
+ );
+ },
+ },
+ {
+ title: "描述",
+ dataIndex: "description",
+ key: "description",
+ render: (text, item, index) => {
+ const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
+ return isEditing ? (
+ {
+ setEditingItem((prev) => ({
+ ...prev || item,
+ description: e.target.value
+ }));
+ }}
+ />
+ ) : (
+ {text}
+ );
+ },
+ },
+ {
+ title: "操作",
+ key: "action",
+ width: 150,
+ render: (_, item, index) => {
+ const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
+ return (
+
+ {isEditing ? (
+ <>
+ handleSave(record, section.key, index)}
+ />
+ {
+ setEditingKey("");
+ setEditingItem(null);
+ }}
+ />
+ >
+ ) : (
+ <>
+ {
+ setEditingKey(`${record.id}-${section.key}-${index}`);
+ setEditingItem(item); // 初始化编辑项
+ }}
+ />
+ handleDeleteItem(record, section.key, index)}
+ >
+
+
+ >
+ )}
+
+ );
+ },
+ },
+]
+case 'task':
+ return [
+ {
+ title: "名称",
+ dataIndex: "name",
+ key: "name",
+ width: "15%",
+ render: (text, item, index) => {
+ const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
+ return isEditing ? (
+ {
+ setEditingItem((prev) => ({
+ ...prev || item,
+ name: e.target.value
+ }));
+ }}
+ />
+ ) : (
+ {text}
+ );
+ },
+ },
+ {
+ title: "执行状态",
+ dataIndex: "unit",
+ key: "unit",
+ render: (text, item, index) => {
+ const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
+ return isEditing ? (
+ {
+ setEditingItem((prev) => ({
+ ...prev || item,
+ unit: e.target.value
+ }));
+ }}
+ />
+ ) : (
+ {text}
+ );
+ },
+ },
+
+ {
+ 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 ? (
+ {
+ setEditingItem((prev) => ({
+ ...prev || item,
+ description: e.target.value
+ }));
+ }}
+ />
+ ) : (
+ {text}
+ );
+ },
+ },
+ {
+ title: "操作",
+ key: "action",
+ render: (_, item, index) => {
+ const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
+ return (
+
+ {isEditing ? (
+ <>
+ handleSave(record, section.key, index)}
+ />
+ {
+ setEditingKey("");
+ setEditingItem(null);
+ }}
+ />
+ >
+ ) : (
+ <>
+ {/* {
+ setEditingKey(`${record.id}-${section.key}-${index}`);
+ setEditingItem(item); // 初始化编辑项
+ }}
+ /> */}
+ handleDeleteItem(record, section.key, index)}
+ >
+
+
+ >
+ )}
+
+ );
+ },
+ },
+ ]
+ case "project":
+ default:[]
+ }
+
+
+
+ }
const expandedRowRender = (record) => {
+
return (
{record.attributes.sections.map((section) => (
@@ -240,164 +527,12 @@ const ServicePage = () => {
{
- const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
- return isEditing ? (
- {
- setEditingItem((prev) => ({
- ...prev || item,
- name: e.target.value
- }));
- }}
- />
- ) : (
- {text}
- );
- },
- },
- {
- title: "单位",
- dataIndex: "unit",
- key: "unit",
- width: "10%",
- render: (text, item, index) => {
- const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
- return isEditing ? (
- {
- setEditingItem((prev) => ({
- ...prev || item,
- unit: e.target.value
- }));
- }}
- />
- ) : (
- {text}
- );
- },
- },
- {
- title: "单价",
- dataIndex: "price",
- key: "price",
- width: "10%",
- render: (text, item, index) => {
- const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
- return isEditing ? (
- {
- setEditingItem((prev) => ({
- ...prev || item,
- price: value
- }));
- }}
- />
- ) : (
- {text}
- );
- },
- },
- {
- title: "数量",
- dataIndex: "quantity",
- key: "quantity",
- width: "10%",
- render: (text, item, index) => {
- const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
- return isEditing ? (
- {
- setEditingItem((prev) => ({
- ...prev || item,
- quantity: value
- }));
- }}
- />
- ) : (
- {text}
- );
- },
- },
- {
- title: "描述",
- dataIndex: "description",
- key: "description",
- render: (text, item, index) => {
- const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
- return isEditing ? (
- {
- setEditingItem((prev) => ({
- ...prev || item,
- description: e.target.value
- }));
- }}
- />
- ) : (
- {text}
- );
- },
- },
- {
- title: "操作",
- key: "action",
- width: 150,
- render: (_, item, index) => {
- const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
- return (
-
- {isEditing ? (
- <>
- handleSave(record, section.key, index)}
- />
- {
- setEditingKey("");
- setEditingItem(null);
- }}
- />
- >
- ) : (
- <>
- {
- setEditingKey(`${record.id}-${section.key}-${index}`);
- setEditingItem(item); // 初始化编辑项
- }}
- />
- handleDeleteItem(record, section.key, index)}
- >
-
-
- >
- )}
-
- );
- },
- },
- ]}
+ columns={table_warp(record,section)}
/>
))}
@@ -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"
/>
- {/* 添加模板类型选择弹窗 */}
setIsModalOpen(false)}
diff --git a/src/routes/routes.js b/src/routes/routes.js
index a0edf9c..b16b4c1 100644
--- a/src/routes/routes.js
+++ b/src/routes/routes.js
@@ -16,13 +16,7 @@ const resourceRoutes = [
icon: "shop",
roles: ["OWNER"],
},
- {
- path: "task",
- component: lazy(() => import("@/pages/resource/resourceTask")),
- name: "任务管理",
- icon: "appstore",
- roles: ["OWNER"],
- },
+
{
path: "task/edit/:id?",
component: lazy(() => import("@/pages/resource/resourceTask/edit")),
@@ -41,6 +35,13 @@ const companyRoutes = [
icon: "file",
roles: ["ADMIN", "OWNER"],
},
+ {
+ path: "task",
+ component: lazy(() => import("@/pages/resource/resourceTask")),
+ name: "任务管理",
+ icon: "appstore",
+ roles: ["OWNER"],
+ },
{
path: "quotaInfo/:id?", // 添加可选的 id 参数
hidden: true,
diff --git a/src/utils/enum.js b/src/utils/enum.js
new file mode 100644
index 0000000..e53f68d
--- /dev/null
+++ b/src/utils/enum.js
@@ -0,0 +1,8 @@
+export const STATUS_OPTIONS = [
+ { label: '未开始', value: 'not_started' },
+ { label: '进行中', value: 'in_progress' },
+ { label: '已完成', value: 'completed' },
+ { label: '已暂停', value: 'paused' },
+ { label: '延期中', value: 'paused' }
+ ];
+
\ No newline at end of file