321 lines
8.1 KiB
Vue
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>
|