Files
tyapi-frontend/src/pages/finance/Invoice.vue
2025-11-24 16:06:44 +08:00

1577 lines
42 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="list-page-container">
<div class="list-page-card">
<!-- 页面头部 -->
<div class="list-page-header mb-4">
<div class="flex justify-between items-start">
<div>
<h1 class="list-page-title">发票申请</h1>
<p class="list-page-subtitle">管理您的发票申请和查看申请记录</p>
</div>
</div>
</div>
<div class="list-page-wrapper">
<!-- 可开票金额信息 -->
<!-- <div class="invoice-info-section">
<div class="invoice-balance-card" :class="getBalanceCardClass()">
<div class="balance-icon">
<i class="el-icon-document"></i>
</div>
<div class="balance-content">
<div class="balance-label">可开票金额</div>
<div class="balance-amount">
¥{{ formatPrice(availableAmount.available_amount || 0) }}
</div>
<div class="balance-details">
<span>总充值¥{{ formatPrice(availableAmount.total_recharged || 0) }}</span>
<span>已开票¥{{ formatPrice(availableAmount.total_invoiced || 0) }}</span>
<span v-if="availableAmount.pending_applications > 0" class="pending-amount">
待处理¥{{ formatPrice(availableAmount.pending_applications || 0) }}
</span>
</div>
</div>
</div>
<div class="invoice-notice-alert">
<el-alert
title="发票申请说明"
description="您只能申请已充值金额(除去赠送)减去已开票金额的部分。普票和专票需要填写不同的企业信息。"
type="info"
:closable="false"
show-icon
/>
</div>
</div> -->
<!-- 标签页 -->
<div class="invoice-tabs">
<div
class="tab-item"
:class="{ active: activeTab === 'info' }"
@click="activeTab = 'info'"
>
<i class="el-icon-edit-outline"></i>
开票信息
</div>
<div
class="tab-item"
:class="{ active: activeTab === 'records' }"
@click="activeTab = 'records'"
>
<i class="el-icon-document"></i>
申请记录
</div>
</div>
<!-- 开票信息管理 -->
<div v-if="activeTab === 'info'" class="info-form-section">
<div class="info-header">
<h3 class="section-title">开票信息管理</h3>
<div class="info-actions">
<el-button
v-if="!isEditing"
type="primary"
size="large"
@click="startEdit"
class="edit-btn"
>
<i class="el-icon-edit"></i>
编辑信息
</el-button>
<el-button
type="primary"
size="large"
@click="showApplyDialog = true"
class="apply-btn"
>
<i class="el-icon-plus"></i>
申请开票
</el-button>
</div>
</div>
<!-- 只读信息显示 -->
<div v-if="!isEditing" class="info-display">
<div class="info-card">
<div class="info-row">
<div class="info-item">
<span class="info-label">公司名称</span>
<span class="info-value">{{ invoiceInfo.company_name || '未填写' }}</span>
</div>
<div class="info-item">
<span class="info-label">纳税人识别号</span>
<span class="info-value">{{ invoiceInfo.taxpayer_id || '未填写' }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item">
<span class="info-label">开户银行</span>
<span class="info-value">{{ invoiceInfo.bank_name || '未填写' }}</span>
</div>
<div class="info-item">
<span class="info-label">银行账号</span>
<span class="info-value">{{ invoiceInfo.bank_account || '未填写' }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item">
<span class="info-label">企业地址</span>
<span class="info-value">{{ invoiceInfo.company_address || '未填写' }}</span>
</div>
<div class="info-item">
<span class="info-label">企业电话</span>
<span class="info-value">{{ invoiceInfo.company_phone || '未填写' }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item">
<span class="info-label">接收邮箱</span>
<span class="info-value">{{ invoiceInfo.receiving_email || '未填写' }}</span>
</div>
</div>
</div>
</div>
<!-- 编辑表单 -->
<el-form
v-else
ref="infoFormRef"
:model="editingInfo"
:rules="infoRules"
label-width="120px"
class="invoice-form"
>
<div class="form-row">
<el-form-item label="公司名称" prop="company_name">
<el-input
v-model="editingInfo.company_name"
placeholder="请输入公司名称"
:disabled="invoiceInfo.company_name_read_only"
:class="{ 'readonly-field': invoiceInfo.company_name_read_only }"
/>
<div v-if="invoiceInfo.company_name_read_only" class="field-note">
<i class="el-icon-info"></i>
公司名称从企业认证信息自动获取不可修改
</div>
</el-form-item>
<el-form-item label="纳税人识别号" prop="taxpayer_id">
<el-input
v-model="editingInfo.taxpayer_id"
placeholder="请输入纳税人识别号"
:disabled="invoiceInfo.taxpayer_id_read_only"
:class="{ 'readonly-field': invoiceInfo.taxpayer_id_read_only }"
/>
<div v-if="invoiceInfo.taxpayer_id_read_only" class="field-note">
<i class="el-icon-info"></i>
纳税人识别号从企业认证信息自动获取不可修改
</div>
</el-form-item>
</div>
<div class="form-row">
<el-form-item label="开户银行" prop="bank_name">
<el-input v-model="editingInfo.bank_name" placeholder="请输入开户银行" />
</el-form-item>
<el-form-item label="银行账号" prop="bank_account">
<el-input v-model="editingInfo.bank_account" placeholder="请输入银行账号" />
</el-form-item>
</div>
<div class="form-row">
<el-form-item label="企业地址" prop="company_address">
<el-input v-model="editingInfo.company_address" placeholder="请输入企业注册地址" />
</el-form-item>
<el-form-item label="企业电话" prop="company_phone">
<el-input v-model="editingInfo.company_phone" placeholder="请输入企业注册电话" />
</el-form-item>
</div>
<div class="form-row">
<el-form-item label="接收邮箱" prop="receiving_email">
<el-input v-model="editingInfo.receiving_email" placeholder="请输入发票接收邮箱" />
</el-form-item>
</div>
<el-form-item>
<div class="form-actions">
<el-button @click="cancelEdit" class="cancel-btn"> 取消 </el-button>
<el-button
type="primary"
@click="handleSaveInfo"
:loading="saveLoading"
class="save-btn"
>
保存信息
</el-button>
</div>
</el-form-item>
</el-form>
<!-- 信息完整性提示 -->
<div class="info-completeness">
<el-alert
title="开票信息要求"
description="申请专票需要填写所有信息,申请普票至少需要公司名称、纳税人识别号和接收邮箱。"
type="info"
:closable="false"
show-icon
/>
</div>
</div>
<!-- 申请记录 -->
<div v-if="activeTab === 'records'" class="records-section">
<h3 class="section-title">申请记录</h3>
<!-- 申请说明 -->
<div class="invoice-notice-section">
<el-alert
title="发票申请说明"
description="发票申请提交后,我们将在 1-7 个工作日内处理您的申请。电子发票将发送至您的邮箱,或在开票记录中下载。"
type="info"
:closable="false"
show-icon
/>
</div>
<!-- 筛选器 -->
<FilterSection>
<FilterItem label="申请状态">
<el-select
v-model="recordsFilter.status"
placeholder="选择申请状态"
clearable
@change="handleFilterChange"
class="w-full"
>
<el-option label="全部状态" value="" />
<el-option label="待处理" value="pending" />
<el-option label="已完成" value="completed" />
<el-option label="已拒绝" value="rejected" />
</el-select>
</FilterItem>
<FilterItem label="时间范围">
<el-date-picker
v-model="recordsFilter.dateRange"
type="datetimerange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
@change="handleDateRangeChange"
class="w-full"
/>
</FilterItem>
<template #stats> 共找到 {{ recordsTotal }} 条申请记录 </template>
<template #buttons>
<el-button @click="resetFilters">重置筛选</el-button>
<el-button type="primary" @click="loadRecords">应用筛选</el-button>
</template>
</FilterSection>
<div v-if="recordsLoading" class="flex justify-center items-center py-12">
<el-loading size="large" />
</div>
<div v-else-if="records.length === 0" class="text-center py-12">
<el-empty description="暂无申请记录" />
</div>
<div v-else class="records-list">
<div v-for="record in records" :key="record.id" class="record-item">
<div class="record-header">
<div class="record-info">
<div class="record-id">申请编号{{ record.id }}</div>
<div class="record-status" :class="getStatusClass(record.status)">
{{ getStatusText(record.status) }}
</div>
</div>
<div class="record-amount">¥{{ formatPrice(record.amount) }}</div>
</div>
<div class="record-details">
<div class="detail-row">
<span class="detail-label">发票类型</span>
<span class="detail-value">{{ getInvoiceTypeText(record.invoice_type) }}</span>
</div>
<div class="detail-row">
<span class="detail-label">申请时间</span>
<span class="detail-value">{{ formatDateTime(record.created_at) }}</span>
</div>
<div v-if="record.processed_at" class="detail-row">
<span class="detail-label">处理时间</span>
<span class="detail-value">{{ formatDateTime(record.processed_at) }}</span>
</div>
<div v-if="record.reject_reason" class="detail-row">
<span class="detail-label">拒绝原因</span>
<span class="detail-value reject-reason">{{ record.reject_reason }}</span>
</div>
</div>
<!-- 开票信息详情 -->
<div class="invoice-info-details">
<div class="info-section-title">开票信息</div>
<div class="info-grid">
<div class="info-item">
<span class="info-label">公司名称</span>
<span class="info-value">{{ record.company_name || '未填写' }}</span>
</div>
<div class="info-item">
<span class="info-label">纳税人识别号</span>
<span class="info-value">{{ record.taxpayer_id || '未填写' }}</span>
</div>
<div class="info-item">
<span class="info-label">接收邮箱</span>
<span class="info-value">{{ record.receiving_email || '未填写' }}</span>
</div>
<div v-if="record.invoice_type === 'special'" class="info-item">
<span class="info-label">开户银行</span>
<span class="info-value">{{ record.bank_name || '未填写' }}</span>
</div>
<div v-if="record.invoice_type === 'special'" class="info-item">
<span class="info-label">银行账号</span>
<span class="info-value">{{ record.bank_account || '未填写' }}</span>
</div>
<div v-if="record.invoice_type === 'special'" class="info-item">
<span class="info-label">企业地址</span>
<span class="info-value">{{ record.company_address || '未填写' }}</span>
</div>
<div v-if="record.invoice_type === 'special'" class="info-item">
<span class="info-label">企业电话</span>
<span class="info-value">{{ record.company_phone || '未填写' }}</span>
</div>
</div>
</div>
<div class="record-actions mt-4">
<el-button
v-if="record.status === 'completed' && record.file_url"
type="primary"
@click="downloadInvoice(record.id)"
>
下载发票
</el-button>
</div>
</div>
</div>
<!-- 分页 -->
<div v-if="recordsTotal > 0" class="records-pagination">
<el-pagination
v-model:current-page="recordsCurrentPage"
v-model:page-size="recordsPageSize"
:page-sizes="[10, 20, 50]"
:total="recordsTotal"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleRecordsSizeChange"
@current-change="handleRecordsCurrentChange"
/>
</div>
</div>
</div>
</div>
<!-- 申请开票弹窗 -->
<el-dialog
v-model="showApplyDialog"
title="申请开票"
width="600px"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<el-form ref="applyFormRef" :model="applyForm" :rules="applyRules" label-width="120px">
<el-form-item label="发票类型" prop="invoice_type">
<el-radio-group v-model="applyForm.invoice_type" class="w-full">
<el-radio label="general">增值税普通发票普票</el-radio>
<el-radio label="special">增值税专用发票专票</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="开票金额" prop="amount">
<el-input
v-model="applyForm.amount"
placeholder="请输入开票金额"
@input="handleAmountInput"
>
<template #prepend>¥</template>
</el-input>
<div class="form-tip">
可开票金额¥{{ formatPrice(availableAmount.available_amount || 0) }}
</div>
</el-form-item>
<!-- 信息预览 -->
<div class="info-preview">
<h4>开票信息预览</h4>
<!-- 申请类型提示 -->
<div class="apply-type-notice">
<el-alert
:title="applyTypeNoticeTitle"
:description="applyTypeNoticeDescription"
:type="applyTypeNoticeType"
:closable="false"
show-icon
/>
</div>
<div class="preview-content">
<div class="preview-item">
<span class="preview-label">公司名称</span>
<span class="preview-value" :class="{ missing: !invoiceInfo.company_name }">
{{ invoiceInfo.company_name || '未填写' }}
</span>
</div>
<div class="preview-item">
<span class="preview-label">纳税人识别号</span>
<span class="preview-value" :class="{ missing: !invoiceInfo.taxpayer_id }">
{{ invoiceInfo.taxpayer_id || '未填写' }}
</span>
</div>
<div class="preview-item">
<span class="preview-label">接收邮箱</span>
<span class="preview-value" :class="{ missing: !invoiceInfo.receiving_email }">
{{ invoiceInfo.receiving_email || '未填写' }}
</span>
</div>
<div v-if="applyForm.invoice_type === 'special'" class="preview-item">
<span class="preview-label">开户银行</span>
<span class="preview-value" :class="{ missing: !invoiceInfo.bank_name }">
{{ invoiceInfo.bank_name || '未填写' }}
</span>
</div>
<div v-if="applyForm.invoice_type === 'special'" class="preview-item">
<span class="preview-label">银行账号</span>
<span class="preview-value" :class="{ missing: !invoiceInfo.bank_account }">
{{ invoiceInfo.bank_account || '未填写' }}
</span>
</div>
<div v-if="applyForm.invoice_type === 'special'" class="preview-item">
<span class="preview-label">企业地址</span>
<span class="preview-value" :class="{ missing: !invoiceInfo.company_address }">
{{ invoiceInfo.company_address || '未填写' }}
</span>
</div>
<div v-if="applyForm.invoice_type === 'special'" class="preview-item">
<span class="preview-label">企业电话</span>
<span class="preview-value" :class="{ missing: !invoiceInfo.company_phone }">
{{ invoiceInfo.company_phone || '未填写' }}
</span>
</div>
</div>
</div>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="showApplyDialog = false">取消</el-button>
<el-button
type="primary"
@click="handleApplyInvoice"
:loading="applyLoading"
:disabled="!canSubmitApplyInDialog"
>
确认申请
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { invoiceApi } from '@/api'
import { ElMessage, ElMessageBox } from 'element-plus'
import FilterItem from '@/components/common/FilterItem.vue'
import FilterSection from '@/components/common/FilterSection.vue'
const router = useRouter()
// 响应式数据
const activeTab = ref('info')
const isEditing = ref(false)
const saveLoading = ref(false)
const applyLoading = ref(false)
const showApplyDialog = ref(false)
const recordsLoading = ref(false)
const records = ref([])
const recordsTotal = ref(0)
const recordsCurrentPage = ref(1)
const recordsPageSize = ref(10)
// 可开票金额信息
const availableAmount = ref({
available_amount: 0,
total_recharged: 0,
total_gifted: 0,
total_invoiced: 0,
pending_applications: 0,
})
// 开票信息(只读)
const invoiceInfo = reactive({
company_name: '',
taxpayer_id: '',
bank_name: '',
bank_account: '',
company_address: '',
company_phone: '',
receiving_email: '',
company_name_read_only: false,
taxpayer_id_read_only: false,
})
// 编辑中的开票信息
const editingInfo = reactive({
company_name: '',
taxpayer_id: '',
bank_name: '',
bank_account: '',
company_address: '',
company_phone: '',
receiving_email: '',
})
// 申请开票表单
const applyFormRef = ref()
const applyForm = reactive({
invoice_type: 'general',
amount: '',
})
// 表单引用
const infoFormRef = ref()
// 记录筛选
const recordsFilter = reactive({
status: '',
dateRange: null,
start_time: '',
end_time: '',
})
// 开票信息验证规则
const infoRules = computed(() => ({
company_name: invoiceInfo.company_name_read_only
? []
: [{ required: true, message: '请输入公司名称', trigger: 'blur' }],
taxpayer_id: invoiceInfo.taxpayer_id_read_only
? []
: [{ required: true, message: '请输入纳税人识别号', trigger: 'blur' }],
receiving_email: [
{ required: true, message: '请输入接收邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' },
],
}))
// 申请开票验证规则
const applyRules = {
invoice_type: [{ required: true, message: '请选择发票类型', trigger: 'change' }],
amount: [
{ required: true, message: '请输入开票金额', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (!value) {
callback()
return
}
const amount = parseFloat(value)
const available = parseFloat(availableAmount.value.available_amount || 0)
if (amount > available) {
callback(new Error(`开票金额不能超过可开票金额 ¥${available}`))
return
}
if (amount <= 0) {
callback(new Error('开票金额必须大于0'))
return
}
callback()
},
trigger: 'blur',
},
],
}
// 初始化
onMounted(() => {
loadAvailableAmount()
loadUserInvoiceInfo()
loadRecords()
})
// 加载可开票金额
const loadAvailableAmount = async () => {
try {
const response = await invoiceApi.getAvailableAmount()
if (response && response.data) {
availableAmount.value = response.data
}
} catch (error) {
console.error('加载可开票金额失败:', error)
}
}
// 加载用户开票信息
const loadUserInvoiceInfo = async () => {
try {
const response = await invoiceApi.getUserInvoiceInfo()
if (response && response.data) {
Object.assign(invoiceInfo, response.data)
}
} catch (error) {
console.error('加载开票信息失败:', error)
}
}
// 加载申请记录
const loadRecords = async () => {
recordsLoading.value = true
try {
const params = {
page: recordsCurrentPage.value,
page_size: recordsPageSize.value,
status: recordsFilter.status,
}
// 添加时间范围筛选
if (recordsFilter.start_time) {
params.start_time = recordsFilter.start_time
}
if (recordsFilter.end_time) {
params.end_time = recordsFilter.end_time
}
const response = await invoiceApi.getUserInvoiceRecords(params)
if (response && response.data) {
records.value = response.data.records || []
recordsTotal.value = response.data.total || 0
}
} catch (error) {
console.error('加载申请记录失败:', error)
} finally {
recordsLoading.value = false
}
}
// 格式化价格
const formatPrice = (price) => {
if (!price) return '0.00'
return Number(price).toFixed(2)
}
// 格式化日期
const formatDate = (date) => {
if (!date) return '-'
return new Date(date).toLocaleDateString('zh-CN')
}
// 格式化日期时间(精确到秒)
const formatDateTime = (date) => {
if (!date) return '-'
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hours = String(d.getHours()).padStart(2, '0')
const minutes = String(d.getMinutes()).padStart(2, '0')
const seconds = String(d.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
// 获取余额卡片样式类
const getBalanceCardClass = () => {
const available = parseFloat(availableAmount.value.available_amount || 0)
if (available <= 0) {
return 'balance-card-empty'
}
return ''
}
// 处理金额输入
const handleAmountInput = (event) => {
const value = event.target.value
// 只允许数字和小数点
const formatted = value.replace(/[^\d.]/g, '')
applyForm.amount = formatted
}
// 获取状态样式类
const getStatusClass = (status) => {
switch (status) {
case 'pending':
return 'status-pending'
case 'completed':
return 'status-completed'
case 'rejected':
return 'status-rejected'
default:
return ''
}
}
// 获取状态文本
const getStatusText = (status) => {
switch (status) {
case 'pending':
return '待处理'
case 'completed':
return '已完成'
case 'rejected':
return '已拒绝'
default:
return '未知状态'
}
}
// 获取发票类型文本
const getInvoiceTypeText = (type) => {
switch (type) {
case 'general':
return '普通发票'
case 'special':
return '专用发票'
default:
return '未知类型'
}
}
// 开始编辑
const startEdit = () => {
// 复制当前信息到编辑表单
Object.assign(editingInfo, invoiceInfo)
isEditing.value = true
}
// 取消编辑
const cancelEdit = () => {
isEditing.value = false
// 重置编辑表单
Object.assign(editingInfo, {
company_name: '',
taxpayer_id: '',
bank_name: '',
bank_account: '',
company_address: '',
company_phone: '',
receiving_email: '',
})
}
// 保存开票信息
const handleSaveInfo = async () => {
if (!infoFormRef.value) return
try {
await infoFormRef.value.validate()
saveLoading.value = true
const response = await invoiceApi.updateUserInvoiceInfo(editingInfo)
if (response && response.success) {
// 更新只读信息
Object.assign(invoiceInfo, editingInfo)
ElMessage.success('开票信息保存成功')
isEditing.value = false
}
} catch (error) {
console.error('保存开票信息失败:', error)
} finally {
saveLoading.value = false
}
}
// 申请开票
const handleApplyInvoice = async () => {
if (!applyFormRef.value) return
try {
await applyFormRef.value.validate()
// 显示确认框
await ElMessageBox.confirm(`确认申请开票 ¥${applyForm.amount} 吗?`, '确认申请', {
confirmButtonText: '确认申请',
cancelButtonText: '取消',
type: 'warning',
})
applyLoading.value = true
const response = await invoiceApi.applyInvoice({
invoice_type: applyForm.invoice_type,
amount: applyForm.amount,
...invoiceInfo,
})
if (response && response.data) {
ElMessage.success('发票申请提交成功')
// 重置表单
applyFormRef.value.resetFields()
// 关闭弹窗
showApplyDialog.value = false
// 重新加载数据
loadAvailableAmount()
loadRecords()
// 切换到记录页
activeTab.value = 'records'
}
} catch (error) {
if (error === 'cancel' || error === 'close') {
return
}
console.error('提交申请失败:', error)
} finally {
applyLoading.value = false
}
}
// 下载发票
const downloadInvoice = async (applicationId) => {
try {
const response = await invoiceApi.downloadInvoiceFile(applicationId)
if (response && response.data) {
// 创建blob URL
const blob = new Blob([response.data], { type: 'application/pdf' })
const url = window.URL.createObjectURL(blob)
// 创建下载链接
const link = document.createElement('a')
link.href = url
link.download = `invoice_${applicationId}.pdf`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
// 清理blob URL
window.URL.revokeObjectURL(url)
ElMessage.success('开始下载发票文件')
}
} catch (error) {
console.error('下载发票失败:', error)
ElMessage.error('下载发票失败,请稍后重试')
}
}
// 计算属性
const canApplyInvoice = computed(() => {
// 普票至少需要:公司名称、纳税人识别号、接收邮箱
const hasBasicInfo =
invoiceInfo.company_name && invoiceInfo.taxpayer_id && invoiceInfo.receiving_email
// 专票还需要:开户银行、银行账号、企业地址、企业电话
const hasSpecialInfo =
invoiceInfo.bank_name &&
invoiceInfo.bank_account &&
invoiceInfo.company_address &&
invoiceInfo.company_phone
return hasBasicInfo && (applyForm.invoice_type === 'general' || hasSpecialInfo)
})
const canSubmitApply = computed(() => {
return canApplyInvoice.value && applyForm.amount && parseFloat(applyForm.amount) > 0
})
// 弹窗中的申请类型提示
const applyTypeNoticeTitle = computed(() => {
if (applyForm.invoice_type === 'general') {
return '普通发票申请'
} else {
return '专用发票申请'
}
})
const applyTypeNoticeDescription = computed(() => {
// 检查基本信息(普票和专票都需要)
const hasBasicInfo =
invoiceInfo.company_name && invoiceInfo.taxpayer_id && invoiceInfo.receiving_email
if (!hasBasicInfo) {
// 基本信息不完整
const missing = []
if (!invoiceInfo.company_name) missing.push('公司名称')
if (!invoiceInfo.taxpayer_id) missing.push('纳税人识别号')
if (!invoiceInfo.receiving_email) missing.push('接收邮箱')
return `请先填写以下信息:${missing.join('、')}`
}
if (applyForm.invoice_type === 'general') {
// 普票基本信息已完整
return '基本信息已完整,请确认开票信息无误后提交申请'
} else {
// 专票需要检查额外信息
const hasSpecialInfo =
invoiceInfo.bank_name &&
invoiceInfo.bank_account &&
invoiceInfo.company_address &&
invoiceInfo.company_phone
if (!hasSpecialInfo) {
// 专票额外信息不完整
const missing = []
if (!invoiceInfo.bank_name) missing.push('开户银行')
if (!invoiceInfo.bank_account) missing.push('银行账号')
if (!invoiceInfo.company_address) missing.push('企业地址')
if (!invoiceInfo.company_phone) missing.push('企业电话')
return `申请专票还需要填写:${missing.join('、')}`
} else {
// 专票信息已完整
return '所有信息已完整,请确认开票信息无误后提交申请'
}
}
})
const applyTypeNoticeType = computed(() => {
// 检查基本信息(普票和专票都需要)
const hasBasicInfo =
invoiceInfo.company_name && invoiceInfo.taxpayer_id && invoiceInfo.receiving_email
if (!hasBasicInfo) {
return 'error' // 基本信息缺失,显示错误提示
}
if (applyForm.invoice_type === 'general') {
return 'success' // 普票基本信息完整,显示成功提示
} else {
// 专票需要检查额外信息
const hasSpecialInfo =
invoiceInfo.bank_name &&
invoiceInfo.bank_account &&
invoiceInfo.company_address &&
invoiceInfo.company_phone
if (!hasSpecialInfo) {
return 'warning' // 专票额外信息缺失,显示警告提示
} else {
return 'success' // 专票信息完整,显示成功提示
}
}
})
// 弹窗中的提交按钮状态
const canSubmitApplyInDialog = computed(() => {
// 检查金额
if (!applyForm.amount || parseFloat(applyForm.amount) <= 0) {
return false
}
// 检查基本信息(普票和专票都需要)
const hasBasicInfo =
invoiceInfo.company_name && invoiceInfo.taxpayer_id && invoiceInfo.receiving_email
if (!hasBasicInfo) {
return false
}
// 如果是专票,还需要检查额外信息
if (applyForm.invoice_type === 'special') {
const hasSpecialInfo =
invoiceInfo.bank_name &&
invoiceInfo.bank_account &&
invoiceInfo.company_address &&
invoiceInfo.company_phone
return hasSpecialInfo
}
return true
})
// 分页处理
const handleRecordsSizeChange = (size) => {
recordsPageSize.value = size
recordsCurrentPage.value = 1
loadRecords()
}
const handleRecordsCurrentChange = (page) => {
recordsCurrentPage.value = page
loadRecords()
}
// 处理筛选变化
const handleFilterChange = () => {
recordsCurrentPage.value = 1
loadRecords()
}
// 处理时间范围变化
const handleDateRangeChange = (range) => {
if (range && range.length === 2) {
recordsFilter.start_time = range[0]
recordsFilter.end_time = range[1]
} else {
recordsFilter.start_time = ''
recordsFilter.end_time = ''
}
recordsCurrentPage.value = 1
loadRecords()
}
// 清除筛选
const resetFilters = () => {
recordsFilter.status = ''
recordsFilter.dateRange = null
recordsFilter.start_time = ''
recordsFilter.end_time = ''
recordsCurrentPage.value = 1
loadRecords()
}
</script>
<style scoped>
/* 发票信息区域 */
.invoice-info-section {
margin-bottom: 20px;
}
.invoice-balance-card {
display: flex;
align-items: center;
background: linear-gradient(135deg, #10b981 80%, #a7f3d0 100%);
border-radius: 12px;
padding: 16px;
color: white;
box-shadow:
0 4px 16px 0 rgba(0, 0, 0, 0.1),
0 2px 8px 0 rgba(16, 185, 129, 0.1);
}
.balance-icon {
font-size: 32px;
margin-right: 16px;
opacity: 0.9;
}
.balance-content {
flex: 1;
}
.balance-label {
font-size: 14px;
opacity: 0.9;
margin-bottom: 4px;
}
.balance-amount {
font-size: 28px;
font-weight: 700;
line-height: 1;
margin-bottom: 4px;
}
.balance-details {
display: flex;
gap: 12px;
font-size: 12px;
opacity: 0.8;
flex-wrap: wrap;
}
.pending-amount {
color: #fde68a;
font-weight: 500;
}
/* 发票申请提示 */
.invoice-notice-alert {
margin: 12px 0;
}
/* 余额状态卡片样式 */
.balance-card-empty {
background: linear-gradient(135deg, #6b7280 60%, #d1d5db 100%) !important;
}
/* 标签页样式 */
.invoice-tabs {
display: flex;
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(226, 232, 240, 0.6);
border-radius: 8px;
padding: 3px;
margin-bottom: 20px;
}
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
color: #64748b;
font-size: 14px;
}
.tab-item:hover {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
.tab-item.active {
background: #3b82f6;
color: white;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
/* 开票信息表单区域 */
.info-form-section {
margin-bottom: 20px;
}
.info-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #1e293b;
margin-bottom: 0;
}
.info-actions {
display: flex;
gap: 12px;
}
.edit-btn {
height: 40px;
font-size: 14px;
font-weight: 600;
}
.apply-btn {
height: 40px;
font-size: 14px;
font-weight: 600;
}
.invoice-form {
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(226, 232, 240, 0.6);
border-radius: 8px;
padding: 16px;
}
/* 只读字段样式 */
.readonly-field .el-input__inner {
background-color: #f5f7fa;
color: #606266;
cursor: not-allowed;
}
.field-note {
font-size: 12px;
color: #909399;
margin-top: 4px;
display: flex;
align-items: center;
gap: 4px;
}
.field-note i {
font-size: 14px;
}
/* 申请类型提示样式 */
.apply-type-notice {
margin-bottom: 16px;
}
/* 缺失字段样式 */
.preview-value.missing {
color: #f56c6c;
font-weight: 500;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
.form-section {
margin-bottom: 16px;
}
.form-section-title {
font-size: 14px;
font-weight: 600;
color: #374151;
margin-bottom: 12px;
padding-bottom: 6px;
border-bottom: 1px solid rgba(226, 232, 240, 0.6);
}
.special-fields {
background: rgba(59, 130, 246, 0.05);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: 6px;
padding: 12px;
margin-top: 12px;
}
.amount-input {
width: 100%;
}
.form-tip {
font-size: 11px;
color: #64748b;
margin-top: 3px;
}
.submit-btn {
width: 100%;
height: 40px;
font-size: 14px;
font-weight: 600;
}
/* 记录区域 */
.records-section {
margin-bottom: 20px;
}
/* 记录区域样式优化 */
.records-section {
margin-bottom: 20px;
}
/* 申请说明样式 */
.invoice-notice-section {
margin-bottom: 16px;
}
/* 开票信息详情样式 */
.invoice-info-details {
margin-top: 16px;
padding: 16px;
background: rgba(248, 250, 252, 0.8);
border: 1px solid rgba(226, 232, 240, 0.6);
border-radius: 8px;
}
.info-section-title {
font-size: 14px;
font-weight: 600;
color: #374151;
margin-bottom: 12px;
padding-bottom: 6px;
border-bottom: 1px solid rgba(226, 232, 240, 0.6);
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 12px;
}
.info-item {
display: flex;
align-items: flex-start;
gap: 8px;
}
.info-label {
font-size: 13px;
color: #6b7280;
min-width: 100px;
flex-shrink: 0;
}
.info-value {
font-size: 13px;
color: #374151;
word-break: break-all;
}
.records-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.record-item {
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(226, 232, 240, 0.6);
border-radius: 8px;
padding: 16px;
transition: all 0.3s ease;
}
.record-item:hover {
border-color: rgba(59, 130, 246, 0.3);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.record-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(226, 232, 240, 0.4);
}
.record-info {
display: flex;
align-items: center;
gap: 12px;
}
.record-id {
font-size: 13px;
color: #64748b;
font-family: 'Courier New', monospace;
}
.record-status {
padding: 3px 10px;
border-radius: 16px;
font-size: 11px;
font-weight: 500;
}
.status-pending {
background: rgba(245, 158, 11, 0.1);
color: #d97706;
border: 1px solid rgba(245, 158, 11, 0.3);
}
.status-completed {
background: rgba(16, 185, 129, 0.1);
color: #059669;
border: 1px solid rgba(16, 185, 129, 0.3);
}
.status-rejected {
background: rgba(239, 68, 68, 0.1);
color: #dc2626;
border: 1px solid rgba(239, 68, 68, 0.3);
}
.record-amount {
font-size: 18px;
font-weight: 700;
color: #10b981;
}
.record-details {
margin-bottom: 12px;
}
.detail-row {
display: flex;
margin-bottom: 6px;
}
.detail-label {
width: 90px;
font-size: 13px;
color: #64748b;
font-weight: 500;
}
.detail-value {
flex: 1;
font-size: 13px;
color: #1e293b;
}
.reject-reason {
color: #dc2626;
font-weight: 500;
}
.record-actions {
display: flex;
justify-content: flex-end;
}
.records-pagination {
margin-top: 16px;
display: flex;
justify-content: center;
}
/* 信息完整性提示 */
.info-completeness {
margin-top: 16px;
}
/* 申请弹窗样式 */
.info-preview {
margin-top: 16px;
padding: 16px;
background: rgba(59, 130, 246, 0.05);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: 8px;
}
.info-preview h4 {
font-size: 14px;
font-weight: 600;
color: #374151;
margin-bottom: 12px;
}
.preview-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.preview-item {
display: flex;
align-items: center;
}
.preview-label {
width: 120px;
font-size: 13px;
color: #64748b;
font-weight: 500;
}
.preview-value {
flex: 1;
font-size: 13px;
color: #1e293b;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* 信息显示样式 */
.info-display {
margin-bottom: 20px;
}
.info-card {
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(226, 232, 240, 0.6);
border-radius: 8px;
padding: 20px;
}
.info-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 16px;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-item {
display: flex;
align-items: center;
}
.info-label {
width: 120px;
font-size: 14px;
color: #64748b;
font-weight: 500;
}
.info-value {
flex: 1;
font-size: 14px;
color: #1e293b;
font-weight: 500;
}
/* 表单操作按钮 */
.form-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.cancel-btn {
height: 40px;
font-size: 14px;
}
.save-btn {
height: 40px;
font-size: 14px;
font-weight: 600;
}
/* 响应式设计 */
@media (max-width: 768px) {
.invoice-balance-card {
padding: 16px;
}
.balance-icon {
font-size: 28px;
margin-right: 12px;
}
.balance-amount {
font-size: 24px;
}
.balance-details {
flex-direction: column;
gap: 6px;
}
.invoice-tabs {
flex-direction: column;
}
.form-row {
grid-template-columns: 1fr;
gap: 12px;
}
.record-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.record-info {
flex-direction: column;
align-items: flex-start;
gap: 6px;
}
.detail-row {
flex-direction: column;
gap: 3px;
}
.detail-label {
width: auto;
}
}
</style>