feat: 发票管理
This commit is contained in:
1 parent
fa056ae272
commit
2032950bb7
18 files changed
+2300
-325
No files matched your search
+38
-32
@@ -3,32 +3,32 @@ 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;
|
||||
invoice_no: string;
|
||||
applicant_name: string;
|
||||
applicant_phone: string;
|
||||
receiver_email: string;
|
||||
order_no: string;
|
||||
order_id: string;
|
||||
order_package_name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
status: 'pending' | 'completed' | 'rejected';
|
||||
status_display: string;
|
||||
amount: number;
|
||||
invoiceTitle: string;
|
||||
taxId?: string;
|
||||
title: string;
|
||||
taxpayer_id?: 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;
|
||||
invoice_type: string;
|
||||
invoice_type_display: string;
|
||||
// 审核/上传相关
|
||||
reject_reason?: string;
|
||||
rejected_at?: string;
|
||||
rejected_by?: string;
|
||||
uploaded_at?: string;
|
||||
uploaded_by?: string;
|
||||
uploader_name?: string;
|
||||
invoice_file_url?: string;
|
||||
}
|
||||
|
||||
// 发票查询请求参数
|
||||
@@ -55,27 +55,29 @@ export interface InvoiceQueryResponse {
|
||||
message: string;
|
||||
timestamp: string;
|
||||
data: {
|
||||
list: Invoice[];
|
||||
total: number;
|
||||
invoices: Invoice[];
|
||||
total_count: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
total_pages?: number;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// 发票统计数据接口响应
|
||||
export interface InvoiceSummaryResponse {
|
||||
code: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
data: {
|
||||
total: number;
|
||||
pending: number;
|
||||
completed: number;
|
||||
rejected: number;
|
||||
total_count: number;
|
||||
pending_count: number;
|
||||
completed_count: number;
|
||||
rejected_count: number;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 查询发票列表
|
||||
*/
|
||||
@@ -101,8 +103,12 @@ export function getInvoiceDetail(id: string): Promise<ApiResponse<Invoice>> {
|
||||
/**
|
||||
* 上传发票文件
|
||||
*/
|
||||
export function uploadInvoice(id: string, data: any): Promise<ApiResponse> {
|
||||
return Axios.post(`/api/v1/invoices/${id}/upload`, data)
|
||||
export function uploadInvoice(data: FormData): Promise<ApiResponse> {
|
||||
return Axios.post('/api/v1/invoices/upload', data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@ 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 EnterpriseLeadsIcon from './svg/EnterpriseLeadsIcon.vue'
|
||||
import RolesIcon from './svg/RolesIcon.vue'
|
||||
import UsersIcon from './svg/UsersIcon.vue'
|
||||
|
||||
@@ -35,6 +36,7 @@ const iconMap = {
|
||||
'customers': CustomersIcon,
|
||||
'licenses': LicensesIcon,
|
||||
'invoices': InvoicesIcon,
|
||||
'enterprise-leads': EnterpriseLeadsIcon,
|
||||
'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="M3 3.5C3 2.67157 3.67157 2 4.5 2H11.5C12.3284 2 13 2.67157 13 3.5V6H15.5C16.3284 6 17 6.67157 17 7.5V16.5C17 17.3284 16.3284 18 15.5 18H4.5C3.67157 18 3 17.3284 3 16.5V3.5ZM4.5 3.5V16.5H15.5V7.5H12V3.5H4.5ZM6.5 6H9.5C9.77614 6 10 6.22386 10 6.5C10 6.77614 9.77614 7 9.5 7H6.5C6.22386 7 6 6.77614 6 6.5C6 6.22386 6.22386 6 6.5 6ZM6.5 9H11.5C11.7761 9 12 9.22386 12 9.5C12 9.77614 11.7761 10 11.5 10H6.5C6.22386 10 6 9.77614 6 9.5C6 9.22386 6.22386 9 6.5 9ZM6.5 12H11.5C11.7761 12 12 12.2239 12 12.5C12 12.7761 11.7761 13 11.5 13H6.5C6.22386 13 6 12.7761 6 12.5C6 12.2239 6.22386 12 6.5 12Z" />
|
||||
</svg>
|
||||
</template>
|
||||
@@ -64,8 +64,10 @@ 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: "enterprise-leads", label: t('navigation.menu.enterpriseLeads'), href: "/enterprise-leads", icon: "enterprise-leads" },
|
||||
{ id: "licenses", label: t('navigation.menu.licenses'), href: "/licenses", icon: "licenses" },
|
||||
{ id: "invoices", label: t('navigation.menu.invoices'), href: "/invoices", icon: "invoices" }
|
||||
{ id: "invoices", label: t('navigation.menu.invoices'), href: "/invoices", icon: "invoices" },
|
||||
{ id: "packages", label: t('navigation.menu.packages'), href: "/packages", icon: "packages" }
|
||||
// 暂时隐藏角色和系统用户菜单,后续版本再开启
|
||||
// { id: "roles", label: t('navigation.menu.roles'), href: "/roles", icon: "roles" },
|
||||
// { id: "users", label: t('navigation.menu.users'), href: "/users", icon: "users" }
|
||||
|
||||
@@ -54,7 +54,9 @@
|
||||
"menu": {
|
||||
"dashboard": "Dashboard",
|
||||
"customers": "Customer Management",
|
||||
"enterpriseLeads": "Enterprise Leads",
|
||||
"licenses": "License Management",
|
||||
"invoices": "Invoice Management",
|
||||
"roles": "Roles & Permissions",
|
||||
"users": "System Users"
|
||||
},
|
||||
@@ -75,10 +77,11 @@
|
||||
"breadcrumb": {
|
||||
"dashboard": "Dashboard",
|
||||
"customers": "Customer Management",
|
||||
"licenses": "License Management",
|
||||
"invoices": "Invoice Management",
|
||||
"invoiceDetail": "Invoice Detail",
|
||||
"licenseList": "License List",
|
||||
"enterpriseLeads": "Enterprise Leads",
|
||||
"licenses": "License Management",
|
||||
"invoices": "Invoice Management",
|
||||
"invoiceDetail": "Invoice Detail",
|
||||
"licenseList": "License List",
|
||||
"createLicense": "Create License",
|
||||
"viewLicense": "View License",
|
||||
"roles": "Roles & Permissions",
|
||||
@@ -90,6 +93,143 @@
|
||||
"unknown": "Unknown Page"
|
||||
}
|
||||
},
|
||||
|
||||
"enterpriseLeads": {
|
||||
"title": "Enterprise Leads",
|
||||
"stats": {
|
||||
"total": "Total Leads",
|
||||
"pending": "Pending",
|
||||
"contacting": "Contacted",
|
||||
"completed": "Converted",
|
||||
"rejected": "Rejected"
|
||||
},
|
||||
"filter": {
|
||||
"searchPlaceholder": "Search by keywords",
|
||||
"search": "Search",
|
||||
"statusPlaceholder": "All Status",
|
||||
"allStatus": "All Status"
|
||||
},
|
||||
"table": {
|
||||
"title": "Enterprise Leads List",
|
||||
"id": "Lead ID",
|
||||
"company": "Company Name",
|
||||
"contact": "Contact",
|
||||
"phone": "Phone",
|
||||
"submittedAt": "Submitted At",
|
||||
"status": "Status",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"status": {
|
||||
"pending": "Pending",
|
||||
"contacting": "Contacted",
|
||||
"completed": "Converted",
|
||||
"rejected": "Rejected"
|
||||
},
|
||||
"actions": {
|
||||
"view": "View",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"refresh": "Refresh"
|
||||
}
|
||||
},
|
||||
|
||||
"invoices": {
|
||||
"title": "Invoice Management",
|
||||
"detailTitle": "Invoice Application Detail",
|
||||
"stats": {
|
||||
"all": "Total Applications",
|
||||
"pending": "Pending",
|
||||
"success": "Invoiced",
|
||||
"rejected": "Rejected"
|
||||
},
|
||||
"filter": {
|
||||
"searchLabel": "Search Invoice/Order No:",
|
||||
"searchPlaceholder": "Enter invoice or order number",
|
||||
"statusLabel": "Status:",
|
||||
"allStatus": "All Status",
|
||||
"pending": "Pending",
|
||||
"success": "Invoiced",
|
||||
"rejected": "Rejected",
|
||||
"timeLabel": "Apply Time:",
|
||||
"reset": "Reset",
|
||||
"query": "Filter"
|
||||
},
|
||||
"table": {
|
||||
"invoiceNo": "Invoice App No",
|
||||
"user": "User",
|
||||
"orderNo": "Order No",
|
||||
"applyTime": "Apply Time",
|
||||
"status": "Status",
|
||||
"amount": "Amount",
|
||||
"operation": "Operation",
|
||||
"listTitle": "Invoice Application List"
|
||||
},
|
||||
"actions": {
|
||||
"view": "View",
|
||||
"upload": "Upload",
|
||||
"reject": "Reject",
|
||||
"back": "Back",
|
||||
"submit": "Submit",
|
||||
"cancel": "Cancel",
|
||||
"confirmUpload": "Confirm Upload",
|
||||
"confirmReject": "Confirm Reject"
|
||||
},
|
||||
"detail": {
|
||||
"basicInfo": "Basic Information",
|
||||
"invoiceInfo": "Invoice Information",
|
||||
"rejectInfo": "Reject Information",
|
||||
"approveInfo": "Approve Information",
|
||||
"invoiceNo": "Invoice App No",
|
||||
"orderNo": "Related Order No",
|
||||
"applyTime": "Apply Time",
|
||||
"status": "Status",
|
||||
"userInfo": "User Info",
|
||||
"phone": "Phone",
|
||||
"amount": "Amount",
|
||||
"email": "Email",
|
||||
"invoiceType": "Invoice Type",
|
||||
"invoiceTitle": "Invoice Title",
|
||||
"taxId": "Tax ID",
|
||||
"content": "Content",
|
||||
"remark": "Remark",
|
||||
"rejectReason": "Reject Reason",
|
||||
"rejectTime": "Reject Time",
|
||||
"rejectUser": "Reject By",
|
||||
"finishTime": "Finish Time",
|
||||
"approveUser": "Upload By",
|
||||
"fileName": "Invoice File",
|
||||
"approveTime": "Upload Time"
|
||||
},
|
||||
"dialog": {
|
||||
"uploadTitle": "Upload Invoice File",
|
||||
"rejectTitle": "Reject Invoice Application",
|
||||
"rejectAlert": "After rejection, the user will be notified and need to resubmit.",
|
||||
"reasonLabel": "Reject Reason",
|
||||
"reasonTip": "Please select or enter the specific reason",
|
||||
"reasonPlaceholder": "Please select reason",
|
||||
"remarkLabel": "Details",
|
||||
"remarkPlaceholder": "Enter detailed instructions to help user modify information",
|
||||
"uploadLabel": "Upload Invoice File",
|
||||
"uploadDraggerText": "Click to upload / drag to this area",
|
||||
"uploadTip": "Support PDF format. Max 10MB",
|
||||
"finishTimeLabel": "Finish Time",
|
||||
"finishTimePlaceholder": "Select date and time",
|
||||
"remarkPlaceholderUpload": "Enter remark",
|
||||
"reasons": {
|
||||
"title_error": "Incorrect title info",
|
||||
"tax_id_error": "Incorrect tax ID",
|
||||
"attachment_error": "Unclear or wrong attachment",
|
||||
"other": "Other reasons"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"uploadSuccess": "Invoice uploaded successfully",
|
||||
"rejectSuccess": "Application rejected",
|
||||
"fetchError": "Failed to fetch data",
|
||||
"fetchDetailError": "Failed to fetch details"
|
||||
}
|
||||
},
|
||||
|
||||
"common": {
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm"
|
||||
|
||||
@@ -54,7 +54,9 @@
|
||||
"menu": {
|
||||
"dashboard": "ダッシュボード",
|
||||
"customers": "顧客管理",
|
||||
"enterpriseLeads": "企業リード",
|
||||
"licenses": "ライセンス管理",
|
||||
"invoices": "発票管理",
|
||||
"roles": "ロールと権限",
|
||||
"users": "システムユーザー"
|
||||
},
|
||||
@@ -75,6 +77,7 @@
|
||||
"breadcrumb": {
|
||||
"dashboard": "ダッシュボード",
|
||||
"customers": "顧客管理",
|
||||
"enterpriseLeads": "企業リード",
|
||||
"licenses": "ライセンス管理",
|
||||
"invoices": "発票管理",
|
||||
"invoiceDetail": "発票申請の詳細",
|
||||
@@ -90,6 +93,142 @@
|
||||
"unknown": "未知ページ"
|
||||
}
|
||||
},
|
||||
|
||||
"enterpriseLeads": {
|
||||
"title": "企業リード",
|
||||
"stats": {
|
||||
"total": "リード総数",
|
||||
"pending": "未連絡",
|
||||
"contacting": "連絡済み",
|
||||
"completed": "成約済み",
|
||||
"rejected": "失注"
|
||||
},
|
||||
"filter": {
|
||||
"searchPlaceholder": "キーワードを入力して検索",
|
||||
"search": "検索",
|
||||
"statusPlaceholder": "全ステータス",
|
||||
"allStatus": "全ステータス"
|
||||
},
|
||||
"table": {
|
||||
"title": "企業リード一覧",
|
||||
"id": "リードID",
|
||||
"company": "企業名",
|
||||
"contact": "担当者",
|
||||
"phone": "連絡先電話",
|
||||
"submittedAt": "提出時間",
|
||||
"status": "ステータス",
|
||||
"actions": "操作"
|
||||
},
|
||||
"status": {
|
||||
"pending": "未連絡",
|
||||
"contacting": "連絡済み",
|
||||
"completed": "成約済み",
|
||||
"rejected": "失注"
|
||||
},
|
||||
"actions": {
|
||||
"view": "表示",
|
||||
"edit": "編集",
|
||||
"delete": "削除",
|
||||
"refresh": "更新"
|
||||
}
|
||||
},
|
||||
|
||||
"invoices": {
|
||||
"title": "発票管理",
|
||||
"detailTitle": "発票申請の詳細",
|
||||
"stats": {
|
||||
"all": "全申請数",
|
||||
"pending": "保留中",
|
||||
"success": "発行済み",
|
||||
"rejected": "却下済み"
|
||||
},
|
||||
"filter": {
|
||||
"searchLabel": "発票番号/注文番号で検索:",
|
||||
"searchPlaceholder": "発票番号または注文番号を入力",
|
||||
"statusLabel": "ステータス:",
|
||||
"allStatus": "全ステータス",
|
||||
"pending": "保留中",
|
||||
"success": "発行済み",
|
||||
"rejected": "却下済み",
|
||||
"timeLabel": "申請時間:",
|
||||
"reset": "リセット",
|
||||
"query": "検索"
|
||||
},
|
||||
"table": {
|
||||
"invoiceNo": "発票申請番号",
|
||||
"user": "ユーザー",
|
||||
"orderNo": "注文番号",
|
||||
"applyTime": "申請時間",
|
||||
"status": "ステータス",
|
||||
"amount": "金額",
|
||||
"operation": "操作",
|
||||
"listTitle": "発票申請一覧"
|
||||
},
|
||||
"actions": {
|
||||
"view": "表示",
|
||||
"upload": "アップロード",
|
||||
"reject": "却下",
|
||||
"back": "戻る",
|
||||
"submit": "送信",
|
||||
"cancel": "キャンセル",
|
||||
"confirmUpload": "アップロード確定",
|
||||
"confirmReject": "却下確定"
|
||||
},
|
||||
"detail": {
|
||||
"basicInfo": "基本情報",
|
||||
"invoiceInfo": "発票情報",
|
||||
"rejectInfo": "却下情報",
|
||||
"approveInfo": "承認情報",
|
||||
"invoiceNo": "発票申請番号",
|
||||
"orderNo": "関連注文番号",
|
||||
"applyTime": "申請時間",
|
||||
"status": "ステータス",
|
||||
"userInfo": "ユーザー情報",
|
||||
"phone": "電話番号",
|
||||
"amount": "金額",
|
||||
"email": "メールアドレス",
|
||||
"invoiceType": "発票タイプ",
|
||||
"invoiceTitle": "発票名義",
|
||||
"taxId": "納税者識別番号",
|
||||
"content": "内容",
|
||||
"remark": "備考",
|
||||
"rejectReason": "却下理由",
|
||||
"rejectTime": "却下時間",
|
||||
"rejectUser": "却下担当者",
|
||||
"finishTime": "完了時間",
|
||||
"approveUser": "アップロード担当者",
|
||||
"fileName": "ファイル名",
|
||||
"approveTime": "アップロード時間"
|
||||
},
|
||||
"dialog": {
|
||||
"uploadTitle": "発票ファイルのアップロード",
|
||||
"rejectTitle": "発票申請の却下",
|
||||
"rejectAlert": "却下後、ユーザーに通知され、再申請が必要になります。",
|
||||
"reasonLabel": "却下理由",
|
||||
"reasonTip": "具体的な却下理由を選択または入力してください",
|
||||
"reasonPlaceholder": "理由を選択してください",
|
||||
"remarkLabel": "詳細説明",
|
||||
"remarkPlaceholder": "ユーザーが情報を修正しやすくなるよう、詳細な説明を入力してください",
|
||||
"uploadLabel": "発票ファイルのアップロード",
|
||||
"uploadDraggerText": "クリックしてアップロード / ドラッグ&ドロップ",
|
||||
"uploadTip": "PDF形式に対応。最大10MB",
|
||||
"finishTimeLabel": "完了時間",
|
||||
"finishTimePlaceholder": "日時を選択",
|
||||
"remarkPlaceholderUpload": "備考を入力してください",
|
||||
"reasons": {
|
||||
"title_error": "名義情報の誤り",
|
||||
"tax_id_error": "納税者識別番号の誤り",
|
||||
"attachment_error": "添付ファイルが不鮮明または誤り",
|
||||
"other": "その他の理由"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"uploadSuccess": "アップロードに成功しました",
|
||||
"rejectSuccess": "申請を却下しました",
|
||||
"fetchError": "データの取得に失敗しました",
|
||||
"fetchDetailError": "詳細の取得に失敗しました"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"cancel": "キャンセル",
|
||||
"confirm": "確認"
|
||||
|
||||
@@ -54,8 +54,10 @@
|
||||
"menu": {
|
||||
"dashboard": "仪表盘",
|
||||
"customers": "客户管理",
|
||||
"enterpriseLeads": "企业线索",
|
||||
"licenses": "授权管理",
|
||||
"invoices": "发票管理",
|
||||
"packages": "套餐管理",
|
||||
"roles": "角色权限",
|
||||
"users": "系统用户"
|
||||
},
|
||||
@@ -76,10 +78,12 @@
|
||||
"breadcrumb": {
|
||||
"dashboard": "仪表盘",
|
||||
"customers": "客户管理",
|
||||
"licenses": "授权管理",
|
||||
"invoices": "发票管理",
|
||||
"invoiceDetail": "发票申请详情",
|
||||
"licenseList": "授权列表",
|
||||
"enterpriseLeads": "企业线索",
|
||||
"licenses": "授权管理",
|
||||
"invoices": "发票管理",
|
||||
"packages": "套餐管理",
|
||||
"invoiceDetail": "发票申请详情",
|
||||
"licenseList": "授权列表",
|
||||
"createLicense": "创建授权",
|
||||
"viewLicense": "查看授权",
|
||||
"roles": "角色权限",
|
||||
@@ -91,6 +95,198 @@
|
||||
"unknown": "未知页面"
|
||||
}
|
||||
},
|
||||
|
||||
"enterpriseLeads": {
|
||||
"title": "企业线索",
|
||||
"stats": {
|
||||
"total": "线索总数",
|
||||
"pending": "待联系",
|
||||
"contacting": "已联系",
|
||||
"completed": "已成交",
|
||||
"rejected": "已失效"
|
||||
},
|
||||
"filter": {
|
||||
"searchPlaceholder": "请输入关键词搜索",
|
||||
"search": "搜索",
|
||||
"statusPlaceholder": "全部状态",
|
||||
"allStatus": "全部状态"
|
||||
},
|
||||
"table": {
|
||||
"title": "企业线索列表",
|
||||
"id": "线索ID",
|
||||
"company": "企业名称",
|
||||
"contact": "联系人",
|
||||
"phone": "联系电话",
|
||||
"submittedAt": "提交时间",
|
||||
"status": "状态",
|
||||
"actions": "操作"
|
||||
},
|
||||
"status": {
|
||||
"pending": "待联系",
|
||||
"contacting": "已联系",
|
||||
"completed": "已成交",
|
||||
"rejected": "已失效"
|
||||
},
|
||||
"actions": {
|
||||
"view": "查看",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"refresh": "刷新"
|
||||
},
|
||||
"detail": {
|
||||
"title": "企业线索详情 - {company}",
|
||||
"basicInfo": "企业基本信息",
|
||||
"requirementInfo": "需求信息",
|
||||
"followUpInfo": "跟进信息",
|
||||
"email": "邮箱地址",
|
||||
"description": "具体需求描述",
|
||||
"otherInfo": "其他补充信息",
|
||||
"followUpDate": "跟进日期",
|
||||
"followUpRecord": "跟进记录",
|
||||
"internalRemark": "内部备注"
|
||||
},
|
||||
"edit": {
|
||||
"title": "编辑企业线索 - {company}",
|
||||
"save": "保存更改",
|
||||
"datePlaceholder": "选择日期",
|
||||
"recordPlaceholder": "记录与客户的沟通情况,反馈,下一步计划...",
|
||||
"remarkPlaceholder": "内部使用的备注信息,客户不可见",
|
||||
"statusRequired": "请选择状态"
|
||||
}
|
||||
},
|
||||
|
||||
"packages": {
|
||||
"title": "套餐管理",
|
||||
"table": {
|
||||
"title": "价格套餐管理",
|
||||
"id": "套餐ID",
|
||||
"name": "套餐名称",
|
||||
"price": "价格",
|
||||
"cycle": "周期",
|
||||
"createdAt": "创建时间",
|
||||
"updatedAt": "更新时间",
|
||||
"status": "状态",
|
||||
"actions": "操作"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用"
|
||||
},
|
||||
"edit": {
|
||||
"title": "编辑套餐",
|
||||
"name": "套餐名称",
|
||||
"type": "套餐类型",
|
||||
"price": "价格",
|
||||
"cycle": "周期/描述",
|
||||
"description": "套餐描述",
|
||||
"features": "套餐特性",
|
||||
"status": "状态",
|
||||
"sort": "显示顺序",
|
||||
"remark": "备注",
|
||||
"save": "保存更改",
|
||||
"descPlaceholder": "体验完整功能,适合新用户试用评估",
|
||||
"remarkPlaceholder": "请输入备注",
|
||||
"featurePlaceholder": "请输入特性内容"
|
||||
}
|
||||
},
|
||||
|
||||
"invoices": {
|
||||
"title": "发票管理",
|
||||
"detailTitle": "发票申请详情",
|
||||
"stats": {
|
||||
"all": "全部发票申请",
|
||||
"pending": "待处理申请",
|
||||
"success": "已开票",
|
||||
"rejected": "已驳回"
|
||||
},
|
||||
"filter": {
|
||||
"searchLabel": "搜索发票号/订单号:",
|
||||
"searchPlaceholder": "输入发票号或订单号",
|
||||
"statusLabel": "开票状态:",
|
||||
"allStatus": "全部状态",
|
||||
"pending": "待处理",
|
||||
"success": "已开票",
|
||||
"rejected": "已驳回",
|
||||
"timeLabel": "申请时间:",
|
||||
"reset": "重置",
|
||||
"query": "筛选"
|
||||
},
|
||||
"table": {
|
||||
"invoiceNo": "发票申请号",
|
||||
"user": "用户",
|
||||
"orderNo": "订单号",
|
||||
"applyTime": "申请时间",
|
||||
"status": "开票状态",
|
||||
"amount": "发票金额",
|
||||
"operation": "操作",
|
||||
"listTitle": "发票申请列表"
|
||||
},
|
||||
"actions": {
|
||||
"view": "查看",
|
||||
"upload": "上传",
|
||||
"reject": "驳回",
|
||||
"back": "返回",
|
||||
"submit": "提交",
|
||||
"cancel": "取消",
|
||||
"confirmUpload": "确认上传",
|
||||
"confirmReject": "确认驳回"
|
||||
},
|
||||
"detail": {
|
||||
"basicInfo": "基本信息",
|
||||
"invoiceInfo": "开票信息",
|
||||
"rejectInfo": "驳回信息",
|
||||
"approveInfo": "通过信息",
|
||||
"invoiceNo": "发票申请号",
|
||||
"orderNo": "关联订单号",
|
||||
"applyTime": "申请时间",
|
||||
"status": "开票状态",
|
||||
"userInfo": "用户信息",
|
||||
"phone": "联系电话",
|
||||
"amount": "开票金额",
|
||||
"email": "收票邮箱",
|
||||
"invoiceType": "发票类型",
|
||||
"invoiceTitle": "发票抬头",
|
||||
"taxId": "纳税人识别号",
|
||||
"content": "开票内容",
|
||||
"remark": "备注信息",
|
||||
"rejectReason": "驳回原因",
|
||||
"rejectTime": "驳回时间",
|
||||
"rejectUser": "驳回人员",
|
||||
"finishTime": "开票完成时间",
|
||||
"approveUser": "上传人员",
|
||||
"fileName": "发票文件",
|
||||
"approveTime": "上传时间"
|
||||
},
|
||||
"dialog": {
|
||||
"uploadTitle": "上传发票文件",
|
||||
"rejectTitle": "驳回发票申请",
|
||||
"rejectAlert": "驳回后,用户将收到通知并需要重新提交申请。",
|
||||
"reasonLabel": "驳回原因",
|
||||
"reasonTip": "请选择或输入驳回的具体原因",
|
||||
"reasonPlaceholder": "请选择驳回原因",
|
||||
"remarkLabel": "详细说明",
|
||||
"remarkPlaceholder": "请输入详细的驳回说明,帮助用户更准确地修改信息",
|
||||
"uploadLabel": "上传发票文件",
|
||||
"uploadDraggerText": "点击上传 / 拖拽到此区域",
|
||||
"uploadTip": "支持PDF格式。最大文件大小10MB",
|
||||
"finishTimeLabel": "开票完成时间",
|
||||
"finishTimePlaceholder": "选择日期时间",
|
||||
"remarkPlaceholderUpload": "输入备注信息",
|
||||
"reasons": {
|
||||
"title_error": "发票抬头信息有误",
|
||||
"tax_id_error": "纳税人识别号不正确",
|
||||
"attachment_error": "附件不清晰或有误",
|
||||
"other": "其他原因"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"uploadSuccess": "发票上传成功",
|
||||
"rejectSuccess": "发票申请已驳回",
|
||||
"fetchError": "获取数据失败",
|
||||
"fetchDetailError": "获取详情失败"
|
||||
}
|
||||
},
|
||||
|
||||
"common": {
|
||||
"cancel": "取消",
|
||||
"confirm": "确定"
|
||||
|
||||
@@ -61,11 +61,21 @@ const routes: Array<RouteRecordRaw> = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: "/enterprise-leads",
|
||||
component: () => import("@/views/EnterpriseLeads/index.vue"),
|
||||
meta: { title: "企业线索", requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: "/invoices",
|
||||
component: () => import("@/views/Invoices/index.vue"),
|
||||
meta: { title: "发票管理", requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: "/packages",
|
||||
component: () => import("@/views/Packages/index.vue"),
|
||||
meta: { title: "套餐管理", requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: "/invoices/detail/:id",
|
||||
name: "invoice-detail",
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface BreadcrumbItem {
|
||||
const routeBreadcrumbKeys: Record<string, string[]> = {
|
||||
'/dashboard': ['dashboard'],
|
||||
'/customers': ['customers'],
|
||||
'/enterprise-leads': ['enterpriseLeads'],
|
||||
'/licenses': ['licenses'],
|
||||
'/invoices': ['invoices'],
|
||||
'/invoices/detail': ['invoices', 'invoiceDetail'],
|
||||
@@ -121,7 +122,7 @@ export function useBreadcrumb() {
|
||||
|
||||
// 根据路径段获取标题
|
||||
function getSegmentTitle(segment: string): string {
|
||||
const knownSegments = ['dashboard', 'customers', 'licenses', 'roles', 'users', 'login', 'settings', 'profile']
|
||||
const knownSegments = ['dashboard', 'customers', 'enterprise-leads', 'licenses', 'roles', 'users', 'login', 'settings', 'profile']
|
||||
|
||||
if (knownSegments.includes(segment)) {
|
||||
return t(`navigation.breadcrumb.${segment}`)
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<div class="dialog_box">
|
||||
<el-dialog v-model="visible" :title="t('enterpriseLeads.detail.title', { company: data?.company })"
|
||||
width="800px" class="lead-detail-dialog" destroy-on-close>
|
||||
<div v-if="data" class="detail-content">
|
||||
<!-- 企业基本信息 -->
|
||||
<div class="detail-section">
|
||||
<h3 class="section-title">{{ t('enterpriseLeads.detail.basicInfo') }}</h3>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="label">{{ t('enterpriseLeads.table.company') }}:</span>
|
||||
<span class="value">{{ data.company }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">{{ t('enterpriseLeads.table.contact') }}:</span>
|
||||
<span class="value">{{ data.contact }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">{{ t('enterpriseLeads.table.phone') }}:</span>
|
||||
<span class="value">{{ data.phone }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 需求信息 -->
|
||||
<div class="detail-section">
|
||||
<h3 class="section-title">{{ t('enterpriseLeads.detail.requirementInfo') }}</h3>
|
||||
<div class="info-list">
|
||||
<div class="info-item">
|
||||
<span class="label">{{ t('enterpriseLeads.detail.email') }}:</span>
|
||||
<span class="value">{{ data.email || 'wangming@sh-smart.com' }}</span>
|
||||
</div>
|
||||
<div class="info-item block">
|
||||
<span class="label">{{ t('enterpriseLeads.detail.description') }}:</span>
|
||||
<span class="value">{{ data.description ||
|
||||
'我们需要为生产线上的500台设备部署专业版授权,同时为办公室的200台电脑部署基础版授权。需要支持离线授权功能,部署时间为下个月初。' }}</span>
|
||||
</div>
|
||||
<div class="info-item block">
|
||||
<span class="label">{{ t('enterpriseLeads.detail.otherInfo') }}:</span>
|
||||
<span class="value">{{ data.otherInfo || '预算范围在50-80万之间,决策时间约2周。' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 跟进信息 -->
|
||||
<div class="detail-section">
|
||||
<h3 class="section-title">{{ t('enterpriseLeads.detail.followUpInfo') }}</h3>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="label">{{ t('enterpriseLeads.table.submittedAt') }}:</span>
|
||||
<span class="value">{{ data.submittedAt }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">{{ t('enterpriseLeads.table.status') }}:</span>
|
||||
<span class="status-value" :class="data.status">{{
|
||||
t(`enterpriseLeads.status.${data.status}`) }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">{{ t('enterpriseLeads.detail.followUpDate') }}:</span>
|
||||
<span class="value">{{ data.followUpDate || '2025-12-21' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-list mt-12">
|
||||
<div class="info-item block">
|
||||
<span class="label">{{ t('enterpriseLeads.detail.followUpRecord') }}:</span>
|
||||
<span class="value">{{ data.followUpRecord || '已初步沟通,客户对专业版的AI功能非常感兴趣,需要提供详细的功能演示。'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="info-item block">
|
||||
<span class="label">{{ t('enterpriseLeads.detail.internalRemark') }}:</span>
|
||||
<span class="value">{{ data.internalRemark || '高价值客户,需要安排技术专家进行演示。' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
data: any
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const { t } = useI18n()
|
||||
const visible = ref(props.modelValue)
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
})
|
||||
|
||||
watch(visible, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-dialog__headerbtn) {
|
||||
top: 10px !important;
|
||||
}
|
||||
|
||||
:deep(.el-dialog) {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
padding: 0 !important;
|
||||
|
||||
|
||||
.el-dialog__header {
|
||||
margin-right: 0;
|
||||
padding: 20px 24px;
|
||||
background: linear-gradient(90deg, #00928A 0%, #00D19E 100%) !important;
|
||||
border-bottom: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.el-dialog__title {
|
||||
color: #fff !important;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.el-dialog__headerbtn {
|
||||
top: 20px;
|
||||
|
||||
.el-dialog__close {
|
||||
color: #fff !important;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&:hover .el-dialog__close {
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.detail-content {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 24px;
|
||||
border: 1px solid #E9E9E9;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
padding-left: 0;
|
||||
position: relative;
|
||||
background-color: #FAFAFA;
|
||||
padding: 11px 24px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
padding: 11px 24px;
|
||||
box-sizing: border-box;
|
||||
|
||||
}
|
||||
|
||||
.info-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 11px 24px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
|
||||
&.block {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #666;
|
||||
flex-shrink: 0;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
font-weight: 500;
|
||||
|
||||
&.contacting {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
&.pending {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
&.completed {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
&.rejected {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mt-12 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="t('enterpriseLeads.edit.title', { company: data?.company })"
|
||||
width="800px"
|
||||
class="lead-edit-dialog"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
class="edit-form"
|
||||
>
|
||||
<div class="form-row">
|
||||
<el-form-item :label="t('enterpriseLeads.table.status')" prop="status" class="flex-1">
|
||||
<el-select v-model="form.status" class="w-full" :placeholder="t('enterpriseLeads.filter.statusPlaceholder')">
|
||||
<el-option :label="t('enterpriseLeads.status.pending')" value="pending" />
|
||||
<el-option :label="t('enterpriseLeads.status.contacting')" value="contacting" />
|
||||
<el-option :label="t('enterpriseLeads.status.completed')" value="completed" />
|
||||
<el-option :label="t('enterpriseLeads.status.rejected')" value="rejected" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('enterpriseLeads.detail.followUpDate')" prop="followUpDate" class="flex-1">
|
||||
<el-date-picker
|
||||
v-model="form.followUpDate"
|
||||
type="date"
|
||||
class="w-full"
|
||||
:placeholder="t('enterpriseLeads.edit.datePlaceholder')"
|
||||
format="YYYY/MM/DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-form-item :label="t('enterpriseLeads.detail.followUpRecord')" prop="followUpRecord">
|
||||
<el-input
|
||||
v-model="form.followUpRecord"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
:placeholder="t('enterpriseLeads.edit.recordPlaceholder')"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('enterpriseLeads.detail.internalRemark')" prop="internalRemark">
|
||||
<el-input
|
||||
v-model="form.internalRemark"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
:placeholder="t('enterpriseLeads.edit.remarkPlaceholder')"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="visible = false">{{ t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" class="btn-save" @click="handleSave">
|
||||
{{ t('enterpriseLeads.edit.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
data: any
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save'])
|
||||
|
||||
const { t } = useI18n()
|
||||
const visible = ref(props.modelValue)
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const form = reactive({
|
||||
status: '',
|
||||
followUpDate: '',
|
||||
followUpRecord: '',
|
||||
internalRemark: ''
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
status: [{ required: true, message: t('enterpriseLeads.edit.statusRequired'), trigger: 'change' }]
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val && props.data) {
|
||||
form.status = props.data.status
|
||||
form.followUpDate = props.data.followUpDate || '2026-01-14'
|
||||
form.followUpRecord = props.data.followUpRecord || ''
|
||||
form.internalRemark = props.data.internalRemark || ''
|
||||
}
|
||||
})
|
||||
|
||||
watch(visible, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
emit('save', { ...props.data, ...form })
|
||||
visible.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-dialog__headerbtn) {
|
||||
top: 10px !important;
|
||||
}
|
||||
:deep(.el-dialog) {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
|
||||
.el-dialog__header {
|
||||
margin-right: 0;
|
||||
padding: 20px 24px;
|
||||
background: linear-gradient(90deg, #00928A 0%, #00D19E 100%) !important;
|
||||
border-bottom: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.el-dialog__title {
|
||||
color: #fff !important;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.el-dialog__headerbtn {
|
||||
top: 20px;
|
||||
.el-dialog__close {
|
||||
color: #fff !important;
|
||||
font-size: 20px;
|
||||
}
|
||||
&:hover .el-dialog__close {
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
padding: 0 24px 24px;
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-form {
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper), :deep(.el-textarea__inner) {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
|
||||
.el-button {
|
||||
padding: 10px 32px;
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
background-color: #00a870 !important;
|
||||
border-color: #00a870 !important;
|
||||
color: #fff !important;
|
||||
&:hover {
|
||||
background-color: #008f5d !important;
|
||||
border-color: #008f5d !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,435 @@
|
||||
<template>
|
||||
<Layout :page-title="t('enterpriseLeads.title')">
|
||||
<div class="enterprise-leads">
|
||||
<div class="stats-grid">
|
||||
<div v-for="stat in stats" :key="stat.key" class="stat-card">
|
||||
<div class="stat-info">
|
||||
<p class="stat-label">{{ t(`enterpriseLeads.stats.${stat.key}`) }}</p>
|
||||
<p class="stat-value">{{ stat.value.toLocaleString() }}</p>
|
||||
</div>
|
||||
<div class="stat-icon" :class="stat.key">
|
||||
<el-icon><component :is="stat.icon" /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-card">
|
||||
<div class="filter-row">
|
||||
<div class="search-group">
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
:placeholder="t('enterpriseLeads.filter.searchPlaceholder')"
|
||||
class="search-input"
|
||||
clearable
|
||||
>
|
||||
<template #append>
|
||||
<el-button class="search-btn">
|
||||
<el-icon><Search /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<div class="status-group">
|
||||
<el-select v-model="filters.status" class="status-select" :placeholder="t('enterpriseLeads.filter.statusPlaceholder')">
|
||||
<el-option :label="t('enterpriseLeads.filter.allStatus')" value="" />
|
||||
<el-option :label="t('enterpriseLeads.status.pending')" value="pending" />
|
||||
<el-option :label="t('enterpriseLeads.status.contacting')" value="contacting" />
|
||||
<el-option :label="t('enterpriseLeads.status.completed')" value="completed" />
|
||||
<el-option :label="t('enterpriseLeads.status.rejected')" value="rejected" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-card">
|
||||
<div class="list-header">
|
||||
<div class="list-title">
|
||||
<span class="title-bar"></span>
|
||||
<span>{{ t('enterpriseLeads.table.title') }}</span>
|
||||
</div>
|
||||
<el-button class="refresh-btn" :icon="Refresh" text>
|
||||
{{ t('enterpriseLeads.actions.refresh') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="tableData"
|
||||
stripe
|
||||
class="leads-table"
|
||||
:header-cell-style="{
|
||||
backgroundColor: '#E6F7F3',
|
||||
color: '#4F4F4F',
|
||||
fontWeight: '600'
|
||||
}"
|
||||
>
|
||||
<el-table-column prop="id" :label="t('enterpriseLeads.table.id')" width="100" />
|
||||
<el-table-column prop="company" :label="t('enterpriseLeads.table.company')" min-width="220" />
|
||||
<el-table-column prop="contact" :label="t('enterpriseLeads.table.contact')" min-width="120" />
|
||||
<el-table-column prop="phone" :label="t('enterpriseLeads.table.phone')" min-width="140" />
|
||||
<el-table-column prop="submittedAt" :label="t('enterpriseLeads.table.submittedAt')" min-width="160" />
|
||||
<el-table-column :label="t('enterpriseLeads.table.status')" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="status-tag" :class="row.status">{{ t(`enterpriseLeads.status.${row.status}`) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('enterpriseLeads.table.actions')" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="action-buttons">
|
||||
<el-button size="small" class="btn-view" @click="handleView(row)">{{ t('enterpriseLeads.actions.view') }}</el-button>
|
||||
<el-button size="small" class="btn-edit" @click="handleEdit(row)">{{ t('enterpriseLeads.actions.edit') }}</el-button>
|
||||
<el-button size="small" class="btn-delete">{{ t('enterpriseLeads.actions.delete') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="prev, pager, next, jumper, sizes, total"
|
||||
:total="total"
|
||||
background
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LeadDetailDialog
|
||||
v-model="detailVisible"
|
||||
:data="selectedLead"
|
||||
/>
|
||||
|
||||
<LeadEditDialog
|
||||
v-model="editVisible"
|
||||
:data="selectedLead"
|
||||
@save="handleUpdate"
|
||||
/>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Layout from '@/components/common/layout/Layout.vue'
|
||||
import { Document, Phone, CircleCheck, CircleClose, Refresh, User, OfficeBuilding, Search } from '@element-plus/icons-vue'
|
||||
import LeadDetailDialog from './components/LeadDetailDialog.vue'
|
||||
import LeadEditDialog from './components/LeadEditDialog.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const stats = ref([
|
||||
{ key: 'total', value: 21978, icon: OfficeBuilding },
|
||||
{ key: 'pending', value: 21978, icon: Phone },
|
||||
{ key: 'contacting', value: 21978, icon: CircleCheck },
|
||||
{ key: 'completed', value: 21978, icon: User }
|
||||
])
|
||||
|
||||
const filters = ref({
|
||||
keyword: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
const tableData = ref([
|
||||
{
|
||||
id: '1001',
|
||||
company: '上海智能制造有限公司',
|
||||
contact: '张教授',
|
||||
phone: '13800138001',
|
||||
submittedAt: '2024-01-15 10:30',
|
||||
status: 'contacting'
|
||||
},
|
||||
{
|
||||
id: '1002',
|
||||
company: '上海智能制造有限公司',
|
||||
contact: '张教授',
|
||||
phone: '13800138001',
|
||||
submittedAt: '2024-01-15 10:30',
|
||||
status: 'contacting'
|
||||
},
|
||||
{
|
||||
id: '1003',
|
||||
company: '上海智能制造有限公司',
|
||||
contact: '张教授',
|
||||
phone: '13800138001',
|
||||
submittedAt: '2024-01-15 10:30',
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: '1004',
|
||||
company: '上海智能制造有限公司',
|
||||
contact: '张教授',
|
||||
phone: '13800138001',
|
||||
submittedAt: '2024-01-15 10:30',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: '1005',
|
||||
company: '上海智能制造有限公司',
|
||||
contact: '张教授',
|
||||
phone: '13800138001',
|
||||
submittedAt: '2024-01-15 10:30',
|
||||
status: 'rejected'
|
||||
}
|
||||
])
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(21978)
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const editVisible = ref(false)
|
||||
const selectedLead = ref<any>(null)
|
||||
|
||||
const handleView = (row: any) => {
|
||||
selectedLead.value = row
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
selectedLead.value = row
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
const handleUpdate = (updatedData: any) => {
|
||||
const index = tableData.value.findIndex(item => item.id === updatedData.id)
|
||||
if (index !== -1) {
|
||||
tableData.value[index] = updatedData
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.enterprise-leads {
|
||||
padding: 24px;
|
||||
background-color: #f0f2f5;
|
||||
min-height: calc(100vh - 80px);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
border: none;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #00a870;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.stat-icon.total {
|
||||
background: #f0f4ff;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.stat-icon.pending {
|
||||
background: #fff8e6;
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.stat-icon.contacting {
|
||||
background: #e6f7f3;
|
||||
color: #00a870;
|
||||
}
|
||||
|
||||
.stat-icon.completed {
|
||||
background: #f5f0ff;
|
||||
color: #7c5cfc;
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-group {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.search-input :deep(.el-input__wrapper) {
|
||||
border-radius: 4px 0 0 4px;
|
||||
box-shadow: 1px 0 0 0 var(--el-input-border-color) inset, 0 1px 0 0 var(--el-input-border-color) inset, 0 -1px 0 0 var(--el-input-border-color) inset !important;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
background: #00a870 !important;
|
||||
border-color: #00a870 !important;
|
||||
color: #fff !important;
|
||||
border-radius: 0 4px 4px 0;
|
||||
width: 44px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.status-select {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.status-select :deep(.el-input__wrapper) {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.list-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
padding: 24px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
background: #00a870;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.leads-table :deep(.el-table__header th) {
|
||||
font-weight: 600;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-tag.contacting {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.status-tag.pending {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.status-tag.completed {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-tag.rejected {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-view, .btn-edit, .btn-delete {
|
||||
border: none;
|
||||
padding: 4px 12px;
|
||||
height: 28px;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.btn-view, .btn-edit {
|
||||
background: #e6f7f3 !important;
|
||||
color: #00a870 !important;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: #fff1f0 !important;
|
||||
color: #f5222d !important;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.pagination-container :deep(.el-pagination.is-background .el-pager li:not(.is-disabled).is-active) {
|
||||
background-color: #00a870;
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.enterprise-leads {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.status-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,16 +1,17 @@
|
||||
<template>
|
||||
<Layout>
|
||||
<Layout :page-title="t('invoices.detailTitle')">
|
||||
<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>
|
||||
<el-button link @click="handleBack">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
{{ t('invoices.title') }}
|
||||
</el-button>
|
||||
</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>
|
||||
<el-button v-if="detailData.status === 'pending'" class="btn-upload" type="primary" @click="handleUpload">{{ t('invoices.actions.upload') }}</el-button>
|
||||
<el-button v-if="detailData.status === 'pending'" class="btn-reject" type="danger" @click="handleReject">{{ t('invoices.actions.reject') }}</el-button>
|
||||
<el-button @click="handleBack">{{ t('invoices.actions.back') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,25 +20,25 @@
|
||||
<div class="detail-card">
|
||||
<div class="card-title">
|
||||
<span class="title-line"></span>
|
||||
<h3>基本信息</h3>
|
||||
<h3>{{ t('invoices.detail.basicInfo') }}</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="开票状态">
|
||||
<el-descriptions-item :label="t('invoices.detail.invoiceNo')">{{ detailData.invoice_no }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('invoices.detail.orderNo')">{{ detailData.order_no }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('invoices.detail.applyTime')">{{ detailData.created_at }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('invoices.detail.status')">
|
||||
<span :class="['status-tag', detailData.status]">
|
||||
{{ detailData.statusLabel }}
|
||||
{{ detailData.status_display }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="用户信息">
|
||||
{{ detailData.user }} <span class="user-type">({{ detailData.userTypeLabel }})</span>
|
||||
<el-descriptions-item :label="t('invoices.detail.userInfo')">
|
||||
{{ detailData.applicant_name }} <span class="user-type">({{ detailData.order_package_name }})</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">{{ detailData.phone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="开票金额">
|
||||
<el-descriptions-item :label="t('invoices.detail.phone')">{{ detailData.applicant_phone }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('invoices.detail.amount')">
|
||||
<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-item :label="t('invoices.detail.email')">{{ detailData.receiver_email }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
@@ -45,14 +46,14 @@
|
||||
<div class="detail-card">
|
||||
<div class="card-title">
|
||||
<span class="title-line"></span>
|
||||
<h3>开票信息</h3>
|
||||
<h3>{{ t('invoices.detail.invoiceInfo') }}</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-item :label="t('invoices.detail.invoiceType')">{{ detailData.invoice_type_display }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('invoices.detail.invoiceTitle')">{{ detailData.title }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('invoices.detail.taxId')">{{ detailData.taxpayer_id }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('invoices.detail.content')">{{ detailData.content }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('invoices.detail.remark')" :span="4">{{ detailData.remark }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
@@ -60,20 +61,20 @@
|
||||
<div class="detail-card reject-info-card" v-if="detailData.status === 'rejected'">
|
||||
<div class="card-title">
|
||||
<span class="title-line"></span>
|
||||
<h3>驳回信息</h3>
|
||||
<h3>{{ t('invoices.detail.rejectInfo') }}</h3>
|
||||
</div>
|
||||
<div class="info-content">
|
||||
<div class="info-row">
|
||||
<span class="label">驳回原因:</span>
|
||||
<span class="value">{{ detailData.rejectReason }}</span>
|
||||
<span class="label">{{ t('invoices.detail.rejectReason') }}:</span>
|
||||
<span class="value">{{ detailData.reject_reason }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">驳回时间:</span>
|
||||
<span class="value">{{ detailData.rejectTime }}</span>
|
||||
<span class="label">{{ t('invoices.detail.rejectTime') }}:</span>
|
||||
<span class="value">{{ detailData.rejected_at }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">驳回人员:</span>
|
||||
<span class="value">{{ detailData.rejectUser }}</span>
|
||||
<span class="label">{{ t('invoices.detail.rejectUser') }}:</span>
|
||||
<span class="value">{{ detailData.rejected_by }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,26 +83,28 @@
|
||||
<div class="detail-card approve-info-card" v-if="detailData.status === 'completed'">
|
||||
<div class="card-title">
|
||||
<span class="title-line"></span>
|
||||
<h3>通过信息</h3>
|
||||
<h3>{{ t('invoices.detail.approveInfo') }}</h3>
|
||||
</div>
|
||||
<div class="info-content">
|
||||
<div class="info-row">
|
||||
<span class="label">开票完成时间:</span>
|
||||
<span class="value">{{ detailData.finishTime }}</span>
|
||||
<span class="label">{{ t('invoices.detail.finishTime') }}:</span>
|
||||
<span class="value">{{ detailData.uploaded_at }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">上传人员:</span>
|
||||
<span class="value">{{ detailData.approveUser }}</span>
|
||||
<span class="label">{{ t('invoices.detail.approveUser') }}:</span>
|
||||
<span class="value">{{ detailData.uploader_name }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">发票文件:</span>
|
||||
<span class="label">{{ t('invoices.detail.fileName') }}:</span>
|
||||
<span class="value">
|
||||
<el-link type="primary" :underline="false">{{ detailData.fileName }} ({{ detailData.fileSize }})</el-link>
|
||||
<el-link v-if="detailData.invoice_file_url" type="primary" :underline="false" :href="detailData.invoice_file_url" target="_blank">
|
||||
{{ detailData.invoice_file_url.split('/').pop() }}
|
||||
</el-link>
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">上传时间:</span>
|
||||
<span class="value">{{ detailData.approveTime }}</span>
|
||||
<span class="label">{{ t('invoices.detail.approveTime') }}:</span>
|
||||
<span class="value">{{ detailData.uploaded_at }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -127,12 +130,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
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'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
@@ -141,20 +148,19 @@ const rejectVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
const detailData = ref<Partial<Invoice>>({
|
||||
invoiceNo: '',
|
||||
orderNo: '',
|
||||
applyTime: '',
|
||||
invoice_no: '',
|
||||
order_no: '',
|
||||
created_at: '',
|
||||
status: 'pending',
|
||||
statusLabel: '待处理',
|
||||
user: '',
|
||||
userType: '',
|
||||
userTypeLabel: '',
|
||||
phone: '',
|
||||
status_display: t('invoices.filter.pending'),
|
||||
applicant_name: '',
|
||||
order_package_name: '',
|
||||
applicant_phone: '',
|
||||
amount: 0,
|
||||
email: '',
|
||||
invoiceType: '',
|
||||
invoiceTitle: '',
|
||||
taxId: '',
|
||||
receiver_email: '',
|
||||
invoice_type_display: '',
|
||||
title: '',
|
||||
taxpayer_id: '',
|
||||
content: '',
|
||||
remark: ''
|
||||
})
|
||||
@@ -166,11 +172,15 @@ const fetchDetail = async () => {
|
||||
try {
|
||||
const res = await getInvoiceDetail(id)
|
||||
if (res.code === '000000' && res.data) {
|
||||
detailData.value = res.data
|
||||
const data = res.data
|
||||
data.created_at = formatDateTime(data.created_at)
|
||||
if (data.rejected_at) data.rejected_at = formatDateTime(data.rejected_at)
|
||||
if (data.uploaded_at) data.uploaded_at = formatDateTime(data.uploaded_at)
|
||||
detailData.value = data
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Fetch invoice detail error:', error)
|
||||
ElMessage.error(error.backendMessage || '获取详情失败')
|
||||
ElMessage.error(error.backendMessage || t('invoices.messages.fetchDetailError'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -189,14 +199,32 @@ const handleUpload = () => {
|
||||
}
|
||||
|
||||
const handleUploadSubmit = async (data: any) => {
|
||||
console.log('handleUploadSubmit detail triggered:', data)
|
||||
if (!detailData.value.invoice_no) {
|
||||
console.error('No detail data invoice_no')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await uploadInvoice(detailData.value.id!, data)
|
||||
const formData = new FormData()
|
||||
formData.append('invoice_no', detailData.value.id!)
|
||||
if (data.fileList && data.fileList.length > 0) {
|
||||
const file = data.fileList[0].raw
|
||||
console.log('File to upload (detail):', file)
|
||||
formData.append('file', file)
|
||||
} else {
|
||||
ElMessage.warning(t('invoices.dialog.uploadTip'))
|
||||
return
|
||||
}
|
||||
|
||||
const res = await uploadInvoice(formData)
|
||||
console.log('Upload response (detail):', res)
|
||||
if (res.code === '000000') {
|
||||
ElMessage.success('上传成功')
|
||||
ElMessage.success(t('invoices.messages.uploadSuccess'))
|
||||
fetchDetail()
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.backendMessage || '上传失败')
|
||||
console.error('Upload error detail:', error)
|
||||
ElMessage.error(error.backendMessage || t('invoices.messages.fetchDetailError'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,14 +233,20 @@ const handleReject = () => {
|
||||
}
|
||||
|
||||
const handleRejectSubmit = async (data: any) => {
|
||||
if (!detailData.value.id) return
|
||||
try {
|
||||
const res = await rejectInvoice(detailData.value.id!, data)
|
||||
const reasonText = data.reason === 'other' ? data.suggestion : t(`invoices.dialog.reasons.${data.reason}`)
|
||||
const res = await rejectInvoice(detailData.value.id, {
|
||||
reject_reason: reasonText,
|
||||
suggestion: data.suggestion
|
||||
})
|
||||
if (res.code === '000000') {
|
||||
ElMessage.success('已驳回')
|
||||
ElMessage.success(t('invoices.messages.rejectSuccess'))
|
||||
fetchDetail()
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.backendMessage || '驳回失败')
|
||||
console.error('Reject error detail:', error)
|
||||
ElMessage.error(error.backendMessage || t('invoices.messages.fetchDetailError'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -220,7 +254,7 @@ const handleRejectSubmit = async (data: any) => {
|
||||
<style lang="scss" scoped>
|
||||
.invoice-detail-container {
|
||||
padding: 24px;
|
||||
background-color: #F0F2F5;
|
||||
background-color: var(--app-bg-color);
|
||||
min-height: calc(100vh - 80px);
|
||||
}
|
||||
|
||||
@@ -229,30 +263,25 @@ const handleRejectSubmit = async (data: any) => {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
background: #fff;
|
||||
background: var(--app-content-bg);
|
||||
padding: 16px 24px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
border: 1px solid var(--app-border-light);
|
||||
box-shadow: var(--app-shadow);
|
||||
|
||||
.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;
|
||||
:deep(.el-button) {
|
||||
font-size: 14px;
|
||||
color: var(--app-text-secondary);
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
.el-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,15 +291,15 @@ const handleRejectSubmit = async (data: any) => {
|
||||
gap: 12px;
|
||||
|
||||
.btn-upload {
|
||||
background-color: #019C7C;
|
||||
border-color: #019C7C;
|
||||
&:hover { background-color: #017c63; border-color: #017c63; }
|
||||
background-color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
&:hover { background-color: var(--el-color-primary-dark-2); border-color: var(--el-color-primary-dark-2); }
|
||||
}
|
||||
|
||||
.btn-reject {
|
||||
background-color: #F5222D;
|
||||
border-color: #F5222D;
|
||||
&:hover { background-color: #d32029; border-color: #d32029; }
|
||||
background-color: var(--el-color-danger);
|
||||
border-color: var(--el-color-danger);
|
||||
&:hover { background-color: var(--el-color-danger-dark-2); border-color: var(--el-color-danger-dark-2); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,10 +310,11 @@ const handleRejectSubmit = async (data: any) => {
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: #fff;
|
||||
background: var(--app-content-bg);
|
||||
padding: 24px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
border: 1px solid var(--app-border-light);
|
||||
box-shadow: var(--app-shadow);
|
||||
|
||||
.card-title {
|
||||
display: flex;
|
||||
@@ -294,7 +324,7 @@ const handleRejectSubmit = async (data: any) => {
|
||||
.title-line {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: #019C7C;
|
||||
background: var(--el-color-primary);
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
@@ -302,7 +332,7 @@ const handleRejectSubmit = async (data: any) => {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
color: var(--app-text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,13 +348,13 @@ const handleRejectSubmit = async (data: any) => {
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #595959;
|
||||
color: var(--app-text-secondary);
|
||||
width: 120px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #262626;
|
||||
color: var(--app-text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -333,11 +363,11 @@ const handleRejectSubmit = async (data: any) => {
|
||||
:deep(.info-descriptions) {
|
||||
.el-descriptions__label {
|
||||
width: 120px;
|
||||
color: #595959;
|
||||
color: var(--app-text-secondary);
|
||||
font-weight: normal;
|
||||
}
|
||||
.el-descriptions__content {
|
||||
color: #262626;
|
||||
color: var(--app-text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,9 +375,9 @@ const handleRejectSubmit = async (data: any) => {
|
||||
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; }
|
||||
&.pending { background: var(--el-color-warning-light-9); color: var(--el-color-warning); border: 1px solid var(--el-color-warning-light-5); }
|
||||
&.completed { background: var(--el-color-success-light-9); color: var(--el-color-success); border: 1px solid var(--el-color-success-light-5); }
|
||||
&.rejected { background: var(--el-color-danger-light-9); color: var(--el-color-danger); border: 1px solid var(--el-color-danger-light-5); }
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
@@ -355,7 +385,7 @@ const handleRejectSubmit = async (data: any) => {
|
||||
}
|
||||
|
||||
.user-type {
|
||||
color: #8C8C8C;
|
||||
color: var(--app-text-secondary);
|
||||
}
|
||||
|
||||
// 弹窗通用基础样式
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="驳回发票申请"
|
||||
:title="t('invoices.dialog.rejectTitle')"
|
||||
width="640px"
|
||||
class="reject-invoice-dialog"
|
||||
:show-close="true"
|
||||
@@ -10,47 +10,47 @@
|
||||
<div class="reject-dialog-content">
|
||||
<div class="reject-alert">
|
||||
<el-icon class="alert-icon"><warning-filled /></el-icon>
|
||||
<span class="alert-text">驳回后,用户将收到通知并需要重新提交申请。</span>
|
||||
<span class="alert-text">{{ t('invoices.dialog.rejectAlert') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-summary" v-if="invoiceData">
|
||||
<div class="summary-item">
|
||||
<span class="label">发票申请号:</span>
|
||||
<span class="value">{{ invoiceData.invoiceNo }}</span>
|
||||
<span class="label">{{ t('invoices.detail.invoiceNo') }}:</span>
|
||||
<span class="value">{{ invoiceData.invoice_no }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">用户信息:</span>
|
||||
<span class="value">{{ invoiceData.user }}</span>
|
||||
<span class="label">{{ t('invoices.detail.userInfo') }}:</span>
|
||||
<span class="value">{{ invoiceData.applicant_name }}</span>
|
||||
</div>
|
||||
<div class="summary-item full-width">
|
||||
<span class="label">发票抬头:</span>
|
||||
<span class="value">{{ invoiceData.invoiceTitle }}</span>
|
||||
<span class="label">{{ t('invoices.detail.invoiceTitle') }}:</span>
|
||||
<span class="value">{{ invoiceData.title }}</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>
|
||||
<span class="section-label required">{{ t('invoices.dialog.reasonLabel') }}</span>
|
||||
<span class="label-tip">{{ t('invoices.dialog.reasonTip') }}</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-form-item prop="reason" :rules="[{ required: true, message: t('invoices.dialog.reasonPlaceholder'), trigger: 'change' }]">
|
||||
<el-select v-model="form.reason" :placeholder="t('invoices.dialog.reasonPlaceholder')" style="width: 100%">
|
||||
<el-option :label="t('invoices.dialog.reasons.title_error')" value="title_error" />
|
||||
<el-option :label="t('invoices.dialog.reasons.tax_id_error')" value="tax_id_error" />
|
||||
<el-option :label="t('invoices.dialog.reasons.attachment_error')" value="attachment_error" />
|
||||
<el-option :label="t('invoices.dialog.reasons.other')" value="other" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<div class="label-row">
|
||||
<span class="section-label">详细说明</span>
|
||||
<span class="section-label">{{ t('invoices.dialog.remarkLabel') }}</span>
|
||||
</div>
|
||||
<el-form-item prop="remark">
|
||||
<el-form-item prop="suggestion">
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
v-model="form.suggestion"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入详细的驳回说明,帮助用户更准确地修改信息"
|
||||
:placeholder="t('invoices.dialog.remarkPlaceholder')"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
@@ -59,8 +59,8 @@
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="danger" @click="handleSubmit">确认驳回</el-button>
|
||||
<el-button @click="visible = false">{{ t('invoices.actions.cancel') }}</el-button>
|
||||
<el-button type="danger" @click="handleSubmit">{{ t('invoices.actions.confirmReject') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -68,9 +68,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { WarningFilled } from '@element-plus/icons-vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
invoiceData: any
|
||||
@@ -82,7 +84,7 @@ const visible = ref(props.modelValue)
|
||||
const formRef = ref()
|
||||
const form = reactive({
|
||||
reason: '',
|
||||
remark: ''
|
||||
suggestion: ''
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
@@ -103,7 +105,6 @@ const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
ElMessage.success('发票申请已驳回')
|
||||
emit('submit', { ...form })
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="上传发票文件"
|
||||
:title="t('invoices.dialog.uploadTitle')"
|
||||
width="680px"
|
||||
class="upload-invoice-dialog"
|
||||
:show-close="true"
|
||||
@@ -11,30 +11,30 @@
|
||||
<!-- 信息摘要 -->
|
||||
<div class="info-summary" v-if="invoiceData">
|
||||
<div class="summary-item">
|
||||
<span class="label">发票申请号:</span>
|
||||
<span class="value">{{ invoiceData.invoiceNo }}</span>
|
||||
<span class="label">{{ t('invoices.detail.invoiceNo') }}:</span>
|
||||
<span class="value">{{ invoiceData.invoice_no }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">关联订单号:</span>
|
||||
<span class="value">{{ invoiceData.orderNo }}</span>
|
||||
<span class="label">{{ t('invoices.detail.orderNo') }}:</span>
|
||||
<span class="value">{{ invoiceData.order_no }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">用户信息:</span>
|
||||
<span class="value">{{ invoiceData.user }} ({{ invoiceData.userTypeLabel || '个人版' }})</span>
|
||||
<span class="label">{{ t('invoices.detail.userInfo') }}:</span>
|
||||
<span class="value">{{ invoiceData.applicant_name }} ({{ invoiceData.order_package_name || '个人版' }})</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">发票抬头:</span>
|
||||
<span class="value">{{ invoiceData.invoiceTitle }}</span>
|
||||
<span class="label">{{ t('invoices.detail.invoiceTitle') }}:</span>
|
||||
<span class="value">{{ invoiceData.title }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">开票金额:</span>
|
||||
<span class="label">{{ t('invoices.detail.amount') }}:</span>
|
||||
<span class="value">¥{{ (invoiceData.amount || 0).toLocaleString(undefined, { minimumFractionDigits: 2 }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传区域 -->
|
||||
<div class="upload-section">
|
||||
<p class="section-label">上传发票文件</p>
|
||||
<p class="section-label">{{ t('invoices.dialog.uploadLabel') }}</p>
|
||||
<el-upload
|
||||
class="invoice-uploader"
|
||||
drag
|
||||
@@ -44,11 +44,11 @@
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
<span class="upload-link">点击上传</span> / 拖拽到此区域
|
||||
<span class="upload-link">{{ t('invoices.dialog.uploadDraggerText').split(' / ')[0] }}</span> / {{ t('invoices.dialog.uploadDraggerText').split(' / ')[1] }}
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持PDF格式。最大文件大小10MB
|
||||
{{ t('invoices.dialog.uploadTip') }}
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
@@ -56,34 +56,13 @@
|
||||
|
||||
<!-- 表单项 -->
|
||||
<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>
|
||||
<!-- 移除了 API 不支持的 issued_at 和 remark 字段 -->
|
||||
</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>
|
||||
<el-button @click="visible = false">{{ t('invoices.actions.cancel') }}</el-button>
|
||||
<el-button type="primary" class="btn-submit" @click="handleSubmit">{{ t('invoices.actions.confirmUpload') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -91,9 +70,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
invoiceData: any
|
||||
@@ -103,25 +84,39 @@ const emit = defineEmits(['update:modelValue', 'submit'])
|
||||
|
||||
const visible = ref(props.modelValue)
|
||||
const form = reactive({
|
||||
finishTime: '2026/01/14 06:20',
|
||||
remark: '',
|
||||
fileList: []
|
||||
fileList: [] as any[]
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
// Reset form when opening
|
||||
form.fileList = []
|
||||
}
|
||||
})
|
||||
|
||||
watch(visible, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const handleChange = (_file: any, fileList: any) => {
|
||||
form.fileList = fileList
|
||||
}
|
||||
|
||||
const handleRemove = (_file: any, fileList: any) => {
|
||||
form.fileList = fileList
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
// Reset form if needed
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
ElMessage.success('发票上传成功')
|
||||
console.log('handleSubmit clicked, fileList:', form.fileList)
|
||||
if (form.fileList.length === 0) {
|
||||
ElMessage.warning(t('invoices.dialog.uploadTip'))
|
||||
return
|
||||
}
|
||||
emit('submit', { ...form })
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
@@ -3,16 +3,24 @@
|
||||
<div class="invoice-container">
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-row">
|
||||
<div v-for="stat in stats" :key="stat.label" class="stat-card" :class="stat.type">
|
||||
<div v-for="stat in stats" :key="stat.key" class="stat-card" :class="stat.type">
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
<div class="stat-label">{{ t(`invoices.stats.${stat.type}`) }}</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>
|
||||
<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>
|
||||
@@ -20,34 +28,29 @@
|
||||
<!-- 搜索和筛选 -->
|
||||
<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 :label="t('invoices.filter.searchLabel')">
|
||||
<el-input v-model="filterForm.keyword" :placeholder="t('invoices.filter.searchPlaceholder')" />
|
||||
</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-form-item :label="t('invoices.filter.statusLabel')">
|
||||
<el-select v-model="filterForm.status" :placeholder="t('invoices.filter.allStatus')" style="width: 150px">
|
||||
<el-option :label="t('invoices.filter.allStatus')" value="" />
|
||||
<el-option :label="t('invoices.filter.pending')" value="pending" />
|
||||
<el-option :label="t('invoices.filter.success')" value="success" />
|
||||
<el-option :label="t('invoices.filter.rejected')" 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 :label="t('invoices.filter.timeLabel')">
|
||||
<el-date-picker v-model="filterForm.dateRange" type="daterange" range-separator="-"
|
||||
:start-placeholder="t('chart.licenseTrend.datePicker.startPlaceholder')"
|
||||
:end-placeholder="t('chart.licenseTrend.datePicker.endPlaceholder')" 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-button @click="resetFilter">{{ t('invoices.filter.reset') }}</el-button>
|
||||
<el-button type="primary" class="btn-filter" @click="handleFilter">{{ t('invoices.filter.query') }}</el-button>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
@@ -57,58 +60,53 @@
|
||||
<div class="table-section" v-loading="loading">
|
||||
<div class="table-header-title">
|
||||
<span class="title-line"></span>
|
||||
<h3>发票申请列表</h3>
|
||||
<h3>{{ t('invoices.table.listTitle') }}</h3>
|
||||
<!-- <el-button
|
||||
type="primary"
|
||||
class="btn-create"
|
||||
@click="handleCreate"
|
||||
style="margin-left: auto"
|
||||
>新增发票申请</el-button
|
||||
>{{ t('invoices.actions.create') }}</el-button
|
||||
> -->
|
||||
</div>
|
||||
<el-table
|
||||
:data="tableData"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
:header-cell-style="{
|
||||
background: '#E6F7FF',
|
||||
color: '#000',
|
||||
borderRight: '1px solid #BAE7FF'
|
||||
backgroundColor: 'var(--app-bg-color)',
|
||||
color: 'var(--app-text-primary)',
|
||||
borderRight: '1px solid var(--app-border-light)'
|
||||
}"
|
||||
:row-style="getRowStyle"
|
||||
>
|
||||
<el-table-column prop="invoiceNo" label="发票申请号" min-width="150" />
|
||||
<el-table-column label="用户" min-width="120">
|
||||
|
||||
<el-table-column prop="invoice_no" :label="t('invoices.table.invoiceNo')" min-width="150" />
|
||||
<el-table-column :label="t('invoices.table.user')" 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>
|
||||
<span class="user-name">{{ row.applicant_name }}</span>
|
||||
<span class="user-type" :class="row.order_package_name">{{ row.order_package_name }}</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">
|
||||
<el-table-column prop="order_no" :label="t('invoices.table.orderNo')" min-width="150" />
|
||||
<el-table-column prop="created_at" :label="t('invoices.table.applyTime')" min-width="180" />
|
||||
<el-table-column :label="t('invoices.table.status')" width="100">
|
||||
<template #default="{ row }">
|
||||
<span class="status-text" :class="row.status">{{ row.statusLabel }}</span>
|
||||
<span class="status-text" :class="row.status">{{ row.status_display }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="发票金额" width="120">
|
||||
<el-table-column prop="amount" :label="t('invoices.table.amount')" width="120">
|
||||
<template #default="{ row }">
|
||||
¥{{ (row.amount || 0).toLocaleString(undefined, { minimumFractionDigits: 2 }) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<el-table-column :label="t('invoices.table.operation')" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" style="color: #019c7c" @click="handleView(row)"
|
||||
>查看</el-button
|
||||
>
|
||||
<el-button link type="primary" style="color: #019c7c" @click="handleView(row)">{{ t('invoices.actions.view') }}</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
|
||||
>
|
||||
<el-button link type="success" style="color: #52c41a" @click="handleUpload(row)">{{ t('invoices.actions.upload') }}</el-button>
|
||||
<el-button link type="danger" style="color: #f5222d" @click="handleReject(row)">{{ t('invoices.actions.reject') }}</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -116,32 +114,18 @@
|
||||
|
||||
<!-- 分页 -->
|
||||
<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 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"
|
||||
/>
|
||||
<UploadInvoiceDialog v-model="uploadVisible" :invoice-data="currentRow" @submit="handleUploadSubmit" />
|
||||
|
||||
<!-- 驳回申请对话框 -->
|
||||
<RejectInvoiceDialog
|
||||
v-model="rejectVisible"
|
||||
:invoice-data="currentRow"
|
||||
@submit="handleRejectSubmit"
|
||||
/>
|
||||
<RejectInvoiceDialog v-model="rejectVisible" :invoice-data="currentRow" @submit="handleRejectSubmit" />
|
||||
|
||||
<!-- 新增申请对话框 -->
|
||||
<el-dialog v-model="createVisible" title="新增发票申请" width="500px">
|
||||
@@ -150,12 +134,7 @@
|
||||
<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-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">
|
||||
@@ -182,18 +161,20 @@ import { Document, Warning, CircleCheck, CircleClose } from '@element-plus/icons
|
||||
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'
|
||||
import { getInvoices, getInvoiceSummary, uploadInvoice, rejectInvoice, type Invoice } from '@/api/invoice'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
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' }
|
||||
{ label: '全部发票申请', value: '0', type: 'all', icon: 'document', key: 'total_count' },
|
||||
{ label: '待处理申请', value: '0', type: 'pending', icon: 'warning', key: 'pending_count' },
|
||||
{ label: '已开票', value: '0', type: 'success', icon: 'success', key: 'completed_count' },
|
||||
{ label: '已驳回', value: '0', type: 'rejected', icon: 'error', key: 'rejected_count' }
|
||||
])
|
||||
|
||||
|
||||
const filterForm = reactive({
|
||||
keyword: '',
|
||||
status: '',
|
||||
@@ -210,16 +191,25 @@ const uploadVisible = ref(false)
|
||||
const rejectVisible = ref(false)
|
||||
const currentRow = ref<any>(null)
|
||||
|
||||
const getRowStyle = ({ rowIndex }: { rowIndex: number }) => {
|
||||
return rowIndex % 2 === 1
|
||||
? { backgroundColor: 'var(--app-bg-color)' }
|
||||
: { backgroundColor: 'var(--app-content-bg)' }
|
||||
}
|
||||
|
||||
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()
|
||||
stats.value.forEach((item: any) => {
|
||||
const value = (res.data as any)[item.key]
|
||||
if (value !== undefined) {
|
||||
item.value = String(value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fetch summary error:', error)
|
||||
}
|
||||
@@ -240,12 +230,18 @@ const fetchData = async () => {
|
||||
}
|
||||
const res = await getInvoices(params)
|
||||
if (res.code === '000000') {
|
||||
tableData.value = res.data.list
|
||||
totalCount.value = res.data.total
|
||||
fetchSummary()
|
||||
res.data.invoices.forEach((item: any) => {
|
||||
item.created_at = formatDateTime(item.created_at)
|
||||
})
|
||||
tableData.value = res.data.invoices
|
||||
totalCount.value = res.data.total_count
|
||||
}
|
||||
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Fetch invoices error:', error)
|
||||
ElMessage.error(error.backendMessage || '获取数据失败')
|
||||
ElMessage.error(error.backendMessage || t('invoices.messages.fetchError'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -253,7 +249,7 @@ const fetchData = async () => {
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
fetchSummary()
|
||||
|
||||
})
|
||||
|
||||
const createForm = reactive({
|
||||
@@ -267,7 +263,7 @@ const handleCreate = () => {
|
||||
}
|
||||
|
||||
const submitCreate = () => {
|
||||
ElMessage.success('提交申请成功')
|
||||
ElMessage.success(t('invoices.messages.createSuccess'))
|
||||
createVisible.value = false
|
||||
}
|
||||
|
||||
@@ -285,7 +281,7 @@ const resetFilter = () => {
|
||||
}
|
||||
|
||||
const handleView = (row: any) => {
|
||||
router.push(`/invoices/detail/${row.id || row.invoiceNo}`)
|
||||
router.push(`/invoices/detail/${row.id || row.invoice_no}`)
|
||||
}
|
||||
|
||||
const handleUpload = (row: any) => {
|
||||
@@ -293,10 +289,35 @@ const handleUpload = (row: any) => {
|
||||
uploadVisible.value = true
|
||||
}
|
||||
|
||||
const handleUploadSubmit = (data: any) => {
|
||||
console.log('Upload Submit:', data)
|
||||
fetchData()
|
||||
fetchSummary()
|
||||
const handleUploadSubmit = async (data: any) => {
|
||||
console.log('handleUploadSubmit triggered:', data)
|
||||
if (!currentRow.value || !currentRow.value.invoice_no) {
|
||||
console.error('No current row or invoice_no')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('invoice_no', currentRow.value.id)
|
||||
if (data.fileList && data.fileList.length > 0) {
|
||||
const file = data.fileList[0].raw
|
||||
console.log('File to upload:', file)
|
||||
formData.append('file', file)
|
||||
} else {
|
||||
ElMessage.warning(t('invoices.dialog.uploadTip'))
|
||||
return
|
||||
}
|
||||
|
||||
const res = await uploadInvoice(formData)
|
||||
console.log('Upload response:', res)
|
||||
if (res.code === '000000') {
|
||||
ElMessage.success(t('invoices.messages.uploadSuccess'))
|
||||
fetchData()
|
||||
fetchSummary()
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Upload invoice error:', error)
|
||||
ElMessage.error(error.backendMessage || t('invoices.messages.fetchError'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleReject = (row: any) => {
|
||||
@@ -304,10 +325,23 @@ const handleReject = (row: any) => {
|
||||
rejectVisible.value = true
|
||||
}
|
||||
|
||||
const handleRejectSubmit = (data: any) => {
|
||||
console.log('Reject Submit:', data)
|
||||
fetchData()
|
||||
fetchSummary()
|
||||
const handleRejectSubmit = async (data: any) => {
|
||||
if (!currentRow.value?.id) return
|
||||
try {
|
||||
const reasonText = data.reason === 'other' ? data.suggestion : t(`invoices.dialog.reasons.${data.reason}`)
|
||||
const res = await rejectInvoice(currentRow.value.id, {
|
||||
reject_reason: reasonText,
|
||||
suggestion: data.suggestion
|
||||
})
|
||||
if (res.code === '000000') {
|
||||
ElMessage.success(t('invoices.messages.rejectSuccess'))
|
||||
fetchData()
|
||||
fetchSummary()
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Reject error:', error)
|
||||
ElMessage.error(error.backendMessage || t('invoices.messages.fetchError'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleSizeChange = (val: number) => {
|
||||
@@ -324,10 +358,11 @@ const handleCurrentChange = (val: number) => {
|
||||
<style lang="scss" scoped>
|
||||
.invoice-container {
|
||||
padding: 24px;
|
||||
background-color: #f0f2f5;
|
||||
background-color: var(--app-bg-color);
|
||||
min-height: calc(100vh - 80px);
|
||||
}
|
||||
|
||||
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
@@ -336,26 +371,26 @@ const handleCurrentChange = (val: number) => {
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
background: var(--app-content-bg);
|
||||
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;
|
||||
box-shadow: var(--app-shadow);
|
||||
border: 1px solid var(--app-border-light);
|
||||
height: 96px;
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
color: var(--app-text-secondary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #019c7c;
|
||||
color: var(--el-color-primary);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@@ -370,31 +405,35 @@ const handleCurrentChange = (val: number) => {
|
||||
}
|
||||
|
||||
&.all .stat-icon-wrapper {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&.pending .stat-icon-wrapper {
|
||||
background: #fff7e6;
|
||||
color: #faad14;
|
||||
background: var(--el-color-warning-light-9);
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
|
||||
&.success .stat-icon-wrapper {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
background: var(--el-color-success-light-9);
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
&.rejected .stat-icon-wrapper {
|
||||
background: #f9f0ff;
|
||||
color: #722ed1;
|
||||
background: var(--el-color-danger-light-9);
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.filter-section {
|
||||
background: #fff;
|
||||
background: var(--app-content-bg);
|
||||
padding: 16px 24px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #f0f0f0;
|
||||
border: 1px solid var(--app-border-light);
|
||||
|
||||
.filter-form {
|
||||
width: 100%;
|
||||
@@ -408,7 +447,7 @@ const handleCurrentChange = (val: number) => {
|
||||
|
||||
.el-form-item__label {
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
color: var(--app-text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -420,6 +459,7 @@ const handleCurrentChange = (val: number) => {
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-right: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
@@ -427,10 +467,12 @@ const handleCurrentChange = (val: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.selected-count {
|
||||
margin-right: 24px;
|
||||
|
||||
.count-badge {
|
||||
background: #ff4d4f;
|
||||
background: var(--el-color-danger);
|
||||
color: #fff;
|
||||
padding: 0 12px;
|
||||
border-radius: 4px;
|
||||
@@ -442,23 +484,28 @@ const handleCurrentChange = (val: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.btn-filter {
|
||||
background-color: #019c7c;
|
||||
border-color: #019c7c;
|
||||
background-color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
padding: 8px 24px;
|
||||
|
||||
&:hover {
|
||||
background-color: #017c63;
|
||||
border-color: #017c63;
|
||||
background-color: var(--el-color-primary-dark-2);
|
||||
border-color: var(--el-color-primary-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.table-section {
|
||||
background: #fff;
|
||||
background: var(--app-content-bg);
|
||||
padding: 24px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border: 1px solid var(--app-border-light);
|
||||
box-shadow: var(--app-shadow);
|
||||
}
|
||||
|
||||
|
||||
.table-header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -494,10 +541,12 @@ const handleCurrentChange = (val: number) => {
|
||||
border-radius: 2px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
|
||||
&.personal {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
&.enterprise {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
@@ -509,9 +558,11 @@ const handleCurrentChange = (val: number) => {
|
||||
&.success {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
&.pending {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
&.rejected {
|
||||
color: #bfbfbf;
|
||||
}
|
||||
@@ -538,6 +589,7 @@ const handleCurrentChange = (val: number) => {
|
||||
color: #262626;
|
||||
height: 54px;
|
||||
border-right: 1px solid #bae7ff;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
@@ -557,8 +609,10 @@ const handleCurrentChange = (val: number) => {
|
||||
.stats-row {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 16px;
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
<template>
|
||||
<div class="loading_box">
|
||||
<el-dialog v-model="visible" :title="t('packages.edit.title')" width="700px" class="package-edit-dialog"
|
||||
destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="edit-form">
|
||||
<div class="form-row">
|
||||
<el-form-item :label="t('packages.edit.name')" prop="name" class="flex-1">
|
||||
<el-input v-model="form.name" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('packages.edit.type')" prop="type" class="flex-1">
|
||||
<el-select v-model="form.type" class="w-full">
|
||||
<el-option label="试用版" value="trial" />
|
||||
<el-option label="基础版" value="basic" />
|
||||
<el-option label="专业版" value="pro" />
|
||||
<el-option label="定制版" value="custom" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<el-form-item :label="t('packages.edit.price')" prop="price" class="flex-1">
|
||||
<el-input v-model="form.price" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('packages.edit.cycle')" prop="cycle" class="flex-1">
|
||||
<el-input v-model="form.cycle" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-form-item :label="t('packages.edit.description')" prop="description">
|
||||
<el-input v-model="form.description" type="textarea" :rows="4" maxlength="500" show-word-limit
|
||||
:placeholder="t('packages.edit.descPlaceholder')" />
|
||||
</el-form-item>
|
||||
|
||||
<div class="features-section">
|
||||
<label class="section-label">{{ t('packages.edit.features') }}</label>
|
||||
<div class="features-list">
|
||||
<div v-for="(feature, index) in form.features" :key="index" class="feature-item">
|
||||
<el-input v-model="form.features[index]"
|
||||
:placeholder="t('packages.edit.featurePlaceholder')" />
|
||||
<el-button class="btn-remove" @click="removeFeature(index)">
|
||||
<el-icon>
|
||||
<Minus />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button class="btn-add" @click="addFeature">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row mt-20">
|
||||
<el-form-item :label="t('packages.edit.status')" prop="status" class="flex-1">
|
||||
<el-select v-model="form.status" class="w-full">
|
||||
<el-option :label="t('packages.status.enabled')" value="enabled" />
|
||||
<el-option :label="t('packages.status.disabled')" value="disabled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('packages.edit.sort')" prop="sort" class="flex-1">
|
||||
<el-input-number v-model="form.sort" :min="1" class="w-full" controls-position="right" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-form-item :label="t('packages.edit.remark')" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="4" maxlength="500" show-word-limit
|
||||
:placeholder="t('packages.edit.remarkPlaceholder')" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="visible = false">{{ t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" class="btn-save" @click="handleSave">
|
||||
{{ t('packages.edit.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Plus, Minus } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
data: any
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save'])
|
||||
|
||||
const { t } = useI18n()
|
||||
const visible = ref(props.modelValue)
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
type: '',
|
||||
price: '',
|
||||
cycle: '',
|
||||
description: '',
|
||||
features: [] as string[],
|
||||
status: 'enabled',
|
||||
sort: 1,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: t('common.required'), trigger: 'blur' }],
|
||||
type: [{ required: true, message: t('common.required'), trigger: 'change' }],
|
||||
price: [{ required: true, message: t('common.required'), trigger: 'blur' }],
|
||||
cycle: [{ required: true, message: t('common.required'), trigger: 'blur' }],
|
||||
description: [{ required: true, message: t('common.required'), trigger: 'blur' }],
|
||||
status: [{ required: true, message: t('common.required'), trigger: 'change' }],
|
||||
sort: [{ required: true, message: t('common.required'), trigger: 'blur' }]
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val && props.data) {
|
||||
form.name = props.data.name || ''
|
||||
form.type = props.data.type || 'trial'
|
||||
form.price = props.data.price || ''
|
||||
form.cycle = props.data.cycle || ''
|
||||
form.description = props.data.description || '体验完整功能,适合新用户试用评估'
|
||||
form.features = props.data.features || ['全部功能试用', '支持1台设备激活', '基础功能支持', '永久授权', '高级功能', '离线授权支持']
|
||||
form.status = props.data.status || 'enabled'
|
||||
form.sort = props.data.sort || 6
|
||||
form.remark = props.data.remark || ''
|
||||
}
|
||||
})
|
||||
|
||||
watch(visible, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const addFeature = () => {
|
||||
form.features.push('')
|
||||
}
|
||||
|
||||
const removeFeature = (index: number) => {
|
||||
form.features.splice(index, 1)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
emit('save', { ...props.data, ...form })
|
||||
visible.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
:deep(.el-dialog__headerbtn) {
|
||||
top: 10px !important;
|
||||
}
|
||||
|
||||
:deep(.el-dialog) {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
|
||||
.el-dialog__header {
|
||||
margin-right: 0;
|
||||
padding: 20px 24px;
|
||||
background: linear-gradient(90deg, #00928A 0%, #00D19E 100%) !important;
|
||||
border-bottom: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.el-dialog__title {
|
||||
color: #fff !important;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.el-dialog__headerbtn {
|
||||
top: 20px;
|
||||
|
||||
.el-dialog__close {
|
||||
color: #fff !important;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-form {
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
padding-bottom: 8px;
|
||||
|
||||
&::before {
|
||||
color: #f56c6c;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper),
|
||||
:deep(.el-textarea__inner) {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.features-section {
|
||||
.section-label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
|
||||
&::before {
|
||||
content: '*';
|
||||
color: #f56c6c;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.features-list {
|
||||
background: #f9f9f9;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-remove {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border: 1px dashed #ccc;
|
||||
color: #999;
|
||||
|
||||
&:hover {
|
||||
color: #f56c6c;
|
||||
border-color: #f56c6c;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
border: 1px dashed #ccc;
|
||||
color: #999;
|
||||
align-self: flex-start;
|
||||
|
||||
&:hover {
|
||||
color: #00a870;
|
||||
border-color: #00a870;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
:deep(.el-dialog__footer){
|
||||
padding-top: 0px !important;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-top: 0px !important;
|
||||
padding-bottom: 20px !important;
|
||||
|
||||
.el-button {
|
||||
padding: 6px 32px;
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
background-color: #00a870 !important;
|
||||
border-color: #00a870 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<Layout :page-title="t('packages.title')">
|
||||
<div class="packages-page">
|
||||
<div class="list-card">
|
||||
<div class="list-header">
|
||||
<div class="list-title">
|
||||
<span class="title-bar"></span>
|
||||
<span>{{ t('packages.table.title') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="tableData"
|
||||
stripe
|
||||
class="packages-table"
|
||||
:header-cell-style="{
|
||||
backgroundColor: '#E6F7F3',
|
||||
color: '#4F4F4F',
|
||||
fontWeight: '600',
|
||||
height: '50px'
|
||||
}"
|
||||
>
|
||||
<el-table-column prop="id" :label="t('packages.table.id')" width="100" />
|
||||
<el-table-column prop="name" :label="t('packages.table.name')" min-width="150" />
|
||||
<el-table-column prop="price" :label="t('packages.table.price')" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'price-free': row.price === '免费' }">{{ row.price }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="cycle" :label="t('packages.table.cycle')" min-width="180" />
|
||||
<el-table-column prop="createdAt" :label="t('packages.table.createdAt')" min-width="180" />
|
||||
<el-table-column prop="updatedAt" :label="t('packages.table.updatedAt')" min-width="180" />
|
||||
<el-table-column :label="t('packages.table.status')" width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="status-tag" :class="row.status">{{ t(`packages.status.${row.status}`) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('packages.table.actions')" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" class="btn-edit" @click="handleEdit(row)">
|
||||
{{ t('enterpriseLeads.actions.edit') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PackageEditDialog
|
||||
v-model="editVisible"
|
||||
:data="selectedPackage"
|
||||
@save="handleUpdate"
|
||||
/>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Layout from '@/components/common/layout/Layout.vue'
|
||||
import PackageEditDialog from './components/PackageEditDialog.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const tableData = ref([
|
||||
{
|
||||
id: '1',
|
||||
name: '试用版',
|
||||
price: '免费',
|
||||
cycle: '有效期至本月25日',
|
||||
createdAt: '2024-01-15 10:30',
|
||||
updatedAt: '2024-01-15 10:30',
|
||||
status: 'enabled',
|
||||
type: 'trial',
|
||||
sort: 1
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '基础版',
|
||||
price: '¥300',
|
||||
cycle: '永久授权 (V1/V2/V3)',
|
||||
createdAt: '2024-01-15 10:30',
|
||||
updatedAt: '2024-01-15 10:30',
|
||||
status: 'enabled',
|
||||
type: 'basic',
|
||||
sort: 2
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '专业版',
|
||||
price: '¥2000',
|
||||
cycle: '永久授权 (V1/V2/V3)',
|
||||
createdAt: '2024-01-15 10:30',
|
||||
updatedAt: '2024-01-15 10:30',
|
||||
status: 'enabled',
|
||||
type: 'pro',
|
||||
sort: 3
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: '定制版',
|
||||
price: '定制方案',
|
||||
cycle: '按需定价',
|
||||
createdAt: '2024-01-15 10:30',
|
||||
updatedAt: '2024-01-15 10:30',
|
||||
status: 'enabled',
|
||||
type: 'custom',
|
||||
sort: 4
|
||||
}
|
||||
])
|
||||
|
||||
const editVisible = ref(false)
|
||||
const selectedPackage = ref<any>(null)
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
selectedPackage.value = row
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
const handleUpdate = (updatedData: any) => {
|
||||
const index = tableData.value.findIndex(item => item.id === updatedData.id)
|
||||
if (index !== -1) {
|
||||
tableData.value[index] = { ...tableData.value[index], ...updatedData }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.packages-page {
|
||||
padding: 24px;
|
||||
background-color: #f0f2f5;
|
||||
min-height: calc(100vh - 80px);
|
||||
}
|
||||
|
||||
.list-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
background: #00a870;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
&.enabled {
|
||||
color: #409eff;
|
||||
}
|
||||
&.disabled {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
border: none;
|
||||
background: #e6f7f3 !important;
|
||||
color: #00a870 !important;
|
||||
padding: 4px 12px;
|
||||
height: 28px;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.price-free {
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user