lumencode 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +271 -0
- package/data/pricing.json +3708 -0
- package/index.js +266 -0
- package/lib/aggregate.js +626 -0
- package/lib/attribution.js +137 -0
- package/lib/blocks.js +86 -0
- package/lib/cache.js +18 -0
- package/lib/config.js +91 -0
- package/lib/git.js +1106 -0
- package/lib/models/usage-record.js +46 -0
- package/lib/parser.js +160 -0
- package/lib/parsers/base.js +67 -0
- package/lib/parsers/claude.js +316 -0
- package/lib/parsers/codex.js +316 -0
- package/lib/parsers/index.js +151 -0
- package/lib/parsers/opencode.js +216 -0
- package/lib/pricing-loader.js +287 -0
- package/lib/record-utils.js +35 -0
- package/lib/report.js +1446 -0
- package/lib/scenario.js +183 -0
- package/lib/server.js +412 -0
- package/lib/table.js +67 -0
- package/package.json +44 -0
- package/public/api.js +109 -0
- package/public/app.js +647 -0
- package/public/charts.js +197 -0
- package/public/config.js +141 -0
- package/public/export.js +282 -0
- package/public/fonts/inter-0.woff2 +0 -0
- package/public/fonts/inter-1.woff2 +0 -0
- package/public/fonts/inter-2.woff2 +0 -0
- package/public/fonts/inter-3.woff2 +0 -0
- package/public/fonts/inter.css +28 -0
- package/public/git-insights.js +123 -0
- package/public/index.html +347 -0
- package/public/style.css +1864 -0
- package/public/ui-state.js +103 -0
- package/public/utils.js +71 -0
- package/public/vendor/alpine.min.js +5 -0
- package/public/vendor/chart.umd.min.js +20 -0
- package/public/work-report.js +118 -0
package/public/charts.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { COLORS, SCENARIO_COLORS, COMMIT_TYPE_COLORS, TEXT } from './config.js';
|
|
2
|
+
import { esc, fmt, fmtShort, destroyChart, setChart } from './utils.js';
|
|
3
|
+
|
|
4
|
+
// ── Doughnut ──
|
|
5
|
+
export function renderDoughnut(canvasId, dataMap, label) {
|
|
6
|
+
destroyChart(canvasId);
|
|
7
|
+
const entries = Object.entries(dataMap).filter(([, v]) => v > 0).sort((a, b) => b[1] - a[1]);
|
|
8
|
+
const wrap = document.getElementById(canvasId).parentElement;
|
|
9
|
+
wrap.style.height = '260px';
|
|
10
|
+
const ctx = document.getElementById(canvasId).getContext('2d');
|
|
11
|
+
const colors = entries.map(([k]) => SCENARIO_COLORS[k] || COLORS[entries.length % COLORS.length]);
|
|
12
|
+
const instance = new Chart(ctx, {
|
|
13
|
+
type: 'doughnut',
|
|
14
|
+
data: {
|
|
15
|
+
labels: entries.map(([k]) => k),
|
|
16
|
+
datasets: [{ data: entries.map(([, v]) => v), backgroundColor: colors, borderWidth: 0, hoverOffset: 4 }],
|
|
17
|
+
},
|
|
18
|
+
options: {
|
|
19
|
+
responsive: true, maintainAspectRatio: false, cutout: '65%',
|
|
20
|
+
plugins: {
|
|
21
|
+
legend: { position: 'bottom', labels: { font: { family: 'Inter', size: 11 }, padding: 12, boxWidth: 10, usePointStyle: true, pointStyle: 'circle' } },
|
|
22
|
+
tooltip: { callbacks: { label: (c) => { const total = c.dataset.data.reduce((s, v) => s + v, 0); return ` ${c.label}: ${c.raw} (${((c.raw / total) * 100).toFixed(1)}%)`; } } },
|
|
23
|
+
},
|
|
24
|
+
onClick: (evt, elements) => {
|
|
25
|
+
if (elements.length === 0) return;
|
|
26
|
+
const clickEntries = Object.entries(dataMap).filter(([, v]) => v > 0).sort((a, b) => b[1] - a[1]);
|
|
27
|
+
const label = clickEntries[elements[0].index]?.[0];
|
|
28
|
+
const scenarioKeyMap = { '编码': 'coding', '测试/QA': 'testing', '调试/排错': 'debugging', '文档': 'documentation', '阅读/研究': 'reading', '规划/设计': 'planning', '代码审查': 'review' };
|
|
29
|
+
const key = scenarioKeyMap[label];
|
|
30
|
+
if (!key) return;
|
|
31
|
+
if (typeof window._drillHandler === 'function') window._drillHandler('scenario', key, label);
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
setChart(canvasId, instance);
|
|
36
|
+
return instance;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Bar ──
|
|
40
|
+
export function renderBar(canvasId, labels, data, datasetLabel) {
|
|
41
|
+
destroyChart(canvasId);
|
|
42
|
+
const ctx = document.getElementById(canvasId).getContext('2d');
|
|
43
|
+
const instance = new Chart(ctx, {
|
|
44
|
+
type: 'bar',
|
|
45
|
+
data: { labels, datasets: [{ label: datasetLabel, data, backgroundColor: '#374151', borderRadius: 6, maxBarThickness: 20, barPercentage: 0.7 }] },
|
|
46
|
+
options: {
|
|
47
|
+
responsive: true, maintainAspectRatio: false, indexAxis: 'y',
|
|
48
|
+
scales: { x: { grid: { color: '#f3f4f6' }, ticks: { font: { family: 'Inter', size: 11 } } }, y: { grid: { display: false }, ticks: { font: { family: 'Inter', size: 12 } } } },
|
|
49
|
+
plugins: { legend: { display: false } },
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
setChart(canvasId, instance);
|
|
53
|
+
return instance;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Commit Type ──
|
|
57
|
+
export function renderCommitTypeChart(typeEntries) {
|
|
58
|
+
destroyChart('commitTypeChart');
|
|
59
|
+
const canvas = document.getElementById('commitTypeChart');
|
|
60
|
+
const wrap = canvas.parentElement;
|
|
61
|
+
wrap.style.height = Math.max(180, typeEntries.length * 32 + 40) + 'px';
|
|
62
|
+
const labels = typeEntries.map(([k]) => k);
|
|
63
|
+
const data = typeEntries.map(([, v]) => v);
|
|
64
|
+
const colors = labels.map(k => COMMIT_TYPE_COLORS[k] || COMMIT_TYPE_COLORS.other);
|
|
65
|
+
const ctx = canvas.getContext('2d');
|
|
66
|
+
const instance = new Chart(ctx, {
|
|
67
|
+
type: 'bar',
|
|
68
|
+
data: { labels, datasets: [{ label: '提交数', data, backgroundColor: colors, borderRadius: 6, maxBarThickness: 20, barPercentage: 0.65, categoryPercentage: 0.85 }] },
|
|
69
|
+
options: {
|
|
70
|
+
responsive: true, maintainAspectRatio: false, indexAxis: 'y',
|
|
71
|
+
scales: { x: { grid: { color: '#f3f4f6' }, ticks: { font: { family: 'Inter', size: 11 }, precision: 0 } }, y: { grid: { display: false }, ticks: { font: { family: 'Inter', size: 12 } } } },
|
|
72
|
+
plugins: { legend: { display: false } },
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
setChart('commitTypeChart', instance);
|
|
76
|
+
return instance;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Trend (dual-axis line) ──
|
|
80
|
+
export function renderTrend(trendData) {
|
|
81
|
+
destroyChart('trendChart');
|
|
82
|
+
const dates = Object.keys(trendData.dailyStats).sort();
|
|
83
|
+
const requests = dates.map(d => trendData.dailyStats[d].requests);
|
|
84
|
+
const tokens = dates.map(d => ((trendData.dailyStats[d].inputTokens || 0) + (trendData.dailyStats[d].outputTokens || 0)) / 1000);
|
|
85
|
+
const labels = dates.map(d => d.slice(5));
|
|
86
|
+
const ctx = document.getElementById('trendChart');
|
|
87
|
+
if (!ctx) return null;
|
|
88
|
+
const instance = new Chart(ctx.getContext('2d'), {
|
|
89
|
+
type: 'line',
|
|
90
|
+
data: {
|
|
91
|
+
labels,
|
|
92
|
+
datasets: [
|
|
93
|
+
{ label: '请求数', data: requests, borderColor: '#111111', backgroundColor: 'rgba(17,17,17,0.08)', fill: true, tension: 0.3, pointRadius: 3, yAxisID: 'y' },
|
|
94
|
+
{ label: 'Token (K)', data: tokens, borderColor: '#8b5cf6', backgroundColor: 'rgba(139,92,246,0.08)', fill: true, tension: 0.3, pointRadius: 3, yAxisID: 'y1' },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
options: {
|
|
98
|
+
responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false },
|
|
99
|
+
scales: {
|
|
100
|
+
y: { position: 'left', grid: { color: '#f3f4f6' }, ticks: { font: { family: 'Inter', size: 11 } }, title: { display: true, text: '请求数', font: { family: 'Inter', size: 12 } } },
|
|
101
|
+
y1: { position: 'right', grid: { display: false }, ticks: { font: { family: 'Inter', size: 11 } }, title: { display: true, text: 'Token (K)', font: { family: 'Inter', size: 12 } } },
|
|
102
|
+
x: { grid: { display: false }, ticks: { font: { family: 'Inter', size: 11 } } },
|
|
103
|
+
},
|
|
104
|
+
plugins: { legend: { position: 'top', labels: { font: { family: 'Inter', size: 12 }, padding: 16 } } },
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
setChart('trendChart', instance);
|
|
108
|
+
return instance;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Cache Efficiency ──
|
|
112
|
+
export function renderCacheEfficiency(canvasId, cacheRead, cacheCreate, inputTokens, costBreakdown) {
|
|
113
|
+
const total = cacheRead + inputTokens + cacheCreate;
|
|
114
|
+
if (total === 0) { destroyChart(canvasId); return null; }
|
|
115
|
+
|
|
116
|
+
destroyChart(canvasId);
|
|
117
|
+
const canvas = document.getElementById(canvasId);
|
|
118
|
+
if (!canvas) return null;
|
|
119
|
+
const ctx = canvas.getContext('2d');
|
|
120
|
+
const instance = new Chart(ctx, {
|
|
121
|
+
type: 'bar',
|
|
122
|
+
data: {
|
|
123
|
+
labels: ['Token 构成'],
|
|
124
|
+
datasets: [
|
|
125
|
+
{ label: '缓存命中', data: [cacheRead], backgroundColor: '#22c55e', borderRadius: 4 },
|
|
126
|
+
{ label: '新输入', data: [inputTokens], backgroundColor: '#3b82f6', borderRadius: 4 },
|
|
127
|
+
{ label: '缓存写入', data: [cacheCreate], backgroundColor: '#f59e0b', borderRadius: 4 },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
options: {
|
|
131
|
+
responsive: true, maintainAspectRatio: false, indexAxis: 'y',
|
|
132
|
+
scales: {
|
|
133
|
+
x: { stacked: true, grid: { color: '#f3f4f6' }, ticks: { font: { family: 'Inter', size: 11 }, callback: v => fmtShort(v) } },
|
|
134
|
+
y: { stacked: true, grid: { display: false }, ticks: { font: { family: 'Inter', size: 12 } } },
|
|
135
|
+
},
|
|
136
|
+
plugins: {
|
|
137
|
+
legend: { position: 'bottom', labels: { font: { family: 'Inter', size: 11 }, padding: 12, boxWidth: 10, usePointStyle: true, pointStyle: 'circle' } },
|
|
138
|
+
tooltip: {
|
|
139
|
+
callbacks: {
|
|
140
|
+
label: (c) => {
|
|
141
|
+
const pct = total > 0 ? ((c.raw / total) * 100).toFixed(1) : 0;
|
|
142
|
+
return ` ${c.dataset.label}: ${fmtShort(c.raw)} (${pct}%)`;
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
setChart(canvasId, instance);
|
|
150
|
+
return instance;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── Model Cost ──
|
|
154
|
+
export function renderModelCostChart(canvasId, models, costBreakdown) {
|
|
155
|
+
if (!costBreakdown?.models?.length) { destroyChart(canvasId); return null; }
|
|
156
|
+
|
|
157
|
+
const entries = costBreakdown.models.filter(m => m.cost > 0);
|
|
158
|
+
if (entries.length === 0) { destroyChart(canvasId); return null; }
|
|
159
|
+
|
|
160
|
+
const labels = entries.map(m => m.name.length > 22 ? '...' + m.name.slice(-19) : m.name);
|
|
161
|
+
const data = entries.map(m => m.cost);
|
|
162
|
+
const modeColors = { actual: '#22c55e', estimated: '#3b82f6', unknown: '#9ca3af' };
|
|
163
|
+
const colors = entries.map(m => modeColors[m.mode] || modeColors.unknown);
|
|
164
|
+
|
|
165
|
+
destroyChart(canvasId);
|
|
166
|
+
const canvas = document.getElementById(canvasId);
|
|
167
|
+
if (!canvas) return null;
|
|
168
|
+
const ctx = canvas.getContext('2d');
|
|
169
|
+
const instance = new Chart(ctx, {
|
|
170
|
+
type: 'bar',
|
|
171
|
+
data: {
|
|
172
|
+
labels,
|
|
173
|
+
datasets: [{ label: '费用 ($)', data, backgroundColor: colors, borderRadius: 6, maxBarThickness: 20, barPercentage: 0.7 }],
|
|
174
|
+
},
|
|
175
|
+
options: {
|
|
176
|
+
responsive: true, maintainAspectRatio: false, indexAxis: 'y',
|
|
177
|
+
scales: {
|
|
178
|
+
x: { grid: { color: '#f3f4f6' }, ticks: { font: { family: 'Inter', size: 11 }, callback: v => '$' + v.toFixed(2) } },
|
|
179
|
+
y: { grid: { display: false }, ticks: { font: { family: 'Inter', size: 12 } } },
|
|
180
|
+
},
|
|
181
|
+
plugins: {
|
|
182
|
+
legend: { display: false },
|
|
183
|
+
tooltip: {
|
|
184
|
+
callbacks: {
|
|
185
|
+
label: (c) => {
|
|
186
|
+
const entry = entries[c.dataIndex];
|
|
187
|
+
const modeLabel = { actual: '实际计费', estimated: '估算', unknown: '未知定价' };
|
|
188
|
+
return ` $${c.raw.toFixed(2)} (${modeLabel[entry?.mode] || entry?.mode}, ${entry?.requests || 0} 次)`;
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
setChart(canvasId, instance);
|
|
196
|
+
return instance;
|
|
197
|
+
}
|
package/public/config.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// API 路径
|
|
2
|
+
export const API = {
|
|
3
|
+
TOOLS: '/api/tools',
|
|
4
|
+
REPORT: '/api/report',
|
|
5
|
+
CONFIG: '/api/config',
|
|
6
|
+
DETAILS: '/api/details',
|
|
7
|
+
SESSIONS: '/api/sessions',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// 灰阶色板(按视觉权重从重到轻)
|
|
11
|
+
export const COLORS = [
|
|
12
|
+
'#111111',
|
|
13
|
+
'#374151',
|
|
14
|
+
'#4b5563',
|
|
15
|
+
'#6b7280',
|
|
16
|
+
'#898989',
|
|
17
|
+
'#9ca3af',
|
|
18
|
+
'#cbd5e1',
|
|
19
|
+
'#e5e7eb',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// 场景色板
|
|
23
|
+
export const SCENARIO_COLORS = {
|
|
24
|
+
'编码': '#8ab8a0',
|
|
25
|
+
'测试/QA': '#c8b880',
|
|
26
|
+
'调试/排错': '#c49090',
|
|
27
|
+
'文档': '#90a8c8',
|
|
28
|
+
'阅读/研究': '#a090c0',
|
|
29
|
+
'规划/设计': '#c8a080',
|
|
30
|
+
'代码审查': '#80b8b8',
|
|
31
|
+
'其他': '#a8a8a8',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// 提交类型色板
|
|
35
|
+
export const COMMIT_TYPE_COLORS = {
|
|
36
|
+
feat: '#8ab8a0',
|
|
37
|
+
fix: '#c49090',
|
|
38
|
+
refactor: '#a090c0',
|
|
39
|
+
docs: '#90a8c8',
|
|
40
|
+
test: '#c8b880',
|
|
41
|
+
chore: '#a8a8a8',
|
|
42
|
+
perf: '#c890b0',
|
|
43
|
+
style: '#80b8b8',
|
|
44
|
+
ci: '#c8a080',
|
|
45
|
+
build: '#a8c880',
|
|
46
|
+
revert: '#c49090',
|
|
47
|
+
other: '#b8b8b8',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// 中文字符串
|
|
51
|
+
export const TEXT = {
|
|
52
|
+
ALL_TOOLS: '全部工具',
|
|
53
|
+
DAILY: '日报',
|
|
54
|
+
WEEKLY: '周报',
|
|
55
|
+
MONTHLY: '月报',
|
|
56
|
+
USAGE: '使用',
|
|
57
|
+
DATA_ANALYSIS: '数据分析',
|
|
58
|
+
LOADING: '加载中...',
|
|
59
|
+
NO_DATA: '无数据',
|
|
60
|
+
NO_MATCH: '无匹配记录',
|
|
61
|
+
COPIED: '已复制',
|
|
62
|
+
COPY: '复制',
|
|
63
|
+
NOT_CONFIGURED: '未配置',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// DOM 元素 ID
|
|
67
|
+
export const ID = {
|
|
68
|
+
TOAST: 'toast',
|
|
69
|
+
DATE_INPUT: 'dateInput',
|
|
70
|
+
PREV_DATE: 'prevDate',
|
|
71
|
+
NEXT_DATE: 'nextDate',
|
|
72
|
+
THEME_BTN: 'themeBtn',
|
|
73
|
+
MOON_ICON: 'moonIcon',
|
|
74
|
+
SUN_ICON: 'sunIcon',
|
|
75
|
+
SETTINGS_BTN: 'settingsBtn',
|
|
76
|
+
SETTINGS_MODAL: 'settingsModal',
|
|
77
|
+
CLOSE_SETTINGS: 'closeSettings',
|
|
78
|
+
SAVE_SETTINGS: 'saveSettings',
|
|
79
|
+
REPORT_TITLE: 'reportTitle',
|
|
80
|
+
REPORT_DATE: 'reportDate',
|
|
81
|
+
EXPORT_CSV_BTN: 'exportCsvBtn',
|
|
82
|
+
PRINT_BTN: 'printBtn',
|
|
83
|
+
EXPORT_JSON_BTN: 'exportJsonBtn',
|
|
84
|
+
EXPORT_HTML_BTN: 'exportHtmlBtn',
|
|
85
|
+
WORK_REPORT_BTN: 'workReportBtn',
|
|
86
|
+
STAT_SESSIONS: 'statSessions',
|
|
87
|
+
STAT_REQUESTS: 'statRequests',
|
|
88
|
+
STAT_PROJECTS: 'statProjects',
|
|
89
|
+
STAT_TOKENS: 'statTokens',
|
|
90
|
+
STAT_TOKEN_BREAKDOWN: 'statTokenBreakdown',
|
|
91
|
+
STAT_COST: 'statCost',
|
|
92
|
+
STAT_COST_MODEL: 'statCostModel',
|
|
93
|
+
TREND_SESSIONS: 'trendSessions',
|
|
94
|
+
TREND_REQUESTS: 'trendRequests',
|
|
95
|
+
TREND_PROJECTS: 'trendProjects',
|
|
96
|
+
TREND_TOKENS: 'trendTokens',
|
|
97
|
+
TREND_COST: 'trendCost',
|
|
98
|
+
STATS_GRID: 'statsGrid',
|
|
99
|
+
ANALYTICS_SECTION: 'analyticsSection',
|
|
100
|
+
CHARTS_DASHBOARD: 'chartsDashboard',
|
|
101
|
+
NO_DATA_HINT: 'noDataHint',
|
|
102
|
+
TREND_SECTION: 'trendSection',
|
|
103
|
+
GIT_SECTION: 'gitSection',
|
|
104
|
+
GIT_STATS: 'gitStats',
|
|
105
|
+
GIT_AI_STATS: 'gitAiStats',
|
|
106
|
+
GIT_INSIGHTS_ROW: 'gitInsightsRow',
|
|
107
|
+
WELCOME_PAGE: 'welcomePage',
|
|
108
|
+
WELCOME_CLAUDE_DIR: 'welcomeClaudeDir',
|
|
109
|
+
WELCOME_REPOS: 'welcomeRepos',
|
|
110
|
+
WELCOME_START_BTN: 'welcomeStartBtn',
|
|
111
|
+
WELCOME_HINT: 'welcomeHint',
|
|
112
|
+
WORK_REPORT_SECTION: 'workReportSection',
|
|
113
|
+
WORK_REPORT_CONTENT: 'workReportContent',
|
|
114
|
+
COPY_WORK_REPORT: 'copyWorkReport',
|
|
115
|
+
DOWNLOAD_MD_BTN: 'downloadMdBtn',
|
|
116
|
+
BACK_TO_REPORT: 'backToReport',
|
|
117
|
+
DRILL_MODAL: 'drillModal',
|
|
118
|
+
DRILL_TITLE: 'drillTitle',
|
|
119
|
+
DRILL_BODY: 'drillBody',
|
|
120
|
+
CLOSE_DRILL: 'closeDrill',
|
|
121
|
+
SCENARIO_CHART: 'scenarioChart',
|
|
122
|
+
MODEL_CHART: 'modelChart',
|
|
123
|
+
PROJECT_CHART: 'projectChart',
|
|
124
|
+
TOOL_CHART: 'toolChart',
|
|
125
|
+
TREND_CHART: 'trendChart',
|
|
126
|
+
COMMIT_TYPE_CHART: 'commitTypeChart',
|
|
127
|
+
CACHE_CHART: 'cacheChart',
|
|
128
|
+
MODEL_COST_CHART: 'modelCostChart',
|
|
129
|
+
CFG_CLAUDE_DIR: 'cfgClaudeDir',
|
|
130
|
+
CFG_REPOS: 'cfgRepos',
|
|
131
|
+
CFG_EXCLUDE: 'cfgExclude',
|
|
132
|
+
CFG_KEYWORDS: 'cfgKeywords',
|
|
133
|
+
FILE_HOTSPOTS_TABLE: 'fileHotspotsTable',
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// localStorage keys
|
|
137
|
+
export const STORAGE = {
|
|
138
|
+
CONFIG: 'ccusage-config',
|
|
139
|
+
THEME: 'ccusage-theme',
|
|
140
|
+
SIDEBAR_COLLAPSED: 'ccusage-sidebar-collapsed',
|
|
141
|
+
};
|
package/public/export.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { TEXT } from './config.js';
|
|
2
|
+
import { esc, fmt, fmtShort, getChart } from './utils.js';
|
|
3
|
+
|
|
4
|
+
// ── CSV 导出 ──
|
|
5
|
+
export function exportCSV(data, period) {
|
|
6
|
+
if (!data) return;
|
|
7
|
+
const { usageStats, gitStats, start, end } = data;
|
|
8
|
+
const lines = [];
|
|
9
|
+
|
|
10
|
+
lines.push('# 概览统计');
|
|
11
|
+
lines.push('指标,数值');
|
|
12
|
+
lines.push(`会话数,${usageStats.sessionCount}`);
|
|
13
|
+
lines.push(`请求数,${usageStats.requestCount}`);
|
|
14
|
+
lines.push(`用户消息,${usageStats.userMessageCount}`);
|
|
15
|
+
lines.push(`覆盖项目,${Object.keys(usageStats.projects).length}`);
|
|
16
|
+
lines.push(`输入Token,${usageStats.inputTokens}`);
|
|
17
|
+
lines.push(`输出Token,${usageStats.outputTokens}`);
|
|
18
|
+
lines.push(`缓存读取Token,${usageStats.cacheRead}`);
|
|
19
|
+
lines.push(`总Token,${usageStats.totalTokens}`);
|
|
20
|
+
if (usageStats.estimatedCost) lines.push(`预估费用,$${usageStats.estimatedCost.toFixed(2)}`);
|
|
21
|
+
if (usageStats.subagentTokens > 0) lines.push(`子Agent Token,${usageStats.subagentTokens}`);
|
|
22
|
+
if (gitStats && gitStats.commits > 0) {
|
|
23
|
+
lines.push(`Git提交,${gitStats.commits}`);
|
|
24
|
+
lines.push(`新增行数,+${gitStats.linesAdded}`);
|
|
25
|
+
lines.push(`删除行数,-${gitStats.linesDeleted}`);
|
|
26
|
+
lines.push(`变更文件,${gitStats.filesChanged}`);
|
|
27
|
+
if (gitStats.aiContribution) {
|
|
28
|
+
const ai = gitStats.aiContribution;
|
|
29
|
+
const pct = Math.round((ai.aiCommits / gitStats.commits) * 100);
|
|
30
|
+
lines.push(`高/中置信AI提交,${ai.aiCommits}/${gitStats.commits} (${pct}%)`);
|
|
31
|
+
lines.push(`高置信提交,${ai.highConfidenceCommits}`);
|
|
32
|
+
lines.push(`AI命中文件新增行,+${ai.aiFileLinesAdded}`);
|
|
33
|
+
lines.push(`AI命中文件删除行,-${ai.aiFileLinesDeleted}`);
|
|
34
|
+
lines.push(`低置信关联提交,${ai.lowConfidenceCommits}`);
|
|
35
|
+
}
|
|
36
|
+
if (gitStats.commitTypes) {
|
|
37
|
+
for (const [t, n] of Object.entries(gitStats.commitTypes).filter(([, v]) => v > 0).sort((a, b) => b[1] - a[1])) {
|
|
38
|
+
lines.push(`commit类型-${t},${n}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
lines.push('');
|
|
43
|
+
|
|
44
|
+
const daily = usageStats.dailyStats || {};
|
|
45
|
+
if (Object.keys(daily).length > 0) {
|
|
46
|
+
lines.push('# 每日明细');
|
|
47
|
+
lines.push('日期,请求数,用户消息,输入Token,输出Token,缓存读取');
|
|
48
|
+
for (const d of Object.keys(daily).sort()) {
|
|
49
|
+
const s = daily[d];
|
|
50
|
+
lines.push(`${d},${s.requests},${s.userMessages || 0},${s.inputTokens},${s.outputTokens},${s.cacheRead || 0}`);
|
|
51
|
+
}
|
|
52
|
+
lines.push('');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const projEntries = Object.entries(usageStats.projects).sort((a, b) => b[1].requests - a[1].requests);
|
|
56
|
+
if (projEntries.length > 0) {
|
|
57
|
+
lines.push('# 项目分布');
|
|
58
|
+
lines.push('项目,请求数,会话数');
|
|
59
|
+
for (const [name, d] of projEntries) {
|
|
60
|
+
const sess = d.sessions instanceof Set ? d.sessions.size : (d.sessions || 0);
|
|
61
|
+
lines.push(`"${name.replace(/"/g, '""')}",${d.requests},${sess}`);
|
|
62
|
+
}
|
|
63
|
+
lines.push('');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const modelEntries = Object.entries(usageStats.models).sort((a, b) => b[1].count - a[1].count);
|
|
67
|
+
if (modelEntries.length > 0) {
|
|
68
|
+
lines.push('# 模型分布');
|
|
69
|
+
lines.push('模型,请求数,输入Token,输出Token,缓存读取');
|
|
70
|
+
for (const [name, d] of modelEntries) {
|
|
71
|
+
lines.push(`"${name}",${d.count},${d.inputTokens},${d.outputTokens},${d.cacheRead || 0}`);
|
|
72
|
+
}
|
|
73
|
+
lines.push('');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const toolEntries = Object.entries(usageStats.tools).sort((a, b) => b[1] - a[1]);
|
|
77
|
+
if (toolEntries.length > 0) {
|
|
78
|
+
lines.push('# 工具使用');
|
|
79
|
+
lines.push('工具,调用次数');
|
|
80
|
+
for (const [name, count] of toolEntries) lines.push(`${name},${count}`);
|
|
81
|
+
lines.push('');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const scenarioEntries = Object.entries(usageStats.scenarios).sort((a, b) => b[1] - a[1]);
|
|
85
|
+
if (scenarioEntries.length > 0) {
|
|
86
|
+
lines.push('# 场景分布');
|
|
87
|
+
lines.push('场景,请求数');
|
|
88
|
+
for (const [name, count] of scenarioEntries) lines.push(`${name},${count}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const csv = lines.join('\n');
|
|
92
|
+
const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8' });
|
|
93
|
+
const url = URL.createObjectURL(blob);
|
|
94
|
+
const a = document.createElement('a');
|
|
95
|
+
a.href = url;
|
|
96
|
+
a.download = `ccusage-${period}-${start}-${end}.csv`;
|
|
97
|
+
a.click();
|
|
98
|
+
URL.revokeObjectURL(url);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Print helper ──
|
|
102
|
+
function printTable(title, headers, rows) {
|
|
103
|
+
if (!rows || rows.length === 0) return '';
|
|
104
|
+
return `<h2>${esc(title)}</h2><table><tr>${headers.map(h => `<th>${esc(h)}</th>`).join('')}</tr>${rows.map(r => `<tr>${r.map(c => `<td>${esc(c)}</td>`).join('')}</tr>`).join('')}</table>`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Print / PDF ──
|
|
108
|
+
export function printReport(data, period) {
|
|
109
|
+
if (!data) return;
|
|
110
|
+
const { usageStats, gitStats, start, end } = data;
|
|
111
|
+
const periodName = period === 'daily' ? TEXT.DAILY : period === 'weekly' ? TEXT.WEEKLY : TEXT.MONTHLY;
|
|
112
|
+
const dateRange = period === 'daily' ? start : `${start} ~ ${end}`;
|
|
113
|
+
|
|
114
|
+
const imgs = {};
|
|
115
|
+
const charts = {};
|
|
116
|
+
// 从 utils 的 getChart 获取所有图表
|
|
117
|
+
for (const key of ['scenarioChart', 'modelChart', 'projectChart', 'toolChart', 'trendChart', 'commitTypeChart']) {
|
|
118
|
+
const ch = getChart(key);
|
|
119
|
+
if (ch) { try { imgs[key] = ch.toBase64Image(); } catch {} }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const projRows = Object.entries(usageStats.projects).sort((a, b) => b[1].requests - a[1].requests).map(([n, d]) => [n, d.requests, d.sessions instanceof Set ? d.sessions.size : (d.sessions || 0)]);
|
|
123
|
+
const modelRows = Object.entries(usageStats.models).sort((a, b) => b[1].count - a[1].count).map(([n, d]) => [n, d.count, fmtShort(d.inputTokens), fmtShort(d.outputTokens)]);
|
|
124
|
+
const toolRows = Object.entries(usageStats.tools).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([n, c]) => [n, c]);
|
|
125
|
+
const scenarioRows = Object.entries(usageStats.scenarios).sort((a, b) => b[1] - a[1]).map(([n, c]) => [n, c]);
|
|
126
|
+
|
|
127
|
+
const html = `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title>Claude Code 使用${periodName}</title>
|
|
128
|
+
<style>
|
|
129
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
130
|
+
body{font-family:'Inter',-apple-system,sans-serif;color:#111;padding:32px 40px;max-width:800px;margin:0 auto;font-size:13px;line-height:1.5}
|
|
131
|
+
h1{font-size:20px;margin-bottom:2px;letter-spacing:-0.3px}
|
|
132
|
+
.sub{color:#6b7280;font-size:12px;margin-bottom:20px}
|
|
133
|
+
.summary{display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin-bottom:20px}
|
|
134
|
+
.s{text-align:center;padding:10px 6px;background:#f5f5f5;border-radius:6px}
|
|
135
|
+
.sv{font-size:18px;font-weight:600;letter-spacing:-0.3px}
|
|
136
|
+
.sl{font-size:10px;color:#6b7280;margin-top:2px}
|
|
137
|
+
h2{font-size:13px;margin-top:18px;margin-bottom:6px;padding-bottom:4px;border-bottom:1px solid #e5e7eb}
|
|
138
|
+
table{width:100%;border-collapse:collapse;font-size:12px;margin-bottom:12px}
|
|
139
|
+
th,td{padding:5px 8px;text-align:left;border-bottom:1px solid #e5e7eb}
|
|
140
|
+
th{font-weight:600;background:#f8f9fa}
|
|
141
|
+
.charts{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:16px 0}
|
|
142
|
+
.cc{text-align:center}
|
|
143
|
+
.cc p{font-size:11px;font-weight:600;margin-bottom:2px}
|
|
144
|
+
.cc img{max-width:100%;height:180px;object-fit:contain}
|
|
145
|
+
.ft{text-align:center;color:#9ca3af;font-size:10px;margin-top:24px;padding-top:12px;border-top:1px solid #e5e7eb}
|
|
146
|
+
@media print{body{padding:20px 24px}@page{margin:15mm}}
|
|
147
|
+
</style></head><body>
|
|
148
|
+
<h1>Claude Code 使用${periodName}</h1>
|
|
149
|
+
<p class="sub">${dateRange} · 生成于 ${new Date().toLocaleString('zh-CN')}</p>
|
|
150
|
+
<div class="summary">
|
|
151
|
+
<div class="s"><div class="sv">${fmt(usageStats.sessionCount)}</div><div class="sl">独立会话</div></div>
|
|
152
|
+
<div class="s"><div class="sv">${fmt(usageStats.requestCount)}</div><div class="sl">交互轮次</div></div>
|
|
153
|
+
<div class="s"><div class="sv">${Object.keys(usageStats.projects).length}</div><div class="sl">覆盖项目</div></div>
|
|
154
|
+
<div class="s"><div class="sv">${fmt(usageStats.totalTokens)}</div><div class="sl">Token 消耗</div></div>
|
|
155
|
+
<div class="s"><div class="sv">${usageStats.estimatedCost ? '$' + usageStats.estimatedCost.toFixed(2) : '-'}</div><div class="sl">预估费用</div></div>
|
|
156
|
+
</div>
|
|
157
|
+
${printTable('项目分布', ['项目', '请求数', '会话数'], projRows)}
|
|
158
|
+
${printTable('模型分布', ['模型', '请求数', '输入', '输出'], modelRows)}
|
|
159
|
+
${printTable('工具使用排行', ['工具', '调用次数'], toolRows)}
|
|
160
|
+
${printTable('场景分布', ['场景', '请求数'], scenarioRows)}
|
|
161
|
+
${gitStats && gitStats.commits > 0 ? printTable('Git 代码产出', ['指标', '数值'], (() => {
|
|
162
|
+
const rows = [['提交次数', gitStats.commits], ['新增行数', '+' + fmt(gitStats.linesAdded)], ['删除行数', '-' + fmt(gitStats.linesDeleted)], ['变更文件', gitStats.filesChanged]];
|
|
163
|
+
if (gitStats.aiContribution) {
|
|
164
|
+
const ai = gitStats.aiContribution;
|
|
165
|
+
const pct = Math.round((ai.aiCommits / gitStats.commits) * 100);
|
|
166
|
+
rows.push(['高/中置信 AI 提交', `${ai.aiCommits}/${gitStats.commits} (${pct}%)`]);
|
|
167
|
+
rows.push(['高置信提交', `${ai.highConfidenceCommits}`]);
|
|
168
|
+
rows.push(['AI 命中文件新增行', `+${ai.aiFileLinesAdded}`]);
|
|
169
|
+
rows.push(['AI 命中文件删除行', `-${ai.aiFileLinesDeleted}`]);
|
|
170
|
+
rows.push(['低置信关联提交', `${ai.lowConfidenceCommits}`]);
|
|
171
|
+
}
|
|
172
|
+
return rows;
|
|
173
|
+
})()) : ''}
|
|
174
|
+
${gitStats && gitStats.commitTypes ? (() => {
|
|
175
|
+
const types = Object.entries(gitStats.commitTypes).filter(([, v]) => v > 0).sort((a, b) => b[1] - a[1]);
|
|
176
|
+
return types.length ? printTable('提交类型分布', ['类型', '数量'], types.map(([t, n]) => [t, n])) : '';
|
|
177
|
+
})() : ''}
|
|
178
|
+
${gitStats && gitStats.fileHotspots?.length ? printTable('文件热点 Top 10', ['文件', '触碰', '+行', '-行'], gitStats.fileHotspots.map(h => [h.path, h.touches, '+' + fmt(h.added), '-' + fmt(h.deleted)])) : ''}
|
|
179
|
+
${(imgs.scenarioChart || imgs.modelChart) ? `<h2>图表</h2><div class="charts">${imgs.scenarioChart ? '<div class="cc"><p>工作类型分布</p><img src="' + imgs.scenarioChart + '"></div>' : ''}${imgs.modelChart ? '<div class="cc"><p>模型使用分布</p><img src="' + imgs.modelChart + '"></div>' : ''}</div><div class="charts">${imgs.projectChart ? '<div class="cc"><p>项目使用分布</p><img src="' + imgs.projectChart + '"></div>' : ''}${imgs.toolChart ? '<div class="cc"><p>工具调用排行</p><img src="' + imgs.toolChart + '"></div>' : ''}</div>` : ''}
|
|
180
|
+
<p class="ft">LumenCode · 数据来自本地日志,不上传至任何服务器</p>
|
|
181
|
+
</body></html>`;
|
|
182
|
+
|
|
183
|
+
const win = window.open('', '_blank');
|
|
184
|
+
win.document.write(html);
|
|
185
|
+
win.document.close();
|
|
186
|
+
setTimeout(() => { win.print(); }, 400);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── JSON Export ──
|
|
190
|
+
export function exportJSON(data, period) {
|
|
191
|
+
if (!data) return;
|
|
192
|
+
const json = JSON.stringify({
|
|
193
|
+
period,
|
|
194
|
+
start: data.start,
|
|
195
|
+
end: data.end,
|
|
196
|
+
usageStats: data.usageStats,
|
|
197
|
+
gitStats: data.gitStats ? {
|
|
198
|
+
commits: data.gitStats.commits,
|
|
199
|
+
linesAdded: data.gitStats.linesAdded,
|
|
200
|
+
linesDeleted: data.gitStats.linesDeleted,
|
|
201
|
+
filesChanged: data.gitStats.filesChanged,
|
|
202
|
+
aiContribution: data.gitStats.aiContribution,
|
|
203
|
+
commitTypes: data.gitStats.commitTypes,
|
|
204
|
+
fileHotspots: data.gitStats.fileHotspots,
|
|
205
|
+
} : null,
|
|
206
|
+
costBreakdown: data.costBreakdown || null,
|
|
207
|
+
exportedAt: new Date().toISOString(),
|
|
208
|
+
}, null, 2);
|
|
209
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
210
|
+
const url = URL.createObjectURL(blob);
|
|
211
|
+
const a = document.createElement('a');
|
|
212
|
+
a.href = url;
|
|
213
|
+
a.download = `ccusage-${period}-${data.start}-${data.end}.json`;
|
|
214
|
+
a.click();
|
|
215
|
+
URL.revokeObjectURL(url);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── HTML Export ──
|
|
219
|
+
export function exportHTML(data, period) {
|
|
220
|
+
if (!data) return;
|
|
221
|
+
const { usageStats, gitStats, start, end } = data;
|
|
222
|
+
const periodName = period === 'daily' ? TEXT.DAILY : period === 'weekly' ? TEXT.WEEKLY : TEXT.MONTHLY;
|
|
223
|
+
|
|
224
|
+
const imgs = {};
|
|
225
|
+
for (const key of ['scenarioChart', 'modelChart', 'projectChart', 'toolChart', 'trendChart', 'commitTypeChart']) {
|
|
226
|
+
const ch = getChart(key);
|
|
227
|
+
if (ch) { try { imgs[key] = ch.toBase64Image(); } catch {} }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const projRows = Object.entries(usageStats.projects).sort((a, b) => b[1].requests - a[1].requests).map(([n, d]) => [n, d.requests, d.sessions instanceof Set ? d.sessions.size : (d.sessions || 0)]);
|
|
231
|
+
const modelRows = Object.entries(usageStats.models).sort((a, b) => b[1].count - a[1].count).map(([n, d]) => [n, d.count, fmtShort(d.inputTokens), fmtShort(d.outputTokens), d.cost ? '$' + d.cost.toFixed(2) : '-']);
|
|
232
|
+
const toolRows = Object.entries(usageStats.tools).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([n, c]) => [n, c]);
|
|
233
|
+
const scenarioRows = Object.entries(usageStats.scenarios).sort((a, b) => b[1] - a[1]).map(([n, c]) => [n, c]);
|
|
234
|
+
|
|
235
|
+
const html = `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title>AI 编码助手使用${periodName}</title>
|
|
236
|
+
<style>
|
|
237
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
238
|
+
body{font-family:'Inter',-apple-system,sans-serif;color:#111;padding:32px 40px;max-width:800px;margin:0 auto;font-size:13px;line-height:1.5}
|
|
239
|
+
h1{font-size:20px;margin-bottom:2px;letter-spacing:-0.3px}
|
|
240
|
+
.sub{color:#6b7280;font-size:12px;margin-bottom:20px}
|
|
241
|
+
.summary{display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin-bottom:20px}
|
|
242
|
+
.s{text-align:center;padding:10px 6px;background:#f5f5f5;border-radius:6px}
|
|
243
|
+
.sv{font-size:18px;font-weight:600;letter-spacing:-0.3px}
|
|
244
|
+
.sl{font-size:10px;color:#6b7280;margin-top:2px}
|
|
245
|
+
h2{font-size:13px;margin-top:18px;margin-bottom:6px;padding-bottom:4px;border-bottom:1px solid #e5e7eb}
|
|
246
|
+
table{width:100%;border-collapse:collapse;font-size:12px;margin-bottom:12px}
|
|
247
|
+
th,td{padding:5px 8px;text-align:left;border-bottom:1px solid #e5e7eb}
|
|
248
|
+
th{font-weight:600;background:#f8f9fa}
|
|
249
|
+
.charts{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:16px 0}
|
|
250
|
+
.cc{text-align:center}
|
|
251
|
+
.cc p{font-size:11px;font-weight:600;margin-bottom:2px}
|
|
252
|
+
.cc img{max-width:100%;height:180px;object-fit:contain}
|
|
253
|
+
.ft{text-align:center;color:#9ca3af;font-size:10px;margin-top:24px;padding-top:12px;border-top:1px solid #e5e7eb}
|
|
254
|
+
@media print{body{padding:20px 24px}@page{margin:15mm}}
|
|
255
|
+
</style></head><body>
|
|
256
|
+
<h1>AI 编码助手使用${periodName}</h1>
|
|
257
|
+
<p class="sub">${start}${end !== start ? ' ~ ' + end : ''} · 生成于 ${new Date().toLocaleString('zh-CN')}</p>
|
|
258
|
+
<div class="summary">
|
|
259
|
+
<div class="s"><div class="sv">${fmt(usageStats.sessionCount)}</div><div class="sl">独立会话</div></div>
|
|
260
|
+
<div class="s"><div class="sv">${fmt(usageStats.requestCount)}</div><div class="sl">交互轮次</div></div>
|
|
261
|
+
<div class="s"><div class="sv">${Object.keys(usageStats.projects).length}</div><div class="sl">覆盖项目</div></div>
|
|
262
|
+
<div class="s"><div class="sv">${fmt(usageStats.totalTokens)}</div><div class="sl">Token 消耗</div></div>
|
|
263
|
+
<div class="s"><div class="sv">${usageStats.estimatedCost ? '$' + usageStats.estimatedCost.toFixed(2) : '-'}</div><div class="sl">预估费用</div></div>
|
|
264
|
+
</div>
|
|
265
|
+
${printTable('项目分布', ['项目', '请求数', '会话数'], projRows)}
|
|
266
|
+
${printTable('模型分布', ['模型', '请求数', '输入', '输出', '费用'], modelRows)}
|
|
267
|
+
${printTable('工具使用排行', ['工具', '调用次数'], toolRows)}
|
|
268
|
+
${printTable('场景分布', ['场景', '请求数'], scenarioRows)}
|
|
269
|
+
${data.costBreakdown?.models?.length ? printTable('模型费用', ['模型', '费用', '计费方式', '请求数'], data.costBreakdown.models.map(m => [m.name, '$' + (m.cost || 0).toFixed(2), m.mode === 'actual' ? '实际' : m.mode === 'estimated' ? '估算' : '未知', m.requests])) : ''}
|
|
270
|
+
${gitStats && gitStats.commits > 0 ? printTable('Git 代码产出', ['指标', '数值'], [['提交次数', gitStats.commits], ['新增行数', '+' + fmt(gitStats.linesAdded)], ['删除行数', '-' + fmt(gitStats.linesDeleted)], ['变更文件', gitStats.filesChanged]]) : ''}
|
|
271
|
+
${(imgs.scenarioChart || imgs.modelChart) ? `<h2>图表</h2><div class="charts">${imgs.scenarioChart ? '<div class="cc"><p>工作类型分布</p><img src="' + imgs.scenarioChart + '"></div>' : ''}${imgs.modelChart ? '<div class="cc"><p>模型使用分布</p><img src="' + imgs.modelChart + '"></div>' : ''}</div>` : ''}
|
|
272
|
+
<p class="ft">LumenCode · 数据来自本地日志,不上传至任何服务器</p>
|
|
273
|
+
</body></html>`;
|
|
274
|
+
|
|
275
|
+
const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
|
|
276
|
+
const url = URL.createObjectURL(blob);
|
|
277
|
+
const a = document.createElement('a');
|
|
278
|
+
a.href = url;
|
|
279
|
+
a.download = `ccusage-${period}-${start}-${end}.html`;
|
|
280
|
+
a.click();
|
|
281
|
+
URL.revokeObjectURL(url);
|
|
282
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
@font-face {
|
|
2
|
+
font-family: 'Inter';
|
|
3
|
+
font-style: normal;
|
|
4
|
+
font-weight: 400;
|
|
5
|
+
font-display: swap;
|
|
6
|
+
src: url(./inter-0.woff2) format('truetype');
|
|
7
|
+
}
|
|
8
|
+
@font-face {
|
|
9
|
+
font-family: 'Inter';
|
|
10
|
+
font-style: normal;
|
|
11
|
+
font-weight: 500;
|
|
12
|
+
font-display: swap;
|
|
13
|
+
src: url(./inter-1.woff2) format('truetype');
|
|
14
|
+
}
|
|
15
|
+
@font-face {
|
|
16
|
+
font-family: 'Inter';
|
|
17
|
+
font-style: normal;
|
|
18
|
+
font-weight: 600;
|
|
19
|
+
font-display: swap;
|
|
20
|
+
src: url(./inter-2.woff2) format('truetype');
|
|
21
|
+
}
|
|
22
|
+
@font-face {
|
|
23
|
+
font-family: 'Inter';
|
|
24
|
+
font-style: normal;
|
|
25
|
+
font-weight: 700;
|
|
26
|
+
font-display: swap;
|
|
27
|
+
src: url(./inter-3.woff2) format('truetype');
|
|
28
|
+
}
|