433 lines
11 KiB
Vue
433 lines
11 KiB
Vue
<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> > </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">
|
||
管理抬头 >
|
||
</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>
|