feat(invoice): 增加发票备注和附件功能

- 新增发票备注和附件上传功能- 实现发票列表按验证时间筛选
- 优化发票列表展示,增加验证时间筛选和关键字搜索
- 修复附件上传相关接口
This commit is contained in:
liuxiaoqing 2025-08-19 22:22:44 +08:00
parent 795620b0f8
commit 8c7422dc68
4 changed files with 173 additions and 32 deletions

View File

@ -134,7 +134,7 @@ class DataService:
"WHERE id = ?",
(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), verify_status,inspectionAmount, invoice_id))
conn.commit()
def update_invoice_desc(self, invoice_id: int|str, desc: str, desc_file: str,status: str):
def update_invoice_desc(self, invoice_id: int|str, desc: str, desc_file: str,status: str) -> str:
conn = DBService().get_conn()
cursor = conn.cursor()
cursor.execute(
@ -145,6 +145,7 @@ class DataService:
"WHERE id = ?",
(desc, desc_file,status, invoice_id))
conn.commit()
return "success"
# 查询验证记录
def get_verify_log(self, file_path: str) -> dict[Any, Any]|None:
conn = DBService().get_conn()
@ -205,9 +206,9 @@ class DataService:
else:
sql += " verify_status = '" + verify_status+"'"
if start_date is not None:
sql += " and verify_time >= '" + start_date.strftime("%Y-%m-%d") + "'"
sql += " and verify_time >= '" + start_date + "'"
if end_date is not None:
sql += " and verify_time <= '" + end_date.strftime("%Y-%m-%d") + "'"
sql += " and verify_time <= '" + end_date + "'"
if value is not None and value != '':
sql += " and ( "
sql += "file_path like '%" + value + "%' or "
@ -243,7 +244,7 @@ class DataService:
sql += "desc_files like '%" + value + "%' "
sql += ")"
sql += " order by verify_time desc"
# print(sql)
print(sql)
cursor.execute(sql)
rows = cursor.fetchall()
datas = []

View File

@ -67,7 +67,13 @@ def reverify():
def list_invoice():
value = request.json.get('value')
verify_status = request.json.get('verify_status')
return dataservice.get_invoice_list(value=value,verify_status=verify_status)
verify_time = request.json.get('verify_time')
start_date = None
end_date = None
if verify_time is not None:
start_date = verify_time[0].split(' ')[0]
end_date = verify_time[1].split(' ')[0]
return dataservice.get_invoice_list(value=value,verify_status=verify_status,start_date=start_date,end_date=end_date)
@app.route('/listInvoiceTimeOut',methods=['POST'])
def list_invoice_time_out():
time_out = request.form.get('timeOut')

Binary file not shown.

View File

