Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ab408ca4f | ||
|
|
5c7c6325b1 | ||
|
|
c92429d139 | ||
|
|
dda86294b0 |
No files matched your search
@@ -2,114 +2,22 @@
|
|||||||
|
|
||||||
暖如灯火,静如留白。暖屿以柔和的色调和舒缓的节奏回归内容本身,为记录与阅读留出一方安静从容的角落。
|
暖如灯火,静如留白。暖屿以柔和的色调和舒缓的节奏回归内容本身,为记录与阅读留出一方安静从容的角落。
|
||||||
|
|
||||||
> 本项目由 AI 开发完成,人工仅进行部分功能测试及问题找寻
|

|
||||||
|
|
||||||
|
> 预览图随安装包分发(包根目录 `WarmIsland Preview.webp`)。
|
||||||
|
|
||||||
## 环境要求
|
## 环境要求
|
||||||
|
|
||||||
- Halo **2.27.0 及以上**(页面布局契约、微信读书页面均依赖 2.27 的新能力)
|
- Halo **2.27.0 及以上**(页面布局契约与微信读书页面依赖 2.27 的新能力)
|
||||||
- v1.2.0 起内置主题配色系统(纯 CSS 变量实现);更低版本的 Halo 请使用主题 v1.1.x
|
- 更低版本的 Halo 请使用主题 v1.1.x;v1.2.0 起内置纯 CSS 变量实现的主题配色系统
|
||||||
|
|
||||||
## 特点
|
|
||||||
|
|
||||||
1. **响应式设计** — 适配桌面端与移动端,支持暗色模式,移动端使用动态视口单位适配地址栏
|
|
||||||
2. **插件兼容性** — 适配 11 款常用 Halo 插件,安装即用;提供页面布局契约,插件前台页面自动复用主题外观
|
|
||||||
3. **内置灯箱** — 主题内置 LightGallery 灯箱,支持 EXIF 信息展示;安装 lightgallery.js 插件后自动禁用
|
|
||||||
4. **可配置性** — 17 个设置分组、122 个配置项,几乎所有模块可独立开关
|
|
||||||
5. **性能优化** — 服务端字数统计(API 扩展包)、图片懒加载与响应式缩略图、首屏整屏吸附(仅首屏边界接管,内容区保留原生滚动)、Hero 离屏停绘、CSS/HTML 压缩;详见[开发指南 - 性能与滚动顺滑](docs/customize/development.md)
|
|
||||||
6. **页面布局契约** — 提供 `templates/layout.html`,插件页面不再回退到 Halo 内置布局
|
|
||||||
|
|
||||||
## 预览
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 已适配插件
|
|
||||||
|
|
||||||
| 插件名称 | 插件标识 | 适配页面路由 | 说明 |
|
|
||||||
|----------|---------|-------------|------|
|
|
||||||
| [瞬间](https://halo.run/store/apps/app-SnwWD) | `PluginMoments` | `/moments` | 瞬间动态页面,支持点赞、评论、标签筛选、顶部封面 |
|
|
||||||
| [图库](https://halo.run/store/apps/app-BmQJW) | `PluginPhotos` | `/photos` | 图片展示页面,支持分组、灯箱预览 |
|
|
||||||
| [友情链接](https://halo.run/store/apps/app-hfbQg) | `PluginLinks` | `/links` | 友链展示页面,支持随机排列、随机传送门、评论区开关 |
|
|
||||||
| [朋友圈](https://github.com/halo-sigs/plugin-friends) | `plugin-friends` | `/friends` | RSS 订阅朋友圈页面 |
|
|
||||||
| [装备管理](https://github.com/chengzhongxue/plugin-equipment) | `equipment` | `/equipments` | 装备展示页面,按分组展示,支持封面图 |
|
|
||||||
| [豆瓣](https://github.com/chengzhongxue/plugin-douban) | `plugin-douban` | `/douban` | 豆瓣记录页面,支持类型/状态/标签筛选,默认音乐;需插件 **v1.2.6+**(SQLite 数据结构) |
|
|
||||||
| [Steam 信息展示](https://github.com/Tim0x0/halo-plugin-steam) | `PluginSteam` | `/steam` | Steam 游戏库页面,含用户资料、统计数据、最近游玩、游戏库分页 |
|
|
||||||
| [微信读书](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` | 全站 | 服务端字数统计、中英文混排排版、可选服务端代码高亮 |
|
|
||||||
|
|
||||||
> **注意**:插件需要单独安装,安装后主题会自动适配对应页面。若未安装某插件,对应页面或功能自动隐藏/降级,不会报错。
|
|
||||||
|
|
||||||
## 自定义页面模板
|
|
||||||
|
|
||||||
在 Halo 后台「页面 → 新建 → 自定义模板」中选择使用:
|
|
||||||
|
|
||||||
| 模板名 | 模板文件 | 说明 |
|
|
||||||
|--------|---------|------|
|
|
||||||
| 留言板 | `page_messageboard.html` | 留言墙页面,标题与副标题可配置 |
|
|
||||||
| 订阅 | `page_subscriptions.html` | 博友文章订阅动态,依赖链接管理插件的 RSS 订阅,支持分页/下滑加载与列表/网格布局 |
|
|
||||||
| 微信读书 | `weread.html` | 微信读书书架展示,依赖微信读书插件,访客免登录查看 |
|
|
||||||
|
|
||||||
## 主要功能
|
|
||||||
|
|
||||||
### 首页
|
|
||||||
|
|
||||||
- Hero 首屏:明暗双背景图、大标题/副标题独立配色、一言(支持自动刷新与淡入淡出过渡)、下滑箭头平滑滚动
|
|
||||||
- 欢迎弹窗:基于访客 IP 地理定位的欢迎提示,支持 5 种位置、自定义 API 与会话去重开关
|
|
||||||
- 滚动吸附:首屏与内容区之间滚轮/触摸自动吸附,进入内容区后恢复原生自由滚动
|
|
||||||
- 文章卡片:可配置显示字数(依赖 API 扩展包)、阅读量、分类、标签、摘要行数
|
|
||||||
|
|
||||||
### 文章
|
|
||||||
|
|
||||||
- 目录(TOC):桌面端侧栏 + 移动端抽屉,滚动高亮、展开模式可选
|
|
||||||
- 上下篇导航:悬停反馈与方向箭头动画
|
|
||||||
- 相关推荐、分享按钮、赞赏、版权声明可独立开关
|
|
||||||
- 字数与阅读时间:优先使用 API 扩展包的服务端统计,未安装时前端计算兜底
|
|
||||||
|
|
||||||
### 全局
|
|
||||||
|
|
||||||
- 点击特效:核心价值观 / 爱心 / 烟花 / 粒子等 9 种可选
|
|
||||||
- 页脚全站统计:文章、分类、评论、访问、点赞、总字数,支持开关与统计项选择(默认开启)
|
|
||||||
- 动效:呼吸动画、滚动渐入,可独立关闭
|
|
||||||
- 备案信息:ICP 与公网安备号,公网安备自动带徽标
|
|
||||||
- 鼠标光感跟随已移除(性能考量)
|
|
||||||
|
|
||||||
## 灯箱集成
|
|
||||||
|
|
||||||
主题内置 LightGallery 灯箱,为图片提供放大预览功能,并支持 EXIF 信息展示。
|
|
||||||
|
|
||||||
### lightgallery.js 插件兼容
|
|
||||||
|
|
||||||
如安装 [lightgallery.js](https://www.lightgalleryjs.com/) 灯箱插件,主题内置灯箱将自动禁用,改由插件接管灯箱功能。以下为 lightgallery.js 插件的路径匹配规则和 DOM 节点选择器:
|
|
||||||
|
|
||||||
| 页面 | 路径匹配规则 | 匹配区域 DOM 节点 |
|
|
||||||
|------|-------------|-------------------|
|
|
||||||
| 文章详情页 | `/archives/*` | `.wi-post__body` |
|
|
||||||
| 瞬间页 | `/moments` | `.wi-moments-page__content` |
|
|
||||||
| 图库页 | `/photos` | `.wi-photos-page__grid` |
|
|
||||||
| 自定义页面 | 用户自定义 | `.wi-page__body` |
|
|
||||||
|
|
||||||
> **注意**:自定义页面的路径匹配由用户自行设定,此处仅提供 DOM 节点选择器 `.wi-page__body`。
|
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
1. 下载主题最新 Release 的 ZIP 包
|
1. 下载最新 Release 的 ZIP 安装包
|
||||||
2. 进入 Halo 后台 → 主题管理 → 安装主题 → 上传 ZIP 包
|
2. Halo 后台 → 主题管理 → 安装主题 → 上传 ZIP
|
||||||
3. 安装完成后点击「启用」
|
3. 点击「启用」
|
||||||
|
|
||||||
> **升级后首页 404「资源不存在 / Template index was not found」**:说明上传的 ZIP 里没有 `templates/`
|
从源码构建:
|
||||||
> 模板文件(从源码仓库打包时漏了构建步骤)。请换用完整安装包重新上传,详见 [FAQ 安装相关](docs/faq.md)。
|
|
||||||
>
|
|
||||||
> **升级后如果整站没了样式**:Halo 页面缓存或前置反向代理里可能还留着上一版本的 HTML,
|
|
||||||
> 它引用的 CSS 文件名带内容哈希,升级后已被删除,请求会直接 500。
|
|
||||||
> 先在 Halo 后台执行一次「清理缓存」(并刷新 CDN / Nginx 缓存)即可。
|
|
||||||
> 主题从 v1.2.1 起会把历史版本的哈希资源一并发进安装包,旧 HTML 也不会请求失败。
|
|
||||||
>
|
|
||||||
> **升级主题或修改配色后,部分页面不生效**:如果安装了「页面静态缓存」插件,
|
|
||||||
> 它只随文章变更自动刷新,主题层的变更需要手动点一次「刷新页面缓存」,
|
|
||||||
> 详见 [FAQ:升级主题或修改配色后部分页面不生效](docs/faq.md#升级主题或修改配色后部分页面的新样式配色不生效)。
|
|
||||||
|
|
||||||
或从源码构建:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.nxxy335.top/nxxy335/halo-theme-WarmIsland.git
|
git clone https://git.nxxy335.top/nxxy335/halo-theme-WarmIsland.git
|
||||||
@@ -118,34 +26,148 @@ pnpm install
|
|||||||
pnpm release # build → snapshot → package
|
pnpm release # build → snapshot → package
|
||||||
```
|
```
|
||||||
|
|
||||||
`pnpm build` 只生成 `templates/`(模板 + 静态资源),不会产出 ZIP;
|
`pnpm build` 只生成 `templates/`,ZIP 由 `pnpm package` 输出到 `dist/warm-island-<version>.zip`;安装包同时含 `docs/*.md` 与预览图,便于离线查阅。
|
||||||
安装包由 `pnpm package` 生成到 `dist/warm-island-<version>.zip`。
|
|
||||||
|
> **升级后首页 404「Template index was not found」**:ZIP 中缺少 `templates/`(源码打包时漏了构建步骤),请换用完整安装包重新上传,详见 [FAQ 安装相关](https://nxxy335.top/WarmIsland/faq.html)。
|
||||||
|
>
|
||||||
|
> **升级后整站没了样式**:Halo 页面缓存或反向代理仍留着上一版 HTML,其引用的哈希 CSS 已被删除。在后台执行一次「清理缓存」并刷新 CDN / Nginx 缓存即可;主题自 v1.2.1 起会把历史哈希资源一并打进安装包。
|
||||||
|
>
|
||||||
|
> **改了主题或配色后部分页面不生效**:若安装了「页面静态缓存」插件,需手动点一次「刷新页面缓存」,详见 [FAQ](https://nxxy335.top/WarmIsland/faq.html)。
|
||||||
|
|
||||||
|
## 特点
|
||||||
|
|
||||||
|
1. **响应式设计** — 桌面 / 移动端适配、暗色模式,移动端用动态视口单位适配地址栏
|
||||||
|
2. **插件兼容** — 适配 10 款常用 Halo 插件,提供页面布局契约,插件前台页面自动复用主题外观
|
||||||
|
3. **内置灯箱** — 随包自托管的 LightGallery(GPLv3),支持 EXIF 展示;安装 lightgallery.js 插件后自动让位
|
||||||
|
4. **可配置性** — 17 个设置分组、126 个配置项,几乎所有模块可独立开关
|
||||||
|
5. **性能优化** — 服务端字数统计(API 扩展包)、图片懒加载与响应式缩略图、首屏整屏吸附(仅边界接管,内容区保留原生滚动)、Hero 离屏停绘、CSS/HTML 压缩;详见[开发指南 - 性能与滚动顺滑](https://nxxy335.top/WarmIsland/customize/development.html)
|
||||||
|
6. **页面布局契约** — 提供 `templates/layout.html`,插件页面不再回退到 Halo 内置布局
|
||||||
|
|
||||||
|
## 主要功能
|
||||||
|
|
||||||
|
### 首页
|
||||||
|
|
||||||
|
- Hero 首屏:明暗双背景图、大标题 / 副标题独立配色、描述文案(默认「无」;API 字段默认预填 `https://v1.hitokoto.cn/`,仅「一言」模式生效,清空则使用内置本地句子库;可选自定义文案与自动刷新)、下滑箭头平滑滚动
|
||||||
|
- 欢迎弹窗:默认显示纯本地静态欢迎语,支持 5 种位置、会话去重开关;仅在显式配置自定义 IP 定位 API 时才显示访客所在地(见[外部资源与隐私](#外部资源与隐私))
|
||||||
|
- 滚动吸附:首屏与内容区之间滚轮 / 触摸自动吸附,进入内容区后恢复原生自由滚动
|
||||||
|
- 文章卡片:可配置显示字数(依赖 API 扩展包)、阅读量、分类、标签、摘要行数
|
||||||
|
|
||||||
|
### 文章
|
||||||
|
|
||||||
|
- 目录(TOC):桌面端侧栏 + 移动端抽屉,滚动高亮、展开模式可选
|
||||||
|
- 代码高亮:由代码高亮插件 / API 扩展包输出,主题提供 Warm / Cold 两套本地配色
|
||||||
|
- 上下篇导航、相关推荐、分享按钮、赞赏、版权声明可独立开关;微信分享改为复制文章链接
|
||||||
|
- 字数与阅读时间:优先使用 API 扩展包的服务端统计,未安装时前端计算兜底
|
||||||
|
|
||||||
|
### 全局
|
||||||
|
|
||||||
|
- 点击特效:核心价值观 / 爱心 / 烟花 / 粒子等 8 种效果可选,也可关闭
|
||||||
|
- 页脚全站统计:文章、分类、评论、访问、点赞、总字数,支持开关与统计项选择(默认开启)
|
||||||
|
- 动效:呼吸动画、滚动渐入,可独立关闭
|
||||||
|
- 备案信息:ICP 与公网安备号,公网安备自动带徽标
|
||||||
|
|
||||||
|
## 已适配插件
|
||||||
|
|
||||||
|
| 插件名称 | 插件标识 | 适配页面路由 | 说明 |
|
||||||
|
|----------|---------|-------------|------|
|
||||||
|
| [瞬间](https://halo.run/store/apps/app-SnwWD) | `PluginMoments` | `/moments` | 瞬间动态页面,支持点赞、评论、标签筛选、顶部封面 |
|
||||||
|
| [图库](https://halo.run/store/apps/app-BmQJW) | `PluginPhotos` | `/photos` | 图片展示页面,支持分组、灯箱预览 |
|
||||||
|
| [友情链接(链接管理)](https://halo.run/store/apps/app-hfbQg) | `PluginLinks` | `/links` | 友链展示页面,支持随机排列、随机传送门、评论区开关;同一插件的 RSS 订阅驱动「订阅」页面 |
|
||||||
|
| [装备管理](https://github.com/chengzhongxue/plugin-equipment) | `equipment` | `/equipments` | 装备展示页面,按分组展示,支持封面图 |
|
||||||
|
| [豆瓣](https://github.com/chengzhongxue/plugin-douban) | `plugin-douban` | `/douban` | 豆瓣记录页面,支持类型 / 状态 / 标签筛选,默认音乐;需插件 **v1.2.6+**(SQLite 数据结构) |
|
||||||
|
| [Steam 信息展示](https://github.com/Tim0x0/halo-plugin-steam) | `PluginSteam` | `/steam` | Steam 游戏库页面,含用户资料、统计数据、最近游玩、游戏库分页 |
|
||||||
|
| [微信读书](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` | 全站 | 服务端字数统计、中英文混排排版、可选服务端代码高亮 |
|
||||||
|
|
||||||
|
> 插件需单独安装;未安装时对应页面或功能自动隐藏 / 降级,不会报错。
|
||||||
|
>
|
||||||
|
> **「朋友圈」(`plugin-friends`)插件已不再提供**:原先的 feed / RSS 聚合能力由官方「链接管理」(PluginLinks)插件的订阅取代,对应下方的「订阅」自定义页面模板。主题保留 `friends.html` 与「朋友圈」设置分组仅用于兼容旧站点,新站点请勿再依赖;插件不可用时会显示空状态。
|
||||||
|
|
||||||
|
## 自定义页面模板
|
||||||
|
|
||||||
|
在 Halo 后台「页面 → 新建 → 自定义模板」中选择使用(页面地址由 slug 决定,无固定路由):
|
||||||
|
|
||||||
|
| 模板名 | 模板文件 | 说明 |
|
||||||
|
|--------|---------|------|
|
||||||
|
| 留言板 | `page_messageboard.html` | 留言墙页面,标题与副标题可配置 |
|
||||||
|
| 订阅 | `page_subscriptions.html` | 博友文章订阅动态,由官方链接管理插件的 RSS 订阅提供,支持分页 / 下滑加载与列表 / 网格布局 |
|
||||||
|
| 微信读书 | `weread.html` | 微信读书书架展示,依赖微信读书插件,访客免登录查看 |
|
||||||
|
|
||||||
|
## 灯箱集成
|
||||||
|
|
||||||
|
主题内置 LightGallery 2.9.0(GPLv3,与主题的 GPL-3.0 一致),为图片提供放大预览与 EXIF 展示。如安装 [lightgallery.js](https://www.lightgalleryjs.com/) 灯箱插件,内置灯箱自动禁用,改由插件接管。其路径匹配规则与 DOM 节点选择器:
|
||||||
|
|
||||||
|
| 页面 | 路径匹配规则 | 匹配区域 DOM 节点 |
|
||||||
|
|------|-------------|-------------------|
|
||||||
|
| 文章详情页 | `/archives/*` | `.wi-post__body` |
|
||||||
|
| 瞬间页 | `/moments` | `.wi-moments-page__content` |
|
||||||
|
| 图库页 | `/photos` | `.wi-photos-page__grid` |
|
||||||
|
| 自定义页面 | 用户自定义 | `.wi-page__body` |
|
||||||
|
|
||||||
|
> 自定义页面的路径匹配由用户自行设定,此处仅提供 DOM 节点选择器 `.wi-page__body`。
|
||||||
|
|
||||||
|
## 外部资源与隐私
|
||||||
|
|
||||||
|
默认配置下,主题不会自动向任何第三方服务发起请求:CSS、JS、字体(LXGW WenKai 本地分片)、图标、Logo 与一言句子库均来自站点自身的 `/themes/warm-island/assets/`,不经过 CDN,也不使用 `preconnect` / `dns-prefetch`。
|
||||||
|
|
||||||
|
### 逐项说明
|
||||||
|
|
||||||
|
| 服务 / 资源 | 用途 | 涉及的数据类型 | 默认状态 | 关闭 / 替换方法 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 主题自身静态资源 `/themes/warm-island/assets/*` | 样式、脚本、字体、图标、Logo、内置句子库 | 无(同域请求,不含用户数据) | 始终启用,渲染必需 | 不可关闭;均为随包自托管文件 |
|
||||||
|
| 自定义一言 API `hero_hitokoto_api` | Hero 首屏随机句子 | 无请求体;发起 GET 时第三方可见访客 IP / User-Agent | **默认不启用**:「描述文案来源」默认「无」,一言元素不渲染,零请求;该字段默认预填 `https://v1.hitokoto.cn/`,仅在「一言」模式生效 | 主题设置 → Hero 首屏 → 描述来源改为「无」或「自定义」,或清空该字段回退到 `assets/data/hitokoto.json` 内置本地句子库 |
|
||||||
|
| 自定义 IP 定位主 / 备选 API `welcome_popup_api` / `welcome_popup_fallback_api` | 欢迎弹窗显示「欢迎来自 XX 的朋友」 | **访客 IP** 与第三方返回的粗略地理位置 | **关闭**:两项均留空,弹窗只显示本地欢迎语;且欢迎弹窗开关本身默认关闭。可选接口见下方清单 | 主题设置 → 欢迎弹窗 → 关闭开关,或清空两个 API 字段 |
|
||||||
|
| 分享按钮:QQ / 微博 / X / Facebook / Telegram | 分享文章到对应平台 | 文章 URL 与标题(写在目标站 URL 参数中) | 默认开启 `qq,wechat,weibo`;**仅在访客点击后**跳转,页面加载时不请求 | 主题设置 → 文章 → 关闭「显示分享按钮」,或在「分享渠道」取消勾选 |
|
||||||
|
| 微信分享弹层 | 生成并复制文章链接 | 文章 URL(浏览器本地字符串,不发送) | 与分享按钮一致 | 同上;已移除二维码远程库,改为「复制链接」 |
|
||||||
|
| 页脚备案 / Halo / 应用市场外链 | 备案信息、致谢、插件安装提示 | 无(访客主动点击才跳转) | 由站点配置决定,默认仅显示 Halo 致谢 | 主题设置 → 页脚 → 关闭对应显示项 |
|
||||||
|
| 插件前台页面(豆瓣、Steam、微信读书、订阅动态等) | 展示插件提供的数据 | 由对应插件决定,主题不发起请求 | 安装并启用对应插件后才出现 | 卸载 / 停用对应插件;主题自动降级隐藏 |
|
||||||
|
| 内置 LightGallery 灯箱 | 图片放大预览、EXIF 展示 | 无(本地打包,不联网) | 默认启用 | 安装 lightgallery.js 插件后主题自动让位 |
|
||||||
|
|
||||||
|
#### 可选第三方接口地址(均为候选值,主题不预置请求)
|
||||||
|
|
||||||
|
| 能力 | 设置项 | 可选地址 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Hero 一言 | `hero_hitokoto_api` | `https://v1.hitokoto.cn/`(默认预填,仅「一言」模式生效;清空则使用内置本地句子库) |
|
||||||
|
| IP 定位主接口 | `welcome_popup_api` | `https://myip.ipip.net/json` 或 `https://r.inews.qq.com/api/ip2city?otype=json` |
|
||||||
|
| IP 定位备选接口 | `welcome_popup_fallback_api` | 同上两个地址任选其一,主接口失败时兜底 |
|
||||||
|
|
||||||
|
**描述文案来源默认为「无」**,此时不渲染一言元素、不发起任何请求。API 字段默认预填 `https://v1.hitokoto.cn/`(仅「一言」模式生效);选择「一言」或「自定义」后若留空,则回退到主题内置的本地句子库(零外部请求)。只有填写后才会向该地址发起请求,第三方可见访客 IP / User-Agent,需自行在站点隐私说明中披露。
|
||||||
|
|
||||||
|
两个 IP 定位接口同样默认留空;填写后会把**访客 IP**发送给该第三方服务换取粗略地理位置,需自行在站点隐私说明中披露。
|
||||||
|
|
||||||
|
### 隐私说明
|
||||||
|
|
||||||
|
| 项目 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| 收集的数据类型 | 默认配置下**无**。主题不读取、不上传用户信息、站点内容、访问日志或订单数据 |
|
||||||
|
| 数据用途 | 仅用于页面渲染;自定义 API 场景下由管理员自行决定 |
|
||||||
|
| 保存位置与期限 | 主题不写入数据库、不上传数据、不生成服务端日志。前端仅在访客本机使用 `sessionStorage`(欢迎弹窗「同会话仅显示一次」)与 `localStorage`(点赞状态),可随时清除浏览器站点数据 |
|
||||||
|
| 是否传输到第三方 | 默认**否**(描述文案来源为「无」、欢迎弹窗关闭、两个 API 字段留空)。仅当管理员显式填写「自定义一言 API」或「自定义 IP 定位 API」时才会向该服务发起请求,其中 IP 定位会暴露访客 IP |
|
||||||
|
| 关闭 / 撤销方式 | 见上表;清空相应设置项或关闭对应开关即可立即停止,无需重装主题 |
|
||||||
|
| 遥测 / 统计 / 崩溃上报 / 广告 / AI 分析 / 跨站追踪 | **均不存在**。主题不含任何埋点、指纹或第三方统计脚本 |
|
||||||
|
|
||||||
|
### 自托管与许可
|
||||||
|
|
||||||
|
- **代码高亮**:主题不自带 Prism.js 运行时。语法高亮请安装 Halo 应用市场的代码高亮插件,或启用「API 扩展包」的服务端代码高亮;主题只提供两套自托管的 `.token` 配色(`assets/vendor/prism-warm.css` / `prism-cold.css`),由「文章 → 代码高亮主题」切换。
|
||||||
|
- **字体**:[霞鹜文楷 LXGW WenKai](https://github.com/lxgw/LxgwWenKai) 以 [SIL OFL 1.1](https://openfontlicense.org/) 发布,由 `cn-font-split` 切分为本地 woff2 分片随主题分发;字体来源与版权声明保留在 `public/assets/fonts/LXGWWenKai/result.css`,上游完整的版权声明、保留字体名条款与 **OFL 1.1 许可证全文**随包存放于 `public/assets/fonts/LXGWWenKai/OFL.txt`(构建后为 `templates/assets/fonts/LXGWWenKai/OFL.txt`)。上游附加许可允许把字体切分 / 转换为 WOFF2 用于网页字体交付。
|
||||||
|
- **灯箱**:LightGallery 2.9.0 以 GPLv3 发布,本主题同样采用 GPL-3.0,按开源许可条款使用(非商业授权)。
|
||||||
|
- **图标**:来源与协议见[图标与字体来源](#图标与字体来源)。
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
```bash
|
| 命令 | 作用 |
|
||||||
# 安装依赖
|
| --- | --- |
|
||||||
pnpm install
|
| `pnpm dev` | 监听文件变化并构建到 `templates/` |
|
||||||
|
| `pnpm build` | 构建模板与静态资源,自动合并历史哈希资源 |
|
||||||
|
| `pnpm snapshot` | 把本次构建的哈希资源快照进 `compat/assets`(发版前执行) |
|
||||||
|
| `pnpm audit:external` | 外部资源、隐私与 README 链接审计(打包时自动执行) |
|
||||||
|
| `pnpm smoke` | 点击特效脚本与 JSON-LD 冒烟测试 |
|
||||||
|
| `pnpm package` | 打包 `dist/warm-island-<version>.zip` |
|
||||||
|
| `pnpm release` | 等价于 `build` → `snapshot` → `package` |
|
||||||
|
|
||||||
# 开发模式(监听文件变化,自动构建到 templates/)
|
发版执行 `pnpm release`,并把 `compat/assets` 的新增文件一起提交。`pnpm package` 会校验模板、资源目录与版本号一致性并执行外部资源审计,任一不通过即报错。
|
||||||
pnpm dev
|
|
||||||
|
|
||||||
# 构建(输出到 templates/,并自动合并历史版本的静态资源)
|
|
||||||
pnpm build
|
|
||||||
|
|
||||||
# 把本次构建的哈希资源快照进 compat/assets(发版前必做)
|
|
||||||
pnpm snapshot
|
|
||||||
|
|
||||||
# 打包 dist/warm-island-<version>.zip
|
|
||||||
pnpm package
|
|
||||||
|
|
||||||
# 点击特效脚本冒烟测试
|
|
||||||
pnpm smoke
|
|
||||||
```
|
|
||||||
|
|
||||||
发版流程:`pnpm release`(等价于 `pnpm build` → `pnpm snapshot` → `pnpm package`),
|
|
||||||
并把 `compat/assets` 的新增文件一起提交。`pnpm package` 会校验模板、资源目录与版本号一致性,
|
|
||||||
产物缺失时直接报错,避免打出不含 `templates/` 的空壳安装包。
|
|
||||||
|
|
||||||
### 目录结构
|
### 目录结构
|
||||||
|
|
||||||
@@ -155,9 +177,13 @@ pnpm smoke
|
|||||||
│ ├── layouts/ # Layout.astro(含页面布局契约渲染模式)
|
│ ├── layouts/ # Layout.astro(含页面布局契约渲染模式)
|
||||||
│ ├── pages/ # 页面模板(构建后输出到 templates/)
|
│ ├── pages/ # 页面模板(构建后输出到 templates/)
|
||||||
│ └── styles/ # SCSS 样式(正文样式在 _content.scss,全局生效)
|
│ └── styles/ # SCSS 样式(正文样式在 _content.scss,全局生效)
|
||||||
├── compat/
|
├── public/
|
||||||
│ └── assets/ # 历史版本哈希静态资源,构建时合并进 templates/assets
|
│ ├── assets/data/ # 内置本地一言句子库(静态 JSON)
|
||||||
├── scripts/ # 构建后置、资源快照、打包、冒烟测试
|
│ ├── assets/fonts/ # LXGW WenKai 本地字体分片 + OFL.txt 许可证全文
|
||||||
|
│ ├── assets/vendor/ # 自托管的代码高亮配色 CSS
|
||||||
|
│ └── assets/logo.png # 主题图标(唯一一份,256×256 压缩版)
|
||||||
|
├── compat/assets/ # 历史版本哈希静态资源,构建时合并进 templates/assets
|
||||||
|
├── scripts/ # 构建后置、资源快照、打包、外部资源与合规审计、冒烟测试
|
||||||
├── templates/ # 构建产物,Halo 实际加载的模板
|
├── templates/ # 构建产物,Halo 实际加载的模板
|
||||||
├── settings.yaml # 后台设置项定义
|
├── settings.yaml # 后台设置项定义
|
||||||
└── theme.yaml # 主题清单与自定义页面模板注册
|
└── theme.yaml # 主题清单与自定义页面模板注册
|
||||||
@@ -165,11 +191,16 @@ pnpm smoke
|
|||||||
|
|
||||||
### 主题开发注意事项
|
### 主题开发注意事项
|
||||||
|
|
||||||
- **静态资源文件名带内容哈希**:升级后旧文件被删除,被缓存的 HTML 仍会请求旧文件名并触发 500,
|
- **静态资源文件名带内容哈希**:升级后旧文件被删除,被缓存的 HTML 仍会请求旧文件名并触发 500,所以每个版本的哈希文件都要留一份(见 `compat/assets` 与 `pnpm snapshot`)
|
||||||
所以每个版本的哈希文件都要留一份(见 `compat/assets` 与 `pnpm snapshot`)
|
- **第三方资源一律自托管并标注许可**,不得引入远程脚本 / 样式 / 字体 / 预连接,`pnpm package` 会拦截回归
|
||||||
- **Thymeleaf 字符串字面量必须加单引号**:`th:text="'文本'"`,裸中文遇到逗号/空格/emoji 会导致页面 500
|
- **常规变量统一用 `th:text` / `th:attr` 转义输出**,`th:utext` 仅用于 Halo 已处理的 HTML 与管理员自定义内容;`target="_blank"` 一律带 `rel="noopener noreferrer"`
|
||||||
|
- **不得把站点内容用 `th:utext` 拼进 `<script>`**:`<script>` 是原始文本元素,HTML 转义不生效且脚本闭合标签可提前结束元素;动态数据走「转义输出到 `<meta>` → `JSON.stringify` → `textContent`」
|
||||||
|
- **后台自定义字段按设计意图原样输出**:`style.custom_css` 与 `footer.footer_custom_html` 的写入权限由 Halo 后台角色控制,开放多人编辑时请自行评估信任边界
|
||||||
|
- **第三方字体必须随包携带许可证全文**:修改 / 切分字体时,`public/assets/fonts/LXGWWenKai/OFL.txt` 与 `result.css` 中的版权声明都要保留,`pnpm package` 会校验
|
||||||
|
- **Thymeleaf 字符串字面量必须加单引号**:`th:text="'文本'"`,裸中文遇到逗号 / 空格 / emoji 会导致页面 500
|
||||||
- **`th:attr` 只用简单表达式**(取属性、Elvis 默认值、三元、拼接、等值比较),表达式对象方法调用会被受限求值器拒绝
|
- **`th:attr` 只用简单表达式**(取属性、Elvis 默认值、三元、拼接、等值比较),表达式对象方法调用会被受限求值器拒绝
|
||||||
- **凡是与后台开关绑定的模板表达式都要给默认值**:`?: '默认值'`,配置未落库时属性会被整体移除,功能会静默失效
|
- **凡是与后台开关绑定的模板表达式都要给默认值**:`?: '默认值'`,配置未落库时属性会被整体移除,功能会静默失效
|
||||||
|
- **截取首字符前先判空**:`#strings.substring('', 0, 1)` 会抛字符串越界异常,统一写成 `!#strings.isEmpty(x) ? #strings.substring(x, 0, 1) : '·'`
|
||||||
- **正文样式必须写在全局样式表**(`src/styles/_content.scss`):Astro scoped 样式对 Thymeleaf 运行时注入的内容无效
|
- **正文样式必须写在全局样式表**(`src/styles/_content.scss`):Astro scoped 样式对 Thymeleaf 运行时注入的内容无效
|
||||||
- **内联脚本统一加 `th:inline="none"`**:避免 Thymeleaf 解析脚本中的 `[[` / `[(` / `${}`
|
- **内联脚本统一加 `th:inline="none"`**:避免 Thymeleaf 解析脚本中的 `[[` / `[(` / `${}`
|
||||||
- **新插入的动画元素要强制回流**(读一次 `offsetWidth`):否则初态与终态在同一帧被合并,transition 不触发
|
- **新插入的动画元素要强制回流**(读一次 `offsetWidth`):否则初态与终态在同一帧被合并,transition 不触发
|
||||||
@@ -182,17 +213,25 @@ pnpm smoke
|
|||||||
- [Thymeleaf](https://www.thymeleaf.org) 模板引擎
|
- [Thymeleaf](https://www.thymeleaf.org) 模板引擎
|
||||||
- [Sass](https://sass-lang.com/) 样式
|
- [Sass](https://sass-lang.com/) 样式
|
||||||
|
|
||||||
|
## 原创声明
|
||||||
|
|
||||||
|
WarmIsland 暖屿为**完全原创**主题:视觉设计、页面结构、Astro 构建链路、Thymeleaf 模板与样式代码均由开发者独立编写,**未基于任何已有 Halo 主题或插件进行 Fork、复制代码或二次开发**,不存在上游应用、上游仓库、派生版本或接续维护关系。因此审核指南 **4.2.1(Fork 与派生应用)与 4.2.2(停止维护应用的接续维护)不适用于本主题**,无需提供上游贡献记录、上游差异说明或停止维护证明。
|
||||||
|
|
||||||
|
主题使用 AI 工具辅助编码与文档编写。第三方开源组件(字体、图标、灯箱)均自托管并保留许可证与署名,来源见[图标与字体来源](#图标与字体来源)与[自托管与许可](#自托管与许可);主题本体以 GPL-3.0 发布,源码仓库为 https://git.nxxy335.top/nxxy335/halo-theme-WarmIsland 。
|
||||||
|
|
||||||
## 许可
|
## 许可
|
||||||
|
|
||||||
[GPL-3.0](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
[GPL-3.0](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
|
||||||
## 图标来源
|
## 图标与字体来源
|
||||||
|
|
||||||
本主题使用的图标来自以下开源项目:
|
本主题使用的图标与字体来自以下开源项目,均随主题自托管分发:
|
||||||
|
|
||||||
| 来源 | 协议 | 用途 |
|
| 来源 | 协议 | 用途 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| [霞鹜文楷 LXGW WenKai](https://github.com/lxgw/LxgwWenKai) | [SIL OFL 1.1](https://openfontlicense.org/) | 全站中文字体(本地 woff2 分片,含保留字体名与版权声明) |
|
||||||
| [Font Awesome](https://fontawesome.com/) Free v7.2.0 | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) / [SIL OFL 1.1](https://scripts.sil.org/OFL) | QQ 分享图标 |
|
| [Font Awesome](https://fontawesome.com/) Free v7.2.0 | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) / [SIL OFL 1.1](https://scripts.sil.org/OFL) | QQ 分享图标 |
|
||||||
| [Simple Icons](https://simpleicons.org/) | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | 微博、X、Facebook、Telegram 分享图标 |
|
| [Simple Icons](https://simpleicons.org/) | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | 微博、X、Facebook、Telegram 分享图标 |
|
||||||
| [Feather Icons](https://feathericons.com/) | [MIT](https://opensource.org/licenses/MIT) | 导航、界面交互图标 |
|
| [Feather Icons](https://feathericons.com/) | [MIT](https://opensource.org/licenses/MIT) | 导航、界面交互图标 |
|
||||||
| [Halo](https://halo.run/) | [GPL-3.0](https://www.gnu.org/licenses/gpl-3.0.en.html) | 主题内置图标 |
|
| [Halo](https://halo.run/) | [GPL-3.0](https://www.gnu.org/licenses/gpl-3.0.en.html) | 主题内置图标 |
|
||||||
|
| [LightGallery](https://www.lightgalleryjs.com/) 2.9.0 | GPLv3 | 图片灯箱(npm 依赖随构建打包) |
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@charset "UTF-8";:root{--bg: #faf7f2;--bg-raised: #f3ede5;--bg-overlay: color-mix(in srgb, var(--bg) 85%, transparent);--ink: #2c2420;--ink-2: #7a6e64;--ink-3: #b5a99e;--rule: #e8e0d6;--accent: #d4764e;--accent-hover: #c4613a;--accent-bg: #fdf0e8;--mist-pink: #f0e4de;--sea-salt: #e8e0d6;--caramel: #8b6f5e;--glass-bg: color-mix(in srgb, var(--bg) 72%, transparent);--glass-border: color-mix(in srgb, var(--rule) 50%, transparent);--shadow-sm: 0 1px 3px color-mix(in srgb, var(--ink) 6%, transparent);--shadow-md: 0 4px 16px color-mix(in srgb, var(--ink) 8%, transparent);--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{--bg: #1e1c1a;--bg-raised: #292623;--bg-overlay: color-mix(in srgb, var(--bg) 85%, transparent);--ink: #e9e4dd;--ink-2: #aaa096;--ink-3: #7c7268;--rule: #3b362f;--accent: #e89a64;--accent-hover: #f2ad79;--accent-bg: #2c231b;--mist-pink: #2e2823;--sea-salt: #302b26;--caramel: #a38b7b;--glass-bg: color-mix(in srgb, var(--bg) 72%, transparent);--glass-border: color-mix(in srgb, var(--rule) 50%, transparent);--shadow-sm: 0 1px 3px rgba(0, 0, 0, .24);--shadow-md: 0 4px 16px rgba(0, 0, 0, .3);--shadow-lg: 0 8px 32px rgba(0, 0, 0, .4);--shadow-glow: 0 0 40px rgba(232, 154, 100, .12)}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;--accentLine truncated
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@charset "UTF-8";:root{--bg: #faf7f2;--bg-raised: #f3ede5;--bg-overlay: color-mix(in srgb, var(--bg) 85%, transparent);--ink: #2c2420;--ink-2: #7a6e64;--ink-3: #b5a99e;--rule: #e8e0d6;--accent: #d4764e;--accent-hover: #c4613a;--accent-bg: #fdf0e8;--mist-pink: #f0e4de;--sea-salt: #e8e0d6;--caramel: #8b6f5e;--glass-bg: color-mix(in srgb, var(--bg) 72%, transparent);--glass-border: color-mix(in srgb, var(--rule) 50%, transparent);--shadow-sm: 0 1px 3px color-mix(in srgb, var(--ink) 6%, transparent);--shadow-md: 0 4px 16px color-mix(in srgb, var(--ink) 8%, transparent);--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{--bg: #1e1c1a;--bg-raised: #292623;--bg-overlay: color-mix(in srgb, var(--bg) 85%, transparent);--ink: #e9e4dd;--ink-2: #aaa096;--ink-3: #7c7268;--rule: #3b362f;--accent: #e89a64;--accent-hover: #f2ad79;--accent-bg: #2c231b;--mist-pink: #2e2823;--sea-salt: #302b26;--caramel: #a38b7b;--glass-bg: color-mix(in srgb, var(--bg) 72%, transparent);--glass-border: color-mix(in srgb, var(--rule) 50%, transparent);--shadow-sm: 0 1px 3px rgba(0, 0, 0, .24);--shadow-md: 0 4px 16px rgba(0, 0, 0, .3);--shadow-lg: 0 8px 32px rgba(0, 0, 0, .4);--shadow-glow: 0 0 40px rgba(232, 154, 100, .12)}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;--accentLine truncated
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
var I=function(){return I=Object.assign||function(t){for(var e,i=1,s=arguments.length;i<s;i++){e=arguments[i];for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])}return t},I.apply(this,arguments)};function N(){for(var o=0,t=0,e=arguments.length;t<e;t++)o+=arguments[t].length;for(var i=Array(o),s=0,t=0;t<e;t++)for(var r=arguments[t],n=0,l=r.length;n<l;n++,s++)i[s]=r[n];return i}var p={afterAppendSlide:"lgAfterAppendSlide",init:"lgInit",hasVideo:"lgHasVideo",containerResize:"lgContainerResize",updateSlides:"lgUpdateSlides",afterAppendSubHtml:"lgAfterAppendSubHtml",beforeOpen:"lgBeforeOpen",afterOpen:"lgAfterOpen",slideItemLoad:"lgSlideItemLoad",beforeSlide:"lgBeforeSlide",afterSlide:"lgAfterSlide",posterClick:"lgPosterClick",dragStart:"lgDragStart",dragMove:"lgDragMove",dragEnd:"lgDragEnd",beforeNextSlide:"lgBeforeNextSlide",beforePrevSlide:"lgBeforePrevSlide",beforeClose:"lgBeforeClose",afterClose:"lgAfterClose"},q={mode:"lg-slide",easing:"ease",speed:400,licenseKey:"0000-0000-000-0000",height:"100%",width:"100%",addClass:"",startClass:"lg-start-zoom",backdropDuration:300,container:"",startAnimationDuration:400,zoomFromOrigin:!0,hideBarsDelay:0,showBarsAfter:1e4,slideDelay:0,supportLegacyBrowser:!0,allowMediaOverlap:!1,videoMaxSize:"1280-720",loadYouTubePoster:!0,defaultCaptionHeight:0,ariaLabelledby:"",ariaDescribedby:"",resetScrollPosition:!0,hideScrollbar:!1,closable:!0,swipeToClose:!0,closeOnTap:!0,showCloseIcon:!0,showMaximizeIcon:!1,loop:!0,escKey:!0,keyPress:!0,trapFocus:!0,controls:!0,slideEndAnimation:!0,hideControlOnEnd:!1,mousewheel:!1,getCaptionFromTitleOrAlt:!0,appendSubHtmlTo:".lg-sub-html",subHtmlSelectorRelative:!1,preload:2,numberOfSlideItemsInDom:10,selector:"",selectWithin:"",nextHtml:"",prevHtml:"",index:0,iframeWidth:"100%",iframeHeight:"100%",iframeMaxWidth:"100%",iframeMaxHeight:"100%",download:!0,counter:!0,appendCounterTo:".lg-toolbar",swipeThreshold:50,enableSwipe:!0,enableDrag:!0,dynamic:!1,dynamicEl:[],extraProps:[],exThumbImage:"",isMobile:void 0,mobileSettings:{controls:!1,showCloseIcon:!1,download:!1},plugins:[],strings:{closeGallery:"Close gallery",toggleMaximize:"Toggle maximize",previousSlide:"Previous slide",nextSlide:"Next slide",download:"Download",playVideo:"Play video",mediaLoadingFailed:"Oops... Failed to load content..."}};function V(){(function(){if(typeof window.CustomEvent=="function")return!1;function o(t,e){e=e||{bubbles:!1,cancelable:!1,detail:null};var i=document.createEvent("CustomEvent");return i.initCustomEvent(t,e.bubbles,e.cancelable,e.detail),i}window.CustomEvent=o})(),(function(){Element.prototype.matches||(Element.prototype.matches=Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector)})()}var D=(function(){function o(t){return this.cssVenderPrefixes=["TransitionDuration","TransitionTimingFunction","Transform","Transition"],this.selector=this._getSelector(t),this.firstElement=this._getFirstEl(),this}return o.generateUUID=function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(t){var e=Math.random()*16|0,i=t=="x"?e:e&3|8;return i.toString(16)})},o.prototype._getSelector=function(t,e){if(e===void 0&&(e=document),typeof t!="string")return t;e=e||document;var i=t.substring(0,1);return i==="#"?e.querySelector(t):e.querySelectorAll(t)},o.prototype._each=function(t){return this.selector?(this.selector.length!==void 0?[].forEach.call(this.selector,t):t(this.selector,0),this):this},o.prototype._setCssVendorPrefix=function(t,e,i){var s=e.replace(/-([a-z])/gi,function(r,n){return n.toUpperCase()});this.cssVenderPrefixes.indexOf(s)!==-1?(t.style[s.charAt(0).toLowerCase()+s.slice(1)]=i,t.style["webkit"+s]=i,t.style["moz"+s]=i,t.style["ms"+s]=i,t.style["o"+s]=i):t.style[s]=i},o.prototype._getFirstEl=function(){return this.selector&&this.selector.length!==void 0?this.selector[0]:this.selector},o.prototype.isEventMatched=function(t,e){var i=e.split(".");return t.split(".").filter(function(s){return s}).every(function(s){return i.indexOf(s)!==-1})},o.prototype.attr=function(t,e){return e===void 0?this.firstElement?this.firstElement.getAttribute(t):"":(this._each(function(i){i.setAttribute(t,e)}),this)},o.prototype.find=function(t){return c(this._getSelector(t,this.selector))},o.prototype.first=function(){return this.selector&&this.selector.length!==void 0?c(this.selector[0]):c(this.selector)},o.prototype.eq=function(t){return c(this.selector[t])},o.prototype.parent=function(){return c(this.selector.parentElement)},o.prototype.get=function(){return this._getFirstEl()},o.prototype.removeAttr=function(t){var e=t.split(" ");return this._each(function(i){e.forEach(function(s){return i.removeAttribute(s)})}),this},o.prototype.wrap=function(t){if(!this.firstElement)return this;var e=document.createElement("div");return e.className=t,this.firstElement.parentNode.insertBefore(e,this.firstElement),this.firstElement.parentNode.removeChild(this.firstElement),e.appendChild(this.firstElement),this},o.prototype.addClasLine truncated
|
||||||
|
<iframe class="lg-object" frameborder="0" `+n+' src="'+s+`" allowfullscreen="true"></iframe>
|
||||||
|
</div>`},getImgMarkup:function(o,t,e,i,s,r){var n=i?'srcset="'+i+'"':"",l=s?'sizes="'+s+'"':"",g="<img "+e+" "+n+" "+l+' class="lg-object lg-image" data-index="'+o+'" src="'+t+'" />',h="";if(r){var d=typeof r=="string"?JSON.parse(r):r;h=d.map(function(u){var a="";return Object.keys(u).forEach(function(m){a+=" "+m+'="'+u[m]+'"'}),"<source "+a+"></source>"})}return""+h+g},getResponsiveSrc:function(o){for(var t=[],e=[],i="",s=0;s<o.length;s++){var r=o[s].split(" ");r[0]===""&&r.splice(0,1),e.push(r[0]),t.push(r[1])}for(var n=window.innerWidth,l=0;l<t.length;l++)if(parseInt(t[l],10)>n){i=e[l];break}return i},isImageLoaded:function(o){return!(!o||!o.complete||o.naturalWidth===0)},getVideoPosterMarkup:function(o,t,e,i,s){var r="";s&&s.youtube?r="lg-has-youtube":s&&s.vimeo?r="lg-has-vimeo":r="lg-has-html5";var n=t;return typeof t!="string"&&(n=t.outerHTML),'<div class="lg-video-cont '+r+'" style="'+e+`">
|
||||||
|
<div class="lg-video-play-button">
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
preserveAspectRatio="xMidYMid"
|
||||||
|
focusable="false"
|
||||||
|
aria-labelledby="`+i+`"
|
||||||
|
role="img"
|
||||||
|
class="lg-video-play-icon"
|
||||||
|
>
|
||||||
|
<title>`+i+`</title>
|
||||||
|
<polygon class="lg-video-play-icon-inner" points="1,0 20,10 1,20"></polygon>
|
||||||
|
</svg>
|
||||||
|
<svg class="lg-video-play-icon-bg" viewBox="0 0 50 50" focusable="false">
|
||||||
|
<circle cx="50%" cy="50%" r="20"></circle></svg>
|
||||||
|
<svg class="lg-video-play-icon-circle" viewBox="0 0 50 50" focusable="false">
|
||||||
|
<circle cx="50%" cy="50%" r="20"></circle>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
`+n+`
|
||||||
|
<img class="lg-object lg-video-poster" src="`+o+`" />
|
||||||
|
</div>`},getFocusableElements:function(o){var t=o.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])'),e=[].filter.call(t,function(i){var s=window.getComputedStyle(i);return s.display!=="none"&&s.visibility!=="hidden"});return e},getDynamicOptions:function(o,t,e,i){var s=[],r=N(j,t);return[].forEach.call(o,function(n){for(var l={},g=0;g<n.attributes.length;g++){var h=n.attributes[g];if(h.specified){var d=K(h.name),u="";r.indexOf(d)>-1&&(u=d),u&&(l[u]=h.value)}}var a=c(n),m=a.find("img").first().attr("alt"),f=a.attr("title"),y=i?a.attr(i):a.find("img").first().attr("src");l.thumb=y,e&&!l.subHtml&&(l.subHtml=f||m||""),l.alt=m||f||"",s.push(l)}),s},isMobile:function(){return/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)},isVideo:function(o,t,e){if(!o){if(t)return{html5:!0};console.error("lightGallery :- data-src is not provided on slide item "+(e+1)+". Please make sure the selector property is properly configured. More info - https://www.lightgalleryjs.com/demos/html-markup/");return}var i=o.match(/\/\/(?:www\.)?youtu(?:\.be|be\.com|be-nocookie\.com)\/(?:watch\?v=|embed\/)?([a-z0-9\-\_\%]+)([\&|?][\S]*)*/i),s=o.match(/\/\/(?:www\.)?(?:player\.)?vimeo.com\/(?:video\/)?([0-9a-z\-_]+)(.*)?/i),r=o.match(/https?:\/\/(.+)?(wistia\.com|wi\.st)\/(medias|embed)\/([0-9a-z\-_]+)(.*)/);if(i)return{youtube:i};if(s)return{vimeo:s};if(r)return{wistia:r}}},$=0,U=(function(){function o(t,e){if(this.lgOpened=!1,this.index=0,this.plugins=[],this.lGalleryOn=!1,this.lgBusy=!1,this.currentItemsInDom=[],this.prevScrollTop=0,this.bodyPaddingRight=0,this.isDummyImageRemoved=!1,this.dragOrSwipeEnabled=!1,this.mediaContainerPosition={top:0,bottom:0},!t)return this;if($++,this.lgId=$,this.el=t,this.LGel=c(t),this.generateSettings(e),this.buildModules(),this.settings.dynamic&&this.settings.dynamicEl!==void 0&&!Array.isArray(this.settings.dynamicEl))throw"When using dynamic mode, you must also define dynamicEl as an Array.";return this.galleryItems=this.getItems(),this.normalizeSettings(),this.init(),this.validateLicense(),this}return o.prototype.generateSettings=function(t){if(this.settings=I(I({},q),t),this.settings.isMobile&&typeof this.settings.isMobile=="function"?this.settings.isMobile():b.isMobile()){var e=I(I({},this.settings.mobileSettings),this.settings.mobileSettings);this.settings=I(I({},this.settings),e)}},o.prototype.normalizeSettings=function(){if(this.settings.slideEndAnimation&&(this.settings.hideControlOnEnd=!1),this.settings.closable||(this.settings.swipeToClose=!1),this.zoomFromOrigin=this.settings.zoomFromOrigin,this.settings.dynamic&&(this.zoomFromOrigin=!1),this.settings.container){var t=this.settings.container;if(typeof t=="function")this.settings.container=t();else if(typeof t=="string"){var e=document.querySelector(t);this.settings.container=e??document.body}}else this.settings.container=document.body;this.settings.preload=Math.min(this.settings.preload,this.galleryItems.length)},o.prototype.init=function(){var t=this;this.addSlideVideoInfo(this.galleryItems),this.buildStructure(),this.LGel.trigger(p.init,{instance:this}),this.settings.keyPress&&this.keyPress(),setTimeout(function(){t.enableDrag(),t.enableSwipe(),t.triggerPosterClick()},50),this.arrow(),this.settings.mousewheel&&this.mousewheel(),this.settings.dynamic||this.openGalleryOnItemClick()},o.prototype.openGalleryOnItemClick=function(){for(var t=this,e=function(r){var n=i.items[r],l=c(n),g=D.generateUUID();l.attr("data-lg-id",g).on("click.lgcustom-item-"+g,function(h){h.preventDefault();var d=t.settings.index||r;t.openGallery(d,n)})},i=this,s=0;s<this.items.length;s++)e(s)},o.prototype.buildModules=function(){var t=this;this.settings.plugins.forEach(function(e){t.plugins.push(new e(t,c))})},o.prototype.validateLicense=function(){this.settings.licenseKey?this.settings.licenseKey==="0000-0000-000-0000"&&console.warn("lightGallery: "+this.settings.licenseKey+" license key is not valid for production use"):console.error("Please provide a valid license key")},o.prototype.getSlideItem=function(t){return c(this.getSlideItemId(t))},o.prototype.getSlideItemId=function(t){return"#lg-item-"+this.lgId+"-"+t},o.prototype.getIdName=function(t){return t+"-"+this.lgId},o.prototype.getElementById=function(t){return c("#"+this.getIdName(t))},o.prototype.manageSingleSlideClassName=function(){this.galleryItems.length<2?this.outer.addClass("lg-single-item"):this.outer.removeClass("lg-single-item")},o.prototype.buildStructure=function(){var t=this,e=this.$container&&this.$container.get();if(!e){var i="",s="";this.settings.controls&&(i='<button type="button" id="'+this.getIdName("lg-prev")+'" aria-label="'+this.settings.strings.previousSlide+'" class="lg-prev lg-icon"> '+this.settings.prevHtml+` </button>
|
||||||
|
<button type="button" id="`+this.getIdName("lg-next")+'" aria-label="'+this.settings.strings.nextSlide+'" class="lg-next lg-icon"> '+this.settings.nextHtml+" </button>"),this.settings.appendSubHtmlTo!==".lg-item"&&(s='<div class="lg-sub-html" role="status" aria-live="polite"></div>');var r="";this.settings.allowMediaOverlap&&(r+="lg-media-overlap ");var n=this.settings.ariaLabelledby?'aria-labelledby="'+this.settings.ariaLabelledby+'"':"",l=this.settings.ariaDescribedby?'aria-describedby="'+this.settings.ariaDescribedby+'"':"",g="lg-container "+this.settings.addClass+" "+(document.body!==this.settings.container?"lg-inline":""),h=this.settings.closable&&this.settings.showCloseIcon?'<button type="button" aria-label="'+this.settings.strings.closeGallery+'" id="'+this.getIdName("lg-close")+'" class="lg-close lg-icon"></button>':"",d=this.settings.showMaximizeIcon?'<button type="button" aria-label="'+this.settings.strings.toggleMaximize+'" id="'+this.getIdName("lg-maximize")+'" class="lg-maximize lg-icon"></button>':"",u=`
|
||||||
|
<div class="`+g+'" id="'+this.getIdName("lg-container")+'" tabindex="-1" aria-modal="true" '+n+" "+l+` role="dialog"
|
||||||
|
>
|
||||||
|
<div id="`+this.getIdName("lg-backdrop")+`" class="lg-backdrop"></div>
|
||||||
|
|
||||||
|
<div id="`+this.getIdName("lg-outer")+'" class="lg-outer lg-use-css3 lg-css3 lg-hide-items '+r+` ">
|
||||||
|
|
||||||
|
<div id="`+this.getIdName("lg-content")+`" class="lg-content">
|
||||||
|
<div id="`+this.getIdName("lg-inner")+`" class="lg-inner">
|
||||||
|
</div>
|
||||||
|
`+i+`
|
||||||
|
</div>
|
||||||
|
<div id="`+this.getIdName("lg-toolbar")+`" class="lg-toolbar lg-group">
|
||||||
|
`+d+`
|
||||||
|
`+h+`
|
||||||
|
</div>
|
||||||
|
`+(this.settings.appendSubHtmlTo===".lg-outer"?s:"")+`
|
||||||
|
<div id="`+this.getIdName("lg-components")+`" class="lg-components">
|
||||||
|
`+(this.settings.appendSubHtmlTo===".lg-sub-html"?s:"")+`
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;c(this.settings.container).append(u),document.body!==this.settings.container&&c(this.settings.container).css("position","relative"),this.outer=this.getElementById("lg-outer"),this.$lgComponents=this.getElementById("lg-components"),this.$backdrop=this.getElementById("lg-backdrop"),this.$container=this.getElementById("lg-container"),this.$inner=this.getElementById("lg-inner"),this.$content=this.getElementById("lg-content"),this.$toolbar=this.getElementById("lg-toolbar"),this.$backdrop.css("transition-duration",this.settings.backdropDuration+"ms");var a=this.settings.mode+" ";this.manageSingleSlideClassName(),this.settings.enableDrag&&(a+="lg-grab "),this.outer.addClass(a),this.$inner.css("transition-timing-function",this.settings.easing),this.$inner.css("transition-duration",this.settings.speed+"ms"),this.settings.download&&this.$toolbar.append('<a id="'+this.getIdName("lg-download")+'" target="_blank" rel="noopener" aria-label="'+this.settings.strings.download+'" download class="lg-download lg-icon"></a>'),this.counter(),c(window).on("resize.lg.global"+this.lgId+" orientationchange.lg.global"+this.lgId,function(){t.refreshOnResize()}),this.hideBars(),this.manageCloseGallery(),this.toggleMaximize(),this.initModules()}},o.prototype.refreshOnResize=function(){if(this.lgOpened){var t=this.galleryItems[this.index],e=t.__slideVideoInfo;this.mediaContainerPosition=this.getMediaContainerPosition();var i=this.mediaContainerPosition,s=i.top,r=i.bottom;if(this.currentImageSize=b.getSize(this.items[this.index],this.outer,s+r,e&&this.settings.videoMaxSize),e&&this.resizeVideoSlide(this.index,this.currentImageSize),this.zoomFromOrigin&&!this.isDummyImageRemoved){var n=this.getDummyImgStyles(this.currentImageSize);this.outer.find(".lg-current .lg-dummy-img").first().attr("style",n)}this.LGel.trigger(p.containerResize)}},o.prototype.resizeVideoSlide=function(t,e){var i=this.getVideoContStyle(e),s=this.getSlideItem(t);s.find(".lg-video-cont").attr("style",i)},o.prototype.updateSlides=function(t,e){if(this.index>t.length-1&&(this.index=t.length-1),t.length===1&&(this.index=0),!t.length){this.closeGallery();return}var i=this.galleryItems[e].src;this.galleryItems=t,this.updateControls(),this.$inner.empty(),this.currentItemsInDom=[];var s=0;this.galleryItems.some(function(r,n){return r.src===i?(s=n,!0):!1}),this.currentItemsInDom=this.organizeSlideItems(s,-1),this.loadContent(s,!0),this.getSlideItem(s).addClass("lg-current"),this.index=s,this.updateCurrentCounter(s),this.LGel.trigger(p.updateSlides)},o.prototype.getItems=function(){if(this.items=[],this.settings.dynamic)return this.settings.dynamicEl||[];if(this.settings.selector==="this")this.items.push(this.el);else if(this.settings.selector)if(typeof this.settings.selector=="string")if(this.settings.selectWithin){var t=c(this.settings.selectWithin);this.items=t.find(this.settings.selector).get()}else this.items=this.el.querySelectorAll(this.settings.selector);else this.items=this.settings.selector;else this.items=this.el.children;return b.getDynamicOptions(this.items,this.settings.extraProps,this.settings.getCaptionFromTitleOrAlt,this.settings.exThumbImage)},o.prototype.shouldHideScrollbar=function(){return this.settings.hideScrollbar&&document.body===this.settings.container},o.prototype.hideScrollbar=function(){if(this.shouldHideScrollbar()){this.bodyPaddingRight=parseFloat(c("body").style().paddingRight);var t=document.documentElement.getBoundingClientRect(),e=window.innerWidth-t.width;c(document.body).css("padding-right",e+this.bodyPaddingRight+"px"),c(document.body).addClass("lg-overlay-open")}},o.prototype.resetScrollBar=function(){this.shouldHideScrollbar()&&(c(document.body).css("padding-right",this.bodyPaddingRight+"px"),c(document.body).removeClass("lg-overlay-open"))},o.prototype.openGallery=function(t,e){var i=this;if(t===void 0&&(t=this.settings.index),!this.lgOpened){this.lgOpened=!0,this.outer.removeClass("lg-hide-items"),this.hideScrollbar(),this.$container.addClass("lg-show");var s=this.getItemsToBeInsertedToDom(t,t);this.currentItemsInDom=s;var r="";s.forEach(function(a){r=r+('<div id="'+a+'" class="lg-item"></div>')}),this.$inner.append(r),this.addHtml(t);var n="";this.mediaContainerPosition=this.getMediaContainerPosition();var l=this.mediaContainerPosition,g=l.top,h=l.bottom;this.settings.allowMediaOverlap||this.setMediaContainerPosition(g,h);var d=this.galleryItems[t].__slideVideoInfo;this.zoomFromOrigin&&e&&(this.currentImageSize=b.getSize(e,this.outer,g+h,d&&this.settings.videoMaxSize),n=b.getTransform(e,this.outer,g,h,this.currentImageSize)),(!this.zoomFromOrigin||!n)&&(this.outer.addClass(this.settings.startClass),this.getSlideItem(t).removeClass("lg-complete"));var u=this.settings.zoomFromOrigin?100:this.settings.backdropDuration;setTimeout(function(){i.outer.addClass("lg-components-open")},u),this.index=t,this.LGel.trigger(p.beforeOpen),this.getSlideItem(t).addClass("lg-current"),this.lGalleryOn=!1,this.prevScrollTop=c(window).scrollTop(),
Line truncated
|
||||||
|
<span id="`+this.getIdName("lg-counter-current")+'" class="lg-counter-current">'+(this.index+1)+` </span> /
|
||||||
|
<span id="`+this.getIdName("lg-counter-all")+'" class="lg-counter-all">'+this.galleryItems.length+" </span></div>";this.outer.find(this.settings.appendCounterTo).append(t)}},o.prototype.addHtml=function(t){var e,i;if(this.galleryItems[t].subHtmlUrl?i=this.galleryItems[t].subHtmlUrl:e=this.galleryItems[t].subHtml,!i)if(e){var s=e.substring(0,1);if(s==="."||s==="#")try{this.settings.subHtmlSelectorRelative&&!this.settings.dynamic?e=c(this.items).eq(t).find(e).first().html():e=c(e).first().html()}catch{console.warn('Error processing subHtml selector "'+e+'"'),e=""}}else e="";if(this.settings.appendSubHtmlTo!==".lg-item")i?b.fetchCaptionFromUrl(i,this.outer.find(".lg-sub-html"),"replace"):this.outer.find(".lg-sub-html").html(e);else{var r=c(this.getSlideItemId(t));i?b.fetchCaptionFromUrl(i,r,"append"):r.append('<div class="lg-sub-html">'+e+"</div>")}typeof e<"u"&&e!==null&&(e===""?this.outer.find(this.settings.appendSubHtmlTo).addClass("lg-empty-html"):this.outer.find(this.settings.appendSubHtmlTo).removeClass("lg-empty-html")),this.LGel.trigger(p.afterAppendSubHtml,{index:t})},o.prototype.preload=function(t){for(var e=1;e<=this.settings.preload&&!(e>=this.galleryItems.length-t);e++)this.loadContent(t+e,!1);for(var i=1;i<=this.settings.preload&&!(t-i<0);i++)this.loadContent(t-i,!1)},o.prototype.getDummyImgStyles=function(t){return t?"width:"+t.width+`px;
|
||||||
|
margin-left: -`+t.width/2+`px;
|
||||||
|
margin-top: -`+t.height/2+`px;
|
||||||
|
height:`+t.height+"px":""},o.prototype.getVideoContStyle=function(t){return t?"width:"+t.width+`px;
|
||||||
|
height:`+t.height+"px":""},o.prototype.getDummyImageContent=function(t,e,i){var s;if(this.settings.dynamic||(s=c(this.items).eq(e)),s){var r=void 0;if(this.settings.exThumbImage?r=s.attr(this.settings.exThumbImage):r=s.find("img").first().attr("src"),!r)return"";var n=this.getDummyImgStyles(this.currentImageSize),l=document.createElement("img");return l.alt=i||"",l.src=r,l.className="lg-dummy-img",l.style.cssText=n,t.addClass("lg-first-slide"),this.outer.addClass("lg-first-slide-loading"),l}return""},o.prototype.setImgMarkup=function(t,e,i){var s=this.galleryItems[i],r=s.alt,n=s.srcset,l=s.sizes,g=s.sources,h="",d=r?'alt="'+r+'"':"";this.isFirstSlideWithZoomAnimation()?h=this.getDummyImageContent(e,i,d):h=b.getImgMarkup(i,t,d,n,l,g);var u=document.createElement("picture");u.className="lg-img-wrap",c(u).append(h),e.prepend(u)},o.prototype.onSlideObjectLoad=function(t,e,i,s){var r=t.find(".lg-object").first();b.isImageLoaded(r.get())||e?i():(r.on("load.lg error.lg",function(){i&&i()}),r.on("error.lg",function(){s&&s()}))},o.prototype.onLgObjectLoad=function(t,e,i,s,r,n){var l=this;this.onSlideObjectLoad(t,n,function(){l.triggerSlideItemLoad(t,e,i,s,r)},function(){t.addClass("lg-complete lg-complete_"),t.html('<span class="lg-error-msg">'+l.settings.strings.mediaLoadingFailed+"</span>")})},o.prototype.triggerSlideItemLoad=function(t,e,i,s,r){var n=this,l=this.galleryItems[e],g=r&&this.getSlideType(l)==="video"&&!l.poster?s:0;setTimeout(function(){t.addClass("lg-complete lg-complete_"),n.LGel.trigger(p.slideItemLoad,{index:e,delay:i||0,isFirstSlide:r})},g)},o.prototype.isFirstSlideWithZoomAnimation=function(){return!!(!this.lGalleryOn&&this.zoomFromOrigin&&this.currentImageSize)},o.prototype.addSlideVideoInfo=function(t){var e=this;t.forEach(function(i,s){i.__slideVideoInfo=b.isVideo(i.src,!!i.video,s),i.__slideVideoInfo&&e.settings.loadYouTubePoster&&!i.poster&&i.__slideVideoInfo.youtube&&(i.poster="//img.youtube.com/vi/"+i.__slideVideoInfo.youtube[1]+"/maxresdefault.jpg")})},o.prototype.loadContent=function(t,e){var i=this,s=this.galleryItems[t],r=c(this.getSlideItemId(t)),n=s.poster,l=s.srcset,g=s.sizes,h=s.sources,d=s.src,u=s.video,a=u&&typeof u=="string"?JSON.parse(u):u;if(s.responsive){var m=s.responsive.split(",");d=b.getResponsiveSrc(m)||d}var f=s.__slideVideoInfo,y="",w=!!s.iframe,v=!this.lGalleryOn,C=0;if(v&&(this.zoomFromOrigin&&this.currentImageSize?C=this.settings.startAnimationDuration+10:C=this.settings.backdropDuration+10),!r.hasClass("lg-loaded")){if(f){var P=this.mediaContainerPosition,F=P.top,B=P.bottom,H=b.getSize(this.items[t],this.outer,F+B,f&&this.settings.videoMaxSize);y=this.getVideoContStyle(H)}if(w){var T=b.getIframeMarkup(this.settings.iframeWidth,this.settings.iframeHeight,this.settings.iframeMaxWidth,this.settings.iframeMaxHeight,d,s.iframeTitle);r.prepend(T)}else if(n){var G="",W=v&&this.zoomFromOrigin&&this.currentImageSize;W&&(G=this.getDummyImageContent(r,t,""));var T=b.getVideoPosterMarkup(n,G||"",y,this.settings.strings.playVideo,f);r.prepend(T)}else if(f){var T='<div class="lg-video-cont " style="'+y+'"></div>';r.prepend(T)}else if(this.setImgMarkup(d,r,t),l||h){var Y=r.find(".lg-object");this.initPictureFill(Y)}(n||f)&&this.LGel.trigger(p.hasVideo,{index:t,src:d,html5Video:a,hasPoster:!!n}),this.LGel.trigger(p.afterAppendSlide,{index:t}),this.lGalleryOn&&this.settings.appendSubHtmlTo===".lg-item"&&this.addHtml(t)}var x=0;C&&!c(document.body).hasClass("lg-from-hash")&&(x=C),this.isFirstSlideWithZoomAnimation()&&(setTimeout(function(){r.removeClass("lg-start-end-progress lg-start-progress").removeAttr("style")},this.settings.startAnimationDuration+100),r.hasClass("lg-loaded")||setTimeout(function(){if(i.getSlideType(s)==="image"){var k=s.alt,R=k?'alt="'+k+'"':"";if(r.find(".lg-img-wrap").append(b.getImgMarkup(t,d,R,l,g,s.sources)),l||h){var Z=r.find(".lg-object");i.initPictureFill(Z)}}(i.getSlideType(s)==="image"||i.getSlideType(s)==="video"&&n)&&(i.onLgObjectLoad(r,t,C,x,!0,!1),i.onSlideObjectLoad(r,!!(f&&f.html5&&!n),function(){i.loadContentOnFirstSlideLoad(t,r,x)},function(){i.loadContentOnFirstSlideLoad(t,r,x)}))},this.settings.startAnimationDuration+100)),r.addClass("lg-loaded"),(!this.isFirstSlideWithZoomAnimation()||this.getSlideType(s)==="video"&&!n)&&this.onLgObjectLoad(r,t,C,x,v,!!(f&&f.html5&&!n)),(!this.zoomFromOrigin||!this.currentImageSize)&&r.hasClass("lg-complete_")&&!this.lGalleryOn&&setTimeout(function(){r.addClass("lg-complete")},this.settings.backdropDuration),this.lGalleryOn=!0,e===!0&&(r.hasClass("lg-complete_")?this.preload(t):r.find(".lg-object").first().on("load.lg error.lg",function(){i.preload(t)}))},o.prototype.loadContentOnFirstSlideLoad=function(t,e,i){var s=this;setTimeout(function(){e.find(".lg-dummy-img").remove(),e.removeClass("lg-first-slide"),s.outer.removeClass("lg-first-slide-loading"),s.isDummyImageRemoved=!0,s.preload(t)},i+300)},o.prototype.getItemsToBeInsertedToDom=function(t,e,i){var s=this;i===void 0&&(i=0);var r=[]
Line truncated
|
||||||
|
<div class="lg-thumb lg-group">
|
||||||
|
</div>
|
||||||
|
</div>`;this.core.outer.addClass("lg-has-thumb"),this.settings.appendThumbnailsTo===".lg-components"?this.core.$lgComponents.append(e):this.core.outer.append(e),this.$thumbOuter=this.core.outer.find(".lg-thumb-outer").first(),this.$lgThumb=this.core.outer.find(".lg-thumb").first(),this.settings.animateThumb&&this.core.outer.find(".lg-thumb").css("transition-duration",this.core.settings.speed+"ms").css("width",this.thumbTotalWidth+"px").css("position","relative"),this.setThumbItemHtml(this.core.galleryItems)},o.prototype.enableThumbDrag=function(){var t=this,e={cords:{startX:0,endX:0},isMoved:!1,newTranslateX:0,startTime:new Date,endTime:new Date,touchMoveTime:0},i=!1;this.$thumbOuter.addClass("lg-grab"),this.core.outer.find(".lg-thumb").first().on("mousedown.lg.thumb",function(s){t.thumbTotalWidth>t.thumbOuterWidth&&(s.preventDefault(),e.cords.startX=s.pageX,e.startTime=new Date,t.thumbClickable=!1,i=!0,t.core.outer.get().scrollLeft+=1,t.core.outer.get().scrollLeft-=1,t.$thumbOuter.removeClass("lg-grab").addClass("lg-grabbing"))}),this.$LG(window).on("mousemove.lg.thumb.global"+this.core.lgId,function(s){t.core.lgOpened&&i&&(e.cords.endX=s.pageX,e=t.onThumbTouchMove(e))}),this.$LG(window).on("mouseup.lg.thumb.global"+this.core.lgId,function(){t.core.lgOpened&&(e.isMoved?e=t.onThumbTouchEnd(e):t.thumbClickable=!0,i&&(i=!1,t.$thumbOuter.removeClass("lg-grabbing").addClass("lg-grab")))})},o.prototype.enableThumbSwipe=function(){var t=this,e={cords:{startX:0,endX:0},isMoved:!1,newTranslateX:0,startTime:new Date,endTime:new Date,touchMoveTime:0};this.$lgThumb.on("touchstart.lg",function(i){t.thumbTotalWidth>t.thumbOuterWidth&&(i.preventDefault(),e.cords.startX=i.targetTouches[0].pageX,t.thumbClickable=!1,e.startTime=new Date)}),this.$lgThumb.on("touchmove.lg",function(i){t.thumbTotalWidth>t.thumbOuterWidth&&(i.preventDefault(),e.cords.endX=i.targetTouches[0].pageX,e=t.onThumbTouchMove(e))}),this.$lgThumb.on("touchend.lg",function(){e.isMoved?e=t.onThumbTouchEnd(e):t.thumbClickable=!0})},o.prototype.rebuildThumbnails=function(){var t=this;this.$thumbOuter.addClass("lg-rebuilding-thumbnails"),setTimeout(function(){t.thumbTotalWidth=t.core.galleryItems.length*(t.settings.thumbWidth+t.settings.thumbMargin),t.$lgThumb.css("width",t.thumbTotalWidth+"px"),t.$lgThumb.empty(),t.setThumbItemHtml(t.core.galleryItems),t.animateThumb(t.core.index)},50),setTimeout(function(){t.$thumbOuter.removeClass("lg-rebuilding-thumbnails")},200)},o.prototype.setTranslate=function(t){this.$lgThumb.css("transform","translate3d(-"+t+"px, 0px, 0px)")},o.prototype.getPossibleTransformX=function(t){return t>this.thumbTotalWidth-this.thumbOuterWidth&&(t=this.thumbTotalWidth-this.thumbOuterWidth),t<0&&(t=0),t},o.prototype.animateThumb=function(t){if(this.$lgThumb.css("transition-duration",this.core.settings.speed+"ms"),this.settings.animateThumb){var e=0;switch(this.settings.currentPagerPosition){case"left":e=0;break;case"middle":e=this.thumbOuterWidth/2-this.settings.thumbWidth/2;break;case"right":e=this.thumbOuterWidth-this.settings.thumbWidth}this.translateX=(this.settings.thumbWidth+this.settings.thumbMargin)*t-1-e,this.translateX>this.thumbTotalWidth-this.thumbOuterWidth&&(this.translateX=this.thumbTotalWidth-this.thumbOuterWidth),this.translateX<0&&(this.translateX=0),this.setTranslate(this.translateX)}},o.prototype.onThumbTouchMove=function(t){return t.newTranslateX=this.translateX,t.isMoved=!0,t.touchMoveTime=new Date().valueOf(),t.newTranslateX-=t.cords.endX-t.cords.startX,t.newTranslateX=this.getPossibleTransformX(t.newTranslateX),this.setTranslate(t.newTranslateX),this.$thumbOuter.addClass("lg-dragging"),t},o.prototype.onThumbTouchEnd=function(t){t.isMoved=!1,t.endTime=new Date,this.$thumbOuter.removeClass("lg-dragging");var e=t.endTime.valueOf()-t.startTime.valueOf(),i=t.cords.endX-t.cords.startX,s=Math.abs(i)/e;return s>.15&&t.endTime.valueOf()-t.touchMoveTime<30?(s+=1,s>2&&(s+=1),s=s+s*(Math.abs(i)/this.thumbOuterWidth),this.$lgThumb.css("transition-duration",Math.min(s-1,2)+"settings"),i=i*s,this.translateX=this.getPossibleTransformX(this.translateX-i),this.setTranslate(this.translateX)):this.translateX=t.newTranslateX,Math.abs(t.cords.endX-t.cords.startX)<this.settings.thumbnailSwipeThreshold&&(this.thumbClickable=!0),t},o.prototype.getThumbHtml=function(t,e,i){var s=this.core.galleryItems[e].__slideVideoInfo||{},r;s.youtube&&this.settings.loadYouTubeThumbnail?r="//img.youtube.com/vi/"+s.youtube[1]+"/"+this.settings.youTubeThumbSize+".jpg":r=t;var n=document.createElement("div");n.setAttribute("data-lg-item-id",e+""),n.className="lg-thumb-item "+(e===this.core.index?"active":""),n.style.cssText="width: "+this.settings.thumbWidth+"px; height: "+this.settings.thumbHeight+"; margin-right: "+this.settings.thumbMargin+"px;";var l=document.createElement("img");return l.alt=i||"",l.setAttribute("data-lg-item-id",e+""),l.src=r,n.appendChild(l),n},o.prototype.setThumbItemHtml=function(t){for(var e=0;e<t.length;e++){var i=this.getLine truncated
|
||||||
|
webkitfullscreenchange.lg.global`+this.core.lgId+`
|
||||||
|
mozfullscreenchange.lg.global`+this.core.lgId+`
|
||||||
|
MSFullscreenChange.lg.global`+this.core.lgId,function(){t.core.lgOpened&&t.core.outer.toggleClass("lg-fullscreen-on")}),this.core.outer.find(".lg-fullscreen").first().on("click.lg",function(){t.isFullScreen()?t.exitFullscreen():t.requestFullscreen()})},o.prototype.closeGallery=function(){this.isFullScreen()&&this.exitFullscreen()},o.prototype.destroy=function(){this.$LG(document).off("fullscreenchange.lg.global"+this.core.lgId+`
|
||||||
|
webkitfullscreenchange.lg.global`+this.core.lgId+`
|
||||||
|
mozfullscreenchange.lg.global`+this.core.lgId+`
|
||||||
|
MSFullscreenChange.lg.global`+this.core.lgId)},o})(),X=new WeakSet;function rt(o){return!!X.has(o)}function nt(o){X.add(o)}function lt(o,t){if(!o)return t||"";var e=o.split("|");if(e.length<8)return t||"";var i=e[0],s=e[1],r=e[2],n=e[3],l=e[4],g=e[5],h=e[6],d=e[7],u=i||s||r||n||l||g||h;if(!u)return t||"";var a='<div class="wi-lg-caption">'+(t||"")+"</div>";if(a+='<div class="wi-lg-exif">',(i||s)&&(a+='<span class="wi-lg-exif__item wi-lg-exif__camera">'+(i?i+" ":"")+s+"</span>"),r&&(a+='<span class="wi-lg-exif__item wi-lg-exif__lens">'+r+"</span>"),a+='<div class="wi-lg-exif__params">',n&&(a+='<span class="wi-lg-exif__item wi-lg-exif__aperture">f/'+n+"</span>"),l&&(a+='<span class="wi-lg-exif__item wi-lg-exif__shutter">'+l+"s</span>"),g&&(a+='<span class="wi-lg-exif__item wi-lg-exif__iso">ISO '+g+"</span>"),h){var m=h+"mm";d&&d!==h&&(m+=" (eq. "+d+"mm)"),a+='<span class="wi-lg-exif__item wi-lg-exif__focal">'+m+"</span>"}return a+="</div></div>",a}function at(o){var t=o.querySelectorAll(".wi-lightgallery-item[data-exif]");t.forEach(function(e){var i=e.getAttribute("data-exif");if(i){var s=e.getAttribute("data-sub-html")||"",r=lt(i,s);r&&r!==s&&e.setAttribute("data-sub-html",r)}})}function gt(){return{selector:".wi-lightgallery-item",plugins:[tt,it,ot],speed:280,mode:"lg-fade",licenseKey:"GPLv3",download:!1,counter:!0,preload:2,thumbWidth:80,thumbHeight:"60px",thumbMargin:4,zoomFromOrigin:!0,actualSize:!1,showZoomInOutIcons:!0,toggleThumb:!0,allowMediaOverlap:!0,mobileSettings:{controls:!0,showCloseIcon:!0,download:!1}}}function O(o){if(!rt(o)){var t=o.querySelectorAll(".wi-lightgallery-item");t.length!==0&&(at(o),J(o,gt()),nt(o))}}function ht(){return window.innerWidth<768}function E(){ut(),dt(),ct(),document.addEventListener("click",function(o){var t=o.target.closest(".lg-toggle-thumb");if(t){o.stopPropagation();var e=t.closest(".lg-outer");e&&e.classList.toggle("wi-thumb-shown")}},!0),document.addEventListener("click",function(o){var t=o.target.closest(".wi-lightgallery-item");t&&setTimeout(function(){var e=document.querySelector(".lg-outer");e&&!e.classList.contains("wi-thumb-shown")&&!ht()&&e.classList.add("wi-thumb-shown")},350)})}function ut(){var o=[".wi-post__body",".wi-page__body"];o.forEach(function(t){var e=document.querySelectorAll(t);e.forEach(function(i){mt(i),O(i)})})}function dt(){var o=document.querySelector(".wi-photos-page__all-groups");if(o){var t=o.querySelectorAll(".wi-photos-page__link");t.forEach(function(r){r.classList.add("wi-lightgallery-item")}),O(o);return}var e=document.querySelector(".wi-photos-page__grid:not(.wi-photos-page__all-groups .wi-photos-page__grid)");if(e){var i=e.querySelectorAll(".wi-photos-page__link");i.forEach(function(r){r.classList.add("wi-lightgallery-item")}),O(e);return}var s=document.querySelectorAll(".wi-photos-page__grid");s.forEach(function(r){ft(r),O(r)})}function ct(){var o=[".wi-moments-page__media",".wi-moments-page__card-media",".wi-moments-page__masonry-media"];o.forEach(function(t){var e=document.querySelectorAll(t);e.forEach(function(i){pt(i),O(i)})})}function mt(o){var t=o.querySelectorAll("img");t.forEach(function(e){var i=e.getAttribute("src");if(i){var s=e.getAttribute("width"),r=e.getAttribute("height");if(!(s&&parseInt(s)<50||r&&parseInt(r)<50)){var n=(e.getAttribute("class")||"").toLowerCase();if(!/emoji|icon|avatar|logo|badge|smiley/.test(n)&&e.getAttribute("role")!=="presentation"&&e.getAttribute("aria-hidden")!=="true"){var l=e.closest("a");if(l){if(l.getAttribute("target")==="_blank")return;if(l.classList.add("wi-lightgallery-item"),!l.getAttribute("data-src")){var g=l.getAttribute("href")||i;l.setAttribute("data-src",g)}var h=e.getAttribute("alt")||"";h&&!l.getAttribute("data-sub-html")&&l.setAttribute("data-sub-html",h)}else{var d=document.createElement("a");d.setAttribute("href",i),d.setAttribute("data-src",i),d.classList.add("wi-lightgallery-item");var u=e.getAttribute("alt")||"";u&&d.setAttribute("data-sub-html",u),e.parentNode.insertBefore(d,e),d.appendChild(e)}}}}})}function ft(o){var t=o.querySelectorAll(".wi-photos-page__link");t.forEach(function(e){e.classList.add("wi-lightgallery-item")})}function pt(o){var t=o.querySelectorAll("img");t.forEach(function(e){var i=e.getAttribute("src");i&&(e.classList.add("wi-lightgallery-item"),e.getAttribute("data-src")||e.setAttribute("data-src",i))})}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){"requestIdleCallback"in window?requestIdleCallback(E,{timeout:2e3}):setTimeout(E,200)}):"requestIdleCallback"in window?requestIdleCallback(E,{timeout:2e3}):setTimeout(E,200);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.wi-reading-progress[data-astro-cid-ftxg5n3y]{position:fixed;top:0;left:0;width:0;height:3px;background:var(--accent);z-index:101;transition:width .1s linear}.wi-post[data-astro-cid-ftxg5n3y]{display:grid;gap:1rem;padding-block:1rem;overflow-x:hidden}.wi-post__header[data-astro-cid-ftxg5n3y]{display:grid;gap:var(--space-lg);padding:var(--space-lg) 0 var(--space-md);border-bottom:1px solid var(--rule);text-align:center}.wi-post__cover-wrap[data-astro-cid-ftxg5n3y]{border-radius:12px;overflow:hidden}.wi-post__cover[data-astro-cid-ftxg5n3y]{width:100%;max-height:480px;object-fit:cover;border-radius:12px}.wi-post__title[data-astro-cid-ftxg5n3y]{font-family:var(--font-sans);font-size:clamp(1.8rem,4vw,2.8rem);font-weight:700;line-height:var(--leading-tight);letter-spacing:-.02em;color:var(--ink);margin:0}.wi-page-accent[data-astro-cid-ftxg5n3y]{display:block;width:48px;height:3px;margin:var(--space-sm) auto 0;border-radius:9999px;background:var(--accent)}.wi-post__meta[data-astro-cid-ftxg5n3y]{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:.4rem;color:var(--ink-3);font-size:var(--text-sm);letter-spacing:var(--tracking-wide)}.wi-post__meta[data-astro-cid-ftxg5n3y] a[data-astro-cid-ftxg5n3y]{color:var(--ink-2);text-decoration:none;transition:color var(--duration-fast) var(--ease-out-quart)}.wi-post__meta[data-astro-cid-ftxg5n3y] a[data-astro-cid-ftxg5n3y]:hover{color:var(--accent)}.wi-post__meta-sep[data-astro-cid-ftxg5n3y]{color:var(--ink-3)}.wi-post__author[data-astro-cid-ftxg5n3y]{display:inline-flex;align-items:center;gap:.35rem}.wi-post__author-avatar[data-astro-cid-ftxg5n3y]{width:20px;height:20px;border-radius:50%;object-fit:cover}.wi-post__meta-categories[data-astro-cid-ftxg5n3y] a[data-astro-cid-ftxg5n3y]+a[data-astro-cid-ftxg5n3y]:before{content:", "}.wi-post__meta-tags[data-astro-cid-ftxg5n3y] a[data-astro-cid-ftxg5n3y]+a[data-astro-cid-ftxg5n3y]:before{content:", "}.wi-post__stats[data-astro-cid-ftxg5n3y]{display:inline-flex;align-items:center;gap:.4rem}.wi-post__stat-item[data-astro-cid-ftxg5n3y]+.wi-post__stat-item[data-astro-cid-ftxg5n3y]:before{content:"·";margin-right:.4rem;color:var(--ink-3)}.wi-post__body[data-astro-cid-ftxg5n3y]{width:100%;font-size:clamp(1.05rem,1rem + .25vw,1.15rem);line-height:1.85;color:#3d3530;overflow-wrap:break-word;word-wrap:break-word}html.dark .wi-post__body[data-astro-cid-ftxg5n3y]{color:#d9d2c9}.wi-toc[data-astro-cid-ftxg5n3y]{display:none}@media(min-width:1280px){.wi-toc[data-astro-cid-ftxg5n3y]{display:block;position:fixed;top:80px;width:220px;max-height:calc(100vh - 100px);overflow-y:auto;z-index:10}}.wi-toc__header[data-astro-cid-ftxg5n3y]{display:flex;align-items:center;padding-bottom:var(--space-sm);margin-bottom:var(--space-sm);border-bottom:1px solid var(--rule)}.wi-toc__title[data-astro-cid-ftxg5n3y]{font-family:var(--font-sans);font-size:var(--text-sm);font-weight:600;color:var(--ink);letter-spacing:var(--tracking-wide)}.wi-toc__nav[data-astro-cid-ftxg5n3y]{display:flex;flex-direction:column;gap:0;overflow:hidden}.wi-toc-fab[data-astro-cid-ftxg5n3y]{display:none;position:fixed;bottom:32px;right:24px;width:44px;height:44px;border-radius:50%;border:1px solid var(--rule);background:var(--bg-raised);color:var(--ink-2);cursor:pointer;z-index:20;align-items:center;justify-content:center;box-shadow:var(--shadow-md);transition:background .2s ease,color .2s ease,border-color .2s ease,opacity .3s ease}.wi-toc-fab[data-astro-cid-ftxg5n3y]:hover{color:var(--accent);border-color:var(--accent)}@media(max-width:1279px){.wi-toc-fab[data-astro-cid-ftxg5n3y]{display:inline-flex;bottom:24px}}@media(max-width:640px){.wi-toc-fab[data-astro-cid-ftxg5n3y]{right:16px;bottom:24px;width:40px;height:40px}.wi-post__share-btn[data-astro-cid-ftxg5n3y]{width:44px;height:44px}.wi-like-btn[data-astro-cid-ftxg5n3y]{padding:.75rem 1.75rem}}.wi-toc-drawer[data-astro-cid-ftxg5n3y]{display:none}.wi-toc-drawer--open[data-astro-cid-ftxg5n3y]{display:block}.wi-toc-drawer__overlay[data-astro-cid-ftxg5n3y]{position:fixed;inset:0;background:#0006;z-index:50}.wi-toc-drawer__panel[data-astro-cid-ftxg5n3y]{position:fixed;bottom:0;left:0;right:0;max-height:60vh;background:var(--bg);border-top:1px solid var(--rule);border-radius:16px 16px 0 0;padding:1.25rem;z-index:51;overflow-y:auto;transform:translateY(0);transition:transform .3s cubic-bezier(.16,1,.3,1)}.wi-toc-drawer__header[data-astro-cid-ftxg5n3y]{display:flex;align-items:center;justify-content:space-between;margin-bottom:.75rem;padding-bottom:.75rem;border-bottom:1px solid var(--rule)}.wi-toc-drawer__title[data-astro-cid-ftxg5n3y]{font-family:var(--font-sans);font-size:var(--text-sm);font-weight:600;color:var(--ink)}.wi-toc-drawer__close[data-astro-cid-ftxg5n3y]{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;background:none;color:var(--ink-3);cursor:pointer}.wi-toc-drawer__close[data-astro-cid-ftxg5n3y]:hover{color:var(--accent)}.wi-toc-drawer__nav[data-astro-cid-ftxg5n3y]{disLine truncated
|
||||||
@@ -35,7 +35,9 @@ export default defineConfig({
|
|||||||
{ text: '瞬间', link: '/config/moments' },
|
{ text: '瞬间', link: '/config/moments' },
|
||||||
{ text: '图库', link: '/config/photos' },
|
{ text: '图库', link: '/config/photos' },
|
||||||
{ text: '友情链接', link: '/config/links' },
|
{ text: '友情链接', link: '/config/links' },
|
||||||
{ text: '朋友圈', link: '/config/friends' },
|
{ text: '订阅', link: '/config/subscriptions' },
|
||||||
|
{ text: '欢迎弹窗', link: '/config/welcome-popup' },
|
||||||
|
{ text: '微信读书', link: '/config/weread' },
|
||||||
{ text: '记得', link: '/config/important-dates' },
|
{ text: '记得', link: '/config/important-dates' },
|
||||||
{ text: '留言板', link: '/config/messageboard' },
|
{ text: '留言板', link: '/config/messageboard' },
|
||||||
{ text: '评论', link: '/config/comment' },
|
{ text: '评论', link: '/config/comment' },
|
||||||
@@ -53,7 +55,7 @@ export default defineConfig({
|
|||||||
{ text: '瞬间', link: '/pages/moments' },
|
{ text: '瞬间', link: '/pages/moments' },
|
||||||
{ text: '图库', link: '/pages/photos' },
|
{ text: '图库', link: '/pages/photos' },
|
||||||
{ text: '友情链接', link: '/pages/links' },
|
{ text: '友情链接', link: '/pages/links' },
|
||||||
{ text: '朋友圈', link: '/pages/friends' },
|
{ text: '订阅', link: '/pages/subscriptions' },
|
||||||
{ text: '记得', link: '/pages/important-dates' },
|
{ text: '记得', link: '/pages/important-dates' },
|
||||||
{ text: '自定义页面', link: '/pages/custom-page' },
|
{ text: '自定义页面', link: '/pages/custom-page' },
|
||||||
{ text: '扩展页面', link: '/pages/extended' },
|
{ text: '扩展页面', link: '/pages/extended' },
|
||||||
|
|||||||
+87
-1
@@ -1,6 +1,92 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
## v1.2.5
|
## v1.2.7
|
||||||
|
|
||||||
|
### 文档全面校准(与最新版主题对齐)
|
||||||
|
|
||||||
|
- 🔌 **移除「朋友圈」插件的全部使用说明**:`plugin-friends` 已不再提供,原先的 feed / RSS 聚合能力
|
||||||
|
由官方 **「链接管理」(PluginLinks)插件的订阅**承担。主题的对应实现是 `page_subscriptions.html`
|
||||||
|
自定义模板与 `subscriptions` 设置分组;新增订阅配置 / 订阅页面文档,删除旧的朋友圈配置与页面文档
|
||||||
|
- 📌 主题保留 `friends.html` 模板与「朋友圈」设置分组**仅用于兼容旧站点**,`pluginFinder` 检测不到
|
||||||
|
该插件时会显示空状态;新站点请勿再依赖,文档已明确标注
|
||||||
|
- ⬆️ 全文统一 Halo 版本要求为 **2.27.0+**(`docs/guide/getting-started.md` 原写 `2.24.0`,与
|
||||||
|
`theme.yaml` 的 `requires: ">=2.27.0"` 不一致)
|
||||||
|
- 🧹 清理与代码不符的描述:已移除的光感跟随 `animation_cursor_glow`、主题并不自带的
|
||||||
|
i18n 国际化、没有对应模板的「足迹页面」;`docs/config/style.md` 补上实际存在的
|
||||||
|
`click_effect_enabled` / `click_effect_type`
|
||||||
|
- 📚 补齐此前完全没有文档的三个设置分组:**欢迎弹窗、订阅、微信读书**;自定义页面模板清单
|
||||||
|
同步为 4 个(默认 / 留言板 / 订阅 / 微信读书)
|
||||||
|
- 🔢 配置数量统一为 **17 组 / 126 项**,插件适配表按实际 **10 款**校准,VitePress 侧边栏同步调整
|
||||||
|
- 🔗 所有文档内部相对链接复核为 0 处失效
|
||||||
|
- 🧾 README 精简与重排:移除开发者自证、回归检查清单与重复的「不加载远程脚本」类宣言,
|
||||||
|
按「安装 → 功能 → 插件 → 隐私 → 开发 → 声明」重排章节(331 行 → 237 行);
|
||||||
|
5.1 逐项披露、许可表与原创声明完整保留
|
||||||
|
|
||||||
|
### 可选 API 设置项文案精简
|
||||||
|
|
||||||
|
- ✂️ 主题设置中「自定义 IP 定位主 API」「自定义 IP 定位备选 API」「自定义一言 API」
|
||||||
|
三个配置项的 `help` 移除三段冗长的隐私披露文案,仅保留功能说明(候选接口地址、留空回退规则)
|
||||||
|
- 🛡️ 第三方数据流向与隐私披露仍由 README「外部资源与隐私」章节、配置文档与 FAQ 完整承载,
|
||||||
|
满足 5.1 要求;设置界面文案精简不影响「默认零第三方请求」的结论
|
||||||
|
- ↩️ 三个字段恢复按功能开关条件显示(「启用欢迎弹窗」开关 / 「描述文案来源」),与 v1.2.6
|
||||||
|
的设计一致;同时修正三个配置块的缩进,与 `settings.yaml` 其余字段对齐
|
||||||
|
- 🔒 `pnpm audit:external` 的 5.1 检查改为:三个字段必须存在、`help` 不得回潮出现披露文案、
|
||||||
|
README 必须保留隐私披露章节
|
||||||
|
|
||||||
|
### 正文引用块留白(与上一轮改动合并)
|
||||||
|
|
||||||
|
- 🎨 修复文章 / 单页 / 留言板正文引用块文字下方空白过大:块内元素外边距与 `padding` 叠加导致
|
||||||
|
双层留白,现统一收敛,并折叠编辑器生成的空段落;引用块上下外边距同步收紧
|
||||||
|
|
||||||
|
## v1.2.6
|
||||||
|
|
||||||
|
> v1.2.5 为审核被拒的未发布版本,从未对外分发;其全部整改内容纳入 v1.2.6 一并发布。
|
||||||
|
> 上一个对外发布版本为 v1.1.1(v1.1.2 – v1.2.5 均未发布)。
|
||||||
|
|
||||||
|
### 本轮变更
|
||||||
|
|
||||||
|
- 🆙 版本号升至 `1.2.6`,与上次被拒的审核快照明确区分
|
||||||
|
- ✍️ 新增「原创声明(4.2.1)」:本主题完全原创,未基于任何已有主题 / 插件派生,4.2.1 / 4.2.2 不适用
|
||||||
|
- 🔇 Hero「描述文案来源」默认值由 `hitokoto` 改为 **`none`** —— 默认不渲染一言元素、不显示描述文案,零外部请求
|
||||||
|
- 🔗 选择「一言」时自动填入官方接口 `https://v1.hitokoto.cn/`;选择「一言」或「自定义」后若 API 留空,
|
||||||
|
则使用主题内置的本地句子库(零外部请求)。**只有填写后才会向该地址发起请求**,需自行在站点隐私说明中披露,
|
||||||
|
详见 README「外部资源与隐私」
|
||||||
|
- 🌐 文档列出两个可选 IP 定位接口:`https://myip.ipip.net/json` 与
|
||||||
|
`https://r.inews.qq.com/api/ip2city?otype=json`(主 / 备选 API 均可选用)。
|
||||||
|
**填写后会把访客 IP 发送给该第三方服务换取地理位置**,需自行在站点隐私说明中披露,
|
||||||
|
详见 README「外部资源与隐私」
|
||||||
|
- 📄 README「外部资源与隐私」新增可选接口清单与逐项披露;docs 的 Hero 配置、FAQ、快速开始与首页页同步更新
|
||||||
|
|
||||||
|
### 上架审核整改(原 v1.2.5 修订,并入 1.2.6)
|
||||||
|
|
||||||
|
- 🔒 **默认零第三方自动请求**:移除全部 21 个模板对 `cdn.jsdelivr.net` 的 `preconnect` / `dns-prefetch`
|
||||||
|
- 🔒 移除文章页 PrismJS 1.29.0、`autoloader`(会按代码语言从 CDN 动态拉取并执行组件)与
|
||||||
|
`qrcode-generator`;代码高亮改由应用市场代码高亮插件 / API 扩展包提供,主题只保留自托管的
|
||||||
|
Warm / Cold token 配色(微信分享改为「复制链接」)
|
||||||
|
- 🔒 一言默认改用内置本地句子库 `assets/data/hitokoto.json`,不再默认请求 `v1.hitokoto.cn`
|
||||||
|
- 🔒 欢迎弹窗默认显示本地静态欢迎语,移除内置的 IP 定位主 / 备选第三方接口;IP 定位改为
|
||||||
|
**管理员显式配置**的可选能力(开启时会向该服务发送访客 IP,需自行披露)
|
||||||
|
- 📄 README 新增「外部资源与隐私」章节(服务名称 / 用途 / 数据类型 / 默认状态 / 关闭方法 /
|
||||||
|
遥测声明)与「验证与回归测试」清单;补充 LXGW WenKai 的 OFL 来源与 LightGallery GPLv3 说明
|
||||||
|
- 🔗 修正上架说明中的无效链接:文档与预览图改为有效绝对地址并随包分发;朋友圈插件链接改为应用市场地址
|
||||||
|
- 🐛 全部 `#strings.substring(..., 0, 1)` 增加空值保护,修复 `friends.html` 作者为空时的字符串越界
|
||||||
|
- 🖼️ `logo.png` 由两份 1024×1024(各约 1.5MB)压缩去重为单份 256×256(约 146KB)
|
||||||
|
- ✅ LightGallery 去掉占位 `licenseKey`,按 GPLv3 开源许可使用,控制台不再出现授权提示
|
||||||
|
- 🧪 新增 `pnpm audit:external` 与打包前置校验:拦截第三方远程加载、预连接、动态加载、
|
||||||
|
README 死链、重复大图;`docs/*.md` 与预览图一并打进安装包
|
||||||
|
|
||||||
|
#### 按 v2026.09.07 审核指南全文补充自查(并入 1.2.6)
|
||||||
|
|
||||||
|
- 🔒 **内容渲染安全(4.4.2)**:文章页 JSON-LD 不再用 `th:utext` 把标题 / 摘要 / 作者 / 封面
|
||||||
|
拼进 `<script>` 原始文本(可被脚本闭合标签截断并注入);改为「Thymeleaf 转义写入 `<meta>`
|
||||||
|
→ `JSON.stringify` → `textContent`」,并新增 `<meta name="author">`。瞬间正文 `content.raw`
|
||||||
|
兜底从 `th:utext` 改为 `th:text` 转义输出(首页区块、瞬间页列表 / 瀑布流、详情页共 4 处)
|
||||||
|
- 📜 **许可证传递(5.2)**:`public/assets/fonts/LXGWWenKai/OFL.txt` 随包携带上游版权声明、
|
||||||
|
保留字体名附加许可与 SIL OFL 1.1 全文;`pnpm build` / `pnpm package` 均校验其存在与内容
|
||||||
|
- 🔢 **上架信息校准(2.2 / 4.6)**:README 设置项按 `settings.yaml` 实测改为 17 组 / 126 项、
|
||||||
|
点击特效改为 8 种效果(可关闭);新增「内容渲染安全」章节与搜索页 / 空结果状态人工回归项
|
||||||
|
- 🧪 合规审计扩展:拦截任意 `<script>` 上的 `th:utext`、缺失字体许可证、README 数字漂移;
|
||||||
|
`pnpm smoke` 新增 JSON-LD 合法性与注入防护的桩 DOM 验证
|
||||||
|
|
||||||
### 兼容性与元数据
|
### 兼容性与元数据
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
| `article_related_title` | 文本 | `相关推荐` | 相关文章标题 |
|
| `article_related_title` | 文本 | `相关推荐` | 相关文章标题 |
|
||||||
| `article_show_share` | 开关 | `true` | 显示分享按钮 |
|
| `article_show_share` | 开关 | `true` | 显示分享按钮 |
|
||||||
| `article_share_title` | 文本 | `分享` | 分享区域标题 |
|
| `article_share_title` | 文本 | `分享` | 分享区域标题 |
|
||||||
|
| `article_extra_spacing` | 开关 | `true` | 中英文混排自动加空格;依赖「API 扩展包」,插件未安装时自动跳过 |
|
||||||
|
| `article_extra_highlight` | 开关 | `false` | 服务端代码高亮;依赖「API 扩展包」全量版(含 JS 运行时),开启后替代主题自带配色,环境不支持请勿开启 |
|
||||||
|
|
||||||
## 详细说明
|
## 详细说明
|
||||||
|
|
||||||
@@ -84,7 +86,7 @@
|
|||||||
|
|
||||||
### 代码高亮主题(article_code_theme)
|
### 代码高亮主题(article_code_theme)
|
||||||
|
|
||||||
WarmIsland 使用 Prism.js 实现代码语法高亮,提供两种主题风格:
|
主题不再加载 Prism.js 运行时与 autoloader,**语法高亮由 Halo 应用市场的代码高亮插件或「API 扩展包」服务端高亮输出**。本项只切换主题自托管的两套 `.token` 配色(`assets/vendor/prism-warm.css` / `prism-cold.css`),不产生任何远程请求:
|
||||||
|
|
||||||
| 值 | 说明 |
|
| 值 | 说明 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -100,7 +102,7 @@ article_code_theme: warm
|
|||||||
```
|
```
|
||||||
|
|
||||||
::: tip
|
::: tip
|
||||||
代码高亮支持多种编程语言,包括 JavaScript、Python、Java、Go、Rust、YAML、JSON、Bash 等。语言检测基于代码块的语言标记自动完成。
|
支持的语言范围取决于所安装的高亮插件(常见的 JavaScript、Python、Java、Go、Rust、YAML、JSON、Bash 等均可覆盖)。语言识别基于代码块的语言标记,建议始终显式标注。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### 目录展开模式(article_toc_expand)
|
### 目录展开模式(article_toc_expand)
|
||||||
@@ -136,7 +138,7 @@ article_related_title: "猜你喜欢"
|
|||||||
| 平台 | 说明 |
|
| 平台 | 说明 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| QQ | 点击后弹出 QQ 分享窗口,自动携带文章标题和链接 |
|
| QQ | 点击后弹出 QQ 分享窗口,自动携带文章标题和链接 |
|
||||||
| 微信 | 扫码分享到微信(生成文章完整 URL 的二维码) |
|
| 微信 | 弹出文章链接并提供「复制链接」按钮,由访客在微信内分享(不加载二维码库) |
|
||||||
| 微博 | 分享到新浪微博 |
|
| 微博 | 分享到新浪微博 |
|
||||||
| X (Twitter) | 分享到 X |
|
| X (Twitter) | 分享到 X |
|
||||||
| Facebook | 分享到 Facebook |
|
| Facebook | 分享到 Facebook |
|
||||||
@@ -157,7 +159,7 @@ article_related_title: "猜你喜欢"
|
|||||||
| Telegram | ❌ |
|
| Telegram | ❌ |
|
||||||
|
|
||||||
::: info 图标来源
|
::: info 图标来源
|
||||||
分享按钮图标来自 [Font Awesome](https://fontawesome.com/)(Free v7.2.0,CC BY 4.0 / SIL OFL 1.1 协议)和 [Simple Icons](https://simpleicons.org/)(CC0 协议)。微信图标点击后会弹出二维码弹窗,用户可使用微信扫码分享。
|
分享按钮图标来自 [Font Awesome](https://fontawesome.com/)(Free v7.2.0,CC BY 4.0 / SIL OFL 1.1 协议)和 [Simple Icons](https://simpleicons.org/)(CC0 协议)。微信图标点击后会弹出「复制链接」弹层,用户复制后可在微信内分享;二维码由远程库生成的旧实现已移除,主题不再加载任何第三方脚本。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
#### 分享区域标题(article_share_title)
|
#### 分享区域标题(article_share_title)
|
||||||
@@ -174,7 +176,7 @@ article_share_channels:
|
|||||||
```
|
```
|
||||||
|
|
||||||
::: tip
|
::: tip
|
||||||
分享功能会自动携带文章标题和完整链接,无需手动配置分享内容。微信分享通过二维码实现,二维码内容为文章的完整 URL(包含域名),用户扫码后可直接打开文章。QQ 分享通过弹窗方式打开 QQ 分享页面。移动端会优先显示适合移动端的分享方式。
|
分享功能会自动携带文章标题和完整链接,无需手动配置分享内容。微信分享通过「复制链接」实现:弹层显示文章完整 URL(含域名),点击按钮写入剪贴板,全程在浏览器本地完成、不发送外部请求。QQ 分享通过弹窗方式打开 QQ 分享页面。除访客主动点击外,页面加载时不会访问任何第三方平台。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## 阅读增强功能
|
## 阅读增强功能
|
||||||
|
|||||||
+2
-17
@@ -13,7 +13,6 @@
|
|||||||
| `label_search` | 文本 | `搜索` | 搜索按钮的标签文案 |
|
| `label_search` | 文本 | `搜索` | 搜索按钮的标签文案 |
|
||||||
| `label_theme_switch` | 文本 | `切换主题` | 主题切换按钮的标签文案 |
|
| `label_theme_switch` | 文本 | `切换主题` | 主题切换按钮的标签文案 |
|
||||||
| `label_archives_title` | 文本 | `归档` | 归档页面的标题文案 |
|
| `label_archives_title` | 文本 | `归档` | 归档页面的标题文案 |
|
||||||
| `language` | 下拉 | `auto` | 主题界面语言 |
|
|
||||||
|
|
||||||
## 详细说明
|
## 详细说明
|
||||||
|
|
||||||
@@ -70,22 +69,8 @@ label_archives_title: "Archives"
|
|||||||
```
|
```
|
||||||
|
|
||||||
::: tip
|
::: tip
|
||||||
标签文案的修改会即时生效,无需重启 Halo。如果使用多语言场景,可以配合 Halo 的多语言插件实现更完整的国际化。
|
标签文案的修改会即时生效,无需重启 Halo。主题**不内置界面语言切换 / 多语言包**,
|
||||||
:::
|
需要多语言站点时请使用 Halo 或相应插件提供的能力;主题内置的少量界面文案可通过上方 3 个标签项自定义。
|
||||||
|
|
||||||
### 语言(language)
|
|
||||||
|
|
||||||
设置主题界面的显示语言,影响导航栏、文章页、错误页等所有主题内置文案。提供以下选项:
|
|
||||||
|
|
||||||
| 值 | 说明 |
|
|
||||||
| --- | --- |
|
|
||||||
| `auto` | 跟随浏览器语言自动匹配(默认) |
|
|
||||||
| `zh_CN` | 简体中文 |
|
|
||||||
| `zh_TW` | 繁體中文 |
|
|
||||||
| `en` | English |
|
|
||||||
|
|
||||||
::: tip
|
|
||||||
选择 `auto` 时,主题会根据访客浏览器的语言设置自动匹配最接近的语言。如果浏览器语言不在支持列表中,将回退到简体中文。
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## 相关页面
|
## 相关页面
|
||||||
|
|||||||
@@ -12,6 +12,12 @@
|
|||||||
| `footer_show_powered` | 开关 | `true` | 显示「Powered by Halo」 |
|
| `footer_show_powered` | 开关 | `true` | 显示「Powered by Halo」 |
|
||||||
| `footer_show_theme` | 开关 | `true` | 显示主题版本 |
|
| `footer_show_theme` | 开关 | `true` | 显示主题版本 |
|
||||||
| `footer_custom_html` | 代码 | — | 自定义 HTML(统计代码等) |
|
| `footer_custom_html` | 代码 | — | 自定义 HTML(统计代码等) |
|
||||||
|
| `footer_show_stats` | 开关 | `true` | 在页脚显示全站统计 |
|
||||||
|
| `footer_stats_items` | 多选 | `post,category,comment,visit` | 统计项:`post` 文章 / `category` 分类 / `comment` 评论 / `visit` 访问 / `upvote` 点赞 / `word` 总字数(总字数依赖「API 扩展包」,未安装时自动隐藏) |
|
||||||
|
| `footer_show_icp` | 开关 | `false` | 显示 ICP 备案号(开启后 `footer_icp` 才会显示) |
|
||||||
|
| `footer_show_mps` | 开关 | `false` | 显示公网安备号与徽标 |
|
||||||
|
| `footer_mps` | 文本 | — | 公网安备号(需开启上方开关) |
|
||||||
|
| `footer_mps_url` | 文本 | — | 公网安备跳转链接,留空默认 `https://beian.mps.gov.cn/` |
|
||||||
|
|
||||||
## 详细说明
|
## 详细说明
|
||||||
|
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
# 朋友圈
|
|
||||||
|
|
||||||
朋友圈模块用于聚合展示你订阅的站点最新动态,类似于 RSS 阅读器的社交化呈现。访客可以在你的站点上浏览友站最新文章,发现更多优质内容。
|
|
||||||
|
|
||||||
## 配置项一览
|
|
||||||
|
|
||||||
| 配置项 | 类型 | 默认值 | 说明 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `friends_page_title` | 文本 | `朋友圈` | 页面标题 |
|
|
||||||
|
|
||||||
## 前置依赖
|
|
||||||
|
|
||||||
::: danger
|
|
||||||
朋友圈功能依赖 Halo 的 **plugin-friends** 插件。如果未安装此插件,朋友圈页面将无法正常工作。
|
|
||||||
|
|
||||||
安装方式:Halo 后台 → **插件** → **安装** → 搜索「朋友圈」或从 [GitHub Releases](https://www.halo.run/store/apps/app-yISsV) 下载安装。
|
|
||||||
:::
|
|
||||||
|
|
||||||
## 详细说明
|
|
||||||
|
|
||||||
### 页面标题(friends_page_title)
|
|
||||||
|
|
||||||
朋友圈页面的标题,显示在页面顶部。
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
friends_page_title: "朋友圈" # 默认
|
|
||||||
friends_page_title: "友圈" # 简称
|
|
||||||
friends_page_title: "Friends" # 英文站点
|
|
||||||
```
|
|
||||||
|
|
||||||
## RSS 订阅说明
|
|
||||||
|
|
||||||
朋友圈的核心机制是通过 RSS 订阅聚合友站内容:
|
|
||||||
|
|
||||||
1. 在 Halo 后台的 Friends 插件设置中添加友站的 RSS 订阅地址
|
|
||||||
2. 插件会定期抓取这些 RSS 源的最新文章
|
|
||||||
3. 朋友圈页面按时间倒序展示所有订阅源的最新文章
|
|
||||||
|
|
||||||
### 添加订阅源
|
|
||||||
|
|
||||||
1. 登录 Halo 后台管理控制台
|
|
||||||
2. 进入 **链接** 管理页面
|
|
||||||
3. 点击 **新建** 或 **编辑** 填写:元数据 **RSS 地址** 友站的 RSS 订阅地址
|
|
||||||
4. 保存后,插件会在下次抓取周期自动拉取内容,亦可进入 **朋友圈** 自行同步
|
|
||||||
|
|
||||||
::: tip
|
|
||||||
大多数博客平台都提供 RSS 订阅功能。常见的 RSS 地址格式:
|
|
||||||
- WordPress:`/feed/`
|
|
||||||
- Halo:`/rss.xml` 或 `/feed/`
|
|
||||||
- Hexo:`/atom.xml` 或 `/rss2.xml`
|
|
||||||
- Typecho:`/feed/`
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: warning
|
|
||||||
RSS 抓取频率取决于插件配置,通常为每 30 分钟到数小时一次。新添加的订阅源可能需要等待一个抓取周期才能在页面显示内容。
|
|
||||||
:::
|
|
||||||
|
|
||||||
## 相关页面
|
|
||||||
|
|
||||||
- [友情链接](/config/links) — 友情链接页面配置
|
|
||||||
- [页脚](/config/footer) — 页脚社交链接配置
|
|
||||||
- [朋友圈页面](/pages/friends) — 朋友圈模板与结构说明
|
|
||||||
+26
-12
@@ -9,9 +9,9 @@ Hero 首屏是 WarmIsland 主题的标志性区域,占据首页首屏的全部
|
|||||||
| `hero_enabled` | 开关 | `true` | 启用 Hero 模块 |
|
| `hero_enabled` | 开关 | `true` | 启用 Hero 模块 |
|
||||||
| `hero_title` | 文本 | — | 主标题(留空则使用站点标题) |
|
| `hero_title` | 文本 | — | 主标题(留空则使用站点标题) |
|
||||||
| `hero_subtitle` | 文本 | — | 副标题(留空则使用站点副标题) |
|
| `hero_subtitle` | 文本 | — | 副标题(留空则使用站点副标题) |
|
||||||
| `hero_description_mode` | 下拉 | `hitokoto` | 描述文案来源:`none` / `hitokoto` / `custom` |
|
| `hero_description_mode` | 下拉 | `none` | 描述文案来源:`none` / `hitokoto` / `custom`,默认不显示描述文案 |
|
||||||
| `hero_hitokoto_api` | URL | `https://v1.hitokoto.cn/` | 一言 API 地址 |
|
| `hero_hitokoto_api` | URL | `https://v1.hitokoto.cn/` | 默认预填官方接口(描述来源非「无」时显示,仅「一言」模式生效);清空则使用内置本地句子库(零外部请求) |
|
||||||
| `hero_hitokoto_categories` | 多选 | `d, i, k` | 一言句子类型 |
|
| `hero_hitokoto_categories` | 多选 | `d, i, k` | 句子类型筛选,仅对自定义 API 生效 |
|
||||||
| `hero_custom_description` | 多行文本 | — | 自定义描述文案 |
|
| `hero_custom_description` | 多行文本 | — | 自定义描述文案 |
|
||||||
| `hero_hitokoto_auto_refresh` | 开关 | `false` | 一言自动刷新 |
|
| `hero_hitokoto_auto_refresh` | 开关 | `false` | 一言自动刷新 |
|
||||||
| `hero_hitokoto_refresh_interval` | 下拉 | `10000` | 一言刷新间隔(秒) |
|
| `hero_hitokoto_refresh_interval` | 下拉 | `10000` | 一言刷新间隔(秒) |
|
||||||
@@ -44,29 +44,43 @@ hero_subtitle: "记录温暖与灵感"
|
|||||||
|
|
||||||
### 描述文案来源(hero_description_mode)
|
### 描述文案来源(hero_description_mode)
|
||||||
|
|
||||||
控制 Hero 区域主标题下方显示的描述文案,提供三种模式:
|
控制 Hero 区域主标题下方显示的描述文案,提供三种模式,**默认为「无」**:
|
||||||
|
|
||||||
| 值 | 说明 |
|
| 值 | 说明 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `none` | 不显示描述文案 |
|
| `none` | **默认**。不显示描述文案,不渲染一言元素,也不发起任何请求 |
|
||||||
| `hitokoto` | 从一言 API 实时获取随机句子(默认) |
|
| `hitokoto` | 显示一句话文案;API 字段默认预填官方接口 `https://v1.hitokoto.cn/`,清空该字段则使用内置本地句子库 |
|
||||||
| `custom` | 显示自定义的描述文案 |
|
| `custom` | 显示自定义的描述文案(静态文本,不发起任何请求) |
|
||||||
|
|
||||||
### 一言模式(hitokoto)
|
### 一言模式(hitokoto)
|
||||||
|
|
||||||
选择 `hitokoto` 模式后,Hero 区域会从一言 API 获取随机句子,每次刷新页面都会展示不同的内容。
|
默认 `none` 时一言元素不渲染。「自定义一言 API」设置项在描述来源非「无」时显示,并默认预填
|
||||||
|
`https://v1.hitokoto.cn/`;切换到 `hitokoto` 后该值生效,浏览器会向该地址发起 GET 请求
|
||||||
|
获取句子(第三方可见访客 IP / User-Agent)。
|
||||||
|
若把该字段清空,则回退到主题内置的本地句子库 `assets/data/hitokoto.json`
|
||||||
|
(随主题自托管的静态 JSON,零外部请求),每次刷新随机展示一条,也不会因为第三方接口不可用而出现空白。
|
||||||
|
可直接编辑该 JSON 替换成自己喜欢的句子。
|
||||||
|
|
||||||
#### 一言 API 地址(hero_hitokoto_api)
|
#### 自定义一言 API(hero_hitokoto_api)
|
||||||
|
|
||||||
默认使用官方 API `https://v1.hitokoto.cn/`。如果你部署了一言的私有实例,可以修改为自建 API 地址。
|
该设置项在「描述文案来源」非「无」时显示,`help` 只说明候选地址与留空回退规则;
|
||||||
|
字段默认预填 `https://v1.hitokoto.cn/`,**仅在「一言」模式下生效**。
|
||||||
|
**选择「一言」或「自定义」后若把该字段留空,则使用主题内置的本地句子库(零外部请求)**。
|
||||||
|
只有填写后才会向该地址发起请求,第三方可见访客 IP / User-Agent;完整的数据流向与隐私披露
|
||||||
|
见 README「外部资源与隐私」章节。也可换成自建实例或其他兼容接口。
|
||||||
|
|
||||||
::: tip
|
::: tip
|
||||||
自建一言 API 可以提升加载速度并避免第三方服务不稳定的问题。部署方式请参考 [hitokoto 官方文档](https://developer.hitokoto.cn/)。
|
「描述文案来源」默认「无」时一言元素不渲染,因此即使字段预填了官方地址,
|
||||||
|
也不会产生任何外部请求。隐私披露统一在 README 与本文档说明,设置界面不重复展示。
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
这是站点管理员主动开启的可选能力。把描述文案来源改回「无」、或清空该字段即可随时停止外部请求,无需重装主题。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
#### 一言句子类型(hero_hitokoto_categories)
|
#### 一言句子类型(hero_hitokoto_categories)
|
||||||
|
|
||||||
通过勾选来筛选获取的句子类型,支持多选:
|
仅在填写了自定义 API 时生效,用于筛选该接口返回的句子类型,支持多选:
|
||||||
|
|
||||||
| 值 | 类型 | 说明 |
|
| 值 | 类型 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
|||||||
@@ -6,8 +6,12 @@
|
|||||||
|
|
||||||
| 配置项 | 类型 | 默认值 | 说明 |
|
| 配置项 | 类型 | 默认值 | 说明 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
|
| `links_apply_form_enabled` | 开关 | `true` | 页底显示友链申请表单;需插件开启「友链申请」,否则自动隐藏 |
|
||||||
| `links_page_title` | 文本 | `友情链接` | 页面标题 |
|
| `links_page_title` | 文本 | `友情链接` | 页面标题 |
|
||||||
| `links_style` | 下拉 | `cards` | 展示样式:`grid` / `list` / `cards` |
|
| `links_style` | 下拉 | `cards` | 展示样式:`grid` / `list` / `cards` |
|
||||||
|
| `links_show_comment` | 开关 | `false` | 页面底部显示评论区 |
|
||||||
|
| `links_random_order` | 开关 | `false` | 友链卡片随机排列 |
|
||||||
|
| `links_random_portal` | 开关 | `true` | 页面右上角显示「随机传送」按钮 |
|
||||||
|
|
||||||
## 前置依赖
|
## 前置依赖
|
||||||
|
|
||||||
@@ -15,6 +19,9 @@
|
|||||||
友情链接功能依赖 Halo 的 **PluginLinks** 插件。如果未安装此插件,友情链接页面将无法正常工作。
|
友情链接功能依赖 Halo 的 **PluginLinks** 插件。如果未安装此插件,友情链接页面将无法正常工作。
|
||||||
|
|
||||||
安装方式:Halo 后台 → **插件** → **安装** → 搜索「链接管理」→ 安装并启用。
|
安装方式:Halo 后台 → **插件** → **安装** → 搜索「链接管理」→ 安装并启用。
|
||||||
|
|
||||||
|
同一个插件还提供 **RSS 订阅能力**(原「朋友圈」插件的 feed 聚合功能已由它取代),
|
||||||
|
对应主题的[订阅配置](/config/subscriptions)与[订阅页面](/pages/subscriptions)。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## 详细说明
|
## 详细说明
|
||||||
@@ -65,6 +72,7 @@ PluginLinks 支持链接分组功能。你可以将友链按不同类别分组
|
|||||||
|
|
||||||
## 相关页面
|
## 相关页面
|
||||||
|
|
||||||
- [朋友圈](/config/friends) — 朋友圈页面配置
|
- [订阅配置](/config/subscriptions) — 同一插件提供的 RSS 订阅动态
|
||||||
|
- [订阅页面](/pages/subscriptions) — 博友文章动态页面
|
||||||
- [页脚](/config/footer) — 页脚社交链接配置
|
- [页脚](/config/footer) — 页脚社交链接配置
|
||||||
- [友情链接页面](/pages/links) — 友链模板与结构说明
|
- [友情链接页面](/pages/links) — 友链模板与结构说明
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
| `moments_style` | 下拉 | `cards` | 展示样式:`cards` / `masonry` |
|
| `moments_style` | 下拉 | `cards` | 展示样式:`cards` / `masonry` |
|
||||||
| `moments_loading_mode` | 下拉 | `infinite_scroll` | 加载方式:`infinite_scroll` / `pagination` |
|
| `moments_loading_mode` | 下拉 | `infinite_scroll` | 加载方式:`infinite_scroll` / `pagination` |
|
||||||
| `moments_page_size` | 数字 | `10` | 每页显示数量 |
|
| `moments_page_size` | 数字 | `10` | 每页显示数量 |
|
||||||
|
| `moments_cover` | 附件 | — | 瞬间页顶部封面图,建议尺寸 1500×500 |
|
||||||
|
|
||||||
## 前置依赖
|
## 前置依赖
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
| `navbar_style` | 下拉 | `glass` | 导航栏样式:`glass` / `minimal` / `float` |
|
| `navbar_style` | 下拉 | `glass` | 导航栏样式:`glass` / `minimal` / `float` |
|
||||||
| `navbar_show_search` | 开关 | `true` | 显示搜索按钮 |
|
| `navbar_show_search` | 开关 | `true` | 显示搜索按钮 |
|
||||||
| `navbar_show_theme_switch` | 开关 | `true` | 显示主题切换按钮 |
|
| `navbar_show_theme_switch` | 开关 | `true` | 显示主题切换按钮 |
|
||||||
|
| `navbar_brand_type` | 下拉 | `title` | 左侧显示:`title` 站点名称 / `logo` Logo |
|
||||||
|
| `navbar_logo` | 附件 | — | 导航栏 Logo;仅在「Logo」模式下显示,留空回退到基础设置的自定义 Logo |
|
||||||
|
| `navbar_logo_radius` | 下拉 | `rounded` | Logo 圆角:`square` / `rounded` / `circle`(Logo 模式下生效) |
|
||||||
|
|
||||||
## 详细说明
|
## 详细说明
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
| `animation_enabled` | 开关 | `true` | 全局动画总开关 |
|
| `animation_enabled` | 开关 | `true` | 全局动画总开关 |
|
||||||
| `animation_breath` | 开关 | `true` | 呼吸动画效果 |
|
| `animation_breath` | 开关 | `true` | 呼吸动画效果 |
|
||||||
| `animation_scroll_reveal` | 开关 | `true` | 滚动渐入动画 |
|
| `animation_scroll_reveal` | 开关 | `true` | 滚动渐入动画 |
|
||||||
| `animation_cursor_glow` | 开关 | `false` | 光感跟随效果 |
|
| `click_effect_enabled` | 开关 | `false` | 鼠标点击效果总开关 |
|
||||||
|
| `click_effect_type` | 下拉 | `values` | 点击效果类型(8 种效果 + 关闭,仅在总开关开启时显示) |
|
||||||
| `custom_css` | 代码 | — | 自定义 CSS 代码 |
|
| `custom_css` | 代码 | — | 自定义 CSS 代码 |
|
||||||
|
|
||||||
## 详细说明
|
## 详细说明
|
||||||
@@ -121,13 +122,11 @@ WarmIsland 提供了丰富的动画效果,所有动画都可以独立开关:
|
|||||||
滚动渐入动画基于 Intersection Observer API 实现,对性能影响极小。如果页面内容较多,建议保持开启以提升浏览体验。
|
滚动渐入动画基于 Intersection Observer API 实现,对性能影响极小。如果页面内容较多,建议保持开启以提升浏览体验。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
#### 光感跟随(animation_cursor_glow)
|
#### 鼠标点击效果(click_effect_enabled / click_effect_type)
|
||||||
|
|
||||||
启用后,鼠标移动时会在光标附近产生柔和的光晕跟随效果,为页面增添灵动感。
|
`click_effect_enabled` 默认关闭;开启后 `click_effect_type` 才会显示,可选 8 种效果:
|
||||||
|
社会主义核心价值观、随机彩色爱心、烟花、烟花波纹、粒子、星星、星星拖尾、粒子拖尾,
|
||||||
::: warning
|
另可选「关闭」。特效在访客点击时由浏览器本地绘制,不加载远程脚本、不发送任何数据。
|
||||||
光感跟随效果在移动端不会生效(因为移动端没有鼠标光标)。在低端设备上可能会影响渲染性能,如遇卡顿建议关闭。
|
|
||||||
:::
|
|
||||||
|
|
||||||
### 自定义 CSS(custom_css)
|
### 自定义 CSS(custom_css)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# 订阅
|
||||||
|
|
||||||
|
订阅模块用于展示博友们的最新文章动态(feed / RSS 聚合)。这项能力由 **Halo 官方「链接管理」(PluginLinks)插件的订阅**提供,主题只负责把插件抓取到的订阅动态渲染成页面。
|
||||||
|
|
||||||
|
> `plugin-friends`(朋友圈)插件已不再提供,原来的 feed 聚合功能由链接管理插件的订阅取代。
|
||||||
|
> 主题中的 `friends.html` 模板与「朋友圈」设置分组仅用于兼容旧站点,新站点请使用订阅页面模板,
|
||||||
|
> 详见[历史兼容说明](#历史兼容说明)。
|
||||||
|
|
||||||
|
## 配置项一览
|
||||||
|
|
||||||
|
| 配置项 | 类型 | 默认值 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `subscriptions_page_title` | 文本 | `订阅` | 页面标题 |
|
||||||
|
| `subscriptions_subtitle` | 文本 | `博友们的最新文章` | 页面副标题 |
|
||||||
|
| `subscriptions_page_size` | 数字 | `20` | 每页展示的动态条数,范围 1–100 |
|
||||||
|
| `subscriptions_loading_mode` | 下拉 | `pagination` | `pagination` 分页 / `infinite_scroll` 下滑加载 |
|
||||||
|
| `subscriptions_layout` | 下拉 | `list` | `list` 列表卡片 / `grid` 网格卡片 |
|
||||||
|
|
||||||
|
## 前置依赖
|
||||||
|
|
||||||
|
::: danger
|
||||||
|
订阅页面依赖 Halo 官方 **「链接管理」(PluginLinks)** 插件的 RSS 订阅能力,
|
||||||
|
可在[应用市场](https://www.halo.run/store/apps/app-hfbQg)安装。未安装或未启用订阅时,
|
||||||
|
页面会显示空状态提示,不会报错。
|
||||||
|
:::
|
||||||
|
|
||||||
|
订阅源由链接管理插件维护(为博友链接配置 RSS 订阅地址、按插件自身的周期抓取),
|
||||||
|
主题通过 Halo 的 `linkFeedFinder` 读取已抓取的动态列表,不自行抓取、不缓存、不向第三方发起请求。
|
||||||
|
订阅源的添加入口与抓取频率以所安装插件版本的说明为准。
|
||||||
|
|
||||||
|
## 详细说明
|
||||||
|
|
||||||
|
### 加载方式(subscriptions_loading_mode)
|
||||||
|
|
||||||
|
- `pagination`:页面底部显示「较新 / 较旧」翻页链接,按时间游标分页。
|
||||||
|
- `infinite_scroll`:滚动到列表底部时自动加载下一页,并显示加载状态与「没有更多了」提示。
|
||||||
|
|
||||||
|
### 布局样式(subscriptions_layout)
|
||||||
|
|
||||||
|
- `list`:单列宽卡片,默认布局,适合长摘要阅读。
|
||||||
|
- `grid`:一行三列(窄屏自动降为两列 / 一列),卡片内容垂直排列。
|
||||||
|
|
||||||
|
两种方式下每条动态都展示作者头像 / 名称、发布日期、文章标题与摘要,标题与作者链接均带
|
||||||
|
`rel="noopener noreferrer"`;作者名为空时会显示占位字符,不会因字符串截取报错。
|
||||||
|
|
||||||
|
## 历史兼容说明
|
||||||
|
|
||||||
|
主题仍保留 `templates/friends.html` 与设置中的「朋友圈」分组(`friends_page_title`),
|
||||||
|
它们只读取旧 `plugin-friends` 插件的数据。该插件已不再提供,因此:
|
||||||
|
|
||||||
|
- 新站点**不要**再创建 `/friends` 页面或依赖这个分组;
|
||||||
|
- 旧站点若已停用该插件,`friends.html` 会显示空状态而不会报错;
|
||||||
|
- 需要 feed 聚合时,请改用 `page_subscriptions.html` 订阅模板。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [订阅页面](/pages/subscriptions) — 页面结构、模板选择与迁移说明
|
||||||
|
- [友情链接配置](/config/links) — 同一插件的友链展示配置
|
||||||
|
- [自定义页面](/pages/custom-page) — 全部自定义页面模板
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# 欢迎弹窗
|
||||||
|
|
||||||
|
欢迎弹窗在首页以浮层形式向访客打招呼。**默认关闭**,开启后也只显示主题内置的本地静态欢迎语,
|
||||||
|
不会自动向任何第三方发起请求。
|
||||||
|
|
||||||
|
## 配置项一览
|
||||||
|
|
||||||
|
| 配置项 | 类型 | 默认值 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `welcome_popup_enabled` | 开关 | `false` | 启用欢迎弹窗 |
|
||||||
|
| `welcome_popup_position` | 下拉 | `top-center` | 弹窗位置,共 5 种 |
|
||||||
|
| `welcome_popup_duration` | 数字 | `5` | 自动消失时间(秒),范围 3–60 |
|
||||||
|
| `welcome_popup_show_once` | 开关 | `true` | 同一浏览器会话内仅显示一次 |
|
||||||
|
| `welcome_popup_api` | URL | 留空 | 自定义 IP 定位主 API(可选) |
|
||||||
|
| `welcome_popup_fallback_api` | URL | 留空 | 自定义 IP 定位备选 API(可选) |
|
||||||
|
|
||||||
|
## 弹窗位置
|
||||||
|
|
||||||
|
| 值 | 位置 |
|
||||||
|
| --- | --- |
|
||||||
|
| `top-center` | 顶端居中(默认) |
|
||||||
|
| `top-left` / `top-right` | 左上 / 右上 |
|
||||||
|
| `bottom-left` / `bottom-right` | 左下 / 右下 |
|
||||||
|
|
||||||
|
`welcome_popup_show_once` 使用浏览器 `sessionStorage` 记录同会话已显示状态;
|
||||||
|
关闭该开关后每次访问首页都会弹出。清除浏览器站点数据即可重置。
|
||||||
|
|
||||||
|
## 可选的 IP 定位能力(默认关闭)
|
||||||
|
|
||||||
|
两个 API 字段仅在「启用欢迎弹窗」开关打开时显示,`help` 只说明候选地址与留空规则;
|
||||||
|
第三方数据流向与隐私披露由本文档下方条目及 README「外部资源与隐私」章节承载。
|
||||||
|
字段**默认留空**,此时弹窗只显示本地欢迎语,不发起任何外部请求。
|
||||||
|
只有管理员显式填写后,浏览器才会向该地址请求并把**访客 IP**暴露给第三方服务以换取粗略地理位置:
|
||||||
|
|
||||||
|
- 候选主 / 备选接口:`https://myip.ipip.net/json`、`https://r.inews.qq.com/api/ip2city?otype=json`
|
||||||
|
- 数据流向与隐私披露要求见 README「外部资源与隐私」章节
|
||||||
|
- 清空字段或关闭弹窗即可立即停止,无需重装主题
|
||||||
|
|
||||||
|
> 主题不预置、不代理这些接口,也不保存访客位置数据;填写前请自行评估服务商的隐私政策。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [基础设置](/config/basic) — 站点名称、Logo 等
|
||||||
|
- [首页](/pages/home) — 首页模块说明
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# 微信读书
|
||||||
|
|
||||||
|
微信读书模块用于展示书架与阅读记录,主题提供 `weread.html` 自定义页面模板,
|
||||||
|
数据由[微信读书插件](https://github.com/yeora26/halo-weread-plugin)提供,访客无需登录即可查看。
|
||||||
|
|
||||||
|
::: info
|
||||||
|
该页面依赖 Halo **2.27+** 的能力;主题声明的兼容范围为 `>=2.27.0`。
|
||||||
|
:::
|
||||||
|
|
||||||
|
## 配置项一览
|
||||||
|
|
||||||
|
| 配置项 | 类型 | 默认值 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `weread_page_title` | 文本 | — | 页面标题(顶部标题与浏览器标签页标题);插件自带的标题区会自动隐藏以避免重复 |
|
||||||
|
| `weread_page_subtitle` | 文本 | — | 页面副标题 |
|
||||||
|
|
||||||
|
留空时由模板回退到默认文案,不会导致页面报错。
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
1. 安装并启用微信读书插件
|
||||||
|
2. Halo 后台 → **页面** → **新建** → 自定义模板选择「**微信读书**」(`WeRead`)
|
||||||
|
3. 发布页面并加入导航菜单
|
||||||
|
|
||||||
|
页面地址由创建页面时设定的 slug 决定,主题不提供固定路由。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [自定义页面](/pages/custom-page) — 全部自定义页面模板
|
||||||
|
- [扩展页面](/pages/extended) — 插件前台页面一览
|
||||||
@@ -54,18 +54,36 @@ pnpm build
|
|||||||
### 打包主题
|
### 打包主题
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm release # 推荐:build → snapshot → package
|
pnpm release # 推荐:build → snapshot → package(含合规审计)
|
||||||
# 或分步:pnpm build && pnpm snapshot && pnpm package
|
# 或分步:pnpm build && pnpm snapshot && pnpm audit:external && pnpm package
|
||||||
```
|
```
|
||||||
|
|
||||||
生成 `dist/warm-island-<version>.zip`,可直接在 Halo 后台上传安装。
|
生成 `dist/warm-island-<version>.zip`,可直接在 Halo 后台上传安装。
|
||||||
ZIP 根目录直接放 `theme.yaml` / `settings.yaml` / `templates/`,不要额外套一层目录。
|
ZIP 根目录直接放 `theme.yaml` / `settings.yaml` / `templates/`,不要额外套一层目录;
|
||||||
|
`docs/*.md` 与预览图也会一并打包,保证上架说明里的引用在制品中真实存在。
|
||||||
|
|
||||||
> **`templates/` 被 `.gitignore` 忽略**,克隆仓库后默认不存在。若跳过 `pnpm build`
|
> **`templates/` 被 `.gitignore` 忽略**,克隆仓库后默认不存在。若跳过 `pnpm build`
|
||||||
> 直接 `pnpm package`,过去会产出不含任何模板的空壳包,上传后 Halo 报
|
> 直接 `pnpm package`,过去会产出不含任何模板的空壳包,上传后 Halo 报
|
||||||
> `Template index was not found.` 并让首页 404。现在打包脚本会先校验
|
> `Template index was not found.` 并让首页 404。现在打包脚本会先校验
|
||||||
> `index.html` 等模板、资源目录与版本号,不满足即报错退出。
|
> `index.html` 等模板、资源目录与版本号,不满足即报错退出。
|
||||||
|
|
||||||
|
### 上架合规审计
|
||||||
|
|
||||||
|
`scripts/audit-external.mjs`(`pnpm audit:external`)在打包前自动执行,对应应用市场审核指南
|
||||||
|
1.4 / 4.4.4 / 4.6 / 5.1,检查:
|
||||||
|
|
||||||
|
- 模板中不含第三方 `preconnect` / `dns-prefetch` / `preload`
|
||||||
|
- 不含 `src` / `href` 指向 http(s) 的远程脚本、样式、图片、媒体
|
||||||
|
- 不含硬编码的外部 `fetch()` / 动态 `import()` / CSS `url(https://...)`,
|
||||||
|
也不含 Prism autoloader 之类的动态加载痕迹与被移除的服务域名
|
||||||
|
- README 中的相对链接都能在安装包清单里找到对应文件
|
||||||
|
- `templates/logo.png` 不存在(只允许一份压缩后的 `templates/assets/logo.png`)
|
||||||
|
|
||||||
|
任一不通过,`pnpm package` 直接 `exit 1`,避免打出不合规制品。
|
||||||
|
`<a href="https://...">` 这类需要访客主动点击的外链是允许的,但必须在 README
|
||||||
|
「外部资源与隐私」章节逐项披露。新增第三方资源时,请先自托管到 `public/assets/`
|
||||||
|
并在 README 的许可表中登记来源与协议。
|
||||||
|
|
||||||
## 性能与滚动顺滑
|
## 性能与滚动顺滑
|
||||||
|
|
||||||
首页滚动曾经卡顿,根因不是 DOM 数量,而是「每帧高成本绘制 + 劫持滚动输入」:
|
首页滚动曾经卡顿,根因不是 DOM 数量,而是「每帧高成本绘制 + 劫持滚动输入」:
|
||||||
@@ -98,60 +116,36 @@ ZIP 根目录直接放 `theme.yaml` / `settings.yaml` / `templates/`,不要额
|
|||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
|
以下按磁盘实际文件列出(`src/pages/` 下每个 `.astro` 对应一个输出模板,共 21 个):
|
||||||
|
|
||||||
```
|
```
|
||||||
WarmIsland/
|
WarmIsland/
|
||||||
├── src/ # 源代码
|
├── src/
|
||||||
│ ├── components/ # Astro 组件
|
│ ├── components/ # 18 个组件:Navbar / Footer / HeroSection / WelcomePopup /
|
||||||
│ │ ├── HeroSection.astro # Hero 首屏
|
│ │ # LightGallery / ClickEffect / ScrollReveal / ThemeSwitcher 等
|
||||||
│ │ ├── PostCard.astro # 文章卡片
|
│ ├── layouts/ # Layout.astro(含页面布局契约渲染模式)
|
||||||
│ │ ├── Pagination.astro # 分页组件
|
│ ├── pages/ # index / post / page / archives / categories / category /
|
||||||
│ │ ├── TableOfContents.astro # 目录组件
|
│ │ # tags / tag / links / friends(历史兼容)/ moments / moment / photos /
|
||||||
│ │ ├── ReadingProgress.astro # 阅读进度条
|
│ │ # douban / equipments / important-dates / steam / weread /
|
||||||
│ │ ├── LikeButton.astro # 点赞按钮
|
│ │ # page_messageboard / page_subscriptions / layout
|
||||||
│ │ ├── CommentSection.astro # 评论区
|
│ └── styles/ # main.scss + 12 个 _*.scss 片段
|
||||||
│ │ ├── Footer.astro # 页脚
|
├── public/
|
||||||
│ │ └── ...
|
│ ├── assets/
|
||||||
│ ├── layouts/ # 布局组件
|
│ │ ├── data/ # hitokoto.json 内置本地句子库
|
||||||
│ │ ├── BaseLayout.astro # 基础布局
|
│ │ ├── fonts/ # LXGW WenKai 本地 woff2 分片与 result.css
|
||||||
│ │ └── PostLayout.astro # 文章布局
|
│ │ ├── vendor/ # 自托管 prism-warm.css / prism-cold.css
|
||||||
│ ├── lib/ # 工具库
|
│ │ └── logo.png # 主题图标(唯一一份,256×256 压缩版)
|
||||||
│ │ ├── pages.ts # 页面路由定义
|
│ ├── error/ # 错误页静态资源
|
||||||
│ │ └── utils.ts # 工具函数
|
│ └── favicon.ico
|
||||||
│ ├── pages/ # 页面入口
|
├── compat/assets/ # 历史版本哈希静态资源,构建时合并进 templates/assets
|
||||||
│ │ ├── index.astro # 首页
|
├── scripts/ # postbuild / snapshot-assets / package / audit-external / smoke-clickfx
|
||||||
│ │ ├── post.astro # 文章详情
|
├── docs/ # VitePress 文档站(*.md 随安装包分发)
|
||||||
│ │ ├── archives.astro # 归档
|
├── templates/ # 构建产物(被 .gitignore 忽略)
|
||||||
│ │ ├── categories.astro # 分类
|
├── dist/ # ZIP 安装包(被 .gitignore 忽略)
|
||||||
│ │ ├── tags.astro # 标签
|
├── theme.yaml # 主题清单与自定义页面模板注册
|
||||||
│ │ ├── moments.astro # 瞬间
|
├── settings.yaml # 后台设置项定义
|
||||||
│ │ ├── photos.astro # 图库
|
├── astro.config.mjs / package.json / pnpm-lock.yaml / tsconfig.json
|
||||||
│ │ ├── links.astro # 友情链接
|
└── WarmIsland Preview.webp
|
||||||
│ │ ├── friends.astro # 朋友圈
|
|
||||||
│ │ ├── page.astro # 自定义页面
|
|
||||||
│ │ ├── page_messageboard.astro # 留言板
|
|
||||||
│ │ ├── douban.astro # 豆瓣
|
|
||||||
│ │ ├── equipments.astro # 装备
|
|
||||||
│ │ ├── footprints.astro # 足迹
|
|
||||||
│ │ └── steam.astro # Steam
|
|
||||||
│ └── styles/ # 样式文件
|
|
||||||
│ ├── main.scss # 主样式入口
|
|
||||||
│ ├── _variables.scss # SCSS 变量
|
|
||||||
│ ├── _colors.scss # 颜色定义
|
|
||||||
│ ├── _typography.scss # 排版
|
|
||||||
│ ├── _spacing.scss # 间距
|
|
||||||
│ ├── _animations.scss # 动画
|
|
||||||
│ ├── _mixins.scss # 混入
|
|
||||||
│ └── components/ # 组件样式
|
|
||||||
├── templates/ # Thymeleaf 模板(构建输出)
|
|
||||||
├── public/ # 静态资源
|
|
||||||
│ └── assets/ # 编译后的 CSS/JS
|
|
||||||
├── docs/ # 文档
|
|
||||||
├── theme.yaml # 主题元信息
|
|
||||||
├── settings.yaml # 主题设置定义
|
|
||||||
├── astro.config.mjs # Astro 配置
|
|
||||||
├── package.json # 项目依赖
|
|
||||||
├── tsconfig.json # TypeScript 配置
|
|
||||||
└── vite.config.ts # Vite 配置
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
@@ -160,7 +154,7 @@ WarmIsland/
|
|||||||
|
|
||||||
| 技术 | 版本 | 用途 |
|
| 技术 | 版本 | 用途 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| [Halo](https://www.halo.run/) | 2.27+ | 博客运行时,提供 Finder API 与主题渲染 |
|
| [Halo](https://www.halo.run/) | 2.27.0+ | 博客运行时,提供 Finder API 与主题渲染 |
|
||||||
| [Astro](https://astro.build) | 6.x | 组件开发,构建输出静态 `.html` 模板 |
|
| [Astro](https://astro.build) | 6.x | 组件开发,构建输出静态 `.html` 模板 |
|
||||||
| [Thymeleaf](https://www.thymeleaf.org/) | - | Halo 服务端模板引擎(源码里直接写 `th:*`) |
|
| [Thymeleaf](https://www.thymeleaf.org/) | - | Halo 服务端模板引擎(源码里直接写 `th:*`) |
|
||||||
|
|
||||||
@@ -170,7 +164,8 @@ WarmIsland/
|
|||||||
|------|------|
|
|------|------|
|
||||||
| [SCSS](https://sass-lang.com/) | 样式预处理,CSS 变量驱动的配色系统 |
|
| [SCSS](https://sass-lang.com/) | 样式预处理,CSS 变量驱动的配色系统 |
|
||||||
| 原生 JavaScript | 滚动、菜单、主题切换、无限滚动等交互,无前端框架运行时 |
|
| 原生 JavaScript | 滚动、菜单、主题切换、无限滚动等交互,无前端框架运行时 |
|
||||||
| [LightGallery](https://www.lightgalleryjs.com/) | 图片灯箱(可选) |
|
| [LightGallery](https://www.lightgalleryjs.com/) 2.9.0(GPLv3) | 图片灯箱,默认内置;安装 lightgallery.js 插件后自动让位 |
|
||||||
|
| [LXGW WenKai](https://github.com/lxgw/LxgwWenKai)(OFL 1.1) | 本地字体分片,随主题自托管,无远程字体请求 |
|
||||||
| Halo 插件生态 | 评论、代码高亮、服务端字数统计等由 Halo 与可选插件提供 |
|
| Halo 插件生态 | 评论、代码高亮、服务端字数统计等由 Halo 与可选插件提供 |
|
||||||
|
|
||||||
### 构建工具
|
### 构建工具
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ else if (!stored && prefersDark) { isDark = true; }
|
|||||||
| `animation_enabled` | 启用动效 | `true` |
|
| `animation_enabled` | 启用动效 | `true` |
|
||||||
| `animation_breath` | 呼吸动画 | `true` |
|
| `animation_breath` | 呼吸动画 | `true` |
|
||||||
| `animation_scroll_reveal` | 滚动渐入 | `true` |
|
| `animation_scroll_reveal` | 滚动渐入 | `true` |
|
||||||
| `animation_cursor_glow` | 光感跟随 | `false` |
|
| `click_effect_enabled` | 鼠标点击效果 | `false` |
|
||||||
|
| `click_effect_type` | 点击效果类型(8 种 + 关闭) | `values` |
|
||||||
|
|
||||||
关闭动效时,`html` 元素添加 `wi-no-animations` 类名。
|
关闭动效时,`html` 元素添加 `wi-no-animations` 类名。
|
||||||
+49
-8
@@ -116,15 +116,54 @@ v1.2.1 起安装包会保留各版本的哈希资源(v1.1.7 之后全部覆盖
|
|||||||
| 瞬间 | [PluginMoments](https://www.halo.run/store/apps/app-SnwWD) |
|
| 瞬间 | [PluginMoments](https://www.halo.run/store/apps/app-SnwWD) |
|
||||||
| 图库 | [PluginPhotos](https://www.halo.run/store/apps/app-BmQJW) |
|
| 图库 | [PluginPhotos](https://www.halo.run/store/apps/app-BmQJW) |
|
||||||
| 友链 | [PluginLinks](https://www.halo.run/store/apps/app-hfbQg) |
|
| 友链 | [PluginLinks](https://www.halo.run/store/apps/app-hfbQg) |
|
||||||
| 朋友圈 | [PluginFriends](https://www.halo.run/store/apps/app-yISsV) |
|
| 订阅动态 | 同为 [PluginLinks](https://www.halo.run/store/apps/app-hfbQg) 的 RSS 订阅能力,需选择「订阅」自定义模板 |
|
||||||
|
|
||||||
安装并启用插件后,页面会自动显示内容。未安装插件时页面会显示友好的提示信息。
|
安装并启用插件后,页面会自动显示内容。未安装插件时页面会显示友好的提示信息。
|
||||||
|
|
||||||
### 一言 API 加载失败?
|
**「朋友圈」插件已不再提供**:feed / RSS 聚合已由官方「链接管理」插件的订阅取代,
|
||||||
|
主题保留 `friends.html` 模板与「朋友圈」设置分组仅用于兼容旧站点。新站点请新建页面并选择
|
||||||
|
「订阅」自定义模板,详见 README「自定义页面模板」与[订阅页面](https://nxxy335.top/WarmIsland/pages/subscriptions.html)。
|
||||||
|
|
||||||
1. 检查网络是否能访问 `https://v1.hitokoto.cn/`
|
### 原来的「朋友圈」页面不能用了,怎么迁移?
|
||||||
2. 如果在国内服务器,可能存在网络延迟
|
|
||||||
3. 可以在主题设置中更换一言 API 地址为自建镜像
|
1. 安装 / 启用官方「链接管理」(PluginLinks)插件,把 RSS 订阅源迁到该插件的订阅中
|
||||||
|
2. Halo 后台 → 页面 → 新建 → 自定义模板选择「订阅」(`page_subscriptions.html`)
|
||||||
|
3. 在主题设置 → 订阅中调整每页条数、分页 / 下滑加载与列表 / 网格布局
|
||||||
|
4. 确认动态正常展示后,删除旧的 `/friends` 页面与菜单项
|
||||||
|
|
||||||
|
旧的 `friends.html` 检测不到 `plugin-friends` 时只会显示空状态,不会影响站点其他页面。
|
||||||
|
|
||||||
|
### 主题会向第三方服务发请求吗?
|
||||||
|
|
||||||
|
**默认不会。** 所有脚本、样式、字体、图标、主题 Logo 与一言句子库都随主题自托管,
|
||||||
|
页面不预连接任何 CDN,也不加载远程脚本;Hero「描述文案来源」默认为「无」,欢迎弹窗默认关闭。
|
||||||
|
只有两类例外:
|
||||||
|
|
||||||
|
1. 访客**主动点击**分享按钮 / 备案链接 / 应用市场链接时才跳转到对应站点;
|
||||||
|
2. 站点管理员在主题设置里**显式填写**了「自定义一言 API」或「自定义 IP 定位 API」——
|
||||||
|
后者会把访客 IP 发送给该第三方服务换取地理位置;数据流向与披露要求见
|
||||||
|
README「外部资源与隐私」。
|
||||||
|
|
||||||
|
可选接口地址(仅作候选,主题不会自动请求):
|
||||||
|
|
||||||
|
| 能力 | 可选地址 |
|
||||||
|
| --- | --- |
|
||||||
|
| 一言 | `https://v1.hitokoto.cn/`(默认预填,仅「一言」模式生效) |
|
||||||
|
| IP 定位主 / 备选 API | `https://myip.ipip.net/json`、`https://r.inews.qq.com/api/ip2city?otype=json` |
|
||||||
|
|
||||||
|
完整逐项说明见 README 的「外部资源与隐私」章节。发版前可运行 `pnpm audit:external` 复核。
|
||||||
|
|
||||||
|
### 一言不显示,或想换成自己的句子?
|
||||||
|
|
||||||
|
1. Hero「描述文案来源」**默认为「无」**,此时不显示描述文案、不渲染一言元素,零外部请求;
|
||||||
|
想显示句子请改为「一言」
|
||||||
|
2. 「自定义一言 API」字段在「描述文案来源」非「无」时显示,且默认预填 `https://v1.hitokoto.cn/`,
|
||||||
|
选择「一言」后该值才生效并产生外部请求;
|
||||||
|
**把该字段清空即回退到主题内置的 `assets/data/hitokoto.json` 本地句子库(零外部请求)**,
|
||||||
|
直接编辑该 JSON 即可替换文案。只有填写后才会向该地址发起请求,数据流向与披露要求见
|
||||||
|
README「外部资源与隐私」
|
||||||
|
3. 「自定义」模式显示「自定义描述文案」中的静态文本,不发起任何请求;
|
||||||
|
「句子类型」筛选仅对填写了 API 的一言模式生效
|
||||||
|
|
||||||
### 文章目录不显示?
|
### 文章目录不显示?
|
||||||
|
|
||||||
@@ -135,9 +174,11 @@ v1.2.1 起安装包会保留各版本的哈希资源(v1.1.7 之后全部覆盖
|
|||||||
|
|
||||||
### 代码高亮不生效?
|
### 代码高亮不生效?
|
||||||
|
|
||||||
1. 确认代码块使用了正确的语言标记(如 \`\`\`python)
|
主题**不再自带 Prism.js 运行时**(含 autoloader),因此不会自动给代码块加语法着色:
|
||||||
2. Prism.js 支持自动语言检测,但部分语言需要手动标记
|
|
||||||
3. 检查浏览器控制台是否有 Prism.js 加载错误
|
1. 安装 Halo 应用市场的代码高亮插件,或启用「API 扩展包」的服务端代码高亮
|
||||||
|
2. 主题设置 → 文章 → 「代码高亮主题」仅切换本地自托管的 Warm / Cold `.token` 配色
|
||||||
|
3. 确认代码块使用了正确的语言标记(如 \`\`\`python),以便插件识别语言
|
||||||
|
|
||||||
## 样式相关
|
## 样式相关
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,15 @@ WarmIsland(暖屿)是一款为 [Halo](https://halo.run) 打造的博客主
|
|||||||
- 🌙 **深色模式** — 跟随系统/浅色/深色三种方案,平滑切换
|
- 🌙 **深色模式** — 跟随系统/浅色/深色三种方案,平滑切换
|
||||||
- 📱 **响应式布局** — 桌面到手机完美适配
|
- 📱 **响应式布局** — 桌面到手机完美适配
|
||||||
- 📝 **沉浸阅读** — 侧边目录、进度条、代码高亮、图片灯箱
|
- 📝 **沉浸阅读** — 侧边目录、进度条、代码高亮、图片灯箱
|
||||||
- 🔌 **插件生态** — 瞬间、图库、友链、朋友圈、豆瓣、装备、Steam
|
- 🔌 **插件生态** — 瞬间、图库、友链、订阅动态、豆瓣、装备、Steam
|
||||||
- ⚙️ **灵活配置** — 全维度自定义面板,无需改代码
|
- 🔒 **默认零第三方请求** — 静态资源全部自托管,可选 API 默认留空
|
||||||
|
- ⚙️ **灵活配置** — 17 个分组 / 126 个配置项,无需改代码
|
||||||
|
|
||||||
## 前置要求
|
## 前置要求
|
||||||
|
|
||||||
- Halo >= 2.24.0
|
- **Halo >= 2.27.0**(主题 `theme.yaml` 声明 `requires: ">=2.27.0"`;页面布局契约与微信读书页面依赖 2.27 的能力)
|
||||||
- 已安装并运行 Halo 实例
|
- 已安装并运行 Halo 实例
|
||||||
|
- 如需插件页面(瞬间、图库、友链、订阅等),另需安装对应插件
|
||||||
|
|
||||||
## 安装方式
|
## 安装方式
|
||||||
|
|
||||||
@@ -33,6 +35,11 @@ WarmIsland(暖屿)是一款为 [Halo](https://halo.run) 打造的博客主
|
|||||||
4. 上传下载的 `.zip` 文件
|
4. 上传下载的 `.zip` 文件
|
||||||
5. 点击 **启用** 激活主题
|
5. 点击 **启用** 激活主题
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
从源码自行打包时,必须先执行 `pnpm release`(build → snapshot → package)。
|
||||||
|
只上传源码或漏跑构建的 ZIP 会因缺少 `templates/` 导致首页 404,详见 [FAQ](/faq)。
|
||||||
|
:::
|
||||||
|
|
||||||
## 初始配置
|
## 初始配置
|
||||||
|
|
||||||
安装并启用主题后,建议按以下顺序进行初始配置:
|
安装并启用主题后,建议按以下顺序进行初始配置:
|
||||||
@@ -49,8 +56,8 @@ WarmIsland(暖屿)是一款为 [Halo](https://halo.run) 打造的博客主
|
|||||||
|
|
||||||
进入 **样式设置**:
|
进入 **样式设置**:
|
||||||
|
|
||||||
- 选择 **配色方案**(跟随系统/浅色/深色)
|
- 选择 **配色方案**(跟随系统/浅色/深色)与 **主题配色**(11 套预设)
|
||||||
- 设置 **强调色**(默认 `#d4764e` 暖橘色)
|
- 设置 **强调色**(留空则跟随所选配色,暖屿·赤陶为 `#d4764e`)
|
||||||
- 选择 **圆角风格** 和 **容器宽度**
|
- 选择 **圆角风格** 和 **容器宽度**
|
||||||
|
|
||||||
### 第三步:导航菜单
|
### 第三步:导航菜单
|
||||||
@@ -64,7 +71,7 @@ WarmIsland(暖屿)是一款为 [Halo](https://halo.run) 打造的博客主
|
|||||||
进入 **Hero 首屏** 设置:
|
进入 **Hero 首屏** 设置:
|
||||||
|
|
||||||
- 设置主标题和副标题
|
- 设置主标题和副标题
|
||||||
- 选择描述文案来源(一言/自定义/无)
|
- 选择描述文案来源(默认「无」/ 一言 / 自定义);默认不显示描述、不发起请求;「自定义一言 API」字段在来源非「无」时显示并默认预填 `https://v1.hitokoto.cn/`(仅「一言」模式生效),清空该字段则使用内置本地句子库(零外部请求)
|
||||||
- 可选上传背景图片
|
- 可选上传背景图片
|
||||||
|
|
||||||
### 第五步:页脚信息
|
### 第五步:页脚信息
|
||||||
@@ -75,7 +82,14 @@ WarmIsland(暖屿)是一款为 [Halo](https://halo.run) 打造的博客主
|
|||||||
- 添加社交链接(GitHub、微博等)
|
- 添加社交链接(GitHub、微博等)
|
||||||
- 可选添加自定义 HTML(统计代码等)
|
- 可选添加自定义 HTML(统计代码等)
|
||||||
|
|
||||||
|
### 可选:订阅动态页面
|
||||||
|
|
||||||
|
如需展示博友文章动态,安装官方「链接管理」(PluginLinks)插件并维护 RSS 订阅源,
|
||||||
|
再新建一个页面选择「订阅」自定义模板即可。原先的「朋友圈」插件已不再提供,
|
||||||
|
详见[订阅页面](/pages/subscriptions)。
|
||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
- [基础设置](/config/basic) — 完整的主题配置说明
|
- [基础设置](/config/basic) — 完整的主题配置说明
|
||||||
- [样式设置](/config/style) — 调整配色、圆角、动画
|
- [样式设置](/config/style) — 调整配色、圆角、动画
|
||||||
|
- [订阅配置](/config/subscriptions) — feed 聚合动态设置
|
||||||
+34
-20
@@ -2,10 +2,15 @@
|
|||||||
|
|
||||||
**WarmIsland(暖屿)** 是一款为 [Halo](https://halo.run) 打造的博客主题。暖如灯火,静如留白——它以柔和的色调和舒缓的节奏回归内容本身,为记录与阅读留出一方安静从容的角落。
|
**WarmIsland(暖屿)** 是一款为 [Halo](https://halo.run) 打造的博客主题。暖如灯火,静如留白——它以柔和的色调和舒缓的节奏回归内容本身,为记录与阅读留出一方安静从容的角落。
|
||||||
|
|
||||||
> 本主题由 AI 开发完成。
|
> 本项目使用 AI 辅助开发,产出已由开发者完成代码审查、构建产物核验与 Halo 开发生态适配。
|
||||||
|
|
||||||
它的名字来源于一个画面——深夜里温暖的小岛。我们希望每一个来到你站点的访客,都能感受到那份安静而舒适的氛围。
|
它的名字来源于一个画面——深夜里温暖的小岛。我们希望每一个来到你站点的访客,都能感受到那份安静而舒适的氛围。
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
- Halo **2.27.0 及以上**(`theme.yaml` 的 `requires` 为 `>=2.27.0`):页面布局契约、微信读书页面均依赖 2.27 的能力
|
||||||
|
- v1.2.0 起内置主题配色系统(纯 CSS 变量实现);更低版本的 Halo 请使用主题 v1.1.x
|
||||||
|
|
||||||
## 设计理念
|
## 设计理念
|
||||||
|
|
||||||
暖屿的设计围绕三个核心词展开:**温暖、沉浸、精致**。
|
暖屿的设计围绕三个核心词展开:**温暖、沉浸、精致**。
|
||||||
@@ -28,19 +33,20 @@
|
|||||||
|
|
||||||
- **暖色调设计语言** — 橘色强调色 + 呼吸光球 + 渐变背景
|
- **暖色调设计语言** — 橘色强调色 + 呼吸光球 + 渐变背景
|
||||||
- **三种导航栏样式** — Glass(毛玻璃)/ Minimal(极简)/ Float(浮动胶囊)
|
- **三种导航栏样式** — Glass(毛玻璃)/ Minimal(极简)/ Float(浮动胶囊)
|
||||||
|
- **内置配色系统** — 11 套预设调色板,每套含浅色 / 深色完整色板
|
||||||
- **深色模式** — 跟随系统、浅色、深色三种方案,一键切换平滑过渡
|
- **深色模式** — 跟随系统、浅色、深色三种方案,一键切换平滑过渡
|
||||||
- **动效系统** — 滚动渐入、光感跟随、呼吸动画,均可独立开关
|
- **动效系统** — 滚动渐入、呼吸动画、点击特效,均可独立开关
|
||||||
|
|
||||||
### 📝 阅读体验
|
### 📝 阅读体验
|
||||||
|
|
||||||
- **文章目录** — 桌面端侧边悬浮 + 移动端底部抽屉,支持展开模式选项
|
- **文章目录** — 桌面端侧边悬浮 + 移动端底部抽屉,支持展开模式选项
|
||||||
- **阅读进度条** — 实时显示阅读进度
|
- **阅读进度条** — 实时显示阅读进度
|
||||||
- **代码高亮** — Prism.js 支持,Warm / Cold 双主题
|
- **代码高亮** — 由代码高亮插件 / API 扩展包输出,主题提供 Warm / Cold 本地配色,不加载远程脚本
|
||||||
- **图片灯箱** — 点击放大,支持左右切换
|
- **图片灯箱** — 点击放大,支持左右切换与 EXIF 信息
|
||||||
- **图片懒加载** — blur-up 渐显效果,优化加载体验
|
- **图片懒加载** — blur-up 渐显效果,优化加载体验
|
||||||
- **点赞功能** — 一键点赞,状态持久化
|
- **点赞功能** — 一键点赞,状态持久化
|
||||||
- **相关文章推荐** — 文章底部推荐相关内容
|
- **相关文章推荐** — 文章底部推荐相关内容
|
||||||
- **分享功能** — 微博 / X / Facebook / Telegram / 复制链接
|
- **分享功能** — QQ / 微博 / X / Facebook / Telegram / 复制文章链接
|
||||||
|
|
||||||
### 📱 响应式
|
### 📱 响应式
|
||||||
|
|
||||||
@@ -49,11 +55,10 @@
|
|||||||
- 文章卡片自动切换为上下布局
|
- 文章卡片自动切换为上下布局
|
||||||
- 目录自动切换为悬浮按钮 + 抽屉
|
- 目录自动切换为悬浮按钮 + 抽屉
|
||||||
|
|
||||||
### 🔍 SEO 与国际化
|
### 🔍 SEO 与结构化数据
|
||||||
|
|
||||||
- **Open Graph 元标签** — 优化社交媒体分享预览
|
- **Open Graph 元标签** — 优化社交媒体分享预览
|
||||||
- **JSON-LD 结构化数据** — 提升搜索引擎理解与展示
|
- **JSON-LD 结构化数据** — 文章标题 / 作者 / 日期 / 封面,由转义后的 `<meta>` 生成,不拼接不可信字符串
|
||||||
- **i18n 国际化** — 简体中文、繁体中文、英语,支持跟随浏览器自动匹配
|
|
||||||
|
|
||||||
### 🔌 插件生态
|
### 🔌 插件生态
|
||||||
|
|
||||||
@@ -64,30 +69,39 @@
|
|||||||
| 瞬间 | PluginMoments | Timeline / Cards / Masonry 三种布局 |
|
| 瞬间 | PluginMoments | Timeline / Cards / Masonry 三种布局 |
|
||||||
| 图库 | PluginPhotos | Masonry / Grid / Carousel + 分组浏览 |
|
| 图库 | PluginPhotos | Masonry / Grid / Carousel + 分组浏览 |
|
||||||
| 友情链接 | PluginLinks | Cards / Grid / List 三种布局 |
|
| 友情链接 | PluginLinks | Cards / Grid / List 三种布局 |
|
||||||
| 朋友圈 | plugin-friends | 友站动态时间线 |
|
| 订阅 | PluginLinks(RSS 订阅) | 博友文章动态,分页 / 下滑加载、列表 / 网格布局 |
|
||||||
| 豆瓣 | 豆瓣插件 | 观影/读书/音乐记录 |
|
| 豆瓣 | 豆瓣插件 | 观影/读书/音乐记录 |
|
||||||
| 装备 | 装备插件 | 按分组展示装备清单 |
|
| 装备 | 装备插件 | 按分组展示装备清单 |
|
||||||
| Steam | Steam 插件 | 游戏资料与徽章 |
|
| Steam | Steam 插件 | 游戏资料与徽章 |
|
||||||
|
| 记得 | plugin-important-dates | 重要日期 / 牵挂的人 / 爱车双视图 |
|
||||||
|
|
||||||
|
> 「朋友圈」(`plugin-friends`)插件已不再提供,其 feed 聚合能力由官方「链接管理」插件的订阅取代,
|
||||||
|
> 详见[订阅页面](/pages/subscriptions)。
|
||||||
|
|
||||||
### ⚙️ 灵活配置
|
### ⚙️ 灵活配置
|
||||||
|
|
||||||
13 个配置分组,覆盖站点的方方面面:
|
17 个配置分组、126 个配置项,覆盖站点的方方面面:
|
||||||
|
|
||||||
- 基础信息、样式、导航栏、Hero 首屏
|
- 基础信息、样式、导航栏、Hero 首屏、欢迎弹窗
|
||||||
- 首页、文章、瞬间、图库、友情链接
|
- 首页、文章、瞬间、图库、友情链接、订阅
|
||||||
- 朋友圈、留言板、评论、页脚
|
- 微信读书、记得、留言板、评论、页脚
|
||||||
|
|
||||||
所有配置都在 Halo 后台的图形化面板中完成,无需修改代码。
|
所有配置都在 Halo 后台的图形化面板中完成,无需修改代码。
|
||||||
|
|
||||||
|
### 🔒 默认零第三方请求
|
||||||
|
|
||||||
|
全部脚本、样式、字体、图标、句子库与主题 Logo 随主题自托管;Hero 描述文案默认「无」、
|
||||||
|
欢迎弹窗默认关闭、两个可选 API 字段默认留空,页面不会自动向第三方发起请求。
|
||||||
|
详见 README「外部资源与隐私」。
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
- **Astro** — 静态站点生成器,编译模板
|
- **Astro 6** — 构建输出 Thymeleaf 模板
|
||||||
- **vite-plugin-halo-theme** — Halo 主题 Vite 插件
|
|
||||||
- **Thymeleaf** — Halo 使用的服务端模板引擎
|
- **Thymeleaf** — Halo 使用的服务端模板引擎
|
||||||
- **Vue 3** — 部分交互组件(搜索、主题切换、光感跟随)
|
- **原生 JavaScript + SCSS** — 交互与样式(无 Vue / 无远程运行时)
|
||||||
- **SCSS** — CSS 预处理器
|
- **LightGallery 2.9.0(GPLv3)** — 图片灯箱,随构建自托管打包
|
||||||
- **Prism.js** — 代码语法高亮
|
- **LXGW WenKai(OFL 1.1)** — 本地字体分片,许可证全文随包分发
|
||||||
- **LightGallery** — 图片灯箱
|
- **代码高亮** — 交由 Halo 应用市场插件提供,主题只自托管配色 CSS
|
||||||
|
|
||||||
## 适用场景
|
## 适用场景
|
||||||
|
|
||||||
@@ -96,7 +110,7 @@ WarmIsland 适合以下类型的站点:
|
|||||||
- 📖 **个人博客** — 记录生活、分享思考
|
- 📖 **个人博客** — 记录生活、分享思考
|
||||||
- 📸 **摄影站** — 图库页面展示作品
|
- 📸 **摄影站** — 图库页面展示作品
|
||||||
- 🎮 **游戏博客** — Steam 页面展示游戏资料
|
- 🎮 **游戏博客** — Steam 页面展示游戏资料
|
||||||
- 📚 **读书笔记** — 豆瓣页面展示阅读记录
|
- 📚 **读书笔记** — 豆瓣 / 微信读书页面展示阅读记录
|
||||||
- 💭 **碎碎念** — 瞬间页面记录日常灵感
|
- 💭 **碎碎念** — 瞬间页面记录日常灵感
|
||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ features:
|
|||||||
- title: 📝 沉浸阅读
|
- title: 📝 沉浸阅读
|
||||||
details: 文章目录侧边悬浮、阅读进度条、代码高亮、图片灯箱,打造专注的阅读体验
|
details: 文章目录侧边悬浮、阅读进度条、代码高亮、图片灯箱,打造专注的阅读体验
|
||||||
- title: 🔌 插件生态
|
- title: 🔌 插件生态
|
||||||
details: 深度集成瞬间、图库、友情链接、朋友圈、豆瓣、装备、Steam 等插件页面
|
details: 深度集成瞬间、图库、友情链接、订阅动态、豆瓣、装备、Steam 等插件页面
|
||||||
- title: ⚙️ 灵活配置
|
- title: ⚙️ 灵活配置
|
||||||
details: 丰富的主题设置面板,涵盖样式、导航、首页、文章、评论等全维度自定义
|
details: 丰富的主题设置面板,涵盖样式、导航、首页、文章、评论等全维度自定义
|
||||||
---
|
---
|
||||||
+30
-13
@@ -1,32 +1,49 @@
|
|||||||
# 自定义页面
|
# 自定义页面
|
||||||
|
|
||||||
自定义页面是 Halo 的独立页面功能,用于创建关于、友链等独立内容页面。
|
自定义页面是 Halo 的独立页面功能,用于创建关于、留言板、订阅动态、微信读书等独立内容页面。
|
||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
1. 进入 Halo 后台 → **页面** → **新建**
|
1. 进入 Halo 后台 → **页面** → **新建**
|
||||||
2. 填写页面标题和内容
|
2. 填写页面标题和内容
|
||||||
3. 可选择自定义模板
|
3. 可选:在「自定义模板」中选择主题提供的模板
|
||||||
4. 发布页面
|
4. 发布页面,并把页面加入导航菜单
|
||||||
|
|
||||||
|
页面地址由创建时设定的 slug 决定,主题不为自定义模板提供固定路由。
|
||||||
|
|
||||||
## 页面模板
|
## 页面模板
|
||||||
|
|
||||||
主题提供以下自定义页面模板:
|
`theme.yaml` 中注册了 3 个自定义模板,加上默认的 `page.html` 共 4 种:
|
||||||
|
|
||||||
| 模板 | 文件 | 说明 |
|
| 模板 | 模板文件 | 说明 |
|
||||||
|------|------|------|
|
|------|---------|------|
|
||||||
| 默认 | page.html | 标准内容页面 |
|
| 默认 | `page.html` | 标准内容页面 |
|
||||||
| 留言板 | page_messageboard.html | 带评论的留言板页面 |
|
| 留言板(MessageBoard) | `page_messageboard.html` | 带评论的留言墙,标题与副标题可配置 |
|
||||||
|
| 订阅(Subscriptions) | `page_subscriptions.html` | 博友文章订阅动态,依赖官方「链接管理」插件的 RSS 订阅 |
|
||||||
|
| 微信读书(WeRead) | `weread.html` | 微信读书书架,依赖微信读书插件,访客免登录查看 |
|
||||||
|
|
||||||
### 留言板模板
|
### 留言板模板
|
||||||
|
|
||||||
选择留言板模板后,页面会自动集成评论组件,访客可以直接留言。标题和副标题使用主题设置中的配置。
|
选择留言板模板后,页面会集成评论组件,访客可以直接留言。标题和副标题在
|
||||||
|
**主题设置 → 留言板** 中配置,详见[留言板配置](/config/messageboard)。
|
||||||
|
|
||||||
使用步骤:
|
### 订阅模板
|
||||||
|
|
||||||
1. 新建页面,选择自定义模板为「留言板」
|
feed / RSS 聚合不再依赖已停止提供的「朋友圈」插件,而由官方
|
||||||
2. 在主题设置 → 留言板中配置标题和副标题
|
「链接管理」(PluginLinks)插件的订阅能力提供。使用步骤:
|
||||||
3. 发布页面,添加到导航菜单
|
|
||||||
|
1. 安装并启用[链接管理插件](https://www.halo.run/store/apps/app-hfbQg),维护博友链接的 RSS 订阅源
|
||||||
|
2. 新建页面,自定义模板选择「订阅」
|
||||||
|
3. 在**主题设置 → 订阅**中调整每页条数、加载方式与布局,详见[订阅配置](/config/subscriptions)
|
||||||
|
|
||||||
|
未安装插件或暂无订阅动态时页面会显示友好提示,不会报错。
|
||||||
|
完整说明见[订阅页面](/pages/subscriptions)。
|
||||||
|
|
||||||
|
### 微信读书模板
|
||||||
|
|
||||||
|
选择「微信读书」模板后展示书架与阅读记录,标题 / 副标题在
|
||||||
|
**主题设置 → 微信读书** 中配置,详见[微信读书配置](/config/weread)。
|
||||||
|
该页面依赖 Halo 2.27+ 的能力。
|
||||||
|
|
||||||
## 页面功能
|
## 页面功能
|
||||||
|
|
||||||
|
|||||||
+25
-20
@@ -6,31 +6,37 @@ WarmIsland 主题深度集成了多款 Halo 插件,提供了丰富的扩展页
|
|||||||
|
|
||||||
| 页面 | 所需插件 | 路由 |
|
| 页面 | 所需插件 | 路由 |
|
||||||
|------|----------|------|
|
|------|----------|------|
|
||||||
| 瞬间 | [PluginMoments](https://www.halo.run/store/apps/app-SnwWD) | /moments |
|
| 瞬间 | [PluginMoments](https://www.halo.run/store/apps/app-SnwWD) | `/moments` |
|
||||||
| 图库 | [PluginPhotos](https://www.halo.run/store/apps/app-BmQJW) | /photos |
|
| 图库 | [PluginPhotos](https://www.halo.run/store/apps/app-BmQJW) | `/photos` |
|
||||||
| 友情链接 | [PluginLinks](https://www.halo.run/store/apps/app-hfbQg) | /links |
|
| 友情链接 | [PluginLinks](https://www.halo.run/store/apps/app-hfbQg) | `/links` |
|
||||||
| 朋友圈 | [plugin-friends](https://www.halo.run/store/apps/app-yISsV) | /friends |
|
| 订阅动态 | [PluginLinks](https://www.halo.run/store/apps/app-hfbQg)(RSS 订阅) | 自定义页面,无固定路由 |
|
||||||
| 豆瓣 | [plugin-douban](https://www.halo.run/store/apps/app-srBOL) | /douban |
|
| 豆瓣 | [plugin-douban](https://www.halo.run/store/apps/app-srBOL) | `/douban` |
|
||||||
| 装备 | [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 |
|
| 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` |
|
||||||
| 记得 | [plugin-important-dates](https://github.com/yyliucha/plugin-important-dates) | /important-dates |
|
|
||||||
|
> 「朋友圈」(`plugin-friends`)插件已不再提供,其 feed / RSS 聚合能力由官方「链接管理」插件的订阅取代。
|
||||||
|
> 主题保留的 `friends.html` 仅供旧站点兼容,新站点请使用订阅模板,详见[订阅页面](/pages/subscriptions)。
|
||||||
|
|
||||||
|
## 订阅页面
|
||||||
|
|
||||||
|
订阅页面依赖官方「链接管理」插件的 RSS 订阅,展示博友们的最新文章,
|
||||||
|
支持分页 / 下滑加载与列表 / 网格布局。它属于自定义页面模板,地址由你创建页面时的 slug 决定。
|
||||||
|
使用前请在 Halo 后台 → 页面 → 新建 → 自定义模板中选择「订阅」。
|
||||||
|
|
||||||
|
详见[订阅页面](/pages/subscriptions)与[订阅配置](/config/subscriptions)。
|
||||||
|
|
||||||
## 豆瓣页面
|
## 豆瓣页面
|
||||||
[豆瓣插件](https://www.halo.run/store/apps/app-srBOL)
|
|
||||||
展示豆瓣观影/读书/音乐记录,支持按类型和状态筛选。
|
[豆瓣插件](https://www.halo.run/store/apps/app-srBOL)展示豆瓣观影/读书/音乐记录,支持按类型和状态筛选。
|
||||||
|
|
||||||
## 装备页面
|
## 装备页面
|
||||||
[装备管理插件](https://www.halo.run/store/apps/app-ytygyqml)
|
|
||||||
展示个人装备清单,按分组分类展示。
|
|
||||||
|
|
||||||
## 足迹页面
|
[装备管理插件](https://www.halo.run/store/apps/app-ytygyqml)展示个人装备清单,按分组分类展示。
|
||||||
[足迹插件](https://www.halo.run/store/apps/app-XCtAQ)
|
|
||||||
展示个人足迹地图。
|
|
||||||
|
|
||||||
## Steam 页面
|
## Steam 页面
|
||||||
[Steam](https://www.halo.run/store/apps/app-0ojqyzfh)
|
|
||||||
展示 Steam 游戏资料,包括:
|
[Steam](https://www.halo.run/store/apps/app-0ojqyzfh)展示 Steam 游戏资料,包括:
|
||||||
|
|
||||||
- 玩家头像和昵称
|
- 玩家头像和昵称
|
||||||
- 游戏统计
|
- 游戏统计
|
||||||
@@ -39,8 +45,7 @@ WarmIsland 主题深度集成了多款 Halo 插件,提供了丰富的扩展页
|
|||||||
|
|
||||||
## 记得页面
|
## 记得页面
|
||||||
|
|
||||||
[记得插件](https://github.com/yyliucha/plugin-important-dates)
|
[记得插件](https://github.com/yyliucha/plugin-important-dates)记录重要的日子、牵挂的人与座驾,「生活 / 爱车」双视图展示:
|
||||||
记录重要的日子、牵挂的人与座驾,「生活 / 爱车」双视图展示:
|
|
||||||
|
|
||||||
- 重要日期(含农历)与倒计时便签
|
- 重要日期(含农历)与倒计时便签
|
||||||
- 牵挂的人:大头贴、昵称、关系与生日倒计时
|
- 牵挂的人:大头贴、昵称、关系与生日倒计时
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
# 朋友圈
|
|
||||||
|
|
||||||
朋友圈页面展示订阅的友站最新文章动态,需要安装 [朋友圈插件](https://www.halo.run/store/apps/app-yISsV)。
|
|
||||||
|
|
||||||
## 功能说明
|
|
||||||
|
|
||||||
页面以时间线形式展示友站更新,每条动态包含:
|
|
||||||
|
|
||||||
- 友站名称和头像
|
|
||||||
- 文章标题和链接
|
|
||||||
- 发布时间
|
|
||||||
|
|
||||||
## 使用方式
|
|
||||||
|
|
||||||
1. 在 Halo 后台安装并启用朋友圈插件
|
|
||||||
2. 在插件设置中添加友站 RSS 订阅源
|
|
||||||
3. 主题自动读取并展示友站动态
|
|
||||||
|
|
||||||
::: tip
|
|
||||||
朋友圈插件负责数据抓取,主题负责前端展示。如需调整抓取频率或内容,请在插件设置中操作。
|
|
||||||
:::
|
|
||||||
|
|
||||||
## 相关配置
|
|
||||||
|
|
||||||
- [朋友圈配置](/config/friends) — 页面标题
|
|
||||||
+1
-1
@@ -16,7 +16,7 @@ Hero 区域占据整个视口高度,包含:
|
|||||||
|
|
||||||
- **主标题**:默认使用站点标题,可在主题设置中自定义
|
- **主标题**:默认使用站点标题,可在主题设置中自定义
|
||||||
- **副标题**:默认使用站点副标题
|
- **副标题**:默认使用站点副标题
|
||||||
- **描述文案**:支持一言 API、自定义文案或关闭
|
- **描述文案**:默认「无」(不显示、零请求);可选「一言」(API 字段默认预填 `https://v1.hitokoto.cn/`,清空则用内置本地句子库)或「自定义」静态文案
|
||||||
- **视觉效果**:渐变背景 + 呼吸光球 + 滚动模糊
|
- **视觉效果**:渐变背景 + 呼吸光球 + 滚动模糊
|
||||||
|
|
||||||
页面滚动时,Hero 区域会产生模糊效果,内容向上淡出,底部有向下箭头提示滚动。
|
页面滚动时,Hero 区域会产生模糊效果,内容向上淡出,底部有向下箭头提示滚动。
|
||||||
|
|||||||
@@ -29,3 +29,5 @@
|
|||||||
## 相关配置
|
## 相关配置
|
||||||
|
|
||||||
- [友情链接配置](/config/links) — 展示样式
|
- [友情链接配置](/config/links) — 展示样式
|
||||||
|
- [订阅配置](/config/subscriptions) — 同一插件提供的 RSS 订阅动态
|
||||||
|
- [订阅页面](/pages/subscriptions) — 博友文章动态页面
|
||||||
+8
-3
@@ -34,10 +34,15 @@
|
|||||||
|
|
||||||
## 代码高亮
|
## 代码高亮
|
||||||
|
|
||||||
使用 Prism.js 实现代码块语法高亮,支持自动语言检测。可在主题设置中选择两种主题:
|
主题**不再自带 Prism.js 运行时**(含按语言从 CDN 动态加载组件的 autoloader)。语法高亮由
|
||||||
|
Halo 应用市场的代码高亮插件或「API 扩展包」服务端高亮输出,主题只提供两套自托管的
|
||||||
|
`.token` 配色,在「主题设置 → 文章 → 代码高亮主题」中切换:
|
||||||
|
|
||||||
- **Warm**:浅色代码高亮,与暖色调设计一致
|
- **Warm**:浅色代码高亮,与暖色调设计一致(默认)
|
||||||
- **Cold**:深色代码高亮(Tomorrow Night),适合代码密集型文章
|
- **Cold**:深色代码高亮(Tomorrow 风格),适合代码密集型文章
|
||||||
|
|
||||||
|
未安装高亮插件时,代码块仍以等宽字体正常显示,只是没有语法着色;主题不会因此报错,
|
||||||
|
也不会发起任何外部请求。
|
||||||
|
|
||||||
## 图片灯箱
|
## 图片灯箱
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# 订阅页面
|
||||||
|
|
||||||
|
订阅页面按时间倒序展示博友们的最新文章,即原先「朋友圈 / feed 聚合」所提供的能力。
|
||||||
|
现在它由 **Halo 官方「链接管理」(PluginLinks)插件的 RSS 订阅**驱动,主题提供
|
||||||
|
`page_subscriptions.html` 自定义模板负责前端展示。
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
1. 安装并启用 [链接管理插件](https://www.halo.run/store/apps/app-hfbQg),在插件中维护博友链接的 RSS 订阅源
|
||||||
|
2. 进入 Halo 后台 → **页面** → **新建**
|
||||||
|
3. 选择自定义模板「**订阅**」(`Subscriptions`,对应 `page_subscriptions.html`)
|
||||||
|
4. 发布页面,并按需加入导航菜单
|
||||||
|
|
||||||
|
> 页面地址由你创建页面时设定的 slug 决定,主题**不提供固定的 `/subscriptions` 路由**;
|
||||||
|
> 只有插件提供的扩展页面(如 `/links`、`/moments`)才是固定路由。
|
||||||
|
|
||||||
|
## 页面内容
|
||||||
|
|
||||||
|
每条订阅动态是一张卡片,包含:
|
||||||
|
|
||||||
|
- 作者头像(缺失时显示首字占位)、作者名称与主页链接
|
||||||
|
- 发布日期
|
||||||
|
- 文章标题(点击跳转原文)、摘要
|
||||||
|
- 空数据时的友好提示,以及未安装链接管理插件时的安装引导
|
||||||
|
|
||||||
|
标题与作者链接均使用 `target="_blank"` + `rel="noopener noreferrer"`,
|
||||||
|
主题本身不会向第三方发起请求。
|
||||||
|
|
||||||
|
## 展示与加载
|
||||||
|
|
||||||
|
在[订阅配置](/config/subscriptions)中可调:
|
||||||
|
|
||||||
|
| 能力 | 可选值 |
|
||||||
|
| --- | --- |
|
||||||
|
| 布局 | 单列列表卡片 / 网格卡片(窄屏自动收敛列数) |
|
||||||
|
| 加载方式 | 底部分页(较新 / 较旧)/ 下滑自动加载 |
|
||||||
|
| 每页条数 | 1–100,默认 20 |
|
||||||
|
| 标题与副标题 | 自定义文本 |
|
||||||
|
|
||||||
|
## 从「朋友圈」迁移
|
||||||
|
|
||||||
|
`plugin-friends`(朋友圈)插件已不再提供,原来的 `/friends` 页面与「朋友圈」设置分组
|
||||||
|
仅作为旧站点兼容保留,模板检测到插件不可用时会显示空状态。迁移步骤:
|
||||||
|
|
||||||
|
1. 安装 / 启用「链接管理」插件,把原本在朋友圈插件里维护的 RSS 订阅源迁到链接管理插件的订阅中
|
||||||
|
2. 新建一个页面并选择「订阅」自定义模板
|
||||||
|
3. 确认动态正常展示后,删除旧的 `/friends` 页面与菜单项
|
||||||
|
|
||||||
|
## 相关配置
|
||||||
|
|
||||||
|
- [订阅配置](/config/subscriptions) — 全部 5 个设置项
|
||||||
|
- [友情链接页面](/pages/links) — 同一插件的友链展示
|
||||||
|
- [扩展页面](/pages/extended) — 插件前台页面一览
|
||||||
+3
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.2.5",
|
"version": "1.2.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.12.0"
|
"node": ">=22.12.0"
|
||||||
@@ -9,8 +9,9 @@
|
|||||||
"build": "astro build && node scripts/postbuild.mjs",
|
"build": "astro build && node scripts/postbuild.mjs",
|
||||||
"snapshot": "node scripts/snapshot-assets.mjs",
|
"snapshot": "node scripts/snapshot-assets.mjs",
|
||||||
"package": "node scripts/package.mjs",
|
"package": "node scripts/package.mjs",
|
||||||
|
"audit:external": "node scripts/audit-external.mjs",
|
||||||
"release": "pnpm run build && pnpm run snapshot && pnpm run package",
|
"release": "pnpm run build && pnpm run snapshot && pnpm run package",
|
||||||
"smoke": "node scripts/smoke-clickfx.mjs",
|
"smoke": "node scripts/smoke-clickfx.mjs && node scripts/smoke-jsonld.mjs",
|
||||||
"astro": "astro",
|
"astro": "astro",
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"note": "WarmIsland 内置本地一言句子库,纯静态数据,零外部请求。站点可自行编辑本文件替换文案。",
|
||||||
|
"sentences": [
|
||||||
|
{ "text": "暖如灯火,静如留白。", "from": "暖屿" },
|
||||||
|
{ "text": "慢慢来,比较快。", "from": "暖屿" },
|
||||||
|
{ "text": "把日子过成自己喜欢的样子。", "from": "暖屿" },
|
||||||
|
{ "text": "风会记得每一朵花的香。", "from": "暖屿" },
|
||||||
|
{ "text": "愿你在忙碌里,仍留一扇看云的窗。", "from": "暖屿" },
|
||||||
|
{ "text": "写下此刻,便是对时间最轻柔的挽留。", "from": "暖屿" },
|
||||||
|
{ "text": "山不争高,水不争先。", "from": "暖屿" },
|
||||||
|
{ "text": "有些答案,要走很远才遇见。", "from": "暖屿" },
|
||||||
|
{ "text": "夜色温柔,适合读一封长信。", "from": "暖屿" },
|
||||||
|
{ "text": "把灯留着,等一个晚归的人。", "from": "暖屿" },
|
||||||
|
{ "text": "心有所栖,便是归处。", "from": "暖屿" },
|
||||||
|
{ "text": "日常里藏着最不动声色的欢喜。", "from": "暖屿" },
|
||||||
|
{ "text": "不必催促春天,花自会开。", "from": "暖屿" },
|
||||||
|
{ "text": "走过的路,都会变成身上的光。", "from": "暖屿" },
|
||||||
|
{ "text": "一杯热茶,足以安顿一个傍晚。", "from": "暖屿" },
|
||||||
|
{ "text": "愿你所念之人,也在念着你。", "from": "暖屿" },
|
||||||
|
{ "text": "安静不是空白,是留给自己呼吸。", "from": "暖屿" },
|
||||||
|
{ "text": "把复杂留给世界,把简单留给生活。", "from": "暖屿" },
|
||||||
|
{ "text": "长路漫漫,慢慢走也算抵达。", "from": "暖屿" },
|
||||||
|
{ "text": "今天也值得被认真记下。", "from": "暖屿" },
|
||||||
|
{ "text": "微光虽小,足以照亮一隅。", "from": "暖屿" },
|
||||||
|
{ "text": "在喧嚣之外,给自己留一座小岛。", "from": "暖屿" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
Copyright 2021-2026 LXGW (https://github.com/lxgw/LxgwWenKai), with Reserved Font Name '霞鹜', '霞鶩', '落霞孤鹜', '落霞孤鶩' and 'LXGW'. [ADDITIONAL PERMISSION] The Reserved Font Names '霞鹜', '霞鶩', '落霞孤鹜', '落霞孤鶩' and 'LXGW' may continue to be used in Modified Versions recompiled from the Original Version, without modifications to the font source code; or in Modified Versions subsetted or converted to other formats (e.g., WOFF/WOFF2) solely for web font delivery, provided such Modified Versions are not made available as installable desktop fonts (e.g., on mainstream platforms like Google Fonts, or third-party non-commercial platforms recognized by the author @lxgw; other web font platforms please contact the author @lxgw for confirmation).
|
||||||
|
Copyright 2020 The Klee Project Authors (https://github.com/fontworks-fonts/Klee)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
Vendored
+77
@@ -0,0 +1,77 @@
|
|||||||
|
/* WarmIsland 自托管代码配色(Cold / 深色,参考 Prism Tomorrow 配色)
|
||||||
|
- 仅定义 .token.* 语法着色,供 Halo 代码高亮插件或「API 扩展包」服务端高亮输出使用。
|
||||||
|
- 主题本身不再加载 Prism.js / autoloader / 任何远程脚本。
|
||||||
|
- 本文件随主题分发,不含 @import、url()、远程字体或外部请求。
|
||||||
|
*/
|
||||||
|
pre[class*="language-"],
|
||||||
|
code[class*="language-"] {
|
||||||
|
color: #cccccc;
|
||||||
|
background: none;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre[class*="language-"] {
|
||||||
|
background: #2d2d2d;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.doctype,
|
||||||
|
.token.cdata {
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.punctuation {
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.property,
|
||||||
|
.token.tag,
|
||||||
|
.token.boolean,
|
||||||
|
.token.number,
|
||||||
|
.token.constant,
|
||||||
|
.token.symbol,
|
||||||
|
.token.deleted {
|
||||||
|
color: #f2777a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.selector,
|
||||||
|
.token.attr-name,
|
||||||
|
.token.string,
|
||||||
|
.token.char,
|
||||||
|
.token.builtin,
|
||||||
|
.token.inserted {
|
||||||
|
color: #99cc99;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.operator,
|
||||||
|
.token.entity,
|
||||||
|
.token.url,
|
||||||
|
.token.variable {
|
||||||
|
color: #f99157;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.atrule,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.keyword {
|
||||||
|
color: #cc99cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.function,
|
||||||
|
.token.class-name {
|
||||||
|
color: #6699cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.regex,
|
||||||
|
.token.important {
|
||||||
|
color: #ffcc66;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.bold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
Vendored
+77
@@ -0,0 +1,77 @@
|
|||||||
|
/* WarmIsland 自托管代码配色(Warm / 浅色)
|
||||||
|
- 仅定义 .token.* 语法着色,供 Halo 代码高亮插件或「API 扩展包」服务端高亮输出使用。
|
||||||
|
- 主题本身不再加载 Prism.js / autoloader / 任何远程脚本。
|
||||||
|
- 本文件随主题分发,不含 @import、url()、远程字体或外部请求。
|
||||||
|
*/
|
||||||
|
pre[class*="language-"],
|
||||||
|
code[class*="language-"] {
|
||||||
|
color: #4b4038;
|
||||||
|
background: none;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre[class*="language-"] {
|
||||||
|
background: #f7f2ec;
|
||||||
|
border: 1px solid rgba(90, 70, 55, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.doctype,
|
||||||
|
.token.cdata {
|
||||||
|
color: #8c8279;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.punctuation {
|
||||||
|
color: #7a6f66;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.property,
|
||||||
|
.token.tag,
|
||||||
|
.token.boolean,
|
||||||
|
.token.number,
|
||||||
|
.token.constant,
|
||||||
|
.token.symbol,
|
||||||
|
.token.deleted {
|
||||||
|
color: #b04a3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.selector,
|
||||||
|
.token.attr-name,
|
||||||
|
.token.string,
|
||||||
|
.token.char,
|
||||||
|
.token.builtin,
|
||||||
|
.token.inserted {
|
||||||
|
color: #567c4e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.operator,
|
||||||
|
.token.entity,
|
||||||
|
.token.url,
|
||||||
|
.token.variable {
|
||||||
|
color: #8a6b3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.atrule,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.keyword {
|
||||||
|
color: #a0527f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.function,
|
||||||
|
.token.class-name {
|
||||||
|
color: #3f6e9e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.regex,
|
||||||
|
.token.important {
|
||||||
|
color: #bd7423;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.bold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB |
@@ -0,0 +1,271 @@
|
|||||||
|
/**
|
||||||
|
* 外部资源与隐私审计(发版前自动检查,Node 内置模块,无第三方依赖)
|
||||||
|
*
|
||||||
|
* 对应 Halo 应用市场审核指南:
|
||||||
|
* - 1.4 远程执行与动态加载:禁止远程加载核心代码 / 动态拉取并执行组件
|
||||||
|
* - 4.4.4 性能与外部资源:不得预连接外部 CDN、加载远程脚本样式、打包无关大资源
|
||||||
|
* - 4.4.2 内容渲染安全:不得把站点内容用 th:utext 拼进 <script> 原始文本
|
||||||
|
* - 5.1 / 5.2 / 2.2:第三方字体必须随包携带许可证全文;可选 API 的数据流向与隐私披露
|
||||||
|
* 由 README「外部资源与隐私」及文档站承载;上架说明数字必须与配置定义一致
|
||||||
|
* - 4.6 / 2.2:不得出现占位内容、无效链接
|
||||||
|
*
|
||||||
|
* 检查项:
|
||||||
|
* 1. 模板中不得有指向第三方的 <link rel=preconnect|dns-prefetch|preload>
|
||||||
|
* 2. 模板中不得有 src/href 直接指向 http(s) 的 script/link/img/iframe/source/video/audio
|
||||||
|
* 3. 模板与静态资源中不得硬编码 fetch("https://...") / import("https://...") / url(https://...)
|
||||||
|
* 4. 不得出现历史上被审核点名的第三方服务域名,或 autoloader 动态加载痕迹
|
||||||
|
* 5. README 中的相对链接必须在安装包清单中真实存在
|
||||||
|
* 6. 任何 <script> 开标签不得带 th:utext;字体许可证全文必须在制品清单中;
|
||||||
|
* README 的设置分组 / 配置项数量必须与 settings.yaml 一致
|
||||||
|
* 7. 三个可选 API 字段存在且 help 为精简功能说明;隐私披露文案不得回潮到设置界面(5.1)
|
||||||
|
*
|
||||||
|
* 说明:<a href="https://..."> 这类需要访客主动点击的分享 / 备案 / 应用市场外链是允许的,
|
||||||
|
* 不属于「自动外部请求」,但已在 README「外部资源与隐私」章节逐项披露。
|
||||||
|
*/
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||||
|
|
||||||
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
|
||||||
|
/** 曾经造成未披露外部请求的服务域名,出现即视为回归 */
|
||||||
|
const BLOCKED_HOSTS = [
|
||||||
|
"cdn.jsdelivr.net",
|
||||||
|
"unpkg.com",
|
||||||
|
"cdnjs.cloudflare.com",
|
||||||
|
"fonts.googleapis.com",
|
||||||
|
"fonts.gstatic.com",
|
||||||
|
"v1.hitokoto.cn",
|
||||||
|
"myip.ipip.net",
|
||||||
|
"r.inews.qq.com",
|
||||||
|
];
|
||||||
|
|
||||||
|
const REMOTE_LOAD_RE =
|
||||||
|
/<(?:script|link|img|iframe|source|video|audio)\b[^>]*\b(?:src|href)\s*=\s*["']https?:\/\/[^"']+["']/gi;
|
||||||
|
const PRECONNECT_RE =
|
||||||
|
/<link\b[^>]*\brel\s*=\s*["'](?:preconnect|dns-prefetch|preload)["'][^>]*\bhref\s*=\s*["']https?:\/\/[^"']+["']/gi;
|
||||||
|
const REMOTE_FETCH_RE = /\b(?:fetch|importScripts)\s*\(\s*["']https?:\/\/[^"']+["']/gi;
|
||||||
|
const REMOTE_IMPORT_RE = /\bimport\s*\(\s*["']https?:\/\/[^"']+["']/gi;
|
||||||
|
const REMOTE_CSS_URL_RE = /url\(\s*["']?https?:\/\/[^)"']+/gi;
|
||||||
|
const AUTOLOADER_RE = /\b(?:prism-autoloader|languages_path|Prism\.plugins\.autoloader)\b/gi;
|
||||||
|
|
||||||
|
function listFiles(dir, exts, out = []) {
|
||||||
|
if (!fs.existsSync(dir)) return out;
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const abs = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
/* 字体目录只有本地 woff2 与版权注释,不参与审计 */
|
||||||
|
if (entry.name === "fonts") continue;
|
||||||
|
listFiles(abs, exts, out);
|
||||||
|
} else if (exts.some((ext) => entry.name.endsWith(ext))) {
|
||||||
|
out.push(abs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function auditTemplates(templatesDir = path.join(root, "templates")) {
|
||||||
|
const problems = [];
|
||||||
|
const files = listFiles(templatesDir, [".html", ".js", ".css"]);
|
||||||
|
const htmlFiles = files.filter((f) => f.endsWith(".html"));
|
||||||
|
/* 对外报告的模板数只算 templates/ 根目录下的 Halo 页面模板;
|
||||||
|
templates/error/ 下的错误页 HTML 同样参与审计,但不计入模板数。 */
|
||||||
|
const htmlCount = htmlFiles.filter((f) => path.dirname(f) === templatesDir).length;
|
||||||
|
|
||||||
|
if (htmlFiles.length === 0) {
|
||||||
|
problems.push("未找到任何 templates/*.html,构建产物不完整");
|
||||||
|
return { fileCount: 0, htmlCount: 0, problems };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const rel = path.relative(root, file).replace(/\\/g, "/");
|
||||||
|
const text = fs.readFileSync(file, "utf8");
|
||||||
|
const isHtml = file.endsWith(".html");
|
||||||
|
|
||||||
|
const rules = isHtml
|
||||||
|
? [
|
||||||
|
["远程预连接 / 预加载", PRECONNECT_RE],
|
||||||
|
["远程脚本 / 样式 / 媒体", REMOTE_LOAD_RE],
|
||||||
|
["硬编码外部 fetch", REMOTE_FETCH_RE],
|
||||||
|
["远程动态 import", REMOTE_IMPORT_RE],
|
||||||
|
["autoloader 动态加载", AUTOLOADER_RE],
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
["硬编码外部 fetch", REMOTE_FETCH_RE],
|
||||||
|
["远程动态 import", REMOTE_IMPORT_RE],
|
||||||
|
["CSS 远程 url()", REMOTE_CSS_URL_RE],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [label, re] of rules) {
|
||||||
|
re.lastIndex = 0;
|
||||||
|
const hit = re.exec(text);
|
||||||
|
if (hit) {
|
||||||
|
problems.push(`${rel}: ${label} → ${hit[0].slice(0, 110)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const host of BLOCKED_HOSTS) {
|
||||||
|
if (isHtml && text.includes(host)) {
|
||||||
|
problems.push(`${rel}: 出现已移除的第三方服务域名 ${host}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { fileCount: files.length, htmlCount, problems };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function auditReadmeLinks(entryNames, readmePath = path.join(root, "README.md")) {
|
||||||
|
const problems = [];
|
||||||
|
if (!fs.existsSync(readmePath)) return ["README.md 不存在"];
|
||||||
|
const readme = fs.readFileSync(readmePath, "utf8");
|
||||||
|
const linkRe = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
|
||||||
|
let match;
|
||||||
|
let checked = 0;
|
||||||
|
while ((match = linkRe.exec(readme)) !== null) {
|
||||||
|
const href = match[1];
|
||||||
|
if (/^(?:https?:|mailto:|#)/.test(href)) continue;
|
||||||
|
let target = href.split("#")[0];
|
||||||
|
if (!target) continue;
|
||||||
|
try {
|
||||||
|
target = decodeURIComponent(target);
|
||||||
|
} catch (e) {
|
||||||
|
/* 保留原值 */
|
||||||
|
}
|
||||||
|
target = target.replace(/^\.\//, "");
|
||||||
|
checked++;
|
||||||
|
if (!entryNames.has(target)) {
|
||||||
|
problems.push(`README.md 相对链接在安装包中不存在:${href}(期望 ${target})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { problems, checked };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合规审计:审核指南 4.4.2(内容渲染安全)、5.2(许可证传递)、2.2(上架信息准确)
|
||||||
|
*
|
||||||
|
* @param {Set<string>|null} entryNames 打包清单;传入时额外校验许可证确实进了制品
|
||||||
|
*/
|
||||||
|
export function auditCompliance(entryNames = null) {
|
||||||
|
const problems = [];
|
||||||
|
const templatesDir = path.join(root, "templates");
|
||||||
|
const oflRel = "templates/assets/fonts/LXGWWenKai/OFL.txt";
|
||||||
|
|
||||||
|
// 1. <script> 是原始文本元素,th:utext 不转义且可被脚本闭合标签截断(4.4.2)
|
||||||
|
const scriptTagRe = /<script\b[^>]*>/gi;
|
||||||
|
for (const file of listFiles(templatesDir, [".html"])) {
|
||||||
|
const rel = path.relative(root, file).replace(/\\/g, "/");
|
||||||
|
const text = fs.readFileSync(file, "utf8");
|
||||||
|
scriptTagRe.lastIndex = 0;
|
||||||
|
let hit;
|
||||||
|
while ((hit = scriptTagRe.exec(text)) !== null) {
|
||||||
|
if (/\bth:utext\b/i.test(hit[0])) {
|
||||||
|
problems.push(`${rel}: <script> 开标签使用了 th:utext(4.4.2 内容注入风险)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. SIL OFL 1.1 要求随字体软件附带版权声明与许可证全文(5.2)
|
||||||
|
const oflPath = path.join(root, oflRel);
|
||||||
|
if (entryNames && !entryNames.has(oflRel)) {
|
||||||
|
problems.push(`安装包缺少 ${oflRel}(5.2 字体许可证传递)`);
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(oflPath)) {
|
||||||
|
problems.push(`构建产物缺少 ${oflRel}(5.2 字体许可证传递)`);
|
||||||
|
} else {
|
||||||
|
const ofl = fs.readFileSync(oflPath, "utf8");
|
||||||
|
if (!/SIL OPEN FONT LICENSE Version 1\.1/.test(ofl) || !/Copyright 2021-2026 LXGW/.test(ofl)) {
|
||||||
|
problems.push(`${oflRel} 内容不完整:需包含上游版权声明与 OFL 1.1 全文`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 上架说明里的配置数量必须与 settings.yaml 一致(2.2 / 4.6)
|
||||||
|
const settingsPath = path.join(root, "settings.yaml");
|
||||||
|
const readmePath = path.join(root, "README.md");
|
||||||
|
let groupCount = 0;
|
||||||
|
let fieldCount = 0;
|
||||||
|
let conciseCount = 0;
|
||||||
|
if (fs.existsSync(settingsPath) && fs.existsSync(readmePath)) {
|
||||||
|
const settings = fs.readFileSync(settingsPath, "utf8");
|
||||||
|
const readme = fs.readFileSync(readmePath, "utf8");
|
||||||
|
groupCount = (settings.match(/^\s*-\s+group:/gm) || []).length;
|
||||||
|
fieldCount = (settings.match(/^\s*-\s+\$formkit:/gm) || []).length;
|
||||||
|
const expected = `${groupCount} 个设置分组、${fieldCount} 个配置项`;
|
||||||
|
if (!readme.includes(expected)) {
|
||||||
|
problems.push(`README 配置数量与 settings.yaml 不一致,应为「${expected}」(2.2)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 5.1 隐私披露:完整的数据流向说明统一由 README「外部资源与隐私」章节及文档站承载,
|
||||||
|
// 设置界面的三个可选 API 字段只保留精简功能说明,不得回潮出现冗长披露文案。
|
||||||
|
const apiFields = [
|
||||||
|
"welcome_popup_api",
|
||||||
|
"welcome_popup_fallback_api",
|
||||||
|
"hero_hitokoto_api",
|
||||||
|
];
|
||||||
|
const bannedSnippets = [
|
||||||
|
"需自行在站点隐私说明中披露",
|
||||||
|
"填写后会把访客 IP 发送给该第三方服务换取地理位置",
|
||||||
|
"填写后同样会把访客 IP 发送给该第三方服务换取地理位置",
|
||||||
|
"只有填写后才会向该地址发起请求",
|
||||||
|
];
|
||||||
|
const blocks = settings.split(/(?=^[ \t]*-[ \t]+\$formkit:)/m).slice(1);
|
||||||
|
for (const name of apiFields) {
|
||||||
|
const block = blocks.find((b) => new RegExp(`^\\s+name:[ \\t]*${name}[ \\t]*$`, "m").test(b));
|
||||||
|
if (!block) {
|
||||||
|
problems.push(`settings.yaml 缺少可选 API 配置项 ${name}(5.1 隐私披露)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const helpLine = block.split(/\r?\n/).find((line) => /^\s+help:/.test(line)) || "";
|
||||||
|
if (!helpLine.trim()) {
|
||||||
|
problems.push(`${name} 缺少 help 功能说明(5.1)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (bannedSnippets.some((snippet) => helpLine.includes(snippet))) {
|
||||||
|
problems.push(`${name} 的 help 出现了已移除的隐私披露文案(5.1)`);
|
||||||
|
} else {
|
||||||
|
conciseCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!readme.includes("外部资源与隐私") || !readme.includes("访客 IP")) {
|
||||||
|
problems.push("README 缺少可选 API 的隐私披露章节(5.1)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { problems, groupCount, fieldCount, conciseCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
function report(label, problems) {
|
||||||
|
if (problems.length === 0) {
|
||||||
|
console.log(`[audit] PASS ${label}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
for (const p of problems) console.error(`[audit] FAIL ${label}: ${p}`);
|
||||||
|
return problems.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCli = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
||||||
|
if (isCli) {
|
||||||
|
const templates = auditTemplates();
|
||||||
|
const compliance = auditCompliance();
|
||||||
|
let failures = 0;
|
||||||
|
failures += report(
|
||||||
|
`模板外部资源(${templates.htmlCount} 个 HTML / ${templates.fileCount} 个文件)`,
|
||||||
|
templates.problems
|
||||||
|
);
|
||||||
|
failures += report(
|
||||||
|
`合规审计 4.4.2 / 5.1 / 5.2 / 2.2(${compliance.groupCount} 组 / ${compliance.fieldCount} 配置项 / ${compliance.conciseCount} 条精简提示)`,
|
||||||
|
compliance.problems
|
||||||
|
);
|
||||||
|
/* 独立运行时按当前磁盘上的打包清单口径检查 README 链接 */
|
||||||
|
const distZip = path.join(root, "dist");
|
||||||
|
const zip = fs.existsSync(distZip)
|
||||||
|
? fs.readdirSync(distZip).filter((f) => f.endsWith(".zip") && f.includes("warm-island"))
|
||||||
|
: [];
|
||||||
|
if (zip.length) {
|
||||||
|
/* 简单口径:zip 存在时仅提示需通过 pnpm package 做完整链接校验 */
|
||||||
|
console.log(`[audit] 提示:README 链接校验在 pnpm package 内执行(发现 ${zip.length} 个安装包)`);
|
||||||
|
} else {
|
||||||
|
console.log("[audit] 提示:README 链接校验在 pnpm package 内执行(当前无安装包)");
|
||||||
|
}
|
||||||
|
console.log(`[audit] 审计共 ${failures} 项问题`);
|
||||||
|
process.exit(failures ? 1 : 0);
|
||||||
|
}
|
||||||
+59
-3
@@ -10,6 +10,7 @@ import fs from "node:fs";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import zlib from "node:zlib";
|
import zlib from "node:zlib";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { auditTemplates, auditReadmeLinks, auditCompliance } from "./audit-external.mjs";
|
||||||
|
|
||||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
|
||||||
@@ -131,11 +132,17 @@ const INCLUDE_ROOT = [
|
|||||||
"settings.yaml",
|
"settings.yaml",
|
||||||
"LICENSE",
|
"LICENSE",
|
||||||
"README.md",
|
"README.md",
|
||||||
|
/* 上架说明引用的预览图必须真实存在于制品中(审核指南 4.6 / 2.2) */
|
||||||
|
"WarmIsland Preview.webp",
|
||||||
"pnpm-lock.yaml",
|
"pnpm-lock.yaml",
|
||||||
"pnpm-workspace.yaml",
|
"pnpm-workspace.yaml",
|
||||||
];
|
];
|
||||||
const INCLUDE_DIRS = ["templates"];
|
const INCLUDE_DIRS = ["templates"];
|
||||||
const EXCLUDE_DIRS = new Set([".git", "node_modules", "src", "dist", "compat", ".astro"]);
|
/* README 引用的 docs/*.md 也要随包分发,避免制品内出现死链 */
|
||||||
|
const INCLUDE_DOCS = ["docs"];
|
||||||
|
const EXCLUDE_DIRS = new Set([
|
||||||
|
".git", "node_modules", "src", "dist", "compat", ".astro", ".vitepress",
|
||||||
|
]);
|
||||||
|
|
||||||
/* ---------------- 构建产物前置校验 ----------------
|
/* ---------------- 构建产物前置校验 ----------------
|
||||||
* templates/ 被 .gitignore 忽略,克隆仓库或清理工作区后默认不存在。
|
* templates/ 被 .gitignore 忽略,克隆仓库或清理工作区后默认不存在。
|
||||||
@@ -173,14 +180,16 @@ for (const name of INCLUDE_ROOT) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function walk(dir, prefix) {
|
function walk(dir, prefix, opts = {}) {
|
||||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
const abs = path.join(dir, entry.name);
|
const abs = path.join(dir, entry.name);
|
||||||
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
if (EXCLUDE_DIRS.has(entry.name)) continue;
|
if (EXCLUDE_DIRS.has(entry.name)) continue;
|
||||||
walk(abs, rel);
|
walk(abs, rel, opts);
|
||||||
} else {
|
} else {
|
||||||
|
/* docs 只收 Markdown 源文件,不收 VitePress 构建产物 / 依赖 README */
|
||||||
|
if (opts.mdOnly && !entry.name.endsWith(".md")) continue;
|
||||||
entries.push({ name: rel, data: fs.readFileSync(abs), date: fs.statSync(abs).mtime });
|
entries.push({ name: rel, data: fs.readFileSync(abs), date: fs.statSync(abs).mtime });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,6 +199,10 @@ for (const dir of INCLUDE_DIRS) {
|
|||||||
const full = path.join(root, dir);
|
const full = path.join(root, dir);
|
||||||
if (fs.existsSync(full)) walk(full, dir);
|
if (fs.existsSync(full)) walk(full, dir);
|
||||||
}
|
}
|
||||||
|
for (const dir of INCLUDE_DOCS) {
|
||||||
|
const full = path.join(root, dir);
|
||||||
|
if (fs.existsSync(full)) walk(full, dir, { mdOnly: true });
|
||||||
|
}
|
||||||
|
|
||||||
if (!entries.some((entry) => entry.name === "templates/index.html")) {
|
if (!entries.some((entry) => entry.name === "templates/index.html")) {
|
||||||
fail("打包清单中缺少 templates/index.html");
|
fail("打包清单中缺少 templates/index.html");
|
||||||
@@ -201,6 +214,49 @@ if (pkgVersion !== version) {
|
|||||||
fail(`版本号不一致:package.json=${pkgVersion},theme.yaml=${version}`);
|
fail(`版本号不一致:package.json=${pkgVersion},theme.yaml=${version}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------- 上架合规前置校验 ----------------
|
||||||
|
* 1. 主题不得出现未披露的第三方自动请求 / 远程动态加载(审核指南 1.4 / 4.4.4)
|
||||||
|
* 2. README 的相对链接必须在制品内真实存在(审核指南 4.6 / 2.2)
|
||||||
|
* 3. logo 只保留一份压缩版,避免打包两份 1.5MB 大图
|
||||||
|
*/
|
||||||
|
const templateAudit = auditTemplates(templatesDir);
|
||||||
|
for (const problem of templateAudit.problems) {
|
||||||
|
console.error(`[package] ${problem}`);
|
||||||
|
}
|
||||||
|
if (templateAudit.problems.length) {
|
||||||
|
fail("外部资源审计未通过:模板仍存在第三方远程加载 / 预连接");
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkAudit = auditReadmeLinks(new Set(entries.map((entry) => entry.name)));
|
||||||
|
for (const problem of linkAudit.problems) {
|
||||||
|
console.error(`[package] ${problem}`);
|
||||||
|
}
|
||||||
|
if (linkAudit.problems.length) {
|
||||||
|
fail("README 存在指向制品中不存在文件的相对链接");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 4.4.2 / 5.2 / 2.2:内容渲染安全、字体许可证传递、上架说明数字一致性 */
|
||||||
|
const entryNameSet = new Set(entries.map((entry) => entry.name));
|
||||||
|
const complianceAudit = auditCompliance(entryNameSet);
|
||||||
|
for (const problem of complianceAudit.problems) {
|
||||||
|
console.error(`[package] ${problem}`);
|
||||||
|
}
|
||||||
|
if (complianceAudit.problems.length) {
|
||||||
|
fail("合规审计未通过(4.4.2 内容渲染安全 / 5.2 许可证传递 / 2.2 上架信息)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(path.join(templatesDir, "logo.png"))) {
|
||||||
|
fail("templates/logo.png 仍存在:应只保留压缩后的 templates/assets/logo.png");
|
||||||
|
}
|
||||||
|
const logoFile = path.join(templatesDir, "assets", "logo.png");
|
||||||
|
if (fs.existsSync(logoFile) && fs.statSync(logoFile).size > 300 * 1024) {
|
||||||
|
fail("templates/assets/logo.png 超过 300KB,请先压缩再打包");
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`[package] 合规审计通过(${templateAudit.htmlCount} 个模板、${linkAudit.checked} 个相对链接、` +
|
||||||
|
`${complianceAudit.groupCount} 组 / ${complianceAudit.fieldCount} 配置项、logo 单份压缩、OFL 全文随包)`
|
||||||
|
);
|
||||||
|
|
||||||
const outFile = path.join(root, "dist", `warm-island-${version}.zip`);
|
const outFile = path.join(root, "dist", `warm-island-${version}.zip`);
|
||||||
buildZip(entries, outFile);
|
buildZip(entries, outFile);
|
||||||
|
|
||||||
|
|||||||
+18
-5
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* 构建后置处理:
|
* 构建后置处理:
|
||||||
* 1. 复制 logo.png 到 assets(保留历史行为)
|
* 1. 校验主题 logo 存在且只保留一份(public/assets/logo.png → templates/assets/logo.png)
|
||||||
* 2. 合并 compat/assets 下的历史版本静态资源,避免站点/代理缓存中的旧 HTML
|
* 2. 合并 compat/assets 下的历史版本静态资源,避免站点/代理缓存中的旧 HTML
|
||||||
* 仍然引用上一版本的哈希文件名时出现 500(FileNotFoundException)
|
* 仍然引用上一版本的哈希文件名时出现 500(FileNotFoundException)
|
||||||
* 3. 校验所有 templates/*.html 引用的静态资源真实存在,缺失即构建失败
|
* 3. 校验所有 templates/*.html 引用的静态资源真实存在,缺失即构建失败
|
||||||
@@ -16,11 +16,24 @@ const templatesDir = path.join(root, "templates");
|
|||||||
|
|
||||||
fs.mkdirSync(assetsDir, { recursive: true });
|
fs.mkdirSync(assetsDir, { recursive: true });
|
||||||
|
|
||||||
// 1. logo
|
// 1. logo:旧实现把 templates/logo.png 再复制一份到 assets,安装包里会有两份 1.5MB 大图。
|
||||||
const logoSrc = path.join(templatesDir, "logo.png");
|
// 现在源文件统一放在 public/assets/logo.png(256x256 压缩版),这里只做存在性校验。
|
||||||
if (fs.existsSync(logoSrc)) {
|
if (!fs.existsSync(path.join(assetsDir, "logo.png"))) {
|
||||||
fs.copyFileSync(logoSrc, path.join(assetsDir, "logo.png"));
|
console.error("[postbuild] 未找到 templates/assets/logo.png(public/assets/logo.png 应随构建复制)");
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
console.log("[postbuild] logo.png 已去重,仅保留 templates/assets/logo.png");
|
||||||
|
|
||||||
|
// 1b. 第三方字体许可证全文必须随构建产物分发(审核指南 5.2:许可证传递 / 版权声明保留)
|
||||||
|
const oflFile = path.join(assetsDir, "fonts", "LXGWWenKai", "OFL.txt");
|
||||||
|
if (!fs.existsSync(oflFile)) {
|
||||||
|
console.error(
|
||||||
|
"[postbuild] 未找到 templates/assets/fonts/LXGWWenKai/OFL.txt" +
|
||||||
|
"(public/assets/fonts/LXGWWenKai/OFL.txt 应随构建复制)"
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log("[postbuild] LXGW WenKai 的 OFL 1.1 许可证全文已随包");
|
||||||
|
|
||||||
// 2. 历史资源合并(已存在的同名文件以当前构建为准,不覆盖)
|
// 2. 历史资源合并(已存在的同名文件以当前构建为准,不覆盖)
|
||||||
if (fs.existsSync(compatDir)) {
|
if (fs.existsSync(compatDir)) {
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* 结构化数据 / 内容渲染安全冒烟测试(审核指南 4.4.2)
|
||||||
|
*
|
||||||
|
* 从构建产物 templates/post.html 提取 JSON-LD 生成脚本,用桩 DOM 执行,验证:
|
||||||
|
* - 输出的 JSON-LD 合法、字段完整、可选字段缺省时不产生非法 JSON
|
||||||
|
* - 标题里注入的脚本闭合标签只作为普通字符串保留,不会变成可执行内容
|
||||||
|
* - 写入方式为 textContent,且模板中的 JSON-LD 标签初始为空
|
||||||
|
*
|
||||||
|
* 仅用 Node 内置模块,无浏览器 / 第三方依赖;需先执行 pnpm build。
|
||||||
|
*/
|
||||||
|
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 postHtmlPath = path.join(root, "templates", "post.html");
|
||||||
|
|
||||||
|
if (!fs.existsSync(postHtmlPath)) {
|
||||||
|
console.error("[jsonld-smoke] 未找到 templates/post.html,请先执行 pnpm build");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = fs.readFileSync(postHtmlPath, "utf8");
|
||||||
|
const blocks = [...html.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)].map((m) => m[1]);
|
||||||
|
const code = blocks.find((block) => block.includes("wi-article-jsonld"));
|
||||||
|
if (!code) {
|
||||||
|
console.error("[jsonld-smoke] 未找到 JSON-LD 生成脚本");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let pass = 0;
|
||||||
|
let fail = 0;
|
||||||
|
|
||||||
|
function check(name, fn) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
console.log(` ✔ ${name}`);
|
||||||
|
pass++;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(` ✘ ${name} -> ${error.message}`);
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(values) {
|
||||||
|
const target = { textContent: "" };
|
||||||
|
const document = {
|
||||||
|
querySelector: (selector) =>
|
||||||
|
values[selector] == null ? null : { getAttribute: () => values[selector] },
|
||||||
|
getElementById: (id) => (id === "wi-article-jsonld" ? target : null),
|
||||||
|
title: "fallback title",
|
||||||
|
};
|
||||||
|
let executed = false;
|
||||||
|
vm.runInNewContext(
|
||||||
|
code,
|
||||||
|
{ document, JSON, console, eval: () => (executed = true) },
|
||||||
|
{ timeout: 1000 }
|
||||||
|
);
|
||||||
|
return { raw: target.textContent, executed };
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = {
|
||||||
|
'meta[property="og:title"]': "普通标题",
|
||||||
|
'meta[property="og:url"]': "https://example.com/archives/a",
|
||||||
|
'meta[property="article:published_time"]': "2026-11-11",
|
||||||
|
'meta[name="author"]': "暖心向阳335",
|
||||||
|
'meta[property="og:site_name"]': "WarmIsland",
|
||||||
|
'meta[property="og:image"]': "https://example.com/cover.png",
|
||||||
|
'meta[property="og:description"]': '含"引号"与\n换行的摘要',
|
||||||
|
};
|
||||||
|
|
||||||
|
check("正常文章生成合法 JSON-LD", () => {
|
||||||
|
const data = JSON.parse(render(base).raw);
|
||||||
|
if (data["@type"] !== "Article" || data.headline !== "普通标题") {
|
||||||
|
throw new Error("标题 / 类型不符");
|
||||||
|
}
|
||||||
|
if (data.author.name !== "暖心向阳335") throw new Error("作者不符");
|
||||||
|
if (!data.description.includes("\n")) throw new Error("换行丢失");
|
||||||
|
if (data.image !== base['meta[property="og:image"]']) throw new Error("封面丢失");
|
||||||
|
});
|
||||||
|
|
||||||
|
check("模板里的 JSON-LD 标签初始为空(动态内容不经 HTML 解析)", () => {
|
||||||
|
const match = html.match(/<script\b[^>]*application\/ld\+json[^>]*>([\s\S]*?)<\/script>/i);
|
||||||
|
if (!match) throw new Error("未找到 JSON-LD 标签");
|
||||||
|
if (match[1].trim() !== "") throw new Error("标签内仍有静态 / 拼接内容,可能被解析为 HTML");
|
||||||
|
});
|
||||||
|
|
||||||
|
check("脚本只用 textContent 写入,不含 innerHTML / document.write / eval", () => {
|
||||||
|
if (!code.includes(".textContent = JSON.stringify")) throw new Error("未使用 textContent 安全赋值");
|
||||||
|
if (/innerHTML|document\.write|\beval\s*\(/.test(code)) throw new Error("存在不安全写入方式");
|
||||||
|
});
|
||||||
|
|
||||||
|
check("标题含脚本闭合标签时只作为 JSON 字符串保留,不产生动态执行", () => {
|
||||||
|
const injected = "</" + "script><img src=x onerror=alert(1)>";
|
||||||
|
const result = render({ ...base, 'meta[property="og:title"]': injected });
|
||||||
|
const data = JSON.parse(result.raw);
|
||||||
|
if (data.headline !== injected || typeof data.headline !== "string") {
|
||||||
|
throw new Error("标题被篡改或不是纯字符串");
|
||||||
|
}
|
||||||
|
if (result.executed) throw new Error("发生动态执行");
|
||||||
|
});
|
||||||
|
|
||||||
|
check("缺失可选字段时不产生 undefined / 非法 JSON", () => {
|
||||||
|
const sparse = { ...base };
|
||||||
|
delete sparse['meta[property="og:image"]'];
|
||||||
|
delete sparse['meta[property="og:description"]'];
|
||||||
|
const data = JSON.parse(render(sparse).raw);
|
||||||
|
if ("image" in data || "description" in data) throw new Error("空字段未剔除");
|
||||||
|
});
|
||||||
|
|
||||||
|
check("全部 meta 缺失时仍输出合法 JSON", () => {
|
||||||
|
const data = JSON.parse(render({}).raw);
|
||||||
|
if (data["@context"] !== "https://schema.org" || data.author.name !== "") {
|
||||||
|
throw new Error("兜底失败");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[jsonld-smoke] 内容渲染安全 ${pass} 项通过 / ${fail} 项失败`);
|
||||||
|
process.exit(fail ? 1 : 0);
|
||||||
+14
-11
@@ -36,7 +36,7 @@ spec:
|
|||||||
id: welcome_popup_enabled
|
id: welcome_popup_enabled
|
||||||
label: 启用欢迎弹窗
|
label: 启用欢迎弹窗
|
||||||
value: false
|
value: false
|
||||||
help: 首页显示"欢迎来自 XX·XX·XX 的朋友"的浮动提示弹窗,基于访客 IP 地理定位
|
help: 首页显示浮动欢迎提示;默认只显示本地静态欢迎语,不发起任何外部请求
|
||||||
- $formkit: select
|
- $formkit: select
|
||||||
name: welcome_popup_position
|
name: welcome_popup_position
|
||||||
id: welcome_popup_position
|
id: welcome_popup_position
|
||||||
@@ -67,16 +67,16 @@ spec:
|
|||||||
- $formkit: url
|
- $formkit: url
|
||||||
name: welcome_popup_api
|
name: welcome_popup_api
|
||||||
id: welcome_popup_api
|
id: welcome_popup_api
|
||||||
label: IP 定位主 API
|
label: 自定义 IP 定位主 API(可选)
|
||||||
value: "https://myip.ipip.net/json"
|
value: ""
|
||||||
help: 返回 JSON 的 IP 定位主接口,默认 IPIP;留空使用默认值。支持 IPIP/腾讯/ipapi.co 等常见格式
|
help: 留空则只显示本地欢迎语。可选接口 https://myip.ipip.net/json 或 https://r.inews.qq.com/api/ip2city?otype=json
|
||||||
if: "$get(welcome_popup_enabled).value === true"
|
if: "$get(welcome_popup_enabled).value === true"
|
||||||
- $formkit: url
|
- $formkit: url
|
||||||
name: welcome_popup_fallback_api
|
name: welcome_popup_fallback_api
|
||||||
id: welcome_popup_fallback_api
|
id: welcome_popup_fallback_api
|
||||||
label: IP 定位备选 API
|
label: 自定义 IP 定位备选 API(可选)
|
||||||
value: "https://r.inews.qq.com/api/ip2city?otype=json"
|
value: ""
|
||||||
help: 主接口失败时尝试的备选接口,默认腾讯 ip2city;留空则不使用备选
|
help: 留空不使用备选;可填 https://myip.ipip.net/json 或 https://r.inews.qq.com/api/ip2city?otype=json 作为主接口的备选
|
||||||
if: "$get(welcome_popup_enabled).value === true"
|
if: "$get(welcome_popup_enabled).value === true"
|
||||||
- $formkit: switch
|
- $formkit: switch
|
||||||
name: welcome_popup_show_once
|
name: welcome_popup_show_once
|
||||||
@@ -275,11 +275,13 @@ spec:
|
|||||||
value: hitokoto
|
value: hitokoto
|
||||||
- label: 自定义
|
- label: 自定义
|
||||||
value: custom
|
value: custom
|
||||||
value: hitokoto
|
value: none
|
||||||
- $formkit: url
|
- $formkit: url
|
||||||
name: hero_hitokoto_api
|
name: hero_hitokoto_api
|
||||||
label: 一言 API 地址
|
label: 自定义一言 API(可选)
|
||||||
value: https://v1.hitokoto.cn/
|
value: "https://v1.hitokoto.cn/"
|
||||||
|
if: "$get(hero_description_mode).value !== 'none'"
|
||||||
|
help: 默认预填官方接口 https://v1.hitokoto.cn/(仅「一言」模式生效);选择「一言」或「自定义」后若留空,则使用主题内置的本地句子库(零外部请求)
|
||||||
- $formkit: checkbox
|
- $formkit: checkbox
|
||||||
name: hero_hitokoto_categories
|
name: hero_hitokoto_categories
|
||||||
label: 一言句子类型
|
label: 一言句子类型
|
||||||
@@ -501,6 +503,7 @@ spec:
|
|||||||
- label: Cold
|
- label: Cold
|
||||||
value: cold
|
value: cold
|
||||||
value: warm
|
value: warm
|
||||||
|
help: 仅切换主题自托管的本地代码配色样式;语法高亮本身由 Halo 应用市场的代码高亮插件或「API 扩展包」服务端高亮提供,主题不再加载任何远程高亮脚本
|
||||||
- $formkit: switch
|
- $formkit: switch
|
||||||
name: article_extra_spacing
|
name: article_extra_spacing
|
||||||
label: 中英文混排自动加空格
|
label: 中英文混排自动加空格
|
||||||
@@ -581,7 +584,7 @@ spec:
|
|||||||
- $formkit: attachment
|
- $formkit: attachment
|
||||||
name: moments_cover
|
name: moments_cover
|
||||||
label: 顶部封面图
|
label: 顶部封面图
|
||||||
help: 朋友圈样式顶部的封面图片,建议尺寸 1500×500
|
help: 瞬间页顶部的封面图片,建议尺寸 1500×500
|
||||||
- $formkit: select
|
- $formkit: select
|
||||||
name: moments_loading_mode
|
name: moments_loading_mode
|
||||||
label: 加载方式
|
label: 加载方式
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
th:unless="${link.spec.logo}"
|
th:unless="${link.spec.logo}"
|
||||||
class="wi-friends__avatar wi-friends__avatar--placeholder"
|
class="wi-friends__avatar wi-friends__avatar--placeholder"
|
||||||
>
|
>
|
||||||
<span th:text="${#strings.substring(link.spec.displayName, 0, 1)}"></span>
|
<span th:text="${!#strings.isEmpty(link.spec.displayName) ? #strings.substring(link.spec.displayName, 0, 1) : '·'}"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="wi-friends__info">
|
<div class="wi-friends__info">
|
||||||
<span class="wi-friends__name" th:text="${link.spec.displayName}"></span>
|
<span class="wi-friends__name" th:text="${link.spec.displayName}"></span>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
class="hero__description"
|
class="hero__description"
|
||||||
th:if="${theme.config?.hero?.hero_description_mode == 'hitokoto'}"
|
th:if="${theme.config?.hero?.hero_description_mode == 'hitokoto'}"
|
||||||
id="wi-hitokoto"
|
id="wi-hitokoto"
|
||||||
th:attr="data-api=${theme.config?.hero?.hero_hitokoto_api ?: 'https://v1.hitokoto.cn/'},data-categories=${theme.config?.hero?.hero_hitokoto_categories},data-auto-refresh=${theme.config?.hero?.hero_hitokoto_auto_refresh == true ? 'true' : 'false'},data-refresh-interval=${theme.config?.hero?.hero_hitokoto_refresh_interval ?: 10000}"
|
th:attr="data-api=${theme.config?.hero?.hero_hitokoto_api ?: ''},data-categories=${theme.config?.hero?.hero_hitokoto_categories},data-auto-refresh=${theme.config?.hero?.hero_hitokoto_auto_refresh == true ? 'true' : 'false'},data-refresh-interval=${theme.config?.hero?.hero_hitokoto_refresh_interval ?: 10000}"
|
||||||
></p>
|
></p>
|
||||||
<p
|
<p
|
||||||
class="hero__description"
|
class="hero__description"
|
||||||
@@ -125,7 +125,10 @@
|
|||||||
var el = document.getElementById("wi-hitokoto");
|
var el = document.getElementById("wi-hitokoto");
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
var api = el.getAttribute("data-api") || "https://v1.hitokoto.cn/";
|
/* 默认读取主题内置的本地句子库(同域静态 JSON,零第三方请求)。
|
||||||
|
仅当站点管理员在主题设置里显式填写自定义 API 时,才会产生外部请求。 */
|
||||||
|
var api = el.getAttribute("data-api");
|
||||||
|
var localUrl = "/themes/warm-island/assets/data/hitokoto.json";
|
||||||
var categories = el.getAttribute("data-categories");
|
var categories = el.getAttribute("data-categories");
|
||||||
var autoRefresh = el.getAttribute("data-auto-refresh") === "true";
|
var autoRefresh = el.getAttribute("data-auto-refresh") === "true";
|
||||||
var refreshInterval = parseInt(el.getAttribute("data-refresh-interval"), 10) || 10000;
|
var refreshInterval = parseInt(el.getAttribute("data-refresh-interval"), 10) || 10000;
|
||||||
@@ -151,15 +154,24 @@
|
|||||||
el.textContent = "\u2026";
|
el.textContent = "\u2026";
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(buildUrl())
|
return fetch(api ? buildUrl() : localUrl)
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then(function (data) {
|
.then(function (data) {
|
||||||
var text = data.hitokoto || "";
|
var text, from;
|
||||||
var from = data.from || data.from_who || "";
|
if (!api && data && Array.isArray(data.sentences) && data.sentences.length) {
|
||||||
if (from) {
|
/* 内置词库:随机取一条,避免每次刷新都是同一句 */
|
||||||
|
var item = data.sentences[Math.floor(Math.random() * data.sentences.length)];
|
||||||
|
text = item.text || "";
|
||||||
|
from = item.from || "";
|
||||||
|
} else {
|
||||||
|
/* 站点显式配置的自定义一言 API */
|
||||||
|
text = data.hitokoto || "";
|
||||||
|
from = data.from || data.from_who || "";
|
||||||
|
}
|
||||||
|
if (from && text) {
|
||||||
text += " \u2014\u2014 " + from;
|
text += " \u2014\u2014 " + from;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,12 @@
|
|||||||
plugins: [lgZoom, lgThumbnail, lgFullscreen],
|
plugins: [lgZoom, lgThumbnail, lgFullscreen],
|
||||||
speed: 280,
|
speed: 280,
|
||||||
mode: 'lg-fade',
|
mode: 'lg-fade',
|
||||||
licenseKey: '0000-0000-0000-0000',
|
/* lightGallery 2.9.0 以 GPLv3 发布,本主题同样采用 GPL-3.0,
|
||||||
|
按其开源许可条款使用(不申请商业授权)。
|
||||||
|
官方示例里的 '0000-0000-000-0000' 会触发
|
||||||
|
"license key is not valid for production use" 警告,
|
||||||
|
这里去掉占位字符串、显式声明许可依据,控制台不再出现该提示。 */
|
||||||
|
licenseKey: 'GPLv3',
|
||||||
download: false,
|
download: false,
|
||||||
counter: true,
|
counter: true,
|
||||||
preload: 2,
|
preload: 2,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
th:unless="${comment.spec.owner.avatar}"
|
th:unless="${comment.spec.owner.avatar}"
|
||||||
class="wi-message-wall__avatar wi-message-wall__avatar--placeholder"
|
class="wi-message-wall__avatar wi-message-wall__avatar--placeholder"
|
||||||
>
|
>
|
||||||
<span th:text="${#strings.substring(comment.spec.owner.displayName, 0, 1)}"></span>
|
<span th:text="${!#strings.isEmpty(comment.spec.owner?.displayName) ? #strings.substring(comment.spec.owner?.displayName, 0, 1) : '·'}"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="wi-message-wall__meta">
|
<div class="wi-message-wall__meta">
|
||||||
<span class="wi-message-wall__name" th:text="${comment.spec.owner.displayName}"></span>
|
<span class="wi-message-wall__name" th:text="${comment.spec.owner.displayName}"></span>
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ const pageConfig = findPageBySlug("moments");
|
|||||||
th:each="moment : ${momentsResult.items}"
|
th:each="moment : ${momentsResult.items}"
|
||||||
>
|
>
|
||||||
<div class="wi-moments-section__dot" th:if="${theme.config?.moments?.moments_style != 'cards'}"></div>
|
<div class="wi-moments-section__dot" th:if="${theme.config?.moments?.moments_style != 'cards'}"></div>
|
||||||
<div class="wi-moments-section__content" th:utext="${moment.spec?.content?.html ?: moment.spec?.content?.raw ?: moment.spec?.content}"></div>
|
<div class="wi-moments-section__content" th:if="${moment.spec?.content?.html}" th:utext="${moment.spec.content.html}"></div>
|
||||||
|
<div class="wi-moments-section__content" th:unless="${moment.spec?.content?.html}" th:text="${moment.spec?.content?.raw ?: ''}"></div>
|
||||||
<time
|
<time
|
||||||
class="wi-moments-section__date"
|
class="wi-moments-section__date"
|
||||||
th:text="${#dates.format(moment.spec?.releaseTime ?: moment.metadata?.creationTimestamp, 'yyyy-MM-dd HH:mm')}"
|
th:text="${#dates.format(moment.spec?.releaseTime ?: moment.metadata?.creationTimestamp, 'yyyy-MM-dd HH:mm')}"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
class="wi-welcome-popup"
|
class="wi-welcome-popup"
|
||||||
id="wi-welcome-popup"
|
id="wi-welcome-popup"
|
||||||
th:if="${theme.config?.welcome_popup?.welcome_popup_enabled == true}"
|
th:if="${theme.config?.welcome_popup?.welcome_popup_enabled == true}"
|
||||||
th:attr="data-position=${theme.config?.welcome_popup?.welcome_popup_position ?: 'top-center'},data-duration=${theme.config?.welcome_popup?.welcome_popup_duration ?: 5},data-api=${theme.config?.welcome_popup?.welcome_popup_api},data-fallback-api=${theme.config?.welcome_popup?.welcome_popup_fallback_api},data-show-once=${theme.config?.welcome_popup?.welcome_popup_show_once != false ? 'true' : 'false'}"
|
th:attr="data-position=${theme.config?.welcome_popup?.welcome_popup_position ?: 'top-center'},data-duration=${theme.config?.welcome_popup?.welcome_popup_duration ?: 5},data-api=${theme.config?.welcome_popup?.welcome_popup_api ?: ''},data-fallback-api=${theme.config?.welcome_popup?.welcome_popup_fallback_api ?: ''},data-show-once=${theme.config?.welcome_popup?.welcome_popup_show_once != false ? 'true' : 'false'}"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
aria-atomic="true"
|
aria-atomic="true"
|
||||||
>
|
>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<circle cx="12" cy="10" r="3"></circle>
|
<circle cx="12" cy="10" r="3"></circle>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="wi-welcome-popup__content" id="wi-welcome-popup-text">加载中…</div>
|
<div class="wi-welcome-popup__content" id="wi-welcome-popup-text">欢迎来访,愿你在这里度过一段安静的时光</div>
|
||||||
<button class="wi-welcome-popup__close" type="button" aria-label="关闭">
|
<button class="wi-welcome-popup__close" type="button" aria-label="关闭">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M18 6 6 18"></path>
|
<path d="M18 6 6 18"></path>
|
||||||
@@ -46,8 +46,12 @@
|
|||||||
var closeBtn = popup.querySelector(".wi-welcome-popup__close");
|
var closeBtn = popup.querySelector(".wi-welcome-popup__close");
|
||||||
var progress = document.getElementById("wi-welcome-popup-progress");
|
var progress = document.getElementById("wi-welcome-popup-progress");
|
||||||
var duration = parseInt(popup.getAttribute("data-duration"), 10) || 5;
|
var duration = parseInt(popup.getAttribute("data-duration"), 10) || 5;
|
||||||
var api = popup.getAttribute("data-api") || "https://myip.ipip.net/json";
|
/* 默认零外部请求:主题内置静态欢迎语,不向任何第三方发送访客 IP。
|
||||||
var fallbackApi = popup.getAttribute("data-fallback-api") || "https://r.inews.qq.com/api/ip2city?otype=json";
|
仅当站点管理员在主题设置里显式填写自定义 IP 定位 API 时才会 fetch;
|
||||||
|
一旦填写,浏览器会向该服务发送访客 IP,需自行在站点隐私说明中披露
|
||||||
|
(详见 README「外部资源与隐私」)。 */
|
||||||
|
var api = popup.getAttribute("data-api") || "";
|
||||||
|
var fallbackApi = popup.getAttribute("data-fallback-api") || "";
|
||||||
|
|
||||||
var hideTimer = null;
|
var hideTimer = null;
|
||||||
var progressRaf = null;
|
var progressRaf = null;
|
||||||
@@ -91,6 +95,14 @@
|
|||||||
hideTimer = setTimeout(hidePopup, duration * 1000);
|
hideTimer = setTimeout(hidePopup, duration * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderWelcome() {
|
||||||
|
if (textEl) {
|
||||||
|
textEl.textContent = "欢迎来访,愿你在这里度过一段安静的时光";
|
||||||
|
}
|
||||||
|
popup.classList.add("wi-welcome-popup--visible");
|
||||||
|
startAutoHide();
|
||||||
|
}
|
||||||
|
|
||||||
function renderMessage(location) {
|
function renderMessage(location) {
|
||||||
var loc = (location || "").trim();
|
var loc = (location || "").trim();
|
||||||
if (!loc) loc = "未知星球";
|
if (!loc) loc = "未知星球";
|
||||||
@@ -102,26 +114,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fetchLocation() {
|
function fetchLocation() {
|
||||||
if (textEl) {
|
|
||||||
textEl.textContent = "加载中…";
|
|
||||||
}
|
|
||||||
popup.classList.add("wi-welcome-popup--visible");
|
popup.classList.add("wi-welcome-popup--visible");
|
||||||
|
|
||||||
/* 按顺序尝试主 API -> 备选 API */
|
/* 按顺序尝试主 API -> 备选 API;两者都为空则完全不发外部请求 */
|
||||||
var endpoints = [];
|
var endpoints = [];
|
||||||
if (api) endpoints.push(api);
|
if (api) endpoints.push(api);
|
||||||
if (fallbackApi && fallbackApi !== api) endpoints.push(fallbackApi);
|
if (fallbackApi && fallbackApi !== api) endpoints.push(fallbackApi);
|
||||||
|
|
||||||
if (endpoints.length === 0) {
|
if (endpoints.length === 0) {
|
||||||
renderMessage("");
|
renderWelcome();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (textEl) textEl.textContent = "正在获取位置…";
|
||||||
var idx = 0;
|
var idx = 0;
|
||||||
|
|
||||||
function tryNext() {
|
function tryNext() {
|
||||||
if (idx >= endpoints.length) {
|
if (idx >= endpoints.length) {
|
||||||
renderMessage("");
|
renderWelcome();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var url = endpoints[idx++];
|
var url = endpoints[idx++];
|
||||||
@@ -167,15 +177,14 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 解析多种 IP API 的返回格式:
|
/* 解析常见 IP 定位接口的返回格式(仅在管理员显式配置自定义 API 时才会用到):
|
||||||
- IPIP: {"ret":"ok","data":{"location":["中国","河北","张家口","","联通"]}}
|
- { data: { location: [...] } }
|
||||||
- 腾讯 ip2city: {"ret":0,"province":"河北","city":"张家口",...}
|
- { province, city, ... } / { regionName, city, ... }
|
||||||
- ipapi.co / ip-api.com: {"country":"China","regionName":"Hebei","city":"Zhangjiakou"}
|
- { pro, city, ... } */
|
||||||
- 太平洋: {"pro":"广东省","city":"广州市",...} */
|
|
||||||
function parseLocation(data) {
|
function parseLocation(data) {
|
||||||
if (!data) return "";
|
if (!data) return "";
|
||||||
|
|
||||||
/* IPIP: data.data.location 数组 */
|
/* data.location 数组格式 */
|
||||||
if (data.data && Array.isArray(data.data.location)) {
|
if (data.data && Array.isArray(data.data.location)) {
|
||||||
var loc = data.data.location
|
var loc = data.data.location
|
||||||
.filter(function (s) {
|
.filter(function (s) {
|
||||||
|
|||||||
@@ -86,8 +86,8 @@ const fragmentAttr = layoutFragment ? "html (head, content)" : undefined;
|
|||||||
th:href="${theme.config?.basic?.favicon ?: '/favicon.ico'}"
|
th:href="${theme.config?.basic?.favicon ?: '/favicon.ico'}"
|
||||||
/>
|
/>
|
||||||
<link rel="alternate" type="application/rss+xml" th:title="${site.title}" th:href="@{/feed.xml}" />
|
<link rel="alternate" type="application/rss+xml" th:title="${site.title}" th:href="@{/feed.xml}" />
|
||||||
<link rel="preconnect" href="https://cdn.jsdelivr.net" />
|
<!-- 主题不预连接任何第三方 CDN:所有脚本、样式、字体均随主题自托管。
|
||||||
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net" />
|
分享链接等外链只在访客主动点击时打开,不产生自动外部请求。 -->
|
||||||
<link rel="stylesheet" th:href="@{/assets/fonts/LXGWWenKai/result.css}" media="print" onload="this.media='all'" />
|
<link rel="stylesheet" th:href="@{/assets/fonts/LXGWWenKai/result.css}" media="print" onload="this.media='all'" />
|
||||||
<noscript><link rel="stylesheet" th:href="@{/assets/fonts/LXGWWenKai/result.css}" /></noscript>
|
<noscript><link rel="stylesheet" th:href="@{/assets/fonts/LXGWWenKai/result.css}" /></noscript>
|
||||||
<style is:inline th:if="${theme.config?.style?.accent_color}" th:utext="${':root { --accent: ' + theme.config?.style?.accent_color + '; --accent-hover: ' + theme.config?.style?.accent_color + '; }'}">/*accent*/</style>
|
<style is:inline th:if="${theme.config?.style?.accent_color}" th:utext="${':root { --accent: ' + theme.config?.style?.accent_color + '; --accent-hover: ' + theme.config?.style?.accent_color + '; }'}">/*accent*/</style>
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ import Layout from "../layouts/Layout.astro";
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="wi-douban__poster wi-douban__poster--placeholder" th:if="${#strings.isEmpty(item.poster)}">
|
<div class="wi-douban__poster wi-douban__poster--placeholder" th:if="${#strings.isEmpty(item.poster)}">
|
||||||
<span th:text="${#strings.substring(item.name ?: '', 0, 1)}"></span>
|
<span th:text="${!#strings.isEmpty(item.name) ? #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.name}"></h3>
|
<h3 class="wi-douban__name" th:text="${item.name}"></h3>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import Layout from "../layouts/Layout.astro";
|
|||||||
class="wi-equipments__cover wi-equipments__cover--placeholder"
|
class="wi-equipments__cover wi-equipments__cover--placeholder"
|
||||||
th:if="${#strings.isEmpty(equip.spec?.cover)}"
|
th:if="${#strings.isEmpty(equip.spec?.cover)}"
|
||||||
>
|
>
|
||||||
<span th:text="${#strings.substring(equip.spec?.displayName ?: '', 0, 1)}"></span>
|
<span th:text="${!#strings.isEmpty(equip.spec?.displayName) ? #strings.substring(equip.spec?.displayName, 0, 1) : '·'}"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="wi-equipments__body">
|
<div class="wi-equipments__body">
|
||||||
<h3
|
<h3
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import Layout from "../layouts/Layout.astro";
|
|||||||
class="wi-friends-page__avatar wi-friends-page__avatar--placeholder"
|
class="wi-friends-page__avatar wi-friends-page__avatar--placeholder"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
th:text="${#strings.substring(spec.author ?: '', 0, 1)}"
|
th:text="${!#strings.isEmpty(spec.author) ? #strings.substring(spec.author, 0, 1) : '·'}"
|
||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="wi-friends-page__author-info">
|
<div class="wi-friends-page__author-info">
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ import Layout from "../layouts/Layout.astro";
|
|||||||
<span class="wi-id__avatar">
|
<span class="wi-id__avatar">
|
||||||
<span
|
<span
|
||||||
class="wi-id__avatar-fallback"
|
class="wi-id__avatar-fallback"
|
||||||
th:text="${#strings.substring(p.displayName, 0, 1)}"
|
th:text="${!#strings.isEmpty(p.displayName) ? #strings.substring(p.displayName, 0, 1) : '·'}"
|
||||||
>老</span
|
>老</span
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -407,7 +407,7 @@ import Layout from "../layouts/Layout.astro";
|
|||||||
<span class="wi-id__avatar">
|
<span class="wi-id__avatar">
|
||||||
<span
|
<span
|
||||||
class="wi-id__avatar-fallback"
|
class="wi-id__avatar-fallback"
|
||||||
th:text="${#strings.substring(p.displayName, 0, 1)}"
|
th:text="${!#strings.isEmpty(p.displayName) ? #strings.substring(p.displayName, 0, 1) : '·'}"
|
||||||
>老</span
|
>老</span
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -466,7 +466,7 @@ import Layout from "../layouts/Layout.astro";
|
|||||||
<span class="wi-id__avatar">
|
<span class="wi-id__avatar">
|
||||||
<span
|
<span
|
||||||
class="wi-id__avatar-fallback"
|
class="wi-id__avatar-fallback"
|
||||||
th:text="${#strings.substring(p.displayName, 0, 1)}"
|
th:text="${!#strings.isEmpty(p.displayName) ? #strings.substring(p.displayName, 0, 1) : '·'}"
|
||||||
>张</span
|
>张</span
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ import Layout from "../layouts/Layout.astro";
|
|||||||
class="wi-links__avatar wi-links__avatar--placeholder"
|
class="wi-links__avatar wi-links__avatar--placeholder"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
th:text="${#strings.substring(link.spec.displayName, 0, 1)}"
|
th:text="${!#strings.isEmpty(link.spec.displayName) ? #strings.substring(link.spec.displayName, 0, 1) : '·'}"
|
||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 可达性检测徽章(plugin-links 后台「友链检测」产生):绿点=可访问,红点=异常,未检测不显示 -->
|
<!-- 可达性检测徽章(plugin-links 后台「友链检测」产生):绿点=可访问,红点=异常,未检测不显示 -->
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import LightGallery from "../components/LightGallery.astro";
|
|||||||
class="wi-moment-detail__avatar wi-moment-detail__avatar--fallback"
|
class="wi-moment-detail__avatar wi-moment-detail__avatar--fallback"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<span th:text="${#strings.substring(site.title, 0, 1)}"></span>
|
<span th:text="${!#strings.isEmpty(site.title) ? #strings.substring(site.title, 0, 1) : '·'}"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="wi-moment-detail__main">
|
<div class="wi-moment-detail__main">
|
||||||
@@ -59,7 +59,8 @@ import LightGallery from "../components/LightGallery.astro";
|
|||||||
></time>
|
></time>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="wi-moment-detail__text" th:utext="${detailContent?.html ?: detailContent?.raw ?: ''}"></div>
|
<div class="wi-moment-detail__text" th:if="${detailContent?.html}" th:utext="${detailContent.html}"></div>
|
||||||
|
<div class="wi-moment-detail__text" th:unless="${detailContent?.html}" th:text="${detailContent?.raw ?: ''}"></div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="wi-moment-detail__media wi-moments-page__card-media"
|
class="wi-moment-detail__media wi-moments-page__card-media"
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import LightGallery from "../components/LightGallery.astro";
|
|||||||
class="wi-moments-page__avatar wi-moments-page__avatar--fallback"
|
class="wi-moments-page__avatar wi-moments-page__avatar--fallback"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<span th:text="${#strings.substring(site.title, 0, 1)}"></span>
|
<span th:text="${!#strings.isEmpty(site.title) ? #strings.substring(site.title, 0, 1) : '·'}"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="wi-moments-page__main">
|
<div class="wi-moments-page__main">
|
||||||
@@ -53,7 +53,8 @@ import LightGallery from "../components/LightGallery.astro";
|
|||||||
class="wi-moments-page__name"
|
class="wi-moments-page__name"
|
||||||
th:text="${moment.owner != null and !#strings.isEmpty(moment.owner.displayName) ? moment.owner.displayName : site.title}"
|
th:text="${moment.owner != null and !#strings.isEmpty(moment.owner.displayName) ? moment.owner.displayName : site.title}"
|
||||||
></div>
|
></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:if="${moment.spec?.content?.html}" th:utext="${moment.spec.content.html}"></div>
|
||||||
|
<div class="wi-moments-page__card-text" th:unless="${moment.spec?.content?.html}" th:text="${moment.spec?.content?.raw ?: ''}"></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}">
|
||||||
<img
|
<img
|
||||||
@@ -111,7 +112,8 @@ import LightGallery from "../components/LightGallery.astro";
|
|||||||
<div class="wi-moments-page__masonry" th:if="${theme.config?.moments?.moments_style == 'masonry'}">
|
<div class="wi-moments-page__masonry" th:if="${theme.config?.moments?.moments_style == 'masonry'}">
|
||||||
<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__masonry-item" th:each="moment : ${momentsResult.items}">
|
<div class="wi-moments-page__masonry-item" th:each="moment : ${momentsResult.items}">
|
||||||
<div class="wi-moments-page__masonry-text" th:utext="${moment.spec?.content?.html ?: moment.spec?.content?.raw ?: moment.spec?.content}"></div>
|
<div class="wi-moments-page__masonry-text" th:if="${moment.spec?.content?.html}" th:utext="${moment.spec.content.html}"></div>
|
||||||
|
<div class="wi-moments-page__masonry-text" th:unless="${moment.spec?.content?.html}" th:text="${moment.spec?.content?.raw ?: ''}"></div>
|
||||||
<div class="wi-moments-page__masonry-media" th:if="${moment.spec.content.medium != null and !#lists.isEmpty(moment.spec.content.medium)}">
|
<div class="wi-moments-page__masonry-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}">
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import Layout from "../layouts/Layout.astro";
|
|||||||
订阅
|
订阅
|
||||||
</h1> <p class="wi-subs__subtitle" th:text="${theme.config?.subscriptions?.subscriptions_subtitle ?: '博友们的最新文章'}">
|
</h1> <p class="wi-subs__subtitle" th:text="${theme.config?.subscriptions?.subscriptions_subtitle ?: '博友们的最新文章'}">
|
||||||
博友们的最新文章
|
博友们的最新文章
|
||||||
</p> <span class="wi-page-accent"></span> </div> <th:block th:if="${feedPage != null and !#lists.isEmpty(feedPage.items)}"> <div class="wi-subs__flow"> <article class="wi-subs__card" th:each="item : ${feedPage.items}" th:with="authorName=${item.author ?: '匿名'}"> <div class="wi-subs__author"> <img th:if="${item.authorLogo}" th:src="${item.authorLogo}" th:alt="${authorName}" class="wi-subs__avatar" loading="lazy" decoding="async"> <div th:unless="${item.authorLogo}" class="wi-subs__avatar wi-subs__avatar--placeholder"> <span th:text="${#strings.substring(authorName, 0, 1)}"></span> </div> <div class="wi-subs__author-info"> <a th:if="${item.authorUrl}" th:href="${item.authorUrl}" target="_blank" rel="noopener noreferrer" class="wi-subs__author-name" th:text="${authorName}"></a> <span th:unless="${item.authorUrl}" class="wi-subs__author-name" th:text="${authorName}"></span> <time class="wi-subs__date" th:if="${item.publishedAt}" th:text="${#temporals.format(item.publishedAt, 'yyyy-MM-dd')}"></time> </div> </div> <div class="wi-subs__body"> <a th:href="${item.url}" target="_blank" rel="noopener noreferrer" class="wi-subs__title-link"> <h2 class="wi-subs__post-title" th:text="${item.title}"></h2> </a> <p class="wi-subs__summary" th:if="${item.summary}" th:text="${item.summary}"></p> </div> </article> </div> <nav class="wi-subs__pagination" th:if="${loadingMode != 'infinite_scroll' and (hasCursor or feedPage.hasNext)}" th:with="permalink=${singlePage.status.permalink}"> <a th:if="${hasCursor}" th:href="@{${permalink}}" class="wi-subs__page-link" th:text="'较新'">
|
</p> <span class="wi-page-accent"></span> </div> <th:block th:if="${feedPage != null and !#lists.isEmpty(feedPage.items)}"> <div class="wi-subs__flow"> <article class="wi-subs__card" th:each="item : ${feedPage.items}" th:with="authorName=${item.author ?: '匿名'}"> <div class="wi-subs__author"> <img th:if="${item.authorLogo}" th:src="${item.authorLogo}" th:alt="${authorName}" class="wi-subs__avatar" loading="lazy" decoding="async"> <div th:unless="${item.authorLogo}" class="wi-subs__avatar wi-subs__avatar--placeholder"> <span th:text="${!#strings.isEmpty(authorName) ? #strings.substring(authorName, 0, 1) : '·'}"></span> </div> <div class="wi-subs__author-info"> <a th:if="${item.authorUrl}" th:href="${item.authorUrl}" target="_blank" rel="noopener noreferrer" class="wi-subs__author-name" th:text="${authorName}"></a> <span th:unless="${item.authorUrl}" class="wi-subs__author-name" th:text="${authorName}"></span> <time class="wi-subs__date" th:if="${item.publishedAt}" th:text="${#temporals.format(item.publishedAt, 'yyyy-MM-dd')}"></time> </div> </div> <div class="wi-subs__body"> <a th:href="${item.url}" target="_blank" rel="noopener noreferrer" class="wi-subs__title-link"> <h2 class="wi-subs__post-title" th:text="${item.title}"></h2> </a> <p class="wi-subs__summary" th:if="${item.summary}" th:text="${item.summary}"></p> </div> </article> </div> <nav class="wi-subs__pagination" th:if="${loadingMode != 'infinite_scroll' and (hasCursor or feedPage.hasNext)}" th:with="permalink=${singlePage.status.permalink}"> <a th:if="${hasCursor}" th:href="@{${permalink}}" class="wi-subs__page-link" th:text="'较新'">
|
||||||
较新
|
较新
|
||||||
</a> <span th:unless="${hasCursor}" class="wi-subs__page-link wi-subs__page-link--placeholder" aria-hidden="true"></span> <span class="wi-subs__page-info"></span> <a th:if="${feedPage.hasNext}" th:href="@{|${permalink}?beforePublishedAt=${feedPage.nextBeforePublishedAt}&beforeId=${feedPage.nextBeforeId}|}" class="wi-subs__page-link" th:text="'较旧'">
|
</a> <span th:unless="${hasCursor}" class="wi-subs__page-link wi-subs__page-link--placeholder" aria-hidden="true"></span> <span class="wi-subs__page-info"></span> <a th:if="${feedPage.hasNext}" th:href="@{|${permalink}?beforePublishedAt=${feedPage.nextBeforePublishedAt}&beforeId=${feedPage.nextBeforeId}|}" class="wi-subs__page-link" th:text="'较旧'">
|
||||||
较旧
|
较旧
|
||||||
|
|||||||
+99
-82
@@ -18,22 +18,43 @@ import LightGallery from "../components/LightGallery.astro";
|
|||||||
<meta th:if="${post.spec.cover}" name="twitter:image" th:content="${post.spec.cover}" />
|
<meta th:if="${post.spec.cover}" name="twitter:image" th:content="${post.spec.cover}" />
|
||||||
<meta th:if="${post.status.excerpt}" name="twitter:description" th:content="${post.status.excerpt}" />
|
<meta th:if="${post.status.excerpt}" name="twitter:description" th:content="${post.status.excerpt}" />
|
||||||
<meta property="article:published_time" th:content="${#dates.format(post.spec.publishTime, 'yyyy-MM-dd')}" />
|
<meta property="article:published_time" th:content="${#dates.format(post.spec.publishTime, 'yyyy-MM-dd')}" />
|
||||||
|
<meta name="author" th:content="${post.contributors != null and !post.contributors.isEmpty() ? post.contributors[0].displayName : site.title}" />
|
||||||
<th:block th:if="${post.categories != null and !post.categories.isEmpty()}">
|
<th:block th:if="${post.categories != null and !post.categories.isEmpty()}">
|
||||||
<meta property="article:section" th:content="${post.categories[0].spec.displayName}" />
|
<meta property="article:section" th:content="${post.categories[0].spec.displayName}" />
|
||||||
</th:block>
|
</th:block>
|
||||||
<th:block th:if="${post.tags != null and !post.tags.isEmpty()}">
|
<th:block th:if="${post.tags != null and !post.tags.isEmpty()}">
|
||||||
<meta th:each="tag : ${post.tags}" property="article:tag" th:content="${tag.spec.displayName}" />
|
<meta th:each="tag : ${post.tags}" property="article:tag" th:content="${tag.spec.displayName}" />
|
||||||
</th:block>
|
</th:block>
|
||||||
<script type="application/ld+json" th:utext="${'{
|
<!-- 4.4.2:结构化数据不再由模板拼接站点内容后以 th:utext 写进脚本文本。
|
||||||
"@context": "https://schema.org",
|
文章标题、摘要、作者、封面等字段先由 Thymeleaf 转义输出到 <meta>,
|
||||||
"@type": "Article",
|
再由 JSON.stringify + textContent 生成合法 JSON-LD;即使内容里含有
|
||||||
"headline": "' + post.spec.title + '",
|
脚本闭合标签,也只会被当作普通文本,不会被浏览器解析执行。 -->
|
||||||
"datePublished": "' + #dates.format(post.spec.publishTime, "yyyy-MM-dd") + '",
|
<script type="application/ld+json" id="wi-article-jsonld"></script>
|
||||||
"author": {
|
<script is:inline th:inline="none">
|
||||||
"@type": "Person",
|
(function () {
|
||||||
"name": "' + (post.contributors != null and !post.contributors.isEmpty() ? post.contributors[0].displayName : site.title) + '"
|
function meta(selector) {
|
||||||
}' + (!#strings.isEmpty(post.spec.cover) ? ', "image": "' + post.spec.cover + '"' : '') + (!#strings.isEmpty(post.status.excerpt) ? ', "description": "' + #strings.replace(post.status.excerpt, '"', '\\"') + '"' : '') + '
|
var el = document.querySelector(selector);
|
||||||
}'}"></script>
|
return el ? el.getAttribute("content") : null;
|
||||||
|
}
|
||||||
|
var data = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Article",
|
||||||
|
headline: meta('meta[property="og:title"]') || document.title,
|
||||||
|
datePublished: meta('meta[property="article:published_time"]'),
|
||||||
|
author: {
|
||||||
|
"@type": "Person",
|
||||||
|
name: meta('meta[name="author"]') || meta('meta[property="og:site_name"]') || ""
|
||||||
|
},
|
||||||
|
mainEntityOfPage: meta('meta[property="og:url"]')
|
||||||
|
};
|
||||||
|
var image = meta('meta[property="og:image"]');
|
||||||
|
var description = meta('meta[property="og:description"]');
|
||||||
|
if (image) data.image = image;
|
||||||
|
if (description) data.description = description;
|
||||||
|
var target = document.getElementById("wi-article-jsonld");
|
||||||
|
if (target) target.textContent = JSON.stringify(data);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|
||||||
<div class="wi-reading-progress" id="wi-reading-progress"></div>
|
<div class="wi-reading-progress" id="wi-reading-progress"></div>
|
||||||
@@ -194,9 +215,11 @@ import LightGallery from "../components/LightGallery.astro";
|
|||||||
<button class="wi-wechat-qr__close" type="button" aria-label="Close">
|
<button class="wi-wechat-qr__close" type="button" aria-label="Close">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="wi-wechat-qr__title" th:text="'微信扫码分享'">微信扫码分享</div>
|
<div class="wi-wechat-qr__title" th:text="'微信分享'">微信分享</div>
|
||||||
<canvas class="wi-wechat-qr__canvas" id="wi-wechat-qr-canvas"></canvas>
|
<!-- 微信分享不加载任何远程二维码库:改为复制文章链接,由访客在微信内分享 -->
|
||||||
<div class="wi-wechat-qr__hint" th:text="'打开微信,扫描二维码即可分享'">打开微信,扫描二维码即可分享</div>
|
<input class="wi-wechat-qr__url" id="wi-wechat-qr-url" type="text" readonly="readonly" aria-label="文章链接" />
|
||||||
|
<button class="wi-wechat-qr__copy" id="wi-wechat-qr-copy" type="button" th:text="'复制链接'">复制链接</button>
|
||||||
|
<div class="wi-wechat-qr__hint" th:text="'复制链接后,在微信中发送给好友'">复制链接后,在微信中发送给好友</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -295,37 +318,20 @@ import LightGallery from "../components/LightGallery.astro";
|
|||||||
|
|
||||||
<LightGallery />
|
<LightGallery />
|
||||||
|
|
||||||
|
<!-- 代码高亮:主题不再加载远程语法高亮运行时与自动加载插件(后者会按代码语言
|
||||||
|
从 CDN 动态拉取并执行组件,属于远程动态加载)。语法高亮改由应用市场的
|
||||||
|
代码高亮插件或「API 扩展包」服务端高亮输出,主题只自托管两套 token 配色。 -->
|
||||||
<link
|
<link
|
||||||
th:if="${theme.config?.article?.article_code_theme == 'cold'}"
|
th:if="${theme.config?.article?.article_code_theme == 'cold'}"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css"
|
href="/themes/warm-island/assets/vendor/prism-cold.css"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
th:if="${theme.config?.article?.article_code_theme != 'cold'}"
|
th:if="${theme.config?.article?.article_code_theme != 'cold'}"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css"
|
href="/themes/warm-island/assets/vendor/prism-warm.css"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<script th:if="${theme.config?.article?.article_show_share != false and #strings.contains(theme.config?.article?.article_share_channels ?: 'qq,wechat,weibo', 'wechat')}" is:inline defer src="https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js"></script>
|
|
||||||
|
|
||||||
<script is:inline defer src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"></script>
|
|
||||||
<script is:inline defer src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
|
|
||||||
<script is:inline th:inline="none">
|
|
||||||
(function () {
|
|
||||||
if (typeof Prism !== "undefined") {
|
|
||||||
Prism.plugins.autoloader.languages_path = "https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/";
|
|
||||||
var body = document.getElementById("wi-post-body");
|
|
||||||
if (body) {
|
|
||||||
body.querySelectorAll("pre code").forEach(function (block) {
|
|
||||||
if (!block.classList.contains("language-")) {
|
|
||||||
Prism.highlightElement(block);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script is:inline th:inline="none">
|
<script is:inline th:inline="none">
|
||||||
(function () {
|
(function () {
|
||||||
var progressBar = document.getElementById("wi-reading-progress");
|
var progressBar = document.getElementById("wi-reading-progress");
|
||||||
@@ -673,58 +679,45 @@ import LightGallery from "../components/LightGallery.astro";
|
|||||||
|
|
||||||
var wechatBtn = document.querySelector(".wi-post__share-btn--wechat");
|
var wechatBtn = document.querySelector(".wi-post__share-btn--wechat");
|
||||||
var wechatQr = document.getElementById("wi-wechat-qr");
|
var wechatQr = document.getElementById("wi-wechat-qr");
|
||||||
var wechatQrCanvas = document.getElementById("wi-wechat-qr-canvas");
|
var wechatUrlInput = document.getElementById("wi-wechat-qr-url");
|
||||||
|
var wechatCopyBtn = document.getElementById("wi-wechat-qr-copy");
|
||||||
var wechatQrClose = wechatQr ? wechatQr.querySelector(".wi-wechat-qr__close") : null;
|
var wechatQrClose = wechatQr ? wechatQr.querySelector(".wi-wechat-qr__close") : null;
|
||||||
var wechatQrGenerated = false;
|
|
||||||
|
|
||||||
function generateQr(text, canvas) {
|
function resolveShareUrl() {
|
||||||
var size = 200;
|
var rawUrl = (wechatBtn && wechatBtn.getAttribute("data-url")) || window.location.pathname;
|
||||||
var cellSize = 5;
|
return rawUrl.startsWith("http") ? rawUrl : window.location.origin + rawUrl;
|
||||||
var qr = null;
|
}
|
||||||
try {
|
|
||||||
var typeNumber = 0;
|
/* 复制链接后由访客在微信内分享,零远程脚本 */
|
||||||
var errorCorrectionLevel = "M";
|
function copyShareUrl() {
|
||||||
var qrData = [];
|
var url = resolveShareUrl();
|
||||||
for (var i = 0; i < text.length; i++) {
|
if (wechatUrlInput) wechatUrlInput.value = url;
|
||||||
qrData.push(text.charCodeAt(i));
|
var done = function () {
|
||||||
}
|
if (!wechatCopyBtn) return;
|
||||||
qr = qrcode(typeNumber, errorCorrectionLevel);
|
wechatCopyBtn.textContent = "已复制";
|
||||||
qr.addData(text);
|
setTimeout(function () {
|
||||||
qr.make();
|
wechatCopyBtn.textContent = "复制链接";
|
||||||
} catch (e) {
|
}, 1800);
|
||||||
return;
|
};
|
||||||
}
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
var moduleCount = qr.getModuleCount();
|
navigator.clipboard.writeText(url).then(done, function () {
|
||||||
var margin = 4;
|
if (wechatUrlInput) wechatUrlInput.select();
|
||||||
var totalCells = moduleCount + margin * 2;
|
});
|
||||||
var canvasSize = totalCells * cellSize;
|
} else if (wechatUrlInput) {
|
||||||
canvas.width = canvasSize;
|
wechatUrlInput.select();
|
||||||
canvas.height = canvasSize;
|
try {
|
||||||
var ctx = canvas.getContext("2d");
|
document.execCommand("copy");
|
||||||
ctx.fillStyle = "#ffffff";
|
done();
|
||||||
ctx.fillRect(0, 0, canvasSize, canvasSize);
|
} catch (e) {}
|
||||||
ctx.fillStyle = "#000000";
|
|
||||||
for (var row = 0; row < moduleCount; row++) {
|
|
||||||
for (var col = 0; col < moduleCount; col++) {
|
|
||||||
if (qr.isDark(row, col)) {
|
|
||||||
ctx.fillRect((col + margin) * cellSize, (row + margin) * cellSize, cellSize, cellSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wechatBtn && wechatQr) {
|
if (wechatBtn && wechatQr) {
|
||||||
wechatBtn.addEventListener("click", function () {
|
wechatBtn.addEventListener("click", function () {
|
||||||
wechatQr.classList.add("wi-wechat-qr--visible");
|
wechatQr.classList.add("wi-wechat-qr--visible");
|
||||||
if (!wechatQrGenerated && wechatQrCanvas) {
|
if (wechatUrlInput) wechatUrlInput.value = resolveShareUrl();
|
||||||
var rawUrl = wechatBtn.getAttribute("data-url") || window.location.pathname;
|
|
||||||
var url = rawUrl.startsWith("http") ? rawUrl : window.location.origin + rawUrl;
|
|
||||||
if (typeof qrcode !== "undefined") {
|
|
||||||
generateQr(url, wechatQrCanvas);
|
|
||||||
wechatQrGenerated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
if (wechatCopyBtn) wechatCopyBtn.addEventListener("click", copyShareUrl);
|
||||||
if (wechatQrClose) {
|
if (wechatQrClose) {
|
||||||
wechatQrClose.addEventListener("click", function () {
|
wechatQrClose.addEventListener("click", function () {
|
||||||
wechatQr.classList.remove("wi-wechat-qr--visible");
|
wechatQr.classList.remove("wi-wechat-qr--visible");
|
||||||
@@ -1255,12 +1248,36 @@ import LightGallery from "../components/LightGallery.astro";
|
|||||||
margin-bottom: var(--space-md);
|
margin-bottom: var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wi-wechat-qr__canvas {
|
.wi-wechat-qr__url {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto var(--space-md);
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0 0 var(--space-sm, 0.5rem);
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--rule);
|
||||||
border-radius: var(--radius-sm, 8px);
|
border-radius: var(--radius-sm, 8px);
|
||||||
background: #fff;
|
background: var(--bg-raised);
|
||||||
padding: 8px;
|
color: var(--ink);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-wechat-qr__copy {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 0 var(--space-md);
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-sm, 8px);
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-wechat-qr__copy:hover {
|
||||||
|
opacity: 0.88;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wi-wechat-qr__hint {
|
.wi-wechat-qr__hint {
|
||||||
|
|||||||
@@ -120,8 +120,35 @@
|
|||||||
border: 1px solid var(--rule);
|
border: 1px solid var(--rule);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- 引用块 ----------
|
||||||
|
全局 blockquote 的 padding 与正文 p 的 margin-bottom 会在块内叠加,
|
||||||
|
文字下方因此出现一整条空白。这里把引用块内外间距收敛为单一节奏:
|
||||||
|
块内子元素不再各自产生外边距,多段之间只保留一个较小的段间距。 */
|
||||||
blockquote {
|
blockquote {
|
||||||
margin-block: 2em;
|
margin-block: 1.25em;
|
||||||
|
padding-block: 0.7em;
|
||||||
|
padding-inline: 1.25em;
|
||||||
|
|
||||||
|
/* :is() 把特异性提到 0,1,2,确保移动端 .wi-post__body p 规则不会让空白回归 */
|
||||||
|
> :is(p, ul, ol, pre, figure, table, h1, h2, h3, h4, h5, h6) {
|
||||||
|
margin-block: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 与上面的归零规则保持同等特异性,靠顺序生效,避免多段引用挤在一起 */
|
||||||
|
> :is(p, ul, ol, pre, figure, table, h1, h2, h3, h4, h5, h6) + * {
|
||||||
|
margin-block-start: 0.55em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 编辑器常在引用块末尾留下空段落,避免再撑出一段空白 */
|
||||||
|
> p:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 仅含一个 <br> 的空段落单独成规则:旧浏览器不支持 :has() 时
|
||||||
|
只会丢弃这一条,不影响上面 p:empty 的兜底 */
|
||||||
|
> p:has(> br:only-child) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(ul, ol) {
|
:is(ul, ol) {
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ spec:
|
|||||||
description: 微信读书书架(依赖微信读书插件)
|
description: 微信读书书架(依赖微信读书插件)
|
||||||
screenshot:
|
screenshot:
|
||||||
file: weread.html
|
file: weread.html
|
||||||
version: "1.2.5"
|
version: "1.2.7"
|
||||||
requires: ">=2.27.0"
|
requires: ">=2.27.0"
|
||||||
license:
|
license:
|
||||||
- name: "GPL-3.0"
|
- name: "GPL-3.0"
|
||||||
|
|||||||
Reference in new issue
Block a user