Commit a6c77b1602389c981e91e7da4d4a6de356222762

Authored by 曾国涛
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.
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 &#39;@/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 }) =&gt; {
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 }) =&gt; {
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 &#39;react&#39;;
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 }) =&gt; {
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 &#39;@/services&#39;;
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 }) =&gt; {
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 }) =&gt; {
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 }) =&gt; {
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 }) =&gt; {
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 = () =&gt; {
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 }) =&gt; {
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 }) =&gt; {
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 }) =&gt; {
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 }) =&gt; {
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 }) =&gt; {
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 }) =&gt; {
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 }) =&gt; {
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 +}
... ...