fix:修改仪表盘页面和授权管理列表页面

This commit is contained in:
13895237362 committed 2025-11-07 15:33:11 +08:00
1 parent a34b834382
commit f6f36c7812
17 files changed
+350 -306

No files matched your search

+12
View File
@@ -1,5 +1,16 @@
import Axios from './https' import Axios from './https'
// 授权统计信息类型
export interface AuthorizationStats {
active_licenses?: number; // 已激活许可证数量
expired_auth_codes?: number; // 已过期授权码数量
expired_licenses?: number; // 已过期许可证数量
expiring_soon_auth_codes?: number; // 30日内即将到期授权码数量
inactive_licenses?: number; // 未激活许可证数量
total_auth_codes?: number; // 总授权码数量
total_licenses?: number; // 总许可证数量
}
// 客户接口类型定义 - 完全使用接口提供的字段名称 // 客户接口类型定义 - 完全使用接口提供的字段名称
export interface Customer { export interface Customer {
id: string; id: string;
@@ -23,6 +34,7 @@ export interface Customer {
updated_at: string; updated_at: string;
created_by: string; created_by: string;
updated_by: string; updated_by: string;
authorization_stats?: AuthorizationStats; // 授权统计信息
} }
// 客户查询请求参数 // 客户查询请求参数
Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 B

After

Width:  |  Height:  |  Size: 479 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 475 B

After

Width:  |  Height:  |  Size: 481 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 487 B

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 488 B

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 493 B

After

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 389 B

After

Width:  |  Height:  |  Size: 444 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 429 B

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

+8 -8
View File
@@ -136,7 +136,7 @@
"table": { "table": {
"code": "License Code", "code": "License Code",
"status": "Status", "status": "Status",
"deploymentType": "Deployment Type", "activationProgress": "Activation Progress",
"endDate": "Expiry Time", "endDate": "Expiry Time",
"description": "Description", "description": "Description",
"operation": "Operation", "operation": "Operation",
@@ -562,13 +562,13 @@
"createTime": "Create Time", "createTime": "Create Time",
"updater": "Updater", "updater": "Updater",
"updateTime": "Update Time", "updateTime": "Update Time",
"totalLicenses": "Total Licenses", "totalLicenses": "Total Authorization Codes",
"activeLicenses": "Expired Licenses", "activeLicenses": "Expired Authorization Codes",
"recentLicense": "Licenses Expiring Within 30 Days", "recentLicense": "Authorization Codes Expiring Within 30 Days",
"expiringSoon": "Total License Count", "expiringSoon": "Total Licenses",
"expiringSoonTime": "Activated License Count", "expiringSoonTime": "Activated Licenses",
"expiredLicenses": "Unactivated License Count", "expiredLicenses": "Expired Licenses",
"expiredTime": "Expired License Count", "expiredTime": "Inactivated Licenses",
"noData": "--", "noData": "--",
"onlineLicense": "Online License", "onlineLicense": "Online License",
"customerNotExist": "Customer data does not exist" "customerNotExist": "Customer data does not exist"
+6 -6
View File
@@ -136,7 +136,7 @@
"table": { "table": {
"code": "ライセンスコード", "code": "ライセンスコード",
"status": "ステータス", "status": "ステータス",
"deploymentType": "デプロイメントタイプ", "activationProgress": "アクティベーション進捗",
"endDate": "有効期限", "endDate": "有効期限",
"description": "説明", "description": "説明",
"operation": "操作", "operation": "操作",
@@ -562,13 +562,13 @@
"createTime": "作成日時", "createTime": "作成日時",
"updater": "更新者", "updater": "更新者",
"updateTime": "更新日時", "updateTime": "更新日時",
"totalLicenses": "総ライセンス数", "totalLicenses": "総認証コード数",
"activeLicenses": "期限切れライセンス", "activeLicenses": "期限切れ認証コード",
"recentLicense": "30日以内に期限切れするライセンスコード数量", "recentLicense": "30日以内に期限切れする認証コード数量",
"expiringSoon": "総ライセンス数量", "expiringSoon": "総ライセンス数量",
"expiringSoonTime": "アクティベート済みライセンス数量", "expiringSoonTime": "アクティベート済みライセンス数量",
"expiredLicenses": "未アクティベートライセンス数量", "expiredLicenses": "期限切れライセンス数量",
"expiredTime": "期限切れライセンス数量", "expiredTime": "未アクティベートライセンス数量",
"noData": "--", "noData": "--",
"onlineLicense": "オンラインライセンス", "onlineLicense": "オンラインライセンス",
"customerNotExist": "顧客データが存在しません" "customerNotExist": "顧客データが存在しません"
+10 -10
View File
@@ -26,13 +26,13 @@
"dashboard": { "dashboard": {
"title": "仪表盘", "title": "仪表盘",
"stats": { "stats": {
"totalCustomers": "客户总数", "totalCustomers": "总授权码",
"totalLicenses": "总授权", "totalLicenses": "活跃许可证",
"activeLicenses": "有效授权", "activeLicenses": "即将过期",
"expired": "过期授权", "expired": "异常警告",
"onlineClients": "在线客户端数量", "onlineClients": "增长率",
"expiringSoon": "到期提醒", "expiringSoon": "授权码增长率",
"authSuccessRate24h": "近24小时授权验证成功率" "authSuccessRate24h": "许可证增长率"
}, },
"recentLicenses": { "recentLicenses": {
"title": "最近授权", "title": "最近授权",
@@ -136,7 +136,7 @@
"table": { "table": {
"code": "授权码", "code": "授权码",
"status": "状态", "status": "状态",
"deploymentType": "部署类型", "activationProgress": "激活进度",
"endDate": "到期时间", "endDate": "到期时间",
"description": "备注", "description": "备注",
"operation": "操作", "operation": "操作",
@@ -567,8 +567,8 @@
"recentLicense": "30日内即将到期授权码数量 ", "recentLicense": "30日内即将到期授权码数量 ",
"expiringSoon": "总许可证数量", "expiringSoon": "总许可证数量",
"expiringSoonTime": "已激活许可证数量", "expiringSoonTime": "已激活许可证数量",
"expiredLicenses": "未激活许可证数量", "expiredLicenses": "已过期许可证数量",
"expiredTime": "已过期许可证数量", "expiredTime": "未激活许可证数量",
"noData": "--", "noData": "--",
"onlineLicense": "在线授权", "onlineLicense": "在线授权",
"customerNotExist": "客户数据不存在" "customerNotExist": "客户数据不存在"
+162 -149
View File
@@ -2,147 +2,157 @@
* @Author: 13895237362 2205451508@qq.com * @Author: 13895237362 2205451508@qq.com
* @Date: 2025-08-20 16:00:00 * @Date: 2025-08-20 16:00:00
* @LastEditors: 13895237362 2205451508@qq.com * @LastEditors: 13895237362 2205451508@qq.com
* @LastEditTime: 2025-09-03 11:41:35 * @LastEditTime: 2025-11-03 15:44:00
* @FilePath: /frontend/src/views/Customers/CustomerView.vue * @FilePath: /frontend/src/views/Customers/CustomerView.vue
* @Description: 客户查看组件 * @Description: 客户查看组件
--> -->
<template> <template>
<!-- 顶部横向区域 - 面包屑和返回按钮 --> <div class="customer-view-container">
<div class="top-section"> <!-- 顶部横向区域 - 面包屑和返回按钮 -->
<!-- 面包屑导航 --> <div class="top-section">
<div class="breadcrumb"> <!-- 面包屑导航 -->
<span class="breadcrumb-item">{{ t('customers.breadcrumb.customerManagement') }}</span> <div class="breadcrumb">
<span class="breadcrumb-separator">></span> <span class="breadcrumb-item">{{ t('customers.breadcrumb.customerManagement') }}</span>
<span class="breadcrumb-current">{{ t('customers.viewCustomer') }}</span> <span class="breadcrumb-separator">></span>
</div> <span class="breadcrumb-current">{{ t('customers.viewCustomer') }}</span>
</div>
<!-- 返回按钮 -->
<div class="form-actions">
<el-button @click="handleBack">{{ t('customers.view.back') }}</el-button>
</div>
</div>
<!-- 主内容区域 --> <!-- 返回按钮 -->
<div class="customer-view" v-loading="loading"> <div class="form-actions">
<!-- 基本信息卡片 --> <el-button @click="handleBack">{{ t('customers.view.back') }}</el-button>
<div class="info-card basic-info">
<h3 class="section-title">{{ t('customers.form.basicInfo') }}</h3>
<div class="info-grid">
<div class="info-item">
<span class="label">{{ t('customers.form.customerName') }}:</span>
<span class="value">{{ customerData.name || t('customers.view.noData') }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('customers.form.customerType') }}:</span>
<span class="value">{{ getCustomerTypeLabel(customerData.type) }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('customers.form.customerLevel') }}:</span>
<span class="value">{{ getCustomerLevelLabel(customerData.level) }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('customers.form.contactPerson') }}:</span>
<span class="value">{{ customerData.contact || t('customers.view.noData') }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('customers.form.email') }}:</span>
<span class="value">{{ customerData.email || t('customers.view.noData') }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('customers.form.status') }}:</span>
<span class="value">{{ getStatusLabel(customerData.status) }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('customers.form.phone') }}:</span>
<span class="value">{{ customerData.phone || t('customers.view.noData') }}</span>
</div>
<div class="info-item full-width">
<span class="label">{{ t('customers.form.address') }}:</span>
<span class="value">{{ customerData.address || t('customers.view.noData') }}</span>
</div>
</div> </div>
</div> </div>
<!-- 商业信息卡片 --> <!-- 主内容区域 -->
<div class="info-card business-info"> <div class="customer-view" v-loading="loading">
<h3 class="section-title">{{ t('customers.form.businessInfo') }}</h3> <!-- 基本信息卡片 -->
<div class="info-grid"> <div class="info-card basic-info">
<div class="info-item"> <h3 class="section-title">{{ t('customers.form.basicInfo') }}</h3>
<span class="label">{{ t('customers.form.companySize') }}:</span> <div class="info-grid">
<span class="value">{{ enumLabels.companySize || t('customers.view.noData') }}</span> <div class="info-item">
</div> <span class="label">{{ t('customers.form.customerName') }}:</span>
<div class="info-item full-width"> <span class="value">{{ customerData.name || t('customers.view.noData') }}</span>
<span class="label">{{ t('customers.form.preferredLicense') }}:</span>
<span class="value">{{ customerData.preferredLicense || t('customers.view.noData') }}</span>
</div>
<div class="info-item full-width">
<span class="label">{{ t('customers.form.description') }}:</span>
<span class="value description">{{ customerData.description || t('customers.view.noData') }}</span>
</div>
</div>
</div>
<!-- 状态信息卡片 -->
<div class="info-card status-info">
<h3 class="section-title">{{ t('customers.view.statusInfo') }}</h3>
<div class="info-grid">
<div class="info-item inline-group full-width">
<div class="inline-pair">
<span class="label">{{ t('customers.view.creator') }}:</span>
<span class="value">{{ customerData.statusRecords?.[0]?.user || t('customers.view.noData') }}</span>
</div> </div>
<div class="inline-pair"> <div class="info-item">
<span class="label">{{ t('customers.view.createTime') }}:</span> <span class="label">{{ t('customers.form.customerType') }}:</span>
<span class="value">{{ formatDate(customerData.statusRecords?.[0]?.time) }}</span> <span class="value">{{ getCustomerTypeLabel(customerData.type) }}</span>
</div> </div>
</div> <div class="info-item">
<div class="info-item inline-group full-width"> <span class="label">{{ t('customers.form.customerLevel') }}:</span>
<div class="inline-pair"> <span class="value">{{ getCustomerLevelLabel(customerData.level) }}</span>
<span class="label">{{ t('customers.view.updater') }}:</span>
<span class="value">{{ customerData.statusRecords?.[1]?.user || t('customers.view.noData') }}</span>
</div> </div>
<div class="inline-pair"> <div class="info-item">
<span class="label">{{ t('customers.view.updateTime') }}:</span> <span class="label">{{ t('customers.form.contactPerson') }}:</span>
<span class="value">{{ formatDate(customerData.statusRecords?.[1]?.time) }}</span> <span class="value">{{ customerData.contact || t('customers.view.noData') }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('customers.form.email') }}:</span>
<span class="value">{{ customerData.email || t('customers.view.noData') }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('customers.form.status') }}:</span>
<span class="value">{{ getStatusLabel(customerData.status) }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('customers.form.phone') }}:</span>
<span class="value">{{ customerData.phone || t('customers.view.noData') }}</span>
</div>
<div class="info-item full-width">
<span class="label">{{ t('customers.form.address') }}:</span>
<span class="value">{{ customerData.address || t('customers.view.noData') }}</span>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- 授权统计卡片 --> <!-- 商业信息卡片 -->
<div class="info-card license-stats"> <div class="info-card business-info">
<h3 class="section-title">{{ t('customers.view.licenseStats') }}</h3> <h3 class="section-title">{{ t('customers.form.businessInfo') }}</h3>
<div class="info-grid"> <div class="info-grid">
<div class="info-item"> <div class="info-item">
<span class="label">{{ t('customers.view.totalLicenses') }}:</span> <span class="label">{{ t('customers.form.companySize') }}:</span>
<span class="value">{{ customerData.totalLicenses || 0 }}</span> <span class="value">{{ enumLabels.companySize || t('customers.view.noData') }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('customers.view.activeLicenses') }}:</span>
<span class="value">{{ customerData.activeLicenses || 0 }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('customers.view.recentLicense') }}:</span>
<span class="value">{{ formatDate(customerData.latestLicenseTime) }}</span>
</div>
<div class="info-item inline-group full-width">
<div class="inline-pair">
<span class="label">{{ t('customers.view.expiringSoon') }}:</span>
<span class="value">{{ customerData.expiringSoonLicenses || 0 }}</span>
</div> </div>
<div class="inline-pair"> <div class="info-item full-width">
<span class="label">{{ t('customers.view.expiringSoonTime') }}:</span> <span class="label">{{ t('customers.form.preferredLicense') }}:</span>
<span class="value">{{ formatDate(customerData.expiringSoonTime) }}</span> <span class="value">{{
customerData.preferredLicense || t('customers.view.noData')
}}</span>
</div>
<div class="info-item full-width">
<span class="label">{{ t('customers.form.description') }}:</span>
<span class="value description">{{
customerData.description || t('customers.view.noData')
}}</span>
</div> </div>
</div> </div>
<div class="info-item inline-group full-width"> </div>
<div class="inline-pair">
<span class="label">{{ t('customers.view.expiredLicenses') }}:</span> <!-- 状态信息卡片 -->
<span class="value">{{ customerData.expiredLicenses || 0 }}</span> <div class="info-card status-info">
<h3 class="section-title">{{ t('customers.view.statusInfo') }}</h3>
<div class="info-grid">
<div class="info-item inline-group full-width">
<div class="inline-pair">
<span class="label">{{ t('customers.view.creator') }}:</span>
<span class="value">{{
customerData.statusRecords?.[0]?.user || t('customers.view.noData')
}}</span>
</div>
<div class="inline-pair">
<span class="label">{{ t('customers.view.createTime') }}:</span>
<span class="value">{{ formatDate(customerData.statusRecords?.[0]?.time) }}</span>
</div>
</div> </div>
<div class="inline-pair"> <div class="info-item inline-group full-width">
<span class="label">{{ t('customers.view.expiredTime') }}:</span> <div class="inline-pair">
<span class="value">{{ formatDate(customerData.expiredTime) }}</span> <span class="label">{{ t('customers.view.updater') }}:</span>
<span class="value">{{
customerData.statusRecords?.[1]?.user || t('customers.view.noData')
}}</span>
</div>
<div class="inline-pair">
<span class="label">{{ t('customers.view.updateTime') }}:</span>
<span class="value">{{ formatDate(customerData.statusRecords?.[1]?.time) }}</span>
</div>
</div>
</div>
</div>
<!-- 授权统计卡片 -->
<div class="info-card license-stats">
<h3 class="section-title">{{ t('customers.view.licenseStats') }}</h3>
<div class="info-grid">
<div class="info-item">
<span class="label">{{ t('customers.view.totalLicenses') }}:</span>
<span class="value">{{ customerData.total_auth_codes || 0 }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('customers.view.activeLicenses') }}:</span>
<span class="value">{{ customerData.expired_auth_codes || 0 }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('customers.view.recentLicense') }}:</span>
<span class="value">{{ customerData.expiring_soon_auth_codes || 0 }}</span>
</div>
<div class="info-item inline-group full-width">
<div class="inline-pair">
<span class="label">{{ t('customers.view.expiringSoon') }}:</span>
<span class="value">{{ customerData.total_licenses || 0 }}</span>
</div>
<div class="inline-pair">
<span class="label">{{ t('customers.view.expiringSoonTime') }}:</span>
<span class="value">{{ customerData.active_licenses || 0 }}</span>
</div>
</div>
<div class="info-item inline-group full-width">
<div class="inline-pair">
<span class="label">{{ t('customers.view.expiredLicenses') }}:</span>
<span class="value">{{ customerData.expired_licenses || 0 }}</span>
</div>
<div class="inline-pair">
<span class="label">{{ t('customers.view.expiredTime') }}:</span>
<span class="value">{{ customerData.inactive_licenses }}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -176,13 +186,13 @@ interface CustomerViewData {
preferredLicense: string preferredLicense: string
description: string description: string
statusRecords?: StatusRecord[] statusRecords?: StatusRecord[]
totalLicenses?: number active_licenses: number
activeLicenses?: number expired_auth_codes: number
expiredLicenses?: number expired_licenses: number
expiringSoonLicenses?: number expiring_soon_auth_codes: number
latestLicenseTime?: string inactive_licenses: number
expiringSoonTime?: string total_auth_codes: number
expiredTime?: string total_licenses: number
} }
const props = defineProps<{ const props = defineProps<{
@@ -208,18 +218,18 @@ const enumLabels = ref({
// 获取客户详情数据 // 获取客户详情数据
const fetchCustomerDetail = async () => { const fetchCustomerDetail = async () => {
if (!props.customerId) return if (!props.customerId) return
loading.value = true loading.value = true
try { try {
// 调用真实API接口 // 调用真实API接口
const response = await getCustomerDetail(props.customerId) const response = await getCustomerDetail(props.customerId)
const customer = response.data const customer = response.data
if (!customer) { if (!customer) {
ElMessage.error(t('customers.view.customerNotExist')) ElMessage.error(t('customers.view.customerNotExist'))
return return
} }
// 使用原始枚举值作为显示值 (移除映射功能) // 使用原始枚举值作为显示值 (移除映射功能)
enumLabels.value = { enumLabels.value = {
customerType: customer.customer_type_display || customer.customer_type, customerType: customer.customer_type_display || customer.customer_type,
@@ -227,7 +237,7 @@ const fetchCustomerDetail = async () => {
status: customer.status_display || customer.status, status: customer.status_display || customer.status,
companySize: customer.company_size_display || customer.company_size || '' companySize: customer.company_size_display || customer.company_size || ''
} }
// 转换API数据为组件需要的格式 // 转换API数据为组件需要的格式
customerData.value = { customerData.value = {
name: customer.customer_name, name: customer.customer_name,
@@ -253,14 +263,14 @@ const fetchCustomerDetail = async () => {
time: customer.updated_at time: customer.updated_at
} }
], ],
// 授权统计数据等待后端接口 // 授权统计数据
totalLicenses: 0, total_auth_codes: customer.authorization_stats?.total_auth_codes || 0, // 总授权码数量
activeLicenses: 0, expired_auth_codes: customer.authorization_stats?.expired_auth_codes || 0, // 已过期授权码数量
expiredLicenses: 0, expiring_soon_auth_codes: customer.authorization_stats?.expiring_soon_auth_codes || 0, // 30日内即将到期授权码数量
expiringSoonLicenses: 0, total_licenses: customer.authorization_stats?.total_licenses || 0, // 总许可证数量
latestLicenseTime: undefined, active_licenses: customer.authorization_stats?.active_licenses || 0, // 已激活许可证数量
expiringSoonTime: undefined, expired_licenses: customer.authorization_stats?.expired_licenses || 0, // 已过期许可证数量
expiredTime: undefined inactive_licenses: customer.authorization_stats?.inactive_licenses || 0 // 未激活许可证数量
} }
} catch (error: any) { } catch (error: any) {
console.error('Get customer detail failed:', error) console.error('Get customer detail failed:', error)
@@ -297,8 +307,6 @@ const getStatusLabel = (status: string | undefined) => {
return enumLabels.value.status || t('customers.view.noData') return enumLabels.value.status || t('customers.view.noData')
} }
// 组件挂载时获取数据 // 组件挂载时获取数据
onMounted(() => { onMounted(() => {
fetchCustomerDetail() fetchCustomerDetail()
@@ -306,6 +314,12 @@ onMounted(() => {
</script> </script>
<style scoped> <style scoped>
/* 容器 */
.customer-view-container {
display: flex;
flex-direction: column;
}
/* 顶部横向区域 */ /* 顶部横向区域 */
.top-section { .top-section {
display: flex; display: flex;
@@ -439,7 +453,6 @@ onMounted(() => {
white-space: pre-wrap; white-space: pre-wrap;
} }
/* Element Plus 按钮样式 */ /* Element Plus 按钮样式 */
:deep(.el-button) { :deep(.el-button) {
font-size: 14px; font-size: 14px;
@@ -460,17 +473,17 @@ onMounted(() => {
gap: 16px; gap: 16px;
align-items: stretch; align-items: stretch;
} }
.info-grid { .info-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 12px; gap: 12px;
} }
.info-item { .info-item {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
} }
.label { .label {
margin-bottom: 4px; margin-bottom: 4px;
margin-right: 0; margin-right: 0;
@@ -481,9 +494,9 @@ onMounted(() => {
.info-card { .info-card {
padding: 16px; padding: 16px;
} }
.customer-view { .customer-view {
gap: 16px; gap: 16px;
} }
} }
</style> </style>
+2 -2
View File
@@ -2,7 +2,7 @@
* @Author: 13895237362 2205451508@qq.com * @Author: 13895237362 2205451508@qq.com
* @Date: 2025-08-12 00:00:00 * @Date: 2025-08-12 00:00:00
* @LastEditors: 13895237362 2205451508@qq.com * @LastEditors: 13895237362 2205451508@qq.com
* @LastEditTime: 2025-10-30 14:45:23 * @LastEditTime: 2025-11-03 11:19:50
* @FilePath: /frontend/src/views/Customers/index.vue * @FilePath: /frontend/src/views/Customers/index.vue
* @Description: 客户管理页面 * @Description: 客户管理页面
--> -->
@@ -129,7 +129,7 @@
{{ formatDateShort(scope.row.created_at) }} {{ formatDateShort(scope.row.created_at) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="t('customers.table.operation')" fixed="right" class-name="action-column" align="center"> <el-table-column :label="t('customers.table.operation')" fixed="right" class-name="action-column" align="center" :width="320">
<template #default="scope"> <template #default="scope">
<div class="action-buttons"> <div class="action-buttons">
<button class="action-btn primary" @click="handleViewLicense(scope.row)">{{ t('customers.actions.viewLicense') }}</button> <button class="action-btn primary" @click="handleViewLicense(scope.row)">{{ t('customers.actions.viewLicense') }}</button>
+61 -97
View File
@@ -2,7 +2,7 @@
* @Author: 13895237362 2205451508@qq.com * @Author: 13895237362 2205451508@qq.com
* @Date: 2025-08-01 09:32:42 * @Date: 2025-08-01 09:32:42
* @LastEditors: 13895237362 2205451508@qq.com * @LastEditors: 13895237362 2205451508@qq.com
* @LastEditTime: 2025-10-28 17:28:18 * @LastEditTime: 2025-11-04 15:19:44
* @FilePath: /frontend/src/views/Dashboard.vue * @FilePath: /frontend/src/views/Dashboard.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
--> -->
@@ -27,10 +27,6 @@
<div class="stat-label">{{ stat.label }}</div> <div class="stat-label">{{ stat.label }}</div>
<div class="stat-value-row"> <div class="stat-value-row">
<div class="stat-value">{{ stat.value }}</div> <div class="stat-value">{{ stat.value }}</div>
<div class="stat-trend" v-if="stat.trend">
<img :src="upIcon" alt="trend up" class="trend-icon" width="16" height="16" />
<span class="trend-value">{{ stat.trend }}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -114,11 +110,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import Layout from '@/components/common/layout/Layout.vue' import Layout from '@/components/common/layout/Layout.vue'
import LicenseTrendChart from '@/components/charts/LicenseTrendChart.vue' import LicenseTrendChart from '@/components/charts/LicenseTrendChart.vue'
import { getRecentAuthorizations, type RecentAuthorizationItem } from '@/api/dashboard' import { getRecentAuthorizations, type RecentAuthorizationItem, getOverviewStats} from '@/api/dashboard'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { formatDate } from '@/utils/date' import { formatDate } from '@/utils/date'
@@ -130,62 +126,20 @@ import icon4 from '@/assets/icons/dashboard/icon4.png'
import icon5 from '@/assets/icons/dashboard/icon5.png' import icon5 from '@/assets/icons/dashboard/icon5.png'
import icon6 from '@/assets/icons/dashboard/icon6.png' import icon6 from '@/assets/icons/dashboard/icon6.png'
import icon7 from '@/assets/icons/dashboard/icon7.png' import icon7 from '@/assets/icons/dashboard/icon7.png'
import upIcon from '@/assets/icons/up-icon.svg'
// 使用国际化 // 使用国际化
const { t } = useI18n() const { t } = useI18n()
// 统计卡片数据 // 统计卡片数据(接口动态获取 value,其余静态配置)
const statsData = computed(() => [ const statsData = ref([
{
id: 1, { id: 1, key: 'total_auth_codes', value: '', label: t('dashboard.stats.totalCustomers'), icon: icon1 },//总授权码
value: '100', { id: 2, key: 'active_licenses', value: '', label: t('dashboard.stats.totalLicenses'), icon: icon2 },//活跃许可证
label: t('dashboard.stats.totalCustomers'), { id: 3, key: 'expiring_soon', value: '', label: t('dashboard.stats.activeLicenses'), icon: icon3 },//即将过期
trend: '+1.5%', { id: 4, key: 'abnormal_alerts', value: '', label: t('dashboard.stats.expired'), icon: icon4 },//异常警告
icon: icon1 { id: 5, key: 'growth_rate', value: '', label: t('dashboard.stats.onlineClients'), icon: icon5 },//增长率
}, { id: 6, key: 'auth_codes', value: '', label: t('dashboard.stats.expiringSoon'), icon: icon6 },//授权码增长率
{ { id: 7, key: 'licenses', value: '', label: t('dashboard.stats.authSuccessRate24h'), icon: icon7 }//许可证增长率
id: 2,
value: '100',
label: t('dashboard.stats.totalLicenses'),
trend: '+8.5%',
icon: icon2
},
{
id: 3,
value: '98',
label: t('dashboard.stats.activeLicenses'),
trend: '+15.6%',
icon: icon3
},
{
id: 4,
value: '2',
label: t('dashboard.stats.expired'),
trend: '+8.6%',
icon: icon4
},
{
id: 5,
value: '600',
label: t('dashboard.stats.onlineClients'),
trend: '',
icon: icon5
},
{
id: 6,
value: '50',
label: t('dashboard.stats.expiringSoon'),
trend: '',
icon: icon6
},
{
id: 7,
value: '98%',
label: t('dashboard.stats.authSuccessRate24h'),
trend: '',
icon: icon7
}
]) ])
// 最近授权数据 - 响应式数据 // 最近授权数据 - 响应式数据
@@ -221,9 +175,30 @@ const fetchRecentAuthorizations = async () => {
} }
} }
// 获取仪表盘统计数据
const fetchDashboardStats = async () => {
try {
const resp = await getOverviewStats()
const data = resp.data
statsData.value.forEach(item => {
// 处理直接映射的字段
if (data[item.key] !== undefined && typeof data[item.key] !== 'object') {
item.value = data[item.key]
}
// 处理嵌套在 growth_rate 中的字段
if (data.growth_rate && data.growth_rate[item.key] !== undefined) {
item.value = data.growth_rate[item.key]
}
})
} catch (e) {
ElMessage.error('获取仪表盘统计数据失败')
}
}
// 组件挂载时获取数据 // 组件挂载时获取数据
onMounted(() => { onMounted(() => {
console.log('Dashboard 组件已挂载') console.log('Dashboard 组件已挂载')
fetchDashboardStats()
fetchRecentAuthorizations() fetchRecentAuthorizations()
}) })
</script> </script>
@@ -282,7 +257,7 @@ onMounted(() => {
.stats-card { .stats-card {
display: flex; display: flex;
align-items: center; align-items: stretch;
gap: 0.83vw; /* 16px/1920 = 0.83vw */ gap: 0.83vw; /* 16px/1920 = 0.83vw */
position: relative; position: relative;
z-index: 1; z-index: 1;
@@ -306,9 +281,13 @@ onMounted(() => {
} }
.stat-icon { .stat-icon {
display: flex;
align-items: center;
height: 100%;
.icon-circle { .icon-circle {
width: 2.08vw; /* 40px/1920 = 2.08vw */ height: 100%;
height: 2.08vw; /* 40px/1920 = 2.08vw */ aspect-ratio: 1; /* 保持圆形 */
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
@@ -316,8 +295,8 @@ onMounted(() => {
justify-content: center; justify-content: center;
.stat-icon-img { .stat-icon-img {
width: 0.83vw; /* 16px/1920 = 0.83vw */ width: 50%; /* icon-circle 的一半 */
height: 0.83vw; /* 16px/1920 = 0.83vw */ height: 50%; /* icon-circle 的一半 */
filter: brightness(0) invert(1) opacity(0.9); /* 将图标转为白色半透明 */ filter: brightness(0) invert(1) opacity(0.9); /* 将图标转为白色半透明 */
object-fit: contain; object-fit: contain;
} }
@@ -327,6 +306,10 @@ onMounted(() => {
.stat-content { .stat-content {
flex: 0 1 auto; flex: 0 1 auto;
min-width: 4.17vw; /* 80px/1920 = 4.17vw */ min-width: 4.17vw; /* 80px/1920 = 4.17vw */
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
.stat-label { .stat-label {
font-size: 0.83vw; /* 16px/1920 = 0.83vw */ font-size: 0.83vw; /* 16px/1920 = 0.83vw */
@@ -349,35 +332,14 @@ onMounted(() => {
} }
} }
.stat-trend {
display: flex;
align-items: center;
gap: 0.21vw; /* 4px/1920 = 0.21vw */
color: rgba(255, 255, 255, 0.8);
.trend-icon {
filter: brightness(0) invert(1) opacity(0.8);
object-fit: contain;
}
.trend-value {
font-size: 0.73vw; /* 14px/1920 = 0.73vw */
font-weight: 500;
}
}
// 紧凑型卡片样式(专门针对到期提醒) // 紧凑型卡片样式(专门针对到期提醒)
.stats-card.compact-card { .stats-card.compact-card {
.stat-content { min-width: 10vw !important; /* 48px/1920 = 2.5vw */
min-width: 2.5vw !important; /* 48px/1920 = 2.5vw */
}
} }
// 大型卡片样式(专门针对近24小时授权验证成功率) // 大型卡片样式(专门针对近24小时授权验证成功率)
.stats-card.large-card { .stats-card.large-card {
.stat-content { min-width: 10vw !important; /* 115px/1920 = 6vw */
min-width: 6vw !important; /* 115px/1920 = 6vw */
}
} }
// 内容区域 - 默认上下布局,充满屏幕宽度和高度 // 内容区域 - 默认上下布局,充满屏幕宽度和高度
@@ -534,13 +496,19 @@ onMounted(() => {
} }
} }
.stat-icon {
height: auto;
}
.stat-icon .icon-circle { .stat-icon .icon-circle {
width: 36px; /* 移动端固定大小 */ height: 100%;
height: 36px; aspect-ratio: 1; /* 保持圆形 */
min-width: 36px; /* 移动端最小大小 */
min-height: 36px;
.stat-icon-img { .stat-icon-img {
width: 14px; width: 50%; /* icon-circle 的一半 */
height: 14px; height: 50%; /* icon-circle 的一半 */
filter: brightness(0) invert(1) opacity(0.9); /* 将图标转为白色半透明 */ filter: brightness(0) invert(1) opacity(0.9); /* 将图标转为白色半透明 */
object-fit: contain; object-fit: contain;
} }
@@ -563,10 +531,6 @@ onMounted(() => {
} }
} }
.stat-trend .trend-value {
font-size: 12px;
}
.content-section { .content-section {
gap: 16px; gap: 16px;
} }
+3 -30
View File
@@ -31,7 +31,7 @@
<div class="license-form"> <div class="license-form">
<h3 class="section-title">{{ t('pages.licenses.form.sections.basicInfo') }}</h3> <h3 class="section-title">{{ t('pages.licenses.form.sections.basicInfo') }}</h3>
<!-- 第一行:关联客户名称,客户ID,创建人 --> <!-- 第一行:关联客户名称,客户ID -->
<div class="fields-row"> <div class="fields-row">
<el-form-item <el-form-item
:label="t('pages.licenses.form.fields.customerName')" :label="t('pages.licenses.form.fields.customerName')"
@@ -69,19 +69,6 @@
disabled disabled
/> />
</el-form-item> </el-form-item>
<el-form-item
:label="t('pages.licenses.form.fields.createdBy')"
prop="created_by"
required
class="field-item"
>
<el-input
v-model="formData.created_by"
:placeholder="t('pages.licenses.form.placeholders.currentUser')"
disabled
/>
</el-form-item>
</div> </div>
<!-- 第二行:备注单独一行 --> <!-- 第二行:备注单独一行 -->
@@ -94,6 +81,8 @@
> >
<el-input <el-input
v-model="formData.description" v-model="formData.description"
type="textarea"
:rows="4"
:placeholder="t('pages.licenses.form.placeholders.enterDescription')" :placeholder="t('pages.licenses.form.placeholders.enterDescription')"
/> />
</el-form-item> </el-form-item>
@@ -240,11 +229,9 @@ import {
} from '@/api/license' } from '@/api/license'
import { getCustomers, type Customer } from '@/api/customer' import { getCustomers, type Customer } from '@/api/customer'
import { getEnumOptions, type RawEnumItem } from '@/api/enum' import { getEnumOptions, type RawEnumItem } from '@/api/enum'
import { useUserStore } from '@/store/modules/user'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const userStore = useUserStore()
const { t } = useI18n() const { t } = useI18n()
// 表单引用 // 表单引用
@@ -263,13 +250,11 @@ const encryptionTypeOptions = ref<RawEnumItem[]>([])
const formData = reactive< const formData = reactive<
AuthorizationCodeCreateRequest & { AuthorizationCodeCreateRequest & {
customer_code: string customer_code: string
created_by: string
validity_type: 'permanent' | 'limited' validity_type: 'permanent' | 'limited'
} }
>({ >({
customer_id: '', customer_id: '',
customer_code: '', customer_code: '',
created_by: '',
validity_type: 'limited', validity_type: 'limited',
description: '', description: '',
validity_days: 365, validity_days: 365,
@@ -511,12 +496,6 @@ const loadCustomerInfo = () => {
} as Customer] } as Customer]
} }
// 设置创建人(从用户store获取当前登录用户)
if (userStore.userInfo) {
formData.created_by = userStore.userInfo.username
} else {
formData.created_by = t('pages.licenses.form.placeholders.currentUser')
}
} }
// 处理授权期限类型变化 // 处理授权期限类型变化
@@ -570,12 +549,6 @@ const loadLicenseDetail = async () => {
// 设置表单字段 // 设置表单字段
formData.customer_code = (data as any).customer_code || data.customer_id || '' formData.customer_code = (data as any).customer_code || data.customer_id || ''
// 编辑模式下显示原始创建人,如果没有则显示当前用户
formData.created_by =
(data as any).created_by ||
(userStore.userInfo
? userStore.userInfo.username
: t('pages.licenses.form.placeholders.currentUser'))
// 计算有效期天数和设置日期范围 // 计算有效期天数和设置日期范围
if (data.start_date && data.end_date) { if (data.start_date && data.end_date) {
+86 -4
View File
@@ -62,11 +62,25 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
prop="deployment_type_display" :label="t('pages.licenses.list.table.activationProgress')"
:label="t('pages.licenses.list.table.deploymentType')" :width="220"
:width="140"
align="center" align="center"
/> >
<template #default="scope">
<div
class="progress-container"
:class="{ 'zero-activated': (scope.row.activated_licenses_count || 0) === 0 }"
>
<el-progress
:percentage="getActivationProgress(scope.row)"
:stroke-width="8"
:show-text="true"
:text-inside="true"
:format="() => `${scope.row.activated_licenses_count || 0}/${scope.row.max_activations}`"
/>
</div>
</template>
</el-table-column>
<el-table-column prop="end_date" :label="t('pages.licenses.list.table.endDate')" :width="180" align="center"> <el-table-column prop="end_date" :label="t('pages.licenses.list.table.endDate')" :width="180" align="center">
<template #default="scope"> <template #default="scope">
{{ formatDate(scope.row.end_date) }} {{ formatDate(scope.row.end_date) }}
@@ -171,6 +185,15 @@ const getStatusClass = (status: string) => {
} }
} }
const getActivationProgress = (row: AuthorizationCode) => {
if (!row.max_activations || row.max_activations === 0) {
return 0
}
const activated = row.activated_licenses_count || 0
return Math.round((activated / row.max_activations) * 100)
}
const handleCreate = () => { const handleCreate = () => {
router.push({ router.push({
name: 'licenses-create', name: 'licenses-create',
@@ -517,6 +540,7 @@ onMounted(async () => {
.status-tag { .status-tag {
padding: $spacing-extra-small $spacing-small; padding: $spacing-extra-small $spacing-small;
margin: 0 10px;
border-radius: $border-radius-small; border-radius: $border-radius-small;
font-size: $font-size-base; font-size: $font-size-base;
font-weight: $font-weight-secondary; font-weight: $font-weight-secondary;
@@ -546,6 +570,64 @@ onMounted(async () => {
line-height: 24px; // 改为24px符合8px栅格 line-height: 24px; // 改为24px符合8px栅格
} }
.progress-container {
width: 100%;
padding: 0 8px;
display: flex;
align-items: center;
justify-content: center;
:deep(.el-progress) {
width: 100%;
min-width: 160px;
}
:deep(.el-progress-bar__outer) {
background: #4763FF1A !important;
min-height: 24px;
position: relative;
}
:deep(.el-progress-bar__inner) {
background: #4763FF !important;
min-height: 24px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
:deep(.el-progress-bar__innerText) {
color: #FFFFFF !important;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
z-index: 10;
pointer-events: none;
}
:deep(.el-progress__text) {
color: #FFFFFF !important;
font-size: 12px;
font-weight: 500;
z-index: 10;
}
// 当 activated_licenses_count 为 0 时,文字显示为红色
&.zero-activated {
:deep(.el-progress-bar__innerText),
:deep(.el-progress__text) {
color: #F56C6C !important;
margin-left: 50px !important;
}
// 当百分比为0时,确保外部文字可见且为红色
:deep(.el-progress-bar__outer) {
position: relative;
}
}
}
// 操作按钮组,严格复制 customers 页 action-btn/action-buttons 样式 // 操作按钮组,严格复制 customers 页 action-btn/action-buttons 样式
.action-buttons { .action-buttons {
gap: $spacing-small; gap: $spacing-small;