Merge branch 'main' of github.com:cedar-v/license-manager

This commit is contained in:
wood911010 committed 2026-02-02 19:58:38 +08:00
commit eba7baaa5f
13 files changed
+786 -327

No files matched your search

+1
View File
@@ -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)
+9 -1
View File
@@ -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)
}
+86
View File
@@ -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}`)
}
+53
View File
@@ -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)
}
+19 -2
View File
@@ -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,18 +2,55 @@
<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"
>
: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"
:model="form"
:rules="rules"
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,28 +72,28 @@
</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.followUpRecord')" prop="follow_up_record">
<el-input
v-model="form.follow_up_record"
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>
<el-form-item :label="t('enterpriseLeads.detail.internalRemark')" prop="internal_note">
<el-input
v-model="form.internal_note"
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">
@@ -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
}
})
+127 -67
View File
@@ -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>
+39 -12
View File
@@ -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') {
ElMessage.success(t('invoices.messages.uploadSuccess'))
fetchDetail()
// 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,71 +1,69 @@
<template>
<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">
<div class="summary-item">
<span class="label">{{ t('invoices.detail.invoiceNo') }}:</span>
<span class="value">{{ invoiceData.invoice_no }}</span>
</div>
<div class="summary-item">
<span class="label">{{ t('invoices.detail.orderNo') }}:</span>
<span class="value">{{ invoiceData.order_no }}</span>
</div>
<div class="summary-item">
<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">{{ t('invoices.detail.invoiceTitle') }}:</span>
<span class="value">{{ invoiceData.title }}</span>
</div>
<div class="summary-item">
<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">{{ t('invoices.dialog.uploadLabel') }}</p>
<el-upload
class="invoice-uploader"
drag
action="#"
:auto-upload="false"
v-model:file-list="form.fileList"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
<span class="upload-link">{{ t('invoices.dialog.uploadDraggerText').split(' / ')[0] }}</span> / {{ t('invoices.dialog.uploadDraggerText').split(' / ')[1] }}
<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">
<div class="summary-item">
<span class="label">{{ t('invoices.detail.invoiceNo') }}:</span>
<span class="value">{{ invoiceData.invoice_no }}</span>
</div>
<template #tip>
<div class="el-upload__tip">
{{ t('invoices.dialog.uploadTip') }}
</div>
</template>
</el-upload>
</div>
<div class="summary-item">
<span class="label">{{ t('invoices.detail.orderNo') }}:</span>
<span class="value">{{ invoiceData.order_no }}</span>
</div>
<div class="summary-item">
<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">{{ t('invoices.detail.invoiceTitle') }}:</span>
<span class="value">{{ invoiceData.title }}</span>
</div>
<div class="summary-item">
<span class="label">{{ t('invoices.detail.amount') }}:</span>
<span class="value">¥{{ (invoiceData.amount || 0).toLocaleString(undefined, { minimumFractionDigits: 2 })
}}</span>
</div>
</div>
<!-- 表单项 -->
<div class="form-section">
<!-- 移除了 API 不支持的 issued_at 和 remark 字段 -->
<!-- 上传区域 -->
<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-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] }}
</div>
<template #tip>
<div class="el-upload__tip">
{{ t('invoices.dialog.uploadTip') }}
</div>
</template>
</el-upload>
</div>
<!-- 表单项 -->
<div class="form-section">
<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>
</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>
</div>
</template>
</el-dialog>
<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>
</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,33 +116,54 @@ 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>
: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: linear-gradient(90deg, #00928A 0%, #00D19E 100%) !important;
margin: 0;
padding: 16px 24px;
.el-dialog__title {
color: #fff;
font-size: 16px;
font-weight: 500;
}
.el-dialog__close {
color: #fff;
&:hover {
color: rgba(255, 255, 255, 0.8);
}
}
}
@mixin dialog-style-base {
border-radius: 4px;
overflow: hidden;
padding: 0;
:deep(.el-dialog__header) {
background-color: #019C7C;
margin: 0;
padding: 16px 24px;
.el-dialog__title {
color: #fff;
font-size: 16px;
font-weight: 500;
}
.el-dialog__close {
color: #fff;
&:hover { color: rgba(255, 255, 255, 0.8); }
}
}
:deep(.el-dialog__body) {
padding: 24px;
@@ -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 {
+34 -16
View File
@@ -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') {
ElMessage.success(t('invoices.messages.uploadSuccess'))
fetchData()
fetchSummary()
// 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 || ''
}
})
+112 -60
View File
@@ -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>