commit-report 1.0.0 → 1.0.2
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 +10 -3
- package/dist/index.js +1818 -447
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/templates/report-scripts/00-advanced-derived.html +462 -0
- package/templates/report-scripts/00-filter-state.html +374 -0
- package/templates/report-scripts/00-report-controls.html +272 -0
- package/templates/report-scripts/01-core.html +255 -0
- package/templates/report-scripts/02-commit-details.html +275 -0
- package/templates/report-scripts/03-basic-charts.html +378 -0
- package/templates/report-scripts/04-trend-charts.html +309 -0
- package/templates/report-scripts/05-tables-team-stability.html +372 -0
- package/templates/report-scripts/06-pressure-churn.html +339 -0
- package/templates/report-scripts/07-collab-debt-ai.html +534 -0
- package/templates/report-scripts/08-engineering.html +200 -0
- package/templates/report-scripts/09-extensions.html +313 -0
- package/templates/report-scripts/10-runtime.html +54 -0
- package/templates/report-sections/01-overview.html +342 -0
- package/templates/report-sections/02-advanced.html +406 -0
- package/templates/report.html +40 -1998
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 高级分析 - 工作压力
|
|
3
|
+
// ============================================================
|
|
4
|
+
function renderWorkPressure() {
|
|
5
|
+
const workPressure = stats.workPressure;
|
|
6
|
+
|
|
7
|
+
// 检查数据是否存在
|
|
8
|
+
if (!workPressure) {
|
|
9
|
+
document.getElementById('work-pressure').innerHTML =
|
|
10
|
+
renderSingleRepoOnlyEmptyState('工作压力分析');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { pressureScore, lateNightCommits, earlyMorningCommits,
|
|
15
|
+
weekendCommits, holidayCommits, offHoursRate } = workPressure;
|
|
16
|
+
|
|
17
|
+
// 1. 渲染压力评分仪表盘
|
|
18
|
+
renderPressureGauge(pressureScore, offHoursRate);
|
|
19
|
+
|
|
20
|
+
// 2. 渲染非工作时间分布
|
|
21
|
+
renderOffHoursBreakdown(lateNightCommits, earlyMorningCommits, weekendCommits, holidayCommits);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 压力评分仪表盘(半圆仪表)
|
|
25
|
+
function renderPressureGauge(pressureScore, offHoursRate) {
|
|
26
|
+
const container = d3.select('#pressure-gauge');
|
|
27
|
+
container.html(''); // 清空
|
|
28
|
+
|
|
29
|
+
// 压力等级颜色映射
|
|
30
|
+
const getColor = (score) => {
|
|
31
|
+
if (score < 30) return '#10b981'; // 绿色 - 低压力
|
|
32
|
+
if (score < 60) return '#f59e0b'; // 橙色 - 中等压力
|
|
33
|
+
return '#ef4444'; // 红色 - 高压力
|
|
34
|
+
};
|
|
35
|
+
const color = getColor(pressureScore);
|
|
36
|
+
|
|
37
|
+
// SVG 尺寸
|
|
38
|
+
const width = 240;
|
|
39
|
+
const height = 140;
|
|
40
|
+
const radius = 90;
|
|
41
|
+
|
|
42
|
+
const svg = container.append('svg')
|
|
43
|
+
.attr('width', width)
|
|
44
|
+
.attr('height', height)
|
|
45
|
+
.append('g')
|
|
46
|
+
.attr('transform', `translate(${width/2}, ${height - 10})`);
|
|
47
|
+
|
|
48
|
+
// 背景半圆(灰色)
|
|
49
|
+
const bgArc = d3.arc()
|
|
50
|
+
.innerRadius(radius - 15)
|
|
51
|
+
.outerRadius(radius)
|
|
52
|
+
.startAngle(-Math.PI / 2)
|
|
53
|
+
.endAngle(Math.PI / 2);
|
|
54
|
+
|
|
55
|
+
svg.append('path')
|
|
56
|
+
.attr('d', bgArc)
|
|
57
|
+
.attr('fill', '#e2e8f0')
|
|
58
|
+
.attr('class', 'dark:fill-slate-600');
|
|
59
|
+
|
|
60
|
+
// 前景半圆(根据压力评分)
|
|
61
|
+
const targetAngle = -Math.PI / 2 + (Math.PI * pressureScore / 100);
|
|
62
|
+
|
|
63
|
+
const foregroundArc = d3.arc()
|
|
64
|
+
.innerRadius(radius - 15)
|
|
65
|
+
.outerRadius(radius)
|
|
66
|
+
.startAngle(-Math.PI / 2)
|
|
67
|
+
.endAngle(targetAngle);
|
|
68
|
+
|
|
69
|
+
svg.append('path')
|
|
70
|
+
.attr('d', foregroundArc)
|
|
71
|
+
.attr('fill', color);
|
|
72
|
+
|
|
73
|
+
// 中心文本 - 压力评分
|
|
74
|
+
svg.append('text')
|
|
75
|
+
.attr('y', -25)
|
|
76
|
+
.attr('text-anchor', 'middle')
|
|
77
|
+
.attr('class', 'text-5xl font-bold')
|
|
78
|
+
.attr('fill', color)
|
|
79
|
+
.text(pressureScore.toFixed(0));
|
|
80
|
+
|
|
81
|
+
// 压力等级标签
|
|
82
|
+
const levelLabels = { low: '低压力', medium: '中等压力', high: '高压力' };
|
|
83
|
+
const level = pressureScore < 30 ? 'low' : pressureScore < 60 ? 'medium' : 'high';
|
|
84
|
+
svg.append('text')
|
|
85
|
+
.attr('y', 0)
|
|
86
|
+
.attr('text-anchor', 'middle')
|
|
87
|
+
.attr('class', 'text-sm font-medium fill-slate-500 dark:fill-slate-400')
|
|
88
|
+
.text(levelLabels[level]);
|
|
89
|
+
|
|
90
|
+
// 非工作时间占比
|
|
91
|
+
d3.select('#pressure-gauge')
|
|
92
|
+
.append('div')
|
|
93
|
+
.attr('class', 'text-center mt-2 text-xs text-slate-400')
|
|
94
|
+
.text(`非工作时间提交: ${(offHoursRate * 100).toFixed(1)}%`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 非工作时间分布
|
|
98
|
+
function renderOffHoursBreakdown(lateNightCommits, earlyMorningCommits, weekendCommits, holidayCommits) {
|
|
99
|
+
const container = document.getElementById('off-hours-breakdown');
|
|
100
|
+
|
|
101
|
+
const total = stats.totalCommits || 1;
|
|
102
|
+
const normalCommits = total - lateNightCommits - earlyMorningCommits - weekendCommits - holidayCommits.reduce((sum, h) => sum + h.commits, 0);
|
|
103
|
+
|
|
104
|
+
const data = [
|
|
105
|
+
{ label: '正常时间', count: normalCommits, color: '#10b981', icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z' },
|
|
106
|
+
{ label: '深夜 (23-02)', count: lateNightCommits, color: '#8b5cf6', icon: 'M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z' },
|
|
107
|
+
{ label: '凌晨 (02-06)', count: earlyMorningCommits, color: '#ef4444', icon: 'M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z' },
|
|
108
|
+
{ label: '周末', count: weekendCommits, color: '#f59e0b', icon: 'M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z' },
|
|
109
|
+
{ label: '假期', count: holidayCommits.reduce((sum, h) => sum + h.commits, 0), color: '#ec4899', icon: 'M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z' }
|
|
110
|
+
].filter(d => d.count > 0);
|
|
111
|
+
|
|
112
|
+
let html = '<div class="space-y-3">';
|
|
113
|
+
|
|
114
|
+
data.forEach(item => {
|
|
115
|
+
const percentage = ((item.count / total) * 100).toFixed(1);
|
|
116
|
+
html += `
|
|
117
|
+
<div class="flex items-center justify-between p-3 bg-white dark:bg-slate-800 rounded-lg border border-slate-100 dark:border-slate-700/50">
|
|
118
|
+
<div class="flex items-center gap-3">
|
|
119
|
+
<div class="p-2 rounded-lg bg-slate-100 dark:bg-slate-700 text-slate-500 dark:text-slate-400">
|
|
120
|
+
<svg class="w-5 h-5" style="color: ${item.color}" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
121
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${item.icon}" />
|
|
122
|
+
</svg>
|
|
123
|
+
</div>
|
|
124
|
+
<div>
|
|
125
|
+
<div class="text-sm font-medium text-slate-700 dark:text-slate-200">${item.label}</div>
|
|
126
|
+
<div class="text-xs text-slate-500 dark:text-slate-400">${item.count} 次提交</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<div class="text-right">
|
|
130
|
+
<div class="text-lg font-bold" style="color: ${item.color}">${percentage}%</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
`;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
html += '</div>';
|
|
137
|
+
|
|
138
|
+
// 假期提交列表
|
|
139
|
+
if (holidayCommits && holidayCommits.length > 0) {
|
|
140
|
+
html += '<div class="mt-6"><h4 class="text-sm font-semibold mb-2 text-slate-600 dark:text-slate-300">假期提交记录</h4></div>';
|
|
141
|
+
html += '<div class="space-y-2 max-h-[150px] overflow-y-auto scrollbar-thin">';
|
|
142
|
+
|
|
143
|
+
holidayCommits.forEach((holiday) => {
|
|
144
|
+
html += `
|
|
145
|
+
<div class="flex items-center justify-between text-sm py-2 px-3 bg-rose-50 dark:bg-rose-900/10 rounded-lg border border-rose-100 dark:border-rose-900/20">
|
|
146
|
+
<span class="text-rose-700 dark:text-rose-300 font-medium">${escapeHtml(holiday.holidayName)}</span>
|
|
147
|
+
<div class="flex items-center gap-3">
|
|
148
|
+
<span class="text-xs text-slate-500">${holiday.date}</span>
|
|
149
|
+
<span class="bg-rose-200 dark:bg-rose-800 text-rose-800 dark:text-rose-100 px-2 py-0.5 rounded text-xs">${holiday.commits}</span>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
`;
|
|
153
|
+
});
|
|
154
|
+
html += '</div>';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
container.innerHTML = html;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============================================================
|
|
161
|
+
// 高级分析 - 贡献者流失
|
|
162
|
+
// ============================================================
|
|
163
|
+
function renderContributorChurn() {
|
|
164
|
+
const contributorChurn = stats.contributorChurn;
|
|
165
|
+
|
|
166
|
+
// 检查数据是否存在
|
|
167
|
+
if (!contributorChurn) {
|
|
168
|
+
document.getElementById('contributor-churn').innerHTML =
|
|
169
|
+
renderSingleRepoOnlyEmptyState('贡献者流失分析');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const { active, occasional, dormant, lost, newJoiners,
|
|
174
|
+
churnRate, retentionRate, growthRate } = contributorChurn;
|
|
175
|
+
|
|
176
|
+
// 1. 渲染关键指标卡片
|
|
177
|
+
renderChurnMetrics(churnRate, retentionRate, growthRate, newJoiners.length);
|
|
178
|
+
|
|
179
|
+
// 2. 渲染漏斗图
|
|
180
|
+
renderChurnFunnel(active.length, occasional.length, dormant.length, lost.length);
|
|
181
|
+
|
|
182
|
+
// 3. 渲染作者状态列表
|
|
183
|
+
renderAuthorStatusList(active, occasional, dormant, lost);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 关键指标卡片
|
|
187
|
+
function renderChurnMetrics(churnRate, retentionRate, growthRate, newJoinersCount) {
|
|
188
|
+
const container = document.getElementById('churn-metrics');
|
|
189
|
+
|
|
190
|
+
let html = '<div class="grid grid-cols-2 md:grid-cols-4 gap-4">';
|
|
191
|
+
|
|
192
|
+
// 辅助函数:生成卡片 HTML
|
|
193
|
+
const createCard = (label, value, color, iconPath) => `
|
|
194
|
+
<div class="bg-slate-50 dark:bg-slate-700/50 rounded-xl p-5 flex flex-col justify-between h-full border border-slate-100 dark:border-slate-700/50">
|
|
195
|
+
<div class="flex items-center justify-between mb-2">
|
|
196
|
+
<div class="text-sm text-slate-500 dark:text-slate-400 font-medium">${label}</div>
|
|
197
|
+
<div class="${color} p-1.5 bg-white dark:bg-slate-800 rounded-lg shadow-sm">
|
|
198
|
+
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
199
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${iconPath}" />
|
|
200
|
+
</svg>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
<div class="text-2xl font-bold ${color}">${value}</div>
|
|
204
|
+
</div>
|
|
205
|
+
`;
|
|
206
|
+
|
|
207
|
+
// 流失率
|
|
208
|
+
const churnColor = churnRate > 0.3 ? 'text-rose-500' : churnRate > 0.15 ? 'text-amber-500' : 'text-emerald-500';
|
|
209
|
+
html += createCard(
|
|
210
|
+
'流失率',
|
|
211
|
+
(churnRate * 100).toFixed(1) + '%',
|
|
212
|
+
churnColor,
|
|
213
|
+
'M13 7a4 4 0 11-8 0 4 4 0 018 0zM9 14a6 6 0 00-6 6v1h12v-1a6 6 0 00-6-6zM21 12h-6'
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// 留存率
|
|
217
|
+
const retentionColor = retentionRate > 0.7 ? 'text-emerald-500' : retentionRate > 0.4 ? 'text-amber-500' : 'text-rose-500';
|
|
218
|
+
html += createCard(
|
|
219
|
+
'留存率',
|
|
220
|
+
(retentionRate * 100).toFixed(1) + '%',
|
|
221
|
+
retentionColor,
|
|
222
|
+
'M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z'
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// 增长率
|
|
226
|
+
const growthColor = growthRate > 0.2 ? 'text-emerald-500' : growthRate > 0 ? 'text-amber-500' : 'text-slate-500';
|
|
227
|
+
html += createCard(
|
|
228
|
+
'增长率',
|
|
229
|
+
(growthRate * 100).toFixed(1) + '%',
|
|
230
|
+
growthColor,
|
|
231
|
+
'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6'
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// 新加入者数
|
|
235
|
+
html += createCard(
|
|
236
|
+
'新加入者',
|
|
237
|
+
newJoinersCount,
|
|
238
|
+
'text-primary-500',
|
|
239
|
+
'M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z'
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
html += '</div>';
|
|
243
|
+
container.innerHTML = html;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 漏斗图(4级分类)
|
|
247
|
+
function renderChurnFunnel(activeCount, occasionalCount, dormantCount, lostCount) {
|
|
248
|
+
const container = document.getElementById('churn-funnel');
|
|
249
|
+
const total = activeCount + occasionalCount + dormantCount + lostCount;
|
|
250
|
+
|
|
251
|
+
if (total === 0) {
|
|
252
|
+
container.innerHTML = '<p class="text-sm text-slate-500 dark:text-slate-400 mt-4">无贡献者数据</p>';
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const levels = [
|
|
257
|
+
{ label: '活跃 (Active)', count: activeCount, color: '#10b981', desc: '<30天' },
|
|
258
|
+
{ label: '偶尔 (Occasional)', count: occasionalCount, color: '#f59e0b', desc: '30-90天' },
|
|
259
|
+
{ label: '休眠 (Dormant)', count: dormantCount, color: '#fb923c', desc: '90-180天' },
|
|
260
|
+
{ label: '流失 (Lost)', count: lostCount, color: '#ef4444', desc: '>180天' }
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
let html = '<div class="space-y-4 py-4">';
|
|
264
|
+
|
|
265
|
+
levels.forEach((level, index) => {
|
|
266
|
+
const percentage = ((level.count / total) * 100).toFixed(1);
|
|
267
|
+
const width = Math.max(30, 100 - index * 15); // 漏斗宽度递减
|
|
268
|
+
|
|
269
|
+
html += `<div class="relative flex justify-center">`;
|
|
270
|
+
html += ` <div class="absolute inset-0 flex items-center justify-center opacity-10 pointer-events-none">`;
|
|
271
|
+
html += ` <div style="width: ${width}%; background-color: ${level.color};" class="h-full rounded-lg"></div>`;
|
|
272
|
+
html += ` </div>`;
|
|
273
|
+
html += ` <div class="w-full max-w-md flex items-center justify-between px-4 py-2 relative z-10">`;
|
|
274
|
+
html += ` <div class="flex items-center gap-3">`;
|
|
275
|
+
html += ` <div class="w-2.5 h-2.5 rounded-full" style="background-color: ${level.color}"></div>`;
|
|
276
|
+
html += ` <div>`;
|
|
277
|
+
html += ` <div class="text-sm font-medium text-slate-700 dark:text-slate-200">${level.label}</div>`;
|
|
278
|
+
html += ` <div class="text-[10px] text-slate-400">${level.desc}</div>`;
|
|
279
|
+
html += ` </div>`;
|
|
280
|
+
html += ` </div>`;
|
|
281
|
+
html += ` <div class="text-right">`;
|
|
282
|
+
html += ` <div class="text-lg font-bold" style="color: ${level.color}">${level.count}</div>`;
|
|
283
|
+
html += ` <div class="text-xs text-slate-400">${percentage}%</div>`;
|
|
284
|
+
html += ` </div>`;
|
|
285
|
+
html += ` </div>`;
|
|
286
|
+
html += `</div>`;
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
html += '</div>';
|
|
290
|
+
container.innerHTML = html;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 作者状态列表
|
|
294
|
+
function renderAuthorStatusList(active, occasional, dormant, lost) {
|
|
295
|
+
const container = document.getElementById('author-status-list');
|
|
296
|
+
|
|
297
|
+
const allAuthors = [
|
|
298
|
+
...active.map(a => ({ ...a, status: '活跃', statusColor: 'emerald', statusBg: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300' })),
|
|
299
|
+
...occasional.map(a => ({ ...a, status: '偶尔', statusColor: 'amber', statusBg: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300' })),
|
|
300
|
+
...dormant.map(a => ({ ...a, status: '休眠', statusColor: 'orange', statusBg: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300' })),
|
|
301
|
+
...lost.map(a => ({ ...a, status: '流失', statusColor: 'rose', statusBg: 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-300' }))
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
if (allAuthors.length === 0) {
|
|
305
|
+
container.innerHTML = '<p class="text-sm text-slate-500 dark:text-slate-400">无作者数据</p>';
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 按最后提交日期降序排序
|
|
310
|
+
allAuthors.sort((a, b) => new Date(b.lastCommitDate).getTime() - new Date(a.lastCommitDate).getTime());
|
|
311
|
+
|
|
312
|
+
let html = '<div class="overflow-x-auto max-h-[350px] overflow-y-auto scrollbar-thin">';
|
|
313
|
+
html += '<table class="w-full text-sm">';
|
|
314
|
+
html += '<thead class="sticky top-0 bg-slate-50 dark:bg-slate-700/50 backdrop-blur-sm z-10">';
|
|
315
|
+
html += '<tr class="text-left border-b border-slate-200 dark:border-slate-600">';
|
|
316
|
+
html += '<th class="py-2 pl-2 text-slate-500 dark:text-slate-400 font-medium">姓名</th>';
|
|
317
|
+
html += '<th class="py-2 text-slate-500 dark:text-slate-400 font-medium">状态</th>';
|
|
318
|
+
html += '<th class="py-2 text-slate-500 dark:text-slate-400 font-medium text-right">上次活跃</th>';
|
|
319
|
+
html += '</tr>';
|
|
320
|
+
html += '</thead>';
|
|
321
|
+
html += '<tbody class="divide-y divide-slate-100 dark:divide-slate-700/50">';
|
|
322
|
+
|
|
323
|
+
allAuthors.forEach((author) => {
|
|
324
|
+
const days = author.daysSinceLastCommit;
|
|
325
|
+
const daysText = days === 0 ? '今天' : `${days}天前`;
|
|
326
|
+
|
|
327
|
+
html += `<tr class="hover:bg-slate-100 dark:hover:bg-slate-800/50 transition-colors">`;
|
|
328
|
+
html += `<td class="py-2.5 pl-2 font-medium text-slate-700 dark:text-slate-300 truncate max-w-[100px]" title="${escapeHtml(author.name)}">${escapeHtml(author.name)}</td>`;
|
|
329
|
+
html += `<td class="py-2.5"><span class="px-2 py-0.5 rounded text-xs font-medium ${author.statusBg}">${author.status}</span></td>`;
|
|
330
|
+
html += `<td class="py-2.5 text-right text-slate-500 dark:text-slate-400 text-xs">${daysText}</td>`;
|
|
331
|
+
html += '</tr>';
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
html += '</tbody>';
|
|
335
|
+
html += '</table>';
|
|
336
|
+
html += '</div>';
|
|
337
|
+
|
|
338
|
+
container.innerHTML = html;
|
|
339
|
+
}
|