文档索引

头像框使用指南

头像框是叠在你头像上的一圈装饰,会出现在顶栏、评论区、讨论区、博客列表和 你的个人主页上。

框有两个来源:站长发放(不定期,不用你申请),或者自己去鱼干商城租 (花小鱼干,见第二节)。你要做的就是决定戴不戴 —— 框不能自己上传。


一、我有哪些框

打开 账号设置 → 头像框(/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 的「鱼干商城」那一节。

本页内容来自仓库 docs/guide/头像框使用指南.md·在 GitHub 上查看