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'
// 授权统计信息类型
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 {
id: string;
@@ -23,6 +34,7 @@ export interface Customer {
updated_at: string;
created_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": {
"code": "License Code",
"status": "Status",
"deploymentType": "Deployment Type",
"activationProgress": "Activation Progress",
"endDate": "Expiry Time",
"description": "Description",
"operation": "Operation",
@@ -562,13 +562,13 @@
"createTime": "Create Time",
"updater": "Updater",
"updateTime": "Update Time",
"totalLicenses": "Total Licenses",
"activeLicenses": "Expired Licenses",
"recentLicense": "Licenses Expiring Within 30 Days",
"expiringSoon": "Total License Count",
"expiringSoonTime": "Activated License Count",
"expiredLicenses": "Unactivated License Count",
"expiredTime": "Expired License Count",
"totalLicenses": "Total Authorization Codes",
"activeLicenses": "Expired Authorization Codes",
"recentLicense": "Authorization Codes Expiring Within 30 Days",
"expiringSoon": "Total Licenses",
"expiringSoonTime": "Activated Licenses",
"expiredLicenses": "Expired Licenses",
"expiredTime": "Inactivated Licenses",
"noData": "--",
"onlineLicense": "Online License",
"customerNotExist": "Customer data does not exist"
+6 -6
View File
@@ -136,7 +136,7 @@
"table": {
"code": "ライセンスコード",
"status": "ステータス",
"deploymentType": "デプロイメントタイプ",
"activationProgress": "アクティベーション進捗",
"endDate": "有効期限",
"description": "説明",
"operation": "操作",
@@ -562,13 +562,13 @@
"createTime": "作成日時",
"updater": "更新者",
"updateTime": "更新日時",
"totalLicenses": "総ライセンス数",
"activeLicenses": "期限切れライセンス",
"recentLicense": "30日以内に期限切れするライセンスコード数量",
"totalLicenses": "総認証コード数",
"activeLicenses": "期限切れ認証コード",
"recentLicense": "30日以内に期限切れする認証コード数量",
"expiringSoon": "総ライセンス数量",
"expiringSoonTime": "アクティベート済みライセンス数量",
"expiredLicenses": "未アクティベートライセンス数量",
"expiredTime": "期限切れライセンス数量",
"expiredLicenses": "期限切れライセンス数量",
"expiredTime": "未アクティベートライセンス数量",
"noData": "--",
"onlineLicense": "オンラインライセンス",
"customerNotExist": "顧客データが存在しません"
+10 -10
View File
@@ -26,13 +26,13 @@
"dashboard": {
"title": "仪表盘",
"stats": {
"totalCustomers": "客户总数",
"totalLicenses": "总授权",
"activeLicenses": "有效授权",
"expired": "过期授权",
"onlineClients": "在线客户端数量",
"expiringSoon": "到期提醒",
"authSuccessRate24h": "近24小时授权验证成功率"
"totalCustomers": "总授权码",
"totalLicenses": "活跃许可证",
"activeLicenses": "即将过期",
"expired": "异常警告",
"onlineClients": "增长率",
"expiringSoon": "授权码增长率",
"authSuccessRate24h": "许可证增长率"
},
"recentLicenses": {
"title": "最近授权",
@@ -136,7 +136,7 @@
"table": {
"code": "授权码",
"status": "状态",
"deploymentType": "部署类型",
"activationProgress": "激活进度",
"endDate": "到期时间",
"description": "备注",
"operation": "操作",
@@ -567,8 +567,8 @@
"recentLicense": "30日内即将到期授权码数量 ",
"expiringSoon": "总许可证数量",
"expiringSoonTime": "已激活许可证数量",
"expiredLicenses": "未激活许可证数量",
"expiredTime": "已过期许可证数量",
"expiredLicenses": "已过期许可证数量",
"expiredTime": "未激活许可证数量",
"noData": "--",
"onlineLicense": "在线授权",
"customerNotExist": "客户数据不存在"
+162 -149
View File
@@ -2,147 +2,157 @@
* @Author: 13895237362 2205451508@qq.com
* @Date: 2025-08-20 16:00:00
* @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
* @Description: 客户查看组件
-->
<template>
<!-- 顶部横向区域 - 面包屑和返回按钮 -->
<div class="top-section">
<!-- 面包屑导航 -->
<div class="breadcrumb">
<span class="breadcrumb-item">{{ t('customers.breadcrumb.customerManagement') }}</span>
<span class="breadcrumb-separator">></span>
<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-container">
<!-- 顶部横向区域 - 面包屑和返回按钮 -->
<div class="top-section">
<!-- 面包屑导航 -->
<div class="breadcrumb">
<span class="breadcrumb-item">{{ t('customers.breadcrumb.customerManagement') }}</span>
<span class="breadcrumb-separator">></span>
<span class="breadcrumb-current">{{ t('customers.viewCustomer') }}</span>
</div>
<!-- 主内容区域 -->
<div class="customer-view" v-loading="loading">
<!-- 基本信息卡片 -->
<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 class="form-actions">
<el-button @click="handleBack">{{ t('customers.view.back') }}</el-button>
</div>
</div>
<!-- 商业信息卡片 -->
<div class="info-card business-info">
<h3 class="section-title">{{ t('customers.form.businessInfo') }}</h3>
<div class="info-grid">
<div class="info-item">
<span class="label">{{ t('customers.form.companySize') }}:</span>
<span class="value">{{ enumLabels.companySize || t('customers.view.noData') }}</span>
</div>
<div class="info-item full-width">
<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 class="customer-view" v-loading="loading">
<!-- 基本信息卡片 -->
<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="inline-pair">
<span class="label">{{ t('customers.view.createTime') }}:</span>
<span class="value">{{ formatDate(customerData.statusRecords?.[0]?.time) }}</span>
<div class="info-item">
<span class="label">{{ t('customers.form.customerType') }}:</span>
<span class="value">{{ getCustomerTypeLabel(customerData.type) }}</span>
</div>
</div>
<div class="info-item inline-group full-width">
<div class="inline-pair">
<span class="label">{{ t('customers.view.updater') }}:</span>
<span class="value">{{ customerData.statusRecords?.[1]?.user || t('customers.view.noData') }}</span>
<div class="info-item">
<span class="label">{{ t('customers.form.customerLevel') }}:</span>
<span class="value">{{ getCustomerLevelLabel(customerData.level) }}</span>
</div>
<div class="inline-pair">
<span class="label">{{ t('customers.view.updateTime') }}:</span>
<span class="value">{{ formatDate(customerData.statusRecords?.[1]?.time) }}</span>
<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 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.totalLicenses || 0 }}</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 class="info-card business-info">
<h3 class="section-title">{{ t('customers.form.businessInfo') }}</h3>
<div class="info-grid">
<div class="info-item">
<span class="label">{{ t('customers.form.companySize') }}:</span>
<span class="value">{{ enumLabels.companySize || t('customers.view.noData') }}</span>
</div>
<div class="inline-pair">
<span class="label">{{ t('customers.view.expiringSoonTime') }}:</span>
<span class="value">{{ formatDate(customerData.expiringSoonTime) }}</span>
<div class="info-item full-width">
<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 class="info-item inline-group full-width">
<div class="inline-pair">
<span class="label">{{ t('customers.view.expiredLicenses') }}:</span>
<span class="value">{{ customerData.expiredLicenses || 0 }}</span>
</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 class="inline-pair">
<span class="label">{{ t('customers.view.createTime') }}:</span>
<span class="value">{{ formatDate(customerData.statusRecords?.[0]?.time) }}</span>
</div>
</div>
<div class="inline-pair">
<span class="label">{{ t('customers.view.expiredTime') }}:</span>
<span class="value">{{ formatDate(customerData.expiredTime) }}</span>
<div class="info-item inline-group full-width">
<div class="inline-pair">
<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>
@@ -176,13 +186,13 @@ interface CustomerViewData {
preferredLicense: string
description: string
statusRecords?: StatusRecord[]
totalLicenses?: number
activeLicenses?: number
expiredLicenses?: number
expiringSoonLicenses?: number
latestLicenseTime?: string
expiringSoonTime?: string
expiredTime?: string
active_licenses: number
expired_auth_codes: number
expired_licenses: number
expiring_soon_auth_codes: number
inactive_licenses: number
total_auth_codes: number
total_licenses: number
}
const props = defineProps<{
@@ -208,18 +218,18 @@ const enumLabels = ref({
// 获取客户详情数据
const fetchCustomerDetail = async () => {
if (!props.customerId) return
loading.value = true
try {
// 调用真实API接口
const response = await getCustomerDetail(props.customerId)
const customer = response.data
if (!customer) {
ElMessage.error(t('customers.view.customerNotExist'))
return
}
// 使用原始枚举值作为显示值 (移除映射功能)
enumLabels.value = {
customerType: customer.customer_type_display || customer.customer_type,
@@ -227,7 +237,7 @@ const fetchCustomerDetail = async () => {
status: customer.status_display || customer.status,
companySize: customer.company_size_display || customer.company_size || ''
}
// 转换API数据为组件需要的格式
customerData.value = {
name: customer.customer_name,
@@ -253,14 +263,14 @@ const fetchCustomerDetail = async () => {
time: customer.updated_at
}
],
// 授权统计数据等待后端接口
totalLicenses: 0,
activeLicenses: 0,
expiredLicenses: 0,
expiringSoonLicenses: 0,
latestLicenseTime: undefined,
expiringSoonTime: undefined,
expiredTime: undefined
// 授权统计数据
total_auth_codes: customer.authorization_stats?.total_auth_codes || 0, // 总授权码数量
expired_auth_codes: customer.authorization_stats?.expired_auth_codes || 0, // 已过期授权码数量
expiring_soon_auth_codes: customer.authorization_stats?.expiring_soon_auth_codes || 0, // 30日内即将到期授权码数量
total_licenses: customer.authorization_stats?.total_licenses || 0, // 总许可证数量
active_licenses: customer.authorization_stats?.active_licenses || 0, // 已激活许可证数量
expired_licenses: customer.authorization_stats?.expired_licenses || 0, // 已过期许可证数量
inactive_licenses: customer.authorization_stats?.inactive_licenses || 0 // 未激活许可证数量
}
} catch (error: any) {
console.error('Get customer detail failed:', error)
@@ -297,8 +307,6 @@ const getStatusLabel = (status: string | undefined) => {
return enumLabels.value.status || t('customers.view.noData')
}
// 组件挂载时获取数据
onMounted(() => {
fetchCustomerDetail()
@@ -306,6 +314,12 @@ onMounted(() => {
</script>
<style scoped>
/* 容器 */
.customer-view-container {
display: flex;
flex-direction: column;
}
/* 顶部横向区域 */
.top-section {
display: flex;
@@ -439,7 +453,6 @@ onMounted(() => {
white-space: pre-wrap;
}
/* Element Plus 按钮样式 */
:deep(.el-button) {
font-size: 14px;
@@ -460,17 +473,17 @@ onMounted(() => {
gap: 16px;
align-items: stretch;
}
.info-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.info-item {
flex-direction: column;
align-items: flex-start;
}
.label {
margin-bottom: 4px;
margin-right: 0;
@@ -481,9 +494,9 @@ onMounted(() => {
.info-card {
padding: 16px;
}
.customer-view {
gap: 16px;
}
}
</style>
</style>
+2 -2
View File
@@ -2,7 +2,7 @@
* @Author: 13895237362 2205451508@qq.com
* @Date: 2025-08-12 00:00:00
* @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
* @Description: 客户管理页面
-->
@@ -129,7 +129,7 @@
{{ formatDateShort(scope.row.created_at) }}
</template>
</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">
<div class="action-buttons">
<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
* @Date: 2025-08-01 09:32:42
* @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
* @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-value-row">
<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>
@@ -114,11 +110,11 @@
</template>
<script setup lang="ts">
import { computed, ref, onMounted } from 'vue'
import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import Layout from '@/components/common/layout/Layout.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 { 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 icon6 from '@/assets/icons/dashboard/icon6.png'
import icon7 from '@/assets/icons/dashboard/icon7.png'
import upIcon from '@/assets/icons/up-icon.svg'
// 使用国际化
const { t } = useI18n()
// 统计卡片数据
const statsData = computed(() => [
{
id: 1,
value: '100',
label: t('dashboard.stats.totalCustomers'),
trend: '+1.5%',
icon: icon1
},
{
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
}
// 统计卡片数据(接口动态获取 value,其余静态配置)
const statsData = ref([
{ id: 1, key: 'total_auth_codes', value: '', label: t('dashboard.stats.totalCustomers'), icon: icon1 },//总授权码
{ id: 2, key: 'active_licenses', value: '', label: t('dashboard.stats.totalLicenses'), icon: icon2 },//活跃许可证
{ id: 3, key: 'expiring_soon', value: '', label: t('dashboard.stats.activeLicenses'), icon: icon3 },//即将过期
{ id: 4, key: 'abnormal_alerts', value: '', label: t('dashboard.stats.expired'), icon: icon4 },//异常警告
{ 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 }//许可证增长率
])
// 最近授权数据 - 响应式数据
@@ -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(() => {
console.log('Dashboard 组件已挂载')
fetchDashboardStats()
fetchRecentAuthorizations()
})
</script>
@@ -282,7 +257,7 @@ onMounted(() => {
.stats-card {
display: flex;
align-items: center;
align-items: stretch;
gap: 0.83vw; /* 16px/1920 = 0.83vw */
position: relative;
z-index: 1;
@@ -306,9 +281,13 @@ onMounted(() => {
}
.stat-icon {
display: flex;
align-items: center;
height: 100%;
.icon-circle {
width: 2.08vw; /* 40px/1920 = 2.08vw */
height: 2.08vw; /* 40px/1920 = 2.08vw */
height: 100%;
aspect-ratio: 1; /* 保持圆形 */
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: flex;
@@ -316,8 +295,8 @@ onMounted(() => {
justify-content: center;
.stat-icon-img {
width: 0.83vw; /* 16px/1920 = 0.83vw */
height: 0.83vw; /* 16px/1920 = 0.83vw */
width: 50%; /* icon-circle 的一半 */
height: 50%; /* icon-circle 的一半 */
filter: brightness(0) invert(1) opacity(0.9); /* 将图标转为白色半透明 */
object-fit: contain;
}
@@ -327,6 +306,10 @@ onMounted(() => {
.stat-content {
flex: 0 1 auto;
min-width: 4.17vw; /* 80px/1920 = 4.17vw */
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
.stat-label {
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 {
.stat-content {
min-width: 2.5vw !important; /* 48px/1920 = 2.5vw */
}
.stats-card.compact-card {
min-width: 10vw !important; /* 48px/1920 = 2.5vw */
}
// 大型卡片样式(专门针对近24小时授权验证成功率)
.stats-card.large-card {
.stat-content {
min-width: 6vw !important; /* 115px/1920 = 6vw */
}
.stats-card.large-card {
min-width: 10vw !important; /* 115px/1920 = 6vw */
}
// 内容区域 - 默认上下布局,充满屏幕宽度和高度
@@ -534,13 +496,19 @@ onMounted(() => {
}
}
.stat-icon {
height: auto;
}
.stat-icon .icon-circle {
width: 36px; /* 移动端固定大小 */
height: 36px;
height: 100%;
aspect-ratio: 1; /* 保持圆形 */
min-width: 36px; /* 移动端最小大小 */
min-height: 36px;
.stat-icon-img {
width: 14px;
height: 14px;
width: 50%; /* icon-circle 的一半 */
height: 50%; /* icon-circle 的一半 */
filter: brightness(0) invert(1) opacity(0.9); /* 将图标转为白色半透明 */
object-fit: contain;
}
@@ -563,10 +531,6 @@ onMounted(() => {
}
}
.stat-trend .trend-value {
font-size: 12px;
}
.content-section {
gap: 16px;
}
+3 -30
View File
@@ -31,7 +31,7 @@
<div class="license-form">
<h3 class="section-title">{{ t('pages.licenses.form.sections.basicInfo') }}</h3>
<!-- 第一行:关联客户名称,客户ID,创建人 -->
<!-- 第一行:关联客户名称,客户ID -->
<div class="fields-row">
<el-form-item
:label="t('pages.licenses.form.fields.customerName')"
@@ -69,19 +69,6 @@
disabled
/>
</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>
<!-- 第二行:备注单独一行 -->
@@ -94,6 +81,8 @@
>
<el-input
v-model="formData.description"
type="textarea"
:rows="4"
:placeholder="t('pages.licenses.form.placeholders.enterDescription')"
/>
</el-form-item>
@@ -240,11 +229,9 @@ import {
} from '@/api/license'
import { getCustomers, type Customer } from '@/api/customer'
import { getEnumOptions, type RawEnumItem } from '@/api/enum'
import { useUserStore } from '@/store/modules/user'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const { t } = useI18n()
// 表单引用
@@ -263,13 +250,11 @@ const encryptionTypeOptions = ref<RawEnumItem[]>([])
const formData = reactive<
AuthorizationCodeCreateRequest & {
customer_code: string
created_by: string
validity_type: 'permanent' | 'limited'
}
>({
customer_id: '',
customer_code: '',
created_by: '',
validity_type: 'limited',
description: '',
validity_days: 365,
@@ -511,12 +496,6 @@ const loadCustomerInfo = () => {
} 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.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) {
+86 -4
View File
@@ -62,11 +62,25 @@
</template>
</el-table-column>
<el-table-column
prop="deployment_type_display"
:label="t('pages.licenses.list.table.deploymentType')"
:width="140"
:label="t('pages.licenses.list.table.activationProgress')"
:width="220"
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">
<template #default="scope">
{{ 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 = () => {
router.push({
name: 'licenses-create',
@@ -517,6 +540,7 @@ onMounted(async () => {
.status-tag {
padding: $spacing-extra-small $spacing-small;
margin: 0 10px;
border-radius: $border-radius-small;
font-size: $font-size-base;
font-weight: $font-weight-secondary;
@@ -546,6 +570,64 @@ onMounted(async () => {
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 样式
.action-buttons {
gap: $spacing-small;