Commit a6c77b1602389c981e91e7da4d4a6de356222762
1 parent
04358c9a
feat(invoice): optimize invoice components and add Chinese currency conversion utility
- Add dynamic invoice type display in Invoice.tsx. - Refactor InvoiceDetailTable.tsx to simplify code and remove unnecessary comments. - Optimize data processing and UI display in InvoiceModal.tsx. - Improve component structure and add orderId mapping in InvoiceRecordDetailModal.tsx. - Enhance InvoicingDrawerForm.tsx with total price calculation and validation.- Implement Chinese currency conversion function in numberUtil.ts. - Various code cleanups and documentation improvements.
Showing
10 changed files
with
246 additions
and
87 deletions
src/pages/Invoice/components/Invoice.tsx
... | ... | @@ -154,7 +154,11 @@ export default ({ data }) => { |
154 | 154 | 发票号码:<InvoiceInfo>{data.invoiceNumber}</InvoiceInfo> |
155 | 155 | </Col> |
156 | 156 | <Col className="title col_18 no-border"> |
157 | - 电子发票(增值税专用发票) | |
157 | + 电子发票( | |
158 | + {data.type === 'SPECIAL_TICKET' | |
159 | + ? '增值税专用发票' | |
160 | + : '增值税普通发票'} | |
161 | + ) | |
158 | 162 | </Col> |
159 | 163 | <UnderLine className="UnderLine"> |
160 | 164 | <div></div> | ... | ... |
src/pages/Invoice/components/InvoiceDetailTable.tsx
... | ... | @@ -3,13 +3,14 @@ import { InvoiceProjectSelect } from '@/pages/Invoice/components/InvoiceProjectS |
3 | 3 | import { |
4 | 4 | ActionType, |
5 | 5 | EditableProTable, |
6 | + ProCard, | |
6 | 7 | ProColumns, |
8 | + ProFormField, | |
7 | 9 | } from '@ant-design/pro-components'; |
8 | 10 | import { useEffect, useRef, useState } from 'react'; |
9 | 11 | |
10 | 12 | export default ({ recordId, details, updateDetails, readOnly }) => { |
11 | 13 | const [editableKeys, setEditableRowKeys] = useState([]); |
12 | - const [invoiceProject, setInvoiceProject] = useState({}); | |
13 | 14 | const ref = useRef<ActionType>(); |
14 | 15 | useEffect(() => { |
15 | 16 | updateDetails(details); |
... | ... | @@ -26,12 +27,7 @@ export default ({ recordId, details, updateDetails, readOnly }) => { |
26 | 27 | ellipsis: true, |
27 | 28 | readonly: readOnly, |
28 | 29 | renderFormItem: () => { |
29 | - return ( | |
30 | - <InvoiceProjectSelect | |
31 | - setInvoiceProject={setInvoiceProject} | |
32 | - readOnly={readOnly} | |
33 | - /> | |
34 | - ); | |
30 | + return <InvoiceProjectSelect readOnly={readOnly} />; | |
35 | 31 | }, |
36 | 32 | }, |
37 | 33 | { |
... | ... | @@ -131,46 +127,27 @@ export default ({ recordId, details, updateDetails, readOnly }) => { |
131 | 127 | onValuesChange: (record, recordList) => { |
132 | 128 | //修改recordList中tid为record.tid的元素,将它的specification属性设置为invoiceProject的specification属性 |
133 | 129 | const records = recordList.map((item) => { |
134 | - if ( | |
135 | - record && | |
136 | - item.tid === record.tid && | |
137 | - item.projectName !== | |
138 | - '*' + | |
139 | - invoiceProject.productAndServiceCatagoryAbbreviation + | |
140 | - '*' + | |
141 | - invoiceProject?.name && | |
142 | - item.specification !== invoiceProject?.specification | |
143 | - ) { | |
144 | - console.log(item.projectName); | |
145 | - console.log(invoiceProject?.name); | |
146 | - item.projectName = | |
147 | - '*' + | |
148 | - invoiceProject.productAndServiceCatagoryAbbreviation + | |
149 | - '*' + | |
150 | - invoiceProject?.name; | |
151 | - item.specification = invoiceProject?.specification; | |
152 | - item.unit = invoiceProject?.unit; | |
153 | - item.taxRate = invoiceProject?.taxRate * 100; | |
154 | - } | |
155 | 130 | return item; |
156 | 131 | }); |
157 | 132 | updateDetails(records); |
158 | 133 | }, |
159 | 134 | }} |
160 | 135 | /> |
161 | - {/*{<ProCard title="表格数据" headerBordered collapsible defaultCollapsed> | |
162 | - <ProFormField | |
163 | - ignoreFormItem | |
164 | - fieldProps={{ | |
165 | - style: { | |
166 | - width: '100%', | |
167 | - }, | |
168 | - }} | |
169 | - mode="read" | |
170 | - valueType="jsonCode" | |
171 | - text={JSON.stringify(details)} | |
172 | - /> | |
173 | - </ProCard>}*/} | |
136 | + { | |
137 | + <ProCard title="表格数据" headerBordered collapsible defaultCollapsed> | |
138 | + <ProFormField | |
139 | + ignoreFormItem | |
140 | + fieldProps={{ | |
141 | + style: { | |
142 | + width: '100%', | |
143 | + }, | |
144 | + }} | |
145 | + mode="read" | |
146 | + valueType="jsonCode" | |
147 | + text={JSON.stringify(details)} | |
148 | + /> | |
149 | + </ProCard> | |
150 | + } | |
174 | 151 | </> |
175 | 152 | ); |
176 | 153 | }; | ... | ... |
src/pages/Invoice/components/InvoiceModal.tsx
... | ... | @@ -6,15 +6,15 @@ import { useEffect, useState } from 'react'; |
6 | 6 | |
7 | 7 | export default ({ recordId, getRecord, button }) => { |
8 | 8 | const [data, setData] = useState<any>({}); |
9 | + const getData = async () => { | |
10 | + let ret = await postServiceInvoiceGetInvoiceRecord({ | |
11 | + query: { | |
12 | + id: recordId, | |
13 | + }, | |
14 | + }); | |
15 | + setData(ret.data); | |
16 | + }; | |
9 | 17 | useEffect(() => { |
10 | - const getData = async () => { | |
11 | - let ret = await postServiceInvoiceGetInvoiceRecord({ | |
12 | - query: { | |
13 | - id: recordId, | |
14 | - }, | |
15 | - }); | |
16 | - setData(ret.data); | |
17 | - }; | |
18 | 18 | if (recordId) { |
19 | 19 | getData(); |
20 | 20 | } |
... | ... | @@ -25,8 +25,12 @@ export default ({ recordId, getRecord, button }) => { |
25 | 25 | title="预览发票" |
26 | 26 | trigger={button ? button : <a type="primary">预览</a>} |
27 | 27 | onOpenChange={(open) => { |
28 | - if (open && getRecord) { | |
29 | - setData(getRecord()); | |
28 | + if (open) { | |
29 | + if (getRecord) { | |
30 | + setData(getRecord()); | |
31 | + } else { | |
32 | + getData(); | |
33 | + } | |
30 | 34 | } |
31 | 35 | }} |
32 | 36 | width={1200} | ... | ... |
src/pages/Invoice/components/InvoiceProjectSelect.tsx
... | ... | @@ -2,12 +2,7 @@ import { postServiceConstListInvoiceDetailNames } from '@/services'; |
2 | 2 | import { Select, Tooltip } from 'antd'; |
3 | 3 | import { useState } from 'react'; |
4 | 4 | |
5 | -export const InvoiceProjectSelect = ({ | |
6 | - readOnly, | |
7 | - value, | |
8 | - onChange, | |
9 | - setInvoiceProject, | |
10 | -}) => { | |
5 | +export const InvoiceProjectSelect = ({ readOnly, value, onChange }) => { | |
11 | 6 | const [options, setOptions] = useState<any[]>([]); |
12 | 7 | // 定义防抖函数 |
13 | 8 | let timeoutId = null; |
... | ... | @@ -20,16 +15,22 @@ export const InvoiceProjectSelect = ({ |
20 | 15 | }, |
21 | 16 | }); |
22 | 17 | const data = res.data; |
23 | - console.log('invoiceProject' + JSON.stringify(data)); | |
18 | + | |
24 | 19 | setOptions( |
25 | 20 | data.map((item) => { |
21 | + console.log(item); | |
26 | 22 | return { |
23 | + key: item.id, | |
27 | 24 | label: |
28 | 25 | '*' + |
29 | 26 | item.productAndServiceCatagoryAbbreviation + |
30 | 27 | '*' + |
31 | 28 | item?.name, |
32 | - value: item.id, | |
29 | + value: | |
30 | + '*' + | |
31 | + item.productAndServiceCatagoryAbbreviation + | |
32 | + '*' + | |
33 | + item?.name, | |
33 | 34 | ...item, |
34 | 35 | }; |
35 | 36 | }), |
... | ... | @@ -48,7 +49,6 @@ export const InvoiceProjectSelect = ({ |
48 | 49 | placeholder="请选择开票项目" |
49 | 50 | filterOption={(input, option) => (option?.label ?? '').includes(input)} |
50 | 51 | onChange={(e) => { |
51 | - setInvoiceProject(options.find((item) => item.value === e)); | |
52 | 52 | onChange(e); |
53 | 53 | }} |
54 | 54 | defaultValue={value} | ... | ... |
src/pages/Invoice/components/InvoiceRecordDetailModal.tsx
... | ... | @@ -51,6 +51,7 @@ export default ({ id, setVisible }) => { |
51 | 51 | id: id, |
52 | 52 | }, |
53 | 53 | }); |
54 | + console.log(ret.data); | |
54 | 55 | const updatedInvoiceDetails = ret.data.invoiceDetails?.map( |
55 | 56 | (item, index) => ({ |
56 | 57 | ...item, // 保留原有属性 |
... | ... | @@ -79,7 +80,23 @@ export default ({ id, setVisible }) => { |
79 | 80 | id: id, |
80 | 81 | }, |
81 | 82 | }); |
82 | - return ret.data; | |
83 | + const data = ret.data; | |
84 | + const orderIdMap = data.orderIdMap; | |
85 | + const orderIdList = []; | |
86 | + //遍历orderIdMap属性。 | |
87 | + for (const key in orderIdMap) { | |
88 | + if (key in Object.getOwnPropertyNames(orderIdMap)) { | |
89 | + const orderId = { | |
90 | + mainId: key, | |
91 | + subIds: orderIdMap[key], | |
92 | + }; | |
93 | + orderIdList.push(orderId); | |
94 | + } | |
95 | + } | |
96 | + return { | |
97 | + ...data, | |
98 | + orderIdList: orderIdList, | |
99 | + }; | |
83 | 100 | }} |
84 | 101 | submitter={{ |
85 | 102 | render: () => { |
... | ... | @@ -233,7 +250,7 @@ export default ({ id, setVisible }) => { |
233 | 250 | /> |
234 | 251 | <ProFormFieldSet |
235 | 252 | name="list" |
236 | - label="子订单号" | |
253 | + label="订单号" | |
237 | 254 | transform={(value: any) => ({ |
238 | 255 | list: value, |
239 | 256 | startTime: value[0], |
... | ... | @@ -241,21 +258,37 @@ export default ({ id, setVisible }) => { |
241 | 258 | })} |
242 | 259 | > |
243 | 260 | <ProFormList |
244 | - name="subOrderIds" | |
261 | + name="orderIdList" | |
245 | 262 | creatorButtonProps={false} |
246 | 263 | itemRender={({}, { record }) => { |
247 | 264 | return ( |
248 | - <> | |
265 | + <Space> | |
249 | 266 | <Button |
267 | + key={record.mainId} | |
250 | 268 | className="pl-1 pr-0" |
251 | 269 | type="link" |
252 | 270 | target="_blank" |
253 | - href={'/order?subOrderId=' + record} | |
271 | + href={'/order?id=' + record.mainId} | |
254 | 272 | > |
255 | - {record} | |
273 | + {record.mainId} | |
256 | 274 | </Button> |
275 | + ( | |
276 | + {record.subIds.map((item) => { | |
277 | + return ( | |
278 | + <Button | |
279 | + key={item} | |
280 | + className="pl-1 pr-0" | |
281 | + type="link" | |
282 | + target="_blank" | |
283 | + href={'/order?subOrderId=' + item} | |
284 | + > | |
285 | + {item} | |
286 | + </Button> | |
287 | + ); | |
288 | + })} | |
289 | + ) | |
257 | 290 | <Divider type="vertical" /> |
258 | - </> | |
291 | + </Space> | |
259 | 292 | ); |
260 | 293 | }} |
261 | 294 | > | ... | ... |
src/pages/Invoice/index.tsx
... | ... | @@ -319,14 +319,14 @@ const InvoicePage = () => { |
319 | 319 | render: (text, record) => { |
320 | 320 | return [ |
321 | 321 | /*<InvoiceRecordDetailModal |
322 | - key="detail" | |
323 | - id={record.id} | |
324 | - subOrderIds={record.subOrderIds} | |
325 | - onClose={()=>{ | |
326 | - waitDealrecordActionRef.current?.reload(); | |
327 | - } | |
328 | - } | |
329 | - />*/ | |
322 | + key="detail" | |
323 | + id={record.id} | |
324 | + subOrderIds={record.subOrderIds} | |
325 | + onClose={()=>{ | |
326 | + waitDealrecordActionRef.current?.reload(); | |
327 | + } | |
328 | + } | |
329 | + />*/ | |
330 | 330 | <a |
331 | 331 | key="detail" |
332 | 332 | onClick={() => { | ... | ... |
src/pages/Order/components/InvoicingDrawerForm.tsx
... | ... | @@ -9,6 +9,7 @@ import { |
9 | 9 | postServiceInvoiceQueryCompanyInfo, |
10 | 10 | } from '@/services'; |
11 | 11 | import { enumToSelect } from '@/utils'; |
12 | +import { convertCurrency } from '@/utils/numberUtil'; | |
12 | 13 | import { |
13 | 14 | DrawerForm, |
14 | 15 | ProCard, |
... | ... | @@ -51,7 +52,19 @@ export default ({ dataList, setVisible, onClose }) => { |
51 | 52 | <InvoiceModal |
52 | 53 | key={'invoicePreview'} |
53 | 54 | button={<Button type="primary"> 发票预览 </Button>} |
54 | - getRecord={form.getFieldsValue} | |
55 | + getRecord={() => { | |
56 | + const totalPrice = dataList.reduce( | |
57 | + (accumulator, currentValue) => { | |
58 | + return accumulator + currentValue.subOrderPayment; | |
59 | + }, | |
60 | + 0, | |
61 | + ); | |
62 | + return { | |
63 | + ...form.getFieldsValue(), | |
64 | + totalPrice: totalPrice, | |
65 | + totalPriceText: convertCurrency(totalPrice), | |
66 | + }; | |
67 | + }} | |
55 | 68 | />, |
56 | 69 | ...defaultDoms, |
57 | 70 | ]; |
... | ... | @@ -148,6 +161,7 @@ export default ({ dataList, setVisible, onClose }) => { |
148 | 161 | width="md" |
149 | 162 | name="partyATaxid" |
150 | 163 | label="购方税号" |
164 | + rules={[{ required: true, message: '购方税号必填' }]} | |
151 | 165 | placeholder="请输入名称" |
152 | 166 | /> |
153 | 167 | <ProFormText |
... | ... | @@ -244,6 +258,7 @@ export default ({ dataList, setVisible, onClose }) => { |
244 | 258 | name="invoiceDetails" |
245 | 259 | label="开票明细" |
246 | 260 | initialValue={dataList.map((item) => { |
261 | + console.log('item:' + JSON.stringify(item)); | |
247 | 262 | return { |
248 | 263 | subOrderId: item.id, |
249 | 264 | /*projectName: item.productName,*/ |
... | ... | @@ -251,7 +266,7 @@ export default ({ dataList, setVisible, onClose }) => { |
251 | 266 | unit: item.unit, |
252 | 267 | quantity: item.quantity, |
253 | 268 | price: item.productPrice, |
254 | - totalPrice: item.totalPayment, | |
269 | + totalPrice: item.subOrderPayment, | |
255 | 270 | }; |
256 | 271 | })} |
257 | 272 | rules={[ |
... | ... | @@ -342,13 +357,36 @@ export default ({ dataList, setVisible, onClose }) => { |
342 | 357 | label="规格型号" |
343 | 358 | rules={[ |
344 | 359 | { |
345 | - message: '规格型号不能超过20个字符!', | |
346 | - max: 20, | |
347 | - }, | |
348 | - { | |
349 | 360 | message: '规格型号不能为空!', |
350 | 361 | required: true, |
351 | 362 | }, |
363 | + { | |
364 | + validator: (_, value) => { | |
365 | + let len = 0; | |
366 | + // 判断是否为全角字符 | |
367 | + for (let i = 0; i < value.length; i++) { | |
368 | + // 获取字符的Unicode值 | |
369 | + const code = value.charCodeAt(i); | |
370 | + // 判断是否为全角字符 | |
371 | + if ( | |
372 | + (code >= 0xff01 && code <= 0xff5e) || | |
373 | + (code >= 0x4e00 && code <= 0x9fff) | |
374 | + ) { | |
375 | + len += 2; // 全角字符 | |
376 | + } else { | |
377 | + len += 1; // 半角字符 | |
378 | + } | |
379 | + } | |
380 | + console.log(value); | |
381 | + console.log(len); | |
382 | + if (len <= 40) { | |
383 | + return Promise.resolve(); | |
384 | + } | |
385 | + return Promise.reject( | |
386 | + new Error('规格型号不能超过40个字符!'), | |
387 | + ); | |
388 | + }, | |
389 | + }, | |
352 | 390 | ]} |
353 | 391 | placeholder="请输入名称" |
354 | 392 | /> | ... | ... |
src/pages/Order/components/OrderDrawer.tsx
... | ... | @@ -1721,10 +1721,7 @@ export default ({ onClose, data, subOrders, orderOptType }) => { |
1721 | 1721 | optionItemRender(item) { |
1722 | 1722 | if (item.type === 'add') { |
1723 | 1723 | return ( |
1724 | - <div | |
1725 | - style={{ fontSize: '11px' }} | |
1726 | - title={item.name + '(新增商品信息)'} | |
1727 | - > | |
1724 | + <div title={item.name + '(新增商品信息)'}> | |
1728 | 1725 | <span style={{ color: '#333333' }}> |
1729 | 1726 | {item.label} |
1730 | 1727 | </span> |
... | ... | @@ -1735,7 +1732,6 @@ export default ({ onClose, data, subOrders, orderOptType }) => { |
1735 | 1732 | } |
1736 | 1733 | return ( |
1737 | 1734 | <div |
1738 | - style={{ fontSize: '11px' }} | |
1739 | 1735 | title={ |
1740 | 1736 | item.label + |
1741 | 1737 | ' | ' + | ... | ... |
src/services/definition.ts
... | ... | @@ -768,6 +768,9 @@ export interface ApplyInvoiceDto { |
768 | 768 | */ |
769 | 769 | isUrgentText?: string; |
770 | 770 | logicDelete?: boolean; |
771 | + orderIdMap?: { | |
772 | + [propertyName: string]: Array<number>; | |
773 | + }; | |
771 | 774 | /** |
772 | 775 | * @description |
773 | 776 | * 买方注册地址 |
... | ... | @@ -1500,6 +1503,9 @@ export interface InvoiceRecordDto { |
1500 | 1503 | */ |
1501 | 1504 | isUrgentText?: string; |
1502 | 1505 | logicDelete?: boolean; |
1506 | + orderIdMap?: { | |
1507 | + [propertyName: string]: Array<number>; | |
1508 | + }; | |
1503 | 1509 | /** |
1504 | 1510 | * @description |
1505 | 1511 | * 买方注册地址 | ... | ... |
src/utils/numberUtil.ts
... | ... | @@ -7,3 +7,104 @@ export function getRandomNumber(numDigits: number) { |
7 | 7 | const max = Math.pow(10, numDigits) - 1; |
8 | 8 | return Math.floor(Math.random() * (max - min + 1)) + min; |
9 | 9 | } |
10 | + | |
11 | +//代码如下所示: | |
12 | +export function convertCurrency(moneyParm) { | |
13 | + let money = moneyParm; | |
14 | + //汉字的数字 | |
15 | + let cnNums = new Array( | |
16 | + '零', | |
17 | + '壹', | |
18 | + '贰', | |
19 | + '叁', | |
20 | + '肆', | |
21 | + '伍', | |
22 | + '陆', | |
23 | + '柒', | |
24 | + '捌', | |
25 | + '玖', | |
26 | + ); | |
27 | + //基本单位 | |
28 | + let cnIntRadice = new Array('', '拾', '佰', '仟'); | |
29 | + //对应整数部分扩展单位 | |
30 | + let cnIntUnits = new Array('', '万', '亿', '兆'); | |
31 | + //对应小数部分单位 | |
32 | + let cnDecUnits = new Array('角', '分', '毫', '厘'); | |
33 | + //整数金额时后面跟的字符 | |
34 | + let cnInteger = '整'; | |
35 | + //整型完以后的单位 | |
36 | + let cnIntLast = '圆'; | |
37 | + //最大处理的数字 | |
38 | + let maxNum = 999999999999999.9999; | |
39 | + //金额整数部分 | |
40 | + let integerNum; | |
41 | + //金额小数部分 | |
42 | + let decimalNum; | |
43 | + //输出的中文金额字符串 | |
44 | + let chineseStr = ''; | |
45 | + //分离金额后用的数组,预定义 | |
46 | + let parts; | |
47 | + if (money === '') { | |
48 | + return ''; | |
49 | + } | |
50 | + money = parseFloat(money); | |
51 | + if (money >= maxNum) { | |
52 | + //超出最大处理数字 | |
53 | + return ''; | |
54 | + } | |
55 | + if (money === 0) { | |
56 | + chineseStr = cnNums[0] + cnIntLast + cnInteger; | |
57 | + return chineseStr; | |
58 | + } | |
59 | + //转换为字符串 | |
60 | + let moneyText = money.toString(); | |
61 | + if (moneyText.indexOf('.') === -1) { | |
62 | + integerNum = moneyText; | |
63 | + decimalNum = ''; | |
64 | + } else { | |
65 | + parts = moneyText.split('.'); | |
66 | + integerNum = parts[0]; | |
67 | + decimalNum = parts[1].substr(0, 4); | |
68 | + } | |
69 | + //获取整型部分转换 | |
70 | + if (parseInt(integerNum, 10) > 0) { | |
71 | + let zeroCount = 0; | |
72 | + let IntLen = integerNum.length; | |
73 | + for (let i = 0; i < IntLen; i++) { | |
74 | + const n = integerNum.substr(i, 1); | |
75 | + const p = IntLen - i - 1; | |
76 | + const q = p / 4; | |
77 | + const m = p % 4; | |
78 | + if (n === '0') { | |
79 | + zeroCount++; | |
80 | + } else { | |
81 | + if (zeroCount > 0) { | |
82 | + chineseStr += cnNums[0]; | |
83 | + } | |
84 | + //归零 | |
85 | + zeroCount = 0; | |
86 | + chineseStr += cnNums[parseInt(n)] + cnIntRadice[m]; | |
87 | + } | |
88 | + if (m === 0 && zeroCount < 4) { | |
89 | + chineseStr += cnIntUnits[q]; | |
90 | + } | |
91 | + } | |
92 | + chineseStr += cnIntLast; | |
93 | + } | |
94 | + //小数部分 | |
95 | + if (decimalNum !== '') { | |
96 | + let decLen = decimalNum.length; | |
97 | + for (let k = 0; k < decLen; k++) { | |
98 | + let n = decimalNum.substr(k, 1); | |
99 | + if (n !== '0') { | |
100 | + chineseStr += cnNums[Number(n)] + cnDecUnits[k]; | |
101 | + } | |
102 | + } | |
103 | + } | |
104 | + if (chineseStr === '') { | |
105 | + chineseStr += cnNums[0] + cnIntLast + cnInteger; | |
106 | + } else if (decimalNum === '') { | |
107 | + chineseStr += cnInteger; | |
108 | + } | |
109 | + return chineseStr; | |
110 | +} | ... | ... |