fix: 修复首页卡片字数与 Hero 滚动,移除光感跟随 (v1.1.6)

修复
- 首页文章卡片开启"显示字数"后无效果:该配置项此前只有声明没有实现,现已在文章流卡片接入服务端字数统计(依赖 API 扩展包,未安装时自动隐藏)
- Hero 下滑箭头滚动不到位:Hero 为固定背景(position: fixed),此前箭头额外减去了导航栏高度,导致内容区上方始终露出一段 Hero 渐变;现与滚动吸附逻辑对齐,内容区顶部直接对齐视口顶部
- 鼠标点击特效开启后无效果:特效类型默认值为 off(关闭),开启开关但不选择类型时静默不生效;默认值改为"社会主义核心价值观"

移除
- 移除鼠标光感跟随功能及对应配置项

优化
- 上下篇导航:新增方向箭头与悬停反馈(背景、位移、变色),移动端收紧内边距
- Hero 一言自动刷新改为淡出→换文案→淡入的过渡动画,避免生硬跳变
- README 全面更新:环境要求、插件适配表(新增微信读书与 API 扩展包)、自定义页面模板、功能说明、目录结构与开发注意事项
This commit is contained in:
2026-09-26 12:29:28 +08:00
parent 430d107d44
commit bcc221c3bd
10 changed files with 180 additions and 105 deletions
+80 -19
View File
@@ -4,13 +4,18 @@
> 本项目由 AI 开发完成,人工仅进行部分功能测试及问题找寻 > 本项目由 AI 开发完成,人工仅进行部分功能测试及问题找寻
## 环境要求
- Halo **2.26.0 及以上**(页面布局契约、微信读书页面均依赖 2.26 的新能力)
## 特点 ## 特点
1. **响应式设计** — 适配桌面端与移动端,支持暗色模式 1. **响应式设计** — 适配桌面端与移动端,支持暗色模式,移动端使用动态视口单位适配地址栏
2. **插件兼容性** — 适配 8 款常用 Halo 插件,安装即用 2. **插件兼容性** — 适配 9 款常用 Halo 插件,安装即用;提供页面布局契约,插件前台页面自动复用主题外观
3. **内置灯箱** — 主题内置 LightGallery 灯箱,支持 EXIF 信息展示;安装 lightgallery.js 插件后自动禁用内置灯箱 3. **内置灯箱** — 主题内置 LightGallery 灯箱,支持 EXIF 信息展示;安装 lightgallery.js 插件后自动禁用
4. **可配置性** — 丰富的后台设置项,支持自定义首页模块、文章卡片、导航栏样式等 4. **可配置性** — 16 个设置分组、117 个配置项,几乎所有模块可独立开关
5. **性能优化** — 图片懒加载、CSS/HTML 压缩、requestAnimationFrame 节流、Vue 组件按需加载 5. **性能优化** — 服务端字数统计(API 扩展包)、图片懒加载与响应式缩略图、滚动吸附、CSS/HTML 压缩
6. **页面布局契约** — 提供 `templates/layout.html`,插件页面不再回退到 Halo 内置布局
## 预览 ## 预览
@@ -20,15 +25,51 @@
| 插件名称 | 插件标识 | 适配页面路由 | 说明 | | 插件名称 | 插件标识 | 适配页面路由 | 说明 |
|----------|---------|-------------|------| |----------|---------|-------------|------|
| [瞬间](https://halo.run/store/apps/app-SnwWD) | `PluginMoments` | `/moments` | 瞬间动态页面,支持点赞、评论、标签筛选 | | [瞬间](https://halo.run/store/apps/app-SnwWD) | `PluginMoments` | `/moments` | 瞬间动态页面,支持点赞、评论、标签筛选、顶部封面 |
| [图库](https://halo.run/store/apps/app-BmQJW) | `PluginPhotos` | `/photos` | 图片展示页面,支持分组、灯箱预览 | | [图库](https://halo.run/store/apps/app-BmQJW) | `PluginPhotos` | `/photos` | 图片展示页面,支持分组、灯箱预览 |
| [友情链接](https://halo.run/store/apps/app-hqbe) | `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` | 豆瓣记录页面,支持类型/状态/标签筛选,默认电影 |
| [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` | 自定义页面 | 书架展示页面,见下方「自定义页面模板」 |
| [API 扩展包](https://github.com/HowieHz/halo-plugin-extra-api) | `extra-api` | 全站 | 服务端字数统计、中英文混排排版、可选服务端代码高亮 |
> **注意**:插件需要单独安装,安装后主题会自动适配对应页面。若未安装某插件,对应页面不会出现。 > **注意**:插件需要单独安装,安装后主题会自动适配对应页面。若未安装某插件,对应页面或功能自动隐藏/降级,不会报错。
## 自定义页面模板
在 Halo 后台「页面 → 新建 → 自定义模板」中选择使用:
| 模板名 | 模板文件 | 说明 |
|--------|---------|------|
| 留言板 | `page_messageboard.html` | 留言墙页面,标题与副标题可配置 |
| 订阅 | `page_subscriptions.html` | 博友文章订阅动态,依赖链接管理插件的 RSS 订阅,支持分页/下滑加载与列表/网格布局 |
| 微信读书 | `weread.html` | 微信读书书架展示,依赖微信读书插件,访客免登录查看 |
## 主要功能
### 首页
- Hero 首屏:明暗双背景图、大标题/副标题独立配色、一言(支持自动刷新与淡入淡出过渡)、下滑箭头平滑滚动
- 欢迎弹窗:基于访客 IP 地理定位的欢迎提示,支持 5 种位置、自定义 API 与会话去重开关
- 滚动吸附:首屏与内容区之间滚轮/触摸自动吸附
- 文章卡片:可配置显示字数(依赖 API 扩展包)、阅读量、分类、标签、摘要行数
### 文章
- 目录(TOC):桌面端侧栏 + 移动端抽屉,滚动高亮、展开模式可选
- 上下篇导航:悬停反馈与方向箭头动画
- 相关推荐、分享按钮、赞赏、版权声明可独立开关
- 字数与阅读时间:优先使用 API 扩展包的服务端统计,未安装时前端计算兜底
### 全局
- 点击特效:核心价值观 / 爱心 / 烟花 / 粒子等 9 种可选
- 页脚全站统计:文章、分类、评论、访问、点赞、总字数,支持开关与统计项选择(默认开启)
- 动效:呼吸动画、滚动渐入,可独立关闭
- 备案信息:ICP 与公网安备号,公网安备自动带徽标
- 鼠标光感跟随已移除(性能考量)
## 灯箱集成 ## 灯箱集成
@@ -56,33 +97,53 @@
或从源码构建: 或从源码构建:
```bash ```bash
git clone https://github.com/warm-island/theme-warm-island.git git clone https://git.nxxy335.top/nxxy335/halo-theme-WarmIsland.git
cd theme-warm-island cd halo-theme-WarmIsland
npm install pnpm install
npm run build pnpm build
``` ```
构建产物在 `templates/` 目录下,将其复制到 Halo 主题目录即可。 构建产物在 `templates/` 目录下,同时会生成 `dist/` 内的安装包。
## 开发 ## 开发
```bash ```bash
# 安装依赖 # 安装依赖
npm install pnpm install
# 开发模式(监听文件变化) # 开发模式(监听文件变化,自动构建到 templates/)
npm run dev pnpm dev
# 构建 # 构建
npm run build pnpm build
``` ```
### 目录结构
```
├── src/
│ ├── components/ # 组件(导航、页脚、Hero、弹窗、特效等)
│ ├── layouts/ # Layout.astro(含页面布局契约渲染模式)
│ ├── pages/ # 页面模板(构建后输出到 templates/)
│ └── styles/ # SCSS 样式(正文样式在 _content.scss,全局生效)
├── templates/ # 构建产物,Halo 实际加载的模板
├── settings.yaml # 后台设置项定义(16 分组 / 117 项)
└── theme.yaml # 主题清单与自定义页面模板注册
```
### 主题开发注意事项
- **Thymeleaf 字符串字面量必须加单引号**:`th:text="'文本'"`,裸中文遇到逗号/空格/emoji 会导致页面 500
- **正文样式必须写在全局样式表**(`src/styles/_content.scss`):Astro scoped 样式对 Thymeleaf 运行时注入的内容无效
- **内联脚本统一加 `th:inline="none"`**:避免 Thymeleaf 解析脚本中的 `[[` / `[(` / `${}`
- 修改主题外观无需单独维护 `layout.astro`:页面布局契约复用 `Layout.astro`,构建时自动生成
## 技术栈 ## 技术栈
- [Halo](https://halo.run) 2.24+ - [Halo](https://halo.run) 2.26+
- [Astro](https://astro.build) + [vite-plugin-halo-theme](https://github.com/halo-sigs/vite-plugin-halo-theme) - [Astro](https://astro.build) + [vite-plugin-halo-theme](https://github.com/halo-sigs/vite-plugin-halo-theme)
- [Thymeleaf](https://www.thymeleaf.org) 模板引擎 - [Thymeleaf](https://www.thymeleaf.org) 模板引擎
- [Vue 3](https://vuejs.org) Islands 交互组件 - [Sass](https://sass-lang.com/) 样式
## 许可 ## 许可
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"version": "1.1.5", "version": "1.1.6",
"type": "module", "type": "module",
"engines": { "engines": {
"node": ">=22.12.0" "node": ">=22.12.0"
+1 -5
View File
@@ -141,10 +141,6 @@ spec:
name: animation_scroll_reveal name: animation_scroll_reveal
label: 滚动渐入 label: 滚动渐入
value: true value: true
- $formkit: switch
name: animation_cursor_glow
label: 光感跟随
value: false
- $formkit: switch - $formkit: switch
name: click_effect_enabled name: click_effect_enabled
id: click_effect_enabled id: click_effect_enabled
@@ -155,7 +151,7 @@ spec:
name: click_effect_type name: click_effect_type
id: click_effect_type id: click_effect_type
label: 点击效果类型 label: 点击效果类型
value: off value: values
if: "$get(click_effect_enabled).value === true" if: "$get(click_effect_enabled).value === true"
options: options:
- label: 关闭 - label: 关闭
-56
View File
@@ -1,56 +0,0 @@
---
---
<div class="wi-cursor-glow" id="wi-cursor-glow"></div>
<script is:inline th:inline="none">
(function(){
var glowEl = document.getElementById("wi-cursor-glow");
if (!glowEl) return;
var rafId = 0;
document.addEventListener("mousemove", function(e) {
if (rafId) return;
rafId = requestAnimationFrame(function() {
glowEl.style.transform = "translate(calc(" + e.clientX + "px - 50%), calc(" + e.clientY + "px - 50%))";
glowEl.classList.add("wi-cursor-glow--visible");
rafId = 0;
});
}, { passive: true });
document.addEventListener("mouseleave", function() {
glowEl.classList.remove("wi-cursor-glow--visible");
});
})();
</script>
<style>
.wi-cursor-glow {
position: fixed;
top: 0;
left: 0;
width: 400px;
height: 400px;
border-radius: 50%;
background: radial-gradient(
circle,
rgba(212, 118, 78, 0.06) 0%,
transparent 70%
);
pointer-events: none;
z-index: 9999;
opacity: 0;
transition: opacity 0.3s ease;
will-change: transform;
}
.wi-cursor-glow--visible {
opacity: 1;
}
html.dark .wi-cursor-glow {
background: radial-gradient(
circle,
rgba(232, 149, 95, 0.04) 0%,
transparent 70%
);
}
</style>
+14 -7
View File
@@ -100,6 +100,7 @@
var top; var top;
if (content) { if (content) {
/* 与首页滚动吸附逻辑保持一致:内容区顶部对齐视口顶部,完全盖住固定背景的 Hero */
top = content.getBoundingClientRect().top + window.pageYOffset; top = content.getBoundingClientRect().top + window.pageYOffset;
} else if (hero) { } else if (hero) {
top = hero.offsetTop + hero.offsetHeight; top = hero.offsetTop + hero.offsetHeight;
@@ -107,16 +108,12 @@
top = window.innerHeight; top = window.innerHeight;
} }
/* 预留固定导航栏的高度,避免内容被顶栏遮住 */
var navbar = document.getElementById("wi-navbar");
var offset = navbar ? navbar.offsetHeight + 8 : 8;
var reduce = var reduce =
window.matchMedia && window.matchMedia &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches; window.matchMedia("(prefers-reduced-motion: reduce)").matches;
window.scrollTo({ window.scrollTo({
top: Math.max(0, top - offset), top: Math.max(0, top),
behavior: reduce ? "auto" : "smooth", behavior: reduce ? "auto" : "smooth",
}); });
}); });
@@ -158,17 +155,27 @@
return res.json(); return res.json();
}) })
.then(function (data) { .then(function (data) {
el.style.opacity = "1";
var text = data.hitokoto || ""; var text = data.hitokoto || "";
var from = data.from || data.from_who || ""; var from = data.from || data.from_who || "";
if (from) { if (from) {
text += " \u2014\u2014 " + from; text += " \u2014\u2014 " + from;
} }
if (initial) {
el.style.opacity = "1";
el.textContent = text; el.textContent = text;
} else {
/* 自动刷新:淡出 → 换文案 → 淡入,避免生硬跳变 */
el.style.opacity = "0";
setTimeout(function () {
el.textContent = text;
el.style.opacity = "1";
}, 280);
}
}) })
.catch(function () { .catch(function () {
// 失败时恢复可见:首次为空,刷新时保留原文案
el.style.opacity = "1"; el.style.opacity = "1";
// 自动刷新失败时保留原文案,避免闪烁成空白
if (initial) el.textContent = ""; if (initial) el.textContent = "";
}); });
} }
+22
View File
@@ -28,10 +28,18 @@ const { variant = "default" } = Astro.props;
</div> </div>
<div class="post-card__content"> <div class="post-card__content">
<div class="post-card__meta">
<time <time
class="post-card__date" class="post-card__date"
th:text="${#dates.format(post.spec.publishTime, 'yyyy-MM-dd')}" th:text="${#dates.format(post.spec.publishTime, 'yyyy-MM-dd')}"
></time> ></time>
<span
class="post-card__words"
th:if="${theme.config?.home?.home_card_show_wordcount == true and pluginFinder.available('extra-api')}"
th:with="words=${extraApiStatsFinder.getPostWordCount({name: post.metadata.name})}"
th:text="${words > 0 ? words + ' 字' : ''}"
></span>
</div>
<h3 class="post-card__title" th:text="${post.spec.title}"></h3> <h3 class="post-card__title" th:text="${post.spec.title}"></h3>
<p <p
th:if="${variant != 'compact'}" th:if="${variant != 'compact'}"
@@ -110,6 +118,20 @@ const { variant = "default" } = Astro.props;
line-height: var(--leading-none); line-height: var(--leading-none);
} }
.post-card__meta {
display: flex;
align-items: center;
gap: 0.6rem;
}
.post-card__words {
font-size: var(--text-xs);
color: var(--ink-3);
line-height: var(--leading-none);
padding-left: 0.6rem;
border-left: 1px solid var(--rule);
}
.post-card__title { .post-card__title {
font-family: var(--font-sans); font-family: var(--font-sans);
font-weight: 700; font-weight: 700;
-3
View File
@@ -3,10 +3,8 @@ import "../styles/main.scss";
import Navbar from "../components/Navbar.astro"; import Navbar from "../components/Navbar.astro";
import Footer from "../components/Footer.astro"; import Footer from "../components/Footer.astro";
import ScrollReveal from "../components/ScrollReveal.astro"; import ScrollReveal from "../components/ScrollReveal.astro";
import CursorGlow from "../components/CursorGlow.astro";
import MobileMenu from "../components/MobileMenu.astro"; import MobileMenu from "../components/MobileMenu.astro";
import ClickEffect from "../components/ClickEffect.astro"; import ClickEffect from "../components/ClickEffect.astro";
interface Props { interface Props {
pageTitle?: string; pageTitle?: string;
contentClass?: string; contentClass?: string;
@@ -119,7 +117,6 @@ const fragmentAttr = layoutFragment ? "html (head, content)" : undefined;
<Footer /> <Footer />
<MobileMenu /> <MobileMenu />
<ScrollReveal th:if="${theme.config?.animation?.animation_enabled != false and theme.config?.animation?.animation_scroll_reveal != false}" /> <ScrollReveal th:if="${theme.config?.animation?.animation_enabled != false and theme.config?.animation?.animation_scroll_reveal != false}" />
<CursorGlow th:if="${theme.config?.animation?.animation_enabled != false and theme.config?.animation?.animation_cursor_glow == true}" />
<ClickEffect /> <ClickEffect />
</body> </body>
</html> </html>
+5
View File
@@ -127,6 +127,11 @@ import WelcomePopup from "../components/WelcomePopup.astro";
</div> </div>
</th:block> </th:block>
<div class="wi-flow__stats"> <div class="wi-flow__stats">
<span
class="wi-flow__stat"
th:if="${theme.config?.home?.home_card_show_wordcount == true and pluginFinder.available('extra-api')}"
th:text="${extraApiStatsFinder.getPostWordCount({name: post.metadata.name}) + ' 字'}"
></span>
<span <span
class="wi-flow__stat" class="wi-flow__stat"
th:if="${theme.config?.home?.home_card_show_visit ?: true}" th:if="${theme.config?.home?.home_card_show_visit ?: true}"
+51 -8
View File
@@ -1315,10 +1315,35 @@ import LightGallery from "../components/LightGallery.astro";
.wi-post__nav-link { .wi-post__nav-link {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.3rem; gap: 0.35rem;
text-decoration: none; text-decoration: none;
max-width: 45%; max-width: 48%;
transition: color var(--duration-fast) var(--ease-out-quart); padding: 0.85rem 1.1rem;
border-radius: var(--radius-md, 12px);
background: transparent;
transition: background 0.2s ease, transform 0.2s var(--ease-out-quart);
}
.wi-post__nav-link:hover {
background: color-mix(in srgb, var(--accent) 7%, transparent);
}
.wi-post__nav-link--prev:hover {
transform: translateX(-3px);
}
.wi-post__nav-link--next:hover {
transform: translateX(3px);
}
.wi-post__nav-label {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-size: var(--text-xs);
color: var(--ink-3);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
} }
.wi-post__nav-link--next { .wi-post__nav-link--next {
@@ -1326,15 +1351,32 @@ import LightGallery from "../components/LightGallery.astro";
margin-left: auto; margin-left: auto;
} }
.wi-post__nav-link--next .wi-post__nav-label {
justify-content: flex-end;
}
.wi-post__nav-link--empty { .wi-post__nav-link--empty {
visibility: hidden; visibility: hidden;
} }
.wi-post__nav-label { .wi-post__nav-link--prev .wi-post__nav-label::before {
font-size: var(--text-xs); content: "←";
color: var(--ink-3); font-size: 0.9em;
letter-spacing: var(--tracking-wider); transition: transform 0.2s var(--ease-out-quart);
text-transform: uppercase; }
.wi-post__nav-link--next .wi-post__nav-label::after {
content: "→";
font-size: 0.9em;
transition: transform 0.2s var(--ease-out-quart);
}
.wi-post__nav-link--prev:hover .wi-post__nav-label::before {
transform: translateX(-2px);
}
.wi-post__nav-link--next:hover .wi-post__nav-label::after {
transform: translateX(2px);
} }
.wi-post__nav-title { .wi-post__nav-title {
@@ -1468,6 +1510,7 @@ import LightGallery from "../components/LightGallery.astro";
.wi-post__nav-link { .wi-post__nav-link {
max-width: 48%; max-width: 48%;
min-width: 0; min-width: 0;
padding: 0.6rem 0.75rem;
} }
.wi-post__nav-title { .wi-post__nav-title {
+1 -1
View File
@@ -28,7 +28,7 @@ spec:
description: 微信读书书架(依赖微信读书插件) description: 微信读书书架(依赖微信读书插件)
screenshot: screenshot:
file: weread.html file: weread.html
version: "1.1.5" version: "1.1.6"
requires: ">=2.24.0" requires: ">=2.24.0"
license: license:
- name: "GPL-3.0" - name: "GPL-3.0"