- hexi core: message handling, rate limiting, cooldown, plugin manager - Custom plugins: BF stats, daily check-in, quotes, persona cards, etc. - Community plugins vendored under hexi/plugins with local fixes - Web admin frontends (learning-chat, persona-admin), unified hexi/web - Tests for rate_limit/cooldown/memes/persona; poetry.lock Co-Authored-By: Claude <noreply@anthropic.com>
344 lines
9.3 KiB
HTML
344 lines
9.3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<style>
|
|
@font-face { font-family: 'MiSansR'; src: url('__FONT_REGULAR__'); }
|
|
@font-face { font-family: 'MiSansB'; src: url('__FONT_BOLD__'); }
|
|
|
|
:root {
|
|
--surface: #f6f6f7;
|
|
--card: #ffffff;
|
|
--hairline: #e3e3e6;
|
|
--text-primary: #1a1a1a;
|
|
--text-secondary: #52514e;
|
|
--text-muted: #898781;
|
|
--axis: #c3c2b7;
|
|
--bar: #2a78d6;
|
|
--bar-fill-top: #5598e7;
|
|
--accent: #2a78d6;
|
|
--game-text: #242424;
|
|
--fallback-bg: #f0f0f2;
|
|
}
|
|
|
|
/* 主题覆盖(light/dark 由 Python 注入) */
|
|
__THEME_CSS__
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
width: 1400px;
|
|
background: var(--surface);
|
|
font-family: 'MiSansR', 'Microsoft YaHei', sans-serif;
|
|
padding: 28px 36px 36px;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
/* ---- 头部:头像 + 昵称 ---- */
|
|
.header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 18px;
|
|
}
|
|
|
|
.avatar {
|
|
width: 80px;
|
|
height: 80px;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
border: 2px solid var(--hairline);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.avatar-fallback {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--fallback-bg);
|
|
font-family: 'MiSansB', 'MiSansR', 'Microsoft YaHei', sans-serif;
|
|
font-size: 34px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.nickname {
|
|
font-family: 'MiSansB', 'MiSansR', 'Microsoft YaHei', sans-serif;
|
|
font-size: 32px;
|
|
display: flex;
|
|
align-items: baseline;
|
|
min-width: 0;
|
|
}
|
|
|
|
.nick-tag {
|
|
font-family: 'MiSansR', 'Microsoft YaHei', sans-serif;
|
|
font-size: 16px;
|
|
color: var(--text-muted);
|
|
margin-left: 8px;
|
|
vertical-align: baseline;
|
|
min-width: 0;
|
|
max-width: 260px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.stats {
|
|
font-size: 20px;
|
|
color: var(--text-secondary);
|
|
margin-top: 6px;
|
|
}
|
|
|
|
/* ---- 双列排行表格 ---- */
|
|
.tables {
|
|
display: flex;
|
|
gap: 20px;
|
|
margin-top: 22px;
|
|
}
|
|
|
|
.col {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
table-layout: fixed;
|
|
}
|
|
|
|
col.rank { width: 72px; }
|
|
col.hours { width: 150px; }
|
|
|
|
th {
|
|
font-size: 18px;
|
|
font-weight: normal;
|
|
color: var(--text-muted);
|
|
text-align: left;
|
|
padding: 6px 8px;
|
|
}
|
|
|
|
td {
|
|
font-size: 25px;
|
|
padding: 12px 8px;
|
|
border-bottom: 1px solid var(--hairline);
|
|
vertical-align: middle;
|
|
}
|
|
|
|
tr:last-child td { border-bottom: none; }
|
|
|
|
.rank { width: 72px; }
|
|
|
|
.rank-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 34px;
|
|
height: 34px;
|
|
border-radius: 8px;
|
|
font-size: 20px;
|
|
font-family: 'MiSansB', 'MiSansR', 'Microsoft YaHei', sans-serif;
|
|
color: #ffffff;
|
|
}
|
|
|
|
.rank-plain {
|
|
display: inline-block;
|
|
width: 34px;
|
|
text-align: center;
|
|
font-size: 25px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.game {
|
|
color: var(--game-text);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.hours {
|
|
width: 150px;
|
|
text-align: right;
|
|
color: var(--text-secondary);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* ---- 图表卡片 ---- */
|
|
.card {
|
|
background: var(--card);
|
|
border-radius: 16px;
|
|
padding: 16px 20px 12px;
|
|
margin-top: 22px;
|
|
}
|
|
|
|
.card-title {
|
|
font-family: 'MiSansB', 'MiSansR', 'Microsoft YaHei', sans-serif;
|
|
font-size: 24px;
|
|
}
|
|
|
|
.card-sub {
|
|
font-size: 18px;
|
|
color: var(--text-muted);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.chart {
|
|
width: 100%;
|
|
height: 330px;
|
|
}
|
|
|
|
/* ---- 底部 ---- */
|
|
.footer {
|
|
font-size: 18px;
|
|
color: var(--text-muted);
|
|
margin-top: 20px;
|
|
text-align: right;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
__AVATAR__
|
|
<div>
|
|
<div class="nickname">__STEAM_NAME____PLUGIN_TAG__</div>
|
|
<div class="stats">共 __GAME_COUNT__ 款游戏 · 总时长 __TOTAL_HOURS__</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tables">
|
|
<div class="col">
|
|
<table>
|
|
<colgroup>
|
|
<col class="rank">
|
|
<col>
|
|
<col class="hours">
|
|
</colgroup>
|
|
<thead>
|
|
<tr>
|
|
<th class="rank">排名</th>
|
|
<th>游戏</th>
|
|
<th class="hours">时长</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
__ROWS_LEFT__
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="col">
|
|
__RIGHT_TABLE__
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">各时长段游戏数量</div>
|
|
<div class="card-sub">全库按时长分段,0 = 未游玩 · 对数轴</div>
|
|
<div class="chart" id="bar"></div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">时长占比</div>
|
|
<div class="card-sub">Top 8 游戏 + 其余聚合</div>
|
|
<div class="chart" id="pie"></div>
|
|
</div>
|
|
|
|
<div class="footer">__FOOTER__</div>
|
|
|
|
<script src="__ECHARTS__"></script>
|
|
<script>
|
|
const DATA = __DATA__;
|
|
|
|
// 主题色从 CSS 变量读取(亮/暗由 Python 注入 :root 覆盖)
|
|
function cssVar(name) {
|
|
return getComputedStyle(document.documentElement)
|
|
.getPropertyValue(name).trim() || '#000';
|
|
}
|
|
|
|
// 各时长段游戏数量柱状图(对数轴:0 时长大桶不会压扁其他桶)
|
|
const bar = echarts.init(document.getElementById('bar'));
|
|
bar.setOption({
|
|
animation: false,
|
|
backgroundColor: 'transparent',
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
backgroundColor: cssVar('--card'),
|
|
borderColor: cssVar('--hairline'),
|
|
textStyle: { color: cssVar('--text-primary'), fontSize: 14 },
|
|
},
|
|
grid: { left: 8, right: 16, top: 30, bottom: 30, containLabel: true },
|
|
xAxis: {
|
|
type: 'category',
|
|
data: DATA.bar.names,
|
|
axisLine: { lineStyle: { color: cssVar('--axis') } },
|
|
axisTick: { show: false },
|
|
axisLabel: { color: cssVar('--text-muted'), fontSize: 15 },
|
|
},
|
|
yAxis: {
|
|
type: 'log',
|
|
show: false,
|
|
splitLine: { lineStyle: { color: cssVar('--hairline') } },
|
|
},
|
|
series: [{
|
|
type: 'bar',
|
|
barWidth: '45%',
|
|
barMaxWidth: 110,
|
|
data: DATA.bar.counts,
|
|
itemStyle: { color: cssVar('--bar'), borderRadius: [5, 5, 0, 0] },
|
|
label: {
|
|
show: true,
|
|
position: 'top',
|
|
color: cssVar('--text-primary'),
|
|
fontSize: 16,
|
|
},
|
|
}],
|
|
});
|
|
|
|
// 时长占比环形饼图(禁用动画:截图可能截到入场动画早期帧导致整图空白)
|
|
const pie = echarts.init(document.getElementById('pie'));
|
|
pie.setOption({
|
|
animation: false,
|
|
backgroundColor: 'transparent',
|
|
color: DATA.pie.colors,
|
|
tooltip: {
|
|
trigger: 'item',
|
|
backgroundColor: cssVar('--card'),
|
|
borderColor: cssVar('--hairline'),
|
|
textStyle: { color: cssVar('--text-primary'), fontSize: 14 },
|
|
},
|
|
legend: {
|
|
orient: 'vertical',
|
|
right: 10,
|
|
top: 'center',
|
|
itemWidth: 12,
|
|
itemHeight: 12,
|
|
textStyle: { color: cssVar('--text-secondary'), fontSize: 14 },
|
|
formatter: function (name) {
|
|
try {
|
|
const item = (DATA.pie.items || []).find(i => i.name === name);
|
|
if (!item || !DATA.pie.total) return name;
|
|
return name + ' ' + (item.value / DATA.pie.total * 100).toFixed(1) + '%';
|
|
} catch (e) {
|
|
return name;
|
|
}
|
|
},
|
|
},
|
|
series: [{
|
|
type: 'pie',
|
|
radius: ['48%', '68%'],
|
|
center: ['37%', '50%'],
|
|
itemStyle: { borderColor: cssVar('--card'), borderWidth: 2 },
|
|
label: {
|
|
color: cssVar('--text-primary'),
|
|
fontSize: 14,
|
|
formatter: function (p) {
|
|
return p.percent >= 5 ? p.percent.toFixed(1) + '%' : '';
|
|
},
|
|
},
|
|
labelLine: { show: false },
|
|
data: DATA.pie.items,
|
|
}],
|
|
});
|
|
|
|
window.__rendered = true;
|
|
</script>
|
|
</body>
|
|
</html>
|