首页 » 软件开发 » Python写一个电子发票管理工具2:前端界面开发(发票页面引入管理工具开发)

Python写一个电子发票管理工具2:前端界面开发(发票页面引入管理工具开发)

萌界大人物 2024-07-24 01:42:37 0

扫一扫用手机浏览

文章目录 [+]

发票管理小工具要支持B/S和C/S两种部署模式,因为涉及到发票这种隐私数据,能够安装到自己电脑运行可能是大部分人更能接受的方式。

先看一下最终的页面效果。

发票夹页面

Python写一个电子发票管理工具2:前端界面开发(发票页面引入管理工具开发) 软件开发
(图片来自网络侵删)

设置页面

添加抬头页面

技术选型

这个工具我不用通常的Python可视化编程如tkinter或Qt来开发PC客户端,给大家介绍一个不太一样的套路,采用前后端分离的模式来实现。

使用FastAPI做服务端,Vue做前端页面。

B/S模式将程序部署到服务器,用户使用浏览器访问即可;C/S模式用python自动打开浏览器页面的方式来运行,打包成exe下载安装。

需求梳理

首先简单地用思维导图将页面需求整理一下。
主要分为两个功能模块:发票管理(取名发票夹)和设置。
发票夹功能为发票的增删改查以及导入导出。
设置目前包括抬头管理和自定义费用类型管理。

CDN模式使用ElementPlus

对Vue熟悉的朋友看下面的内容就相当简单了,用Vue3和ElementPlus开发网页。
对于网页前端或Vue不太熟悉的朋友可以先看一下Vue的文档和ElementPlus的文档,Vue学习起来还是很简单的。

因为功能很简单这里我直接使用一个单页面来开发这个页面,这样用Vue就相当于Jquery一样。
不需要nodejs,不需要脚手架,使用起来相当简单。
但是这种用法仅限于类似的简单项目,稍微多几个页面还是需要模块化开发,便于代码复用、代码阅读和代码管理。

首先我们用ElmentPlus提供的CDN引入模式(注意:CDN不稳定网站就无法显示了)写一个有两个菜单的页面,通过点击菜单切换显示的内容。
这里需要引入vue、element-plus的css和js(安装 | Element Plus)。

说明1:可以通过浏览器调试界面查看当前使用的vue和elementplus版本,在CDN链接中指定版本和实际css与js链接,这样可以避免版本升级后引入问题,并且省去几次302跳转加快加载时间。

说明2:C/S版本将css和js都下载到本地打包,不使用CDN。

<script src="https://unpkg.com/vue@3.2.33/dist/vue.global.js"></script><!-- import CSS --><link rel="stylesheet" href="https://unpkg.com/element-plus@2.2.0/dist/index.css"><!-- import JavaScript --><script src="https://unpkg.com/element-plus@2.2.0/dist/index.full.js"></script>

<html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width,initial-scale=1.0" /> <title>我的发票夹</title> <script src="https://unpkg.com/vue@3.2.33/dist/vue.global.js"></script> <!-- import CSS --> <link rel="stylesheet" href="https://unpkg.com/element-plus@2.2.0/dist/index.css"> <!-- import JavaScript --> <script src="https://unpkg.com/element-plus@2.2.0/dist/index.full.js"></script> <style> body { margin: 0; } .el-header { --el-header-padding: 0 0; } </style> </head> <body> <div id="app"> <div> <el-container> <el-header> <el-menu :default-active="activeMenuIndex" class="el-menu-demo" mode="horizontal" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b" @select="handleMenuSelect" > <el-menu-item index="1"> 发票夹</el-menu-item> <el-menu-item index="2">设置</el-menu-item> </el-menu> </el-header> <el-main> <div v-show="activeMenuIndex==='1'"> 发票夹 </div> <div v-show="activeMenuIndex==='2'"> 设置 </div> </div> <script> const App = { setup(){ const activeMenuIndex = Vue.ref('1'); const handleMenuSelect = (key, keyPath) => { activeMenuIndex.value = key; }; return { activeMenuIndex, handleMenuSelect, } }, }; const app = Vue.createApp(App); app.use(ElementPlus); vm = app.mount("#app"); </script> </body></html>

使用Icon图标

新版的ElementPlus提供了CDN模式的Icon,需要引入以下js,并且对图标组件进行全局注册。

