feat(发票管理): 新增发票详情功能和优化发票信息展示

- 在会员API中添加获取发票详情的接口
- 更新发票模态框,支持电子普通发票和增值税专用发票的详细信息展示
- 在订单详情页和支付页面优化发票信息的显示逻辑
- 增加发票信息的校验和格式化处理
This commit is contained in:
田香琪
2026-04-13 18:52:32 +08:00
parent 2e8d257140
commit b6a2dbc23a
7 changed files with 1069 additions and 382 deletions

View File

@@ -147,9 +147,14 @@ export const getReceiptPage = params => {
return getRequest(`/trade/receipt`, params);
};
//获取发票列表
export const invoicing = id => {
return postRequest(`/trade/receipt/${id}/invoicing`);
//获取发票详情
export const getReceiptDetail = id => {
return getRequest(`/trade/receipt/get/${id}`);
};
//开发票
export const invoicing = (id, params) => {
return postRequest(`/trade/receipt/${id}/invoicing`, params);
};
//查询包裹列表

View File

@@ -21,7 +21,11 @@
<Button @click="handleReset" class="search-btn">重置</Button>
</Form>
</Card>
<Card>
<div class="receipt-tip">
订单状态为已发货/已完成可开票
</div>
<Table class="mt_10" :loading="loading" border :columns="columns" :data="data" ref="table">
<!-- 订单详情格式化 -->
<template slot="orderSlot" slot-scope="scope">
@@ -33,17 +37,135 @@
show-total show-elevator show-sizer></Page>
</Row>
</Card>
<Modal
v-model="receiptModalVisible"
title="发票信息"
:mask-closable="false"
width="680"
>
<div v-if="receiptDetailLoading" class="receipt-modal-loading">
<Spin size="large" />
</div>
<div v-else class="receipt-modal-content">
<div v-if="hasValue(currentReceipt.orderSn)" class="receipt-item">
<span class="receipt-label">订单号</span>
<span class="receipt-value">{{ currentReceipt.orderSn }}</span>
</div>
<div v-if="hasValue(currentReceipt.memberName)" class="receipt-item">
<span class="receipt-label">会员名称</span>
<span class="receipt-value">{{ currentReceipt.memberName }}</span>
</div>
<div v-if="hasValue(currentReceipt.receiptType) || hasValue(currentReceipt.invoiceKind)" class="receipt-item">
<span class="receipt-label">发票类型</span>
<span class="receipt-value">{{ formatReceiptType(currentReceipt) }}</span>
</div>
<div v-if="hasValue(currentReceipt.receiptTitle)" class="receipt-item">
<span class="receipt-label">发票抬头</span>
<span class="receipt-value">{{ currentReceipt.receiptTitle }}</span>
</div>
<div v-if="hasValue(currentReceipt.companyName)" class="receipt-item">
<span class="receipt-label">单位名称</span>
<span class="receipt-value">{{ currentReceipt.companyName }}</span>
</div>
<div v-if="hasValue(currentReceipt.personalName)" class="receipt-item">
<span class="receipt-label">个人名称</span>
<span class="receipt-value">{{ currentReceipt.personalName }}</span>
</div>
<div v-if="hasValue(currentReceipt.taxpayerId)" class="receipt-item">
<span class="receipt-label">纳税人识别号</span>
<span class="receipt-value">{{ currentReceipt.taxpayerId }}</span>
</div>
<div v-if="hasValue(currentReceipt.companyAddress)" class="receipt-item">
<span class="receipt-label">单位地址</span>
<span class="receipt-value">{{ currentReceipt.companyAddress }}</span>
</div>
<div v-if="hasValue(currentReceipt.companyPhone)" class="receipt-item">
<span class="receipt-label">单位电话</span>
<span class="receipt-value">{{ currentReceipt.companyPhone }}</span>
</div>
<div v-if="hasValue(currentReceipt.bankName)" class="receipt-item">
<span class="receipt-label">开户银行</span>
<span class="receipt-value">{{ currentReceipt.bankName }}</span>
</div>
<div v-if="hasValue(currentReceipt.bankAccount)" class="receipt-item">
<span class="receipt-label">银行账号</span>
<span class="receipt-value">{{ currentReceipt.bankAccount }}</span>
</div>
<div v-if="hasValue(currentReceipt.receiptContent)" class="receipt-item">
<span class="receipt-label">发票内容</span>
<span class="receipt-value">{{ currentReceipt.receiptContent }}</span>
</div>
<div v-if="hasPrice(currentReceipt.receiptPrice)" class="receipt-item">
<span class="receipt-label">发票金额</span>
<span class="receipt-value">{{ formatPrice(currentReceipt.receiptPrice) }}</span>
</div>
<div v-if="hasValue(currentReceipt.receiptPhone)" class="receipt-item">
<span class="receipt-label">收票人手机</span>
<span class="receipt-value">{{ currentReceipt.receiptPhone }}</span>
</div>
<div v-if="hasValue(currentReceipt.receiptEmail)" class="receipt-item">
<span class="receipt-label">收票人邮箱</span>
<span class="receipt-value">{{ currentReceipt.receiptEmail }}</span>
</div>
<div v-if="hasValue(getInvoiceAddress(currentReceipt))" class="receipt-item">
<span class="receipt-label">发票附件</span>
<span class="receipt-value">
<a @click="viewInvoiceFile(getInvoiceAddress(currentReceipt))">查看附件</a>
</span>
</div>
</div>
<div slot="footer">
<template v-if="receiptModalMode === 'invoicing'">
<Upload
:action="uploadFileUrl"
:data="receiptUploadData"
:headers="{ ...accessToken }"
:format="['jpg', 'jpeg', 'png', 'pdf']"
:max-size="10240"
:on-success="handleInvoiceUploadSuccess"
:on-error="handleInvoiceUploadError"
:on-format-error="handleInvoiceFormatError"
:on-exceeded-size="handleInvoiceMaxSize"
:show-upload-list="false"
style="display: inline-block; margin-right: 8px"
>
<Button :disabled="receiptDetailLoading">上传发票</Button>
</Upload>
<Button @click="receiptModalVisible = false">取消</Button>
<Button
type="primary"
:loading="invoiceSubmitting"
@click="submitInvoicing"
>
确认开票
</Button>
</template>
<Button v-else @click="receiptModalVisible = false">关闭</Button>
</div>
</Modal>
</div>
</template>
<script>
import * as API_Order from "@/api/order";
import { uploadFile } from "@/libs/axios";
export default {
name: "receipt",
data() {
return {
loading: true, // 表单加载状态
receiptModalVisible: false,
receiptDetailLoading: false,
invoiceSubmitting: false,
receiptModalMode: "detail",
uploadFileUrl: uploadFile,
accessToken: {},
receiptUploadData: {
directoryPath: "receipt"
},
currentReceipt: {},
selectedReceiptRow: null,
searchForm: {
// 搜索框初始化对象
pageNumber: 1, // 当前页数
@@ -107,7 +229,7 @@ export default {
width: 100,
tooltip: true,
render: (h, params) => {
if (params.row.receiptStatus === 0) {
if (Number(params.row.receiptStatus) === 0) {
return h("div", [
h("tag", { props: { color: "volcano" } }, "未开票"),
]);
@@ -162,12 +284,21 @@ export default {
fixed: 'right',
width: 200,
render: (h, params) => {
const disabled = !(((params.row.orderStatus === "COMPLETED" || params.row.orderStatus === "DELIVERED")) && params.row.receiptStatus === 0);
const disabled = !this.canInvoicing(params.row);
const detailStyle = { color: "#2d8cf0", cursor: "pointer", textDecoration: "none", marginRight: "12px" };
const style = disabled
? { color: "#c5c8ce", cursor: "not-allowed", textDecoration: "none" }
: { color: "#2d8cf0", cursor: "pointer", textDecoration: "none" };
const on = disabled ? {} : { click: () => { this.invoicing(params.row); } };
const on = disabled ? {} : { click: () => { this.openReceiptModal(params.row, "invoicing"); } };
return h("div", [
h(
"a",
{
style: detailStyle,
on: { click: () => { this.openReceiptModal(params.row, "detail"); } },
},
"详情"
),
h(
"a",
{
@@ -185,6 +316,102 @@ export default {
};
},
methods: {
canInvoicing(row) {
if (!row) return false;
const orderStatus = row.orderStatus;
const receiptStatus = Number(row.receiptStatus);
return (orderStatus === "COMPLETED" || orderStatus === "DELIVERED") && receiptStatus === 0;
},
initUploadAccessToken() {
this.accessToken = {
accessToken: this.getStore("accessToken")
};
},
hasValue(value) {
if (value === null || value === undefined) return false;
return String(value).trim() !== "";
},
hasPrice(value) {
return value !== null && value !== undefined && value !== "";
},
formatValue(value) {
if (value === null || value === undefined) return "暂无";
const text = String(value).trim();
return text ? text : "暂无";
},
formatPrice(value) {
if (value === null || value === undefined || value === "") return "暂无";
return `${value}`;
},
isVatSpecialReceipt(receipt) {
if (!receipt) return false;
const receiptType = receipt.receiptType != null ? String(receipt.receiptType).trim() : "";
return receiptType === "2" || receiptType === "增值税专用发票" || receipt.invoiceKind === "VAT_SPECIAL";
},
formatReceiptType(receipt) {
if (!receipt || (!receipt.receiptType && !receipt.invoiceKind)) return "电子普通发票";
const receiptType = receipt.receiptType != null ? String(receipt.receiptType).trim() : "";
if (receiptType === "电子普通发票" || receiptType === "增值税专用发票") return receiptType;
return this.isVatSpecialReceipt(receipt) ? "增值税专用发票" : "电子普通发票";
},
formatReceiptHeaderType(receipt) {
if (!receipt) return "暂无";
if (this.isVatSpecialReceipt(receipt)) return "单位";
if (receipt.companyName) return "单位";
if (receipt.personalName) return "个人";
const receiptTitle = receipt.receiptTitle != null ? String(receipt.receiptTitle).trim() : "";
if (receiptTitle === "单位" || receiptTitle === "个人") return receiptTitle;
return receipt.taxpayerId ? "单位" : "个人";
},
getReceiptTitleLabel(receipt) {
return this.formatReceiptHeaderType(receipt) === "单位" ? "单位名称" : "个人名称";
},
getReceiptTitleName(receipt) {
if (!receipt) return "";
if (receipt.companyName) return receipt.companyName;
if (receipt.personalName) return receipt.personalName;
const receiptTitle = receipt.receiptTitle != null ? String(receipt.receiptTitle).trim() : "";
if (receiptTitle === "单位" || receiptTitle === "个人") return "";
return receiptTitle;
},
getInvoiceAddress(receipt) {
if (!receipt) return "";
return receipt.invoiceAddress || receipt.invoiceFileUrl || "";
},
buildInvoicingPayload() {
const invoiceAddress = this.getInvoiceAddress(this.currentReceipt);
return invoiceAddress ? { invoiceAddress } : {};
},
handleInvoiceUploadSuccess(res) {
if (res && res.success && res.result) {
this.$set(this.currentReceipt, "invoiceAddress", res.result);
if (this.selectedReceiptRow) {
this.$set(this.selectedReceiptRow, "invoiceAddress", res.result);
}
this.$Message.success("发票上传成功");
} else {
this.$Message.error((res && res.message) || "发票上传失败");
}
},
handleInvoiceUploadError() {
this.$Message.error("发票上传失败");
},
handleInvoiceFormatError() {
this.$Notice.warning({
title: "文件格式不正确",
desc: "请上传 jpg、jpeg、png 或 pdf 格式文件"
});
},
handleInvoiceMaxSize() {
this.$Notice.warning({
title: "超过文件大小限制",
desc: "发票附件不能超过 10MB"
});
},
viewInvoiceFile(url) {
if (!url) return;
window.open(url, "_blank");
},
// 初始化数据
init() {
this.getData();
@@ -232,25 +459,54 @@ export default {
this.total = this.data.length;
this.loading = false;
},
//开发票
invoicing(params) {
this.$Modal.confirm({
title: "确认开票",
content: "您确认已经开具发票 ?",
loading: true,
onOk: () => {
API_Order.invoicing(params.id).then((res) => {
if (res.success) {
this.$Message.success("开票成功");
}
this.$Modal.remove();
this.getData();
});
},
});
async openReceiptModal(row, mode = "detail") {
if (!row) return;
this.receiptModalMode = mode;
this.selectedReceiptRow = row;
this.currentReceipt = { ...row };
this.receiptModalVisible = true;
this.receiptDetailLoading = true;
try {
const res = await API_Order.getReceiptDetail(row.id);
if (res.success && res.result) {
this.currentReceipt = { ...row, ...res.result };
} else {
this.$Message.warning("发票详情获取失败,已展示列表中的发票信息");
}
} catch (e) {
this.$Message.error("发票详情获取失败");
} finally {
this.receiptDetailLoading = false;
}
},
async submitInvoicing() {
if (!this.selectedReceiptRow) return;
if (!this.canInvoicing(this.selectedReceiptRow)) {
this.$Message.warning("当前订单状态不支持开票");
return;
}
const params = this.buildInvoicingPayload();
if (!params.invoiceAddress) {
this.$Message.warning("请先上传发票");
return;
}
this.invoiceSubmitting = true;
try {
const res = await API_Order.invoicing(this.selectedReceiptRow.id, params);
if (res.success) {
this.$Message.success("开票成功");
this.receiptModalVisible = false;
this.getData();
}
} catch (e) {
this.$Message.error("开票失败");
} finally {
this.invoiceSubmitting = false;
}
},
},
mounted() {
this.initUploadAccessToken();
this.init();
},
};
@@ -258,4 +514,44 @@ export default {
<style lang="scss">
// 建议引入通用样式 可删除下面样式代码
@import "@/styles/table-common.scss";
.receipt-modal-loading {
display: flex;
align-items: center;
justify-content: center;
min-height: 240px;
}
.receipt-modal-content {
max-height: 460px;
overflow-y: auto;
}
.receipt-item {
display: flex;
margin-bottom: 12px;
line-height: 22px;
}
.receipt-tip {
margin-bottom: 16px;
padding: 8px 12px;
color: #ff9900;
background: #fff7e6;
border: 1px solid #ffd591;
border-radius: 4px;
}
.receipt-label {
width: 110px;
color: #515a6e;
flex-shrink: 0;
text-align: right;
}
.receipt-value {
flex: 1;
color: #17233d;
word-break: break-all;
}
</style>