feat: enhance dashboard stats API and UI with new metrics and improved localization
This commit is contained in:
1 parent
8410a2cac4
commit
bc5d654a59
8 files changed
+248
-101
No files matched your search
@@ -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 设备列表查询请求结构(客户用户接口)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "最近のライセンス",
|
||||
|
||||
@@ -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": "最近授权",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 内容区域 - 默认上下布局,充满屏幕宽度和高度
|
||||
|
||||
@@ -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% */
|
||||
|
||||
Reference in new issue
Block a user