374 lines
9.4 KiB
Vue
374 lines
9.4 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { storeToRefs } from 'pinia'
|
||
import { RouterLink } from 'vue-router'
|
||
import { ElMessage } from 'element-plus'
|
||
import { FilterPanel, PaginationBar, StatusTag } from '@/components'
|
||
// 页面容器:数据只经 store,不直接 import api
|
||
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 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 onToggleSelect(id: string): void {
|
||
if (selectedIds.value.includes(id)) {
|
||
selectedIds.value = selectedIds.value.filter((item) => item !== id)
|
||
}
|
||
else {
|
||
selectedIds.value = [...selectedIds.value, 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>
|
||
|
||
<div class="invoices__tabs">
|
||
<button
|
||
v-for="tab in STATUS_TABS"
|
||
:key="tab.value"
|
||
class="invoices__tab"
|
||
:class="{ 'is-active': invoiceQuery.status === tab.value }"
|
||
@click="onTab(tab.value)"
|
||
>
|
||
{{ tab.label }}
|
||
</button>
|
||
</div>
|
||
|
||
<FilterPanel
|
||
v-model:keyword="keyword"
|
||
keyword-placeholder="搜索发票抬头 / 账单号 / 项目名称"
|
||
@search="onSearch"
|
||
@reset="onReset"
|
||
/>
|
||
|
||
<div class="invoices__table card">
|
||
<table class="invoices__table-el">
|
||
<thead>
|
||
<tr>
|
||
<th class="invoices__check">
|
||
多选
|
||
</th>
|
||
<th>序号</th>
|
||
<th>账单名称</th>
|
||
<th>账单号</th>
|
||
<th>交易流水号</th>
|
||
<th>项目名称</th>
|
||
<th>订单金额</th>
|
||
<th>状态</th>
|
||
<th>申请时间</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(invoice, index) in invoices" :key="invoice.id">
|
||
<td class="invoices__check">
|
||
<input
|
||
type="checkbox"
|
||
:checked="selectedIds.includes(invoice.id)"
|
||
@change="onToggleSelect(invoice.id)"
|
||
>
|
||
</td>
|
||
<td>{{ index + 1 }}</td>
|
||
<td>{{ invoice.planName }}</td>
|
||
<td>{{ invoice.billNo }}</td>
|
||
<td>{{ invoice.tradeNo ?? '—' }}</td>
|
||
<td>{{ invoice.projectName }}</td>
|
||
<td class="invoices__amount">¥{{ formatMoney(invoice.amount) }}</td>
|
||
<td>
|
||
<StatusTag :label="invoice.status" :variant="STATUS_VARIANTS[invoice.status]" />
|
||
</td>
|
||
<td>{{ invoice.applyTime }}</td>
|
||
<td class="invoices__ops">
|
||
<template v-if="invoice.status === '已开票'">
|
||
<button class="invoices__link" @click="onPreview">
|
||
预览
|
||
</button>
|
||
<button class="invoices__link" @click="onDownload">
|
||
下载
|
||
</button>
|
||
<button class="invoices__link" @click="onSendEmail">
|
||
发送邮箱
|
||
</button>
|
||
</template>
|
||
<span v-else-if="invoice.status === '开票中'" class="invoices__dash">—</span>
|
||
<button v-else class="invoices__link" @click="onApply(invoice)">
|
||
申请开票
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="invoices.length === 0">
|
||
<td colspan="10" class="invoices__empty text-secondary">
|
||
暂无发票记录
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</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;
|
||
|
||
// 与财务中心其他页一致:面包屑 11px、前缀灰当前页深灰
|
||
&__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 {
|
||
.flex(row, flex-start);
|
||
gap: @spacing-sm;
|
||
}
|
||
|
||
&__tab {
|
||
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;
|
||
transition: all 0.2s;
|
||
|
||
&:hover {
|
||
border-color: @primary-color;
|
||
color: @primary-color;
|
||
}
|
||
|
||
&.is-active {
|
||
border-color: @primary-color;
|
||
background: @primary-color;
|
||
color: @white;
|
||
}
|
||
}
|
||
|
||
&__table {
|
||
padding: @spacing-base;
|
||
border-radius: @radius-lg;
|
||
background: @white;
|
||
box-shadow: @shadow-base;
|
||
overflow-x: auto;
|
||
}
|
||
|
||
&__table-el {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: @font-size-base;
|
||
color: @text-color;
|
||
|
||
th,
|
||
td {
|
||
padding: @spacing-sm;
|
||
text-align: left;
|
||
white-space: nowrap;
|
||
border-bottom: 1px solid @border-color;
|
||
}
|
||
|
||
th {
|
||
background: @bg-color-light;
|
||
color: @text-color-secondary;
|
||
font-weight: 500;
|
||
}
|
||
|
||
tbody tr:hover {
|
||
background: @bg-color-light;
|
||
}
|
||
}
|
||
|
||
&__check {
|
||
width: 48px;
|
||
text-align: center;
|
||
}
|
||
|
||
&__amount {
|
||
color: @heading-color;
|
||
font-weight: 600;
|
||
}
|
||
|
||
&__ops {
|
||
.flex(row, flex-start);
|
||
gap: @spacing-sm;
|
||
}
|
||
|
||
&__link {
|
||
color: @primary-color;
|
||
font-size: @font-size-base;
|
||
transition: color 0.2s;
|
||
|
||
&:hover {
|
||
color: @primary-hover;
|
||
}
|
||
}
|
||
|
||
&__dash {
|
||
color: @text-color-secondary;
|
||
}
|
||
|
||
&__empty {
|
||
text-align: center;
|
||
padding: @spacing-xl 0;
|
||
}
|
||
|
||
&__tip {
|
||
font-size: @font-size-sm;
|
||
}
|
||
}
|
||
</style>
|