Compare commits
4
Commits
4e3001674a
...
5ad34cf233
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ad34cf233 | ||
|
|
5e50c01c18 | ||
|
|
0e87b3011e | ||
|
|
0eb5d8e06b |
@@ -27,3 +27,4 @@ dist-ssr
|
|||||||
.gradle
|
.gradle
|
||||||
build
|
build
|
||||||
templates
|
templates
|
||||||
|
.workbuddy/
|
||||||
|
|||||||
@@ -1,120 +1,167 @@
|
|||||||
# Whitespace 留白
|
# Whitespace 留白
|
||||||
|
|
||||||
一个移植自 Typecho 主题 [whitespace 留白](https://kehua.me) 的 [Halo](https://www.halo.run/) 主题,主打极简留白风格与清晰的排版。
|
一个移植自 Typecho 主题 [whitespace 留白](https://kehua.me) 的 [Halo](https://www.halo.run/) 主题,主打极简留白风格与清晰的排版,并为多款常用插件提供开箱即用的适配页面。
|
||||||
|
|
||||||
> 主题名 `whitespace`,显示名 **Whitespace 留白**,版本 `0.1.0`,作者 **暖心向阳335**。
|
> 本项目由 AI 开发完成,人工仅进行部分功能测试及问题找寻。
|
||||||
> 要求 Halo ≥ 2.26.0。源码在 `src/`,构建产物在 `templates/`。
|
|
||||||
> 源码仓库:https://git.nxxy335.top/nxxy335/halo-theme-whitespace
|
|
||||||
|
|
||||||
## 特性
|
## 环境要求
|
||||||
|
|
||||||
- 极简留白风格,自带浅色 / 深色双主题(跟随系统或手动切换,记忆访客选择)
|
- Halo **2.26.0 及以上**
|
||||||
- 首页可选展示最新一篇置顶文章(`featured` 设置)
|
- 构建依赖 Node.js + pnpm(`src/` 经 Vite 构建生成 `templates/`,Halo 实际只读 `templates/`)
|
||||||
- 文章 / 页面 / 归档 / 分类 / 标签 / 作者 / 关于 等完整页面
|
|
||||||
- 文章页结构化数据(Schema.org `BlogPosting` JSON-LD)与社交分享标签(canonical / Open Graph / Twitter Card),均可在主题设置中关闭
|
|
||||||
- 内置搜索按钮(依赖官方 `PluginSearchWidget` 插件,未安装则不显示)
|
|
||||||
- 阅读进度条、代码块复制、相对时间、回到顶部、移动端菜单等交互
|
|
||||||
- **二级导航**:菜单项的子项自动渲染为下拉菜单(桌面端 hover/focus 展开,移动端嵌套缩进展开)
|
|
||||||
- **页脚可关闭的主题署名**:默认「Theme by WhiteSpace」并链接到主题仓库,可在设置中修改名称 / 链接或整体关闭
|
|
||||||
- **页脚全站数据统计**:文章 / 分类 / 标签 / 评论 / 访问量 / 字数,可按项开关,整体可关闭
|
|
||||||
- **文章卡片字数 + 阅读时长**:可在设置中关闭(需 `halo-plugin-extra-api` 提供数据)
|
|
||||||
- **全站 lightgallery.js 灯箱**:文章、页面、瞬间、图库等页面的图片点击放大预览(按需懒加载,可在设置中整体关闭)
|
|
||||||
- 多语言:`i18n/default.properties`(回退)+ `i18n/zh_CN.properties`
|
|
||||||
- 提供 `layout.html` 布局契约,供使用布局契约的插件页面复用主题导航 / 页脚 / 暗色样式
|
|
||||||
|
|
||||||
## 插件适配
|
## 特点
|
||||||
|
|
||||||
主题已为以下插件提供适配页面(在 `src/` 下以同名模板提供,构建后进入 `templates/`):
|
1. **极简留白风格** — 浅色 / 深色双主题(跟随系统或手动切换,记忆访客选择),专注内容本身的清晰排版
|
||||||
|
2. **二级导航** — 菜单子项自动渲染为下拉菜单(桌面端 hover/focus 展开,移动端嵌套缩进)
|
||||||
|
3. **插件兼容性** — 适配 8 款常用 Halo 插件,安装即用;提供页面布局契约,插件前台页面自动复用主题外观
|
||||||
|
4. **全站灯箱** — 主题内置 lightgallery.js 灯箱,文章、页面、瞬间、图库等图片点击放大预览(按需懒加载,可在设置中整体关闭)
|
||||||
|
5. **可配置性** — 署名、全站统计、字数显示、相册封面、预览模式等均可在后台开关
|
||||||
|
6. **性能优化** — 服务端字数统计(API 扩展包)、图片懒加载、CSS/HTML 压缩、静态资源带内容哈希
|
||||||
|
|
||||||
- **plugin-moments**:`/moments` 瞬间列表页 + `/moments/{name}` 瞬间详情页(完整内容、媒体、标签、作者信息与评论区);列表页发布人与发布时间同行对齐
|
## 已适配插件
|
||||||
- **plugin-photos**:`/photos` 图库页 —— 首屏按相册分组展示(封面默认自动取相册内第一张图,可在主题设置「图库」中为每个相册手动配置封面);点击相册进入 `?group=photo-group-*` 瀑布流视图(一行三张、页面加宽),支持**预览模式**(lightgallery 点击放大,默认值可在设置中配置,访客可在前台自行切换)
|
|
||||||
- **plugin-important-dates**:`/important-dates` 重要日子页(即将到来提醒、全部日期倒计时、重要标记、人员生日)
|
|
||||||
- **plugin-links**:`/links` 友链页 —— 「全部」视图按分组展示并显示分组标题,卡片简介最多两行、卡片高度统一;友链申请表单已**直接内嵌**在友链页底部(点击「申请友链」展开,调用插件公共 REST API,含图形验证码);并额外提供
|
|
||||||
- `links-subscribe.html`:RSS 订阅动态聚合页(使用 `linkFeedFinder`);每条仅显示作者与发布时间,作者名链接到来源博客,摘要默认折叠为 5 行
|
|
||||||
- **halo-weread-plugin**:`weread.html` 微信读书书架页(前端拉取插件 `/shelf-html` 并注入)
|
|
||||||
- **plugin-douban**:`douban.html` 豆瓣收藏页(按「读过 / 在读 / 看过 / 在追」分栏展示书影音,使用 `doubanFinder.getMovies(...)`;未安装插件时显示提示)。在控制台新建单页并选择「豆瓣页面」模板即可启用
|
|
||||||
- **留言板**:`page_guestbook.html` 以评论区作为留言板的单页模板
|
|
||||||
- **plugin-extra-api**:API 扩展包(无独立页面),主题已适配其 shiki 代码高亮与暗色样式;并在文章卡片 / 详情页展示**字数 + 阅读时长**、归档页展示**全站总字数**、瞬间页展示**单条字数**(通过 `extraApiStatsFinder`,插件未安装时自动隐藏)
|
|
||||||
|
|
||||||
> `weread` / `留言板` / `链接订阅` 均为「自定义页面模板」,在控制台新建单页并选择对应模板即可启用。`/links` 友链页与内嵌的友链申请无需单独建单页。
|
| 插件名称 | 插件标识 | 适配页面路由 | 说明 |
|
||||||
|
| ----------------------------------------------------------- | ------------------------ | ------------------ | ---------------------------------------------------- |
|
||||||
|
| [瞬间](https://github.com/halo-sigs/plugin-moments) | `plugin-moments` | `/moments` | 瞬间动态列表页,含 `/moments/{name}` 详情页(完整内容、媒体、标签、作者信息、评论区) |
|
||||||
|
| [图库](https://github.com/halo-sigs/plugin-photos) | `plugin-photos` | `/photos` | 图片展示页面,按相册分组、封面自动选取、瀑布流 + 预览模式 |
|
||||||
|
| [重要日子](https://github.com/yyliucha/plugin-important-dates) | `plugin-important-dates` | `/important-dates` | 即将到来提醒、全部日期倒计时、重要标记、人员生日 |
|
||||||
|
| [友情链接](https://github.com/halo-sigs/plugin-links) | `plugin-links` | `/links` | 友链展示页,按分组显示标题、卡片统一高度;内嵌申请表单 |
|
||||||
|
| [链接订阅](https://github.com/halo-sigs/plugin-links) | `plugin-links` | `/subscriptions` | RSS 订阅动态聚合,作者链接来源博客、摘要折叠 5 行 |
|
||||||
|
| [豆瓣](https://github.com/chengzhongxue/plugin-douban) | `plugin-douban` | `/douban` | 豆瓣记录页,按「读过 / 在读 / 看过 / 在追」分栏展示书影音 |
|
||||||
|
| [微信读书](https://github.com/yeora26/halo-weread-plugin) | `halo-weread-plugin` | 自定义页面 | 微信读书书架展示,访客免登录查看 |
|
||||||
|
| [API 扩展包](https://github.com/HowieHz/halo-plugin-extra-api) | `extra-api` | 全站 | 服务端字数统计、代码高亮、全站/单条字数展示 |
|
||||||
|
|
||||||
|
> **注意**:插件需单独安装,安装后主题自动适配对应页面。若未安装某插件,对应页面或功能自动隐藏 / 降级,不会报错。
|
||||||
|
> 豆瓣插件建议 **v1.2.6+**(SQLite 数据结构)。
|
||||||
|
|
||||||
|
## 自定义页面模板
|
||||||
|
|
||||||
|
在 Halo 后台「页面 → 新建 → 自定义模板」中选择使用:
|
||||||
|
|
||||||
|
| 模板名 | 模板文件 | 说明 |
|
||||||
|
| ---- | --------------------- | --------------------- |
|
||||||
|
| 留言板 | `page_guestbook.html` | 以评论区作为留言板的单页 |
|
||||||
|
| 微信读书 | `weread.html` | 微信读书书架展示,依赖微信读书插件 |
|
||||||
|
| 豆瓣页面 | `douban.html` | 豆瓣记录页,依赖豆瓣插件 |
|
||||||
|
|
||||||
|
## 主要功能
|
||||||
|
|
||||||
|
### 首页
|
||||||
|
|
||||||
|
- 可选展示最新一篇置顶文章(`featured` 设置)
|
||||||
|
- 文章卡片:可配置显示字数 + 阅读时长(依赖 API 扩展包)、分类、标签、摘要
|
||||||
|
- 文章卡片分类之间带间距,避免拥挤
|
||||||
|
|
||||||
|
### 文章
|
||||||
|
|
||||||
|
- 结构化数据(Schema.org `BlogPosting` JSON-LD)与社交分享标签(canonical / Open Graph / Twitter Card),均可在设置中关闭
|
||||||
|
- 阅读进度条、代码块复制、相对时间、回到顶部
|
||||||
|
- 正文排版:列表(ul/ol/li)、嵌套列表、表格、分隔线、列表标记配色
|
||||||
|
- 图片点击放大(内置 lightgallery 灯箱)
|
||||||
|
|
||||||
|
### 全局
|
||||||
|
|
||||||
|
- 二级导航:菜单子项渲染为下拉菜单,hover 桥接避免鼠标下移时收起
|
||||||
|
- 页脚可关闭署名:默认「Theme by WhiteSpace」并链接到主题仓库,名称 / 链接可改
|
||||||
|
- 页脚全站统计:文章、分类、标签、评论、访问量、字数,支持开关与统计项选择
|
||||||
|
- 备案信息:ICP 与公网安备号,公网安备自动带徽标
|
||||||
|
- 暗色模式评论区可见性已适配
|
||||||
|
|
||||||
|
### 图库
|
||||||
|
|
||||||
|
- 首屏按相册网格展示,封面 4:3 自适应填满,默认自动取相册内第一张图(分组无图时回退取首图)
|
||||||
|
- 后台可为每个相册手动指定封面:「相册」直接从图库插件**已存在的分组中下拉选择**,无需手输名称
|
||||||
|
- 点击相册进入 `?group=photo-group-*` 瀑布流视图(一行三张、页面加宽)
|
||||||
|
- 相册切换由前端按 URL 上的 `group` 参数完成,不依赖服务端变量,因此**不受页面静态缓存影响**
|
||||||
|
- 预览模式:默认值可在设置中配置(默认开),访客可在前台自行切换(开 → 点击图片灯箱放大;关 → 点击进入图片详情)
|
||||||
|
|
||||||
|
### 瞬间
|
||||||
|
|
||||||
|
- 列表页发布人与发布时间同行对齐,每条展示标签
|
||||||
|
- 详情页:完整内容、图片 / 视频 / 音频、标签、作者信息、发布时间与评论区
|
||||||
|
|
||||||
|
### 灯箱集成
|
||||||
|
|
||||||
|
主题内置 lightgallery.js 灯箱,为图片提供放大预览。覆盖文章、自定义页面、瞬间(正文 + 媒体)、图库瀑布流;设置「基础 → 全站图片灯箱」可整体关闭。
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
1. 下载主题最新 Release 的 ZIP 包
|
||||||
|
2. 进入 Halo 后台 → 主题管理 → 安装主题 → 上传 ZIP 包
|
||||||
|
3. 安装完成后点击「启用」
|
||||||
|
|
||||||
|
> **升级后如果整站没了样式或部分页面不生效**:Halo 页面缓存或前置反向代理可能还留着上一版本的 HTML,它引用的 CSS 文件名带内容哈希,升级后已被删除,请求会直接 500 或样式错乱。先在 Halo 后台执行一次「**清理缓存**」(并刷新 CDN / Nginx 缓存);若安装了「页面静态缓存」插件,还需手动点一次「刷新页面缓存」。
|
||||||
|
|
||||||
|
或从源码构建:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.nxxy335.top/nxxy335/halo-theme-whitespace.git
|
||||||
|
cd halo-theme-whitespace
|
||||||
|
pnpm install
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
构建产物在 `templates/` 目录下,同时会生成 `dist/` 内的安装包 `whitespace-<version>.zip`。
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# 开发模式(监听文件变化,自动构建到 templates/)
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# 构建(输出到 templates/,并打包 dist/whitespace-<version>.zip)
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# 格式化 + Lint 修复
|
||||||
|
pnpm check
|
||||||
|
```
|
||||||
|
|
||||||
## 目录结构
|
## 目录结构
|
||||||
|
|
||||||
```
|
```text
|
||||||
src/ 源码目录(在此编辑)
|
├── src/
|
||||||
├── partials/
|
│ ├── partials/ # 公共布局(layout.html 布局契约、seo.html 片段)
|
||||||
│ ├── layout.html 公共布局(导航 / 页脚 / 主题切换 / 脚本入口)
|
│ ├── modules/ # 文章卡片 / 元信息 / 分页等可复用片段
|
||||||
│ └── seo.html SEO 片段(canonical / OG / Twitter,受开关控制)
|
│ ├── error/ # 404 与通用错误页
|
||||||
├── modules/
|
│ ├── css/main.css # 全部样式(双主题变量、响应式、正文排版)
|
||||||
│ ├── post-card.html 文章列表卡片
|
│ ├── js/ # main.ts 全站脚本 / post.ts 文章脚本 / lightbox.ts 灯箱
|
||||||
│ ├── post-meta.html 文章元信息(时间 / 分类 / 浏览量)
|
│ ├── index.html post.html page.html
|
||||||
│ └── pagination.html 分页导航
|
│ ├── archives.html category.html tag.html categories.html tags.html author.html
|
||||||
├── error/ 错误页(404 / 通用错误)
|
│ ├── moments.html moment.html photos.html important-dates.html
|
||||||
│ ├── 404.html
|
│ ├── weread.html douban.html links.html links-subscribe.html page_guestbook.html
|
||||||
│ └── error.html
|
│ └── i18n/ # 多语言消息
|
||||||
├── css/main.css 全部样式(含双主题变量、响应式、打印)
|
├── templates/ # 构建产物,Halo 实际加载的模板(勿手动编辑)
|
||||||
├── js/main.ts 全站脚本
|
├── dist/ # 打包安装包 whitespace-<version>.zip
|
||||||
├── js/post.ts 文章页脚本(代码复制 / 阅读进度)
|
├── settings.yaml # 后台设置项定义
|
||||||
├── index.html post.html page.html page_about.html
|
└── theme.yaml # 主题清单与自定义页面模板注册
|
||||||
├── archives.html category.html tag.html categories.html tags.html author.html
|
|
||||||
├── weread.html douban.html links-subscribe.html page_guestbook.html
|
|
||||||
└── i18n/ 多语言消息
|
|
||||||
theme.yaml 主题元数据(必填)
|
|
||||||
settings.yaml 控制台主题设置表单
|
|
||||||
templates/ 构建产物(Halo 实际读取,勿手动编辑)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> `src/` 与 `templates/` 的关系:`src/` 经 Vite 构建生成 `templates/`。开发或发版前务必执行构建,使 `templates/` 与源码一致。**不要**手动修改 `templates/`。
|
## 主题开发注意事项
|
||||||
|
|
||||||
## 开发与构建
|
- **Thymeleaf 字符串字面量必须加单引号**:`th:text="'文本'"`,裸中文遇到逗号 / 空格 / emoji 会导致页面 500
|
||||||
|
- **`th:attr` 只用简单表达式**(取属性、Elvis 默认值、三元、拼接、等值比较),表达式对象方法调用会被受限求值器拒绝
|
||||||
|
- **凡是后台开关绑定的模板表达式都要给默认值**:`?: '默认值'`,配置未落库时属性会被整体移除,功能会静默失效
|
||||||
|
- **内联脚本统一加 `th:inline="none"`**:避免 Thymeleaf 解析脚本中的 `[[` / `[(` / `${}`
|
||||||
|
- **BEM 双下划线 `__` 不要跨引号边界**:出现在 `th:*` 表达式里会被当作预处理分隔符导致 500
|
||||||
|
- **禁止静态类调用**:`java.math.BigInteger.valueOf(...)` 等被 Halo 受限 SpEL 禁止,整数运算改用 `longValue()` / 整数除法
|
||||||
|
- **`SinglePage` 描述在 `status.excerpt`**,不是 `spec.description`
|
||||||
|
- **升级主题务必清理 Halo 页面缓存**,否则旧 HTML 引用已删除的哈希资源会 500
|
||||||
|
|
||||||
```bash
|
## 技术栈
|
||||||
pnpm install # 安装依赖
|
|
||||||
pnpm dev # 监听模式构建(开发时用)
|
|
||||||
pnpm build-only # 仅构建(tsc + vp build),生成 templates/
|
|
||||||
pnpm build # 构建并打包为可上传的 ZIP(theme-package-cli)
|
|
||||||
pnpm check # 格式化 + Lint 修复
|
|
||||||
```
|
|
||||||
|
|
||||||
构建输出:
|
- [Halo](https://halo.run) 2.26+
|
||||||
|
- [Vite](https://vitejs.dev/) + [@halo-dev/vite-plugin-halo-theme](https://github.com/halo-sigs/vite-plugin-halo-theme)
|
||||||
|
- [Thymeleaf](https://www.thymeleaf.org) 模板引擎
|
||||||
|
- [lightgallery.js](https://www.lightgalleryjs.com/) 灯箱
|
||||||
|
- TypeScript
|
||||||
|
|
||||||
- 页面与片段 → `templates/*.html`、`templates/modules/*.html`、`templates/error/*.html`
|
## 许可
|
||||||
- 静态资源 → `templates/assets/*`(文件名带内容哈希,运行时以 `/themes/whitespace/assets/...` 提供)
|
|
||||||
|
|
||||||
## 主题设置
|
[GPL-3.0](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
|
||||||
在 Halo 控制台「主题 → 设置」中可配置:
|
|
||||||
|
|
||||||
| 分组 | 字段 | 说明 |
|
|
||||||
| ------- | ------------------ | -------------------------------------------- |
|
|
||||||
| basic | color_scheme | 默认配色:跟随系统 / 浅色 / 深色 |
|
|
||||||
| basic | footer_copyright | 页脚自定义版权(支持 HTML,留空不显示) |
|
|
||||||
| basic | show_theme_credit | 页脚是否显示「Theme by」署名(默认开) |
|
|
||||||
| basic | theme_credit_text | 署名名称(默认「WhiteSpace」,可改) |
|
|
||||||
| basic | theme_credit_url | 署名跳转链接(默认主题仓库地址) |
|
|
||||||
| basic | show_word_count | 文章卡片是否显示字数与阅读时长(需 extra-api)|
|
|
||||||
| basic | enable_lightbox | 全站 lightgallery 图片灯箱(默认开) |
|
|
||||||
| featured| enable | 首页是否展示置顶文章 |
|
|
||||||
| about | avatar / bio / github_url / twitter_url / contact_email | 关于页(自定义模板 `page_about.html`)内容 |
|
|
||||||
| seo | enable_metadata | 输出 canonical / Open Graph / Twitter Card |
|
|
||||||
| seo | enable_structured_data | 输出文章页 BlogPosting JSON-LD |
|
|
||||||
| footer | icp_number / icp_url | ICP 备案号与查询链接(默认工信部备案管理系统) |
|
|
||||||
| footer | police_number / police_url | 公网安备号与查询链接(默认全国互联网安全管理服务平台) |
|
|
||||||
| stats | enable | 页脚是否显示全站数据统计(默认关) |
|
|
||||||
| stats | show_posts / show_categories / show_tags / show_comments / show_visits | 各项统计的显隐开关 |
|
|
||||||
| stats | show_words | 全站总字数(需 extra-api 插件,默认开) |
|
|
||||||
| photos | preview_default | 相册瀑布流是否默认开启预览模式(默认开) |
|
|
||||||
| photos | album_covers | 各相册手动封面(「相册名称 + 封面图」列表,留空自动取第一张) |
|
|
||||||
|
|
||||||
## 约定与说明
|
|
||||||
|
|
||||||
- Logo / favicon / SEO description·keywords / 代码注入优先使用 Halo 系统设置;主题仅输出 canonical、OG、Twitter 与结构化数据(可在 `seo` 分组关闭,避免与插件重复)。
|
|
||||||
- 社交分享 / 结构化数据中用 `status.permalink` 与 `site.routes.*` 拼出地址,不写死可配置路由。
|
|
||||||
- 搜索由官方 `PluginSearchWidget` 插件提供,按钮仅在插件可用时出现。
|
|
||||||
- 浏览量来自 Halo 原生统计(`post.stats.visit`),无需主题自行计数。
|
|
||||||
- 字数 / 阅读时长来自 `halo-plugin-extra-api` 的 `extraApiStatsFinder`(插件未安装时相关展示自动隐藏),全站字数为缓存计算结果。
|
|
||||||
|
|
||||||
## 致谢
|
## 致谢
|
||||||
|
|
||||||
- 原主题:[whitespace 留白](https://kehua.me) by kehua
|
- 原主题:[whitespace 留白](https://kehua.me) by kehua
|
||||||
- 基于 Halo 官方 [theme-vite-starter](https://github.com/halo-dev/theme-vite-starter) 脚手架移植
|
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ error.404.message=Sorry, the page you visited does not exist or has been deleted
|
|||||||
error.back.home=Back to home
|
error.back.home=Back to home
|
||||||
error.back.prev=Go back
|
error.back.prev=Go back
|
||||||
footer.theme=Theme by {0}
|
footer.theme=Theme by {0}
|
||||||
about.social.github=GitHub
|
|
||||||
about.social.twitter=Twitter
|
|
||||||
about.social.email=Email
|
|
||||||
post.words={0} words
|
post.words={0} words
|
||||||
post.reading_time=About {0} min
|
post.reading_time=About {0} min
|
||||||
archive.stats.words=Total words
|
archive.stats.words=Total words
|
||||||
@@ -47,6 +44,5 @@ stats.comments={0} comments
|
|||||||
stats.visits={0} visits
|
stats.visits={0} visits
|
||||||
stats.words={0} words
|
stats.words={0} words
|
||||||
douban.plugin_required=Douban plugin (plugin-douban) not detected. Please install and enable it, then set your Douban ID in the plugin settings.
|
douban.plugin_required=Douban plugin (plugin-douban) not detected. Please install and enable it, then set your Douban ID in the plugin settings.
|
||||||
photos.photo_count={0} photos
|
|
||||||
moment.back_to_list=Back to moments
|
moment.back_to_list=Back to moments
|
||||||
moment.tags=Tags
|
moment.tags=Tags
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ error.404.message=抱歉,您访问的页面不存在或已被删除。
|
|||||||
error.back.home=返回首页
|
error.back.home=返回首页
|
||||||
error.back.prev=返回上页
|
error.back.prev=返回上页
|
||||||
footer.theme=Theme by {0}
|
footer.theme=Theme by {0}
|
||||||
about.social.github=GitHub
|
|
||||||
about.social.twitter=Twitter
|
|
||||||
about.social.email=邮箱
|
|
||||||
post.words={0} 字
|
post.words={0} 字
|
||||||
post.reading_time=约 {0} 分钟
|
post.reading_time=约 {0} 分钟
|
||||||
archive.stats.words=总字数
|
archive.stats.words=总字数
|
||||||
@@ -47,6 +44,5 @@ stats.comments={0} 条评论
|
|||||||
stats.visits={0} 次访问
|
stats.visits={0} 次访问
|
||||||
stats.words={0} 字
|
stats.words={0} 字
|
||||||
douban.plugin_required=未检测到豆瓣插件(plugin-douban),请安装并启用后在插件设置中填写豆瓣 ID。
|
douban.plugin_required=未检测到豆瓣插件(plugin-douban),请安装并启用后在插件设置中填写豆瓣 ID。
|
||||||
photos.photo_count={0} 张照片
|
|
||||||
moment.back_to_list=返回瞬间列表
|
moment.back_to_list=返回瞬间列表
|
||||||
moment.tags=标签
|
moment.tags=标签
|
||||||
|
|||||||
@@ -60,34 +60,6 @@ spec:
|
|||||||
label: 首页展示置顶文章
|
label: 首页展示置顶文章
|
||||||
value: true
|
value: true
|
||||||
help: 在首页顶部以大标题样式展示最新的一篇置顶文章(需在文章设置中开启置顶)
|
help: 在首页顶部以大标题样式展示最新的一篇置顶文章(需在文章设置中开启置顶)
|
||||||
- group: about
|
|
||||||
label: 关于页面
|
|
||||||
formSchema:
|
|
||||||
- $formkit: attachment
|
|
||||||
name: avatar
|
|
||||||
label: 博主头像
|
|
||||||
value: ""
|
|
||||||
help: 关于页面展示的头像,留空则使用站点 Logo
|
|
||||||
- $formkit: textarea
|
|
||||||
name: bio
|
|
||||||
label: 个人简介
|
|
||||||
value: "一个热爱生活、热爱技术的普通人"
|
|
||||||
help: 关于页面头像下方显示的简介
|
|
||||||
- $formkit: text
|
|
||||||
name: github_url
|
|
||||||
label: GitHub 链接
|
|
||||||
value: ""
|
|
||||||
placeholder: https://github.com/yourname
|
|
||||||
- $formkit: text
|
|
||||||
name: twitter_url
|
|
||||||
label: Twitter 链接
|
|
||||||
value: ""
|
|
||||||
placeholder: https://x.com/yourname
|
|
||||||
- $formkit: text
|
|
||||||
name: contact_email
|
|
||||||
label: 联系邮箱
|
|
||||||
value: ""
|
|
||||||
placeholder: mail@example.com
|
|
||||||
- group: seo
|
- group: seo
|
||||||
label: SEO
|
label: SEO
|
||||||
formSchema:
|
formSchema:
|
||||||
@@ -158,25 +130,3 @@ spec:
|
|||||||
name: show_words
|
name: show_words
|
||||||
label: 全站总字数(需 extra-api 插件)
|
label: 全站总字数(需 extra-api 插件)
|
||||||
value: true
|
value: true
|
||||||
- group: photos
|
|
||||||
label: 图库
|
|
||||||
formSchema:
|
|
||||||
- $formkit: checkbox
|
|
||||||
name: preview_default
|
|
||||||
label: 相册默认开启预览模式
|
|
||||||
value: true
|
|
||||||
help: 进入相册瀑布流时默认开启点击放大预览,访客可在前台自行切换
|
|
||||||
- $formkit: repeater
|
|
||||||
name: album_covers
|
|
||||||
label: 相册封面
|
|
||||||
value: []
|
|
||||||
help: 为相册手动指定封面;不配置时自动使用相册内第一张图片。「相册名称」填写图库页面显示的相册名
|
|
||||||
children:
|
|
||||||
- $formkit: text
|
|
||||||
name: group
|
|
||||||
label: 相册名称
|
|
||||||
validation: required
|
|
||||||
- $formkit: attachment
|
|
||||||
name: cover
|
|
||||||
label: 封面图
|
|
||||||
validation: required
|
|
||||||
|
|||||||
+47
-36
@@ -79,8 +79,9 @@
|
|||||||
></p>
|
></p>
|
||||||
|
|
||||||
<th:block th:if="${pluginFinder.available('plugin-douban')}">
|
<th:block th:if="${pluginFinder.available('plugin-douban')}">
|
||||||
|
<!-- 读过 -->
|
||||||
<th:block
|
<th:block
|
||||||
th:with="movies=${doubanFinder.getMovies(24, 0, 'book', 'done')}"
|
th:with="res=${doubanFinder.list(1, 24, 'book', 'done')}, movies=${res != null ? res.items : null}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="douban-section"
|
class="douban-section"
|
||||||
@@ -91,27 +92,28 @@
|
|||||||
<a
|
<a
|
||||||
th:each="m : ${movies}"
|
th:each="m : ${movies}"
|
||||||
class="douban-card"
|
class="douban-card"
|
||||||
th:href="${m.spec.link}"
|
th:href="${m.link}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="douban-cover"
|
class="douban-cover"
|
||||||
th:src="${m.spec.poster}"
|
th:src="${m.poster}"
|
||||||
th:alt="${m.spec.name}"
|
th:alt="${m.name}"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
<p class="douban-name" th:text="${m.spec.name}">名称</p>
|
<p class="douban-name" th:text="${m.name}">名称</p>
|
||||||
<p class="douban-meta">
|
<p class="douban-meta">
|
||||||
<span
|
<span
|
||||||
th:if="${not #strings.isEmpty(m.spec.year)}"
|
th:if="${not #strings.isEmpty(m.year)}"
|
||||||
th:text="${m.spec.year}"
|
th:text="${m.year}"
|
||||||
>年份</span
|
>年份</span
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
th:if="${not #strings.isEmpty(m.spec.douban_score)}"
|
th:if="${not #strings.isEmpty(m.score)}"
|
||||||
class="douban-score"
|
class="douban-score"
|
||||||
th:text="'★ ' + ${m.spec.douban_score}"
|
th:text="'★ ' + ${m.score}"
|
||||||
>评分</span
|
>评分</span
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
@@ -119,8 +121,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
|
<!-- 在读 -->
|
||||||
<th:block
|
<th:block
|
||||||
th:with="movies=${doubanFinder.getMovies(24, 0, 'book', 'doing')}"
|
th:with="res=${doubanFinder.list(1, 24, 'book', 'doing')}, movies=${res != null ? res.items : null}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="douban-section"
|
class="douban-section"
|
||||||
@@ -131,27 +135,28 @@
|
|||||||
<a
|
<a
|
||||||
th:each="m : ${movies}"
|
th:each="m : ${movies}"
|
||||||
class="douban-card"
|
class="douban-card"
|
||||||
th:href="${m.spec.link}"
|
th:href="${m.link}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="douban-cover"
|
class="douban-cover"
|
||||||
th:src="${m.spec.poster}"
|
th:src="${m.poster}"
|
||||||
th:alt="${m.spec.name}"
|
th:alt="${m.name}"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
<p class="douban-name" th:text="${m.spec.name}">名称</p>
|
<p class="douban-name" th:text="${m.name}">名称</p>
|
||||||
<p class="douban-meta">
|
<p class="douban-meta">
|
||||||
<span
|
<span
|
||||||
th:if="${not #strings.isEmpty(m.spec.year)}"
|
th:if="${not #strings.isEmpty(m.year)}"
|
||||||
th:text="${m.spec.year}"
|
th:text="${m.year}"
|
||||||
>年份</span
|
>年份</span
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
th:if="${not #strings.isEmpty(m.spec.douban_score)}"
|
th:if="${not #strings.isEmpty(m.score)}"
|
||||||
class="douban-score"
|
class="douban-score"
|
||||||
th:text="'★ ' + ${m.spec.douban_score}"
|
th:text="'★ ' + ${m.score}"
|
||||||
>评分</span
|
>评分</span
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
@@ -159,8 +164,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
|
<!-- 看过 -->
|
||||||
<th:block
|
<th:block
|
||||||
th:with="movies=${doubanFinder.getMovies(24, 0, 'movie', 'done')}"
|
th:with="res=${doubanFinder.list(1, 24, 'movie', 'done')}, movies=${res != null ? res.items : null}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="douban-section"
|
class="douban-section"
|
||||||
@@ -171,27 +178,28 @@
|
|||||||
<a
|
<a
|
||||||
th:each="m : ${movies}"
|
th:each="m : ${movies}"
|
||||||
class="douban-card"
|
class="douban-card"
|
||||||
th:href="${m.spec.link}"
|
th:href="${m.link}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="douban-cover"
|
class="douban-cover"
|
||||||
th:src="${m.spec.poster}"
|
th:src="${m.poster}"
|
||||||
th:alt="${m.spec.name}"
|
th:alt="${m.name}"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
<p class="douban-name" th:text="${m.spec.name}">名称</p>
|
<p class="douban-name" th:text="${m.name}">名称</p>
|
||||||
<p class="douban-meta">
|
<p class="douban-meta">
|
||||||
<span
|
<span
|
||||||
th:if="${not #strings.isEmpty(m.spec.year)}"
|
th:if="${not #strings.isEmpty(m.year)}"
|
||||||
th:text="${m.spec.year}"
|
th:text="${m.year}"
|
||||||
>年份</span
|
>年份</span
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
th:if="${not #strings.isEmpty(m.spec.douban_score)}"
|
th:if="${not #strings.isEmpty(m.score)}"
|
||||||
class="douban-score"
|
class="douban-score"
|
||||||
th:text="'★ ' + ${m.spec.douban_score}"
|
th:text="'★ ' + ${m.score}"
|
||||||
>评分</span
|
>评分</span
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
@@ -199,8 +207,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
|
<!-- 在追 -->
|
||||||
<th:block
|
<th:block
|
||||||
th:with="movies=${doubanFinder.getMovies(24, 0, 'drama', 'doing')}"
|
th:with="res=${doubanFinder.list(1, 24, 'drama', 'doing')}, movies=${res != null ? res.items : null}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="douban-section"
|
class="douban-section"
|
||||||
@@ -211,27 +221,28 @@
|
|||||||
<a
|
<a
|
||||||
th:each="m : ${movies}"
|
th:each="m : ${movies}"
|
||||||
class="douban-card"
|
class="douban-card"
|
||||||
th:href="${m.spec.link}"
|
th:href="${m.link}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="douban-cover"
|
class="douban-cover"
|
||||||
th:src="${m.spec.poster}"
|
th:src="${m.poster}"
|
||||||
th:alt="${m.spec.name}"
|
th:alt="${m.name}"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
<p class="douban-name" th:text="${m.spec.name}">名称</p>
|
<p class="douban-name" th:text="${m.name}">名称</p>
|
||||||
<p class="douban-meta">
|
<p class="douban-meta">
|
||||||
<span
|
<span
|
||||||
th:if="${not #strings.isEmpty(m.spec.year)}"
|
th:if="${not #strings.isEmpty(m.year)}"
|
||||||
th:text="${m.spec.year}"
|
th:text="${m.year}"
|
||||||
>年份</span
|
>年份</span
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
th:if="${not #strings.isEmpty(m.spec.douban_score)}"
|
th:if="${not #strings.isEmpty(m.score)}"
|
||||||
class="douban-score"
|
class="douban-score"
|
||||||
th:text="'★ ' + ${m.spec.douban_score}"
|
th:text="'★ ' + ${m.score}"
|
||||||
>评分</span
|
>评分</span
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
+158
-30
@@ -31,37 +31,61 @@
|
|||||||
.links-list a {
|
.links-list a {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 0.75rem;
|
gap: 0.8rem;
|
||||||
padding: 1rem;
|
padding: 1.1rem 1.25rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-lg, 0.75rem);
|
||||||
background: var(--color-bg-secondary);
|
background: var(--color-bg-secondary);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
transition: border-color var(--transition-fast);
|
transition:
|
||||||
|
border-color var(--transition-fast),
|
||||||
|
box-shadow var(--transition-fast);
|
||||||
}
|
}
|
||||||
.links-list a:hover {
|
.links-list a:hover {
|
||||||
border-color: var(--color-primary);
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
.links-list img {
|
.links-avatar {
|
||||||
width: 2.5rem;
|
position: relative;
|
||||||
height: 2.5rem;
|
display: block;
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
object-fit: cover;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.links-avatar img {
|
||||||
|
display: block;
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
object-fit: cover;
|
||||||
|
background: var(--color-bg);
|
||||||
|
}
|
||||||
|
/* 头像右下角状态圆点 */
|
||||||
|
.links-avatar::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: -0.1rem;
|
||||||
|
bottom: -0.1rem;
|
||||||
|
width: 0.55rem;
|
||||||
|
height: 0.55rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #2ea043;
|
||||||
|
border: 2px solid var(--color-bg-secondary);
|
||||||
|
}
|
||||||
.links-list .links-body {
|
.links-list .links-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
padding-top: 0.1rem;
|
||||||
}
|
}
|
||||||
.links-list .links-name {
|
.links-list .links-name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
.links-list .links-desc {
|
.links-list .links-desc {
|
||||||
margin: 0.25rem 0 0;
|
margin: 0.3rem 0 0;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.55;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
@@ -77,18 +101,79 @@
|
|||||||
.links-section:first-of-type .links-group-title {
|
.links-section:first-of-type .links-group-title {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
/* 申请友链按钮 */
|
||||||
.links-apply {
|
.links-apply {
|
||||||
margin-top: 2rem;
|
margin-top: 2.5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.link-apply-panel {
|
.btn-apply {
|
||||||
max-width: 32rem;
|
display: inline-block;
|
||||||
margin: 1.25rem auto 0;
|
padding: 0.6rem 1.75rem;
|
||||||
text-align: left;
|
border: 1px solid var(--color-primary);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: #fff;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
opacity var(--transition-fast),
|
||||||
|
transform var(--transition-fast);
|
||||||
|
}
|
||||||
|
.btn-apply:hover {
|
||||||
|
opacity: 0.88;
|
||||||
|
}
|
||||||
|
.btn-apply:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 申请弹窗(运行时移至 body 下,避免被父级 stacking context 遮挡) */
|
||||||
|
.link-apply-modal[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.link-apply-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.link-apply-dialog {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: min(26rem, calc(100vw - 2rem));
|
||||||
|
max-height: min(85vh, 44rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--color-bg);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg, 0.75rem);
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
|
||||||
|
padding: 1.5rem;
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
.link-apply-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.75rem;
|
||||||
|
right: 0.75rem;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.link-apply-close:hover {
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
}
|
}
|
||||||
.link-apply-title {
|
.link-apply-title {
|
||||||
margin: 0 0 0.75rem;
|
margin: 0 0 1rem;
|
||||||
font-size: 1.125rem;
|
font-size: 1.125rem;
|
||||||
|
padding-right: 2rem;
|
||||||
}
|
}
|
||||||
.apply-form .form-row {
|
.apply-form .form-row {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
@@ -174,12 +259,14 @@
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
|
<span class="links-avatar">
|
||||||
<img
|
<img
|
||||||
th:if="${not #strings.isEmpty(link.spec.logo)}"
|
th:if="${not #strings.isEmpty(link.spec.logo)}"
|
||||||
th:src="@{${link.spec.logo}}"
|
th:src="@{${link.spec.logo}}"
|
||||||
alt=""
|
alt=""
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
</span>
|
||||||
<span class="links-body">
|
<span class="links-body">
|
||||||
<span class="links-name" th:text="${link.spec.displayName}"
|
<span class="links-name" th:text="${link.spec.displayName}"
|
||||||
>链接名称</span
|
>链接名称</span
|
||||||
@@ -206,12 +293,14 @@
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
|
<span class="links-avatar">
|
||||||
<img
|
<img
|
||||||
th:if="${not #strings.isEmpty(link.spec.logo)}"
|
th:if="${not #strings.isEmpty(link.spec.logo)}"
|
||||||
th:src="@{${link.spec.logo}}"
|
th:src="@{${link.spec.logo}}"
|
||||||
alt=""
|
alt=""
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
</span>
|
||||||
<span class="links-body">
|
<span class="links-body">
|
||||||
<span class="links-name" th:text="${link.spec.displayName}"
|
<span class="links-name" th:text="${link.spec.displayName}"
|
||||||
>链接名称</span
|
>链接名称</span
|
||||||
@@ -235,12 +324,14 @@
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
|
<span class="links-avatar">
|
||||||
<img
|
<img
|
||||||
th:if="${not #strings.isEmpty(link.spec.logo)}"
|
th:if="${not #strings.isEmpty(link.spec.logo)}"
|
||||||
th:src="@{${link.spec.logo}}"
|
th:src="@{${link.spec.logo}}"
|
||||||
alt=""
|
alt=""
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
</span>
|
||||||
<span class="links-body">
|
<span class="links-body">
|
||||||
<span class="links-name" th:text="${link.spec.displayName}"
|
<span class="links-name" th:text="${link.spec.displayName}"
|
||||||
>链接名称</span
|
>链接名称</span
|
||||||
@@ -259,14 +350,35 @@
|
|||||||
|
|
||||||
<div class="links-apply">
|
<div class="links-apply">
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary"
|
class="btn-apply"
|
||||||
id="link-apply-toggle"
|
id="link-apply-toggle"
|
||||||
type="button"
|
type="button"
|
||||||
th:text="#{links.apply}"
|
th:text="#{links.apply}"
|
||||||
>申请友链</button>
|
>申请友链</button>
|
||||||
<div class="link-apply-panel apply-form" id="link-apply-panel" hidden>
|
</div>
|
||||||
<h2 class="link-apply-title" th:text="#{links.apply}">申请友链</h2>
|
</section>
|
||||||
<form id="link-apply-form" novalidate>
|
|
||||||
|
<!-- 申请友链弹窗:脚本运行时移至 body 下 -->
|
||||||
|
<div class="link-apply-modal" id="link-apply-modal" hidden>
|
||||||
|
<div class="link-apply-backdrop" data-close></div>
|
||||||
|
<div
|
||||||
|
class="link-apply-dialog"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="link-apply-title"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="link-apply-close"
|
||||||
|
type="button"
|
||||||
|
data-close
|
||||||
|
aria-label="关闭"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
<h2 class="link-apply-title" id="link-apply-title" th:text="#{links.apply}">
|
||||||
|
申请友链
|
||||||
|
</h2>
|
||||||
|
<form class="apply-form" id="link-apply-form" novalidate>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="f-url">网站地址 *</label>
|
<label for="f-url">网站地址 *</label>
|
||||||
<input id="f-url" name="url" type="url" required placeholder="https://example.com" />
|
<input id="f-url" name="url" type="url" required placeholder="https://example.com" />
|
||||||
@@ -302,12 +414,11 @@
|
|||||||
<input id="f-captcha" name="captchaCode" type="text" maxlength="5" required autocomplete="off" />
|
<input id="f-captcha" name="captchaCode" type="text" maxlength="5" required autocomplete="off" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" type="submit">提交申请</button>
|
<button class="btn-apply" type="submit">提交申请</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="apply-status" id="apply-status" role="alert"></div>
|
<div class="apply-status" id="apply-status" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="comments-section" th:if="${haloCommentEnabled}">
|
<div class="comments-section" th:if="${haloCommentEnabled}">
|
||||||
<halo:comment group="plugin.halo.run" kind="Plugin" th:attr="name=${pluginName}" />
|
<halo:comment group="plugin.halo.run" kind="Plugin" th:attr="name=${pluginName}" />
|
||||||
@@ -326,12 +437,35 @@
|
|||||||
var API = "/apis/api.link.halo.run/v1alpha1/link-applications";
|
var API = "/apis/api.link.halo.run/v1alpha1/link-applications";
|
||||||
var CAPTCHA = "/apis/api.link.halo.run/v1alpha1/link-applications/captcha";
|
var CAPTCHA = "/apis/api.link.halo.run/v1alpha1/link-applications/captcha";
|
||||||
var toggle = document.getElementById("link-apply-toggle");
|
var toggle = document.getElementById("link-apply-toggle");
|
||||||
var panel = document.getElementById("link-apply-panel");
|
var modal = document.getElementById("link-apply-modal");
|
||||||
var form = document.getElementById("link-apply-form");
|
var form = document.getElementById("link-apply-form");
|
||||||
var img = document.getElementById("captcha-img");
|
var img = document.getElementById("captcha-img");
|
||||||
var status = document.getElementById("apply-status");
|
var status = document.getElementById("apply-status");
|
||||||
var challengeId = null;
|
var challengeId = null;
|
||||||
|
|
||||||
|
// 弹窗移至 body 下,避免被页面父级 stacking context 遮挡
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
function openModal() {
|
||||||
|
modal.hidden = false;
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
if (!challengeId) loadCaptcha();
|
||||||
|
}
|
||||||
|
function closeModal() {
|
||||||
|
modal.hidden = true;
|
||||||
|
document.body.style.overflow = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle.addEventListener("click", openModal);
|
||||||
|
|
||||||
|
modal.addEventListener("click", function (e) {
|
||||||
|
if (e.target.closest("[data-close]")) closeModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keydown", function (e) {
|
||||||
|
if (e.key === "Escape" && !modal.hidden) closeModal();
|
||||||
|
});
|
||||||
|
|
||||||
function loadCaptcha() {
|
function loadCaptcha() {
|
||||||
fetch(CAPTCHA, { method: "POST" })
|
fetch(CAPTCHA, { method: "POST" })
|
||||||
.then(function (r) {
|
.then(function (r) {
|
||||||
@@ -348,13 +482,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle.addEventListener("click", function () {
|
|
||||||
panel.hidden = !panel.hidden;
|
|
||||||
if (!panel.hidden && !challengeId) {
|
|
||||||
loadCaptcha();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
img.addEventListener("click", loadCaptcha);
|
img.addEventListener("click", loadCaptcha);
|
||||||
|
|
||||||
form.addEventListener("submit", function (e) {
|
form.addEventListener("submit", function (e) {
|
||||||
@@ -394,6 +521,7 @@
|
|||||||
loadCaptcha();
|
loadCaptcha();
|
||||||
status.className = "apply-status ok";
|
status.className = "apply-status ok";
|
||||||
status.textContent = "申请已提交,等待管理员审核。";
|
status.textContent = "申请已提交,等待管理员审核。";
|
||||||
|
setTimeout(closeModal, 1500);
|
||||||
} else {
|
} else {
|
||||||
return r.json().then(function (err) {
|
return r.json().then(function (err) {
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
+23
-23
@@ -37,19 +37,10 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
.moment-owner-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
.moment-owner-name {
|
.moment-owner-name {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.moment-owner-bio {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
.moment-meta {
|
.moment-meta {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -69,17 +60,35 @@
|
|||||||
.moment-tags a {
|
.moment-tags a {
|
||||||
color: var(--color-link);
|
color: var(--color-link);
|
||||||
}
|
}
|
||||||
.moment-media img,
|
.moment-media {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 0.3rem;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
.moment-media img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
}
|
||||||
|
.moment-media img:only-child {
|
||||||
|
aspect-ratio: auto;
|
||||||
|
width: auto;
|
||||||
|
max-width: min(100%, 22rem);
|
||||||
|
max-height: 26rem;
|
||||||
|
}
|
||||||
.moment-media video {
|
.moment-media video {
|
||||||
|
grid-column: 1 / -1;
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
|
||||||
margin-top: 1rem;
|
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
}
|
}
|
||||||
.moment-media audio {
|
.moment-media audio {
|
||||||
|
grid-column: 1 / -1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
}
|
||||||
.comments-section {
|
.comments-section {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
@@ -104,18 +113,9 @@
|
|||||||
th:alt="${moment.owner.displayName}"
|
th:alt="${moment.owner.displayName}"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<span class="moment-owner-info">
|
<span class="moment-owner-name" th:text="${moment.owner.displayName}"
|
||||||
<span
|
|
||||||
class="moment-owner-name"
|
|
||||||
th:text="${moment.owner.displayName}"
|
|
||||||
>用户名</span
|
>用户名</span
|
||||||
>
|
>
|
||||||
<span
|
|
||||||
class="moment-owner-bio"
|
|
||||||
th:if="${not #strings.isEmpty(moment.owner.bio)}"
|
|
||||||
th:text="${moment.owner.bio}"
|
|
||||||
></span>
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
<span class="moment-meta">
|
<span class="moment-meta">
|
||||||
<time
|
<time
|
||||||
|
|||||||
+36
-8
@@ -29,13 +29,15 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
opacity: 0.75;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
}
|
||||||
.moments article header > a {
|
.moments article header > a {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
.moments article header > a:hover {
|
||||||
|
color: var(--color-link-hover);
|
||||||
}
|
}
|
||||||
.moments .moment-owner {
|
.moments .moment-owner {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -43,11 +45,15 @@
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
.moments .moment-owner img {
|
.moments .moment-owner img {
|
||||||
width: 2rem;
|
width: 2.25rem;
|
||||||
height: 2rem;
|
height: 2.25rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
.moments .moment-owner .moment-owner-name {
|
||||||
|
color: var(--color-text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
.moments .moment-tags {
|
.moments .moment-tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -58,17 +64,39 @@
|
|||||||
.moments .moment-tags a {
|
.moments .moment-tags a {
|
||||||
color: var(--color-link);
|
color: var(--color-link);
|
||||||
}
|
}
|
||||||
.moments .moment-media img,
|
/* 图片排版:仿微信朋友圈(多图九宫格方图,单图放大保持原比例) */
|
||||||
|
.moments .moment-media {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 0.3rem;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
.moments .moment-media img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
}
|
||||||
|
.moments .moment-media > :only-child {
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
|
.moments .moment-media img:only-child {
|
||||||
|
aspect-ratio: auto;
|
||||||
|
width: auto;
|
||||||
|
max-width: min(100%, 22rem);
|
||||||
|
max-height: 26rem;
|
||||||
|
}
|
||||||
.moments .moment-media video {
|
.moments .moment-media video {
|
||||||
|
grid-column: 1 / -1;
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
|
||||||
margin-top: 1rem;
|
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
}
|
}
|
||||||
.moments .moment-media audio {
|
.moments .moment-media audio {
|
||||||
|
grid-column: 1 / -1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
<include src="partials/layout.html">
|
|
||||||
<template name="head">
|
|
||||||
<title th:text="|${singlePage.spec.title} - ${site.title}|">
|
|
||||||
About
|
|
||||||
</title>
|
|
||||||
<th:block
|
|
||||||
th:with="seoTitle=${singlePage.spec.title}, seoUrl=${site.url + singlePage.status.permalink}, seoDesc=${singlePage.status.excerpt}, seoImage=${singlePage.spec.cover}, seoType='website'"
|
|
||||||
>
|
|
||||||
<include src="seo.html"></include>
|
|
||||||
</th:block>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<!-- 关于页面样式 -->
|
|
||||||
<div class="page-content about-page">
|
|
||||||
<div class="about-header">
|
|
||||||
<div class="about-avatar">
|
|
||||||
<img
|
|
||||||
th:src="${not #strings.isEmpty(theme.config?.about?.avatar) ? theme.config.about.avatar : ((singlePage.owner != null and not #strings.isEmpty(singlePage.owner.avatar)) ? singlePage.owner.avatar : site.logo)}"
|
|
||||||
th:alt="${singlePage.owner != null ? singlePage.owner.displayName : site.title}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<h1
|
|
||||||
class="about-name"
|
|
||||||
th:text="${singlePage.owner != null ? singlePage.owner.displayName : site.title}"
|
|
||||||
>
|
|
||||||
About
|
|
||||||
</h1>
|
|
||||||
<p
|
|
||||||
class="about-bio"
|
|
||||||
th:if="${not #strings.isEmpty(theme.config?.about?.bio)}"
|
|
||||||
th:text="${theme.config.about.bio}"
|
|
||||||
></p>
|
|
||||||
<div
|
|
||||||
class="about-social"
|
|
||||||
th:if="${not #strings.isEmpty(theme.config?.about?.github_url) or not #strings.isEmpty(theme.config?.about?.twitter_url) or not #strings.isEmpty(theme.config?.about?.contact_email)}"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
th:if="${not #strings.isEmpty(theme.config?.about?.github_url)}"
|
|
||||||
th:href="${theme.config.about.github_url}"
|
|
||||||
class="social-link"
|
|
||||||
th:title="#{about.social.github}"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path
|
|
||||||
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
th:if="${not #strings.isEmpty(theme.config?.about?.twitter_url)}"
|
|
||||||
th:href="${theme.config.about.twitter_url}"
|
|
||||||
class="social-link"
|
|
||||||
th:title="#{about.social.twitter}"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path
|
|
||||||
d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
th:if="${not #strings.isEmpty(theme.config?.about?.contact_email)}"
|
|
||||||
th:href="|mailto:${theme.config.about.contact_email}|"
|
|
||||||
class="social-link"
|
|
||||||
th:title="#{about.social.email}"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"
|
|
||||||
></path>
|
|
||||||
<polyline points="22,6 12,13 2,6"></polyline>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="about-content article-content">
|
|
||||||
<th:block th:utext="${singlePage.content.content}"></th:block>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</include>
|
|
||||||
+137
-261
@@ -6,85 +6,66 @@
|
|||||||
.container {
|
.container {
|
||||||
max-width: 75rem;
|
max-width: 75rem;
|
||||||
}
|
}
|
||||||
:where(.photos-groups) {
|
|
||||||
margin: 1.5rem 0;
|
/* 分组导航(图库标题下方) */
|
||||||
}
|
:where(.photos-nav) {
|
||||||
:where(.photos-groups__list, .photos-grid, .photos-card__tags) {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
:where(.photos-groups__list, .photos-card__tags) {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.75rem;
|
justify-content: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
margin: 1.25rem 0 0;
|
||||||
}
|
}
|
||||||
:where(.photos-group__link) {
|
:where(.photos-nav button) {
|
||||||
display: inline-flex;
|
padding: 0.3rem 1rem;
|
||||||
gap: 0.25rem;
|
|
||||||
align-items: baseline;
|
|
||||||
color: var(--color-link);
|
|
||||||
}
|
|
||||||
:where(.photos-group__link):hover {
|
|
||||||
color: var(--color-link-hover);
|
|
||||||
}
|
|
||||||
:where(.photos-group.is-active .photos-group__link) {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 相册网格 */
|
|
||||||
:where(.albums-grid) {
|
|
||||||
list-style: none;
|
|
||||||
margin: 1.5rem 0 0;
|
|
||||||
padding: 0;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(
|
|
||||||
auto-fill,
|
|
||||||
minmax(min(16rem, 100%), 1fr)
|
|
||||||
);
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
:where(.albums-card) {
|
|
||||||
display: block;
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
:where(.albums-card:hover .albums-card__name) {
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
:where(.albums-card__cover) {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 4 / 3;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
background: var(--color-bg-secondary);
|
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
color var(--transition-fast),
|
||||||
|
border-color var(--transition-fast),
|
||||||
|
background var(--transition-fast);
|
||||||
}
|
}
|
||||||
:where(.albums-card__cover img) {
|
:where(.photos-nav button:hover) {
|
||||||
position: absolute;
|
color: var(--color-primary);
|
||||||
inset: 0;
|
border-color: var(--color-primary);
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
}
|
||||||
:where(.albums-card__cover img.album-cover-custom) {
|
:where(.photos-nav button.is-active) {
|
||||||
z-index: 1;
|
color: #fff;
|
||||||
}
|
border-color: var(--color-primary);
|
||||||
:where(.albums-card__name) {
|
background: var(--color-primary);
|
||||||
margin: 0.75rem 0 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
:where(.albums-card__count) {
|
|
||||||
margin: 0.25rem 0 0;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 瀑布流(一行三张) */
|
/* 分组区块 */
|
||||||
|
:where(.photos-group)[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
:where(.photos-group-title) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 2rem 0 0;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
:where(.photos-group-title::before) {
|
||||||
|
content: "";
|
||||||
|
width: 0.2rem;
|
||||||
|
height: 1.1em;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--color-primary);
|
||||||
|
}
|
||||||
|
:where(.photos-group-section:first-of-type .photos-group-title) {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 瀑布流(一行三张,图片自适应原比例) */
|
||||||
:where(.photos-masonry) {
|
:where(.photos-masonry) {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 1.5rem 0 0;
|
margin: 1rem 0 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
columns: 3;
|
columns: 3;
|
||||||
column-gap: 1rem;
|
column-gap: 1rem;
|
||||||
@@ -93,23 +74,20 @@
|
|||||||
break-inside: avoid;
|
break-inside: avoid;
|
||||||
margin: 0 0 1rem;
|
margin: 0 0 1rem;
|
||||||
}
|
}
|
||||||
:where(.photos-masonry .photos-card__media) {
|
:where(.photos-item) {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
:where(.photos-masonry img) {
|
:where(.photos-item img) {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
background: var(--color-bg-secondary);
|
background: var(--color-bg-secondary);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.4s ease;
|
||||||
}
|
}
|
||||||
:where(.photos-masonry .photos-card__caption) {
|
:where(.photos-item img.is-loaded) {
|
||||||
margin: 0.4rem 0 0;
|
opacity: 1;
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
:where(.photos-masonry) {
|
:where(.photos-masonry) {
|
||||||
@@ -122,37 +100,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 预览模式切换 */
|
|
||||||
:where(.photos-toolbar) {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
:where(.photos-preview-toggle) {
|
|
||||||
padding: 0.4rem 0.9rem;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
background: var(--color-bg-secondary);
|
|
||||||
color: var(--color-text);
|
|
||||||
font: inherit;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
:where(.photos-preview-toggle)[aria-pressed="true"] {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
:where(.photos-empty) {
|
:where(.photos-empty) {
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
padding: 2rem 0;
|
padding: 2rem 0;
|
||||||
}
|
text-align: center;
|
||||||
:where(.photos-pagination) {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</template>
|
</template>
|
||||||
@@ -163,116 +114,49 @@
|
|||||||
<h1 class="photos-header__title" th:text="${title}">图库</h1>
|
<h1 class="photos-header__title" th:text="${title}">图库</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav class="photos-groups" aria-label="照片分组">
|
|
||||||
<ul class="photos-groups__list">
|
|
||||||
<li
|
|
||||||
class="photos-group"
|
|
||||||
th:classappend="${#strings.isEmpty(group)} ? 'is-active' : ''"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="photos-group__link"
|
|
||||||
th:href="${photoUrl.list()}"
|
|
||||||
th:attr="aria-current=${#strings.isEmpty(group) ? 'page' : null}"
|
|
||||||
>全部相册</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
class="photos-group"
|
|
||||||
th:each="photoGroup : ${groups}"
|
|
||||||
th:classappend="${group == photoGroup.metadata.name} ? 'is-active' : ''"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="photos-group__link"
|
|
||||||
th:href="${photoUrl.list(photoGroup.metadata.name)}"
|
|
||||||
th:attr="aria-current=${group == photoGroup.metadata.name ? 'page' : null}"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="photos-group__name"
|
|
||||||
th:text="${photoGroup.spec.displayName}"
|
|
||||||
>相册</span
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="photos-group__count"
|
|
||||||
th:if="${photoGroup.status != null and photoGroup.status.photoCount != null}"
|
|
||||||
th:text="|(${photoGroup.status.photoCount})|"
|
|
||||||
></span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- 相册视图:按分组展示各相册 -->
|
|
||||||
<th:block th:if="${#strings.isEmpty(group)}">
|
|
||||||
<th:block th:with="albums=${photoFinder.groupBy()}">
|
|
||||||
<ul class="albums-grid" th:if="${not #lists.isEmpty(albums)}">
|
|
||||||
<li th:each="album : ${albums}">
|
|
||||||
<a
|
|
||||||
class="albums-card"
|
|
||||||
th:href="${photoUrl.list(album.metadata.name)}"
|
|
||||||
>
|
|
||||||
<span class="albums-card__cover">
|
|
||||||
<img
|
|
||||||
th:if="${not #lists.isEmpty(album.photos)}"
|
|
||||||
th:src="${album.photos[0].spec.cover != null and !#strings.isEmpty(album.photos[0].spec.cover) ? album.photos[0].spec.cover : album.photos[0].spec.url}"
|
|
||||||
th:alt="${album.spec.displayName}"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<th:block
|
<th:block
|
||||||
th:if="${not #lists.isEmpty(theme.config?.photos?.album_covers)}"
|
th:with="albums=${photoFinder.groupBy()}, anyCheck=${photoFinder.list(1, 1)}, anyPhotos=${anyCheck != null ? anyCheck.items : null}"
|
||||||
>
|
>
|
||||||
<img
|
<!-- 分组导航 -->
|
||||||
class="album-cover-custom"
|
<nav
|
||||||
th:each="cv : ${theme.config.photos.album_covers}"
|
class="photos-nav"
|
||||||
th:if="${cv != null and cv.group == album.spec.displayName and not #strings.isEmpty(cv.cover)}"
|
id="photos-nav"
|
||||||
th:src="${cv.cover}"
|
aria-label="图库分组"
|
||||||
th:alt="${album.spec.displayName}"
|
th:if="${not #lists.isEmpty(albums)}"
|
||||||
loading="lazy"
|
>
|
||||||
/>
|
<button type="button" class="is-active" data-group="">全部</button>
|
||||||
</th:block>
|
<button
|
||||||
</span>
|
type="button"
|
||||||
<p
|
th:each="album : ${albums}"
|
||||||
class="albums-card__name"
|
th:attr="data-group=${album.metadata.name}"
|
||||||
th:text="${album.spec.displayName}"
|
th:text="${album.spec.displayName}"
|
||||||
>
|
>
|
||||||
相册
|
分组
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
class="albums-card__count"
|
|
||||||
th:if="${album.status != null and album.status.photoCount != null}"
|
|
||||||
th:text="#{photos.photo_count(${album.status.photoCount})}"
|
|
||||||
>
|
|
||||||
12 张照片
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p class="photos-empty" th:if="${#lists.isEmpty(albums)}" role="status">
|
|
||||||
暂无相册
|
|
||||||
</p>
|
|
||||||
</th:block>
|
|
||||||
</th:block>
|
|
||||||
|
|
||||||
<!-- 相册内瀑布流视图 -->
|
|
||||||
<th:block th:unless="${#strings.isEmpty(group)}">
|
|
||||||
<div class="photos-toolbar" th:if="${not #lists.isEmpty(photos.items)}">
|
|
||||||
<button
|
|
||||||
class="photos-preview-toggle"
|
|
||||||
id="photos-preview-toggle"
|
|
||||||
type="button"
|
|
||||||
th:attr="data-default=${theme.config?.photos?.preview_default != false ? 'on' : 'off'}"
|
|
||||||
>
|
|
||||||
预览模式
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</nav>
|
||||||
|
|
||||||
|
<!-- 各分组照片,一次全部渲染;默认「全部」视图,前台脚本切换 -->
|
||||||
|
<th:block th:if="${not #lists.isEmpty(albums)}">
|
||||||
|
<section
|
||||||
|
class="photos-group-section"
|
||||||
|
th:each="album : ${albums}"
|
||||||
|
th:attr="data-group=${album.metadata.name}"
|
||||||
|
>
|
||||||
|
<h2 class="photos-group-title" th:text="${album.spec.displayName}">
|
||||||
|
分组
|
||||||
|
</h2>
|
||||||
|
<th:block th:with="rawPhotos=${album.photos}">
|
||||||
|
<th:block
|
||||||
|
th:with="photos=${(rawPhotos != null and !#lists.isEmpty(rawPhotos)) ? rawPhotos : photoFinder.listBy(album.metadata.name)}"
|
||||||
|
>
|
||||||
<ul
|
<ul
|
||||||
class="photos-masonry"
|
class="photos-masonry"
|
||||||
th:if="${not #lists.isEmpty(photos.items)}"
|
th:if="${not #lists.isEmpty(photos)}"
|
||||||
th:attr="data-lightbox=${theme.config?.basic?.enable_lightbox != false ? 'true' : null}"
|
th:attr="data-lightbox=${theme.config?.basic?.enable_lightbox != false ? 'true' : null}"
|
||||||
>
|
>
|
||||||
<li th:each="photo : ${photos.items}">
|
<li th:each="photo : ${photos}">
|
||||||
<a
|
<a
|
||||||
class="photos-card__media"
|
class="photos-item"
|
||||||
th:href="${photoUrl.detail(photo)}"
|
th:href="${photoUrl.detail(photo)}"
|
||||||
th:title="${photo.spec.displayName}"
|
th:title="${photo.spec.displayName}"
|
||||||
>
|
>
|
||||||
@@ -280,89 +164,81 @@
|
|||||||
th:src="${#strings.isEmpty(photo.spec.cover) ? photo.spec.url : photo.spec.cover}"
|
th:src="${#strings.isEmpty(photo.spec.cover) ? photo.spec.url : photo.spec.cover}"
|
||||||
th:alt="${photo.spec.displayName}"
|
th:alt="${photo.spec.displayName}"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<p
|
|
||||||
class="photos-card__caption"
|
|
||||||
th:if="${not #strings.isEmpty(photo.spec.displayName)}"
|
|
||||||
th:text="${photo.spec.displayName}"
|
|
||||||
>
|
|
||||||
照片
|
|
||||||
</p>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</th:block>
|
||||||
|
</th:block>
|
||||||
|
</section>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
class="photos-empty"
|
class="photos-empty"
|
||||||
th:if="${#lists.isEmpty(photos.items)}"
|
th:if="${#lists.isEmpty(anyPhotos)}"
|
||||||
role="status"
|
role="status"
|
||||||
>
|
>
|
||||||
暂无照片
|
暂无照片
|
||||||
</p>
|
</p>
|
||||||
|
</th:block>
|
||||||
<nav
|
|
||||||
class="photos-pagination"
|
|
||||||
th:if="${photos.hasPrevious() || photos.hasNext()}"
|
|
||||||
aria-label="分页"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="photos-pagination__previous"
|
|
||||||
th:if="${photos.hasPrevious()}"
|
|
||||||
th:href="${photos.prevUrl}"
|
|
||||||
>上一页</a
|
|
||||||
>
|
|
||||||
<span class="photos-pagination__status">
|
|
||||||
第 <strong th:text="${photos.page}">1</strong> 页,共
|
|
||||||
<strong th:text="${photos.totalPages}">1</strong> 页
|
|
||||||
</span>
|
|
||||||
<a
|
|
||||||
class="photos-pagination__next"
|
|
||||||
th:if="${photos.hasNext()}"
|
|
||||||
th:href="${photos.nextUrl}"
|
|
||||||
>下一页</a
|
|
||||||
>
|
|
||||||
</nav>
|
|
||||||
</th:block>
|
</th:block>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script th:inline="none">
|
<script th:inline="none">
|
||||||
(function () {
|
(function () {
|
||||||
var btn = document.getElementById("photos-preview-toggle");
|
var nav = document.getElementById("photos-nav");
|
||||||
var masonry = document.querySelector(".photos-masonry");
|
if (!nav) return;
|
||||||
if (!btn || !masonry) return;
|
|
||||||
|
|
||||||
var KEY = "whitespace-photo-preview";
|
var sections = document.querySelectorAll(".photos-group-section");
|
||||||
var defaultOn = btn.getAttribute("data-default") === "on";
|
var buttons = nav.querySelectorAll("button");
|
||||||
var stored = null;
|
|
||||||
|
// 深链接:?group=xxx(仅前台读取,不依赖服务端参数)
|
||||||
|
var initial = "";
|
||||||
try {
|
try {
|
||||||
stored = localStorage.getItem(KEY);
|
initial = new URLSearchParams(window.location.search).get("group") || "";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/* ignore */
|
initial = "";
|
||||||
}
|
}
|
||||||
var mode = stored === null ? defaultOn : stored === "on";
|
|
||||||
|
|
||||||
function apply() {
|
function apply(group) {
|
||||||
if (mode) {
|
Array.prototype.forEach.call(buttons, function (btn) {
|
||||||
masonry.setAttribute("data-lightbox", "true");
|
var active = btn.getAttribute("data-group") === group;
|
||||||
|
btn.classList.toggle("is-active", active);
|
||||||
|
});
|
||||||
|
Array.prototype.forEach.call(sections, function (sec) {
|
||||||
|
var match = group === "" || sec.getAttribute("data-group") === group;
|
||||||
|
if (match) {
|
||||||
|
sec.removeAttribute("hidden");
|
||||||
} else {
|
} else {
|
||||||
masonry.removeAttribute("data-lightbox");
|
sec.setAttribute("hidden", "");
|
||||||
}
|
}
|
||||||
btn.textContent = mode ? "预览模式:开" : "预览模式:关";
|
});
|
||||||
btn.setAttribute("aria-pressed", mode ? "true" : "false");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
btn.addEventListener("click", function () {
|
nav.addEventListener("click", function (e) {
|
||||||
mode = !mode;
|
var btn = e.target.closest("button[data-group]");
|
||||||
try {
|
if (!btn) return;
|
||||||
localStorage.setItem(KEY, mode ? "on" : "off");
|
apply(btn.getAttribute("data-group"));
|
||||||
} catch (e) {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
// 重新加载以按新模式初始化灯箱
|
|
||||||
window.location.reload();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
apply();
|
if (initial && nav.querySelector('button[data-group="' + initial + '"]')) {
|
||||||
|
apply(initial);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片加载后淡入
|
||||||
|
Array.prototype.forEach.call(
|
||||||
|
document.querySelectorAll(".photos-item img"),
|
||||||
|
function (img) {
|
||||||
|
if (img.complete && img.naturalWidth > 0) {
|
||||||
|
img.classList.add("is-loaded");
|
||||||
|
} else {
|
||||||
|
img.addEventListener("load", function () {
|
||||||
|
img.classList.add("is-loaded");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</include>
|
</include>
|
||||||
|
|||||||
+29
-1
@@ -1,6 +1,34 @@
|
|||||||
<include src="partials/layout.html">
|
<include src="partials/layout.html">
|
||||||
<template name="head">
|
<template name="head">
|
||||||
<title th:text="|#{tags.title} - ${site.title}|">Tags</title>
|
<title th:text="|#{tags.title} - ${site.title}|">Tags</title>
|
||||||
|
<style>
|
||||||
|
/* 标签云:紧凑标签片,保留间距 */
|
||||||
|
.tag-cloud {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.65rem 0.75rem;
|
||||||
|
margin: 1.75rem 0 0;
|
||||||
|
}
|
||||||
|
.tag-cloud .tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.35rem 0.95rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
transition:
|
||||||
|
color var(--transition-fast),
|
||||||
|
border-color var(--transition-fast);
|
||||||
|
}
|
||||||
|
.tag-cloud .tag:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<th:block
|
<th:block
|
||||||
th:with="seoTitle=#{tags.title}, seoUrl=${site.url + site.routes.tagsUri}, seoDesc=null, seoImage=${site.logo}, seoType='website'"
|
th:with="seoTitle=#{tags.title}, seoUrl=${site.url + site.routes.tagsUri}, seoDesc=null, seoImage=${site.logo}, seoType='website'"
|
||||||
>
|
>
|
||||||
@@ -19,7 +47,7 @@
|
|||||||
th:href="@{${tag.status.permalink}}"
|
th:href="@{${tag.status.permalink}}"
|
||||||
class="tag"
|
class="tag"
|
||||||
th:text="${tag.spec.displayName}"
|
th:text="${tag.spec.displayName}"
|
||||||
th:title="${tag.postCount != null ? tag.postCount + ' ' + #{'tag.count'} : ''}"
|
th:title="|${tag.postCount} #{tag.count}|"
|
||||||
></a>
|
></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+1
-5
@@ -14,17 +14,13 @@ spec:
|
|||||||
issues: https://git.nxxy335.top/nxxy335/halo-theme-whitespace/issues
|
issues: https://git.nxxy335.top/nxxy335/halo-theme-whitespace/issues
|
||||||
settingName: "whitespace-setting"
|
settingName: "whitespace-setting"
|
||||||
configMapName: "whitespace-configMap"
|
configMapName: "whitespace-configMap"
|
||||||
version: 0.2.0
|
version: 0.4.0
|
||||||
requires: ">=2.26.0"
|
requires: ">=2.26.0"
|
||||||
license:
|
license:
|
||||||
- name: "GPL-3.0"
|
- name: "GPL-3.0"
|
||||||
url: "https://github.com/halo-dev/theme-vite-starter/blob/main/LICENSE"
|
url: "https://github.com/halo-dev/theme-vite-starter/blob/main/LICENSE"
|
||||||
customTemplates:
|
customTemplates:
|
||||||
page:
|
page:
|
||||||
- name: 关于页面
|
|
||||||
description: 关于页面布局,展示头像、个人简介与社交链接
|
|
||||||
screenshot:
|
|
||||||
file: page_about.html
|
|
||||||
- name: 微信读书页面
|
- name: 微信读书页面
|
||||||
description: 适配 halo-weread-plugin,展示微信读书书架与读书笔记
|
description: 适配 halo-weread-plugin,展示微信读书书架与读书笔记
|
||||||
screenshot:
|
screenshot:
|
||||||
|
|||||||
Reference in New Issue
Block a user