Wei_佳 a33a80063d chore: 更新项目配置并移除GitHub链接组件
- 更新应用标题为中文"管理后台"
- 简化package.json中的项目名称为"admin-web"
- 删除GithubSite.vue组件及其相关导入
- 移除header中的GitHub链接图标
- 清理发票列表中的权限指令装饰器
- 清理用户列表中的权限指令装饰器
- 优化项目配置和组件结构
2025-11-27 18:07:52 +08:00

391 lines
9.9 KiB
Vue

<script setup>
import { h, onMounted, ref, resolveDirective, withDirectives } from 'vue'
import dayjs from 'dayjs'
import { NButton, NInput, NTag, NSelect, NDatePicker, useMessage, useDialog } 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 $message = useMessage()
const dialog = useDialog()
// 开票/查看弹窗相关状态
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: 'refunded' },
]
onMounted(() => {
$table.value?.handleSearch()
})
// 状态标签渲染
const renderStatus = (status) => {
const statusMap = {
pending: { type: 'warning', text: '未开票' },
invoiced: { type: 'success', 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: 150,
align: 'center',
fixed: 'right',
render(row) {
const buttons = []
if (row.status === 'pending') {
// 未开票:显示"开票"和"退款"按钮
buttons.push(
withDirectives(
h(
NButton,
{
size: 'small',
type: 'primary',
onClick: () => handleInvoice(row),
},
{ default: () => '开票' }
),
)
)
buttons.push(
withDirectives(
h(
NButton,
{
size: 'small',
type: 'warning',
onClick: () => handleRefund(row),
style: { marginLeft: '8px' },
},
{ default: () => '退款' }
),
)
)
} else if (row.status === 'invoiced') {
// 已开票:显示"查看"按钮
buttons.push(
h(
NButton,
{
size: 'small',
type: 'default',
onClick: () => handleInvoice(row),
},
{ default: () => '查看' }
)
)
}
// 已退款状态不显示任何按钮
return h('div', { style: 'display: flex; gap: 8px; justify-content: center;' }, buttons)
},
},
]
// 开票按钮:未开票可编辑,其余只读
function handleInvoice(row) {
const editable = row.status === 'pending'
invoiceModalMode.value = editable ? 'send' : 'view'
currentInvoice.value = row
invoiceModalVisible.value = true
}
// 退款处理
function handleRefund(row) {
dialog.warning({
title: '确认退款',
content: `确定要将此订单标记为已退款吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
const invoiceId = row.invoice_id
if (!invoiceId) {
$message.error('无法获取发票ID')
return
}
await api.refundInvoice({
id: invoiceId,
status: 'refunded',
})
$message.success('退款成功')
$table.value?.handleSearch()
} catch (error) {
console.error(error)
$message.error(error?.message || '退款失败')
}
},
})
}
// 确认发送邮件
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_urls: formData.attachments, // 映射 attachments -> file_url
file_url: formData.attachments, // 映射 attachments -> file_url
status:'success'
}
let res = await api.sendInvoice(payload)
if (res.code === 200 && res.msg) {
$message.success(res.msg)
}
if (res.code === 200 && res.data && res.data.msg) {
$message.success(res.data.msg)
}
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>