This commit is contained in:
qiuyuan 2025-12-31 10:01:12 +08:00
parent 876358ea28
commit 086b4761db

View File

@ -96,83 +96,300 @@
</a-col>
</a-row>
<!-- 单个开票确认模态框 -->
<!-- 单个开票模态框 -->
<a-modal
v-model:visible="singleInvoiceModalVisible"
title="开票确认"
:title="isBatch ? '批量开票' : '开票申请'"
@ok="handleSingleInvoiceConfirm"
@cancel="singleInvoiceModalVisible = false"
@cancel="handleInvoiceModalCancel"
:confirm-loading="singleInvoiceLoading"
centered
width="800px"
:okText="isBatch ? '确认批量开票' : '确认开票'"
>
<div class="invoice-confirm-content">
<div class="confirm-item">
<span class="confirm-label">流水号</span>
<span class="confirm-value">{{ currentInvoiceRecord?.serial_number || '-' }}</span>
<div class="invoice-modal-content">
<!-- 订单信息区域 -->
<div class="order-info-section">
<div class="section-title">
<a-icon type="shopping" />
<span style="margin-left: 8px;">订单信息</span>
</div>
<div class="order-info-grid">
<template v-if="isBatch">
<!-- 批量开票显示多个订单 -->
<div class="order-info-item">
<span class="info-label">订单数量</span>
<span class="info-value">{{ selectedRows.length }} 个订单</span>
</div>
<div class="order-info-item">
<span class="info-label">总金额</span>
<span class="info-value total-amount">¥{{ selectedTotalAmount.toFixed(2) }}</span>
</div>
<div class="order-list-preview" style="grid-column: span 2;">
<div class="preview-title">订单列表</div>
<div class="order-list">
<div v-for="(order, index) in selectedRows.slice(0, 3)" :key="order.id" class="order-item">
<span>{{ order.serial_number }}</span>
<span>-¥{{ order.real_amount || '0.00' }}</span>
</div>
<div v-if="selectedRows.length > 3" class="order-more">
{{ selectedRows.length }} 个订单
</div>
</div>
</div>
</template>
<template v-else>
<!-- 单个开票显示单个订单 -->
<div class="order-info-item">
<span class="info-label">订单编号</span>
<span class="info-value">{{ currentInvoiceRecord?.serial_number || '-' }}</span>
</div>
<div class="order-info-item">
<span class="info-label">开票金额</span>
<span class="info-value">¥{{ currentInvoiceRecord?.real_amount || '0.00' }}</span>
</div>
</template>
</div>
</div>
<div class="confirm-item">
<span class="confirm-label">交易时间</span>
<span class="confirm-value">{{ currentInvoiceRecord?.created_at ? dayjs(currentInvoiceRecord.created_at).format('YYYY-MM-DD HH:mm') : '-' }}</span>
</div>
<div class="confirm-item">
<span class="confirm-label">交易金额</span>
<span class="confirm-value">¥{{ currentInvoiceRecord?.real_amount || '0.00' }}</span>
</div>
<div class="confirm-tips">
确认要为该订单申请发票吗
<!-- 发票信息区域 -->
<div class="invoice-info-section">
<div class="section-title">
<a-icon type="file-text" />
<span style="margin-left: 8px;">发票信息</span>
</div>
<a-form
ref="invoiceFormRef"
:model="invoiceFormData"
:rules="invoiceFormRules"
layout="vertical"
class="invoice-form"
>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="发票类型" name="invoiceType" required>
<a-select
v-model:value="invoiceFormData.invoiceType"
placeholder="请选择发票类型"
>
<a-select-option value="normal">普通发票</a-select-option>
<a-select-option value="special">专用发票</a-select-option>
<a-select-option value="electronic">电子发票</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="开票类型" name="billingType" required>
<a-select
v-model:value="invoiceFormData.billingType"
placeholder="请选择开票类型"
>
<a-select-option value="personal">个人</a-select-option>
<a-select-option value="company">企业</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="抬头类型" name="titleType" required>
<a-radio-group v-model:value="invoiceFormData.titleType">
<a-radio value="personal">个人抬头</a-radio>
<a-radio value="company">公司抬头</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item
label="发票抬头"
name="invoiceTitle"
required
v-if="invoiceFormData.titleType === 'company'"
>
<a-select
v-model:value="invoiceFormData.invoiceTitle"
placeholder="请选择发票抬头"
:options="invoiceTitleOptions"
@search="handleSearchInvoiceTitle"
show-search
allow-clear
>
<template #dropdownRender="{ menuNode: menu }">
<v-nodes :vnodes="menu" />
<a-divider style="margin: 4px 0" />
<div
style="padding: 8px; cursor: pointer;"
@click="handleAddInvoiceTitle"
>
<a-icon type="plus" /> 新增发票抬头
</div>
</template>
</a-select>
</a-form-item>
<a-form-item
label="发票抬头"
name="personalTitle"
required
v-else
>
<a-input
v-model:value="invoiceFormData.personalTitle"
placeholder="请输入个人姓名"
/>
</a-form-item>
<a-form-item
label="纳税人识别号"
name="taxNumber"
v-if="invoiceFormData.titleType === 'company' && invoiceFormData.invoiceTitle"
>
<a-input
v-model:value="invoiceFormData.taxNumber"
placeholder="请输入纳税人识别号"
/>
</a-form-item>
<a-form-item label="备注" name="remark">
<a-textarea
v-model:value="invoiceFormData.remark"
placeholder="请输入开票备注(可选)"
:rows="2"
/>
</a-form-item>
</a-form>
</div>
</div>
</a-modal>
<!-- 批量开票确认模态框 -->
<!-- 新增发票抬头模态框 -->
<a-modal
v-model:visible="batchInvoiceModalVisible"
title="批量开票确认"
@ok="handleBatchInvoiceConfirm"
@cancel="batchInvoiceModalVisible = false"
:confirm-loading="batchInvoiceLoading"
v-model:visible="addTitleModalVisible"
title="新增发票抬头"
@ok="handleAddTitleConfirm"
@cancel="addTitleModalVisible = false"
width="500px"
centered
width="600px"
>
<div class="batch-invoice-content">
<div class="selected-count">
已选择 {{ selectedRowKeys.length }} 条记录进行批量开票
</div>
<div class="total-amount">
<span class="total-label">合计金额</span>
<span class="total-value">¥{{ selectedTotalAmount.toFixed(2) }}</span>
</div>
<div class="invoice-list" style="max-height: 300px; overflow-y: auto; margin-top: 16px;">
<a-table
:dataSource="selectedInvoiceRecords"
:columns="selectedColumns"
size="small"
:pagination="false"
bordered
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'created_at'">
{{ record.created_at ? dayjs(record.created_at).format('YYYY-MM-DD HH:mm') : '-' }}
</template>
<template v-if="column.key === 'real_amount'">
-¥{{ record.real_amount || '0.00' }}
</template>
</template>
</a-table>
</div>
<div class="batch-tips" style="margin-top: 16px; color: #ff4d4f;">
注意批量开票将为所有选中的订单统一生成一张发票
</div>
</div>
<a-form :model="newTitleForm" layout="vertical">
<a-form-item label="公司名称" required>
<a-input
v-model:value="newTitleForm.companyName"
placeholder="请输入公司全称"
/>
</a-form-item>
<a-form-item label="纳税人识别号" required>
<a-input
v-model:value="newTitleForm.taxNumber"
placeholder="请输入纳税人识别号"
/>
</a-form-item>
<a-form-item label="公司地址">
<a-input
v-model:value="newTitleForm.address"
placeholder="请输入公司地址"
/>
</a-form-item>
<a-form-item label="开户银行">
<a-input
v-model:value="newTitleForm.bank"
placeholder="请输入开户银行"
/>
</a-form-item>
<a-form-item label="银行账号">
<a-input
v-model:value="newTitleForm.bankAccount"
placeholder="请输入银行账号"
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { ref, onBeforeMount, computed } from 'vue'
import { ref, onBeforeMount, computed, reactive, nextTick } from 'vue'
import { usePagination } from '@/hooks'
import { useList } from '@/apis/admin'
import dayjs from 'dayjs'
import { message } from 'ant-design-vue'
import { message, FormInstance } from 'ant-design-vue'
import type { Rule } from 'ant-design-vue/es/form'
//
const mockData = [
{
id: '1',
serial_number: 'SN202312270001',
created_at: '2023-12-27 10:30:00',
host_id: 'HOST-001',
host_case_id: 'CASE-001',
computing_power_model: 'RTX 4090',
gpu_count: 2,
billing_start: '2023-12-27 10:00:00',
billing_end: '2023-12-27 11:00:00',
bill_time: '1小时',
price: '50.00',
real_amount: '100.00',
status: '待开票'
},
{
id: '2',
serial_number: 'SN202312270002',
created_at: '2023-12-27 11:45:00',
host_id: 'HOST-002',
host_case_id: 'CASE-002',
computing_power_model: 'RTX 4080',
gpu_count: 1,
billing_start: '2023-12-27 11:30:00',
billing_end: '2023-12-27 12:30:00',
bill_time: '1小时',
price: '40.00',
real_amount: '40.00',
status: '待开票'
},
{
id: '3',
serial_number: 'SN202312270003',
created_at: '2023-12-27 14:20:00',
host_id: 'HOST-003',
host_case_id: 'CASE-003',
computing_power_model: 'RTX 3090',
gpu_count: 4,
billing_start: '2023-12-27 14:00:00',
billing_end: '2023-12-27 15:00:00',
bill_time: '1小时',
price: '30.00',
real_amount: '120.00',
status: '已开票'
},
{
id: '4',
serial_number: 'SN202312270004',
created_at: '2023-12-27 16:15:00',
host_id: 'HOST-004',
host_case_id: 'CASE-004',
computing_power_model: 'A100',
gpu_count: 2,
billing_start: '2023-12-27 16:00:00',
billing_end: '2023-12-27 17:00:00',
bill_time: '1小时',
price: '80.00',
real_amount: '160.00',
status: '待开票'
},
{
id: '5',
serial_number: 'SN202312270005',
created_at: '2023-12-27 18:30:00',
host_id: 'HOST-005',
host_case_id: 'CASE-005',
computing_power_model: 'H100',
gpu_count: 1,
billing_start: '2023-12-27 18:00:00',
billing_end: '2023-12-27 19:00:00',
bill_time: '1小时',
price: '100.00',
real_amount: '100.00',
status: '待开票'
}
]
//
const { listData, paginationState, resetPagination, searchFormData } = usePagination()
@ -183,13 +400,67 @@ const selectedRows = ref<any[]>([])
//
const singleInvoiceModalVisible = ref(false)
const batchInvoiceModalVisible = ref(false)
const addTitleModalVisible = ref(false)
const singleInvoiceLoading = ref(false)
const batchInvoiceLoading = ref(false)
const isBatch = ref(false) //
//
const currentInvoiceRecord = ref<any>(null)
//
const invoiceFormRef = ref<FormInstance>()
//
const invoiceTitleOptions = ref([
{ value: 'company1', label: '北京科技有限公司' },
{ value: 'company2', label: '上海信息技术有限公司' },
{ value: 'company3', label: '广州人工智能有限公司' },
{ value: 'company4', label: '深圳云计算有限公司' }
])
//
const newTitleForm = reactive({
companyName: '',
taxNumber: '',
address: '',
bank: '',
bankAccount: ''
})
//
const invoiceFormData = reactive({
invoiceType: undefined,
billingType: undefined,
titleType: 'company',
invoiceTitle: undefined,
personalTitle: '',
taxNumber: '',
remark: ''
})
//
const invoiceFormRules = reactive<Record<string, Rule[]>>({
invoiceType: [
{ required: true, message: '请选择发票类型', trigger: 'change' }
],
billingType: [
{ required: true, message: '请选择开票类型', trigger: 'change' }
],
titleType: [
{ required: true, message: '请选择抬头类型', trigger: 'change' }
],
invoiceTitle: [
{ required: true, message: '请选择发票抬头', trigger: 'change' }
],
personalTitle: [
{ required: true, message: '请输入个人姓名', trigger: 'blur' }
],
taxNumber: [
{ required: true, message: '请输入纳税人识别号', trigger: 'blur' },
{ pattern: /^[A-Z0-9]{15}$|^[A-Z0-9]{18}$|^[A-Z0-9]{20}$/, message: '请输入正确的纳税人识别号' }
]
})
//
const columns = ref([
{ title: '流水号', dataIndex: 'serial_number', key: 'serial_number' },
@ -202,6 +473,7 @@ const columns = ref([
{ title: '计费时长', dataIndex: 'bill_time', key: 'bill_time' },
{ title: '单价', dataIndex: 'price', key: 'price' },
{ title: '交易金额', dataIndex: 'real_amount', key: 'real_amount' },
{ title: '状态', dataIndex: 'status', key: 'status' },
{
title: '操作',
key: 'action',
@ -210,13 +482,6 @@ const columns = ref([
}
])
//
const selectedColumns = ref([
{ title: '流水号', dataIndex: 'serial_number', key: 'serial_number', width: 120 },
{ title: '交易时间', dataIndex: 'created_at', key: 'created_at', width: 150 },
{ title: '交易金额', dataIndex: 'real_amount', key: 'real_amount', width: 100, align: 'right' }
])
//
const rowSelection = computed(() => ({
selectedRowKeys: selectedRowKeys.value,
@ -225,7 +490,7 @@ const rowSelection = computed(() => ({
selectedRows.value = selectedRows
},
getCheckboxProps: (record: any) => ({
disabled: record.status === '已开票' //
disabled: record.status === '已开票'
})
}))
@ -237,13 +502,16 @@ const selectedTotalAmount = computed(() => {
}, 0)
})
//
const selectedInvoiceRecords = computed(() => {
return selectedRows.value
})
//
const initMockData = () => {
listData.value = mockData
paginationState.total = mockData.length
}
onBeforeMount(() => {
getPageList()
//
initMockData()
// getPageList()
})
const getPageList = async () => {
@ -255,6 +523,8 @@ const getPageList = async () => {
console.log('订单列表:', res)
} catch (error: any) {
console.error('产品优势请求失败:', error)
// API使
initMockData()
}
}
@ -293,33 +563,11 @@ function handleResetSearch() {
*/
function handleSingleInvoice(record: any) {
currentInvoiceRecord.value = record
isBatch.value = false
resetInvoiceForm()
singleInvoiceModalVisible.value = true
}
/**
* 确认单个开票
*/
async function handleSingleInvoiceConfirm() {
singleInvoiceLoading.value = true
try {
// API
// await invoiceApi.createSingleInvoice(currentInvoiceRecord.value.id)
// API
await new Promise(resolve => setTimeout(resolve, 1000))
message.success('开票申请已提交成功!')
singleInvoiceModalVisible.value = false
//
getPageList()
} catch (error) {
message.error('开票申请提交失败,请稍后重试')
} finally {
singleInvoiceLoading.value = false
}
}
/**
* 批量去开票
*/
@ -328,23 +576,89 @@ function handleBatchInvoice() {
message.warning('请先选择要开票的记录')
return
}
batchInvoiceModalVisible.value = true
isBatch.value = true
resetInvoiceForm()
singleInvoiceModalVisible.value = true
}
/**
* 确认批量开票
* 重置发票表单
*/
async function handleBatchInvoiceConfirm() {
batchInvoiceLoading.value = true
function resetInvoiceForm() {
invoiceFormData.invoiceType = undefined
invoiceFormData.billingType = undefined
invoiceFormData.titleType = 'company'
invoiceFormData.invoiceTitle = undefined
invoiceFormData.personalTitle = ''
invoiceFormData.taxNumber = ''
invoiceFormData.remark = ''
//
nextTick(() => {
invoiceFormRef.value?.clearValidate()
})
}
/**
* 关闭开票模态框
*/
function handleInvoiceModalCancel() {
singleInvoiceModalVisible.value = false
resetInvoiceForm()
}
/**
* 确认开票
*/
async function handleSingleInvoiceConfirm() {
try {
// API
// await invoiceApi.createBatchInvoice(selectedRowKeys.value)
//
await invoiceFormRef.value?.validate()
singleInvoiceLoading.value = true
//
const titleInfo = invoiceFormData.titleType === 'company'
? {
titleType: 'company',
companyName: invoiceFormData.invoiceTitle,
taxNumber: invoiceFormData.taxNumber
}
: {
titleType: 'personal',
personalName: invoiceFormData.personalTitle
}
//
const invoiceData = {
...titleInfo,
invoiceType: invoiceFormData.invoiceType,
billingType: invoiceFormData.billingType,
remark: invoiceFormData.remark,
orders: isBatch.value
? selectedRows.value.map(row => ({
id: row.id,
serialNumber: row.serial_number,
amount: row.real_amount
}))
: [{
id: currentInvoiceRecord.value.id,
serialNumber: currentInvoiceRecord.value.serial_number,
amount: currentInvoiceRecord.value.real_amount
}]
}
console.log('提交开票数据:', invoiceData)
// API
await new Promise(resolve => setTimeout(resolve, 1500))
message.success(`批量开票申请已提交成功,共 ${selectedRowKeys.value.length} 条记录`)
batchInvoiceModalVisible.value = false
const successMessage = isBatch.value
? `批量开票申请已提交成功,共 ${selectedRows.value.length} 条记录`
: '开票申请已提交成功!'
message.success(successMessage)
singleInvoiceModalVisible.value = false
//
selectedRowKeys.value = []
@ -352,12 +666,66 @@ async function handleBatchInvoiceConfirm() {
//
getPageList()
} catch (error) {
message.error('批量开票申请提交失败,请稍后重试')
console.error('表单验证失败:', error)
if (error && typeof error === 'object' && 'errorFields' in error) {
message.error('请完善开票信息')
} else {
const errorMessage = isBatch.value
? '批量开票申请提交失败,请稍后重试'
: '开票申请提交失败,请稍后重试'
message.error(errorMessage)
}
} finally {
batchInvoiceLoading.value = false
singleInvoiceLoading.value = false
}
}
/**
* 搜索发票抬头
*/
function handleSearchInvoiceTitle(value: string) {
console.log('搜索发票抬头:', value)
//
}
/**
* 添加发票抬头
*/
function handleAddInvoiceTitle() {
addTitleModalVisible.value = true
}
/**
* 确认新增发票抬头
*/
function handleAddTitleConfirm() {
if (!newTitleForm.companyName || !newTitleForm.taxNumber) {
message.error('请填写公司名称和纳税人识别号')
return
}
//
const newOption = {
value: newTitleForm.companyName,
label: newTitleForm.companyName
}
invoiceTitleOptions.value.push(newOption)
//
invoiceFormData.invoiceTitle = newTitleForm.companyName
invoiceFormData.taxNumber = newTitleForm.taxNumber
//
Object.keys(newTitleForm).forEach(key => {
newTitleForm[key] = ''
})
addTitleModalVisible.value = false
message.success('发票抬头添加成功')
}
</script>
<style scoped>
@ -367,75 +735,118 @@ async function handleBatchInvoiceConfirm() {
justify-content: space-between;
}
.invoice-confirm-content {
padding: 8px 0;
.invoice-modal-content {
max-height: 60vh;
overflow-y: auto;
padding-right: 8px;
}
.confirm-item {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
.order-info-section,
.invoice-info-section {
margin-bottom: 24px;
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 20px;
background: #fff;
}
.confirm-item:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.confirm-label {
color: #666;
font-weight: 500;
}
.confirm-value {
font-weight: 600;
color: #333;
}
.confirm-tips {
margin-top: 16px;
padding: 12px;
background-color: #f6ffed;
border-radius: 6px;
color: #52c41a;
text-align: center;
}
.batch-invoice-content .selected-count {
.section-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
color: #1890ff;
margin-bottom: 16px;
display: flex;
align-items: center;
}
.total-amount {
.order-info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.order-info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background-color: #fafafa;
background: #fafafa;
border-radius: 6px;
margin-bottom: 16px;
}
.total-label {
font-size: 16px;
.info-label {
color: #666;
font-weight: 500;
}
.info-value {
font-weight: 600;
color: #333;
}
.total-value {
font-size: 20px;
font-weight: 700;
.total-amount {
color: #ff4d4f;
font-size: 18px;
}
.batch-tips {
font-size: 14px;
.order-list-preview {
margin-top: 8px;
}
.preview-title {
font-weight: 500;
margin-bottom: 8px;
color: #666;
}
.order-list {
max-height: 120px;
overflow-y: auto;
border: 1px solid #f0f0f0;
border-radius: 6px;
padding: 8px;
background: #fafafa;
}
.order-item {
display: flex;
justify-content: space-between;
padding: 6px 8px;
border-bottom: 1px solid #f0f0f0;
}
.order-item:last-child {
border-bottom: none;
}
.order-more {
text-align: center;
padding: 8px;
color: #999;
font-style: italic;
}
.invoice-form {
margin-top: 8px;
}
/* 自定义滚动条 */
.invoice-modal-content::-webkit-scrollbar {
width: 6px;
}
.invoice-modal-content::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.invoice-modal-content::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.invoice-modal-content::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 适配响应式 */
@ -446,10 +857,26 @@ async function handleBatchInvoiceConfirm() {
gap: 12px;
}
.total-amount {
.order-info-grid {
grid-template-columns: 1fr;
}
.order-info-item {
flex-direction: column;
align-items: flex-start;
gap: 8px;
gap: 4px;
}
}
/* 必填字段标记 */
:deep(.ant-form-item-required::before) {
content: '*';
color: #ff4d4f;
margin-right: 4px;
}
/* 表单间距优化 */
:deep(.ant-form-item) {
margin-bottom: 16px;
}
</style>