「数据速览」下面那句「下面几句是从上面的数字直接读出来的,没有推测」, 是在向读者解释「我没有编」—— 那是作者视角的免责,不是访客需要的信息。 读者看到「没有推测」只会想:那我该怀疑什么?删掉,卡片留标题 + 结论列表 即可(`stats.insight.title` 已经说明这组是什么)。 顺着把整页 40 余条文案过了一遍,同类的还有页脚: 「累计数字与图表是当天重算一次的快照(当天第一次访问时算,服务重启也会重算)」 → 「累计数字与图表每天更新一次」 访客不需要知道缓存是懒重算、更不需要知道重启会触发重算;真要精确的新鲜度, 上一行「快照时间」已经给了到秒的值。这条注的职责只是说清「累计按天、今日实时」。 保留的两处,判断过不属于这个问题: - `stats.foot.privacy` 与顶部 `stats.lead` 后半句近乎同义 —— 页脚隐私声明是惯例, 重复一次不算噪音,删了反而有人以为底部没声明。 - 「(实时)」在 60 秒缓存下仍成立。 删掉说明行后标题与列表之间只剩 4px,给 `.tb-insights` 补 `margin: 8px 0 0` (原来靠 `tb-card-note` 的 16px 撑开)。顺手修掉模板里一条过期注释 —— 它还写着「四条时间」「哪个是基准日」,上一轮已改成三行「快照时间」。 i18n 四语言同步(有词条数量对等测试兜底);build + i18n/stats/template/templates 测试全绿,中英页脚与数据速览卡片截图确认过间距。
631 lines
19 KiB
HTML
631 lines
19 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);
|
||
}
|
||
|
||
/* 洞察:这组卡片没有说明行,靠 margin-top 补出与标题的间隔 */
|
||
.tb-insights {
|
||
margin: 8px 0 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>
|
||
<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.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" .}}
|