初步完成发票提交

main
wuyize 2023-01-01 16:19:30 +08:00
parent 68431ef6e6
commit 81cafa8010
10 changed files with 276 additions and 197 deletions

View File

@ -9,6 +9,7 @@ import {RouterProvider} from "react-router-dom";
import CustomRouter from "./router/router"; import CustomRouter from "./router/router";
import {PersistGate} from 'redux-persist/integration/react' import {PersistGate} from 'redux-persist/integration/react'
import {ConfigProvider} from "antd"; import {ConfigProvider} from "antd";
import zhCN from 'antd/locale/zh_CN';
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement document.getElementById('root') as HTMLElement
@ -17,7 +18,7 @@ root.render(
<React.StrictMode> <React.StrictMode>
<Provider store={store}> <Provider store={store}>
<PersistGate loading={null} persistor={persistor}> <PersistGate loading={null} persistor={persistor}>
<ConfigProvider theme={{ <ConfigProvider locale={zhCN} theme={{
token: { token: {
colorPrimary: "#1890ff", colorPrimary: "#1890ff",
colorSuccess: "#52c41a", colorSuccess: "#52c41a",

106
src/models/Invoice.ts Normal file
View File

@ -0,0 +1,106 @@
import {Staff} from "./Staff";
import {Reimbursement} from "./Reimbursement";
import {Dayjs} from "dayjs"
export interface Invoice {
invoiceAmount: number;
invoiceCode: string;
invoiceDate: string;
invoiceDeparture?: string;
invoiceDestination?: string;
invoiceFileUri: string;
invoiceId: number;
invoiceKind: number;
invoiceName?: string;
invoiceNo: string;
invoiceNote: string;
/**
* -1: 0: 1: 2:
*/
invoiceState: number;
invoiceThumbnailUri: string;
invoiceUploader: Staff;
invoiceUploadTime: string;
modified: boolean;
reimbursement: Reimbursement;
}
export interface InvoiceIdentifyResponse {
invoiceAmount: number;
invoiceCheckCode?: string;
invoiceCode: string;
invoiceDate: string|Dayjs;
invoiceDeparture?: string;
invoiceDestination?: string;
invoiceExtraInfo: InvoiceExtraInfo[];
invoiceFileName: string;
invoiceKind: number;
invoiceName?: string;
invoiceNo: string;
invoiceNote?: string;
invoiceRegionCode?: string;
invoiceSellerTaxCode?: string;
}
export interface InvoiceExtraInfo {
name: string;
value: string;
}
export const invoiceTypeNameMap = new Map ([
[0, '出租车发票'],
[1, '定额发票'],
[2, '火车票'],
[3, '增值税发票'],
[5, '机票行程单'],
[8, '通用机打发票'],
[9, '汽车票'],
[10, '轮船票'],
[11, '增值税发票(卷票)'],
[12, '购车发票'],
[13, '过路过桥费发票'],
[15, '非税发票'],
[16, '全电发票']
])
export const invoiceItemsMap = new Map([
['invoiceNo', '发票编号'],
['invoiceCode', '发票代码'],
['invoiceAmount', '金额'],
['invoiceDate', '开票日期'],
['invoiceKind', '发票种类'],
['invoiceCheckCode', '校验码'],
['invoiceRegionCode', '区域码'],
['invoiceSellerTaxCode', '卖方税号'],
['invoiceNote', '备注'],
['invoiceDeparture', '出发地'],
['invoiceDestination', '目的地'],
['invoiceName', '发票名称'],
])
export const invoiceTypeItemsMap = new Map ([
[13, ['invoiceName', 'invoiceNo', 'invoiceCode', 'invoiceAmount', 'invoiceDate', 'invoiceDeparture', 'invoiceDestination']],
])
/*
{
"invoiceFileName": "e86d8d76c0507dcfb5364172a4832494.jpg",
"invoiceNo": "35971607",
"invoiceCode": "121011972201",
"invoiceAmount": 1000,
"invoiceDate": "2019-07-11",
"invoiceKind": 13,
"invoiceCheckCode": "",
"invoiceRegionCode": "",
"invoiceSellerTaxCode": "",
"invoiceExtraInfo": {
"时间": "13:01",
"发票消费类型": "交通",
"高速标志": "1"
},
"invoiceNote": null,
"invoiceDeparture": "苏家屯",
"invoiceDestination": "灯塔",
"invoiceName": "辽宁省高速公路通行费专用发票"
}*/

View File

@ -18,52 +18,10 @@ export interface Department {
departmentName: string; departmentName: string;
} }
export interface Invoice {
invoiceAmount: number;
invoiceCode: string;
invoiceDate: string;
invoiceDeparture?: string;
invoiceDestination?: string;
invoiceFileUri: string;
invoiceId: number;
invoiceKind: number;
invoiceName?: string;
invoiceNo: string;
invoiceNote: string;
/**
* -1: 0: 1: 2:
*/
invoiceState: number;
invoiceThumbnailUri: string;
invoiceUploader: Staff;
invoiceUploadTime: string;
modified: boolean;
reimbursement: Reimbursement;
}
export interface InvoiceIdentifyResponse {
invoiceAmount: number;
invoiceCheckCode?: string;
invoiceCode: string;
invoiceDate: string;
invoiceDeparture?: string;
invoiceDestination?: string;
invoiceExtraInfo: InvoiceExtraInfo[];
invoiceFileName: string;
invoiceKind: number;
invoiceName?: string;
invoiceNo: string;
invoiceNote?: string;
invoiceRegionCode?: string;
invoiceSellerTaxCode?: string;
}
export interface InvoiceExtraInfo {
name: string;
value: string;
}
type Nullable<T> = T | undefined | null; type Nullable<T> = T | undefined | null;
export class InvoiceSearchOption { export class InvoiceSearchOption {
invoiceNo: Nullable<string> invoiceNo: Nullable<string>
invoiceCode: Nullable<string> invoiceCode: Nullable<string>
@ -77,11 +35,13 @@ export class InvoiceSearchOption{
invoiceUploader: Nullable<string> invoiceUploader: Nullable<string>
pageNum: number pageNum: number
pageSize: number pageSize: number
constructor() { constructor() {
this.pageNum = 0 this.pageNum = 0
this.pageSize = 20 this.pageSize = 20
this.clear() this.clear()
} }
clear() { clear() {
this.invoiceNo = null this.invoiceNo = null
this.invoiceCode = null this.invoiceCode = null
@ -125,6 +85,7 @@ export class InvoiceCommit {
invoiceRegionCode: Nullable<string> invoiceRegionCode: Nullable<string>
invoiceSellerTaxCode: Nullable<string> invoiceSellerTaxCode: Nullable<string>
invoiceExtraInfo: Nullable<string> | [] | Map<any, string> invoiceExtraInfo: Nullable<string> | [] | Map<any, string>
constructor() { constructor() {
this.invoiceFileName = "" this.invoiceFileName = ""
this.invoiceNo = "" this.invoiceNo = ""
@ -138,6 +99,7 @@ export class InvoiceCommit {
this.invoiceSellerTaxCode = null this.invoiceSellerTaxCode = null
this.invoiceExtraInfo = null this.invoiceExtraInfo = null
} }
setValue(props: FormData) { setValue(props: FormData) {
console.log("1111" + props.toString()) console.log("1111" + props.toString())
this.invoiceFileName = props.get("invoiceFileName")!.toString() this.invoiceFileName = props.get("invoiceFileName")!.toString()
@ -153,6 +115,7 @@ export class InvoiceCommit {
//this.invoiceExtraInfo = props.get("invoiceExtraInfo")!. //this.invoiceExtraInfo = props.get("invoiceExtraInfo")!.
} }
} }
export interface Reimbursement { export interface Reimbursement {
/** /**
* *

View File

@ -38,6 +38,7 @@ const staffSlice = createSlice({
state.staffName = action.payload.staffName state.staffName = action.payload.staffName
state.managingDepartment = action.payload.managingDepartment state.managingDepartment = action.payload.managingDepartment
state.staffDepartments = action.payload.staffDepartments state.staffDepartments = action.payload.staffDepartments
}, },
}, },
}); });

View File

@ -71,7 +71,7 @@ function HeaderBar(props: any) {
const departmentToString = (staff: Staff) => { const departmentToString = (staff: Staff) => {
let result = staff.managingDepartment === null ? '' : staff.managingDepartment.departmentName + '主管' let result = staff.managingDepartment === null ? '' : staff.managingDepartment.departmentName + '主管'
for (const department of staff.staffDepartments) { for (const department of staff.staffDepartments) {
result += ' ' + department.departmentName result += '\xa0\xa0' + department.departmentName
} }
return result return result
} }
@ -85,7 +85,7 @@ function HeaderBar(props: any) {
flexDirection: 'row-reverse', flexDirection: 'row-reverse',
alignItems: 'center' alignItems: 'center'
}}> }}>
<Dropdown menu={{items, onClick}} placement="bottomRight" arrow> <Dropdown overlayStyle={{minWidth: '150px'}} menu={{items, onClick}} placement="bottomRight" arrow>
<Button type="text" style={{ <Button type="text" style={{
marginRight: '12px', height: '100%', marginRight: '12px', height: '100%',
display: 'flex', justifyContent: 'center', alignItems: 'center' display: 'flex', justifyContent: 'center', alignItems: 'center'
@ -96,7 +96,7 @@ function HeaderBar(props: any) {
width: '24px', height: '24px', backgroundColor: colorPrimary, borderRadius: '12px', width: '24px', height: '24px', backgroundColor: colorPrimary, borderRadius: '12px',
display: 'flex', justifyContent: 'center', alignItems: 'center' display: 'flex', justifyContent: 'center', alignItems: 'center'
}}> }}>
<span style={{color: 'white'}}>{staff.staffName[0]}</span> <span style={{color: 'white', fontSize: '10px'}}>{staff.staffName[0]}</span>
</div> </div>
</Button> </Button>
</Dropdown> </Dropdown>

View File

@ -18,7 +18,7 @@ import {
import type {MenuProps, PaginationProps} from 'antd'; import type {MenuProps, PaginationProps} from 'antd';
import {DownOutlined, UploadOutlined} from '@ant-design/icons'; import {DownOutlined, UploadOutlined} from '@ant-design/icons';
import React, {ReactElement, JSXElementConstructor, ReactFragment, ReactPortal, useState} from "react"; import React, {ReactElement, JSXElementConstructor, ReactFragment, ReactPortal, useState} from "react";
import {Invoice, InvoiceCommit, InvoiceSearchOption} from "../../../models/Staff" import {InvoiceCommit, InvoiceSearchOption} from "../../../models/Staff"
import {Space, Typography} from 'antd'; import {Space, Typography} from 'antd';
import {SizeType} from "antd/es/config-provider/SizeContext"; import {SizeType} from "antd/es/config-provider/SizeContext";
import axios from "axios"; import axios from "axios";
@ -28,6 +28,7 @@ import change = Simulate.change;
import {useNavigate} from "react-router-dom"; import {useNavigate} from "react-router-dom";
import {useAppDispatch} from "../../../models/hooks"; import {useAppDispatch} from "../../../models/hooks";
import InvoiceUploadView from "./InvoiceUploadView"; import InvoiceUploadView from "./InvoiceUploadView";
import {Invoice, invoiceTypeNameMap} from "../../../models/Invoice";
const {Meta} = Card; const {Meta} = Card;
const {Search} = Input; const {Search} = Input;
@ -238,7 +239,7 @@ class InvoiceItem extends React.Component<any, any> {
> >
<div style={{marginTop: -20}}> <div style={{marginTop: -20}}>
<li style={{fontWeight: "bold", fontSize: 20}}>¥{(this.state.invoiceAmount / 100.0).toFixed(2)}</li> <li style={{fontWeight: "bold", fontSize: 20}}>¥{(this.state.invoiceAmount / 100.0).toFixed(2)}</li>
<li>{this.state.invoiceKind}</li> <li>{invoiceTypeNameMap.get(this.state.invoiceKind)}</li>
<li>{this.state.invoiceNo}</li> <li>{this.state.invoiceNo}</li>
<li>{this.state.invoiceDate}</li> <li>{this.state.invoiceDate}</li>
</div> </div>
@ -262,6 +263,9 @@ class InvoiceListView extends React.Component<any, any> {
totalNum: 2, totalNum: 2,
PaginationKey: -2 PaginationKey: -2
} }
}
componentDidMount(){
this.searchInvoiceContent() this.searchInvoiceContent()
} }

View File

@ -1,13 +1,19 @@
import React, {useEffect, useRef, useState} from 'react'; import React, {useEffect, useRef, useState} from 'react';
import {Button, Modal, UploadProps, message, Upload, UploadFile, Form, Input, DatePicker} from 'antd'; import {Button, Modal, UploadProps, message, Upload, UploadFile, Form, Input, DatePicker, Select, InputNumber} from 'antd';
import {InboxOutlined, UploadOutlined} from '@ant-design/icons'; import {InboxOutlined, UploadOutlined} from '@ant-design/icons';
import axiosInstance from "../../../utils/axiosInstance"; import axiosInstance from "../../../utils/axiosInstance";
import {InvoiceCommit, InvoiceIdentifyResponse} from "../../../models/Staff"; import {InvoiceCommit} from "../../../models/Staff";
import dayjs from 'dayjs'; import dayjs, {Dayjs} from 'dayjs';
import TextArea from "antd/es/input/TextArea"; import TextArea from "antd/es/input/TextArea";
import {constants} from "http2"; import {constants} from "http2";
import type {FormInstance} from 'antd/es/form'; import type {FormInstance} from 'antd/es/form';
import {useAppDispatch} from "../../../models/hooks"; import {useAppDispatch} from "../../../models/hooks";
import {
InvoiceIdentifyResponse,
invoiceItemsMap,
invoiceTypeItemsMap,
invoiceTypeNameMap,
} from "../../../models/Invoice";
const {Dragger} = Upload; const {Dragger} = Upload;
@ -22,17 +28,6 @@ const props: UploadProps = {
console.log(file) console.log(file)
return false return false
}, },
onChange(info) {
// const { status } = info.file;
// if (status !== 'uploading') {
// console.log('here---------'+info.file, info.fileList);
// }
// if (status === 'done') {
// message.success(`${info.file.name} file uploaded successfully.`);
// } else if (status === 'error') {
// message.error(`${info.file.name} file upload failed.`);
// }
},
onDrop(e) { onDrop(e) {
console.log('Dropped files', e.dataTransfer.files); console.log('Dropped files', e.dataTransfer.files);
}, },
@ -86,7 +81,7 @@ function UpLoadModal(props: any) {
return ( return (
<Modal <Modal
open={open} open={open}
title="Title" title="上传发票"
onOk={handleOk} onOk={handleOk}
onCancel={handleCancel} onCancel={handleCancel}
footer={[ footer={[
@ -105,104 +100,109 @@ function UpLoadModal(props: any) {
function FormModal(props: any) { function FormModal(props: any) {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false)
const [invoice, setInvoice] = useState({})
const [form] = Form.useForm(); const [form] = Form.useForm();
useEffect(() => { useEffect(() => {
setOpen(props.open) setOpen(props.open)
console.log(props.invoiceIdentifyResponse)
let i = {...props.invoiceIdentifyResponse}
setInvoice(i)
form.setFieldsValue(i)
}, [props]); }, [props]);
const handleCancel = () => { const handleCancel = () => {
setOpen(false); setOpen(false);
}; };
const submit = () => { const submit = () => {
let result = form!.getFieldsValue() setLoading(true)
console.log(form.getFieldsValue())
let result:InvoiceIdentifyResponse = {...invoice , ...form.getFieldsValue()} as InvoiceIdentifyResponse
console.log(result)
if (typeof result.invoiceDate !== "string") {
result.invoiceDate = result.invoiceDate.format("YYYY-MM-DD") result.invoiceDate = result.invoiceDate.format("YYYY-MM-DD")
result.invoiceExtraInfo = new Map(result.invoiceExtraInfo.split("\n").map((entry: string) => entry.split(": "))) }
console.log(result.invoiceExtraInfo.split("\n").map((entry: string) => entry.split(": ")))
// let extraInfo=[] result.invoiceAmount*=100
// let line = result.invoiceExtraInfo.split('\n') console.log(result)
// for(let i=0;i<line.length;i++){
// let tmp = line[i].split(": ")
// if(tmp.length===2){
// extraInfo.push({name:tmp[0],value:tmp[1]})
// }
// }
// result.invoiceExtraInfo=extraInfo
console.log(result.invoiceExtraInfo)
axiosInstance({ axiosInstance({
url: 'common/invoice/submit', url: 'common/invoice/submit',
method: 'POST', method: 'POST',
data: result data: result
}).then(response => { }).then(response => {
//TODO
console.log(response.data) console.log(response.data)
setLoading(false)
setOpen(false)
}).catch(function (error) { }).catch(function (error) {
console.log(error) console.log(error)
setLoading(false)
}) })
} }
const inputComponent = (item: string) => {
if (item === 'invoiceDate')
return <DatePicker/>
else if(item === 'invoiceAmount')
return <InputNumber
style={{ width: '100%' }}
formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
/>
else
return <Input/>
}
let formItems = invoiceTypeItemsMap.get(props.invoiceIdentifyResponse?.invoiceKind)?.map((item, index) => {
return (
<Form.Item
key={index}
name={item}
label={invoiceItemsMap.get(item)}
>
{inputComponent(item)}
</Form.Item>
)
})
const getInvoiceKindOptions = () => {
let options: { value: number; label: string; }[] = []
invoiceTypeNameMap.forEach(function (value, key, map) {
options.push({
value: key,
label: value,
})
})
return options
}
return ( return (
<Modal <Modal
open={open} open={open}
title="提交发票"
title="Title"
onOk={submit} onOk={submit}
onCancel={handleCancel} onCancel={handleCancel}
footer={[ footer={[
<Button key="cancel" onClick={handleCancel}> <Button key="cancel" onClick={handleCancel}>
</Button>, </Button>,
<Button key="next" type="primary" loading={false} onClick={submit} <Button key="next" type="primary" loading={loading} onClick={submit}
> >
</Button> </Button>
]} ]}
> >
<Form form={form}> <Form form={form} layout="horizontal" labelCol={{span: 4}}
<Form.Item
name={`invoiceNo`}
label={`发票编码`}
> >
<Input/>
</Form.Item>
<Form.Item
name={`invoiceCode`}
label={`发票代码`}
>
<Input/>
</Form.Item>
<Form.Item <Form.Item
name={`invoiceKind`} name={`invoiceKind`}
label={`发票类型`} label={`发票类型`}
> >
<Input/> <Select
options={getInvoiceKindOptions()}
/>
</Form.Item> </Form.Item>
{formItems}
<Form.Item <Form.Item
name={`invoiceAmount`} name={`invoiceNote`}
label={`计税总额`} label={`备注`}
>
<Input/>
</Form.Item>
<Form.Item
name={`invoiceAmountWithoutTax`}
label={`非税金额`}
>
<Input/>
</Form.Item>
<Form.Item
name={`invoiceDate`}
label={`开票日期`}
>
<DatePicker/>
</Form.Item>
<Form.Item
name={`invoiceCheckCode`}
label={`核验码`}
>
<Input/>
</Form.Item>
<Form.Item
name={`invoiceExtraInfo`}
label={`其他信息`}
> >
<TextArea/> <TextArea/>
</Form.Item> </Form.Item>
@ -213,44 +213,32 @@ function FormModal(props: any) {
class InvoiceUploadView extends React.Component<any, any> { class InvoiceUploadView extends React.Component<any, any> {
formRef = React.createRef<FormInstance>();
constructor(props: {}) { constructor(props: {}) {
super(props); super(props);
this.state = { this.state = {
loading: false, uploadModalOpen: false,
open: false, formModalOpen: false,
uploadOpen: false, invoiceIdentifyResponse: null
uploadStep: 0,
invoiceCommit: new InvoiceCommit(),
}
this.state.invoiceCommit.invoiceNo = "init"
} }
setUploadOpen(value: Boolean) {
this.setState({uploadOpen: value})
} }
setOpen(value: Boolean) { setOpen(value: Boolean) {
this.setState({open: value}) this.setState({uploadModalOpen: value})
} }
showUploadView = () => { showUploadView = () => {
this.setOpen(true); this.setOpen(true);
}; };
handleUploadCanCel = () => {
this.setUploadOpen(false);
}
handleNextStep = (response: InvoiceIdentifyResponse) => { handleNextStep = (response: InvoiceIdentifyResponse) => {
this.setState({open: false, uploadOpen: true}) response.invoiceDate = dayjs(response.invoiceDate)
response.invoiceAmount /= 100.
this.setState({uploadModalOpen: false, formModalOpen: true, invoiceIdentifyResponse: response})
} }
render() { render() {
console.log(this.state.invoiceCommit)
console.log(this.state.uploadStep)
// @ts-ignore // @ts-ignore
return ( return (
@ -259,8 +247,9 @@ class InvoiceUploadView extends React.Component<any, any> {
size="large"> size="large">
</Button> </Button>
<UpLoadModal open={this.state.open} nextStep={this.handleNextStep}/> <UpLoadModal open={this.state.uploadModalOpen} nextStep={this.handleNextStep}/>
<FormModal open={this.state.uploadOpen}/> <FormModal open={this.state.formModalOpen}
invoiceIdentifyResponse={this.state.invoiceIdentifyResponse}/>
</>); </>);
} }

View File

@ -1,5 +1,5 @@
import React from 'react'; import React, {useState} from 'react';
import {Button, Form, Input} from 'antd'; import {Button, Form, Input, Alert} from 'antd';
import {LockOutlined, UserOutlined} from '@ant-design/icons'; import {LockOutlined, UserOutlined} from '@ant-design/icons';
import './LoginView.css'; import './LoginView.css';
import loginImg from '../../assets/login.png' import loginImg from '../../assets/login.png'
@ -12,7 +12,8 @@ import {setToken} from "../../models/store";
function LoginView() { function LoginView() {
const navigate = useNavigate(); const navigate = useNavigate();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [showAlert, setShowAlert] = useState(false)
const [alertMessage, setAlertMessage] = useState('')
const onFinish = (values: any) => { const onFinish = (values: any) => {
console.log(values) console.log(values)
const data = new FormData(); const data = new FormData();
@ -30,7 +31,8 @@ function LoginView() {
navigate('/invoice/mine') navigate('/invoice/mine')
}).catch(function (error) { }).catch(function (error) {
console.log(error); console.log(error);
//showAlert.value = true setAlertMessage(error.response.data.msg)
setShowAlert(true)
}); });
console.log('Success:', values); console.log('Success:', values);
}; };
@ -38,6 +40,9 @@ function LoginView() {
console.log('Failed:', errorInfo); console.log('Failed:', errorInfo);
}; };
const afterClose = () =>{
setShowAlert(false)
}
return ( return (
<div className="background"> <div className="background">
@ -93,7 +98,16 @@ function LoginView() {
<Input.Password prefix={<LockOutlined/>} <Input.Password prefix={<LockOutlined/>}
placeholder="密码"/> placeholder="密码"/>
</Form.Item> </Form.Item>
{showAlert&&
<Alert
style={{marginBottom: 20, height: 32}}
message={alertMessage}
type="error"
closable
showIcon
afterClose={afterClose}
/>
}
<Form.Item> <Form.Item>
<Button type="primary" htmlType="submit" style={{width: '100%'}}> <Button type="primary" htmlType="submit" style={{width: '100%'}}>

View File

@ -15,7 +15,7 @@ axiosInstance.interceptors.request.use(
function (config: AxiosRequestConfig) { function (config: AxiosRequestConfig) {
// @ts-ignore // @ts-ignore
config.headers.Authorization = "Bearer " + store.getState().token.accessToken config.headers.Authorization = "Bearer " + store.getState().token.accessToken
console.log(config) //console.log(config)
return config return config
}, },
function (error) { function (error) {

View File

@ -18,7 +18,8 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx" "jsx": "react-jsx",
"downlevelIteration": true
}, },
"include": [ "include": [
"src" "src"