报价单魔魁

This commit is contained in:
‘Liammcl’
2024-12-18 02:01:29 +08:00
parent 585c9b7bf8
commit 9b4a7f5fd8
14 changed files with 1326 additions and 390 deletions

View File

@@ -0,0 +1,414 @@
import React, { useState, useEffect } from 'react';
import { Form, Input, InputNumber, Select, Button, Space, Card, Table, Typography } from 'antd';
import { PlusOutlined, ArrowLeftOutlined, SaveOutlined } from '@ant-design/icons';
import { supabase } from '@/config/supabase';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { round } from 'lodash';
const { TextArea } = Input;
const { Title } = Typography;
// 添加货币符号映射
const CURRENCY_SYMBOLS = {
CNY: '¥',
TWD: 'NT$',
USD: '$'
};
const QuotationForm = () => {
const { id } = useParams();
const [searchParams] = useSearchParams();
const isEdit = searchParams.get('edit') === 'true';
const isView = id && !isEdit;
const [form] = Form.useForm();
const navigate = useNavigate();
const [dataSource, setDataSource] = useState([{ id: Date.now() }]);
const [totalAmount, setTotalAmount] = useState(0);
const [currentCurrency, setCurrentCurrency] = useState('CNY');
const calculateTotal = (items = []) => {
const total = items.reduce((sum, item) => {
const itemAmount = round(
Number(item?.quantity || 0) * Number(item?.price || 0),
2
);
return round(sum + itemAmount, 2);
}, 0);
setTotalAmount(total);
};
const handleValuesChange = (changedValues, allValues) => {
if (changedValues.currency) {
setCurrentCurrency(changedValues.currency);
}
if (changedValues.items) {
const changedField = Object.keys(changedValues.items[0])[0];
if (changedField === 'quantity' || changedField === 'price') {
calculateTotal(allValues.items);
}
}
};
const columns = [
{
title: '名称',
dataIndex: 'productName',
width: '40%',
render: (_, record, index) => (
<Form.Item
name={['items', index, 'productName']}
rules={[{ required: true, message: '请输入名称' }]}
style={{ margin: 0 }}
>
<Input placeholder="请输入名称" />
</Form.Item>
),
},
{
title: '数量',
dataIndex: 'quantity',
width: '20%',
render: (_, record, index) => (
<Form.Item
name={['items', index, 'quantity']}
rules={[{ required: true, message: '请输入数量' }]}
style={{ margin: 0 }}
>
<InputNumber
min={1}
precision={2}
placeholder="数量"
onChange={(value) => {
const values = form.getFieldValue('items');
values[index].quantity = value;
calculateTotal(values);
}}
/>
</Form.Item>
),
},
{
title: '单价',
dataIndex: 'price',
width: '20%',
render: (_, record, index) => (
<Form.Item
name={['items', index, 'price']}
rules={[{ required: true, message: '请输入单价' }]}
style={{ margin: 0 }}
>
<InputNumber
min={0}
precision={2}
placeholder="单价"
onChange={(value) => {
const values = form.getFieldValue('items');
values[index].price = value;
calculateTotal(values);
}}
/>
</Form.Item>
),
},
{
title: '说明',
dataIndex: 'note',
width: '40%',
render: (_, record, index) => (
<Form.Item
name={['items', index, 'note']}
style={{ margin: 0 }}
>
<Input placeholder="备注说明" />
</Form.Item>
),
},
{
title: '操作',
width: '10%',
render: (_, record, index) => (
<Button
type="link"
danger
onClick={() => {
const newData = dataSource.filter(item => item.id !== record.id);
setDataSource(newData);
const values = form.getFieldValue('items');
values.splice(index, 1);
form.setFieldValue('items', values);
calculateTotal(values);
}}
>
删除
</Button>
),
},
];
const fetchQuotationDetail = async () => {
try {
const { data, error } = await supabase
.from('resources')
.select('*')
.eq('id', id)
.single();
if (error) throw error;
if (isEdit) {
// 设置表单初始值
form.setFieldsValue({
quataName: data.attributes.quataName,
companyName: data.attributes.companyName,
supplierName: data.attributes.supplierName,
description: data.attributes.description,
currency: data.attributes.currency,
items: data.attributes.items,
});
setDataSource(data.attributes.items.map((item, index) => ({
id: Date.now() + index,
...item
})));
setCurrentCurrency(data.attributes.currency);
calculateTotal(data.attributes.items);
}
} catch (error) {
console.error('获取报价单详情失败:', error);
}
};
useEffect(() => {
if (id) {
fetchQuotationDetail();
}
}, [id]);
const onFinish = async (values) => {
try {
const quotationData = {
type: 'quota',
attributes: {
quataName: values.quataName,
companyName: values.companyName,
supplierName: values.supplierName,
description: values.description,
currency: values.currency,
items: values.items,
totalAmount
}
};
let result;
if (id) {
result = await supabase
.from('resources')
.update(quotationData)
.eq('id', id)
.select();
} else {
result = await supabase
.from('resources')
.insert([quotationData])
.select();
}
if (result.error) throw result.error;
navigate('/company/quotation');
} catch (error) {
console.error('保存失败:', error);
}
};
return (
<div className="bg-gradient-to-b from-gray-50 to-white min-h-screen p-6">
<Card
className="shadow-lg rounded-lg border-0"
title={
<div className="flex justify-between items-center py-2">
<div className="flex items-center space-x-3">
<Title level={4} className="mb-0 text-gray-800">
{id ? (isEdit ? '编辑报价单' : '查看报价单') : '新建报价单'}
</Title>
<span className="text-gray-400 text-sm">
{id ? (isEdit ? '请修改报价单信息' : '报价单详情') : '请填写报价单信息'}
</span>
</div>
<Space size="middle">
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/company/quotation')}
>
返回
</Button>
{!isView && (
<Button
type="primary"
icon={<SaveOutlined />}
onClick={() => form.submit()}
>
保存
</Button>
)}
</Space>
</div>
}
bodyStyle={{ backgroundColor: '#fff' }}
>
<Form
form={form}
onFinish={onFinish}
layout="vertical"
onValuesChange={handleValuesChange}
initialValues={{
currency: 'CNY',
items: [{}]
}}
className="space-y-6"
disabled={isView}
>
{/* 基本信息卡片 */}
<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}
>
<div className="grid grid-cols-2 gap-8">
<Form.Item
name="quataName"
label={<span className="text-gray-700 font-medium">活动名称</span>}
rules={[{ required: true, message: '活动名称' }]}
>
<Input
placeholder="请输入活动名称"
className="rounded-md hover:border-blue-400 focus:border-blue-500"
/>
</Form.Item>
<Form.Item
name="companyName"
label={<span className="text-gray-700 font-medium">公司名称</span>}
rules={[{ required: true, message: '请输入公司名称' }]}
>
<Input
placeholder="请输入公司名称"
className="rounded-md hover:border-blue-400 focus:border-blue-500"
/>
</Form.Item>
<Form.Item
name="supplierName"
label={<span className="text-gray-700 font-medium">供应商名称</span>}
rules={[{ required: true, message: '请输入供应商名称' }]}
>
<Input
placeholder="请输入供应商名称"
className="rounded-md hover:border-blue-400 focus:border-blue-500"
/>
</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-green-500 rounded-full"/>
<span>报价明细</span>
</span>
}
bordered={false}
extra={
!isView && (
<Button
type="dashed"
icon={<PlusOutlined className="text-green-500" />}
onClick={() => {
const newId = Date.now();
setDataSource([...dataSource, { id: newId }]);
}}
>
新增一栏
</Button>
)
}
>
<Table
dataSource={dataSource}
columns={columns}
pagination={false}
rowKey="id"
className="border-t border-gray-100"
rowClassName="hover:bg-gray-50 transition-colors"
/>
<div className="flex justify-end items-center mt-6 space-x-4 pt-4 border-t border-gray-100">
<span className="text-gray-600 font-medium">总金额:</span>
<span className="text-2xl font-semibold text-blue-600">
{CURRENCY_SYMBOLS[currentCurrency]}
{totalAmount.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})}
</span>
<Form.Item
name="currency"
style={{ margin: 0 }}
rules={[{ required: true }]}
>
<Select
className="w-28 rounded-md"
onChange={(value) => setCurrentCurrency(value)}
dropdownClassName="rounded-md shadow-lg"
>
<Select.Option value="CNY">RMB</Select.Option>
<Select.Option value="TWD">台币</Select.Option>
<Select.Option value="USD">美元</Select.Option>
</Select>
</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-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>
</Card>
</div>
);
};
export default QuotationForm;