Files
cz-web/src/views/finance/invoices.vue
2026-10-09 17:17:43 +08:00

433 lines
11 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.
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { RouterLink } from 'vue-router'
import { ElMessage, ElButton, ElRadioGroup, ElRadioButton, ElTable, ElTableColumn } from 'element-plus'
import { FilterPanel, PaginationBar, StatusTag } from '@/components'
import { useFinanceStore } from '@/stores/modules/finance'
import type { Invoice, InvoiceStatus } from '@/types/business/finance'
import { formatMoney } from '@/utils/format'
const financeStore = useFinanceStore()
const { titles, invoices, invoiceQuery, invoiceTotal } = storeToRefs(financeStore)
const keyword = ref('')
const selectedIds = ref<string[]>([])
const defaultTitle = computed(() => titles.value.find((item) => item.isDefault) ?? titles.value[0] ?? null)
const STATUS_TABS: Array<{ label: string; value: InvoiceStatus | 'all' }> = [
{ label: '全部', value: 'all' },
{ label: '已开票', value: '已开票' },
{ label: '开票中', value: '开票中' },
{ label: '未开票', value: '未开票' },
]
const STATUS_VARIANTS: Record<InvoiceStatus, 'success' | 'warning' | 'info'> = {
已开票: 'success',
开票中: 'warning',
未开票: 'info',
}
function onTab(value: InvoiceStatus | 'all'): void {
if (invoiceQuery.value.status === value) return
financeStore.setInvoiceQuery({ ...invoiceQuery.value, status: value, keyword: keyword.value })
selectedIds.value = []
void financeStore.searchInvoices()
}
function onTabChange(val: string | number | boolean | undefined): void {
onTab(val as InvoiceStatus | 'all')
}
function onSearch(): void {
financeStore.setInvoiceQuery({ ...invoiceQuery.value, keyword: keyword.value })
void financeStore.searchInvoices()
}
function onReset(): void {
keyword.value = ''
selectedIds.value = []
financeStore.setInvoiceQuery({ keyword: '', status: 'all', page: 1, pageSize: invoiceQuery.value.pageSize })
void financeStore.fetchInvoices()
}
function onPageChange(page: number): void {
void financeStore.changeInvoicePage(page)
}
async function onApply(invoice: Invoice): Promise<void> {
await financeStore.applyInvoice(invoice.id)
ElMessage.success('开票申请已提交')
}
function onPreview(): void {
ElMessage.info('附件待接口接入')
}
function onDownload(): void {
ElMessage.info('附件待接口接入')
}
function onSendEmail(): void {
ElMessage.success('发票已发送至绑定邮箱(演示)')
}
function onSelectionChange(rows: Invoice[]): void {
selectedIds.value = rows.map((row) => row.id)
}
onMounted(() => {
keyword.value = invoiceQuery.value.keyword
void Promise.all([financeStore.fetchTitles(), financeStore.fetchInvoices()])
})
</script>
<template>
<div class="invoices">
<nav class="invoices__breadcrumb text-secondary">
<RouterLink to="/finance" class="link">
财务中心
</RouterLink>
<span> &gt; </span>
<span>发票申请</span>
</nav>
<section v-if="defaultTitle" class="invoices__title-card card">
<div class="invoices__title-main">
<div class="invoices__title-head">
<span class="invoices__title-label text-secondary">默认发票抬头</span>
<StatusTag :label="defaultTitle.type" variant="primary" />
<StatusTag label="默认" variant="success" />
</div>
<p class="invoices__title-name">
{{ defaultTitle.title }}
</p>
<p class="invoices__title-tax text-secondary">
税号:{{ defaultTitle.taxNo }}
</p>
</div>
<RouterLink to="/finance/invoice-titles" class="invoices__title-manage">
管理抬头 &gt;
</RouterLink>
</section>
<el-radio-group
class="invoices__tabs"
:model-value="invoiceQuery.status"
@change="onTabChange"
>
<el-radio-button
v-for="tab in STATUS_TABS"
:key="tab.value"
:value="tab.value"
>
{{ tab.label }}
</el-radio-button>
</el-radio-group>
<FilterPanel
v-model:keyword="keyword"
keyword-placeholder="搜索发票抬头 / 账单号 / 项目名称"
@search="onSearch"
@reset="onReset"
/>
<div class="invoices__table card">
<el-table
class="invoices__table-el"
:data="invoices"
row-key="id"
@selection-change="onSelectionChange"
>
<el-table-column
type="selection"
width="48"
/>
<el-table-column
type="index"
label="序号"
width="60"
/>
<el-table-column
prop="planName"
label="账单名称"
min-width="180"
show-overflow-tooltip
/>
<el-table-column
prop="billNo"
label="账单号"
width="150"
show-overflow-tooltip
/>
<el-table-column
prop="tradeNo"
label="交易流水号"
min-width="180"
show-overflow-tooltip
/>
<el-table-column
prop="projectName"
label="项目名称"
min-width="160"
show-overflow-tooltip
/>
<el-table-column
label="订单金额"
width="110"
>
<template #default="{ row }">
<span class="invoices__amount">¥{{ formatMoney((row as Invoice).amount) }}</span>
</template>
</el-table-column>
<el-table-column
label="状态"
width="90"
>
<template #default="{ row }">
<StatusTag
:label="(row as Invoice).status"
:variant="STATUS_VARIANTS[(row as Invoice).status]"
/>
</template>
</el-table-column>
<el-table-column
prop="applyTime"
label="申请时间"
width="140"
show-overflow-tooltip
/>
<el-table-column
label="操作"
width="180"
fixed="right"
>
<template #default="{ row }">
<span class="invoices__ops">
<template v-if="(row as Invoice).status === '已开票'">
<el-button
link
type="primary"
class="invoices__link"
@click="onPreview"
>
预览
</el-button>
<el-button
link
type="primary"
class="invoices__link"
@click="onDownload"
>
下载
</el-button>
<el-button
link
type="primary"
class="invoices__link"
@click="onSendEmail"
>
发送邮箱
</el-button>
</template>
<span v-else-if="(row as Invoice).status === '开票中'" class="invoices__dash">—</span>
<el-button
v-else
link
type="primary"
class="invoices__link"
@click="onApply(row as Invoice)"
>
申请开票
</el-button>
</span>
</template>
</el-table-column>
<template #empty>
<span class="invoices__empty text-secondary">暂无发票记录</span>
</template>
</el-table>
</div>
<PaginationBar
:page="invoiceQuery.page"
:page-size="invoiceQuery.pageSize"
:total="invoiceTotal"
@page-change="onPageChange"
/>
<p class="invoices__tip text-secondary">
开票将使用默认抬头,如需调整请前往 <RouterLink to="/finance/invoice-titles" class="link">管理抬头</RouterLink>。
</p>
</div>
</template>
<style lang="less" scoped>
.invoices {
.flex(column, flex-start);
align-items: stretch;
gap: @spacing-base;
width: 100%;
max-width: @container-width;
margin: 0 auto;
padding: @spacing-lg @spacing-base @spacing-xxl;
&__breadcrumb {
font-size: 11px;
color: @text-color-disabled;
> span:last-child {
color: @text-color;
}
}
&__title-card {
.flex(row, space-between, center);
padding: @spacing-base @spacing-lg;
border-radius: @radius-lg;
background: @white;
box-shadow: @shadow-base;
}
&__title-main {
.flex(column, flex-start, stretch);
gap: @spacing-xs;
}
&__title-head {
.flex(row, flex-start, center);
gap: @spacing-sm;
}
&__title-label {
font-size: @font-size-sm;
}
&__title-name {
font-size: @font-size-lg;
font-weight: 600;
color: @heading-color;
}
&__title-tax {
font-size: @font-size-base;
}
&__title-manage {
color: @primary-color;
font-size: @font-size-base;
white-space: nowrap;
&:hover {
color: @primary-hover;
}
}
&__tabs {
:deep(.el-radio-button__inner) {
height: 34px;
padding: 0 @spacing-base;
border: 1px solid @border-color;
border-radius: @radius-base;
background: @white;
color: @text-color;
font-size: @font-size-base;
box-shadow: none;
}
:deep(.el-radio-button + .el-radio-button) {
margin-left: @spacing-sm;
}
:deep(.el-radio-button.is-active .el-radio-button__inner) {
border-color: @primary-color;
background: @primary-color;
color: @white;
box-shadow: -1px 0 0 0 @primary-color;
}
}
&__table {
padding: @spacing-base;
border-radius: @radius-lg;
background: @white;
box-shadow: @shadow-base;
overflow-x: auto;
}
&__table-el {
width: 100%;
font-size: @font-size-base;
color: @text-color;
:deep(th.el-table__cell) {
background: @bg-color-light;
border-bottom: 1px solid @border-color;
color: @text-color-secondary;
font-size: @font-size-base;
font-weight: 500;
text-align: left;
}
:deep(td.el-table__cell) {
padding: @spacing-sm 0;
border-bottom: 1px solid @border-color;
font-size: @font-size-base;
}
:deep(.el-table__body tr:hover > td.el-table__cell) {
background: @bg-color-light;
}
:deep(.el-table .cell) {
padding-left: @spacing-sm;
padding-right: @spacing-sm;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
&__amount {
color: @heading-color;
font-weight: 600;
}
&__ops {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: @spacing-sm;
}
&__link {
padding: 0;
border: none;
background: transparent;
color: @primary-color;
font-size: @font-size-base;
transition: color 0.2s;
&:hover {
color: @primary-hover;
background: transparent;
}
}
&__dash {
color: @text-color-secondary;
}
&__empty {
text-align: center;
padding: @spacing-xl 0;
}
&__tip {
font-size: @font-size-sm;
}
}
</style>