Files
sansenhoshiandClaude b61d09f09f Add HeXi bot codebase: custom plugins, web frontends, tests
- 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>
2026-09-01 13:13:40 +08:00

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>