服务模块
This commit is contained in:
@@ -18,16 +18,12 @@ import {
|
||||
ArrowLeftOutlined,
|
||||
SaveOutlined,
|
||||
DeleteOutlined,
|
||||
CloseOutlined,
|
||||
EditOutlined,
|
||||
CheckOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { supabase } from "@/config/supabase";
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Title, Text } = Typography;
|
||||
import SectionList from '@/components/SectionList'
|
||||
const { Title } = Typography;
|
||||
|
||||
// 添加货币符号映射
|
||||
const CURRENCY_SYMBOLS = {
|
||||
@@ -47,7 +43,7 @@ const QuotationForm = () => {
|
||||
const [dataSource, setDataSource] = useState([{ id: Date.now() }]);
|
||||
const [totalAmount, setTotalAmount] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentCurrency, setCurrentCurrency] = useState("CNY");
|
||||
const [currentCurrency, setCurrentCurrency] = useState("TWD");
|
||||
const [customers, setCustomers] = useState([]);
|
||||
const [selectedCustomers, setSelectedCustomers] = useState([]);
|
||||
const [formValues, setFormValues] = useState({});
|
||||
@@ -737,304 +733,28 @@ const QuotationForm = () => {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 报价单细卡片 */}
|
||||
<Form.List name="sections">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
{fields.map((field, sectionIndex) => (
|
||||
<Card
|
||||
key={`section-${field.key}`}
|
||||
className="shadow-sm rounded-lg"
|
||||
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 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>
|
||||
<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}
|
||||
>
|
||||
<SectionList
|
||||
type="quotation"
|
||||
form={form}
|
||||
isView={isView}
|
||||
formValues={formValues}
|
||||
currentCurrency={currentCurrency}
|
||||
/>
|
||||
</Card>
|
||||
</Form>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user