<script src="https://unpkg.com/@element-plus/icons-vue@1.1.4/dist/index.iife.min.js"></script>const app = Vue.createApp(App);app.use(ElementPlus);//注册icon组件for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component);}

下面为发票夹和设置添加图标:

<el-menu-item index="1"><el-icon><folder></folder></el-icon>发票夹</el-menu-item><el-menu-item index="2"><el-icon><setting></setting></el-icon>设置</el-menu-item>

图标就出来了

注意1:直接复制ElementPlus示例代码到html中是不能正常显示的,因为<folder />这样单标签的写法是不可以的,因为这些标签都不是html原生的标签,必须写成<folder></folder>这样的双标签。

注意2:使用两个或以上单词的组件,如<FolderAdd/>,需要使用-隔开单词<Folder-Add></Folder-Add>。

当然,不使用Icon组件,直接使用SVG也可以。
例如上面的folder图标,将源码中的SVG直接拷贝出来使用就可以。

<el-icon><folder></folder></el-icon><!--直接替换svg--><el-icon><svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-ba633cb8=""><path fill="currentColor" d="M128 192v640h768V320H485.76L357.504 192H128zm-32-64h287.872l128.384 128H928a32 32 0 0 1 32 32v576a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32z"></path></svg></el-icon>国际化

因为ElementPlus默认语言是英语,所以需要引入中文国际化组件才能显示中文。
引入方法如下:

<!--引入中文国际化--><script src="https://unpkg.com/element-plus@2.2.0/dist/locale/zh-cn.js"></script>app.use(ElementPlus, {locale: ElementPlusLocaleZhCn,});JS加载完再显示页面

这样的单html页面,在js加载完之前,会显示一些页面标签和文字,然后再展示正常页面。
如下图:

可以先将body设置为不显示,然后onload后再显示。

<body style="display:none"> ...... <script> ...... window.onload = () => document.body.style.display='block' </script> </script></body>页面代码

页面就是施展CV大法了,选择需要使用的组件,将ElementPlus页面上的示例代码拷贝粘贴,修改样式和JS代码,基础页面就写完了。
接下来就是定义接口、设计数据库和编写前后端逻辑代码了~

