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

@@ -180,6 +180,15 @@ export function receiptList () {
});
}
// 发票详情
export function receiptDetail (id) {
return request({
url: `/buyer/trade/receipt/${id}`,
method: Method.GET,
needToken: true
});
}
// 保存发票信息
export function saveReceipt (params) {
return request({

View File

@@ -1,39 +1,83 @@
<template>
<div class="invoice-modal">
<Modal v-model="invoiceAvailable" width="600" footer-hide>
<Modal v-model="invoiceAvailable" width="640" footer-hide>
<p slot="header">
<span>发票信息</span>
</p>
<!-- 普通发票 -->
<div class="nav-content">
<Form :model="invoiceForm" ref="form" label-position="left" :rules="ruleInline" :label-width="110">
<Form :model="invoiceForm" ref="form" label-position="left" :rules="formRules" :label-width="118">
<FormItem label="发票类型">
<RadioGroup v-model="invoice" type="button" button-style="solid">
<Radio @on-change="changeInvoice" :label="1">电子普通发票</Radio>
<Radio :label="2" :disabled="true">增值税专用发票</Radio>
</RadioGroup>
</FormItem>
<FormItem label="发票抬头">
<RadioGroup v-model="type" @on-change="changeInvoice" type="button" button-style="solid">
<Radio :label="1">个人</Radio>
<Radio :label="2">单位</Radio>
</RadioGroup>
</FormItem>
<FormItem label="个人名称" v-if="type === 1" prop="receiptTitle">
<i-input v-model="invoiceForm.receiptTitle"></i-input>
</FormItem>
<FormItem label="单位名称" v-if="type === 2" prop="receiptTitle">
<i-input v-model="invoiceForm.receiptTitle"></i-input>
</FormItem>
<FormItem label="纳税人识别号" v-if="type === 2" prop="taxpayerId">
<i-input v-model="invoiceForm.taxpayerId"></i-input>
</FormItem>
<FormItem label="发票内容">
<RadioGroup v-model="invoiceForm.receiptContent" type="button" button-style="solid">
<Radio label="商品明细">商品明细</Radio>
<Radio label="商品类别">商品类别</Radio>
<RadioGroup v-model="invoice" type="button" button-style="solid" @on-change="onInvoiceKindChange">
<Radio :label="1">电子普通发票</Radio>
<Radio :label="2">增值税专用发票</Radio>
</RadioGroup>
</FormItem>
<!-- 电子普通发票 -->
<div v-if="invoice === 1" key="invoice-normal">
<FormItem label="发票抬头">
<RadioGroup v-model="type" type="button" button-style="solid" @on-change="onHeaderTypeChange">
<Radio :label="1">个人</Radio>
<Radio :label="2">单位</Radio>
</RadioGroup>
</FormItem>
<FormItem :label="type === 1 ? '个人名称' : '单位名称'" :prop="type === 1 ? 'personalName' : 'companyName'">
<i-input v-if="type === 1" v-model="invoiceForm.personalName"></i-input>
<i-input v-else v-model="invoiceForm.companyName"></i-input>
</FormItem>
<FormItem label="纳税人识别号" v-if="type === 2" prop="taxpayerId">
<i-input v-model="invoiceForm.taxpayerId"></i-input>
</FormItem>
<FormItem label="发票内容">
<RadioGroup v-model="invoiceForm.receiptContent" type="button" button-style="solid">
<Radio label="商品明细">商品明细</Radio>
<Radio label="商品类别">商品类别</Radio>
</RadioGroup>
</FormItem>
<FormItem label="收票人手机" prop="receiptPhone">
<i-input v-model="invoiceForm.receiptPhone"></i-input>
</FormItem>
<FormItem label="收票人邮箱" prop="receiptEmail">
<i-input v-model="invoiceForm.receiptEmail" placeholder="可选"></i-input>
</FormItem>
</div>
<!-- 增值税专用发票固定为单位 -->
<div v-else key="invoice-vat">
<FormItem label="发票抬头">
<span class="inv-title-fixed">单位</span>
</FormItem>
<FormItem label="单位名称" prop="companyName">
<i-input v-model="invoiceForm.companyName" placeholder="与营业执照一致"></i-input>
</FormItem>
<FormItem label="纳税人识别号" prop="taxpayerId">
<i-input v-model="invoiceForm.taxpayerId"></i-input>
</FormItem>
<FormItem label="单位地址" prop="companyAddress">
<i-input v-model="invoiceForm.companyAddress" placeholder="注册地址"></i-input>
</FormItem>
<FormItem label="单位电话" prop="companyPhone">
<i-input v-model="invoiceForm.companyPhone" placeholder="固话或手机"></i-input>
</FormItem>
<FormItem label="开户银行" prop="bankName">
<i-input v-model="invoiceForm.bankName"></i-input>
</FormItem>
<FormItem label="银行账号" prop="bankAccount">
<i-input v-model="invoiceForm.bankAccount"></i-input>
</FormItem>
<FormItem label="发票内容">
<RadioGroup v-model="invoiceForm.receiptContent" type="button" button-style="solid">
<Radio label="商品明细">商品明细</Radio>
</RadioGroup>
</FormItem>
<FormItem label="收票人手机" prop="receiptPhone">
<i-input v-model="invoiceForm.receiptPhone"></i-input>
</FormItem>
<FormItem label="收票人邮箱" prop="receiptEmail">
<i-input v-model="invoiceForm.receiptEmail" placeholder="可选"></i-input>
</FormItem>
</div>
</Form>
<div style="text-align: center">
<Button type="primary" :loading="loading" @click="submit">保存发票信息</Button>
@@ -45,106 +89,209 @@
</template>
<script>
import { receiptSelect } from '@/api/cart.js';
import { TINumber } from '@/plugins/RegExp.js';
import { mobile, email } from '@/plugins/RegExp.js';
export default {
name: 'invoiceModal',
data () {
return {
invoice: 1, // 发票类型
invoiceAvailable: false, // 模态框显隐
loading: false, // 提交状态
invoice: 1, // 1 电子普通发票 2 增值税专用发票
invoiceAvailable: false,
loading: false,
invoiceForm: {
// 普票表单
receiptTitle: '', // 发票抬头
taxpayerId: '', // 纳税人识别号
receiptContent: '商品明细' // 发票内容
receiptTitle: '',
personalName: '',
companyName: '',
taxpayerId: '',
receiptContent: '商品明细',
receiptPhone: '',
receiptEmail: '',
companyAddress: '',
companyPhone: '',
bankName: '',
bankAccount: ''
},
type: 1, // 1 个人 2 单位
ruleInline: {
taxpayerId: [
{ required: true, message: '请填写纳税人识别号' },
{ pattern: TINumber, message: '请填写正确的纳税人识别号' }
]
}
type: 1
};
},
props: ['invoiceData'],
computed: {
formRules () {
const receiptPhoneRules = [
{ required: true, message: '请填写收票人手机', trigger: 'blur' },
{ pattern: mobile, message: '请填写正确的手机号码', trigger: 'blur' }
];
// 收票人邮箱可选:仅校验格式;提示文案须与手机区分,避免误用「请填写收票人手机」
const receiptEmailRules = [
{
validator (rule, val, cb) {
const s = val != null ? String(val).trim() : '';
if (!s) {
cb();
return;
}
if (!email.test(s)) {
cb(new Error('请填写正确的收票人邮箱'));
return;
}
cb();
},
trigger: 'blur'
}
];
if (this.invoice === 2) {
return {
companyName: [{ required: true, message: '请填写单位名称', trigger: 'blur' }],
taxpayerId: [{ required: true, message: '请填写纳税人识别号', trigger: 'blur' }],
companyAddress: [{ required: true, message: '请填写单位地址', trigger: 'blur' }],
companyPhone: [{ required: true, message: '请填写单位电话', trigger: 'blur' }],
bankName: [{ required: true, message: '请填写开户银行', trigger: 'blur' }],
bankAccount: [{ required: true, message: '请填写银行账号', trigger: 'blur' }],
receiptPhone: receiptPhoneRules,
receiptEmail: receiptEmailRules
};
}
const rules = {
receiptPhone: receiptPhoneRules,
receiptEmail: receiptEmailRules
};
if (this.type === 1) {
rules.personalName = [{ required: true, message: '请填写个人名称', trigger: 'blur' }];
} else {
rules.companyName = [{ required: true, message: '请填写单位名称', trigger: 'blur' }];
rules.taxpayerId = [{ required: true, message: '请填写纳税人识别号', trigger: 'blur' }];
}
return rules;
}
},
watch: {
// 回显的发票信息
invoiceData: {
handler (val) {
this.invoiceForm = { ...val };
if (!val) return;
this.invoiceForm = { ...this.invoiceForm, ...val };
if (!this.invoiceForm.receiptPhone) this.invoiceForm.receiptPhone = '';
if (!this.invoiceForm.receiptEmail) this.invoiceForm.receiptEmail = '';
if (!this.invoiceForm.receiptContent) this.invoiceForm.receiptContent = '商品明细';
if (!this.invoiceForm.companyAddress) this.invoiceForm.companyAddress = '';
if (!this.invoiceForm.companyPhone) this.invoiceForm.companyPhone = '';
if (!this.invoiceForm.bankName) this.invoiceForm.bankName = '';
if (!this.invoiceForm.bankAccount) this.invoiceForm.bankAccount = '';
if (!this.invoiceForm.personalName) this.invoiceForm.personalName = '';
if (!this.invoiceForm.companyName) this.invoiceForm.companyName = '';
if (val.taxpayerId) {
const rt = val.receiptType;
if (rt === 2 || rt === '2' || val.invoiceKind === 'VAT_SPECIAL') {
this.invoice = 2;
this.type = 2;
} else {
this.type = 1;
this.invoice = 1;
if (val.taxpayerId) this.type = 2;
else this.type = 1;
}
// 兼容后端 receiptTitle 存「个人/单位」,实际名称在 personalName/companyName旧数据名称可能只在 receiptTitle
const titleAsName = (t) => (t && t !== '个人' && t !== '单位' ? t : '');
if (this.invoice === 2) {
this.invoiceForm.companyName = val.companyName || titleAsName(val.receiptTitle) || '';
} else if (this.type === 2) {
this.invoiceForm.companyName = val.companyName || titleAsName(val.receiptTitle) || '';
this.invoiceForm.personalName = val.personalName || '';
} else {
this.invoiceForm.personalName = val.personalName || titleAsName(val.receiptTitle) || '';
this.invoiceForm.companyName = val.companyName || '';
}
if (this.invoice === 1) {
this.invoiceForm.companyAddress = '';
this.invoiceForm.companyPhone = '';
this.invoiceForm.bankName = '';
this.invoiceForm.bankAccount = '';
}
if (this.invoice === 2) {
this.invoiceForm.personalName = '';
}
},
deep: true,
immeadite: true
immediate: true
}
},
methods: {
/**
* 选择发票抬头
*/
changeInvoice (val) {
onHeaderTypeChange (val) {
if (this.invoice === 1 && val === 1 && this.invoiceForm.personalName === '个人') {
this.invoiceForm.personalName = '';
}
this.$nextTick(() => {
this.type = val;
if (this.$refs.form) this.$refs.form.clearValidate();
});
},
onInvoiceKindChange (val) {
this.type = val === 2 ? 2 : 1;
this.invoiceForm = {
receiptTitle: '',
personalName: '',
companyName: '',
taxpayerId: '',
receiptContent: '商品明细',
receiptPhone: '',
receiptEmail: '',
companyAddress: '',
companyPhone: '',
bankName: '',
bankAccount: ''
};
this.$nextTick(() => {
if (this.$refs.form) this.$refs.form.clearValidate();
});
},
/**
* 保存判断
*/
save () {
let flage = true;
// 保存分为两种类型,个人以及企业
const { receiptTitle } = JSON.parse(
JSON.stringify(this.invoiceForm)
);
// 判断是否填写发票抬头
if (!receiptTitle) {
this.$Message.error('请填写发票抬头!');
flage = false;
return false;
async save () {
if (this.invoice === 2) {
this.invoiceForm.receiptContent = '商品明细';
}
if (this.type === 2) {
this.$refs.form.validate((valid) => {
if (!valid) {
flage = false;
}
});
} else {
delete this.invoiceForm.taxpayerId;
}
return flage;
return await new Promise(resolve => {
this.$refs.form.validate(valid => resolve(valid));
});
},
// 保存发票信息
async submit () {
if (this.save()) {
this.loading = true;
let submit = {
way: this.$route.query.way,
...this.invoiceForm
};
let receipt = await receiptSelect(submit);
if (receipt.success) {
this.$emit('change', true);
}
this.loading = false;
async submit () {
if (!await this.save()) return;
this.loading = true;
const raw = JSON.parse(JSON.stringify(this.invoiceForm));
if (this.invoice === 1) {
delete raw.companyAddress;
delete raw.companyPhone;
delete raw.bankName;
delete raw.bankAccount;
delete raw.invoiceAddress;
if (this.type === 1) {
delete raw.taxpayerId;
}
} else {
delete raw.personalName;
}
const isCompanyTitle = this.invoice === 2 || this.type === 2;
const titleName = isCompanyTitle
? (raw.companyName != null ? String(raw.companyName).trim() : '')
: (raw.personalName != null ? String(raw.personalName).trim() : '');
const submit = {
way: this.$route.query.way,
...raw,
receiptType: this.invoice,
receiptTitle: isCompanyTitle ? '单位' : '个人',
personalName: isCompanyTitle ? '' : titleName,
companyName: isCompanyTitle ? titleName : ''
};
let receipt = await receiptSelect(submit);
if (receipt.success) {
this.$emit('change', true);
}
this.loading = false;
}
}
};
</script>
<style lang="scss" scoped>
/** 普票 */
.inv-type {
text-align: center;
}
@@ -158,7 +305,12 @@ export default {
}
.nav-content {
width: 500px;
width: 520px;
margin: 10px auto;
}
.inv-title-fixed {
line-height: 32px;
color: #515a6e;
}
</style>

View File

@@ -95,12 +95,43 @@
</div>
<div class="order-card" v-if="order.order.payStatus === 'PAID'">
<h3>发票信息</h3>
<template v-if="order.order.needReceipt && order.receipt">
<p>发票抬头:{{ order.receipt.receiptTitle }}</p>
<p>发票内容{{ order.receipt.receiptContent }}</p>
<!-- 以 li_receipt 为准needReceipt 可能为 null/false 导致有 receipt 仍显示「未开发票」 -->
<template v-if="hasValidReceipt()">
<p>发票类型{{ formatReceiptType(order.receipt) }}</p>
<p>发票抬头:{{ formatReceiptHeaderType(order.receipt) }}</p>
<p v-if="isPersonalReceipt(order.receipt) && order.receipt.personalName">
个人名称:{{ order.receipt.personalName }}
</p>
<p v-if="order.receipt.companyName">
单位名称:{{ order.receipt.companyName }}
</p>
<p v-if="order.receipt.taxpayerId">
纳税人识别号:{{ order.receipt.taxpayerId }}
</p>
<p v-if="order.receipt.companyAddress">
单位地址:{{ order.receipt.companyAddress }}
</p>
<p v-if="order.receipt.companyPhone">
单位电话:{{ order.receipt.companyPhone }}
</p>
<p v-if="order.receipt.bankName">
开户银行:{{ order.receipt.bankName }}
</p>
<p v-if="order.receipt.bankAccount">
银行账号:{{ order.receipt.bankAccount }}
</p>
<p>发票内容:{{ order.receipt.receiptContent }}</p>
<p v-if="order.receipt.receiptPhone">
收票人手机:{{ order.receipt.receiptPhone }}
</p>
<p v-if="order.receipt.receiptEmail">
收票人邮箱:{{ order.receipt.receiptEmail }}
</p>
<div v-if="Number(order.receipt.receiptStatus) === 1" class="receipt-action">
<Button size="small" type="primary" ghost @click="viewReceiptInvoice(order.receipt)">
查看发票
</Button>
</div>
</template>
<div v-else style="color: #999; margin-left: 5px">未开发票</div>
</div>
@@ -307,7 +338,7 @@ import {
cancelOrder,
getPackage
} from "@/api/order.js";
import { afterSaleReason } from "@/api/member";
import { afterSaleReason, receiptDetail } from "@/api/member";
export default {
name: "order-detail",
data() {
@@ -328,6 +359,51 @@ export default {
};
},
methods: {
isVatSpecialReceipt (receipt) {
if (!receipt) return false;
const rt = receipt.receiptType != null ? String(receipt.receiptType).trim() : "";
if (rt === "2" || rt === "增值税专用发票" || receipt.invoiceKind === "VAT_SPECIAL") {
return true;
}
return false;
},
isPersonalReceipt (receipt) {
return !this.isVatSpecialReceipt(receipt);
},
formatReceiptType (receipt) {
if (!receipt) return "";
const rt = receipt.receiptType != null ? String(receipt.receiptType).trim() : "";
if (rt === "电子普通发票" || rt === "增值税专用发票") return rt;
return this.isVatSpecialReceipt(receipt) ? "增值税专用发票" : "电子普通发票";
},
formatReceiptHeaderType (receipt) {
if (!receipt) return "";
if (this.isVatSpecialReceipt(receipt)) return "单位";
if (receipt.taxpayerId) return "单位";
return "个人";
},
hasValidReceipt () {
const receipt = this.order && this.order.receipt;
if (!receipt) return false;
const content = receipt.receiptContent ? String(receipt.receiptContent).trim() : '';
const title = receipt.receiptTitle ? String(receipt.receiptTitle).trim() : '';
const taxpayerId = receipt.taxpayerId ? String(receipt.taxpayerId).trim() : '';
// 后端有时会返回占位 receipt如 receiptContent=不开发票),这里过滤掉
if (content === '不开发票') return false;
return Boolean(content || title || taxpayerId);
},
getInvoiceAddress (receipt) {
if (!receipt) return "";
return receipt.invoiceAddress || receipt.invoiceFileUrl || "";
},
viewReceiptInvoice (receipt) {
const invoiceAddress = this.getInvoiceAddress(receipt);
if (!invoiceAddress) {
this.$Message.warning("暂无发票附件");
return;
}
window.open(invoiceAddress, "_blank");
},
// 退款状态枚举
refundPriceList(status) {
switch (status) {
@@ -359,11 +435,21 @@ export default {
});
window.open(routeUrl.href, "_blank");
},
getDetail() {
async getDetail() {
// 获取订单详情
orderDetail(this.$route.query.sn).then((res) => {
orderDetail(this.$route.query.sn).then(async (res) => {
if (res.success) {
this.order = res.result;
const receiptId =
(res.result.order && res.result.order.receiptId) ||
res.result.receiptId ||
(res.result.receipt && res.result.receipt.id);
if (receiptId) {
const receiptRes = await receiptDetail(receiptId);
if (receiptRes && receiptRes.success && receiptRes.result) {
this.$set(this.order, "receipt", receiptRes.result);
}
}
this.progressList = res.result.orderLogs;
if (this.order.order.deliveryMethod === 'LOGISTICS') {
this.getOrderPackage(this.order.order.sn);
@@ -625,6 +711,10 @@ table {
overflow-x: auto;
}
.receipt-action {
margin-top: 12px;
}
.express-log {
/*margin: 5px -10px 5px 5px;*/
padding: 10px;

View File

@@ -157,8 +157,11 @@
</span></span>
</div>
<div class="inovice-content">
<span>{{ invoiceData.receiptTitle }}</span>
<span>{{ invoiceData.receiptContent }}</span>
<template v-if="hasInvoiceInfo">
<span>{{ invoiceDisplayTitle }}</span>
<span>{{ invoiceData.receiptContent }}</span>
</template>
<span v-else>不开发票</span>
<span @click="editInvoice">编辑</span>
</div>
</div>
@@ -258,6 +261,28 @@ import { canUseCouponList } from "@/api/member.js";
export default {
name: "Pay",
components: { invoiceModal, addressManage },
computed: {
hasInvoiceInfo() {
const title =
this.invoiceData.personalName ||
this.invoiceData.companyName ||
this.invoiceData.receiptTitle;
const content = this.invoiceData.receiptContent;
if (content && String(content).trim() === "不开发票") return false;
return Boolean(
(title && String(title).trim()) ||
(content && String(content).trim())
);
},
invoiceDisplayTitle() {
return (
this.invoiceData.personalName ||
this.invoiceData.companyName ||
this.invoiceData.receiptTitle ||
""
);
},
},
data() {
return {
selectedStoreAddress: 'm',
@@ -270,8 +295,7 @@ export default {
storeMoreAddr: false,
invoiceData: {
// 发票数据
receiptTitle: "个人",
receiptContent: "不开发票",
//receiptContent: "不开发票",
},
searchForm: {
pageNumber: 1,