任务模块

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

@@ -19,6 +19,7 @@
"@monaco-editor/react": "^4.6.0",
"@supabase/supabase-js": "^2.38.4",
"antd": "^5.11.0",
"dayjs": "^1.11.13",
"dnd-kit": "^0.0.2",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.2",

3
pnpm-lock.yaml generated
View File

@@ -35,6 +35,9 @@ importers:
antd:
specifier: ^5.11.0
version: 5.22.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
dayjs:
specifier: ^1.11.13
version: 1.11.13
dnd-kit:
specifier: ^0.0.2
version: 0.0.2

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;

View File

@@ -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>
</>
);
};

View File

@@ -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)}

View File

@@ -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,

8
src/utils/enum.js Normal file
View File

@@ -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' }
];