服务模块
This commit is contained in:
@@ -1,26 +1,47 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo } from "react";
|
||||||
import { Form, Input, InputNumber, Button, Card, Typography, Modal, message, Divider, Select } from 'antd';
|
import {
|
||||||
import { PlusOutlined, DeleteOutlined, EditOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
Form,
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
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 { supabase } from "@/config/supabase";
|
||||||
import { supabaseService } from "@/hooks/supabaseService";
|
import { supabaseService } from "@/hooks/supabaseService";
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
const SectionList = ({
|
const SectionList = ({
|
||||||
form,
|
form,
|
||||||
isView,
|
isView,
|
||||||
formValues,
|
formValues,
|
||||||
type,
|
type,
|
||||||
currentCurrency = 'CNY'
|
currentCurrency = "TWD",
|
||||||
}) => {
|
}) => {
|
||||||
const [editingSectionIndex, setEditingSectionIndex] = useState(null);
|
const [editingSectionIndex, setEditingSectionIndex] = useState(null);
|
||||||
const [editingSectionName, setEditingSectionName] = useState('');
|
const [editingSectionName, setEditingSectionName] = useState("");
|
||||||
const [templateModalVisible, setTemplateModalVisible] = useState(false);
|
const [templateModalVisible, setTemplateModalVisible] = useState(false);
|
||||||
const [availableSections, setAvailableSections] = useState([]);
|
const [availableSections, setAvailableSections] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [units, setUnits] = useState([]);
|
const [units, setUnits] = useState([]);
|
||||||
const [loadingUnits, setLoadingUnits] = useState(false);
|
const [loadingUnits, setLoadingUnits] = useState(false);
|
||||||
|
const CURRENCY_SYMBOLS = {
|
||||||
|
CNY: "¥",
|
||||||
|
TWD: "NT$",
|
||||||
|
USD: "$",
|
||||||
|
};
|
||||||
|
|
||||||
// 内部计算方法
|
|
||||||
const calculateItemAmount = (quantity, price) => {
|
const calculateItemAmount = (quantity, price) => {
|
||||||
const safeQuantity = Number(quantity) || 0;
|
const safeQuantity = Number(quantity) || 0;
|
||||||
const safePrice = Number(price) || 0;
|
const safePrice = Number(price) || 0;
|
||||||
@@ -36,19 +57,13 @@ const SectionList = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatCurrency = (amount) => {
|
const formatCurrency = (amount) => {
|
||||||
const CURRENCY_SYMBOLS = {
|
|
||||||
CNY: "¥",
|
|
||||||
TWD: "NT$",
|
|
||||||
USD: "$",
|
|
||||||
};
|
|
||||||
const safeAmount = Number(amount) || 0;
|
const safeAmount = Number(amount) || 0;
|
||||||
return `${CURRENCY_SYMBOLS[currentCurrency] || ""}${safeAmount.toLocaleString("zh-CN", {
|
return `${CURRENCY_SYMBOLS[currentCurrency] || "NT$"}${safeAmount.toLocaleString("zh-TW", {
|
||||||
minimumFractionDigits: 2,
|
minimumFractionDigits: 2,
|
||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
||||||
})}`;
|
})}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取可用的小节模板
|
|
||||||
const fetchAvailableSections = async () => {
|
const fetchAvailableSections = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -56,6 +71,7 @@ const SectionList = ({
|
|||||||
.from("resources")
|
.from("resources")
|
||||||
.select("*")
|
.select("*")
|
||||||
.eq("type", "sections")
|
.eq("type", "sections")
|
||||||
|
.eq("attributes->>template_type", [type])
|
||||||
.order("created_at", { ascending: false });
|
.order("created_at", { ascending: false });
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
@@ -68,195 +84,235 @@ const SectionList = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理小节名称编辑
|
|
||||||
const handleSectionNameEdit = (sectionIndex, initialValue) => {
|
const handleSectionNameEdit = (sectionIndex, initialValue) => {
|
||||||
setEditingSectionIndex(sectionIndex);
|
setEditingSectionIndex(sectionIndex);
|
||||||
setEditingSectionName(initialValue || '');
|
setEditingSectionName(initialValue || "");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSectionNameSave = () => {
|
const handleSectionNameSave = () => {
|
||||||
if (!editingSectionName.trim()) {
|
if (!editingSectionName.trim()) {
|
||||||
message.error('请输入小节名称');
|
message.error("请输入小节名称");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sections = form.getFieldValue('sections');
|
const sections = form.getFieldValue("sections");
|
||||||
const newSections = [...sections];
|
const newSections = [...sections];
|
||||||
newSections[editingSectionIndex] = {
|
newSections[editingSectionIndex] = {
|
||||||
...newSections[editingSectionIndex],
|
...newSections[editingSectionIndex],
|
||||||
sectionName: editingSectionName.trim()
|
sectionName: editingSectionName.trim(),
|
||||||
};
|
};
|
||||||
form.setFieldValue('sections', newSections);
|
form.setFieldValue("sections", newSections);
|
||||||
setEditingSectionIndex(null);
|
setEditingSectionIndex(null);
|
||||||
setEditingSectionName('');
|
setEditingSectionName("");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加项目
|
|
||||||
const handleAddItem = (add) => {
|
const handleAddItem = (add) => {
|
||||||
add({
|
add({
|
||||||
key: uuidv4(),
|
key: uuidv4(),
|
||||||
name: '',
|
name: "",
|
||||||
description: '',
|
description: "",
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
price: 0,
|
price: 0,
|
||||||
unit: ''
|
unit: "",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理使用模板
|
|
||||||
const handleUseTemplate = (template, add) => {
|
const handleUseTemplate = (template, add) => {
|
||||||
const newSection = {
|
const newSection = {
|
||||||
key: uuidv4(),
|
key: uuidv4(),
|
||||||
sectionName: template.attributes.name,
|
sectionName: template.attributes.name,
|
||||||
items: (template.attributes.items || []).map(item => ({
|
items: (template.attributes.items || []).map((item) => ({
|
||||||
key: uuidv4(),
|
key: uuidv4(),
|
||||||
name: item.name || '',
|
name: item.name || "",
|
||||||
description: item.description || '',
|
description: item.description || "",
|
||||||
price: item.price || 0,
|
price: item.price || 0,
|
||||||
quantity: item.quantity || 1,
|
quantity: item.quantity || 1,
|
||||||
unit: item.unit || '',
|
unit: item.unit || "",
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
add(newSection);
|
add(newSection);
|
||||||
setTemplateModalVisible(false);
|
setTemplateModalVisible(false);
|
||||||
message.success('套用模版成功');
|
message.success("套用模版成功");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理创建自定义小节
|
|
||||||
const handleCreateCustom = (add, fieldsLength) => {
|
const handleCreateCustom = (add, fieldsLength) => {
|
||||||
add({
|
add({
|
||||||
key: uuidv4(),
|
key: uuidv4(),
|
||||||
sectionName: `服务类型 ${fieldsLength + 1}`,
|
sectionName: `服务类型 ${fieldsLength + 1}`,
|
||||||
items: [{
|
items: [
|
||||||
key: uuidv4(),
|
{
|
||||||
name: '',
|
key: uuidv4(),
|
||||||
description: '',
|
name: "",
|
||||||
quantity: 1,
|
description: "",
|
||||||
price: 0,
|
quantity: 1,
|
||||||
unit: ''
|
price: 0,
|
||||||
}]
|
unit: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
setTemplateModalVisible(false);
|
setTemplateModalVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取单位列表
|
|
||||||
const fetchUnits = async () => {
|
const fetchUnits = async () => {
|
||||||
setLoadingUnits(true);
|
setLoadingUnits(true);
|
||||||
try {
|
try {
|
||||||
const { data: units } = await supabaseService.select('resources', {
|
const { data: units } = await supabaseService.select("resources", {
|
||||||
filter: {
|
filter: {
|
||||||
'type': { eq: 'units' },
|
type: { eq: "units" },
|
||||||
'attributes->>template_type': { in: `(${type},common)` }
|
"attributes->>template_type": { in: `(${type},common)` },
|
||||||
},
|
},
|
||||||
order: {
|
order: {
|
||||||
column: 'created_at',
|
column: "created_at",
|
||||||
ascending: false
|
ascending: false,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
setUnits(units || []);
|
setUnits(units || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('获取单位列表失败');
|
message.error("获取单位列表失败");
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingUnits(false);
|
setLoadingUnits(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 在组件加载时获取单位列表
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUnits();
|
fetchUnits();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 新增单位
|
|
||||||
const handleAddUnit = async (unitName) => {
|
const handleAddUnit = async (unitName) => {
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase
|
const { error } = await supabase.from("resources").insert([
|
||||||
.from('resources')
|
{
|
||||||
.insert([{
|
type: "units",
|
||||||
type: 'units',
|
|
||||||
attributes: {
|
attributes: {
|
||||||
name: unitName
|
name: unitName,
|
||||||
|
template_type: type,
|
||||||
},
|
},
|
||||||
schema_version: 1
|
schema_version: 1,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
message.success('新增单位成功');
|
message.success("新增单位成功");
|
||||||
fetchUnits();
|
fetchUnits();
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('新增单位失败');
|
message.error("新增单位失败");
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 模板选择弹窗内容
|
|
||||||
const renderTemplateModalContent = (add, fieldsLength) => (
|
const renderTemplateModalContent = (add, fieldsLength) => (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
{availableSections.length > 0 ? (
|
||||||
{availableSections.map(section => (
|
<div className="flex flex-col">
|
||||||
<div
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
key={section.id}
|
{availableSections.map((section) => (
|
||||||
className="group relative bg-white rounded-lg shadow-sm border border-gray-200 hover:shadow-lg transition-all duration-300 cursor-pointer"
|
<div
|
||||||
onClick={() => handleUseTemplate(section, add)}
|
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"
|
||||||
<div className="p-6">
|
onClick={() => handleUseTemplate(section, add)}
|
||||||
<div className="text-center mb-4">
|
>
|
||||||
<h3 className="text-lg font-medium group-hover:text-blue-500 transition-colors">
|
<div className="p-6">
|
||||||
{section.attributes.name}
|
<div className="text-center mb-4">
|
||||||
</h3>
|
<h3 className="text-lg font-medium group-hover:text-blue-500 transition-colors">
|
||||||
<div className="text-sm text-gray-500 mt-1">
|
{section.attributes.name}
|
||||||
{section.attributes.items?.length || 0} 个项目
|
</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">
|
||||||
|
{formatCurrency(item.price)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{(section.attributes.items || []).length > 3 && (
|
||||||
|
<div className="text-sm text-gray-500 text-center">
|
||||||
|
还有 {section.attributes.items.length - 3} 个项目...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 pt-4 border-t flex justify-between items-center">
|
||||||
|
<span className="text-sm text-gray-600">总金额</span>
|
||||||
|
<span className="text-base font-medium text-blue-500">
|
||||||
|
{formatCurrency(
|
||||||
|
(section.attributes.items || []).reduce(
|
||||||
|
(sum, item) =>
|
||||||
|
sum + (item.price * (item.quantity || 1) || 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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">{formatCurrency(item.price)}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{(section.attributes.items || []).length > 3 && (
|
|
||||||
<div className="text-sm text-gray-500 text-center">
|
|
||||||
还有 {section.attributes.items.length - 3} 个项目...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4 pt-4 border-t flex justify-between items-center">
|
|
||||||
<span className="text-sm text-gray-600">总金额</span>
|
|
||||||
<span className="text-base font-medium text-blue-500">
|
|
||||||
{formatCurrency(
|
|
||||||
(section.attributes.items || []).reduce(
|
|
||||||
(sum, item) => sum + (item.price * (item.quantity || 1) || 0),
|
|
||||||
0
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider />
|
<div className="flex justify-center">
|
||||||
|
<Button
|
||||||
<div className="flex justify-center">
|
type="primary"
|
||||||
<Button
|
icon={<PlusOutlined />}
|
||||||
type="dashed"
|
onClick={() => handleCreateCustom(add, fieldsLength)}
|
||||||
icon={<PlusOutlined />}
|
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)}
|
>
|
||||||
className="w-1/3 border-2"
|
<span className="font-medium">自定义模块</span>
|
||||||
>
|
</Button>
|
||||||
自定义模块
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 修改项目小计的计算,将其封装为 memo 组件
|
|
||||||
const ItemSubtotal = React.memo(({ quantity, price, currentCurrency }) => {
|
const ItemSubtotal = React.memo(({ quantity, price, currentCurrency }) => {
|
||||||
const subtotal = useMemo(() => {
|
const subtotal = useMemo(() => {
|
||||||
const safeQuantity = Number(quantity) || 0;
|
const safeQuantity = Number(quantity) || 0;
|
||||||
@@ -273,7 +329,6 @@ const SectionList = ({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 修改小节总计的计算,将其封装为 memo 组件
|
|
||||||
const SectionTotal = React.memo(({ items, currentCurrency }) => {
|
const SectionTotal = React.memo(({ items, currentCurrency }) => {
|
||||||
const total = useMemo(() => {
|
const total = useMemo(() => {
|
||||||
if (!Array.isArray(items)) return 0;
|
if (!Array.isArray(items)) return 0;
|
||||||
@@ -281,7 +336,7 @@ const SectionList = ({
|
|||||||
if (!item) return sum;
|
if (!item) return sum;
|
||||||
const safeQuantity = Number(item.quantity) || 0;
|
const safeQuantity = Number(item.quantity) || 0;
|
||||||
const safePrice = Number(item.price) || 0;
|
const safePrice = Number(item.price) || 0;
|
||||||
return sum + (safeQuantity * safePrice);
|
return sum + safeQuantity * safePrice;
|
||||||
}, 0);
|
}, 0);
|
||||||
}, [items]);
|
}, [items]);
|
||||||
|
|
||||||
@@ -302,11 +357,11 @@ const SectionList = ({
|
|||||||
<Form.List name="sections">
|
<Form.List name="sections">
|
||||||
{(fields, { add, remove }) => (
|
{(fields, { add, remove }) => (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4 overflow-auto">
|
||||||
{fields.map((field, sectionIndex) => (
|
{fields.map((field, sectionIndex) => (
|
||||||
<Card
|
<Card
|
||||||
key={field.key}
|
key={field.key}
|
||||||
className="shadow-sm rounded-lg"
|
className="shadow-sm rounded-lg min-w-[1000px]"
|
||||||
type="inner"
|
type="inner"
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -315,7 +370,9 @@ const SectionList = ({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Input
|
<Input
|
||||||
value={editingSectionName}
|
value={editingSectionName}
|
||||||
onChange={(e) => setEditingSectionName(e.target.value)}
|
onChange={(e) =>
|
||||||
|
setEditingSectionName(e.target.value)
|
||||||
|
}
|
||||||
onPressEnter={handleSectionNameSave}
|
onPressEnter={handleSectionNameSave}
|
||||||
autoFocus
|
autoFocus
|
||||||
className="w-48"
|
className="w-48"
|
||||||
@@ -331,7 +388,7 @@ const SectionList = ({
|
|||||||
icon={<CloseOutlined />}
|
icon={<CloseOutlined />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingSectionIndex(null);
|
setEditingSectionIndex(null);
|
||||||
setEditingSectionName('');
|
setEditingSectionName("");
|
||||||
}}
|
}}
|
||||||
className="text-red-500"
|
className="text-red-500"
|
||||||
/>
|
/>
|
||||||
@@ -340,17 +397,26 @@ const SectionList = ({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="w-1 h-4 bg-purple-500 rounded-full" />
|
<span className="w-1 h-4 bg-purple-500 rounded-full" />
|
||||||
<Text strong className="text-lg">
|
<Text strong className="text-lg">
|
||||||
{form.getFieldValue(['sections', sectionIndex, 'sectionName'])
|
{form.getFieldValue([
|
||||||
|| `服务类型 ${sectionIndex + 1}`}
|
"sections",
|
||||||
|
sectionIndex,
|
||||||
|
"sectionName",
|
||||||
|
]) || `服务类型 ${sectionIndex + 1}`}
|
||||||
</Text>
|
</Text>
|
||||||
{!isView && (
|
{!isView && (
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
onClick={() => handleSectionNameEdit(
|
onClick={() =>
|
||||||
sectionIndex,
|
handleSectionNameEdit(
|
||||||
form.getFieldValue(['sections', sectionIndex, 'sectionName'])
|
sectionIndex,
|
||||||
)}
|
form.getFieldValue([
|
||||||
|
"sections",
|
||||||
|
sectionIndex,
|
||||||
|
"sectionName",
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
className="text-gray-400 hover:text-blue-500"
|
className="text-gray-400 hover:text-blue-500"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -368,11 +434,9 @@ const SectionList = ({
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* 项目列表 */}
|
|
||||||
<Form.List name={[field.name, "items"]}>
|
<Form.List name={[field.name, "items"]}>
|
||||||
{(itemFields, { add: addItem, remove: removeItem }) => (
|
{(itemFields, { add: addItem, remove: removeItem }) => (
|
||||||
<>
|
<>
|
||||||
{/* 表头 */}
|
|
||||||
<div className="grid grid-cols-[3fr_4fr_1fr_1fr_2fr_1fr_40px] gap-4 mb-2 text-gray-500 px-2">
|
<div className="grid grid-cols-[3fr_4fr_1fr_1fr_2fr_1fr_40px] gap-4 mb-2 text-gray-500 px-2">
|
||||||
<div>项目明细</div>
|
<div>项目明细</div>
|
||||||
<div>描述/备注</div>
|
<div>描述/备注</div>
|
||||||
@@ -383,7 +447,6 @@ const SectionList = ({
|
|||||||
<div></div>
|
<div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 项目列表 */}
|
|
||||||
{itemFields.map((itemField, itemIndex) => (
|
{itemFields.map((itemField, itemIndex) => (
|
||||||
<div
|
<div
|
||||||
key={itemField.key}
|
key={itemField.key}
|
||||||
@@ -413,9 +476,10 @@ const SectionList = ({
|
|||||||
loading={loadingUnits}
|
loading={loadingUnits}
|
||||||
showSearch
|
showSearch
|
||||||
allowClear
|
allowClear
|
||||||
options={units.map(unit => ({
|
style={{ minWidth: "120px" }}
|
||||||
|
options={units.map((unit) => ({
|
||||||
label: unit.attributes.name,
|
label: unit.attributes.name,
|
||||||
value: unit.attributes.name
|
value: unit.attributes.name,
|
||||||
}))}
|
}))}
|
||||||
onDropdownVisibleChange={(open) => {
|
onDropdownVisibleChange={(open) => {
|
||||||
if (open) fetchUnits();
|
if (open) fetchUnits();
|
||||||
@@ -423,48 +487,32 @@ const SectionList = ({
|
|||||||
dropdownRender={(menu) => (
|
dropdownRender={(menu) => (
|
||||||
<>
|
<>
|
||||||
{menu}
|
{menu}
|
||||||
<Divider style={{ margin: '8px 0' }} />
|
<Divider style={{ margin: "12px 0" }} />
|
||||||
<Select.Option value="ADD_NEW">
|
<div style={{ padding: "4px" }}>
|
||||||
<Button
|
<Input.Search
|
||||||
type="text"
|
placeholder="输入新单位名称"
|
||||||
icon={<PlusOutlined />}
|
enterButton={<PlusOutlined />}
|
||||||
block
|
onSearch={async (value) => {
|
||||||
onClick={(e) => {
|
if (!value.trim()) return;
|
||||||
e.stopPropagation();
|
if (
|
||||||
Modal.confirm({
|
await handleAddUnit(value.trim())
|
||||||
title: '新增单位',
|
) {
|
||||||
content: (
|
const currentItems =
|
||||||
<Input
|
form.getFieldValue([
|
||||||
placeholder="请输入单位名称"
|
"sections",
|
||||||
onChange={(e) => {
|
field.name,
|
||||||
Modal.confirm.update({
|
"items",
|
||||||
okButtonProps: {
|
]);
|
||||||
disabled: !e.target.value.trim()
|
currentItems[itemField.name].unit =
|
||||||
}
|
value.trim();
|
||||||
});
|
form.setFieldValue(
|
||||||
}}
|
["sections", field.name, "items"],
|
||||||
ref={(input) => {
|
currentItems
|
||||||
if (input) {
|
);
|
||||||
setTimeout(() => input.focus(), 100);
|
}
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
onOk: async (close) => {
|
|
||||||
const unitName = document.querySelector('.ant-modal-content input').value.trim();
|
|
||||||
if (await handleAddUnit(unitName)) {
|
|
||||||
const currentItems = form.getFieldValue(['sections', field.name, 'items']);
|
|
||||||
currentItems[itemField.name].unit = unitName;
|
|
||||||
form.setFieldValue(['sections', field.name, 'items'], currentItems);
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
新增单位
|
</div>
|
||||||
</Button>
|
|
||||||
</Select.Option>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -474,18 +522,34 @@ const SectionList = ({
|
|||||||
name={[itemField.name, "quantity"]}
|
name={[itemField.name, "quantity"]}
|
||||||
className="!mb-0"
|
className="!mb-0"
|
||||||
>
|
>
|
||||||
<InputNumber placeholder="数量" min={0} className="w-full" />
|
<InputNumber
|
||||||
|
placeholder="数量"
|
||||||
|
min={0}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
{...itemField}
|
{...itemField}
|
||||||
name={[itemField.name, "price"]}
|
name={[itemField.name, "price"]}
|
||||||
className="!mb-0"
|
className="!mb-0"
|
||||||
>
|
>
|
||||||
<InputNumber placeholder="单价" min={0} className="w-full" />
|
<InputNumber
|
||||||
|
placeholder="单价"
|
||||||
|
min={0}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<ItemSubtotal
|
<ItemSubtotal
|
||||||
quantity={formValues?.sections?.[sectionIndex]?.items?.[itemIndex]?.quantity}
|
quantity={
|
||||||
price={formValues?.sections?.[sectionIndex]?.items?.[itemIndex]?.price}
|
formValues?.sections?.[sectionIndex]?.items?.[
|
||||||
|
itemIndex
|
||||||
|
]?.quantity
|
||||||
|
}
|
||||||
|
price={
|
||||||
|
formValues?.sections?.[sectionIndex]?.items?.[
|
||||||
|
itemIndex
|
||||||
|
]?.price
|
||||||
|
}
|
||||||
currentCurrency={currentCurrency}
|
currentCurrency={currentCurrency}
|
||||||
/>
|
/>
|
||||||
{!isView && itemFields.length > 1 && (
|
{!isView && itemFields.length > 1 && (
|
||||||
@@ -511,7 +575,7 @@ const SectionList = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SectionTotal
|
<SectionTotal
|
||||||
items={formValues?.sections?.[sectionIndex]?.items}
|
items={formValues?.sections?.[sectionIndex]?.items}
|
||||||
currentCurrency={currentCurrency}
|
currentCurrency={currentCurrency}
|
||||||
/>
|
/>
|
||||||
@@ -555,4 +619,5 @@ const SectionList = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SectionList;
|
|
||||||
|
export default SectionList;
|
||||||
|
|||||||
@@ -18,16 +18,12 @@ import {
|
|||||||
ArrowLeftOutlined,
|
ArrowLeftOutlined,
|
||||||
SaveOutlined,
|
SaveOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
CloseOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
CheckOutlined,
|
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { supabase } from "@/config/supabase";
|
import { supabase } from "@/config/supabase";
|
||||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import SectionList from '@/components/SectionList'
|
||||||
const { TextArea } = Input;
|
const { Title } = Typography;
|
||||||
const { Title, Text } = Typography;
|
|
||||||
|
|
||||||
// 添加货币符号映射
|
// 添加货币符号映射
|
||||||
const CURRENCY_SYMBOLS = {
|
const CURRENCY_SYMBOLS = {
|
||||||
@@ -47,7 +43,7 @@ const QuotationForm = () => {
|
|||||||
const [dataSource, setDataSource] = useState([{ id: Date.now() }]);
|
const [dataSource, setDataSource] = useState([{ id: Date.now() }]);
|
||||||
const [totalAmount, setTotalAmount] = useState(0);
|
const [totalAmount, setTotalAmount] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [currentCurrency, setCurrentCurrency] = useState("CNY");
|
const [currentCurrency, setCurrentCurrency] = useState("TWD");
|
||||||
const [customers, setCustomers] = useState([]);
|
const [customers, setCustomers] = useState([]);
|
||||||
const [selectedCustomers, setSelectedCustomers] = useState([]);
|
const [selectedCustomers, setSelectedCustomers] = useState([]);
|
||||||
const [formValues, setFormValues] = useState({});
|
const [formValues, setFormValues] = useState({});
|
||||||
@@ -737,304 +733,28 @@ const QuotationForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 报价单细卡片 */}
|
<Card
|
||||||
<Form.List name="sections">
|
className="shadow-sm rounded-lg"
|
||||||
{(fields, { add, remove }) => (
|
type="inner"
|
||||||
<>
|
title={
|
||||||
<div className="space-y-4">
|
<span className="flex items-center space-x-2 text-gray-700">
|
||||||
{fields.map((field, sectionIndex) => (
|
<span className="w-1 h-4 bg-blue-500 rounded-full" />
|
||||||
<Card
|
<span>服务明细</span>
|
||||||
key={`section-${field.key}`}
|
</span>
|
||||||
className="shadow-sm rounded-lg"
|
}
|
||||||
type="inner"
|
bordered={false}
|
||||||
title={
|
>
|
||||||
<div className="flex items-center justify-between">
|
<SectionList
|
||||||
<div className="flex items-center gap-2">
|
type="quotation"
|
||||||
{editingSectionIndex === sectionIndex ? (
|
form={form}
|
||||||
<div className="flex items-center gap-2">
|
isView={isView}
|
||||||
<Input
|
formValues={formValues}
|
||||||
value={editingSectionName}
|
currentCurrency={currentCurrency}
|
||||||
onChange={(e) =>
|
/>
|
||||||
setEditingSectionName(e.target.value)
|
</Card>
|
||||||
}
|
|
||||||
onPressEnter={handleSectionNameSave}
|
|
||||||
autoFocus
|
|
||||||
className="w-48"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
icon={<CheckOutlined />}
|
|
||||||
onClick={handleSectionNameSave}
|
|
||||||
className="text-green-500 hover:text-green-600"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
icon={<CloseOutlined />}
|
|
||||||
onClick={handleSectionNameCancel}
|
|
||||||
className="text-red-500 hover:text-red-600"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="w-1 h-4 bg-purple-500 rounded-full" />
|
|
||||||
<Text
|
|
||||||
strong
|
|
||||||
className="text-lg dark:text-gray-200"
|
|
||||||
>
|
|
||||||
{form.getFieldValue([
|
|
||||||
"sections",
|
|
||||||
sectionIndex,
|
|
||||||
"sectionName",
|
|
||||||
]) || `服务类<EFBFBD><EFBFBD> ${sectionIndex + 1}`}
|
|
||||||
</Text>
|
|
||||||
{(!id || isEdit) && (
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
onClick={() =>
|
|
||||||
handleSectionNameEdit(
|
|
||||||
sectionIndex,
|
|
||||||
form.getFieldValue([
|
|
||||||
"sections",
|
|
||||||
sectionIndex,
|
|
||||||
"sectionName",
|
|
||||||
]) || `服务类型 ${sectionIndex + 1}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="text-gray-400 hover:text-blue-500"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Form.List name={[field.name, "items"]}>
|
|
||||||
{(itemFields, { add: addItem, remove: removeItem }) => (
|
|
||||||
<>
|
|
||||||
{/* 表头 */}
|
|
||||||
<div className="grid grid-cols-[3fr_4fr_1fr_1fr_2fr_1fr_40px] 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-right">小计</div>
|
|
||||||
<div>操作</div>
|
|
||||||
</div>
|
|
||||||
{itemFields.map((itemField, itemIndex) => (
|
|
||||||
<div
|
|
||||||
key={`${sectionIndex}-${itemIndex}`}
|
|
||||||
className="grid grid-cols-[3fr_4fr_1fr_1fr_2fr_1fr_40px] gap-4 mb-4 items-start"
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
{...itemField}
|
|
||||||
name={[itemField.name, "productName"]}
|
|
||||||
className="!mb-0"
|
|
||||||
>
|
|
||||||
<Input placeholder="服务项目名称" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
{...itemField}
|
|
||||||
name={[itemField.name, "note"]}
|
|
||||||
className="!mb-0"
|
|
||||||
>
|
|
||||||
<Input placeholder="请输入描述/备注" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
{...itemField}
|
|
||||||
name={[itemField.name, "unit"]}
|
|
||||||
className="!mb-0"
|
|
||||||
>
|
|
||||||
<Input placeholder="单位" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
{...itemField}
|
|
||||||
name={[itemField.name, "quantity"]}
|
|
||||||
className="!mb-0"
|
|
||||||
>
|
|
||||||
<InputNumber
|
|
||||||
placeholder="数量"
|
|
||||||
min={0}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
{...itemField}
|
|
||||||
name={[itemField.name, "price"]}
|
|
||||||
className="!mb-0"
|
|
||||||
>
|
|
||||||
<InputNumber
|
|
||||||
placeholder="单价"
|
|
||||||
min={0}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<div className="text-right">
|
|
||||||
<span className="text-gray-500">
|
|
||||||
{formatCurrency(
|
|
||||||
calculateItemAmount(
|
|
||||||
formValues?.sections?.[sectionIndex]
|
|
||||||
?.items?.[itemIndex]?.quantity,
|
|
||||||
formValues?.sections?.[sectionIndex]
|
|
||||||
?.items?.[itemIndex]?.price
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{!isView && (
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
danger
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
onClick={() => removeItem(itemField.name)}
|
|
||||||
className="flex items-center justify-center"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{!isView && (
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
onClick={() =>
|
|
||||||
handleAddItem(addItem, sectionIndex)
|
|
||||||
}
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
className="w-full hover:border-blue-400 hover:text-blue-500 mb-4"
|
|
||||||
>
|
|
||||||
添加服务项目
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex justify-end border-t pt-4">
|
|
||||||
<span className="text-gray-500">
|
|
||||||
小计总额:
|
|
||||||
<span className="text-blue-500 font-medium ml-2">
|
|
||||||
{formatCurrency(
|
|
||||||
calculateSectionTotal(
|
|
||||||
formValues?.sections?.[sectionIndex]
|
|
||||||
?.items
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Form.List>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Add section button */}
|
|
||||||
{!isView && (
|
|
||||||
<div className="mt-6 flex justify-center">
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
onClick={handleAddSection}
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
className="w-1/3 border-2 hover:border-blue-400 hover:text-blue-500"
|
|
||||||
>
|
|
||||||
新建小节
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 总金额统计 */}
|
|
||||||
<div className="mt-6 space-y-4 pt-4 border-t">
|
|
||||||
<div className="flex justify-end items-center space-x-4">
|
|
||||||
<span className="text-gray-600 font-medium">税前总计:</span>
|
|
||||||
<span className="text-2xl font-semibold text-blue-600">
|
|
||||||
{formatCurrency(calculateTotalAmount(formValues?.sections))}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end items-center space-x-4">
|
|
||||||
<span className="text-gray-600">税率:</span>
|
|
||||||
<div style={{ width: '150px' }}>
|
|
||||||
<Input
|
|
||||||
value={taxRate}
|
|
||||||
suffix="%"
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value.replace(/[^\d]/g, '');
|
|
||||||
setTaxRate(Number(value) || 0);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end items-center space-x-4">
|
|
||||||
<span className="text-gray-600 font-medium">税后总计:</span>
|
|
||||||
<span className="text-xl font-semibold text-blue-600">
|
|
||||||
{formatCurrency(afterTaxAmount)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end items-center space-x-4">
|
|
||||||
<span className="text-gray-600">折扣价:</span>
|
|
||||||
<div style={{ width: '150px' }}>
|
|
||||||
<Input
|
|
||||||
value={discount}
|
|
||||||
prefix={CURRENCY_SYMBOLS[currentCurrency]}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value.replace(/[^\d]/g, '');
|
|
||||||
setDiscount(Number(value) || 0);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end items-center space-x-4">
|
|
||||||
<span className="text-gray-600 font-medium">最终金额:</span>
|
|
||||||
<span className="text-2xl font-semibold text-blue-600">
|
|
||||||
{formatCurrency(discount || afterTaxAmount)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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-purple-500 rounded-full" />
|
|
||||||
<span>补充说明</span>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
bordered={false}
|
|
||||||
>
|
|
||||||
<Form.Item name="description" className="mb-0">
|
|
||||||
<TextArea
|
|
||||||
rows={4}
|
|
||||||
placeholder="请输入补充说明信息"
|
|
||||||
className="rounded-md hover:border-purple-400 focus:border-purple-500"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Card>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Form.List>
|
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
<Modal
|
|
||||||
title={
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
|
|
||||||
选择小节模版
|
|
||||||
</h3>
|
|
||||||
}
|
|
||||||
open={templateModalVisible}
|
|
||||||
onCancel={() => setTemplateModalVisible(false)}
|
|
||||||
footer={null}
|
|
||||||
width={800}
|
|
||||||
className="dark:bg-gray-800"
|
|
||||||
closeIcon={
|
|
||||||
<CloseOutlined className="text-gray-500 dark:text-gray-400" />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{renderTemplateModalContent()}
|
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -354,7 +354,8 @@ const QuotationPage = () => {
|
|||||||
<AppstoreOutlined /> 使用选中模板
|
<AppstoreOutlined /> 使用选中模板
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
width={800}
|
width={900}
|
||||||
|
className="template-modal dark:bg-gray-800"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex justify-center items-center h-[400px]">
|
<div className="flex justify-center items-center h-[400px]">
|
||||||
@@ -363,63 +364,74 @@ const QuotationPage = () => {
|
|||||||
) : templates.length === 0 ? (
|
) : templates.length === 0 ? (
|
||||||
<Empty description="暂无可用模板" />
|
<Empty description="暂无可用模板" />
|
||||||
) : (
|
) : (
|
||||||
<div className="max-h-[600px] overflow-y-auto px-1">
|
<div className="max-h-[600px] overflow-y-auto px-2">
|
||||||
{getTemplatesByCategory().map((group, groupIndex) => (
|
{getTemplatesByCategory().map((group, groupIndex) => (
|
||||||
<div key={groupIndex} className="mb-6 last:mb-2">
|
<div key={groupIndex} className="mb-8 last:mb-2">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<div className="h-6 w-1 bg-blue-500 rounded-full"></div>
|
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||||
<h3 className="text-base font-medium text-gray-700">
|
|
||||||
{group.name}
|
{group.name}
|
||||||
<span className="ml-2 text-sm text-gray-400 font-normal">
|
<span className="ml-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
({group.templates.length})
|
({group.templates.length})
|
||||||
</span>
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
{group.templates.map(template => (
|
{group.templates.map(template => (
|
||||||
<div
|
<div
|
||||||
key={template.id}
|
key={template.id}
|
||||||
className={`
|
|
||||||
p-3 border rounded-lg cursor-pointer transition-all
|
|
||||||
${selectedTemplateId === template.id
|
|
||||||
? 'border-blue-500 bg-blue-50/50'
|
|
||||||
: 'border-gray-200 hover:border-blue-300 hover:bg-gray-50/50'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
onClick={() => handleTemplateSelect(template.id)}
|
onClick={() => handleTemplateSelect(template.id)}
|
||||||
|
className={`
|
||||||
|
relative p-4 rounded-xl cursor-pointer transition-all duration-200
|
||||||
|
${
|
||||||
|
selectedTemplateId === template.id
|
||||||
|
? 'ring-2 ring-blue-500 bg-blue-50/40 dark:bg-blue-900/40'
|
||||||
|
: 'hover:bg-gray-50 dark:hover:bg-gray-700/50 border border-gray-200 dark:border-gray-700 shadow-sm hover:shadow-md'
|
||||||
|
}
|
||||||
|
dark:bg-gray-800
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-start gap-2 mb-2">
|
<div className="flex justify-between items-start gap-3 mb-3">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h4 className="font-medium text-gray-800 truncate">
|
<h4 className="text-base font-medium text-gray-900 dark:text-gray-100 truncate">
|
||||||
{template.attributes.templateName}
|
{template.attributes.templateName}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-xs text-gray-500 mt-1 line-clamp-1">
|
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1 line-clamp-2">
|
||||||
{template.attributes.description || '暂无描述'}
|
{template.attributes.description || '暂无描述'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-red-500 font-medium whitespace-nowrap text-sm">
|
<div className="text-blue-600 dark:text-blue-400 font-medium whitespace-nowrap">
|
||||||
¥{template.attributes.totalAmount?.toLocaleString()}
|
¥{template.attributes.totalAmount?.toLocaleString()}
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-2">
|
||||||
{template.attributes.sections.map((section, index) => (
|
{template.attributes.sections.map((section, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="bg-white/80 px-2 py-1 rounded text-xs border border-gray-100"
|
className="bg-white dark:bg-gray-700 rounded-lg p-2.5 text-sm border border-gray-100 dark:border-gray-600"
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="font-medium text-blue-600 truncate flex-1">
|
<span className="font-medium text-gray-700 dark:text-gray-200 truncate flex-1">
|
||||||
{section.sectionName}
|
{section.sectionName}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-gray-400 ml-1">
|
<span className="text-gray-500 dark:text-gray-400 ml-2 text-xs">
|
||||||
{section.items.length}项
|
{section.items.length}项
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{selectedTemplateId === template.id && (
|
||||||
|
<div className="absolute top-3 right-3">
|
||||||
|
<div className="w-5 h-5 bg-blue-500 dark:bg-blue-600 rounded-full flex items-center justify-center">
|
||||||
|
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ const QuotationTemplate = ({ id, isView, onCancel,isEdit }) => {
|
|||||||
'attributes->>template_type': { eq: TYPE }
|
'attributes->>template_type': { eq: TYPE }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log(data,'data');
|
|
||||||
|
|
||||||
if (data?.[0]) {
|
if (data?.[0]) {
|
||||||
const formData = {
|
const formData = {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const ServiceForm = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gradient-to-b from-gray-50 to-white dark:from-gray-800 dark:to-gray-900/90 min-h-screen p-2">
|
<div className="bg-gradient-to-b from-gray-50 to-white dark:from-gray-800 dark:to-gray-900/90 min-h-screen p-2" >
|
||||||
<Card
|
<Card
|
||||||
className="shadow-lg rounded-lg border-0"
|
className="shadow-lg rounded-lg border-0"
|
||||||
title={
|
title={
|
||||||
|
|||||||
@@ -42,23 +42,26 @@ const ResourceManagement = () => {
|
|||||||
onChange={setActiveType}
|
onChange={setActiveType}
|
||||||
type="card"
|
type="card"
|
||||||
className="bg-white rounded-lg shadow-sm"
|
className="bg-white rounded-lg shadow-sm"
|
||||||
items={TEMPLATE_TYPES.map(type => ({
|
items={TEMPLATE_TYPES.map(type => {
|
||||||
key: type.key,
|
return ({
|
||||||
label: (
|
key: type.key,
|
||||||
<span className="flex items-center gap-2">
|
label: (
|
||||||
{type.icon}
|
<span className="flex items-center gap-2">
|
||||||
<span>{type.label}</span>
|
{type.icon}
|
||||||
</span>
|
<span>{type.label}</span>
|
||||||
),
|
</span>
|
||||||
children: (
|
),
|
||||||
<div className="p-6">
|
children: (
|
||||||
<Card>
|
<div className="p-6">
|
||||||
<Classify typeList={filterOption} activeType={activeType} setActiveType={setActiveType} />
|
<Card>
|
||||||
<Unit typeList={filterOption} activeType={activeType} />
|
<Classify typeList={filterOption} activeType={activeType} setActiveType={setActiveType} />
|
||||||
</Card>
|
<Unit typeList={filterOption} activeType={activeType} />
|
||||||
</div>
|
<Sections typeList={filterOption} activeType={activeType} />
|
||||||
)
|
</Card>
|
||||||
}))}
|
</div>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,127 +1,127 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Table, Button, Drawer, Modal, Form, Input, InputNumber, Space, message, Popconfirm, Select } from 'antd';
|
import {
|
||||||
import { PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons';
|
Table,
|
||||||
import { supabase } from '@/config/supabase';
|
Button,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Space,
|
||||||
|
message,
|
||||||
|
Popconfirm,
|
||||||
|
Select,
|
||||||
|
Segmented,
|
||||||
|
InputNumber,
|
||||||
|
Card,
|
||||||
|
Typography
|
||||||
|
} from 'antd';
|
||||||
|
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
|
import { supabaseService } from '@/hooks/supabaseService';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
const SectionManagement = () => {
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const SectionsManagement = ({ activeType = 'quotation', typeList }) => {
|
||||||
const [data, setData] = useState([]);
|
const [data, setData] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
const [editingKey, setEditingKey] = useState('');
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
|
||||||
const [editingRecord, setEditingRecord] = useState(null);
|
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [units, setUnits] = useState([]);
|
const [filterType, setFilterType] = useState('all');
|
||||||
|
|
||||||
// 获取子模块数据
|
const fetchSections = async (type = activeType, filterTypeValue = filterType) => {
|
||||||
const fetchSections = async () => {
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const { data: sections, error } = await supabase
|
let filterCondition;
|
||||||
.from('resources')
|
|
||||||
.select('*')
|
switch (filterTypeValue) {
|
||||||
.eq('type', 'sections');
|
case 'current':
|
||||||
|
filterCondition = { eq: type };
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filterCondition = { eq: type };
|
||||||
|
}
|
||||||
|
|
||||||
if (error) throw error;
|
const { data: sections } = await supabaseService.select('resources', {
|
||||||
|
filter: {
|
||||||
|
'type': { eq: 'sections' },
|
||||||
|
'attributes->>template_type': filterCondition
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
column: 'created_at',
|
||||||
|
ascending: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
setData(sections || []);
|
setData(sections || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('获取子模块数据失败');
|
message.error('获取模块数据失败');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取单位数据
|
|
||||||
const fetchUnits = async () => {
|
|
||||||
try {
|
|
||||||
const { data: unitsData, error } = await supabase
|
|
||||||
.from('resources')
|
|
||||||
.select('*')
|
|
||||||
.eq('type', 'units')
|
|
||||||
.order('created_at', { ascending: false });
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
setUnits(unitsData || []);
|
|
||||||
} catch (error) {
|
|
||||||
message.error('获取单位数据失败');
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (drawerVisible) {
|
fetchSections(activeType, filterType);
|
||||||
fetchSections();
|
}, [activeType]);
|
||||||
}
|
|
||||||
fetchUnits();
|
|
||||||
}, [drawerVisible]);
|
|
||||||
|
|
||||||
// 打开新增/编辑模态框
|
const handleAdd = () => {
|
||||||
const showModal = async (record = null) => {
|
const newData = {
|
||||||
setModalVisible(true);
|
id: Date.now().toString(),
|
||||||
setEditingRecord(record);
|
attributes: {
|
||||||
|
|
||||||
if (record) {
|
|
||||||
try {
|
|
||||||
const { data: section, error } = await supabase
|
|
||||||
.from('resources')
|
|
||||||
.select('*')
|
|
||||||
.eq('id', record.id)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
|
|
||||||
form.setFieldsValue({
|
|
||||||
name: section.attributes.name,
|
|
||||||
items: section.attributes.items
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
message.error('获取子模块详情失败');
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
form.setFieldsValue({
|
|
||||||
name: '',
|
name: '',
|
||||||
items: [{ name: '', description: '', price: 0, quantity: 1, unit: '' }]
|
template_type: activeType,
|
||||||
});
|
items: [{
|
||||||
}
|
key: uuidv4(),
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
quantity: 1,
|
||||||
|
price: 0,
|
||||||
|
unit: ''
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
isNew: true
|
||||||
|
};
|
||||||
|
setData([newData, ...data]);
|
||||||
|
setEditingKey(newData.id);
|
||||||
|
form.setFieldsValue(newData.attributes);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 保存子模块数据
|
const handleSave = async (record) => {
|
||||||
const handleSave = async (values) => {
|
|
||||||
try {
|
try {
|
||||||
if (editingRecord) {
|
const values = await form.validateFields();
|
||||||
// 更新
|
const items = form.getFieldValue(['items']) || [];
|
||||||
const { error } = await supabase
|
|
||||||
.from('resources')
|
// 验证items数组
|
||||||
.update({
|
if (!items.length || !items.some(item => item.name)) {
|
||||||
|
message.error('请至少添加一个有效的服务项目');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (record.isNew) {
|
||||||
|
await supabaseService.insert('resources', {
|
||||||
|
type: 'sections',
|
||||||
|
attributes: {
|
||||||
|
name: values.name,
|
||||||
|
template_type: activeType,
|
||||||
|
items: items.filter(item => item.name), // 只保存有名称的项目
|
||||||
|
},
|
||||||
|
schema_version: 1
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await supabaseService.update('resources',
|
||||||
|
{ id: record.id },
|
||||||
|
{
|
||||||
attributes: {
|
attributes: {
|
||||||
name: values.name,
|
name: values.name,
|
||||||
items: values.items
|
template_type: activeType,
|
||||||
|
items: items.filter(item => item.name),
|
||||||
},
|
},
|
||||||
updated_at: new Date().toISOString()
|
updated_at: new Date().toISOString()
|
||||||
})
|
}
|
||||||
.eq('id', editingRecord.id);
|
);
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
} else {
|
|
||||||
// 新增
|
|
||||||
const { error } = await supabase
|
|
||||||
.from('resources')
|
|
||||||
.insert([{
|
|
||||||
type: 'sections',
|
|
||||||
attributes: {
|
|
||||||
name: values.name,
|
|
||||||
items: values.items
|
|
||||||
},
|
|
||||||
schema_version: 1
|
|
||||||
}]);
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message.success('保存成功');
|
message.success('保存成功');
|
||||||
setModalVisible(false);
|
setEditingKey('');
|
||||||
form.resetFields();
|
|
||||||
fetchSections();
|
fetchSections();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('保存失败');
|
message.error('保存失败');
|
||||||
@@ -129,15 +129,9 @@ const SectionManagement = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 删除子模块
|
const handleDelete = async (record) => {
|
||||||
const handleDelete = async (id) => {
|
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase
|
await supabaseService.delete('resources', { id: record.id });
|
||||||
.from('resources')
|
|
||||||
.delete()
|
|
||||||
.eq('id', id);
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
message.success('删除成功');
|
message.success('删除成功');
|
||||||
fetchSections();
|
fetchSections();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -146,290 +140,226 @@ const SectionManagement = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawerColumns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: '项目名称',
|
title: '模块名称',
|
||||||
dataIndex: 'name',
|
dataIndex: ['attributes', 'name'],
|
||||||
},
|
width: 200,
|
||||||
{
|
render: (text, record) => {
|
||||||
title: '描述',
|
const isEditing = record.id === editingKey;
|
||||||
dataIndex: 'description',
|
return isEditing ? (
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '单价',
|
|
||||||
dataIndex: 'price',
|
|
||||||
render: (price) => `¥${price}`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '数量',
|
|
||||||
dataIndex: 'quantity',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '单位',
|
|
||||||
dataIndex: 'unit',
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 添加模态框内表格列定义
|
|
||||||
const modalColumns = [
|
|
||||||
{
|
|
||||||
title: '项目名称',
|
|
||||||
dataIndex: 'name',
|
|
||||||
render: (_, __, index) => (
|
|
||||||
<Form.Item
|
|
||||||
name={[index, 'name']}
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
>
|
|
||||||
<Input placeholder="项目名称" />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '描述',
|
|
||||||
dataIndex: 'description',
|
|
||||||
render: (_, __, index) => (
|
|
||||||
<Form.Item
|
|
||||||
name={[index, 'description']}
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
>
|
|
||||||
<Input placeholder="描述" />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '单价',
|
|
||||||
dataIndex: 'price',
|
|
||||||
render: (_, __, index) => (
|
|
||||||
<Form.Item
|
|
||||||
name={[index, 'price']}
|
|
||||||
rules={[{ required: true, message: '请输入单价!' }]}
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
>
|
|
||||||
<InputNumber
|
|
||||||
min={0}
|
|
||||||
placeholder="单价"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '数量',
|
|
||||||
dataIndex: 'quantity',
|
|
||||||
render: (_, __, index) => (
|
|
||||||
<Form.Item
|
|
||||||
name={[index, 'quantity']}
|
|
||||||
rules={[{ required: true, message: '请输入数量!' }]}
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
>
|
|
||||||
<InputNumber
|
|
||||||
min={1}
|
|
||||||
placeholder="数量"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '单位',
|
|
||||||
dataIndex: 'unit',
|
|
||||||
render: (_, __, index) => (
|
|
||||||
<Form.Item
|
|
||||||
name={[index, 'unit']}
|
|
||||||
rules={[{ required: true, message: '请选择或输入单位!' }]}
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
placeholder="请选择或输入单位"
|
|
||||||
className="w-full"
|
|
||||||
showSearch
|
|
||||||
allowClear
|
|
||||||
options={units.map(unit => ({
|
|
||||||
label: unit.attributes.name,
|
|
||||||
value: unit.attributes.name
|
|
||||||
}))}
|
|
||||||
filterOption={(input, option) =>
|
|
||||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
render: (_, __, index, { remove }) => (
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
danger
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
onClick={() => remove(index)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
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={1000}
|
|
||||||
onClose={() => setDrawerVisible(false)}
|
|
||||||
open={drawerVisible}
|
|
||||||
className="dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col h-full">
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
onClick={() => showModal()}
|
|
||||||
className="mb-4 w-32 flex items-center justify-center"
|
|
||||||
>
|
|
||||||
新增子模块
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
{data.map((section) => (
|
|
||||||
<div
|
|
||||||
key={section.id}
|
|
||||||
className="bg-white rounded-lg shadow-sm dark:bg-gray-800 border border-gray-200 dark:border-gray-700"
|
|
||||||
>
|
|
||||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
|
||||||
<h3 className="text-lg font-medium text-gray-800 dark:text-gray-200">
|
|
||||||
{section.attributes.name}
|
|
||||||
</h3>
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
onClick={() => showModal(section)}
|
|
||||||
className="text-blue-600 hover:text-blue-500"
|
|
||||||
>
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
<Popconfirm
|
|
||||||
title="确定要删除吗?"
|
|
||||||
onConfirm={() => handleDelete(section.id)}
|
|
||||||
okButtonProps={{
|
|
||||||
className: "bg-red-500 hover:bg-red-600 border-red-500"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
danger
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
className="text-red-600 hover:text-red-500"
|
|
||||||
/>
|
|
||||||
</Popconfirm>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
<div className="p-4">
|
|
||||||
<Table
|
|
||||||
scroll={{ x: true }}
|
|
||||||
dataSource={section.attributes.items}
|
|
||||||
columns={drawerColumns}
|
|
||||||
pagination={false}
|
|
||||||
rowKey={(record, index) => `${section.id}-${index}`}
|
|
||||||
className="border dark:border-gray-700 rounded-lg"
|
|
||||||
rowClassName="hover:bg-gray-50 dark:hover:bg-gray-700/50"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Drawer>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
title={`${editingRecord ? '编辑' : '新增'}子模块`}
|
|
||||||
open={modalVisible}
|
|
||||||
onCancel={() => {
|
|
||||||
setModalVisible(false);
|
|
||||||
setEditingRecord(null);
|
|
||||||
form.resetFields();
|
|
||||||
}}
|
|
||||||
footer={null}
|
|
||||||
width={1200}
|
|
||||||
destroyOnClose={true}
|
|
||||||
>
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
onFinish={handleSave}
|
|
||||||
layout="vertical"
|
|
||||||
className="mt-4"
|
|
||||||
>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="name"
|
name="name"
|
||||||
label="子模块名称"
|
style={{ margin: 0 }}
|
||||||
rules={[{ required: true, message: '请输入子模块名称!' }]}
|
rules={[{ required: true, message: '请输入模块名称!' }]}
|
||||||
>
|
>
|
||||||
<Input placeholder="请输入子模块名称" />
|
<Input
|
||||||
|
placeholder="请输入模块名称"
|
||||||
|
className="rounded-md"
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
) : (
|
||||||
<Form.List name="items">
|
<span className="text-gray-700 font-medium">{text}</span>
|
||||||
{(fields, { add, remove }) => (
|
);
|
||||||
<div className="bg-white rounded-lg border dark:bg-gray-800 dark:border-gray-700">
|
},
|
||||||
<Table
|
},
|
||||||
dataSource={fields}
|
{
|
||||||
columns={modalColumns.map(col => ({
|
title: '服务项目',
|
||||||
...col,
|
dataIndex: ['attributes', 'items'],
|
||||||
render: (...args) => col.render(...args, { remove })
|
render: (items, record) => {
|
||||||
}))}
|
const isEditing = record.id === editingKey;
|
||||||
pagination={false}
|
|
||||||
rowKey="key"
|
if (isEditing) {
|
||||||
className="mb-4"
|
return (
|
||||||
/>
|
<Form.List name="items">
|
||||||
<div className="p-4 border-t dark:border-gray-700">
|
{(fields, { add, remove }) => (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<Card key={field.key} size="small" className="bg-gray-50">
|
||||||
|
<div className="grid grid-cols-6 gap-2">
|
||||||
|
<Form.Item
|
||||||
|
{...field}
|
||||||
|
name={[field.name, 'name']}
|
||||||
|
className="col-span-2 mb-0"
|
||||||
|
>
|
||||||
|
<Input placeholder="项目名称" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
{...field}
|
||||||
|
name={[field.name, 'unit']}
|
||||||
|
className="mb-0"
|
||||||
|
>
|
||||||
|
<Input placeholder="单位" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
{...field}
|
||||||
|
name={[field.name, 'quantity']}
|
||||||
|
className="mb-0"
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
placeholder="数量"
|
||||||
|
min={0}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
{...field}
|
||||||
|
name={[field.name, 'price']}
|
||||||
|
className="mb-0"
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
placeholder="单价"
|
||||||
|
min={0}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={() => remove(field.name)}
|
||||||
|
className="flex items-center justify-center"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
<Button
|
<Button
|
||||||
type="dashed"
|
type="dashed"
|
||||||
onClick={() => add({
|
onClick={() => add({
|
||||||
|
key: uuidv4(),
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
unit: '',
|
||||||
price: 0,
|
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
unit: ''
|
price: 0
|
||||||
})}
|
})}
|
||||||
icon={<PlusOutlined />}
|
className="w-full"
|
||||||
className="w-full hover:border-blue-400 hover:text-blue-500
|
|
||||||
dark:border-gray-600 dark:text-gray-400 dark:hover:text-blue-400"
|
|
||||||
>
|
>
|
||||||
添加项目
|
<PlusOutlined /> 添加服务项目
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</Form.List>
|
||||||
</Form.List>
|
);
|
||||||
|
}
|
||||||
|
|
||||||
<div className="flex justify-end gap-4 mt-6">
|
return (
|
||||||
<Button onClick={() => {
|
<div className="space-y-1">
|
||||||
setModalVisible(false);
|
{(items || []).map((item, index) => (
|
||||||
setEditingRecord(null);
|
<div key={index} className="flex justify-between text-sm">
|
||||||
form.resetFields();
|
<span className="text-gray-600">{item.name}</span>
|
||||||
}}>
|
<span className="text-gray-500">
|
||||||
取消
|
{item.quantity} {item.unit} × ¥{item.price}
|
||||||
</Button>
|
</span>
|
||||||
<Button type="primary" htmlType="submit">
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 160,
|
||||||
|
fixed: 'right',
|
||||||
|
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>
|
||||||
|
<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,
|
||||||
|
items: record.attributes.items
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</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>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow-sm">
|
||||||
|
<Form form={form}>
|
||||||
|
<Table
|
||||||
|
scroll={{ x: 1200 }}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={data}
|
||||||
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
|
pagination={{
|
||||||
|
pageSize: 10,
|
||||||
|
showTotal: (total) => `共 ${total} 条`,
|
||||||
|
className: "px-4"
|
||||||
|
}}
|
||||||
|
className="rounded-lg"
|
||||||
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SectionManagement;
|
export default SectionsManagement;
|
||||||
@@ -79,7 +79,6 @@ export const TeamTable = ({ tableLoading,pagination,dataSource, onTableChange,on
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '归属',
|
title: '归属',
|
||||||
dataIndex: 'type',
|
|
||||||
dataIndex: ["attributes", "type"],
|
dataIndex: ["attributes", "type"],
|
||||||
key: "type",
|
key: "type",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user