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

+20 -9
View File
@@ -169,19 +169,30 @@ type HeartbeatResponse struct {
HeartbeatInterval int `json:"heartbeat_interval"` // 下次心跳间隔(秒)
}
// StatsOverviewResponse 授权概览统计响应结构
// StatsOverviewResponse stats overview API response
type StatsOverviewResponse struct {
TotalAuthCodes int64 `json:"total_auth_codes"` // 总授权码数量(所有授权码的累计总数)
ActiveLicenses int64 `json:"active_licenses"` // 活跃许可证数量(状态为active的许可证总数)
ExpiringSoon int64 `json:"expiring_soon"` // 即将过期数量(30天内即将过期的授权码,且未被锁定)
AbnormalAlerts int64 `json:"abnormal_alerts"` // 异常告警数量(活跃许可证中心跳超时的数量,超过配置的心跳超时时间未收到心跳)
GrowthRate GrowthRate `json:"growth_rate"` // 增长率(同比上月)
// Stock metrics
TotalAuthCodes int64 `json:"total_auth_codes"` // total authorization codes ever created
ActiveLicenses int64 `json:"active_licenses"` // licenses with status=active
// Flow metrics
TodayNewLicenses int64 `json:"today_new_licenses"` // licenses created today
YesterdayNewLicenses int64 `json:"yesterday_new_licenses"` // licenses created yesterday (for comparison)
MonthNewAuthCodes int64 `json:"month_new_auth_codes"` // auth codes created this calendar month
// Risk metrics
ExpiringIn7Days int64 `json:"expiring_in_7days"` // auth codes expiring within 7 days, not locked
ExpiringIn30Days int64 `json:"expiring_in_30days"` // auth codes expiring within 30 days, not locked
AbnormalAlerts int64 `json:"abnormal_alerts"` // active licenses with heartbeat timeout
// Growth rates shown as sub-text, not standalone cards
GrowthRate GrowthRate `json:"growth_rate"`
}
// GrowthRate 增长率结构(同比上月:当前值相比一个月前同一时刻值的增长率)
// GrowthRate month-over-month growth rates (shown as sub-text on dashboard cards)
type GrowthRate struct {
AuthCodes float64 `json:"auth_codes"` // 授权码增长率(%)(当前总授权码数相比一个月前总授权码数的增长率)
Licenses float64 `json:"licenses"` // 许可证增长率(%)(当前活跃许可证数相比一个月前活跃许可证数的增长率)
AuthCodesMoM float64 `json:"auth_codes_mom"` // auth codes MoM growth rate (%)
LicensesMoM float64 `json:"licenses_mom"` // active licenses MoM growth rate (%)
}
// DeviceListRequest 设备列表查询请求结构(客户用户接口)
+45 -22
View File
@@ -716,76 +716,99 @@ func parseJSONField(raw models.JSON) map[string]interface{} {
return result
}
// GetStatsOverview 获取授权概览统计
// GetStatsOverview returns dashboard overview statistics
func (s *licenseService) GetStatsOverview(ctx context.Context) (*models.StatsOverviewResponse, error) {
lang := pkgcontext.GetLanguageFromContext(ctx)
// 获取配置信息
// Load heartbeat timeout config
cfg := config.GetConfig()
offlineTimeout := time.Duration(cfg.License.HeartbeatTimeout) * time.Second
// 计算时间点
now := time.Now()
onlineThreshold := now.Add(-offlineTimeout)
// Time boundaries
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
yesterdayStart := todayStart.AddDate(0, 0, -1)
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
expire7d := now.AddDate(0, 0, 7)
expire30d := now.AddDate(0, 0, 30)
lastMonth := now.AddDate(0, -1, 0)
var stats models.StatsOverviewResponse
// 执行统计查询
err := s.db.Transaction(func(tx *gorm.DB) error {
// 1. 总授权码数量
// 1. Total auth codes (stock)
if err := tx.Model(&models.AuthorizationCode{}).Count(&stats.TotalAuthCodes).Error; err != nil {
return err
}
// 2. 活跃许可证数量(状态为active)
// 2. Active licenses (stock)
if err := tx.Model(&models.License{}).Where("status = ?", "active").Count(&stats.ActiveLicenses).Error; err != nil {
return err
}
// 3. 即将过期数量(授权码在30天内过期,且状态为active)
expireThreshold := now.AddDate(0, 0, 30)
if err := tx.Model(&models.AuthorizationCode{}).
Where("end_date <= ? AND end_date > ? AND is_locked = false", expireThreshold, now).
Count(&stats.ExpiringSoon).Error; err != nil {
// 3. Licenses created today (flow)
if err := tx.Model(&models.License{}).Where("created_at >= ?", todayStart).Count(&stats.TodayNewLicenses).Error; err != nil {
return err
}
// 4. 异常告警数量(许可证超时未心跳)
// 4. Licenses created yesterday (flow, for comparison)
if err := tx.Model(&models.License{}).Where("created_at >= ? AND created_at < ?", yesterdayStart, todayStart).Count(&stats.YesterdayNewLicenses).Error; err != nil {
return err
}
// 5. Auth codes created this calendar month (flow)
if err := tx.Model(&models.AuthorizationCode{}).Where("created_at >= ?", monthStart).Count(&stats.MonthNewAuthCodes).Error; err != nil {
return err
}
// 6. Expiring within 7 days (risk, not locked)
if err := tx.Model(&models.AuthorizationCode{}).
Where("end_date <= ? AND end_date > ? AND is_locked = false", expire7d, now).
Count(&stats.ExpiringIn7Days).Error; err != nil {
return err
}
// 7. Expiring within 30 days (risk, not locked)
if err := tx.Model(&models.AuthorizationCode{}).
Where("end_date <= ? AND end_date > ? AND is_locked = false", expire30d, now).
Count(&stats.ExpiringIn30Days).Error; err != nil {
return err
}
// 8. Abnormal alerts: active licenses with heartbeat timeout
if err := tx.Model(&models.License{}).
Where("status = 'active' AND (last_heartbeat < ? OR last_heartbeat IS NULL)", onlineThreshold).
Count(&stats.AbnormalAlerts).Error; err != nil {
return err
}
// 5. 计算同比上月的授权码和许可证数量(用于计算增长率)
// 9. MoM growth rates (sub-text only, not standalone cards)
var lastMonthAuthCodes, lastMonthActiveLicenses int64
// 上月同期授权码总数(一个月前同一时刻的累计总数)
if err := tx.Model(&models.AuthorizationCode{}).
Where("created_at <= ?", lastMonth).
Count(&lastMonthAuthCodes).Error; err != nil {
return err
}
// 上月同期活跃许可证总数(一个月前同一时刻的状态为active的许可证数)
if err := tx.Model(&models.License{}).
Where("status = ? AND created_at <= ?", "active", lastMonth).
Count(&lastMonthActiveLicenses).Error; err != nil {
return err
}
// 计算增长率(同比上月:当前总数相比一个月前总数的增长率)
if lastMonthAuthCodes > 0 {
stats.GrowthRate.AuthCodes = float64(stats.TotalAuthCodes-lastMonthAuthCodes) / float64(lastMonthAuthCodes) * 100
} else {
stats.GrowthRate.AuthCodes = 0
stats.GrowthRate.AuthCodesMoM = float64(stats.TotalAuthCodes-lastMonthAuthCodes) / float64(lastMonthAuthCodes) * 100
} else if stats.TotalAuthCodes > 0 {
stats.GrowthRate.AuthCodesMoM = 100.0
}
if lastMonthActiveLicenses > 0 {
stats.GrowthRate.Licenses = float64(stats.ActiveLicenses-lastMonthActiveLicenses) / float64(lastMonthActiveLicenses) * 100
} else {
stats.GrowthRate.Licenses = 0
stats.GrowthRate.LicensesMoM = float64(stats.ActiveLicenses-lastMonthActiveLicenses) / float64(lastMonthActiveLicenses) * 100
} else if stats.ActiveLicenses > 0 {
stats.GrowthRate.LicensesMoM = 100.0
}
return nil
+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% */