@ -3,11 +3,19 @@
import {h, onMounted, ref} from "vue";
import {
SearchOutlined,
FileSearchOutlined
FileSearchOutlined,
PlusOutlined
} from '@ant-design/icons-vue'
import request from '#/utils/request.ts';
import {baseURL} from "../utils/baseurl.ts";
import type {TableProps} from "ant-design-vue";
import type {TableProps, UploadChangeParam, UploadProps} from "ant-design-vue";
import { message } from 'ant-design-vue';
import zhCN from 'ant-design-vue/es/locale/zh_CN';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import type {UploadFile} from "ant-design-vue/es/upload/interface";
const [messageApi, contextHolder] = message.useMessage();
interface DataType {
id: string;
status: string;
@ -57,7 +65,6 @@ const columns = [
dataIndex: 'invoiceDate',
key: 'invoiceDate',
},
{
title: '查验结果',
dataIndex: 'verify_status',
@ -90,6 +97,7 @@ let searchParams = ref({
params: {
verify_status: 'success',
value: null,
verify_time: null,
}
});
//
@ -111,6 +119,7 @@ const formData = ref({
status: false,
desc: '',
desc_files: '',
fileIds: new Array<String>(),
});
const handleOk = () => {
@ -119,28 +128,125 @@ const handleOk = () => {
invoiceId: formData.value.id,
status: formData.value.status?'yes':'no',
desc: formData.value.desc,
desc_files: formData.value.desc_files,
desc_files: formData.value.fileIds.join(','),
}).then(( res ) => {
console.log(res)
if (res.code === 200) {
if(res.status == 200){
openModal.value = false;
confirmLoading.value = false;
messageApi.success("修改成功")
}
})
};
const handleOpen = (record) => {
formData.value = record
if (record.status === 'yes') {
modalTitle.value = "备注"
}else {
modalTitle.value = "入账"
}
formData.value = Object.assign({}, record);
formData.value.status = (record.status === 'yes')
formData.value.fileIds = record.desc_files.split(',');
if (record.desc_files) {
let paths = record.desc_files.split(",")
fileList.value = []
for(let i = 0; i < paths.length; i++) {
fileList.value.push({
uid: paths[i],
name: paths[i].split("/")[2],
url: baseURL+"/"+paths[i],
});
}
}
openModal.value = true;
};
//
const getList = () => {
request.post("/listInvoice", {
verify_status : 'success'
verify_status : searchParams.value.params.verify_status,
value : searchParams.value.params.value,
verify_time : searchParams.value.params.verify_time,
}).then(( res ) => {
console.log(res)
data.value = res.data
})
}
const fileList = ref<UploadProps['fileList']>([])
const previewVisible = ref(false);
const previewImage = ref('');
const previewTitle = ref('');
function getBase64(file: File) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
}
//
const handleCancelPreview = () => {
previewVisible.value = false;
previewTitle.value = '';
};
const handlePreview = async (file: UploadProps['fileList'][number]) => {
if (!file.url && !file.preview) {
file.preview = (await getBase64(file.originFileObj)) as string;
}
previewImage.value = file.url || file.preview;
previewVisible.value = true;
previewTitle.value = file.name || file.url.substring(file.url.lastIndexOf('/') + 1);
};
const handleFileChange = (info: UploadChangeParam) => {
if(info.file.status === 'done'){
info.file.uid = info.file.response.file_path
formData.value.fileIds.push(info.file.response.file_path)
}
}
const handleFileRemove = (file: UploadFile ) => {
let index = formData.value.fileIds.indexOf(file.uid)
formData.value.fileIds.splice(index, 1);
}
const handleUpload = (options: any) => {
const { file, onSuccess, onError, onProgress } = options;
// FormData
const formData1 = new FormData();
formData1.append('file', file);
// 使 XMLHttpRequest
const xhr = new XMLHttpRequest();
//
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
onProgress({ percent: (event.loaded / event.total) * 100 }, file);
}
};
//
xhr.onload = () => {
if (xhr.status === 200 || xhr.status === 201) {
console.log(xhr.responseText);
onSuccess(xhr.responseText, file);
let res = JSON.parse(xhr.responseText)
if(formData.value.desc_files){
formData.value.desc_files += ",";
}else{
formData.value.desc_files = ""
}
formData.value.desc_files += res.file_path;
} else {
onError(new Error(`Upload failed with status ${xhr.status}`));
}
};
//
xhr.onerror = () => {
onError(new Error('Network error'));
};
//
xhr.open('POST', baseURL+'/upload');
xhr.send(formData1);
};
dayjs.locale('zh-cn')
onMounted(() => {
getList()
})
@ -151,11 +257,15 @@ onMounted(() => {
<a-row :gutter="20" >
<a-col :span="22" :align="'end'" offset="2">
<a-form v-model:value="searchParams" layout="inline">
<a-form-item label="开票时间">
<a-range-picker></a-range-picker>
<a-form-item label="验证时间">
<a-range-picker
v-model:value="searchParams.params.verify_time"
valueFormat="YYYY-MM-DD HH:mm:ss"
:locale="zhCN.DatePicker"
></a-range-picker>
</a-form-item>
<a-form-item label="销售方">
<a-input v-model="searchParams.params.value"></a-input>
<a-form-item label="关键字">
<a-input v-model:value="searchParams.params.value" allow-clear></a-input>
</a-form-item>
<a-form-item label="验证结果">
<a-radio-group v-model:value="searchParams.params.verify_status">
@ -164,7 +274,7 @@ onMounted(() => {
</a-radio-group>
</a-form-item>
<a-form-item>
<a-button type="primary" :icon="h(SearchOutlined)">搜索</a-button>
<a-button type="primary" :icon="h(SearchOutlined)" @click="getList">搜索</a-button>
</a-form-item>
</a-form>
</a-col>
@ -177,36 +287,60 @@ onMounted(() => {
<a-tag v-if="record.verify_status === 'fail'" color="red">异常</a-tag>
</template>
<template v-if="column.key === 'action'" >
<a-button type="primary" v-if="record.status ==='yes'">备注</a-button>
<a-button type="primary" v-if="!record.status" @click="handleOpen(record)">入账</a-button>
<a-button type="primary" v-if="record.status ==='yes'" @click="handleOpen(record)">备注</a-button>
<a-button type="primary" v-if="record.status ==='no'" @click="handleOpen(record)">入账</a-button>
</template>
</template>
<template #expandedRowRender="{ record }">
<a-row >
<a-col :span="8">
<a-image-preview-group v-if="record.desc_files">
<a-image :width="200" v-for="file in record.desc_files.split(',')" :src="baseURL+'/'+file"/>
</a-image-preview-group>
</a-col>
<a-col :span="16">
<a-typography-title :level="4" >备注</a-typography-title>
<a-typography-paragraph>
<blockquote>{{ record.desc }}asdsad</blockquote>
</a-typography-paragraph>
</a-col>
<a-col :span="8">
<a-image-preview-group>
<a-image :width="200" v-for="file in record.desc_list.split(',')" :src="baseURL+file"/>
</a-image-preview-group>
</a-col>
</a-row>
</template>
<template #expandColumnTitle>
<FileSearchOutlined />
</template>
</a-table>
<a-modal v-model:open="openModal" title="Title" :confirm-loading="confirmLoading" @ok="handleOk">
<a-form v-model:value="formData" layout="vertical">
<a-form-item label="是否入账">
<a-switch v-model:value="formData.status"></a-switch>
<a-modal v-model:open="openModal" :title="modalTitle" :confirm-loading="confirmLoading" @ok="handleOk">
<a-form v-model:value="formData" >
<a-form-item label="是否已报销">
<a-switch v-model:checked="formData.status"></a-switch>
</a-form-item>
<a-form-item label="备注">
<a-textarea v-model:value="formData.desc" :rows="6"></a-textarea>
</a-form-item>
<a-form-item label="附件">
<div class="clearfix">
<a-upload
v-model:file-list="fileList"
:customRequest="handleUpload"
@change="handleFileChange"
@remove="handleFileRemove"
list-type="picture-card"
@preview="handlePreview"
>
<div>
<PlusOutlined />
<div style="margin-top: 8px">上传</div>
</div>
</a-upload>
<a-modal :open="previewVisible" :title="previewTitle" :footer="null" @cancel="handleCancelPreview">
<img alt="example" style="width: 100%" :src="previewImage" />
</a-modal>
</div>
</a-form-item>
</a-form>
</a-modal>
<context-holder />
</template>
<style scoped>
.table-operations {