头像框使用指南
头像框是叠在你头像上的一圈装饰,会出现在顶栏、评论区、讨论区、博客列表和 你的个人主页上。
框有两个来源:站长发放(不定期,不用你申请),或者自己去鱼干商城租 (花小鱼干,见第二节)。你要做的就是决定戴不戴 —— 框不能自己上传。
一、我有哪些框
打开 账号设置 → 头像框(/settings#avatar-frame)。
那一页分两块:
- 当前装备:你现在戴的那个,右边有一颗「摘下」。
- 我持有的框:你手上所有的框,每个右边有一颗「戴上」。
点一下就生效,不用保存。戴上之后顶栏的头像会立刻变,不用刷新页面。
个人主页右上角也有一顆「换个头像框」,点了直接跳到那一页。
二、去鱼干商城租一款
打开 小鱼干 → 鱼干市场,往下翻到「鱼干商城」那一块
(/fish/market#shop)。
那里列着当前在租的头像框,1 鱼干 / 天。选好天数(1 到 30 天), 点「租用」,确认一下花费就完成了。钱从你的小鱼干余额里扣。
租期是叠加的:手上那个还剩 3 天,再租 7 天,就变成总共 10 天 —— 不会把之前的天数冲掉,也不用等到期了再续。
| 你看到的 | 什么意思 |
|---|---|
| 当前持有到 2026-10-20 12:00:00 | 你已经租着,到期时间是这个 |
| 上次租的已过期 | 租期到了,框已经自动摘下来 |
| 这款头像框的素材还没传上来,暂时买不了 | 站上缺那张图(站长还没补上),补上就能买 |
小鱼干从哪来?每日签到、给别人的文章投喂、或者去鱼干练手盘碰碰运气。
三、限时的框到期会怎样
框大多是限时的:站长发放时定一个期限(比如「中秋活动」那一款只发 30 天), 商城租的则是你买的那几天。
到期后:
- 框会自动消失,你什么都不用做。
- 那一行还会留在列表里,标着「已过期」,但「戴上」按钮是灰的。
- 自己租的:到期前再去商城租一次就行,天数会接在现有到期之后。
- 站长发的:想让站长续期,找站长说一声 —— 续上之后框会立刻回来, 不需要你重新戴。
「已过期」和「已下架」是两回事:
| 标记 | 什么意思 | 你能做什么 |
|---|---|---|
| 已过期 | 期限到了 | 商城租的去商城续;站长发的找站长续 |
| 已下架 | 站长把这款框整个撤了,以后不再发 | 只能摘下,不能再戴 |
| 素材缺失 | 站上缺那张图(站长还没补上) | 等站长补图 —— 补上就会自己出现,不用重新戴 |
四、常见问题
戴上之后没看到框?
先看清单里那一行有没有标「素材缺失」或「已过期」。两者都会让框显示不出来, 原因不一样(前者等站长补图,后者等续期),所以分开标。
框会盖住我的头像吗?
不会。框素材中间是透明的,露出下面的头像。如果你看到自己的脸被挡住, 那是站长传错了素材,跟他反馈一下。
能同时戴好几个吗?
不能,一次只有一个。换一个就是直接覆盖。
摘下来之后还在吗?
在。摘下只是「不戴」,框还是你的,随时可以再戴上。
能自己上传一张图当框吗?
不能,本站不接受上传头像框。框的素材由站长统一出 —— 但你可以花小鱼干租, 见第二节。
租来的框,租期里能一直戴吗?能换吗?
能。租期内它就是你手上的框:随时戴上、摘下、换成别的,都行。 每次租用只能租一款,自己选天数。
五、给站长:怎么加一款新框
这一节是运维向的,普通用户看到这里就够了。
0. 从一个能跑的起点开始
scripts/make-frame-demos.mjs 就是几款示例框的源码(纯 SVG,用 sharp 光栅化成
PNG-32)。改颜色、改粗细、加装饰都在那里面,然后:
node scripts/make-frame-demos.mjs # 写到 public/static/frames/
node scripts/make-frame-demos.mjs /tmp/x # 或者写到别处先看看
它同时是出图规格的活文档 —— 几何怎么算、为什么粗细有上限、<key> 与文件名的对应,
都写在注释里。
⚠️ 改了那个脚本就必须重跑它,并把
public/static/frames/的改动一起提交。 素材是随代码入库的,只改脚本不重跑的话,站点会继续显示旧图 —— 不报错、 日志里什么都没有。tests/unit/frame-assets.test.ts与脚本写的manifest.json就是为这件事设的(改了不重跑,那条用例当场红,报错里会告诉你跑哪条命令)。
1. 出图规格
找画师画的场合,直接把
docs/guide/头像框出图规范.md那一份发给他 —— 它是自包含的(不假设对方读过本站任何文档),下面这几条是精简版。
框是一张叠在头像上的 PNG,中间必须透明。几条硬要求:
- 正方形。规格是「画布就是框」——所有头像都是方的(20 / 24 / 32 / 34 / 120px 六档),非正方形的素材会被留白(不会被拉伸,但会小一圈)。
- 带透明通道(PNG-32)。没有 alpha 的素材会盖住所有人的脸 —— 那是全站 15 处一起坏,而且你可能只在自己的主页看一眼。 导出时别「压平 / flatten」。
- 建议 ≥ 256×256。它会缩到 20px(博客列表的作者头像)——太细的线条 (<1.5px)在那个尺寸下会糊成一片。画完缩到 20px 看一眼再定稿。
- 越界的部分会被裁掉。框画布之外的东西(伸出去的翅膀、超出边界的王冠)
不会显示 —— 头像盒子自带
overflow: hidden。 - 深浅两个主题各看一眼。框是不透明的图案,底色对比在深色主题下可能不够。
⚠️ 两条实测出来的硬约束
① 环的粗细上限 = 画布的 8%。 这是个几何约束,不是审美选择:头像自己的圆角 就是 8%,而框的外缘必须正好落在那个圆角上(否则四个角会露出一小块没被盖住的头像)。 内外两条圆弧共享圆心,粗细才均匀 —— 一环宽度一旦超过 8%,内缘圆角会被挤成 0, 四角就变厚,看着像画歪了。
换算到实际尺寸:20px 的头像上,环最粗只有 1.6px。
② 20px 下能区分的维度基本只剩颜色。 六款示例框缩到 20px,除了颜色几乎一模一样
(corner 的四角、dashed 的点全糊了)。这不是示例做得不好,是 20px 的物理限制。
所以:
- 别指望细节在小尺寸下活下来。想让两款框在博客列表里区分得开,换配色比 加装饰有效。
- 装饰(角标、翅膀、纹理)只在 56px 以上(讨论区、个人主页)看得见 —— 那是「彩蛋」,不是识别手段。
- 定稿前一定要缩到 20px 看一眼。生成脚本会顺手把每款缩一份出来
(
_preview-20px-<key>.png,_前缀 = 扫盘会跳过,不会被当成可用的框)。
2. 放素材
把文件拷成 public/static/frames/<key>.png:
public/static/frames/
sakura.png → key 是 "sakura"
laurel.png → key 是 "laurel"
<key>只能用小写字母、数字与连字符(它同时是 URL 里的一段)。- 只认
.png。.PNG也认;.jpg/.gif/.webp/.svg一律忽略。 .或_开头的文件会被跳过(编辑器临时文件、macOS 的资源叉)。- 目录是平铺的一层,没有子目录。
- 素材随代码入库(我们自己画的,所以住
public/static/而不是运行时数据目录instance/)—— 出图 / 拷图之后要提交,部署侧什么都不用做。 手画的框(没进脚本的那种)也可以,不会被上面那条守卫拦 —— 它只管脚本生成的那些。
3. 登记
在 src/lib/frame-refs.ts 的 FRAMES 里加一条:
export const FRAME_KEYS = ['sakura', 'laurel'] as const;
export const FRAMES: Record<FrameKey, FrameDef> = {
sakura: { label: '樱花', description: '2026 春季限定。' },
laurel: { label: '桂冠', description: '给最早的那批人。' },
};
FRAMES 是穷尽的 Record:加了 key 不补条目,tsc 当场报错。
不需要数据库迁移 —— user_frames.frame_key 是文本,不是外键。
4. 发放
npm run cli -- frame grant <用户名> sakura --days 30 -r "2026 春季活动"
npm run cli -- frame grant <用户名> laurel # 不带 --days = 永久
细节见 docs/cli.md 的「头像框」一节。这条命令不受商城影响:
在售的那款框照旧可以这样直接发(比如当奖品),发完之后它在商城仍然买得到。
5. 自查(★ 别跳过这一步)
npm run cli -- frame list --keys
它会列出白名单里每一个 key 的素材状态与租金:
sakura 樱花 素材:有 透明通道:有 18244 字节 租金:—
fishblue 鱼干蓝 素材:有 透明通道:有 6953 字节 租金:1 鱼干/天
laurel 桂冠 素材:缺失 透明通道:— — 字节 租金:—
这是唯一能主动发现「登记了但忘了传图」的地方。 那种情况下全站都不会显示 这个框,而页面不会有任何报错 —— 用户那边看起来跟「我没这个框」一模一样。
透明通道:无 也要处理:那张素材会盖住用户的脸。
6. 下架一款框
把 FRAMES 里那条加上 retired: true:
sakura: { label: '樱花', description: '…', retired: true },
⚠️ 不要从 FRAME_KEYS 里删掉 key。 删了之后系统就认不出它了 ——
设置面板显示不出那一行,还戴着它的用户会摘不掉(只能看它一直挂在头上)。
置 retired 则:列表里还看得见、标着「已下架」、能摘不能戴。
7. 定价 / 上架到鱼干商城
鱼干商城(/fish/market#shop)在租的框 = 给 FRAMES 那条加一个 rentPerDay:
fishblue: { label: '鱼干蓝', description: '…', rentPerDay: 1 },
就这样 —— 不需要迁移。在架清单由 frame-refs.ts 的 rentableFrameKeys()
现算(有价 + 未退役),商城的服务端组件直接读它。
⚠️ 但上架第二款时要多改一处:商城面板是按「在售只有一款」写的
(ShopPanel.tsx 里 const item = items[0],刻意不为将来的样子预留结构)。
只定价、不改面板的话,原先在售的那一款会从页面上消失 —— 服务端照样接受它的
key、页面不报任何错。有一条绊线用例(tests/unit/frame-shop-guard.test.ts)会在
这种情形下当场变红,那时把面板改成逐款一块表单即可。
几条只在这一处生效的规矩:
- 单位是「鱼干 / 天」,用户在页面上自选 1–30 天(上下限是两个常量
FRAME_RENT_MIN_DAYS/FRAME_RENT_MAX_DAYS,改它们页面文案会跟着变)。 - 改价 = 改全站定价。已租出去的不受影响(到期时刻早就写进库了), 只影响之后每一笔。
- 下架:把
retired置 true 就同时从商城消失(见上一节)—— 两件事用同一个开关, 所以不会出现「已下架却还能买」。 - 盘上没有素材时商城会拒卖(409),而不是收钱给一个看不见的框。 补上图立刻就能买,不用重启。
- ⚠️ 别写
rentPerDay: 0想当免费框 —— 免费租借这条路不存在(记账内核拒收 0 单位)。判据是「非正数 = 不卖」:商城不列它、接口回 400「配置有误」,frame list --keys会给一条黄色警告点名。要免费送就用npm run cli -- frame grant <用户名> <key>(那条不经过鱼干)。 - 想「只发不卖」:不加
rentPerDay就行 —— 其余五款都是这样。
到期、续租、扣款与发框的原子性、为什么这条接口只认会话 —— 都在
docs/architecture.md§6.14 的「鱼干商城」那一节。