feat: 优化发票列表显示付款凭证、调整列宽并新增权限控制,同时为用户管理模块的增删改操作添加权限验证

This commit is contained in:
Wei_佳 2025-11-20 17:03:56 +08:00
parent 2ff5421c27
commit a0e857b115
5 changed files with 130 additions and 519 deletions

View File

@ -17,10 +17,10 @@ INSERT INTO menu (id, name, menu_type, icon, path, "order", parent_id, is_hidden
VALUES
(16, '交易管理', 'catalog', 'carbon:receipt', '/transaction', 3, 0, 0, 'Layout', 0, '/transaction/invoice', datetime('now'), datetime('now'));
-- 插入二级菜单:开票记录
-- 插入二级菜单:交易管理
INSERT INTO menu (id, name, menu_type, icon, path, "order", parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
VALUES
(17, '开票记录', 'menu', 'carbon:document', 'invoice', 1, 16, 0, '/transaction/invoice', 0, NULL, datetime('now'), datetime('now'));
(17, '交易管理', 'menu', 'carbon:document', 'invoice', 1, 16, 0, '/transaction/invoice', 0, NULL, datetime('now'), datetime('now'));
-- ========================================
-- 3. 估值管理菜单
@ -57,7 +57,7 @@ INSERT INTO role_menu (role_id, menu_id)
VALUES
(1, 22), -- 工作台
(1, 16), -- 交易管理
(1, 17), -- 开票记录
(1, 17), -- 交易管理
(1, 18), -- 估值管理
(1, 19), -- 审核列表
(1, 20), -- 用户管理
@ -68,7 +68,7 @@ INSERT INTO role_menu (role_id, menu_id)
VALUES
(2, 22), -- 工作台
(2, 16), -- 交易管理
(2, 17), -- 开票记录
(2, 17), -- 交易管理
(2, 18), -- 估值管理
(2, 19); -- 审核列表
-- 注意:普通用户不分配用户管理权限

View File

@ -323,229 +323,10 @@ export default {
createAppUser: (data = {}) => request.post('/app-user/register', data),
updateAppUser: (data = {}) => request.post('/app-user/update', data),
deleteAppUser: (params = {}) => request.delete('/app-user/delete', { params }),
// invoice (开票记录)
getInvoiceList: (params = {}) => {
// Mock 数据
const mockInvoices = [
{
id: 1001,
created_at: '2024-11-10T09:30:00Z',
ticket_type: 'electronic',
phone: '13800138001',
email: 'zhangsan@company1.com',
company_name: '北京科技有限公司',
tax_number: '91110000123456789A',
register_address: '北京市朝阳区科技园区A座1001室',
register_phone: '010-12345678',
bank_name: '中国工商银行北京分行',
bank_account: '6222021234567890123',
invoice_type: 'special',
status: 'pending'
},
{
id: 1002,
created_at: '2024-11-09T14:20:00Z',
ticket_type: 'paper',
phone: '13800138002',
email: 'lisi@company2.com',
company_name: '上海贸易股份有限公司',
tax_number: '91310000987654321B',
register_address: '上海市浦东新区金融街B座2002室',
register_phone: '021-87654321',
bank_name: '中国建设银行上海分行',
bank_account: '6217001234567890124',
invoice_type: 'normal',
status: 'invoiced'
},
{
id: 1003,
created_at: '2024-11-08T16:45:00Z',
ticket_type: 'electronic',
phone: '13800138003',
email: 'wangwu@company3.com',
company_name: '深圳创新科技有限公司',
tax_number: '91440300456789012C',
register_address: '深圳市南山区高新技术园C座3003室',
register_phone: '0755-23456789',
bank_name: '招商银行深圳分行',
bank_account: '6214851234567890125',
invoice_type: 'special',
status: 'rejected'
},
{
id: 1004,
created_at: '2024-11-07T11:15:00Z',
ticket_type: 'paper',
phone: '13800138004',
email: 'zhaoliu@company4.com',
company_name: '广州制造业集团有限公司',
tax_number: '91440100789012345D',
register_address: '广州市天河区商务中心D座4004室',
register_phone: '020-34567890',
bank_name: '中国银行广州分行',
bank_account: '6013821234567890126',
invoice_type: 'normal',
status: 'pending'
},
{
id: 1005,
created_at: '2024-11-06T08:30:00Z',
ticket_type: 'electronic',
phone: '13800138005',
email: 'sunqi@company5.com',
company_name: '杭州互联网科技有限公司',
tax_number: '91330100012345678E',
register_address: '杭州市西湖区互联网小镇E座5005室',
register_phone: '0571-45678901',
bank_name: '浙商银行杭州分行',
bank_account: '6228481234567890127',
invoice_type: 'special',
status: 'invoiced'
},
{
id: 1006,
created_at: '2024-11-05T13:20:00Z',
ticket_type: 'paper',
phone: '13800138006',
email: 'zhouba@company6.com',
company_name: '成都软件开发有限公司',
tax_number: '91510100345678901F',
register_address: '成都市高新区软件园F座6006室',
register_phone: '028-56789012',
bank_name: '中国农业银行成都分行',
bank_account: '6230521234567890128',
invoice_type: 'normal',
status: 'pending'
},
{
id: 1007,
created_at: '2024-11-04T15:45:00Z',
ticket_type: 'electronic',
phone: '13800138007',
email: 'wujiu@company7.com',
company_name: '武汉新能源科技有限公司',
tax_number: '91420100678901234G',
register_address: '武汉市江汉区新能源产业园G座7007室',
register_phone: '027-67890123',
bank_name: '交通银行武汉分行',
bank_account: '6222601234567890129',
invoice_type: 'special',
status: 'invoiced'
},
{
id: 1008,
created_at: '2024-11-03T10:10:00Z',
ticket_type: 'paper',
phone: '13800138008',
email: 'zhengshi@company8.com',
company_name: '西安电子商务有限公司',
tax_number: '91610100901234567H',
register_address: '西安市雁塔区电商产业园H座8008室',
register_phone: '029-78901234',
bank_name: '中信银行西安分行',
bank_account: '6217711234567890130',
invoice_type: 'normal',
status: 'refunded'
},
{
id: 1009,
created_at: '2024-11-02T14:30:00Z',
ticket_type: 'electronic',
phone: '13800138009',
email: 'wangwu@company9.com',
company_name: '天津物流科技有限公司',
tax_number: '91120000234567890I',
register_address: '天津市滨海新区物流园I座9009室',
register_phone: '022-89012345',
bank_name: '民生银行天津分行',
bank_account: '6226221234567890131',
invoice_type: 'special',
status: 'refunded'
},
{
id: 1010,
created_at: '2024-11-01T11:45:00Z',
ticket_type: 'paper',
phone: '13800138010',
email: 'liuliu@company10.com',
company_name: '重庆智能制造有限公司',
tax_number: '91500000567890123J',
register_address: '重庆市渝北区智能制造园J座1010室',
register_phone: '023-90123456',
bank_name: '华夏银行重庆分行',
bank_account: '6228881234567890132',
invoice_type: 'normal',
status: 'rejected'
}
]
// 模拟分页和搜索
let filteredInvoices = [...mockInvoices]
// 手机号搜索
if (params.phone) {
filteredInvoices = filteredInvoices.filter(invoice =>
invoice.phone.includes(params.phone)
)
}
// 公司名称搜索
if (params.company_name) {
filteredInvoices = filteredInvoices.filter(invoice =>
invoice.company_name.includes(params.company_name)
)
}
// 公司税号搜索
if (params.tax_number) {
filteredInvoices = filteredInvoices.filter(invoice =>
invoice.tax_number.includes(params.tax_number)
)
}
// 状态筛选
if (params.status) {
filteredInvoices = filteredInvoices.filter(invoice =>
invoice.status === params.status
)
}
// 提交时间筛选
if (params.created_at && Array.isArray(params.created_at) && params.created_at.length === 2) {
const [startDate, endDate] = params.created_at
filteredInvoices = filteredInvoices.filter(invoice => {
const invoiceDate = new Date(invoice.created_at)
return invoiceDate >= new Date(startDate) && invoiceDate <= new Date(endDate)
})
}
// 分页处理
const page = params.page || 1
const pageSize = params.page_size || 10
const startIndex = (page - 1) * pageSize
const endIndex = startIndex + pageSize
const paginatedInvoices = filteredInvoices.slice(startIndex, endIndex)
// 返回 Promise 模拟异步请求
return new Promise((resolve) => {
setTimeout(() => {
resolve({
data: paginatedInvoices,
total: filteredInvoices.length,
page: page,
page_size: pageSize
})
}, 300) // 模拟网络延迟
})
},
getInvoiceById: (params = {}) => request.get('/invoice/detail', { params }),
createInvoice: (data = {}) => request.post('/invoice/create', data),
updateInvoice: (data = {}) => request.post('/invoice/update', data),
deleteInvoice: (params = {}) => request.delete('/invoice/delete', { params }),
updateInvoiceStatus: (data = {}) => request.post('/invoice/update-status', data),
remindInvoice: (data = {}) => request.post('/invoice/remind', data),
refundInvoice: (data = {}) => request.post('/invoice/refund', data),
sendInvoice: (data = {}) => request.post('/invoice/send', data),
// invoice (交易管理-对公转账记录)
getInvoiceList: (params = {}) => request.get('/transactions/receipts', { params }),
getInvoiceById: (params = {}) => request.get(`/transactions/receipts/${params.id}`, { params }),
sendInvoice: (data = {}) => request.post('/transactions/send-email', data),
// valuation (估值评估)
getValuationList: (params = {}) => {
// 模拟分页和搜索

View File

@ -1,26 +1,14 @@
<script setup>
import { h, onMounted, ref, resolveDirective, withDirectives } from 'vue'
import {
NButton,
NForm,
NFormItem,
NInput,
NTag,
NPopconfirm,
NSelect,
NDatePicker,
} from 'naive-ui'
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 CrudModal from '@/components/table/CrudModal.vue'
import CrudTable from '@/components/table/CrudTable.vue'
import InvoiceModal from './InvoiceModal.vue'
import { formatDate, renderIcon } from '@/utils'
import { useCRUD } from '@/composables'
import { formatDate } from '@/utils'
import api from '@/api'
import TheIcon from '@/components/icon/TheIcon.vue'
defineOptions({ name: '开票记录' })
@ -30,50 +18,17 @@ const vPermission = resolveDirective('permission')
// /
const invoiceModalVisible = ref(false)
const invoiceModalMode = ref('invoice') // 'invoice' 'view'
const invoiceModalMode = ref('view') // 'send' 'view'
const currentInvoice = ref(null)
//
const statusOptions = [
{ label: '全部', value: '' },
{ label: '未开票', value: 'pending' },
{ label: '已开票', value: 'invoiced' },
{ label: '已退款', value: 'refunded' },
{ label: '已拒绝', value: 'rejected' },
{ label: '已退款', value: 'refunded' },
]
//
const invoiceTypeOptions = [
{ label: '增值税普通发票', value: 'normal' },
{ label: '增值税专用发票', value: 'special' },
]
//
const ticketTypeOptions = [
{ label: '纸质发票', value: 'paper' },
{ label: '电子发票', value: 'electronic' },
]
const {
modalVisible,
modalTitle,
modalAction,
modalLoading,
handleSave,
modalForm,
modalFormRef,
handleEdit,
handleDelete,
handleAdd,
} = useCRUD({
name: '开票记录',
initForm: {},
doCreate: api.createInvoice,
doUpdate: api.updateInvoice,
doDelete: api.deleteInvoice,
refresh: () => $table.value?.handleSearch(),
})
onMounted(() => {
$table.value?.handleSearch()
})
@ -83,10 +38,10 @@ const renderStatus = (status) => {
const statusMap = {
pending: { type: 'warning', text: '未开票' },
invoiced: { type: 'success', text: '已开票' },
refunded: { type: 'info', text: '已退款' },
rejected: { type: 'error', text: '已拒绝' },
refunded: { type: 'info', text: '已退款' },
}
const config = statusMap[status] || { type: 'default', text: '未知' }
const config = statusMap[status] || { type: 'default', text: '-' }
return h(NTag, { type: config.type }, { default: () => config.text })
}
@ -99,50 +54,73 @@ const renderInvoiceType = (type) => {
return typeMap[type] || type
}
//
const renderTicketType = (type) => {
const typeMap = {
paper: '纸质发票',
electronic: '电子发票',
}
return typeMap[type] || type
}
const columns = [
{
title: 'ID',
key: 'id',
width: 60,
align: 'center',
},
{
title: '提交时间',
key: 'created_at',
width: 100,
align: 'center',
render(row) {
return formatDate(row.created_at)
},
},
{
title: '供票类型',
key: 'ticket_type',
key: 'receiptId',
width: 80,
align: 'center',
render(row) {
return renderTicketType(row.ticket_type)
return row.receipt?.id || '-'
},
},
{
title: '提交时间',
key: 'submitted_at',
width: 140,
align: 'center',
render(row) {
return formatDate(row.submitted_at)
},
},
{
title: '付款凭证',
key: 'receipt',
width: 140,
align: 'center',
render(row) {
const list = Array.isArray(row.receipt) ? row.receipt : row.receipt ? [row.receipt] : []
const urls = list
.map((item) => (typeof item === 'string' ? item : item?.url))
.filter(Boolean)
if (!urls.length) return '-'
return h(
'div',
{ style: 'display:flex; gap:6px; justify-content:center;' },
urls.slice(0, 3).map((url, idx) =>
h(
'a',
{
href: url,
target: '_blank',
rel: 'noopener noreferrer',
key: `${url}-${idx}`,
style: 'display:inline-block',
},
{
default: () =>
h('img', {
src: url,
style:
'width:46px;height:46px;object-fit:cover;border-radius:4px;border:1px solid #e5e6eb;',
alt: '付款凭证',
}),
}
)
)
)
},
},
{
title: '手机号',
key: 'phone',
width: 100,
width: 110,
align: 'center',
ellipsis: { tooltip: true },
},
{
title: '邮箱号',
key: 'email',
title: '微信号',
key: 'wechat',
width: 120,
align: 'center',
ellipsis: { tooltip: true },
@ -164,35 +142,42 @@ const columns = [
{
title: '注册地址',
key: 'register_address',
width: 150,
width: 180,
align: 'center',
ellipsis: { tooltip: true },
},
{
title: '注册电话',
key: 'register_phone',
width: 100,
width: 120,
align: 'center',
ellipsis: { tooltip: true },
},
{
title: '开户银行',
key: 'bank_name',
width: 120,
width: 140,
align: 'center',
ellipsis: { tooltip: true },
},
{
title: '银行账号',
key: 'bank_account',
width: 150,
width: 180,
align: 'center',
ellipsis: { tooltip: true },
},
{
title: '接收邮箱',
key: 'email',
width: 180,
align: 'center',
ellipsis: { tooltip: true },
},
{
title: '开票类型',
key: 'invoice_type',
width: 120,
width: 130,
align: 'center',
render(row) {
return renderInvoiceType(row.invoice_type)
@ -201,7 +186,7 @@ const columns = [
{
title: '状态',
key: 'status',
width: 80,
width: 90,
align: 'center',
render(row) {
return renderStatus(row.status)
@ -210,159 +195,63 @@ const columns = [
{
title: '操作',
key: 'actions',
width: 180,
width: 80,
align: 'center',
fixed: 'right',
render(row) {
return [
// -
row.status === 'pending' &&
h(
NButton,
{
size: 'small',
type: 'success',
style: 'margin-right: 8px;',
onClick: () => handleInvoice(row),
},
{
default: () => '开票',
}
),
// 退 -
row.status === 'pending' &&
h(
NPopconfirm,
{
onPositiveClick: () => handleRefund(row),
},
{
trigger: () =>
h(
NButton,
{
size: 'small',
type: 'primary',
style: 'margin-right: 8px;',
},
{
default: () => '退款',
}
),
default: () => h('div', {}, '确认退款?'),
}
),
// -
row.status === 'invoiced' &&
h(
NButton,
{
size: 'small',
type: 'warning',
style: 'margin-right: 8px;',
onClick: () => handleView(row),
},
{
default: () => '查看',
}
),
]
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']]
)
},
},
]
//
async function handleUpdateStatus(row, status) {
//
async function handleInvoice(row) {
const editable = row.status === 'pending'
invoiceModalMode.value = editable ? 'send' : 'view'
invoiceModalVisible.value = true
await fetchDetail(row)
}
async function fetchDetail(row) {
try {
await api.updateInvoiceStatus({ id: row.id, status })
$message.success('开票成功')
$table.value?.handleSearch()
const id = row?.receipt?.id || row.id
const { data } = await api.getInvoiceById({ id })
currentInvoice.value = data || row
} catch (error) {
$message.error('开票失败: ' + error.message)
currentInvoice.value = row
$message.error(error?.message || '获取详情失败')
}
}
// 退
async function handleRefund(row) {
try {
// pending退refunded
if (row.status === 'pending') {
await api.updateInvoiceStatus({ id: row.id, status: 'refunded' })
$message.success('退款成功')
} else {
await api.refundInvoice({ id: row.id })
$message.success('退款成功')
}
$table.value?.handleSearch()
} catch (error) {
$message.error('退款失败: ' + error.message)
}
}
//
function handleInvoice(row) {
currentInvoice.value = row
invoiceModalMode.value = 'invoice'
invoiceModalVisible.value = true
}
//
function handleView(row) {
currentInvoice.value = row
invoiceModalMode.value = 'view'
invoiceModalVisible.value = true
}
// /
//
async function handleInvoiceConfirm(data) {
try {
if (invoiceModalMode.value === 'invoice') {
//
await api.sendInvoice(data)
await api.updateInvoiceStatus({ id: data.id, status: 'invoiced' })
$message.success('开票成功')
} else {
//
await api.sendInvoice(data)
$message.success('发送成功')
}
await api.sendInvoice({
email: data.email,
subject: data.subject,
body: data.body,
file_url: data.file_url || currentInvoice.value?.receipt?.url,
})
$message.success('发送成功')
invoiceModalVisible.value = false
$table.value?.handleSearch()
} catch (error) {
$message.error('操作失败: ' + error.message)
$message.error(error?.message || '操作失败')
}
}
const validateForm = {
company_name: [
{
required: true,
message: '请输入公司名称',
trigger: ['input', 'blur'],
},
],
tax_number: [
{
required: true,
message: '请输入公司税号',
trigger: ['input', 'blur'],
},
],
phone: [
{
required: true,
message: '请输入手机号',
trigger: ['input', 'blur'],
},
],
email: [
{
required: true,
message: '请输入邮箱',
trigger: ['input', 'blur'],
},
],
}
</script>
<template>
@ -372,12 +261,13 @@ const validateForm = {
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="queryItems.created_at"
v-model:value="queryItems.submitted_at"
type="daterange"
clearable
placeholder="请选择提交时间"
@ -428,70 +318,6 @@ const validateForm = {
</template>
</CrudTable>
<!-- 新增/编辑 弹窗 -->
<CrudModal
v-model:visible="modalVisible"
:title="modalTitle"
:loading="modalLoading"
@save="handleSave"
>
<NForm
ref="modalFormRef"
label-placement="left"
label-align="left"
:label-width="100"
:model="modalForm"
:rules="validateForm"
>
<NFormItem label="公司名称" path="company_name">
<NInput v-model:value="modalForm.company_name" clearable placeholder="请输入公司名称" />
</NFormItem>
<NFormItem label="公司税号" path="tax_number">
<NInput v-model:value="modalForm.tax_number" clearable placeholder="请输入公司税号" />
</NFormItem>
<NFormItem label="手机号" path="phone">
<NInput v-model:value="modalForm.phone" clearable placeholder="请输入手机号" />
</NFormItem>
<NFormItem label="邮箱" path="email">
<NInput v-model:value="modalForm.email" clearable placeholder="请输入邮箱" />
</NFormItem>
<NFormItem label="注册地址" path="register_address">
<NInput
v-model:value="modalForm.register_address"
clearable
placeholder="请输入注册地址"
/>
</NFormItem>
<NFormItem label="注册电话" path="register_phone">
<NInput
v-model:value="modalForm.register_phone"
clearable
placeholder="请输入注册电话"
/>
</NFormItem>
<NFormItem label="开户银行" path="bank_name">
<NInput v-model:value="modalForm.bank_name" clearable placeholder="请输入开户银行" />
</NFormItem>
<NFormItem label="银行账号" path="bank_account">
<NInput v-model:value="modalForm.bank_account" clearable placeholder="请输入银行账号" />
</NFormItem>
<NFormItem label="供票类型" path="ticket_type">
<NSelect
v-model:value="modalForm.ticket_type"
:options="ticketTypeOptions"
placeholder="请选择供票类型"
/>
</NFormItem>
<NFormItem label="开票类型" path="invoice_type">
<NSelect
v-model:value="modalForm.invoice_type"
:options="invoiceTypeOptions"
placeholder="请选择开票类型"
/>
</NFormItem>
</NForm>
</CrudModal>
<!-- 开票/查看弹窗 -->
<InvoiceModal
v-model:visible="invoiceModalVisible"

View File

@ -45,15 +45,9 @@ const invoiceColumns = [
]
const logColumns = [
{ title: 'ID', key: 'id', width: 80 },
{ title: '操作时间', key: 'operation_time', width: 180 },
{ title: '操作人', key: 'operator_name', width: 120 },
{ title: '操作类型', key: 'op_type', width: 120, ellipsis: { tooltip: true } },
{
title: '备注',
key: 'remark',
ellipsis: { tooltip: true },
render: (row) => row.remark || '-',
},
{ title: '操作记录', key: 'operation_detail', ellipsis: { tooltip: true } },
]
function handleClose() {

View File

@ -266,6 +266,16 @@ const validateForm = {
@keypress.enter="$table?.handleSearch()"
/>
</QueryBarItem>
<QueryBarItem label="ID" :label-width="60">
<NInput
v-model:value="queryItems.id"
clearable
type="text"
placeholder="请输入ID"
style="width: 200px"
@keypress.enter="$table?.handleSearch()"
/>
</QueryBarItem>
<QueryBarItem label="注册时间" :label-width="70">
<NDatePicker
v-model:value="queryItems.created_at"