feat: enhance dashboard stats API and UI with new metrics and improved localization

This commit is contained in:
jie committed 2026-03-13 01:17:24 +08:00
1 parent 8410a2cac4
commit bc5d654a59
8 files changed
+248 -101

No files matched your search

+22 -1
View File
@@ -58,6 +58,27 @@ export const getAuthorizationTrend = (params: {
}
// 获取仪表盘统计数据(新接口,返回卡片区数据)
export const getOverviewStats = () => {
export interface GrowthRate {
auth_codes_mom: number
licenses_mom: number
}
export interface StatsOverviewData {
// stock metrics
total_auth_codes: number
active_licenses: number
// flow metrics
today_new_licenses: number
yesterday_new_licenses: number
month_new_auth_codes: number
// risk metrics
expiring_in_7days: number
expiring_in_30days: number
abnormal_alerts: number
// growth rates (sub-text only)
growth_rate: GrowthRate
}
export const getOverviewStats = (): Promise<ApiResponse<StatsOverviewData>> => {
return Axios.get('/api/v1/stats/overview')
}
+17 -6
View File
@@ -22,7 +22,12 @@
"heroTitleLine2": "Management",
"heroSubtitle": "Streamline your software licensing workflow with our enterprise-grade platform.",
"stats": {
"totalAuthCodes": "Total Auth Codes",
"totalAuthCodesSubLabel": "2.4K new this month",
"activeLicenses": "Active Licenses",
"activeLicensesSubLabel": "+12% vs. last month",
"systemUptime": "System Uptime",
"systemUptimeSubLabel": "24/7 Online",
"uptime": "Uptime",
"support": "Support"
},
@@ -37,13 +42,19 @@
"dashboard": {
"title": "Dashboard",
"stats": {
"totalCustomers": "Total Customers",
"totalLicenses": "Total Licenses",
"totalAuthCodes": "Total Auth Codes",
"activeLicenses": "Active Licenses",
"expired": "Expired",
"onlineClients": "Online Clients",
"expiringSoon": "Expiring Soon",
"authSuccessRate24h": "24h Auth Success Rate"
"todayNewLicenses": "New Today",
"expiringIn7Days": "Expiring in 7 Days",
"expiringIn30Days": "Expiring in 30 Days",
"abnormalAlerts": "Abnormal Alerts",
"subText": {
"monthNew": "{n} new this month",
"vsLastMonth": "{rate}% vs. last month",
"yesterday": "{n} yesterday",
"urgent": "Urgent Follow-up",
"heartbeatTimeout": "Heartbeat Timeout"
}
},
"recentLicenses": {
"title": "Recent Licenses",
+17 -6
View File
@@ -22,7 +22,12 @@
"heroTitleLine2": "ライセンス管理",
"heroSubtitle": "エンタープライズグレードのプラットフォームで、ソフトウェアライセンス管理を効率化。",
"stats": {
"totalAuthCodes": "認可コード総数",
"totalAuthCodesSubLabel": "今月 2.4K 件追加",
"activeLicenses": "アクティブライセンス",
"activeLicensesSubLabel": "先月比 +12%",
"systemUptime": "システム稼働率",
"systemUptimeSubLabel": "24/7 オンライン",
"uptime": "稼働率",
"support": "サポート"
},
@@ -37,13 +42,19 @@
"dashboard": {
"title": "ダッシュボード",
"stats": {
"totalCustomers": "顧客総数",
"totalLicenses": "総ライセンス数",
"totalAuthCodes": "認可コード総数",
"activeLicenses": "アクティブライセンス",
"expired": "期限切れ",
"onlineClients": "オンラインクライアント数",
"expiringSoon": "期限近し",
"authSuccessRate24h": "24時間認証成功率"
"todayNewLicenses": "本日の新規",
"expiringIn7Days": "7日以内に期限切れ",
"expiringIn30Days": "30日以内に期限切れ",
"abnormalAlerts": "異常アラート",
"subText": {
"monthNew": "今月 {n} 件追加",
"vsLastMonth": "先月比 {rate}%",
"yesterday": "昨日 {n} 件",
"urgent": "緊急対応が必要",
"heartbeatTimeout": "ハートビートタイムアウト"
}
},
"recentLicenses": {
"title": "最近のライセンス",
+18 -7
View File
@@ -22,7 +22,12 @@
"heroTitleLine2": "管理系统",
"heroSubtitle": "赋能软件商业化,构建 B2B 与 B2C 全场景的软件授权体系。",
"stats": {
"totalAuthCodes": "授权码总量",
"totalAuthCodesSubLabel": "本月新增 2.4K",
"activeLicenses": "活跃许可证",
"activeLicensesSubLabel": "较上月 +12%",
"systemUptime": "系统可用性",
"systemUptimeSubLabel": "24/7 在线",
"uptime": "可用率",
"support": "全天支持"
},
@@ -37,13 +42,19 @@
"dashboard": {
"title": "仪表盘",
"stats": {
"totalCustomers": "总授权码",
"totalLicenses": "活跃许可证",
"activeLicenses": "即将过期",
"expired": "异常警告",
"onlineClients": "增长率",
"expiringSoon": "授权码增长率",
"authSuccessRate24h": "许可证增长率"
"totalAuthCodes": "授权码总量",
"activeLicenses": "活跃许可证",
"todayNewLicenses": "今日新增授权",
"expiringIn7Days": "7天内到期",
"expiringIn30Days": "30天内到期",
"abnormalAlerts": "异常告警",
"subText": {
"monthNew": "本月新增 {n} 个",
"vsLastMonth": "较上月 {rate}%",
"yesterday": "昨日 {n} 个",
"urgent": "紧急跟进",
"heartbeatTimeout": "心跳超时"
}
},
"recentLicenses": {
"title": "最近授权",
+79 -32
View File
@@ -10,11 +10,10 @@
<Layout app-name="Cedar-V" :page-title="t('dashboard.title')">
<!-- 页面内容 -->
<div class="content-container dashboard">
<!-- 统计卡片区域 -->
<!-- stats card area -->
<div class="stats-section">
<div
class="stats-card"
:class="{ 'compact-card': stat.id === 6, 'large-card': stat.id === 7 }"
v-for="stat in statsData"
:key="stat.id"
>
@@ -28,6 +27,9 @@
<div class="stat-value-row">
<div class="stat-value">{{ stat.value }}</div>
</div>
<div v-if="stat.subText" class="stat-sub-text" :class="stat.subTextClass">
{{ stat.subText }}
</div>
</div>
</div>
</div>
@@ -114,7 +116,7 @@ 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, getOverviewStats} from '@/api/dashboard'
import { getRecentAuthorizations, type RecentAuthorizationItem, getOverviewStats, type StatsOverviewData } from '@/api/dashboard'
import { ElMessage } from 'element-plus'
import { formatDate } from '@/utils/date'
@@ -129,16 +131,14 @@ import icon7 from '@/assets/icons/dashboard/icon7.png'
// 使用国际化
const { t } = useI18n()
// 统计卡片数据(接口动态获取 value,其余静态配置)
// stats card definitions — value and subText are filled dynamically from API
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 }//许可证增长率
{ id: 1, key: 'total_auth_codes', value: '', label: t('dashboard.stats.totalAuthCodes'), icon: icon1, subText: '', subTextClass: '' },
{ id: 2, key: 'active_licenses', value: '', label: t('dashboard.stats.activeLicenses'), icon: icon2, subText: '', subTextClass: '' },
{ id: 3, key: 'today_new_licenses', value: '', label: t('dashboard.stats.todayNewLicenses'), icon: icon3, subText: '', subTextClass: '' },
{ id: 4, key: 'expiring_in_7days', value: '', label: t('dashboard.stats.expiringIn7Days'), icon: icon4, subText: '', subTextClass: '' },
{ id: 5, key: 'expiring_in_30days', value: '', label: t('dashboard.stats.expiringIn30Days'), icon: icon6, subText: '', subTextClass: '' },
{ id: 6, key: 'abnormal_alerts', value: '', label: t('dashboard.stats.abnormalAlerts'), icon: icon7, subText: '', subTextClass: '' },
])
// 最近授权数据 - 响应式数据
@@ -174,29 +174,57 @@ const fetchRecentAuthorizations = async () => {
}
}
// 获取仪表盘统计数据
// Populates statsData cards from API response
const applyStatsData = (data: StatsOverviewData) => {
statsData.value.forEach(item => {
const raw = (data as any)[item.key]
if (raw !== undefined) item.value = String(raw)
})
// Card 1: total auth codes + month new as sub-text
const card1 = statsData.value.find(c => c.key === 'total_auth_codes')
if (card1) {
card1.subText = t('dashboard.stats.subText.monthNew').replace('{n}', String(data.month_new_auth_codes))
card1.subTextClass = 'sub-positive'
}
// Card 2: active licenses + MoM growth rate as sub-text
const card2 = statsData.value.find(c => c.key === 'active_licenses')
if (card2 && data.growth_rate) {
const rate = data.growth_rate.licenses_mom
card2.subText = t('dashboard.stats.subText.vsLastMonth').replace('{rate}', `${rate >= 0 ? '+' : ''}${rate.toFixed(2)}`)
card2.subTextClass = rate >= 0 ? 'sub-positive' : 'sub-negative'
}
// Card 3: today new licenses + yesterday count as sub-text
const card3 = statsData.value.find(c => c.key === 'today_new_licenses')
if (card3) {
card3.subText = t('dashboard.stats.subText.yesterday').replace('{n}', String(data.yesterday_new_licenses))
card3.subTextClass = ''
}
// Card 4: expiring in 7 days — fixed urgent sub-text
const card4 = statsData.value.find(c => c.key === 'expiring_in_7days')
if (card4) {
card4.subText = data.expiring_in_7days > 0 ? t('dashboard.stats.subText.urgent') : ''
card4.subTextClass = data.expiring_in_7days > 0 ? 'sub-warning' : ''
}
// Card 6: abnormal alerts — heartbeat timeout hint
const card6 = statsData.value.find(c => c.key === 'abnormal_alerts')
if (card6) {
card6.subText = t('dashboard.stats.subText.heartbeatTimeout')
card6.subTextClass = data.abnormal_alerts > 0 ? 'sub-warning' : ''
}
}
// Fetch dashboard overview stats from API
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) {
const rateValue = data.growth_rate[item.key]
// 如果是数字类型,格式化为保留两位小数的百分比
if (typeof rateValue === 'number') {
item.value = `${rateValue.toFixed(2)}%`
} else {
item.value = rateValue
}
}
})
} catch (e) {
ElMessage.error('获取仪表盘统计数据失败')
applyStatsData(resp.data)
} catch {
ElMessage.error(t('dashboard.fetchError') || 'Failed to load dashboard stats')
}
}
@@ -353,6 +381,25 @@ onMounted(() => {
font-size: 1.25vw; /* 24px/1920 = 0.73vw */
color: rgba(255, 255, 255, 0.9);
}
.stat-sub-text {
font-size: 0.63vw; /* ~12px */
margin-top: 0.16vw;
color: rgba(255, 255, 255, 0.65);
white-space: nowrap;
&.sub-positive {
color: rgba(200, 255, 220, 0.9);
}
&.sub-negative {
color: rgba(255, 200, 200, 0.9);
}
&.sub-warning {
color: rgba(255, 230, 150, 0.9);
}
}
}
// 内容区域 - 默认上下布局,充满屏幕宽度和高度
+30 -18
View File
@@ -35,18 +35,21 @@
<p class="hero-subtitle">{{ t('login.heroSubtitle') }}</p>
<div class="hero-stats">
<div class="stat-item">
<span class="stat-label">{{ t('login.stats.totalAuthCodes') }}</span>
<span class="stat-number">10K+</span>
<span class="stat-sublabel">{{ t('login.stats.totalAuthCodesSubLabel') }}</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-label">{{ t('login.stats.activeLicenses') }}</span>
<span class="stat-number">8,234</span>
<span class="stat-sublabel">{{ t('login.stats.activeLicensesSubLabel') }}</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-label">{{ t('login.stats.systemUptime') }}</span>
<span class="stat-number">99.9%</span>
<span class="stat-label">{{ t('login.stats.uptime') }}</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-number">24/7</span>
<span class="stat-label">{{ t('login.stats.support') }}</span>
<span class="stat-sublabel">{{ t('login.stats.systemUptimeSubLabel') }}</span>
</div>
</div>
</div>
@@ -657,26 +660,35 @@ $phi-inv: 38.2%;
.stat-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.stat-number {
font-size: 24px;
font-weight: 700;
color: #ffffff;
align-items: flex-start;
gap: 2px;
}
.stat-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(255, 255, 255, 0.7);
font-weight: 400;
margin-bottom: 2px;
}
.stat-number {
font-size: 28px;
font-weight: 600;
color: #ffffff;
line-height: 1.2;
margin-bottom: 2px;
}
.stat-sublabel {
font-size: 12px;
color: rgba(255, 255, 255, 0.75);
font-weight: 400;
}
.stat-divider {
width: 1px;
height: 40px;
background: rgba(255, 255, 255, 0.2);
height: 56px;
background: rgba(255, 255, 255, 0.15);
}
/* 右侧登录区域 - 38.2% */