Merge branch 'main' of https://github.com/cedar-v/license-manager
This commit is contained in:
commit
42a5803c06
20 files changed
+2029
-795
No files matched your search
@@ -0,0 +1,228 @@
|
||||
# 技术契约与架构设计书
|
||||
## 功能:授权趋势图表自适应溢出修复
|
||||
|
||||
**版本**:v1.0
|
||||
**日期**:2026-03-13
|
||||
**设计者**:System Architect
|
||||
**状态**:待实现
|
||||
|
||||
---
|
||||
|
||||
## 一、问题定位(Root Cause Analysis)
|
||||
|
||||
通过审计截图与源码,确认存在 **三条独立的溢出根因**,相互叠加导致图表在不同视口宽度下随机超出卡片边界。
|
||||
|
||||
### 根因 A — CSS 高度链断裂(最高优先级)
|
||||
|
||||
**文件**:`frontend/src/views/Dashboard.vue`
|
||||
|
||||
```
|
||||
.content-container { min-height: calc(100vh - 80px); } ← ①
|
||||
└─ .dashboard { height: 100%; } ← ② ❌ 100% of min-height = 0!
|
||||
└─ .content-section { flex: 1; min-height: 0; } ← ③
|
||||
└─ .chart-section { flex: 0.5; min-height: 300px; }
|
||||
└─ .license-trend-chart { height: 100%; } ← ④ 同样失效
|
||||
```
|
||||
|
||||
CSS 规范:`height: 100%` 只解析相对于父元素的 **`height`** 属性,而非 `min-height`。步骤②处 `.dashboard` 获取的 `height: 100%` 实际解析为 `auto`,导致整条 flex 高度链全部退化为 `auto`/内容高度,图表实际渲染高度由内容撑开,触发溢出。
|
||||
|
||||
**现象**:桌面端图表卡片高度不稳定,ECharts canvas 随视口/数据量变化而溢出卡片底部。
|
||||
|
||||
### 根因 B — ECharts Grid 使用绝对像素 + `containLabel: false`
|
||||
|
||||
**文件**:`frontend/src/components/charts/LicenseTrendChart.vue`
|
||||
|
||||
```js
|
||||
grid: {
|
||||
left: 60, // 固定像素,容器小时 Y 轴标签溢出左侧
|
||||
right: 50, // 固定像素,容器小时溢出右侧
|
||||
top: 30,
|
||||
bottom: 60,
|
||||
containLabel: false // ❌ 不包含坐标轴标签在 grid 范围内
|
||||
}
|
||||
```
|
||||
|
||||
当容器宽度较小时,ECharts 按绝对像素保留边距并绘制轴标签于 grid 外部,标签实际渲染于 canvas 边缘外,出现截断/溢出。
|
||||
|
||||
**现象**:窄屏或侧边栏展开时,X 轴日期标签被裁切或与右侧内容重叠。
|
||||
|
||||
### 根因 C — 父容器 `overflow: hidden` 裁切 ECharts tooltip
|
||||
|
||||
**文件**:`frontend/src/components/charts/LicenseTrendChart.vue`
|
||||
|
||||
```scss
|
||||
.license-trend-chart {
|
||||
overflow: hidden; // ❌ 裁切了 ECharts tooltip 的绝对定位弹层
|
||||
}
|
||||
```
|
||||
|
||||
ECharts 的 tooltip 默认渲染为 `position: absolute` 的 `div` 元素,若其最近的 `overflow: hidden` 祖先正好是卡片容器,tooltip 悬浮层会被硬裁。移动端尤其明显(截图二:tooltip "授权数量: 8" 被截断于卡片底部)。
|
||||
|
||||
---
|
||||
|
||||
## 二、数据模型层(Schema)
|
||||
|
||||
本次修复为纯前端 CSS/布局层变更,**不涉及任何数据模型、API 接口或后端逻辑修改**。API 契约保持不变。
|
||||
|
||||
---
|
||||
|
||||
## 三、后端服务层(API)
|
||||
|
||||
**无变更**。后端 `GET /api/v1/dashboard/authorization-trend` 接口及响应格式 `AuthorizationTrendResponse` 不受影响。
|
||||
|
||||
---
|
||||
|
||||
## 四、前端交互层(View)变更契约
|
||||
|
||||
### 4.1 变更文件清单
|
||||
|
||||
| 操作 | 文件路径 | 变更范围 |
|
||||
|------|----------|----------|
|
||||
| 修改 | `frontend/src/views/Dashboard.vue` | `.content-container` 的高度策略 |
|
||||
| 修改 | `frontend/src/components/charts/LicenseTrendChart.vue` | Grid 配置 + overflow + 高度策略 |
|
||||
|
||||
---
|
||||
|
||||
### 4.2 `frontend/src/views/Dashboard.vue` — 修改意图
|
||||
|
||||
**目标**:修复高度链断裂,使 flex 容器的高度能正确传递给子组件。
|
||||
|
||||
#### 改动 1:`.content-container` 改用 `height` 替代 `min-height`
|
||||
|
||||
```scss
|
||||
// ❌ 修改前
|
||||
.content-container {
|
||||
min-height: calc(100vh - 80px);
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✅ 修改后
|
||||
.content-container {
|
||||
height: calc(100vh - 80px); // 使用确定的 height,让 flex 子项 height: 100% 可以解析
|
||||
min-height: 600px; // 保留最小高度防止内容压扁,但用 min-height 做限底
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
> **风险评估**:`Dashboard.vue` 的 `height` 被 `Layout` 组件 `overflow: auto` 的滚动容器包裹,切换为固定 `height` 不会影响滚动行为,但需验证 Layout 组件是否已设置 `overflow-y: auto`。
|
||||
|
||||
---
|
||||
|
||||
### 4.3 `frontend/src/components/charts/LicenseTrendChart.vue` — 修改意图
|
||||
|
||||
**目标 A**:修复 ECharts Grid overflow(根因 B)。
|
||||
|
||||
#### 改动 1:`chartOption.grid` 启用 `containLabel: true` 并将边距改为百分比
|
||||
|
||||
```js
|
||||
// ❌ 修改前
|
||||
grid: {
|
||||
left: 60,
|
||||
right: 50,
|
||||
top: 30,
|
||||
bottom: 60,
|
||||
containLabel: false
|
||||
}
|
||||
|
||||
// ✅ 修改后
|
||||
grid: {
|
||||
left: '3%', // 改为百分比,随容器缩放
|
||||
right: '3%',
|
||||
top: 30,
|
||||
bottom: 30, // containLabel: true 后 ECharts 自动为标签留空间,无需预留 60px
|
||||
containLabel: true // 轴标签始终包含在 grid 区域内,不会超出 canvas
|
||||
}
|
||||
```
|
||||
|
||||
**目标 B**:解决 tooltip 被 `overflow: hidden` 裁切(根因 C)。
|
||||
|
||||
#### 改动 2:`v-chart` 的 tooltip 改为挂载到 body
|
||||
|
||||
在 `chartOption.tooltip` 中追加:
|
||||
|
||||
```js
|
||||
tooltip: {
|
||||
// ...现有配置不变...
|
||||
appendToBody: true, // tooltip DOM 挂载到 <body>,完全逃离 overflow: hidden 容器
|
||||
}
|
||||
```
|
||||
|
||||
> 此方案比改 `overflow: visible` 更稳健,不破坏卡片的 `border-radius` 裁切效果。`overflow: hidden` 保留,仅 tooltip 逃逸。
|
||||
|
||||
**目标 C**:稳定图表 canvas 在 flex 链中的高度(根因 A 的组件侧保障)。
|
||||
|
||||
#### 改动 3:`.chart-container` 添加 `position: relative` + `.trend-chart` 使用位置绝对铺满
|
||||
|
||||
为防止在高度链无法修复的回退场景下图表仍能铺满容器:
|
||||
|
||||
```scss
|
||||
// ✅ 修改后
|
||||
.chart-container {
|
||||
padding: 1.35vw 1.25vw 1.25vw;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
position: relative; // 新增:建立定位上下文
|
||||
|
||||
.trend-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 12.8vw; // 保持现有移动端 fallback
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端覆盖(已有,维持原样)
|
||||
@media (max-width: 1024px) {
|
||||
.chart-container .trend-chart {
|
||||
height: 200px;
|
||||
min-height: unset;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、全栈影响面分析(Full-Stack Impact Analysis)
|
||||
|
||||
| 层级 | 受影响组件 | 影响类型 | 处理方式 |
|
||||
|------|-----------|----------|----------|
|
||||
| 数据层 | 无 | — | 无需处理 |
|
||||
| API 层 | 无 | — | 无需处理 |
|
||||
| 状态层 | 无 | — | 无需处理 |
|
||||
| 视图组件 | `Dashboard.vue` | 高度策略调整 | 必须修改 |
|
||||
| 子组件 | `LicenseTrendChart.vue` | Grid 配置 + tooltip + overflow | 必须修改 |
|
||||
| 其他页面 | 其他使用 `Layout` 组件的页面 | 可能受 `.content-container` 修改影响 | 修改前审查;若 `.content-container` 属全局 class 则需回归测试 |
|
||||
|
||||
> ⚠️ **注意**:如果 `.content-container` 是全局共用 class(定义在全局 CSS 而非 scoped),则修改会影响所有使用该 class 的页面。需先确认作用域:当前 Dashboard.vue 的 `<style lang="scss" scoped>` 限定了 `.content-container` 只作用于该页面,因此是安全的局部修改。
|
||||
|
||||
---
|
||||
|
||||
## 六、待验证事项(Acceptance Criteria)
|
||||
|
||||
- [ ] 1920×1080 桌面端:图表无溢出,X 轴日期标签完整显示
|
||||
- [ ] 1440×900:图表无溢出,轴标签不被截断
|
||||
- [ ] 1280×800:图表卡片高度稳定,边距正常
|
||||
- [ ] 768px 平板:`.chart-section min-height: 300px` 生效,图表可读
|
||||
- [ ] 375px 手机:固定 200px 高度正常,tooltip 不被卡片边界截断
|
||||
- [ ] 暗模式:tooltip 背景色与轴标签颜色不变
|
||||
- [ ] 快捷选择切换 7 天 / 30 天:图表 resize 正常,无残影溢出
|
||||
|
||||
---
|
||||
|
||||
## 七、文件清单汇总
|
||||
|
||||
### 必须修改
|
||||
1. `frontend/src/views/Dashboard.vue`
|
||||
- SCSS `.content-container`:`min-height` → `height` + 添加 `min-height` 保底
|
||||
|
||||
2. `frontend/src/components/charts/LicenseTrendChart.vue`
|
||||
- JS `chartOption.grid`:`containLabel: false` + 固定 px → `containLabel: true` + `%` 边距
|
||||
- JS `chartOption.tooltip`:新增 `appendToBody: true`
|
||||
- SCSS `.chart-container`:新增 `position: relative`
|
||||
|
||||
### 无需新建
|
||||
本次修复不需要创建任何新文件。
|
||||
|
||||
---
|
||||
|
||||
*本文档由 System Architect 技能生成,作为 task-coder 的实现输入。实现时严格遵循上述契约,禁止超出本文档定义的修改范围。*
|
||||
@@ -25,8 +25,13 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
# 设置pnpm
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
# 缓存node_modules(不缓存构建输出,确保每次都是最新构建)
|
||||
- name: Cache dependencies
|
||||
@@ -34,7 +39,8 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
frontend/node_modules
|
||||
key: ${{ runner.os }}-frontend-deps-${{ hashFiles('frontend/package-lock.json') }}
|
||||
~/.pnpm-store
|
||||
key: ${{ runner.os }}-frontend-deps-${{ hashFiles('frontend/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-frontend-deps-
|
||||
|
||||
@@ -42,7 +48,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd frontend
|
||||
npm ci
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
# 清理旧的构建目录(确保使用最新代码构建)
|
||||
- name: Clean old build artifacts
|
||||
@@ -59,9 +65,9 @@ jobs:
|
||||
export VITE_OUTDIR=production
|
||||
export VITE_ADDRESS_BASE_URL=/production/
|
||||
# 先执行类型检查
|
||||
npm run type-check
|
||||
pnpm run type-check
|
||||
# 然后执行构建
|
||||
npm run build:prod
|
||||
pnpm run build:prod
|
||||
# 检查构建结果
|
||||
echo "Build completed, checking production directory:"
|
||||
ls -la production/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
# 技术契约与架构设计书:Dashboard 统计卡片重设计
|
||||
|
||||
**版本**:v1.0
|
||||
**日期**:2026-03-13
|
||||
**作者**:system-architect
|
||||
**状态**:待评审
|
||||
|
||||
---
|
||||
|
||||
## 一、现状诊断
|
||||
|
||||
### 当前 API 响应结构
|
||||
|
||||
```json
|
||||
{
|
||||
"total_auth_codes": 12,
|
||||
"active_licenses": 2,
|
||||
"expiring_soon": 0,
|
||||
"abnormal_alerts": 2,
|
||||
"growth_rate": {
|
||||
"auth_codes": 100.00,
|
||||
"licenses": 0.00
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 现存问题清单
|
||||
|
||||
| # | 问题 | 严重性 |
|
||||
|---|------|--------|
|
||||
| 1 | `growth_rate` 语义不清——是"同比"上月还是"环比"?无时间标注 | 高 |
|
||||
| 2 | 两个增长率字段(auth_codes / licenses)对用户的业务价值极低,不知道要对比什么 | 高 |
|
||||
| 3 | 前端 `statsData` 中的 `key` 映射与 i18n label 不一致(注释与实际label错位) | 中 |
|
||||
| 4 | `expiring_soon` 统计的是"30天内到期的授权码",但卡片标签写的是"即将过期",口径模糊 | 中 |
|
||||
| 5 | 没有"今日新增"这类高频关注的运营指标 | 中 |
|
||||
| 6 | 增长率算法缺陷:上月总数为0时直接返回0而非100% | 低 |
|
||||
|
||||
---
|
||||
|
||||
## 二、竞品分析(License Management SaaS 参考)
|
||||
|
||||
### Keygen.sh / LicenseSpring / Cryptlex 共性 Dashboard 指标
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 高频关注(运营视角) │
|
||||
│ • 今日新增授权码 / 本月新增授权码(绝对数 + 环比昨日/上月)│
|
||||
│ • 活跃设备数(在线 License) │
|
||||
│ • 即将到期(7天 / 30天)→ 需要跟进续费 │
|
||||
│ • 异常告警(心跳超时 / 被撤销) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 中频关注(管理视角) │
|
||||
│ • 授权码总量(存量指标) │
|
||||
│ • 活跃许可证总量(存量指标) │
|
||||
│ • 月度增长率(MoM Growth Rate,附说明文字"vs 上月") │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 关键竞品设计原则
|
||||
|
||||
1. **增长率必须标注对比周期**:统一用「vs. 上月同期」,数字旁展示箭头 ↑ ↓ 和颜色区分正负
|
||||
2. **增长率不放卡片主数值**:作为副文本展示(小字 + 颜色),主数值展示绝对数
|
||||
3. **即将到期细分**:7天 为紧急(红),30天 为预警(橙)
|
||||
4. **今日新增**:运营最关注的高频指标,竞品均在首位展示
|
||||
|
||||
---
|
||||
|
||||
## 三、重设计方案
|
||||
|
||||
### 3.1 新指标体系(6张卡片)
|
||||
|
||||
| 位置 | 指标名 | 计算口径 | 副文本 |
|
||||
|------|--------|---------|--------|
|
||||
| 卡片1 | 授权码总量 | `COUNT(authorization_codes)` | ↑ 本月新增 N 个 |
|
||||
| 卡片2 | 活跃许可证 | `COUNT(licenses WHERE status='active')` | ↑ 较上月 +X% |
|
||||
| 卡片3 | 今日新增授权 | `COUNT(licenses WHERE DATE(created_at)=TODAY)` | 昨日 N 个 |
|
||||
| 卡片4 | 7天内到期 | `COUNT(auth_codes WHERE end_date BETWEEN now AND now+7d AND NOT locked)` | 紧急跟进 |
|
||||
| 卡片5 | 30天内到期 | `COUNT(auth_codes WHERE end_date BETWEEN now AND now+30d AND NOT locked)` | 含7天内 |
|
||||
| 卡片6 | 异常告警 | `COUNT(licenses WHERE status='active' AND heartbeat_timeout)` | 心跳超时 |
|
||||
|
||||
> 删除:`auth_codes 增长率`、`licenses 增长率` 作为独立卡片(改为副文本方式展示)
|
||||
|
||||
### 3.2 增长率展示方式(副文本,不占卡片)
|
||||
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ 🔑 授权码总量 │
|
||||
│ 12 │
|
||||
│ ↑ 本月新增 2 个 │ ← 副文本:绝对增量
|
||||
└──────────────────────────┘
|
||||
|
||||
┌──────────────────────────┐
|
||||
│ ✅ 活跃许可证 │
|
||||
│ 98 │
|
||||
│ ↑ +12.5% vs. 上月 │ ← 副文本:百分比增长率 + 标注
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.3 新 API Response Schema
|
||||
|
||||
```go
|
||||
// StatsOverviewResponse 重设计
|
||||
type StatsOverviewResponse struct {
|
||||
// 存量指标
|
||||
TotalAuthCodes int64 `json:"total_auth_codes"` // 授权码总量
|
||||
ActiveLicenses int64 `json:"active_licenses"` // 活跃许可证
|
||||
|
||||
// 流量指标
|
||||
TodayNewLicenses int64 `json:"today_new_licenses"` // 今日新增许可证
|
||||
YesterdayNewLicenses int64 `json:"yesterday_new_licenses"` // 昨日新增(用于对比)
|
||||
MonthNewAuthCodes int64 `json:"month_new_auth_codes"` // 本月新增授权码
|
||||
|
||||
// 风险指标
|
||||
ExpiringIn7Days int64 `json:"expiring_in_7days"` // 7天内到期
|
||||
ExpiringIn30Days int64 `json:"expiring_in_30days"` // 30天内到期
|
||||
AbnormalAlerts int64 `json:"abnormal_alerts"` // 异常告警
|
||||
|
||||
// 增长率(作为副文本,不占独立卡片)
|
||||
GrowthRate struct {
|
||||
AuthCodesMoM float64 `json:"auth_codes_mom"` // 授权码月环比
|
||||
LicensesMoM float64 `json:"licenses_mom"` // 许可证月环比
|
||||
} `json:"growth_rate"`
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、影响面分析(全栈)
|
||||
|
||||
### 后端改动文件
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `backend/internal/models/license.go` | 修改 | 更新 `StatsOverviewResponse` struct |
|
||||
| `backend/internal/service/license_service.go` | 修改 | 重写 `GetStatsOverview()` 实现,增加今日/昨日新增查询 |
|
||||
| `backend/internal/api/handlers/license_handler.go` | 无需修改 | Handler 层直接透传 |
|
||||
|
||||
### 前端改动文件
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `frontend/src/views/Dashboard.vue` | 修改 | 重新定义 `statsData` 6张卡片,增加副文本字段渲染 |
|
||||
| `frontend/src/i18n/locales/zh.json` | 修改 | 更新 `dashboard.stats.*` key |
|
||||
| `frontend/src/i18n/locales/en.json` | 修改 | 同上 |
|
||||
| `frontend/src/i18n/locales/ja.json` | 修改 | 同上 |
|
||||
| `frontend/src/api/dashboard.ts` | 修改 | 更新 `getOverviewStats` 返回类型定义 |
|
||||
|
||||
---
|
||||
|
||||
## 五、实施优先级建议
|
||||
|
||||
**P0(立即)**:
|
||||
- 重命名"即将过期"为"30天内到期",语义清晰化
|
||||
- 增长率卡片改为副文本方式(不移除数据,只改展示层)
|
||||
- 增长率数字旁加"vs. 上月"标注
|
||||
|
||||
**P1(本迭代)**:
|
||||
- 新增"今日新增"卡片(需要后端加一个 COUNT 查询)
|
||||
- "即将过期"拆分7天/30天两档
|
||||
|
||||
**P2(下迭代)**:
|
||||
- 卡片支持点击跳转到过滤后的列表页
|
||||
@@ -0,0 +1,133 @@
|
||||
# 技术契约与架构设计书:前端加载性能优化
|
||||
|
||||
**版本**:v1.0
|
||||
**日期**:2026-03-11
|
||||
**作者**:system-architect
|
||||
**状态**:待 task-coder 实施
|
||||
|
||||
---
|
||||
|
||||
## 一、问题诊断(根因分析)
|
||||
|
||||
### 1.1 每次页面跳转都重新加载 Element Plus CSS
|
||||
|
||||
**根因**:`vite.config.ts` 中 `ElementPlusResolver({ importStyle: 'css' })` 配置让 `unplugin-vue-components` 在每个使用到 Element Plus 组件的 `.vue` 文件中注入独立的 CSS import(`el-dropdown-menu.css`、`el-button-group.css` 等)。与此同时 `main.ts` 还保留了 `import 'element-plus/dist/index.css'`(全量 CSS),两者并存形成**双重加载**。
|
||||
|
||||
**表现**:网络面板中看到 `el-dropdown-menu.css`、`el-button-group.css`、`element_plus_es_components_loading_s...` 等碎片 CSS 在每次路由切换时被重新请求。
|
||||
|
||||
**正确做法**:二选一——
|
||||
- **方案 A(推荐)**:继续使用 `element-plus/dist/index.css` 全量 CSS(已在 main.ts 中),关闭 resolver 的 `importStyle`。全量 CSS 首次加载后长期缓存,零碎的 per-component CSS 完全消失。
|
||||
- 方案 B:彻底删除 `element-plus/dist/index.css`,完全依赖 resolver 按需注入。需要确保每个组件都被 resolver 覆盖,否则样式丢失。
|
||||
|
||||
### 1.2 字体每次导航重复加载(视觉上的误判)
|
||||
|
||||
**根因**:浏览器缓存策略问题。`@fontsource/noto-sans-sc` 输出的 woff2 文件名**不含内容 hash**(如 `noto-sans-sc-116-400-normal.woff2`),而 Vite dev 服务器对字体文件**不设置强缓存 header**(`Cache-Control: no-cache`)。结果是浏览器每次导航都发起带 If-None-Match 的条件请求,即使返回 304,devtools 也会显示为"加载"。
|
||||
|
||||
**生产环境额外问题**:vite.config.ts 的 `assetFileNames` 对字体匹配正确会加 hash,但 `@fontsource` 的 CSS 中 `src` 路径是相对路径,构建时 Vite 自动处理后会有 hash,缓存正常。
|
||||
|
||||
### 1.3 PangMenZhengDao.ttf:1004 KB,仅 LicenseSearch.vue 使用
|
||||
|
||||
这是最严重的单点问题。一个装饰性标题字体(仅 `LicenseSearch.vue` 的 `.platform-title` 用到)以 **1 MB 的 TTF 格式**全局加载。
|
||||
|
||||
- 全局加载:`main.ts` → `fonts.css` → 预加载所有字体
|
||||
- 即使用户从未访问授权查询页,这 1 MB 也会被下载
|
||||
|
||||
### 1.4 Noto Sans SC:两个字重 = 每字重 ~15 个 woff2 分片
|
||||
|
||||
400 + 700 两个字重,每次页面含中文时浏览器匹配 unicode-range 触发下载,共约 20-30 个分片文件(每个 25-33 KB)。这是 CJK 字体的固有代价,无法消除,但可以:
|
||||
- 替换为系统字体(零网络开销)
|
||||
- 转为自托管 WOFF2 子集(仅保留常用汉字)
|
||||
|
||||
---
|
||||
|
||||
## 二、优化方案设计
|
||||
|
||||
### 方案一(必做):移除 ElementPlusResolver CSS 双重加载
|
||||
|
||||
**影响层**:构建配置层
|
||||
**改动意图**:将 `importStyle: 'css'` 改为 `importStyle: false`,消除每个组件的碎片 CSS 请求,改由全局 `element-plus/dist/index.css` 统一提供样式。
|
||||
|
||||
**数据收益**:消除约 10-20 个 CSS 请求(每次路由跳转)。
|
||||
|
||||
---
|
||||
|
||||
### 方案二(必做):PangMenZhengDao 懒加载隔离
|
||||
|
||||
**影响层**:CSS 层 + 字体加载策略层
|
||||
**改动意图**:
|
||||
1. 从 `fonts.css` 中删除 `@font-face` 全局声明
|
||||
2. 将 `@font-face` 声明移入 `LicenseSearch.vue` 的 `<style scoped>`,利用路由懒加载天然隔离,仅在用户访问该页时触发字体下载
|
||||
|
||||
**数据收益**:首屏及非授权查询页节省 1 MB 传输。
|
||||
|
||||
---
|
||||
|
||||
### 方案三(推荐):Noto Sans SC → 系统字体降级
|
||||
|
||||
**影响层**:CSS 变量层(variables.scss)
|
||||
**改动意图**:将 `$font-family-primary` 中的 `'Noto Sans SC'` 降级为第二选择,保留兜底。同时评估是否保留 `@fontsource` 导入(影响跨平台一致性)。
|
||||
|
||||
如果要保留字体一致性,最小化方案是:
|
||||
- **只导入 400**(删除 700 的导入),weight:700 的文字交给浏览器合成(faux-bold),视觉差异极小
|
||||
- 用 `font-display: optional` 替代 `swap` 以避免 FOUT
|
||||
|
||||
**数据收益**:减少 ~50% 的字体请求数(约 15 个)。
|
||||
|
||||
---
|
||||
|
||||
### 方案四(加分):开发服务器字体缓存优化
|
||||
|
||||
**影响层**:vite.config.ts server 配置
|
||||
**改动意图**:为 dev server 添加字体文件 `Cache-Control` 响应头,避免浏览器 devtools 中字体反复显示为"加载中"的视觉误导。
|
||||
|
||||
---
|
||||
|
||||
## 三、数据模型层(Schema)
|
||||
|
||||
无数据库变更。
|
||||
|
||||
---
|
||||
|
||||
## 四、后端服务层(API)
|
||||
|
||||
无需改动。
|
||||
|
||||
---
|
||||
|
||||
## 五、前端交互层(View)— 目标文件清单
|
||||
|
||||
| # | 文件路径 | 操作 | 改动说明 |
|
||||
|---|---------|------|---------|
|
||||
| 1 | `frontend/vite.config.ts` | **修改** | `ElementPlusResolver` 的 `importStyle: 'css'` → `false`;添加 dev server font cache header |
|
||||
| 2 | `frontend/src/assets/styles/fonts.css` | **修改** | 删除 `PangMenZhengDao` 的 `@font-face` 块 |
|
||||
| 3 | `frontend/src/views/Licenses/LicenseSearch.vue` | **修改** | 在 `<style scoped>` 顶部加入 `PangMenZhengDao` 的 `@font-face` 声明 |
|
||||
| 4 | `frontend/src/main.ts` | **修改** | 删除 `@fontsource/noto-sans-sc/700.css` 导入(可选,评估后决定)|
|
||||
|
||||
**共 4 个文件,不超过 task-coder 单批限制(5个)。**
|
||||
|
||||
---
|
||||
|
||||
## 六、实施优先级
|
||||
|
||||
```
|
||||
P0(必做,无副作用):
|
||||
- 文件1:ElementPlusResolver importStyle fix
|
||||
- 文件2 + 文件3:PangMenZhengDao 懒加载隔离
|
||||
|
||||
P1(可选,有视觉权衡):
|
||||
- 文件4:删除 700 字重导入
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、预期收益对比
|
||||
|
||||
| 指标 | 优化前 | 优化后(P0) |
|
||||
|------|-------|------------|
|
||||
| 路由跳转触发的 CSS 请求 | ~15 个碎片 CSS | 0 |
|
||||
| 非授权查询页的字体传输 | 1 MB (PangMenZhengDao) | 0 |
|
||||
| 首次访问总字体请求数 | ~35 个 | ~20 个 |
|
||||
|
||||
---
|
||||
|
||||
*→ 移交 `task-coder` 按上述文件清单实施。*
|
||||
@@ -0,0 +1,135 @@
|
||||
# 技术契约与架构设计书:登录页国际化补全
|
||||
|
||||
**版本**:v1.0
|
||||
**日期**:2026-03-13
|
||||
**作者**:system-architect
|
||||
**状态**:待 task-coder 实施
|
||||
|
||||
---
|
||||
|
||||
## 一、问题诊断
|
||||
|
||||
`src/views/Login.vue` 中存在 **8 处**硬编码英文字符串,未通过 `t()` 调用 i18n 系统,导致切换语言后这些文案不随语言切换。
|
||||
|
||||
### 硬编码清单(按 DOM 位置)
|
||||
|
||||
| 位置 | 当前硬编码值 | 目标 key |
|
||||
|------|-------------|---------|
|
||||
| `.hero-title` | `Secure License<br/>Management` | `login.heroTitleLine1` / `login.heroTitleLine2` |
|
||||
| `.hero-subtitle` | `Streamline your software licensing...` | `login.heroSubtitle` |
|
||||
| stat-label "Active Licenses" | `Active Licenses` | `login.stats.activeLicenses` |
|
||||
| stat-label "Uptime" | `Uptime` | `login.stats.uptime` |
|
||||
| stat-label "Support" | `Support` | `login.stats.support` |
|
||||
| `.title` (card) | `Welcome back` | `login.welcomeTitle` |
|
||||
| `.subtitle` (card) | `Sign in to your account to continue` | `login.welcomeSubtitle` |
|
||||
| `.login-footer p` | `© 2025 Cedar-V. All rights reserved.` | `login.copyright` |
|
||||
|
||||
> **保持原样(不需国际化)**:品牌名 `Cedar-V`、数字统计值 `10K+` / `99.9%` / `24/7`、语言选项标签(自语言名称自描述惯例)。
|
||||
|
||||
---
|
||||
|
||||
## 二、数据模型层(Schema)— i18n 键值设计
|
||||
|
||||
### 新增键(追加至三端 `login` 对象末尾,位于 `error` 块之后)
|
||||
|
||||
```jsonc
|
||||
// en.json
|
||||
"heroTitleLine1": "Secure License",
|
||||
"heroTitleLine2": "Management",
|
||||
"heroSubtitle": "Streamline your software licensing workflow with our enterprise-grade platform.",
|
||||
"stats": {
|
||||
"activeLicenses": "Active Licenses",
|
||||
"uptime": "Uptime",
|
||||
"support": "Support"
|
||||
},
|
||||
"welcomeTitle": "Welcome back",
|
||||
"welcomeSubtitle": "Sign in to your account to continue",
|
||||
"copyright": "© 2025 Cedar-V. All rights reserved."
|
||||
|
||||
// zh.json
|
||||
"heroTitleLine1": "安全许可证",
|
||||
"heroTitleLine2": "管理平台",
|
||||
"heroSubtitle": "使用我们的企业级平台,简化您的软件许可证管理工作流程。",
|
||||
"stats": {
|
||||
"activeLicenses": "活跃许可证",
|
||||
"uptime": "可用率",
|
||||
"support": "全天支持"
|
||||
},
|
||||
"welcomeTitle": "欢迎回来",
|
||||
"welcomeSubtitle": "登录您的账户以继续",
|
||||
"copyright": "© 2025 Cedar-V. 保留所有权利。"
|
||||
|
||||
// ja.json
|
||||
"heroTitleLine1": "セキュアな",
|
||||
"heroTitleLine2": "ライセンス管理",
|
||||
"heroSubtitle": "エンタープライズグレードのプラットフォームで、ソフトウェアライセンス管理を効率化。",
|
||||
"stats": {
|
||||
"activeLicenses": "アクティブライセンス",
|
||||
"uptime": "稼働率",
|
||||
"support": "サポート"
|
||||
},
|
||||
"welcomeTitle": "おかえりなさい",
|
||||
"welcomeSubtitle": "アカウントにサインインして続けてください",
|
||||
"copyright": "© 2025 Cedar-V. All rights reserved."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、前端交互层(View)— 修改意图
|
||||
|
||||
### 目标文件
|
||||
|
||||
| 文件 | 操作 |
|
||||
|------|------|
|
||||
| `src/i18n/locales/en.json` | 追加 8 个新键至 `login` 对象 |
|
||||
| `src/i18n/locales/zh.json` | 追加 8 个新键至 `login` 对象 |
|
||||
| `src/i18n/locales/ja.json` | 追加 8 个新键至 `login` 对象 |
|
||||
| `src/views/Login.vue` | 模板层 8 处硬编码 → `{{ t('login.*') }}` |
|
||||
|
||||
### Login.vue 模板变更对照
|
||||
|
||||
```html
|
||||
<!-- BEFORE -->
|
||||
<h1 class="hero-title">Secure License<br/>Management</h1>
|
||||
|
||||
<!-- AFTER -->
|
||||
<h1 class="hero-title">{{ t('login.heroTitleLine1') }}<br/>{{ t('login.heroTitleLine2') }}</h1>
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- BEFORE -->
|
||||
<p class="hero-subtitle">Streamline your software licensing workflow with our enterprise-grade platform.</p>
|
||||
|
||||
<!-- AFTER -->
|
||||
<p class="hero-subtitle">{{ t('login.heroSubtitle') }}</p>
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- BEFORE (3 stat labels) -->
|
||||
<span class="stat-label">Active Licenses</span>
|
||||
<span class="stat-label">Uptime</span>
|
||||
<span class="stat-label">Support</span>
|
||||
|
||||
<!-- AFTER -->
|
||||
<span class="stat-label">{{ t('login.stats.activeLicenses') }}</span>
|
||||
<span class="stat-label">{{ t('login.stats.uptime') }}</span>
|
||||
<span class="stat-label">{{ t('login.stats.support') }}</span>
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- BEFORE -->
|
||||
<h2 class="title">Welcome back</h2>
|
||||
<p class="subtitle">Sign in to your account to continue</p>
|
||||
|
||||
<!-- AFTER -->
|
||||
<h2 class="title">{{ t('login.welcomeTitle') }}</h2>
|
||||
<p class="subtitle">{{ t('login.welcomeSubtitle') }}</p>
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- BEFORE -->
|
||||
<p>© 2025 Cedar-V. All rights reserved.</p>
|
||||
|
||||
<!-- AFTER -->
|
||||
<p>{{ t('login.copyright') }}</p>
|
||||
```
|
||||
@@ -42,6 +42,8 @@
|
||||
"sass": "^1.89.2",
|
||||
"terser": "^5.43.1",
|
||||
"typescript": "^5.8.3",
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"unplugin-vue-components": "^31.0.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vitest": "^2.0.0",
|
||||
|
||||
Generated
+223
@@ -78,6 +78,12 @@ importers:
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.9.3
|
||||
unplugin-auto-import:
|
||||
specifier: ^21.0.0
|
||||
version: 21.0.0(@vueuse/core@10.11.1(vue@3.5.27(typescript@5.9.3)))
|
||||
unplugin-vue-components:
|
||||
specifier: ^31.0.0
|
||||
version: 31.0.0(vue@3.5.27(typescript@5.9.3))
|
||||
vite:
|
||||
specifier: ^5.0.0
|
||||
version: 5.4.21(@types/node@22.19.9)(sass@1.97.3)(terser@5.46.0)
|
||||
@@ -318,6 +324,9 @@ packages:
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
'@jridgewell/remapping@2.3.5':
|
||||
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@@ -816,6 +825,11 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
acorn@8.16.0:
|
||||
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
ajv@6.12.6:
|
||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||
|
||||
@@ -897,6 +911,10 @@ packages:
|
||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||
engines: {node: '>= 14.16.0'}
|
||||
|
||||
chokidar@5.0.0:
|
||||
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
|
||||
cliui@8.0.1:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -918,6 +936,12 @@ packages:
|
||||
concat-map@0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
||||
confbox@0.1.8:
|
||||
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
|
||||
|
||||
confbox@0.2.4:
|
||||
resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -1023,6 +1047,10 @@ packages:
|
||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
escape-string-regexp@5.0.0:
|
||||
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
eslint-config-prettier@9.1.2:
|
||||
resolution: {integrity: sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==}
|
||||
hasBin: true
|
||||
@@ -1105,6 +1133,9 @@ packages:
|
||||
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
exsolve@1.0.8:
|
||||
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
|
||||
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
@@ -1300,6 +1331,9 @@ packages:
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
js-tokens@9.0.1:
|
||||
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
|
||||
|
||||
js-yaml@4.1.1:
|
||||
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
|
||||
hasBin: true
|
||||
@@ -1323,6 +1357,10 @@ packages:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
local-pkg@1.1.2:
|
||||
resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
locate-path@6.0.0:
|
||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1379,6 +1417,9 @@ packages:
|
||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
mlly@1.8.1:
|
||||
resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
@@ -1402,6 +1443,9 @@ packages:
|
||||
nth-check@2.1.1:
|
||||
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||
|
||||
obug@2.1.1:
|
||||
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
|
||||
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
@@ -1447,6 +1491,9 @@ packages:
|
||||
pathe@1.1.2:
|
||||
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
pathval@2.0.1:
|
||||
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
|
||||
engines: {node: '>= 14.16'}
|
||||
@@ -1471,6 +1518,12 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
pkg-types@1.3.1:
|
||||
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
||||
|
||||
pkg-types@2.3.0:
|
||||
resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
|
||||
|
||||
postcss-selector-parser@6.1.2:
|
||||
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -1499,6 +1552,9 @@ packages:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
quansync@0.2.11:
|
||||
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
|
||||
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
@@ -1506,6 +1562,10 @@ packages:
|
||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||
engines: {node: '>= 14.18.0'}
|
||||
|
||||
readdirp@5.0.0:
|
||||
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
|
||||
require-directory@2.1.1:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1549,6 +1609,9 @@ packages:
|
||||
engines: {node: '>=14.0.0'}
|
||||
hasBin: true
|
||||
|
||||
scule@1.3.0:
|
||||
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
|
||||
|
||||
semver@7.7.4:
|
||||
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1602,6 +1665,9 @@ packages:
|
||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
strip-literal@3.1.0:
|
||||
resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
|
||||
|
||||
supports-color@7.2.0:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1672,13 +1738,50 @@ packages:
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
ufo@1.6.3:
|
||||
resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
|
||||
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
unimport@5.7.0:
|
||||
resolution: {integrity: sha512-njnL6sp8lEA8QQbZrt+52p/g4X0rw3bnGGmUcJnt1jeG8+iiqO779aGz0PirCtydAIVcuTBRlJ52F0u46z309Q==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
|
||||
universalify@2.0.1:
|
||||
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
|
||||
unplugin-auto-import@21.0.0:
|
||||
resolution: {integrity: sha512-vWuC8SwqJmxZFYwPojhOhOXDb5xFhNNcEVb9K/RFkyk/3VnfaOjzitWN7v+8DEKpMjSsY2AEGXNgt6I0yQrhRQ==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
peerDependencies:
|
||||
'@nuxt/kit': ^4.0.0
|
||||
'@vueuse/core': '*'
|
||||
peerDependenciesMeta:
|
||||
'@nuxt/kit':
|
||||
optional: true
|
||||
'@vueuse/core':
|
||||
optional: true
|
||||
|
||||
unplugin-utils@0.3.1:
|
||||
resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
unplugin-vue-components@31.0.0:
|
||||
resolution: {integrity: sha512-4ULwfTZTLuWJ7+S9P7TrcStYLsSRkk6vy2jt/WTfgUEUb0nW9//xxmrfhyHUEVpZ2UKRRwfRb8Yy15PDbVZf+Q==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
peerDependencies:
|
||||
'@nuxt/kit': ^3.2.2 || ^4.0.0
|
||||
vue: ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
'@nuxt/kit':
|
||||
optional: true
|
||||
|
||||
unplugin@2.3.11:
|
||||
resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
@@ -1824,6 +1927,9 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
webpack-virtual-modules@0.6.2:
|
||||
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
|
||||
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -2023,6 +2129,11 @@ snapshots:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
||||
'@jridgewell/remapping@2.3.5':
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/source-map@0.3.11':
|
||||
@@ -2554,6 +2665,8 @@ snapshots:
|
||||
|
||||
acorn@8.15.0: {}
|
||||
|
||||
acorn@8.16.0: {}
|
||||
|
||||
ajv@6.12.6:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
@@ -2634,6 +2747,10 @@ snapshots:
|
||||
dependencies:
|
||||
readdirp: 4.1.2
|
||||
|
||||
chokidar@5.0.0:
|
||||
dependencies:
|
||||
readdirp: 5.0.0
|
||||
|
||||
cliui@8.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
@@ -2654,6 +2771,10 @@ snapshots:
|
||||
|
||||
concat-map@0.0.1: {}
|
||||
|
||||
confbox@0.1.8: {}
|
||||
|
||||
confbox@0.2.4: {}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
@@ -2773,6 +2894,8 @@ snapshots:
|
||||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
|
||||
escape-string-regexp@5.0.0: {}
|
||||
|
||||
eslint-config-prettier@9.1.2(eslint@8.57.1):
|
||||
dependencies:
|
||||
eslint: 8.57.1
|
||||
@@ -2889,6 +3012,8 @@ snapshots:
|
||||
|
||||
expect-type@1.3.0: {}
|
||||
|
||||
exsolve@1.0.8: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-diff@1.3.0: {}
|
||||
@@ -3067,6 +3192,8 @@ snapshots:
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
js-tokens@9.0.1: {}
|
||||
|
||||
js-yaml@4.1.1:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
@@ -3092,6 +3219,12 @@ snapshots:
|
||||
prelude-ls: 1.2.1
|
||||
type-check: 0.4.0
|
||||
|
||||
local-pkg@1.1.2:
|
||||
dependencies:
|
||||
mlly: 1.8.1
|
||||
pkg-types: 2.3.0
|
||||
quansync: 0.2.11
|
||||
|
||||
locate-path@6.0.0:
|
||||
dependencies:
|
||||
p-locate: 5.0.0
|
||||
@@ -3139,6 +3272,13 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
mlly@1.8.1:
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
pathe: 2.0.3
|
||||
pkg-types: 1.3.1
|
||||
ufo: 1.6.3
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
muggle-string@0.4.1: {}
|
||||
@@ -3156,6 +3296,8 @@ snapshots:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
|
||||
obug@2.1.1: {}
|
||||
|
||||
once@1.4.0:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
@@ -3199,6 +3341,8 @@ snapshots:
|
||||
|
||||
pathe@1.1.2: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
pathval@2.0.1: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
@@ -3217,6 +3361,18 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
|
||||
pkg-types@1.3.1:
|
||||
dependencies:
|
||||
confbox: 0.1.8
|
||||
mlly: 1.8.1
|
||||
pathe: 2.0.3
|
||||
|
||||
pkg-types@2.3.0:
|
||||
dependencies:
|
||||
confbox: 0.2.4
|
||||
exsolve: 1.0.8
|
||||
pathe: 2.0.3
|
||||
|
||||
postcss-selector-parser@6.1.2:
|
||||
dependencies:
|
||||
cssesc: 3.0.0
|
||||
@@ -3240,10 +3396,14 @@ snapshots:
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
quansync@0.2.11: {}
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
readdirp@4.1.2: {}
|
||||
|
||||
readdirp@5.0.0: {}
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
@@ -3306,6 +3466,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@parcel/watcher': 2.5.6
|
||||
|
||||
scule@1.3.0: {}
|
||||
|
||||
semver@7.7.4: {}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
@@ -3345,6 +3507,10 @@ snapshots:
|
||||
|
||||
strip-json-comments@3.1.1: {}
|
||||
|
||||
strip-literal@3.1.0:
|
||||
dependencies:
|
||||
js-tokens: 9.0.1
|
||||
|
||||
supports-color@7.2.0:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
@@ -3399,10 +3565,65 @@ snapshots:
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
ufo@1.6.3: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
unimport@5.7.0:
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
escape-string-regexp: 5.0.0
|
||||
estree-walker: 3.0.3
|
||||
local-pkg: 1.1.2
|
||||
magic-string: 0.30.21
|
||||
mlly: 1.8.1
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.3
|
||||
pkg-types: 2.3.0
|
||||
scule: 1.3.0
|
||||
strip-literal: 3.1.0
|
||||
tinyglobby: 0.2.15
|
||||
unplugin: 2.3.11
|
||||
unplugin-utils: 0.3.1
|
||||
|
||||
universalify@2.0.1: {}
|
||||
|
||||
unplugin-auto-import@21.0.0(@vueuse/core@10.11.1(vue@3.5.27(typescript@5.9.3))):
|
||||
dependencies:
|
||||
local-pkg: 1.1.2
|
||||
magic-string: 0.30.21
|
||||
picomatch: 4.0.3
|
||||
unimport: 5.7.0
|
||||
unplugin: 2.3.11
|
||||
unplugin-utils: 0.3.1
|
||||
optionalDependencies:
|
||||
'@vueuse/core': 10.11.1(vue@3.5.27(typescript@5.9.3))
|
||||
|
||||
unplugin-utils@0.3.1:
|
||||
dependencies:
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.3
|
||||
|
||||
unplugin-vue-components@31.0.0(vue@3.5.27(typescript@5.9.3)):
|
||||
dependencies:
|
||||
chokidar: 5.0.0
|
||||
local-pkg: 1.1.2
|
||||
magic-string: 0.30.21
|
||||
mlly: 1.8.1
|
||||
obug: 2.1.1
|
||||
picomatch: 4.0.3
|
||||
tinyglobby: 0.2.15
|
||||
unplugin: 2.3.11
|
||||
unplugin-utils: 0.3.1
|
||||
vue: 3.5.27(typescript@5.9.3)
|
||||
|
||||
unplugin@2.3.11:
|
||||
dependencies:
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
acorn: 8.15.0
|
||||
picomatch: 4.0.3
|
||||
webpack-virtual-modules: 0.6.2
|
||||
|
||||
uri-js@4.4.1:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
@@ -3555,6 +3776,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
webpack-virtual-modules@0.6.2: {}
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
@@ -30,7 +30,7 @@ $font-size-small: 12px; // 改为12px
|
||||
$font-size-extra-small: 12px;
|
||||
|
||||
// 字体系列
|
||||
$font-family-primary: 'Noto Sans SC', 'Source Han Sans CN', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
$font-family-primary: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', 'PingFang SC', 'Hiragino Sans GB', 'Noto Sans SC', 'Source Han Sans CN', sans-serif;
|
||||
|
||||
// 字体粗细
|
||||
$font-weight-primary: 500;
|
||||
|
||||
@@ -121,7 +121,6 @@ const appStore = useAppStore()
|
||||
|
||||
// 快捷选择选项
|
||||
const quickOptions = computed(() => [
|
||||
{ label: t('chart.licenseTrend.quickOptions.today'), value: 'today' },
|
||||
{ label: t('chart.licenseTrend.quickOptions.week'), value: 'week' },
|
||||
{ label: t('chart.licenseTrend.quickOptions.month'), value: 'month' }
|
||||
])
|
||||
@@ -160,11 +159,11 @@ const chartOption = computed(() => {
|
||||
|
||||
return {
|
||||
grid: {
|
||||
left: 60,
|
||||
right: 30,
|
||||
left: '3%', // percentage so margins scale with container width
|
||||
right: '3%',
|
||||
top: 30,
|
||||
bottom: 60,
|
||||
containLabel: false
|
||||
bottom: 30, // reduced: containLabel handles room for axis labels
|
||||
containLabel: true // axis labels are always contained within the grid boundary
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
@@ -217,6 +216,7 @@ const chartOption = computed(() => {
|
||||
fontSize: 12
|
||||
},
|
||||
extraCssText: 'box-shadow: 0px 4px 12px 0px rgba(59, 210, 180, 0.2); backdrop-filter: blur(4px);',
|
||||
appendToBody: true, // mount tooltip DOM on <body> to escape overflow:hidden on card
|
||||
formatter: (params: any) => {
|
||||
const data = params[0]
|
||||
return `${t('chart.licenseTrend.tooltip.licenseCount')}: ${data.value}`
|
||||
@@ -277,24 +277,17 @@ const handleQuickSelect = (value: string) => {
|
||||
let endDate: Date
|
||||
|
||||
switch (value) {
|
||||
case 'today':
|
||||
// 本日:今天到今天
|
||||
case 'week':
|
||||
// 最近7天:今天往前6天
|
||||
startDate = new Date(today)
|
||||
startDate.setDate(today.getDate() - 6)
|
||||
endDate = new Date(today)
|
||||
break
|
||||
case 'week':
|
||||
// 本周:本周一到本周日
|
||||
const currentDay = today.getDay() // 0=周日, 1=周一, ..., 6=周六
|
||||
const mondayOffset = currentDay === 0 ? 6 : currentDay - 1 // 计算到周一的偏移
|
||||
startDate = new Date(today)
|
||||
startDate.setDate(today.getDate() - mondayOffset)
|
||||
endDate = new Date(startDate)
|
||||
endDate.setDate(startDate.getDate() + 6) // 周日
|
||||
break
|
||||
case 'month':
|
||||
// 本月:当前月1号到当前月末
|
||||
startDate = new Date(today.getFullYear(), today.getMonth(), 1)
|
||||
endDate = new Date(today.getFullYear(), today.getMonth() + 1, 0) // 下个月0号=本月最后一天
|
||||
// 最近30天:今天往前29天
|
||||
startDate = new Date(today)
|
||||
startDate.setDate(today.getDate() - 29)
|
||||
endDate = new Date(today)
|
||||
break
|
||||
default:
|
||||
return
|
||||
@@ -395,7 +388,7 @@ onMounted(() => {
|
||||
border-radius: 0.42vw; /* 8px/1920 = 0.42vw */
|
||||
box-shadow: var(--app-shadow);
|
||||
overflow: hidden;
|
||||
height: 100%; /* 充满容器高度 */
|
||||
/* Height is driven by the chart canvas below; card grows to fit content */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -506,14 +499,15 @@ onMounted(() => {
|
||||
|
||||
.chart-container {
|
||||
padding: 1.35vw 1.25vw 1.25vw; /* 26px 24px 24px → vw */
|
||||
flex: 1; /* 占据剩余高度 */
|
||||
flex-shrink: 0; /* chart canvas height drives card size — never compress */
|
||||
display: flex;
|
||||
min-height: 0; /* 防止flex溢出 */
|
||||
position: relative; /* establish stacking context for chart canvas */
|
||||
|
||||
.trend-chart {
|
||||
width: 100%;
|
||||
height: 100%; /* 充满容器 */
|
||||
min-height: 12.8vw; /* 最小高度246px/1920 = 12.8vw,确保图表可读性 */
|
||||
height: 18vw; /* ~346px at 1920px reference width */
|
||||
max-height: 248px; /* keeps total card height ≤ 352px (248 + ~50px padding + ~54px header) */
|
||||
min-height: 200px; /* absolute floor so chart is always readable */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,23 @@
|
||||
"usernameMinLength": "Username must be at least 3 characters",
|
||||
"passwordRequired": "Please enter password",
|
||||
"passwordMinLength": "Password must be at least 6 characters"
|
||||
}
|
||||
},
|
||||
"heroTitleLine1": "Secure License",
|
||||
"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"
|
||||
},
|
||||
"welcomeTitle": "Welcome back",
|
||||
"welcomeSubtitle": "Sign in to your account to continue",
|
||||
"copyright": "© 2025 Cedar-V. All rights reserved."
|
||||
},
|
||||
"sidebar": {
|
||||
"expand": "Expand Sidebar",
|
||||
@@ -26,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",
|
||||
@@ -295,9 +317,8 @@
|
||||
"licenseTrend": {
|
||||
"title": "License Trend",
|
||||
"quickOptions": {
|
||||
"today": "Today",
|
||||
"week": "This Week",
|
||||
"month": "This Month"
|
||||
"week": "Last 7 Days",
|
||||
"month": "Last 30 Days"
|
||||
},
|
||||
"datePicker": {
|
||||
"rangeSeparator": "to",
|
||||
@@ -314,9 +335,8 @@
|
||||
"endDate": "End Date",
|
||||
"separator": "to",
|
||||
"quickOptions": {
|
||||
"today": "Today",
|
||||
"week": "This Week",
|
||||
"month": "This Month"
|
||||
"week": "Last 7 Days",
|
||||
"month": "Last 30 Days"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,7 +17,23 @@
|
||||
"usernameMinLength": "ユーザー名は3文字以上である必要があります",
|
||||
"passwordRequired": "パスワードを入力してください",
|
||||
"passwordMinLength": "パスワードは6文字以上である必要があります"
|
||||
}
|
||||
},
|
||||
"heroTitleLine1": "セキュアな",
|
||||
"heroTitleLine2": "ライセンス管理",
|
||||
"heroSubtitle": "エンタープライズグレードのプラットフォームで、ソフトウェアライセンス管理を効率化。",
|
||||
"stats": {
|
||||
"totalAuthCodes": "認可コード総数",
|
||||
"totalAuthCodesSubLabel": "今月 2.4K 件追加",
|
||||
"activeLicenses": "アクティブライセンス",
|
||||
"activeLicensesSubLabel": "先月比 +12%",
|
||||
"systemUptime": "システム稼働率",
|
||||
"systemUptimeSubLabel": "24/7 オンライン",
|
||||
"uptime": "稼働率",
|
||||
"support": "サポート"
|
||||
},
|
||||
"welcomeTitle": "おかえりなさい",
|
||||
"welcomeSubtitle": "アカウントにサインインして続けてください",
|
||||
"copyright": "© 2025 Cedar-V. All rights reserved."
|
||||
},
|
||||
"sidebar": {
|
||||
"expand": "サイドバーを展開",
|
||||
@@ -26,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": "最近のライセンス",
|
||||
@@ -294,9 +316,8 @@
|
||||
"licenseTrend": {
|
||||
"title": "ライセンストレンド",
|
||||
"quickOptions": {
|
||||
"today": "今日",
|
||||
"week": "今週",
|
||||
"month": "今月"
|
||||
"week": "直7日間",
|
||||
"month": "直30日間"
|
||||
},
|
||||
"datePicker": {
|
||||
"rangeSeparator": "から",
|
||||
@@ -313,9 +334,8 @@
|
||||
"endDate": "終了日付",
|
||||
"separator": "から",
|
||||
"quickOptions": {
|
||||
"today": "今日",
|
||||
"week": "今週",
|
||||
"month": "今月"
|
||||
"week": "直7日間",
|
||||
"month": "直30日間"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,7 +17,23 @@
|
||||
"usernameMinLength": "用户名至少需要3个字符",
|
||||
"passwordRequired": "请输入密码",
|
||||
"passwordMinLength": "密码至少需要6个字符"
|
||||
}
|
||||
},
|
||||
"heroTitleLine1": "软件授权",
|
||||
"heroTitleLine2": "管理系统",
|
||||
"heroSubtitle": "赋能软件商业化,构建 B2B 与 B2C 全场景的软件授权体系。",
|
||||
"stats": {
|
||||
"totalAuthCodes": "授权码总量",
|
||||
"totalAuthCodesSubLabel": "本月新增 2.4K",
|
||||
"activeLicenses": "活跃许可证",
|
||||
"activeLicensesSubLabel": "较上月 +12%",
|
||||
"systemUptime": "系统可用性",
|
||||
"systemUptimeSubLabel": "24/7 在线",
|
||||
"uptime": "可用率",
|
||||
"support": "全天支持"
|
||||
},
|
||||
"welcomeTitle": "授权管理平台",
|
||||
"welcomeSubtitle": "登录您的账户",
|
||||
"copyright": "© 2025 Cedar-V. 保留所有权利。"
|
||||
},
|
||||
"sidebar": {
|
||||
"expand": "展开侧边栏",
|
||||
@@ -26,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": "最近授权",
|
||||
@@ -328,9 +350,8 @@
|
||||
"licenseTrend": {
|
||||
"title": "授权趋势",
|
||||
"quickOptions": {
|
||||
"today": "本日",
|
||||
"week": "本周",
|
||||
"month": "本月"
|
||||
"week": "最近7天",
|
||||
"month": "最近30天"
|
||||
},
|
||||
"datePicker": {
|
||||
"rangeSeparator": "至",
|
||||
@@ -347,9 +368,8 @@
|
||||
"endDate": "结束日期",
|
||||
"separator": "至",
|
||||
"quickOptions": {
|
||||
"today": "本日",
|
||||
"week": "本周",
|
||||
"month": "本月"
|
||||
"week": "最近7天",
|
||||
"month": "最近30天"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+1
-11
@@ -10,11 +10,6 @@ import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
// Noto Sans SC 字体(@fontsource 提供预优化子集化版本)
|
||||
import '@fontsource/noto-sans-sc/400.css'
|
||||
import '@fontsource/noto-sans-sc/500.css'
|
||||
import '@fontsource/noto-sans-sc/700.css'
|
||||
|
||||
// 全局样式系统
|
||||
import './assets/styles/fonts.css' // 其他本地字体
|
||||
import './assets/styles/global.scss'
|
||||
@@ -25,10 +20,9 @@ import pinia from './store'
|
||||
import { useAppStore } from './store/modules/app'
|
||||
import { useUserStore } from './store/modules/user'
|
||||
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import i18n, { getCurrentElementLocale } from './i18n'
|
||||
import i18n from './i18n'
|
||||
|
||||
|
||||
const app = createApp(App)
|
||||
@@ -38,10 +32,6 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
// 正确配置Element Plus语言包
|
||||
app.use(ElementPlus, {
|
||||
locale: getCurrentElementLocale()
|
||||
})
|
||||
app.use(pinia)
|
||||
app.use(i18n)
|
||||
app.use(router)
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,9 +237,12 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* content-container样式 */
|
||||
/* content-container styles */
|
||||
.content-container {
|
||||
min-height: calc(100vh - 80px);
|
||||
/* Use a definite height so flex children with height:100% can resolve correctly.
|
||||
min-height alone does not propagate to children in CSS. */
|
||||
height: calc(100vh - 80px);
|
||||
min-height: 600px; /* floor so content is never crushed on very small viewports */
|
||||
padding: 24px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -353,6 +384,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 内容区域 - 默认上下布局,充满屏幕宽度和高度
|
||||
@@ -364,10 +414,9 @@ onMounted(() => {
|
||||
min-height: 0; /* 防止flex子元素溢出 */
|
||||
}
|
||||
|
||||
// 图表区域 - 占据50%高度
|
||||
// Chart section: auto-sized by chart card content, never shrinks
|
||||
.chart-section {
|
||||
flex: 0.5; /* 50%高度 */
|
||||
min-height: 300px; /* 确保最小高度 */
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -380,8 +429,8 @@ onMounted(() => {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0.6; /* 占据60%高度 */
|
||||
min-height: 0; /* 防止flex子元素溢出 */
|
||||
flex: 1; /* fill all remaining space after chart card */
|
||||
min-height: 250px; /* ensure table always has usable space */
|
||||
}
|
||||
|
||||
.card-header {
|
||||
|
||||
@@ -184,7 +184,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, reactive, computed, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules, type MessageBoxInputData } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { AuthorizationCode, LicenseDevice, LicenseDeviceCreateRequest } from '@/api/license'
|
||||
import { getLicenseDevices, getLicenseDeviceDetail, createLicenseDevice, downloadLicenseFile, revokeLicense } from '@/api/license'
|
||||
@@ -421,7 +421,7 @@ const handleRevokeLicense = async (device: LicenseDevice) => {
|
||||
|
||||
let reason = ''
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt(
|
||||
const { value } = (await ElMessageBox.prompt(
|
||||
t('pages.licenses.detail.messages.revokeConfirm'),
|
||||
t('pages.licenses.detail.messages.revokeTitle'),
|
||||
{
|
||||
@@ -441,7 +441,7 @@ const handleRevokeLicense = async (device: LicenseDevice) => {
|
||||
},
|
||||
inputErrorMessage: t('pages.licenses.detail.messages.revokeReasonRequired')
|
||||
}
|
||||
)
|
||||
)) as MessageBoxInputData
|
||||
reason = value.trim()
|
||||
} catch {
|
||||
return
|
||||
|
||||
+804
-631
File diff suppressed because it is too large.
Load diff
+31
-8
@@ -4,6 +4,9 @@ import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
import { visualizer } from 'rollup-plugin-visualizer' // 打包分析插件
|
||||
import { constants } from 'zlib' // 静态引入zlib常量
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
|
||||
// 获取当前时间戳,用于构建输出的文件名
|
||||
const Timestamp = new Date().getTime();
|
||||
@@ -69,6 +72,19 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
||||
}
|
||||
}),
|
||||
|
||||
// Auto-import Element Plus components from templates
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver({ importStyle: 'css' })],
|
||||
dts: false
|
||||
}),
|
||||
|
||||
// Auto-import Element Plus APIs (ElMessage, ElNotification, etc.)
|
||||
AutoImport({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
imports: ['vue', 'vue-router', 'pinia'],
|
||||
dts: false
|
||||
}),
|
||||
|
||||
// 打包分析插件(仅在显式启用时生效;容器/CI 默认不打开浏览器)
|
||||
enableVisualizer && visualizer({ open: false })
|
||||
].filter(Boolean), // 过滤掉false的插件
|
||||
@@ -100,9 +116,14 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
||||
'element-plus',
|
||||
'@element-plus/icons-vue',
|
||||
'axios',
|
||||
'vue-i18n'
|
||||
],
|
||||
exclude: ['@vueuse/core'] // 排除某些包的预构建
|
||||
'vue-i18n',
|
||||
'echarts/core',
|
||||
'echarts/charts',
|
||||
'echarts/components',
|
||||
'echarts/renderers',
|
||||
'vue-echarts',
|
||||
'@vueuse/core'
|
||||
]
|
||||
},
|
||||
|
||||
// 模块解析配置
|
||||
@@ -152,15 +173,17 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
||||
output: {
|
||||
// 优化的手动分块策略
|
||||
manualChunks: {
|
||||
// Vue核心
|
||||
// Vue core
|
||||
vue: ['vue', 'vue-router', 'pinia'],
|
||||
// Element Plus相关
|
||||
// Element Plus (tree-shaken via auto-import, but base bundle still needed)
|
||||
elementPlus: ['element-plus'],
|
||||
// Element Plus图标
|
||||
// Element Plus icons
|
||||
elementIcons: ['@element-plus/icons-vue'],
|
||||
// 工具库
|
||||
// ECharts (large library, isolated for better caching)
|
||||
echarts: ['echarts', 'vue-echarts'],
|
||||
// Utilities
|
||||
utils: ['axios'],
|
||||
// 其他第三方库单独打包
|
||||
// i18n
|
||||
vendor: ['vue-i18n']
|
||||
},
|
||||
// 输出文件命名规则(优化缓存策略)
|
||||
|
||||
Reference in new issue
Block a user