全站视觉移植复刻:把 Firefly 改造成 animal-island-ui 风格的七期工程(含复刻清单与踩坑全录)
- 1备案通过后的图床重生记:把 CloudFlare-ImgBed 从 Telegram 渠道搬回境内服务器的完整复刻手册
- 2全站动效补完与 anime.js 数字滚动:五项动效计划的落地、四轮实测踩坑与复刻清单
- 3全站视觉移植复刻:把 Firefly 改造成 animal-island-ui 风格的七期工程(含复刻清单与踩坑全录)本文
- 4导航栏 UI 改造全记录:卡片边框、悬浮胶囊、三段收拢与 PC 收拢态快捷面板(附完整复现指南)
- 5博客性能优化全记录:Lighthouse 报告拆解到帧率根因排查(A–F 六项实战)
- 6Firefly 博客实战:从零搭建任意设备可用的发布台(Vercel + GitHub API)
- 7评论区回复时表情面板被截断:一次 overflow 裁剪排查实录
- 8Firefly 博客实战:给文章页加阅读进度条与圆环百分比
- 9Firefly 博客接入友链朋友圈:从零开始的完整部署教程
- 10从零开始搭建个人图床
- 11文章中添加音乐播放器
- 12基于Firefly主题的列表封面位置改造
- 13基于rehype的博客PDF嵌入组件实现
- 14Twikoo评论系统部署踩坑记录:Vercel域名无法访问的解决之路
- 15这是我的第一篇BLOG
全站视觉移植复刻:把 Firefly 改造成 animal-island-ui 风格的七期工程(含复刻清单与踩坑全录)
2026-10-11 至 10-12,本博客完成了一次全站视觉移植:从原有主题换成 animal-island-ui(治愈系海岛风格 React 组件库)的设计语言。22 个文件修改(+275/−73 行)+ 6 个字体文件 + 33 份参考规范,功能零损失。
本文写给两类读者:
- 未来的维护者(包括新会话的 AI 助手):读完应知道每一期改了哪些文件、为什么、精确色值在哪、验证基线是多少、哪些坑已经踩过;
- 想复现的访客:文末「复刻清单」从零列出全部步骤,所有关键代码在正文中可照抄。
〇、决策:为什么不引库、也不从零重写
起因是想把博客「完全变成 animal-island-ui 的风格」。三条路线对比后选了风格移植:
| 路线 | 结论 | 原因 |
|---|---|---|
| 直接安装它的 React npm 包 | ❌ | 本博客是 Astro 7 + Svelte 5 岛屿,混入 React 运行时得不偿失;且库里没有文章卡、目录、归档等博客部件 |
| 移植它的设计规范到现有站点 | ✅ | 风格 = CSS + design token,可整体搬运;内容、友圈、发布台、评论、搜索、性能优化成果全部保留 |
| 按它的风格从零重写博客 | ❌ | 功能与内容全部归零,按月计的工程量 |
关键依据:项目作者自己为「不装库只抄风格」留了官方通道——docs/design-system/css-variables.md 第一句就是 “When re-implementing the style without depending on the component library, declare the following variables on :root”。README 里那条 import 'animal-island-ui/style' 规则是库使用者的须知,不装库时不仅不适用(没有它的组件需要被样式化),而且不该引(会把整套 reset 和组件类灌进 Tailwind 体系打架)。
暗色模式:用户明确不做,所有改造只管亮色(涉及暗色的规则都用 :root:not(.dark) 或原样保留)。
一、准备工作:把规范拉到本地
风格复刻的「唯一真相源」是仓库里的两处:
- 技能包
skills/animal-island-ui-style/(SKILL.md 中英双语 + 13 份组件 props 参考) - 设计系统
docs/design-system/(design-tokens.md 精确值、design-rules.md 七律与 16 条反模式、components/ 逐组件像素级 CSS)
本机 raw.githubusercontent.com 直连不稳,用 api.github.com 的 contents 接口拿 base64 再解码最稳,一个 Node 脚本批量拉:
// 拉取单个文件:GET https://api.github.com/repos/<owner>/<repo>/contents/<path>// 返回 JSON 的 content 字段即 base64,解码写盘即可const res = await fetch(url, { headers: { Accept: "application/vnd.github+json" } });const json = await res.json();await writeFile(dest, Buffer.from(json.content, "base64").toString("utf8"));共下载 33 个文件,落在 docs/animal-island-ui-style/(含 design-system/ 子目录)。本站后续所有色值都以这份本地规范为准,动样式前先查它。
技能包里 14 条「硬规则」中真正管视觉的核心几条:
- 不用纯黑文字(
#000/#111),不用冷灰背景(#fafafa/#f5f5f5) - 交互元素圆角 ≥ 12px,按钮和输入框一律 50px pill
- 3D 堆叠阴影(
0 5px 0 #bdaea0)只属于 primary 按钮,default 类只用柔和抬升影 - 字体圆体(Nunito + Noto Sans SC),字重 body 500、标题 600–900,永不低于 400
- 动效统一
cubic-bezier(0.4, 0, 0.2, 1),时长 0.15–0.35s - 焦点环:输入类黄色
#ffcc00、按钮薄荷#19c8b9,绝不用冷蓝
核心 token(完整值见本地 design-system/css-variables.md):
:root { --animal-primary: #19c8b9; /* hover #3dd4c6 / active #11a89b */ --animal-text: #794f27; /* 标题/主文字(大地棕) */ --animal-text-body: #725d42; /* 正文 */ --animal-text-secondary: #9f927d; /* 次级 */ --animal-bg: #f8f8f0; /* 页面底(羊皮纸) */ --animal-bg-content: rgb(247, 243, 223); /* 卡片/内容区(奶油) */ --animal-border: #c4b89e; --animal-radius: 18px; --animal-radius-pill: 50px; --animal-shadow-btn: #bdaea0; /* 3D 堆叠色 */ --animal-ease: cubic-bezier(0.4, 0, 0.2, 1);}二、七期实录
一期:全局色板地基(4 文件)
目标:全站从冷灰/白/黑换成羊皮纸 + 大地棕 + 薄荷。
| 文件 | 改动 |
|---|---|
src/config/siteConfig.ts | themeColor.hue: 40 → 185。站点主色是 oklch 色相机制,185 ≈ #19c8b9 的薄荷色相,全站 var(--primary) 派生色自动跟随 |
src/utils/setting-utils.ts | getDefaultHue 兜底 "250" → "185" |
src/styles/variables.styl | 亮色中性色板整块替换(见下表)+ 阴影暖化 |
src/styles/main.css | @theme 里重定义 Tailwind 的 black(见下) |
variables.styl 亮色 :root 关键映射(暗色块未动):
| 变量 | 旧值 | 新值 |
|---|---|---|
--primary | oklch(0.70 0.14 var(--hue)) | oklch(0.75 0.13 var(--hue))(hue=185 时精确对准 #19c8b9) |
--page-bg | oklch 灰 | #f8f8f0 |
--card-bg | white | #f7f3df |
--card-bg-transparent | 白透明 | rgb(247 243 223 / …) |
--btn-content | oklch | #794f27 |
--btn-regular-bg | oklch | #fffbe7(hover #f4eeda / active #e9e1c6) |
--btn-plain-bg-hover / active | oklch | #efe9d6 / #fdfbf4 |
--deep-text | 近黑 oklch | #794f27 |
--title-active | oklch | #11a89b |
--line-divider / --line-color | 冷黑透明 | rgba(121,79,39,.12) / .16 |
--card-border-color | black | #c4b89e |
--meta-divider / --content-meta | 冷黑透明 | rgba(121,79,39,.22) / .72 |
--muted | oklch | #f0ece2 |
--selection-bg | oklch | #e6f9f6 |
--license-block-bg | 冷黑透明 | rgba(121,79,39,.06) |
--float-panel-bg | white | #f8f8f0 |
--inline-code-bg/color、--enter-btn-* 引用了上面的 btn 变量,自动跟随,无需单独改。链接、TOC、admonitions 等 var(--hue) 派生项在 hue 185 下自动变薄荷/青绿。
阴影冷黑 → 暖棕(rgba(61, 52, 40, ·),色值取自规范的 elevation 阴影):
--shadow-sm: 0 2px 4px rgba(61, 52, 40, 0.06);--shadow-md: 0 3px 10px rgba(61, 52, 40, 0.1);--shadow-lg: 0 8px 24px rgba(61, 52, 40, 0.14);--shadow-xl: 0 12px 32px rgba(61, 52, 40, 0.18);--shadow-navbar: 0 4px 16px rgba(61, 52, 40, 0.12);--shadow-button: 0 3px 10px rgba(61, 52, 40, 0.16);杠杆点:@theme 重定义 black(main.css):
@theme { --color-black: #5c3a1a; /* 暖深棕,替代纯黑语义 */}Tailwind 的 text-black/75、bg-black/5 等全部引用主题变量,一行改动让全站 34 处散落的 text-black/* 和 .text-90/75/50 工具类自动转暖,不用逐个模板改。
二期:按钮系统(11 文件)
关键机制(复刻必读):main.css 里 .btn-* 原有规则都在 @layer components(161–619 行的大块)内,而模板里散落的 rounded-lg 等是 utilities 层——层序上 utilities 永远赢。所以按钮的 pill 化规则刻意写在文件末尾、不进任何层:未分层的 normal 声明在级联里排最后,无条件压过所有分层规则。
/* main.css 末尾(未分层)—— animal-island 二期·按钮系统 */.btn-plain, .btn-regular, .btn-card { border-radius: 50px; /* 硬规则:按钮一律 pill */ transition: background-color 0.15s var(--ease-standard), color 0.15s var(--ease-standard), box-shadow 0.2s var(--ease-standard), transform 0.15s var(--ease-standard), opacity 0.15s var(--ease-standard);}/* default 填充按钮:柔和抬升投影 rest → hover 加深 */.btn-regular, .btn-card { box-shadow: 0 2px 4px 0 rgba(61,52,40,0.06); }.btn-regular:hover, .btn-card:hover { box-shadow: 0 3px 10px 0 rgba(61,52,40,0.1); }/* ghost 按钮透明底不投影,hover 填底后再给,避免浮出脏晕 */.btn-plain:hover { box-shadow: 0 2px 4px 0 rgba(61,52,40,0.06); }
/* primary 主按钮:3D 堆叠阴影,全站唯一(硬规则 5) */.btn-primary { background-color: var(--primary); color: #fff; border-radius: 50px; font-weight: 600; box-shadow: 0 5px 0 0 #bdaea0; transition: background-color 0.15s var(--ease-standard), color 0.15s var(--ease-standard), box-shadow 0.15s var(--ease-standard), transform 0.15s var(--ease-standard);}.btn-primary:hover { background-color: #3dd4c6; transform: translateY(-1px); box-shadow: 0 6px 0 0 #bdaea0;}.btn-primary:active { background-color: #11a89b; transform: translateY(2px); box-shadow: 0 1px 0 0 #bdaea0;}btn-primary 接入了 10 处真 CTA(只在原类上追加 class,背景/圆角/hover 由未分层规则统一接管):
404.astro、sponsor.astro、atom.astro、rss.astro、posts/[...slug].astro(赞助按钮×2)、EncryptedContent.astro(解锁提交)、AnimeCard/AnimeDetailModal、BilibiliCard/BilibiliDetailModal。
刻意不给 3D 的:分页当前页、筛选器激活态(状态指示非动作按钮)、导航栏 ghost 胶囊(只配柔和影)。
焦点环不用动——全站已有 outline: 2px solid var(--primary) 基线(main.css @layer components),默认色相下就是薄荷。
三期:导航栏 / 浮动 UI / 页脚(5 文件)
navbar.css:吸顶滚动投影rgba(0,0,0,.1)→rgba(61,52,40,.12);壁纸模式下拉/浮动面板的冷白玻璃 → 奶油玻璃rgba(248,248,240,.55),冷黑描边 →rgba(121,79,39,.1)FloatingControls.astro(阅读进度徽章):外框与圆环底轨 →rgba(121,79,39,.15)FloatingButton.astro:冷黑 1px 框 → 同上FloatingTOC.astro:空目录占位字text-gray-500→text-(--content-meta)Footer.astro:零改动——它的border-black/10和text-50被一期的--color-black重定义自动救了
四期:卡片系统统一 + 灰阶大手术(2 文件)
杠杆点二:整个灰阶重定义。全站 neutral 206 处 + gray 37 处冷灰,在 @theme 里整阶换成羊皮纸阶:
@theme { --color-neutral-50: #fbfaf3; --color-gray-50: #fbfaf3; --color-neutral-100: #f0ece2; --color-gray-100: #f0ece2; --color-neutral-200: #e5ddc8; --color-gray-200: #e5ddc8; --color-neutral-300: #c4b89e; --color-gray-300: #c4b89e; --color-neutral-400: #b3a68d; --color-gray-400: #b3a68d; --color-neutral-500: #9f927d; --color-gray-500: #9f927d; --color-neutral-600: #8a7b66; --color-gray-600: #8a7b66; --color-neutral-700: #725d42; --color-gray-700: #725d42; --color-neutral-800: #5a4426; --color-gray-800: #5a4426; --color-neutral-900: #44331c; --color-gray-900: #44331c; --color-neutral-950: #2e2110; --color-gray-950: #2e2110;}卡片试点收编:早期在 PostCard.astro scoped 样式里做过一版写死色值的试点,四期把它删掉,升格为 main.css 末尾的全局规则(发现 ProjectCard 复用了 post-card-wrapper 类名,白捡覆盖):
/* 偏离规范硬规则 8(Card 无 box-shadow):本站页面底与卡片底同为羊皮纸、 色差极小,需要暖色抬升投影做分离;hover 上浮 -2px 与规范一致。 */.post-card-wrapper.card-base,.album-card.card-base { background-color: var(--card-bg); box-shadow: 0 3px 10px rgba(61, 52, 40, 0.1); transition: transform 0.3s var(--ease-standard), box-shadow 0.3s var(--ease-standard), background-color 0.15s ease;}.post-card-wrapper.card-base:hover,.album-card.card-base:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(61, 52, 40, 0.14);}.post-card-wrapper .post-card-title { color: var(--deep-text); }.post-card-wrapper a.post-card-title:hover,.post-card-wrapper .post-card-title:hover { color: var(--title-active); }.post-card-wrapper .post-card-title::before { background-color: var(--primary); }.post-card-wrapper .description { color: var(--content-meta); }.album-card .album-card-title { color: var(--deep-text); }.album-card .album-card-title:hover { color: var(--title-active); }banner 横幅没改:它的配色只有「照片上的白字 + 黑投影」,属于图片可读性机制,不在违规用色范围。
五期:Markdown 正文与代码块(2 文件)
真相:文章正文一直吃的是 @tailwindcss/typography 插件的默认冷灰(body = gray-700 蓝灰、标题 = gray-900 冷近黑)。插件的 --tw-prose-* 变量挂在 .prose 类上(注意:这个类藏在 Astro 的 class:list 表达式里,grep class="...prose..." 抓不到)。
markdown.css 末尾用未分层规则整组覆盖,仅亮色生效:
:root:not(.dark) .prose,:root:not(.dark) .custom-md { --tw-prose-body: #725d42; --tw-prose-headings: #794f27; --tw-prose-lead: #794f27; --tw-prose-bold: #5c3a1a; --tw-prose-counters: #9f927d; --tw-prose-bullets: #8a7b66; --tw-prose-hr: #d4c9b4; --tw-prose-quotes: #725d42; --tw-prose-quote-borders: #d4c9b4; --tw-prose-captions: #9f927d; --tw-prose-code: #794f27;}代码块(expressive-code.css):亮色 one-light 默认冷白 #fafafa → 奶油,圆角对齐规范 20px:
html[data-theme='one-light'] body:not(.wallpaper-transparent) .expressive-code .frame { background-color: #f7f3df !important; border: 1px solid rgba(121, 79, 39, 0.14); border-radius: 20px;}壁纸透明模式的既有规则(半透明 --card-bg-transparent)保留,只补了 border-radius: 20px;两种模式条件互斥不冲突。行内代码、表格、blockquote 引条在前几期已随变量自动转暖。
排版期:Nunito 圆体接入(2 文件 + 6 字体)
现状原为:正文 Latin = Work Sans,标题链 = Playfair + 思源宋体(衬线)——与规范「圆体无衬线」完全相反。
- 下载 Nunito 静态 woff2(fontsource 发行文件,latin 子集,6 字重共约 97KB)。jsdelivr/fastly/unpkg 三源轮询,失败回落 npm 塔包:
public/assets/fonts/nunito-latin-{400,500,600,700,800,900}-normal.woff2-
src/config/fontConfig.ts:fontsList新增 Nunito 定义(provider: "local",六个 variants 指向上列文件)selected: ["--font-work-sans", …] → ["--font-nunito", "--font-hanyi-wenhei"](拉丁 Nunito,CJK 回退汉仪文黑)headingFont: ["--font-playfair", "--font-noto-serif-sc"] → ["--font-nunito", "--font-hanyi-wenhei"](处刑衬线链)bannerSubtitleFont: "--font-inter" → "--font-nunito"(横幅主标题 Zen Maru Gothic 本来就是圆体,保留)
-
main.css:@theme的--font-sans→'Nunito', 'Noto Sans SC', …- 正文字重 500(规范 body 500):
@layer base { body { font-weight: 500; }}必须放 base 层——放未分层会因层序压过 utilities 的 font-bold/font-medium,全站粗体全废。
改 fontConfig 后 dev server 必须重启(文件头注释写明),且首次冷启动约 57 秒(配置变更触发 content store 清空重建)——排查「dev 卡死」时先数到 60 秒再下结论。
终验:静态三件套
| 检查 | 结果 |
|---|---|
pnpm type-check | 0 错 |
pnpm check | 12 errors,全部为 anime.astro/vndb.astro/i18n 存量基线(原基线 17),改动文件零新增 |
pnpm build(带 NODE_OPTIONS=--use-system-ca) | exit 0,Pagefind 33 页索引正常 |
浏览器探针法(改样式后即时验证,不开截图也能拿到铁证):
// 计算色值 / 字体 / 圆角getComputedStyle(document.querySelector('.custom-md h2')).colordocument.fonts.check('700 16px Nunito') // 字体是否真加载// 变量挂载点排查:沿祖先链找 --tw-prose-body 在哪一层有值三、关键机制与坑(复刻必读)
- Tailwind v4 层序是本次的核心手法:
@layer utilities>components>base,而未分层 > 所有分层。想让全局换肤盖过模板里散落的工具类(rounded-*、text-black/*),唯一干净解法是把规则写在层外。反过来,想让 utility 还能覆盖(如font-bold),就必须把规则放进base层。 --color-black/neutral/gray三个主题变量是最大杠杆:改 3 行定义 = 全站 240+ 处冷色自动转暖。新写样式前先想「这属于哪个语义族」,能挂族就不写死。data-theme属性是双语义:它同时是「Expressive Code 高亮主题名」(one-light/one-dark-pro)和「友圈 FCL 样式的明暗信号」。想做规范里的深巧克力代码块(#2b2118)需要让亮色模式也用暗色语法主题——会破坏友圈桥接,必须先把 EC 主题选择解耦到独立属性(如themeSelector: 'html[data-ec-theme]')。这是本文搁置的可选工程。localStorage.hue覆盖默认色相:站点机制是存储值优先于siteConfig。移植后旧浏览器仍会用旧色相渲染出「奶油底+琥珀主色」的缝合怪,控制台localStorage.removeItem("hue")后刷新。- grep 会被 Astro 语法骗:
class:list={["prose …"]}里的类名用class="..."模式搜不到,定位样式来源时优先浏览器探针(getComputedStyle),不要只信文本搜索。 - 本机
pnpm是 ps1 脚本,Start-Process -FilePath "pnpm"间歇报「不是有效 Win32 应用」,后台拉起要用pnpm.cmd全路径。 - Playwright 截图会等
document.fonts.ready,Google Fonts 被墙时永不返回——本机验证截图要么先拦截字体域名,要么走 CDPPage.captureScreenshot。
四、复刻清单(从零照做)
- 下载规范:api.github.com 批量拉
skills/animal-island-ui-style/+docs/design-system/到本地参考目录 - 定默认色相:换算目标主色的 oklch 色相,改
siteConfig.themeColor.hue与setting-utils.ts兜底 - 改
variables.styl亮色中性块 + 阴影暖化(对照本文一期映射表) main.css的@theme加入--color-black与 neutral/gray 双阶暖色映射main.css文件末尾(层外) 加按钮 pill / 柔和投影 /btn-primary3D 规则,逐处给真 CTA 追加btn-primary- 导航栏、浮动组件里手写冷黑
rgba(0,0,0,·)的地方换暖棕 main.css层外加卡片全局规则(对照四期代码)markdown.css末尾加:root:not(.dark)守卫的 11 个--tw-prose-*;expressive-code.css加奶油代码块规则- 下载 Nunito woff2 →
fontConfig.ts加定义并切selected/headingFont→@theme --font-sans→@layer base字重 500 - 重启 dev(等足 60 秒冷启动)→
pnpm type-check && pnpm check && pnpm build三件套
五、未尽事项
- 深巧克力代码块(
#2b2118+#3d3028描边):等data-theme解耦,方案见「坑 3」 - 暗色模式:按用户要求未设计;现所有暖色规则以亮色为界,将来做暗色需另出一套映射
- Noto Sans SC 替换汉仪文黑:CJK 已是黑体、视觉差距小,且要接 10MB+ 字体进子集管线,暂无必要
- 筛选器激活态、分页当前页是「状态」而非「动作」,按规范不配 3D,但如要强化主按钮气质可再议
font-showcase.astro/png两个未跟踪文件非本次工程产物,未纳入提交
六、验收状态
七期全部完成并验收:type-check 0 错、check 12 错(全为存量)、build 全链路 exit 0。相关基建另有前作:13文章(阅读进度)、18文章(性能优化)、19文章(导航栏改造)。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!




