原来这条折线只有横轴日期,纵轴没有任何参照:读者看到峰谷不知道自己 在看 3 次还是 300 次的起伏。现在补上三条刻度(上限 / 一半 / 0)加对应 的横向虚线。 纵轴上限取 niceMax(峰值),不是峰值本身。峰值正好压在顶格里时,读者 分不清那到底是确切的数还是被裁掉的数;向上取整到 1/2/1.5/3/4/6/8×10^n 之后顶部留白固定,三条刻度又都是整数和好读的数(1.5 倍只在量级≥100 时参与,否则算出来是 7.5 这种半格)。上限恒为偶数,中间刻度必然是整数。 刻度文字不放进 SVG:viewBox 用 preserveAspectRatio="none" 横向拉伸填满 卡片,里面的文字会跟着被拉扁,窄屏尤其明显。改成左边一列 HTML 标签 + 横向虚线仍在 SVG 里(描边锁 non-scaling-stroke)。列宽靠一份看不见的 「最宽刻度」占位撑开,不写死 em。 同时把 Polyline/AreaPath 收敛成单个 NewLineChart:坐标、面积、刻度必须 共用同一个上限,分两次算就会各按自己看到的峰值归一化,出现刻度和线错开 的图。面积路径改为沿基线闭合,不再漏进轴标签的留白区。 新增 chart_test.go 锁住三条不变式:中间刻度必为整数、上限不离开峰值一倍、 刻度/折线/面积共用上限;另有 0..2000 的穷举检查。
634 lines
20 KiB
HTML
634 lines
20 KiB
HTML
{{template "base/header" .}}
|
||
<style>
|
||
/* 全站数据页:只用令牌与 CSS 画图(折线 SVG、柱状/分布条用百分比高度、环形用 pathLength、
|
||
热力图用 grid + 主色透明度)。不引图表库,页面在禁用 JS 时也完整。 */
|
||
.tb-dash {
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.tb-dash-head {
|
||
padding: 4px 2px 14px;
|
||
}
|
||
.tb-dash-head h2 {
|
||
margin: 0;
|
||
font-size: var(--tb-fs-xl);
|
||
font-weight: 600;
|
||
color: var(--tb-text);
|
||
}
|
||
.tb-dash-head p {
|
||
margin: 8px 0 0;
|
||
font-size: var(--tb-fs-md);
|
||
line-height: 1.7;
|
||
color: var(--tb-text-muted);
|
||
}
|
||
|
||
/* 今日条 */
|
||
.tb-today {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: baseline;
|
||
gap: 6px 18px;
|
||
padding: 12px 16px;
|
||
margin-bottom: 12px;
|
||
background: var(--tb-primary-weak);
|
||
border-radius: var(--tb-radius-sm);
|
||
font-size: var(--tb-fs-md);
|
||
color: var(--tb-text-secondary);
|
||
}
|
||
.tb-today b {
|
||
font-size: var(--tb-fs-lg);
|
||
color: var(--tb-primary);
|
||
font-weight: 700;
|
||
}
|
||
.tb-today .tb-today-tag {
|
||
font-size: var(--tb-fs-xs);
|
||
color: var(--tb-text-muted);
|
||
}
|
||
|
||
/* 卡片 */
|
||
.tb-card {
|
||
background: var(--tb-surface);
|
||
border: 1px solid var(--tb-border);
|
||
border-radius: var(--tb-radius);
|
||
padding: 18px 18px 20px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.tb-card-title {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: baseline;
|
||
gap: 8px;
|
||
margin: 0 0 4px;
|
||
font-size: var(--tb-fs-lg);
|
||
font-weight: 600;
|
||
color: var(--tb-text);
|
||
}
|
||
.tb-card-note {
|
||
margin: 0 0 16px;
|
||
font-size: var(--tb-fs-xs);
|
||
line-height: 1.6;
|
||
color: var(--tb-text-muted);
|
||
}
|
||
|
||
/* KPI 网格 */
|
||
.tb-kpis {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
@media (min-width: 560px) {
|
||
.tb-kpis { grid-template-columns: repeat(3, 1fr); }
|
||
}
|
||
.tb-kpi {
|
||
padding: 14px 12px;
|
||
background: var(--tb-surface);
|
||
border: 1px solid var(--tb-border);
|
||
border-radius: var(--tb-radius-sm);
|
||
}
|
||
.tb-kpi-num {
|
||
display: block;
|
||
font-size: 1.375rem;
|
||
line-height: 1.2;
|
||
font-weight: 700;
|
||
color: var(--tb-text);
|
||
}
|
||
.tb-kpi-num.tb-kpi-accent { color: var(--tb-primary); }
|
||
.tb-kpi-lbl {
|
||
display: block;
|
||
margin-top: 4px;
|
||
font-size: var(--tb-fs-xs);
|
||
color: var(--tb-text-muted);
|
||
}
|
||
|
||
/* 洞察 */
|
||
.tb-insights {
|
||
margin: 0;
|
||
padding: 0;
|
||
list-style: none;
|
||
}
|
||
.tb-insights li {
|
||
position: relative;
|
||
padding: 6px 0 6px 18px;
|
||
font-size: var(--tb-fs-md);
|
||
line-height: 1.7;
|
||
color: var(--tb-text-secondary);
|
||
}
|
||
.tb-insights li::before {
|
||
content: "";
|
||
position: absolute;
|
||
left: 4px;
|
||
top: 14px;
|
||
width: 5px;
|
||
height: 5px;
|
||
border-radius: 50%;
|
||
background: var(--tb-primary);
|
||
opacity: .55;
|
||
}
|
||
|
||
/* 折线图 */
|
||
/* 左边一列纵轴刻度 + 右边画布:刻度文字不能放进 SVG ——
|
||
viewBox 用了 preserveAspectRatio="none" 横向拉伸填满卡片,
|
||
放在里面的文字会跟着被拉扁(窄屏上尤其明显)。 */
|
||
.tb-line-wrap {
|
||
display: grid;
|
||
grid-template-columns: auto minmax(0, 1fr);
|
||
column-gap: 6px;
|
||
margin-top: 4px;
|
||
}
|
||
.tb-line-y {
|
||
position: relative;
|
||
height: 148px;
|
||
font-size: var(--tb-fs-xs);
|
||
font-variant-numeric: tabular-nums;
|
||
color: var(--tb-text-muted);
|
||
}
|
||
/* 刻度全是绝对定位的,撑不开这一列;用一份看不见的「最宽刻度」占位,
|
||
让列宽自己贴合数字长度(上限那个数位数最多),不用写死 em。 */
|
||
.tb-line-ruler {
|
||
display: block;
|
||
visibility: hidden;
|
||
}
|
||
.tb-line-tick {
|
||
position: absolute;
|
||
right: 0;
|
||
white-space: nowrap;
|
||
/* 刻度值以刻度线为中心对齐,而不是以格子顶部对齐 */
|
||
transform: translateY(-50%);
|
||
}
|
||
.tb-line {
|
||
display: block;
|
||
width: 100%;
|
||
height: 148px;
|
||
/* 折线只占上边 148px,刻度标签可能略高于画布,别被裁掉 */
|
||
overflow: visible;
|
||
}
|
||
.tb-line .tb-grid {
|
||
stroke: var(--tb-border);
|
||
stroke-width: 1;
|
||
stroke-dasharray: 3 5;
|
||
/* 同折线:viewBox 被拉伸,描边宽度必须锁像素,否则横向会被算成几倍粗 */
|
||
vector-effect: non-scaling-stroke;
|
||
}
|
||
.tb-line .tb-grid.is-base {
|
||
stroke-dasharray: none;
|
||
}
|
||
.tb-line .tb-line-area {
|
||
/* 比 --tb-primary-weak(0.10)略深一档:折线下方的面积要能看出「有」,但不能抢走线条本身 */
|
||
fill: var(--tb-primary);
|
||
fill-opacity: .14;
|
||
stroke: none;
|
||
}
|
||
.tb-line .tb-line-path {
|
||
fill: none;
|
||
stroke: var(--tb-primary);
|
||
stroke-width: 2;
|
||
stroke-linejoin: round;
|
||
stroke-linecap: round;
|
||
/* viewBox 与显示宽度不一致,必须锁定描边像素,否则窄屏上线会被压细或拉粗 */
|
||
vector-effect: non-scaling-stroke;
|
||
}
|
||
.tb-axis {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-top: 6px;
|
||
font-size: var(--tb-fs-xs);
|
||
color: var(--tb-text-muted);
|
||
}
|
||
|
||
/* 月度柱状 */
|
||
.tb-bars {
|
||
display: grid;
|
||
grid-template-columns: repeat(12, 1fr);
|
||
gap: 5px;
|
||
align-items: end;
|
||
}
|
||
.tb-bar-col {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
height: 100%;
|
||
}
|
||
.tb-bar-track {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
width: 100%;
|
||
height: 108px;
|
||
}
|
||
.tb-bar-track i {
|
||
display: block;
|
||
width: 100%;
|
||
min-height: 2px;
|
||
border-radius: 3px 3px 0 0;
|
||
background: var(--tb-primary);
|
||
opacity: .82;
|
||
}
|
||
.tb-bar-col:hover .tb-bar-track i { opacity: 1; }
|
||
.tb-bar-lbl {
|
||
margin-top: 6px;
|
||
font-size: 0.6875rem;
|
||
color: var(--tb-text-muted);
|
||
}
|
||
.tb-bar-col.is-current .tb-bar-lbl { color: var(--tb-primary); font-weight: 600; }
|
||
|
||
/* 构成环 */
|
||
.tb-donuts {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 10px;
|
||
}
|
||
.tb-donut {
|
||
text-align: center;
|
||
}
|
||
.tb-donut-ring {
|
||
position: relative;
|
||
width: 76px;
|
||
margin: 0 auto;
|
||
}
|
||
.tb-donut-ring svg {
|
||
display: block;
|
||
width: 100%;
|
||
height: auto;
|
||
}
|
||
.tb-donut-ring .tb-ring-track {
|
||
fill: none;
|
||
stroke: var(--tb-surface-2);
|
||
stroke-width: 6;
|
||
}
|
||
.tb-donut-ring .tb-ring-val {
|
||
fill: none;
|
||
stroke: var(--tb-primary);
|
||
stroke-width: 6;
|
||
stroke-linecap: round;
|
||
}
|
||
.tb-donut-pct {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: var(--tb-fs-md);
|
||
font-weight: 700;
|
||
color: var(--tb-text);
|
||
}
|
||
.tb-donut-cap {
|
||
margin-top: 8px;
|
||
font-size: var(--tb-fs-xs);
|
||
line-height: 1.5;
|
||
color: var(--tb-text-muted);
|
||
}
|
||
.tb-donut-cap b {
|
||
display: block;
|
||
margin-top: 2px;
|
||
color: var(--tb-text-secondary);
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 热力图 */
|
||
.tb-heat {
|
||
/* 24 列在 750px 容器里会把方块撑到 26px 见方,整块图比上面的折线还高。
|
||
限宽后每格约 20px,既看得清又不至于把页面拉得太长。 */
|
||
max-width: 620px;
|
||
margin: 0 auto;
|
||
}
|
||
.tb-heat-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 3px;
|
||
}
|
||
.tb-heat-lbl {
|
||
flex: 0 0 26px;
|
||
font-size: 0.6875rem;
|
||
color: var(--tb-text-muted);
|
||
text-align: center;
|
||
}
|
||
.tb-heat-cells {
|
||
display: grid;
|
||
grid-template-columns: repeat(24, 1fr);
|
||
gap: 2px;
|
||
flex: 1 1 auto;
|
||
}
|
||
.tb-heat-cells i {
|
||
display: block;
|
||
aspect-ratio: 1 / 1;
|
||
border-radius: 2px;
|
||
background: var(--tb-surface-2);
|
||
}
|
||
.tb-heat-cells i.lv1 { background: var(--tb-primary); opacity: .18; }
|
||
.tb-heat-cells i.lv2 { background: var(--tb-primary); opacity: .38; }
|
||
.tb-heat-cells i.lv3 { background: var(--tb-primary); opacity: .62; }
|
||
.tb-heat-cells i.lv4 { background: var(--tb-primary); opacity: 1; }
|
||
.tb-heat-hours {
|
||
display: grid;
|
||
grid-template-columns: repeat(24, 1fr);
|
||
gap: 2px;
|
||
margin: 6px 0 0 34px;
|
||
font-size: 0.625rem;
|
||
color: var(--tb-text-muted);
|
||
}
|
||
.tb-heat-hours span { text-align: center; }
|
||
.tb-heat-legend {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
gap: 4px;
|
||
margin-top: 10px;
|
||
font-size: var(--tb-fs-xs);
|
||
color: var(--tb-text-muted);
|
||
}
|
||
.tb-heat-legend i {
|
||
display: block;
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 2px;
|
||
background: var(--tb-surface-2);
|
||
}
|
||
.tb-heat-legend i.lv1 { background: var(--tb-primary); opacity: .18; }
|
||
.tb-heat-legend i.lv2 { background: var(--tb-primary); opacity: .38; }
|
||
.tb-heat-legend i.lv3 { background: var(--tb-primary); opacity: .62; }
|
||
.tb-heat-legend i.lv4 { background: var(--tb-primary); opacity: 1; }
|
||
|
||
/* 分布条 */
|
||
.tb-dist + .tb-dist { margin-top: 20px; }
|
||
.tb-dist-title {
|
||
margin: 0 0 10px;
|
||
font-size: var(--tb-fs-md);
|
||
font-weight: 600;
|
||
color: var(--tb-text-secondary);
|
||
}
|
||
.tb-dist-row {
|
||
/* 标签列用 em 而不是 px:英文桶名("100–199 chars")比中文长一截,
|
||
写死 84px 会让它折成两行,把整组分布条的高度带偏。 */
|
||
display: grid;
|
||
grid-template-columns: 7.5em 1fr 3.6em;
|
||
gap: 10px;
|
||
align-items: center;
|
||
margin-bottom: 6px;
|
||
font-size: var(--tb-fs-xs);
|
||
color: var(--tb-text-muted);
|
||
}
|
||
.tb-dist-track {
|
||
height: 8px;
|
||
border-radius: 4px;
|
||
background: var(--tb-surface-2);
|
||
overflow: hidden;
|
||
}
|
||
.tb-dist-track i {
|
||
display: block;
|
||
height: 100%;
|
||
min-width: 2px;
|
||
border-radius: 4px;
|
||
background: var(--tb-primary);
|
||
opacity: .78;
|
||
}
|
||
.tb-dist-num { text-align: right; }
|
||
|
||
.tb-dash-foot {
|
||
margin: 4px 2px 0;
|
||
font-size: var(--tb-fs-xs);
|
||
line-height: 1.8;
|
||
color: var(--tb-text-muted);
|
||
}
|
||
/* 四条时间各自成行、标签左列对齐:挤在「A:x · B:y」一行里,两组日期
|
||
靠一个间隔号分开,扫一眼分不清哪个是基准日哪个是最后提问。 */
|
||
.tb-foot-times {
|
||
display: grid;
|
||
grid-template-columns: max-content minmax(0, 1fr);
|
||
column-gap: 8px;
|
||
row-gap: 2px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.tb-foot-note {
|
||
margin: 0;
|
||
}
|
||
.tb-dash-empty {
|
||
padding: 40px 16px;
|
||
text-align: center;
|
||
font-size: var(--tb-fs-md);
|
||
color: var(--tb-text-muted);
|
||
}
|
||
</style>
|
||
|
||
{{ with .Overview }}
|
||
{{ $ov := . }}
|
||
<div class="tb-dash">
|
||
|
||
<div class="tb-dash-head">
|
||
<h2>{{t $.LangCode "stats.title"}}</h2>
|
||
<p>{{t $.LangCode "stats.lead"}}</p>
|
||
</div>
|
||
|
||
<!-- 今日实时:不参与「每天一次」的缓存,每次打开都是当下数字 -->
|
||
<div class="tb-today">
|
||
<span>{{t $.LangCode "stats.today.questions"}} <b>{{NumInt .TodayQuestions}}</b></span>
|
||
<span>{{t $.LangCode "stats.today.answers"}} <b>{{NumInt .TodayAnswers}}</b></span>
|
||
<span class="tb-today-tag">{{t $.LangCode "stats.today.tag"}}</span>
|
||
</div>
|
||
|
||
<!-- 累计概览 -->
|
||
<div class="tb-kpis">
|
||
<div class="tb-kpi">
|
||
<span class="tb-kpi-num">{{NumInt .RegisteredUsers}}</span>
|
||
<span class="tb-kpi-lbl">{{t $.LangCode "stats.kpi.users"}}</span>
|
||
</div>
|
||
<div class="tb-kpi">
|
||
<span class="tb-kpi-num">{{NumInt .TotalQuestions}}</span>
|
||
<span class="tb-kpi-lbl">{{t $.LangCode "stats.kpi.questions"}}</span>
|
||
</div>
|
||
<div class="tb-kpi">
|
||
<span class="tb-kpi-num">{{NumInt .Answered}}</span>
|
||
<span class="tb-kpi-lbl">{{t $.LangCode "stats.kpi.answered"}}</span>
|
||
</div>
|
||
<div class="tb-kpi">
|
||
<span class="tb-kpi-num tb-kpi-accent">{{PctText .Answered .TotalQuestions}}%</span>
|
||
<span class="tb-kpi-lbl">{{t $.LangCode "stats.kpi.answerRate"}}</span>
|
||
</div>
|
||
<div class="tb-kpi">
|
||
<span class="tb-kpi-num">{{NumInt .ActiveBoxes}}</span>
|
||
<span class="tb-kpi-lbl">{{t $.LangCode "stats.kpi.activeBoxes"}}</span>
|
||
</div>
|
||
<div class="tb-kpi">
|
||
<span class="tb-kpi-num">{{NumInt .TotalMessages}}</span>
|
||
<span class="tb-kpi-lbl">{{t $.LangCode "stats.kpi.messages"}}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文字结论 -->
|
||
{{ if .Insights }}
|
||
<div class="tb-card">
|
||
<h3 class="tb-card-title">{{t $.LangCode "stats.insight.title"}}</h3>
|
||
<p class="tb-card-note">{{t $.LangCode "stats.insight.note"}}</p>
|
||
<ul class="tb-insights">
|
||
{{ range .Insights }}<li>{{ . }}</li>{{ end }}
|
||
</ul>
|
||
</div>
|
||
{{ end }}
|
||
|
||
<!-- 近 30 天趋势 -->
|
||
<div class="tb-card">
|
||
<h3 class="tb-card-title">{{t $.LangCode "stats.trend.dailyTitle"}}</h3>
|
||
<p class="tb-card-note">{{t $.LangCode "stats.trend.dailyNote"}}</p>
|
||
{{ $line := LineChart .DailyValues 640 148 12 10 }}
|
||
<div class="tb-line-wrap">
|
||
<!-- 纵轴刻度:SVG 横向被拉伸,文字只能留在外面用 HTML 定位 -->
|
||
<div class="tb-line-y" aria-hidden="true">
|
||
<span class="tb-line-ruler">{{ (index $line.Ticks 0).Label }}</span>
|
||
{{ range $line.Ticks }}<span class="tb-line-tick" style="top:{{ .TopPct }}%">{{ .Label }}</span>{{ end }}
|
||
</div>
|
||
<div class="tb-line-plot">
|
||
<svg class="tb-line" viewBox="0 0 640 148" preserveAspectRatio="none" role="img"
|
||
aria-label="{{t $.LangCode "stats.trend.dailyTitle"}}">
|
||
{{ range $line.Ticks }}
|
||
<line class="tb-grid{{ if .Base }} is-base{{ end }}" x1="0" x2="640" y1="{{ .Y }}" y2="{{ .Y }}"></line>
|
||
{{ end }}
|
||
{{ if $line.HasData }}
|
||
<path class="tb-line-area" d="{{ $line.Area }}"></path>
|
||
<polyline class="tb-line-path" points="{{ $line.Points }}"></polyline>
|
||
{{ end }}
|
||
</svg>
|
||
{{ if eq (len .Daily) 30 }}
|
||
<div class="tb-axis">
|
||
<span>{{ (index .Daily 0).Label }}</span>
|
||
<span>{{ (index .Daily 15).Label }}</span>
|
||
<span>{{ (index .Daily 29).Label }}</span>
|
||
</div>
|
||
{{ end }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 近 12 个月 -->
|
||
<div class="tb-card">
|
||
<h3 class="tb-card-title">{{t $.LangCode "stats.trend.monthlyTitle"}}</h3>
|
||
<p class="tb-card-note">{{t $.LangCode "stats.trend.monthlyNote"}}</p>
|
||
<div class="tb-bars">
|
||
{{ range $i, $p := .Monthly }}
|
||
<div class="tb-bar-col{{ if $p.Current }} is-current{{ end }}" title="{{ $p.Month }} · {{ NumInt $p.N }}">
|
||
<div class="tb-bar-track"><i style="height:{{ printf "%.1f" (index $ov.MonthlyPcts $i) }}%"></i></div>
|
||
<span class="tb-bar-lbl">{{ $p.Label }}</span>
|
||
</div>
|
||
{{ end }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 构成 -->
|
||
<div class="tb-card">
|
||
<h3 class="tb-card-title">{{t $.LangCode "stats.mix.title"}}</h3>
|
||
<p class="tb-card-note">{{t $.LangCode "stats.mix.note"}}</p>
|
||
<div class="tb-donuts">
|
||
<div class="tb-donut">
|
||
<div class="tb-donut-ring">
|
||
<svg viewBox="0 0 42 42" role="img" aria-label="{{t $.LangCode "stats.mix.public"}}">
|
||
<circle class="tb-ring-track" cx="21" cy="21" r="15.915" pathLength="100"></circle>
|
||
<circle class="tb-ring-val" cx="21" cy="21" r="15.915" pathLength="100"
|
||
stroke-dasharray="{{ PctText .Public .TotalQuestions }} 100"
|
||
transform="rotate(-90 21 21)"></circle>
|
||
</svg>
|
||
<span class="tb-donut-pct">{{ PctText .Public .TotalQuestions }}%</span>
|
||
</div>
|
||
<div class="tb-donut-cap">{{t $.LangCode "stats.mix.public"}}<b>{{ NumInt .Public }}</b></div>
|
||
</div>
|
||
<div class="tb-donut">
|
||
<div class="tb-donut-ring">
|
||
<svg viewBox="0 0 42 42" role="img" aria-label="{{t $.LangCode "stats.mix.answered"}}">
|
||
<circle class="tb-ring-track" cx="21" cy="21" r="15.915" pathLength="100"></circle>
|
||
<circle class="tb-ring-val" cx="21" cy="21" r="15.915" pathLength="100"
|
||
stroke-dasharray="{{ PctText .Answered .TotalQuestions }} 100"
|
||
transform="rotate(-90 21 21)"></circle>
|
||
</svg>
|
||
<span class="tb-donut-pct">{{ PctText .Answered .TotalQuestions }}%</span>
|
||
</div>
|
||
<div class="tb-donut-cap">{{t $.LangCode "stats.mix.answered"}}<b>{{ NumInt .Answered }}</b></div>
|
||
</div>
|
||
<div class="tb-donut">
|
||
<div class="tb-donut-ring">
|
||
<svg viewBox="0 0 42 42" role="img" aria-label="{{t $.LangCode "stats.mix.login"}}">
|
||
<circle class="tb-ring-track" cx="21" cy="21" r="15.915" pathLength="100"></circle>
|
||
<circle class="tb-ring-val" cx="21" cy="21" r="15.915" pathLength="100"
|
||
stroke-dasharray="{{ PctText .LoggedIn .TotalQuestions }} 100"
|
||
transform="rotate(-90 21 21)"></circle>
|
||
</svg>
|
||
<span class="tb-donut-pct">{{ PctText .LoggedIn .TotalQuestions }}%</span>
|
||
</div>
|
||
<div class="tb-donut-cap">{{t $.LangCode "stats.mix.login"}}<b>{{ NumInt .LoggedIn }}</b></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 时段热力图 -->
|
||
<div class="tb-card">
|
||
<h3 class="tb-card-title">{{t $.LangCode "stats.heat.title"}}</h3>
|
||
<p class="tb-card-note">{{t $.LangCode "stats.heat.note" .HeatmapDays}}</p>
|
||
<div class="tb-heat">
|
||
{{ range $i, $row := .HeatmapRows }}
|
||
<div class="tb-heat-row">
|
||
<span class="tb-heat-lbl">{{ index $ov.WeekdayLabels $i }}</span>
|
||
<div class="tb-heat-cells">
|
||
{{ range $row }}<i class="lv{{ HeatLevel . $ov.HeatmapMax }}" title="{{ . }}"></i>{{ end }}
|
||
</div>
|
||
</div>
|
||
{{ end }}
|
||
<div class="tb-heat-hours">
|
||
<span>0</span><span></span><span></span><span></span><span></span><span></span><span>6</span>
|
||
<span></span><span></span><span></span><span></span><span></span><span>12</span>
|
||
<span></span><span></span><span></span><span></span><span></span><span>18</span>
|
||
<span></span><span></span><span></span><span></span><span>23</span>
|
||
</div>
|
||
<div class="tb-heat-legend">
|
||
<span>{{t $.LangCode "stats.heat.less"}}</span>
|
||
<i></i><i class="lv1"></i><i class="lv2"></i><i class="lv3"></i><i class="lv4"></i>
|
||
<span>{{t $.LangCode "stats.heat.more"}}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 内容与节奏 -->
|
||
<div class="tb-card">
|
||
<h3 class="tb-card-title">{{t $.LangCode "stats.content.title"}}</h3>
|
||
<p class="tb-card-note">{{t $.LangCode "stats.content.note"}}</p>
|
||
|
||
<div class="tb-dist">
|
||
<h4 class="tb-dist-title">{{t $.LangCode "stats.content.lengthTitle"}}</h4>
|
||
{{ range $i, $n := .LengthBins }}
|
||
<div class="tb-dist-row">
|
||
<span>{{ index $ov.LengthLabels $i }}</span>
|
||
<div class="tb-dist-track"><i style="width:{{ printf "%.1f" (index $ov.LengthPcts $i) }}%"></i></div>
|
||
<span class="tb-dist-num">{{ NumInt $n }}</span>
|
||
</div>
|
||
{{ end }}
|
||
</div>
|
||
|
||
<div class="tb-dist">
|
||
<h4 class="tb-dist-title">{{t $.LangCode "stats.content.replyTitle"}}</h4>
|
||
{{ range $i, $n := .ReplyBins }}
|
||
<div class="tb-dist-row">
|
||
<span>{{ index $ov.ReplyLabels $i }}</span>
|
||
<div class="tb-dist-track"><i style="width:{{ printf "%.1f" (index $ov.ReplyPcts $i) }}%"></i></div>
|
||
<span class="tb-dist-num">{{ NumInt $n }}</span>
|
||
</div>
|
||
{{ end }}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tb-dash-foot">
|
||
<div class="tb-foot-times">
|
||
<span>{{t $.LangCode "stats.foot.dataDate"}}</span>
|
||
<span>{{ .DataDate }}</span>
|
||
<span>{{t $.LangCode "stats.foot.updated"}}</span>
|
||
<span>{{ .UpdatedAt }}</span>
|
||
<span>{{t $.LangCode "stats.foot.prevMonth"}}</span>
|
||
<span>{{ if .LastQuestionAt }}{{ .LastQuestionAt }}{{ else }}-{{ end }}</span>
|
||
<span>{{t $.LangCode "stats.foot.prevAnswer"}}</span>
|
||
<span>{{ if .LastAnswerAt }}{{ .LastAnswerAt }}{{ else }}-{{ end }}</span>
|
||
</div>
|
||
<p class="tb-foot-note">{{t $.LangCode "stats.foot.cacheNote"}}</p>
|
||
<p class="tb-foot-note">{{t $.LangCode "stats.foot.privacy"}}</p>
|
||
</div>
|
||
</div>
|
||
{{ else }}
|
||
<div class="tb-dash-empty">{{t .LangCode "stats.unavailable"}}</div>
|
||
{{ end }}
|
||
|
||
{{template "base/footer" .}}
|