报价单魔魁
This commit is contained in:
414
src/pages/company/quotation/detail/index.jsx
Normal file
414
src/pages/company/quotation/detail/index.jsx
Normal 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;
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Table, Button, Modal, Form, Input, message, Popconfirm } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Card, Table, Button, message, Popconfirm, Tag, Space, Tooltip } from 'antd';
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import { useResources } from '@/hooks/resource/useResource';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { formatCurrency } from '@/utils/format'; // 假设你有这个工具函数,如果没有我会提供
|
||||
|
||||
const QuotationPage = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [editingId, setEditingId] = useState(null);
|
||||
const navigate = useNavigate();
|
||||
const [pagination, setPagination] = useState({ current: 1, pageSize: 10 });
|
||||
const [sorter, setSorter] = useState({ field: 'created_at', order: 'descend' });
|
||||
|
||||
@@ -15,8 +15,6 @@ const QuotationPage = () => {
|
||||
loading,
|
||||
total,
|
||||
fetchResources: fetchQuotations,
|
||||
createResource: createQuotation,
|
||||
updateResource: updateQuotation,
|
||||
deleteResource: deleteQuotation
|
||||
} = useResources(pagination, sorter);
|
||||
|
||||
@@ -35,109 +33,119 @@ const QuotationPage = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (values) => {
|
||||
try {
|
||||
const quotationData = {
|
||||
external_id: values.id,
|
||||
attributes: {
|
||||
customerName: values.customerName,
|
||||
status: values.status || '新建',
|
||||
},
|
||||
type: 'quota'
|
||||
};
|
||||
|
||||
if (editingId) {
|
||||
await updateQuotation(editingId, quotationData);
|
||||
} else {
|
||||
await createQuotation(quotationData);
|
||||
}
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
fetchQuotations();
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
try {
|
||||
await deleteQuotation(id);
|
||||
message.success('删除成功');
|
||||
fetchQuotations();
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
message.error('删除失败:' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '报价单号',
|
||||
dataIndex: ['external_id'],
|
||||
key: 'external_id',
|
||||
sorter: true,
|
||||
title: '报价单名称',
|
||||
dataIndex: ['attributes', 'quataName'],
|
||||
key: 'quataName',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '客户名称',
|
||||
dataIndex: ['attributes', 'customerName'],
|
||||
key: 'customerName',
|
||||
title: '客户信息',
|
||||
dataIndex: ['attributes', 'companyName'],
|
||||
key: 'companyName',
|
||||
render: (companyName) => (
|
||||
<Tag color="blue">{companyName}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '供应商',
|
||||
dataIndex: ['attributes', 'supplierName'],
|
||||
key: 'supplierName',
|
||||
render: (supplierName) => (
|
||||
<Tag color="green">{supplierName}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '报价总额',
|
||||
dataIndex: ['attributes', 'totalAmount'],
|
||||
key: 'totalAmount',
|
||||
align: 'right',
|
||||
render: (amount) => (
|
||||
<span style={{ color: '#f50', fontWeight: 'bold' }}>
|
||||
¥{amount?.toLocaleString()}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建日期',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
sorter: true,
|
||||
render: (text) => new Date(text).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: ['attributes', 'status'],
|
||||
key: 'status',
|
||||
width: 180,
|
||||
render: (text) => (
|
||||
<span>{new Date(text).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width:180,
|
||||
render: (_, record) => (
|
||||
<span>
|
||||
<Space size="mini">
|
||||
<Button
|
||||
size='mini'
|
||||
type="link"
|
||||
onClick={() => {
|
||||
setEditingId(record.id);
|
||||
form.setFieldsValue({
|
||||
id: record.external_id,
|
||||
customerName: record.attributes?.customerName,
|
||||
status: record.attributes?.status,
|
||||
});
|
||||
setVisible(true);
|
||||
}}
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => navigate(`/company/quotaInfo/preview/${record.id}`)}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
<Button
|
||||
size='mini'
|
||||
type="link"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => navigate(`/company/quotaInfo/${record.id}?edit=true`)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个报价单吗?"
|
||||
description="删除后将无法恢复!"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Button type="link" danger>
|
||||
<Button size='mini' type="link" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</span>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Card
|
||||
title="报价单管理"
|
||||
title={
|
||||
<Space>
|
||||
<span>报价单管理</span>
|
||||
<Tag color="blue">{total} 个报价单</Tag>
|
||||
</Space>
|
||||
}
|
||||
className='h-full w-full overflow-auto'
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
setEditingId(null);
|
||||
form.resetFields();
|
||||
setVisible(true);
|
||||
}}
|
||||
onClick={() => navigate('/company/quotaInfo')}
|
||||
>
|
||||
新增报价单
|
||||
</Button>
|
||||
@@ -152,55 +160,11 @@ const QuotationPage = () => {
|
||||
pagination={{
|
||||
...pagination,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={editingId ? '编辑报价单' : '新增报价单'}
|
||||
open={visible}
|
||||
onCancel={() => {
|
||||
setVisible(false);
|
||||
setEditingId(null);
|
||||
form.resetFields();
|
||||
}}
|
||||
footer={null}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
<Form.Item
|
||||
name="id"
|
||||
label="报价单号"
|
||||
rules={[{ required: true, message: '请输入报价单号' }]}
|
||||
>
|
||||
<Input placeholder="请输入报价单号" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="customerName"
|
||||
label="客户名称"
|
||||
rules={[{ required: true, message: '请输入客户名称' }]}
|
||||
>
|
||||
<Input placeholder="请输入客户名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="status"
|
||||
label="状态"
|
||||
initialValue="新建"
|
||||
>
|
||||
<Input placeholder="请输入状态" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" block>
|
||||
提交
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
264
src/pages/company/quotation/view/index.jsx
Normal file
264
src/pages/company/quotation/view/index.jsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Typography, Descriptions, Table, Button, Space, Spin } from 'antd';
|
||||
import { ArrowLeftOutlined, PrinterOutlined, EditOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { supabase } from '@/config/supabase';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import PrintView from '@/components/PrintView';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const printStyles = `
|
||||
@media print {
|
||||
/* 隐藏所有按钮和不需要的元素 */
|
||||
button, .no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 只打印卡片内容 */
|
||||
.ant-card {
|
||||
box-shadow: none !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* 移除背景色 */
|
||||
body, .bg-gray-50 {
|
||||
background: white !important;
|
||||
min-height: auto !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* 确保内容完整打印 */
|
||||
.ant-card-body {
|
||||
padding: 24px !important;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
/* 优化表格打印样式 */
|
||||
.ant-table {
|
||||
page-break-inside: auto !important;
|
||||
}
|
||||
|
||||
.ant-table-row {
|
||||
page-break-inside: avoid !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const QuotationView = () => {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchQuotationDetail = async () => {
|
||||
try {
|
||||
const { data: quotation, error } = await supabase
|
||||
.from('resources')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
setData(quotation);
|
||||
} catch (error) {
|
||||
console.error('获取报价单<E4BBB7><E58D95>情失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchQuotationDetail();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
width: 100,
|
||||
render: (_, __, index) => index + 1,
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'productName',
|
||||
width: '40%',
|
||||
},
|
||||
{
|
||||
title: '数量',
|
||||
dataIndex: 'quantity',
|
||||
width: '20%',
|
||||
align: 'right',
|
||||
render: (value) => value?.toLocaleString('zh-CN', { minimumFractionDigits: 2 }),
|
||||
},
|
||||
{
|
||||
title: '单价',
|
||||
dataIndex: 'price',
|
||||
width: '20%',
|
||||
align: 'right',
|
||||
render: (value) => value?.toLocaleString('zh-CN', { minimumFractionDigits: 2 }),
|
||||
},
|
||||
{
|
||||
title: '小计',
|
||||
width: '20%',
|
||||
align: 'right',
|
||||
render: (_, record) => (
|
||||
(record.quantity * record.price)?.toLocaleString('zh-CN', { minimumFractionDigits: 2 })
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handlePrint = () => {
|
||||
const printWindow = window.open('', '_blank');
|
||||
const printContent = ReactDOMServer.renderToString(<PrintView data={data} />);
|
||||
printWindow.document.write(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>打印报价单</title>
|
||||
<link rel="stylesheet" href="${window.location.origin}/antd.min.css">
|
||||
<style>
|
||||
@media print {
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${printContent}
|
||||
<script>
|
||||
window.onload = function() {
|
||||
window.print();
|
||||
window.onafterprint = function() {
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
printWindow.document.close();
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-screen">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{printStyles}</style>
|
||||
|
||||
<div className="bg-gray-50 min-h-screen ">
|
||||
<Card className="max-w-5xl mx-auto shadow-lg">
|
||||
<div className="flex justify-between items-center mb-6 no-print">
|
||||
<Space>
|
||||
<Button
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/company/quotation')}
|
||||
>
|
||||
返回列表
|
||||
</Button>
|
||||
</Space>
|
||||
<Space>
|
||||
<Button
|
||||
icon={<EditOutlined />}
|
||||
type="primary"
|
||||
onClick={() => navigate(`/company/quotaInfo/${data.id}?edit=true`)}
|
||||
>
|
||||
编辑报价单
|
||||
</Button>
|
||||
<Button
|
||||
icon={<PrinterOutlined />}
|
||||
onClick={handlePrint}
|
||||
>
|
||||
打印报价单
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div className="text-center mb-8">
|
||||
<Title level={2} className="!mb-2">{data.attributes.quataName}</Title>
|
||||
<Text type="secondary">报价单号:{data.id}</Text>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-6 rounded-lg mb-8">
|
||||
<Descriptions column={2} bordered>
|
||||
<Descriptions.Item label="客户公司" span={1}>
|
||||
{data.attributes.companyName}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="供应商" span={1}>
|
||||
{data.attributes.supplierName}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="报价日期" span={1}>
|
||||
{new Date(data.created_at).toLocaleDateString('zh-CN')}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="报价有效期" span={1}>
|
||||
{new Date(Date.now() + 30*24*60*60*1000).toLocaleDateString('zh-CN')}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<Title level={4} className="mb-4">报价明细</Title>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data.attributes.items}
|
||||
pagination={false}
|
||||
rowKey={(record, index) => index}
|
||||
className="border rounded-lg"
|
||||
summary={() => (
|
||||
<Table.Summary fixed>
|
||||
<Table.Summary.Row className="bg-gray-50 font-bold">
|
||||
<Table.Summary.Cell index={0} colSpan={4} align="right">
|
||||
总计({data.attributes.currency}):
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={1} align="right">
|
||||
{data.attributes.totalAmount?.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
style: 'currency',
|
||||
currency: data.attributes.currency
|
||||
})}
|
||||
</Table.Summary.Cell>
|
||||
</Table.Summary.Row>
|
||||
</Table.Summary>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{data.attributes.description && (
|
||||
<div className="bg-gray-50 p-6 rounded-lg">
|
||||
<Title level={4} className="mb-4">补充说明</Title>
|
||||
<Text>{data.attributes.description}</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8 pt-8 border-t border-gray-200">
|
||||
<Text type="secondary" className="block mb-2">
|
||||
注意事项:
|
||||
</Text>
|
||||
<ul className="text-gray-500 text-sm">
|
||||
<li>1. 本报价单有效期为30天</li>
|
||||
<li>2. 最终解释权归本公司所有</li>
|
||||
<li>3. 如有疑问请及时联系我们</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuotationView;
|
||||
Reference in New Issue
Block a user