From 9b4a7f5fd896b546d8490055e88885b515ad4eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=98Liammcl=E2=80=99?= Date: Wed, 18 Dec 2024 02:01:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=A5=E4=BB=B7=E5=8D=95=E9=AD=94=E9=AD=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 + pnpm-lock.yaml | 22 + src/components/Layout/MainLayout.jsx | 21 +- src/components/PrintView/index.jsx | 119 ++++ src/config/routes.js | 13 + src/hooks/resource/useResource.js | 16 +- src/pages/company/quotation/detail/index.jsx | 414 ++++++++++++++ src/pages/company/quotation/index.jsx | 184 +++--- src/pages/company/quotation/view/index.jsx | 264 +++++++++ src/pages/notFound/index.jsx | 34 ++ src/pages/resource/bucket/index.jsx | 558 +++++++++++-------- src/routes/AppRoutes.jsx | 58 +- src/services/supabase/resource.js | 9 +- src/utils/menuUtils.jsx | 2 +- 14 files changed, 1326 insertions(+), 390 deletions(-) create mode 100644 src/components/PrintView/index.jsx create mode 100644 src/pages/company/quotation/detail/index.jsx create mode 100644 src/pages/company/quotation/view/index.jsx create mode 100644 src/pages/notFound/index.jsx diff --git a/package.json b/package.json index 0eb6789..3b4a4da 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,10 @@ "@monaco-editor/react": "^4.6.0", "@supabase/supabase-js": "^2.38.4", "antd": "^5.11.0", + "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-infinite-scroll-component": "^6.1.0", "react-router-dom": "^6.18.0", "recharts": "^2.9.0", "styled-components": "^6.1.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c131343..89ec394 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,12 +20,18 @@ importers: antd: specifier: ^5.11.0 version: 5.22.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + lodash: + specifier: ^4.17.21 + version: 4.17.21 react: specifier: ^18.2.0 version: 18.3.1 react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) + react-infinite-scroll-component: + specifier: ^6.1.0 + version: 6.1.0(react@18.3.1) react-router-dom: specifier: ^6.18.0 version: 6.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1884,6 +1890,11 @@ packages: peerDependencies: react: ^18.3.1 + react-infinite-scroll-component@6.1.0: + resolution: {integrity: sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==} + peerDependencies: + react: '>=16.0.0' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -2139,6 +2150,10 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + throttle-debounce@2.3.0: + resolution: {integrity: sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==} + engines: {node: '>=8'} + throttle-debounce@5.0.2: resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} engines: {node: '>=12.22'} @@ -4369,6 +4384,11 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-infinite-scroll-component@6.1.0(react@18.3.1): + dependencies: + react: 18.3.1 + throttle-debounce: 2.3.0 + react-is@16.13.1: {} react-is@18.3.1: {} @@ -4713,6 +4733,8 @@ snapshots: dependencies: any-promise: 1.3.0 + throttle-debounce@2.3.0: {} + throttle-debounce@5.0.2: {} tiny-invariant@1.3.3: {} diff --git a/src/components/Layout/MainLayout.jsx b/src/components/Layout/MainLayout.jsx index 67aed42..b9d6c3a 100644 --- a/src/components/Layout/MainLayout.jsx +++ b/src/components/Layout/MainLayout.jsx @@ -12,19 +12,22 @@ const MainLayout = () => { const { isDarkMode } = useTheme(); return ( - + - +
- }> + + + + }> diff --git a/src/components/PrintView/index.jsx b/src/components/PrintView/index.jsx new file mode 100644 index 0000000..cc2769a --- /dev/null +++ b/src/components/PrintView/index.jsx @@ -0,0 +1,119 @@ +import React from 'react'; +import { Table } from 'antd'; + +const PrintView = ({ data }) => { + const columns = [ + { + title: '序号', + dataIndex: 'index', + width: 60, + 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 }) + ), + }, + ]; + + return ( +
+
+

{data.attributes.quataName}

+
报价单号:{data.id}
+
+ +
+
+ 客户公司 + {data.attributes.companyName} +
+
+ 供应商 + {data.attributes.supplierName} +
+
+ 报价日期 + + {new Date(data.created_at).toLocaleDateString('zh-CN')} + +
+
+ 报价有效期 + + {new Date(Date.now() + 30*24*60*60*1000).toLocaleDateString('zh-CN')} + +
+
+ +
+
报价明细
+ index} + bordered + summary={() => ( + + + + 总计({data.attributes.currency}): + + + + {data.attributes.totalAmount?.toLocaleString('zh-CN', { + minimumFractionDigits: 2, + style: 'currency', + currency: data.attributes.currency + })} + + + + + )} + /> + + + {data.attributes.description && ( +
+
补充说明
+
{data.attributes.description}
+
+ )} + +
+
注意事项:
+
    +
  • 本报价单有效期为30天
  • +
  • 最终解释权归本公司所有
  • +
  • 如有疑问请及时联系我们
  • +
