Files
tyapi-frontend/src/pages/finance/Invoice.vue

1577 lines
42 KiB
Vue
Raw Normal View History

2025-11-24 16:06:44 +08:00
<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>