<html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width,initial-scale=1.0" /> <title>我的发票夹</title> <!-- 引入vuejs --> <script src="https://unpkg.com/vue@3.2.33/dist/vue.global.js"></script> <!--引入element-plus css--> <link rel="stylesheet" href="https://unpkg.com/element-plus@2.2.0/dist/index.css"> <!--引入element-plus--> <script src="https://unpkg.com/element-plus@2.2.0/dist/index.full.js"></script> <!--引入element-plus中文国际化--> <script src="https://unpkg.com/element-plus@2.2.0/dist/locale/zh-cn.js"></script> <!--引入element-plus Icon--> <script src="https://unpkg.com/@element-plus/icons-vue@1.1.4/dist/index.iife.min.js"></script> <!--引入axios--> <script src="https://unpkg.com/axios@0.27.2/dist/axios.min.js"></script> <style> body { margin: 0; } .el-header { --el-header-padding: 0 0; } .el-range-editor.el-input__inner { padding: 0 5px; } .el-table .cell{ font-size: 14px; padding: 0 6px; } </style> </head> <body style="display:none"> <div id="app"> <div> <el-container> <el-header> <el-menu :default-active="activeMenuIndex" mode="horizontal" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b" @select="handleMenuSelect" > <el-menu-item index="1"><el-icon><folder></folder></el-icon>发票夹</el-menu-item> <el-menu-item index="2"><el-icon><setting></setting></el-icon>设置</el-menu-item> </el-menu> </el-header> <el-main> <div v-show="activeMenuIndex==='1'"> <el-form v-model="invoiceSearch" :inline="true" style="margin-bottom: 0;"> <el-form-item label=""> <el-radio-group v-model="invoiceSearch.timeType"> <el-radio-button label="printTime" >开票时间</el-radio-button> <el-radio-button label="importTime" >导入时间</el-radio-button> </el-radio-group> </el-form-item> <el-form-item label=""> <el-date-picker v-model="invoiceSearch.value1" type="monthrange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" style="width: 200px;" /> </el-form-item> <el-form-item label=""> <el-select v-model="invoiceSearch.purchaser_name" clearable placeholder="公司名"> <el-option v-for="item in purchasers" :key="item.name" :label="item.name" :value="item.name" /> </el-select> </el-form-item> <el-form-item label=""> <el-select v-model="invoiceSearch.used" clearable placeholder="发票状态" style="width:100px"> <el-option v-for="item in invoiceStatus" :key="item.value" :label="item.label" :value="item.value" /> </el-select> </el-form-item> <el-form-item label=""> <el-select v-model="invoiceSearch.feeType" multiple placeholder="费用类型" style="width:300px"> <el-option v-for="item in feeTypes" :key="item" :label="item" :value="item" /> </el-select> </el-form-item> <el-form-item label=""> <el-button type="primary" ><el-icon style="margin-right:6px"><search></search></el-icon>搜索</el-button> <el-button type="default"><el-icon style="margin-right:6px"><refresh-left></refresh-left></el-icon>重置</el-button> </el-form-item> </el-form> <el-divider style="margin: 0;"></el-divider> <el-row style="margin: 18px 0 8px 0;"> <el-col :span="24"> <el-button type="primary" plain v-on:click="handleClick" style="margin-bottom:10px"> <el-icon style="margin-right:6px"><folder-add></folder-add></el-icon>批量导入 </el-button> <el-button type="success" plain v-on:click="handleClick" style="margin-bottom:10px"> <el-icon style="margin-right:6px"><plus></plus></el-icon>手动添加 </el-button> <el-button type="danger" plain v-on:click="handleClick" style="margin-bottom:10px"> <el-icon style="margin-right:6px"><delete></delete></el-icon>删除 </el-button> <el-dropdown style="margin:0 0 10px 12px"> <el-button type="warning" plain v-on:click="handleClick" style="margin-bottom:10px"> <el-icon style="margin-right:6px"><download></download></el-icon>导出 <el-icon class="el-icon--right"></el-icon><arrow-down></arrow-down></el-button> <template #dropdown> <el-dropdown-menu> <el-dropdown-item v-on:click="handleClick">Excel报表</el-dropdown-item> <el-dropdown-item>合并PDF发票</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> <el-dropdown style="margin:0 0 10px 12px"> <el-button type=""> 设置费用类型<el-icon class="el-icon--right"><arrow-down></arrow-down></el-icon> </el-button> <template #dropdown> <el-dropdown-menu> <el-dropdown-item v-on:click="handleClick" v-for="item in feeTypes" :key="item">{{item}}</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> <el-dropdown style="margin:0 0 10px 12px"> <el-button type=""> 设置发票状态<el-icon class="el-icon--right"><arrow-down></arrow-down></el-icon> </el-button> <template #dropdown> <el-dropdown-menu> <el-dropdown-item v-on:click="handleClick">已使用</el-dropdown-item> <el-dropdown-item>未使用</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </el-col> </el-row> <div> <el-table :data="invoiceList"> <el-table-column type="selection" width="40"></el-table-column> <el-table-column prop="date" label="添加日期" width="110" ></el-table-column> <el-table-column prop="date" label="开票日期" width="110" ></el-table-column> <el-table-column prop="purchaser_name" label="购买方"></el-table-column> <el-table-column prop="seller_name" label="销售方" > <template #default="scope"> <el-popover effect="light" trigger="hover" placement="top"> <template #default> <div>名称: {{ scope.row.seller_name }}</div> <div>税号: {{ scope.row.seller_tax_number }}</div> </template> <template #reference> <el-tag>{{ scope.row.seller_name }}</el-tag> </template> </el-popover> </template> </el-table-column> <el-table-column prop="service_type" label="项目类型" ></el-table-column> <el-table-column prop="total" label="价税合计" width="100" ></el-table-column> <el-table-column prop="code" label="发票代码" width="130" ></el-table-column> <el-table-column prop="number" label="发票号码" width="100" ></el-table-column> <el-table-column prop="check_code" label="校验码" width="190" ></el-table-column> <el-table-column prop="fee_type" label="费用类型" width="120"></el-table-column> <el-table-column prop="used" label="发票状态" width="80" ></el-table-column> <!-- <el-table-column prop="address" label="Address"></el-table-column> --> <el-table-column label="操作" width="135"> <template #default="scope"> <el-button size="small" @click="handleEdit(scope.$index, scope.row)">修改</el-button> <el-button size="small" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button> </template> </el-table-column> </el-table> </div> <div style="margin-top: 18px;"> <el-pagination v-model:currentPage="currentPage" v-model:page-size="pageSize" :page-sizes="[10, 20, 50]" small="small" background="background" layout="total, sizes, prev, pager, next" :total="50" /> <!-- @size-change="handleSizeChange" @current-change="handleCurrentChange" --> </div> </div> <div v-show="activeMenuIndex==='2'"> <el-collapse v-model="activeNames" @change="handleChange"> <el-collapse-item title="发票抬头" name="1" > <div> <el-button type="primary" v-on:click="purchaserDialogVisible=true"> <el-icon style="margin-right:6px"><plus></plus></el-icon>添加抬头 </el-button> <el-tag type="success" style="margin-left:10px"> <el-icon><warning></warning></el-icon> 用于校验有效发票抬头 </el-tag> </div> <div> <el-table :data="purchasers" table-layout="auto"> <el-table-column prop="name" label="名称" width="300"></el-table-column> <el-table-column prop="tax_number" label="税号" width="200" ></el-table-column> <el-table-column prop="address" label="单位地址"></el-table-column> <el-table-column prop="phone" label="电话号码" ></el-table-column> <el-table-column prop="bank_name" label="开户银行" ></el-table-column> <el-table-column prop="bank_account" label="银行账号" ></el-table-column> <el-table-column label="操作" width="135"> <template #default="scope"> <el-button size="small" @click="handleEdit(scope.$index, scope.row)">修改</el-button> <el-button size="small" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button> </template> </el-table-column> </el-table> </div> </el-collapse-item> <el-collapse-item title="费用类型" name="2"> <div> <el-tag v-for="tag in feeTypes" :key="tag" style="margin: 4px 8px;" closable :disable-transitions="false" @close="handleClose(tag)" > {{ tag }} </el-tag> <el-input style="width: 100px" v-if="inputVisible" v-model="inputValue" size="small" @keyup.enter="handleInputConfirm" @blur="handleInputConfirm" ref="tagInput" ></el-input> <el-button v-else size="small" @click="showInput"> + 添加费用类型 </el-button> </div> </el-collapse-item> </el-collapse> </div> </el-main> </el-container> </div> <el-dialog v-model="purchaserDialogVisible" :title="purchaserDialogTitle" width="500"> <el-form :model="purchaser"> <el-form-item label="名称" label-width="150"> <el-input v-model="purchaser.name" clearable placeholder="单位名称或个人姓名(必填)" style="width:250px"></el-input> </el-form-item> <el-form-item label="税号" label-width="150"> <el-input v-model="purchaser.tax_number" clearable placeholder="税号(选填)" style="width:250px"></el-input> </el-form-item> <el-form-item label="单位地址" label-width="150"> <el-input v-model="purchaser.address" clearable placeholder="单位地址(选填)" style="width:250px"></el-input> </el-form-item> <el-form-item label="电话号码" label-width="150"> <el-input v-model="purchaser.phone" clearable placeholder="电话号码(选填)" style="width:250px"></el-input> </el-form-item> <el-form-item label="开户银行" label-width="150"> <el-input v-model="purchaser.bank_name" clearable placeholder="开户银行(选填)" style="width:250px"></el-input> </el-form-item> <el-form-item label="银行账号" label-width="150"> <el-input v-model="purchaser.bank_account" clearable placeholder="银行账号(选填)" style="width:250px"></el-input> </el-form-item> </el-form> <template #footer> <span class="dialog-footer"> <el-button type="primary" @click="purchaserDialogVisible = false">确定</el-button> <el-button @click="purchaserDialogVisible = false">取消</el-button> </span> </template> </el-dialog> </div> <script> const App = { setup(){ const message = Vue.ref("Hello Element Plus"); const activeMenuIndex = Vue.ref('1'); const invoiceSearch = Vue.ref({ timeType: 'printTime', value1: ['2022-01', '2023-01'], value2: '', feeType: '', }); const pageSize = Vue.ref(20); const currentPage = Vue.ref(1); const activeNames = Vue.ref(['1','2']); const invoiceStatus = Vue.ref([{ value: '1', label: '已使用', }, { value: '0', label: '未使用', }]); const purchasers = Vue.ref([ { name: '购买方(中国)网络技术有限公司', tax_number: '91320301MA1FQ5XXX', address: '广东省深圳市南山区深南大道', phone: '010-88888888', bank_name: '中国工商银行', bank_account: '62220231234567899999', }, ]); const invoiceList = Vue.ref([ { date: '2012/05/11', purchaser_name: '购买方(中国)网络技术有限公司', seller_name: '销售方信息服务有限公司', seller_tax_number: '91320301MA1FQ5000', service_type: '经纪代理服务机票代理服务', total: '10000.00', code: '011001700123', number: '93072888', check_code: '07963653756209149998', fee_type: '差旅费', used: '已使用', name: '差旅费', address: 'No. 189, Grove St, Los Angeles', }, { date: '2012/05/11', purchaser_name: '购买方(中国)网络技术有限公司', seller_name: '销售方信息服务有限公司', seller_tax_number: '91320301MA1FQ5000', service_type: '经纪代理服务机票代理服务', total: '10000.00', code: '011001700123', number: '93072888', check_code: '07963653756209149998', fee_type: '差旅费', used: '已使用', name: '差旅费', address: 'No. 189, Grove St, Los Angeles', }, ]); const feeTypes = Vue.ref(['交通','住宿','餐饮','通讯','办公','快递','其他']); const tagInput = Vue.ref() const inputValue = Vue.ref(''); const inputVisible = Vue.ref(false); const purchaserDialogVisible = Vue.ref(false); const purchaserDialogTitle = Vue.ref('添加发票抬头') const purchaser = Vue.reactive({}); const handleMenuSelect = (key, keyPath) => { activeMenuIndex.value = key; }; const handleClick = () => { ElementPlus.ElMessageBox.confirm( '点击按钮XX,是否确定XXX?', '系统提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning', } ) .then(() => { ElementPlus.ElMessage({ type: 'success', message: 'XXX成功', }) }) .catch(() => { ElementPlus.ElMessage({ type: 'info', message: 'XXXX取消', }) }) }; const handleEdit = (index, row) => { console.log(index, row); }; const handleChange = (val) => console.log(val); const handleClose = (tag) =>{ feeTypes.value.splice(feeTypes.value.indexOf(tag), 1); }; const showInput = () =>{ inputVisible.value = true Vue.nextTick(() => { tagInput.value.focus() }) }; const handleInputConfirm = () => { if (inputValue.value) { feeTypes.value.push(inputValue.value) } inputVisible.value = false inputValue.value = '' }; return { message, activeMenuIndex, invoiceSearch, pageSize, currentPage, activeNames, invoiceStatus, purchasers, invoiceList, feeTypes, tagInput, inputValue, inputVisible, purchaserDialogVisible, purchaser, purchaserDialogTitle, handleMenuSelect, handleClick, handleEdit, handleChange, handleClose, showInput, handleInputConfirm, } }, }; const app = Vue.createApp(App); app.use(ElementPlus, {locale: ElementPlusLocaleZhCn,}); for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component); } vm = app.mount("#app"); window.onload = function(){ document.body.style.display='block' } </script> </body></html>