+
+ + ); +}; + +export default PrintView; \ No newline at end of file diff --git a/src/config/routes.js b/src/config/routes.js index 25332c4..cd8f0f7 100644 --- a/src/config/routes.js +++ b/src/config/routes.js @@ -31,6 +31,19 @@ const companyRoutes = [ component: lazy(() => import('@/pages/company/quotation')), name: '报价单', icon: 'file', + }, { + path: 'quotaInfo/:id?', // 添加可选的 id 参数 + hidden: true, + component: lazy(() => import('@/pages/company/quotation/detail')), + name: '报价单详情', + icon: 'file', + }, + { + path: 'quotaInfo/preview/:id?', // 添加可选的 id 参数 + hidden: true, + component: lazy(() => import('@/pages/company/quotation/view')), + name: '报价单预览', + icon: 'file', }, { path: 'customer', diff --git a/src/hooks/resource/useResource.js b/src/hooks/resource/useResource.js index c8f380c..248b7ca 100644 --- a/src/hooks/resource/useResource.js +++ b/src/hooks/resource/useResource.js @@ -37,8 +37,8 @@ export const useResources = (initialPagination, initialSorter) => { return { data, total: newTotal }; } catch (error) { - console.error('获取资源列表失败:', error); - message.error('获取资源列表失败'); + console.error('获取列表失败:', error); + message.error('获取列表失败'); } finally { setLoading(false); } @@ -48,10 +48,10 @@ export const useResources = (initialPagination, initialSorter) => { try { const newResource = await resourceService.createResource(values); await fetchResources({ current: 1 }); - message.success('创建资源成功'); + message.success('创建成功'); return newResource; } catch (error) { - message.error('创建资源失败'); + message.error('创建失败'); throw error; } }; @@ -60,10 +60,10 @@ export const useResources = (initialPagination, initialSorter) => { try { const updatedResource = await resourceService.updateResource(id, values); await fetchResources({ current: currentPagination.current }); - message.success('更新资源成功'); + message.success('更新成功'); return updatedResource; } catch (error) { - message.error('更新资源失败'); + message.error('更新失败'); throw error; } }; @@ -75,9 +75,9 @@ export const useResources = (initialPagination, initialSorter) => { ? currentPagination.current - 1 : currentPagination.current; await fetchResources({ current: newCurrent }); - message.success('删除资源成功'); + message.success('删除成功'); } catch (error) { - message.error('删除资源失败'); + message.error('删除失败'); throw error; } }; diff --git a/src/pages/company/quotation/detail/index.jsx b/src/pages/company/quotation/detail/index.jsx new file mode 100644 index 0000000..0f4b093 --- /dev/null +++ b/src/pages/company/quotation/detail/index.jsx @@ -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) => ( + + + + ), + }, + + { + title: '数量', + dataIndex: 'quantity', + width: '20%', + render: (_, record, index) => ( + + { + const values = form.getFieldValue('items'); + values[index].quantity = value; + calculateTotal(values); + }} + /> + + ), + }, + { + title: '单价', + dataIndex: 'price', + width: '20%', + render: (_, record, index) => ( + + { + const values = form.getFieldValue('items'); + values[index].price = value; + calculateTotal(values); + }} + /> + + ), + }, + { + title: '说明', + dataIndex: 'note', + width: '40%', + render: (_, record, index) => ( + + + + ), + }, + { + title: '操作', + width: '10%', + render: (_, record, index) => ( + + ), + }, + ]; + + 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 ( +
+ +
+ + {id ? (isEdit ? '编辑报价单' : '查看报价单') : '新建报价单'} + + + {id ? (isEdit ? '请修改报价单信息' : '报价单详情') : '请填写报价单信息'} + +
+ + + {!isView && ( + + )} + +
+ } + bodyStyle={{ backgroundColor: '#fff' }} + > + + {/* 基本信息卡片 */} + + + 基本信息 + + } + bordered={false} + > +
+ 活动名称} + rules={[{ required: true, message: '活动名称' }]} + > + + + + 公司名称} + rules={[{ required: true, message: '请输入公司名称' }]} + > + + + + 供应商名称} + rules={[{ required: true, message: '请输入供应商名称' }]} + > + + +
+
+ + {/* 报价明细卡片 */} + + + 报价明细 + + } + bordered={false} + extra={ + !isView && ( + + ) + } + > +
+ +
+ 总金额: + + {CURRENCY_SYMBOLS[currentCurrency]} + {totalAmount.toLocaleString('zh-CN', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + })} + + + + +
+ + + + + 补充说明 + + } + bordered={false} + > + +