Merge branch 'main' of github.com:cedar-v/license-manager
This commit is contained in:
commit
eba7baaa5f
13 files changed
+696
-237
No files matched your search
@@ -6,6 +6,7 @@ import { errorCodeType } from './errorCodeType'
|
||||
import { generateAcceptLanguageHeader } from '@/utils/language'
|
||||
// 路由
|
||||
import router from '@/router'
|
||||
export const envUrl1 = import.meta.env.VITE_API_BASE_URL
|
||||
|
||||
const envUrl = import.meta.env.VITE_API_BASE_URL
|
||||
console.log('API Base URL:', envUrl)
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface Invoice {
|
||||
order_package_name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
status: 'pending' | 'completed' | 'rejected';
|
||||
status: 'pending' | 'success' | 'rejected';
|
||||
status_display: string;
|
||||
amount: number;
|
||||
title: string;
|
||||
@@ -117,3 +117,11 @@ export function uploadInvoice(data: FormData): Promise<ApiResponse> {
|
||||
export function rejectInvoice(id: string, data: any): Promise<ApiResponse> {
|
||||
return Axios.post(`/api/v1/invoices/${id}/reject`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员发票开票
|
||||
*/
|
||||
export function issueInvoice(id: string, data: { invoice_file_url: string, issued_at: string }): Promise<ApiResponse> {
|
||||
return Axios.post(`/api/v1/invoices/${id}/issue`, data)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import Axios from './https'
|
||||
|
||||
// 企业线索接口类型定义
|
||||
export interface Lead {
|
||||
id: string;
|
||||
lead_no: string;
|
||||
company_name: string;
|
||||
contact_name: string;
|
||||
contact_phone: string;
|
||||
contact_email: string;
|
||||
requirement: string;
|
||||
extra_info: string;
|
||||
status: 'pending' | 'contacting' | 'completed' | 'rejected';
|
||||
follow_up_date: string | null;
|
||||
follow_up_record: string;
|
||||
internal_note: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 查询请求参数
|
||||
export interface LeadQueryRequest {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
search?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
// 通用响应接口
|
||||
export interface ApiResponse<T = any> {
|
||||
code: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
// 分页数据响应接口
|
||||
export interface LeadQueryResponse {
|
||||
code: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
data: {
|
||||
leads: Lead[];
|
||||
total_count: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取企业线索列表
|
||||
*/
|
||||
export function getLeads(params?: LeadQueryRequest): Promise<LeadQueryResponse> {
|
||||
return Axios.get('/api/leads', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取企业线索统计数据 (假设有这个接口,如果没有则前端计算或者等后续补充)
|
||||
*/
|
||||
export function getLeadSummary(): Promise<ApiResponse<any>> {
|
||||
return Axios.get('/api/leads/summary')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取企业线索详情
|
||||
*/
|
||||
export function getLeadDetail(id: string | number): Promise<ApiResponse<Lead>> {
|
||||
return Axios.get(`/api/leads/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新企业线索
|
||||
*/
|
||||
export function updateLead(id: string | number, data: Partial<Lead>): Promise<ApiResponse<Lead>> {
|
||||
return Axios.put(`/api/leads/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除企业线索
|
||||
*/
|
||||
export function deleteLead(id: string | number): Promise<ApiResponse<any>> {
|
||||
return Axios.delete(`/api/leads/${id}`)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import Axios from './https'
|
||||
|
||||
// 套餐接口类型定义
|
||||
export interface Package {
|
||||
id: string | number;
|
||||
name: string;
|
||||
price: number;
|
||||
price_description?: string;
|
||||
duration_description: string;
|
||||
status: number; // 1: enabled, 2: disabled
|
||||
type: string;
|
||||
sort_order: number;
|
||||
description?: string;
|
||||
features?: any;
|
||||
remark?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 套餐列表响应定义
|
||||
export interface PackageListResponse {
|
||||
packages: Package[];
|
||||
total_count: number;
|
||||
}
|
||||
|
||||
// 通用响应接口
|
||||
export interface ApiResponse<T = any> {
|
||||
code: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取套餐列表
|
||||
*/
|
||||
export function getPackages(): Promise<ApiResponse<PackageListResponse>> {
|
||||
return Axios.get('/api/packages')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取套餐详情
|
||||
*/
|
||||
export function getPackageDetail(id: string | number): Promise<ApiResponse<Package>> {
|
||||
return Axios.get(`/api/packages/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新套餐
|
||||
*/
|
||||
export function updatePackage(id: string | number, data: Partial<Package>): Promise<ApiResponse<Package>> {
|
||||
return Axios.put(`/api/packages/${id}`, data)
|
||||
}
|
||||
@@ -152,6 +152,19 @@
|
||||
"recordPlaceholder": "记录与客户的沟通情况,反馈,下一步计划...",
|
||||
"remarkPlaceholder": "内部使用的备注信息,客户不可见",
|
||||
"statusRequired": "请选择状态"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "请输入企业名称",
|
||||
"contactRequired": "请输入联系人"
|
||||
},
|
||||
"messages": {
|
||||
"fetchError": "获取列表失败",
|
||||
"fetchDetailError": "获取详情失败",
|
||||
"updateSuccess": "更新成功",
|
||||
"updateError": "更新失败",
|
||||
"deleteSuccess": "删除成功",
|
||||
"deleteError": "删除失败",
|
||||
"deleteConfirm": "确认删除该线索吗?此操作不可撤销。"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -162,6 +175,8 @@
|
||||
"id": "套餐ID",
|
||||
"name": "套餐名称",
|
||||
"price": "价格",
|
||||
"free": "免费",
|
||||
"customPlan": "定制方案",
|
||||
"cycle": "周期",
|
||||
"createdAt": "创建时间",
|
||||
"updatedAt": "更新时间",
|
||||
@@ -179,12 +194,14 @@
|
||||
"price": "价格",
|
||||
"cycle": "周期/描述",
|
||||
"description": "套餐描述",
|
||||
"priceDescription": "价格描述",
|
||||
"features": "套餐特性",
|
||||
"status": "状态",
|
||||
"sort": "显示顺序",
|
||||
"remark": "备注",
|
||||
"save": "保存更改",
|
||||
"descPlaceholder": "体验完整功能,适合新用户试用评估",
|
||||
"priceDescPlaceholder": "例如:定制方案 / 按需定价",
|
||||
"remarkPlaceholder": "请输入备注",
|
||||
"featurePlaceholder": "请输入特性内容"
|
||||
}
|
||||
@@ -196,7 +213,7 @@
|
||||
"stats": {
|
||||
"all": "全部发票申请",
|
||||
"pending": "待处理申请",
|
||||
"success": "已开票",
|
||||
"completed": "已开票",
|
||||
"rejected": "已驳回"
|
||||
},
|
||||
"filter": {
|
||||
@@ -205,7 +222,7 @@
|
||||
"statusLabel": "开票状态:",
|
||||
"allStatus": "全部状态",
|
||||
"pending": "待处理",
|
||||
"success": "已开票",
|
||||
"completed": "已开票",
|
||||
"rejected": "已驳回",
|
||||
"timeLabel": "申请时间:",
|
||||
"reset": "重置",
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<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">
|
||||
<el-dialog v-model="visible" :title="t('enterpriseLeads.detail.title', { company: detailData?.company_name })"
|
||||
width="800px" class="lead-detail-dialog" destroy-on-close v-loading="loading">
|
||||
<div v-if="detailData" 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>
|
||||
<span class="value">{{ detailData.company_name }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">{{ t('enterpriseLeads.table.contact') }}:</span>
|
||||
<span class="value">{{ data.contact }}</span>
|
||||
<span class="value">{{ detailData.contact_name }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">{{ t('enterpriseLeads.table.phone') }}:</span>
|
||||
<span class="value">{{ data.phone }}</span>
|
||||
<span class="value">{{ detailData.contact_phone }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -28,16 +28,15 @@
|
||||
<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>
|
||||
<span class="value">{{ detailData.contact_email || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item block">
|
||||
<span class="label">{{ t('enterpriseLeads.detail.description') }}:</span>
|
||||
<span class="value">{{ data.description ||
|
||||
'我们需要为生产线上的500台设备部署专业版授权,同时为办公室的200台电脑部署基础版授权。需要支持离线授权功能,部署时间为下个月初。' }}</span>
|
||||
<span class="value">{{ detailData.requirement || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item block">
|
||||
<span class="label">{{ t('enterpriseLeads.detail.otherInfo') }}:</span>
|
||||
<span class="value">{{ data.otherInfo || '预算范围在50-80万之间,决策时间约2周。' }}</span>
|
||||
<span class="value">{{ detailData.extra_info || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,27 +47,26 @@
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="label">{{ t('enterpriseLeads.table.submittedAt') }}:</span>
|
||||
<span class="value">{{ data.submittedAt }}</span>
|
||||
<span class="value">{{ detailData.created_at }}</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>
|
||||
<span class="status-value" :class="detailData.status">{{
|
||||
t(`enterpriseLeads.status.${detailData.status}`) }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">{{ t('enterpriseLeads.detail.followUpDate') }}:</span>
|
||||
<span class="value">{{ data.followUpDate || '2025-12-21' }}</span>
|
||||
<span class="value">{{ detailData.follow_up_date || '-' }}</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>
|
||||
<span class="value">{{ detailData.follow_up_record || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item block">
|
||||
<span class="label">{{ t('enterpriseLeads.detail.internalRemark') }}:</span>
|
||||
<span class="value">{{ data.internalRemark || '高价值客户,需要安排技术专家进行演示。' }}</span>
|
||||
<span class="value">{{ detailData.internal_note || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,19 +78,47 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getLeadDetail, type Lead } from '@/api/lead'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
data: any
|
||||
id: string | number | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const { t } = useI18n()
|
||||
const visible = ref(props.modelValue)
|
||||
const loading = ref(false)
|
||||
const detailData = ref<Lead | null>(null)
|
||||
|
||||
const fetchDetail = async (id: string | number) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getLeadDetail(id)
|
||||
if (res.code === '000000' && res.data) {
|
||||
const data = res.data
|
||||
detailData.value = {
|
||||
...data,
|
||||
created_at: formatDateTime(data.created_at || ''),
|
||||
follow_up_date: data.follow_up_date ? formatDateTime(data.follow_up_date) : null
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Fetch lead detail error:', error)
|
||||
ElMessage.error(error.backendMessage || t('enterpriseLeads.messages.fetchDetailError'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val && props.id) {
|
||||
fetchDetail(props.id)
|
||||
}
|
||||
})
|
||||
|
||||
watch(visible, (val) => {
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
<div>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="t('enterpriseLeads.edit.title', { company: data?.company })"
|
||||
:title="t('enterpriseLeads.edit.title', { company: detailData?.company_name })"
|
||||
width="800px"
|
||||
class="lead-edit-dialog"
|
||||
destroy-on-close
|
||||
v-loading="loading"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
@@ -14,6 +15,42 @@
|
||||
label-position="top"
|
||||
class="edit-form"
|
||||
>
|
||||
<div class="form-row">
|
||||
<el-form-item :label="t('enterpriseLeads.table.company')" prop="company_name" class="flex-1">
|
||||
<el-input v-model="form.company_name" :placeholder="t('enterpriseLeads.table.company')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('enterpriseLeads.table.contact')" prop="contact_name" class="flex-1">
|
||||
<el-input v-model="form.contact_name" :placeholder="t('enterpriseLeads.table.contact')" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<el-form-item :label="t('enterpriseLeads.table.phone')" prop="contact_phone" class="flex-1">
|
||||
<el-input v-model="form.contact_phone" :placeholder="t('enterpriseLeads.table.phone')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('enterpriseLeads.detail.email')" prop="contact_email" class="flex-1">
|
||||
<el-input v-model="form.contact_email" :placeholder="t('enterpriseLeads.detail.email')" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-form-item :label="t('enterpriseLeads.detail.description')" prop="requirement">
|
||||
<el-input
|
||||
v-model="form.requirement"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:placeholder="t('enterpriseLeads.detail.description')"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('enterpriseLeads.detail.otherInfo')" prop="extra_info">
|
||||
<el-input
|
||||
v-model="form.extra_info"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
:placeholder="t('enterpriseLeads.detail.otherInfo')"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<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')">
|
||||
@@ -23,9 +60,9 @@
|
||||
<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-form-item :label="t('enterpriseLeads.detail.followUpDate')" prop="follow_up_date" class="flex-1">
|
||||
<el-date-picker
|
||||
v-model="form.followUpDate"
|
||||
v-model="form.follow_up_date"
|
||||
type="date"
|
||||
class="w-full"
|
||||
:placeholder="t('enterpriseLeads.edit.datePlaceholder')"
|
||||
@@ -35,9 +72,9 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-form-item :label="t('enterpriseLeads.detail.followUpRecord')" prop="followUpRecord">
|
||||
<el-form-item :label="t('enterpriseLeads.detail.followUpRecord')" prop="follow_up_record">
|
||||
<el-input
|
||||
v-model="form.followUpRecord"
|
||||
v-model="form.follow_up_record"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="500"
|
||||
@@ -46,9 +83,9 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('enterpriseLeads.detail.internalRemark')" prop="internalRemark">
|
||||
<el-form-item :label="t('enterpriseLeads.detail.internalRemark')" prop="internal_note">
|
||||
<el-input
|
||||
v-model="form.internalRemark"
|
||||
v-model="form.internal_note"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="500"
|
||||
@@ -74,10 +111,12 @@
|
||||
import { ref, watch, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { getLeadDetail, type Lead } from '@/api/lead'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
data: any
|
||||
id: string | number | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save'])
|
||||
@@ -85,25 +124,58 @@ const emit = defineEmits(['update:modelValue', 'save'])
|
||||
const { t } = useI18n()
|
||||
const visible = ref(props.modelValue)
|
||||
const formRef = ref<FormInstance>()
|
||||
const loading = ref(false)
|
||||
const detailData = ref<Lead | null>(null)
|
||||
|
||||
const form = reactive({
|
||||
company_name: '',
|
||||
contact_email: '',
|
||||
contact_name: '',
|
||||
contact_phone: '',
|
||||
extra_info: '',
|
||||
requirement: '',
|
||||
status: '',
|
||||
followUpDate: '',
|
||||
followUpRecord: '',
|
||||
internalRemark: ''
|
||||
follow_up_date: '',
|
||||
follow_up_record: '',
|
||||
internal_note: ''
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
status: [{ required: true, message: t('enterpriseLeads.edit.statusRequired'), trigger: 'change' }]
|
||||
status: [{ required: true, message: t('enterpriseLeads.edit.statusRequired'), trigger: 'change' }],
|
||||
company_name: [{ required: true, message: t('enterpriseLeads.validation.nameRequired'), trigger: 'blur' }],
|
||||
contact_name: [{ required: true, message: t('enterpriseLeads.validation.contactRequired'), trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const fetchDetail = async (id: string | number) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getLeadDetail(id)
|
||||
if (res.code === '000000' && res.data) {
|
||||
const data = res.data
|
||||
detailData.value = data
|
||||
form.company_name = data.company_name || ''
|
||||
form.contact_email = data.contact_email || ''
|
||||
form.contact_name = data.contact_name || ''
|
||||
form.contact_phone = data.contact_phone || ''
|
||||
form.extra_info = data.extra_info || ''
|
||||
form.requirement = data.requirement || ''
|
||||
form.status = data.status
|
||||
form.follow_up_date = data.follow_up_date || ''
|
||||
form.follow_up_record = data.follow_up_record || ''
|
||||
form.internal_note = data.internal_note || ''
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Fetch lead detail error:', error)
|
||||
ElMessage.error(error.backendMessage || t('enterpriseLeads.messages.fetchDetailError'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
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 || ''
|
||||
if (val && props.id) {
|
||||
fetchDetail(props.id)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -115,7 +187,7 @@ const handleSave = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
emit('save', { ...props.data, ...form })
|
||||
emit('save', { id: props.id, ...form })
|
||||
visible.value = false
|
||||
}
|
||||
})
|
||||
|
||||
@@ -21,16 +21,17 @@
|
||||
:placeholder="t('enterpriseLeads.filter.searchPlaceholder')"
|
||||
class="search-input"
|
||||
clearable
|
||||
@keyup.enter="handleFilter"
|
||||
>
|
||||
<template #append>
|
||||
<el-button class="search-btn">
|
||||
<el-button class="search-btn" @click="handleFilter">
|
||||
<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-select v-model="filters.status" class="status-select" :placeholder="t('enterpriseLeads.filter.statusPlaceholder')" clearable @change="handleFilter">
|
||||
<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" />
|
||||
@@ -41,13 +42,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-card">
|
||||
<div class="list-card" v-loading="loading">
|
||||
<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>
|
||||
<el-button class="refresh-btn" :icon="Refresh" text @click="handleRefresh">
|
||||
{{ t('enterpriseLeads.actions.refresh') }}
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -62,12 +63,12 @@
|
||||
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">
|
||||
<el-table-column prop="id" :label="t('enterpriseLeads.table.id')" />
|
||||
<el-table-column prop="company_name" :label="t('enterpriseLeads.table.company')" />
|
||||
<el-table-column prop="contact_name" :label="t('enterpriseLeads.table.contact')" width="100" />
|
||||
<el-table-column prop="contact_phone" :label="t('enterpriseLeads.table.phone')" />
|
||||
<el-table-column prop="created_at" :label="t('enterpriseLeads.table.submittedAt')" />
|
||||
<el-table-column :label="t('enterpriseLeads.table.status')" width="80">
|
||||
<template #default="{ row }">
|
||||
<span class="status-tag" :class="row.status">{{ t(`enterpriseLeads.status.${row.status}`) }}</span>
|
||||
</template>
|
||||
@@ -77,7 +78,7 @@
|
||||
<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>
|
||||
<el-button size="small" class="btn-delete" @click="handleDelete(row)">{{ t('enterpriseLeads.actions.delete') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -98,32 +99,35 @@
|
||||
|
||||
<LeadDetailDialog
|
||||
v-model="detailVisible"
|
||||
:data="selectedLead"
|
||||
:id="selectedLead?.id"
|
||||
/>
|
||||
|
||||
<LeadEditDialog
|
||||
v-model="editVisible"
|
||||
:data="selectedLead"
|
||||
:id="selectedLead?.id"
|
||||
@save="handleUpdate"
|
||||
/>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted, watch } 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 { Phone, CircleCheck, Refresh, User, OfficeBuilding, Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import LeadDetailDialog from './components/LeadDetailDialog.vue'
|
||||
import LeadEditDialog from './components/LeadEditDialog.vue'
|
||||
import { getLeads, getLeadSummary, updateLead, deleteLead, type Lead } from '@/api/lead'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
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 }
|
||||
{ key: 'total', value: 0, icon: OfficeBuilding, field: 'total_count' },
|
||||
{ key: 'pending', value: 0, icon: Phone, field: 'pending_count' },
|
||||
{ key: 'contacting', value: 0, icon: CircleCheck, field: 'contacted_count' },
|
||||
{ key: 'completed', value: 0, icon: User, field: 'converted_count' }
|
||||
])
|
||||
|
||||
const filters = ref({
|
||||
@@ -131,57 +135,75 @@ const filters = ref({
|
||||
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 tableData = ref<Lead[]>([])
|
||||
const loading = ref(false)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(21978)
|
||||
const total = ref(0)
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const editVisible = ref(false)
|
||||
const selectedLead = ref<any>(null)
|
||||
|
||||
const fetchSummary = async () => {
|
||||
try {
|
||||
const res = await getLeadSummary()
|
||||
if (res.code === '000000' && res.data) {
|
||||
stats.value.forEach(item => {
|
||||
if (res.data[item.field] !== undefined) {
|
||||
item.value = res.data[item.field]
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fetch lead summary error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
page_size: pageSize.value,
|
||||
search: filters.value.keyword,
|
||||
status: filters.value.status
|
||||
}
|
||||
const res = await getLeads(params)
|
||||
if (res.code === '000000' && res.data) {
|
||||
tableData.value = res.data.leads.map((item: any) => ({
|
||||
...item,
|
||||
created_at: formatDateTime(item.created_at)
|
||||
}))
|
||||
total.value = res.data.total_count
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Fetch leads error:', error)
|
||||
ElMessage.error(error.backendMessage || t('enterpriseLeads.messages.fetchError'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
fetchSummary()
|
||||
})
|
||||
|
||||
watch([page, pageSize], () => {
|
||||
fetchData()
|
||||
})
|
||||
|
||||
const handleFilter = () => {
|
||||
page.value = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchData()
|
||||
fetchSummary()
|
||||
}
|
||||
|
||||
const handleView = (row: any) => {
|
||||
selectedLead.value = row
|
||||
detailVisible.value = true
|
||||
@@ -192,10 +214,48 @@ const handleEdit = (row: any) => {
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
const handleUpdate = (updatedData: any) => {
|
||||
const index = tableData.value.findIndex(item => item.id === updatedData.id)
|
||||
if (index !== -1) {
|
||||
tableData.value[index] = updatedData
|
||||
const handleDelete = (row: any) => {
|
||||
ElMessageBox.confirm(
|
||||
t('enterpriseLeads.messages.deleteConfirm'),
|
||||
t('common.confirm'),
|
||||
{
|
||||
confirmButtonText: t('common.confirm'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
type: 'warning',
|
||||
}
|
||||
).then(async () => {
|
||||
try {
|
||||
const res = await deleteLead(row.id)
|
||||
if (res.code === '000000') {
|
||||
ElMessage.success(t('enterpriseLeads.messages.deleteSuccess'))
|
||||
fetchData()
|
||||
fetchSummary()
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Delete lead error:', error)
|
||||
ElMessage.error(error.backendMessage || t('enterpriseLeads.messages.deleteError'))
|
||||
}
|
||||
}).catch(() => {
|
||||
// Cancelled
|
||||
})
|
||||
}
|
||||
|
||||
const handleUpdate = async (updatedData: any) => {
|
||||
try {
|
||||
const { id, ...data } = updatedData
|
||||
// 格式化日期为 ISO 8601 格式 (2026-02-05T10:00:00Z),去除毫秒
|
||||
if (data.follow_up_date) {
|
||||
data.follow_up_date = new Date(data.follow_up_date).toISOString().replace(/\.\d{3}/, '')
|
||||
}
|
||||
const res = await updateLead(id, data)
|
||||
if (res.code === '000000') {
|
||||
ElMessage.success(t('enterpriseLeads.messages.updateSuccess'))
|
||||
fetchData()
|
||||
fetchSummary()
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Update lead error:', error)
|
||||
ElMessage.error(error.backendMessage || t('enterpriseLeads.messages.updateError'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 通过信息 (仅在状态为已开票时显示) -->
|
||||
<div class="detail-card approve-info-card" v-if="detailData.status === 'completed'">
|
||||
<div class="detail-card approve-info-card" v-if="detailData.status_display === '已开票'">
|
||||
<div class="card-title">
|
||||
<span class="title-line"></span>
|
||||
<h3>{{ t('invoices.detail.approveInfo') }}</h3>
|
||||
@@ -97,7 +97,7 @@
|
||||
<div class="info-row">
|
||||
<span class="label">{{ t('invoices.detail.fileName') }}:</span>
|
||||
<span class="value">
|
||||
<el-link v-if="detailData.invoice_file_url" type="primary" :underline="false" :href="detailData.invoice_file_url" target="_blank">
|
||||
<el-link @click="downloadFile(detailData.invoice_file_url!)" v-if="detailData.invoice_file_url" type="primary" >
|
||||
{{ detailData.invoice_file_url.split('/').pop() }}
|
||||
</el-link>
|
||||
</span>
|
||||
@@ -136,8 +136,9 @@ 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 { getInvoiceDetail, uploadInvoice, rejectInvoice, issueInvoice, type Invoice } from '@/api/invoice'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import { envUrl1 } from '@/api/https'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
@@ -200,31 +201,49 @@ 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')
|
||||
if (!detailData.value.id) {
|
||||
console.error('No detail data id')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('invoice_no', detailData.value.id!)
|
||||
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'))
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const res = await uploadInvoice(formData)
|
||||
console.log('Upload response (detail):', res)
|
||||
if (res.code === '000000') {
|
||||
// 1. 上传文件
|
||||
const uploadRes = await uploadInvoice(formData)
|
||||
console.log('Upload response (detail):', uploadRes)
|
||||
|
||||
if (uploadRes.code === '000000' && (uploadRes.data?.file_url || uploadRes.data?.url)) {
|
||||
const fileUrl = uploadRes.data.file_url || uploadRes.data.url
|
||||
// 2. 调用开票接口
|
||||
const issueRes = await issueInvoice(detailData.value.id, {
|
||||
invoice_file_url: fileUrl,
|
||||
issued_at: data.issued_at
|
||||
})
|
||||
|
||||
if (issueRes.code === '000000') {
|
||||
ElMessage.success(t('invoices.messages.uploadSuccess'))
|
||||
uploadVisible.value = false
|
||||
fetchDetail()
|
||||
}
|
||||
} else {
|
||||
throw new Error(uploadRes.message || 'Upload failed')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Upload error detail:', error)
|
||||
ElMessage.error(error.backendMessage || t('invoices.messages.fetchDetailError'))
|
||||
ElMessage.error(error.backendMessage || error.message || t('invoices.messages.fetchDetailError'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,6 +268,14 @@ const handleRejectSubmit = async (data: any) => {
|
||||
ElMessage.error(error.backendMessage || t('invoices.messages.fetchDetailError'))
|
||||
}
|
||||
}
|
||||
|
||||
// 每一项的点击事件
|
||||
const downloadFile = (e:string)=>{
|
||||
if (!e) return
|
||||
// 如果是相对路径,拼上基础路径
|
||||
const url = e.startsWith('http') ? e : envUrl1 + e
|
||||
window.open(url)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="t('invoices.dialog.uploadTitle')"
|
||||
width="680px"
|
||||
class="upload-invoice-dialog"
|
||||
:show-close="true"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div>
|
||||
<el-dialog v-model="visible" :title="t('invoices.dialog.uploadTitle')" width="680px" class="upload-invoice-dialog"
|
||||
:show-close="true" @close="handleClose">
|
||||
<div class="upload-dialog-content">
|
||||
<!-- 信息摘要 -->
|
||||
<div class="info-summary" v-if="invoiceData">
|
||||
@@ -28,23 +23,19 @@
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">{{ t('invoices.detail.amount') }}:</span>
|
||||
<span class="value">¥{{ (invoiceData.amount || 0).toLocaleString(undefined, { minimumFractionDigits: 2 }) }}</span>
|
||||
<span class="value">¥{{ (invoiceData.amount || 0).toLocaleString(undefined, { minimumFractionDigits: 2 })
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传区域 -->
|
||||
<div class="upload-section">
|
||||
<p class="section-label">{{ t('invoices.dialog.uploadLabel') }}</p>
|
||||
<el-upload
|
||||
class="invoice-uploader"
|
||||
drag
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
v-model:file-list="form.fileList"
|
||||
>
|
||||
<el-upload class="invoice-uploader" drag action="#" :auto-upload="false" v-model:file-list="form.fileList">
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
<span class="upload-link">{{ t('invoices.dialog.uploadDraggerText').split(' / ')[0] }}</span> / {{ t('invoices.dialog.uploadDraggerText').split(' / ')[1] }}
|
||||
<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">
|
||||
@@ -56,16 +47,23 @@
|
||||
|
||||
<!-- 表单项 -->
|
||||
<div class="form-section">
|
||||
<!-- 移除了 API 不支持的 issued_at 和 remark 字段 -->
|
||||
<div class="form-item">
|
||||
<p class="section-label">{{ t('invoices.dialog.finishTimeLabel') }}</p>
|
||||
<el-date-picker v-model="form.issued_at" type="datetime"
|
||||
:placeholder="t('invoices.dialog.finishTimePlaceholder')" style="width: 100%" format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<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>
|
||||
<el-button type="primary" class="btn-submit" @click="handleSubmit">{{ t('invoices.actions.confirmUpload')
|
||||
}}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -84,7 +82,8 @@ const emit = defineEmits(['update:modelValue', 'submit'])
|
||||
|
||||
const visible = ref(props.modelValue)
|
||||
const form = reactive({
|
||||
fileList: [] as any[]
|
||||
fileList: [] as any[],
|
||||
issued_at:""
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
@@ -117,19 +116,30 @@ const handleSubmit = () => {
|
||||
ElMessage.warning(t('invoices.dialog.uploadTip'))
|
||||
return
|
||||
}
|
||||
if (!form.issued_at) {
|
||||
ElMessage.warning(t('invoices.dialog.finishTimePlaceholder'))
|
||||
return
|
||||
}
|
||||
emit('submit', { ...form })
|
||||
visible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@mixin dialog-style-base {
|
||||
border-radius: 4px;
|
||||
:deep(.el-dialog__headerbtn) {
|
||||
top: 10px !important;
|
||||
}
|
||||
|
||||
:deep(.el-dialog) {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
padding-bottom: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__header) {
|
||||
background-color: #019C7C;
|
||||
background: linear-gradient(90deg, #00928A 0%, #00D19E 100%) !important;
|
||||
margin: 0;
|
||||
padding: 16px 24px;
|
||||
|
||||
@@ -141,9 +151,19 @@ const handleSubmit = () => {
|
||||
|
||||
.el-dialog__close {
|
||||
color: #fff;
|
||||
&:hover { color: rgba(255, 255, 255, 0.8); }
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin dialog-style-base {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
|
||||
|
||||
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 24px;
|
||||
@@ -172,8 +192,14 @@ const handleSubmit = () => {
|
||||
|
||||
.summary-item {
|
||||
font-size: 14px;
|
||||
.label { color: #8C8C8C; }
|
||||
.value { color: #262626; }
|
||||
|
||||
.label {
|
||||
color: #8C8C8C;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +217,10 @@ const handleSubmit = () => {
|
||||
:deep(.el-upload-dragger) {
|
||||
border: 1px dashed #D9D9D9;
|
||||
padding: 30px;
|
||||
&:hover { border-color: #019C7C; }
|
||||
|
||||
&:hover {
|
||||
border-color: #019C7C;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-link {
|
||||
@@ -223,7 +252,11 @@ const handleSubmit = () => {
|
||||
background-color: #019C7C;
|
||||
border-color: #019C7C;
|
||||
padding: 8px 32px;
|
||||
&:hover { background-color: #017c63; border-color: #017c63; }
|
||||
|
||||
&:hover {
|
||||
background-color: #017c63;
|
||||
border-color: #017c63;
|
||||
}
|
||||
}
|
||||
|
||||
.el-button {
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<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.completed')" value="issued" />
|
||||
<el-option :label="t('invoices.filter.rejected')" value="rejected" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -84,8 +84,8 @@
|
||||
<el-table-column :label="t('invoices.table.user')" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<div class="user-info">
|
||||
<span class="user-name">{{ row.applicant_name }}</span>
|
||||
<span class="user-type" :class="row.order_package_name">{{ row.order_package_name }}</span>
|
||||
<span class="user-name">{{ row.cu_user_phone }}</span>
|
||||
<!-- <span class="user-type" :class="row.order_package_name">{{ row.order_package_name }}</span> -->
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -161,7 +161,7 @@ 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, uploadInvoice, rejectInvoice, type Invoice } from '@/api/invoice'
|
||||
import { getInvoices, getInvoiceSummary, uploadInvoice, rejectInvoice, issueInvoice, type Invoice } from '@/api/invoice'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -170,7 +170,7 @@ const router = useRouter()
|
||||
const stats = ref([
|
||||
{ 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: 'completed', icon: 'success', key: 'issued_count' },
|
||||
{ label: '已驳回', value: '0', type: 'rejected', icon: 'error', key: 'rejected_count' }
|
||||
])
|
||||
|
||||
@@ -291,10 +291,11 @@ const handleUpload = (row: any) => {
|
||||
|
||||
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')
|
||||
if (!currentRow.value || !currentRow.value.id) {
|
||||
console.error('No current row or id')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('invoice_no', currentRow.value.id)
|
||||
@@ -304,19 +305,36 @@ const handleUploadSubmit = async (data: any) => {
|
||||
formData.append('file', file)
|
||||
} else {
|
||||
ElMessage.warning(t('invoices.dialog.uploadTip'))
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const res = await uploadInvoice(formData)
|
||||
console.log('Upload response:', res)
|
||||
if (res.code === '000000') {
|
||||
// 1. 上传文件
|
||||
const uploadRes = await uploadInvoice(formData)
|
||||
console.log('Upload response:', uploadRes)
|
||||
|
||||
if (uploadRes.code === '000000' && (uploadRes.data?.file_url || uploadRes.data?.url)) {
|
||||
const fileUrl = uploadRes.data.file_url || uploadRes.data.url
|
||||
// 2. 调用开票接口
|
||||
const issueRes = await issueInvoice(currentRow.value.id, {
|
||||
invoice_file_url: fileUrl,
|
||||
issued_at: data.issued_at ? new Date(data.issued_at).toISOString().replace(/\.\d{3}/, '') : ''
|
||||
})
|
||||
|
||||
if (issueRes.code === '000000') {
|
||||
ElMessage.success(t('invoices.messages.uploadSuccess'))
|
||||
uploadVisible.value = false
|
||||
fetchData()
|
||||
fetchSummary()
|
||||
}
|
||||
} else {
|
||||
throw new Error(uploadRes.message || 'Upload failed')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Upload invoice error:', error)
|
||||
ElMessage.error(error.backendMessage || t('invoices.messages.fetchError'))
|
||||
ElMessage.error(error.backendMessage || error.message || t('invoices.messages.fetchError'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,7 +432,7 @@ const handleCurrentChange = (val: number) => {
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
|
||||
&.success .stat-icon-wrapper {
|
||||
&.completed .stat-icon-wrapper {
|
||||
background: var(--el-color-success-light-9);
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
@@ -555,7 +573,7 @@ const handleCurrentChange = (val: number) => {
|
||||
}
|
||||
|
||||
.status-text {
|
||||
&.success {
|
||||
&.completed {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<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="professional" />
|
||||
<el-option label="定制版" value="custom" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -19,13 +19,17 @@
|
||||
|
||||
<div class="form-row">
|
||||
<el-form-item :label="t('packages.edit.price')" prop="price" class="flex-1">
|
||||
<el-input v-model="form.price" />
|
||||
<el-input-number v-model="form.price" :min="0" class="w-full" controls-position="right" />
|
||||
</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 :label="t('packages.edit.cycle')" prop="duration_description" class="flex-1">
|
||||
<el-input v-model="form.duration_description" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-form-item :label="t('packages.edit.priceDescription')" prop="price_description">
|
||||
<el-input v-model="form.price_description" :placeholder="t('packages.edit.priceDescPlaceholder')" />
|
||||
</el-form-item>
|
||||
|
||||
<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')" />
|
||||
@@ -34,7 +38,7 @@
|
||||
<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">
|
||||
<div v-for="(_, 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)">
|
||||
@@ -54,12 +58,12 @@
|
||||
<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-option :label="t('packages.status.enabled')" :value="1" />
|
||||
<el-option :label="t('packages.status.disabled')" :value="2" />
|
||||
</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 :label="t('packages.edit.sort')" prop="sort_order" class="flex-1">
|
||||
<el-input-number v-model="form.sort_order" :min="1" class="w-full" controls-position="right" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
@@ -101,12 +105,13 @@ const formRef = ref<FormInstance>()
|
||||
const form = reactive({
|
||||
name: '',
|
||||
type: '',
|
||||
price: '',
|
||||
cycle: '',
|
||||
price: 0,
|
||||
price_description: '',
|
||||
duration_description: '',
|
||||
description: '',
|
||||
features: [] as string[],
|
||||
status: 'enabled',
|
||||
sort: 1,
|
||||
status: 1,
|
||||
sort_order: 1,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
@@ -114,10 +119,10 @@ 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' }],
|
||||
duration_description: [{ 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' }]
|
||||
sort_order: [{ required: true, message: t('common.required'), trigger: 'blur' }]
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
@@ -125,12 +130,13 @@ watch(() => props.modelValue, (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.price = Number(props.data.price) || 0
|
||||
form.price_description = props.data.price_description || ''
|
||||
form.duration_description = props.data.duration_description || ''
|
||||
form.description = props.data.description || ''
|
||||
form.features = Array.isArray(props.data.features) ? [...props.data.features] : []
|
||||
form.status = props.data.status === 1 ? 1 : 2
|
||||
form.sort_order = props.data.sort_order || 1
|
||||
form.remark = props.data.remark || ''
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<template>
|
||||
<Layout :page-title="t('packages.title')">
|
||||
<div class="packages-page">
|
||||
<div class="list-card">
|
||||
<div class="list-card" v-loading="loading">
|
||||
<div class="list-header">
|
||||
<div class="list-title">
|
||||
<span class="title-bar"></span>
|
||||
<span>{{ t('packages.table.title') }}</span>
|
||||
</div>
|
||||
<el-button class="refresh-btn" :icon="Refresh" text @click="fetchData">
|
||||
{{ t('enterpriseLeads.actions.refresh') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
@@ -24,15 +27,27 @@
|
||||
<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>
|
||||
<span :class="{ 'price-free': (row.type === 'trial' && (row.price === 0 || row.price === '0')) || row.type === 'custom' }">
|
||||
<template v-if="row.type === 'custom'">
|
||||
{{ t('packages.table.customPlan') }}
|
||||
</template>
|
||||
<template v-else-if="row.type === 'trial' && (row.price === 0 || row.price === '0')">
|
||||
{{ t('packages.table.free') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ typeof row.price === 'number' ? `¥${row.price}` : row.price }}
|
||||
</template>
|
||||
</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 prop="duration_description" :label="t('packages.table.cycle')" min-width="180" />
|
||||
<el-table-column prop="created_at" :label="t('packages.table.createdAt')" min-width="180" />
|
||||
<el-table-column prop="updated_at" :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>
|
||||
<span class="status-tag" :class="row.status === 1 ? 'enabled' : 'disabled'">
|
||||
{{ row.status === 1 ? t('packages.status.enabled') : t('packages.status.disabled') }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('packages.table.actions')" width="100" fixed="right">
|
||||
@@ -55,74 +70,111 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Layout from '@/components/common/layout/Layout.vue'
|
||||
import PackageEditDialog from './components/PackageEditDialog.vue'
|
||||
import { getPackages, updatePackage, getPackageDetail, type Package } from '@/api/package'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
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 tableData = ref<Package[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getPackages()
|
||||
if (res.code === '000000' && res.data) {
|
||||
tableData.value = res.data.packages.map((item: Package) => {
|
||||
let features = []
|
||||
try {
|
||||
if (typeof item.features === 'string' && item.features) {
|
||||
const parsed = JSON.parse(item.features)
|
||||
features = Array.isArray(parsed) ? parsed : [item.features]
|
||||
} else if (Array.isArray(item.features)) {
|
||||
features = item.features
|
||||
}
|
||||
])
|
||||
} catch (e) {
|
||||
features = item.features ? [item.features] : []
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
features,
|
||||
created_at: formatDateTime(item.created_at),
|
||||
updated_at: formatDateTime(item.updated_at)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Fetch packages error:', error)
|
||||
ElMessage.error(error.backendMessage || t('enterpriseLeads.messages.fetchError'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
|
||||
const editVisible = ref(false)
|
||||
const selectedPackage = ref<any>(null)
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
selectedPackage.value = row
|
||||
editVisible.value = true
|
||||
const handleEdit = async (row: any) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getPackageDetail(row.id)
|
||||
if (res.code === '000000' && res.data) {
|
||||
const detail = res.data
|
||||
let features = []
|
||||
try {
|
||||
if (typeof detail.features === 'string' && detail.features) {
|
||||
const parsed = JSON.parse(detail.features)
|
||||
features = Array.isArray(parsed) ? parsed : [detail.features]
|
||||
} else if (Array.isArray(detail.features)) {
|
||||
features = detail.features
|
||||
}
|
||||
} catch (e) {
|
||||
features = detail.features ? [detail.features] : []
|
||||
}
|
||||
|
||||
const handleUpdate = (updatedData: any) => {
|
||||
const index = tableData.value.findIndex(item => item.id === updatedData.id)
|
||||
if (index !== -1) {
|
||||
tableData.value[index] = { ...tableData.value[index], ...updatedData }
|
||||
selectedPackage.value = {
|
||||
...detail,
|
||||
features
|
||||
}
|
||||
editVisible.value = true
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Fetch package detail error:', error)
|
||||
ElMessage.error(error.backendMessage || t('enterpriseLeads.messages.fetchDetailError'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdate = async (updatedData: any) => {
|
||||
try {
|
||||
const { id, created_at, updated_at, ...data } = updatedData
|
||||
// 将 features 数组转回字符串供后端存储
|
||||
if (Array.isArray(data.features)) {
|
||||
data.features = JSON.stringify(data.features)
|
||||
}
|
||||
const res = await updatePackage(id, data)
|
||||
if (res.code === '000000') {
|
||||
ElMessage.success(t('enterpriseLeads.messages.updateSuccess'))
|
||||
fetchData()
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Update package error:', error)
|
||||
ElMessage.error(error.backendMessage || t('enterpriseLeads.messages.updateError'))
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
Reference in new issue
Block a user