标签:

相关文章

语言中的借用,文化交融的桥梁

自古以来,人类社会的交流与发展离不开语言的传播。在漫长的历史长河中,各民族、各地区之间的文化相互碰撞、交融,产生了许多独特的语言现...

软件开发 2025-01-01 阅读1 评论0

机顶盒协议,守护数字生活的新卫士

随着科技的飞速发展,数字家庭逐渐走进千家万户。在这个时代,机顶盒成为了连接我们与丰富多彩的数字世界的重要桥梁。而机顶盒协议,作为保...

软件开发 2025-01-01 阅读1 评论0

语言基础在现代社会的重要性及方法步骤

语言是人类沟通的桥梁,是社会发展的基础。语言基础作为语言学习的基石,对于个人、社会乃至国家的发展具有重要意义。本文将从语言基础在现...

软件开发 2025-01-01 阅读2 评论0

粤语电影,传承文化,点亮时代之光

粤语电影,作为中国电影产业的一朵奇葩,以其独特的地域特色、丰富的文化内涵和鲜明的艺术风格,赢得了广大观众的喜爱。本文将从粤语电影的...

软件开发 2025-01-01 阅读3 评论0

苹果游戏语言,塑造未来娱乐体验的基石

随着科技的飞速发展,游戏产业逐渐成为全球娱乐市场的重要支柱。在我国,游戏产业更是蓬勃发展,吸引了无数玩家和投资者的目光。而在这其中...

软件开发 2025-01-01 阅读1 评论0