fix: 发票
This commit is contained in:
1 parent
8f18d1e442
commit
7f4e2592b7
15 files changed
+4354
-3
No files matched your search
@@ -7,9 +7,9 @@
|
||||
# @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
###
|
||||
# 开发环境接口地址
|
||||
VITE_API_BASE_URL = 'http://localhost:18888'
|
||||
# VITE_API_BASE_URL = 'http://localhost:18888'
|
||||
# VITE_API_BASE_URL = 'http://111.113.214.109:18888'
|
||||
# VITE_API_BASE_URL = 'http://lm.cedar-v.com:18080'
|
||||
VITE_API_BASE_URL = 'http://lm.cedar-v.com:18080'
|
||||
|
||||
|
||||
# # 开发环境打包名称
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import Axios from './https'
|
||||
|
||||
// 发票详情接口类型定义
|
||||
export interface Invoice {
|
||||
id: string;
|
||||
invoiceNo: string;
|
||||
user: string;
|
||||
userType: string;
|
||||
userTypeLabel: string;
|
||||
orderNo: string;
|
||||
time: string;
|
||||
status: 'pending' | 'success' | 'rejected' | 'completed';
|
||||
statusLabel: string;
|
||||
amount: number;
|
||||
invoiceTitle: string;
|
||||
taxId?: string;
|
||||
content?: string;
|
||||
remark?: string;
|
||||
// 详情字段
|
||||
applyTime?: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
invoiceType?: string;
|
||||
rejectReason?: string;
|
||||
rejectTime?: string;
|
||||
rejectUser?: string;
|
||||
finishTime?: string;
|
||||
approveUser?: string;
|
||||
approveTime?: string;
|
||||
fileName?: string;
|
||||
fileSize?: string;
|
||||
}
|
||||
|
||||
// 发票查询请求参数
|
||||
export interface InvoiceQueryRequest {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
keyword?: string;
|
||||
status?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}
|
||||
|
||||
// 通用响应接口
|
||||
export interface ApiResponse<T = any> {
|
||||
code: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
// 分页数据响应接口
|
||||
export interface InvoiceQueryResponse {
|
||||
code: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
data: {
|
||||
list: Invoice[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
};
|
||||
}
|
||||
|
||||
// 发票统计数据接口响应
|
||||
export interface InvoiceSummaryResponse {
|
||||
code: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
data: {
|
||||
total: number;
|
||||
pending: number;
|
||||
completed: number;
|
||||
rejected: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询发票列表
|
||||
*/
|
||||
export function getInvoices(params?: InvoiceQueryRequest): Promise<InvoiceQueryResponse> {
|
||||
return Axios.get('/api/v1/invoices', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取发票统计数据
|
||||
*/
|
||||
export function getInvoiceSummary(): Promise<InvoiceSummaryResponse> {
|
||||
return Axios.get('/api/v1/invoices/summary')
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取发票详情
|
||||
*/
|
||||
export function getInvoiceDetail(id: string): Promise<ApiResponse<Invoice>> {
|
||||
return Axios.get(`/api/v1/invoices/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传发票文件
|
||||
*/
|
||||
export function uploadInvoice(id: string, data: any): Promise<ApiResponse> {
|
||||
return Axios.post(`/api/v1/invoices/${id}/upload`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 驳回发票申请
|
||||
*/
|
||||
export function rejectInvoice(id: string, data: any): Promise<ApiResponse> {
|
||||
return Axios.post(`/api/v1/invoices/${id}/reject`, data)
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { computed } from 'vue'
|
||||
import DashboardIcon from './svg/DashboardIcon.vue'
|
||||
import CustomersIcon from './svg/CustomersIcon.vue'
|
||||
import LicensesIcon from './svg/LicensesIcon.vue'
|
||||
import InvoicesIcon from './svg/InvoicesIcon.vue'
|
||||
import RolesIcon from './svg/RolesIcon.vue'
|
||||
import UsersIcon from './svg/UsersIcon.vue'
|
||||
|
||||
@@ -33,6 +34,7 @@ const iconMap = {
|
||||
'dashboard': DashboardIcon,
|
||||
'customers': CustomersIcon,
|
||||
'licenses': LicensesIcon,
|
||||
'invoices': InvoicesIcon,
|
||||
'roles': RolesIcon,
|
||||
'users': UsersIcon
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 2H4C2.9 2 2 2.9 2 4V16C2 17.1 2.9 18 4 18H16C17.1 18 18 17.1 18 16V4C18 2.9 17.1 2 16 2ZM16 16H4V4H16V16ZM14 10H6V12H14V10ZM14 6H6V8H14V6ZM10 14H6V16H10V14Z" fill="currentColor"/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -64,7 +64,8 @@ const { t } = useI18n()
|
||||
const defaultNavItems = computed(() => [
|
||||
{ id: "dashboard", label: t('navigation.menu.dashboard'), href: "/dashboard", icon: "dashboard" },
|
||||
{ id: "customers", label: t('navigation.menu.customers'), href: "/customers", icon: "customers" },
|
||||
{ id: "licenses", label: t('navigation.menu.licenses'), href: "/licenses", icon: "licenses" }
|
||||
{ id: "licenses", label: t('navigation.menu.licenses'), href: "/licenses", icon: "licenses" },
|
||||
{ id: "invoices", label: t('navigation.menu.invoices'), href: "/invoices", icon: "invoices" }
|
||||
// 暂时隐藏角色和系统用户菜单,后续版本再开启
|
||||
// { id: "roles", label: t('navigation.menu.roles'), href: "/roles", icon: "roles" },
|
||||
// { id: "users", label: t('navigation.menu.users'), href: "/users", icon: "users" }
|
||||
|
||||
@@ -76,6 +76,8 @@
|
||||
"dashboard": "Dashboard",
|
||||
"customers": "Customer Management",
|
||||
"licenses": "License Management",
|
||||
"invoices": "Invoice Management",
|
||||
"invoiceDetail": "Invoice Detail",
|
||||
"licenseList": "License List",
|
||||
"createLicense": "Create License",
|
||||
"viewLicense": "View License",
|
||||
|
||||
@@ -76,6 +76,8 @@
|
||||
"dashboard": "ダッシュボード",
|
||||
"customers": "顧客管理",
|
||||
"licenses": "ライセンス管理",
|
||||
"invoices": "発票管理",
|
||||
"invoiceDetail": "発票申請の詳細",
|
||||
"licenseList": "ライセンス一覧",
|
||||
"createLicense": "ライセンス作成",
|
||||
"viewLicense": "ライセンス表示",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"dashboard": "仪表盘",
|
||||
"customers": "客户管理",
|
||||
"licenses": "授权管理",
|
||||
"invoices": "发票管理",
|
||||
"roles": "角色权限",
|
||||
"users": "系统用户"
|
||||
},
|
||||
@@ -76,6 +77,8 @@
|
||||
"dashboard": "仪表盘",
|
||||
"customers": "客户管理",
|
||||
"licenses": "授权管理",
|
||||
"invoices": "发票管理",
|
||||
"invoiceDetail": "发票申请详情",
|
||||
"licenseList": "授权列表",
|
||||
"createLicense": "创建授权",
|
||||
"viewLicense": "查看授权",
|
||||
|
||||
@@ -61,6 +61,17 @@ const routes: Array<RouteRecordRaw> = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: "/invoices",
|
||||
component: () => import("@/views/Invoices/index.vue"),
|
||||
meta: { title: "发票管理", requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: "/invoices/detail/:id",
|
||||
name: "invoice-detail",
|
||||
component: () => import("@/views/Invoices/InvoiceDetail.vue"),
|
||||
meta: { title: "发票申请详情", requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: "/roles",
|
||||
component: () => import("@/views/Roles.vue"),
|
||||
|
||||
@@ -23,6 +23,8 @@ const routeBreadcrumbKeys: Record<string, string[]> = {
|
||||
'/dashboard': ['dashboard'],
|
||||
'/customers': ['customers'],
|
||||
'/licenses': ['licenses'],
|
||||
'/invoices': ['invoices'],
|
||||
'/invoices/detail': ['invoices', 'invoiceDetail'],
|
||||
'/roles': ['roles'],
|
||||
'/users': ['users'],
|
||||
'/login': ['login']
|
||||
|
||||
@@ -0,0 +1,548 @@
|
||||
<template>
|
||||
<Layout>
|
||||
<div class="invoice-detail-container" v-loading="loading">
|
||||
<div class="detail-header">
|
||||
<div class="header-left">
|
||||
<span class="back-link" @click="handleBack">发票管理</span>
|
||||
<span class="separator">/</span>
|
||||
<span class="current-page">发票申请详情</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button v-if="detailData.status === 'pending'" class="btn-upload" type="primary" @click="handleUpload">上传发票</el-button>
|
||||
<el-button v-if="detailData.status === 'pending'" class="btn-reject" type="danger" @click="handleReject">驳回申请</el-button>
|
||||
<el-button @click="handleBack">返回</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-content">
|
||||
<!-- 基本信息 -->
|
||||
<div class="detail-card">
|
||||
<div class="card-title">
|
||||
<span class="title-line"></span>
|
||||
<h3>基本信息</h3>
|
||||
</div>
|
||||
<el-descriptions :column="4" class="info-descriptions">
|
||||
<el-descriptions-item label="发票申请号">{{ detailData.invoiceNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="关联订单号">{{ detailData.orderNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="申请时间">{{ detailData.applyTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="开票状态">
|
||||
<span :class="['status-tag', detailData.status]">
|
||||
{{ detailData.statusLabel }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="用户信息">
|
||||
{{ detailData.user }} <span class="user-type">({{ detailData.userTypeLabel }})</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">{{ detailData.phone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="开票金额">
|
||||
<span class="amount-value">¥{{ (detailData.amount || 0).toLocaleString(undefined, { minimumFractionDigits: 2 }) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="收票邮箱">{{ detailData.email }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 开票信息 -->
|
||||
<div class="detail-card">
|
||||
<div class="card-title">
|
||||
<span class="title-line"></span>
|
||||
<h3>开票信息</h3>
|
||||
</div>
|
||||
<el-descriptions :column="4" class="info-descriptions">
|
||||
<el-descriptions-item label="发票类型">{{ detailData.invoiceType }}</el-descriptions-item>
|
||||
<el-descriptions-item label="发票抬头">{{ detailData.invoiceTitle }}</el-descriptions-item>
|
||||
<el-descriptions-item label="纳税人识别号">{{ detailData.taxId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="开票内容">{{ detailData.content }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注信息" :span="4">{{ detailData.remark }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 驳回信息 (仅在状态为驳回时显示) -->
|
||||
<div class="detail-card reject-info-card" v-if="detailData.status === 'rejected'">
|
||||
<div class="card-title">
|
||||
<span class="title-line"></span>
|
||||
<h3>驳回信息</h3>
|
||||
</div>
|
||||
<div class="info-content">
|
||||
<div class="info-row">
|
||||
<span class="label">驳回原因:</span>
|
||||
<span class="value">{{ detailData.rejectReason }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">驳回时间:</span>
|
||||
<span class="value">{{ detailData.rejectTime }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">驳回人员:</span>
|
||||
<span class="value">{{ detailData.rejectUser }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 通过信息 (仅在状态为已开票时显示) -->
|
||||
<div class="detail-card approve-info-card" v-if="detailData.status === 'completed'">
|
||||
<div class="card-title">
|
||||
<span class="title-line"></span>
|
||||
<h3>通过信息</h3>
|
||||
</div>
|
||||
<div class="info-content">
|
||||
<div class="info-row">
|
||||
<span class="label">开票完成时间:</span>
|
||||
<span class="value">{{ detailData.finishTime }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">上传人员:</span>
|
||||
<span class="value">{{ detailData.approveUser }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">发票文件:</span>
|
||||
<span class="value">
|
||||
<el-link type="primary" :underline="false">{{ detailData.fileName }} ({{ detailData.fileSize }})</el-link>
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">上传时间:</span>
|
||||
<span class="value">{{ detailData.approveTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传发票对话框 -->
|
||||
<UploadInvoiceDialog
|
||||
v-model="uploadVisible"
|
||||
:invoice-data="detailData"
|
||||
@submit="handleUploadSubmit"
|
||||
/>
|
||||
|
||||
<!-- 驳回申请对话框 -->
|
||||
<RejectInvoiceDialog
|
||||
v-model="rejectVisible"
|
||||
:invoice-data="detailData"
|
||||
@submit="handleRejectSubmit"
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import Layout from '@/components/common/layout/Layout.vue'
|
||||
import UploadInvoiceDialog from './components/UploadInvoiceDialog.vue'
|
||||
import RejectInvoiceDialog from './components/RejectInvoiceDialog.vue'
|
||||
import { getInvoiceDetail, uploadInvoice, rejectInvoice, type Invoice } from '@/api/invoice'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const uploadVisible = ref(false)
|
||||
const rejectVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
const detailData = ref<Partial<Invoice>>({
|
||||
invoiceNo: '',
|
||||
orderNo: '',
|
||||
applyTime: '',
|
||||
status: 'pending',
|
||||
statusLabel: '待处理',
|
||||
user: '',
|
||||
userType: '',
|
||||
userTypeLabel: '',
|
||||
phone: '',
|
||||
amount: 0,
|
||||
email: '',
|
||||
invoiceType: '',
|
||||
invoiceTitle: '',
|
||||
taxId: '',
|
||||
content: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const fetchDetail = async () => {
|
||||
const id = route.params.id as string
|
||||
if (!id) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getInvoiceDetail(id)
|
||||
if (res.code === '000000' && res.data) {
|
||||
detailData.value = res.data
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Fetch invoice detail error:', error)
|
||||
ElMessage.error(error.backendMessage || '获取详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDetail()
|
||||
})
|
||||
|
||||
const handleBack = () => {
|
||||
router.push('/invoices')
|
||||
}
|
||||
|
||||
const handleUpload = () => {
|
||||
uploadVisible.value = true
|
||||
}
|
||||
|
||||
const handleUploadSubmit = async (data: any) => {
|
||||
try {
|
||||
const res = await uploadInvoice(detailData.value.id!, data)
|
||||
if (res.code === '000000') {
|
||||
ElMessage.success('上传成功')
|
||||
fetchDetail()
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.backendMessage || '上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleReject = () => {
|
||||
rejectVisible.value = true
|
||||
}
|
||||
|
||||
const handleRejectSubmit = async (data: any) => {
|
||||
try {
|
||||
const res = await rejectInvoice(detailData.value.id!, data)
|
||||
if (res.code === '000000') {
|
||||
ElMessage.success('已驳回')
|
||||
fetchDetail()
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.backendMessage || '驳回失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.invoice-detail-container {
|
||||
padding: 24px;
|
||||
background-color: #F0F2F5;
|
||||
min-height: calc(100vh - 80px);
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
background: #fff;
|
||||
padding: 16px 24px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
|
||||
.back-link {
|
||||
color: #8C8C8C;
|
||||
cursor: pointer;
|
||||
&:hover { color: #019C7C; }
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: 0 8px;
|
||||
color: #BFBFBF;
|
||||
}
|
||||
|
||||
.current-page {
|
||||
color: #262626;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
.btn-upload {
|
||||
background-color: #019C7C;
|
||||
border-color: #019C7C;
|
||||
&:hover { background-color: #017c63; border-color: #017c63; }
|
||||
}
|
||||
|
||||
.btn-reject {
|
||||
background-color: #F5222D;
|
||||
border-color: #F5222D;
|
||||
&:hover { background-color: #d32029; border-color: #d32029; }
|
||||
}
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
|
||||
.card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.title-line {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: #019C7C;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
.info-content {
|
||||
.info-row {
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
display: flex;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #595959;
|
||||
width: 120px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.info-descriptions) {
|
||||
.el-descriptions__label {
|
||||
width: 120px;
|
||||
color: #595959;
|
||||
font-weight: normal;
|
||||
}
|
||||
.el-descriptions__content {
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
padding: 2px 8px;
|
||||
border-radius: 2px;
|
||||
font-size: 12px;
|
||||
&.pending { background: #FFF7E6; color: #FAAD14; border: 1px solid #FFE7BA; }
|
||||
&.completed { background: #F6FFED; color: #52C41A; border: 1px solid #B7EB8F; }
|
||||
&.rejected { background: #FFF1F0; color: #F5222D; border: 1px solid #FFA39E; }
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-type {
|
||||
color: #8C8C8C;
|
||||
}
|
||||
|
||||
// 弹窗通用基础样式
|
||||
@mixin dialog-style-base {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
|
||||
.el-dialog__header {
|
||||
background-color: #019C7C;
|
||||
margin: 0;
|
||||
padding: 16px 24px;
|
||||
|
||||
.el-dialog__title {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.el-dialog__close {
|
||||
color: #fff;
|
||||
&:hover { color: rgba(255, 255, 255, 0.8); }
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
padding: 16px 24px 24px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
// 上传弹窗样式还原
|
||||
:deep(.upload-invoice-dialog) {
|
||||
@include dialog-style-base;
|
||||
}
|
||||
|
||||
// 驳回弹窗样式
|
||||
:deep(.reject-invoice-dialog) {
|
||||
@include dialog-style-base;
|
||||
|
||||
.el-form-item__label {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.reject-dialog-content {
|
||||
.reject-alert {
|
||||
background-color: #FFFBE6;
|
||||
border: 1px solid #FFE58F;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.alert-icon {
|
||||
color: #FAAD14;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.alert-text {
|
||||
color: #262626;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-summary {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
background: #FAFAFA;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #F0F0F0;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.summary-item {
|
||||
font-size: 14px;
|
||||
.label { color: #8C8C8C; }
|
||||
.value { color: #262626; }
|
||||
&.full-width {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.section-label {
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
font-weight: 500;
|
||||
&.required::before {
|
||||
content: '*';
|
||||
color: #F5222D;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.label-tip {
|
||||
font-size: 12px;
|
||||
color: #BFBFBF;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-dialog-content {
|
||||
.info-summary {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
background: #FAFAFA;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #F0F0F0;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.summary-item {
|
||||
font-size: 14px;
|
||||
.label { color: #8C8C8C; }
|
||||
.value { color: #262626; }
|
||||
}
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.invoice-uploader {
|
||||
:deep(.el-upload-dragger) {
|
||||
border: 1px dashed #D9D9D9;
|
||||
padding: 30px;
|
||||
&:hover { border-color: #019C7C; }
|
||||
}
|
||||
|
||||
.upload-link {
|
||||
color: #019C7C;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.el-upload__tip {
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
color: #BFBFBF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
|
||||
.btn-submit {
|
||||
background-color: #019C7C;
|
||||
border-color: #019C7C;
|
||||
padding: 8px 32px;
|
||||
&:hover { background-color: #017c63; border-color: #017c63; }
|
||||
}
|
||||
|
||||
.el-button {
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="驳回发票申请"
|
||||
width="640px"
|
||||
class="reject-invoice-dialog"
|
||||
:show-close="true"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="reject-dialog-content">
|
||||
<div class="reject-alert">
|
||||
<el-icon class="alert-icon"><warning-filled /></el-icon>
|
||||
<span class="alert-text">驳回后,用户将收到通知并需要重新提交申请。</span>
|
||||
</div>
|
||||
|
||||
<div class="info-summary" v-if="invoiceData">
|
||||
<div class="summary-item">
|
||||
<span class="label">发票申请号:</span>
|
||||
<span class="value">{{ invoiceData.invoiceNo }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">用户信息:</span>
|
||||
<span class="value">{{ invoiceData.user }}</span>
|
||||
</div>
|
||||
<div class="summary-item full-width">
|
||||
<span class="label">发票抬头:</span>
|
||||
<span class="value">{{ invoiceData.invoiceTitle }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form :model="form" ref="formRef" label-position="top">
|
||||
<div class="label-row">
|
||||
<span class="section-label required">驳回原因</span>
|
||||
<span class="label-tip">请选择或输入驳回的具体原因</span>
|
||||
</div>
|
||||
<el-form-item prop="reason" :rules="[{ required: true, message: '请选择驳回原因', trigger: 'change' }]">
|
||||
<el-select v-model="form.reason" placeholder="请选择驳回原因" style="width: 100%">
|
||||
<el-option label="发票抬头信息有误" value="title_error" />
|
||||
<el-option label="纳税人识别号不正确" value="tax_id_error" />
|
||||
<el-option label="附件不清晰或有误" value="attachment_error" />
|
||||
<el-option label="其他原因" value="other" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<div class="label-row">
|
||||
<span class="section-label">详细说明</span>
|
||||
</div>
|
||||
<el-form-item prop="remark">
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入详细的驳回说明,帮助用户更准确地修改信息"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="danger" @click="handleSubmit">确认驳回</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { WarningFilled } from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
invoiceData: any
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'submit'])
|
||||
|
||||
const visible = ref(props.modelValue)
|
||||
const formRef = ref()
|
||||
const form = reactive({
|
||||
reason: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
})
|
||||
|
||||
watch(visible, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
ElMessage.success('发票申请已驳回')
|
||||
emit('submit', { ...form })
|
||||
visible.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@mixin dialog-style-base {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
|
||||
:deep(.el-dialog__header) {
|
||||
background-color: #019C7C;
|
||||
margin: 0;
|
||||
padding: 16px 24px;
|
||||
|
||||
.el-dialog__title {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.el-dialog__close {
|
||||
color: #fff;
|
||||
&:hover { color: rgba(255, 255, 255, 0.8); }
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__footer) {
|
||||
padding: 16px 24px 24px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.reject-invoice-dialog {
|
||||
@include dialog-style-base;
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.reject-dialog-content {
|
||||
.reject-alert {
|
||||
background-color: #FFFBE6;
|
||||
border: 1px solid #FFE58F;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.alert-icon {
|
||||
color: #FAAD14;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.alert-text {
|
||||
color: #262626;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-summary {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
background: #FAFAFA;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #F0F0F0;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.summary-item {
|
||||
font-size: 14px;
|
||||
.label { color: #8C8C8C; }
|
||||
.value { color: #262626; }
|
||||
&.full-width {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.section-label {
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
font-weight: 500;
|
||||
&.required::before {
|
||||
content: '*';
|
||||
color: #F5222D;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.label-tip {
|
||||
font-size: 12px;
|
||||
color: #BFBFBF;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
|
||||
.el-button {
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="上传发票文件"
|
||||
width="680px"
|
||||
class="upload-invoice-dialog"
|
||||
:show-close="true"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="upload-dialog-content">
|
||||
<!-- 信息摘要 -->
|
||||
<div class="info-summary" v-if="invoiceData">
|
||||
<div class="summary-item">
|
||||
<span class="label">发票申请号:</span>
|
||||
<span class="value">{{ invoiceData.invoiceNo }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">关联订单号:</span>
|
||||
<span class="value">{{ invoiceData.orderNo }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">用户信息:</span>
|
||||
<span class="value">{{ invoiceData.user }} ({{ invoiceData.userTypeLabel || '个人版' }})</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">发票抬头:</span>
|
||||
<span class="value">{{ invoiceData.invoiceTitle }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">开票金额:</span>
|
||||
<span class="value">¥{{ (invoiceData.amount || 0).toLocaleString(undefined, { minimumFractionDigits: 2 }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传区域 -->
|
||||
<div class="upload-section">
|
||||
<p class="section-label">上传发票文件</p>
|
||||
<el-upload
|
||||
class="invoice-uploader"
|
||||
drag
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
v-model:file-list="form.fileList"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
<span class="upload-link">点击上传</span> / 拖拽到此区域
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持PDF格式。最大文件大小10MB
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</div>
|
||||
|
||||
<!-- 表单项 -->
|
||||
<div class="form-section">
|
||||
<div class="form-item">
|
||||
<p class="section-label">开票完成时间</p>
|
||||
<el-date-picker
|
||||
v-model="form.finishTime"
|
||||
type="datetime"
|
||||
placeholder="选择日期时间"
|
||||
format="YYYY/MM/DD HH:mm"
|
||||
value-format="YYYY/MM/DD HH:mm"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<p class="section-label">备注 (可选)</p>
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="输入备注信息"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" class="btn-submit" @click="handleSubmit">确认上传</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
invoiceData: any
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'submit'])
|
||||
|
||||
const visible = ref(props.modelValue)
|
||||
const form = reactive({
|
||||
finishTime: '2026/01/14 06:20',
|
||||
remark: '',
|
||||
fileList: []
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
})
|
||||
|
||||
watch(visible, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
// Reset form if needed
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
ElMessage.success('发票上传成功')
|
||||
emit('submit', { ...form })
|
||||
visible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@mixin dialog-style-base {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
|
||||
:deep(.el-dialog__header) {
|
||||
background-color: #019C7C;
|
||||
margin: 0;
|
||||
padding: 16px 24px;
|
||||
|
||||
.el-dialog__title {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.el-dialog__close {
|
||||
color: #fff;
|
||||
&:hover { color: rgba(255, 255, 255, 0.8); }
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__footer) {
|
||||
padding: 16px 24px 24px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-invoice-dialog {
|
||||
@include dialog-style-base;
|
||||
}
|
||||
|
||||
.upload-dialog-content {
|
||||
.info-summary {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
background: #FAFAFA;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #F0F0F0;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.summary-item {
|
||||
font-size: 14px;
|
||||
.label { color: #8C8C8C; }
|
||||
.value { color: #262626; }
|
||||
}
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.invoice-uploader {
|
||||
:deep(.el-upload-dragger) {
|
||||
border: 1px dashed #D9D9D9;
|
||||
padding: 30px;
|
||||
&:hover { border-color: #019C7C; }
|
||||
}
|
||||
|
||||
.upload-link {
|
||||
color: #019C7C;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.el-upload__tip {
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
color: #BFBFBF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
|
||||
.btn-submit {
|
||||
background-color: #019C7C;
|
||||
border-color: #019C7C;
|
||||
padding: 8px 32px;
|
||||
&:hover { background-color: #017c63; border-color: #017c63; }
|
||||
}
|
||||
|
||||
.el-button {
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,526 @@
|
||||
<template>
|
||||
<Layout :page-title="t('navigation.menu.invoices')">
|
||||
<div class="invoice-container">
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-row">
|
||||
<div v-for="stat in stats" :key="stat.label" class="stat-card" :class="stat.type">
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
<div class="stat-value">{{ stat.value }}</div>
|
||||
</div>
|
||||
<div class="stat-icon-wrapper">
|
||||
<el-icon v-if="stat.icon === 'document'"><Document /></el-icon>
|
||||
<el-icon v-else-if="stat.icon === 'warning'"><Warning /></el-icon>
|
||||
<el-icon v-else-if="stat.icon === 'success'"><CircleCheck /></el-icon>
|
||||
<el-icon v-else-if="stat.icon === 'error'"><CircleClose /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="filter-section">
|
||||
<el-form :inline="true" :model="filterForm" class="filter-form">
|
||||
<el-form-item label="搜索发票号/订单号:">
|
||||
<el-input v-model="filterForm.keyword" placeholder="输入发票号或订单号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开票状态:">
|
||||
<el-select v-model="filterForm.status" placeholder="全部状态" style="width: 150px">
|
||||
<el-option label="全部状态" value="" />
|
||||
<el-option label="待处理" value="pending" />
|
||||
<el-option label="已开票" value="success" />
|
||||
<el-option label="已驳回" value="rejected" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div class="filter-right">
|
||||
<div class="selected-count">
|
||||
<span class="count-badge">{{ totalCount }}</span>
|
||||
</div>
|
||||
<el-form-item label="申请时间:">
|
||||
|
||||
<el-date-picker
|
||||
v-model="filterForm.dateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
format="YYYY年MM月DD日"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="resetFilter">重置</el-button>
|
||||
<el-button type="primary" class="btn-filter" @click="handleFilter">筛选</el-button>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<div class="table-section" v-loading="loading">
|
||||
<div class="table-header-title">
|
||||
<span class="title-line"></span>
|
||||
<h3>发票申请列表</h3>
|
||||
<el-button type="primary" class="btn-create" @click="handleCreate" style="margin-left: auto">新增发票申请</el-button>
|
||||
</div>
|
||||
<el-table :data="tableData" stripe style="width: 100%" :header-cell-style="{ background: '#E6F7FF', color: '#000', borderRight: '1px solid #BAE7FF' }">
|
||||
<el-table-column prop="invoiceNo" label="发票申请号" min-width="150" />
|
||||
<el-table-column label="用户" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<div class="user-info">
|
||||
<span class="user-name">{{ row.user }}</span>
|
||||
<span class="user-type" :class="row.userType">{{ row.userTypeLabel }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orderNo" label="订单号" min-width="150" />
|
||||
<el-table-column prop="time" label="申请时间" min-width="180" />
|
||||
<el-table-column label="开票状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<span class="status-text" :class="row.status">{{ row.statusLabel }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="发票金额" width="120">
|
||||
<template #default="{ row }">
|
||||
¥{{ (row.amount || 0).toLocaleString(undefined, { minimumFractionDigits: 2 }) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" style="color: #019C7C" @click="handleView(row)">查看</el-button>
|
||||
<template v-if="row.status === 'pending'">
|
||||
<el-button link type="success" style="color: #52C41A" @click="handleUpload(row)">上传</el-button>
|
||||
<el-button link type="danger" style="color: #F5222D" @click="handleReject(row)">驳回</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="prev, pager, next, jumper, sizes, total"
|
||||
:total="totalCount"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
</el-pagination>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传发票对话框 -->
|
||||
<UploadInvoiceDialog
|
||||
v-model="uploadVisible"
|
||||
:invoice-data="currentRow"
|
||||
@submit="handleUploadSubmit"
|
||||
/>
|
||||
|
||||
<!-- 驳回申请对话框 -->
|
||||
<RejectInvoiceDialog
|
||||
v-model="rejectVisible"
|
||||
:invoice-data="currentRow"
|
||||
@submit="handleRejectSubmit"
|
||||
/>
|
||||
|
||||
<!-- 新增申请对话框 -->
|
||||
<el-dialog v-model="createVisible" title="新增发票申请" width="500px">
|
||||
<el-form :model="createForm" label-width="100px">
|
||||
<el-form-item label="订单号">
|
||||
<el-input v-model="createForm.orderNo" placeholder="请输入订单号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开票金额">
|
||||
<el-input-number v-model="createForm.amount" :precision="2" :step="100" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="抬头类型">
|
||||
<el-radio-group v-model="createForm.userType">
|
||||
<el-radio label="personal">个人</el-radio>
|
||||
<el-radio label="enterprise">企业</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="createVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitCreate">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Layout from '@/components/common/layout/Layout.vue'
|
||||
import { Document, Warning, CircleCheck, CircleClose } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import UploadInvoiceDialog from './components/UploadInvoiceDialog.vue'
|
||||
import RejectInvoiceDialog from './components/RejectInvoiceDialog.vue'
|
||||
import { getInvoices, getInvoiceSummary, type Invoice } from '@/api/invoice'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
const stats = ref([
|
||||
{ label: '全部发票申请', value: '0', type: 'all', icon: 'document', key: 'total' },
|
||||
{ label: '待处理申请', value: '0', type: 'pending', icon: 'warning', key: 'pending' },
|
||||
{ label: '已开票', value: '0', type: 'success', icon: 'success', key: 'completed' },
|
||||
{ label: '已驳回', value: '0', type: 'rejected', icon: 'error', key: 'rejected' }
|
||||
])
|
||||
|
||||
const filterForm = reactive({
|
||||
keyword: '',
|
||||
status: '',
|
||||
dateRange: [] as any[]
|
||||
})
|
||||
|
||||
const tableData = ref<Invoice[]>([])
|
||||
const loading = ref(false)
|
||||
const totalCount = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const createVisible = ref(false)
|
||||
const uploadVisible = ref(false)
|
||||
const rejectVisible = ref(false)
|
||||
const currentRow = ref<any>(null)
|
||||
|
||||
const fetchSummary = async () => {
|
||||
try {
|
||||
const res = await getInvoiceSummary()
|
||||
if (res.code === '000000' && res.data) {
|
||||
stats.value.forEach(stat => {
|
||||
if (stat.key && (res.data as any)[stat.key] !== undefined) {
|
||||
stat.value = (res.data as any)[stat.key].toString()
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fetch summary error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
page: currentPage.value,
|
||||
page_size: pageSize.value,
|
||||
keyword: filterForm.keyword,
|
||||
status: filterForm.status,
|
||||
}
|
||||
if (filterForm.dateRange && filterForm.dateRange.length === 2) {
|
||||
params.start_date = filterForm.dateRange[0]
|
||||
params.end_date = filterForm.dateRange[1]
|
||||
}
|
||||
const res = await getInvoices(params)
|
||||
if (res.code === '000000') {
|
||||
tableData.value = res.data.list
|
||||
totalCount.value = res.data.total
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Fetch invoices error:', error)
|
||||
ElMessage.error(error.backendMessage || '获取数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
fetchSummary()
|
||||
})
|
||||
|
||||
|
||||
const createForm = reactive({
|
||||
orderNo: '',
|
||||
amount: 0,
|
||||
userType: 'personal'
|
||||
})
|
||||
|
||||
const handleCreate = () => {
|
||||
createVisible.value = true
|
||||
}
|
||||
|
||||
const submitCreate = () => {
|
||||
ElMessage.success('提交申请成功')
|
||||
createVisible.value = false
|
||||
}
|
||||
|
||||
const handleFilter = () => {
|
||||
currentPage.value = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const resetFilter = () => {
|
||||
filterForm.keyword = ''
|
||||
filterForm.status = ''
|
||||
filterForm.dateRange = []
|
||||
currentPage.value = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleView = (row: any) => {
|
||||
router.push(`/invoices/detail/${row.id || row.invoiceNo}`)
|
||||
}
|
||||
|
||||
const handleUpload = (row: any) => {
|
||||
currentRow.value = row
|
||||
uploadVisible.value = true
|
||||
}
|
||||
|
||||
const handleUploadSubmit = (data: any) => {
|
||||
console.log('Upload Submit:', data)
|
||||
fetchData()
|
||||
fetchSummary()
|
||||
}
|
||||
|
||||
const handleReject = (row: any) => {
|
||||
currentRow.value = row
|
||||
rejectVisible.value = true
|
||||
}
|
||||
|
||||
const handleRejectSubmit = (data: any) => {
|
||||
console.log('Reject Submit:', data)
|
||||
fetchData()
|
||||
fetchSummary()
|
||||
}
|
||||
|
||||
|
||||
const handleSizeChange = (val: number) => {
|
||||
pageSize.value = val
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val: number) => {
|
||||
currentPage.value = val
|
||||
fetchData()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.invoice-container {
|
||||
padding: 24px;
|
||||
background-color: #F0F2F5;
|
||||
min-height: calc(100vh - 80px);
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 20px 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
border: 1px solid transparent;
|
||||
height: 96px;
|
||||
|
||||
&.all { border: 2px solid #1890FF; }
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #8C8C8C;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #019C7C;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-icon-wrapper {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
&.all .stat-icon-wrapper { background: #E6F7FF; color: #1890FF; }
|
||||
&.pending .stat-icon-wrapper { background: #FFF7E6; color: #FAAD14; }
|
||||
&.success .stat-icon-wrapper { background: #F6FFED; color: #52C41A; }
|
||||
&.rejected .stat-icon-wrapper { background: #F9F0FF; color: #722ED1; }
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
background: #fff;
|
||||
padding: 16px 24px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #F0F0F0;
|
||||
|
||||
.filter-form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 24px;
|
||||
|
||||
.el-form-item__label {
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-right: 12px;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selected-count {
|
||||
margin-right: 24px;
|
||||
.count-badge {
|
||||
background: #FF4D4F;
|
||||
color: #fff;
|
||||
padding: 0 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-filter {
|
||||
background-color: #019C7C;
|
||||
border-color: #019C7C;
|
||||
padding: 8px 24px;
|
||||
&:hover {
|
||||
background-color: #017c63;
|
||||
border-color: #017c63;
|
||||
}
|
||||
}
|
||||
|
||||
.table-section {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #F0F0F0;
|
||||
}
|
||||
|
||||
.table-header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.title-line {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: #019C7C;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.user-name {
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.user-type {
|
||||
font-size: 12px;
|
||||
padding: 0 8px;
|
||||
border-radius: 2px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
&.personal { background: #E6F7FF; color: #1890FF; }
|
||||
&.enterprise { background: #F6FFED; color: #52C41A; }
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
&.success { color: #1890FF; }
|
||||
&.pending { color: #FAAD14; }
|
||||
&.rejected { color: #BFBFBF; }
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
:deep(.el-pagination) {
|
||||
.el-pager li.is-active {
|
||||
background-color: #019C7C;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
--el-table-header-bg-color: #E6F7FF;
|
||||
border: 1px solid #BAE7FF;
|
||||
|
||||
.el-table__header th {
|
||||
color: #262626;
|
||||
height: 54px;
|
||||
border-right: 1px solid #BAE7FF;
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table__body td {
|
||||
height: 64px;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.el-table__row--striped {
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
.stats-row {
|
||||
gap: 16px;
|
||||
}
|
||||
.stat-card {
|
||||
padding: 16px;
|
||||
.stat-value { font-size: 24px; }
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.stats-row {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+2665
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user