8 Commits
Author SHA1 Message Date
nxxy335 c1b08ad915 docs: 补充「升级后部分页面配色不生效」排查记录
根因为「页面静态缓存」插件只随文章变更刷新缓存,主题升级/设置变更
不会失效,旧 HTML 停留在 v1.1.x(无 data-palette、旧 CSS)。
FAQ 新增专项条目与清缓存步骤;README 安装节加指引;changelog 补记。
2026-09-27 17:49:34 +08:00
nxxy335 125d0592c9 fix: 修复升级后整站无样式与点击特效失效
- 旧哈希静态资源随版本升级被删除,被缓存的 HTML 仍请求旧文件名导致
  FileNotFoundException 500,补 compat/assets 历史资源并在构建时合并
- postbuild 增加静态资源引用校验,缺失即构建失败
- ClickEffect 改为常驻配置节点 + data 属性驱动,避免配置未落库时静默失效;
  强制回流修正 transition 不触发;特效层挂到 body 下避免被导航栏遮挡
- 新增 scripts: postbuild/snapshot-assets/package/smoke-clickfx
- 版本 1.2.1
2026-09-27 16:27:51 +08:00
nxxy335 a127d5a314 feat(v1.2.0): 主题配色系统 + Hero 文字效果/一言颜色;修复 /douban 500、友链悬浮光圈裁切、瞬间顶图间距;Halo 版本要求 2.26+
- 新增官方预设调色板 11 套(浅/深双套色板,玻璃/阴影自动跟随),强调色留空跟随配色
- Hero 一言独立颜色配置(浅/深);文字显示效果:阴影/光晕/半透明遮罩/渐变遮罩/容器;默认无效果,移除自带散光
- /douban 适配 plugin-douban 1.2.6 SQLite 扁平化数据结构(item.spec.* -> item.*)
- 友链卡片去掉 overflow:hidden,修复悬浮光圈被裁切;徽章简化为仅显示可达性
- 瞬间页顶图与首条卡片增加间距;theme-color 随配色/明暗联动
2026-09-27 15:16:46 +08:00
nxxy335 a08782a959 fix: 修复记得页 500 与留言板裁切,统一页头,完善友链/瞬间/暗色/排版 (v1.1.12)
记得(/important-dates)
- 修复 500:th:classappend 里 BEM 类名 wi-id__car 的两个 __ 触发 Thymeleaf
  __...__ 预处理,提取片段含引号导致 Could not parse as expression;
  皮肤修饰类改为 wi-id-skin-cool/cute/neutral(表达式内不再出现 __)
- 牵挂的人卡片:倒计时改到右上角与日期卡片一致,生日一行显示不再折行;
  网格改为桌面三列 / 平板两列 / 手机一列
- 页头与订阅/记得统一(标题-副标题-强调线间距)

友链(/links)
- 再次消除两处 __ 预处理隐患:回执修饰类改 wi-links-check-ok/error,
  验证码 onerror 由 th:onerror="|...|" 改为普通静态属性(不参与表达式解析)
- 新增反链/可达性检测徽章:读取 link.status.verification,头像右下角
  绿点=访问正常(反链有效),红点=访问异常或未检测到反链,未检测不显示

留言板
- 移除 -124px 负 margin 补偿(头部卡片顶部被导航盖住的根因),
  装饰引号 top 改为正值不再被 overflow 裁掉

瞬间
- 修复九宫格失效根因:lightgallery 处理时把每张图包进 <a>,导致
  :has(:only-child) 恒真、全部图片退化为单列大图;改为直接把 img
  作为图库项(data-src),瀑布流样式同样补上 4 图 2x2 规则

