321 lines
8.1 KiB
Vue

<script setup>
import { h, onMounted, ref, resolveDirective, withDirectives } from 'vue'
import dayjs from 'dayjs'
import { NButton, NInput, NTag, NSelect, NDatePicker } from 'naive-ui'
import CommonPage from '@/components/page/CommonPage.vue'
import QueryBarItem from '@/components/query-bar/QueryBarItem.vue'
import CrudTable from '@/components/table/CrudTable.vue'
import InvoiceModal from './InvoiceModal.vue'
import { formatDate, formatDateTime } from '@/utils'
import api from '@/api'
defineOptions({ name: '开票记录' })
const $table = ref(null)
const queryItems = ref({})
const vPermission = resolveDirective('permission')
// 开票/查看弹窗相关状态
const invoiceModalVisible = ref(false)
const invoiceModalMode = ref('view') // 'send' 或 'view'
const currentInvoice = ref(null)
const submittedDateRange = ref(null)
const handleDateRangeChange = (val) => {
if (val && val.length === 2) {
queryItems.value.submitted_start = dayjs(val[0]).startOf('day').format('YYYY-MM-DD HH:mm:ss')
queryItems.value.submitted_end = dayjs(val[1]).endOf('day').format('YYYY-MM-DD HH:mm:ss')
} else {
queryItems.value.submitted_start = null
queryItems.value.submitted_end = null
}
$table.value?.handleSearch()
}
// 状态选项
const statusOptions = [
{ label: '未开票', value: 'pending' },
{ label: '已开票', value: 'invoiced' },
{ label: '已拒绝', value: 'rejected' },
{ label: '已退款', value: 'refunded' },
]
onMounted(() => {
$table.value?.handleSearch()
})
// 状态标签渲染
const renderStatus = (status) => {
const statusMap = {
pending: { type: 'warning', text: '未开票' },
invoiced: { type: 'success', text: '已开票' },
rejected: { type: 'error', text: '已拒绝' },
refunded: { type: 'info', text: '已退款' },
}
const config = statusMap[status] || { type: 'default', text: '-' }
return h(NTag, { type: config.type }, { default: () => config.text })
}
// 开票类型渲染
const renderInvoiceType = (type) => {
const typeMap = {
normal: '增值税普通发票',
special: '增值税专用发票',
}
return typeMap[type] || type
}
const columns = [
{
title: 'ID',
key: 'id',
width: 80,
align: 'center',
},
{
title: '提交时间',
key: 'submitted_at',
width: 140,
align: 'center',
render(row) {
return formatDate(row.submitted_at)
},
},
{
title: '付款凭证',
key: 'receipts',
width: 140,
align: 'center',
render(row) {
const list = Array.isArray(row.receipts) ? row.receipts : []
const urls = list.map((item) => item?.url).filter(Boolean)
if (!urls.length) return '-'
return h(
'div',
{ style: 'display:flex; gap:6px; justify-content:center; align-items: center;' },
urls.slice(0, 3).map((url, idx) =>
h('img', {
src: url,
style:
'width:40px;height:40px;object-fit:cover;border-radius:4px;border:1px solid #e5e6eb;cursor:pointer;',
alt: '付款凭证',
onClick: () => window.open(url, '_blank'),
})
)
)
},
},
{
title: '手机号',
key: 'phone',
width: 110,
align: 'center',
ellipsis: { tooltip: true },
},
{
title: '微信号',
key: 'wechat',
width: 120,
align: 'center',
ellipsis: { tooltip: true },
},
{
title: '公司名称',
key: 'company_name',
width: 150,
align: 'center',
ellipsis: { tooltip: true },
},
{
title: '公司税号',
key: 'tax_number',
width: 150,
align: 'center',
ellipsis: { tooltip: true },
},
{
title: '注册地址',
key: 'register_address',
width: 180,
align: 'center',
ellipsis: { tooltip: true },
},
{
title: '注册电话',
key: 'register_phone',
width: 120,
align: 'center',
ellipsis: { tooltip: true },
},
{
title: '开户银行',
key: 'bank_name',
width: 140,
align: 'center',
ellipsis: { tooltip: true },
},
{
title: '银行账号',
key: 'bank_account',
width: 180,
align: 'center',
ellipsis: { tooltip: true },
},
{
title: '接收邮箱',
key: 'email',
width: 180,
align: 'center',
ellipsis: { tooltip: true },
},
{
title: '开票类型',
key: 'invoice_type',
width: 130,
align: 'center',
render(row) {
return renderInvoiceType(row.invoice_type)
},
},
{
title: '状态',
key: 'status',
width: 90,
align: 'center',
render(row) {
return renderStatus(row.status)
},
},
{
title: '操作',
key: 'actions',
width: 80,
align: 'center',
fixed: 'right',
render(row) {
const editable = row.status === 'pending'
return withDirectives(
h(
NButton,
{
size: 'small',
type: editable ? 'primary' : 'default',
onClick: () => handleInvoice(row),
},
{ default: () => '开票' }
),
[[vPermission, 'post/api/v1/transactions/send-email']]
)
},
},
]
// 开票按钮:未开票可编辑,其余只读
function handleInvoice(row) {
const editable = row.status === 'pending'
invoiceModalMode.value = editable ? 'send' : 'view'
currentInvoice.value = row
invoiceModalVisible.value = true
}
// 确认发送邮件
// 确认发送邮件
async function handleInvoiceConfirm(formData) {
try {
const payload = {
receipt_id: formData.id,
email: formData.email,
subject: formData.email, // 用户要求 subject 传 email
body: formData.content, // 映射 content -> body
file_url: formData.attachments, // 映射 attachments -> file_url
}
await api.sendInvoice(payload)
$message.success('发送成功')
invoiceModalVisible.value = false
$table.value?.handleSearch()
} catch (error) {
console.error(error)
$message.error(error?.message || '操作失败')
}
}
</script>
<template>
<CommonPage show-footer title="开票记录">
<!-- 表格 -->
<CrudTable
ref="$table"
v-model:query-items="queryItems"
:columns="columns"
:row-key="(row) => row.receipt?.id || row.id"
:get-data="api.getInvoiceList"
>
<template #queryBar>
<QueryBarItem label="提交时间" :label-width="80">
<NDatePicker
v-model:value="submittedDateRange"
type="daterange"
clearable
placeholder="请选择提交时间"
style="width: 280px"
@update:value="handleDateRangeChange"
/>
</QueryBarItem>
<QueryBarItem label="手机号" :label-width="80">
<NInput
v-model:value="queryItems.phone"
clearable
type="text"
placeholder="请输入手机号"
style="width: 200px"
@keypress.enter="$table?.handleSearch()"
/>
</QueryBarItem>
<QueryBarItem label="状态" :label-width="80">
<NSelect
v-model:value="queryItems.status"
:options="statusOptions"
placeholder="请选择状态"
clearable
style="width: 200px"
@update:value="$table?.handleSearch()"
/>
</QueryBarItem>
<QueryBarItem label="公司名称" :label-width="80">
<NInput
v-model:value="queryItems.company_name"
clearable
type="text"
placeholder="请输入公司名称"
style="width: 200px"
@keypress.enter="$table?.handleSearch()"
/>
</QueryBarItem>
<QueryBarItem label="公司税号" :label-width="80">
<NInput
v-model:value="queryItems.tax_number"
clearable
type="text"
placeholder="请输入公司税号"
style="width: 200px"
@keypress.enter="$table?.handleSearch()"
/>
</QueryBarItem>
</template>
</CrudTable>
<!-- 开票/查看弹窗 -->
<InvoiceModal
v-model:visible="invoiceModalVisible"
:invoice-data="currentInvoice"
:mode="invoiceModalMode"
@confirm="handleInvoiceConfirm"
/>
</CommonPage>
</template>