| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- <!-- 发票管理页面 -->
- <template>
- <div class="page-container">
- <header class="page-head">
- <h1>发票管理</h1>
- <p class="page-desc">统一管理开票、作废、下载与发票详情查询。</p>
- </header>
- <section class="panel">
- <div class="panel-top">
- <h2>发票列表</h2>
- </div>
- <div class="filter-block">
- <el-form :model="searchForm" class="filter-form" @submit.prevent>
- <div class="filter-grid">
- <el-form-item label="发票号">
- <el-input v-model="searchForm.invoiceNo" placeholder="发票号" clearable />
- </el-form-item>
- <el-form-item label="客户名称">
- <el-input v-model="searchForm.clientName" placeholder="客户名称" clearable />
- </el-form-item>
- <el-form-item label="发票类型">
- <el-select v-model="searchForm.invoiceType" placeholder="请选择类型" clearable>
- <el-option label="全部" value="" />
- <el-option label="普通发票" value="normal" />
- <el-option label="增值税发票" value="vat" />
- </el-select>
- </el-form-item>
- <el-form-item label="状态">
- <el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
- <el-option label="全部" value="" />
- <el-option label="待开具" value="pending" />
- <el-option label="已开具" value="issued" />
- <el-option label="已作废" value="void" />
- </el-select>
- </el-form-item>
- </div>
- <div class="filter-actions-row">
- <span class="filter-hint">按发票号、客户、类型与状态快速检索。</span>
- <div class="filter-actions-end">
- <el-button class="btn-ghost" @click="handleReset">重置</el-button>
- <el-button class="btn-main" type="primary" @click="handleSearch">搜索</el-button>
- </div>
- </div>
- </el-form>
- </div>
- <div class="list-body list-wrap">
- <el-table :data="tableData" v-loading="loading" stripe class="list-table">
- <el-table-column prop="invoiceNo" label="发票号" min-width="180" />
- <el-table-column prop="orderNo" label="订单编号" min-width="180" />
- <el-table-column prop="clientName" label="客户名称" min-width="150" />
- <el-table-column prop="invoiceType" label="发票类型" min-width="120">
- <template #default="{ row }">
- <el-tag size="small">{{ row.invoiceType === 'normal' ? '普通发票' : '增值税发票' }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="title" label="发票抬头" min-width="150" show-overflow-tooltip />
- <el-table-column prop="invoiceAmount" label="发票金额(元)" min-width="120">
- <template #default="{ row }">
- <span class="amount-text">¥{{ row.invoiceAmount }}</span>
- </template>
- </el-table-column>
- <el-table-column prop="status" label="状态" min-width="100">
- <template #default="{ row }">
- <el-tag :type="getStatusType(row.status)" size="small">{{ getStatusText(row.status) }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="createAt" label="创建时间" min-width="180" />
- <el-table-column label="操作" min-width="180" fixed="right">
- <template #default="{ row }">
- <el-button class="btn-op-item primary" @click="handleView(row)">查看</el-button>
- <el-button v-if="row.status === 'pending'" class="btn-op-item" @click="handleIssue(row)">开具</el-button>
- <el-button v-if="row.status === 'issued'" class="btn-op-item" @click="handleDownload(row)">下载</el-button>
- <el-button v-if="row.status === 'pending'" class="btn-op-item" @click="handleVoid(row)">作废</el-button>
- </template>
- </el-table-column>
- </el-table>
- <div class="pagination list-pager-bar">
- <el-pagination
- v-model:current-page="pagination.page"
- v-model:page-size="pagination.pageSize"
- :total="pagination.total"
- :page-sizes="[10, 20, 50]"
- layout="total, sizes, prev, pager, next"
- @size-change="loadData"
- @current-change="loadData"
- />
- </div>
- </div>
- </section>
- <!-- 查看详情弹窗 -->
- <el-dialog v-model="viewVisible" title="发票详情" width="700px" class="invoice-dialog">
- <el-descriptions :column="2" border>
- <el-descriptions-item label="发票号">{{ viewData.invoiceNo }}</el-descriptions-item>
- <el-descriptions-item label="订单编号">{{ viewData.orderNo }}</el-descriptions-item>
- <el-descriptions-item label="客户名称">{{ viewData.clientName }}</el-descriptions-item>
- <el-descriptions-item label="发票类型">
- <el-tag size="small">{{ viewData.invoiceType === 'normal' ? '普通发票' : '增值税发票' }}</el-tag>
- </el-descriptions-item>
- <el-descriptions-item label="发票抬头" :span="2">{{ viewData.title }}</el-descriptions-item>
- <el-descriptions-item label="发票金额(元)">
- <span class="amount-text">¥{{ viewData.invoiceAmount }}</span>
- </el-descriptions-item>
- <el-descriptions-item label="税率">{{ viewData.taxRate }}%</el-descriptions-item>
- <el-descriptions-item label="税额">¥{{ viewData.taxAmount }}</el-descriptions-item>
- <el-descriptions-item label="发票状态">
- <el-tag :type="getStatusType(viewData.status)" size="small">{{ getStatusText(viewData.status) }}</el-tag>
- </el-descriptions-item>
- <el-descriptions-item v-if="viewData.invoiceType === 'vat'" label="税号">{{ viewData.taxNo }}</el-descriptions-item>
- <el-descriptions-item v-if="viewData.invoiceType === 'vat'" label="开户银行">{{ viewData.bank }}</el-descriptions-item>
- <el-descriptions-item v-if="viewData.invoiceType === 'vat'" label="银行账号">{{ viewData.bankAccount }}</el-descriptions-item>
- <el-descriptions-item v-if="viewData.invoiceType === 'vat'" label="开票地址">{{ viewData.address }}</el-descriptions-item>
- <el-descriptions-item v-if="viewData.invoiceType === 'vat'" label="电话">{{ viewData.phone }}</el-descriptions-item>
- <el-descriptions-item label="开具时间">{{ viewData.issuedAt || '-' }}</el-descriptions-item>
- <el-descriptions-item label="开具人">{{ viewData.issuedByName || '-' }}</el-descriptions-item>
- <el-descriptions-item label="备注" :span="2">{{ viewData.remark || '-' }}</el-descriptions-item>
- <el-descriptions-item label="创建时间">{{ viewData.createAt }}</el-descriptions-item>
- </el-descriptions>
- <template #footer>
- <el-button class="btn-ghost" @click="viewVisible = false">关闭</el-button>
- <el-button v-if="viewData.status === 'issued'" class="btn-main" type="primary" @click="handleDownload(viewData)">下载发票</el-button>
- </template>
- </el-dialog>
- <!-- 开具发票弹窗 -->
- <el-dialog v-model="issueVisible" title="开具发票" width="600px" class="invoice-dialog">
- <el-form :model="issueForm" label-width="100px">
- <el-form-item label="发票文件">
- <el-input v-model="issueForm.invoiceUrl" placeholder="请输入发票文件URL或上传发票文件" />
- </el-form-item>
- <el-form-item label="发票号码">
- <el-input v-model="issueForm.invoiceNo" placeholder="请输入发票号码" />
- </el-form-item>
- <el-form-item label="开票日期">
- <el-date-picker v-model="issueForm.issueDate" type="date" placeholder="选择开票日期" value-format="YYYY-MM-DD" />
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button class="btn-ghost" @click="issueVisible = false">取消</el-button>
- <el-button class="btn-main" type="primary" @click="confirmIssue" :loading="submitLoading">确定开具</el-button>
- </template>
- </el-dialog>
- <!-- 作废确认弹窗 -->
- <el-dialog v-model="voidVisible" title="确认作废" width="400px" class="invoice-dialog">
- <p style="text-align: center; padding: 20px 0;">确定要作废这张发票吗?</p>
- <template #footer>
- <el-button class="btn-ghost" @click="voidVisible = false">取消</el-button>
- <el-button class="btn-main" type="danger" @click="confirmVoid" :loading="submitLoading">确认作废</el-button>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, reactive, onMounted } from 'vue'
- import { ElMessage, ElMessageBox } from 'element-plus'
- import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
- import { fetchInvoiceList, fetchInvoiceDetail, fetchIssueInvoice, fetchVoidInvoice, fetchDownloadInvoice } from '@/api/business'
- defineOptions({ name: 'InvoiceManagement' })
- const searchForm = reactive({
- invoiceNo: '',
- clientName: '',
- invoiceType: '',
- status: ''
- })
- const tableData = ref<any[]>([])
- const loading = ref(false)
- const pagination = reactive({
- page: 1,
- pageSize: 10,
- total: 0
- })
- const viewVisible = ref(false)
- const viewData = ref<any>({})
- const issueVisible = ref(false)
- const issueForm = reactive({
- id: '',
- invoiceUrl: '',
- invoiceNo: '',
- issueDate: ''
- })
- const voidVisible = ref(false)
- const voidData = ref<any>({})
- const submitLoading = ref(false)
- const getStatusType = (status: string) => {
- const map: Record<string, string> = {
- pending: 'warning',
- issued: 'success',
- void: 'info'
- }
- return map[status] || 'info'
- }
- const getStatusText = (status: string) => {
- const map: Record<string, string> = {
- pending: '待开具',
- issued: '已开具',
- void: '已作废'
- }
- return map[status] || status
- }
- const loadData = async () => {
- loading.value = true
- try {
- const res = await fetchInvoiceList({
- page: pagination.page,
- pageSize: pagination.pageSize,
- invoiceNo: searchForm.invoiceNo || undefined,
- clientName: searchForm.clientName || undefined,
- invoiceType: searchForm.invoiceType || undefined,
- status: searchForm.status || undefined
- })
- tableData.value = res.list
- pagination.total = res.total
- } catch (error) {
- console.error('加载数据失败:', error)
- } finally {
- loading.value = false
- }
- }
- const handleSearch = () => {
- pagination.page = 1
- loadData()
- }
- const handleReset = () => {
- searchForm.invoiceNo = ''
- searchForm.clientName = ''
- searchForm.invoiceType = ''
- searchForm.status = ''
- pagination.page = 1
- loadData()
- }
- const handleView = async (row: any) => {
- try {
- const res = await fetchInvoiceDetail(row.id)
- viewData.value = res
- viewVisible.value = true
- } catch (error) {
- console.error('加载详情失败:', error)
- }
- }
- const handleIssue = (row: any) => {
- issueForm.id = row.id
- issueForm.invoiceUrl = ''
- issueForm.invoiceNo = ''
- issueForm.issueDate = ''
- issueVisible.value = true
- }
- const confirmIssue = async () => {
- if (!issueForm.invoiceUrl && !issueForm.invoiceNo) {
- ElMessage.warning('请填写发票文件URL或发票号码')
- return
- }
- submitLoading.value = true
- try {
- await withFullscreenLoading(async () => {
- await fetchIssueInvoice(issueForm.id, issueForm.invoiceUrl || undefined)
- ElMessage.success('发票开具成功')
- issueVisible.value = false
- loadData()
- }, fullscreenLoadingText.save)
- } catch (error) {
- console.error('开具失败:', error)
- ElMessage.error('开具失败,请重试')
- } finally {
- submitLoading.value = false
- }
- }
- const handleVoid = (row: any) => {
- voidData.value = row
- voidVisible.value = true
- }
- const confirmVoid = async () => {
- submitLoading.value = true
- try {
- await withFullscreenLoading(async () => {
- await fetchVoidInvoice(voidData.value.id)
- ElMessage.success('发票已作废')
- voidVisible.value = false
- loadData()
- }, fullscreenLoadingText.delete)
- } catch (error) {
- console.error('作废失败:', error)
- } finally {
- submitLoading.value = false
- }
- }
- const handleDownload = async (row: any) => {
- try {
- const res = await fetchDownloadInvoice(row.id)
- // 如果返回的是文件URL,直接打开
- if (res.url) {
- window.open(res.url, '_blank')
- } else {
- // 否则提示开发中
- ElMessage.info('发票文件下载中...')
- }
- } catch (error) {
- console.error('下载失败:', error)
- // 如果 API 不可用,使用备选方案
- if (row.invoiceUrl) {
- window.open(row.invoiceUrl, '_blank')
- } else {
- ElMessage.error('发票文件不存在,请稍后重试')
- }
- }
- }
- onMounted(() => {
- loadData()
- })
- </script>
- <style lang="scss" scoped>
- .amount-text {
- color: #dc2626;
- font-weight: 600;
- }
- </style>
|