全局
- 暗色模式配色调为偏中性暖灰(bg #1e1c1a / raised #292623),降低黄褐闷感,
  同步 theme-color 与依赖旧色值的变量
- 正文排版:p 字号 1.05~1.15rem、行高 1.85,H1~H6 逐级放大、层级更清晰

新增「文章组件」插件适配
- 新增 src/styles/_widgets.scss,将插件 --xhhao-com-* 变量映射到主题变量,
  明暗模式自动跟随
2026-09-27 10:09:28 +08:00
nxxy335 28b1f90b26 fix: 修复 /links 页面 500(th:attr 受限求值)并校正友链申请回执 (v1.1.11)
问题:GET /links 抛 TemplateProcessingException ——
"Instantiation of new objects and access to static classes or parameters is forbidden"
定位到 links.html 第 126 行第 978 列:
  th:attr="value=${param.value != null and !#lists.isEmpty(param.value) ? param.value[0] : ''}"
Thymeleaf 对 th:attr 走受限表达式求值,禁止表达式对象的方法调用(#lists.isEmpty 等),
且校验发生在解析阶段,运行时短路(param.value 为 null)也救不了。同样的表达式
放在 th:with 里是合法的 —— 这正是本主题其他页面的既有写法。

修复:
- 全部取值逻辑移入 th:with(采用主题已验证写法 param?.x + x.length > 0 ? x[0] : null),
  输出改用单值属性 th:value,彻底不在 th:attr 里放复杂表达式
- 校正申请回执语义:插件实际回跳参数是 applied=success|error|disabled,
  另有 message / field / value(旧版误判为 applied=true)
  - 统一单条提示:有 message 显示 message,否则成功文案
  - applied=success 用强调色,error 用红色
  - applied=disabled 时隐藏表单,保留「未开放」提示
  - field/value 回填到对应输入框(displayName / url)
- 新增 .wi-links__apply-result--ok 样式
- 版本升至 1.1.11

同时沉淀校验:新增扫描脚本检查构建产物中 th:attr 是否含 #xxx. 方法调用 / new / T() / @bean,
全主题扫描结果为 0。
2026-09-26 20:33:54 +08:00
nxxy335 d3ba189a7d feat: 新增「记得」插件(plugin-important-dates)主题适配页面 (v1.1.10)
- 新增 templates/important-dates.html 覆盖插件默认模板,融入暖屿外观
  - 居中标题区(标题/副标题可配置)+ 强调线,与书架、友链等页面一致
  - 生活 / 爱车双视图切换,地址栏记忆 ?view=car
  - 到期提醒横幅(重要日期 + 座驾到期),支持逐条忽略并记录在本地
  - 重要日期便签卡片:倒计时、日期、农历标记、关联的人
  - 牵挂的人卡片:大头贴(失效回退首字)、昵称、关系、脱敏生日与倒计时
  - 爱车卡片:封面相册、分类品牌、脱敏车牌、到期徽章(临近/逾期)、车主与驾驶人
  - 相册浮层运行时移到 body,避免被 .wi-main 层叠上下文困住而被导航/页脚遮挡
  - 暗黑模式随主题变量自动适配
- settings.yaml 新增 important_dates 设置组(页面标题/副标题)
- 文档:README 插件表、docs/config/important-dates.md、docs/pages/important-dates.md
- 版本升至 1.1.10
2026-09-26 17:30:06 +08:00
nxxy335 9f677a9f0d feat: Hero 背景图完整显示、导航二级菜单、瞬间朋友圈化、友链申请表单 (v1.1.9)
- Hero:背景图 opacity 0.15 -> 1 完整显示(此前近乎透明的背景图是"蒙灰"观感的
  主因);标题/副标题/一言按明暗模式补轻投影保证复杂图片上的可读性
- 导航栏二级菜单:桌面端纯 CSS 悬停/聚焦展开(零 JS),带悬停桥防误关、
  下拉箭头旋转反馈;移动端菜单以缩进子项形式呈现
- 瞬间页朋友圈化:头像(moment.owner.avatar,缺省回退站点名首字占位)+ 昵称
  + 正文 + 九宫格图片 + 时间操作;九宫格遵循微信规则(单图原比例、2/4 图两列、
  其余三列)
- 友情链接页适配 plugin-links 友链申请:同源表单(POST /links/apply/submit +
  csrfToken + GET /links/apply/captcha 验证码图,点击刷新),支持名称/链接/
  Logo/描述/邮箱/反链/RSS 字段、提交结果与错误提示;插件未开放申请时验证码
  加载失败自动隐藏表单;新增 links_apply_form_enabled 开关(默认开启)
2026-09-26 16:14:20 +08:00
nxxy335 c1950e142a fix: 修复笔记弹窗被导航栏和页脚遮挡,书架统计居中 (v1.1.8)
- 笔记弹窗被遮挡:弹窗是 position:fixed 但由 innerHTML 注入在 .wi-main 内,
  .wi-main 自带层叠上下文(z-index:1),弹窗的 z-index:10000 无法超越导航栏
  (z-index:100)和页脚(DOM 靠后的同级 z-index:1)。现注入后将弹窗移至 body 下,
  彻底脱离该层叠上下文
- 弹窗移出 .wr-shelf 后脱离其 --wr-* 变量作用域,为弹窗补独立暗黑适配
  (:global 选择器 + 变量映射 + 面板/关闭按钮/文字兜底),避免深色下回退为墨色文字
- 书架统计数据改为水平居中,紧贴主题标题区(标题+强调线)下方,与全站版式一致
- 暗黑下书架分隔线(--wr-line)比全站 rule 略提亮,年份分组装饰线在深色底可见
2026-09-26 15:29:14 +08:00
67 changed files with 3751 additions and 261 deletions
+36 -5
View File
@@ -7,13 +7,14 @@
## 环境要求 ## 环境要求
- Halo **2.26.0 及以上**(页面布局契约、微信读书页面均依赖 2.26 的新能力) - Halo **2.26.0 及以上**(页面布局契约、微信读书页面均依赖 2.26 的新能力)
- v1.2.0 起内置主题配色系统(纯 CSS 变量实现);更低版本的 Halo 请使用主题 v1.1.x
## 特点 ## 特点
1. **响应式设计** — 适配桌面端与移动端,支持暗色模式,移动端使用动态视口单位适配地址栏 1. **响应式设计** — 适配桌面端与移动端,支持暗色模式,移动端使用动态视口单位适配地址栏
2. **插件兼容性** — 适配 9 款常用 Halo 插件,安装即用;提供页面布局契约,插件前台页面自动复用主题外观 2. **插件兼容性** — 适配 11 款常用 Halo 插件,安装即用;提供页面布局契约,插件前台页面自动复用主题外观
3. **内置灯箱** — 主题内置 LightGallery 灯箱,支持 EXIF 信息展示;安装 lightgallery.js 插件后自动禁用 3. **内置灯箱** — 主题内置 LightGallery 灯箱,支持 EXIF 信息展示;安装 lightgallery.js 插件后自动禁用
4. **可配置性** — 16 个设置分组、117 个配置项,几乎所有模块可独立开关 4. **可配置性** — 17 个设置分组、122 个配置项,几乎所有模块可独立开关
5. **性能优化** — 服务端字数统计(API 扩展包)、图片懒加载与响应式缩略图、滚动吸附、CSS/HTML 压缩 5. **性能优化** — 服务端字数统计(API 扩展包)、图片懒加载与响应式缩略图、滚动吸附、CSS/HTML 压缩
6. **页面布局契约** — 提供 `templates/layout.html`,插件页面不再回退到 Halo 内置布局 6. **页面布局契约** — 提供 `templates/layout.html`,插件页面不再回退到 Halo 内置布局
@@ -30,9 +31,11 @@
| [友情链接](https://halo.run/store/apps/app-hfbQg) | `PluginLinks` | `/links` | 友链展示页面,支持随机排列、随机传送门、评论区开关 | | [友情链接](https://halo.run/store/apps/app-hfbQg) | `PluginLinks` | `/links` | 友链展示页面,支持随机排列、随机传送门、评论区开关 |
| [朋友圈](https://github.com/halo-sigs/plugin-friends) | `plugin-friends` | `/friends` | RSS 订阅朋友圈页面 | | [朋友圈](https://github.com/halo-sigs/plugin-friends) | `plugin-friends` | `/friends` | RSS 订阅朋友圈页面 |
| [装备管理](https://github.com/chengzhongxue/plugin-equipment) | `equipment` | `/equipments` | 装备展示页面,按分组展示,支持封面图 | | [装备管理](https://github.com/chengzhongxue/plugin-equipment) | `equipment` | `/equipments` | 装备展示页面,按分组展示,支持封面图 |
| [豆瓣](https://github.com/chengzhongxue/plugin-douban) | `plugin-douban` | `/douban` | 豆瓣记录页面,支持类型/状态/标签筛选,默认电影 | | [豆瓣](https://github.com/chengzhongxue/plugin-douban) | `plugin-douban` | `/douban` | 豆瓣记录页面,支持类型/状态/标签筛选,默认音乐;需插件 **v1.2.6+**(SQLite 数据结构) |
| [Steam 信息展示](https://github.com/Tim0x0/halo-plugin-steam) | `PluginSteam` | `/steam` | Steam 游戏库页面,含用户资料、统计数据、最近游玩、游戏库分页 | | [Steam 信息展示](https://github.com/Tim0x0/halo-plugin-steam) | `PluginSteam` | `/steam` | Steam 游戏库页面,含用户资料、统计数据、最近游玩、游戏库分页 |
| [微信读书](https://github.com/yeora26/halo-weread-plugin) | `halo-weread-plugin` | 自定义页面 | 书架展示页面,见下方「自定义页面模板」 | | [微信读书](https://github.com/yeora26/halo-weread-plugin) | `halo-weread-plugin` | 自定义页面 | 书架展示页面,见下方「自定义页面模板」 |
| [记得](https://github.com/yyliucha/plugin-important-dates) | `plugin-important-dates` | `/important-dates` | 重要日期 / 牵挂的人 / 爱车双视图页面,见下方「扩展页面」 |
| [文章组件](https://github.com/acanyo/plugin-content-widgets) | `plugin-content-widgets` | 文章内 | 提示框、标签页、时间线等内容组件,主题已映射 `--xhhao-com-*` 变量,自动跟随明暗模式 |
| [API 扩展包](https://github.com/HowieHz/halo-plugin-extra-api) | `extra-api` | 全站 | 服务端字数统计、中英文混排排版、可选服务端代码高亮 | | [API 扩展包](https://github.com/HowieHz/halo-plugin-extra-api) | `extra-api` | 全站 | 服务端字数统计、中英文混排排版、可选服务端代码高亮 |
> **注意**:插件需要单独安装,安装后主题会自动适配对应页面。若未安装某插件,对应页面或功能自动隐藏/降级,不会报错。 > **注意**:插件需要单独安装,安装后主题会自动适配对应页面。若未安装某插件,对应页面或功能自动隐藏/降级,不会报错。
@@ -94,6 +97,15 @@
2. 进入 Halo 后台 → 主题管理 → 安装主题 → 上传 ZIP 包 2. 进入 Halo 后台 → 主题管理 → 安装主题 → 上传 ZIP 包
3. 安装完成后点击「启用」 3. 安装完成后点击「启用」
> **升级后如果整站没了样式**:Halo 页面缓存或前置反向代理里可能还留着上一版本的 HTML,
> 它引用的 CSS 文件名带内容哈希,升级后已被删除,请求会直接 500。
> 先在 Halo 后台执行一次「清理缓存」(并刷新 CDN / Nginx 缓存)即可。
> 主题从 v1.2.1 起会把历史版本的哈希资源一并发进安装包,旧 HTML 也不会请求失败。
>
> **升级主题或修改配色后,部分页面不生效**:如果安装了「页面静态缓存」插件,
> 它只随文章变更自动刷新,主题层的变更需要手动点一次「刷新页面缓存」,
> 详见 [FAQ:升级主题或修改配色后部分页面不生效](docs/faq.md#升级主题或修改配色后部分页面的新样式配色不生效)。
或从源码构建: 或从源码构建:
```bash ```bash
@@ -114,10 +126,21 @@ pnpm install
# 开发模式(监听文件变化,自动构建到 templates/) # 开发模式(监听文件变化,自动构建到 templates/)
pnpm dev pnpm dev
# 构建 # 构建(输出到 templates/,并自动合并历史版本的静态资源)
pnpm build pnpm build
# 把本次构建的哈希资源快照进 compat/assets(发版前必做)
pnpm snapshot
# 打包 dist/warm-island-<version>.zip
pnpm package
# 点击特效脚本冒烟测试
pnpm smoke
``` ```
发版流程:`pnpm build` → `pnpm snapshot` → `pnpm package`,并把 `compat/assets` 的新增文件一起提交。
### 目录结构 ### 目录结构
``` ```
@@ -126,16 +149,24 @@ pnpm build
│ ├── layouts/ # Layout.astro(含页面布局契约渲染模式) │ ├── layouts/ # Layout.astro(含页面布局契约渲染模式)
│ ├── pages/ # 页面模板(构建后输出到 templates/) │ ├── pages/ # 页面模板(构建后输出到 templates/)
│ └── styles/ # SCSS 样式(正文样式在 _content.scss,全局生效) │ └── styles/ # SCSS 样式(正文样式在 _content.scss,全局生效)
├── compat/
│ └── assets/ # 历史版本哈希静态资源,构建时合并进 templates/assets
├── scripts/ # 构建后置、资源快照、打包、冒烟测试
├── templates/ # 构建产物,Halo 实际加载的模板 ├── templates/ # 构建产物,Halo 实际加载的模板
├── settings.yaml # 后台设置项定义(16 分组 / 117 项) ├── settings.yaml # 后台设置项定义
└── theme.yaml # 主题清单与自定义页面模板注册 └── theme.yaml # 主题清单与自定义页面模板注册
``` ```
### 主题开发注意事项 ### 主题开发注意事项
- **静态资源文件名带内容哈希**:升级后旧文件被删除,被缓存的 HTML 仍会请求旧文件名并触发 500,
所以每个版本的哈希文件都要留一份(见 `compat/assets` 与 `pnpm snapshot`)
- **Thymeleaf 字符串字面量必须加单引号**:`th:text="'文本'"`,裸中文遇到逗号/空格/emoji 会导致页面 500 - **Thymeleaf 字符串字面量必须加单引号**:`th:text="'文本'"`,裸中文遇到逗号/空格/emoji 会导致页面 500
- **`th:attr` 只用简单表达式**(取属性、Elvis 默认值、三元、拼接、等值比较),表达式对象方法调用会被受限求值器拒绝
- **凡是与后台开关绑定的模板表达式都要给默认值**:`?: '默认值'`,配置未落库时属性会被整体移除,功能会静默失效
- **正文样式必须写在全局样式表**(`src/styles/_content.scss`):Astro scoped 样式对 Thymeleaf 运行时注入的内容无效 - **正文样式必须写在全局样式表**(`src/styles/_content.scss`):Astro scoped 样式对 Thymeleaf 运行时注入的内容无效
- **内联脚本统一加 `th:inline="none"`**:避免 Thymeleaf 解析脚本中的 `[[` / `[(` / `${}` - **内联脚本统一加 `th:inline="none"`**:避免 Thymeleaf 解析脚本中的 `[[` / `[(` / `${}`
- **新插入的动画元素要强制回流**(读一次 `offsetWidth`):否则初态与终态在同一帧被合并,transition 不触发
- 修改主题外观无需单独维护 `layout.astro`:页面布局契约复用 `Layout.astro`,构建时自动生成 - 修改主题外观无需单独维护 `layout.astro`:页面布局契约复用 `Layout.astro`,构建时自动生成
## 技术栈 ## 技术栈
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
.wi-douban[data-astro-cid-nevwzhq2]{padding-block:1rem}.wi-douban__header[data-astro-cid-nevwzhq2]{text-align:center;margin-bottom:var(--space-3xl)}.wi-douban__title[data-astro-cid-nevwzhq2]{font-size:var(--text-4xl);margin-bottom:var(--space-sm)}.wi-page-accent[data-astro-cid-nevwzhq2]{display:block;width:48px;height:3px;margin:var(--space-sm) auto 0;border-radius:9999px;background:var(--accent)}.wi-douban__filters[data-astro-cid-nevwzhq2]{display:flex;flex-direction:column;gap:var(--space-md);margin-bottom:var(--space-2xl)}.wi-douban__filter-group[data-astro-cid-nevwzhq2]{display:flex;flex-wrap:wrap;gap:8px;justify-content:center}.wi-douban__filter-tag[data-astro-cid-nevwzhq2]{display:inline-block;padding:6px 16px;border-radius:20px;font-size:var(--text-sm);color:var(--ink-2);background:var(--bg-raised);border:1px solid var(--rule);text-decoration:none;transition:color var(--duration-fast) var(--ease-out-quart),background var(--duration-fast) var(--ease-out-quart),border-color var(--duration-fast) var(--ease-out-quart)}.wi-douban__filter-tag--sm[data-astro-cid-nevwzhq2]{padding:4px 12px;font-size:var(--text-xs)}.wi-douban__filter-tag[data-astro-cid-nevwzhq2]:hover{color:var(--accent);border-color:var(--accent)}.wi-douban__filter-tag--active[data-astro-cid-nevwzhq2]{color:#fff;background:var(--accent);border-color:var(--accent)}.wi-douban__grid[data-astro-cid-nevwzhq2]{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:var(--space-lg)}.wi-douban__card[data-astro-cid-nevwzhq2]{position:relative;display:flex;gap:var(--space-md);padding:var(--space-lg);background:var(--bg-raised);border:1px solid var(--rule);border-radius:16px;transition:transform var(--duration-normal) var(--ease-out-expo),box-shadow var(--duration-normal) var(--ease-out-expo),border-color var(--duration-normal) var(--ease-out-expo)}.wi-douban__card[data-astro-cid-nevwzhq2]:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg);border-color:var(--accent)}.wi-douban__card-link[data-astro-cid-nevwzhq2]{position:absolute;inset:0;z-index:1}.wi-douban__poster[data-astro-cid-nevwzhq2]{flex-shrink:0;width:80px;height:112px;border-radius:8px;overflow:hidden;background:var(--bg)}.wi-douban__card[data-astro-cid-nevwzhq2][data-type=music] .wi-douban__poster[data-astro-cid-nevwzhq2]{width:80px;height:80px;border-radius:8px}.wi-douban__poster-img[data-astro-cid-nevwzhq2]{width:100%;height:100%;object-fit:cover}.wi-douban__poster--placeholder[data-astro-cid-nevwzhq2]{display:flex;align-items:center;justify-content:center;background:var(--accent-bg);color:var(--accent);font-size:var(--text-2xl);font-weight:700}.wi-douban__body[data-astro-cid-nevwzhq2]{display:flex;flex-direction:column;gap:4px;min-width:0;flex:1}.wi-douban__name[data-astro-cid-nevwzhq2]{font-size:var(--text-md);font-weight:600;color:var(--ink);line-height:var(--leading-tight);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wi-douban__subtitle[data-astro-cid-nevwzhq2]{font-size:var(--text-xs);color:var(--ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wi-douban__meta[data-astro-cid-nevwzhq2]{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:var(--text-xs);color:var(--ink-3)}.wi-douban__score[data-astro-cid-nevwzhq2]{display:inline-flex;align-items:center;gap:2px;color:#f59e0b;font-weight:600}.wi-douban__star[data-astro-cid-nevwzhq2]{color:#f59e0b}.wi-douban__my-score[data-astro-cid-nevwzhq2]{color:var(--accent);font-weight:500}.wi-douban__tags[data-astro-cid-nevwzhq2]{display:flex;flex-wrap:wrap;gap:4px}.wi-douban__tag[data-astro-cid-nevwzhq2]{font-size:11px;color:var(--ink-3);padding:1px 6px;border-radius:4px;background:var(--bg);border:1px solid var(--rule)}.wi-douban__remark[data-astro-cid-nevwzhq2]{font-size:var(--text-sm);color:var(--ink-2);line-height:var(--leading-relaxed);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;margin-top:4px;position:relative;z-index:2}@media(max-width:640px){.wi-douban__grid[data-astro-cid-nevwzhq2]{grid-template-columns:1fr}.wi-douban__card[data-astro-cid-nevwzhq2]{padding:var(--space-md)}.wi-douban__poster[data-astro-cid-nevwzhq2]{width:64px;height:90px}.wi-douban__card[data-astro-cid-nevwzhq2][data-type=music] .wi-douban__poster[data-astro-cid-nevwzhq2]{width:64px;height:64px}}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+54
View File
@@ -0,0 +1,54 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>
<json>
<![CDATA[
{
"fontFamily": "lg",
"majorVersion": 2,
"minorVersion": 0,
"fontURL": "",
"copyright": "",
"license": "",
"licenseURL": "",
"description": "Font generated by IcoMoon.",
"version": "Version 2.0",
"fontId": "lg",
"psName": "lg",
"subFamily": "Regular",
"fullName": "lg"
}
]]>
</json>
</metadata>
<defs>
<font id="lg" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe01a;" glyph-name="pause_circle_outline" data-tags="pause_circle_outline" d="M554 256.667v340h86v-340h-86zM512 84.667q140 0 241 101t101 241-101 241-241 101-241-101-101-241 101-241 241-101zM512 852.667q176 0 301-125t125-301-125-301-301-125-301 125-125 301 125 301 301 125zM384 256.667v340h86v-340h-86z" />
<glyph unicode="&#xe01d;" glyph-name="play_circle_outline" data-tags="play_circle_outline" d="M512 84.667q140 0 241 101t101 241-101 241-241 101-241-101-101-241 101-241 241-101zM512 852.667q176 0 301-125t125-301-125-301-301-125-301 125-125 301 125 301 301 125zM426 234.667v384l256-192z" />
<glyph unicode="&#xe033;" glyph-name="stack-2" data-tags="stack-2" d="M384 853.334h426.667q53 0 90.5-37.5t37.5-90.5v-426.667q0-53-37.5-90.5t-90.5-37.5h-426.667q-53 0-90.5 37.5t-37.5 90.5v426.667q0 53 37.5 90.5t90.5 37.5zM170.667 675.334v-547.333q0-17.667 12.5-30.167t30.167-12.5h547.333q-13.333-37.667-46.333-61.5t-74.333-23.833h-426.667q-53 0-90.5 37.5t-37.5 90.5v426.667q0 41.333 23.833 74.333t61.5 46.333zM810.667 768h-426.667q-17.667 0-30.167-12.5t-12.5-30.167v-426.667q0-17.667 12.5-30.167t30.167-12.5h426.667q17.667 0 30.167 12.5t12.5 30.167v426.667q0 17.667-12.5 30.167t-30.167 12.5z" />
<glyph unicode="&#xe070;" glyph-name="clear" data-tags="clear" d="M810 664.667l-238-238 238-238-60-60-238 238-238-238-60 60 238 238-238 238 60 60 238-238 238 238z" />
<glyph unicode="&#xe094;" glyph-name="arrow-left" data-tags="arrow-left" d="M426.667 768q17.667 0 30.167-12.5t12.5-30.167q0-18-12.667-30.333l-225.667-225.667h665q17.667 0 30.167-12.5t12.5-30.167-12.5-30.167-30.167-12.5h-665l225.667-225.667q12.667-12.333 12.667-30.333 0-17.667-12.5-30.167t-30.167-12.5q-18 0-30.333 12.333l-298.667 298.667q-12.333 13-12.333 30.333t12.333 30.333l298.667 298.667q12.667 12.333 30.333 12.333z" />
<glyph unicode="&#xe095;" glyph-name="arrow-right" data-tags="arrow-right" d="M597.333 768q18 0 30.333-12.333l298.667-298.667q12.333-12.333 12.333-30.333t-12.333-30.333l-298.667-298.667q-12.333-12.333-30.333-12.333-18.333 0-30.5 12.167t-12.167 30.5q0 18 12.333 30.333l226 225.667h-665q-17.667 0-30.167 12.5t-12.5 30.167 12.5 30.167 30.167 12.5h665l-226 225.667q-12.333 12.333-12.333 30.333 0 18.333 12.167 30.5t30.5 12.167z" />
<glyph unicode="&#xe0f2;" glyph-name="vertical_align_bottom" data-tags="vertical_align_bottom" d="M170 128.667h684v-86h-684v86zM682 384.667l-170-172-170 172h128v426h84v-426h128z" />
<glyph unicode="&#xe1ff;" glyph-name="apps" data-tags="apps" d="M682 84.667v172h172v-172h-172zM682 340.667v172h172v-172h-172zM426 596.667v172h172v-172h-172zM682 768.667h172v-172h-172v172zM426 340.667v172h172v-172h-172zM170 340.667v172h172v-172h-172zM170 84.667v172h172v-172h-172zM426 84.667v172h172v-172h-172zM170 596.667v172h172v-172h-172z" />
<glyph unicode="&#xe20c;" glyph-name="fullscreen" data-tags="fullscreen" d="M598 724.667h212v-212h-84v128h-128v84zM726 212.667v128h84v-212h-212v84h128zM214 512.667v212h212v-84h-128v-128h-84zM298 340.667v-128h128v-84h-212v212h84z" />
<glyph unicode="&#xe20d;" glyph-name="fullscreen_exit" data-tags="fullscreen_exit" d="M682 596.667h128v-84h-212v212h84v-128zM598 128.667v212h212v-84h-128v-128h-84zM342 596.667v128h84v-212h-212v84h128zM214 256.667v84h212v-212h-84v128h-128z" />
<glyph unicode="&#xe311;" glyph-name="zoom_in" data-tags="zoom_in" d="M512 512.667h-86v-86h-42v86h-86v42h86v86h42v-86h86v-42zM406 340.667q80 0 136 56t56 136-56 136-136 56-136-56-56-136 56-136 136-56zM662 340.667l212-212-64-64-212 212v34l-12 12q-76-66-180-66-116 0-197 80t-81 196 81 197 197 81 196-81 80-197q0-104-66-180l12-12h34z" />
<glyph unicode="&#xe312;" glyph-name="zoom_out" data-tags="zoom_out" d="M298 554.667h214v-42h-214v42zM406 340.667q80 0 136 56t56 136-56 136-136 56-136-56-56-136 56-136 136-56zM662 340.667l212-212-64-64-212 212v34l-12 12q-76-66-180-66-116 0-197 80t-81 196 81 197 197 81 196-81 80-197q0-104-66-180l12-12h34z" />
<glyph unicode="&#xe80d;" glyph-name="share" data-tags="share" d="M768 252.667c68 0 124-56 124-124s-56-126-124-126-124 58-124 126c0 10 0 20 2 28l-302 176c-24-22-54-34-88-34-70 0-128 58-128 128s58 128 128 128c34 0 64-12 88-34l300 174c-2 10-4 20-4 30 0 70 58 128 128 128s128-58 128-128-58-128-128-128c-34 0-64 14-88 36l-300-176c2-10 4-20 4-30s-2-20-4-30l304-176c22 20 52 32 84 32z" />
<glyph unicode="&#xe900;" glyph-name="rotate_left" data-tags="rotate_left" d="M554 764.667q126-16 213-112t87-226-87-226-213-112v86q92 16 153 87t61 165-61 165-153 87v-166l-194 190 194 194v-132zM302 156.667l62 62q46-34 106-44v-86q-96 12-168 68zM260 384.667q10-58 42-106l-60-60q-56 74-68 166h86zM304 574.667q-36-52-44-106h-86q12 90 70 166z" />
<glyph unicode="&#xe901;" glyph-name="rotate_right" data-tags="rotate_right" d="M720 278.667q34 46 44 106h86q-12-92-68-166zM554 174.667q60 10 106 44l62-62q-72-56-168-68v86zM850 468.667h-86q-10 60-44 106l62 60q58-72 68-166zM664 702.667l-194-190v166q-92-16-153-87t-61-165 61-165 153-87v-86q-126 16-213 112t-87 226 87 226 213 112v132z" />
<glyph unicode="&#xe902;" glyph-name="swap_horiz" data-tags="swap_horiz" d="M896 554.667l-170-170v128h-300v84h300v128zM298 468.667v-128h300v-84h-300v-128l-170 170z" />
<glyph unicode="&#xe903;" glyph-name="swap_vert" data-tags="swap_vert" d="M384 810.667l170-170h-128v-300h-84v300h-128zM682 212.667h128l-170-170-170 170h128v300h84v-300z" />
<glyph unicode="&#xe904;" glyph-name="facebook-with-circle" data-tags="facebook-with-circle" d="M512 952.32c-271.462 0-491.52-220.058-491.52-491.52s220.058-491.52 491.52-491.52 491.52 220.058 491.52 491.52-220.058 491.52-491.52 491.52zM628.429 612.659h-73.882c-8.755 0-18.483-11.52-18.483-26.829v-53.35h92.416l-13.978-76.083h-78.438v-228.403h-87.194v228.403h-79.104v76.083h79.104v44.749c0 64.205 44.544 116.378 105.677 116.378h73.882v-80.947z" />
<glyph unicode="&#xe905;" glyph-name="google-with-circle" data-tags="google+-with-circle" d="M512 952.32c-271.462 0-491.52-220.058-491.52-491.52s220.058-491.52 491.52-491.52 491.52 220.058 491.52 491.52-220.058 491.52-491.52 491.52zM483.686 249.805c-30.874-15.002-64.102-16.589-76.954-16.589-2.458 0-3.84 0-3.84 0s-1.178 0-2.765 0c-20.070 0-119.962 4.608-119.962 95.59 0 89.395 108.8 96.41 142.131 96.41h0.87c-19.251 25.702-15.258 51.61-15.258 51.61-1.69-0.102-4.147-0.205-7.168-0.205-12.544 0-36.762 1.997-57.549 15.411-25.498 16.384-38.4 44.288-38.4 82.893 0 109.107 119.142 113.51 120.32 113.613h118.989v-2.611c0-13.312-23.91-15.923-40.192-18.125-5.53-0.819-16.64-1.894-19.763-3.482 30.157-16.128 35.021-41.421 35.021-79.104 0-42.906-16.794-65.587-34.611-81.51-11.059-9.882-19.712-17.613-19.712-28.006 0-10.189 11.878-20.582 25.702-32.717 22.579-19.917 53.555-47.002 53.555-92.723 0-47.258-20.326-81.050-60.416-100.454zM742.4 460.8h-76.8v-76.8h-51.2v76.8h-76.8v51.2h76.8v76.8h51.2v-76.8h76.8v-51.2zM421.018 401.92c-2.662 0-5.325-0.102-8.038-0.307-22.733-1.69-43.725-10.189-58.88-24.013-15.053-13.619-22.733-30.822-21.658-48.179 2.304-36.403 41.37-57.702 88.832-54.323 46.694 3.379 77.824 30.31 75.571 66.714-2.15 34.202-31.898 60.109-75.827 60.109zM465.766 599.808c-12.39 43.52-32.358 56.422-63.386 56.422-3.328 0-6.707-0.512-9.933-1.382-13.466-3.84-24.166-15.053-30.106-31.744-6.093-16.896-6.451-34.509-1.229-54.579 9.472-35.891 34.97-61.901 60.672-61.901 3.379 0 6.758 0.41 9.933 1.382 28.109 7.885 45.722 50.79 34.048 91.802z" />
<glyph unicode="&#xe906;" glyph-name="pinterest-with-circle" data-tags="pinterest-with-circle" d="M512 952.32c-271.462 0-491.52-220.058-491.52-491.52s220.058-491.52 491.52-491.52 491.52 220.058 491.52 491.52-220.058 491.52-491.52 491.52zM545.638 344.32c-31.539 2.406-44.749 18.022-69.427 32.973-13.568-71.219-30.157-139.52-79.309-175.206-15.206 107.725 22.221 188.518 39.629 274.381-29.645 49.92 3.533 150.323 66.099 125.645 76.954-30.515-66.662-185.6 29.747-205.005 100.659-20.173 141.773 174.694 79.36 237.978-90.214 91.494-262.502 2.099-241.306-128.87 5.12-32 38.246-41.728 13.21-85.914-57.702 12.8-74.957 58.317-72.704 118.989 3.533 99.328 89.242 168.909 175.155 178.483 108.698 12.083 210.688-39.885 224.819-142.182 15.821-115.405-49.101-240.282-165.274-231.27z" />
<glyph unicode="&#xe907;" glyph-name="twitter-with-circle" data-tags="twitter-with-circle" d="M512 952.32c-271.462 0-491.52-220.058-491.52-491.52s220.058-491.52 491.52-491.52 491.52 220.058 491.52 491.52-220.058 491.52-491.52 491.52zM711.936 549.683c0.205-4.198 0.256-8.397 0.256-12.493 0-128-97.331-275.507-275.405-275.507-54.682 0-105.574 15.974-148.378 43.52 7.526-0.922 15.258-1.28 23.091-1.28 45.363 0 87.091 15.411 120.218 41.421-42.342 0.819-78.080 28.774-90.419 67.174 5.888-1.075 11.93-1.69 18.176-1.69 8.806 0 17.408 1.178 25.498 3.379-44.288 8.909-77.67 48.026-77.67 94.925v1.178c13.056-7.219 28.006-11.622 43.878-12.134-26.010 17.408-43.059 47.002-43.059 80.64 0 17.715 4.762 34.406 13.107 48.691 47.77-58.573 119.040-97.075 199.526-101.222-1.69 7.117-2.509 14.49-2.509 22.118 0 53.402 43.315 96.819 96.819 96.819 27.802 0 52.992-11.776 70.656-30.618 22.067 4.403 42.752 12.39 61.44 23.501-7.219-22.579-22.528-41.574-42.547-53.606 19.61 2.406 38.246 7.578 55.603 15.309-12.954-19.405-29.389-36.506-48.282-50.125z" />
<glyph unicode="&#xe908;" glyph-name="message-circle" data-tags="message-circle" d="M938.667 448.128v21.205c0 0.725-0.043 1.621-0.085 2.475-5.803 99.755-47.488 190.336-112.725 258.176-68.352 71.125-162.731 117.419-268.843 123.264-0.683 0.043-1.536 0.085-2.347 0.085h-20.864c-59.947 0.683-122.965-13.227-181.931-43.008-52.181-26.496-97.749-63.488-133.931-108.16-56.405-69.717-89.899-158.080-89.941-253.696-0.597-54.4 10.795-111.36 35.157-165.419l-75.605-226.859c-2.816-8.363-3.072-17.835 0-26.965 7.467-22.357 31.616-34.432 53.973-26.965l226.731 75.563c49.493-22.485 105.984-35.243 165.376-35.115 58.539 0.384 115.797 13.141 168.149 36.949 81.579 37.163 151.040 101.248 193.749 186.667 27.477 53.291 43.307 115.84 43.136 181.803zM853.333 447.872c0.128-52.267-12.459-101.333-33.664-142.464-34.176-68.352-88.832-118.827-153.259-148.139-41.387-18.859-86.827-28.971-133.376-29.269-52.096-0.128-101.163 12.459-142.293 33.664-10.624 5.504-22.528 6.059-33.067 2.56l-162.261-54.101 54.101 162.261c3.755 11.221 2.56 22.912-2.389 32.725-23.552 46.677-34.304 96.171-33.792 142.421 0.043 76.331 26.411 145.92 70.955 200.917 28.629 35.371 64.768 64.725 106.24 85.76 46.592 23.552 96.085 34.304 142.336 33.792h19.456c83.712-4.565 158.037-41.003 212.011-97.152 51.285-53.376 84.139-124.416 89.003-202.795z" />
<glyph unicode="&#xe909;" glyph-name="maximize-2" data-tags="maximize-2" d="M793.003 768l-225.835-225.835c-16.683-16.683-16.683-43.691 0-60.331s43.691-16.683 60.331 0l225.835 225.835v-153.003c0-23.552 19.115-42.667 42.667-42.667s42.667 19.115 42.667 42.667v256c0 5.803-1.152 11.307-3.243 16.341s-5.163 9.728-9.216 13.781c-0.043 0.043-0.043 0.043-0.085 0.085-3.925 3.925-8.619 7.083-13.781 9.216-5.035 2.091-10.539 3.243-16.341 3.243h-256c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667zM230.997 85.334l225.835 225.835c16.683 16.683 16.683 43.691 0 60.331s-43.691 16.683-60.331 0l-225.835-225.835v153.003c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667v-256c0-23.552 19.115-42.667 42.667-42.667h256c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667z" />
<glyph unicode="&#xe90a;" glyph-name="minimize-2" data-tags="minimize-2" d="M700.331 554.667l225.835 225.835c16.683 16.683 16.683 43.691 0 60.331s-43.691 16.683-60.331 0l-225.835-225.835v153.003c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667v-256c0-5.803 1.152-11.307 3.243-16.341s5.163-9.728 9.216-13.781c0.043-0.043 0.043-0.043 0.085-0.085 3.925-3.925 8.619-7.083 13.781-9.216 5.035-2.091 10.539-3.243 16.341-3.243h256c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667zM158.165 12.502l225.835 225.835v-153.003c0-23.552 19.115-42.667 42.667-42.667s42.667 19.115 42.667 42.667v256c0 5.803-1.152 11.307-3.243 16.341s-5.163 9.728-9.216 13.781c-0.043 0.043-0.043 0.043-0.085 0.085-4.096 4.053-8.789 7.125-13.781 9.216-5.035 2.091-10.539 3.243-16.341 3.243h-256c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667h153.003l-225.835-225.835c-16.683-16.683-16.683-43.691 0-60.331s43.691-16.683 60.331 0z" />
</font></defs></svg>

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
@@ -36,6 +36,7 @@ export default defineConfig({
{ text: '图库', link: '/config/photos' }, { text: '图库', link: '/config/photos' },
{ text: '友情链接', link: '/config/links' }, { text: '友情链接', link: '/config/links' },
{ text: '朋友圈', link: '/config/friends' }, { text: '朋友圈', link: '/config/friends' },
{ text: '记得', link: '/config/important-dates' },
{ text: '留言板', link: '/config/messageboard' }, { text: '留言板', link: '/config/messageboard' },
{ text: '评论', link: '/config/comment' }, { text: '评论', link: '/config/comment' },
{ text: '页脚', link: '/config/footer' }, { text: '页脚', link: '/config/footer' },
@@ -53,6 +54,7 @@ export default defineConfig({
{ text: '图库', link: '/pages/photos' }, { text: '图库', link: '/pages/photos' },
{ text: '友情链接', link: '/pages/links' }, { text: '友情链接', link: '/pages/links' },
{ text: '朋友圈', link: '/pages/friends' }, { text: '朋友圈', link: '/pages/friends' },
{ text: '记得', link: '/pages/important-dates' },
{ text: '自定义页面', link: '/pages/custom-page' }, { text: '自定义页面', link: '/pages/custom-page' },
{ text: '扩展页面', link: '/pages/extended' }, { text: '扩展页面', link: '/pages/extended' },
] ]
+59
View File
@@ -1,5 +1,64 @@
# 更新日志 # 更新日志
## v1.2.1
### 问题修复
- 🐛 **修复升级后整站没有样式**:Astro 会给 CSS/JS 加内容哈希,升级后旧文件被删除,被缓存的 HTML 仍请求旧文件名,
Halo 直接抛 `FileNotFoundException` 返回 500,样式全部丢失。主题现在会把各版本的哈希资源一并保留
(`compat/assets` 在构建时合并进 `templates/assets`,已覆盖 v1.1.7 起的所有版本)
- 🐛 **避免页面出现 500**:新增构建期静态资源引用校验,模板引用的任一缺失资源都会让构建失败
- 🐛 **修复鼠标点击效果开启后没有任何作用**:旧实现用 `th:if` 判断开关、且直接读取配置值,
一旦配置项未随版本写入(或被 Thymeleaf 整体移除属性)就读取为空,脚本直接跳过;
同时修复了多种特效在插入节点后随即改写 `transform` / `opacity`,导致浏览器把初态与终态合并、
transition 完全不触发的问题(表现为点了没反应)
- 🐛 次要修复:点击特效节点统一挂载到 `body` 下的独立图层(`z-index: 12050`),不再被导航栏、页脚遮挡
### 其他
- 🧰 新增构建脚本:`pnpm snapshot`(历史资源快照)、`pnpm package`(打安装包)、`pnpm smoke`(点击特效冒烟测试)
### 补充排查记录:升级后部分页面配色/新样式不生效
上线后收到反馈:更换主题配色后,**首页与瞬间页不跟随**,其他页面正常。经排查
(对比浏览器实际加载的资源、CDP 读取计算样式、比对响应头)确认:
- 浏览器拿到的首页/瞬间 HTML 是 **v1.1.11 / v1.1.12 时期**的旧页面(引用旧哈希 CSS、
`<html>` 上没有 v1.2.0 新增的 `data-palette` 属性)
- 响应头 `X-Halo-Cache-At` 与 `Cache-Control: max-age=3, s-maxage=120` 表明这些页面
被 **「页面静态缓存」插件** 缓存,而该插件只随文章变更自动刷新,
**主题升级 / 主题设置变更不会刷新缓存**
- 裸 `curl`(无浏览器请求头)会绕过该缓存拿到新 HTML,因此服务端"看起来"一切正常
这是缓存层行为、不是主题缺陷,**在 Halo 后台点一次「刷新页面缓存」即可**;
排查步骤已写入 [FAQ](faq.md#升级主题或修改配色后部分页面的新样式配色不生效)。
## v1.2.0
### 主题配色系统
- ✨ 新增「主题配色」设置(总体样式 → 主题配色),内置 11 套官方预调配色:暖屿·赤陶(默认)、抹茶、黛蓝、樱粉、暮紫、海盐、琥珀、梅酒、石墨、可拿铁、天青
- 🎨 每套配色包含浅色 / 深色两套完整色板,玻璃拟态、遮罩、阴影色调自动跟随当前配色
- 🎨 自定义「强调色」改为留空跟随所选配色;填写后仍可单独覆盖
- 📱 浏览器地址栏 theme-color 随配色与明暗模式联动(含切换按钮)
### Hero 首屏
- ✨ 一言文字新增独立颜色配置(浅色 / 深色模式各一项,此前只能跟随副标题颜色)
- ✨ 新增「文字显示效果」设置:阴影 / 光晕 / 半透明遮罩层 / 渐变遮罩 / 容器(半透明边框圆角矩形 + 加大留白)
- 🐛 默认改为无任何效果:移除标题、副标题与一言自带的白色散光投影
### 问题修复
- 🐛 修复 /douban 页面 500:适配豆瓣插件 v1.2.6 的 SQLite 数据结构(字段由 `item.spec.*` / `item.faves.*` 扁平化为 `item.*` / `item.faves*`)
- 🐛 修复友情链接卡片悬浮光圈被裁切的问题(卡片容器不再裁切悬浮阴影与上移位移)
- 🐛 修复瞬间页面顶图与最新一条瞬间卡片的边框贴合的问题,顶图下方增加间距
- 🐛 友情链接卡片徽章简化为仅展示站点可达性(绿点 = 可访问,红点 = 异常),不再联合反链检测结果判断
### 其他
- ⬆️ Halo 版本要求提升至 **2.26+**
## v1.1.1 ## v1.1.1
### 瞬间页面 ### 瞬间页面
+35
View File
@@ -13,7 +13,17 @@ Hero 首屏是 WarmIsland 主题的标志性区域,占据首页首屏的全部
| `hero_hitokoto_api` | URL | `https://v1.hitokoto.cn/` | 一言 API 地址 | | `hero_hitokoto_api` | URL | `https://v1.hitokoto.cn/` | 一言 API 地址 |
| `hero_hitokoto_categories` | 多选 | `d, i, k` | 一言句子类型 | | `hero_hitokoto_categories` | 多选 | `d, i, k` | 一言句子类型 |
| `hero_custom_description` | 多行文本 | — | 自定义描述文案 | | `hero_custom_description` | 多行文本 | — | 自定义描述文案 |
| `hero_hitokoto_auto_refresh` | 开关 | `false` | 一言自动刷新 |
| `hero_hitokoto_refresh_interval` | 下拉 | `10000` | 一言刷新间隔(秒) |
| `hero_background_image` | 附件 | — | 背景图片 | | `hero_background_image` | 附件 | — | 背景图片 |
| `hero_background_image_dark` | 附件 | — | 深色模式背景图片 |
| `hero_title_color_light` | 颜色 | — | 大标题颜色(浅色模式),留空使用主题默认 |
| `hero_title_color_dark` | 颜色 | — | 大标题颜色(深色模式),留空使用主题默认 |
| `hero_subtitle_color_light` | 颜色 | — | 副标题颜色(浅色模式),留空使用主题默认 |
| `hero_subtitle_color_dark` | 颜色 | — | 副标题颜色(深色模式),留空使用主题默认 |
| `hero_quote_color_light` | 颜色 | — | 一言文字颜色(浅色模式),留空使用主题默认 |
| `hero_quote_color_dark` | 颜色 | — | 一言文字颜色(深色模式),留空使用主题默认 |
| `hero_text_effect` | 下拉 | `none` | 文字显示效果:`none` / `shadow` / `glow` / `scrim` / `gradient` / `frame` |
## 详细说明 ## 详细说明
@@ -102,6 +112,31 @@ hero_custom_description: "用文字记录生活,用代码构建世界。"
背景图片会自动应用模糊和暗化处理,以确保前景文字的可读性。建议选择色彩柔和、不包含大量细节的图片作为背景,避免干扰文案阅读。 背景图片会自动应用模糊和暗化处理,以确保前景文字的可读性。建议选择色彩柔和、不包含大量细节的图片作为背景,避免干扰文案阅读。
::: :::
### 文字颜色
标题、副标题、一言的颜色均可分别配置浅色 / 深色模式的取值,留空使用主题默认:
- **`hero_title_color_light` / `hero_title_color_dark`** — 大标题颜色。
- **`hero_subtitle_color_light` / `hero_subtitle_color_dark`** — 副标题颜色。
- **`hero_quote_color_light` / `hero_quote_color_dark`** — 一言文字颜色(`hero_description_mode` 为 `hitokoto` 或 `custom` 时生效)。
### 文字显示效果(hero_text_effect)
为 Hero 文字(标题 / 副标题 / 一言)叠加辅助可读效果。**默认 `none` 不使用任何效果**——v1.2.0 起,文字周围默认不再有淡淡的白色散光投影:
| 值 | 效果 | 说明 |
| --- | --- | --- |
| `none` | 无效果(默认) | 纯净文字,无任何附加投影与衬底 |
| `shadow` | 阴影 | 文字下方叠加柔和的深色投影,适合复杂背景图 |
| `glow` | 光晕 | 文字周围叠加白色光晕,暗色背景图上更醒目 |
| `scrim` | 半透明遮罩层 | 文字背后垫一块毛玻璃圆角衬底,整体提升可读性 |
| `gradient` | 渐变遮罩 | 文字背后垫斜向渐变衬底,左深右浅自然过渡 |
| `frame` | 容器 | 把文字"装"进一个带半透明边框的圆角矩形,加大内边距与字距留白 |
::: tip
遮罩类(`scrim` / `gradient`)与容器(`frame`)作用于整个文字区块,浅色模式为白色系衬底、深色模式为黑色系衬底;`frame` 会在浅色模式下使用墨色描边、深色模式下使用白色描边。
:::
## 视觉效果 ## 视觉效果
Hero 区域包含以下精心设计的视觉效果: Hero 区域包含以下精心设计的视觉效果:
+40
View File
@@ -0,0 +1,40 @@
# 记得
「记得」模块对应 [plugin-important-dates](https://github.com/yyliucha/plugin-important-dates) 插件的前台页面 `/important-dates`,用于展示重要日期、牵挂的人与座驾(爱车)信息。
## 配置项一览
| 配置项 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `idates_page_title` | 文本 | `记得` | 页面标题,留空时使用插件自带标题 |
| `idates_page_subtitle` | 文本 | `记着重要的日子、惦记的人,还有陪你跑的爱车` | 页面副标题 |
```yaml
idates_page_title: "记得" # 默认
idates_page_title: "纪念日" # 自定义
idates_page_subtitle: "记着重要的日子、惦记的人,还有陪你跑的爱车"
```
## 前置依赖
::: danger
本页依赖 Halo 的 **plugin-important-dates**(记得)插件。未安装插件时 `/important-dates` 路由不存在,页面不会显示。
安装方式:Halo 后台 → **插件** → **安装** → 上传 jar,或直接前往 [GitHub Releases](https://github.com/yyliucha/plugin-important-dates/releases) 下载。
:::
## 数据来源
页面数据全部由插件提供,主题不读取任何隐私字段:
- `dates` — 重要日期(名称、日期、农历标记、关联人员、倒计时)
- `people` — 牵挂的人(昵称、关系、脱敏生日、生日倒计时、大头贴)
- `cars` — 座驾(分类、品牌型号、脱敏车牌、到期事项、相册)
- `reminders` / `carEvents` — 顶部到期提醒横幅
是否展示「重要」标记、大头贴、爱车视图等,均由插件自己的「设置」控制,主题侧只负责呈现。
## 相关页面
- [扩展页面](/pages/extended) — 全部插件扩展页面一览
- [记得页面](/pages/important-dates) — 页面结构与模板覆盖说明
+25 -2
View File
@@ -7,7 +7,8 @@
| 配置项 | 类型 | 默认值 | 说明 | | 配置项 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `color_scheme` | 单选 | `system` | 配色方案:`system` / `light` / `dark` | | `color_scheme` | 单选 | `system` | 配色方案:`system` / `light` / `dark` |
| `accent_color` | 颜色 | `#d4764e` | 全局强调色 | | `color_palette` | 下拉 | `warm-island` | 主题配色(官方预设调色板,含深浅两套色板) |
| `accent_color` | 颜色 | — | 自定义强调色;留空跟随所选主题配色 |
| `border_radius` | 下拉 | `medium` | 圆角风格:`small` / `medium` / `large` | | `border_radius` | 下拉 | `medium` | 圆角风格:`small` / `medium` / `large` |
| `layout_container_width` | 下拉 | `medium` | 容器宽度:`narrow` / `medium` / `wide` | | `layout_container_width` | 下拉 | `medium` | 容器宽度:`narrow` / `medium` / `wide` |
| `animation_enabled` | 开关 | `true` | 全局动画总开关 | | `animation_enabled` | 开关 | `true` | 全局动画总开关 |
@@ -32,6 +33,28 @@
选择 `system` 时,主题会监听操作系统的 `prefers-color-scheme` 媒体查询,实时响应用户的系统设置变化。访问者也可以通过导航栏的主题切换按钮手动覆盖当前模式。 选择 `system` 时,主题会监听操作系统的 `prefers-color-scheme` 媒体查询,实时响应用户的系统设置变化。访问者也可以通过导航栏的主题切换按钮手动覆盖当前模式。
::: :::
### 主题配色(color_palette)
v1.2.0 起主题内置配色系统,提供 11 套官方预调配色,每套均包含浅色与深色两套完整色板(背景、文字、分隔线、强调色、玻璃拟态、阴影色调等会整体联动):
| 值 | 名称 | 色彩倾向 |
| --- | --- | --- |
| `warm-island` | 暖屿 · 赤陶(默认) | 暖橘陶土,米白纸感 |
| `matcha` | 抹茶 | 清新草绿,米绿纸感 |
| `indigo` | 黛蓝 | 沉稳蓝调,冷白纸感 |
| `sakura` | 樱粉 | 柔和粉调,樱白纸感 |
| `violet` | 暮紫 | 温柔紫调,淡紫纸感 |
| `sea` | 海盐 | 青碧蓝绿,浅青纸感 |
| `amber` | 琥珀 | 蜂蜜金黄,暖黄纸感 |
| `plum` | 梅酒 | 酒红梅紫,粉白纸感 |
| `graphite` | 石墨 | 中性灰黑,极简无彩 |
| `cocoa` | 可拿铁 | 咖啡可可,奶咖纸感 |
| `azure` | 天青 | 明快天蓝,浅蓝纸感 |
::: warning
下方「强调色」留空时,将使用所选配色自带的强调色。**老站点升级前如果保存过强调色(默认 `#d4764e`),切换配色后强调色不会变化——清空强调色并保存即可恢复跟随配色。**
:::
### 强调色(accent_color) ### 强调色(accent_color)
强调色是 WarmIsland 主题的灵魂,它会影响全站以下元素的颜色: 强调色是 WarmIsland 主题的灵魂,它会影响全站以下元素的颜色:
@@ -43,7 +66,7 @@
- 选中状态颜色 - 选中状态颜色
- Hero 区域渐变色的组成部分 - Hero 区域渐变色的组成部分
默认值 `#d4764e` 是暖屿标志性的暖橘色,与主题的温暖气质相呼应。 留空时跟随上方「主题配色」自带的强调色(暖屿·赤陶配色即 `#d4764e` 暖橘色)。
::: tip ::: tip
选择强调色时,建议同时考虑浅色和深色模式下的可读性。可以在 [CSS 变量](/customize/css-variables) 页面查看所有受影响的变量,进行更精细的调整。 选择强调色时,建议同时考虑浅色和深色模式下的可读性。可以在 [CSS 变量](/customize/css-variables) 页面查看所有受影响的变量,进行更精细的调整。
+53 -1
View File
@@ -6,7 +6,7 @@
检查以下几点: 检查以下几点:
1. 确认 Halo 版本 >= 2.24.0 1. 确认 Halo 版本 >= 2.26.0
2. 确认主题已启用 2. 确认主题已启用
3. 清除浏览器缓存后重试(Ctrl+Shift+R 强制刷新) 3. 清除浏览器缓存后重试(Ctrl+Shift+R 强制刷新)
4. 检查 Halo 后台日志是否有报错 4. 检查 Halo 后台日志是否有报错
@@ -26,8 +26,60 @@ docker cp templates/ halo:/root/.halo2/themes/warm-island/
docker restart halo docker restart halo
``` ```
### 升级主题后整站都没有样式?
主题的 CSS / JS 文件名带内容哈希,每次构建都会变化。如果 Halo 页面缓存或前置反向代理
(OpenResty、CDN 等)里还留着上一版本的 HTML,它请求的仍是已被删除的旧文件名,
Halo 找不到文件时会返回 **500**(日志里能看到 `FileNotFoundException: .../templates/assets/Layout.xxxx.css`),
于是样式全部失效。
依次处理:
1. Halo 后台执行一次「清理缓存」(设置 → 缓存,或直接重启 Halo / 容器)
2. 刷新 CDN、反向代理缓存
3. 浏览器强制刷新(Ctrl+Shift+R)
v1.2.1 起安装包会保留各版本的哈希资源(v1.1.7 之后全部覆盖),
即使缓存里的旧 HTML 被再次请求也不会 500,但最终仍建议清一次缓存以使用最新样式。
### 升级主题或修改配色后,部分页面的新样式/配色不生效?
先说结论:**这不是主题问题,是「页面静态缓存」插件缓存了旧版 HTML。**
排查特征(本站实际案例):
- 换了主题配色,首页和瞬间页永远是默认配色,其他页面正常
- 用 `curl` 直接请求同一个 URL 却能拿到新 HTML(极具迷惑性——缓存插件对
非浏览器请求会绕过缓存,所以服务端"看起来"一切正常)
- 响应头里有 `X-Halo-Cache-At` 和 `Cache-Control: max-age=3, s-maxage=120, must-revalidate`
—— 这是「页面静态缓存」插件的特征头,`X-Halo-Cache-At` 就是缓存写入时间
- 浏览器里页面引用的主题 CSS 还是旧版本的哈希文件名
原因:「页面静态缓存」插件**只监听文章内容的变动**自动刷新缓存,
**主题升级、主题设置(配色、Hero、样式等)变更都不会刷新缓存**。
缓存里的旧 HTML 是旧版本主题渲染的:没有新版的配色属性、引用的还是旧 CSS,
所以新功能在那些页面上永远不出现,直到缓存被清理或被新访问覆盖。
处理步骤:
1. Halo 后台 → 仪表盘 → 快捷访问 → 添加「**刷新页面缓存**」入口 → 点击清理一次
2. 浏览器强制刷新(Ctrl+Shift+R)
3. 如果配了 CDN(插件会给已缓存页面发 `s-maxage=120`),确认 CDN 缓存策略为
**遵循源站**,这样 CDN 最多 2 分钟就会回源,无需手动刷新
**长期建议**:以后每次**升级主题或修改主题设置**后,都点一次「刷新页面缓存」,
否则未登录访客最长会一直看到旧页面(插件不会自动感知主题层的变更)。
## 功能相关 ## 功能相关
### 鼠标点击效果 / 拖尾效果开启了却没反应?
1. 确认「总体样式 → 鼠标点击效果」开关已开启并点击了保存
2. 确认「点击效果类型」没有选择「关闭」(选了关闭时即使开启开关也不会有任何特效)
3. 清除浏览器缓存后重试;老版本的浏览器请更新到较新的 Chrome / Edge / Firefox / Safari
若仍无效,打开浏览器控制台(F12)检查是否有 JS 报错 —— 第三方脚本报错会中断后续脚本执行。
### 搜索按钮不显示? ### 搜索按钮不显示?
搜索功能依赖 Halo 搜索插件: 搜索功能依赖 Halo 搜索插件:
+13
View File
@@ -14,6 +14,7 @@ WarmIsland 主题深度集成了多款 Halo 插件,提供了丰富的扩展页
| 装备 | [equipment](https://www.halo.run/store/apps/app-ytygyqml) | /equipments | | 装备 | [equipment](https://www.halo.run/store/apps/app-ytygyqml) | /equipments |
| 足迹 | [footprint](https://www.halo.run/store/apps/app-XCtAQ) | /footprints | | 足迹 | [footprint](https://www.halo.run/store/apps/app-XCtAQ) | /footprints |
| Steam | [steam](https://www.halo.run/store/apps/app-0ojqyzfh) | /steam | | Steam | [steam](https://www.halo.run/store/apps/app-0ojqyzfh) | /steam |
| 记得 | [plugin-important-dates](https://github.com/yyliucha/plugin-important-dates) | /important-dates |
## 豆瓣页面 ## 豆瓣页面
[豆瓣插件](https://www.halo.run/store/apps/app-srBOL) [豆瓣插件](https://www.halo.run/store/apps/app-srBOL)
@@ -36,6 +37,18 @@ WarmIsland 主题深度集成了多款 Halo 插件,提供了丰富的扩展页
- 徽章展示 - 徽章展示
- 最近游戏 - 最近游戏
## 记得页面
[记得插件](https://github.com/yyliucha/plugin-important-dates)
记录重要的日子、牵挂的人与座驾,「生活 / 爱车」双视图展示:
- 重要日期(含农历)与倒计时便签
- 牵挂的人:大头贴、昵称、关系与生日倒计时
- 座驾:脱敏车牌、到期事项徽章、车主与驾驶人
- 相册:点击车辆封面全屏只读浏览
详见 [记得页面](/pages/important-dates)。
::: info ::: info
扩展页面需要安装对应的 Halo 插件才能使用。如果插件未安装,页面会显示友好的提示信息。 扩展页面需要安装对应的 Halo 插件才能使用。如果插件未安装,页面会显示友好的提示信息。
::: :::
+55
View File
@@ -0,0 +1,55 @@
# 记得页面
`/important-dates` 由 [plugin-important-dates](https://github.com/yyliucha/plugin-important-dates) 插件提供路由,主题提供同名模板 `templates/important-dates.html` 覆盖插件默认样式,使页面完全融入 WarmIsland 的外观(导航、页脚、暗色模式)。
## 页面结构
```
记得(标题 + 副标题 + 强调线)
├─ 生活 / 爱车 视图切换(插件开启「前台爱车模块」时出现)
├─ 到期提醒横幅(生活:重要日期;爱车:保险 / 年检 / 保养)
├─ 生活视图
│ ├─ 重要日期(便签卡片:倒计时、日期、农历标记、关联的人)
│ └─ 牵挂的人(大头贴、昵称、关系、脱敏生日与倒计时)
└─ 爱车视图
├─ 我的爱车(封面、分类品牌、脱敏车牌、到期徽章、车主/驾驶人)
└─ 车主与驾驶人 / 其他重要的人
```
## 交互
- **视图切换**:点击「生活 / 爱车」即时切换,地址栏记忆 `?view=car`
- **相册**:点击车辆封面全屏浏览相册(只读),`Esc` 或点击空白处关闭;未上传照片时给出轻提示
- **忽略提醒**:横幅右侧 ✕ 表示「本次到期周期内不再提示」,记录在浏览器本地,到期日变化后自动恢复
## 模板覆盖机制
插件通过 Halo 的 `TemplateNameResolver` 解析模板名:
1. 主题存在 `templates/important-dates.html` → 使用主题模板
2. 否则回退插件自带默认模板(会用主题的 `layout.html` 页面布局契约)
删除主题内的 `important-dates.html` 即可回到插件默认样式;插件不会写入或修改任何主题文件。
## 可用模板变量
| 变量 | 说明 |
| --- | --- |
| `title` | 页面标题(插件默认「记得」) |
| `dates` / `people` | 重要日期、人员列表 |
| `reminders` | 即将到来的重要日期(提醒横幅) |
| `cars` / `carEvents` | 座驾列表、即将到期的座驾事项 |
| `carPeople` / `otherPeople` / `personCars` | 车主驾驶人、其他人员、人员与车辆关系 |
| `showCarSection` / `carSkinEnabled` / `showAvatar` / `showImportantTag` | 各展示开关 |
| `allowDismiss` | 是否允许逐条忽略提醒 |
## 注意事项
::: warning
`PersonVo` 没有 `gender` 字段,自定义模板时不要写 `${p.gender}`,否则会触发 SpEL `EL1008E` 导致页面 500。
:::
## 相关页面
- [记得配置](/config/important-dates) — 页面标题与副标题
- [扩展页面](/pages/extended) — 其它插件扩展页面
+5 -2
View File
@@ -1,12 +1,15 @@
{ {
"version": "1.1.7", "version": "1.2.1",
"type": "module", "type": "module",
"engines": { "engines": {
"node": ">=22.12.0" "node": ">=22.12.0"
}, },
"scripts": { "scripts": {
"dev": "nodemon --config nodemon.json --exec 'astro build'", "dev": "nodemon --config nodemon.json --exec 'astro build'",
"build": "astro build && node -e \"const fs=require('fs');const src='templates/logo.png';const dst='templates/assets/logo.png';if(fs.existsSync(src)){fs.copyFileSync(src,dst)}\"", "build": "astro build && node scripts/postbuild.mjs",
"snapshot": "node scripts/snapshot-assets.mjs",
"package": "node scripts/package.mjs",
"smoke": "node scripts/smoke-clickfx.mjs",
"astro": "astro", "astro": "astro",
"format": "prettier --write ." "format": "prettier --write ."
}, },
+172
View File
@@ -0,0 +1,172 @@
/**
* 打包主题安装包:dist/warm-island-<version>.zip
*
* 安装包结构与 Halo 主题目录一致(根目录直接放 theme.yaml / settings.yaml /
* templates),与历史版本安装包保持相同布局,可直接上传安装。
*
* 仅依赖 Node 内置模块,无需额外依赖。
*/
import fs from "node:fs";
import path from "node:path";
import zlib from "node:zlib";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
/** 读取 theme.yaml 中的版本号(避免手抄版本号不一致) */
function readThemeVersion() {
const yaml = fs.readFileSync(path.join(root, "theme.yaml"), "utf8");
const match = yaml.match(/^\s*version:\s*["']?([^"'\n]+)["']?\s*$/m);
if (!match) {
throw new Error("theme.yaml 中未找到 version 字段");
}
return match[1].trim();
}
/* ---------------- CRC32 ---------------- */
const CRC_TABLE = (() => {
const table = new Int32Array(256);
for (let i = 0; i < 256; i++) {
let c = i;
for (let k = 0; k < 8; k++) {
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
}
table[i] = c;
}
return table;
})();
function crc32(buf) {
let c = 0xffffffff;
for (let i = 0; i < buf.length; i++) {
c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
}
return (c ^ 0xffffffff) >>> 0;
}
/* ---------------- ZIP 写入 ---------------- */
function dosDateTime(date = new Date()) {
const time =
((date.getHours() & 0x1f) << 11) |
((date.getMinutes() & 0x3f) << 5) |
((date.getSeconds() / 2) & 0x1f);
const day =
(((date.getFullYear() - 1980) & 0x7f) << 9) |
(((date.getMonth() + 1) & 0xf) << 5) |
(date.getDate() & 0x1f);
return { time, day };
}
function buildZip(entries, outFile) {
const chunks = [];
const central = [];
let offset = 0;
for (const entry of entries) {
const nameBuf = Buffer.from(entry.name, "utf8");
const raw = entry.data;
const crc = crc32(raw);
const compressed = zlib.deflateRawSync(raw, { level: 9 });
const useDeflate = compressed.length < raw.length;
const body = useDeflate ? compressed : raw;
const method = useDeflate ? 8 : 0;
const { time, day } = dosDateTime(entry.date);
const local = Buffer.alloc(30);
local.writeUInt32LE(0x04034b50, 0);
local.writeUInt16LE(20, 4); // version needed
local.writeUInt16LE(0x0800, 6); // UTF-8 名称
local.writeUInt16LE(method, 8);
local.writeUInt16LE(time, 10);
local.writeUInt16LE(day, 12);
local.writeUInt32LE(crc, 14);
local.writeUInt32LE(body.length, 18);
local.writeUInt32LE(raw.length, 22);
local.writeUInt16LE(nameBuf.length, 26);
local.writeUInt16LE(0, 28);
chunks.push(local, nameBuf, body);
const cd = Buffer.alloc(46);
cd.writeUInt32LE(0x02014b50, 0);
cd.writeUInt16LE(20, 4); // version made by
cd.writeUInt16LE(20, 6); // version needed
cd.writeUInt16LE(0x0800, 8);
cd.writeUInt16LE(method, 10);
cd.writeUInt16LE(time, 12);
cd.writeUInt16LE(day, 14);
cd.writeUInt32LE(crc, 16);
cd.writeUInt32LE(body.length, 20);
cd.writeUInt32LE(raw.length, 24);
cd.writeUInt16LE(nameBuf.length, 28);
cd.writeUInt16LE(0, 30); // extra
cd.writeUInt16LE(0, 32); // comment
cd.writeUInt16LE(0, 34); // disk start
cd.writeUInt16LE(0, 36); // internal attrs
cd.writeUInt32LE(0, 38); // external attrs
cd.writeUInt32LE(offset, 42);
central.push(Buffer.concat([cd, nameBuf]));
offset += local.length + nameBuf.length + body.length;
}
const centralBuf = Buffer.concat(central);
const eocd = Buffer.alloc(22);
eocd.writeUInt32LE(0x06054b50, 0);
eocd.writeUInt16LE(0, 4);
eocd.writeUInt16LE(0, 6);
eocd.writeUInt16LE(entries.length, 8);
eocd.writeUInt16LE(entries.length, 10);
eocd.writeUInt32LE(centralBuf.length, 12);
eocd.writeUInt32LE(offset, 16);
eocd.writeUInt16LE(0, 20);
fs.mkdirSync(path.dirname(outFile), { recursive: true });
fs.writeFileSync(outFile, Buffer.concat([...chunks, centralBuf, eocd]));
}
/* ---------------- 收集文件 ---------------- */
const INCLUDE_ROOT = [
"theme.yaml",
"settings.yaml",
"LICENSE",
"README.md",
"pnpm-lock.yaml",
"pnpm-workspace.yaml",
];
const INCLUDE_DIRS = ["templates"];
const EXCLUDE_DIRS = new Set([".git", "node_modules", "src", "dist", "compat", ".astro"]);
const entries = [];
for (const name of INCLUDE_ROOT) {
const full = path.join(root, name);
if (fs.existsSync(full)) {
entries.push({ name, data: fs.readFileSync(full), date: fs.statSync(full).mtime });
}
}
function walk(dir, prefix) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const abs = path.join(dir, entry.name);
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
if (EXCLUDE_DIRS.has(entry.name)) continue;
walk(abs, rel);
} else {
entries.push({ name: rel, data: fs.readFileSync(abs), date: fs.statSync(abs).mtime });
}
}
}
for (const dir of INCLUDE_DIRS) {
const full = path.join(root, dir);
if (fs.existsSync(full)) walk(full, dir);
}
const version = readThemeVersion();
const outFile = path.join(root, "dist", `warm-island-${version}.zip`);
buildZip(entries, outFile);
const sizeMB = (fs.statSync(outFile).size / 1024 / 1024).toFixed(2);
console.log(`[package] dist/warm-island-${version}.zip(${entries.length} 个文件,${sizeMB}MB)`);
+80
View File
@@ -0,0 +1,80 @@
/**
* 构建后置处理:
* 1. 复制 logo.png 到 assets(保留历史行为)
* 2. 合并 compat/assets 下的历史版本静态资源,避免站点/代理缓存中的旧 HTML
* 仍然引用上一版本的哈希文件名时出现 500(FileNotFoundException)
* 3. 校验所有 templates/*.html 引用的静态资源真实存在,缺失即构建失败
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const assetsDir = path.join(root, "templates", "assets");
const compatDir = path.join(root, "compat", "assets");
const templatesDir = path.join(root, "templates");
fs.mkdirSync(assetsDir, { recursive: true });
// 1. logo
const logoSrc = path.join(templatesDir, "logo.png");
if (fs.existsSync(logoSrc)) {
fs.copyFileSync(logoSrc, path.join(assetsDir, "logo.png"));
}
// 2. 历史资源合并(已存在的同名文件以当前构建为准,不覆盖)
if (fs.existsSync(compatDir)) {
let copied = 0;
let skipped = 0;
for (const entry of fs.readdirSync(compatDir, { withFileTypes: true })) {
if (!entry.isFile()) continue;
const src = path.join(compatDir, entry.name);
const dst = path.join(assetsDir, entry.name);
if (fs.existsSync(dst)) {
skipped++;
continue;
}
fs.copyFileSync(src, dst);
copied++;
}
console.log(`[compat] 历史静态资源:复制 ${copied} 个,当前构建已覆盖 ${skipped} 个`);
}
// 3. 资源引用校验
const missing = [];
const checked = new Set();
/** 主题静态资源前缀,必须与 astro.config.mjs 的 base 保持一致 */
const prefixes = ["/themes/warm-island/assets/"];
/** Thymeleaf 的 @{...} 链接表达式同样需要校验 */
const REFERENCE_RE =
/(?:href|src)=["']([^"']*?\/(?:themes\/[^/]+)\/assets\/[^"']+?)["']/g;
for (const file of fs.readdirSync(templatesDir)) {
if (!file.endsWith(".html")) continue;
const html = fs.readFileSync(path.join(templatesDir, file), "utf8");
REFERENCE_RE.lastIndex = 0;
let match;
while ((match = REFERENCE_RE.exec(html)) !== null) {
let ref = match[1];
if (ref.includes("@{")) continue; // 由 Halo 运行时解析,跳过
for (const prefix of prefixes) {
if (!ref.startsWith(prefix)) continue;
const rel = ref.slice(prefix.length).split("#")[0].split("?")[0];
const key = `${file}:${rel}`;
if (checked.has(key)) continue;
checked.add(key);
if (!fs.existsSync(path.join(assetsDir, rel))) {
missing.push(`${file} -> ${ref}`);
}
}
}
}
if (missing.length > 0) {
console.error(`[verify] 发现 ${missing.length} 处静态资源缺失:`);
for (const item of missing) console.error(` - ${item}`);
process.exit(1);
}
console.log(`[verify] 静态资源引用校验通过(${checked.size} 条引用)`);
+154
View File
@@ -0,0 +1,154 @@
/**
* 点击特效脚本冒烟测试(Node 内置模块,无第三方依赖)。
*
* 直接从构建产物 templates/index.html 中取出特效脚本,放进桩 DOM 里执行,
* 校验:启用/禁用判定、拖尾与点击分支、节点生成与定位。
*
* 这类 bug 的共同点是"不报错但没效果",靠读代码很难发现,所以固化成测试。
* 用法:npm run build && npm run smoke
*/
import fs from "node:fs";
import path from "node:path";
import vm from "node:vm";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const template = path.join(root, "templates", "index.html");
if (!fs.existsSync(template)) {
console.error("[smoke] 未找到 templates/index.html,请先执行 npm run build");
process.exit(1);
}
const html = fs.readFileSync(template, "utf8");
const anchor = html.indexOf('id="wi-click-effect"');
if (anchor === -1) {
console.error("[smoke] 模板中未找到 #wi-click-effect 节点");
process.exit(1);
}
const scriptOpen = html.indexOf("<script", anchor);
const scriptStart = html.indexOf(">", scriptOpen) + 1;
const scriptEnd = html.indexOf("</script>", scriptStart);
const code = html.slice(scriptStart, scriptEnd);
const failures = [];
function makeEl(tag) {
return {
tagName: tag,
className: "",
innerHTML: "",
textContent: "",
children: [],
parentNode: null,
style: {},
_attrs: {},
get offsetWidth() {
return 1;
},
getAttribute(name) {
return Object.prototype.hasOwnProperty.call(this._attrs, name) ? this._attrs[name] : null;
},
appendChild(child) {
if (child.parentNode) return child;
child.parentNode = this;
this.children.push(child);
return child;
},
removeChild(child) {
const i = this.children.indexOf(child);
if (i >= 0) {
this.children.splice(i, 1);
child.parentNode = null;
}
return child;
},
};
}
function run(attrs, expectEnabled, label) {
const cfg = makeEl("div");
Object.assign(cfg._attrs, attrs);
const body = makeEl("body");
const listeners = {};
const sandbox = {
document: {
getElementById: (id) => (id === "wi-click-effect" ? cfg : null),
createElement: makeEl,
body,
addEventListener(type, fn) {
(listeners[type] = listeners[type] || []).push(fn);
},
},
window: {},
requestAnimationFrame: (fn) => fn(),
setTimeout: () => 0,
};
vm.createContext(sandbox);
vm.runInContext(code, sandbox);
const hasListener = Object.keys(listeners).length > 0;
if (hasListener !== expectEnabled) {
failures.push(`${label}:期望 ${expectEnabled ? "注册监听" : "不注册监听"},实际相反`);
return;
}
if (!expectEnabled) {
console.log(` ✔ ${label} -> 已跳过`);
return;
}
const layer = body.children.find((c) => c.className === "wi-click-fx-layer");
if (!layer) {
failures.push(`${label}:未创建 .wi-click-fx-layer`);
return;
}
if (layer.children.length !== 0) {
failures.push(`${label}:初始化时特效层不应有子节点`);
return;
}
const type = attrs["data-effect"] || "(缺失,走兜底 values)";
if (listeners.click) {
listeners.click[0]({ clientX: 120, clientY: 240 });
} else if (listeners.mousemove) {
listeners.mousemove[0]({ clientX: 120, clientY: 240 });
} else {
failures.push(`${label}:未注册 click 或 mousemove 监听`);
return;
}
const spawned = layer.children;
if (spawned.length === 0) {
failures.push(`${label}:effect=${type} 触发后未生成任何特效节点`);
return;
}
const first = spawned[0];
if (!String(first.className).startsWith("wi-click-fx ")) {
failures.push(`${label}:特效节点缺少 wi-click-fx 基类(${first.className})`);
}
if (first.style.left !== "120px" || first.style.top !== "240px") {
failures.push(`${label}:定位不正确(${first.style.left}, ${first.style.top})`);
}
console.log(` ✔ ${label} effect=${type} -> ${spawned.length} 个节点,className=${first.className}`);
}
console.log("禁用场景:");
run({ "data-enabled": "false", "data-effect": "values" }, false, "显式关闭");
run({ "data-effect": "values" }, false, "配置缺失");
run({ "data-enabled": "true", "data-effect": "off" }, false, "类型选了「关闭」");
console.log("点击场景:");
for (const effect of ["values", "hearts", "fireworks", "ripples", "particles", "stars"]) {
run({ "data-enabled": "true", "data-effect": effect }, true, effect);
}
console.log("兜底与拖尾:");
run({ "data-enabled": "true" }, true, "type 缺省");
run({ "data-enabled": "true", "data-effect": "star-trail" }, true, "star-trail");
run({ "data-enabled": "true", "data-effect": "particle-trail" }, true, "particle-trail");
if (failures.length > 0) {
console.error("\n[smoke] 失败:");
for (const f of failures) console.error(" - " + f);
process.exit(1);
}
console.log("\n[smoke] 点击特效全部通过");
+63
View File
@@ -0,0 +1,63 @@
/**
* 把当前构建产出的哈希静态资源快照进 compat/assets。
*
* 为什么要这么做:
* Astro 给 CSS/JS 加内容哈希,主题升级后旧文件会被删除。若站点缓存或反向代理
* (Halo 页面缓存 / Nginx / CDN)里还留着上一版本的 HTML,它仍然会请求旧哈希
* 文件,Halo 找不到就直接抛 FileNotFoundException 返回 500,表现为"整站没样式"。
* 把每个版本的哈希文件都留一份,旧 HTML 就永远不会请求失败。
*
* 用法:npm run build && npm run snapshot(随后把 compat/assets 的新增文件提交进仓库)
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const assetsDir = path.join(root, "templates", "assets");
const compatDir = path.join(root, "compat", "assets");
if (!fs.existsSync(assetsDir)) {
console.error("[snapshot] 未找到 templates/assets,请先执行 npm run build");
process.exit(1);
}
fs.mkdirSync(compatDir, { recursive: true });
/** 名称固定、体积大的资源不需要进兼容层 */
const SKIP_EXACT = new Set(["logo.png"]);
const SKIP_DIRS = new Set(["fonts"]);
let added = 0;
let updated = 0;
let unchanged = 0;
for (const entry of fs.readdirSync(assetsDir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (SKIP_DIRS.has(entry.name)) continue;
// 哈希资源都在根目录,子目录(如 fonts)不参与快照
continue;
}
if (SKIP_EXACT.has(entry.name)) continue;
const src = path.join(assetsDir, entry.name);
const dst = path.join(compatDir, entry.name);
const stat = fs.statSync(src);
if (!fs.existsSync(dst)) {
fs.copyFileSync(src, dst);
added++;
} else {
const dstBuf = fs.readFileSync(dst);
const srcBuf = fs.readFileSync(src);
if (dstBuf.length !== stat.size || !dstBuf.equals(srcBuf)) {
fs.copyFileSync(src, dst);
updated++;
} else {
unchanged++;
}
}
}
console.log(
`[snapshot] 新增 ${added} / 更新 ${updated} / 无变化 ${unchanged} 个历史资源(compat/assets)`
);
+74 -3
View File
@@ -99,10 +99,38 @@ spec:
- label: 深色 - label: 深色
value: dark value: dark
value: system value: system
- $formkit: select
name: color_palette
label: 主题配色
options:
- label: 暖屿 · 赤陶(默认)
value: warm-island
- label: 抹茶
value: matcha
- label: 黛蓝
value: indigo
- label: 樱粉
value: sakura
- label: 暮紫
value: violet
- label: 海盐
value: sea
- label: 琥珀
value: amber
- label: 梅酒
value: plum
- label: 石墨
value: graphite
- label: 可拿铁
value: cocoa
- label: 天青
value: azure
value: warm-island
help: 整套官方预调配色(含深浅两套色板);下方的强调色留空时跟随所选配色,填写后将覆盖配色中的强调色
- $formkit: color - $formkit: color
name: accent_color name: accent_color
label: 强调色 label: 强调色
value: "#d4764e" help: 自定义强调色;留空使用「主题配色」自带的强调色
- $formkit: select - $formkit: select
name: border_radius name: border_radius
label: 圆角风格 label: 圆角风格
@@ -329,11 +357,37 @@ spec:
- $formkit: color - $formkit: color
name: hero_subtitle_color_light name: hero_subtitle_color_light
label: 浅色模式副标题颜色 label: 浅色模式副标题颜色
help: Hero 副标题和一言文字颜色(浅色模式);留空使用主题默认 help: Hero 副标题文字颜色(浅色模式);留空使用主题默认
- $formkit: color - $formkit: color
name: hero_subtitle_color_dark name: hero_subtitle_color_dark
label: 深色模式副标题颜色 label: 深色模式副标题颜色
help: Hero 副标题和一言文字颜色(深色模式);留空使用主题默认 help: Hero 副标题文字颜色(深色模式);留空使用主题默认
- $formkit: color
name: hero_quote_color_light
label: 浅色模式一言颜色
help: 一言(描述文案来源为"一言"时)的文字颜色(浅色模式);留空使用主题默认
- $formkit: color
name: hero_quote_color_dark
label: 深色模式一言颜色
help: 一言的文字颜色(深色模式);留空使用主题默认
- $formkit: select
name: hero_text_effect
label: 文字显示效果
options:
- label: 无效果(默认)
value: none
- label: 阴影
value: shadow
- label: 光晕
value: glow
- label: 半透明遮罩层
value: scrim
- label: 渐变遮罩
value: gradient
- label: 容器(半透明边框圆角矩形)
value: frame
value: none
help: 为 Hero 文字(标题/副标题/一言)叠加的辅助可读效果;默认无效果。遮罩类与容器类会在文字背后垫一层衬底并增加留白
- group: home - group: home
label: 首页 label: 首页
formSchema: formSchema:
@@ -575,6 +629,11 @@ spec:
- group: links - group: links
label: 友情链接 label: 友情链接
formSchema: formSchema:
- $formkit: switch
name: links_apply_form_enabled
label: 显示友链申请表单
value: true
help: 在友链页底部展示访客申请表单;需链接管理插件开启「友链申请」功能,未开启时表单自动隐藏
- $formkit: text - $formkit: text
name: links_page_title name: links_page_title
label: 页面标题 label: 页面标题
@@ -659,6 +718,18 @@ spec:
name: weread_page_subtitle name: weread_page_subtitle
label: 页面副标题 label: 页面副标题
placeholder: "来自微信读书的同步藏书" placeholder: "来自微信读书的同步藏书"
- group: important_dates
label: 记得
formSchema:
- $formkit: text
name: idates_page_title
label: 页面标题
placeholder: "记得"
help: 对应「记得」插件的前台页面 /important-dates;留空时使用插件自带的标题
- $formkit: text
name: idates_page_subtitle
label: 页面副标题
placeholder: "记着重要的日子、惦记的人,还有陪你跑的爱车"
- group: messageboard - group: messageboard
label: 留言板 label: 留言板
formSchema: formSchema:
+188 -102
View File
@@ -1,14 +1,35 @@
--- ---
/*
* 点击特效组件。
*
* 设计要点:容器节点始终输出,由脚本读取 data-enabled / data-effect 决定行为。
* 旧写法用 th:if 依赖配置值严格等于布尔 true,一旦配置未落库、值不是布尔,
* 或 Thymeleaf 求值把属性移除,节点就不会渲染 —— 特效静默失效且无任何报错。
*
* 特效元素统一挂到 body 下的 .wi-click-fx-layer:
* .wi-main 是独立的层叠上下文(z-index:1),留在里面会被导航栏/页脚遮住。
*
* spawn() 里必须强制回流,否则新节点的初态与终态在同一帧被合并,
* transition 不会触发,表现同样是"点了没反应"。
*/
--- ---
<div id="wi-click-effect" th:if="${theme.config?.style?.click_effect_enabled == true}" th:attr="data-effect=${theme.config?.style?.click_effect_type}"> <div
id="wi-click-effect"
hidden
th:attr="data-effect=${theme.config?.style?.click_effect_type ?: 'values'},data-enabled=${theme.config?.style?.click_effect_enabled}"
></div>
<script is:inline th:inline="none"> <script is:inline th:inline="none">
(function () { (function () {
var container = document.getElementById("wi-click-effect"); var cfg = document.getElementById("wi-click-effect");
if (!container) return; if (!cfg) return;
var effect = container.getAttribute("data-effect") || "off";
if (effect === "off" || !effect) return; if ((cfg.getAttribute("data-enabled") || "").trim() !== "true") return;
var effect = (cfg.getAttribute("data-effect") || "").trim();
if (!effect) effect = "values";
if (effect === "off") return;
/* 社会主义核心价值观 */ /* 社会主义核心价值观 */
var CORE_VALUES = [ var CORE_VALUES = [
@@ -23,23 +44,47 @@
"#51cf66", "#94d82d", "#fcc419", "#ff922b" "#51cf66", "#94d82d", "#fcc419", "#ff922b"
]; ];
/* 创建单个 DOM 元素并加入舞台 */ function pick(list) {
return list[Math.floor(Math.random() * list.length)];
}
/*
* 特效层统一挂在 body 下:
* .wi-main 自身是层叠上下文(z-index:1),留在里面会被导航栏/页脚遮住;
* 同时该层 position:fixed 且铺满视口,内部 fixed 子元素的
* left/top 仍等价于 clientX/clientY。
*/
var layer = document.createElement("div");
layer.className = "wi-click-fx-layer";
document.body.appendChild(layer);
/* 创建单个 DOM 元素并加入特效层,返回后即可设置终态样式 */
function spawn(el, x, y) { function spawn(el, x, y) {
el.style.left = x + "px"; el.style.left = x + "px";
el.style.top = y + "px"; el.style.top = y + "px";
container.appendChild(el); layer.appendChild(el);
/*
* 强制回流:新插入的元素若在同一帧内被改写 transform/opacity,
* 浏览器会把初态与终态合并,transition 不会触发 —— 表现为"没有任何效果"。
*/
void el.offsetWidth;
return el;
}
function removeLater(el, delay) {
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
}, delay);
} }
/* ========== 1. 社会主义核心价值观 ========== */ /* ========== 1. 社会主义核心价值观 ========== */
function spawnValueText(x, y) { function spawnValueText(x, y) {
var el = document.createElement("span"); var el = document.createElement("span");
el.className = "wi-click-fx wi-click-fx--text"; el.className = "wi-click-fx wi-click-fx--text";
el.textContent = CORE_VALUES[Math.floor(Math.random() * CORE_VALUES.length)]; el.textContent = pick(CORE_VALUES);
el.style.color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)]; el.style.color = pick(RANDOM_COLORS);
spawn(el, x, y); spawn(el, x, y);
setTimeout(function () { removeLater(el, 1300);
if (el.parentNode) el.parentNode.removeChild(el);
}, 1200);
} }
/* ========== 2. 随机彩色爱心 ========== */ /* ========== 2. 随机彩色爱心 ========== */
@@ -47,89 +92,61 @@
var el = document.createElement("span"); var el = document.createElement("span");
el.className = "wi-click-fx wi-click-fx--heart"; el.className = "wi-click-fx wi-click-fx--heart";
el.innerHTML = "&#10084;"; el.innerHTML = "&#10084;";
el.style.color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)]; el.style.color = pick(RANDOM_COLORS);
var dx = (Math.random() - 0.5) * 80;
spawn(el, x, y); spawn(el, x, y);
requestAnimationFrame(function () { el.style.transform = "translate(calc(-50% + " + ((Math.random() - 0.5) * 80).toFixed(1) + "px), -90px) scale(0.3)";
el.style.transform = "translate(" + dx + "px, -80px) scale(0.2)";
el.style.opacity = "0"; el.style.opacity = "0";
}); removeLater(el, 1100);
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
}, 1000);
} }
/* ========== 3. 烟花 ========== */ /* ========== 3. 烟花 ========== */
function spawnFirework(x, y) { function spawnFirework(x, y) {
var count = 16; var count = 18;
var color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)]; var color = pick(RANDOM_COLORS);
for (var i = 0; i < count; i++) { for (var i = 0; i < count; i++) {
var el = document.createElement("span"); var el = document.createElement("span");
el.className = "wi-click-fx wi-click-fx--spark"; el.className = "wi-click-fx wi-click-fx--spark";
el.style.background = color; el.style.background = color;
el.style.boxShadow = "0 0 6px " + color; el.style.boxShadow = "0 0 6px " + color;
var angle = (Math.PI * 2 * i) / count; var angle = (Math.PI * 2 * i) / count;
var dist = 40 + Math.random() * 30; var dist = 40 + Math.random() * 34;
var dx = Math.cos(angle) * dist;
var dy = Math.sin(angle) * dist;
spawn(el, x, y); spawn(el, x, y);
requestAnimationFrame(function (e, tx, ty) { el.style.transform = "translate(calc(-50% + " + (Math.cos(angle) * dist).toFixed(1) + "px), calc(-50% + " + (Math.sin(angle) * dist).toFixed(1) + "px)) scale(0)";
return function () { el.style.opacity = "0";
e.style.transform = "translate(" + tx + "px, " + ty + "px) scale(0)"; removeLater(el, 900);
e.style.opacity = "0";
};
}(el, dx, dy));
(function (e) {
setTimeout(function () {
if (e.parentNode) e.parentNode.removeChild(e);
}, 800);
})(el);
} }
} }
/* ========== 4. 烟花波纹 ========== */ /* ========== 4. 烟花波纹 ========== */
function spawnRipple(x, y) { function spawnRipple(x, y) {
var color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)]; var color = pick(RANDOM_COLORS);
for (var r = 0; r < 2; r++) { for (var r = 0; r < 2; r++) {
var el = document.createElement("span"); var el = document.createElement("span");
el.className = "wi-click-fx wi-click-fx--ripple"; el.className = "wi-click-fx wi-click-fx--ripple";
el.style.borderColor = color; el.style.borderColor = color;
spawn(el, x, y); spawn(el, x, y);
(function (e, delay) { (function (el, delay) {
setTimeout(function () { setTimeout(function () {
requestAnimationFrame(function () { el.style.transform = "translate(-50%, -50%) scale(4)";
e.style.transform = "translate(-50%, -50%) scale(4)"; el.style.opacity = "0";
e.style.opacity = "0"; removeLater(el, 700);
});
}, delay); }, delay);
setTimeout(function () { })(el, r * 160);
if (e.parentNode) e.parentNode.removeChild(e); removeLater(el, 160 + 900);
}, delay + 700);
})(el, r * 150);
} }
} }
/* ========== 5. 粒子 ========== */ /* ========== 5. 粒子 ========== */
function spawnParticles(x, y, n) { function spawnParticles(x, y, n) {
n = n || 8; n = n || 10;
for (var i = 0; i < n; i++) { for (var i = 0; i < n; i++) {
var el = document.createElement("span"); var el = document.createElement("span");
el.className = "wi-click-fx wi-click-fx--particle"; el.className = "wi-click-fx wi-click-fx--particle";
el.style.background = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)]; el.style.background = pick(RANDOM_COLORS);
var dx = (Math.random() - 0.5) * 60;
var dy = (Math.random() - 0.5) * 60 - 20;
spawn(el, x, y); spawn(el, x, y);
requestAnimationFrame(function (e, tx, ty) { el.style.transform = "translate(calc(-50% + " + ((Math.random() - 0.5) * 60).toFixed(1) + "px), calc(-50% + " + ((Math.random() - 0.5) * 60 - 20).toFixed(1) + "px)) scale(0)";
return function () { el.style.opacity = "0";
e.style.transform = "translate(" + tx + "px, " + ty + "px) scale(0)"; removeLater(el, 800);
e.style.opacity = "0";
};
}(el, dx, dy));
(function (e) {
setTimeout(function () {
if (e.parentNode) e.parentNode.removeChild(e);
}, 700);
})(el);
} }
} }
@@ -138,17 +155,11 @@
var el = document.createElement("span"); var el = document.createElement("span");
el.className = "wi-click-fx wi-click-fx--star"; el.className = "wi-click-fx wi-click-fx--star";
el.innerHTML = "&#9733;"; el.innerHTML = "&#9733;";
el.style.color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)]; el.style.color = pick(RANDOM_COLORS);
var dx = (Math.random() - 0.5) * 50;
var dy = (Math.random() - 0.5) * 50 - 30;
spawn(el, x, y); spawn(el, x, y);
requestAnimationFrame(function () { el.style.transform = "translate(calc(-50% + " + ((Math.random() - 0.5) * 50).toFixed(1) + "px), calc(-50% + " + ((Math.random() - 0.5) * 50 - 30).toFixed(1) + "px)) scale(1.6)";
el.style.transform = "translate(" + dx + "px, " + dy + "px) scale(1.6)";
el.style.opacity = "0"; el.style.opacity = "0";
}); removeLater(el, 900);
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
}, 800);
} }
/* 点击分发 */ /* 点击分发 */
@@ -162,26 +173,28 @@
}; };
/* 拖尾效果 */ /* 拖尾效果 */
var trailHandlers = { function spawnTrailStar(x, y) {
"star-trail": function (x, y) {
var el = document.createElement("span"); var el = document.createElement("span");
el.className = "wi-click-fx wi-click-fx--trail-star"; el.className = "wi-click-fx wi-click-fx--trail-star";
el.innerHTML = "&#9733;"; el.innerHTML = "&#9733;";
el.style.color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)]; el.style.color = pick(RANDOM_COLORS);
spawn(el, x, y); spawn(el, x, y);
setTimeout(function () { el.style.opacity = "0";
if (el.parentNode) el.parentNode.removeChild(el); removeLater(el, 700);
}, 600); }
},
"particle-trail": function (x, y) { function spawnTrailParticle(x, y) {
var el = document.createElement("span"); var el = document.createElement("span");
el.className = "wi-click-fx wi-click-fx--trail-particle"; el.className = "wi-click-fx wi-click-fx--trail-particle";
el.style.background = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)]; el.style.background = pick(RANDOM_COLORS);
spawn(el, x, y); spawn(el, x, y);
setTimeout(function () { el.style.opacity = "0";
if (el.parentNode) el.parentNode.removeChild(el); removeLater(el, 600);
}, 500);
} }
var trailHandlers = {
"star-trail": spawnTrailStar,
"particle-trail": spawnTrailParticle
}; };
var isTrail = effect.indexOf("trail") !== -1; var isTrail = effect.indexOf("trail") !== -1;
@@ -189,55 +202,128 @@
if (isTrail) { if (isTrail) {
var handler = trailHandlers[effect]; var handler = trailHandlers[effect];
if (!handler) return; if (!handler) return;
var lastX = 0, lastY = 0; var lastX = 0;
var throttle = false; var lastY = 0;
document.addEventListener("mousemove", function (e) { var throttling = false;
document.addEventListener(
"mousemove",
function (e) {
lastX = e.clientX; lastX = e.clientX;
lastY = e.clientY; lastY = e.clientY;
if (throttle) return; if (throttling) return;
throttle = true; throttling = true;
requestAnimationFrame(function () { requestAnimationFrame(function () {
throttle = false; throttling = false;
handler(lastX, lastY); handler(lastX, lastY);
}); });
}, { passive: true }); },
{ passive: true }
);
} else { } else {
var handler = clickHandlers[effect]; var handler = clickHandlers[effect];
if (!handler) return; if (!handler) return;
document.addEventListener("click", function (e) { document.addEventListener(
"click",
function (e) {
handler(e.clientX, e.clientY); handler(e.clientX, e.clientY);
}, { passive: true }); },
{ passive: true }
);
} }
})(); })();
</script> </script>
<style is:global> <style is:global>
.wi-click-fx{position:fixed;pointer-events:none;will-change:transform,opacity /* 特效层:铺满视口的固定层,统一承载所有点击/拖尾元素 */
.wi-click-fx-layer {
position: fixed;
inset: 0;
z-index: 12050;
pointer-events: none;
overflow: hidden;
} }
.wi-click-fx--text{font-size:18px;font-weight:700;transform:translate(-50%,-50%);opacity:0;animation:wi-fx-text 1.2s ease-out forwards .wi-click-fx {
position: fixed;
pointer-events: none;
will-change: transform, opacity;
user-select: none;
} }
.wi-click-fx--heart{font-size:20px;transform:translate(-50%,-50%) scale(1);opacity:1;transition:transform 1s cubic-bezier(.16,1,.3,1),opacity 1s ease-out .wi-click-fx--text {
font-size: 18px;
font-weight: 700;
transform: translate(-50%, -50%);
animation: wi-fx-text 1.2s ease-out forwards;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
} }
.wi-click-fx--spark{width:6px;height:6px;border-radius:50%;transform:translate(-50%,-50%) scale(1);opacity:1;transition:transform .8s cubic-bezier(.16,1,.3,1),opacity .8s ease-out .wi-click-fx--heart {
font-size: 20px;
opacity: 1;
transition: transform 1s cubic-bezier(0.16, 1, 0.3, 1), opacity 1s ease-out;
} }
.wi-click-fx--ripple{width:30px;height:30px;border:2px solid;border-radius:50%;transform:translate(-50%,-50%) scale(.5);opacity:1;transition:transform .7s ease-out,opacity .7s ease-out .wi-click-fx--spark {
width: 6px;
height: 6px;
border-radius: 50%;
opacity: 1;
transition: transform 0.8s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.8s ease-out;
} }
.wi-click-fx--particle{width:8px;height:8px;border-radius:50%;transform:translate(-50%,-50%) scale(1);opacity:1;transition:transform .7s cubic-bezier(.16,1,.3,1),opacity .7s ease-out .wi-click-fx--ripple {
width: 30px;
height: 30px;
border: 2px solid;
border-radius: 50%;
transform: translate(-50%, -50%) scale(0.5);
opacity: 0.9;
transition: transform 0.7s ease-out, opacity 0.7s ease-out;
} }
.wi-click-fx--star{font-size:16px;transform:translate(-50%,-50%) scale(1);opacity:1;transition:transform .8s cubic-bezier(.16,1,.3,1),opacity .8s ease-out .wi-click-fx--particle {
width: 8px;
height: 8px;
border-radius: 50%;
opacity: 1;
transition: transform 0.7s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.7s ease-out;
} }
.wi-click-fx--trail-star{font-size:12px;transform:translate(-50%,-50%) scale(1);opacity:.8;transition:opacity .6s ease-out,transform .6s ease-out .wi-click-fx--star {
font-size: 16px;
opacity: 1;
transition: transform 0.8s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.8s ease-out;
} }
.wi-click-fx--trail-particle{width:6px;height:6px;border-radius:50%;transform:translate(-50%,-50%) scale(1);opacity:.8;transition:opacity .5s ease-out,transform .5s ease-out .wi-click-fx--trail-star {
font-size: 12px;
transform: translate(-50%, -50%);
opacity: 0.9;
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
} }
@keyframes wi-fx-text{0%{opacity:0;transform:translate(-50%,-50%) scale(.5)}20%{opacity:1;transform:translate(-50%,-80%) scale(1.1)}to{opacity:0;transform:translate(-50%,-120%) scale(.9)}} .wi-click-fx--trail-particle {
width: 6px;
height: 6px;
border-radius: 50%;
transform: translate(-50%, -50%);
opacity: 0.9;
transition: opacity 0.5s ease-out, transform 0.5s ease-out;
}
@keyframes wi-fx-text {
0% {
opacity: 0;
transform: translate(-50%, -50%) scale(0.5);
}
20% {
opacity: 1;
transform: translate(-50%, -80%) scale(1.1);
}
to {
opacity: 0;
transform: translate(-50%, -120%) scale(0.9);
}
}
</style> </style>
+93 -3
View File
@@ -5,12 +5,14 @@
<style is:inline th:if="${theme.config?.hero?.hero_title_color_dark != null and theme.config?.hero?.hero_title_color_dark != ''}" th:utext="${' html.dark { --hero-title-text: ' + theme.config?.hero?.hero_title_color_dark + '; } '}">/*hero-title-dark*/</style> <style is:inline th:if="${theme.config?.hero?.hero_title_color_dark != null and theme.config?.hero?.hero_title_color_dark != ''}" th:utext="${' html.dark { --hero-title-text: ' + theme.config?.hero?.hero_title_color_dark + '; } '}">/*hero-title-dark*/</style>
<style is:inline th:if="${theme.config?.hero?.hero_subtitle_color_light != null and theme.config?.hero?.hero_subtitle_color_light != ''}" th:utext="${' :root { --hero-subtitle-text: ' + theme.config?.hero?.hero_subtitle_color_light + '; } '}">/*hero-subtitle-light*/</style> <style is:inline th:if="${theme.config?.hero?.hero_subtitle_color_light != null and theme.config?.hero?.hero_subtitle_color_light != ''}" th:utext="${' :root { --hero-subtitle-text: ' + theme.config?.hero?.hero_subtitle_color_light + '; } '}">/*hero-subtitle-light*/</style>
<style is:inline th:if="${theme.config?.hero?.hero_subtitle_color_dark != null and theme.config?.hero?.hero_subtitle_color_dark != ''}" th:utext="${' html.dark { --hero-subtitle-text: ' + theme.config?.hero?.hero_subtitle_color_dark + '; } '}">/*hero-subtitle-dark*/</style> <style is:inline th:if="${theme.config?.hero?.hero_subtitle_color_dark != null and theme.config?.hero?.hero_subtitle_color_dark != ''}" th:utext="${' html.dark { --hero-subtitle-text: ' + theme.config?.hero?.hero_subtitle_color_dark + '; } '}">/*hero-subtitle-dark*/</style>
<style is:inline th:if="${theme.config?.hero?.hero_quote_color_light != null and theme.config?.hero?.hero_quote_color_light != ''}" th:utext="${' :root { --hero-quote-text: ' + theme.config?.hero?.hero_quote_color_light + '; } '}">/*hero-quote-light*/</style>
<style is:inline th:if="${theme.config?.hero?.hero_quote_color_dark != null and theme.config?.hero?.hero_quote_color_dark != ''}" th:utext="${' html.dark { --hero-quote-text: ' + theme.config?.hero?.hero_quote_color_dark + '; } '}">/*hero-quote-dark*/</style>
<section <section
class="hero" class="hero"
id="wi-hero" id="wi-hero"
th:if="${theme.config?.hero?.hero_enabled != false}" th:if="${theme.config?.hero?.hero_enabled != false}"
th:classappend="${theme.config?.animation?.animation_breath == false} ? 'hero--no-breath'" th:classappend="${(theme.config?.animation?.animation_breath == false ? 'hero--no-breath ' : '') + 'hero--fx-' + (theme.config?.hero?.hero_text_effect ?: 'none')}"
> >
<div class="hero__bg"> <div class="hero__bg">
<div class="hero__orb hero__orb--1"></div> <div class="hero__orb hero__orb--1"></div>
@@ -281,7 +283,7 @@
inset: 0; inset: 0;
background-size: cover; background-size: cover;
background-position: center; background-position: center;
opacity: 0.15; opacity: 1;
} }
/* 深色模式背景图切换:未配置双图时不生效,保持单一背景图 */ /* 深色模式背景图切换:未配置双图时不生效,保持单一背景图 */
@@ -325,13 +327,91 @@
.hero__description { .hero__description {
font-size: var(--text-lg); font-size: var(--text-lg);
color: var(--ink-2); color: var(--hero-quote-text, var(--ink-2));
line-height: var(--leading-relaxed); line-height: var(--leading-relaxed);
max-width: 560px; max-width: 560px;
text-align: center; text-align: center;
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
} }
/* ===== 文字显示效果(后台「Hero 首屏 → 文字显示效果」,默认 none 无任何附加效果) ===== */
.hero--fx-shadow .hero__title,
.hero--fx-shadow .hero__subtitle,
.hero--fx-shadow .hero__description {
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.22), 0 2px 10px rgba(0, 0, 0, 0.16);
}
html.dark .hero--fx-shadow .hero__title,
html.dark .hero--fx-shadow .hero__subtitle,
html.dark .hero--fx-shadow .hero__description {
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6), 0 2px 12px rgba(0, 0, 0, 0.4);
}
.hero--fx-glow .hero__title,
.hero--fx-glow .hero__subtitle,
.hero--fx-glow .hero__description {
text-shadow: 0 0 10px rgba(255, 255, 255, 0.85), 0 0 26px rgba(255, 255, 255, 0.5);
}
html.dark .hero--fx-glow .hero__title,
html.dark .hero--fx-glow .hero__subtitle,
html.dark .hero--fx-glow .hero__description {
text-shadow: 0 0 8px rgba(255, 255, 255, 0.45), 0 0 24px rgba(255, 255, 255, 0.22);
}
/* 半透明遮罩层:文字背后垫一块毛玻璃衬底 */
.hero--fx-scrim .hero__content {
background: rgba(255, 255, 255, 0.45);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 24px;
padding: var(--space-2xl);
}
html.dark .hero--fx-scrim .hero__content {
background: rgba(0, 0, 0, 0.4);
}
/* 渐变遮罩:斜向渐变衬底 */
.hero--fx-gradient .hero__content {
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.55) 0%,
rgba(255, 255, 255, 0.18) 100%
);
border-radius: 24px;
padding: var(--space-2xl);
}
html.dark .hero--fx-gradient .hero__content {
background: linear-gradient(
135deg,
rgba(0, 0, 0, 0.5) 0%,
rgba(0, 0, 0, 0.16) 100%
);
}
/* 容器:半透明边框圆角矩形 + 更大留白,把文字"装"进设计好的容器里 */
.hero--fx-frame .hero__content {
border: 1px solid color-mix(in srgb, var(--ink) 22%, transparent);
background: color-mix(in srgb, var(--bg) 30%, transparent);
border-radius: 24px;
padding: var(--space-2xl) var(--space-3xl);
gap: var(--space-xl);
}
html.dark .hero--fx-frame .hero__content {
border-color: color-mix(in srgb, #ffffff 32%, transparent);
}
.hero--fx-frame .hero__title {
letter-spacing: 0.08em;
}
.hero--fx-frame .hero__subtitle {
letter-spacing: 0.22em;
}
.hero__scroll-indicator { .hero__scroll-indicator {
position: absolute; position: absolute;
bottom: 2rem; bottom: 2rem;
@@ -425,6 +505,16 @@
gap: var(--space-md); gap: var(--space-md);
} }
.hero--fx-scrim .hero__content,
.hero--fx-gradient .hero__content {
padding: var(--space-xl) var(--space-lg);
}
.hero--fx-frame .hero__content {
padding: var(--space-xl) var(--space-lg);
gap: var(--space-lg);
}
.hero__description { .hero__description {
max-width: 90vw; max-width: 90vw;
} }
+7 -14
View File
@@ -236,24 +236,17 @@
} }
function processMomentsMedia(container) { function processMomentsMedia(container) {
// 注意:不要用 <a> 包裹瞬间图片。九宫格布局依赖
// .wi-moments-page__card-media 的直接子元素数量(:has/:nth-child/:only-child),
// 每张图各自包一层 <a> 后,img 永远是「唯一子元素」,全部图片都会退化成单列大图。
// 直接把 img 本身作为 lightgallery 项即可(data-src 提供原图)。
var images = container.querySelectorAll('img'); var images = container.querySelectorAll('img');
images.forEach(function(img) { images.forEach(function(img) {
var src = img.getAttribute('src'); var src = img.getAttribute('src');
if (!src) return; if (!src) return;
img.classList.add('wi-lightgallery-item');
var parentA = img.closest('a'); if (!img.getAttribute('data-src')) {
if (parentA) { img.setAttribute('data-src', src);
parentA.classList.add('wi-lightgallery-item');
if (!parentA.getAttribute('data-src')) {
parentA.setAttribute('data-src', src);
}
} else {
var a = document.createElement('a');
a.setAttribute('href', src);
a.setAttribute('data-src', src);
a.classList.add('wi-lightgallery-item');
img.parentNode.insertBefore(a, img);
a.appendChild(img);
} }
}); });
} }
+18
View File
@@ -21,6 +21,16 @@
class="wi-mobile-menu__link" class="wi-mobile-menu__link"
th:data-href="${menuItem.status.href}" th:data-href="${menuItem.status.href}"
></a> ></a>
<th:block th:if="${not #lists.isEmpty(menuItem.children)}">
<a
th:each="child : ${menuItem.children}"
th:href="@{${child.status.href}}"
th:target="${child.spec?.target?.value == '_blank'} ? '_blank' : '_self'"
th:text="${'| ' + child.status.displayName}"
class="wi-mobile-menu__link wi-mobile-menu__link--sub"
th:data-href="${child.status.href}"
></a>
</th:block>
</th:block> </th:block>
</th:block> </th:block>
</th:block> </th:block>
@@ -213,6 +223,14 @@
background var(--duration-fast) var(--ease-out-quart); background var(--duration-fast) var(--ease-out-quart);
} }
/* 二级菜单项:缩进 + 弱化,与一级项区分 */
.wi-mobile-menu__link--sub {
padding-left: 2rem;
font-size: var(--text-base);
font-weight: 400;
color: var(--ink-3);
}
.wi-mobile-menu__link:hover { .wi-mobile-menu__link:hover {
color: var(--accent); color: var(--accent);
background: var(--bg-raised); background: var(--bg-raised);
+102 -1
View File
@@ -32,15 +32,34 @@ import ThemeSwitcher from "./ThemeSwitcher.astro";
<th:block th:if="${menuFinder != null}"> <th:block th:if="${menuFinder != null}">
<th:block th:with="menu = ${menuFinder.getPrimary()}"> <th:block th:with="menu = ${menuFinder.getPrimary()}">
<th:block th:if="${menu != null and menu.menuItems != null and not #lists.isEmpty(menu.menuItems)}"> <th:block th:if="${menu != null and menu.menuItems != null and not #lists.isEmpty(menu.menuItems)}">
<a <div
th:each="menuItem : ${menu.menuItems}" th:each="menuItem : ${menu.menuItems}"
class="wi-navbar__item"
th:classappend="${not #lists.isEmpty(menuItem.children)} ? 'wi-navbar__item--has-children'"
>
<a
th:href="@{${menuItem.status.href}}" th:href="@{${menuItem.status.href}}"
th:target="${menuItem.spec?.target?.value == '_blank'} ? '_blank' : '_self'" th:target="${menuItem.spec?.target?.value == '_blank'} ? '_blank' : '_self'"
th:text="${menuItem.status.displayName}" th:text="${menuItem.status.displayName}"
class="wi-navbar__link" class="wi-navbar__link"
th:data-href="${menuItem.status.href}" th:data-href="${menuItem.status.href}"
th:aria-haspopup="${not #lists.isEmpty(menuItem.children)} ? 'true' : null"
> >
</a> </a>
<div
class="wi-navbar__submenu"
th:if="${not #lists.isEmpty(menuItem.children)}"
>
<a
th:each="child : ${menuItem.children}"
th:href="@{${child.status.href}}"
th:target="${child.spec?.target?.value == '_blank'} ? '_blank' : '_self'"
th:text="${child.status.displayName}"
class="wi-navbar__submenu-link"
>
</a>
</div>
</div>
</th:block> </th:block>
</th:block> </th:block>
</th:block> </th:block>
@@ -257,6 +276,88 @@ import ThemeSwitcher from "./ThemeSwitcher.astro";
min-width: 0; min-width: 0;
} }
/* 一级菜单项容器(含子菜单时作为下拉的定位锚点) */
.wi-navbar__item {
position: relative;
display: inline-flex;
}
/* 二级下拉:纯 CSS 悬停/聚焦展开,无 JS 开销 */
.wi-navbar__submenu {
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%) translateY(6px);
display: flex;
flex-direction: column;
min-width: 168px;
padding: 6px;
margin: 0;
list-style: none;
border-radius: var(--radius-md, 12px);
background: var(--bg-raised);
border: 1px solid var(--rule);
box-shadow: var(--shadow-lg);
opacity: 0;
visibility: hidden;
transition: opacity 0.18s ease, transform 0.18s var(--ease-out-quart),
visibility 0.18s;
z-index: 101;
}
/* 悬停桥:避免鼠标从一级移到二级途中经过空隙导致菜单关闭 */
.wi-navbar__item--has-children::after {
content: "";
position: absolute;
left: 0;
right: 0;
top: 100%;
height: 10px;
}
.wi-navbar__item--has-children:hover .wi-navbar__submenu,
.wi-navbar__item--has-children:focus-within .wi-navbar__submenu {
opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(0);
}
.wi-navbar__submenu-link {
display: block;
padding: 0.5rem 0.75rem;
border-radius: 8px;
font-size: var(--text-sm);
color: var(--ink-2);
text-decoration: none;
white-space: nowrap;
transition: color 0.15s ease, background 0.15s ease;
}
.wi-navbar__submenu-link:hover {
color: var(--accent);
background: color-mix(in srgb, var(--accent) 8%, transparent);
}
/* 有子菜单的一级项带下拉小箭头 */
.wi-navbar__item--has-children > .wi-navbar__link::after {
content: "";
display: inline-block;
width: 4px;
height: 4px;
margin-left: 0.3rem;
margin-top: -2px;
border-right: 1.5px solid currentColor;
border-bottom: 1.5px solid currentColor;
transform: rotate(45deg);
opacity: 0.6;
transition: transform 0.18s var(--ease-out-quart);
}
.wi-navbar__item--has-children:hover > .wi-navbar__link::after {
transform: rotate(225deg);
margin-top: 2px;
}
.wi-navbar__link { .wi-navbar__link {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
+2 -1
View File
@@ -57,8 +57,9 @@
document.documentElement.classList.toggle("dark", isDark); document.documentElement.classList.toggle("dark", isDark);
document.documentElement.setAttribute("data-color-scheme", isDark ? "dark" : "light"); document.documentElement.setAttribute("data-color-scheme", isDark ? "dark" : "light");
localStorage.setItem("wi-theme", isDark ? "dark" : "light"); localStorage.setItem("wi-theme", isDark ? "dark" : "light");
var bgPair = window.WI_PALETTE_BG || { light: "#faf7f2", dark: "#1e1c1a" };
var tc = document.querySelector('meta[name="theme-color"]'); var tc = document.querySelector('meta[name="theme-color"]');
if (tc) tc.setAttribute("content", isDark ? "#1a1614" : "#faf7f2"); if (tc) tc.setAttribute("content", isDark ? bgPair.dark : bgPair.light);
updateIcons(); updateIcons();
setTimeout(function() { setTimeout(function() {
document.documentElement.classList.remove("wi-theme-transition"); document.documentElement.classList.remove("wi-theme-transition");
+22 -5
View File
@@ -24,6 +24,7 @@ const fragmentAttr = layoutFragment ? "html (head, content)" : undefined;
class={home ? 'wi-home-snap' : ''} class={home ? 'wi-home-snap' : ''}
style="overflow-x:hidden" style="overflow-x:hidden"
th:classappend="${theme.config?.animation?.animation_enabled == false} ? 'wi-no-animations'" th:classappend="${theme.config?.animation?.animation_enabled == false} ? 'wi-no-animations'"
th:attr="data-palette=${theme.config?.style?.color_palette ?: 'warm-island'}"
xmlns:th={layoutFragment ? "https://www.thymeleaf.org" : undefined} xmlns:th={layoutFragment ? "https://www.thymeleaf.org" : undefined}
th:fragment={fragmentAttr} th:fragment={fragmentAttr}
> >
@@ -32,7 +33,7 @@ const fragmentAttr = layoutFragment ? "html (head, content)" : undefined;
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#faf7f2" /> <meta name="theme-color" content="#faf7f2" />
<meta th:if="${theme.config?.basic?.site_description}" name="description" th:content="${theme.config?.basic?.site_description}" /> <meta th:if="${theme.config?.basic?.site_description}" name="description" th:content="${theme.config?.basic?.site_description}" />
<script is:inline th:attr="data-scheme=${theme.config?.style?.color_scheme}"> <script is:inline th:inline="none" th:attr="data-scheme=${theme.config?.style?.color_scheme}">
(function () { (function () {
var stored = localStorage.getItem("wi-theme"); var stored = localStorage.getItem("wi-theme");
var prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; var prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
@@ -55,14 +56,30 @@ const fragmentAttr = layoutFragment ? "html (head, content)" : undefined;
if (isDark) { if (isDark) {
document.documentElement.classList.add("dark"); document.documentElement.classList.add("dark");
document.documentElement.setAttribute("data-color-scheme", "dark"); document.documentElement.setAttribute("data-color-scheme", "dark");
var tc = document.querySelector('meta[name="theme-color"]');
if (tc) tc.setAttribute("content", "#1a1614");
} else { } else {
document.documentElement.classList.remove("dark"); document.documentElement.classList.remove("dark");
document.documentElement.setAttribute("data-color-scheme", "light"); document.documentElement.setAttribute("data-color-scheme", "light");
var tc = document.querySelector('meta[name="theme-color"]');
if (tc) tc.setAttribute("content", "#faf7f2");
} }
/* 浏览器地址栏颜色跟随当前配色(含主题配色系统) */
var paletteBg = {
"warm-island": ["#faf7f2", "#1e1c1a"],
"matcha": ["#f6f8f1", "#1b1f1a"],
"indigo": ["#f5f7fa", "#191d23"],
"sakura": ["#faf5f6", "#201b1c"],
"violet": ["#f7f5fa", "#1c1a21"],
"sea": ["#f3f8f7", "#181f1e"],
"amber": ["#faf7ef", "#201d16"],
"plum": ["#f9f4f5", "#1f1a1b"],
"graphite": ["#f7f7f6", "#1b1b1a"],
"cocoa": ["#f8f4f0", "#1e1a17"],
"azure": ["#f4f8fb", "#172026"]
};
var pal = document.documentElement.getAttribute("data-palette");
var pair = (pal && paletteBg[pal]) || paletteBg["warm-island"];
var tc = document.querySelector('meta[name="theme-color"]');
if (tc) tc.setAttribute("content", isDark ? pair[1] : pair[0]);
/* 供 ThemeSwitcher 切换深浅色时取当前配色的背景色 */
window.WI_PALETTE_BG = { light: pair[0], dark: pair[1] };
})(); })();
</script> </script>
<link <link
+21 -21
View File
@@ -119,68 +119,68 @@ import Layout from "../layouts/Layout.astro";
<article <article
class="wi-douban__card" class="wi-douban__card"
th:each="item : ${doubans.items}" th:each="item : ${doubans.items}"
th:if="${!hasGenre or #sets.contains(item.spec?.genres, currentGenre)}" th:if="${!hasGenre or #sets.contains(item.genres, currentGenre)}"
th:attr="data-type=${currentType}" th:attr="data-type=${currentType}"
> >
<a <a
class="wi-douban__card-link" class="wi-douban__card-link"
th:if="${!#strings.isEmpty(item.spec?.link)}" th:if="${!#strings.isEmpty(item.link)}"
th:href="${item.spec?.link}" th:href="${item.link}"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
tabindex="-1" tabindex="-1"
aria-hidden="true" aria-hidden="true"
></a> ></a>
<div class="wi-douban__poster" th:if="${!#strings.isEmpty(item.spec?.poster)}"> <div class="wi-douban__poster" th:if="${!#strings.isEmpty(item.poster)}">
<img <img
th:src="${item.spec?.poster}" th:src="${item.poster}"
th:alt="${item.spec?.name}" th:alt="${item.name}"
class="wi-douban__poster-img" class="wi-douban__poster-img"
loading="lazy" loading="lazy"
decoding="async" decoding="async"
/> />
</div> </div>
<div class="wi-douban__poster wi-douban__poster--placeholder" th:if="${#strings.isEmpty(item.spec?.poster)}"> <div class="wi-douban__poster wi-douban__poster--placeholder" th:if="${#strings.isEmpty(item.poster)}">
<span th:text="${#strings.substring(item.spec?.name ?: '', 0, 1)}"></span> <span th:text="${#strings.substring(item.name ?: '', 0, 1)}"></span>
</div> </div>
<div class="wi-douban__body"> <div class="wi-douban__body">
<h3 class="wi-douban__name" th:text="${item.spec?.name}"></h3> <h3 class="wi-douban__name" th:text="${item.name}"></h3>
<p <p
class="wi-douban__subtitle" class="wi-douban__subtitle"
th:if="${!#strings.isEmpty(item.spec?.cardSubtitle)}" th:if="${!#strings.isEmpty(item.cardSubtitle)}"
th:text="${item.spec?.cardSubtitle}" th:text="${item.cardSubtitle}"
></p> ></p>
<div class="wi-douban__meta"> <div class="wi-douban__meta">
<span <span
class="wi-douban__score" class="wi-douban__score"
th:if="${!#strings.isEmpty(item.spec?.score) and item.spec?.score != '0'}" th:if="${!#strings.isEmpty(item.score) and item.score != '0'}"
> >
<svg class="wi-douban__star" viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg> <svg class="wi-douban__star" viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
<th:block th:text="${item.spec?.score}"></th:block> <th:block th:text="${item.score}"></th:block>
</span> </span>
<span <span
class="wi-douban__my-score" class="wi-douban__my-score"
th:if="${!#strings.isEmpty(item.faves?.score)}" th:if="${!#strings.isEmpty(item.favesScore)}"
> >
<span th:text="'我的:'">My:</span><th:block th:text="${item.faves?.score}"></th:block> <span th:text="'我的:'">My:</span><th:block th:text="${item.favesScore}"></th:block>
</span> </span>
<span <span
class="wi-douban__year" class="wi-douban__year"
th:if="${!#strings.isEmpty(item.spec?.year)}" th:if="${!#strings.isEmpty(item.year)}"
th:text="${item.spec?.year}" th:text="${item.year}"
></span> ></span>
</div> </div>
<div class="wi-douban__tags" th:if="${item.spec?.genres != null and !#sets.isEmpty(item.spec?.genres)}"> <div class="wi-douban__tags" th:if="${item.genres != null and !#sets.isEmpty(item.genres)}">
<span <span
class="wi-douban__tag" class="wi-douban__tag"
th:each="genre : ${item.spec?.genres}" th:each="genre : ${item.genres}"
th:text="${genre}" th:text="${genre}"
></span> ></span>
</div> </div>
<p <p
class="wi-douban__remark" class="wi-douban__remark"
th:if="${!#strings.isEmpty(item.faves?.remark)}" th:if="${!#strings.isEmpty(item.favesRemark)}"
th:text="${item.faves?.remark}" th:text="${item.favesRemark}"
></p> ></p>
</div> </div>
</article> </article>
File diff suppressed because it is too large Load Diff
+294 -1
View File
@@ -53,9 +53,12 @@ import Layout from "../layouts/Layout.astro";
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="wi-links__card" class="wi-links__card"
th:with="verification=${link.status?.verification},
accessState=${verification?.access?.state != null ? verification.access.state.name() : null}"
> >
<div class="wi-links__card-glow"></div> <div class="wi-links__card-glow"></div>
<div class="wi-links__card-inner"> <div class="wi-links__card-inner">
<span class="wi-links__avatar-wrap">
<img <img
th:if="${link.spec.logo}" th:if="${link.spec.logo}"
th:src="${link.spec.logo}" th:src="${link.spec.logo}"
@@ -70,6 +73,18 @@ import Layout from "../layouts/Layout.astro";
th:text="${#strings.substring(link.spec.displayName, 0, 1)}" th:text="${#strings.substring(link.spec.displayName, 0, 1)}"
></span> ></span>
</div> </div>
<!-- 可达性检测徽章(plugin-links 后台「友链检测」产生):绿点=可访问,红点=异常,未检测不显示 -->
<span
class="wi-links__check wi-links__check--ok"
th:if="${accessState == 'ACCESSIBLE'}"
th:title="'站点访问正常'"
></span>
<span
class="wi-links__check wi-links__check--bad"
th:if="${accessState == 'INACCESSIBLE'}"
th:title="'站点访问异常'"
></span>
</span>
<div class="wi-links__info"> <div class="wi-links__info">
<span <span
class="wi-links__name" class="wi-links__name"
@@ -87,6 +102,123 @@ import Layout from "../layouts/Layout.astro";
</div> </div>
</th:block> </th:block>
<!--
友链申请:表单 POST 到插件的 /links/apply/submit,插件以 303 回跳 /links,
并带上 applied(success / error / disabled)、message、field、value 四个查询参数。
注意(踩过坑):th:attr 的赋值里不能调用 #lists 等表达式对象的方法,
Thymeleaf 会对 th:attr 走受限求值并抛
"Instantiation of new objects and access to static classes or parameters is forbidden"。
取值逻辑一律先在 th:with 里算成变量,再用 th:value 之类的单值属性输出。
-->
<div
class="wi-links__apply"
th:if="${theme.config?.links?.links_apply_form_enabled != false}"
th:with="appliedList=${param?.applied},
applied=${appliedList != null and appliedList.length > 0 ? appliedList[0] : null},
messageList=${param?.message},
applyMsg=${messageList != null and messageList.length > 0 ? messageList[0] : null},
fieldList=${param?.field},
applyField=${fieldList != null and fieldList.length > 0 ? fieldList[0] : null},
valueList=${param?.value},
applyValue=${valueList != null and valueList.length > 0 ? valueList[0] : null}"
>
<h2 class="wi-links__apply-title" th:text="'申请友链'">申请友链</h2>
<p
class="wi-links__apply-result"
th:if="${applied != null}"
th:classappend="${applied == 'success'} ? 'wi-links-check-ok' : 'wi-links-check-error'"
th:text="${applyMsg != null ? applyMsg : '申请已提交,等待站长审核 🌙'}"
role="status"
></p>
<th:block th:unless="${applied == 'disabled'}">
<form
class="wi-links__apply-form"
method="post"
th:action="@{/links/apply/submit}"
>
<input type="hidden" name="_csrf" th:value="${csrfToken}" />
<div class="wi-links__apply-row">
<label class="wi-links__apply-field">
<span th:text="'站点名称 *'">站点名称 *</span>
<input
type="text"
name="displayName"
required
maxlength="50"
placeholder="你的站点名称"
th:value="${applyField == 'displayName' ? applyValue : null}"
/>
</label>
<label class="wi-links__apply-field">
<span th:text="'站点链接 *'">站点链接 *</span>
<input
type="url"
name="url"
required
placeholder="https://"
th:value="${applyField == 'url' ? applyValue : null}"
/>
</label>
</div>
<div class="wi-links__apply-row">
<label class="wi-links__apply-field">
<span th:text="'图标地址'">图标地址</span>
<input type="url" name="logo" placeholder="https://" />
</label>
<label class="wi-links__apply-field">
<span th:text="'联系邮箱'">联系邮箱</span>
<input type="email" name="email" placeholder="用于通知审核结果" />
</label>
</div>
<label class="wi-links__apply-field">
<span th:text="'站点描述'">站点描述</span>
<input type="text" name="description" maxlength="120" placeholder="一句话介绍你的站点" />
</label>
<label class="wi-links__apply-field">
<span th:text="'反链页面'">反链页面</span>
<input type="url" name="backlink" placeholder="https://" />
</label>
<label class="wi-links__apply-field">
<span th:text="'RSS / Atom 地址(每行一个)'">RSS / Atom 地址(每行一个)</span>
<textarea name="feedUrls" rows="2" placeholder="https://"></textarea>
</label>
<div class="wi-links__apply-row">
<label class="wi-links__apply-field">
<span th:text="'验证码 *'">验证码 *</span>
<input type="text" name="captchaCode" required maxlength="8" autocomplete="off" placeholder="图中五位字符" />
</label>
<div class="wi-links__apply-captcha">
<img
class="wi-links__apply-captcha-img"
th:src="@{/links/apply/captcha}"
alt="验证码"
width="120"
height="44"
loading="lazy"
title="点击刷新验证码"
onclick="this.src=this.src.split('?')[0]+'?ts='+Date.now()"
onerror="var c=this.closest('.wi-links__apply');if(c)c.classList.add('wi-links__apply--unavailable')"
/>
</div>
</div>
<button
class="wi-links__apply-submit"
type="submit"
th:text="'提交申请'"
>
提交申请
</button>
</form>
</th:block>
<p
class="wi-links__apply-unavailable-tip"
th:text="'友链申请暂未开放。'"
hidden
></p>
</div>
<div class="wi-links__comment" th:if="${theme.config?.links?.links_show_comment == true}"> <div class="wi-links__comment" th:if="${theme.config?.links?.links_show_comment == true}">
<halo:comment group="plugin.halo.run" kind="Plugin" name="PluginLinks"></halo:comment> <halo:comment group="plugin.halo.run" kind="Plugin" name="PluginLinks"></halo:comment>
</div> </div>
@@ -189,6 +321,139 @@ import Layout from "../layouts/Layout.astro";
margin-top: var(--space-3xl); margin-top: var(--space-3xl);
} }
/* ===== 友链申请表单 ===== */
.wi-links__apply {
margin-top: var(--space-3xl);
padding: var(--space-xl);
border: 1px solid var(--rule);
border-radius: var(--radius-lg, 16px);
background: var(--bg-raised);
}
.wi-links__apply-title {
text-align: center;
font-size: var(--text-xl);
font-weight: 700;
color: var(--ink);
margin-bottom: var(--space-lg);
}
.wi-links__apply-result {
text-align: center;
font-size: var(--text-sm);
color: var(--accent);
margin-bottom: var(--space-md);
}
.wi-links__apply-result--ok,
.wi-links-check-ok {
color: var(--accent);
}
.wi-links__apply-result--error,
.wi-links-check-error {
color: #e6162d;
}
html.dark .wi-links__apply-result--error,
html.dark .wi-links-check-error {
color: #ff6b6b;
}
.wi-links__apply-form {
display: flex;
flex-direction: column;
gap: var(--space-md);
}
.wi-links__apply-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-md);
}
.wi-links__apply-field {
display: flex;
flex-direction: column;
gap: 0.35rem;
font-size: var(--text-sm);
color: var(--ink-2);
}
.wi-links__apply-field input,
.wi-links__apply-field textarea {
padding: 0.6rem 0.8rem;
border: 1px solid var(--rule);
border-radius: 10px;
background: var(--bg);
color: var(--ink);
font-family: inherit;
font-size: var(--text-sm);
transition: border-color 0.15s ease, box-shadow 0.15s ease;
resize: vertical;
}
.wi-links__apply-field input:focus,
.wi-links__apply-field textarea:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent);
}
.wi-links__apply-captcha {
display: flex;
align-items: flex-end;
padding-bottom: 2px;
}
.wi-links__apply-captcha-img {
border-radius: 8px;
border: 1px solid var(--rule);
cursor: pointer;
}
.wi-links__apply-submit {
align-self: center;
padding: 0.65rem 2.2rem;
min-height: 44px;
border: none;
border-radius: 9999px;
font-size: var(--text-sm);
font-weight: 600;
font-family: inherit;
color: #fff;
background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 70%, #000));
cursor: pointer;
transition: transform var(--duration-fast) var(--ease-out-quart), opacity 0.15s ease;
}
.wi-links__apply-submit:hover {
transform: translateY(-1px);
opacity: 0.95;
}
.wi-links__apply-submit:active {
transform: translateY(0) scale(0.98);
}
/* 插件未开放申请时(验证码 404)自动隐藏表单并提示 */
.wi-links__apply--unavailable .wi-links__apply-form {
display: none;
}
.wi-links__apply--unavailable .wi-links__apply-unavailable-tip {
display: block !important;
text-align: center;
color: var(--ink-3);
font-size: var(--text-sm);
}
@media (max-width: 640px) {
.wi-links__apply-row {
grid-template-columns: 1fr;
}
}
.wi-links__title { .wi-links__title {
font-size: var(--text-4xl); font-size: var(--text-4xl);
margin-bottom: var(--space-xs); margin-bottom: var(--space-xs);
@@ -257,8 +522,9 @@ import Layout from "../layouts/Layout.astro";
border-radius: 16px; border-radius: 16px;
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
overflow: hidden;
display: flex; display: flex;
/* 注意:这里绝不能加 overflow:hidden —— 悬浮时内层卡片会上移 4px 并投射大范围
阴影,裁切会把阴影切出一条"断开的光带"、卡片顶部被削平(v1.2.0 修复) */
} }
.wi-links__card-glow { .wi-links__card-glow {
@@ -302,6 +568,13 @@ import Layout from "../layouts/Layout.astro";
border-color: color-mix(in srgb, var(--accent) 40%, var(--glass-border)); border-color: color-mix(in srgb, var(--accent) 40%, var(--glass-border));
} }
.wi-links__avatar-wrap {
position: relative;
flex: none;
display: inline-flex;
line-height: 1;
}
.wi-links__avatar { .wi-links__avatar {
width: 48px; width: 48px;
height: 48px; height: 48px;
@@ -312,6 +585,26 @@ import Layout from "../layouts/Layout.astro";
transition: transform var(--duration-normal) var(--ease-out-expo); transition: transform var(--duration-normal) var(--ease-out-expo);
} }
/* 反链 / 可达性检测徽章:绿色正常,红色异常 */
.wi-links__check {
position: absolute;
right: -3px;
bottom: -3px;
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid var(--bg);
box-shadow: var(--shadow-sm);
}
.wi-links__check--ok {
background: #22c55e;
}
.wi-links__check--bad {
background: #ef4444;
}
.wi-links__card:hover .wi-links__avatar { .wi-links__card:hover .wi-links__avatar {
transform: scale(1.08); transform: scale(1.08);
} }
+75 -4
View File
@@ -31,6 +31,28 @@ import LightGallery from "../components/LightGallery.astro";
<div class="wi-moments-page__cards" th:if="${theme.config?.moments?.moments_style == 'cards' or theme.config?.moments?.moments_style == null or theme.config?.moments?.moments_style == 'timeline'}"> <div class="wi-moments-page__cards" th:if="${theme.config?.moments?.moments_style == 'cards' or theme.config?.moments?.moments_style == null or theme.config?.moments?.moments_style == 'timeline'}">
<th:block th:if="${momentsResult != null and not #lists.isEmpty(momentsResult.items)}"> <th:block th:if="${momentsResult != null and not #lists.isEmpty(momentsResult.items)}">
<div class="wi-moments-page__card-item" th:each="moment : ${momentsResult.items}"> <div class="wi-moments-page__card-item" th:each="moment : ${momentsResult.items}">
<div class="wi-moments-page__card-layout">
<img
th:if="${moment.owner != null and !#strings.isEmpty(moment.owner.avatar)}"
class="wi-moments-page__avatar"
th:src="${moment.owner.avatar}"
th:alt="''"
loading="lazy"
decoding="async"
/>
<div
th:unless="${moment.owner != null and !#strings.isEmpty(moment.owner.avatar)}"
class="wi-moments-page__avatar wi-moments-page__avatar--fallback"
aria-hidden="true"
>
<span th:text="${#strings.substring(site.title, 0, 1)}"></span>
</div>
<div class="wi-moments-page__main">
<div
class="wi-moments-page__name"
th:text="${moment.owner != null and !#strings.isEmpty(moment.owner.displayName) ? moment.owner.displayName : site.title}"
></div>
<div class="wi-moments-page__card-text" th:utext="${moment.spec?.content?.html ?: moment.spec?.content?.raw ?: moment.spec?.content}"></div> <div class="wi-moments-page__card-text" th:utext="${moment.spec?.content?.html ?: moment.spec?.content?.raw ?: moment.spec?.content}"></div>
<div class="wi-moments-page__card-media" th:if="${moment.spec.content.medium != null and !#lists.isEmpty(moment.spec.content.medium)}"> <div class="wi-moments-page__card-media" th:if="${moment.spec.content.medium != null and !#lists.isEmpty(moment.spec.content.medium)}">
<th:block th:each="media : ${moment.spec.content.medium}"> <th:block th:each="media : ${moment.spec.content.medium}">
@@ -75,6 +97,8 @@ import LightGallery from "../components/LightGallery.astro";
th:attr="name=${moment.metadata.name}" th:attr="name=${moment.metadata.name}"
/> />
</div> </div>
</div> <!-- /__main -->
</div> <!-- /__card-layout -->
</div> </div>
</th:block> </th:block>
<th:block th:if="${momentsResult == null or #lists.isEmpty(momentsResult.items)}"> <th:block th:if="${momentsResult == null or #lists.isEmpty(momentsResult.items)}">
@@ -387,6 +411,8 @@ import LightGallery from "../components/LightGallery.astro";
position: relative; position: relative;
width: 100%; width: 100%;
height: clamp(220px, 30vw, 380px); height: clamp(220px, 30vw, 380px);
/* 顶图与首条瞬间的卡片边框不能贴死:留出与页面头部一致的呼吸间距 */
margin-bottom: var(--space-2xl);
overflow: hidden; overflow: hidden;
background: var(--bg-raised); background: var(--bg-raised);
} }
@@ -527,6 +553,43 @@ import LightGallery from "../components/LightGallery.astro";
transform: translateY(-3px); transform: translateY(-3px);
} }
/* 朋友圈式布局:头像在左,昵称/正文/九宫格在右 */
.wi-moments-page__card-layout {
display: flex;
gap: var(--space-md);
align-items: flex-start;
}
.wi-moments-page__avatar {
flex-shrink: 0;
width: 44px;
height: 44px;
border-radius: 10px;
object-fit: cover;
}
.wi-moments-page__avatar--fallback {
display: flex;
align-items: center;
justify-content: center;
background: color-mix(in srgb, var(--accent) 14%, transparent);
color: var(--accent);
font-weight: 700;
font-size: var(--text-lg);
}
.wi-moments-page__main {
flex: 1;
min-width: 0;
}
.wi-moments-page__name {
font-size: var(--text-base);
font-weight: 600;
color: var(--accent);
margin-bottom: var(--space-xs);
}
.wi-moments-page__card-text { .wi-moments-page__card-text {
font-size: var(--text-base); font-size: var(--text-base);
line-height: var(--leading-relaxed); line-height: var(--leading-relaxed);
@@ -568,8 +631,15 @@ import LightGallery from "../components/LightGallery.astro";
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
.wi-moments-page__card-media:has(.wi-moments-page__card-image:nth-child(4)) { /* 微信九宫格规则:4 张时 2x2 排布 */
grid-template-columns: repeat(3, 1fr); .wi-moments-page__card-media:has(.wi-moments-page__card-image:nth-child(4)):not(:has(.wi-moments-page__card-image:nth-child(5))) {
grid-template-columns: repeat(2, 1fr);
max-width: 72%;
}
/* 三列九宫格限制宽度,避免大屏下图片单元过大 */
.wi-moments-page__card-media:has(.wi-moments-page__card-image:nth-child(3)) {
max-width: 100%;
} }
.wi-moments-page__card-footer { .wi-moments-page__card-footer {
@@ -647,8 +717,9 @@ import LightGallery from "../components/LightGallery.astro";
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
.wi-moments-page__masonry-media:has(.wi-moments-page__masonry-image:nth-child(4)) { /* 微信九宫格规则:4 张时 2x2 排布 */
grid-template-columns: repeat(3, 1fr); .wi-moments-page__masonry-media:has(.wi-moments-page__masonry-image:nth-child(4)):not(:has(.wi-moments-page__masonry-image:nth-child(5))) {
grid-template-columns: repeat(2, 1fr);
} }
.wi-moments-page__masonry-footer { .wi-moments-page__masonry-footer {
+2 -4
View File
@@ -45,8 +45,6 @@ import Layout from "../layouts/Layout.astro";
.wi-messageboard { .wi-messageboard {
display: grid; display: grid;
gap: var(--space-lg); gap: var(--space-lg);
margin-top: calc(-1 * 100px - var(--space-lg));
padding-top: 100px;
padding-block-end: 1rem; padding-block-end: 1rem;
} }
@@ -70,8 +68,8 @@ import Layout from "../layouts/Layout.astro";
color: var(--accent); color: var(--accent);
opacity: 0.18; opacity: 0.18;
position: absolute; position: absolute;
top: -0.15em; top: 0.05em;
left: 0.15em; left: 0.2em;
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;
} }
+3 -3
View File
@@ -897,15 +897,15 @@ import LightGallery from "../components/LightGallery.astro";
.wi-post__body { .wi-post__body {
width: 100%; width: 100%;
font-size: 1.05rem; font-size: clamp(1.05rem, 1rem + 0.25vw, 1.15rem);
line-height: 1.8; line-height: 1.85;
color: #3d3530; color: #3d3530;
overflow-wrap: break-word; overflow-wrap: break-word;
word-wrap: break-word; word-wrap: break-word;
} }
html.dark .wi-post__body { html.dark .wi-post__body {
color: #d4cdc4; color: #d9d2c9;
} }
/* 正文本身及内部元素样式统一放在全局样式 src/styles/_content.scss, /* 正文本身及内部元素样式统一放在全局样式 src/styles/_content.scss,
+61 -8
View File
@@ -69,6 +69,14 @@ import Layout from "../layouts/Layout.astro";
.then(function (html) { .then(function (html) {
container.innerHTML = html; container.innerHTML = html;
/* 笔记弹窗是 position:fixed,但 innerHTML 会把它留在 .wi-main 内,
而 .wi-main 有自己的层叠上下文(z-index:1),导致弹窗被导航栏
(z-index:100)和页脚遮挡。移到 body 下即可恢复正常层级。 */
var modal = document.getElementById("wrNotesModal");
if (modal && modal.parentNode !== document.body) {
document.body.appendChild(modal);
}
/* innerHTML 插入的脚本不会执行,需要重建后才会运行 */ /* innerHTML 插入的脚本不会执行,需要重建后才会运行 */
var oldScripts = container.querySelectorAll("script"); var oldScripts = container.querySelectorAll("script");
Array.prototype.forEach.call(oldScripts, function (oldScript) { Array.prototype.forEach.call(oldScripts, function (oldScript) {
@@ -86,7 +94,7 @@ import Layout from "../layouts/Layout.astro";
</Layout> </Layout>
<style> <style>
/* 头部结构与订阅/图库/友链等页面保持一致:居中标题 + 副标题 + 强调线 */ /* 头部结构与订阅/图库/友链/记得等页面保持一致:居中标题 + 副标题 + 强调线 */
.wi-weread { .wi-weread {
padding-block: 1rem 3rem; padding-block: 1rem 3rem;
} }
@@ -98,14 +106,23 @@ import Layout from "../layouts/Layout.astro";
.wi-weread__title { .wi-weread__title {
font-size: var(--text-4xl); font-size: var(--text-4xl);
margin-bottom: var(--space-xs); margin-bottom: var(--space-sm);
} }
.wi-weread__subtitle { .wi-weread__subtitle {
font-size: var(--text-sm); font-size: var(--text-sm);
color: var(--ink-3); color: var(--ink-3);
letter-spacing: var(--tracking-wide); letter-spacing: var(--tracking-wide);
margin-block-end: 0; margin-bottom: var(--space-sm);
}
.wi-page-accent {
display: block;
width: 48px;
height: 3px;
margin: var(--space-sm) auto 0;
border-radius: 9999px;
background: var(--accent);
} }
.wi-weread__shelf { .wi-weread__shelf {
@@ -149,6 +166,19 @@ import Layout from "../layouts/Layout.astro";
padding-top: 0; padding-top: 0;
} }
/* 统计数据水平居中,紧贴主题标题区(标题 + 强调线)下方 */
.wi-weread__shelf :global(.wr-head) {
justify-content: center;
}
.wi-weread__shelf :global(.wr-head-stats) {
justify-content: center;
}
.wi-weread__shelf :global(.wr-head-stats span) {
text-align: center;
}
.wi-weread__shelf :global(img) { .wi-weread__shelf :global(img) {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
@@ -161,12 +191,13 @@ import Layout from "../layouts/Layout.astro";
/* ===== 暗黑模式:插件内容为苹果风格浅色配色,深色下需要接管 ===== */ /* ===== 暗黑模式:插件内容为苹果风格浅色配色,深色下需要接管 ===== */
/* 覆盖插件内部变量(按其命名约定 --wr-*,未命中也无副作用) */ /* 覆盖插件内部变量(按其命名约定 --wr-*,未命中也无副作用)。
--wr-line 比全站分隔线略亮:年份分组等装饰线在深色底上需要保证可见 */
html.dark .wi-weread__shelf :global(.wr-shelf) { html.dark .wi-weread__shelf :global(.wr-shelf) {
--wr-ink: var(--ink); --wr-ink: var(--ink);
--wr-muted: var(--ink-2); --wr-muted: var(--ink-2);
--wr-bg: var(--bg-raised); --wr-bg: var(--bg-raised);
--wr-line: var(--rule); --wr-line: #4a443c;
--wr-fill: color-mix(in srgb, var(--ink) 8%, transparent); --wr-fill: color-mix(in srgb, var(--ink) 8%, transparent);
} }
@@ -179,8 +210,30 @@ import Layout from "../layouts/Layout.astro";
color: var(--ink-2); color: var(--ink-2);
} }
/* 笔记弹窗浅色底在深色页面中过于突兀 */ /* ===== 笔记弹窗:运行时被移到 body 下,脱离了 .wr-shelf 的变量作用域,
html.dark .wi-weread__shelf :global(.wr-notes-modal) { 需要独立适配(选择器整体 :global,否则 scoped 属性匹配不到注入节点) ===== */
background: color-mix(in srgb, var(--bg) 94%, transparent); :global(html.dark .wr-notes-modal) {
--wr-ink: var(--ink);
--wr-muted: var(--ink-2);
--wr-bg: var(--bg-raised);
--wr-line: #4a443c;
--wr-fill: color-mix(in srgb, var(--ink) 8%, transparent);
}
:global(html.dark .wr-notes-dialog) {
background: var(--bg-raised);
}
:global(html.dark .wr-notes-close) {
background: color-mix(in srgb, var(--ink) 10%, transparent);
color: var(--ink);
}
:global(html.dark .wr-notes-modal :where(h3, h4, strong)) {
color: var(--ink);
}
:global(html.dark .wr-notes-modal :where(p, span, small, em, li)) {
color: var(--ink-2);
} }
</style> </style>
+30 -26
View File
@@ -1,7 +1,9 @@
:root { :root {
--bg: #faf7f2; --bg: #faf7f2;
--bg-raised: #f3ede5; --bg-raised: #f3ede5;
--bg-overlay: rgba(250, 247, 242, 0.85); /* 派生变量基于 --bg / --rule 动态混合:调色板只需覆盖核心变量,
玻璃拟态、遮罩层等会自动跟随当前配色(暖屿默认时与旧值完全一致) */
--bg-overlay: color-mix(in srgb, var(--bg) 85%, transparent);
--ink: #2c2420; --ink: #2c2420;
--ink-2: #7a6e64; --ink-2: #7a6e64;
--ink-3: #b5a99e; --ink-3: #b5a99e;
@@ -12,33 +14,35 @@
--mist-pink: #f0e4de; --mist-pink: #f0e4de;
--sea-salt: #e8e0d6; --sea-salt: #e8e0d6;
--caramel: #8b6f5e; --caramel: #8b6f5e;
--glass-bg: rgba(250, 247, 242, 0.72); --glass-bg: color-mix(in srgb, var(--bg) 72%, transparent);
--glass-border: rgba(232, 224, 214, 0.5); --glass-border: color-mix(in srgb, var(--rule) 50%, transparent);
--shadow-sm: 0 1px 3px rgba(44, 36, 32, 0.06); /* 阴影色由墨色动态混合,保证换配色后阴影色调一致 */
--shadow-md: 0 4px 16px rgba(44, 36, 32, 0.08); --shadow-sm: 0 1px 3px color-mix(in srgb, var(--ink) 6%, transparent);
--shadow-lg: 0 8px 32px rgba(44, 36, 32, 0.12); --shadow-md: 0 4px 16px color-mix(in srgb, var(--ink) 8%, transparent);
--shadow-glow: 0 0 40px rgba(212, 118, 78, 0.15); --shadow-lg: 0 8px 32px color-mix(in srgb, var(--ink) 12%, transparent);
--shadow-glow: 0 0 40px color-mix(in srgb, var(--accent) 15%, transparent);
} }
html.dark { html.dark {
--bg: #1a1614; /* 暗色基准:偏中性、只保留一丝暖意,避免大面积黄褐底色带来的闷感 */
--bg-raised: #26211d; --bg: #1e1c1a;
--bg-overlay: rgba(26, 22, 20, 0.85); --bg-raised: #292623;
--ink: #ede6de; --bg-overlay: color-mix(in srgb, var(--bg) 85%, transparent);
--ink-2: #a2978c; --ink: #e9e4dd;
--ink-2: #aaa096;
/* 深色背景下人眼对低对比更敏感,次要文字与分隔线比浅色模式略提亮 */ /* 深色背景下人眼对低对比更敏感,次要文字与分隔线比浅色模式略提亮 */
--ink-3: #6f6459; --ink-3: #7c7268;
--rule: #38322b; --rule: #3b362f;
--accent: #e8955f; --accent: #e89a64;
--accent-hover: #f0a872; --accent-hover: #f2ad79;
--accent-bg: #2a1e16; --accent-bg: #2c231b;
--mist-pink: #2e2420; --mist-pink: #2e2823;
--sea-salt: #2e2822; --sea-salt: #302b26;
--caramel: #a08878; --caramel: #a38b7b;
--glass-bg: rgba(26, 22, 20, 0.72); --glass-bg: color-mix(in srgb, var(--bg) 72%, transparent);
--glass-border: rgba(46, 40, 34, 0.5); --glass-border: color-mix(in srgb, var(--rule) 50%, transparent);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.2); --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.24);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.25); --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.35); --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.4);
--shadow-glow: 0 0 40px rgba(232, 149, 95, 0.12); --shadow-glow: 0 0 40px rgba(232, 154, 100, 0.12);
} }
+6 -6
View File
@@ -33,37 +33,37 @@
} }
h1 { h1 {
font-size: clamp(1.45rem, 1.3rem + 0.7vw, 1.75rem); font-size: clamp(1.6rem, 1.4rem + 1vw, 2rem);
margin-top: 2em; margin-top: 2em;
margin-bottom: 0.7em; margin-bottom: 0.7em;
} }
h2 { h2 {
font-size: clamp(1.3rem, 1.2rem + 0.5vw, 1.5rem); font-size: clamp(1.42rem, 1.3rem + 0.6vw, 1.68rem);
margin-top: 1.9em; margin-top: 1.9em;
margin-bottom: 0.65em; margin-bottom: 0.65em;
} }
h3 { h3 {
font-size: clamp(1.15rem, 1.08rem + 0.35vw, 1.28rem); font-size: clamp(1.24rem, 1.16rem + 0.4vw, 1.4rem);
margin-top: 1.7em; margin-top: 1.7em;
margin-bottom: 0.6em; margin-bottom: 0.6em;
} }
h4 { h4 {
font-size: clamp(1.05rem, 1rem + 0.25vw, 1.13rem); font-size: clamp(1.1rem, 1.05rem + 0.25vw, 1.2rem);
margin-top: 1.5em; margin-top: 1.5em;
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
h5 { h5 {
font-size: 1rem; font-size: 1.04rem;
margin-top: 1.4em; margin-top: 1.4em;
margin-bottom: 0.45em; margin-bottom: 0.45em;
} }
h6 { h6 {
font-size: 0.95rem; font-size: 0.98rem;
font-weight: 600; font-weight: 600;
color: var(--ink-3); color: var(--ink-3);
margin-top: 1.4em; margin-top: 1.4em;
+320
View File
@@ -0,0 +1,320 @@
/*
* 主题配色系统:官方预设调色板
* ---------------------------------------------------------------
* 每个[调色板]覆盖核心色彩变量(浅色 html[data-palette="x"] /
* 深色 html[data-palette="x"].dark 两套)。
* 派生变量(--bg-overlay、--glass-*、阴影色等)在 _colors.scss 中
* 由 --bg / --rule / --ink / --accent 动态混合而成,会自动跟随。
* data-palette 由 layout 头部根据后台「总体样式 → 主题配色」输出;
* 默认值 warm-island(暖屿·赤陶)即 _colors.scss 的基准值,无需覆盖。
*/
/* 抹茶 */
html[data-palette="matcha"] {
--bg: #f6f8f1;
--bg-raised: #ecf1e3;
--ink: #26302a;
--ink-2: #6d7a6e;
--ink-3: #a8b3a4;
--rule: #dde5d4;
--accent: #6f9d52;
--accent-hover: #5d8943;
--accent-bg: #ecf3e2;
--mist-pink: #e7eddc;
--sea-salt: #dfe8d3;
--caramel: #7d8a68;
}
html[data-palette="matcha"].dark {
--bg: #1b1f1a;
--bg-raised: #252b22;
--ink: #e6e9e0;
--ink-2: #a3ab9c;
--ink-3: #767e70;
--rule: #363d33;
--accent: #9cc47f;
--accent-hover: #aed493;
--accent-bg: #242c1f;
--mist-pink: #262d22;
--sea-salt: #282f24;
--caramel: #98a387;
}
/* 黛蓝 */
html[data-palette="indigo"] {
--bg: #f5f7fa;
--bg-raised: #eaeff5;
--ink: #232a33;
--ink-2: #66707d;
--ink-3: #a3adb8;
--rule: #dce2ea;
--accent: #4a6fa5;
--accent-hover: #3d5d8c;
--accent-bg: #e9eff8;
--mist-pink: #e2e8f0;
--sea-salt: #dbe4ee;
--caramel: #707c8e;
}
html[data-palette="indigo"].dark {
--bg: #191d23;
--bg-raised: #222831;
--ink: #e4e7ec;
--ink-2: #9aa4b1;
--ink-3: #6d7683;
--rule: #323a45;
--accent: #7fa3d1;
--accent-hover: #93b4dc;
--accent-bg: #1f2632;
--mist-pink: #232830;
--sea-salt: #252b34;
--caramel: #8b95a6;
}
/* 樱粉 */
html[data-palette="sakura"] {
--bg: #faf5f6;
--bg-raised: #f3eaec;
--ink: #322a2c;
--ink-2: #7d6e72;
--ink-3: #b4a6aa;
--rule: #ecdfe2;
--accent: #d97387;
--accent-hover: #c85d72;
--accent-bg: #fbeef1;
--mist-pink: #f2e2e6;
--sea-salt: #eddfe3;
--caramel: #96757e;
}
html[data-palette="sakura"].dark {
--bg: #201b1c;
--bg-raised: #2a2425;
--ink: #ece5e6;
--ink-2: #b0a4a7;
--ink-3: #7f7276;
--rule: #3c3436;
--accent: #e89aab;
--accent-hover: #f0aec0;
--accent-bg: #2d2225;
--mist-pink: #2f2729;
--sea-salt: #31282a;
--caramel: #a88d94;
}
/* 暮紫 */
html[data-palette="violet"] {
--bg: #f7f5fa;
--bg-raised: #eeeaf5;
--ink: #2b2733;
--ink-2: #6f6a7c;
--ink-3: #aaa5b6;
--rule: #e2ddeb;
--accent: #8a6fc0;
--accent-hover: #7759ae;
--accent-bg: #f0eaf9;
--mist-pink: #e8e2f0;
--sea-salt: #e3dcee;
--caramel: #7f7590;
}
html[data-palette="violet"].dark {
--bg: #1c1a21;
--bg-raised: #26222f;
--ink: #e8e5ee;
--ink-2: #a49eaf;
--ink-3: #746e80;
--rule: #373241;
--accent: #b49ae0;
--accent-hover: #c3ace8;
--accent-bg: #262033;
--mist-pink: #292434;
--sea-salt: #2b2536;
--caramel: #948caa;
}
/* 海盐 */
html[data-palette="sea"] {
--bg: #f3f8f7;
--bg-raised: #e8f0ee;
--ink: #21302d;
--ink-2: #647872;
--ink-3: #9fb1ac;
--rule: #d9e6e3;
--accent: #3d9c8f;
--accent-hover: #2f8578;
--accent-bg: #e5f3f0;
--mist-pink: #dee9e6;
--sea-salt: #d6e5e1;
--caramel: #6c8680;
}
html[data-palette="sea"].dark {
--bg: #181f1e;
--bg-raised: #212a28;
--ink: #e3eae8;
--ink-2: #9aaba6;
--ink-3: #6d7d79;
--rule: #2f3b39;
--accent: #6cc3b4;
--accent-hover: #82d0c2;
--accent-bg: #1d2a27;
--mist-pink: #222c2a;
--sea-salt: #242e2c;
--caramel: #869894;
}
/* 琥珀 */
html[data-palette="amber"] {
--bg: #faf7ef;
--bg-raised: #f3eddc;
--ink: #322b1e;
--ink-2: #7d7360;
--ink-3: #b5ab94;
--rule: #eae1cb;
--accent: #b97f1c;
--accent-hover: #9f6a0e;
--accent-bg: #f9efd8;
--mist-pink: #f0e8d2;
--sea-salt: #ebe2ca;
--caramel: #96835c;
}
html[data-palette="amber"].dark {
--bg: #201d16;
--bg-raised: #2a261d;
--ink: #ebe6d9;
--ink-2: #b0a893;
--ink-3: #7f7866;
--rule: #3c3729;
--accent: #e0a83e;
--accent-hover: #ebba5c;
--accent-bg: #2c251a;
--mist-pink: #2f2a1e;
--sea-salt: #312c20;
--caramel: #a89a76;
}
/* 梅酒 */
html[data-palette="plum"] {
--bg: #f9f4f5;
--bg-raised: #f1e8ea;
--ink: #2f2427;
--ink-2: #796a6f;
--ink-3: #b1a2a7;
--rule: #e9dde0;
--accent: #a4506b;
--accent-hover: #8d3f59;
--accent-bg: #f7e9ee;
--mist-pink: #ecdee2;
--sea-salt: #e5d8db;
--caramel: #8d6f77;
}
html[data-palette="plum"].dark {
--bg: #1f1a1b;
--bg-raised: #292223;
--ink: #eae2e4;
--ink-2: #ada0a4;
--ink-3: #7c6e73;
--rule: #3a3133;
--accent: #d98aa3;
--accent-hover: #e39fb5;
--accent-bg: #2b2023;
--mist-pink: #2d2527;
--sea-salt: #2f2729;
--caramel: #a08890;
}
/* 石墨 */
html[data-palette="graphite"] {
--bg: #f7f7f6;
--bg-raised: #eeedeb;
--ink: #262626;
--ink-2: #6e6e6c;
--ink-3: #ababa8;
--rule: #e0e0de;
--accent: #3f3f3e;
--accent-hover: #262626;
--accent-bg: #ececea;
--mist-pink: #e9e9e7;
--sea-salt: #e5e5e3;
--caramel: #7b7b78;
}
html[data-palette="graphite"].dark {
--bg: #1b1b1a;
--bg-raised: #252524;
--ink: #e8e8e6;
--ink-2: #a5a5a2;
--ink-3: #757572;
--rule: #353534;
--accent: #c9c9c5;
--accent-hover: #d8d8d4;
--accent-bg: #262625;
--mist-pink: #2a2a29;
--sea-salt: #2c2c2b;
--caramel: #8f8f8c;
}
/* 可拿铁 */
html[data-palette="cocoa"] {
--bg: #f8f4f0;
--bg-raised: #f0e8e1;
--ink: #2e2722;
--ink-2: #786c62;
--ink-3: #b3a89d;
--rule: #e7ddd3;
--accent: #9a6b4f;
--accent-hover: #835539;
--accent-bg: #f5e9e0;
--mist-pink: #ecdfd4;
--sea-salt: #e6d9cd;
--caramel: #8a6f5b;
}
html[data-palette="cocoa"].dark {
--bg: #1e1a17;
--bg-raised: #282320;
--ink: #eae4dd;
--ink-2: #aca199;
--ink-3: #7b7169;
--rule: #3a332e;
--accent: #cd9878;
--accent-hover: #daa98c;
--accent-bg: #2a211c;
--mist-pink: #2d2721;
--sea-salt: #2f2923;
--caramel: #a08d7c;
}
/* 天青 */
html[data-palette="azure"] {
--bg: #f4f8fb;
--bg-raised: #e9f1f7;
--ink: #223038;
--ink-2: #62737e;
--ink-3: #a0b0ba;
--rule: #dbe7ee;
--accent: #3b82c4;
--accent-hover: #2e6ba5;
--accent-bg: #e7f1f9;
--mist-pink: #e0ebf2;
--sea-salt: #d8e6ef;
--caramel: #6d8090;
}
html[data-palette="azure"].dark {
--bg: #172026;
--bg-raised: #202a31;
--ink: #e3eaef;
--ink-2: #99a8b3;
--ink-3: #6b7a85;
--rule: #2e3a42;
--accent: #74b3e0;
--accent-hover: #88c1e8;
--accent-bg: #1d2830;
--mist-pink: #222c34;
--sea-salt: #243038;
--caramel: #8496a3;
}
+21
View File
@@ -0,0 +1,21 @@
/* ==========================================================================
「文章组件」插件(plugin-content-widgets)主题适配
--------------------------------------------------------------------------
插件自带样式与挂载逻辑,主题只需要把它的 --xhhao-com-* 变量
映射到主题变量上:组件即可自动跟随主题的暖色 / 暗色模式。
变量清单见插件 README:https://github.com/acanyo/plugin-content-widgets
========================================================================== */
:root {
--xhhao-com-text-1: var(--ink);
--xhhao-com-text-2: var(--ink-2);
--xhhao-com-bg: var(--bg);
--xhhao-com-bg-card: var(--bg-raised);
--xhhao-com-bg-2: var(--bg-raised);
--xhhao-com-border: var(--rule);
--xhhao-com-primary: var(--accent);
--xhhao-com-shadow: rgb(15 23 42 / 0.08);
}
/* 组件自带的浅色 fallback 偏冷灰,暗色下显突兀;
变量跟随主题后,暗色模式自动生效,无需单独覆盖。 */
+2
View File
@@ -1,6 +1,7 @@
@use 'variables' as *; @use 'variables' as *;
@use 'mixins'; @use 'mixins';
@use 'colors'; @use 'colors';
@use 'palettes';
@use 'typography'; @use 'typography';
@use 'content'; @use 'content';
@use 'spacing'; @use 'spacing';
@@ -8,6 +9,7 @@
@use 'comment'; @use 'comment';
@use 'search'; @use 'search';
@use 'lightgallery'; @use 'lightgallery';
@use 'widgets';
*, *,
*::before, *::before,
+2 -2
View File
@@ -28,8 +28,8 @@ spec:
description: 微信读书书架(依赖微信读书插件) description: 微信读书书架(依赖微信读书插件)
screenshot: screenshot:
file: weread.html file: weread.html
version: "1.1.7" version: "1.2.1"
requires: ">=2.24.0" requires: ">=2.26.0"
license: license:
- name: "GPL-3.0" - name: "GPL-3.0"
url: "https://www.gnu.org/licenses/gpl-3.0.en.html" url: "https://www.gnu.org/licenses/gpl-3.0.en.html"