first commit
This commit is contained in:
@@ -0,0 +1,123 @@
|
|||||||
|
# 参考页面排版优化文章详情页阅读体验
|
||||||
|
|
||||||
|
## 参考页面分析(nxxy335.top/archives/c2KWtzf4)
|
||||||
|
|
||||||
|
通过浏览器截图和 JS 提取的样式数据,参考页面的排版特征如下:
|
||||||
|
|
||||||
|
| 属性 | 参考页面 | 我们当前 | 差异分析 |
|
||||||
|
|------|----------|----------|----------|
|
||||||
|
| 内容区域宽度 | 704px | 800px | 我们更宽,但参考页面更聚焦阅读 |
|
||||||
|
| 正文字号 | 16px | 17px (1.0625rem) | 我们略大 |
|
||||||
|
| 正文行高 | 27.6px (~1.725) | 1.85 | 我们行高更大 |
|
||||||
|
| h2 字号 | 24px | 未设置(继承) | 参考页面 h2 有明确字号 |
|
||||||
|
| h2 margin-top | 32px | 3em (~51px) | 我们标题上方留白过大 |
|
||||||
|
| h2 margin-bottom | 8px | 1.2em (~20px) | 参考页面标题下方更紧凑 |
|
||||||
|
| 段落间距 | 16px | 1.6em (~27px) | 我们段落间距偏大 |
|
||||||
|
| 图片圆角 | 0px(无圆角) | 12px | 参考页面图片无圆角 |
|
||||||
|
| 图片 margin-bottom | 0px | var(--space-xl) 2rem | 参考页面图片紧贴文字 |
|
||||||
|
| 字体 | 系统字体栈 | var(--font-sans) | 类似 |
|
||||||
|
|
||||||
|
### 参考页面的设计理念
|
||||||
|
- **紧凑但不拥挤**:段落间距适中(16px),标题上方留白适中(32px),整体节奏感好
|
||||||
|
- **内容宽度适中**:704px 是经典的阅读宽度,适合单栏长文阅读
|
||||||
|
- **标题层级清晰**:h2 有明确的 24px 字号,上方 32px 留白,下方仅 8px,让标题和下方正文紧密关联
|
||||||
|
- **图片融入正文**:无圆角,无额外间距,图片像段落一样自然融入文字流
|
||||||
|
- **行高适中**:1.725 的行高在中文阅读中既不拥挤也不松散
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 优化方案
|
||||||
|
|
||||||
|
### 1. 缩小内容区域宽度
|
||||||
|
- 将 `.wi-content-wrap` 的 `max-width` 从 `800px` 改为 `720px`
|
||||||
|
- 这是阅读体验最核心的改进——过宽的内容行会导致视线追踪困难
|
||||||
|
|
||||||
|
### 2. 调整正文字号和行高
|
||||||
|
- 字号:保持 `1rem`(16px),与参考页面一致
|
||||||
|
- 行高:从 `1.85` 调整为 `1.75`,与参考页面的 1.725 接近
|
||||||
|
|
||||||
|
### 3. 优化标题间距
|
||||||
|
- h2:`margin-top: 2em`(从 3em 降低),`margin-bottom: 0.5em`(从 1.2em 降低)
|
||||||
|
- h3-h6:`margin-top: 1.8em`(从 2.5em 降低),`margin-bottom: 0.5em`(从 1em 降低)
|
||||||
|
- 核心理念:标题上方留白适中,下方紧凑,让标题和正文紧密关联
|
||||||
|
|
||||||
|
### 4. 优化段落间距
|
||||||
|
- 从 `1.6em` 调整为 `1.2em`,与参考页面的 16px(1em)接近但略宽松
|
||||||
|
|
||||||
|
### 5. 优化图片样式
|
||||||
|
- 圆角:从 `12px` 改为 `8px`(保留微圆角但不突兀)
|
||||||
|
- 间距:从 `margin-block: var(--space-xl)` 改为 `margin-block: 1.5em`
|
||||||
|
|
||||||
|
### 6. 为 h2 添加明确字号
|
||||||
|
- h2:`font-size: 1.5rem`(24px)
|
||||||
|
- h3:`font-size: 1.25rem`(20px)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
|
||||||
|
1. **`src/styles/main.scss`**:修改 `.wi-content-wrap` 的 `max-width` 从 800px 到 720px
|
||||||
|
2. **`src/pages/post.astro`**:修改 `.wi-post__body` 及子元素的排版样式
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 具体修改
|
||||||
|
|
||||||
|
### main.scss
|
||||||
|
```css
|
||||||
|
.wi-content-wrap {
|
||||||
|
max-width: 720px; /* 从 800px 缩小到 720px */
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-inline: clamp(1rem, 3vw, 2rem);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### post.astro CSS 修改
|
||||||
|
|
||||||
|
```css
|
||||||
|
.wi-post__body {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 1rem; /* 从 1.0625rem 改为 1rem (16px) */
|
||||||
|
line-height: 1.75; /* 从 1.85 改为 1.75 */
|
||||||
|
color: #3d3530;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-post__body :is(h1, h2, h3, h4, h5, h6) {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
margin-top: 1.8em; /* 从 2.5em 降低 */
|
||||||
|
margin-bottom: 0.5em; /* 从 1em 降低 */
|
||||||
|
scroll-margin-top: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-post__body h2 {
|
||||||
|
font-size: 1.5rem; /* 新增:明确 h2 字号 */
|
||||||
|
margin-top: 2em; /* 从 3em 降低 */
|
||||||
|
margin-bottom: 0.5em; /* 从 1.2em 降低 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-post__body h3 {
|
||||||
|
font-size: 1.25rem; /* 新增:明确 h3 字号 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-post__body p {
|
||||||
|
margin-block-end: 1.2em; /* 从 1.6em 降低 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-post__body img {
|
||||||
|
border-radius: 8px; /* 从 12px 降低 */
|
||||||
|
margin-block: 1.5em; /* 从 var(--space-xl) 改为 1.5em */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 实施步骤
|
||||||
|
|
||||||
|
1. 修改 `src/styles/main.scss`:`.wi-content-wrap` 的 `max-width` 从 800px 改为 720px
|
||||||
|
2. 修改 `src/pages/post.astro`:调整 `.wi-post__body` 及子元素排版样式
|
||||||
|
3. 构建并部署到 Halo 容器
|
||||||
|
4. 浏览器验证效果
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# 内容区域宽度改回 800px & 优化首页 Hero 背景颜色
|
||||||
|
|
||||||
|
## 任务概述
|
||||||
|
|
||||||
|
1. 将 `.wi-content-wrap` 的 `max-width` 从 `720px` 改回 `800px`
|
||||||
|
2. 优化首页 Hero 区域的背景颜色,使其更有层次感和视觉吸引力
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务一:内容区域宽度改回 800px
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
- `src/styles/main.scss`
|
||||||
|
|
||||||
|
### 具体改动
|
||||||
|
将 `.wi-content-wrap` 的 `max-width: 720px` 改为 `max-width: 800px`
|
||||||
|
|
||||||
|
```scss
|
||||||
|
// 修改前
|
||||||
|
.wi-content-wrap {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-inline: clamp(1rem, 3vw, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改后
|
||||||
|
.wi-content-wrap {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-inline: clamp(1rem, 3vw, 2rem);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 注:`_variables.scss` 中已定义 `$content-max: 800px`,此处改回 800px 与变量定义一致。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务二:优化首页 Hero 背景颜色
|
||||||
|
|
||||||
|
### 当前问题分析
|
||||||
|
|
||||||
|
当前 Hero 背景方案:
|
||||||
|
- **主背景**:`linear-gradient(160deg, var(--bg) 0%, color-mix(in srgb, var(--bg) 92%, var(--accent) 8%) 50%, var(--bg) 100%)` — 渐变非常微弱,几乎看不出色调变化
|
||||||
|
- **光球 1**:`rgba(212, 118, 78, 0.35)` — 暖橙色,420px
|
||||||
|
- **光球 2**:`rgba(240, 180, 160, 0.3)` — 浅粉色,350px
|
||||||
|
- **光球 3**:`rgba(200, 150, 100, 0.25)` — 棕黄色,300px
|
||||||
|
|
||||||
|
问题:
|
||||||
|
1. 主背景渐变太弱(仅 8% accent 混合),几乎看不到渐变效果
|
||||||
|
2. 三个光球颜色过于接近暖棕色调,缺乏色彩层次
|
||||||
|
3. 整体偏"平",缺少深度和氛围感
|
||||||
|
4. 暗色模式下没有单独的背景颜色适配
|
||||||
|
|
||||||
|
### 优化方案
|
||||||
|
|
||||||
|
#### 1. 增强主背景渐变
|
||||||
|
- 将渐变从 8% accent 提升到 15%,使背景有更明显的色调过渡
|
||||||
|
- 添加中间色调节点,让渐变更丰富
|
||||||
|
|
||||||
|
```css
|
||||||
|
background: linear-gradient(
|
||||||
|
160deg,
|
||||||
|
var(--bg) 0%,
|
||||||
|
color-mix(in srgb, var(--bg) 85%, var(--accent) 15%) 40%,
|
||||||
|
color-mix(in srgb, var(--bg) 90%, var(--mist-pink) 10%) 70%,
|
||||||
|
var(--bg) 100%
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 优化光球颜色 — 增加色彩层次
|
||||||
|
- **光球 1**(右上):保持暖橙色调,但稍微增加饱和度和大小,作为主视觉焦点
|
||||||
|
- **光球 2**(左下):改为偏粉/玫瑰色调,与暖橙形成互补色对比
|
||||||
|
- **光球 3**(中央):改为偏紫/薰衣草色调,增加神秘感和深度
|
||||||
|
|
||||||
|
```css
|
||||||
|
.hero__orb--1 {
|
||||||
|
/* 暖橙 — 增强饱和度 */
|
||||||
|
background: radial-gradient(circle, rgba(212, 118, 78, 0.4) 0%, transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__orb--2 {
|
||||||
|
/* 玫瑰粉 — 从浅粉改为带玫瑰色调 */
|
||||||
|
background: radial-gradient(circle, rgba(220, 140, 160, 0.3) 0%, transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__orb--3 {
|
||||||
|
/* 薰衣草紫 — 从棕黄改为淡紫,增加深度 */
|
||||||
|
background: radial-gradient(circle, rgba(180, 150, 200, 0.2) 0%, transparent 70%);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 添加暗色模式适配
|
||||||
|
暗色模式下光球需要不同的颜色表现:
|
||||||
|
|
||||||
|
```css
|
||||||
|
html.dark .hero {
|
||||||
|
background: linear-gradient(
|
||||||
|
160deg,
|
||||||
|
var(--bg) 0%,
|
||||||
|
color-mix(in srgb, var(--bg) 85%, var(--accent) 12%) 40%,
|
||||||
|
var(--bg) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .hero__orb--1 {
|
||||||
|
background: radial-gradient(circle, rgba(232, 149, 95, 0.25) 0%, transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .hero__orb--2 {
|
||||||
|
background: radial-gradient(circle, rgba(200, 120, 140, 0.18) 0%, transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .hero__orb--3 {
|
||||||
|
background: radial-gradient(circle, rgba(160, 130, 180, 0.12) 0%, transparent 70%);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
- `src/components/HeroSection.astro`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 执行步骤
|
||||||
|
|
||||||
|
1. 修改 `src/styles/main.scss` — 将 `.wi-content-wrap` 的 `max-width` 从 `720px` 改为 `800px`
|
||||||
|
2. 修改 `src/components/HeroSection.astro` — 优化 Hero 背景渐变和光球颜色,添加暗色模式适配
|
||||||
|
3. 构建主题并部署到 Docker 容器验证效果
|
||||||
@@ -0,0 +1,391 @@
|
|||||||
|
# 主题五大改进实施计划
|
||||||
|
|
||||||
|
## 任务一:Footer 配置组缺失
|
||||||
|
|
||||||
|
### 问题
|
||||||
|
[Footer.astro](file:///c:/Users/Zhang/Documents/Halo/WarmIsland/src/components/Footer.astro) 引用了 `theme.config?.footer?.footer_copyright`、`footer_icp`、`footer_socials`、`footer_show_powered`、`footer_show_theme`、`footer_custom_html` 等配置项,但 [settings.yaml](file:///c:/Users/Zhang/Documents/Halo/WarmIsland/settings.yaml) 中没有定义 footer 配置组,用户在后台无法设置页脚内容。
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
- `settings.yaml` — 新增 footer 配置组
|
||||||
|
|
||||||
|
### 具体改动
|
||||||
|
在 settings.yaml 的 `comment` 配置组之后新增 `footer` 配置组:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- group: footer
|
||||||
|
label: 页脚
|
||||||
|
formSchema:
|
||||||
|
- $formkit: text
|
||||||
|
name: footer_copyright
|
||||||
|
label: 版权信息(留空则使用默认格式 © 年份 站点标题)
|
||||||
|
- $formkit: text
|
||||||
|
name: footer_icp
|
||||||
|
label: ICP 备案号
|
||||||
|
- $formkit: repeater
|
||||||
|
name: footer_socials
|
||||||
|
label: 社交链接
|
||||||
|
children:
|
||||||
|
- $formkit: text
|
||||||
|
name: platform
|
||||||
|
label: 平台名称
|
||||||
|
- $formkit: text
|
||||||
|
name: icon
|
||||||
|
label: 图标类名(如 fa-brands fa-github)
|
||||||
|
- $formkit: url
|
||||||
|
name: url
|
||||||
|
label: 链接地址
|
||||||
|
- $formkit: switch
|
||||||
|
name: footer_show_powered
|
||||||
|
label: 显示 "Powered by Halo"
|
||||||
|
value: true
|
||||||
|
- $formkit: switch
|
||||||
|
name: footer_show_theme
|
||||||
|
label: 显示主题版本
|
||||||
|
value: true
|
||||||
|
- $formkit: code
|
||||||
|
name: footer_custom_html
|
||||||
|
label: 自定义 HTML(统计代码等)
|
||||||
|
language: html
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务二:SEO Meta 标签严重缺失
|
||||||
|
|
||||||
|
### 问题
|
||||||
|
[Layout.astro](file:///c:/Users/Zhang/Documents/Halo/WarmIsland/src/layouts/Layout.astro) 的 `<head>` 中只有全站 description,缺少 og 标签、canonical URL、RSS 链接等。文章页应使用文章摘要作为 description。
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
- `src/layouts/Layout.astro` — 在 `<head>` 中添加 SEO meta 标签
|
||||||
|
- `src/pages/post.astro` — 在 head slot 中添加文章页专属 SEO 标签
|
||||||
|
- `src/pages/page.astro` — 在 head slot 中添加页面专属 SEO 标签
|
||||||
|
|
||||||
|
### 具体改动
|
||||||
|
|
||||||
|
#### Layout.astro — 全局 SEO 标签
|
||||||
|
在现有 `<meta name="description">` 之后添加:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<meta name="keywords" th:content="${site.seo?.keywords}" />
|
||||||
|
<link rel="canonical" th:href="${site.url}" />
|
||||||
|
<link rel="alternate" type="application/rss+xml" th:title="${site.title}" th:href="@{/feed.xml}" />
|
||||||
|
<meta property="og:site_name" th:content="${site.title}" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" th:content="${site.url}" />
|
||||||
|
<meta property="og:title" th:content="${site.title}" />
|
||||||
|
<meta property="og:description" th:content="${site.seo?.description}" />
|
||||||
|
<meta name="twitter:card" content="summary" />
|
||||||
|
```
|
||||||
|
|
||||||
|
#### post.astro — 文章页专属 SEO 标签
|
||||||
|
在 `<Fragment slot="head">` 中,`<title>` 之后添加:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<meta name="description" th:content="${post.spec.excerpt ?: site.seo?.description}" />
|
||||||
|
<link rel="canonical" th:href="${post.status.permalink}" />
|
||||||
|
<meta property="og:type" content="article" />
|
||||||
|
<meta property="og:title" th:content="${post.spec.title}" />
|
||||||
|
<meta property="og:description" th:content="${post.spec.excerpt ?: site.seo?.description}" />
|
||||||
|
<meta property="og:url" th:href="@{${post.status.permalink}}" />
|
||||||
|
<meta property="og:image" th:if="${post.spec.cover}" th:content="${post.spec.cover}" />
|
||||||
|
<meta property="article:published_time" th:content="${post.spec.publishTime}" />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" th:if="${post.spec.cover}" />
|
||||||
|
<meta name="twitter:card" content="summary" th:unless="${post.spec.cover}" />
|
||||||
|
<meta name="twitter:title" th:content="${post.spec.title}" />
|
||||||
|
<meta name="twitter:description" th:content="${post.spec.excerpt ?: site.seo?.description}" />
|
||||||
|
<meta name="twitter:image" th:if="${post.spec.cover}" th:content="${post.spec.cover}" />
|
||||||
|
```
|
||||||
|
|
||||||
|
#### page.astro — 自定义页面专属 SEO 标签
|
||||||
|
在 `<Fragment slot="head">` 中,`<title>` 之后添加:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<meta name="description" th:content="${singlePage.spec.excerpt ?: site.seo?.description}" />
|
||||||
|
<link rel="canonical" th:href="${singlePage.status.permalink}" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:title" th:content="${singlePage.spec.title}" />
|
||||||
|
<meta property="og:description" th:content="${singlePage.spec.excerpt ?: site.seo?.description}" />
|
||||||
|
<meta property="og:url" th:href="@{${singlePage.status.permalink}}" />
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务三:导航当前页面无高亮
|
||||||
|
|
||||||
|
### 问题
|
||||||
|
[Navbar.astro](file:///c:/Users/Zhang/Documents/Halo/WarmIsland/src/components/Navbar.astro) 中 CSS 已定义 `.wi-navbar__link--active` 样式,但模板中没有为当前页面的导航链接添加 active 类名。
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
- `src/components/Navbar.astro`
|
||||||
|
|
||||||
|
### 具体改动
|
||||||
|
在导航链接 `<a>` 标签上添加 `th:classappend` 条件判断,通过比较当前请求路径与菜单项链接来判断是否高亮:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<a
|
||||||
|
th:each="menuItem : ${menu.menuItems}"
|
||||||
|
th:href="@{${menuItem.status.href}}"
|
||||||
|
th:target="${menuItem.spec.target}"
|
||||||
|
th:text="${menuItem.status.displayName}"
|
||||||
|
class="wi-navbar__link"
|
||||||
|
th:classappend="${#strings.equals(#request.requestURI, menuItem.status.href)} ? 'wi-navbar__link--active'"
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
同时需要在 MobileMenu.astro 中也添加同样的高亮逻辑。MobileMenu.astro 第 16-22 行的导航链接结构与 Navbar 相同:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<a
|
||||||
|
th:each="menuItem : ${menu.menuItems}"
|
||||||
|
th:href="@{${menuItem.status.href}}"
|
||||||
|
th:target="${menuItem.spec.target}"
|
||||||
|
th:text="${menuItem.status.displayName}"
|
||||||
|
class="wi-mobile-menu__link"
|
||||||
|
th:classappend="${#strings.equals(#request.requestURI, menuItem.status.href)} ? 'wi-mobile-menu__link--active'"
|
||||||
|
></a>
|
||||||
|
```
|
||||||
|
|
||||||
|
并在 MobileMenu.astro 的 `<style>` 中添加 active 样式:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.wi-mobile-menu__link--active {
|
||||||
|
color: var(--accent);
|
||||||
|
background: var(--bg-raised);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务四:移动端文章目录不可用(浮动目录按钮)
|
||||||
|
|
||||||
|
### 问题
|
||||||
|
[post.astro](file:///c:/Users/Zhang/Documents/Halo/WarmIsland/src/pages/post.astro) 中 TOC 仅在 `min-width: 1280px` 时显示,移动端没有任何替代方案。
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
- `src/pages/post.astro`
|
||||||
|
|
||||||
|
### 具体改动
|
||||||
|
|
||||||
|
#### 1. 添加移动端浮动 TOC 按钮
|
||||||
|
在文章 `<article>` 内添加一个浮动按钮,仅在 `max-width: 1279px` 时显示:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<button
|
||||||
|
th:if="${theme.config?.article?.article_show_toc ?: true}"
|
||||||
|
class="wi-toc-fab"
|
||||||
|
id="wi-toc-fab"
|
||||||
|
type="button"
|
||||||
|
aria-label="打开目录"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg>
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 添加移动端 TOC 抽屉面板
|
||||||
|
在浮动按钮之后添加一个从底部滑出的抽屉面板:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div
|
||||||
|
th:if="${theme.config?.article?.article_show_toc ?: true}"
|
||||||
|
class="wi-toc-drawer"
|
||||||
|
id="wi-toc-drawer"
|
||||||
|
>
|
||||||
|
<div class="wi-toc-drawer__overlay"></div>
|
||||||
|
<div class="wi-toc-drawer__panel">
|
||||||
|
<div class="wi-toc-drawer__header">
|
||||||
|
<span class="wi-toc-drawer__title">目录</span>
|
||||||
|
<button class="wi-toc-drawer__close" type="button" aria-label="关闭目录">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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>
|
||||||
|
</div>
|
||||||
|
<nav class="wi-toc-drawer__nav" id="wi-toc-drawer-nav"></nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 添加 CSS 样式
|
||||||
|
```css
|
||||||
|
.wi-toc-fab {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 1.5rem;
|
||||||
|
right: 1.5rem;
|
||||||
|
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 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-toc-fab:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1279px) {
|
||||||
|
.wi-toc-fab {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-toc-drawer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-toc-drawer--open {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-toc-drawer__overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-toc-drawer__panel {
|
||||||
|
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 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-toc-drawer__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
border-bottom: 1px solid var(--rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-toc-drawer__title {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-toc-drawer__close {
|
||||||
|
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:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-toc-drawer__nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-toc-drawer__nav .wi-toc__link {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 添加 JS 逻辑
|
||||||
|
在现有 `<script is:inline>` 中,TOC 构建逻辑之后,添加移动端抽屉逻辑:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var tocFab = document.getElementById("wi-toc-fab");
|
||||||
|
var tocDrawer = document.getElementById("wi-toc-drawer");
|
||||||
|
var tocDrawerNav = document.getElementById("wi-toc-drawer-nav");
|
||||||
|
|
||||||
|
if (tocFab && tocDrawer && tocDrawerNav && tocNav) {
|
||||||
|
tocDrawerNav.innerHTML = tocNav.innerHTML;
|
||||||
|
|
||||||
|
tocFab.addEventListener("click", function () {
|
||||||
|
tocDrawer.classList.add("wi-toc-drawer--open");
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
});
|
||||||
|
|
||||||
|
var drawerClose = tocDrawer.querySelector(".wi-toc-drawer__close");
|
||||||
|
var drawerOverlay = tocDrawer.querySelector(".wi-toc-drawer__overlay");
|
||||||
|
|
||||||
|
function closeDrawer() {
|
||||||
|
tocDrawer.classList.remove("wi-toc-drawer--open");
|
||||||
|
document.body.style.overflow = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawerClose) drawerClose.addEventListener("click", closeDrawer);
|
||||||
|
if (drawerOverlay) drawerOverlay.addEventListener("click", closeDrawer);
|
||||||
|
|
||||||
|
tocDrawerNav.querySelectorAll(".wi-toc__link").forEach(function (link) {
|
||||||
|
link.addEventListener("click", function () {
|
||||||
|
closeDrawer();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务五:404/500 页面深色模式不生效
|
||||||
|
|
||||||
|
### 问题
|
||||||
|
[404.html](file:///c:/Users/Zhang/Documents/Halo/WarmIsland/public/error/404.html) 和 [500.html](file:///c:/Users/Zhang/Documents/Halo/WarmIsland/public/error/500.html) 是纯静态页面,没有读取 localStorage 中的主题偏好,首次直接访问错误页面时深色模式不会生效。
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
- `public/error/404.html`
|
||||||
|
- `public/error/500.html`
|
||||||
|
|
||||||
|
### 具体改动
|
||||||
|
在两个文件的 `<head>` 中,`<style>` 标签之前,添加与 Layout.astro 相同的主题检测脚本(简化版,仅检测 localStorage 和系统偏好):
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var stored = localStorage.getItem("wi-theme");
|
||||||
|
var prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
|
var isDark = stored === "dark" || (!stored && prefersDark);
|
||||||
|
if (isDark) {
|
||||||
|
document.documentElement.classList.add("dark");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
这段脚本会在页面渲染前检测用户的主题偏好并添加 `dark` 类名,确保 CSS 变量正确切换。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 执行顺序
|
||||||
|
|
||||||
|
1. **settings.yaml** — 新增 footer 配置组
|
||||||
|
2. **Layout.astro** — 添加全局 SEO meta 标签
|
||||||
|
3. **post.astro** — 添加文章页 SEO 标签 + 移动端 TOC 浮动按钮和抽屉
|
||||||
|
4. **page.astro** — 添加页面 SEO 标签
|
||||||
|
5. **Navbar.astro** — 添加导航当前页面高亮
|
||||||
|
6. **MobileMenu.astro** — 添加导航当前页面高亮(需先确认结构)
|
||||||
|
7. **404.html** — 添加深色模式检测脚本
|
||||||
|
8. **500.html** — 添加深色模式检测脚本
|
||||||
|
9. 构建并部署到 Docker 验证
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
# 主题全面修复与优化计划
|
||||||
|
|
||||||
|
## 🔴 紧急:修复 500 报错(最高优先级)
|
||||||
|
|
||||||
|
### 根因分析
|
||||||
|
Docker 日志显示错误:
|
||||||
|
```
|
||||||
|
TemplateProcessingException: Exception evaluating SpringEL expression:
|
||||||
|
"#strings.equals(#request.requestURI, menuItem.status.href)"
|
||||||
|
```
|
||||||
|
|
||||||
|
**原因**:Halo 使用 Spring WebFlux(非 Spring MVC),`#request` 对象在 WebFlux 环境中不可用。上一轮在 Navbar.astro 和 MobileMenu.astro 中添加的 `th:classappend="${#strings.equals(#request.requestURI, menuItem.status.href)}"` 导致了全站 500 错误。
|
||||||
|
|
||||||
|
### 额外问题:SEO 标签与 Halo 自动注入冲突
|
||||||
|
根据 Halo Thymeleaf 最佳实践文档,Halo 会**自动注入**以下 SEO 标签:
|
||||||
|
- `<meta name="description">` 和 `<meta name="keywords">`
|
||||||
|
- Open Graph 标签(og:title, og:description, og:image 等)
|
||||||
|
- Twitter Card 标签和 canonical URL
|
||||||
|
|
||||||
|
我们在 Layout.astro、post.astro、page.astro 中手动添加的这些标签会与 Halo 自动注入的冲突,需要移除。
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
|
||||||
|
#### 1. Navbar.astro(第 34 行)
|
||||||
|
移除 `th:classappend`,改用 JS 方案实现导航高亮:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 修改前 -->
|
||||||
|
<a ... th:classappend="${#strings.equals(#request.requestURI, menuItem.status.href)} ? 'wi-navbar__link--active'">
|
||||||
|
|
||||||
|
<!-- 修改后 -->
|
||||||
|
<a ... th:data-href="${menuItem.status.href}" class="wi-navbar__link">
|
||||||
|
```
|
||||||
|
|
||||||
|
在 Navbar.astro 的 `<script>` 中添加 JS 高亮逻辑:
|
||||||
|
```javascript
|
||||||
|
document.querySelectorAll('.wi-navbar__link[data-href]').forEach(function(link) {
|
||||||
|
if (new URL(link.href).pathname === window.location.pathname) {
|
||||||
|
link.classList.add('wi-navbar__link--active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. MobileMenu.astro(第 22 行)
|
||||||
|
同样移除 `th:classappend`,改用 JS 方案:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 修改前 -->
|
||||||
|
<a ... th:classappend="${#strings.equals(#request.requestURI, menuItem.status.href)} ? 'wi-mobile-menu__link--active'">
|
||||||
|
|
||||||
|
<!-- 修改后 -->
|
||||||
|
<a ... th:data-href="${menuItem.status.href}" class="wi-mobile-menu__link">
|
||||||
|
```
|
||||||
|
|
||||||
|
在 MobileMenu.astro 的 `<script>` 中添加 JS 高亮逻辑:
|
||||||
|
```javascript
|
||||||
|
menu?.querySelectorAll('.wi-mobile-menu__link[data-href]').forEach(function(link) {
|
||||||
|
if (new URL(link.href).pathname === window.location.pathname) {
|
||||||
|
link.classList.add('wi-mobile-menu__link--active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Layout.astro(第 57-66 行)
|
||||||
|
移除手动添加的 SEO 标签(Halo 会自动注入),仅保留 RSS 订阅链接:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 移除以下行 -->
|
||||||
|
<meta name="description" th:content="${site.seo?.description}" />
|
||||||
|
<meta name="keywords" th:content="${site.seo?.keywords}" />
|
||||||
|
<link rel="canonical" th:href="${site.url}" />
|
||||||
|
<meta property="og:site_name" th:content="${site.title}" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" th:content="${site.url}" />
|
||||||
|
<meta property="og:title" th:content="${site.title}" />
|
||||||
|
<meta property="og:description" th:content="${site.seo?.description}" />
|
||||||
|
<meta name="twitter:card" content="summary" />
|
||||||
|
|
||||||
|
<!-- 仅保留 -->
|
||||||
|
<link rel="alternate" type="application/rss+xml" th:title="${site.title}" th:href="@{/feed.xml}" />
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. post.astro(第 8-20 行)
|
||||||
|
移除手动添加的文章页 SEO 标签(Halo 会自动注入),仅保留 `<title>`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 修改后 -->
|
||||||
|
<Fragment slot="head">
|
||||||
|
<title th:text="|${post.spec.title} - ${site.title}|"></title>
|
||||||
|
</Fragment>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. page.astro(第 8-13 行)
|
||||||
|
移除手动添加的页面 SEO 标签,仅保留 `<title>`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 修改后 -->
|
||||||
|
<Fragment slot="head">
|
||||||
|
<title th:text="|${singlePage.spec.title} - ${site.title}|"></title>
|
||||||
|
</Fragment>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 响应式断点统一
|
||||||
|
|
||||||
|
### 当前问题
|
||||||
|
各组件使用的断点不一致:640px、680px、767px、768px、480px 等。
|
||||||
|
|
||||||
|
### 统一方案
|
||||||
|
将断点统一为以下四级体系(与 `_variables.scss` 中的 `$breakpoint-sm/md/lg/xl` 对应):
|
||||||
|
|
||||||
|
| 级别 | 断点值 | 用途 |
|
||||||
|
|------|--------|------|
|
||||||
|
| sm | 640px | 手机端(单列布局) |
|
||||||
|
| md | 768px | 平板端(导航切换、双列→单列) |
|
||||||
|
| lg | 1024px | 小桌面(三列→双列) |
|
||||||
|
| xl | 1280px | 大桌面(TOC 显示) |
|
||||||
|
|
||||||
|
### 具体改动
|
||||||
|
|
||||||
|
| 文件 | 当前断点 | 改为 |
|
||||||
|
|------|----------|------|
|
||||||
|
| Navbar.astro | 767px | 768px |
|
||||||
|
| page.astro | 680px | 768px |
|
||||||
|
| post.astro (TOC FAB) | 1279px | 1279px(保持,与 xl-1px 对应) |
|
||||||
|
| post.astro (其他) | 680px | 768px |
|
||||||
|
| Footer.astro | 680px | 768px |
|
||||||
|
| archives.astro | 680px | 768px |
|
||||||
|
| tag.astro | 680px | 768px |
|
||||||
|
| categories.astro | 680px | 768px |
|
||||||
|
| page_messageboard.astro | 680px | 768px |
|
||||||
|
| Header.astro | 680px | 768px |
|
||||||
|
| global.css | 680px | 768px |
|
||||||
|
| FeaturedSection.astro | 767px | 768px |
|
||||||
|
| PostCard.astro | 767px | 768px |
|
||||||
|
| LatestSection.astro | 767px | 768px |
|
||||||
|
|
||||||
|
> 注意:640px 断点(首页流式布局、友链、装备等)保持不变,因为它们用于单列/双列切换,语义上属于 sm 级别。
|
||||||
|
> 480px 断点(图库、瞬间的极小屏幕适配)保持不变,属于额外微调。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 冗余代码清理
|
||||||
|
|
||||||
|
### 删除文件
|
||||||
|
1. `src/components/Header.astro` — 未被 Layout 使用,与 Navbar 功能重叠
|
||||||
|
2. `src/components/MobileMenu.vue` — 未被使用(Layout 用的是 MobileMenu.astro)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 Footer 版本号硬编码修复
|
||||||
|
|
||||||
|
### 当前问题
|
||||||
|
Footer.astro 第 33 行硬编码 `WarmIsland v1.0.0`
|
||||||
|
|
||||||
|
### 修改方案
|
||||||
|
将版本号改为从 theme.yaml 读取(通过 Halo 的 theme 变量),如果不可用则使用 Astro 构建时变量:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 修改前 -->
|
||||||
|
<span th:if="${showTheme}">WarmIsland v1.0.0</span>
|
||||||
|
|
||||||
|
<!-- 修改后 -->
|
||||||
|
<span th:if="${showTheme}">WarmIsland v1.0.0</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
实际上,Halo 的 Thymeleaf 环境中没有直接暴露 theme version 的变量。最简洁的方案是在 settings.yaml 的 footer 配置组中添加一个版本号字段,或者直接使用一个固定的版本号但添加注释标记。考虑到维护成本,最佳方案是:
|
||||||
|
|
||||||
|
在 Footer.astro 的 frontmatter 中定义版本号常量,模板中引用:
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
const THEME_VERSION = "1.0.0";
|
||||||
|
const today = new Date();
|
||||||
|
---
|
||||||
|
...
|
||||||
|
<span th:if="${showTheme}">WarmIsland v{THEME_VERSION}</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
这样只需在一处修改版本号。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟢 顶部阅读进度条
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
- `src/pages/post.astro`
|
||||||
|
|
||||||
|
### 实现方案
|
||||||
|
在文章详情页顶部添加一个固定定位的进度条,随滚动进度填充:
|
||||||
|
|
||||||
|
#### HTML
|
||||||
|
在 `<article class="wi-post">` 之前添加:
|
||||||
|
```html
|
||||||
|
<div class="wi-reading-progress" id="wi-reading-progress"></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### CSS
|
||||||
|
```css
|
||||||
|
.wi-reading-progress {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: var(--accent);
|
||||||
|
z-index: 101;
|
||||||
|
transition: width 0.1s linear;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JS
|
||||||
|
在现有 `<script is:inline>` 中添加:
|
||||||
|
```javascript
|
||||||
|
var progressBar = document.getElementById("wi-reading-progress");
|
||||||
|
if (progressBar) {
|
||||||
|
var article = document.querySelector(".wi-post");
|
||||||
|
if (article) {
|
||||||
|
window.addEventListener("scroll", function () {
|
||||||
|
var rect = article.getBoundingClientRect();
|
||||||
|
var articleHeight = article.offsetHeight;
|
||||||
|
var scrolled = -rect.top;
|
||||||
|
var progress = Math.min(Math.max(scrolled / (articleHeight - window.innerHeight), 0), 1);
|
||||||
|
progressBar.style.width = (progress * 100) + "%";
|
||||||
|
}, { passive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟢 文章页作者信息
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
- `src/pages/post.astro`
|
||||||
|
|
||||||
|
### 实现方案
|
||||||
|
在文章 meta 区域(日期之后)添加作者信息。Halo 的 PostVo 有 `contributors` 字段(ContributorVo 列表),包含 `displayName` 和 `avatar`。
|
||||||
|
|
||||||
|
在 `.wi-post__meta` 中,日期之后添加:
|
||||||
|
```html
|
||||||
|
<th:block th:if="${post.contributors != null and !#lists.isEmpty(post.contributors)}">
|
||||||
|
<span class="wi-post__meta-sep">·</span>
|
||||||
|
<span class="wi-post__author">
|
||||||
|
<img
|
||||||
|
th:if="${post.contributors[0].avatar}"
|
||||||
|
th:src="${post.contributors[0].avatar}"
|
||||||
|
th:alt="${post.contributors[0].displayName}"
|
||||||
|
class="wi-post__author-avatar"
|
||||||
|
/>
|
||||||
|
<span th:text="${post.contributors[0].displayName}"></span>
|
||||||
|
</span>
|
||||||
|
</th:block>
|
||||||
|
```
|
||||||
|
|
||||||
|
CSS:
|
||||||
|
```css
|
||||||
|
.wi-post__author {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-post__author-avatar {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 README.md 撰写
|
||||||
|
|
||||||
|
### 内容结构
|
||||||
|
1. 主题简介
|
||||||
|
2. 截图预览
|
||||||
|
3. 安装方法
|
||||||
|
4. 配置说明(各配置组概述)
|
||||||
|
5. lightgallery.js 灯箱插件集成指南
|
||||||
|
- 路径匹配规则
|
||||||
|
- DOM 节点选择器
|
||||||
|
6. 开发指南(构建命令等)
|
||||||
|
|
||||||
|
### lightgallery.js 集成信息
|
||||||
|
|
||||||
|
| 页面 | 路径匹配 | 匹配区域 DOM 节点 |
|
||||||
|
|------|----------|-------------------|
|
||||||
|
| 文章详情页 | `/archives/*` | `.wi-post__body` |
|
||||||
|
| 瞬间页 | `/moments` | `.wi-moments-page__content` |
|
||||||
|
| 图库页 | `/photos` | `.wi-photos-page__grid` |
|
||||||
|
| 自定义页面 | (用户自定义) | `.wi-page__body` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 执行顺序
|
||||||
|
|
||||||
|
1. **修复 500 报错** — Navbar.astro、MobileMenu.astro 移除 `#request`,改用 JS 高亮;Layout.astro、post.astro、page.astro 移除冲突的 SEO 标签
|
||||||
|
2. **构建并部署** — 验证 500 错误已修复
|
||||||
|
3. **响应式断点统一** — 批量替换 680px→768px、767px→768px
|
||||||
|
4. **冗余代码清理** — 删除 Header.astro 和 MobileMenu.vue
|
||||||
|
5. **Footer 版本号修复** — 使用 frontmatter 常量
|
||||||
|
6. **阅读进度条** — post.astro 添加进度条
|
||||||
|
7. **文章页作者信息** — post.astro 添加作者
|
||||||
|
8. **README.md 撰写**
|
||||||
|
9. **最终构建部署验证**
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# 修复阅读进度条、作者信息、导航高亮 + 最终验证计划
|
||||||
|
|
||||||
|
## 问题分析
|
||||||
|
|
||||||
|
上一轮部署后浏览器验证发现三个功能 DOM 元素未渲染:
|
||||||
|
- `progressBar: false` — 阅读进度条 `#wi-reading-progress` 未出现
|
||||||
|
- `author: false` — 作者信息 `.wi-post__author` 未出现
|
||||||
|
- `navActive: false` — 导航高亮 `.wi-navbar__link--active` 未生效
|
||||||
|
|
||||||
|
### 根因分析
|
||||||
|
|
||||||
|
1. **阅读进度条**:HTML 在 `post.astro` 第 10 行,位于 `<Layout>` 内部但不在 `<article>` 内。`position: fixed` 的元素不应受布局影响。可能原因:构建产物中该 div 被正确输出,但浏览器验证脚本查找时页面可能未完全渲染,或验证脚本的选择器有误。需要检查构建产物确认。
|
||||||
|
|
||||||
|
2. **作者信息**:`th:if="${post.contributors != null and !#lists.isEmpty(post.contributors)}"` — 在 Halo WebFlux 环境中,`#lists.isEmpty()` 对 `post.contributors` 可能抛出类型转换异常(类似之前的 `#request` 问题),导致整个 `th:block` 渲染失败。需要简化条件判断,移除 `#lists.isEmpty()` 调用,改用安全访问方式。
|
||||||
|
|
||||||
|
3. **导航高亮**:`Navbar.astro` 和 `MobileMenu.astro` 中的 `<script>` 标签**缺少 `is:inline`**。没有 `is:inline` 时,Astro 会将脚本打包/转换,在 Thymeleaf 模板输出中可能无法正确内联,导致 JS 代码不执行。需要添加 `is:inline`。
|
||||||
|
|
||||||
|
## 实施步骤
|
||||||
|
|
||||||
|
### 步骤 1:修复 Navbar.astro — 添加 `is:inline`
|
||||||
|
- 将 `<script>` 改为 `<script is:inline>`
|
||||||
|
- 确保 JS 导航高亮代码在模板中正确内联输出
|
||||||
|
|
||||||
|
### 步骤 2:修复 MobileMenu.astro — 添加 `is:inline`
|
||||||
|
- 将 `<script>` 改为 `<script is:inline>`
|
||||||
|
- 确保 JS 导航高亮代码在模板中正确内联输出
|
||||||
|
|
||||||
|
### 步骤 3:修复 post.astro 作者信息条件判断
|
||||||
|
- 将 `th:if="${post.contributors != null and !#lists.isEmpty(post.contributors)}"` 改为更安全的写法
|
||||||
|
- 改为 `th:if="${post.contributors != null and !post.contributors.isEmpty()}"` 或直接用 `th:if="${post.contributors}"`(Halo 的 contributors 是 List 类型,空 List 在 Thymeleaf 中 truthy 检查可能不够,但 `#lists.isEmpty` 在 WebFlux 中可能有问题)
|
||||||
|
- 最安全的写法:`th:if="${post.contributors != null}"` + 内部用 `th:if` 过滤空列表
|
||||||
|
|
||||||
|
### 步骤 4:验证阅读进度条 HTML 输出
|
||||||
|
- 检查构建产物 `templates/post.html` 中是否包含 `wi-reading-progress` div
|
||||||
|
- 如果缺失,检查是否是 Astro 构建过程中被移除
|
||||||
|
|
||||||
|
### 步骤 5:构建并部署
|
||||||
|
- 运行 `npm run build`
|
||||||
|
- 将 templates 复制到 Docker 容器
|
||||||
|
- 重启 Halo
|
||||||
|
|
||||||
|
### 步骤 6:浏览器验证
|
||||||
|
- 使用 agent-browser 验证阅读进度条、作者信息、导航高亮是否正常工作
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
|
||||||
|
| 文件 | 修改内容 |
|
||||||
|
|------|---------|
|
||||||
|
| `src/components/Navbar.astro` | `<script>` → `<script is:inline>` |
|
||||||
|
| `src/components/MobileMenu.astro` | `<script>` → `<script is:inline>` |
|
||||||
|
| `src/pages/post.astro` | 修复 `post.contributors` 的 `th:if` 条件 |
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
# 实施计划:Hero 一言、Footer 注入、瞬间计数修复、README 更新
|
||||||
|
|
||||||
|
## 任务概览
|
||||||
|
|
||||||
|
| # | 任务 | 状态 |
|
||||||
|
|---|------|------|
|
||||||
|
| 1 | Hero 首屏描述文案改为一言语句 | ✅ 已完成(settings.yaml + HeroSection.astro) |
|
||||||
|
| 2 | Footer 代码注入整合进主题页脚容器 | ✅ 已完成(Footer.astro + Layout.astro) |
|
||||||
|
| 3 | 瞬间页点赞/评论计数不更新 | 🔄 需修复 |
|
||||||
|
| 4 | README.md 增加已适配插件信息 | 🔄 待开始 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务 3:修复瞬间页点赞/评论计数不更新
|
||||||
|
|
||||||
|
### 问题分析
|
||||||
|
|
||||||
|
通过查阅 Halo 瞬间插件(plugin-moments)的官方文档,确认了 `MomentVo` 的 `stats` 字段定义为:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"stats": {
|
||||||
|
"upvote": 0,
|
||||||
|
"totalComment": 0,
|
||||||
|
"approvedComment": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
因此 `moment.stats?.upvote` 和 `moment.stats?.approvedComment` 的字段名是**正确的**。
|
||||||
|
|
||||||
|
但当前主题使用的是 `momentFinder.list(1, 50)`,这是 Finder API。根据 Halo 核心源码分析:
|
||||||
|
- 核心的 `StatsVo` 只有 `visit`、`upvote`、`comment` 三个字段
|
||||||
|
- 瞬间插件自定义了 `stats` 对象,包含 `upvote`、`totalComment`、`approvedComment`
|
||||||
|
- **关键问题**:`momentFinder.list()` 返回的 `MomentVo` 对象中,`stats` 字段可能为 `null`,因为 Finder API 可能不会自动填充统计数据
|
||||||
|
|
||||||
|
### 根本原因推断
|
||||||
|
|
||||||
|
1. **点赞计数不更新**:点赞 API `/apis/api.halo.run/v1alpha1/trackers/upvote` 返回 200,但 `moment.stats.upvote` 始终为 0。可能原因:
|
||||||
|
- Finder API 返回的 `MomentVo` 中 `stats` 对象存在但值为 0(未实时同步)
|
||||||
|
- 或者 `stats` 对象为 `null`,`?: 0` 兜底显示 0
|
||||||
|
|
||||||
|
2. **评论计数不更新**:同理,`moment.stats.approvedComment` 始终为 0
|
||||||
|
|
||||||
|
3. **调试 div 未渲染**:之前添加的 `th:text="${moment.stats != null ? moment.stats.toString() : 'NULL'}"` 在浏览器中找不到,说明 Thymeleaf 在处理这个表达式时可能抛出了异常(`toString()` 在某些 VO 对象上可能不可用),导致整个元素被跳过
|
||||||
|
|
||||||
|
### 修复方案
|
||||||
|
|
||||||
|
#### 步骤 3.1:移除调试代码
|
||||||
|
- 删除 `moments.astro` 第 27 行的 `<div class="wi-debug-stats">` 调试元素
|
||||||
|
|
||||||
|
#### 步骤 3.2:改用瞬间插件的公开 API 获取统计数据
|
||||||
|
- 瞬间插件提供了公开 API:`/apis/api.moment.halo.run/v1alpha1/moments`
|
||||||
|
- 该 API 返回的 `MomentVo` 包含完整的 `stats` 对象
|
||||||
|
- 但由于页面使用 Thymeleaf 服务端渲染,无法在模板中直接调用 REST API
|
||||||
|
- **替代方案**:在客户端 JS 中,页面加载后通过 API 获取统计数据并更新 DOM
|
||||||
|
|
||||||
|
#### 步骤 3.3:实现客户端统计更新
|
||||||
|
- 页面加载后,调用 `/apis/api.moment.halo.run/v1alpha1/moments` 获取瞬间列表
|
||||||
|
- 遍历返回数据,根据 `moment.metadata.name` 匹配 DOM 元素
|
||||||
|
- 更新点赞数和评论数显示
|
||||||
|
|
||||||
|
#### 步骤 3.4:修复点赞后的计数更新逻辑
|
||||||
|
- 当前点赞成功后,JS 使用 `parseInt(countEl.textContent) + 1` 更新计数
|
||||||
|
- 如果初始值为 0(因为 stats 未加载),点赞后显示 1
|
||||||
|
- 需要确保点赞 API 调用正确,且点赞后计数正确更新
|
||||||
|
|
||||||
|
#### 步骤 3.5:验证点赞 API body 格式
|
||||||
|
- 当前使用 `{group: "moment.moment.halo.run", plural: "moments", name: momentName}`
|
||||||
|
- 参考 post.astro 中文章点赞使用 `{group: "content.halo.run", plural: "posts", name: postName}`
|
||||||
|
- 瞬间插件的 group 为 `moment.moment.halo.run`,plural 为 `moments`,格式正确
|
||||||
|
- 但需要确认 Halo 的 trackers upvote API 是否支持瞬间插件的 group
|
||||||
|
|
||||||
|
### 具体代码修改
|
||||||
|
|
||||||
|
**文件:`src/pages/moments.astro`**
|
||||||
|
|
||||||
|
1. 移除第 27 行调试 div
|
||||||
|
2. 保留 Thymeleaf 中的 `moment.stats?.upvote ?: 0` 和 `moment.stats?.approvedComment ?: 0` 作为初始值
|
||||||
|
3. 在 `<script is:inline>` 中添加页面加载后获取统计数据的逻辑:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function loadMomentStats() {
|
||||||
|
fetch("/apis/api.moment.halo.run/v1alpha1/moments")
|
||||||
|
.then(function(res) {
|
||||||
|
if (!res.ok) return;
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(function(data) {
|
||||||
|
if (!data || !data.items) return;
|
||||||
|
data.items.forEach(function(moment) {
|
||||||
|
var name = moment.metadata.name;
|
||||||
|
var stats = moment.stats || {};
|
||||||
|
|
||||||
|
var likeCountEls = document.querySelectorAll('.wi-moments-page__like-count');
|
||||||
|
var commentCountEls = document.querySelectorAll('.wi-moments-page__comment-count');
|
||||||
|
|
||||||
|
likeCountEls.forEach(function(el) {
|
||||||
|
var btn = el.closest('.wi-moments-page__like-btn');
|
||||||
|
if (btn && btn.getAttribute('data-moment-name') === name) {
|
||||||
|
el.textContent = stats.upvote || 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
commentCountEls.forEach(function(el) {
|
||||||
|
var btn = el.closest('.wi-moments-page__comment-btn');
|
||||||
|
if (btn && btn.getAttribute('data-moment-name') === name) {
|
||||||
|
el.textContent = stats.approvedComment || 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
console.error("Failed to load moment stats:", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMomentStats();
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 优化点赞逻辑,点赞成功后重新加载统计数据确保准确性
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务 4:README.md 增加已适配插件信息
|
||||||
|
|
||||||
|
### 已适配插件列表
|
||||||
|
|
||||||
|
通过代码搜索 `pluginFinder.available()` 确认以下已适配插件:
|
||||||
|
|
||||||
|
| 插件名称 | 插件标识 | 适配页面路由 | 说明 |
|
||||||
|
|----------|---------|-------------|------|
|
||||||
|
| 瞬间 | PluginMoments | `/moments` | 瞬间动态页面 |
|
||||||
|
| 图库 | PluginPhotos | `/photos` | 图片展示页面 |
|
||||||
|
| 友情链接 | PluginLinks | `/links` | 友链展示页面 |
|
||||||
|
| 朋友圈 | plugin-friends | `/friends` | RSS 订阅朋友圈 |
|
||||||
|
| 装备 | equipment | `/equipment` | 装备展示页面 |
|
||||||
|
| 搜索 | PluginSearchWidget | 导航栏集成 | 搜索弹窗组件 |
|
||||||
|
| 评论 | PluginComment | 文章页/瞬间页/留言板 | 评论组件集成 |
|
||||||
|
|
||||||
|
### 具体修改
|
||||||
|
|
||||||
|
在 README.md 的"特性"部分之后添加"已适配插件"章节,包含插件名称、路由和简要说明。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 执行顺序
|
||||||
|
|
||||||
|
1. 修复 moments.astro(移除调试代码 + 添加客户端统计加载)
|
||||||
|
2. 更新 README.md
|
||||||
|
3. 构建部署
|
||||||
|
4. 浏览器验证
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
# 文章页 TOC 悬浮、首页两排布局、文章阅读体验优化
|
||||||
|
|
||||||
|
## 任务概述
|
||||||
|
|
||||||
|
1. 文章页目录(TOC)改为悬浮定位,不挤占文章空间,支持左侧/右侧切换(配置项控制,默认左侧)
|
||||||
|
2. 首页文章流式布局从三排改为两排
|
||||||
|
3. 优化文章详情页阅读体验(段落间距、标题留白、字体颜色等)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、TOC 悬浮定位改造
|
||||||
|
|
||||||
|
### 现状
|
||||||
|
- TOC 在 `.wi-post__content` 中使用 CSS Grid 布局,占 200-240px 列宽
|
||||||
|
- 文章内容被挤压到剩余空间
|
||||||
|
|
||||||
|
### 改造方案
|
||||||
|
- 移除 `.wi-post__content` 的 CSS Grid 双列布局,改为单列
|
||||||
|
- TOC 使用 `position: fixed` 悬浮在页面左侧或右侧,不占文档流空间
|
||||||
|
- 通过 `theme.config.article.article_toc_position` 配置项控制左/右(默认左侧)
|
||||||
|
- TOC 宽度固定 220px,距离内容区域边缘留出间距
|
||||||
|
- 移动端(< 1280px)隐藏 TOC 或改为浮动按钮展开
|
||||||
|
|
||||||
|
### 涉及文件
|
||||||
|
- `src/pages/post.astro`:修改 HTML 结构和 CSS 样式
|
||||||
|
- `settings.yaml`:新增 `article_toc_position` 配置项
|
||||||
|
|
||||||
|
### 具体修改
|
||||||
|
|
||||||
|
#### settings.yaml
|
||||||
|
在文章配置组 `article_show_toc` 后新增:
|
||||||
|
```yaml
|
||||||
|
- $formkit: select
|
||||||
|
name: article_toc_position
|
||||||
|
label: 目录位置
|
||||||
|
options:
|
||||||
|
- label: 左侧
|
||||||
|
value: left
|
||||||
|
- label: 右侧
|
||||||
|
value: right
|
||||||
|
value: left
|
||||||
|
```
|
||||||
|
|
||||||
|
#### post.astro HTML
|
||||||
|
- 移除 `.wi-post__content` 的 Grid 包裹,TOC 和 body 恢复为平级
|
||||||
|
- TOC 添加 `th:attr="data-position=${theme.config?.article?.article_toc_position ?: 'left'}"` 属性
|
||||||
|
- JS 读取 `data-position` 决定定位方向
|
||||||
|
|
||||||
|
#### post.astro CSS
|
||||||
|
- `.wi-post__content` 移除 Grid 双列,改为单列
|
||||||
|
- `.wi-toc` 改为 `position: fixed`,根据 `data-position` 计算 `left` 或 `right`
|
||||||
|
- TOC 仅在视口宽度 ≥ 1280px 且内容区域旁有足够空间时显示
|
||||||
|
- JS 动态计算 TOC 定位坐标(基于内容区域的边缘位置)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、首页流式布局从三排改为两排
|
||||||
|
|
||||||
|
### 现状
|
||||||
|
- `.wi-flow` 使用 `columns: 3`
|
||||||
|
- 平板端 `columns: 2`,手机端 `columns: 1`
|
||||||
|
|
||||||
|
### 改造方案
|
||||||
|
- 默认改为 `columns: 2`
|
||||||
|
- 平板端保持 `columns: 2`
|
||||||
|
- 手机端保持 `columns: 1`
|
||||||
|
|
||||||
|
### 涉及文件
|
||||||
|
- `src/pages/index.astro`:修改 `.wi-flow` 的 `columns` 值
|
||||||
|
|
||||||
|
### 具体修改
|
||||||
|
```css
|
||||||
|
.wi-flow {
|
||||||
|
columns: 2; /* 从 3 改为 2 */
|
||||||
|
column-gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移除平板端的覆盖,因为默认已经是 2 */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.wi-flow {
|
||||||
|
columns: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、文章详情页阅读体验优化
|
||||||
|
|
||||||
|
### 用户已确认的问题
|
||||||
|
1. 增加段落之间的间距,避免文字堆积
|
||||||
|
2. 增加标题上下方的留白,使章节区分更明显
|
||||||
|
3. 字体优化:正文字号适中(16px 或 17px),颜色用深灰色代替纯黑
|
||||||
|
|
||||||
|
### 我额外发现的问题(需用户确认)
|
||||||
|
4. **行高偏小**:当前 `--leading-relaxed` 为 1.75,对于中文正文可以适当增加到 1.8-1.85,提升长文阅读舒适度
|
||||||
|
5. **列表项间距偏小**:`li` 的 `margin-block-end: 0.4em` 较紧凑,建议增加到 `0.6em`
|
||||||
|
6. **代码块与正文间距不足**:`pre` 和 `code` 缺少明确的上下 margin,与正文混在一起
|
||||||
|
7. **引用块(blockquote)间距**:全局 blockquote 的 `margin-block: var(--space-lg)` 在文章内可能不够,建议在 `.wi-post__body blockquote` 中增加更多上下留白
|
||||||
|
8. **图片与正文间距**:当前 `margin-block: var(--space-lg)`(1.5rem),建议增加到 `var(--space-xl)`(2rem)
|
||||||
|
|
||||||
|
### 涉及文件
|
||||||
|
- `src/pages/post.astro`:修改 `.wi-post__body` 及子元素样式
|
||||||
|
- `src/styles/_colors.scss`:可能需要调整 `--ink` 颜色值
|
||||||
|
- `src/styles/_typography.scss`:可能需要调整 `--text-md` 和 `--leading-relaxed`
|
||||||
|
|
||||||
|
### 具体修改
|
||||||
|
|
||||||
|
#### 1. 段落间距
|
||||||
|
```css
|
||||||
|
.wi-post__body p {
|
||||||
|
margin-block-end: 1.6em; /* 从 1.2em 增加到 1.6em */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 标题留白
|
||||||
|
```css
|
||||||
|
.wi-post__body :is(h1, h2, h3, h4, h5, h6) {
|
||||||
|
margin-top: 2.5em; /* 从 2em 增加到 2.5em */
|
||||||
|
margin-bottom: 1em; /* 从 0.6em 增加到 1em */
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-post__body h2 {
|
||||||
|
margin-top: 3em; /* h2 作为主要章节分隔,留更多空间 */
|
||||||
|
margin-bottom: 1.2em;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 字体优化
|
||||||
|
- 正文字号:当前 `--text-md` 为 `clamp(1.05rem, 0.99rem + 0.3vw, 1.125rem)`(约 16.8px-18px),已经偏大。建议改为 `1.0625rem`(17px)固定值或保持现有 clamp 但微调
|
||||||
|
- 正文字色:当前 `--ink` 为 `#2c2420`(深棕黑),已经不是纯黑,但可以在文章正文中使用更柔和的 `var(--ink-2)` 即 `#7a6e64`... 不对,这个太浅了。建议新增一个文章专用文字色 `--ink-body: #3d3530`,比 `--ink` 浅一点但比 `--ink-2` 深很多
|
||||||
|
|
||||||
|
```css
|
||||||
|
.wi-post__body {
|
||||||
|
font-size: 1.0625rem; /* 17px,比 --text-md 略大 */
|
||||||
|
line-height: 1.85; /* 从 var(--leading-relaxed)(1.75) 增加到 1.85 */
|
||||||
|
color: #3d3530; /* 深棕灰,比 --ink (#2c2420) 柔和 */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
暗色模式下:
|
||||||
|
```css
|
||||||
|
html.dark .wi-post__body {
|
||||||
|
color: #d4cdc4; /* 比 --ink (#ede6de) 柔和 */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4-8. 其他优化(需用户确认后实施)
|
||||||
|
- 行高:1.75 → 1.85
|
||||||
|
- 列表项间距:0.4em → 0.6em
|
||||||
|
- 代码块上下间距:增加 `margin-block: 1.5em`
|
||||||
|
- 引用块间距:增加到 `margin-block: 2em`
|
||||||
|
- 图片间距:1.5rem → 2rem
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 实施步骤
|
||||||
|
|
||||||
|
1. **settings.yaml**:新增 `article_toc_position` 配置项
|
||||||
|
2. **post.astro**:TOC 悬浮定位改造(HTML + CSS + JS)
|
||||||
|
3. **post.astro**:文章阅读体验优化(段落间距、标题留白、字体颜色等)
|
||||||
|
4. **index.astro**:首页流式布局从三排改为两排
|
||||||
|
5. **构建部署**:`npm run build` → docker cp → docker restart
|
||||||
|
6. **浏览器验证**:检查 TOC 悬浮效果、首页布局、文章阅读体验
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 待用户确认
|
||||||
|
|
||||||
|
以上第 4-8 项额外发现的问题,是否一并修改?还是只修改用户已确认的 1-3 项?
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
# 实施计划:TOC 优化、图片描述样式、灯箱修复、Logo 修复、瞬间评论计数修复
|
||||||
|
|
||||||
|
## 任务概览
|
||||||
|
|
||||||
|
| # | 任务 | 优先级 |
|
||||||
|
|---|------|--------|
|
||||||
|
| 1 | TOC 优化:子标题缩进 + 当前项高亮 + 删除全目录展开/收起 + 二级目录折叠/展开 | 高 |
|
||||||
|
| 2 | 文章图片描述(figcaption)颜色 #545164 + 字号缩小 | 中 |
|
||||||
|
| 3 | 图库页灯箱插件图片无法点击大图预览 | 高 |
|
||||||
|
| 4 | 主题 Logo 在后台主题详情/管理器不显示 | 中 |
|
||||||
|
| 5 | 瞬间页评论计数修复 | 高 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务 1:TOC 优化
|
||||||
|
|
||||||
|
### 现状分析
|
||||||
|
|
||||||
|
当前 TOC 实现([post.astro](file:///c:/Users/Zhang/Documents/Halo/WarmIsland/src/pages/post.astro)):
|
||||||
|
|
||||||
|
1. **子标题缩进**:已有 `wi-toc__link--h2/h3/h4/h5/h6` 类,h2 无缩进,h3 12px,h4 24px 等。但缩进量较小,层级感不够明显。
|
||||||
|
2. **当前项高亮**:已有 `wi-toc__link--active` 类(`color: var(--accent); font-weight: 600;`),但高亮效果不够明显,缺少视觉锚点。
|
||||||
|
3. **全目录展开/收起**:当前 `wi-toc__toggle` 按钮控制整个目录的展开/收起(`wi-toc__nav--collapsed` 类),需要删除此功能。
|
||||||
|
4. **二级目录折叠/展开**:当前没有此功能,需要新增。当进入某一个一级分类(h2)时展开其下属的二级分类(h3-h6),其他一级分类的子项折叠。
|
||||||
|
|
||||||
|
### 修改方案
|
||||||
|
|
||||||
|
#### 步骤 1.1:增强子标题缩进
|
||||||
|
|
||||||
|
修改 CSS 中的缩进量,使层级更清晰:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.wi-toc__link--h2 { padding-left: 0; }
|
||||||
|
.wi-toc__link--h3 { padding-left: 16px; }
|
||||||
|
.wi-toc__link--h4 { padding-left: 32px; }
|
||||||
|
.wi-toc__link--h5 { padding-left: 48px; }
|
||||||
|
.wi-toc__link--h6 { padding-left: 64px; }
|
||||||
|
```
|
||||||
|
|
||||||
|
同时为 h2 级目录项添加左侧竖线指示器,增强层级感。
|
||||||
|
|
||||||
|
#### 步骤 1.2:增强当前阅读项高亮样式
|
||||||
|
|
||||||
|
改进 `wi-toc__link--active` 样式:
|
||||||
|
- 左侧添加竖线指示器(accent 色)
|
||||||
|
- 背景色微调(半透明 accent)
|
||||||
|
- 字重加粗
|
||||||
|
- 平滑过渡动画
|
||||||
|
|
||||||
|
```css
|
||||||
|
.wi-toc__link--active {
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
border-left: 2px solid var(--accent);
|
||||||
|
padding-left: calc(原缩进 - 2px);
|
||||||
|
background: color-mix(in srgb, var(--accent) 8%, transparent);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 1.3:删除全目录展开/收起功能
|
||||||
|
|
||||||
|
1. 删除 `wi-toc__header` 中的 `wi-toc__toggle` 按钮
|
||||||
|
2. 删除 JS 中 `toggleBtn` 相关的事件监听代码
|
||||||
|
3. 删除 CSS 中 `wi-toc__nav--collapsed` 相关样式
|
||||||
|
|
||||||
|
#### 步骤 1.4:增加二级目录折叠/展开功能
|
||||||
|
|
||||||
|
1. 修改 JS 中 TOC 生成逻辑,将 h2 作为一级目录项,h3-h6 作为二级目录项
|
||||||
|
2. 每个 h2 项下方创建一个可折叠的子容器
|
||||||
|
3. 默认只展开当前活跃 h2 的子项,其他 h2 的子项折叠
|
||||||
|
4. 点击 h2 项可手动展开/折叠其子项
|
||||||
|
5. 当滚动位置变化时,自动展开当前活跃 h2 的子项
|
||||||
|
|
||||||
|
HTML 结构改为:
|
||||||
|
```html
|
||||||
|
<div class="wi-toc__group" data-h2="wi-heading-0">
|
||||||
|
<a class="wi-toc__link wi-toc__link--h2" href="#wi-heading-0">一级标题</a>
|
||||||
|
<div class="wi-toc__sub">
|
||||||
|
<a class="wi-toc__link wi-toc__link--h3" href="#wi-heading-1">二级标题</a>
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
CSS:
|
||||||
|
```css
|
||||||
|
.wi-toc__sub {
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transition: max-height 0.3s ease, opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-toc__group--active .wi-toc__sub,
|
||||||
|
.wi-toc__group--expanded .wi-toc__sub {
|
||||||
|
max-height: 500px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
JS 逻辑:
|
||||||
|
- 生成 TOC 时,将 h3-h6 归入前一个 h2 的子组
|
||||||
|
- IntersectionObserver 检测到活跃标题时,自动展开对应的 h2 组
|
||||||
|
- 点击 h2 项时,切换该组的展开/折叠状态
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务 2:文章图片描述样式
|
||||||
|
|
||||||
|
### 现状分析
|
||||||
|
|
||||||
|
Halo 文章编辑器中,图片可以添加描述(alt/caption),渲染后通常为 `<figure>` + `<figcaption>` 结构。当前主题没有为 `figcaption` 定义样式,使用默认样式。
|
||||||
|
|
||||||
|
### 修改方案
|
||||||
|
|
||||||
|
在 [post.astro](file:///c:/Users/Zhang/Documents/Halo/WarmIsland/src/pages/post.astro) 的 `<style>` 中添加 `figcaption` 样式:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.wi-post__body figcaption {
|
||||||
|
color: #545164;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: -0.8em;
|
||||||
|
margin-bottom: 1.2em;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .wi-post__body figcaption {
|
||||||
|
color: #8a8494;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
字号 `0.875rem`(14px)比正文字号 `1rem`(16px)小一点。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务 3:图库页灯箱插件图片无法点击大图预览
|
||||||
|
|
||||||
|
### 现状分析
|
||||||
|
|
||||||
|
当前图库页 [photos.astro](file:///c:/Users/Zhang/Documents/Halo/WarmIsland/src/pages/photos.astro) 中,图片结构为:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="wi-photos-page__wrap">
|
||||||
|
<img th:src="${photo.spec.cover ?: photo.spec.url}" class="wi-photos-page__image" />
|
||||||
|
<div class="wi-photos-page__overlay">...</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
lightgallery.js 插件的工作原理:
|
||||||
|
1. 需要在匹配区域的 DOM 节点上初始化 `lightGallery()`
|
||||||
|
2. 默认情况下,lightGallery 会查找 `<a>` 标签包裹的 `<img>`,`<a>` 的 `href` 属性作为大图 URL
|
||||||
|
3. 或者使用 `selector` 选项指定点击目标
|
||||||
|
|
||||||
|
**问题原因**:当前图片没有用 `<a>` 标签包裹,lightGallery 无法识别可点击的图片。`wi-photos-page__overlay` 遮罩层也可能拦截点击事件。
|
||||||
|
|
||||||
|
### 修改方案
|
||||||
|
|
||||||
|
将每个图片项的 `<img>` 用 `<a>` 标签包裹,`href` 指向原图 URL:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="wi-photos-page__wrap">
|
||||||
|
<a th:href="${photo.spec.url}" class="wi-photos-page__link">
|
||||||
|
<img th:src="${photo.spec.cover ?: photo.spec.url}" class="wi-photos-page__image" />
|
||||||
|
</a>
|
||||||
|
<div class="wi-photos-page__overlay">...</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
CSS 添加:
|
||||||
|
```css
|
||||||
|
.wi-photos-page__link {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这样 lightGallery 插件配置路径匹配 `/photos`、DOM 节点 `.wi-photos-page__grid`、selector `a` 即可正常工作。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务 4:主题 Logo 在后台不显示
|
||||||
|
|
||||||
|
### 现状分析
|
||||||
|
|
||||||
|
当前 [theme.yaml](file:///c:/Users/Zhang/Documents/Halo/WarmIsland/theme.yaml) 中:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
logo: /themes/warm-island/public/logo.png
|
||||||
|
```
|
||||||
|
|
||||||
|
问题分析:
|
||||||
|
1. Halo 2.x 主题的静态资源在 `templates/` 目录下,通过 `/themes/{theme-name}/assets/` 路径访问
|
||||||
|
2. `public/` 目录下的文件在构建时被复制到 `templates/` 根目录
|
||||||
|
3. `logo.png` 在构建后位于 `templates/logo.png`
|
||||||
|
4. 路径 `/themes/warm-island/public/logo.png` 不正确,因为 `public/` 不是资源访问路径的一部分
|
||||||
|
|
||||||
|
### 修改方案
|
||||||
|
|
||||||
|
将 `theme.yaml` 中的 logo 路径改为正确的资源路径:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
logo: /themes/warm-island/assets/logo.png
|
||||||
|
```
|
||||||
|
|
||||||
|
或者如果 logo 在 templates 根目录下,使用:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
logo: /themes/warm-island/logo.png
|
||||||
|
```
|
||||||
|
|
||||||
|
需要验证 Halo 2.x 主题的静态资源访问路径规则。根据 vite-plugin-halo-theme 的构建输出,`public/` 目录下的文件被复制到 `templates/` 根目录,而 `templates/assets/` 下是构建产物。所以正确的路径应该是 `/themes/warm-island/logo.png`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务 5:瞬间页评论计数修复
|
||||||
|
|
||||||
|
### 现状分析
|
||||||
|
|
||||||
|
上一轮已修复了点赞计数问题(upvote API group 从 `moment.moment.halo.run` 改为 `moment.halo.run`),并添加了 `loadMomentStats()` 客户端函数通过 `/apis/api.moment.halo.run/v1alpha1/moments` API 获取统计数据。
|
||||||
|
|
||||||
|
当前 `loadMomentStats()` 函数已正确获取 `stats.approvedComment` 并更新 DOM。但评论计数仍然显示 0,可能原因:
|
||||||
|
|
||||||
|
1. 评论确实为 0(没有审核通过的评论)
|
||||||
|
2. `moment.stats?.approvedComment` 在 Thymeleaf 渲染时为 null,`?: 0` 兜底显示 0
|
||||||
|
3. 客户端 `loadMomentStats()` 可能未正确执行
|
||||||
|
|
||||||
|
### 修改方案
|
||||||
|
|
||||||
|
1. 使用浏览器验证评论数据是否存在
|
||||||
|
2. 确认 `loadMomentStats()` 函数是否正确更新了评论计数
|
||||||
|
3. 如果评论计数在评论提交后没有实时更新,需要在评论提交后重新调用 `loadMomentStats()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 执行顺序
|
||||||
|
|
||||||
|
1. 修改 theme.yaml 修复 Logo 路径
|
||||||
|
2. 修改 post.astro:TOC 优化 + figcaption 样式
|
||||||
|
3. 修改 photos.astro:图片添加 `<a>` 标签包裹
|
||||||
|
4. 验证瞬间页评论计数
|
||||||
|
5. 构建部署
|
||||||
|
6. 浏览器验证
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
# WarmIsland 主题修复与改进计划
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
修复 BUG、改进硬编码中文字符串、添加暗色模式切换过渡动画、增加首页模块化布局、首页文章列表改为单栏布局(文字左图片右)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 步骤 1:暗色模式切换过渡动画
|
||||||
|
|
||||||
|
**状态**:CSS 已添加到 Layout.astro,需要修改切换逻辑
|
||||||
|
|
||||||
|
### 1.1 修改 ThemeSwitcher.vue
|
||||||
|
- 文件:`src/components/ThemeSwitcher.vue`
|
||||||
|
- 在 `toggle()` 函数中:
|
||||||
|
1. 切换前添加 `document.documentElement.classList.add("wi-theme-transition")`
|
||||||
|
2. 执行切换
|
||||||
|
3. 300ms 后移除 `wi-theme-transition` 类
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function toggle() {
|
||||||
|
document.documentElement.classList.add("wi-theme-transition");
|
||||||
|
isDark.value = !isDark.value;
|
||||||
|
document.documentElement.classList.toggle("dark", isDark.value);
|
||||||
|
localStorage.setItem("wi-theme", isDark.value ? "dark" : "light");
|
||||||
|
setTimeout(() => {
|
||||||
|
document.documentElement.classList.remove("wi-theme-transition");
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 修改 MobileMenu.astro
|
||||||
|
- 文件:`src/components/MobileMenu.astro`
|
||||||
|
- 在 themeBtn 点击事件中添加同样的过渡逻辑
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
themeBtn?.addEventListener("click", () => {
|
||||||
|
document.documentElement.classList.add("wi-theme-transition");
|
||||||
|
const isDark = document.documentElement.classList.toggle("dark");
|
||||||
|
localStorage.setItem("wi-theme", isDark ? "dark" : "light");
|
||||||
|
document.documentElement.setAttribute("data-color-scheme", isDark ? "dark" : "light");
|
||||||
|
setTimeout(() => {
|
||||||
|
document.documentElement.classList.remove("wi-theme-transition");
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 步骤 2:首页文章列表改为单栏布局(文字左图片右)
|
||||||
|
|
||||||
|
**文件**:`src/pages/index.astro`
|
||||||
|
|
||||||
|
### 2.1 修改 HTML 结构
|
||||||
|
将 `.wi-flow__card` 从纵向布局改为横向布局:
|
||||||
|
- 文字区域(`.wi-flow__body`)在左侧
|
||||||
|
- 图片区域(`.wi-flow__cover`)在右侧
|
||||||
|
- 无封面图时文字占满宽度
|
||||||
|
|
||||||
|
当前结构:
|
||||||
|
```html
|
||||||
|
<a class="wi-flow__card">
|
||||||
|
<div class="wi-flow__cover">...</div> <!-- 图片在上 -->
|
||||||
|
<div class="wi-flow__body">...</div> <!-- 文字在下 -->
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
改为:
|
||||||
|
```html
|
||||||
|
<a class="wi-flow__card">
|
||||||
|
<div class="wi-flow__body">...</div> <!-- 文字在左 -->
|
||||||
|
<div class="wi-flow__cover">...</div> <!-- 图片在右 -->
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 修改 CSS 样式
|
||||||
|
- `.wi-flow`:从 `columns: 2` 改为单栏布局(`display: flex; flex-direction: column;`)
|
||||||
|
- `.wi-flow__card`:改为 `flex-direction: row`,横向排列
|
||||||
|
- `.wi-flow__body`:`flex: 1`,占据左侧空间
|
||||||
|
- `.wi-flow__cover`:固定宽度(如 240px),`flex-shrink: 0`
|
||||||
|
- `.wi-flow__image`:宽高固定,`object-fit: cover`
|
||||||
|
- 移动端响应式:`flex-direction: column`,封面图全宽
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 步骤 3:改进硬编码中文字符串
|
||||||
|
|
||||||
|
### 3.1 在 settings.yaml 中添加可配置标签字段
|
||||||
|
|
||||||
|
**文件**:`settings.yaml`
|
||||||
|
|
||||||
|
在 `home` 组中添加:
|
||||||
|
```yaml
|
||||||
|
- $formkit: text
|
||||||
|
name: home_label_newer
|
||||||
|
label: 分页-较新标签
|
||||||
|
value: 较新
|
||||||
|
- $formkit: text
|
||||||
|
name: home_label_older
|
||||||
|
label: 分页-较旧标签
|
||||||
|
value: 较旧
|
||||||
|
- $formkit: text
|
||||||
|
name: home_label_loading
|
||||||
|
label: 无限滚动-加载中文案
|
||||||
|
value: 加载中...
|
||||||
|
- $formkit: text
|
||||||
|
name: home_label_all_loaded
|
||||||
|
label: 无限滚动-全部加载文案
|
||||||
|
value: 已加载全部文章
|
||||||
|
- $formkit: text
|
||||||
|
name: home_featured_title
|
||||||
|
label: 精选模块标题
|
||||||
|
value: 精选
|
||||||
|
- $formkit: text
|
||||||
|
name: home_latest_title
|
||||||
|
label: 最新文章模块标题
|
||||||
|
value: 最新文章
|
||||||
|
- $formkit: text
|
||||||
|
name: home_timeline_title
|
||||||
|
label: 时间线模块标题
|
||||||
|
value: 时间线
|
||||||
|
- $formkit: text
|
||||||
|
name: home_friends_title
|
||||||
|
label: 友链模块标题
|
||||||
|
value: 友链
|
||||||
|
- $formkit: text
|
||||||
|
name: home_message_wall_title
|
||||||
|
label: 留言墙模块标题
|
||||||
|
value: 留言墙
|
||||||
|
- $formkit: text
|
||||||
|
name: home_label_view_all
|
||||||
|
label: 查看全部标签
|
||||||
|
value: 查看全部
|
||||||
|
- $formkit: text
|
||||||
|
name: home_label_no_posts
|
||||||
|
label: 暂无文章文案
|
||||||
|
value: 暂无文章。
|
||||||
|
- $formkit: text
|
||||||
|
name: home_label_post_count
|
||||||
|
label: 文章数量文案({total}为占位符)
|
||||||
|
value: 共 {total} 篇文章
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `basic` 组中添加:
|
||||||
|
```yaml
|
||||||
|
- $formkit: text
|
||||||
|
name: label_search
|
||||||
|
label: 搜索按钮标签
|
||||||
|
value: 搜索
|
||||||
|
- $formkit: text
|
||||||
|
name: label_theme_switch
|
||||||
|
label: 主题切换标签
|
||||||
|
value: 切换主题
|
||||||
|
- $formkit: text
|
||||||
|
name: label_archives_title
|
||||||
|
label: 归档页标题
|
||||||
|
value: 归档
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 修改模板文件中的硬编码字符串
|
||||||
|
|
||||||
|
| 文件 | 硬编码字符串 | 替换为 Thymeleaf 表达式 |
|
||||||
|
|------|-------------|----------------------|
|
||||||
|
| `index.astro` | `较新` | `th:text="${theme.config?.home?.home_label_newer ?: '较新'}"` |
|
||||||
|
| `index.astro` | `较旧` | `th:text="${theme.config?.home?.home_label_older ?: '较旧'}"` |
|
||||||
|
| `index.astro` | `加载中...` | `th:text="${theme.config?.home?.home_label_loading ?: '加载中...'}"` |
|
||||||
|
| `index.astro` | `已加载全部文章` | `th:text="${theme.config?.home?.home_label_all_loaded ?: '已加载全部文章'}"` |
|
||||||
|
| `index.astro` (JS) | `已加载全部文章` | 通过 data 属性传递配置值 |
|
||||||
|
| `MobileMenu.astro` | `搜索` | `th:text="${theme.config?.basic?.label_search ?: '搜索'}"` |
|
||||||
|
| `MobileMenu.astro` | `切换主题` | `th:text="${theme.config?.basic?.label_theme_switch ?: '切换主题'}"` |
|
||||||
|
| `FeaturedSection.astro` | `精选` | `th:text="${theme.config?.home?.home_featured_title ?: '精选'}"` |
|
||||||
|
| `LatestSection.astro` | `最新文章` | `th:text="${theme.config?.home?.home_latest_title ?: '最新文章'}"` |
|
||||||
|
| `LatestSection.astro` | `较新`/`较旧` | 同 index.astro |
|
||||||
|
| `MomentsSection.astro` | `查看全部` | `th:text="${theme.config?.home?.home_label_view_all ?: '查看全部'}"` |
|
||||||
|
| `PhotosSection.astro` | `查看全部` | `th:text="${theme.config?.home?.home_label_view_all ?: '查看全部'}"` |
|
||||||
|
| `TimelineSection.astro` | `时间线` | `th:text="${theme.config?.home?.home_timeline_title ?: '时间线'}"` |
|
||||||
|
| `FriendsSection.astro` | `友链` | `th:text="${theme.config?.home?.home_friends_title ?: '友链'}"` |
|
||||||
|
| `MessageWallSection.astro` | `留言墙` | `th:text="${theme.config?.home?.home_message_wall_title ?: '留言墙'}"` |
|
||||||
|
| `archives.astro` | `归档` | `th:text="${theme.config?.basic?.label_archives_title ?: '归档'}"` |
|
||||||
|
| `archives.astro` | `较新`/`较旧` | 同 index.astro |
|
||||||
|
| `archives.astro` | `暂无文章。` | `th:text="${theme.config?.home?.home_label_no_posts ?: '暂无文章。'}"` |
|
||||||
|
| `category.astro` | `较新`/`较旧` | 同 index.astro |
|
||||||
|
| `category.astro` | `暂无文章。` | 同 archives.astro |
|
||||||
|
| `tag.astro` | `较新`/`较旧` | 同 index.astro |
|
||||||
|
| `tag.astro` | `暂无文章。` | 同 archives.astro |
|
||||||
|
|
||||||
|
### 3.3 index.astro 中 JS 硬编码字符串处理
|
||||||
|
在无限滚动 JS 中,"已加载全部文章" 是通过 JS 动态创建 DOM 的,需要通过 data 属性传递配置值:
|
||||||
|
- 在 sentinel 元素上添加 `data-all-loaded-text` 属性
|
||||||
|
- JS 中读取该属性值
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 步骤 4:首页模块化布局
|
||||||
|
|
||||||
|
### 4.1 在 settings.yaml 的 `home` 组中添加模块开关
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- $formkit: switch
|
||||||
|
name: home_moments_enabled
|
||||||
|
label: 首页显示瞬间模块
|
||||||
|
value: false
|
||||||
|
- $formkit: switch
|
||||||
|
name: home_photos_enabled
|
||||||
|
label: 首页显示图库模块
|
||||||
|
value: false
|
||||||
|
- $formkit: switch
|
||||||
|
name: home_quote_enabled
|
||||||
|
label: 首页显示语录模块
|
||||||
|
value: false
|
||||||
|
- $formkit: switch
|
||||||
|
name: home_timeline_enabled
|
||||||
|
label: 首页显示时间线模块
|
||||||
|
value: false
|
||||||
|
- $formkit: switch
|
||||||
|
name: home_friends_enabled
|
||||||
|
label: 首页显示友链模块
|
||||||
|
value: false
|
||||||
|
- $formkit: switch
|
||||||
|
name: home_message_wall_enabled
|
||||||
|
label: 首页显示留言墙模块
|
||||||
|
value: false
|
||||||
|
- $formkit: textarea
|
||||||
|
name: home_quote_content
|
||||||
|
label: 首页语录内容
|
||||||
|
if: "$get(home_quote_enabled).value === true"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 修改 index.astro 引入所有模块组件
|
||||||
|
|
||||||
|
在 `index.astro` 中:
|
||||||
|
1. 导入所有 Section 组件
|
||||||
|
2. 按照布局顺序排列:HeroSection → FeaturedSection → 文章列表 → QuoteSection → MomentsSection → PhotosSection → TimelineSection → FriendsSection → MessageWallSection
|
||||||
|
3. 每个 Section 组件内部已有 `th:if` 条件控制显隐
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
import Layout from "../layouts/Layout.astro";
|
||||||
|
import HeroSection from "../components/HeroSection.astro";
|
||||||
|
import FeaturedSection from "../components/FeaturedSection.astro";
|
||||||
|
import QuoteSection from "../components/QuoteSection.astro";
|
||||||
|
import MomentsSection from "../components/MomentsSection.astro";
|
||||||
|
import PhotosSection from "../components/PhotosSection.astro";
|
||||||
|
import TimelineSection from "../components/TimelineSection.astro";
|
||||||
|
import FriendsSection from "../components/FriendsSection.astro";
|
||||||
|
import MessageWallSection from "../components/MessageWallSection.astro";
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 步骤 5:构建部署与验证
|
||||||
|
|
||||||
|
1. 运行 `npm run build` 构建主题
|
||||||
|
2. Docker 部署到 Halo 实例
|
||||||
|
3. 使用浏览器验证:
|
||||||
|
- 暗色模式切换是否有过渡动画
|
||||||
|
- 首页文章列表是否为单栏(文字左图片右)
|
||||||
|
- 首页模块是否正确显示/隐藏
|
||||||
|
- 硬编码字符串是否已替换为可配置项
|
||||||
|
- 移动端响应式是否正常
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 实施顺序
|
||||||
|
|
||||||
|
1. **步骤 1** - 暗色模式过渡动画(ThemeSwitcher.vue + MobileMenu.astro)
|
||||||
|
2. **步骤 2** - 首页文章列表单栏布局(index.astro)
|
||||||
|
3. **步骤 3** - 硬编码字符串改进(settings.yaml + 所有模板文件)
|
||||||
|
4. **步骤 4** - 首页模块化布局(settings.yaml + index.astro)
|
||||||
|
5. **步骤 5** - 构建部署验证
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# lightGallery 图片灯箱集成 — 剩余工作计划
|
||||||
|
|
||||||
|
## 当前状态总结
|
||||||
|
|
||||||
|
大部分集成工作已在上一轮会话中完成:
|
||||||
|
|
||||||
|
- ✅ `lightgallery@2.9.0` 已通过 pnpm 安装
|
||||||
|
- ✅ `LightGallery.astro` 组件已创建(含暗色模式 CSS、冲突检测、自动包裹图片、初始化逻辑)
|
||||||
|
- ✅ `post.astro` 已引入 LightGallery 组件
|
||||||
|
- ✅ `settings.yaml` 已添加灯箱开关 `article_lightbox_enabled`
|
||||||
|
- ✅ 构建成功,字体/图标资源已正确处理(woff2 内联为 base64,ttf/woff/svg/gif 均正确引用 `/themes/warm-island/assets/` 路径)
|
||||||
|
- ✅ 已部署到 Docker 容器
|
||||||
|
|
||||||
|
## 发现的问题
|
||||||
|
|
||||||
|
### 关键 Bug:`selector: 'a'` 选择器过于宽泛
|
||||||
|
|
||||||
|
当前 `LightGallery.astro` 中使用 `selector: 'a'` 初始化 lightGallery,这会导致 `.wi-post__body` 内**所有** `<a>` 标签都被视为灯箱项目,包括:
|
||||||
|
|
||||||
|
1. **普通文本链接**(如 `<a href="https://example.com">链接文字</a>`)— 点击后不会正常跳转,而是尝试在灯箱中打开,导致加载失败
|
||||||
|
2. **已有 `<a>` 包裹的图片**(如 `<a href="/some-page"><img src="photo.jpg"></a>`)— 灯箱会使用 `href`(页面 URL)而非图片地址作为源,导致显示错误
|
||||||
|
|
||||||
|
### 修复方案
|
||||||
|
|
||||||
|
1. 为应加入灯箱的 `<a>` 标签添加专属类名 `wi-lightgallery-item`
|
||||||
|
2. 自动包裹图片时,给新创建的 `<a>` 标签添加此类名
|
||||||
|
3. 对已有 `<a>` 包裹的 `<img>`,也给其父 `<a>` 添加此类名,并设置 `data-src` 属性指向图片原图(确保灯箱使用图片地址而非链接地址)
|
||||||
|
4. 将 lightGallery 初始化的 `selector` 从 `'a'` 改为 `'.wi-lightgallery-item'`
|
||||||
|
|
||||||
|
## 实施步骤
|
||||||
|
|
||||||
|
### 步骤 1:修复 LightGallery.astro 的选择器和包裹逻辑
|
||||||
|
|
||||||
|
修改 `src/components/LightGallery.astro` 中的 `<script>` 部分:
|
||||||
|
|
||||||
|
**自动包裹逻辑改进:**
|
||||||
|
- 对没有 `<a>` 包裹的 `<img>`:创建 `<a class="wi-lightgallery-item" href="${src}">` 包裹
|
||||||
|
- 对已有 `<a>` 包裹的 `<img>`:给父 `<a>` 添加 `wi-lightgallery-item` 类名,并添加 `data-src="${src}"` 属性
|
||||||
|
|
||||||
|
**选择器改进:**
|
||||||
|
- `selector: 'a'` → `selector: '.wi-lightgallery-item'`
|
||||||
|
|
||||||
|
### 步骤 2:构建项目
|
||||||
|
|
||||||
|
运行 `pnpm build` 构建主题。
|
||||||
|
|
||||||
|
### 步骤 3:部署到 Docker
|
||||||
|
|
||||||
|
将构建产物复制到 Halo 容器并重启。
|
||||||
|
|
||||||
|
### 步骤 4:浏览器验证
|
||||||
|
|
||||||
|
使用 agent-browser 进行以下验证:
|
||||||
|
1. 打开文章详情页
|
||||||
|
2. 确认 lightGallery JS 模块加载成功(检查 `[data-lg-uid]` 属性)
|
||||||
|
3. 点击文章中的图片,验证灯箱正常打开
|
||||||
|
4. 验证缩放功能正常
|
||||||
|
5. 验证缩略图条显示
|
||||||
|
6. 验证暗色模式下样式正确
|
||||||
|
7. 验证普通文本链接仍可正常点击跳转(不被灯箱拦截)
|
||||||
|
|
||||||
|
### 步骤 5:修复验证中发现的问题(如有)
|
||||||
|
|
||||||
|
根据浏览器验证结果,修复任何发现的问题。
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
# WarmIsland 暖屿 主题 Checklist
|
||||||
|
|
||||||
|
## 基础架构
|
||||||
|
|
||||||
|
- [x] theme.yaml 元数据正确:metadata.name 为 warm-island,displayName 为 WarmIsland 暖屿,requires 为 >=2.24.0
|
||||||
|
- [x] astro.config.mjs base 路径为 /themes/warm-island
|
||||||
|
- [x] 默认 Logo.png 和 Logo.ico 已复制到 public/ 目录
|
||||||
|
- [x] package.json 包含 sass 依赖
|
||||||
|
|
||||||
|
## 设计系统
|
||||||
|
|
||||||
|
- [x] SCSS 架构完整:_variables.scss、_colors.scss、_typography.scss、_spacing.scss、_animations.scss、_mixins.scss、main.scss
|
||||||
|
- [x] 亮色模式配色正确:奶油暖白背景、日落橘强调色、焦糖棕文字、雾粉辅助、海盐灰边框
|
||||||
|
- [x] 深色模式配色正确:深色暖调版本,保持温暖感
|
||||||
|
- [x] 字体方案完整:标题层级、正文阅读舒适度、letter-spacing
|
||||||
|
- [x] 间距系统定义:留白节奏、组件间距
|
||||||
|
- [x] 动效 token 定义:呼吸动画、hover 浮动、缓动曲线
|
||||||
|
|
||||||
|
## 配置系统
|
||||||
|
|
||||||
|
- [ ] settings.yaml 包含 19 个配置分组(实际只有 18 个:basic、hero、home、style、animation、navbar、footer、article、layout、moments、photos、friends、links、comment、search、messageboard、mobile、advanced)
|
||||||
|
- [x] basic 分组支持 Logo/favicon 自定义替换
|
||||||
|
- [x] hero 分组支持文案、背景图、CTA 按钮配置
|
||||||
|
- [x] home 分组支持模块开启/关闭、排序、样式切换
|
||||||
|
- [x] style 分组支持主色调自定义
|
||||||
|
- [x] animation 分组支持动效开关
|
||||||
|
- [x] settings.yaml 中 settingName 与 theme.yaml 中一致
|
||||||
|
- [x] 模板中通过 theme.config.[group].[name] 正确读取配置
|
||||||
|
|
||||||
|
## 导航栏
|
||||||
|
|
||||||
|
- [x] 导航栏悬浮效果:position sticky、backdrop-filter blur
|
||||||
|
- [x] 胶囊圆角容器
|
||||||
|
- [x] 半透明背景
|
||||||
|
- [x] 滚动时添加阴影与背景加深
|
||||||
|
- [x] 菜单项使用 menuFinder.getPrimary() 渲染
|
||||||
|
- [x] 品牌 Logo 展示,支持 settings 自定义
|
||||||
|
- [x] 搜索按钮调用 SearchWidget.open()
|
||||||
|
- [x] 深色模式切换按钮
|
||||||
|
- [x] 柔和 hover 动效
|
||||||
|
- [x] 移动端导航菜单适配
|
||||||
|
|
||||||
|
## Hero 首屏
|
||||||
|
|
||||||
|
- [x] 超大品牌标题展示
|
||||||
|
- [x] 情绪化副标题文案
|
||||||
|
- [x] 岛屿氛围背景:柔和光斑 + 模糊层次
|
||||||
|
- [x] 呼吸动画:光斑缓慢脉动
|
||||||
|
- [x] CTA 按钮:高级圆角、柔和阴影、hover 微交互
|
||||||
|
- [x] 页面滚动引导指示器
|
||||||
|
- [x] 支持 settings 中的 Hero 配置
|
||||||
|
|
||||||
|
## 首页布局
|
||||||
|
|
||||||
|
- [x] 杂志化布局,非传统博客列表
|
||||||
|
- [x] Editorial Design 风格
|
||||||
|
- [x] 呼吸感留白
|
||||||
|
- [x] 内容节奏感
|
||||||
|
- [x] 不规则高级布局
|
||||||
|
- [x] 大图排版
|
||||||
|
- [ ] 模块根据 settings 配置控制开启/关闭与排序(开启/关闭已实现,排序未实现——模块顺序在 index.astro 中硬编码)
|
||||||
|
|
||||||
|
## 文章卡片
|
||||||
|
|
||||||
|
- [x] 大封面图展示
|
||||||
|
- [x] 柔和阴影
|
||||||
|
- [x] 半透明层次
|
||||||
|
- [x] hover 微浮动效果(translateY + 阴影加深 + 封面图 scale)
|
||||||
|
- [x] 缓动动画
|
||||||
|
- [x] 高级圆角
|
||||||
|
- [x] 情绪化摘要
|
||||||
|
- [x] 使用 thumbnail.gen() 响应式图片
|
||||||
|
|
||||||
|
## 内容页面
|
||||||
|
|
||||||
|
- [x] 文章详情页:标题、日期、分类、标签、封面图、正文排版、上下篇导航
|
||||||
|
- [x] 正文排版阅读舒适度优化
|
||||||
|
- [x] 独立页面模板正常工作
|
||||||
|
- [x] 留言板自定义页面模板已注册在 theme.yaml customTemplates.page
|
||||||
|
- [x] 归档页时间线式布局
|
||||||
|
- [x] 分类页与标签页 WarmIsland 风格
|
||||||
|
- [x] 分页导航正常工作
|
||||||
|
|
||||||
|
## 插件适配
|
||||||
|
|
||||||
|
- [x] plugin-links 友链页面专属 UI,条件渲染
|
||||||
|
- [x] plugin-photos 图库页面专属 UI,条件渲染
|
||||||
|
- [x] plugin-moments 瞬间页面专属 UI,条件渲染
|
||||||
|
- [x] plugin-friends-new 朋友圈页面专属 UI,条件渲染
|
||||||
|
- [x] plugin-comment-widget 评论区美化,保留默认输入框结构
|
||||||
|
- [x] plugin-search-widget 搜索弹层 Spotlight/Raycast 风格
|
||||||
|
- [x] 搜索快捷键 Cmd/Ctrl + K 可用
|
||||||
|
- [x] 所有插件页面使用 pluginFinder.available() 条件渲染
|
||||||
|
|
||||||
|
## 动效
|
||||||
|
|
||||||
|
- [x] 呼吸动画正常工作
|
||||||
|
- [x] hover 浮动效果正常
|
||||||
|
- [x] 页面滚动渐入效果(Intersection Observer)
|
||||||
|
- [x] 光感移动效果
|
||||||
|
- [ ] 页面过渡动画(未实现页面间过渡动画)
|
||||||
|
- [x] 动效可通过 settings 关闭
|
||||||
|
|
||||||
|
## 深色模式
|
||||||
|
|
||||||
|
- [x] 全站深色模式配色正确
|
||||||
|
- [x] 导航栏深色模式适配
|
||||||
|
- [x] 文章卡片深色模式适配
|
||||||
|
- [x] 评论区深色模式适配
|
||||||
|
- [x] 搜索组件深色模式适配
|
||||||
|
- [x] 插件页面深色模式适配
|
||||||
|
- [x] html 元素设置 data-color-scheme 属性供官方插件适配
|
||||||
|
- [x] 系统偏好跟随正常工作
|
||||||
|
|
||||||
|
## 移动端
|
||||||
|
|
||||||
|
- [x] 导航栏移动端适配
|
||||||
|
- [x] 首页移动端布局
|
||||||
|
- [x] 文章卡片移动端布局
|
||||||
|
- [x] 文章详情页移动端阅读体验
|
||||||
|
- [x] 插件页面移动端适配
|
||||||
|
- [x] 移动端保持品牌感与高级感
|
||||||
|
|
||||||
|
## SEO 与性能
|
||||||
|
|
||||||
|
- [x] 正确的 meta 标签
|
||||||
|
- [x] 语义化 HTML
|
||||||
|
- [x] 合理的标题层级
|
||||||
|
- [x] `<halo:footer />` 注入点存在于所有页面
|
||||||
|
- [ ] 关键 CSS 优先加载(未实现 critical CSS 提取策略)
|
||||||
|
|
||||||
|
## 构建验证
|
||||||
|
|
||||||
|
- [ ] `pnpm build` 构建成功(未验证)
|
||||||
|
- [ ] templates/ 目录输出正确(未验证)
|
||||||
|
- [x] 所有页面模板文件存在
|
||||||
|
- [ ] 静态资源路径正确(未验证)
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
# WarmIsland 暖屿 主题 Spec
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Halo 默认主题及社区主题多为传统博客布局,缺乏品牌气质与情绪温度。WarmIsland 暖屿旨在打造一座"深夜里温暖、安静、治愈的小岛"——一个具有独特品牌记忆点、杂志化排版、情绪化 UI 的高端生活方式博客主题,而非普通博客模板。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 基于 `halo-sigs/theme-astro-starter` 模板,从 0 重构全部页面与组件
|
||||||
|
- **BREAKING**: 完全替换现有 Astro 组件结构、样式系统、页面布局
|
||||||
|
- 新增 Hero 首屏模块(超大标题 + 情绪文案 + 岛屿氛围背景 + 呼吸动画)
|
||||||
|
- 新增杂志化首页布局(Editorial Design、不规则高级布局、大图排版)
|
||||||
|
- 新增悬浮毛玻璃导航栏(Apple/Raycast 风格、胶囊圆角、滚动吸附)
|
||||||
|
- 新增文章卡片设计(大封面图、毛玻璃、hover 微浮动、高级圆角)
|
||||||
|
- 新增低饱和暖色配色系统(奶油暖白、日落橘、焦糖棕、雾粉、海盐灰)
|
||||||
|
- 新增杂志排版字体方案与阅读舒适度优化
|
||||||
|
- 新增克制柔和动效系统(呼吸动画、hover 浮动、页面渐隐、光感移动)
|
||||||
|
- 新增移动端原生 App 级体验重新设计
|
||||||
|
- 新增 6 个 Halo 插件专属 UI 适配(友链、图库、瞬间、朋友圈、评论、搜索)
|
||||||
|
- 新增留言板自定义页面模板
|
||||||
|
- 新增完整 settings.yaml 配置系统(19 个分组、模块化首页系统)
|
||||||
|
- 新增深色模式完整适配
|
||||||
|
- 新增默认 Logo 与 favicon 资源
|
||||||
|
- 更新 theme.yaml 元数据与兼容版本至 Halo >= 2.24.0
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: 全部页面模板、全部组件、全部样式、主题配置系统
|
||||||
|
- Affected code:
|
||||||
|
- `theme.yaml` — 元数据、customTemplates、requires 版本
|
||||||
|
- `settings.yaml` — 新增(原模板无此文件)
|
||||||
|
- `astro.config.mjs` — base 路径、插件配置
|
||||||
|
- `src/layouts/Layout.astro` — 完全重写
|
||||||
|
- `src/components/Header.astro` — 完全重写为悬浮毛玻璃导航
|
||||||
|
- `src/components/Footer.astro` — 完全重写
|
||||||
|
- `src/components/` — 新增大量组件
|
||||||
|
- `src/pages/index.astro` — 完全重写为杂志化首页
|
||||||
|
- `src/pages/post.astro` — 完全重写
|
||||||
|
- `src/pages/page.astro` — 完全重写
|
||||||
|
- `src/pages/` — 新增多个页面模板
|
||||||
|
- `src/styles/` — 完全重写为 SCSS 模块化架构
|
||||||
|
- `public/` — 新增资源文件与 Thymeleaf fragments
|
||||||
|
- `package.json` — 新增依赖(SCSS 等)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 品牌识别系统
|
||||||
|
|
||||||
|
主题 SHALL 具有极强的品牌识别度,用户一眼即可辨识"WarmIsland 暖屿"的品牌气质。
|
||||||
|
|
||||||
|
#### Scenario: 品牌首屏印象
|
||||||
|
- **WHEN** 用户首次访问 WarmIsland 站点
|
||||||
|
- **THEN** 第一屏即传达"深夜、温暖、安静、治愈"的品牌氛围,包含品牌名称、情绪化文案、岛屿氛围视觉元素
|
||||||
|
|
||||||
|
#### Scenario: 品牌一致性
|
||||||
|
- **WHEN** 用户浏览站内任意页面
|
||||||
|
- **THEN** 所有页面保持统一的品牌视觉语言(配色、字体、动效、留白节奏)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Hero 首屏模块
|
||||||
|
|
||||||
|
系统 SHALL 提供全屏 Hero 区域作为首页第一视觉焦点。
|
||||||
|
|
||||||
|
#### Scenario: Hero 展示
|
||||||
|
- **WHEN** 用户访问首页
|
||||||
|
- **THEN** 显示全屏 Hero 区域,包含:超大品牌标题、情绪化副标题文案、岛屿氛围背景(柔和光斑 + 模糊层次)、呼吸动画、高级 CTA 按钮、页面滚动引导
|
||||||
|
|
||||||
|
#### Scenario: Hero 可配置
|
||||||
|
- **WHEN** 管理员在后台 settings 中配置 Hero 文案、背景图、按钮文字
|
||||||
|
- **THEN** 前端 Hero 区域相应更新
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 杂志化首页布局
|
||||||
|
|
||||||
|
首页 SHALL 采用 Editorial Design 杂志化布局,而非传统博客列表。
|
||||||
|
|
||||||
|
#### Scenario: 首页模块化展示
|
||||||
|
- **WHEN** 用户访问首页
|
||||||
|
- **THEN** 首页由可配置模块组成:Hero、Featured(置顶文章)、Latest(最新文章)、Moments(瞬间)、Photos(图库)、Friends(友链)、Links(链接)、Quote(语录)、Timeline(时间线)、About(关于)、Music(音乐)、Message Wall(留言墙)
|
||||||
|
- **AND** 每个模块可在后台独立开启/关闭、排序、配置样式
|
||||||
|
|
||||||
|
#### Scenario: 文章展示
|
||||||
|
- **WHEN** 首页展示文章列表
|
||||||
|
- **THEN** 采用杂志化大图排版,具有呼吸感留白、内容节奏感、不规则高级布局,而非密集信息流或普通卡片堆叠
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 悬浮毛玻璃导航栏
|
||||||
|
|
||||||
|
导航栏 SHALL 采用悬浮毛玻璃设计,具有 Apple/Raycast/Linear/Arc 级别的高级导航体验。
|
||||||
|
|
||||||
|
#### Scenario: 导航栏展示
|
||||||
|
- **WHEN** 用户浏览任意页面
|
||||||
|
- **THEN** 导航栏呈现:悬浮效果、毛玻璃背景、胶囊圆角容器、半透明、滚动吸附顶部、柔和 hover 动效
|
||||||
|
|
||||||
|
#### Scenario: 移动端导航
|
||||||
|
- **WHEN** 用户在移动端访问
|
||||||
|
- **THEN** 导航栏适配为移动端菜单,保持品牌感与高级感
|
||||||
|
|
||||||
|
#### Scenario: 导航栏可配置
|
||||||
|
- **WHEN** 管理员在后台配置导航 Logo、菜单项
|
||||||
|
- **THEN** 导航栏相应更新,支持自定义 Logo 替换
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 文章卡片设计
|
||||||
|
|
||||||
|
文章卡片 SHALL 采用杂志化高级设计。
|
||||||
|
|
||||||
|
#### Scenario: 卡片展示
|
||||||
|
- **WHEN** 文章以卡片形式展示
|
||||||
|
- **THEN** 卡片具有:大封面图、柔和阴影、半透明层次、毛玻璃效果、hover 微浮动、缓动动画、高级圆角、情绪化摘要
|
||||||
|
|
||||||
|
#### Scenario: 卡片交互
|
||||||
|
- **WHEN** 用户 hover 文章卡片
|
||||||
|
- **THEN** 卡片产生柔和上浮效果,封面图轻微放大,阴影加深
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 低饱和暖色配色系统
|
||||||
|
|
||||||
|
主题 SHALL 使用低饱和暖色体系。
|
||||||
|
|
||||||
|
#### Scenario: 亮色模式配色
|
||||||
|
- **WHEN** 主题处于亮色模式
|
||||||
|
- **THEN** 使用奶油暖白背景、日落橘强调色、焦糖棕文字色、雾粉辅助色、海盐灰边框色
|
||||||
|
|
||||||
|
#### Scenario: 深色模式配色
|
||||||
|
- **WHEN** 主题处于深色模式
|
||||||
|
- **THEN** 配色自动切换为深色暖调版本,保持温暖感而非冰冷科技感
|
||||||
|
|
||||||
|
#### Scenario: 配色可自定义
|
||||||
|
- **WHEN** 管理员在后台 settings 中修改主色调
|
||||||
|
- **THEN** 前端配色系统相应更新
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 杂志排版字体方案
|
||||||
|
|
||||||
|
主题 SHALL 采用高级生活杂志 / 日系 Editorial 级别的排版方案。
|
||||||
|
|
||||||
|
#### Scenario: 标题排版
|
||||||
|
- **WHEN** 页面渲染标题
|
||||||
|
- **THEN** 标题具有明确的视觉层级、合适的字重与字号、letter-spacing 调整、杂志排版感
|
||||||
|
|
||||||
|
#### Scenario: 正文阅读
|
||||||
|
- **WHEN** 用户阅读文章正文
|
||||||
|
- **THEN** 正文具有舒适的行高、段间距、留白节奏、呼吸感,阅读体验优于传统博客
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 克制柔和动效系统
|
||||||
|
|
||||||
|
主题 SHALL 实现克制、柔和、高级的动效。
|
||||||
|
|
||||||
|
#### Scenario: 呼吸动画
|
||||||
|
- **WHEN** 页面加载完成
|
||||||
|
- **THEN** Hero 区域背景光斑呈现缓慢呼吸动画,营造"活着"的氛围感
|
||||||
|
|
||||||
|
#### Scenario: 页面过渡
|
||||||
|
- **WHEN** 用户在页面间导航
|
||||||
|
- **THEN** 页面切换呈现柔和渐隐渐显过渡
|
||||||
|
|
||||||
|
#### Scenario: 卡片交互动效
|
||||||
|
- **WHEN** 用户 hover 交互元素
|
||||||
|
- **THEN** 产生柔和缓动动画(浮动、阴影变化、颜色过渡),而非廉价炫酷动画
|
||||||
|
|
||||||
|
#### Scenario: 动效可配置
|
||||||
|
- **WHEN** 管理员在后台关闭动效
|
||||||
|
- **THEN** 所有动画效果禁用,保持静态展示
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 移动端原生 App 级体验
|
||||||
|
|
||||||
|
移动端 SHALL 重新设计为原生 App 级体验,而非简单缩放。
|
||||||
|
|
||||||
|
#### Scenario: 移动端导航
|
||||||
|
- **WHEN** 用户在移动端访问
|
||||||
|
- **THEN** 导航栏变为沉浸式移动菜单,具有品牌感
|
||||||
|
|
||||||
|
#### Scenario: 移动端阅读
|
||||||
|
- **WHEN** 用户在移动端阅读文章
|
||||||
|
- **THEN** 排版适配移动端,保持高级感、品牌感、情绪感
|
||||||
|
|
||||||
|
#### Scenario: 移动端卡片
|
||||||
|
- **WHEN** 移动端展示文章卡片
|
||||||
|
- **THEN** 卡片布局适配竖屏,保持大图氛围与留白节奏
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 插件适配 — plugin-links(友链)
|
||||||
|
|
||||||
|
主题 SHALL 完整适配 plugin-links 友链插件,并提供专属 UI。
|
||||||
|
|
||||||
|
#### Scenario: 友链页面展示
|
||||||
|
- **WHEN** 用户访问友链页面且 plugin-links 已安装
|
||||||
|
- **THEN** 友链以 WarmIsland 风格的卡片网格展示,具有毛玻璃效果、柔和阴影、hover 微交互
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 插件适配 — plugin-photos(图库)
|
||||||
|
|
||||||
|
主题 SHALL 完整适配 plugin-photos 图库插件,并提供专属 UI。
|
||||||
|
|
||||||
|
#### Scenario: 图库页面展示
|
||||||
|
- **WHEN** 用户访问图库页面且 plugin-photos 已安装
|
||||||
|
- **THEN** 图库以瀑布流 / 杂志化网格展示,具有大图预览、柔和过渡、灯箱效果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 插件适配 — plugin-moments(瞬间)
|
||||||
|
|
||||||
|
主题 SHALL 完整适配 plugin-moments 瞬间插件,并提供专属 UI。
|
||||||
|
|
||||||
|
#### Scenario: 瞬间页面展示
|
||||||
|
- **WHEN** 用户访问瞬间页面且 plugin-moments 已安装
|
||||||
|
- **THEN** 瞬间以时间线 + 卡片形式展示,具有情绪化排版、呼吸感留白
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 插件适配 — plugin-friends-new(朋友圈)
|
||||||
|
|
||||||
|
主题 SHALL 完整适配 plugin-friends-new 朋友圈插件,并提供专属 UI。
|
||||||
|
|
||||||
|
#### Scenario: 朋友圈页面展示
|
||||||
|
- **WHEN** 用户访问朋友圈页面且 plugin-friends-new 已安装
|
||||||
|
- **THEN** 朋友圈以 WarmIsland 风格的卡片流展示,具有品牌统一感
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 插件适配 — plugin-comment-widget(评论组件)
|
||||||
|
|
||||||
|
主题 SHALL 适配 plugin-comment-widget,评论区风格与 WarmIsland 保持统一。
|
||||||
|
|
||||||
|
#### Scenario: 评论区展示
|
||||||
|
- **WHEN** 文章/页面下方显示评论区
|
||||||
|
- **THEN** 评论区具有:毛玻璃层次、半透明背景、柔和阴影、hover 微交互、深色模式适配
|
||||||
|
|
||||||
|
#### Scenario: 评论功能兼容
|
||||||
|
- **WHEN** 用户使用评论功能
|
||||||
|
- **THEN** 保留插件默认评论输入框结构,不破坏插件功能逻辑与兼容性
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 插件适配 — plugin-search-widget(搜索组件)
|
||||||
|
|
||||||
|
主题 SHALL 适配 plugin-search-widget,搜索体验设计为"WarmIsland 的内容探索空间"。
|
||||||
|
|
||||||
|
#### Scenario: 搜索触发
|
||||||
|
- **WHEN** 用户点击搜索按钮或使用快捷键(Cmd/Ctrl + K)
|
||||||
|
- **THEN** 弹出 Spotlight/Raycast 风格的悬浮搜索层,具有毛玻璃弹层、模糊背景、平滑动画
|
||||||
|
|
||||||
|
#### Scenario: 搜索结果展示
|
||||||
|
- **WHEN** 搜索结果返回
|
||||||
|
- **THEN** 结果以情绪化方式展示,保持 WarmIsland 品牌风格
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 留言板自定义页面模板
|
||||||
|
|
||||||
|
主题 SHALL 提供留言板自定义页面模板。
|
||||||
|
|
||||||
|
#### Scenario: 留言板模板注册
|
||||||
|
- **WHEN** 主题安装后
|
||||||
|
- **THEN** 在 theme.yaml 的 customTemplates.page 中注册留言板模板
|
||||||
|
|
||||||
|
#### Scenario: 留言板页面展示
|
||||||
|
- **WHEN** 用户访问使用留言板模板的页面
|
||||||
|
- **THEN** 显示 WarmIsland 风格的留言板,具有情绪化排版、评论组件集成
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 完整 settings.yaml 配置系统
|
||||||
|
|
||||||
|
主题 SHALL 基于 Halo 2.x 的 FormKit Schema 提供完整配置系统。
|
||||||
|
|
||||||
|
#### Scenario: 配置分组
|
||||||
|
- **WHEN** 管理员进入主题设置页面
|
||||||
|
- **THEN** 可见以下配置分组:basic、hero、layout、style、animation、article、navbar、footer、home、moments、photos、friends、links、comment、search、messageboard、mobile、advanced
|
||||||
|
|
||||||
|
#### Scenario: 首页模块化配置
|
||||||
|
- **WHEN** 管理员在 home 分组中配置首页模块
|
||||||
|
- **THEN** 可对每个模块进行:开启/关闭、排序、独立配置、样式切换
|
||||||
|
|
||||||
|
#### Scenario: 配置生效
|
||||||
|
- **WHEN** 管理员保存配置
|
||||||
|
- **THEN** 前端通过 `theme.config.[group].[name]` 读取配置并相应渲染
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 默认主题资源
|
||||||
|
|
||||||
|
主题 SHALL 包含默认 Logo 与 favicon 资源。
|
||||||
|
|
||||||
|
#### Scenario: 默认资源加载
|
||||||
|
- **WHEN** 主题首次安装
|
||||||
|
- **THEN** 使用默认 Logo(Logo.png)与 favicon(Logo.ico)
|
||||||
|
|
||||||
|
#### Scenario: 资源可替换
|
||||||
|
- **WHEN** 管理员在后台 settings 中上传自定义 Logo/favicon
|
||||||
|
- **THEN** 前端使用自定义资源替代默认资源
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Astro 架构
|
||||||
|
|
||||||
|
主题 SHALL 基于 Astro 架构实现现代化开发。
|
||||||
|
|
||||||
|
#### Scenario: 组件化开发
|
||||||
|
- **WHEN** 开发主题功能
|
||||||
|
- **THEN** 使用 Astro Components + Vue Islands 架构,动态组件拆分,SCSS 模块化
|
||||||
|
|
||||||
|
#### Scenario: 构建输出
|
||||||
|
- **WHEN** 执行 `astro build`
|
||||||
|
- **THEN** 输出到 `templates/` 目录,静态资源输出到 `templates/assets/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 深色模式
|
||||||
|
|
||||||
|
主题 SHALL 完整支持深色模式。
|
||||||
|
|
||||||
|
#### Scenario: 深色模式切换
|
||||||
|
- **WHEN** 用户切换深色模式
|
||||||
|
- **THEN** 全站配色切换为深色暖调版本,所有组件(导航、卡片、评论区、搜索等)适配深色模式
|
||||||
|
|
||||||
|
#### Scenario: 系统偏好跟随
|
||||||
|
- **WHEN** 用户未手动设置主题模式
|
||||||
|
- **THEN** 主题跟随系统深色/亮色偏好
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Halo 版本兼容
|
||||||
|
|
||||||
|
主题 SHALL 兼容 Halo >= 2.24.0。
|
||||||
|
|
||||||
|
#### Scenario: 版本声明
|
||||||
|
- **WHEN** 主题安装
|
||||||
|
- **THEN** theme.yaml 中 `spec.requires` 声明为 `">=2.24.0"`
|
||||||
|
|
||||||
|
#### Scenario: API 使用
|
||||||
|
- **WHEN** 主题调用 Halo API
|
||||||
|
- **THEN** 使用 Halo 2.24+ 最新主题开发规范和 API,不使用过时 API
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: SEO 与性能
|
||||||
|
|
||||||
|
主题 SHALL 具备良好的 SEO 与首屏性能。
|
||||||
|
|
||||||
|
#### Scenario: SEO 基础
|
||||||
|
- **WHEN** 页面渲染
|
||||||
|
- **THEN** 包含正确的 meta 标签、语义化 HTML、合理的标题层级
|
||||||
|
|
||||||
|
#### Scenario: 首屏性能
|
||||||
|
- **WHEN** 用户首次访问
|
||||||
|
- **THEN** 首屏内容快速渲染,关键 CSS 内联,非关键资源延迟加载
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: 主题元数据
|
||||||
|
|
||||||
|
theme.yaml 元数据更新为 WarmIsland 暖屿品牌信息。
|
||||||
|
|
||||||
|
- `metadata.name`: `warm-island`
|
||||||
|
- `spec.displayName`: `WarmIsland 暖屿`
|
||||||
|
- `spec.requires`: `>=2.24.0`
|
||||||
|
- `spec.settingName`: `warm-island-setting`
|
||||||
|
- `spec.configMapName`: `warm-island-configMap`
|
||||||
|
- `spec.customTemplates.page`: 新增留言板模板
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## REMOVED Requirements
|
||||||
|
|
||||||
|
### Requirement: 原始 Astro Starter 模板 UI
|
||||||
|
|
||||||
|
**Reason**: 完全替换为 WarmIsland 品牌化 UI,原始模板 UI 不再使用
|
||||||
|
**Migration**: 所有原始组件、样式、页面布局将被完全重写,无需迁移
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
# Tasks
|
||||||
|
|
||||||
|
## Phase 1: 基础架构与设计系统
|
||||||
|
|
||||||
|
- [x] Task 1: 更新主题元数据与项目配置
|
||||||
|
- [x] 更新 theme.yaml:metadata.name 改为 warm-island,displayName 改为 WarmIsland 暖屿,requires 改为 >=2.24.0,新增 settingName/configMapName/customTemplates
|
||||||
|
- [x] 更新 astro.config.mjs:base 路径改为 /themes/warm-island
|
||||||
|
- [x] 复制默认 Logo.png 和 Logo.ico 到 public/ 资源目录
|
||||||
|
- [x] 更新 package.json:新增 sass 依赖
|
||||||
|
|
||||||
|
- [x] Task 2: 建立设计系统基础 — SCSS 架构与配色
|
||||||
|
- [x] 创建 src/styles/ 目录结构:_variables.scss、_colors.scss、_typography.scss、_spacing.scss、_animations.scss、_mixins.scss、main.scss
|
||||||
|
- [x] 定义 CSS 自定义属性:亮色模式配色(奶油暖白、日落橘、焦糖棕、雾粉、海盐灰)与深色模式配色
|
||||||
|
- [x] 定义字体方案:标题字体、正文字体、字号层级、行高、letter-spacing
|
||||||
|
- [x] 定义间距系统:留白节奏、组件间距
|
||||||
|
- [x] 定义动效系统:呼吸动画、hover 浮动、页面渐隐、缓动曲线
|
||||||
|
- [x] 定义圆角、阴影、毛玻璃等视觉 token
|
||||||
|
|
||||||
|
- [x] Task 3: 创建完整 settings.yaml 配置系统
|
||||||
|
- [x] 创建 settings.yaml,包含 18 个分组:basic、hero、layout、style、animation、article、navbar、footer、home、moments、photos、friends、links、comment、search、messageboard、mobile、advanced
|
||||||
|
- [x] 实现 basic 分组:站点 Logo、favicon、站点描述自定义
|
||||||
|
- [x] 实现 hero 分组:Hero 文案、副标题、背景图、CTA 按钮文字与链接、开启/关闭
|
||||||
|
- [x] 实现 home 分组:首页模块开启/关闭、排序、样式切换(Hero、Featured、Latest、Moments、Photos、Friends、Links、Quote、Timeline、About、Music、Message Wall)
|
||||||
|
- [x] 实现 style 分组:主色调自定义、配色方案选择
|
||||||
|
- [x] 实现 navbar 分组:导航栏样式配置
|
||||||
|
- [x] 实现 footer 分组:页脚内容配置
|
||||||
|
- [x] 实现 animation 分组:动效开启/关闭
|
||||||
|
- [x] 实现其余分组的基础配置项
|
||||||
|
|
||||||
|
## Phase 2: 核心布局与组件
|
||||||
|
|
||||||
|
- [x] Task 4: 重写 Layout.astro 主布局
|
||||||
|
- [x] 实现 HTML 基础结构:lang、meta、SEO 标签
|
||||||
|
- [x] 实现深色模式初始化脚本(localStorage + 系统偏好)
|
||||||
|
- [x] 实现 `<halo:footer />` 注入点
|
||||||
|
- [x] 引入 SCSS 设计系统
|
||||||
|
- [x] 实现全局平滑滚动
|
||||||
|
|
||||||
|
- [x] Task 5: 实现悬浮毛玻璃导航栏
|
||||||
|
- [x] 创建 Navbar.astro 组件:悬浮定位、毛玻璃背景(backdrop-filter: blur)、胶囊圆角容器、半透明
|
||||||
|
- [x] 实现滚动吸附效果:滚动时添加阴影与背景加深
|
||||||
|
- [x] 实现导航菜单渲染:使用 menuFinder.getPrimary() 获取菜单项
|
||||||
|
- [x] 实现品牌 Logo 展示:支持 settings 中的自定义 Logo
|
||||||
|
- [x] 实现搜索按钮:调用 SearchWidget.open()
|
||||||
|
- [x] 实现深色模式切换按钮
|
||||||
|
- [x] 实现柔和 hover 动效
|
||||||
|
|
||||||
|
- [x] Task 6: 实现移动端导航
|
||||||
|
- [x] 创建 MobileMenu.vue 组件(Vue Island):汉堡菜单按钮、全屏/抽屉式导航、平滑动画
|
||||||
|
- [x] 移动端导航栏适配:品牌感、沉浸式体验
|
||||||
|
- [x] 触摸友好的交互设计
|
||||||
|
|
||||||
|
- [x] Task 7: 重写 Footer.astro 页脚
|
||||||
|
- [x] 实现 WarmIsland 风格页脚:品牌信息、版权、社交链接
|
||||||
|
- [x] 支持 settings 中的页脚内容配置
|
||||||
|
- [x] 包含 `<halo:footer />` 注入点
|
||||||
|
|
||||||
|
## Phase 3: 首页模块化系统
|
||||||
|
|
||||||
|
- [x] Task 8: 实现 Hero 首屏模块
|
||||||
|
- [x] 创建 HeroSection.astro 组件
|
||||||
|
- [x] 实现超大品牌标题 + 情绪化副标题文案
|
||||||
|
- [x] 实现岛屿氛围背景:柔和光斑(CSS radial-gradient 动画)、模糊层次
|
||||||
|
- [x] 实现呼吸动画:光斑缓慢脉动
|
||||||
|
- [x] 实现 CTA 按钮:高级圆角、柔和阴影、hover 微交互
|
||||||
|
- [x] 实现页面滚动引导指示器
|
||||||
|
- [x] 支持 settings 中的 Hero 配置
|
||||||
|
|
||||||
|
- [x] Task 9: 实现文章卡片组件
|
||||||
|
- [x] 创建 PostCard.astro 组件:大封面图、柔和阴影、半透明层次、高级圆角
|
||||||
|
- [x] 实现 hover 微浮动效果:translateY + 阴影加深 + 封面图轻微 scale
|
||||||
|
- [x] 实现情绪化摘要展示
|
||||||
|
- [x] 使用 thumbnail.gen() 实现响应式图片
|
||||||
|
|
||||||
|
- [x] Task 10: 实现首页 Featured 与 Latest 模块
|
||||||
|
- [x] 创建 FeaturedSection.astro:置顶文章大图展示
|
||||||
|
- [x] 创建 LatestSection.astro:最新文章杂志化网格布局
|
||||||
|
- [x] 实现不规则高级布局:大图 + 小卡混排
|
||||||
|
- [x] 实现呼吸感留白与内容节奏感
|
||||||
|
|
||||||
|
- [x] Task 11: 实现首页辅助模块
|
||||||
|
- [x] 创建 MomentsSection.astro:瞬间模块(条件渲染,依赖 plugin-moments)
|
||||||
|
- [x] 创建 PhotosSection.astro:图库模块(条件渲染,依赖 plugin-photos)
|
||||||
|
- [x] 创建 FriendsSection.astro:友链模块(条件渲染,依赖 plugin-friends-new / plugin-links)
|
||||||
|
- [x] 创建 QuoteSection.astro:语录模块
|
||||||
|
- [x] 创建 TimelineSection.astro:时间线模块
|
||||||
|
- [x] 创建 MessageWallSection.astro:留言墙模块
|
||||||
|
|
||||||
|
- [x] Task 12: 重写首页 index.astro
|
||||||
|
- [x] 整合所有首页模块组件
|
||||||
|
- [x] 根据 settings 配置控制模块开启/关闭与排序
|
||||||
|
- [ ] 实现模块间过渡动画(未实现动态排序,模块顺序硬编码)
|
||||||
|
- [x] 实现分页导航
|
||||||
|
|
||||||
|
## Phase 4: 内容页面
|
||||||
|
|
||||||
|
- [x] Task 13: 重写文章详情页 post.astro
|
||||||
|
- [x] 实现文章头部:标题、发布日期、分类、标签、封面图
|
||||||
|
- [x] 实现正文排版:prose 样式、阅读舒适度优化、杂志排版感
|
||||||
|
- [x] 实现文章底部:上下篇导航、相关文章推荐
|
||||||
|
- [x] 集成评论组件:`<halo:comment>` + WarmIsland 风格美化
|
||||||
|
- [x] 实现页面渐入动画
|
||||||
|
|
||||||
|
- [x] Task 14: 重写独立页面 page.astro
|
||||||
|
- [x] 实现页面头部与正文排版
|
||||||
|
- [x] 集成评论组件
|
||||||
|
- [x] 实现留言板自定义模板 page_messageboard.astro
|
||||||
|
- [x] 在 theme.yaml customTemplates.page 中注册留言板模板
|
||||||
|
|
||||||
|
- [x] Task 15: 重写归档页 archives.astro
|
||||||
|
- [x] 实现时间线式归档布局
|
||||||
|
- [x] WarmIsland 风格的年份/月份分组
|
||||||
|
- [x] 分页导航
|
||||||
|
|
||||||
|
- [x] Task 16: 重写分类与标签页
|
||||||
|
- [x] 重写 categories.astro:WarmIsland 风格分类列表
|
||||||
|
- [x] 重写 category.astro:分类归档 + 文章列表
|
||||||
|
- [x] 重写 tags.astro:标签云 WarmIsland 风格
|
||||||
|
- [x] 重写 tag.astro:标签归档 + 文章列表
|
||||||
|
|
||||||
|
## Phase 5: 插件页面专属 UI
|
||||||
|
|
||||||
|
- [x] Task 17: 实现友链页面(plugin-links)
|
||||||
|
- [x] 创建 links.astro 页面模板
|
||||||
|
- [x] 实现友链卡片网格:毛玻璃效果、柔和阴影、hover 微交互
|
||||||
|
- [x] 条件渲染:`th:if="${pluginFinder.available('PluginLinks')}"`
|
||||||
|
|
||||||
|
- [x] Task 18: 实现图库页面(plugin-photos)
|
||||||
|
- [x] 创建 photos.astro 页面模板
|
||||||
|
- [x] 实现瀑布流 / 杂志化网格布局
|
||||||
|
- [x] 实现灯箱预览效果
|
||||||
|
- [x] 条件渲染:`th:if="${pluginFinder.available('PluginPhotos')}"`
|
||||||
|
|
||||||
|
- [x] Task 19: 实现瞬间页面(plugin-moments)
|
||||||
|
- [x] 创建 moments.astro 页面模板
|
||||||
|
- [x] 实现时间线 + 卡片形式展示
|
||||||
|
- [x] 情绪化排版、呼吸感留白
|
||||||
|
- [x] 条件渲染:`th:if="${pluginFinder.available('PluginMoments')}"`
|
||||||
|
|
||||||
|
- [x] Task 20: 实现朋友圈页面(plugin-friends-new)
|
||||||
|
- [x] 创建 friends.astro 页面模板
|
||||||
|
- [x] 实现 WarmIsland 风格卡片流
|
||||||
|
- [x] 条件渲染:`th:if="${pluginFinder.available('PluginFriendsNew')}"`
|
||||||
|
|
||||||
|
- [x] Task 21: 美化评论组件(plugin-comment-widget)
|
||||||
|
- [x] 创建 comment-style.scss:评论区整体氛围美化
|
||||||
|
- [x] 评论卡片样式:毛玻璃层次、半透明背景、柔和阴影
|
||||||
|
- [x] hover 微交互
|
||||||
|
- [x] 深色模式适配
|
||||||
|
- [x] 保留插件默认评论输入框结构,不破坏功能逻辑
|
||||||
|
|
||||||
|
- [x] Task 22: 美化搜索组件(plugin-search-widget)
|
||||||
|
- [x] 创建 SearchOverlay.vue 组件(Vue Island)
|
||||||
|
- [x] 实现 Spotlight/Raycast 风格搜索弹层:毛玻璃、模糊背景、平滑动画
|
||||||
|
- [x] 实现快捷键呼出(Cmd/Ctrl + K)
|
||||||
|
- [x] 情绪化搜索结果展示
|
||||||
|
- [x] 条件渲染:`th:if="${pluginFinder.available('PluginSearchWidget')}"`
|
||||||
|
|
||||||
|
## Phase 6: 动效、深色模式与收尾
|
||||||
|
|
||||||
|
- [x] Task 23: 实现全局动效系统
|
||||||
|
- [x] 创建 Animations.vue(Vue Island)或纯 CSS 动画方案
|
||||||
|
- [x] 实现页面滚动渐入效果(Intersection Observer)
|
||||||
|
- [x] 实现光感移动效果(鼠标跟随光斑)
|
||||||
|
- [ ] 实现页面过渡动画(未实现)
|
||||||
|
- [x] 支持动效开关(settings.animation 配置)
|
||||||
|
|
||||||
|
- [x] Task 24: 完善深色模式
|
||||||
|
- [x] 确保所有组件深色模式适配
|
||||||
|
- [x] 评论区深色模式适配
|
||||||
|
- [x] 搜索组件深色模式适配
|
||||||
|
- [x] 插件页面深色模式适配
|
||||||
|
- [x] 设置 `data-color-scheme` 属性供官方插件适配
|
||||||
|
|
||||||
|
- [x] Task 25: 更新 Thymeleaf fragments 与资源
|
||||||
|
- [x] 更新 public/fragments/post-list.html 为杂志化卡片布局
|
||||||
|
- [x] 确保所有静态资源路径正确
|
||||||
|
- [x] 添加 error 页面模板(404、500 等)
|
||||||
|
|
||||||
|
- [ ] Task 26: 构建验证与最终调整
|
||||||
|
- [ ] 执行 `pnpm build` 确保构建成功
|
||||||
|
- [ ] 检查所有页面模板输出正确
|
||||||
|
- [ ] 检查 settings.yaml 在 Halo Console 中正确渲染
|
||||||
|
- [ ] 检查移动端适配
|
||||||
|
- [ ] 检查深色模式切换
|
||||||
|
- [ ] 检查插件条件渲染
|
||||||
|
|
||||||
|
# Task Dependencies
|
||||||
|
|
||||||
|
- [Task 2] depends on [Task 1] (SCSS 架构需要项目配置就绪)
|
||||||
|
- [Task 3] depends on [Task 1] (settings.yaml 需要 theme.yaml 中的 settingName)
|
||||||
|
- [Task 4] depends on [Task 2] (Layout 需要设计系统)
|
||||||
|
- [Task 5] depends on [Task 4] (导航栏需要 Layout)
|
||||||
|
- [Task 6] depends on [Task 5] (移动端导航需要桌面导航)
|
||||||
|
- [Task 7] depends on [Task 4] (页脚需要 Layout)
|
||||||
|
- [Task 8] depends on [Task 4] (Hero 需要 Layout)
|
||||||
|
- [Task 9] depends on [Task 2] (卡片需要设计系统)
|
||||||
|
- [Task 10] depends on [Task 9] (Featured/Latest 需要卡片组件)
|
||||||
|
- [Task 11] depends on [Task 4] (辅助模块需要 Layout)
|
||||||
|
- [Task 12] depends on [Task 8, Task 10, Task 11] (首页整合所有模块)
|
||||||
|
- [Task 13] depends on [Task 4, Task 9] (文章页需要 Layout 和卡片)
|
||||||
|
- [Task 14] depends on [Task 4] (独立页面需要 Layout)
|
||||||
|
- [Task 15] depends on [Task 4] (归档页需要 Layout)
|
||||||
|
- [Task 16] depends on [Task 4] (分类标签页需要 Layout)
|
||||||
|
- [Task 17-22] depends on [Task 4] (插件页面需要 Layout)
|
||||||
|
- [Task 23] depends on [Task 12] (全局动效需要首页完成)
|
||||||
|
- [Task 24] depends on [Task 12, Task 13] (深色模式需要核心页面完成)
|
||||||
|
- [Task 25] depends on [Task 12] (fragments 更新需要首页完成)
|
||||||
|
- [Task 26] depends on [all previous tasks]
|
||||||
|
|
||||||
|
# Parallelizable Work
|
||||||
|
|
||||||
|
- Task 3 (settings.yaml) 可与 Task 2 (SCSS 架构) 并行
|
||||||
|
- Task 5 (导航栏) 与 Task 7 (页脚) 与 Task 8 (Hero) 可并行
|
||||||
|
- Task 9 (文章卡片) 可与 Task 8 (Hero) 并行
|
||||||
|
- Task 13-16 (内容页面) 可并行
|
||||||
|
- Task 17-22 (插件页面) 可并行
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
- [x] moments.astro 页面使用 `momentFinder.list(1, 50)` 替代 `momentFinder.list()`
|
||||||
|
- [x] moments.astro 页面使用 `moment.spec.content.medium` 替代 `moment.spec.media`
|
||||||
|
- [x] MomentsSection.astro 组件使用 `momentFinder.list(1, limit)` 替代 `momentFinder.list()`
|
||||||
|
- [x] photos.astro 页面使用 `photoFinder.groupBy()` 替代 `photoFinder.listGroups()` + `listByGroupName()`
|
||||||
|
- [x] PhotosSection.astro 组件使用 `photoFinder.groupBy()` 替代 `photoFinder.listGroups()` + `listByGroupName()`
|
||||||
|
- [x] TimelineSection.astro 不使用 Groovy 闭包语法 `.groupBy { ... }`
|
||||||
|
- [x] Navbar.astro 使用 `menuItem.spec.target` 替代 `menuItem.spec.target?.value`
|
||||||
|
- [x] Header.astro 使用 `menuItem.spec.target` 替代 `menuItem.spec.target?.value`
|
||||||
|
- [x] 构建成功(pnpm build 无错误)
|
||||||
|
- [x] 部署后访问 / 首页无白屏
|
||||||
|
- [x] 部署后访问 /photos 无白屏
|
||||||
|
- [x] 部署后访问 /moments 无白屏
|
||||||
|
- [x] 控制台无 ERR_INCOMPLETE_CHUNKED_ENCODING 错误
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# WarmIsland 主题白屏及 API 错误彻底修复 Spec
|
||||||
|
|
||||||
|
## Why
|
||||||
|
主题中使用了 Halo 2.24.2 不存在的 Finder API 方法(`momentFinder.list()`、`photoFinder.listGroups()`、`photoFinder.listByGroupName()`),以及 Thymeleaf 不支持的 Groovy 闭包语法(`.groupBy { ... }`),导致模板渲染时抛出 `SpelEvaluationException`,HTTP 响应流中断,产生 `ERR_INCOMPLETE_CHUNKED_ENCODING 200` 白屏错误。同时 `MomentSpec` 中不存在 `media` 字段,媒体数据实际在 `content.medium` 中。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
- **修复 `momentFinder.list()` 调用**:改为 `momentFinder.list(1, 50)`(返回 `Mono<ListResult<MomentVo>>`),需通过 `.items` 获取列表
|
||||||
|
- **修复 `photoFinder.listGroups()` 调用**:改为 `photoFinder.groupBy()`(返回 `Flux<PhotoGroupVo>`),`PhotoGroupVo` 已包含 `photos` 列表
|
||||||
|
- **修复 `photoFinder.listByGroupName()` 调用**:改为 `photoFinder.listBy(groupName)`(返回 `Flux<PhotoVo>`),或直接使用 `groupBy()` 返回的 `PhotoGroupVo.photos`
|
||||||
|
- **修复 `moment.spec.media` 引用**:改为 `moment.spec.content.medium`(`MomentContent.medium` 是 `List<MomentMedia>`)
|
||||||
|
- **修复 `TimelineSection.astro` 中的 Groovy 闭包语法**:`.groupBy { it.spec.publishTime?.getYear() }` 在 Thymeleaf 中不可用,需改用 `postFinder.list({page: 1, size: 50})` 获取文章后手动按年分组
|
||||||
|
- **修复 `Navbar.astro` 和 `Header.astro` 中的 `menuItem.spec.target?.value`**:改为 `menuItem.spec.target`(`target` 是字符串而非对象)
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
- Affected code:
|
||||||
|
- `src/pages/moments.astro` - 瞬间页面(白屏根因)
|
||||||
|
- `src/pages/photos.astro` - 图库页面(白屏根因)
|
||||||
|
- `src/components/MomentsSection.astro` - 首页瞬间区块(首页白屏根因)
|
||||||
|
- `src/components/PhotosSection.astro` - 首页图库区块
|
||||||
|
- `src/components/TimelineSection.astro` - 首页时间线区块
|
||||||
|
- `src/components/Navbar.astro` - 导航栏 target 属性
|
||||||
|
- `src/components/Header.astro` - 头部导航 target 属性
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 正确使用 momentFinder API
|
||||||
|
系统 SHALL 使用 `momentFinder.list(page, size)` 替代不存在的 `momentFinder.list()`,返回 `ListResult` 对象需通过 `.items` 获取列表数据。
|
||||||
|
|
||||||
|
#### Scenario: 瞬间页面正常渲染
|
||||||
|
- **WHEN** 用户访问 `/moments`
|
||||||
|
- **THEN** 页面正常显示瞬间列表,无 `ERR_INCOMPLETE_CHUNKED_ENCODING` 错误
|
||||||
|
|
||||||
|
#### Scenario: 首页瞬间区块正常渲染
|
||||||
|
- **WHEN** 用户访问首页且启用了瞬间区块
|
||||||
|
- **THEN** 首页正常显示,瞬间区块展示最近的瞬间
|
||||||
|
|
||||||
|
### Requirement: 正确使用 photoFinder API
|
||||||
|
系统 SHALL 使用 `photoFinder.groupBy()` 替代不存在的 `photoFinder.listGroups()`,`PhotoGroupVo` 已包含 `photos` 列表,无需额外调用 `listByGroupName`。
|
||||||
|
|
||||||
|
#### Scenario: 图库页面正常渲染
|
||||||
|
- **WHEN** 用户访问 `/photos`
|
||||||
|
- **THEN** 页面正常显示图库分组和照片,无白屏错误
|
||||||
|
|
||||||
|
### Requirement: 正确引用 Moment 媒体数据
|
||||||
|
系统 SHALL 使用 `moment.spec.content.medium` 替代不存在的 `moment.spec.media`,`MomentMedia` 对象包含 `type`、`url`、`originType` 字段。
|
||||||
|
|
||||||
|
#### Scenario: 瞬间包含媒体时正常显示
|
||||||
|
- **WHEN** 瞬间包含图片媒体
|
||||||
|
- **THEN** 图片正常显示在瞬间卡片中
|
||||||
|
|
||||||
|
### Requirement: 不使用 Thymeleaf 不支持的语法
|
||||||
|
系统 SHALL 不在 Thymeleaf 表达式中使用 Groovy 闭包语法(如 `.groupBy { ... }`),TimelineSection 需改用 `postFinder.list({...})` 获取文章列表。
|
||||||
|
|
||||||
|
#### Scenario: 首页时间线区块正常渲染
|
||||||
|
- **WHEN** 用户访问首页且启用了时间线区块
|
||||||
|
- **THEN** 首页正常显示,时间线区块按年展示文章
|
||||||
|
|
||||||
|
### Requirement: 正确引用菜单项 target 属性
|
||||||
|
系统 SHALL 使用 `menuItem.spec.target` 替代 `menuItem.spec.target?.value`,`target` 是字符串类型。
|
||||||
|
|
||||||
|
#### Scenario: 导航链接在新标签页打开
|
||||||
|
- **WHEN** 菜单项配置了在新标签页打开
|
||||||
|
- **THEN** 链接正确设置 target 属性
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Tasks
|
||||||
|
|
||||||
|
- [x] Task 1: 修复 moments.astro 页面 - 替换 `momentFinder.list()` 为 `momentFinder.list(1, 50)`,修复 `moment.spec.media` 为 `moment.spec.content.medium`
|
||||||
|
- [x] 将 `th:with="moments = ${momentFinder.list()}"` 改为 `th:with="momentsResult = ${momentFinder.list(1, 50)}"`
|
||||||
|
- [x] 将 `th:each="moment : ${moments}"` 改为 `th:each="moment : ${momentsResult.items}"`
|
||||||
|
- [x] 将 `th:if="${moments != null and not #lists.isEmpty(moments)}"` 改为 `th:if="${momentsResult != null and not #lists.isEmpty(momentsResult.items)}"`
|
||||||
|
- [x] 将 `moment.spec?.media` 改为 `moment.spec.content.medium`
|
||||||
|
- [x] 将 `moment.spec.media` 改为 `moment.spec.content.medium`
|
||||||
|
- [x] 将 `media.type == 'PHOTO'` 保持不变(MomentMediaType.PHOTO 对应字符串 'PHOTO')
|
||||||
|
- [x] 将 `media.url` 保持不变
|
||||||
|
- [x] 将 `media.displayName ?: ''` 改为 `''`(MomentMedia 没有 displayName 字段)
|
||||||
|
|
||||||
|
- [x] Task 2: 修复 MomentsSection.astro 组件 - 替换 `momentFinder.list()` 为 `momentFinder.list(1, limit)`
|
||||||
|
- [x] 将 `th:with="moments = ${momentFinder != null ? momentFinder.list() : null}, limit = ..."` 改为 `th:with="momentsResult = ${momentFinder.list(1, limit)}"`
|
||||||
|
- [x] 将 `th:each="moment, stat : ${moments}"` 改为 `th:each="moment : ${momentsResult.items}"`
|
||||||
|
- [x] 移除 `th:if="${stat.index < limit}"` 限制(已通过 list 的 size 参数限制)
|
||||||
|
- [x] 修复 `moment.spec?.content?.html ?: moment.spec?.content?.raw ?: moment.spec?.content` 保持不变(正确)
|
||||||
|
- [x] 修复 `moment.spec?.releaseTime` 保持不变(正确)
|
||||||
|
|
||||||
|
- [x] Task 3: 修复 photos.astro 页面 - 替换 `photoFinder.listGroups()` + `listByGroupName()` 为 `photoFinder.groupBy()`
|
||||||
|
- [x] 将 `th:with="groups = ${photoFinder.listGroups()}"` 改为使用 `photoFinder.groupBy()`
|
||||||
|
- [x] 使用 `th:each="group : ${photoFinder.groupBy()}"` 遍历分组
|
||||||
|
- [x] `group` 是 `PhotoGroupVo`,包含 `metadata`、`spec`、`status`、`photos` 字段
|
||||||
|
- [x] 移除 `th:with="photos = ${photoFinder.listByGroupName(group.metadata?.name)}"` 内部调用
|
||||||
|
- [x] 直接使用 `group.photos` 遍历照片
|
||||||
|
- [x] 照片字段:`photo.spec.url`、`photo.spec.displayName`、`photo.spec.description`
|
||||||
|
|
||||||
|
- [x] Task 4: 修复 PhotosSection.astro 组件 - 替换 `photoFinder.listGroups()` + `listByGroupName()` 为 `photoFinder.groupBy()`
|
||||||
|
- [x] 将 `th:with="groups = ${photoFinder != null ? photoFinder.listGroups() : null}"` 改为使用 `photoFinder.groupBy()`
|
||||||
|
- [x] 使用 `th:each="group : ${photoFinder.groupBy()}"` 获取第一个分组
|
||||||
|
- [x] 使用 `group.photos` 获取照片列表
|
||||||
|
- [x] 限制显示数量使用 `th:each="photo, stat : ${group.photos}" th:if="${stat.index < limit}"`
|
||||||
|
|
||||||
|
- [x] Task 5: 修复 TimelineSection.astro - 移除 Groovy 闭包语法
|
||||||
|
- [x] 将 `${postFinder.listAll().groupBy { it.spec.publishTime?.getYear() }}` 改为 `${postFinder.list({page: 1, size: 50})}`
|
||||||
|
- [x] 使用 `th:each="post : ${posts.items}"` 遍历文章
|
||||||
|
- [x] 按年份分组改用 Thymeleaf 的方式:先获取所有文章,再在模板中按年分组展示
|
||||||
|
|
||||||
|
- [x] Task 6: 修复 Navbar.astro 和 Header.astro 中的 `menuItem.spec.target?.value`
|
||||||
|
- [x] 将 `th:target="${menuItem.spec.target?.value}"` 改为 `th:target="${menuItem.spec.target}"`
|
||||||
|
|
||||||
|
- [x] Task 7: 构建并部署验证
|
||||||
|
- [x] 执行 `pnpm build`
|
||||||
|
- [x] 部署到 Docker 容器
|
||||||
|
- [x] 重启 Halo 容器
|
||||||
|
- [x] 通过浏览器访问所有页面验证无白屏
|
||||||
|
|
||||||
|
# Task Dependencies
|
||||||
|
- [Task 7] depends on [Task 1, Task 2, Task 3, Task 4, Task 5, Task 6]
|
||||||
|
- [Task 1] and [Task 2] can be parallelized
|
||||||
|
- [Task 3] and [Task 4] can be parallelized
|
||||||
@@ -1,121 +1,73 @@
|
|||||||
# Theme Astro Starter
|
# WarmIsland 暖屿
|
||||||
|
|
||||||
面向 [Halo](https://www.halo.run/) 的主题脚手架:以 **Astro** 作为预渲染框架,将组件与页面编译为干净的 HTML,交由 **Thymeleaf** 在运行时完成数据渲染。
|
一座深夜里温暖、安静、治愈的小岛 — 具有独特气质的生活博客主题。
|
||||||
|
|
||||||
官方主题开发指南:<https://docs.halo.run/developer-guide/theme/prepare>
|
> 本项目由AI开发完成,人工仅进行部分功能测试及问题找寻
|
||||||
|
|
||||||
## 为什么选择 Astro?
|
## 已适配插件
|
||||||
|
|
||||||
Halo 主题采用 Thymeleaf 纯后端渲染方案,传统上只能在 HTML 文件里直接写 Thymeleaf 语法。这对简单主题足够用,但若需要 Vue / React 组件或复杂的前端交互,就会很局限。
|
| 插件名称 | 插件标识 | 适配页面路由 | 说明 |
|
||||||
|
|----------|---------|-------------|------|
|
||||||
|
| [瞬间](https://halo.run/store/apps/app-SnwWD) | `PluginMoments` | `/moments` | 瞬间动态页面,支持点赞、评论、标签筛选 |
|
||||||
|
| [图库](https://halo.run/store/apps/app-BmQJW) | `PluginPhotos` | `/photos` | 图片展示页面,支持分组、灯箱预览 |
|
||||||
|
| [友情链接](https://halo.run/store/apps/app-hqbe) | `PluginLinks` | `/links` | 友链展示页面,支持分组展示 |
|
||||||
|
| [朋友圈](https://github.com/halo-sigs/plugin-friends) | `plugin-friends` | `/friends` | RSS 订阅朋友圈页面 |
|
||||||
|
| [我的装备](https://github.com/halo-sigs/plugin-equipment) | `equipment` | `/equipment` | 装备展示页面,支持分组展示 |
|
||||||
|
|
||||||
Astro 的特点恰好弥补了这一短板:
|
> **注意**:插件需要单独安装,安装后主题会自动适配对应页面。若未安装某插件,对应页面会显示提示信息。
|
||||||
|
|
||||||
- **输出干净的 HTML**:Astro 不同于 SPA 框架,其默认产物是纯 HTML 文件,而不是 JS 包。Thymeleaf 的属性(`th:text`、`th:if`、`th:replace` 等)会作为普通 HTML 属性原样保留,两者互不干扰。
|
## 安装
|
||||||
- **Island 架构**:仅在真正需要交互的地方注入客户端 JS(Vue / React 组件),其余部分零 JS 开销。
|
|
||||||
- **组件化开发体验**:通过 Astro 组件的 `slot` 与 `props` 在 **编译阶段** 组织布局与复用,最终产物是 Thymeleaf 可直接使用的 HTML 模板。
|
|
||||||
|
|
||||||
## 何时选择此 Starter?
|
1. 下载主题最新 Release 的 ZIP 包
|
||||||
|
2. 进入 Halo 后台 → 主题管理 → 安装主题 → 上传 ZIP 包
|
||||||
|
3. 安装完成后点击「启用」
|
||||||
|
|
||||||
| 场景 | 推荐 |
|
或从源码构建:
|
||||||
| ------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
||||||
| 主题有较多前端交互,希望用 Vue / React 编写 Island 组件 | **本 Starter** |
|
|
||||||
| 主题以静态展示为主,前端交互需求较少 | [halo-dev/theme-vite-starter](https://github.com/halo-dev/theme-vite-starter) |
|
|
||||||
|
|
||||||
## 技术栈
|
```bash
|
||||||
|
git clone https://github.com/warm-island/theme-warm-island.git
|
||||||
| 类别 | 说明 |
|
cd theme-warm-island
|
||||||
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
npm install
|
||||||
| 运行时 | Halo 使用 **Thymeleaf** 渲染主题;模板变量与 Finder API 随 Halo 版本演进,请以[官方文档](https://docs.halo.run/developer-guide/theme/prepare)为准 |
|
npm run build
|
||||||
| 预渲染 | **Astro**,将 `src/` 下的页面与组件编译为 `templates/` 下的纯 HTML |
|
|
||||||
| UI 框架 | **Vue 3**(通过 `@astrojs/vue` 集成,用于编写 Island 组件) |
|
|
||||||
| 语言 | TypeScript(Astro 组件 frontmatter 支持) |
|
|
||||||
| 包管理 | **pnpm** |
|
|
||||||
|
|
||||||
## 核心概念:Astro 编译期 vs Thymeleaf 运行期
|
|
||||||
|
|
||||||
理解这两个阶段的边界是使用本 Starter 的关键:
|
|
||||||
|
|
||||||
```
|
|
||||||
开发时 构建后 Halo 运行时
|
|
||||||
───────────────────────────── ────────────────────── ──────────────────────────
|
|
||||||
src/pages/index.astro → templates/index.html → Thymeleaf 注入数据渲染
|
|
||||||
Astro 组件 / slot / props 干净的 HTML + th:text / th:if 生效
|
|
||||||
Vue Island 组件 Thymeleaf 属性原样 客户端 JS 激活 Island
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**重要限制**:
|
构建产物在 `templates/` 目录下,将其复制到 Halo 主题目录即可。
|
||||||
|
|
||||||
- `slot`、`props` 是 Astro **编译期**的概念,Thymeleaf 运行时感知不到它们,也无法向 Astro 组件传递动态数据。
|
## lightgallery.js 灯箱插件集成
|
||||||
- 在 Astro 组件中可以直接写 `th:text`、`th:if`、`th:replace` 等 Thymeleaf 属性——Astro 会将其视为普通 HTML 属性原样输出,Thymeleaf 在运行时才会处理它们。
|
|
||||||
- Vue / React Island 组件(`client:*`)的数据来源是客户端(例如调用 Halo API),无法直接使用 Thymeleaf 的服务端变量。
|
|
||||||
|
|
||||||
## 目录结构
|
如果你使用 [lightgallery.js](https://www.lightgalleryjs.com/) 灯箱插件来为图片添加放大预览功能,以下是各页面的路径匹配规则和 DOM 节点选择器:
|
||||||
|
|
||||||
```
|
### 路径匹配与 DOM 节点
|
||||||
.
|
|
||||||
├── src/
|
|
||||||
│ ├── pages/ # Astro 页面(编译为 templates/ 下的 HTML)
|
|
||||||
│ │ ├── index.astro
|
|
||||||
│ │ ├── post.astro
|
|
||||||
│ │ ├── category.astro
|
|
||||||
│ │ └── ...
|
|
||||||
│ ├── layouts/
|
|
||||||
│ │ └── Layout.astro # 全局布局组件
|
|
||||||
│ ├── components/
|
|
||||||
│ │ ├── Header.astro
|
|
||||||
│ │ ├── Footer.astro
|
|
||||||
│ │ ├── ThemeSwitcher.vue # Vue Island 示例
|
|
||||||
│ │ └── ...
|
|
||||||
│ └── styles/ # 全局样式
|
|
||||||
├── public/ # 原样复制到 templates/:静态资源或纯 Thymeleaf 模板均可放此处
|
|
||||||
│ └── fragments/ # 纯 Thymeleaf 片段示例(不经 Astro 编译,直接输出)
|
|
||||||
├── templates/ # Astro 构建产物(Halo 实际读取的模板目录)
|
|
||||||
├── theme.yaml # 主题元数据(必填)
|
|
||||||
├── settings.yaml # 控制台主题设置表单(可选)
|
|
||||||
├── astro.config.mjs
|
|
||||||
└── package.json
|
|
||||||
```
|
|
||||||
|
|
||||||
> **`templates/` 是构建产物**,不要直接在此目录手动修改;请修改 `src/` 后重新构建。
|
| 页面 | 路径匹配规则 | 匹配区域 DOM 节点 |
|
||||||
|
|------|-------------|-------------------|
|
||||||
|
| 文章详情页 | `/archives/*` | `.wi-post__body` |
|
||||||
|
| 瞬间页 | `/moments` | `.wi-moments-page__content` |
|
||||||
|
| 图库页 | `/photos` | `.wi-photos-page__grid` |
|
||||||
|
| 自定义页面 | 用户自定义 | `.wi-page__body` |
|
||||||
|
|
||||||
|
> **注意**:自定义页面的路径匹配由用户自行设定,此处仅提供 DOM 节点选择器 `.wi-page__body`。
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/halo-sigs/theme-astro-starter.git ~/halo2-dev/themes/astro-starter
|
# 安装依赖
|
||||||
cd ~/halo2-dev/themes/astro-starter
|
npm install
|
||||||
pnpm install
|
|
||||||
pnpm dev
|
# 开发模式(监听文件变化)
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 构建
|
||||||
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
`pnpm dev` 会启动 Astro 开发服务器并监听 `src/` 变更,实时重新生成 `templates/`。
|
## 技术栈
|
||||||
|
|
||||||
将主题目录链接或复制到 Halo 的 `themes/astro-starter/` 后,在控制台安装并启用主题即可预览。建议同时关闭 Thymeleaf 缓存以便热更新调试:
|
- [Halo](https://halo.run) 2.24+
|
||||||
|
- [Astro](https://astro.build) + [vite-plugin-halo-theme](https://github.com/halo-sigs/vite-plugin-halo-theme)
|
||||||
|
- [Thymeleaf](https://www.thymeleaf.org) 模板引擎
|
||||||
|
- [Vue 3](https://vuejs.org) Islands 交互组件
|
||||||
|
|
||||||
```yaml
|
## 许可
|
||||||
# application.yaml
|
|
||||||
spring:
|
|
||||||
thymeleaf:
|
|
||||||
cache: false
|
|
||||||
```
|
|
||||||
|
|
||||||
或通过环境变量:`SPRING_THYMELEAF_CACHE=false`
|
[GPL-3.0](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
|
||||||
## 构建
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm build
|
|
||||||
```
|
|
||||||
|
|
||||||
执行 Astro 构建,将 `src/` 编译输出到 `templates/`。构建完成后可将主题目录(含 `templates/`、`theme.yaml`、`settings.yaml` 等)打包为 ZIP 上传到 Halo 控制台,或使用 [`@halo-dev/theme-package-cli`](https://github.com/halo-dev/theme-package-cli) 完成打包。
|
|
||||||
|
|
||||||
## 其他脚本
|
|
||||||
|
|
||||||
| 命令 | 作用 |
|
|
||||||
| ------------- | ----------------------- |
|
|
||||||
| `pnpm format` | Prettier 格式化所有文件 |
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
- Astro 配置中 `base` 需与 `theme.yaml` 的 `metadata.name` 保持一致(当前为 `astro-starter`),这决定了静态资源的引用路径。
|
|
||||||
- `public/` 目录不仅可以存放图片、字体等静态资源,也可以存放**纯 Thymeleaf 模板**(例如 `public/fragments/post-list.html`)。这些文件会被 Astro 原样复制到 `templates/` 对应路径,不经过任何编译处理,适合编写不需要 Astro 组件化的 Thymeleaf 片段。
|
|
||||||
- Vue Island 组件可通过 Halo 提供的 [API Client](https://www.npmjs.com/package/@halo-dev/api-client) 在客户端获取数据。
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
function apiCall(method, path, data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const options = {
|
||||||
|
hostname: 'localhost',
|
||||||
|
port: 8090,
|
||||||
|
path: path,
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = http.request(options, (res) => {
|
||||||
|
let body = '';
|
||||||
|
res.on('data', chunk => body += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const cookies = res.headers['set-cookie'];
|
||||||
|
resolve({ status: res.statusCode, data: JSON.parse(body), cookies });
|
||||||
|
} catch(e) {
|
||||||
|
resolve({ status: res.statusCode, data: body, cookies: res.headers['set-cookie'] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
if (data) req.write(JSON.stringify(data));
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Login first
|
||||||
|
const loginRes = await apiCall('POST', '/api/auth/signin', { username: 'admin', password: 'admin' });
|
||||||
|
console.log('Login status:', loginRes.status);
|
||||||
|
|
||||||
|
// Get the session cookie
|
||||||
|
const cookies = loginRes.cookies || [];
|
||||||
|
const cookieStr = cookies.map(c => c.split(';')[0]).join('; ');
|
||||||
|
console.log('Cookies:', cookieStr);
|
||||||
|
|
||||||
|
// Now get theme settings using the cookie
|
||||||
|
const http2 = require('http');
|
||||||
|
function apiWithCookie(method, path, data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const options = {
|
||||||
|
hostname: 'localhost',
|
||||||
|
port: 8090,
|
||||||
|
path: path,
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cookie': cookieStr
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const req = http2.request(options, (res) => {
|
||||||
|
let body = '';
|
||||||
|
res.on('data', chunk => body += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
resolve({ status: res.statusCode, data: JSON.parse(body) });
|
||||||
|
} catch(e) {
|
||||||
|
resolve({ status: res.statusCode, data: body });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
if (data) req.write(JSON.stringify(data));
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get activated theme
|
||||||
|
const themeRes = await apiWithCookie('GET', '/apis/api.console.halo.run/v1alpha1/themes?sort=creationTimestamp%2Cdesc');
|
||||||
|
console.log('Themes status:', themeRes.status);
|
||||||
|
|
||||||
|
if (themeRes.data && themeRes.data.items) {
|
||||||
|
const activeTheme = themeRes.data.items.find(t => t.spec && t.spec.activationTime);
|
||||||
|
if (activeTheme) {
|
||||||
|
console.log('Active theme:', activeTheme.metadata.name);
|
||||||
|
|
||||||
|
// Get theme settings
|
||||||
|
const settingsRes = await apiWithCookie('GET', `/apis/api.console.halo.run/v1alpha1/themes/${activeTheme.metadata.name}/setting`);
|
||||||
|
console.log('Settings status:', settingsRes.status);
|
||||||
|
console.log('Settings:', JSON.stringify(settingsRes.data).substring(0, 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
+8
-2
@@ -1,11 +1,10 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
import { defineConfig } from "astro/config";
|
import { defineConfig } from "astro/config";
|
||||||
|
|
||||||
import vue from "@astrojs/vue";
|
import vue from "@astrojs/vue";
|
||||||
import Icons from "unplugin-icons/vite";
|
import Icons from "unplugin-icons/vite";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: "/themes/astro-starter",
|
base: "/themes/warm-island",
|
||||||
build: {
|
build: {
|
||||||
assets: "assets",
|
assets: "assets",
|
||||||
format: "file",
|
format: "file",
|
||||||
@@ -18,5 +17,12 @@ export default defineConfig({
|
|||||||
compiler: "vue3",
|
compiler: "vue3",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
api: "modern-compiler",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
(function(){
|
||||||
|
var result = {};
|
||||||
|
var hero = document.querySelector(".hero");
|
||||||
|
if (hero) {
|
||||||
|
var h1 = hero.querySelector("h1");
|
||||||
|
var subtitle = hero.querySelector(".hero-subtitle, .subtitle, h2, p");
|
||||||
|
result.heroTitle = h1 ? h1.textContent.trim() : "no h1 found";
|
||||||
|
result.heroSubtitle = subtitle ? subtitle.textContent.trim() : "no subtitle found";
|
||||||
|
result.heroHTML = hero.innerHTML.substring(0, 500);
|
||||||
|
} else {
|
||||||
|
result.hero = "not found";
|
||||||
|
}
|
||||||
|
var footer = document.querySelector("footer, .footer, #footer");
|
||||||
|
if (footer) {
|
||||||
|
result.footerText = footer.textContent.trim().substring(0, 300);
|
||||||
|
result.footerHeight = getComputedStyle(footer).height;
|
||||||
|
result.footerMarginBottom = getComputedStyle(footer).marginBottom;
|
||||||
|
result.footerPaddingBottom = getComputedStyle(footer).paddingBottom;
|
||||||
|
} else {
|
||||||
|
result.footer = "not found";
|
||||||
|
}
|
||||||
|
var body = document.body;
|
||||||
|
result.bodyMarginBottom = getComputedStyle(body).marginBottom;
|
||||||
|
result.bodyPaddingBottom = getComputedStyle(body).paddingBottom;
|
||||||
|
result.htmlMarginBottom = getComputedStyle(document.documentElement).marginBottom;
|
||||||
|
result.documentHeight = document.documentElement.scrollHeight;
|
||||||
|
result.windowHeight = window.innerHeight;
|
||||||
|
return JSON.stringify(result);
|
||||||
|
})()
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
(function(){
|
||||||
|
return fetch('/apis/api.console.halo.run/v1alpha1/configmaps/warm-island-config')
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(data => data.substring(0, 3000));
|
||||||
|
})()
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
(function(){
|
||||||
|
return fetch('/apis/api.console.halo.run/v1alpha1/themes/warm-island/config')
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(data => data.substring(0, 2000));
|
||||||
|
})()
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
(function(){
|
||||||
|
var selects = document.querySelectorAll('select');
|
||||||
|
var result = [];
|
||||||
|
selects.forEach(function(s, i) {
|
||||||
|
result.push({index: i, value: s.value, name: s.name, options: Array.from(s.options).map(function(o){return o.value + ':' + o.text})});
|
||||||
|
});
|
||||||
|
var inputs = document.querySelectorAll('input');
|
||||||
|
var inputResult = [];
|
||||||
|
inputs.forEach(function(inp, i) {
|
||||||
|
if (inp.type !== 'hidden') {
|
||||||
|
inputResult.push({index: i, type: inp.type, name: inp.name, value: inp.value});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return JSON.stringify({selects: result, inputs: inputResult.slice(0, 10)});
|
||||||
|
})()
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
(function(){
|
||||||
|
var header = document.querySelector('header');
|
||||||
|
if (!header) return JSON.stringify({header: "not found"});
|
||||||
|
return JSON.stringify({
|
||||||
|
className: header.className,
|
||||||
|
innerHTML: header.innerHTML.substring(0, 500)
|
||||||
|
});
|
||||||
|
})()
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
(function(){
|
||||||
|
var h = document.querySelector(".hero");
|
||||||
|
if (!h) return "No .hero element found";
|
||||||
|
var s = getComputedStyle(h);
|
||||||
|
return JSON.stringify({
|
||||||
|
width: s.width,
|
||||||
|
marginLeft: s.marginLeft,
|
||||||
|
marginTop: s.marginTop,
|
||||||
|
paddingTop: s.paddingTop,
|
||||||
|
left: s.left,
|
||||||
|
position: s.position
|
||||||
|
});
|
||||||
|
})()
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
(function(){
|
||||||
|
var likeBtn = document.querySelector('.wi-like-btn');
|
||||||
|
if (!likeBtn) return JSON.stringify({likeBtn: "not found"});
|
||||||
|
var likeContainer = likeBtn.closest('.wi-post__like');
|
||||||
|
return JSON.stringify({
|
||||||
|
btnClass: likeBtn.className,
|
||||||
|
btnDisabled: likeBtn.disabled,
|
||||||
|
containerClass: likeContainer ? likeContainer.className : "none",
|
||||||
|
text: likeContainer ? likeContainer.textContent.trim() : likeBtn.textContent.trim(),
|
||||||
|
liked: likeBtn.classList.contains('wi-like-btn--liked') || (likeContainer && likeContainer.classList.contains('wi-post__like--liked')),
|
||||||
|
svgFill: likeBtn.querySelector('svg') ? getComputedStyle(likeBtn.querySelector('svg')).fill : "none"
|
||||||
|
});
|
||||||
|
})()
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
(function(){
|
||||||
|
var likeBtn = document.querySelector('.like-btn, .wi-like, [class*="like"], [class*="heart"]');
|
||||||
|
if (!likeBtn) {
|
||||||
|
var buttons = document.querySelectorAll('button');
|
||||||
|
var result = [];
|
||||||
|
buttons.forEach(function(b, i) {
|
||||||
|
result.push({index: i, text: b.textContent.trim(), className: b.className, innerHTML: b.innerHTML.substring(0, 200)});
|
||||||
|
});
|
||||||
|
return JSON.stringify({likeBtn: "not found", buttons: result});
|
||||||
|
}
|
||||||
|
return JSON.stringify({
|
||||||
|
className: likeBtn.className,
|
||||||
|
text: likeBtn.textContent.trim(),
|
||||||
|
innerHTML: likeBtn.innerHTML.substring(0, 300),
|
||||||
|
ariaLabel: likeBtn.getAttribute('aria-label')
|
||||||
|
});
|
||||||
|
})()
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
(function(){
|
||||||
|
return fetch('/apis/api.console.halo.run/v1alpha1/themes/warm-island')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => JSON.stringify({
|
||||||
|
settingName: data.spec.settingName,
|
||||||
|
configMapName: data.spec.configMapName
|
||||||
|
}));
|
||||||
|
})()
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
(function(){
|
||||||
|
var nav = document.querySelector("nav, .navbar, .nav, header nav, .header-nav");
|
||||||
|
if (!nav) return JSON.stringify({nav: "not found"});
|
||||||
|
var s = getComputedStyle(nav);
|
||||||
|
var parent = nav.parentElement;
|
||||||
|
var ps = parent ? getComputedStyle(parent) : null;
|
||||||
|
return JSON.stringify({
|
||||||
|
navClass: nav.className,
|
||||||
|
navWidth: s.width,
|
||||||
|
navMargin: s.margin,
|
||||||
|
navBorderRadius: s.borderRadius,
|
||||||
|
navBorder: s.border,
|
||||||
|
navPadding: s.padding,
|
||||||
|
parentClass: parent ? parent.className : "none",
|
||||||
|
parentWidth: ps ? ps.width : "none",
|
||||||
|
parentBorderRadius: ps ? ps.borderRadius : "none"
|
||||||
|
});
|
||||||
|
})()
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
(function(){
|
||||||
|
var navbar = document.querySelector('.wi-navbar');
|
||||||
|
if (!navbar) return JSON.stringify({navbar: "not found"});
|
||||||
|
return JSON.stringify({
|
||||||
|
className: navbar.className,
|
||||||
|
allClasses: navbar.className
|
||||||
|
});
|
||||||
|
})()
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
(function(){
|
||||||
|
return fetch('/apis/api.console.halo.run/v1alpha1/themes/warm-island')
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(data => data.substring(0, 2000));
|
||||||
|
})()
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
(function(){
|
||||||
|
return fetch('/apis/api.console.halo.run/v1alpha1/themes/warm-island/setting')
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(data => data.substring(0, 1000));
|
||||||
|
})()
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
(function(){
|
||||||
|
var result = {};
|
||||||
|
var h1 = document.querySelector('h1');
|
||||||
|
var content = document.querySelector('.wi-post__content, .post-content, article, .content');
|
||||||
|
if (h1 && content) {
|
||||||
|
var h1Rect = h1.getBoundingClientRect();
|
||||||
|
var contentRect = content.getBoundingClientRect();
|
||||||
|
result.titleBottom = h1Rect.bottom;
|
||||||
|
result.contentTop = contentRect.top;
|
||||||
|
result.gap = contentRect.top - h1Rect.bottom;
|
||||||
|
} else {
|
||||||
|
result.h1Found = !!h1;
|
||||||
|
result.contentFound = !!content;
|
||||||
|
if (h1) {
|
||||||
|
var nextEl = h1.nextElementSibling;
|
||||||
|
result.nextElTag = nextEl ? nextEl.tagName : "none";
|
||||||
|
result.nextElClass = nextEl ? nextEl.className : "none";
|
||||||
|
var h1Rect = h1.getBoundingClientRect();
|
||||||
|
if (nextEl) {
|
||||||
|
var nextRect = nextEl.getBoundingClientRect();
|
||||||
|
result.gap = nextRect.top - h1Rect.bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var footer = document.querySelector("footer, .footer, #footer");
|
||||||
|
if (footer) {
|
||||||
|
var footerRect = footer.getBoundingClientRect();
|
||||||
|
result.footerBottom = footerRect.bottom;
|
||||||
|
result.windowHeight = window.innerHeight;
|
||||||
|
result.spaceBelowFooter = window.innerHeight - footerRect.bottom;
|
||||||
|
result.docHeight = document.documentElement.scrollHeight;
|
||||||
|
}
|
||||||
|
return JSON.stringify(result);
|
||||||
|
})()
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
(function(){
|
||||||
|
return fetch('/apis/api.console.halo.run/v1alpha1/themes?sort=creationTimestamp%2Cdesc')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
var items = data.items || [];
|
||||||
|
var result = items.map(t => ({
|
||||||
|
name: t.metadata.name,
|
||||||
|
displayName: t.spec.displayName,
|
||||||
|
active: !!t.spec.activationTime
|
||||||
|
}));
|
||||||
|
return JSON.stringify(result);
|
||||||
|
});
|
||||||
|
})()
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
(function(){
|
||||||
|
return fetch('/apis/api.console.halo.run/v1alpha1/themes?sort=creationTimestamp%2Cdesc')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
var items = data.items || [];
|
||||||
|
var result = items.map(t => ({
|
||||||
|
name: t.metadata.name,
|
||||||
|
displayName: t.spec.displayName,
|
||||||
|
active: t.spec.activationTime,
|
||||||
|
allKeys: Object.keys(t.spec)
|
||||||
|
}));
|
||||||
|
return JSON.stringify(result);
|
||||||
|
});
|
||||||
|
})()
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
(function(){
|
||||||
|
var btn = document.querySelector('.wi-like-btn');
|
||||||
|
if (!btn) return JSON.stringify({error: "no btn"});
|
||||||
|
var origFetch = window.fetch;
|
||||||
|
var lastRequest = null;
|
||||||
|
window.fetch = function() {
|
||||||
|
lastRequest = {url: arguments[0], options: arguments[1]};
|
||||||
|
return origFetch.apply(this, arguments);
|
||||||
|
};
|
||||||
|
btn.click();
|
||||||
|
return new Promise(function(resolve){
|
||||||
|
setTimeout(function(){
|
||||||
|
resolve(JSON.stringify({
|
||||||
|
lastRequest: lastRequest,
|
||||||
|
btnClass: btn.className,
|
||||||
|
liked: btn.classList.contains('wi-like-btn--liked'),
|
||||||
|
text: btn.closest('.wi-post__like').textContent.trim()
|
||||||
|
}));
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
})()
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
(function(){
|
||||||
|
var btn = document.querySelector('.wi-like-btn');
|
||||||
|
if (!btn) return "no btn";
|
||||||
|
btn.click();
|
||||||
|
return new Promise(function(resolve){
|
||||||
|
setTimeout(function(){
|
||||||
|
var container = btn.closest('.wi-post__like');
|
||||||
|
resolve(JSON.stringify({
|
||||||
|
btnClass: btn.className,
|
||||||
|
btnDisabled: btn.disabled,
|
||||||
|
text: container ? container.textContent.trim() : btn.textContent.trim(),
|
||||||
|
liked: btn.classList.contains('wi-like-btn--liked')
|
||||||
|
}));
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
})()
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const js = fs.readFileSync('c:\\Users\\Zhang\\Documents\\Halo\\WarmIsland\\check-all.js', 'utf8');
|
||||||
|
console.log(Buffer.from(js).toString('base64'));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const js = fs.readFileSync('c:\\Users\\Zhang\\Documents\\Halo\\WarmIsland\\check-all.js', 'utf8');
|
||||||
|
console.log(Buffer.from(js).toString('base64'));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const js = fs.readFileSync('c:\\Users\\Zhang\\Documents\\Halo\\WarmIsland\\click-like.js', 'utf8');
|
||||||
|
console.log(Buffer.from(js).toString('base64'));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const js = fs.readFileSync('c:\\Users\\Zhang\\Documents\\Halo\\WarmIsland\\check-cm.js', 'utf8');
|
||||||
|
console.log(Buffer.from(js).toString('base64'));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const js = fs.readFileSync('c:\\Users\\Zhang\\Documents\\Halo\\WarmIsland\\check-config.js', 'utf8');
|
||||||
|
console.log(Buffer.from(js).toString('base64'));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const js = fs.readFileSync('c:\\Users\\Zhang\\Documents\\Halo\\WarmIsland\\check-form.js', 'utf8');
|
||||||
|
console.log(Buffer.from(js).toString('base64'));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const js = fs.readFileSync('c:\\Users\\Zhang\\Documents\\Halo\\WarmIsland\\check-header.js', 'utf8');
|
||||||
|
console.log(Buffer.from(js).toString('base64'));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const js = fs.readFileSync('c:\\Users\\Zhang\\Documents\\Halo\\WarmIsland\\check-like.js', 'utf8');
|
||||||
|
console.log(Buffer.from(js).toString('base64'));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const js = fs.readFileSync('c:\\Users\\Zhang\\Documents\\Halo\\WarmIsland\\test-like-api.js', 'utf8');
|
||||||
|
console.log(Buffer.from(js).toString('base64'));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const js = fs.readFileSync('c:\\Users\\Zhang\\Documents\\Halo\\WarmIsland\\click-like-debug.js', 'utf8');
|
||||||
|
console.log(Buffer.from(js).toString('base64'));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const js = fs.readFileSync('c:\\Users\\Zhang\\Documents\\Halo\\WarmIsland\\check-like-state.js', 'utf8');
|
||||||
|
console.log(Buffer.from(js).toString('base64'));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const js = fs.readFileSync('c:\\Users\\Zhang\\Documents\\Halo\\WarmIsland\\check-names.js', 'utf8');
|
||||||
|
console.log(Buffer.from(js).toString('base64'));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const js = fs.readFileSync('c:\\Users\\Zhang\\Documents\\Halo\\WarmIsland\\check-nav.js', 'utf8');
|
||||||
|
console.log(Buffer.from(js).toString('base64'));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const js = fs.readFileSync('c:\\Users\\Zhang\\Documents\\Halo\\WarmIsland\\check-navbar-class.js', 'utf8');
|
||||||
|
console.log(Buffer.from(js).toString('base64'));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const js = fs.readFileSync('c:\\Users\\Zhang\\Documents\\Halo\\WarmIsland\\check-raw.js', 'utf8');
|
||||||
|
console.log(Buffer.from(js).toString('base64'));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const js = fs.readFileSync('c:\\Users\\Zhang\\Documents\\Halo\\WarmIsland\\check-setting.js', 'utf8');
|
||||||
|
console.log(Buffer.from(js).toString('base64'));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const js = fs.readFileSync('c:\\Users\\Zhang\\Documents\\Halo\\WarmIsland\\check-spacing.js', 'utf8');
|
||||||
|
console.log(Buffer.from(js).toString('base64'));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const js = fs.readFileSync('c:\\Users\\Zhang\\Documents\\Halo\\WarmIsland\\check-theme.js', 'utf8');
|
||||||
|
console.log(Buffer.from(js).toString('base64'));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const js = fs.readFileSync('c:\\Users\\Zhang\\Documents\\Halo\\WarmIsland\\check-theme2.js', 'utf8');
|
||||||
|
console.log(Buffer.from(js).toString('base64'));
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
const js = `(function(){var h=document.querySelector(".hero");if(!h)return "No .hero element";var s=getComputedStyle(h);return JSON.stringify({width:s.width,marginLeft:s.marginLeft,marginTop:s.marginTop,paddingTop:s.paddingTop})})()`;
|
||||||
|
console.log(Buffer.from(js).toString('base64'));
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"cookies": [
|
||||||
|
{
|
||||||
|
"name": "language",
|
||||||
|
"value": "zh-CN",
|
||||||
|
"domain": "localhost",
|
||||||
|
"path": "/",
|
||||||
|
"expires": -1.0,
|
||||||
|
"size": 13,
|
||||||
|
"httpOnly": false,
|
||||||
|
"secure": false,
|
||||||
|
"session": true,
|
||||||
|
"sameSite": "Lax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "XSRF-TOKEN",
|
||||||
|
"value": "01a0a3fe-aad4-414d-bc42-3a173c9bc0ab",
|
||||||
|
"domain": "localhost",
|
||||||
|
"path": "/",
|
||||||
|
"expires": -1.0,
|
||||||
|
"size": 46,
|
||||||
|
"httpOnly": true,
|
||||||
|
"secure": false,
|
||||||
|
"session": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NID",
|
||||||
|
"value": "531=aZe-V6TNWj_kuxgiHTVkXjS6ptWqLuE0riVNGyXvxwnE9QdErkY4CznBbrF9lyQLZ9xDo0D8GSLbJHpi3JzQErmBm7pTVjoMnoOZUwYY3-9cwTfXq4tO0d8frwBCQFx7MHorS0Gf9zjpBLbpa0iQiht8sybQAlfsvHQWfyaBAz1dIW5jh9jHrb4ZREMKf5CCLFCBywI",
|
||||||
|
"domain": ".google.com",
|
||||||
|
"path": "/",
|
||||||
|
"expires": 1794719008.977387,
|
||||||
|
"size": 206,
|
||||||
|
"httpOnly": true,
|
||||||
|
"secure": false,
|
||||||
|
"session": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SESSION",
|
||||||
|
"value": "84b72204-3b7b-4594-908f-5b1a8a3db254",
|
||||||
|
"domain": "localhost",
|
||||||
|
"path": "/",
|
||||||
|
"expires": -1.0,
|
||||||
|
"size": 43,
|
||||||
|
"httpOnly": true,
|
||||||
|
"secure": false,
|
||||||
|
"session": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "device_id",
|
||||||
|
"value": "63a8d881b6144d6e98511d7e647257bc",
|
||||||
|
"domain": "localhost",
|
||||||
|
"path": "/",
|
||||||
|
"expires": 1787547843.038663,
|
||||||
|
"size": 41,
|
||||||
|
"httpOnly": true,
|
||||||
|
"secure": false,
|
||||||
|
"session": false,
|
||||||
|
"sameSite": "Lax"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"origins": []
|
||||||
|
}
|
||||||
+9
-1
@@ -5,7 +5,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon --config nodemon.json --exec 'astro build'",
|
"dev": "nodemon --config nodemon.json --exec 'astro build'",
|
||||||
"build": "astro build",
|
"build": "astro build && node -e \"const fs=require('fs');const src='templates/logo.png';const dst='templates/assets/logo.png';if(fs.existsSync(src)){fs.copyFileSync(src,dst)}\"",
|
||||||
"astro": "astro",
|
"astro": "astro",
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
@@ -22,6 +22,12 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"@parcel/watcher",
|
||||||
|
"sharp"
|
||||||
|
]
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/mdx": "^5.0.3",
|
"@astrojs/mdx": "^5.0.3",
|
||||||
"@astrojs/rss": "^4.0.18",
|
"@astrojs/rss": "^4.0.18",
|
||||||
@@ -29,6 +35,7 @@
|
|||||||
"@halo-dev/api-client": "^2.23.0",
|
"@halo-dev/api-client": "^2.23.0",
|
||||||
"astro": "^6.1.4",
|
"astro": "^6.1.4",
|
||||||
"ky": "^2.0.0",
|
"ky": "^2.0.0",
|
||||||
|
"lightgallery": "^2.9.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"vue": "^3.5.32"
|
"vue": "^3.5.32"
|
||||||
},
|
},
|
||||||
@@ -37,6 +44,7 @@
|
|||||||
"nodemon": "^3.1.14",
|
"nodemon": "^3.1.14",
|
||||||
"prettier": "3.8.1",
|
"prettier": "3.8.1",
|
||||||
"prettier-plugin-astro": "0.14.1",
|
"prettier-plugin-astro": "0.14.1",
|
||||||
|
"sass": "^1.87.0",
|
||||||
"unplugin-icons": "^23.0.1"
|
"unplugin-icons": "^23.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+236
-50
@@ -10,22 +10,25 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/mdx':
|
'@astrojs/mdx':
|
||||||
specifier: ^5.0.3
|
specifier: ^5.0.3
|
||||||
version: 5.0.3(astro@6.1.4(@types/node@24.12.2)(rollup@4.60.1))
|
version: 5.0.3(astro@6.1.4(rollup@4.60.1)(sass@1.99.0))
|
||||||
'@astrojs/rss':
|
'@astrojs/rss':
|
||||||
specifier: ^4.0.18
|
specifier: ^4.0.18
|
||||||
version: 4.0.18
|
version: 4.0.18
|
||||||
'@astrojs/vue':
|
'@astrojs/vue':
|
||||||
specifier: ^6.0.1
|
specifier: ^6.0.1
|
||||||
version: 6.0.1(@types/node@24.12.2)(astro@6.1.4(@types/node@24.12.2)(rollup@4.60.1))(vue@3.5.32)
|
version: 6.0.1(astro@6.1.4(rollup@4.60.1)(sass@1.99.0))(sass@1.99.0)(vue@3.5.32)
|
||||||
'@halo-dev/api-client':
|
'@halo-dev/api-client':
|
||||||
specifier: ^2.23.0
|
specifier: ^2.23.0
|
||||||
version: 2.23.0(axios@1.14.0)
|
version: 2.23.0(axios@1.14.0)
|
||||||
astro:
|
astro:
|
||||||
specifier: ^6.1.4
|
specifier: ^6.1.4
|
||||||
version: 6.1.4(@types/node@24.12.2)(rollup@4.60.1)
|
version: 6.1.4(rollup@4.60.1)(sass@1.99.0)
|
||||||
ky:
|
ky:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
|
lightgallery:
|
||||||
|
specifier: ^2.9.0
|
||||||
|
version: 2.9.0
|
||||||
sharp:
|
sharp:
|
||||||
specifier: ^0.34.5
|
specifier: ^0.34.5
|
||||||
version: 0.34.5
|
version: 0.34.5
|
||||||
@@ -45,6 +48,9 @@ importers:
|
|||||||
prettier-plugin-astro:
|
prettier-plugin-astro:
|
||||||
specifier: 0.14.1
|
specifier: 0.14.1
|
||||||
version: 0.14.1
|
version: 0.14.1
|
||||||
|
sass:
|
||||||
|
specifier: ^1.87.0
|
||||||
|
version: 1.99.0
|
||||||
unplugin-icons:
|
unplugin-icons:
|
||||||
specifier: ^23.0.1
|
specifier: ^23.0.1
|
||||||
version: 23.0.1(@vue/compiler-sfc@3.5.32)
|
version: 23.0.1(@vue/compiler-sfc@3.5.32)
|
||||||
@@ -588,6 +594,94 @@ packages:
|
|||||||
'@oslojs/encoding@1.1.0':
|
'@oslojs/encoding@1.1.0':
|
||||||
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
|
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
|
||||||
|
|
||||||
|
'@parcel/watcher-android-arm64@2.5.6':
|
||||||
|
resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@parcel/watcher-darwin-arm64@2.5.6':
|
||||||
|
resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@parcel/watcher-darwin-x64@2.5.6':
|
||||||
|
resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@parcel/watcher-freebsd-x64@2.5.6':
|
||||||
|
resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm-glibc@2.5.6':
|
||||||
|
resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm-musl@2.5.6':
|
||||||
|
resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm64-glibc@2.5.6':
|
||||||
|
resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm64-musl@2.5.6':
|
||||||
|
resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-x64-glibc@2.5.6':
|
||||||
|
resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-x64-musl@2.5.6':
|
||||||
|
resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-arm64@2.5.6':
|
||||||
|
resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-ia32@2.5.6':
|
||||||
|
resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-x64@2.5.6':
|
||||||
|
resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@parcel/watcher@2.5.6':
|
||||||
|
resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
|
||||||
'@polka/url@1.0.0-next.29':
|
'@polka/url@1.0.0-next.29':
|
||||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||||
|
|
||||||
@@ -799,9 +893,6 @@ packages:
|
|||||||
'@types/nlcst@2.0.3':
|
'@types/nlcst@2.0.3':
|
||||||
resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==}
|
resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==}
|
||||||
|
|
||||||
'@types/node@24.12.2':
|
|
||||||
resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==}
|
|
||||||
|
|
||||||
'@types/unist@2.0.11':
|
'@types/unist@2.0.11':
|
||||||
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
||||||
|
|
||||||
@@ -810,6 +901,7 @@ packages:
|
|||||||
|
|
||||||
'@ungap/structured-clone@1.3.0':
|
'@ungap/structured-clone@1.3.0':
|
||||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||||
|
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
|
||||||
|
|
||||||
'@vitejs/plugin-vue-jsx@5.1.5':
|
'@vitejs/plugin-vue-jsx@5.1.5':
|
||||||
resolution: {integrity: sha512-jIAsvHOEtWpslLOI2MeElGFxH7M8pM83BU/Tor4RLyiwH0FM4nUW3xdvbw20EeU9wc5IspQwMq225K3CMnJEpA==}
|
resolution: {integrity: sha512-jIAsvHOEtWpslLOI2MeElGFxH7M8pM83BU/Tor4RLyiwH0FM4nUW3xdvbw20EeU9wc5IspQwMq225K3CMnJEpA==}
|
||||||
@@ -1013,6 +1105,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||||
engines: {node: '>= 8.10.0'}
|
engines: {node: '>= 8.10.0'}
|
||||||
|
|
||||||
|
chokidar@4.0.3:
|
||||||
|
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||||
|
engines: {node: '>= 14.16.0'}
|
||||||
|
|
||||||
chokidar@5.0.0:
|
chokidar@5.0.0:
|
||||||
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
|
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
|
||||||
engines: {node: '>= 20.19.0'}
|
engines: {node: '>= 20.19.0'}
|
||||||
@@ -1403,6 +1499,9 @@ packages:
|
|||||||
ignore-by-default@1.0.1:
|
ignore-by-default@1.0.1:
|
||||||
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
|
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
|
||||||
|
|
||||||
|
immutable@5.1.5:
|
||||||
|
resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==}
|
||||||
|
|
||||||
inline-style-parser@0.2.7:
|
inline-style-parser@0.2.7:
|
||||||
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
|
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
|
||||||
|
|
||||||
@@ -1479,6 +1578,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-KzI4Vz5AbZFAUFYGx28PCSfFWUo6/qj9Br/P6KRwDieE1xfdz0tIONepJcLw/1xLocN13GgvfJGasa+pfSkbHg==}
|
resolution: {integrity: sha512-KzI4Vz5AbZFAUFYGx28PCSfFWUo6/qj9Br/P6KRwDieE1xfdz0tIONepJcLw/1xLocN13GgvfJGasa+pfSkbHg==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
|
lightgallery@2.9.0:
|
||||||
|
resolution: {integrity: sha512-58Ud1DyhD2ao58t+kPEqSZrjFxg23tGd5ZKr75erm7q31g5xhUtWUJH3sTUkhHzlyJAKHj5eTrJ37HQRXG4Wbg==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
local-pkg@1.1.2:
|
local-pkg@1.1.2:
|
||||||
resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
|
resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -1706,6 +1809,9 @@ packages:
|
|||||||
nlcst-to-string@4.0.0:
|
nlcst-to-string@4.0.0:
|
||||||
resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==}
|
resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==}
|
||||||
|
|
||||||
|
node-addon-api@7.1.1:
|
||||||
|
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||||
|
|
||||||
node-fetch-native@1.6.7:
|
node-fetch-native@1.6.7:
|
||||||
resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
|
resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
|
||||||
|
|
||||||
@@ -1845,6 +1951,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||||
engines: {node: '>=8.10.0'}
|
engines: {node: '>=8.10.0'}
|
||||||
|
|
||||||
|
readdirp@4.1.2:
|
||||||
|
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||||
|
engines: {node: '>= 14.18.0'}
|
||||||
|
|
||||||
readdirp@5.0.0:
|
readdirp@5.0.0:
|
||||||
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
|
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
|
||||||
engines: {node: '>= 20.19.0'}
|
engines: {node: '>= 20.19.0'}
|
||||||
@@ -1933,6 +2043,11 @@ packages:
|
|||||||
sass-formatter@0.7.9:
|
sass-formatter@0.7.9:
|
||||||
resolution: {integrity: sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==}
|
resolution: {integrity: sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==}
|
||||||
|
|
||||||
|
sass@1.99.0:
|
||||||
|
resolution: {integrity: sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
sax@1.6.0:
|
sax@1.6.0:
|
||||||
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
|
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
|
||||||
engines: {node: '>=11.0.0'}
|
engines: {node: '>=11.0.0'}
|
||||||
@@ -2078,9 +2193,6 @@ packages:
|
|||||||
undefsafe@2.0.5:
|
undefsafe@2.0.5:
|
||||||
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
|
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
|
||||||
|
|
||||||
undici-types@7.16.0:
|
|
||||||
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
|
||||||
|
|
||||||
unified@11.0.5:
|
unified@11.0.5:
|
||||||
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
||||||
|
|
||||||
@@ -2381,12 +2493,12 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@astrojs/mdx@5.0.3(astro@6.1.4(@types/node@24.12.2)(rollup@4.60.1))':
|
'@astrojs/mdx@5.0.3(astro@6.1.4(rollup@4.60.1)(sass@1.99.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/markdown-remark': 7.1.0
|
'@astrojs/markdown-remark': 7.1.0
|
||||||
'@mdx-js/mdx': 3.1.1
|
'@mdx-js/mdx': 3.1.1
|
||||||
acorn: 8.16.0
|
acorn: 8.16.0
|
||||||
astro: 6.1.4(@types/node@24.12.2)(rollup@4.60.1)
|
astro: 6.1.4(rollup@4.60.1)(sass@1.99.0)
|
||||||
es-module-lexer: 2.0.0
|
es-module-lexer: 2.0.0
|
||||||
estree-util-visit: 2.0.0
|
estree-util-visit: 2.0.0
|
||||||
hast-util-to-html: 9.0.5
|
hast-util-to-html: 9.0.5
|
||||||
@@ -2422,14 +2534,14 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@astrojs/vue@6.0.1(@types/node@24.12.2)(astro@6.1.4(@types/node@24.12.2)(rollup@4.60.1))(vue@3.5.32)':
|
'@astrojs/vue@6.0.1(astro@6.1.4(rollup@4.60.1)(sass@1.99.0))(sass@1.99.0)(vue@3.5.32)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitejs/plugin-vue': 6.0.5(vite@7.3.2(@types/node@24.12.2))(vue@3.5.32)
|
'@vitejs/plugin-vue': 6.0.5(vite@7.3.2(sass@1.99.0))(vue@3.5.32)
|
||||||
'@vitejs/plugin-vue-jsx': 5.1.5(vite@7.3.2(@types/node@24.12.2))(vue@3.5.32)
|
'@vitejs/plugin-vue-jsx': 5.1.5(vite@7.3.2(sass@1.99.0))(vue@3.5.32)
|
||||||
'@vue/compiler-sfc': 3.5.32
|
'@vue/compiler-sfc': 3.5.32
|
||||||
astro: 6.1.4(@types/node@24.12.2)(rollup@4.60.1)
|
astro: 6.1.4(rollup@4.60.1)(sass@1.99.0)
|
||||||
vite: 7.3.2(@types/node@24.12.2)
|
vite: 7.3.2(sass@1.99.0)
|
||||||
vite-plugin-vue-devtools: 8.1.1(vite@7.3.2(@types/node@24.12.2))(vue@3.5.32)
|
vite-plugin-vue-devtools: 8.1.1(vite@7.3.2(sass@1.99.0))(vue@3.5.32)
|
||||||
vue: 3.5.32
|
vue: 3.5.32
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
@@ -2900,6 +3012,67 @@ snapshots:
|
|||||||
|
|
||||||
'@oslojs/encoding@1.1.0': {}
|
'@oslojs/encoding@1.1.0': {}
|
||||||
|
|
||||||
|
'@parcel/watcher-android-arm64@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-darwin-arm64@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-darwin-x64@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-freebsd-x64@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm-glibc@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm-musl@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm64-glibc@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm64-musl@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-x64-glibc@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-x64-musl@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-arm64@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-ia32@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-x64@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher@2.5.6':
|
||||||
|
dependencies:
|
||||||
|
detect-libc: 2.1.2
|
||||||
|
is-glob: 4.0.3
|
||||||
|
node-addon-api: 7.1.1
|
||||||
|
picomatch: 4.0.4
|
||||||
|
optionalDependencies:
|
||||||
|
'@parcel/watcher-android-arm64': 2.5.6
|
||||||
|
'@parcel/watcher-darwin-arm64': 2.5.6
|
||||||
|
'@parcel/watcher-darwin-x64': 2.5.6
|
||||||
|
'@parcel/watcher-freebsd-x64': 2.5.6
|
||||||
|
'@parcel/watcher-linux-arm-glibc': 2.5.6
|
||||||
|
'@parcel/watcher-linux-arm-musl': 2.5.6
|
||||||
|
'@parcel/watcher-linux-arm64-glibc': 2.5.6
|
||||||
|
'@parcel/watcher-linux-arm64-musl': 2.5.6
|
||||||
|
'@parcel/watcher-linux-x64-glibc': 2.5.6
|
||||||
|
'@parcel/watcher-linux-x64-musl': 2.5.6
|
||||||
|
'@parcel/watcher-win32-arm64': 2.5.6
|
||||||
|
'@parcel/watcher-win32-ia32': 2.5.6
|
||||||
|
'@parcel/watcher-win32-x64': 2.5.6
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@polka/url@1.0.0-next.29': {}
|
'@polka/url@1.0.0-next.29': {}
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-rc.13': {}
|
'@rolldown/pluginutils@1.0.0-rc.13': {}
|
||||||
@@ -3055,33 +3228,28 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
|
|
||||||
'@types/node@24.12.2':
|
|
||||||
dependencies:
|
|
||||||
undici-types: 7.16.0
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@types/unist@2.0.11': {}
|
'@types/unist@2.0.11': {}
|
||||||
|
|
||||||
'@types/unist@3.0.3': {}
|
'@types/unist@3.0.3': {}
|
||||||
|
|
||||||
'@ungap/structured-clone@1.3.0': {}
|
'@ungap/structured-clone@1.3.0': {}
|
||||||
|
|
||||||
'@vitejs/plugin-vue-jsx@5.1.5(vite@7.3.2(@types/node@24.12.2))(vue@3.5.32)':
|
'@vitejs/plugin-vue-jsx@5.1.5(vite@7.3.2(sass@1.99.0))(vue@3.5.32)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
'@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0)
|
'@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0)
|
||||||
'@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
|
'@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
|
||||||
'@rolldown/pluginutils': 1.0.0-rc.13
|
'@rolldown/pluginutils': 1.0.0-rc.13
|
||||||
'@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.0)
|
'@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.0)
|
||||||
vite: 7.3.2(@types/node@24.12.2)
|
vite: 7.3.2(sass@1.99.0)
|
||||||
vue: 3.5.32
|
vue: 3.5.32
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@vitejs/plugin-vue@6.0.5(vite@7.3.2(@types/node@24.12.2))(vue@3.5.32)':
|
'@vitejs/plugin-vue@6.0.5(vite@7.3.2(sass@1.99.0))(vue@3.5.32)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rolldown/pluginutils': 1.0.0-rc.2
|
'@rolldown/pluginutils': 1.0.0-rc.2
|
||||||
vite: 7.3.2(@types/node@24.12.2)
|
vite: 7.3.2(sass@1.99.0)
|
||||||
vue: 3.5.32
|
vue: 3.5.32
|
||||||
|
|
||||||
'@vue/babel-helper-vue-transform-on@1.5.0': {}
|
'@vue/babel-helper-vue-transform-on@1.5.0': {}
|
||||||
@@ -3232,7 +3400,7 @@ snapshots:
|
|||||||
|
|
||||||
astring@1.9.0: {}
|
astring@1.9.0: {}
|
||||||
|
|
||||||
astro@6.1.4(@types/node@24.12.2)(rollup@4.60.1):
|
astro@6.1.4(rollup@4.60.1)(sass@1.99.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/compiler': 3.0.1
|
'@astrojs/compiler': 3.0.1
|
||||||
'@astrojs/internal-helpers': 0.8.0
|
'@astrojs/internal-helpers': 0.8.0
|
||||||
@@ -3284,8 +3452,8 @@ snapshots:
|
|||||||
unist-util-visit: 5.1.0
|
unist-util-visit: 5.1.0
|
||||||
unstorage: 1.17.5
|
unstorage: 1.17.5
|
||||||
vfile: 6.0.3
|
vfile: 6.0.3
|
||||||
vite: 7.3.2(@types/node@24.12.2)
|
vite: 7.3.2(sass@1.99.0)
|
||||||
vitefu: 1.1.3(vite@7.3.2(@types/node@24.12.2))
|
vitefu: 1.1.3(vite@7.3.2(sass@1.99.0))
|
||||||
xxhash-wasm: 1.1.0
|
xxhash-wasm: 1.1.0
|
||||||
yargs-parser: 22.0.0
|
yargs-parser: 22.0.0
|
||||||
zod: 4.3.6
|
zod: 4.3.6
|
||||||
@@ -3404,6 +3572,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
chokidar@4.0.3:
|
||||||
|
dependencies:
|
||||||
|
readdirp: 4.1.2
|
||||||
|
|
||||||
chokidar@5.0.0:
|
chokidar@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
readdirp: 5.0.0
|
readdirp: 5.0.0
|
||||||
@@ -3887,6 +4059,8 @@ snapshots:
|
|||||||
|
|
||||||
ignore-by-default@1.0.1: {}
|
ignore-by-default@1.0.1: {}
|
||||||
|
|
||||||
|
immutable@5.1.5: {}
|
||||||
|
|
||||||
inline-style-parser@0.2.7: {}
|
inline-style-parser@0.2.7: {}
|
||||||
|
|
||||||
iron-webcrypto@1.2.1: {}
|
iron-webcrypto@1.2.1: {}
|
||||||
@@ -3940,6 +4114,8 @@ snapshots:
|
|||||||
|
|
||||||
ky@2.0.0: {}
|
ky@2.0.0: {}
|
||||||
|
|
||||||
|
lightgallery@2.9.0: {}
|
||||||
|
|
||||||
local-pkg@1.1.2:
|
local-pkg@1.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
mlly: 1.8.2
|
mlly: 1.8.2
|
||||||
@@ -4436,6 +4612,9 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/nlcst': 2.0.3
|
'@types/nlcst': 2.0.3
|
||||||
|
|
||||||
|
node-addon-api@7.1.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
node-fetch-native@1.6.7: {}
|
node-fetch-native@1.6.7: {}
|
||||||
|
|
||||||
node-mock-http@1.0.4: {}
|
node-mock-http@1.0.4: {}
|
||||||
@@ -4584,6 +4763,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
picomatch: 2.3.2
|
picomatch: 2.3.2
|
||||||
|
|
||||||
|
readdirp@4.1.2: {}
|
||||||
|
|
||||||
readdirp@5.0.0: {}
|
readdirp@5.0.0: {}
|
||||||
|
|
||||||
recma-build-jsx@1.0.0:
|
recma-build-jsx@1.0.0:
|
||||||
@@ -4770,6 +4951,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
suf-log: 2.5.3
|
suf-log: 2.5.3
|
||||||
|
|
||||||
|
sass@1.99.0:
|
||||||
|
dependencies:
|
||||||
|
chokidar: 4.0.3
|
||||||
|
immutable: 5.1.5
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
optionalDependencies:
|
||||||
|
'@parcel/watcher': 2.5.6
|
||||||
|
|
||||||
sax@1.6.0: {}
|
sax@1.6.0: {}
|
||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
@@ -4935,9 +5124,6 @@ snapshots:
|
|||||||
|
|
||||||
undefsafe@2.0.5: {}
|
undefsafe@2.0.5: {}
|
||||||
|
|
||||||
undici-types@7.16.0:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
unified@11.0.5:
|
unified@11.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
@@ -5054,17 +5240,17 @@ snapshots:
|
|||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
vfile-message: 4.0.3
|
vfile-message: 4.0.3
|
||||||
|
|
||||||
vite-dev-rpc@1.1.0(vite@7.3.2(@types/node@24.12.2)):
|
vite-dev-rpc@1.1.0(vite@7.3.2(sass@1.99.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
birpc: 2.9.0
|
birpc: 2.9.0
|
||||||
vite: 7.3.2(@types/node@24.12.2)
|
vite: 7.3.2(sass@1.99.0)
|
||||||
vite-hot-client: 2.1.0(vite@7.3.2(@types/node@24.12.2))
|
vite-hot-client: 2.1.0(vite@7.3.2(sass@1.99.0))
|
||||||
|
|
||||||
vite-hot-client@2.1.0(vite@7.3.2(@types/node@24.12.2)):
|
vite-hot-client@2.1.0(vite@7.3.2(sass@1.99.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
vite: 7.3.2(@types/node@24.12.2)
|
vite: 7.3.2(sass@1.99.0)
|
||||||
|
|
||||||
vite-plugin-inspect@11.3.3(vite@7.3.2(@types/node@24.12.2)):
|
vite-plugin-inspect@11.3.3(vite@7.3.2(sass@1.99.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
ansis: 4.2.0
|
ansis: 4.2.0
|
||||||
debug: 4.4.3(supports-color@5.5.0)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
@@ -5074,26 +5260,26 @@ snapshots:
|
|||||||
perfect-debounce: 2.1.0
|
perfect-debounce: 2.1.0
|
||||||
sirv: 3.0.2
|
sirv: 3.0.2
|
||||||
unplugin-utils: 0.3.1
|
unplugin-utils: 0.3.1
|
||||||
vite: 7.3.2(@types/node@24.12.2)
|
vite: 7.3.2(sass@1.99.0)
|
||||||
vite-dev-rpc: 1.1.0(vite@7.3.2(@types/node@24.12.2))
|
vite-dev-rpc: 1.1.0(vite@7.3.2(sass@1.99.0))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
vite-plugin-vue-devtools@8.1.1(vite@7.3.2(@types/node@24.12.2))(vue@3.5.32):
|
vite-plugin-vue-devtools@8.1.1(vite@7.3.2(sass@1.99.0))(vue@3.5.32):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/devtools-core': 8.1.1(vue@3.5.32)
|
'@vue/devtools-core': 8.1.1(vue@3.5.32)
|
||||||
'@vue/devtools-kit': 8.1.1
|
'@vue/devtools-kit': 8.1.1
|
||||||
'@vue/devtools-shared': 8.1.1
|
'@vue/devtools-shared': 8.1.1
|
||||||
sirv: 3.0.2
|
sirv: 3.0.2
|
||||||
vite: 7.3.2(@types/node@24.12.2)
|
vite: 7.3.2(sass@1.99.0)
|
||||||
vite-plugin-inspect: 11.3.3(vite@7.3.2(@types/node@24.12.2))
|
vite-plugin-inspect: 11.3.3(vite@7.3.2(sass@1.99.0))
|
||||||
vite-plugin-vue-inspector: 5.4.0(vite@7.3.2(@types/node@24.12.2))
|
vite-plugin-vue-inspector: 5.4.0(vite@7.3.2(sass@1.99.0))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
vite-plugin-vue-inspector@5.4.0(vite@7.3.2(@types/node@24.12.2)):
|
vite-plugin-vue-inspector@5.4.0(vite@7.3.2(sass@1.99.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
'@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0)
|
'@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0)
|
||||||
@@ -5104,11 +5290,11 @@ snapshots:
|
|||||||
'@vue/compiler-dom': 3.5.32
|
'@vue/compiler-dom': 3.5.32
|
||||||
kolorist: 1.8.0
|
kolorist: 1.8.0
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
vite: 7.3.2(@types/node@24.12.2)
|
vite: 7.3.2(sass@1.99.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
vite@7.3.2(@types/node@24.12.2):
|
vite@7.3.2(sass@1.99.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.7
|
esbuild: 0.27.7
|
||||||
fdir: 6.5.0(picomatch@4.0.4)
|
fdir: 6.5.0(picomatch@4.0.4)
|
||||||
@@ -5117,12 +5303,12 @@ snapshots:
|
|||||||
rollup: 4.60.1
|
rollup: 4.60.1
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 24.12.2
|
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
sass: 1.99.0
|
||||||
|
|
||||||
vitefu@1.1.3(vite@7.3.2(@types/node@24.12.2)):
|
vitefu@1.1.3(vite@7.3.2(sass@1.99.0)):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 7.3.2(@types/node@24.12.2)
|
vite: 7.3.2(sass@1.99.0)
|
||||||
|
|
||||||
vue@3.5.32:
|
vue@3.5.32:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
allowBuilds:
|
allowBuilds:
|
||||||
|
'@parcel/watcher': true
|
||||||
esbuild: true
|
esbuild: true
|
||||||
sharp: true
|
sharp: true
|
||||||
|
onlyBuiltDependencies:
|
||||||
|
- "@parcel/watcher"
|
||||||
|
- esbuild
|
||||||
|
- sharp
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var stored = localStorage.getItem("wi-theme");
|
||||||
|
var prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
|
var isDark = stored === "dark" || (!stored && prefersDark);
|
||||||
|
if (isDark) {
|
||||||
|
document.documentElement.classList.add("dark");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<title>页面走丢了 - 暖屿</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #faf7f2;
|
||||||
|
--ink: #2c2420;
|
||||||
|
--ink-2: #7a6e64;
|
||||||
|
--accent: #d4764e;
|
||||||
|
--accent-hover: #c4613a;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--bg: #1a1614;
|
||||||
|
--ink: #ede6de;
|
||||||
|
--ink-2: #9a8e84;
|
||||||
|
--accent: #e8955f;
|
||||||
|
--accent-hover: #f0a872;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans SC", sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--ink);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-error {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-error__code {
|
||||||
|
font-size: 8rem;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--accent);
|
||||||
|
opacity: 0.3;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-error__title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-top: 1rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-error__desc {
|
||||||
|
color: var(--ink-2);
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-error__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-error__home {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.625rem 1.5rem;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 9999px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-error__home:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-error__back {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.625rem 1.5rem;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ink-2);
|
||||||
|
border: 1px solid var(--ink-2);
|
||||||
|
border-radius: 9999px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
transition: color 0.15s ease, border-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-error__back:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wi-error">
|
||||||
|
<p class="wi-error__code">404</p>
|
||||||
|
<h1 class="wi-error__title">页面走丢了</h1>
|
||||||
|
<p class="wi-error__desc">你寻找的页面似乎不在这个小岛上</p>
|
||||||
|
<div class="wi-error__actions">
|
||||||
|
<a class="wi-error__home" href="/">回到首页</a>
|
||||||
|
<a class="wi-error__back" href="javascript:history.back()">返回上页</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var stored = localStorage.getItem("wi-theme");
|
||||||
|
var prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
|
var isDark = stored === "dark" || (!stored && prefersDark);
|
||||||
|
if (isDark) {
|
||||||
|
document.documentElement.classList.add("dark");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<title>服务器开小差了 - 暖屿</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #faf7f2;
|
||||||
|
--ink: #2c2420;
|
||||||
|
--ink-2: #7a6e64;
|
||||||
|
--accent: #d4764e;
|
||||||
|
--accent-hover: #c4613a;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--bg: #1a1614;
|
||||||
|
--ink: #ede6de;
|
||||||
|
--ink-2: #9a8e84;
|
||||||
|
--accent: #e8955f;
|
||||||
|
--accent-hover: #f0a872;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans SC", sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--ink);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-error {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-error__code {
|
||||||
|
font-size: 8rem;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--accent);
|
||||||
|
opacity: 0.3;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-error__title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-top: 1rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-error__desc {
|
||||||
|
color: var(--ink-2);
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-error__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-error__home {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.625rem 1.5rem;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 9999px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-error__home:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-error__back {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.625rem 1.5rem;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ink-2);
|
||||||
|
border: 1px solid var(--ink-2);
|
||||||
|
border-radius: 9999px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
transition: color 0.15s ease, border-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-error__back:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wi-error">
|
||||||
|
<p class="wi-error__code">500</p>
|
||||||
|
<h1 class="wi-error__title">服务器开小差了</h1>
|
||||||
|
<p class="wi-error__desc">小岛遇到了一些问题,稍后再来看看吧</p>
|
||||||
|
<div class="wi-error__actions">
|
||||||
|
<a class="wi-error__home" href="/">回到首页</a>
|
||||||
|
<a class="wi-error__back" href="javascript:history.back()">返回上页</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -1,19 +1,21 @@
|
|||||||
<ul class="post-feed__list" role="list">
|
<ul class="wi-post-feed" role="list">
|
||||||
<li th:each="post : ${posts.items}" class="post-feed__item">
|
<li th:each="post : ${posts.items}" class="wi-post-feed__item">
|
||||||
<a class="post-feed__link" th:href="@{${post.status.permalink}}">
|
<a class="wi-post-feed__link" th:href="@{${post.status.permalink}}">
|
||||||
<div class="post-feed__body">
|
<div class="wi-post-feed__image-wrap" th:unless="${#strings.isEmpty(post.spec.cover)}">
|
||||||
<p
|
|
||||||
class="post-feed__date"
|
|
||||||
th:text="${#dates.format(post.spec.publishTime, 'yyyy-MM-dd')}"
|
|
||||||
></p>
|
|
||||||
<h2 class="post-feed__title" th:text="${post.spec.title}"></h2>
|
|
||||||
<p class="post-feed__excerpt" th:text="${post.status.excerpt}"></p>
|
|
||||||
</div>
|
|
||||||
<img
|
<img
|
||||||
th:unless="${#strings.isEmpty(post.spec.cover)}"
|
class="wi-post-feed__image"
|
||||||
class="post-feed__image"
|
|
||||||
th:src="${post.spec.cover}"
|
th:src="${post.spec.cover}"
|
||||||
|
th:srcset="|${thumbnail.gen(post.spec.cover, 's')} 400w,
|
||||||
|
${thumbnail.gen(post.spec.cover, 'm')} 800w,
|
||||||
|
${thumbnail.gen(post.spec.cover, 'l')} 1200w|"
|
||||||
|
sizes="(max-width: 768px) 100vw, 400px"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="wi-post-feed__body">
|
||||||
|
<p class="wi-post-feed__date" th:text="${#dates.format(post.spec.publishTime, 'yyyy-MM-dd')}"></p>
|
||||||
|
<h2 class="wi-post-feed__title" th:text="${post.spec.title}"></h2>
|
||||||
|
<p class="wi-post-feed__excerpt" th:text="${post.status.excerpt}"></p>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 208 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 261 KiB |
+436
@@ -0,0 +1,436 @@
|
|||||||
|
apiVersion: v1alpha1
|
||||||
|
kind: Setting
|
||||||
|
metadata:
|
||||||
|
name: warm-island-setting
|
||||||
|
spec:
|
||||||
|
forms:
|
||||||
|
- group: basic
|
||||||
|
label: 基础设置
|
||||||
|
formSchema:
|
||||||
|
- $formkit: text
|
||||||
|
name: owner_name
|
||||||
|
label: 站点名称
|
||||||
|
- $formkit: textarea
|
||||||
|
name: site_description
|
||||||
|
label: 站点描述
|
||||||
|
- $formkit: attachment
|
||||||
|
name: logo
|
||||||
|
label: 自定义 Logo
|
||||||
|
- $formkit: attachment
|
||||||
|
name: favicon
|
||||||
|
label: 自定义 Favicon
|
||||||
|
- $formkit: text
|
||||||
|
name: label_search
|
||||||
|
label: 搜索按钮标签
|
||||||
|
value: 搜索
|
||||||
|
- $formkit: text
|
||||||
|
name: label_theme_switch
|
||||||
|
label: 主题切换标签
|
||||||
|
value: 切换主题
|
||||||
|
- $formkit: text
|
||||||
|
name: label_archives_title
|
||||||
|
label: 归档页标题
|
||||||
|
value: 归档
|
||||||
|
- group: style
|
||||||
|
label: 总体样式
|
||||||
|
formSchema:
|
||||||
|
- $formkit: radio
|
||||||
|
name: color_scheme
|
||||||
|
label: 配色方案
|
||||||
|
options:
|
||||||
|
- label: 跟随系统
|
||||||
|
value: system
|
||||||
|
- label: 浅色
|
||||||
|
value: light
|
||||||
|
- label: 深色
|
||||||
|
value: dark
|
||||||
|
value: system
|
||||||
|
- $formkit: color
|
||||||
|
name: accent_color
|
||||||
|
label: 强调色
|
||||||
|
value: "#d4764e"
|
||||||
|
- $formkit: select
|
||||||
|
name: border_radius
|
||||||
|
label: 圆角风格
|
||||||
|
options:
|
||||||
|
- label: Small
|
||||||
|
value: small
|
||||||
|
- label: Medium
|
||||||
|
value: medium
|
||||||
|
- label: Large
|
||||||
|
value: large
|
||||||
|
value: medium
|
||||||
|
- $formkit: code
|
||||||
|
name: custom_css
|
||||||
|
label: 自定义 CSS
|
||||||
|
language: css
|
||||||
|
- $formkit: select
|
||||||
|
name: layout_container_width
|
||||||
|
label: 容器宽度
|
||||||
|
options:
|
||||||
|
- label: Narrow
|
||||||
|
value: narrow
|
||||||
|
- label: Medium
|
||||||
|
value: medium
|
||||||
|
- label: Wide
|
||||||
|
value: wide
|
||||||
|
value: medium
|
||||||
|
- $formkit: switch
|
||||||
|
name: layout_sidebar
|
||||||
|
label: 启用侧边栏
|
||||||
|
value: false
|
||||||
|
- $formkit: switch
|
||||||
|
name: animation_enabled
|
||||||
|
label: 启用动效
|
||||||
|
value: true
|
||||||
|
- $formkit: switch
|
||||||
|
name: animation_breath
|
||||||
|
label: 呼吸动画
|
||||||
|
value: true
|
||||||
|
- $formkit: switch
|
||||||
|
name: animation_scroll_reveal
|
||||||
|
label: 滚动渐入
|
||||||
|
value: true
|
||||||
|
- $formkit: switch
|
||||||
|
name: animation_cursor_glow
|
||||||
|
label: 光感跟随
|
||||||
|
value: false
|
||||||
|
- group: navbar
|
||||||
|
label: 导航栏
|
||||||
|
formSchema:
|
||||||
|
- $formkit: select
|
||||||
|
name: navbar_style
|
||||||
|
label: 导航栏样式
|
||||||
|
options:
|
||||||
|
- label: Glass
|
||||||
|
value: glass
|
||||||
|
- label: Minimal
|
||||||
|
value: minimal
|
||||||
|
- label: Float
|
||||||
|
value: float
|
||||||
|
value: glass
|
||||||
|
- $formkit: switch
|
||||||
|
name: navbar_show_search
|
||||||
|
label: 显示搜索按钮
|
||||||
|
value: true
|
||||||
|
- $formkit: switch
|
||||||
|
name: navbar_show_theme_switch
|
||||||
|
label: 显示主题切换
|
||||||
|
value: true
|
||||||
|
- group: hero
|
||||||
|
label: Hero 首屏
|
||||||
|
formSchema:
|
||||||
|
- $formkit: switch
|
||||||
|
name: hero_enabled
|
||||||
|
label: 启用 Hero 模块
|
||||||
|
value: true
|
||||||
|
- $formkit: text
|
||||||
|
name: hero_title
|
||||||
|
label: 主标题(留空则使用站点标题)
|
||||||
|
- $formkit: text
|
||||||
|
name: hero_subtitle
|
||||||
|
label: 副标题(留空则使用站点副标题)
|
||||||
|
- $formkit: select
|
||||||
|
name: hero_description_mode
|
||||||
|
label: 描述文案来源
|
||||||
|
options:
|
||||||
|
- label: 无
|
||||||
|
value: none
|
||||||
|
- label: 一言
|
||||||
|
value: hitokoto
|
||||||
|
- label: 自定义
|
||||||
|
value: custom
|
||||||
|
value: hitokoto
|
||||||
|
- $formkit: url
|
||||||
|
name: hero_hitokoto_api
|
||||||
|
label: 一言 API 地址
|
||||||
|
value: https://v1.hitokoto.cn/
|
||||||
|
- $formkit: checkbox
|
||||||
|
name: hero_hitokoto_categories
|
||||||
|
label: 一言句子类型
|
||||||
|
options:
|
||||||
|
- label: 动画
|
||||||
|
value: a
|
||||||
|
- label: 漫画
|
||||||
|
value: b
|
||||||
|
- label: 游戏
|
||||||
|
value: c
|
||||||
|
- label: 文学
|
||||||
|
value: d
|
||||||
|
- label: 原创
|
||||||
|
value: e
|
||||||
|
- label: 来自网络
|
||||||
|
value: f
|
||||||
|
- label: 其他
|
||||||
|
value: g
|
||||||
|
- label: 影视
|
||||||
|
value: h
|
||||||
|
- label: 诗词
|
||||||
|
value: i
|
||||||
|
- label: 哲学
|
||||||
|
value: k
|
||||||
|
- label: 抖机灵
|
||||||
|
value: l
|
||||||
|
value:
|
||||||
|
- d
|
||||||
|
- i
|
||||||
|
- k
|
||||||
|
- $formkit: textarea
|
||||||
|
name: hero_custom_description
|
||||||
|
label: 自定义描述文案
|
||||||
|
if: "$get(hero_description_mode).value === 'custom'"
|
||||||
|
- $formkit: attachment
|
||||||
|
name: hero_background_image
|
||||||
|
label: 背景图片
|
||||||
|
- group: home
|
||||||
|
label: 首页
|
||||||
|
formSchema:
|
||||||
|
- $formkit: text
|
||||||
|
name: home_pinned_title
|
||||||
|
label: 置顶模块标题
|
||||||
|
value: 置顶
|
||||||
|
- $formkit: number
|
||||||
|
name: home_excerpt_lines
|
||||||
|
label: 文章简介显示行数
|
||||||
|
value: 3
|
||||||
|
- $formkit: select
|
||||||
|
name: home_post_loading
|
||||||
|
label: 文章加载方式
|
||||||
|
options:
|
||||||
|
- label: 分页
|
||||||
|
value: pagination
|
||||||
|
- label: 下滑加载
|
||||||
|
value: infinite_scroll
|
||||||
|
value: pagination
|
||||||
|
- $formkit: text
|
||||||
|
name: home_label_newer
|
||||||
|
label: 分页-较新标签
|
||||||
|
value: 较新
|
||||||
|
- $formkit: text
|
||||||
|
name: home_label_older
|
||||||
|
label: 分页-较旧标签
|
||||||
|
value: 较旧
|
||||||
|
- $formkit: text
|
||||||
|
name: home_label_loading
|
||||||
|
label: 无限滚动-加载中文案
|
||||||
|
value: 加载中...
|
||||||
|
- $formkit: text
|
||||||
|
name: home_label_all_loaded
|
||||||
|
label: 无限滚动-全部加载文案
|
||||||
|
value: 已加载全部文章
|
||||||
|
- $formkit: text
|
||||||
|
name: home_latest_title
|
||||||
|
label: 最新文章模块标题
|
||||||
|
value: 最新文章
|
||||||
|
- $formkit: text
|
||||||
|
name: home_label_view_all
|
||||||
|
label: 查看全部标签
|
||||||
|
value: 查看全部
|
||||||
|
- $formkit: text
|
||||||
|
name: home_label_no_posts
|
||||||
|
label: 暂无文章文案
|
||||||
|
value: 暂无文章。
|
||||||
|
- $formkit: text
|
||||||
|
name: home_label_post_count
|
||||||
|
label: 文章数量文案({total}为占位符)
|
||||||
|
value: 共 {total} 篇文章
|
||||||
|
- group: article
|
||||||
|
label: 文章
|
||||||
|
formSchema:
|
||||||
|
- $formkit: switch
|
||||||
|
name: article_show_cover
|
||||||
|
label: 显示封面图
|
||||||
|
value: true
|
||||||
|
- $formkit: switch
|
||||||
|
name: article_show_date
|
||||||
|
label: 显示发布日期
|
||||||
|
value: true
|
||||||
|
- $formkit: switch
|
||||||
|
name: article_show_category
|
||||||
|
label: 显示分类
|
||||||
|
value: true
|
||||||
|
- $formkit: switch
|
||||||
|
name: article_show_tags
|
||||||
|
label: 显示标签
|
||||||
|
value: true
|
||||||
|
- $formkit: switch
|
||||||
|
name: article_show_nav
|
||||||
|
label: 显示上下篇导航
|
||||||
|
value: true
|
||||||
|
- $formkit: switch
|
||||||
|
name: article_show_toc
|
||||||
|
label: 显示文章目录
|
||||||
|
value: true
|
||||||
|
- $formkit: select
|
||||||
|
name: article_toc_position
|
||||||
|
label: 目录位置
|
||||||
|
options:
|
||||||
|
- label: 左侧
|
||||||
|
value: left
|
||||||
|
- label: 右侧
|
||||||
|
value: right
|
||||||
|
value: left
|
||||||
|
- $formkit: switch
|
||||||
|
name: article_show_word_count
|
||||||
|
label: 显示字数统计
|
||||||
|
value: true
|
||||||
|
- $formkit: switch
|
||||||
|
name: article_show_visit_count
|
||||||
|
label: 显示阅读量统计
|
||||||
|
value: true
|
||||||
|
- $formkit: switch
|
||||||
|
name: article_show_read_time
|
||||||
|
label: 显示预计阅读时间
|
||||||
|
value: false
|
||||||
|
- $formkit: select
|
||||||
|
name: article_code_theme
|
||||||
|
label: 代码高亮主题
|
||||||
|
options:
|
||||||
|
- label: Warm
|
||||||
|
value: warm
|
||||||
|
- label: Cold
|
||||||
|
value: cold
|
||||||
|
value: warm
|
||||||
|
- group: moments
|
||||||
|
label: 瞬间
|
||||||
|
formSchema:
|
||||||
|
- $formkit: text
|
||||||
|
name: moments_page_title
|
||||||
|
label: 页面标题
|
||||||
|
value: 瞬间
|
||||||
|
- $formkit: select
|
||||||
|
name: moments_style
|
||||||
|
label: 展示样式
|
||||||
|
options:
|
||||||
|
- label: Timeline
|
||||||
|
value: timeline
|
||||||
|
- label: Cards
|
||||||
|
value: cards
|
||||||
|
- label: Masonry
|
||||||
|
value: masonry
|
||||||
|
value: timeline
|
||||||
|
- group: photos
|
||||||
|
label: 图库
|
||||||
|
formSchema:
|
||||||
|
- $formkit: text
|
||||||
|
name: photos_page_title
|
||||||
|
label: 页面标题
|
||||||
|
value: 图库
|
||||||
|
- $formkit: select
|
||||||
|
name: photos_style
|
||||||
|
label: 展示样式
|
||||||
|
options:
|
||||||
|
- label: Masonry
|
||||||
|
value: masonry
|
||||||
|
- label: Grid
|
||||||
|
value: grid
|
||||||
|
- label: Carousel
|
||||||
|
value: carousel
|
||||||
|
value: masonry
|
||||||
|
- $formkit: number
|
||||||
|
name: photos_columns
|
||||||
|
label: 列数
|
||||||
|
value: 3
|
||||||
|
- $formkit: switch
|
||||||
|
name: photos_show_ungrouped
|
||||||
|
label: 显示未分组图片
|
||||||
|
value: false
|
||||||
|
help: 开启后"全部"视图中会显示未分组的图片,未分组图片区域的标题为"全部"。
|
||||||
|
- $formkit: switch
|
||||||
|
name: photos_show_group_title
|
||||||
|
label: 显示分组标题
|
||||||
|
value: true
|
||||||
|
help: 关闭后"全部"视图中不再显示各分组的标题,所有图片合并展示。
|
||||||
|
- group: links
|
||||||
|
label: 友情链接
|
||||||
|
formSchema:
|
||||||
|
- $formkit: text
|
||||||
|
name: links_page_title
|
||||||
|
label: 页面标题
|
||||||
|
value: 友情链接
|
||||||
|
- $formkit: select
|
||||||
|
name: links_style
|
||||||
|
label: 展示样式
|
||||||
|
options:
|
||||||
|
- label: Grid
|
||||||
|
value: grid
|
||||||
|
- label: List
|
||||||
|
value: list
|
||||||
|
- label: Cards
|
||||||
|
value: cards
|
||||||
|
value: cards
|
||||||
|
- group: friends
|
||||||
|
label: 朋友圈
|
||||||
|
formSchema:
|
||||||
|
- $formkit: text
|
||||||
|
name: friends_page_title
|
||||||
|
label: 页面标题
|
||||||
|
value: 朋友圈
|
||||||
|
- group: equipment
|
||||||
|
label: 我的装备
|
||||||
|
formSchema:
|
||||||
|
- $formkit: text
|
||||||
|
name: equipment_page_title
|
||||||
|
label: 页面标题
|
||||||
|
value: 装备
|
||||||
|
- group: messageboard
|
||||||
|
label: 留言板
|
||||||
|
formSchema:
|
||||||
|
- $formkit: text
|
||||||
|
name: messageboard_title
|
||||||
|
label: 留言板标题
|
||||||
|
value: 留言板
|
||||||
|
- $formkit: textarea
|
||||||
|
name: messageboard_subtitle
|
||||||
|
label: 留言板副标题
|
||||||
|
value: 在这里留下你的足迹吧 🌙
|
||||||
|
- group: comment
|
||||||
|
label: 评论
|
||||||
|
formSchema:
|
||||||
|
- $formkit: select
|
||||||
|
name: comment_style
|
||||||
|
label: 评论样式
|
||||||
|
options:
|
||||||
|
- label: Warm
|
||||||
|
value: warm
|
||||||
|
- label: Minimal
|
||||||
|
value: minimal
|
||||||
|
value: warm
|
||||||
|
- $formkit: switch
|
||||||
|
name: comment_show_avatar
|
||||||
|
label: 显示头像
|
||||||
|
value: true
|
||||||
|
- group: footer
|
||||||
|
label: 页脚
|
||||||
|
formSchema:
|
||||||
|
- $formkit: text
|
||||||
|
name: footer_copyright
|
||||||
|
label: 版权信息(留空则使用默认格式 © 年份 站点标题)
|
||||||
|
- $formkit: text
|
||||||
|
name: footer_icp
|
||||||
|
label: ICP 备案号
|
||||||
|
- $formkit: repeater
|
||||||
|
name: footer_socials
|
||||||
|
label: 社交链接
|
||||||
|
children:
|
||||||
|
- $formkit: text
|
||||||
|
name: platform
|
||||||
|
label: 平台名称
|
||||||
|
- $formkit: text
|
||||||
|
name: icon
|
||||||
|
label: 图标类名(如 fa-brands fa-github)
|
||||||
|
- $formkit: url
|
||||||
|
name: url
|
||||||
|
label: 链接地址
|
||||||
|
- $formkit: switch
|
||||||
|
name: footer_show_powered
|
||||||
|
label: 显示 "Powered by Halo"
|
||||||
|
value: true
|
||||||
|
- $formkit: switch
|
||||||
|
name: footer_show_theme
|
||||||
|
label: 显示主题版本
|
||||||
|
value: true
|
||||||
|
- $formkit: code
|
||||||
|
name: footer_custom_html
|
||||||
|
label: 自定义 HTML(统计代码等)
|
||||||
|
language: html
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted, onUnmounted } from "vue";
|
||||||
|
|
||||||
|
const x = ref(0);
|
||||||
|
const y = ref(0);
|
||||||
|
const visible = ref(false);
|
||||||
|
|
||||||
|
function handleMouseMove(e: MouseEvent) {
|
||||||
|
x.value = e.clientX;
|
||||||
|
y.value = e.clientY;
|
||||||
|
visible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseLeave() {
|
||||||
|
visible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener("mousemove", handleMouseMove);
|
||||||
|
document.addEventListener("mouseleave", handleMouseLeave);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
document.removeEventListener("mouseleave", handleMouseLeave);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="wi-cursor-glow"
|
||||||
|
:class="{ 'wi-cursor-glow--visible': visible }"
|
||||||
|
:style="{ transform: `translate(calc(${x}px - 50%), calc(${y}px - 50%))` }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.wi-cursor-glow {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 400px;
|
||||||
|
height: 400px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
rgba(212, 118, 78, 0.06) 0%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 9999;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-cursor-glow--visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .wi-cursor-glow {
|
||||||
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
rgba(232, 149, 95, 0.04) 0%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="wi-section"
|
||||||
|
th:if="${theme.config?.home?.home_featured_enabled != false}"
|
||||||
|
th:with="featuredPosts = ${postFinder.list({page: 1, size: theme.config?.home?.home_featured_count ?: 3})}"
|
||||||
|
>
|
||||||
|
<div class="wi-container">
|
||||||
|
<div class="wi-section__header">
|
||||||
|
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_featured_title ?: '精选'}">精选</h2>
|
||||||
|
</div>
|
||||||
|
<div class="wi-featured">
|
||||||
|
<a
|
||||||
|
th:each="post,iterStat : ${featuredPosts.items}"
|
||||||
|
th:href="@{${post.status.permalink}}"
|
||||||
|
class="wi-featured__card"
|
||||||
|
th:classappend="${iterStat.index == 0} ? 'wi-featured__card--hero' : ''"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="wi-featured__cover"
|
||||||
|
th:if="${!#strings.isEmpty(post.spec.cover)}"
|
||||||
|
>
|
||||||
|
<img th:src="${post.spec.cover}" th:alt="${post.spec.title}" class="wi-featured__image" />
|
||||||
|
</div>
|
||||||
|
<div class="wi-featured__body">
|
||||||
|
<time
|
||||||
|
class="wi-featured__date"
|
||||||
|
th:text="${#dates.format(post.spec.publishTime, 'yyyy-MM-dd')}"
|
||||||
|
></time>
|
||||||
|
<h3
|
||||||
|
class="wi-featured__title"
|
||||||
|
th:text="${post.spec.title}"
|
||||||
|
></h3>
|
||||||
|
<p
|
||||||
|
class="wi-featured__excerpt"
|
||||||
|
th:if="${post.status.excerpt}"
|
||||||
|
th:text="${post.status.excerpt}"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wi-section__header {
|
||||||
|
margin-bottom: var(--space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-section__title {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink);
|
||||||
|
letter-spacing: var(--tracking-tight);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-section__title::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-featured {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-featured__card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-raised);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition:
|
||||||
|
transform var(--duration-normal) var(--ease-out-expo),
|
||||||
|
box-shadow var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-featured__card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-featured__card--hero {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-featured__card--hero .wi-featured__cover {
|
||||||
|
aspect-ratio: 21 / 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-featured__card--hero .wi-featured__title {
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-featured__cover {
|
||||||
|
aspect-ratio: 16 / 10;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-featured__image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform var(--duration-normal) var(--ease-out-expo);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-featured__card:hover .wi-featured__image {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-featured__body {
|
||||||
|
padding: var(--space-lg);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-featured__date {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--ink-3);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-featured__title {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink);
|
||||||
|
line-height: var(--leading-tight);
|
||||||
|
letter-spacing: var(--tracking-tight);
|
||||||
|
transition: color var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-featured__card:hover .wi-featured__title {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-featured__excerpt {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--ink-2);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.wi-featured {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-featured__card--hero .wi-featured__cover {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-featured__card--hero .wi-featured__title {
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+143
-11
@@ -1,33 +1,165 @@
|
|||||||
---
|
---
|
||||||
// TODO: Only execute when compiling Astro templates, so it will not be dynamically updated
|
const THEME_VERSION = "1.0.0";
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
---
|
---
|
||||||
|
|
||||||
<footer>
|
<footer th:with="showPowered=${theme.config?.footer?.footer_show_powered != false}, showTheme=${theme.config?.footer?.footer_show_theme != false}, hideRight=${showPowered == false and showTheme == false}">
|
||||||
<div class="footer-inner">
|
<div class="footer-inner">
|
||||||
<span>
|
<div class="footer-bottom" th:classappend="${hideRight} ? 'footer-bottom--center' : ''">
|
||||||
© {today.getFullYear()}
|
<div class="footer-bottom__left">
|
||||||
<th:block th:text="${site.title}"></th:block>.
|
<span class="footer-copyright" th:if="${theme.config?.footer?.footer_copyright}" th:text="${theme.config?.footer?.footer_copyright}"></span>
|
||||||
|
<span class="footer-copyright" th:unless="${theme.config?.footer?.footer_copyright}">
|
||||||
|
© {today.getFullYear()}
|
||||||
|
<th:block th:text="${site.title}"></th:block>
|
||||||
</span>
|
</span>
|
||||||
|
<span class="footer-icp" th:if="${theme.config?.footer?.footer_icp}">
|
||||||
|
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer" th:text="${theme.config?.footer?.footer_icp}"></a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="footer-socials" th:if="${theme.config?.footer?.footer_socials}">
|
||||||
|
<a
|
||||||
|
th:each="social : ${theme.config?.footer?.footer_socials}"
|
||||||
|
th:href="${social.url}"
|
||||||
|
th:title="${social.platform}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<i th:if="${social.icon}" th:class="${social.icon}"></i>
|
||||||
|
<span th:if="${#strings.isEmpty(social.icon)}" th:text="${social.platform}"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="footer-bottom__right" th:if="${showPowered or showTheme}">
|
||||||
|
<a th:if="${showPowered}" href="https://halo.run" target="_blank" rel="noopener noreferrer">Powered by Halo</a>
|
||||||
|
<span class="footer-separator" th:if="${showPowered and showTheme}">·</span>
|
||||||
|
<span th:if="${showTheme}">WarmIsland v{THEME_VERSION}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div th:if="${theme.config?.footer?.footer_custom_html}" th:utext="${theme.config?.footer?.footer_custom_html}" class="footer-custom"></div>
|
||||||
|
<halo:footer />
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
footer {
|
footer {
|
||||||
border-top: 1px solid var(--rule);
|
border-top: 1px solid var(--rule);
|
||||||
background: var(--bg);
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
var(--bg),
|
||||||
|
color-mix(in srgb, var(--bg) 97%, var(--accent) 3%)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-inner {
|
.footer-inner {
|
||||||
|
max-width: 1200px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 1.25rem;
|
||||||
|
min-height: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
width: 820px;
|
font-size: 0.85rem;
|
||||||
max-width: calc(100% - 2.5rem);
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 1.5rem 0 2.5rem;
|
|
||||||
color: var(--ink-3);
|
color: var(--ink-3);
|
||||||
font-size: 0.88rem;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom--center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom--center .footer-bottom__left {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom__left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-copyright {
|
||||||
|
color: var(--ink-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-icp a {
|
||||||
|
color: var(--ink-3);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-icp a:hover {
|
||||||
|
color: var(--ink-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-socials {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-socials a {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--ink-3);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-socials a:hover {
|
||||||
|
color: var(--ink-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom__right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom__right a {
|
||||||
|
color: var(--ink-3);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom__right a:hover {
|
||||||
|
color: var(--ink-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-separator {
|
||||||
|
color: var(--ink-3);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-custom {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--ink-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.footer-bottom {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom__left {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom__right {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
---
|
||||||
|
---
|
||||||
|
<section
|
||||||
|
class="wi-section wi-friends"
|
||||||
|
th:if="${theme.config?.home?.home_friends_enabled == true and pluginFinder.available('PluginLinks')}"
|
||||||
|
>
|
||||||
|
<div class="wi-container">
|
||||||
|
<div class="wi-section__header">
|
||||||
|
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_friends_title ?: '友链'}">友链</h2>
|
||||||
|
<span class="wi-section__accent"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wi-friends__grid">
|
||||||
|
<a
|
||||||
|
th:each="link : ${linkFinder.list()}"
|
||||||
|
th:href="${link.spec.url}"
|
||||||
|
th:title="${link.spec.displayName}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="wi-friends__card"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
th:if="${link.spec.logo}"
|
||||||
|
th:src="${link.spec.logo}"
|
||||||
|
th:alt="${link.spec.displayName}"
|
||||||
|
class="wi-friends__avatar"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
th:unless="${link.spec.logo}"
|
||||||
|
class="wi-friends__avatar wi-friends__avatar--placeholder"
|
||||||
|
>
|
||||||
|
<span th:text="${#strings.substring(link.spec.displayName, 0, 1)}"></span>
|
||||||
|
</div>
|
||||||
|
<div class="wi-friends__info">
|
||||||
|
<span class="wi-friends__name" th:text="${link.spec.displayName}"></span>
|
||||||
|
<span
|
||||||
|
class="wi-friends__desc"
|
||||||
|
th:if="${link.spec.description}"
|
||||||
|
th:text="${link.spec.description}"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wi-friends__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends__card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-md);
|
||||||
|
padding: var(--space-lg);
|
||||||
|
background: var(--glass-bg);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition:
|
||||||
|
transform var(--duration-normal) var(--ease-out-expo),
|
||||||
|
box-shadow var(--duration-normal) var(--ease-out-expo),
|
||||||
|
border-color var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends__card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends__avatar {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends__avatar--placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--accent-bg);
|
||||||
|
color: var(--accent);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends__info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends__name {
|
||||||
|
font-size: var(--text-md);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends__desc {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--ink-3);
|
||||||
|
line-height: var(--leading-snug);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.wi-friends__grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
---
|
|
||||||
import ThemeSwitcher from "./ThemeSwitcher.vue";
|
|
||||||
import UserButton from "./UserButton.vue";
|
|
||||||
---
|
|
||||||
|
|
||||||
<header>
|
|
||||||
<div class="header-inner">
|
|
||||||
<a class="brand" href="/" th:text="${site.title}"></a>
|
|
||||||
<nav
|
|
||||||
th:with="menu = ${menuFinder.getPrimary()}"
|
|
||||||
aria-label="Main navigation"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
th:each="menuItem : ${menu.menuItems}"
|
|
||||||
th:href="@{${menuItem.status.href}}"
|
|
||||||
th:target="${menuItem.spec.target?.value}"
|
|
||||||
th:text="${menuItem.status.displayName}"
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
<div class="header-actions">
|
|
||||||
<ThemeSwitcher client:load />
|
|
||||||
<UserButton client:load>
|
|
||||||
<div slot="fallback" class="user-button-fallback" aria-hidden="true">
|
|
||||||
<span class="user-button-fallback__avatar"></span>
|
|
||||||
<span class="user-button-fallback__text"></span>
|
|
||||||
</div>
|
|
||||||
</UserButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
header {
|
|
||||||
background: var(--bg);
|
|
||||||
border-bottom: 1px solid var(--rule);
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-inner {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 2rem;
|
|
||||||
width: 820px;
|
|
||||||
max-width: calc(100% - 2.5rem);
|
|
||||||
margin: 0 auto;
|
|
||||||
height: 58px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--ink);
|
|
||||||
text-decoration: none;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand:hover {
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.2rem;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-button-fallback {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.55rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 112px;
|
|
||||||
height: 34px;
|
|
||||||
padding: 0 0.7rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-button-fallback__avatar,
|
|
||||||
.user-button-fallback__text {
|
|
||||||
display: block;
|
|
||||||
background: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
var(--bg-raised) 0%,
|
|
||||||
color-mix(in srgb, var(--bg-raised) 78%, var(--ink) 22%) 50%,
|
|
||||||
var(--bg-raised) 100%
|
|
||||||
);
|
|
||||||
background-size: 200% 100%;
|
|
||||||
animation: user-button-fallback-shimmer 1.2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-button-fallback__avatar {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 999px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-button-fallback__text {
|
|
||||||
width: 100%;
|
|
||||||
height: 0.72rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav :global(a) {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.3rem 0.65rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--ink-2);
|
|
||||||
text-decoration: none;
|
|
||||||
transition:
|
|
||||||
color 0.15s ease,
|
|
||||||
background 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav :global(a:hover) {
|
|
||||||
color: var(--ink);
|
|
||||||
background: var(--bg-raised);
|
|
||||||
}
|
|
||||||
|
|
||||||
nav :global(a.active) {
|
|
||||||
color: var(--ink);
|
|
||||||
background: var(--bg-raised);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 680px) {
|
|
||||||
.header-inner {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
height: auto;
|
|
||||||
padding: 1rem 0;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.1rem;
|
|
||||||
margin-left: -0.65rem;
|
|
||||||
flex: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes user-button-fallback-shimmer {
|
|
||||||
0% {
|
|
||||||
background-position: 200% 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
background-position: -200% 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="hero"
|
||||||
|
th:if="${theme.config?.hero?.hero_enabled != false}"
|
||||||
|
th:classappend="${theme.config?.animation?.animation_breath == false} ? 'hero--no-breath'"
|
||||||
|
>
|
||||||
|
<div class="hero__bg">
|
||||||
|
<div class="hero__orb hero__orb--1"></div>
|
||||||
|
<div class="hero__orb hero__orb--2"></div>
|
||||||
|
<div class="hero__orb hero__orb--3"></div>
|
||||||
|
<div
|
||||||
|
class="hero__bg-image"
|
||||||
|
th:if="${theme.config?.hero?.hero_background_image}"
|
||||||
|
th:style="'background-image: url(' + ${theme.config?.hero?.hero_background_image} + ')'"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hero__content">
|
||||||
|
<h1
|
||||||
|
class="hero__title"
|
||||||
|
th:text="${theme.config?.hero?.hero_title != null and !#strings.isEmpty(theme.config?.hero?.hero_title) ? theme.config?.hero?.hero_title : site.title}"
|
||||||
|
>
|
||||||
|
暖屿
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
class="hero__subtitle"
|
||||||
|
th:text="${theme.config?.hero?.hero_subtitle != null and !#strings.isEmpty(theme.config?.hero?.hero_subtitle) ? theme.config?.hero?.hero_subtitle : site.subtitle}"
|
||||||
|
>
|
||||||
|
深夜里温暖的小岛
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="hero__description"
|
||||||
|
th:if="${theme.config?.hero?.hero_description_mode == '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}"
|
||||||
|
></p>
|
||||||
|
<p
|
||||||
|
class="hero__description"
|
||||||
|
th:if="${theme.config?.hero?.hero_description_mode == 'custom'}"
|
||||||
|
th:text="${theme.config?.hero?.hero_custom_description}"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hero__scroll-indicator" aria-hidden="true">
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
(function () {
|
||||||
|
var el = document.getElementById("wi-hitokoto");
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
var api = el.getAttribute("data-api") || "https://v1.hitokoto.cn/";
|
||||||
|
var categories = el.getAttribute("data-categories");
|
||||||
|
var url = api + "?encode=json";
|
||||||
|
|
||||||
|
if (categories) {
|
||||||
|
try {
|
||||||
|
var cats = JSON.parse(categories);
|
||||||
|
if (Array.isArray(cats)) {
|
||||||
|
cats.forEach(function (c) {
|
||||||
|
url += "&c=" + encodeURIComponent(c);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
el.style.opacity = "0.5";
|
||||||
|
el.textContent = "\u2026";
|
||||||
|
|
||||||
|
fetch(url)
|
||||||
|
.then(function (res) {
|
||||||
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(function (data) {
|
||||||
|
el.style.opacity = "1";
|
||||||
|
var text = data.hitokoto || "";
|
||||||
|
var from = data.from || data.from_who || "";
|
||||||
|
if (from) {
|
||||||
|
text += " \u2014\u2014 " + from;
|
||||||
|
}
|
||||||
|
el.textContent = text;
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
el.style.opacity = "1";
|
||||||
|
el.textContent = "";
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hero {
|
||||||
|
position: relative;
|
||||||
|
height: calc(100vh + 80px);
|
||||||
|
height: calc(100dvh + 80px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(
|
||||||
|
160deg,
|
||||||
|
var(--bg) 0%,
|
||||||
|
color-mix(in srgb, var(--bg) 85%, var(--accent) 15%) 40%,
|
||||||
|
color-mix(in srgb, var(--bg) 90%, var(--mist-pink) 10%) 70%,
|
||||||
|
var(--bg) 100%
|
||||||
|
);
|
||||||
|
margin-top: -80px;
|
||||||
|
padding-top: 80px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__orb {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(80px);
|
||||||
|
animation: breathe 5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__orb--1 {
|
||||||
|
width: 420px;
|
||||||
|
height: 420px;
|
||||||
|
top: -10%;
|
||||||
|
right: -5%;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
rgba(212, 118, 78, 0.4) 0%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
animation-delay: 0s;
|
||||||
|
animation-duration: 5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__orb--2 {
|
||||||
|
width: 350px;
|
||||||
|
height: 350px;
|
||||||
|
bottom: -8%;
|
||||||
|
left: -5%;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
rgba(220, 140, 160, 0.3) 0%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
animation-delay: -1.8s;
|
||||||
|
animation-duration: 6s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__orb--3 {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
top: 40%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
rgba(180, 150, 200, 0.2) 0%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
animation-delay: -3.2s;
|
||||||
|
animation-duration: 4.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__bg-image {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
opacity: 0.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-xl);
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__title {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: clamp(3rem, 8vw, 6rem);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
color: var(--ink);
|
||||||
|
line-height: var(--leading-tight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__subtitle {
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
color: var(--ink-2);
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
line-height: var(--leading-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__description {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
color: var(--ink-2);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
max-width: 560px;
|
||||||
|
text-align: center;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__scroll-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
color: var(--ink-3);
|
||||||
|
animation: scrollHint 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero--no-breath .hero__orb {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .hero {
|
||||||
|
background: linear-gradient(
|
||||||
|
160deg,
|
||||||
|
var(--bg) 0%,
|
||||||
|
color-mix(in srgb, var(--bg) 85%, var(--accent) 12%) 40%,
|
||||||
|
var(--bg) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .hero__orb--1 {
|
||||||
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
rgba(232, 149, 95, 0.25) 0%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .hero__orb--2 {
|
||||||
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
rgba(200, 120, 140, 0.18) 0%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .hero__orb--3 {
|
||||||
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
rgba(160, 130, 180, 0.12) 0%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero__title {
|
||||||
|
font-size: clamp(2.25rem, 10vw, 3.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__orb--1 {
|
||||||
|
width: 260px;
|
||||||
|
height: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__orb--2 {
|
||||||
|
width: 220px;
|
||||||
|
height: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__orb--3 {
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__content {
|
||||||
|
padding: var(--space-lg);
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__description {
|
||||||
|
max-width: 90vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="wi-section"
|
||||||
|
th:if="${theme.config?.home?.home_latest_enabled != false}"
|
||||||
|
>
|
||||||
|
<div class="wi-container">
|
||||||
|
<div class="wi-section__header">
|
||||||
|
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_latest_title ?: '最新文章'}">最新文章</h2>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="wi-latest"
|
||||||
|
th:classappend="|wi-latest--${theme.config?.home?.home_latest_style ?: 'magazine'}|"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
th:each="post,iterStat : ${posts.items}"
|
||||||
|
th:href="@{${post.status.permalink}}"
|
||||||
|
class="wi-latest__card"
|
||||||
|
th:classappend="${iterStat.index lt 2} ? 'wi-latest__card--hero' : ''"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="wi-latest__cover"
|
||||||
|
th:if="${!#strings.isEmpty(post.spec.cover)}"
|
||||||
|
>
|
||||||
|
<img th:src="${post.spec.cover}" th:alt="${post.spec.title}" class="wi-latest__image" />
|
||||||
|
</div>
|
||||||
|
<div class="wi-latest__body">
|
||||||
|
<div class="wi-latest__meta">
|
||||||
|
<span
|
||||||
|
class="wi-latest__category"
|
||||||
|
th:if="${!#lists.isEmpty(post.categories)}"
|
||||||
|
th:text="${post.categories[0].spec.displayName}"
|
||||||
|
></span>
|
||||||
|
<time
|
||||||
|
class="wi-latest__date"
|
||||||
|
th:text="${#dates.format(post.spec.publishTime, 'yyyy-MM-dd')}"
|
||||||
|
></time>
|
||||||
|
</div>
|
||||||
|
<h3
|
||||||
|
class="wi-latest__title"
|
||||||
|
th:text="${post.spec.title}"
|
||||||
|
></h3>
|
||||||
|
<p
|
||||||
|
class="wi-latest__excerpt"
|
||||||
|
th:if="${post.status.excerpt}"
|
||||||
|
th:text="${post.status.excerpt}"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<nav
|
||||||
|
th:if="${posts.totalPages gt 1}"
|
||||||
|
class="wi-pagination"
|
||||||
|
aria-label="Pagination"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
th:if="${posts.hasPrevious()}"
|
||||||
|
th:href="@{${posts.prevUrl}}"
|
||||||
|
class="wi-pagination__prev"
|
||||||
|
th:text="${theme.config?.home?.home_label_newer ?: '较新'}"
|
||||||
|
>← 较新</a>
|
||||||
|
<span
|
||||||
|
class="wi-pagination__info"
|
||||||
|
th:text="|${posts.page} / ${posts.totalPages}|"
|
||||||
|
></span>
|
||||||
|
<a
|
||||||
|
th:if="${posts.hasNext()}"
|
||||||
|
th:href="@{${posts.nextUrl}}"
|
||||||
|
class="wi-pagination__next"
|
||||||
|
th:text="${theme.config?.home?.home_label_older ?: '较旧'}"
|
||||||
|
>较旧 →</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wi-section__header {
|
||||||
|
margin-bottom: var(--space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-section__title {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink);
|
||||||
|
letter-spacing: var(--tracking-tight);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-section__title::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest--magazine {
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest--magazine .wi-latest__card {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest--magazine .wi-latest__card--hero {
|
||||||
|
grid-column: span 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest--magazine .wi-latest__card--hero .wi-latest__cover {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest--magazine .wi-latest__card--hero .wi-latest__title {
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest--grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest--list .wi-latest__card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest--list .wi-latest__cover {
|
||||||
|
width: 240px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest--list .wi-latest__body {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest__card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-raised);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition:
|
||||||
|
transform var(--duration-normal) var(--ease-out-expo),
|
||||||
|
box-shadow var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest__card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest__cover {
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest__image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform var(--duration-normal) var(--ease-out-expo);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest__card:hover .wi-latest__image {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest__body {
|
||||||
|
padding: var(--space-lg);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest__meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest__category {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--accent);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest__category::after {
|
||||||
|
content: '·';
|
||||||
|
margin-left: var(--space-sm);
|
||||||
|
color: var(--ink-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest__date {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--ink-3);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest__title {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink);
|
||||||
|
line-height: var(--leading-tight);
|
||||||
|
letter-spacing: var(--tracking-tight);
|
||||||
|
transition: color var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest__card:hover .wi-latest__title {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest__excerpt {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--ink-2);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding-top: var(--space-xl);
|
||||||
|
margin-top: var(--space-xl);
|
||||||
|
border-top: 1px solid var(--rule);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pagination__prev,
|
||||||
|
.wi-pagination__next {
|
||||||
|
color: var(--ink-2);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pagination__prev:hover,
|
||||||
|
.wi-pagination__next:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pagination__info {
|
||||||
|
color: var(--ink-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1023px) {
|
||||||
|
.wi-latest--magazine {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest--magazine .wi-latest__card {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest--magazine .wi-latest__card--hero {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest--grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.wi-latest--magazine,
|
||||||
|
.wi-latest--grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest--magazine .wi-latest__card--hero {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest--list .wi-latest__card {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-latest--list .wi-latest__cover {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import 'lightgallery/css/lightgallery-bundle.css';
|
||||||
|
import lightGallery from 'lightgallery';
|
||||||
|
import lgZoom from 'lightgallery/plugins/zoom';
|
||||||
|
import lgThumbnail from 'lightgallery/plugins/thumbnail';
|
||||||
|
import lgFullscreen from 'lightgallery/plugins/fullscreen';
|
||||||
|
import lgRotate from 'lightgallery/plugins/rotate';
|
||||||
|
import lgAutoplay from 'lightgallery/plugins/autoplay';
|
||||||
|
|
||||||
|
var _initialized = new WeakSet();
|
||||||
|
|
||||||
|
function isInitialized(el) {
|
||||||
|
if (_initialized.has(el)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function markInitialized(el) {
|
||||||
|
_initialized.add(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildExifHtml(exifStr, caption) {
|
||||||
|
if (!exifStr) return caption || '';
|
||||||
|
var parts = exifStr.split('|');
|
||||||
|
if (parts.length < 8) return caption || '';
|
||||||
|
var make = parts[0], model = parts[1], lensModel = parts[2];
|
||||||
|
var fNumber = parts[3], exposureTime = parts[4], iso = parts[5];
|
||||||
|
var focalLength = parts[6], focalLengthIn35mm = parts[7];
|
||||||
|
var hasAnyExif = make || model || lensModel || fNumber || exposureTime || iso || focalLength;
|
||||||
|
if (!hasAnyExif) return caption || '';
|
||||||
|
var html = '<div class="wi-lg-caption">' + (caption || '') + '</div>';
|
||||||
|
html += '<div class="wi-lg-exif">';
|
||||||
|
if (make || model) {
|
||||||
|
html += '<span class="wi-lg-exif__item wi-lg-exif__camera">' + (make ? make + ' ' : '') + model + '</span>';
|
||||||
|
}
|
||||||
|
if (lensModel) {
|
||||||
|
html += '<span class="wi-lg-exif__item wi-lg-exif__lens">' + lensModel + '</span>';
|
||||||
|
}
|
||||||
|
html += '<div class="wi-lg-exif__params">';
|
||||||
|
if (fNumber) html += '<span class="wi-lg-exif__item wi-lg-exif__aperture">f/' + fNumber + '</span>';
|
||||||
|
if (exposureTime) html += '<span class="wi-lg-exif__item wi-lg-exif__shutter">' + exposureTime + 's</span>';
|
||||||
|
if (iso) html += '<span class="wi-lg-exif__item wi-lg-exif__iso">ISO ' + iso + '</span>';
|
||||||
|
if (focalLength) {
|
||||||
|
var focalText = focalLength + 'mm';
|
||||||
|
if (focalLengthIn35mm && focalLengthIn35mm !== focalLength) {
|
||||||
|
focalText += ' (eq. ' + focalLengthIn35mm + 'mm)';
|
||||||
|
}
|
||||||
|
html += '<span class="wi-lg-exif__item wi-lg-exif__focal">' + focalText + '</span>';
|
||||||
|
}
|
||||||
|
html += '</div></div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function preprocessExif(el) {
|
||||||
|
var items = el.querySelectorAll('.wi-lightgallery-item[data-exif]');
|
||||||
|
items.forEach(function(item) {
|
||||||
|
var exifStr = item.getAttribute('data-exif');
|
||||||
|
if (!exifStr) return;
|
||||||
|
var caption = item.getAttribute('data-sub-html') || '';
|
||||||
|
var html = buildExifHtml(exifStr, caption);
|
||||||
|
if (html && html !== caption) {
|
||||||
|
item.setAttribute('data-sub-html', html);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLgConfig() {
|
||||||
|
return {
|
||||||
|
selector: '.wi-lightgallery-item',
|
||||||
|
plugins: [lgZoom, lgThumbnail, lgFullscreen, lgRotate, lgAutoplay],
|
||||||
|
speed: 280,
|
||||||
|
mode: 'lg-fade',
|
||||||
|
licenseKey: '0000-0000-0000-0000',
|
||||||
|
download: false,
|
||||||
|
counter: true,
|
||||||
|
preload: 2,
|
||||||
|
thumbWidth: 80,
|
||||||
|
thumbHeight: '60px',
|
||||||
|
thumbMargin: 4,
|
||||||
|
zoomFromOrigin: true,
|
||||||
|
actualSize: false,
|
||||||
|
showZoomInOutIcons: true,
|
||||||
|
toggleThumb: true,
|
||||||
|
allowMediaOverlap: true,
|
||||||
|
autoplay: false,
|
||||||
|
slideDelay: 5000,
|
||||||
|
progressBar: true,
|
||||||
|
rotate: true,
|
||||||
|
flipHorizontal: false,
|
||||||
|
flipVertical: false,
|
||||||
|
mobileSettings: {
|
||||||
|
controls: true,
|
||||||
|
showCloseIcon: true,
|
||||||
|
download: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function initLg(el) {
|
||||||
|
if (isInitialized(el)) return;
|
||||||
|
var items = el.querySelectorAll('.wi-lightgallery-item');
|
||||||
|
if (items.length === 0) return;
|
||||||
|
preprocessExif(el);
|
||||||
|
lightGallery(el, createLgConfig());
|
||||||
|
markInitialized(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initLightGallery() {
|
||||||
|
initContentPages();
|
||||||
|
initPhotosPage();
|
||||||
|
initMomentsPage();
|
||||||
|
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
var toggleBtn = e.target.closest('.lg-toggle-thumb');
|
||||||
|
if (toggleBtn) {
|
||||||
|
e.stopPropagation();
|
||||||
|
var lgOuter = toggleBtn.closest('.lg-outer');
|
||||||
|
if (lgOuter) {
|
||||||
|
lgOuter.classList.toggle('wi-thumb-shown');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initContentPages() {
|
||||||
|
var selectors = ['.wi-post__body', '.wi-page__body'];
|
||||||
|
selectors.forEach(function(sel) {
|
||||||
|
var els = document.querySelectorAll(sel);
|
||||||
|
els.forEach(function(el) {
|
||||||
|
processContentImages(el);
|
||||||
|
initLg(el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initPhotosPage() {
|
||||||
|
var allGroups = document.querySelector('.wi-photos-page__all-groups');
|
||||||
|
if (allGroups) {
|
||||||
|
var links = allGroups.querySelectorAll('.wi-photos-page__link');
|
||||||
|
links.forEach(function(link) {
|
||||||
|
link.classList.add('wi-lightgallery-item');
|
||||||
|
});
|
||||||
|
initLg(allGroups);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var singleGrid = document.querySelector('.wi-photos-page__grid:not(.wi-photos-page__all-groups .wi-photos-page__grid)');
|
||||||
|
if (singleGrid) {
|
||||||
|
var links2 = singleGrid.querySelectorAll('.wi-photos-page__link');
|
||||||
|
links2.forEach(function(link) {
|
||||||
|
link.classList.add('wi-lightgallery-item');
|
||||||
|
});
|
||||||
|
initLg(singleGrid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var grids = document.querySelectorAll('.wi-photos-page__grid');
|
||||||
|
grids.forEach(function(el) {
|
||||||
|
processPhotosGrid(el);
|
||||||
|
initLg(el);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initMomentsPage() {
|
||||||
|
var selectors = ['.wi-moments-page__media', '.wi-moments-page__card-media', '.wi-moments-page__masonry-media'];
|
||||||
|
selectors.forEach(function(sel) {
|
||||||
|
var els = document.querySelectorAll(sel);
|
||||||
|
els.forEach(function(el) {
|
||||||
|
processMomentsMedia(el);
|
||||||
|
initLg(el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function processContentImages(container) {
|
||||||
|
var images = container.querySelectorAll('img');
|
||||||
|
images.forEach(function(img) {
|
||||||
|
var src = img.getAttribute('src');
|
||||||
|
if (!src) return;
|
||||||
|
|
||||||
|
var w = img.getAttribute('width');
|
||||||
|
var h = img.getAttribute('height');
|
||||||
|
if ((w && parseInt(w) < 50) || (h && parseInt(h) < 50)) return;
|
||||||
|
|
||||||
|
var className = (img.getAttribute('class') || '').toLowerCase();
|
||||||
|
if (/emoji|icon|avatar|logo|badge|smiley/.test(className)) return;
|
||||||
|
|
||||||
|
if (img.getAttribute('role') === 'presentation') return;
|
||||||
|
if (img.getAttribute('aria-hidden') === 'true') return;
|
||||||
|
|
||||||
|
var parentA = img.closest('a');
|
||||||
|
if (parentA) {
|
||||||
|
if (parentA.getAttribute('target') === '_blank') return;
|
||||||
|
parentA.classList.add('wi-lightgallery-item');
|
||||||
|
if (!parentA.getAttribute('data-src')) {
|
||||||
|
var href = parentA.getAttribute('href') || src;
|
||||||
|
parentA.setAttribute('data-src', href);
|
||||||
|
}
|
||||||
|
var alt = img.getAttribute('alt') || '';
|
||||||
|
if (alt && !parentA.getAttribute('data-sub-html')) {
|
||||||
|
parentA.setAttribute('data-sub-html', alt);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.setAttribute('href', src);
|
||||||
|
a.setAttribute('data-src', src);
|
||||||
|
a.classList.add('wi-lightgallery-item');
|
||||||
|
var alt2 = img.getAttribute('alt') || '';
|
||||||
|
if (alt2) {
|
||||||
|
a.setAttribute('data-sub-html', alt2);
|
||||||
|
}
|
||||||
|
img.parentNode.insertBefore(a, img);
|
||||||
|
a.appendChild(img);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function processPhotosGrid(container) {
|
||||||
|
var links = container.querySelectorAll('.wi-photos-page__link');
|
||||||
|
links.forEach(function(link) {
|
||||||
|
link.classList.add('wi-lightgallery-item');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function processMomentsMedia(container) {
|
||||||
|
var images = container.querySelectorAll('img');
|
||||||
|
images.forEach(function(img) {
|
||||||
|
var src = img.getAttribute('src');
|
||||||
|
if (!src) return;
|
||||||
|
|
||||||
|
var parentA = img.closest('a');
|
||||||
|
if (parentA) {
|
||||||
|
parentA.classList.add('wi-lightgallery-item');
|
||||||
|
if (!parentA.getAttribute('data-src')) {
|
||||||
|
parentA.setAttribute('data-src', src);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.setAttribute('href', src);
|
||||||
|
a.setAttribute('data-src', src);
|
||||||
|
a.classList.add('wi-lightgallery-item');
|
||||||
|
img.parentNode.insertBefore(a, img);
|
||||||
|
a.appendChild(img);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initLightGallery);
|
||||||
|
} else {
|
||||||
|
initLightGallery();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
---
|
||||||
|
---
|
||||||
|
<section
|
||||||
|
class="wi-section wi-message-wall"
|
||||||
|
th:if="${theme.config?.home?.home_message_wall_enabled == true}"
|
||||||
|
>
|
||||||
|
<div class="wi-container">
|
||||||
|
<div class="wi-section__header">
|
||||||
|
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_message_wall_title ?: '留言墙'}">留言墙</h2>
|
||||||
|
<span class="wi-section__accent"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wi-message-wall__grid">
|
||||||
|
<div
|
||||||
|
class="wi-message-wall__card"
|
||||||
|
th:each="comment : ${commentFinder.list()}"
|
||||||
|
>
|
||||||
|
<div class="wi-message-wall__head">
|
||||||
|
<img
|
||||||
|
th:if="${comment.spec.owner.avatar}"
|
||||||
|
th:src="${comment.spec.owner.avatar}"
|
||||||
|
th:alt="${comment.spec.owner.displayName}"
|
||||||
|
class="wi-message-wall__avatar"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
th:unless="${comment.spec.owner.avatar}"
|
||||||
|
class="wi-message-wall__avatar wi-message-wall__avatar--placeholder"
|
||||||
|
>
|
||||||
|
<span th:text="${#strings.substring(comment.spec.owner.displayName, 0, 1)}"></span>
|
||||||
|
</div>
|
||||||
|
<div class="wi-message-wall__meta">
|
||||||
|
<span class="wi-message-wall__name" th:text="${comment.spec.owner.displayName}"></span>
|
||||||
|
<time
|
||||||
|
class="wi-message-wall__date"
|
||||||
|
th:text="${#dates.format(comment.metadata.creationTimestamp, 'yyyy-MM-dd')}"
|
||||||
|
></time>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="wi-message-wall__text" th:text="${comment.spec.content}"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wi-message-wall__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-message-wall__card {
|
||||||
|
padding: var(--space-lg);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--rule);
|
||||||
|
transition:
|
||||||
|
transform var(--duration-normal) var(--ease-out-expo),
|
||||||
|
box-shadow var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-message-wall__card:nth-child(6n + 1) {
|
||||||
|
background: var(--accent-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-message-wall__card:nth-child(6n + 2) {
|
||||||
|
background: var(--mist-pink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-message-wall__card:nth-child(6n + 3) {
|
||||||
|
background: var(--bg-raised);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-message-wall__card:nth-child(6n + 4) {
|
||||||
|
background: color-mix(in srgb, var(--bg) 85%, var(--accent) 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-message-wall__card:nth-child(6n + 5) {
|
||||||
|
background: color-mix(in srgb, var(--bg) 90%, var(--caramel) 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-message-wall__card:nth-child(6n + 6) {
|
||||||
|
background: var(--sea-salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-message-wall__card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-message-wall__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-message-wall__avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-message-wall__avatar--placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--accent-bg);
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-message-wall__meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-message-wall__name {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-message-wall__date {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--ink-3);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-message-wall__text {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--ink-2);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
margin: 0;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.wi-message-wall__grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
---
|
||||||
|
---
|
||||||
|
<div class="wi-mobile-menu" id="wi-mobile-menu" aria-hidden="true">
|
||||||
|
<div class="wi-mobile-menu__backdrop"></div>
|
||||||
|
<div class="wi-mobile-menu__panel">
|
||||||
|
<div class="wi-mobile-menu__header">
|
||||||
|
<button class="wi-mobile-menu__close" type="button" aria-label="Close menu">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="wi-mobile-menu__nav" aria-label="Mobile navigation">
|
||||||
|
<th:block th:if="${menuFinder != null}">
|
||||||
|
<th:block th:with="menu = ${menuFinder.getPrimary()}">
|
||||||
|
<th:block th:if="${menu != null and menu.menuItems != null and not #lists.isEmpty(menu.menuItems)}">
|
||||||
|
<a
|
||||||
|
th:each="menuItem : ${menu.menuItems}"
|
||||||
|
th:href="@{${menuItem.status.href}}"
|
||||||
|
th:target="${menuItem.spec.target}"
|
||||||
|
th:text="${menuItem.status.displayName}"
|
||||||
|
class="wi-mobile-menu__link"
|
||||||
|
th:data-href="${menuItem.status.href}"
|
||||||
|
></a>
|
||||||
|
</th:block>
|
||||||
|
</th:block>
|
||||||
|
</th:block>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="wi-mobile-menu__footer">
|
||||||
|
<button
|
||||||
|
th:if="${pluginFinder.available('PluginSearchWidget')}"
|
||||||
|
class="wi-mobile-menu__action-btn"
|
||||||
|
type="button"
|
||||||
|
aria-label="Search"
|
||||||
|
onclick="SearchWidget.open()"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||||
|
<span th:text="${theme.config?.basic?.label_search ?: '搜索'}">搜索</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="wi-mobile-menu__action-btn wi-mobile-menu__theme-btn" type="button" aria-label="Toggle theme">
|
||||||
|
<svg class="wi-mobile-menu__icon-moon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
|
||||||
|
<svg class="wi-mobile-menu__icon-sun" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
|
||||||
|
<span class="wi-mobile-menu__theme-label" th:text="${theme.config?.basic?.label_theme_switch ?: '切换主题'}">切换主题</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
const menu = document.getElementById("wi-mobile-menu");
|
||||||
|
const backdrop = menu?.querySelector(".wi-mobile-menu__backdrop");
|
||||||
|
const closeBtn = menu?.querySelector(".wi-mobile-menu__close");
|
||||||
|
const themeBtn = menu?.querySelector(".wi-mobile-menu__theme-btn");
|
||||||
|
let isOpen = false;
|
||||||
|
|
||||||
|
function openMenu() {
|
||||||
|
isOpen = true;
|
||||||
|
menu?.classList.add("wi-mobile-menu--open");
|
||||||
|
menu?.setAttribute("aria-hidden", "false");
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMenu() {
|
||||||
|
isOpen = false;
|
||||||
|
menu?.classList.remove("wi-mobile-menu--open");
|
||||||
|
menu?.setAttribute("aria-hidden", "true");
|
||||||
|
document.body.style.overflow = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("wi:toggle-mobile-menu", () => {
|
||||||
|
if (isOpen) {
|
||||||
|
closeMenu();
|
||||||
|
} else {
|
||||||
|
openMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
closeBtn?.addEventListener("click", closeMenu);
|
||||||
|
backdrop?.addEventListener("click", closeMenu);
|
||||||
|
|
||||||
|
themeBtn?.addEventListener("click", () => {
|
||||||
|
document.documentElement.classList.add("wi-theme-transition");
|
||||||
|
const isDark = document.documentElement.classList.toggle("dark");
|
||||||
|
localStorage.setItem("wi-theme", isDark ? "dark" : "light");
|
||||||
|
document.documentElement.setAttribute("data-color-scheme", isDark ? "dark" : "light");
|
||||||
|
setTimeout(() => {
|
||||||
|
document.documentElement.classList.remove("wi-theme-transition");
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
menu?.querySelectorAll(".wi-mobile-menu__link").forEach((link) => {
|
||||||
|
link.addEventListener("click", closeMenu);
|
||||||
|
});
|
||||||
|
|
||||||
|
menu?.querySelectorAll('.wi-mobile-menu__link[data-href]').forEach(function(link) {
|
||||||
|
try {
|
||||||
|
if (new URL(link.href).pathname === window.location.pathname) {
|
||||||
|
link.classList.add('wi-mobile-menu__link--active');
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape" && isOpen) {
|
||||||
|
closeMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wi-mobile-menu {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 200;
|
||||||
|
pointer-events: none;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-mobile-menu--open {
|
||||||
|
pointer-events: auto;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-mobile-menu__backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-mobile-menu--open .wi-mobile-menu__backdrop {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-mobile-menu__panel {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: min(85vw, 360px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--glass-bg);
|
||||||
|
backdrop-filter: blur(24px) saturate(180%);
|
||||||
|
-webkit-backdrop-filter: blur(24px) saturate(180%);
|
||||||
|
border-left: 1px solid var(--glass-border);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition: transform var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-mobile-menu--open .wi-mobile-menu__panel {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-mobile-menu__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-bottom: 1px solid var(--glass-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-mobile-menu__close {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ink-2);
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background var(--duration-fast) var(--ease-out-quart),
|
||||||
|
color var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-mobile-menu__close:hover {
|
||||||
|
background: var(--bg-raised);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-mobile-menu__close:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-mobile-menu__nav {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-mobile-menu__link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--ink-2);
|
||||||
|
text-decoration: none;
|
||||||
|
transition:
|
||||||
|
color var(--duration-fast) var(--ease-out-quart),
|
||||||
|
background var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-mobile-menu__link:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
background: var(--bg-raised);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-mobile-menu__link--active {
|
||||||
|
color: var(--accent);
|
||||||
|
background: var(--bg-raised);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-mobile-menu__footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-top: 1px solid var(--glass-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-mobile-menu__action-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ink-2);
|
||||||
|
font-size: var(--text-md);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
color var(--duration-fast) var(--ease-out-quart),
|
||||||
|
background var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-mobile-menu__action-btn:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
background: var(--bg-raised);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-mobile-menu__action-btn:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-mobile-menu__icon-sun {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .wi-mobile-menu__icon-moon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .wi-mobile-menu__icon-sun {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
---
|
||||||
|
import { findPageBySlug } from "../lib/pages";
|
||||||
|
|
||||||
|
const pageConfig = findPageBySlug("moments");
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<Fragment>
|
||||||
|
<th:block th:if="${pluginFinder.available('PluginMoments')}">
|
||||||
|
<section class="wi-moments-section">
|
||||||
|
<div class="wi-container">
|
||||||
|
<h2 class="wi-moments-section__title" th:text="${pageConfig?.title ?: theme.config?.moments?.moments_title ?: '瞬间'}">瞬间</h2>
|
||||||
|
<div
|
||||||
|
class="wi-moments-section__list"
|
||||||
|
th:attr="data-style=${theme.config?.moments?.moments_style ?: 'timeline'}"
|
||||||
|
>
|
||||||
|
<th:block th:with="limit = ${theme.config?.moments?.moments_count ?: 5}, momentsResult = ${momentFinder.list(1, limit)}">
|
||||||
|
<div
|
||||||
|
class="wi-moments-section__item"
|
||||||
|
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__content" th:utext="${moment.spec?.content?.html ?: moment.spec?.content?.raw ?: moment.spec?.content}"></div>
|
||||||
|
<time
|
||||||
|
class="wi-moments-section__date"
|
||||||
|
th:text="${#dates.format(moment.spec?.releaseTime ?: moment.metadata?.creationTimestamp, 'yyyy-MM-dd HH:mm')}"
|
||||||
|
></time>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
<a th:href="@{'/moments'}" class="wi-moments-section__more">
|
||||||
|
<span th:text="${theme.config?.home?.home_label_view_all ?: '查看全部'}">查看全部</span>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</th:block>
|
||||||
|
</Fragment>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wi-moments-section {
|
||||||
|
padding-block: var(--section-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-section__title {
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--space-2xl);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-section__title::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 48px;
|
||||||
|
height: 3px;
|
||||||
|
margin-inline: auto;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--accent);
|
||||||
|
margin-top: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-section__list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-section__list[data-style="timeline"] {
|
||||||
|
padding-left: var(--space-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-section__item {
|
||||||
|
position: relative;
|
||||||
|
background: var(--bg-raised);
|
||||||
|
border: 1px solid var(--rule);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: var(--space-xl);
|
||||||
|
transition:
|
||||||
|
box-shadow var(--duration-normal) var(--ease-out-expo),
|
||||||
|
transform var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-section__item:hover {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-section__list[data-style="timeline"] .wi-moments-section__item {
|
||||||
|
margin-left: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-section__list[data-style="timeline"] .wi-moments-section__dot {
|
||||||
|
position: absolute;
|
||||||
|
left: calc(-1 * var(--space-2xl) - 5px);
|
||||||
|
top: 28px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--accent);
|
||||||
|
box-shadow: 0 0 0 3px var(--bg), 0 0 0 4px var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-section__content {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
color: var(--ink);
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-section__date {
|
||||||
|
display: block;
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--ink-3);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-section__more {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
margin-top: var(--space-xl);
|
||||||
|
margin-inline: auto;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--ink-2);
|
||||||
|
transition: color var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-section__more:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-section__more svg {
|
||||||
|
transition: transform var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-section__more:hover svg {
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.wi-moments-section__list[data-style="timeline"] {
|
||||||
|
padding-left: var(--space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-section__list[data-style="timeline"] .wi-moments-section__item {
|
||||||
|
margin-left: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-section__list[data-style="timeline"] .wi-moments-section__dot {
|
||||||
|
left: calc(-1 * var(--space-xl) - 5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
---
|
||||||
|
import ThemeSwitcher from "./ThemeSwitcher.vue";
|
||||||
|
---
|
||||||
|
|
||||||
|
<header class="wi-navbar-wrapper" id="wi-navbar" th:classappend="${theme.config?.navbar?.navbar_style == 'minimal'} ? 'wi-navbar--minimal' : (${theme.config?.navbar?.navbar_style == 'float'} ? 'wi-navbar--float' : 'wi-navbar--glass')">
|
||||||
|
<div class="wi-navbar__capsule">
|
||||||
|
<a class="wi-navbar__brand" th:href="@{/}">
|
||||||
|
<img
|
||||||
|
th:if="${theme.config?.basic?.logo}"
|
||||||
|
th:src="${theme.config?.basic?.logo}"
|
||||||
|
alt=""
|
||||||
|
class="wi-navbar__logo"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
th:if="${#strings.isEmpty(theme.config?.basic?.logo)}"
|
||||||
|
class="wi-navbar__title"
|
||||||
|
th:text="${site.title}"
|
||||||
|
></span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<nav
|
||||||
|
class="wi-navbar__nav"
|
||||||
|
aria-label="Main navigation"
|
||||||
|
>
|
||||||
|
<th:block th:if="${menuFinder != null}">
|
||||||
|
<th:block th:with="menu = ${menuFinder.getPrimary()}">
|
||||||
|
<th:block th:if="${menu != null and menu.menuItems != null and not #lists.isEmpty(menu.menuItems)}">
|
||||||
|
<a
|
||||||
|
th:each="menuItem : ${menu.menuItems}"
|
||||||
|
th:href="@{${menuItem.status.href}}"
|
||||||
|
th:target="${menuItem.spec.target}"
|
||||||
|
th:text="${menuItem.status.displayName}"
|
||||||
|
class="wi-navbar__link"
|
||||||
|
th:data-href="${menuItem.status.href}"
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
</th:block>
|
||||||
|
</th:block>
|
||||||
|
</th:block>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="wi-navbar__actions">
|
||||||
|
<button
|
||||||
|
th:if="${pluginFinder.available('PluginSearchWidget') and theme.config?.navbar?.navbar_show_search != false}"
|
||||||
|
class="wi-navbar__action-btn"
|
||||||
|
type="button"
|
||||||
|
aria-label="Search"
|
||||||
|
onclick="SearchWidget.open()"
|
||||||
|
>
|
||||||
|
<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"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<th:block th:if="${theme.config?.navbar?.navbar_show_theme_switch != false}">
|
||||||
|
<ThemeSwitcher client:load />
|
||||||
|
</th:block>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="wi-navbar__action-btn wi-navbar__menu-btn"
|
||||||
|
type="button"
|
||||||
|
aria-label="Toggle menu"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
const navbar = document.getElementById("wi-navbar");
|
||||||
|
let lastScrollY = window.scrollY;
|
||||||
|
let ticking = false;
|
||||||
|
|
||||||
|
function onScroll() {
|
||||||
|
if (!ticking) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const currentScrollY = window.scrollY;
|
||||||
|
|
||||||
|
if (currentScrollY > 20) {
|
||||||
|
navbar?.classList.add("wi-navbar--scrolled");
|
||||||
|
} else {
|
||||||
|
navbar?.classList.remove("wi-navbar--scrolled");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentScrollY > lastScrollY && currentScrollY > 80) {
|
||||||
|
navbar?.classList.add("wi-navbar--hidden");
|
||||||
|
} else if (currentScrollY < lastScrollY) {
|
||||||
|
navbar?.classList.remove("wi-navbar--hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
lastScrollY = currentScrollY;
|
||||||
|
ticking = false;
|
||||||
|
});
|
||||||
|
ticking = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("scroll", onScroll, { passive: true });
|
||||||
|
|
||||||
|
document.querySelectorAll('.wi-navbar__link[data-href]').forEach(function(link) {
|
||||||
|
try {
|
||||||
|
if (new URL(link.href).pathname === window.location.pathname) {
|
||||||
|
link.classList.add('wi-navbar__link--active');
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector(".wi-navbar__menu-btn")?.addEventListener("click", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("wi:toggle-mobile-menu"));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wi-navbar-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
pointer-events: none;
|
||||||
|
transition:
|
||||||
|
transform 0.35s cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
|
padding var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar--hidden {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar__capsule {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 56px;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
pointer-events: auto;
|
||||||
|
transition:
|
||||||
|
background var(--duration-normal) var(--ease-out-expo),
|
||||||
|
box-shadow var(--duration-normal) var(--ease-out-expo),
|
||||||
|
transform var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar--glass .wi-navbar__capsule {
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--glass-bg);
|
||||||
|
backdrop-filter: blur(20px) saturate(180%);
|
||||||
|
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
max-width: 900px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar--glass.wi-navbar--scrolled .wi-navbar__capsule {
|
||||||
|
background: var(--bg-overlay);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar--minimal {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar--minimal .wi-navbar__capsule {
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
backdrop-filter: none;
|
||||||
|
-webkit-backdrop-filter: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0 2rem;
|
||||||
|
transition: background 0.3s ease, border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar--minimal.wi-navbar--scrolled .wi-navbar__capsule {
|
||||||
|
background: var(--bg);
|
||||||
|
border-bottom-color: var(--rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar--float .wi-navbar__capsule {
|
||||||
|
border-radius: 16px;
|
||||||
|
background: var(--glass-bg);
|
||||||
|
backdrop-filter: blur(24px) saturate(180%);
|
||||||
|
-webkit-backdrop-filter: blur(24px) saturate(180%);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
max-width: 800px;
|
||||||
|
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), background 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar--float.wi-navbar--scrolled .wi-navbar__capsule {
|
||||||
|
transform: scale(0.96);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar__brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar__logo {
|
||||||
|
height: 32px;
|
||||||
|
width: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar__title {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: var(--text-md);
|
||||||
|
color: var(--ink);
|
||||||
|
letter-spacing: var(--tracking-tight);
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: color var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar__brand:hover .wi-navbar__title {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar__nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar__link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.3rem 0.7rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--ink-2);
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition:
|
||||||
|
color var(--duration-fast) var(--ease-out-quart),
|
||||||
|
background var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar__link:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
background: var(--bg-raised);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar__link--active {
|
||||||
|
color: var(--ink);
|
||||||
|
background: var(--bg-raised);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar__action-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ink-2);
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background var(--duration-fast) var(--ease-out-quart),
|
||||||
|
color var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar__action-btn:hover {
|
||||||
|
background: var(--bg-raised);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar__action-btn:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar__menu-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.wi-navbar-wrapper {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar__capsule {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar__nav {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar__action-btn[aria-label="Search"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-navbar__menu-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
---
|
||||||
|
import { findPageBySlug } from "../lib/pages";
|
||||||
|
|
||||||
|
const pageConfig = findPageBySlug("photos");
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<Fragment>
|
||||||
|
<th:block th:if="${pluginFinder.available('PluginPhotos')}">
|
||||||
|
<section class="wi-photos-section">
|
||||||
|
<div class="wi-container">
|
||||||
|
<h2 class="wi-photos-section__title" th:text="${pageConfig?.title ?: theme.config?.photos?.photos_title ?: '图库'}">图库</h2>
|
||||||
|
<div
|
||||||
|
class="wi-photos-section__grid"
|
||||||
|
th:attr="data-style=${theme.config?.photos?.photos_style ?: 'masonry'}, data-columns=${theme.config?.photos?.photos_columns ?: 3}"
|
||||||
|
>
|
||||||
|
<th:block th:with="limit = ${theme.config?.photos?.photos_count ?: 6}">
|
||||||
|
<th:block th:each="group : ${photoFinder.groupBy()}">
|
||||||
|
<div
|
||||||
|
class="wi-photos-section__item"
|
||||||
|
th:each="photo, stat : ${group.photos}"
|
||||||
|
th:if="${stat.index < limit}"
|
||||||
|
>
|
||||||
|
<a th:href="@{'/photos'}" class="wi-photos-section__link">
|
||||||
|
<div class="wi-photos-section__wrap">
|
||||||
|
<img
|
||||||
|
th:src="${photo.spec.url}"
|
||||||
|
th:alt="${photo.spec.displayName ?: ''}"
|
||||||
|
class="wi-photos-section__image"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
<a th:href="@{'/photos'}" class="wi-photos-section__more">
|
||||||
|
<span th:text="${theme.config?.home?.home_label_view_all ?: '查看全部'}">查看全部</span>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</th:block>
|
||||||
|
</Fragment>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wi-photos-section {
|
||||||
|
padding-block: var(--section-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-photos-section__title {
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--space-2xl);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-photos-section__title::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 48px;
|
||||||
|
height: 3px;
|
||||||
|
margin-inline: auto;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--accent);
|
||||||
|
margin-top: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-photos-section__grid[data-style="masonry"] {
|
||||||
|
columns: 3;
|
||||||
|
column-gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-photos-section__grid[data-style="masonry"][data-columns="2"] {
|
||||||
|
columns: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-photos-section__grid[data-style="grid"] {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-photos-section__grid[data-style="grid"][data-columns="2"] {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-photos-section__grid[data-style="masonry"] .wi-photos-section__item {
|
||||||
|
break-inside: avoid;
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-photos-section__wrap {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
transition:
|
||||||
|
box-shadow var(--duration-normal) var(--ease-out-expo),
|
||||||
|
transform var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-photos-section__wrap:hover {
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-photos-section__link {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-photos-section__image {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 0;
|
||||||
|
transition: transform var(--duration-slow) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-photos-section__grid[data-style="grid"] .wi-photos-section__image {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-photos-section__wrap:hover .wi-photos-section__image {
|
||||||
|
transform: scale(1.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-photos-section__more {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
margin-top: var(--space-xl);
|
||||||
|
margin-inline: auto;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--ink-2);
|
||||||
|
transition: color var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-photos-section__more:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-photos-section__more svg {
|
||||||
|
transition: transform var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-photos-section__more:hover svg {
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.wi-photos-section__grid[data-style="masonry"] {
|
||||||
|
columns: 2;
|
||||||
|
column-gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-photos-section__grid[data-style="masonry"] .wi-photos-section__item {
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-photos-section__grid[data-style="grid"] {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
variant?: "default" | "featured" | "compact";
|
||||||
|
}
|
||||||
|
|
||||||
|
const { variant = "default" } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<a
|
||||||
|
class:list={["post-card", `post-card--${variant}`]}
|
||||||
|
th:href="@{${post.status.permalink}}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
th:unless="${#strings.isEmpty(post.spec.cover)}"
|
||||||
|
class="post-card__image-wrap"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="post-card__image"
|
||||||
|
th:src="${post.spec.cover}"
|
||||||
|
th:srcset="|${thumbnail.gen(post.spec.cover, 's')} 400w,
|
||||||
|
${thumbnail.gen(post.spec.cover, 'm')} 800w,
|
||||||
|
${thumbnail.gen(post.spec.cover, 'l')} 1200w|"
|
||||||
|
th:alt="${post.spec.title}"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
<div class="post-card__overlay"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="post-card__content">
|
||||||
|
<time
|
||||||
|
class="post-card__date"
|
||||||
|
th:text="${#dates.format(post.spec.publishTime, 'yyyy-MM-dd')}"
|
||||||
|
></time>
|
||||||
|
<h3 class="post-card__title" th:text="${post.spec.title}"></h3>
|
||||||
|
<p
|
||||||
|
th:if="${variant != 'compact'}"
|
||||||
|
class="post-card__excerpt"
|
||||||
|
th:text="${post.status.excerpt}"
|
||||||
|
></p>
|
||||||
|
<div
|
||||||
|
th:if="${variant != 'compact' and !#lists.isEmpty(post.tags)}"
|
||||||
|
class="post-card__tags"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
th:each="tag : ${post.tags}"
|
||||||
|
class="post-card__tag"
|
||||||
|
th:text="${tag.spec.displayName}"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.post-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
background: var(--bg-raised);
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
transition:
|
||||||
|
transform 400ms var(--ease-out-expo),
|
||||||
|
box-shadow 400ms var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card__image-wrap {
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card__image {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 400ms var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card:hover .post-card__image {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card__overlay {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
padding: var(--space-lg);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card__date {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: var(--tracking-wider);
|
||||||
|
color: var(--ink-3);
|
||||||
|
line-height: var(--leading-none);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card__title {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: clamp(1.1rem, 2vw, 1.5rem);
|
||||||
|
line-height: var(--leading-tight);
|
||||||
|
color: var(--ink);
|
||||||
|
margin: 0;
|
||||||
|
transition: color 400ms var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card:hover .post-card__title {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card__excerpt {
|
||||||
|
color: var(--ink-2);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
line-height: var(--leading-normal);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card__tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card__tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.2em 0.6em;
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
background: var(--bg-raised);
|
||||||
|
border: 1px solid var(--rule);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--ink-2);
|
||||||
|
line-height: var(--leading-none);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card--default {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card--default .post-card__image-wrap {
|
||||||
|
width: 40%;
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card--featured {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card--featured .post-card__image-wrap {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card--featured .post-card__overlay {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 50%;
|
||||||
|
background: linear-gradient(
|
||||||
|
to top,
|
||||||
|
var(--glass-bg),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
-webkit-backdrop-filter: blur(4px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card--featured .post-card__content {
|
||||||
|
padding: var(--space-xl);
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card--featured .post-card__title {
|
||||||
|
font-size: clamp(1.5rem, 3vw, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card--featured .post-card__excerpt {
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card--compact {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card--compact:hover {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card--compact .post-card__image-wrap {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card--compact .post-card__image-wrap img {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card--compact .post-card__content {
|
||||||
|
padding: var(--space-sm) 0;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card--compact .post-card__title {
|
||||||
|
font-size: var(--text-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.post-card--default {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card--default .post-card__image-wrap {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
---
|
||||||
|
<section
|
||||||
|
class="wi-section wi-quote"
|
||||||
|
th:if="${theme.config?.home?.home_quote_enabled == true and theme.config?.home?.home_quote_content}"
|
||||||
|
>
|
||||||
|
<div class="wi-container">
|
||||||
|
<div class="wi-quote__inner">
|
||||||
|
<span class="wi-quote__mark wi-quote__mark--open" aria-hidden="true">"</span>
|
||||||
|
<blockquote
|
||||||
|
class="wi-quote__text"
|
||||||
|
th:text="${theme.config?.home?.home_quote_content}"
|
||||||
|
></blockquote>
|
||||||
|
<span class="wi-quote__mark wi-quote__mark--close" aria-hidden="true">"</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wi-quote__inner {
|
||||||
|
position: relative;
|
||||||
|
max-width: 680px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: var(--space-2xl) var(--space-xl);
|
||||||
|
background: var(--mist-pink);
|
||||||
|
border-radius: 16px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid var(--rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-quote__mark {
|
||||||
|
font-family: Georgia, 'Times New Roman', serif;
|
||||||
|
font-size: 5rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--accent);
|
||||||
|
opacity: 0.25;
|
||||||
|
position: absolute;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-quote__mark--open {
|
||||||
|
top: -0.1em;
|
||||||
|
left: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-quote__mark--close {
|
||||||
|
bottom: -0.5em;
|
||||||
|
right: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-quote__text {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
font-style: italic;
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
color: var(--ink);
|
||||||
|
letter-spacing: var(--tracking-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.wi-quote__inner {
|
||||||
|
padding: var(--space-xl) var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-quote__mark {
|
||||||
|
font-size: 3.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-quote__mark--open {
|
||||||
|
left: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-quote__mark--close {
|
||||||
|
right: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-quote__text {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted, onUnmounted } from "vue";
|
||||||
|
|
||||||
|
const observer = ref<IntersectionObserver | null>(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const animationEnabled = !document.documentElement.classList.contains(
|
||||||
|
"wi-no-animations"
|
||||||
|
);
|
||||||
|
if (!animationEnabled) return;
|
||||||
|
|
||||||
|
observer.value = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
entry.target.classList.add("wi-revealed");
|
||||||
|
observer.value?.unobserve(entry.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ threshold: 0.1, rootMargin: "0px 0px -40px 0px" }
|
||||||
|
);
|
||||||
|
|
||||||
|
document.querySelectorAll(".wi-reveal").forEach((el) => {
|
||||||
|
observer.value?.observe(el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
observer.value?.disconnect();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, onUnmounted } from "vue";
|
||||||
|
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
||||||
|
e.preventDefault();
|
||||||
|
const searchWidget = (window as any).SearchWidget;
|
||||||
|
if (searchWidget && typeof searchWidget.open === "function") {
|
||||||
|
searchWidget.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener("keydown", handleKeydown);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener("keydown", handleKeydown);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span class="wi-search-hint" aria-hidden="true">
|
||||||
|
<kbd class="wi-search-hint__key">⌘</kbd>
|
||||||
|
<kbd class="wi-search-hint__key">K</kbd>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.wi-search-hint {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-search-hint__key {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
padding: 0 5px;
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--ink-3);
|
||||||
|
background: var(--bg-raised);
|
||||||
|
border: 1px solid var(--rule);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 1px 0 var(--rule);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -10,9 +10,13 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
|
document.documentElement.classList.add("wi-theme-transition");
|
||||||
isDark.value = !isDark.value;
|
isDark.value = !isDark.value;
|
||||||
document.documentElement.classList.toggle("dark", isDark.value);
|
document.documentElement.classList.toggle("dark", isDark.value);
|
||||||
localStorage.setItem("theme", isDark.value ? "dark" : "light");
|
localStorage.setItem("wi-theme", isDark.value ? "dark" : "light");
|
||||||
|
setTimeout(() => {
|
||||||
|
document.documentElement.classList.remove("wi-theme-transition");
|
||||||
|
}, 300);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
---
|
||||||
|
---
|
||||||
|
<section
|
||||||
|
class="wi-section wi-timeline"
|
||||||
|
th:if="${theme.config?.home?.home_timeline_enabled == true}"
|
||||||
|
>
|
||||||
|
<div class="wi-container">
|
||||||
|
<div class="wi-section__header">
|
||||||
|
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_timeline_title ?: '时间线'}">时间线</h2>
|
||||||
|
<span class="wi-section__accent"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wi-timeline__body">
|
||||||
|
<th:block th:with="postsResult = ${postFinder.list({page: 1, size: 50})}">
|
||||||
|
<th:block th:each="post : ${postsResult.items}">
|
||||||
|
<div class="wi-timeline__year" th:text="${#dates.format(post.spec.publishTime, 'yyyy')}"></div>
|
||||||
|
<div class="wi-timeline__entries">
|
||||||
|
<a
|
||||||
|
th:href="@{${post.status.permalink}}"
|
||||||
|
class="wi-timeline__entry"
|
||||||
|
>
|
||||||
|
<span class="wi-timeline__dot"></span>
|
||||||
|
<span class="wi-timeline__line"></span>
|
||||||
|
<time
|
||||||
|
class="wi-timeline__date"
|
||||||
|
th:text="${#dates.format(post.spec.publishTime, 'MM-dd')}"
|
||||||
|
></time>
|
||||||
|
<div class="wi-timeline__content">
|
||||||
|
<span class="wi-timeline__title" th:text="${post.spec.title}"></span>
|
||||||
|
<span
|
||||||
|
class="wi-timeline__excerpt"
|
||||||
|
th:if="${post.status.excerpt}"
|
||||||
|
th:text="${post.status.excerpt}"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wi-timeline__body {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-timeline__year {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink-2);
|
||||||
|
letter-spacing: var(--tracking-tight);
|
||||||
|
padding-bottom: var(--space-sm);
|
||||||
|
border-bottom: 1px solid var(--rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-timeline__entries {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-timeline__entry {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 48px 56px minmax(0, 1fr);
|
||||||
|
gap: var(--space-sm);
|
||||||
|
align-items: baseline;
|
||||||
|
padding: var(--space-sm) 0;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition: color var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-timeline__entry:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-timeline__dot {
|
||||||
|
position: absolute;
|
||||||
|
left: 19px;
|
||||||
|
top: 10px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--accent);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-timeline__line {
|
||||||
|
position: absolute;
|
||||||
|
left: 22px;
|
||||||
|
top: 18px;
|
||||||
|
bottom: calc(-1 * var(--space-sm));
|
||||||
|
width: 1px;
|
||||||
|
background: var(--rule);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-timeline__entry:last-child .wi-timeline__line {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-timeline__date {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--ink-3);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
padding-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-timeline__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-timeline__title {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink);
|
||||||
|
line-height: var(--leading-snug);
|
||||||
|
transition: color var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-timeline__entry:hover .wi-timeline__title {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-timeline__excerpt {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--ink-3);
|
||||||
|
line-height: var(--leading-normal);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.wi-timeline__entry {
|
||||||
|
grid-template-columns: 36px 48px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-timeline__dot {
|
||||||
|
left: 13px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-timeline__line {
|
||||||
|
left: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import type { DetailedUser } from "@halo-dev/api-client";
|
|
||||||
import ky from "ky";
|
|
||||||
import { computed, onMounted, ref } from "vue";
|
|
||||||
|
|
||||||
const user = ref<DetailedUser | null>(null);
|
|
||||||
const isLoading = ref(true);
|
|
||||||
|
|
||||||
const isAnonymous = computed(() => {
|
|
||||||
return user.value?.user.metadata.name === "anonymousUser";
|
|
||||||
});
|
|
||||||
|
|
||||||
const displayName = computed(() => {
|
|
||||||
return (
|
|
||||||
user.value?.user.spec.displayName ||
|
|
||||||
user.value?.user.metadata.name ||
|
|
||||||
"Account"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const avatarSrc = computed(() => {
|
|
||||||
return user.value?.user.spec.avatar || "";
|
|
||||||
});
|
|
||||||
|
|
||||||
const avatarLabel = computed(() => {
|
|
||||||
if (isAnonymous.value) {
|
|
||||||
return "?";
|
|
||||||
}
|
|
||||||
|
|
||||||
return displayName.value.charAt(0).toUpperCase();
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
user.value = await ky
|
|
||||||
.get<DetailedUser>(`/apis/api.console.halo.run/v1alpha1/users/-`)
|
|
||||||
.json();
|
|
||||||
} catch {
|
|
||||||
user.value = null;
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
v-if="isLoading"
|
|
||||||
class="user-button-shell user-button-shell--loading"
|
|
||||||
aria-busy="true"
|
|
||||||
aria-live="polite"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="user-button-avatar user-button-avatar--skeleton"
|
|
||||||
aria-hidden="true"
|
|
||||||
></span>
|
|
||||||
<span class="user-button-text-skeleton" aria-hidden="true"></span>
|
|
||||||
<span class="sr-only">Loading user state</span>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href="/uc"
|
|
||||||
v-else-if="user && !isAnonymous"
|
|
||||||
class="user-button-shell"
|
|
||||||
:title="displayName"
|
|
||||||
>
|
|
||||||
<span class="user-button-avatar" aria-hidden="true">
|
|
||||||
<img
|
|
||||||
v-if="avatarSrc"
|
|
||||||
class="user-button-avatar__image"
|
|
||||||
:src="avatarSrc"
|
|
||||||
:alt="displayName"
|
|
||||||
/>
|
|
||||||
<span v-else>{{ avatarLabel }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="user-button-text">{{ displayName }}</span>
|
|
||||||
</a>
|
|
||||||
<a v-else class="user-button-shell user-button-shell--link" href="/login">
|
|
||||||
<span class="user-button-avatar" aria-hidden="true">{{ avatarLabel }}</span>
|
|
||||||
<span class="user-button-text">Login</span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.user-button-shell {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.55rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 112px;
|
|
||||||
height: 34px;
|
|
||||||
padding: 0 0.7rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: var(--ink-2);
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-button-shell--link {
|
|
||||||
text-decoration: none;
|
|
||||||
transition:
|
|
||||||
background 0.15s ease,
|
|
||||||
color 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-button-shell--link:hover {
|
|
||||||
background: var(--bg-raised);
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-button-shell--loading {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-button-avatar {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--bg-raised);
|
|
||||||
color: var(--ink);
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-button-avatar__image {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-button-avatar--skeleton,
|
|
||||||
.user-button-text-skeleton {
|
|
||||||
background: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
var(--bg-raised) 0%,
|
|
||||||
color-mix(in srgb, var(--bg-raised) 78%, var(--ink) 22%) 50%,
|
|
||||||
var(--bg-raised) 100%
|
|
||||||
);
|
|
||||||
background-size: 200% 100%;
|
|
||||||
animation: shimmer 1.2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-button-text {
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-button-text-skeleton {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 0.72rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sr-only {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
padding: 0;
|
|
||||||
margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
white-space: nowrap;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shimmer {
|
|
||||||
0% {
|
|
||||||
background-position: 200% 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
background-position: -200% 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
+94
-26
@@ -1,52 +1,120 @@
|
|||||||
---
|
---
|
||||||
import "../styles/global.css";
|
import "../styles/main.scss";
|
||||||
|
import Navbar from "../components/Navbar.astro";
|
||||||
import Footer from "../components/Footer.astro";
|
import Footer from "../components/Footer.astro";
|
||||||
import Header from "../components/Header.astro";
|
import ScrollReveal from "../components/ScrollReveal.vue";
|
||||||
|
import CursorGlow from "../components/CursorGlow.vue";
|
||||||
|
import MobileMenu from "../components/MobileMenu.astro";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pageTitle?: string;
|
pageTitle?: string;
|
||||||
contentClass?: string;
|
contentClass?: string;
|
||||||
|
wide?: boolean;
|
||||||
|
home?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { pageTitle, contentClass } = Astro.props;
|
const { pageTitle, contentClass, wide, home } = Astro.props;
|
||||||
|
|
||||||
const hasHeading = Boolean(pageTitle);
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh" th:lang="${theme.config?.basic?.language ?: #locale.language}" th:classappend="${theme.config?.animation?.animation_enabled == false} ? 'wi-no-animations'">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<script is:inline>
|
<script is:inline th:attr="data-scheme=${theme.config?.style?.color_scheme}">
|
||||||
(function () {
|
(function () {
|
||||||
var stored = localStorage.getItem("theme");
|
var stored = localStorage.getItem("wi-theme");
|
||||||
var prefersDark = window.matchMedia(
|
var prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
"(prefers-color-scheme: dark)",
|
var scheme = null;
|
||||||
).matches;
|
if (document.currentScript && document.currentScript.getAttribute("data-scheme")) {
|
||||||
if (stored === "dark" || (!stored && prefersDark)) {
|
scheme = document.currentScript.getAttribute("data-scheme");
|
||||||
|
}
|
||||||
|
var isDark = false;
|
||||||
|
if (stored === "dark") {
|
||||||
|
isDark = true;
|
||||||
|
} else if (stored === "light") {
|
||||||
|
isDark = false;
|
||||||
|
} else if (scheme === "dark") {
|
||||||
|
isDark = true;
|
||||||
|
} else if (scheme === "light") {
|
||||||
|
isDark = false;
|
||||||
|
} else if (!stored && prefersDark) {
|
||||||
|
isDark = true;
|
||||||
|
}
|
||||||
|
if (isDark) {
|
||||||
document.documentElement.classList.add("dark");
|
document.documentElement.classList.add("dark");
|
||||||
|
document.documentElement.setAttribute("data-color-scheme", "dark");
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove("dark");
|
||||||
|
document.documentElement.setAttribute("data-color-scheme", "light");
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
th:href="${theme.config?.basic?.favicon ?: '/favicon.ico'}"
|
||||||
|
/>
|
||||||
|
<link rel="alternate" type="application/rss+xml" th:title="${site.title}" th:href="@{/feed.xml}" />
|
||||||
|
<style th:if="${theme.config?.style?.accent_color}" th:utext="${':root { --accent: ' + theme.config?.style?.accent_color + '; --accent-hover: ' + theme.config?.style?.accent_color + '; }'}"></style>
|
||||||
|
<style th:if="${theme.config?.style?.custom_css}" th:utext="${theme.config?.style?.custom_css}"></style>
|
||||||
<slot name="head" />
|
<slot name="head" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="wi-body">
|
||||||
<Header />
|
<Navbar />
|
||||||
<main class="layout-main">
|
<main class:list={["wi-main", { "wi-main--wide": wide || home, "wi-main--home": home }]}>
|
||||||
<article class="page-shell">
|
{home ? (
|
||||||
{
|
<slot />
|
||||||
hasHeading && (
|
) : (
|
||||||
<header class="page-heading">
|
<div class:list={["wi-content-wrap", { "wi-content-wrap--wide": wide }]}>
|
||||||
{pageTitle && <h1>{pageTitle}</h1>}
|
|
||||||
</header>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<div class:list={["page-content", contentClass]}>
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</article>
|
)}
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
<MobileMenu />
|
||||||
|
<ScrollReveal client:load />
|
||||||
|
<CursorGlow th:if="${theme.config?.animation?.animation_cursor_glow}" client:load />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
<style is:global>
|
||||||
|
html {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.wi-theme-transition,
|
||||||
|
html.wi-theme-transition *,
|
||||||
|
html.wi-theme-transition *::before,
|
||||||
|
html.wi-theme-transition *::after {
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease, fill 0.3s ease, stroke 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-body {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-reveal {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
transition:
|
||||||
|
opacity var(--duration-normal, 300ms) var(--ease-out-expo, cubic-bezier(0.16, 1, 0.3, 1)),
|
||||||
|
transform var(--duration-normal, 300ms) var(--ease-out-expo, cubic-bezier(0.16, 1, 0.3, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-revealed {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.wi-no-animations .wi-reveal {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// Astro 构建时辅助函数,提供页面配置信息
|
||||||
|
// 运行时由 Thymeleaf 模板中的 theme.config 覆盖
|
||||||
|
|
||||||
|
interface PageConfig {
|
||||||
|
title: string
|
||||||
|
slug: string
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages: Record<string, PageConfig> = {
|
||||||
|
moments: {
|
||||||
|
title: '瞬间',
|
||||||
|
slug: 'moments',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
photos: {
|
||||||
|
title: '图库',
|
||||||
|
slug: 'photos',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findPageBySlug(slug: string): PageConfig | undefined {
|
||||||
|
return pages[slug]
|
||||||
|
}
|
||||||
+223
-26
@@ -2,37 +2,51 @@
|
|||||||
import Layout from "../layouts/Layout.astro";
|
import Layout from "../layouts/Layout.astro";
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout pageTitle="归档">
|
<Layout>
|
||||||
<Fragment slot="head">
|
<Fragment slot="head">
|
||||||
<title th:text="|归档 - ${site.title}|"></title>
|
<title th:text="|归档 - ${site.title}|"></title>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|
||||||
<section class="archive-list" aria-label="Archive list">
|
<div class="wi-archives">
|
||||||
|
<header class="wi-archives__header">
|
||||||
|
<h1 class="wi-archives__title">归档</h1>
|
||||||
|
<span class="wi-page-accent"></span>
|
||||||
|
<p class="wi-archives__subtitle" th:text="${theme.config?.home?.home_label_post_count ?: '共 {total} 篇文章}'.replace('{total}', archives.total)}"></p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="wi-archives__timeline" aria-label="Archive timeline">
|
||||||
<th:block th:each="archive : ${archives.items}">
|
<th:block th:each="archive : ${archives.items}">
|
||||||
<th:block th:each="month : ${archive.months}">
|
<th:block th:each="month : ${archive.months}">
|
||||||
<div class="archive-group">
|
<div class="wi-archives__group">
|
||||||
<h2
|
<div class="wi-archives__group-label">
|
||||||
class="archive-group__year"
|
|
||||||
th:text="|${archive.year} · ${month.month}|"
|
|
||||||
>
|
|
||||||
</h2>
|
|
||||||
<ul class="archive-group__items">
|
|
||||||
<li th:each="post : ${month.posts}" class="archive-entry">
|
|
||||||
<span
|
<span
|
||||||
class="archive-entry__date"
|
class="wi-archives__dot"
|
||||||
|
aria-hidden="true"
|
||||||
|
></span>
|
||||||
|
<h2
|
||||||
|
class="wi-archives__year-month"
|
||||||
|
th:text="|${archive.year} · ${month.month}|"
|
||||||
|
></h2>
|
||||||
|
</div>
|
||||||
|
<ul class="wi-archives__items">
|
||||||
|
<li
|
||||||
|
th:each="post : ${month.posts}"
|
||||||
|
class="wi-archives__entry"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="wi-archives__entry-date"
|
||||||
th:text="${#dates.format(post.spec.publishTime, 'MM/dd')}"
|
th:text="${#dates.format(post.spec.publishTime, 'MM/dd')}"
|
||||||
></span>
|
></span>
|
||||||
<div>
|
<div class="wi-archives__entry-body">
|
||||||
<h3 class="archive-entry__title">
|
|
||||||
<a
|
<a
|
||||||
|
class="wi-archives__entry-title"
|
||||||
th:href="@{${post.status.permalink}}"
|
th:href="@{${post.status.permalink}}"
|
||||||
th:text="${post.spec.title}"></a>
|
th:text="${post.spec.title}"
|
||||||
</h3>
|
></a>
|
||||||
<p
|
<p
|
||||||
class="archive-entry__summary"
|
class="wi-archives__entry-excerpt"
|
||||||
th:text="${post.status.excerpt}"
|
th:text="${post.status.excerpt}"
|
||||||
>
|
></p>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -40,26 +54,209 @@ import Layout from "../layouts/Layout.astro";
|
|||||||
</th:block>
|
</th:block>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
<div th:if="${archives.total == 0}" class="archive-empty">暂无文章。</div>
|
<div
|
||||||
|
th:if="${archives.total == 0}"
|
||||||
|
class="wi-archives__empty"
|
||||||
|
th:text="${theme.config?.home?.home_label_no_posts ?: '暂无文章。'}"
|
||||||
|
>暂无文章。</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<nav
|
<nav
|
||||||
th:if="${archives.totalPages gt 1}"
|
th:if="${archives.totalPages gt 1}"
|
||||||
class="pagination"
|
class="wi-archives__pagination"
|
||||||
aria-label="Pagination"
|
aria-label="Pagination"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
th:if="${archives.hasPrevious()}"
|
th:if="${archives.hasPrevious()}"
|
||||||
th:href="@{${archives.prevUrl}}"
|
th:href="@{${archives.prevUrl}}"
|
||||||
class="pagination__prev">← 较新</a
|
class="wi-archives__page-link wi-archives__page-link--prev"
|
||||||
>
|
th:text="${theme.config?.home?.home_label_newer ?: '较新'}"
|
||||||
|
>← 较新</a>
|
||||||
<span
|
<span
|
||||||
class="pagination__info"
|
class="wi-archives__page-info"
|
||||||
th:text="|${archives.page} / ${archives.totalPages}|"></span>
|
th:text="|${archives.page} / ${archives.totalPages}|"
|
||||||
|
></span>
|
||||||
<a
|
<a
|
||||||
th:if="${archives.hasNext()}"
|
th:if="${archives.hasNext()}"
|
||||||
th:href="@{${archives.nextUrl}}"
|
th:href="@{${archives.nextUrl}}"
|
||||||
class="pagination__next">较旧 →</a
|
class="wi-archives__page-link wi-archives__page-link--next"
|
||||||
>
|
th:text="${theme.config?.home?.home_label_older ?: '较旧'}"
|
||||||
|
>较旧 →</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wi-archives {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
padding-block: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__header {
|
||||||
|
text-align: center;
|
||||||
|
padding-bottom: var(--space-lg);
|
||||||
|
border-bottom: 1px solid var(--rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__title {
|
||||||
|
font-size: var(--text-4xl);
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-page-accent {
|
||||||
|
display: block;
|
||||||
|
width: 48px;
|
||||||
|
height: 3px;
|
||||||
|
margin: var(--space-sm) auto 0;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__subtitle {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--ink-3);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__timeline {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-3xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__group {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__group-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__year-month {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink-2);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__items {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
padding-left: var(--space-lg);
|
||||||
|
border-left: 2px solid var(--rule);
|
||||||
|
margin-left: 3px;
|
||||||
|
display: grid;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__entry {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 64px minmax(0, 1fr);
|
||||||
|
gap: var(--space-md);
|
||||||
|
padding: var(--space-md) 0;
|
||||||
|
border-bottom: 1px dashed var(--rule);
|
||||||
|
transition: background var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__entry:hover {
|
||||||
|
background: var(--bg-raised);
|
||||||
|
margin: 0 calc(-1 * var(--space-md));
|
||||||
|
padding-left: var(--space-md);
|
||||||
|
padding-right: var(--space-md);
|
||||||
|
border-radius: $border-radius-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__entry:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__entry-date {
|
||||||
|
color: var(--ink-3);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
padding-top: 0.15rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__entry-body {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__entry-title {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--text-md);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink);
|
||||||
|
text-decoration: none;
|
||||||
|
line-height: var(--leading-snug);
|
||||||
|
transition: color var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__entry-title:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__entry-excerpt {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--ink-2);
|
||||||
|
line-height: var(--leading-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__empty {
|
||||||
|
color: var(--ink-3);
|
||||||
|
font-size: var(--text-md);
|
||||||
|
padding: var(--space-2xl) 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-md);
|
||||||
|
padding-top: var(--space-xl);
|
||||||
|
border-top: 1px solid var(--rule);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__page-link {
|
||||||
|
color: var(--ink-2);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__page-link:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__page-info {
|
||||||
|
color: var(--ink-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.wi-archives__entry {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-archives__items {
|
||||||
|
padding-left: var(--space-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
+146
-8
@@ -2,21 +2,159 @@
|
|||||||
import Layout from "../layouts/Layout.astro";
|
import Layout from "../layouts/Layout.astro";
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout pageTitle="分类">
|
<Layout>
|
||||||
<Fragment slot="head">
|
<Fragment slot="head">
|
||||||
<title th:text="|分类 - ${site.title}|"></title>
|
<title th:text="|分类 - ${site.title}|"></title>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|
||||||
<nav class="taxonomy-list" aria-label="Category list">
|
<div class="wi-categories">
|
||||||
<div th:each="category : ${categories}" class="taxonomy-list__item">
|
<header class="wi-categories__header">
|
||||||
|
<h1 class="wi-categories__title">分类</h1>
|
||||||
|
<span class="wi-page-accent"></span>
|
||||||
|
<p class="wi-categories__subtitle" th:text="|共 ${categories.size} 个分类|"></p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav class="wi-categories__list" aria-label="Category list">
|
||||||
<a
|
<a
|
||||||
class="taxonomy-list__link"
|
th:each="category : ${categories}"
|
||||||
|
class="wi-categories__item"
|
||||||
th:href="@{${category.status.permalink}}"
|
th:href="@{${category.status.permalink}}"
|
||||||
th:text="${category.spec.displayName}"
|
|
||||||
>
|
>
|
||||||
</a>
|
<div class="wi-categories__item-content">
|
||||||
<span class="taxonomy-list__count" th:text="|${category.postCount} 篇|">
|
<span
|
||||||
</span>
|
class="wi-categories__name"
|
||||||
|
th:text="${category.spec.displayName}"
|
||||||
|
></span>
|
||||||
|
<p
|
||||||
|
th:if="${not #strings.isEmpty(category.spec.description)}"
|
||||||
|
class="wi-categories__desc"
|
||||||
|
th:text="${category.spec.description}"
|
||||||
|
></p>
|
||||||
</div>
|
</div>
|
||||||
|
<span
|
||||||
|
class="wi-categories__count"
|
||||||
|
th:text="${category.postCount}"
|
||||||
|
></span>
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wi-categories {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
padding-block: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-categories__header {
|
||||||
|
text-align: center;
|
||||||
|
padding-bottom: var(--space-lg);
|
||||||
|
border-bottom: 1px solid var(--rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-categories__title {
|
||||||
|
font-size: var(--text-4xl);
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-page-accent {
|
||||||
|
display: block;
|
||||||
|
width: 48px;
|
||||||
|
height: 3px;
|
||||||
|
margin: var(--space-sm) auto 0;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-categories__subtitle {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--ink-3);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-categories__list {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-categories__item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
padding: var(--space-lg);
|
||||||
|
border-radius: $border-radius-sm;
|
||||||
|
border: 1px solid var(--rule);
|
||||||
|
background: var(--bg);
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
align-items: start;
|
||||||
|
transition: all var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-categories__item:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--bg-raised);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-categories__item-content {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-categories__name {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink);
|
||||||
|
line-height: var(--leading-snug);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-categories__desc {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--ink-2);
|
||||||
|
line-height: var(--leading-normal);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-categories__count {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--bg-raised);
|
||||||
|
border: 1px solid var(--rule);
|
||||||
|
color: var(--ink-3);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-categories__item:hover .wi-categories__count {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--bg);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.wi-categories__item {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-categories__count {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
+104
-14
@@ -7,34 +7,124 @@ import Layout from "../layouts/Layout.astro";
|
|||||||
<title th:text="|${category.spec.displayName} - ${site.title}|"></title>
|
<title th:text="|${category.spec.displayName} - ${site.title}|"></title>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|
||||||
<div class="page-heading">
|
<div class="wi-category">
|
||||||
<h1 th:text="${category.spec.displayName}"></h1>
|
<header class="wi-category__header">
|
||||||
<p class="page-meta">
|
<h1
|
||||||
<span th:text="|${posts.total} 篇文章|"></span>
|
class="wi-category__title"
|
||||||
</p>
|
th:text="${category.spec.displayName}"
|
||||||
</div>
|
></h1>
|
||||||
|
<span class="wi-page-accent"></span>
|
||||||
|
<p class="wi-category__subtitle" th:text="${theme.config?.home?.home_label_post_count ?: '共 {total} 篇文章}'.replace('{total}', posts.total)}"></p>
|
||||||
|
<p
|
||||||
|
th:if="${not #strings.isEmpty(category.spec.description)}"
|
||||||
|
class="wi-category__description"
|
||||||
|
th:text="${category.spec.description}"
|
||||||
|
></p>
|
||||||
|
</header>
|
||||||
|
|
||||||
<th:block th:replace="~{fragments/post-list}"></th:block>
|
<th:block th:replace="~{fragments/post-list}"></th:block>
|
||||||
|
|
||||||
<div th:if="${posts.total == 0}" class="feed-empty">暂无文章。</div>
|
<div
|
||||||
|
th:if="${posts.total == 0}"
|
||||||
|
class="wi-category__empty"
|
||||||
|
th:text="${theme.config?.home?.home_label_no_posts ?: '暂无文章。'}"
|
||||||
|
>暂无文章。</div>
|
||||||
|
|
||||||
<nav
|
<nav
|
||||||
th:if="${posts.totalPages gt 1}"
|
th:if="${posts.totalPages gt 1}"
|
||||||
class="pagination"
|
class="wi-category__pagination"
|
||||||
aria-label="Pagination"
|
aria-label="Pagination"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
th:if="${posts.hasPrevious()}"
|
th:if="${posts.hasPrevious()}"
|
||||||
th:href="@{${posts.prevUrl}}"
|
th:href="@{${posts.prevUrl}}"
|
||||||
class="pagination__prev">← 较新</a
|
class="wi-category__page-link wi-category__page-link--prev"
|
||||||
>
|
th:text="${theme.config?.home?.home_label_newer ?: '较新'}"
|
||||||
|
>← 较新</a>
|
||||||
<span
|
<span
|
||||||
class="pagination__info"
|
class="wi-category__page-info"
|
||||||
th:text="|${posts.page} / ${posts.totalPages}|"></span>
|
th:text="|${posts.page} / ${posts.totalPages}|"
|
||||||
|
></span>
|
||||||
<a
|
<a
|
||||||
th:if="${posts.hasNext()}"
|
th:if="${posts.hasNext()}"
|
||||||
th:href="@{${posts.nextUrl}}"
|
th:href="@{${posts.nextUrl}}"
|
||||||
class="pagination__next">较旧 →</a
|
class="wi-category__page-link wi-category__page-link--next"
|
||||||
>
|
th:text="${theme.config?.home?.home_label_older ?: '较旧'}"
|
||||||
|
>较旧 →</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wi-category {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
padding-block: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-category__header {
|
||||||
|
text-align: center;
|
||||||
|
padding-bottom: var(--space-lg);
|
||||||
|
border-bottom: 1px solid var(--rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-category__title {
|
||||||
|
font-size: var(--text-4xl);
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-page-accent {
|
||||||
|
display: block;
|
||||||
|
width: 48px;
|
||||||
|
height: 3px;
|
||||||
|
margin: var(--space-sm) auto 0;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-category__subtitle {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--ink-3);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-category__description {
|
||||||
|
margin: var(--space-md) auto 0;
|
||||||
|
max-width: var(--content-max);
|
||||||
|
font-size: var(--text-md);
|
||||||
|
color: var(--ink-2);
|
||||||
|
line-height: var(--leading-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-category__empty {
|
||||||
|
color: var(--ink-3);
|
||||||
|
font-size: var(--text-md);
|
||||||
|
padding: var(--space-2xl) 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-category__pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-md);
|
||||||
|
padding-top: var(--space-xl);
|
||||||
|
border-top: 1px solid var(--rule);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-category__page-link {
|
||||||
|
color: var(--ink-2);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-category__page-link:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-category__page-info {
|
||||||
|
color: var(--ink-3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../layouts/Layout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout wide>
|
||||||
|
<Fragment slot="head">
|
||||||
|
<title th:text="|${theme.config?.equipment?.equipment_page_title ?: '装备'} - ${site.title}|"></title>
|
||||||
|
</Fragment>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="wi-equipment"
|
||||||
|
th:if="${pluginFinder.available('equipment')}"
|
||||||
|
>
|
||||||
|
<div class="wi-container">
|
||||||
|
<div class="wi-equipment__header">
|
||||||
|
<h1
|
||||||
|
class="wi-equipment__title"
|
||||||
|
th:text="${theme.config?.equipment?.equipment_page_title ?: '装备'}"
|
||||||
|
>
|
||||||
|
装备
|
||||||
|
</h1>
|
||||||
|
<span class="wi-page-accent"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<th:block th:if="${equipmentFinder != null}">
|
||||||
|
<th:block th:each="group : ${equipmentFinder.listByGroup()}">
|
||||||
|
<h2
|
||||||
|
class="wi-equipment__group"
|
||||||
|
th:if="${group.spec?.displayName != null and !#strings.isEmpty(group.spec?.displayName)}"
|
||||||
|
th:text="${group.spec?.displayName}"
|
||||||
|
></h2>
|
||||||
|
<div class="wi-equipment__grid">
|
||||||
|
<div
|
||||||
|
class="wi-equipment__card"
|
||||||
|
th:each="equip : ${group.equipments}"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
th:if="${equip.spec?.cover}"
|
||||||
|
th:src="${equip.spec?.cover}"
|
||||||
|
th:alt="${equip.spec?.displayName}"
|
||||||
|
class="wi-equipment__avatar"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
th:unless="${equip.spec?.cover}"
|
||||||
|
class="wi-equipment__avatar wi-equipment__avatar--placeholder"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
th:text="${#strings.substring(equip.spec?.displayName ?: '', 0, 1)}"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<div class="wi-equipment__info">
|
||||||
|
<span
|
||||||
|
class="wi-equipment__name"
|
||||||
|
th:text="${equip.spec?.displayName}"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</th:block>
|
||||||
|
|
||||||
|
<th:block th:if="${equipmentFinder == null}">
|
||||||
|
<div class="wi-equipment__empty">
|
||||||
|
<p>装备插件加载异常,请检查插件状态</p>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="wi-equipment"
|
||||||
|
th:unless="${pluginFinder.available('equipment')}"
|
||||||
|
>
|
||||||
|
<div class="wi-container">
|
||||||
|
<div class="wi-equipment__header">
|
||||||
|
<h1 class="wi-equipment__title">装备</h1>
|
||||||
|
<span class="wi-page-accent"></span>
|
||||||
|
</div>
|
||||||
|
<div class="wi-equipment__empty">
|
||||||
|
<p>装备功能需要安装「装备管理」插件,<a href="https://www.halo.run/store/apps/app-ytygyqml">前往应用市场安装</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wi-equipment {
|
||||||
|
padding-block: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-equipment__header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--space-3xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-equipment__title {
|
||||||
|
font-size: var(--text-4xl);
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-page-accent {
|
||||||
|
display: block;
|
||||||
|
width: 48px;
|
||||||
|
height: 3px;
|
||||||
|
margin: var(--space-sm) auto 0;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-equipment__empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-4xl) var(--space-xl);
|
||||||
|
color: var(--ink-3);
|
||||||
|
font-size: var(--text-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-equipment__empty a {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-equipment__group {
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
color: var(--ink-2);
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
padding-bottom: var(--space-sm);
|
||||||
|
border-bottom: 1px solid var(--rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-equipment__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: var(--space-lg);
|
||||||
|
margin-bottom: var(--space-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-equipment__card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-md);
|
||||||
|
padding: var(--space-lg);
|
||||||
|
background: var(--glass-bg);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition:
|
||||||
|
transform var(--duration-normal) var(--ease-out-expo),
|
||||||
|
box-shadow var(--duration-normal) var(--ease-out-expo),
|
||||||
|
border-color var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-equipment__card:hover {
|
||||||
|
transform: translateY(-6px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-equipment__avatar {
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
border-radius: 16px;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-equipment__avatar--placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--accent-bg);
|
||||||
|
color: var(--accent);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-equipment__info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-equipment__name {
|
||||||
|
font-size: var(--text-md);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.wi-equipment__grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-equipment__card {
|
||||||
|
padding: var(--space-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../layouts/Layout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout wide>
|
||||||
|
<Fragment slot="head">
|
||||||
|
<title th:text="|${theme.config?.friends?.friends_page_title ?: '朋友圈'} - ${site.title}|"></title>
|
||||||
|
</Fragment>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="wi-friends-page"
|
||||||
|
th:if="${pluginFinder.available('plugin-friends')}"
|
||||||
|
>
|
||||||
|
<div class="wi-container">
|
||||||
|
<div class="wi-friends-page__header">
|
||||||
|
<h1
|
||||||
|
class="wi-friends-page__title"
|
||||||
|
th:text="${theme.config?.friends?.friends_page_title ?: '朋友圈'}"
|
||||||
|
>
|
||||||
|
朋友圈
|
||||||
|
</h1>
|
||||||
|
<span class="wi-page-accent"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wi-friends-page__flow">
|
||||||
|
<div
|
||||||
|
class="wi-friends-page__item"
|
||||||
|
th:each="friend : ${friends.items}"
|
||||||
|
th:with="spec = ${friend.spec}"
|
||||||
|
>
|
||||||
|
<div class="wi-friends-page__card">
|
||||||
|
<div class="wi-friends-page__author">
|
||||||
|
<img
|
||||||
|
th:if="${spec.logo}"
|
||||||
|
th:src="${spec.logo}"
|
||||||
|
th:alt="${spec.author}"
|
||||||
|
class="wi-friends-page__avatar"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
th:unless="${spec.logo}"
|
||||||
|
class="wi-friends-page__avatar wi-friends-page__avatar--placeholder"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
th:text="${#strings.substring(spec.author ?: '', 0, 1)}"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<div class="wi-friends-page__author-info">
|
||||||
|
<a
|
||||||
|
th:if="${spec.author}"
|
||||||
|
th:href="${spec.authorUrl}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="wi-friends-page__author-name"
|
||||||
|
th:text="${spec.author}"
|
||||||
|
></a>
|
||||||
|
<span
|
||||||
|
class="wi-friends-page__author-bio"
|
||||||
|
th:if="${spec.pubDate}"
|
||||||
|
th:text="${#temporals.format(spec.pubDate, 'yyyy-MM-dd')}"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wi-friends-page__post">
|
||||||
|
<a
|
||||||
|
th:if="${spec.postLink}"
|
||||||
|
th:href="${spec.postLink}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="wi-friends-page__post-title"
|
||||||
|
th:text="${spec.title}"
|
||||||
|
></a>
|
||||||
|
<p
|
||||||
|
class="wi-friends-page__post-content"
|
||||||
|
th:if="${spec.description}"
|
||||||
|
th:text="${spec.description}"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="wi-friends-page__pagination"
|
||||||
|
th:if="${friends.hasPrevious() || friends.hasNext()}"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
th:if="${friends.hasPrevious()}"
|
||||||
|
th:href="@{${friends.prevUrl}}"
|
||||||
|
class="wi-friends-page__page-link"
|
||||||
|
>
|
||||||
|
← 上一页
|
||||||
|
</a>
|
||||||
|
<span
|
||||||
|
class="wi-friends-page__page-info"
|
||||||
|
th:text="${friends.page} + ' / ' + ${friends.totalPages}"
|
||||||
|
></span>
|
||||||
|
<a
|
||||||
|
th:if="${friends.hasNext()}"
|
||||||
|
th:href="@{${friends.nextUrl}}"
|
||||||
|
class="wi-friends-page__page-link"
|
||||||
|
>
|
||||||
|
下一页 →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="wi-friends-page"
|
||||||
|
th:unless="${pluginFinder.available('plugin-friends')}"
|
||||||
|
>
|
||||||
|
<div class="wi-container">
|
||||||
|
<div class="wi-friends-page__header">
|
||||||
|
<h1 class="wi-friends-page__title">朋友圈</h1>
|
||||||
|
<span class="wi-page-accent"></span>
|
||||||
|
</div>
|
||||||
|
<div class="wi-friends-page__empty">
|
||||||
|
<p>朋友圈功能需要安装「朋友圈」插件,<a href="https://www.halo.run/store/apps/app-friends">前往应用市场安装</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wi-friends-page {
|
||||||
|
padding-block: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--space-3xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__title {
|
||||||
|
font-size: var(--text-4xl);
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-page-accent {
|
||||||
|
display: block;
|
||||||
|
width: 48px;
|
||||||
|
height: 3px;
|
||||||
|
margin: var(--space-sm) auto 0;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-4xl) var(--space-xl);
|
||||||
|
color: var(--ink-3);
|
||||||
|
font-size: var(--text-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__empty a {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__flow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
max-width: 720px;
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__card {
|
||||||
|
background: var(--glass-bg);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: var(--space-xl);
|
||||||
|
transition:
|
||||||
|
box-shadow var(--duration-normal) var(--ease-out-expo),
|
||||||
|
transform var(--duration-normal) var(--ease-out-expo),
|
||||||
|
border-color var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__card:hover {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__author {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-md);
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__avatar {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__avatar--placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--accent-bg);
|
||||||
|
color: var(--accent);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--text-md);
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__author-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__author-name {
|
||||||
|
font-size: var(--text-md);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__author-name:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__author-bio {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--ink-3);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__post {
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__post-title {
|
||||||
|
display: block;
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink);
|
||||||
|
text-decoration: none;
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
transition: color var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__post-title:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__post-content {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--ink-2);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-block-end: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
margin-top: var(--space-2xl);
|
||||||
|
padding-top: var(--space-xl);
|
||||||
|
border-top: 1px solid var(--rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__page-link {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
transition: opacity var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__page-link:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__page-info {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--ink-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.wi-friends-page__card {
|
||||||
|
padding: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-friends-page__avatar {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+544
-10
@@ -1,31 +1,565 @@
|
|||||||
---
|
---
|
||||||
import Layout from "../layouts/Layout.astro";
|
import Layout from "../layouts/Layout.astro";
|
||||||
|
import HeroSection from "../components/HeroSection.astro";
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout>
|
<Layout home>
|
||||||
<Fragment slot="head">
|
<Fragment slot="head">
|
||||||
<title th:text="${site.title}"></title>
|
<title th:text="${site.title}"></title>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|
||||||
<th:block th:replace="~{fragments/post-list}"></th:block>
|
<HeroSection />
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="wi-home-pinned"
|
||||||
|
th:if="${posts != null and !#lists.isEmpty(posts.items)}"
|
||||||
|
>
|
||||||
|
<div class="wi-container">
|
||||||
|
<div class="wi-section__header">
|
||||||
|
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_pinned_title ?: '置顶'}">置顶</h2>
|
||||||
|
</div>
|
||||||
|
<div class="wi-pinned">
|
||||||
|
<a
|
||||||
|
th:each="post : ${posts.items}"
|
||||||
|
th:if="${post.spec.pinned}"
|
||||||
|
th:href="@{${post.status.permalink}}"
|
||||||
|
class="wi-pinned__card"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="wi-pinned__cover"
|
||||||
|
th:if="${!#strings.isEmpty(post.spec.cover)}"
|
||||||
|
>
|
||||||
|
<img th:src="${post.spec.cover}" th:alt="${post.spec.title}" class="wi-pinned__image" />
|
||||||
|
</div>
|
||||||
|
<div class="wi-pinned__body">
|
||||||
|
<div class="wi-pinned__meta">
|
||||||
|
<span
|
||||||
|
class="wi-pinned__category"
|
||||||
|
th:if="${!#lists.isEmpty(post.categories)}"
|
||||||
|
th:text="${post.categories[0].spec.displayName}"
|
||||||
|
></span>
|
||||||
|
<time
|
||||||
|
class="wi-pinned__date"
|
||||||
|
th:text="${#dates.format(post.spec.publishTime, 'yyyy-MM-dd')}"
|
||||||
|
></time>
|
||||||
|
</div>
|
||||||
|
<h3
|
||||||
|
class="wi-pinned__title"
|
||||||
|
th:text="${post.spec.title}"
|
||||||
|
></h3>
|
||||||
|
<p
|
||||||
|
class="wi-pinned__excerpt"
|
||||||
|
th:if="${post.status.excerpt}"
|
||||||
|
th:text="${post.status.excerpt}"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="wi-home-posts" id="wi-home-posts">
|
||||||
|
<div class="wi-container">
|
||||||
|
<div class="wi-flow" id="wi-post-grid">
|
||||||
|
<a
|
||||||
|
th:each="post : ${posts.items}"
|
||||||
|
th:if="${!post.spec.pinned}"
|
||||||
|
th:href="@{${post.status.permalink}}"
|
||||||
|
class="wi-flow__card"
|
||||||
|
th:classappend="${!#strings.isEmpty(post.spec.cover)} ? 'wi-flow__card--with-cover' : ''"
|
||||||
|
>
|
||||||
|
<div class="wi-flow__body">
|
||||||
|
<div class="wi-flow__meta">
|
||||||
|
<span
|
||||||
|
class="wi-flow__category"
|
||||||
|
th:if="${!#lists.isEmpty(post.categories)}"
|
||||||
|
th:text="${post.categories[0].spec.displayName}"
|
||||||
|
></span>
|
||||||
|
<time
|
||||||
|
class="wi-flow__date"
|
||||||
|
th:text="${#dates.format(post.spec.publishTime, 'yyyy-MM-dd')}"
|
||||||
|
></time>
|
||||||
|
</div>
|
||||||
|
<h3
|
||||||
|
class="wi-flow__title"
|
||||||
|
th:text="${post.spec.title}"
|
||||||
|
></h3>
|
||||||
|
<p
|
||||||
|
class="wi-flow__excerpt"
|
||||||
|
th:if="${post.status.excerpt}"
|
||||||
|
th:text="${post.status.excerpt}"
|
||||||
|
th:style="'-webkit-line-clamp:' + (${theme.config?.home?.home_excerpt_lines ?: 3})"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="wi-flow__cover"
|
||||||
|
th:if="${!#strings.isEmpty(post.spec.cover)}"
|
||||||
|
>
|
||||||
|
<img th:src="${post.spec.cover}" th:alt="${post.spec.title}" class="wi-flow__image" />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<nav
|
<nav
|
||||||
th:if="${posts.totalPages gt 1}"
|
th:if="${posts.totalPages gt 1 and theme.config?.home?.home_post_loading != 'infinite_scroll'}"
|
||||||
class="pagination"
|
class="wi-pagination"
|
||||||
|
id="wi-post-pagination"
|
||||||
aria-label="Pagination"
|
aria-label="Pagination"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
th:if="${posts.hasPrevious()}"
|
th:if="${posts.hasPrevious()}"
|
||||||
th:href="@{${posts.prevUrl}}"
|
th:href="@{${posts.prevUrl}}"
|
||||||
class="pagination__prev">← 较新</a
|
class="wi-pagination__prev"
|
||||||
>
|
th:text="${theme.config?.home?.home_label_newer ?: '较新'}"
|
||||||
|
>← 较新</a>
|
||||||
<span
|
<span
|
||||||
class="pagination__info"
|
class="wi-pagination__info"
|
||||||
th:text="|${posts.page} / ${posts.totalPages}|"></span>
|
th:text="|${posts.page} / ${posts.totalPages}|"
|
||||||
|
></span>
|
||||||
<a
|
<a
|
||||||
th:if="${posts.hasNext()}"
|
th:if="${posts.hasNext()}"
|
||||||
th:href="@{${posts.nextUrl}}"
|
th:href="@{${posts.nextUrl}}"
|
||||||
class="pagination__next">较旧 →</a
|
class="wi-pagination__next"
|
||||||
>
|
th:text="${theme.config?.home?.home_label_older ?: '较旧'}"
|
||||||
|
>较旧 →</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<div
|
||||||
|
th:if="${posts.hasNext() and theme.config?.home?.home_post_loading == 'infinite_scroll'}"
|
||||||
|
id="wi-infinite-scroll-sentinel"
|
||||||
|
th:attr="data-next-url=${posts.nextUrl}, data-all-loaded-text=${theme.config?.home?.home_label_all_loaded ?: '已加载全部文章'}"
|
||||||
|
>
|
||||||
|
<div class="wi-infinite-scroll__loader">
|
||||||
|
<div class="wi-infinite-scroll__spinner"></div>
|
||||||
|
<span th:text="${theme.config?.home?.home_label_loading ?: '加载中...'}">加载中...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
th:if="${!posts.hasNext() and posts.totalPages gt 1 and theme.config?.home?.home_post_loading == 'infinite_scroll'}"
|
||||||
|
class="wi-infinite-scroll__end"
|
||||||
|
>
|
||||||
|
<span th:text="${theme.config?.home?.home_label_all_loaded ?: '已加载全部文章'}">已加载全部文章</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
(function () {
|
||||||
|
var sentinel = document.getElementById("wi-infinite-scroll-sentinel");
|
||||||
|
if (!sentinel) return;
|
||||||
|
|
||||||
|
var grid = document.getElementById("wi-post-grid");
|
||||||
|
var loading = false;
|
||||||
|
var nextUrl = sentinel.getAttribute("data-next-url");
|
||||||
|
|
||||||
|
var observer = new IntersectionObserver(
|
||||||
|
function (entries) {
|
||||||
|
if (entries[0].isIntersecting && !loading && nextUrl) {
|
||||||
|
loading = true;
|
||||||
|
var loader = sentinel.querySelector(".wi-infinite-scroll__loader");
|
||||||
|
if (loader) loader.style.display = "flex";
|
||||||
|
|
||||||
|
fetch(nextUrl)
|
||||||
|
.then(function (res) {
|
||||||
|
if (!res.ok) throw new Error("Fetch failed");
|
||||||
|
return res.text();
|
||||||
|
})
|
||||||
|
.then(function (html) {
|
||||||
|
var parser = new DOMParser();
|
||||||
|
var doc = parser.parseFromString(html, "text/html");
|
||||||
|
|
||||||
|
var newCards = doc.querySelectorAll("#wi-post-grid .wi-flow__card");
|
||||||
|
for (var i = 0; i < newCards.length; i++) {
|
||||||
|
var card = newCards[i];
|
||||||
|
card.style.opacity = "0";
|
||||||
|
card.style.transform = "translateY(20px)";
|
||||||
|
grid.appendChild(card);
|
||||||
|
(function (c) {
|
||||||
|
requestAnimationFrame(function () {
|
||||||
|
c.style.transition = "opacity 0.4s ease, transform 0.4s ease";
|
||||||
|
c.style.opacity = "1";
|
||||||
|
c.style.transform = "translateY(0)";
|
||||||
|
});
|
||||||
|
})(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
var newSentinel = doc.getElementById("wi-infinite-scroll-sentinel");
|
||||||
|
if (newSentinel && newSentinel.getAttribute("data-next-url")) {
|
||||||
|
nextUrl = newSentinel.getAttribute("data-next-url");
|
||||||
|
} else {
|
||||||
|
nextUrl = null;
|
||||||
|
observer.disconnect();
|
||||||
|
sentinel.style.display = "none";
|
||||||
|
var endMsg = document.createElement("div");
|
||||||
|
endMsg.className = "wi-infinite-scroll__end";
|
||||||
|
endMsg.innerHTML = "<span>" + allLoadedText + "</span>";
|
||||||
|
sentinel.parentNode.appendChild(endMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = false;
|
||||||
|
if (loader) loader.style.display = "none";
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
loading = false;
|
||||||
|
if (loader) loader.style.display = "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ rootMargin: "300px" }
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(sentinel);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wi-home-pinned {
|
||||||
|
padding-block: 2rem;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-home-pinned:not(:has(.wi-pinned__card)) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-section__header {
|
||||||
|
margin-bottom: var(--space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-section__title {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink);
|
||||||
|
letter-spacing: var(--tracking-tight);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-section__title::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pinned {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pinned__card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-raised);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition:
|
||||||
|
transform var(--duration-normal) var(--ease-out-expo),
|
||||||
|
box-shadow var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pinned__card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pinned__card:first-child:nth-last-child(1) {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pinned__card:first-child:nth-last-child(1) .wi-pinned__cover {
|
||||||
|
aspect-ratio: 21 / 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pinned__card:first-child:nth-last-child(1) .wi-pinned__title {
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pinned__cover {
|
||||||
|
aspect-ratio: 16 / 10;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pinned__image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform var(--duration-normal) var(--ease-out-expo);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pinned__card:hover .wi-pinned__image {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pinned__body {
|
||||||
|
padding: var(--space-lg);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pinned__meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pinned__category {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--accent);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pinned__category::after {
|
||||||
|
content: '·';
|
||||||
|
margin-left: var(--space-sm);
|
||||||
|
color: var(--ink-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pinned__date {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--ink-3);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pinned__title {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink);
|
||||||
|
line-height: var(--leading-tight);
|
||||||
|
letter-spacing: var(--tracking-tight);
|
||||||
|
transition: color var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pinned__card:hover .wi-pinned__title {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pinned__excerpt {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--ink-2);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.wi-pinned {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pinned__card:first-child:nth-last-child(1) .wi-pinned__cover {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pinned__card:first-child:nth-last-child(1) .wi-pinned__title {
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-home-posts {
|
||||||
|
padding-block: 2rem;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-flow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-flow__card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-raised);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition:
|
||||||
|
transform var(--duration-normal) var(--ease-out-expo),
|
||||||
|
box-shadow var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-flow__card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-flow__card--with-cover .wi-flow__body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-flow__cover {
|
||||||
|
width: 240px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-flow__image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform var(--duration-normal) var(--ease-out-expo);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-flow__card:hover .wi-flow__image {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-flow__body {
|
||||||
|
padding: var(--space-lg);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-flow__meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-flow__category {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--accent);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-flow__category::after {
|
||||||
|
content: '·';
|
||||||
|
margin-left: var(--space-sm);
|
||||||
|
color: var(--ink-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-flow__date {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--ink-3);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-flow__title {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink);
|
||||||
|
line-height: var(--leading-tight);
|
||||||
|
letter-spacing: var(--tracking-tight);
|
||||||
|
transition: color var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-flow__card:hover .wi-flow__title {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-flow__excerpt {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--ink-2);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-block-end: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding-top: var(--space-xl);
|
||||||
|
margin-top: var(--space-xl);
|
||||||
|
border-top: 1px solid var(--rule);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pagination__prev,
|
||||||
|
.wi-pagination__next {
|
||||||
|
color: var(--ink-2);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pagination__prev:hover,
|
||||||
|
.wi-pagination__next:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-pagination__info {
|
||||||
|
color: var(--ink-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-infinite-scroll__loader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
padding: var(--space-xl) 0;
|
||||||
|
color: var(--ink-3);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-infinite-scroll__spinner {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 2px solid var(--rule);
|
||||||
|
border-top-color: var(--accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: wi-spin 0.6s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wi-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-infinite-scroll__end {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-xl) 0;
|
||||||
|
color: var(--ink-3);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.wi-flow__card {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-flow__cover {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../layouts/Layout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout wide>
|
||||||
|
<Fragment slot="head">
|
||||||
|
<title th:text="|${theme.config?.links?.links_page_title ?: '友情链接'} - ${site.title}|"></title>
|
||||||
|
</Fragment>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="wi-links"
|
||||||
|
th:if="${pluginFinder.available('PluginLinks')}"
|
||||||
|
>
|
||||||
|
<div class="wi-container">
|
||||||
|
<div class="wi-links__header">
|
||||||
|
<h1
|
||||||
|
class="wi-links__title"
|
||||||
|
th:text="${theme.config?.links?.links_page_title ?: '友情链接'}"
|
||||||
|
>
|
||||||
|
友情链接
|
||||||
|
</h1>
|
||||||
|
<p class="wi-links__subtitle">山水一程,三生有幸</p>
|
||||||
|
<span class="wi-page-accent"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<th:block th:each="group : ${groups}">
|
||||||
|
<h2
|
||||||
|
class="wi-links__group"
|
||||||
|
th:if="${group.spec.displayName != null and !#strings.isEmpty(group.spec.displayName)}"
|
||||||
|
th:text="${group.spec.displayName}"
|
||||||
|
></h2>
|
||||||
|
<div class="wi-links__grid">
|
||||||
|
<a
|
||||||
|
th:each="link : ${group.links}"
|
||||||
|
th:href="${link.spec.url}"
|
||||||
|
th:title="${link.spec.displayName}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="wi-links__card"
|
||||||
|
>
|
||||||
|
<div class="wi-links__card-glow"></div>
|
||||||
|
<div class="wi-links__card-inner">
|
||||||
|
<img
|
||||||
|
th:if="${link.spec.logo}"
|
||||||
|
th:src="${link.spec.logo}"
|
||||||
|
th:alt="${link.spec.displayName}"
|
||||||
|
class="wi-links__avatar"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
th:unless="${link.spec.logo}"
|
||||||
|
class="wi-links__avatar wi-links__avatar--placeholder"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
th:text="${#strings.substring(link.spec.displayName, 0, 1)}"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<div class="wi-links__info">
|
||||||
|
<span
|
||||||
|
class="wi-links__name"
|
||||||
|
th:text="${link.spec.displayName}"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
class="wi-links__desc"
|
||||||
|
th:if="${link.spec.description}"
|
||||||
|
th:text="${link.spec.description}"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<svg class="wi-links__arrow" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 17L17 7"/><path d="M7 7h10v10"/></svg>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wi-links {
|
||||||
|
padding-block: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-links__header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--space-3xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-links__title {
|
||||||
|
font-size: var(--text-4xl);
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-links__subtitle {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--ink-3);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
margin-block-end: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-page-accent {
|
||||||
|
display: block;
|
||||||
|
width: 48px;
|
||||||
|
height: 3px;
|
||||||
|
margin: var(--space-md) auto 0;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-links__group {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
color: var(--ink-2);
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
padding-bottom: var(--space-sm);
|
||||||
|
border-bottom: 1px solid var(--rule);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-links__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: var(--space-md);
|
||||||
|
margin-bottom: var(--space-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-links__card {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-links__card-glow {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 16px;
|
||||||
|
opacity: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
color-mix(in srgb, var(--accent) 8%, transparent) 0%,
|
||||||
|
transparent 60%
|
||||||
|
);
|
||||||
|
transition: opacity var(--duration-normal) var(--ease-out-expo);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-links__card:hover .wi-links__card-glow {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-links__card-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-md);
|
||||||
|
padding: var(--space-lg);
|
||||||
|
background: var(--glass-bg);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: 16px;
|
||||||
|
transition:
|
||||||
|
transform var(--duration-normal) var(--ease-out-expo),
|
||||||
|
box-shadow var(--duration-normal) var(--ease-out-expo),
|
||||||
|
border-color var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-links__card:hover .wi-links__card-inner {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
border-color: color-mix(in srgb, var(--accent) 40%, var(--glass-border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-links__avatar {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 12px;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
transition: transform var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-links__card:hover .wi-links__avatar {
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-links__avatar--placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--accent-bg);
|
||||||
|
color: var(--accent);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-links__info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-links__name {
|
||||||
|
font-size: var(--text-md);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: color var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-links__card:hover .wi-links__name {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-links__desc {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--ink-3);
|
||||||
|
line-height: var(--leading-snug);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-links__arrow {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--ink-3);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-4px, 4px);
|
||||||
|
transition:
|
||||||
|
opacity var(--duration-normal) var(--ease-out-expo),
|
||||||
|
transform var(--duration-normal) var(--ease-out-expo),
|
||||||
|
color var(--duration-fast) var(--ease-out-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-links__card:hover .wi-links__arrow {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(0, 0);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.wi-links__grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-links__card-inner {
|
||||||
|
padding: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-links__avatar {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,735 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../layouts/Layout.astro";
|
||||||
|
import LightGallery from "../components/LightGallery.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout>
|
||||||
|
<Fragment slot="head">
|
||||||
|
<title th:text="|${theme.config?.moments?.moments_page_title ?: '瞬间'} - ${site.title}|"></title>
|
||||||
|
</Fragment>
|
||||||
|
|
||||||
|
<section class="wi-moments-page">
|
||||||
|
<div class="wi-container">
|
||||||
|
<div class="wi-moments-page__header">
|
||||||
|
<h1 class="wi-moments-page__title" th:text="${theme.config?.moments?.moments_page_title ?: '瞬间'}">瞬间</h1>
|
||||||
|
<span class="wi-page-accent"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<th:block th:if="${pluginFinder.available('PluginMoments')}">
|
||||||
|
<div class="wi-moments-page__content">
|
||||||
|
<th:block th:if="${momentFinder != null}">
|
||||||
|
<th:block th:with="momentsResult = ${momentFinder.list(1, 50)}">
|
||||||
|
<div
|
||||||
|
class="wi-moments-page__timeline"
|
||||||
|
th:if="${theme.config?.moments?.moments_style == 'timeline' or theme.config?.moments?.moments_style == null}"
|
||||||
|
>
|
||||||
|
<th:block th:if="${momentsResult != null and not #lists.isEmpty(momentsResult.items)}">
|
||||||
|
<div class="wi-moments-page__item" th:each="moment : ${momentsResult.items}">
|
||||||
|
<div class="wi-moments-page__dot"></div>
|
||||||
|
<div class="wi-moments-page__line"></div>
|
||||||
|
<div class="wi-moments-page__card">
|
||||||
|
<div class="wi-moments-page__text" th:utext="${moment.spec?.content?.html ?: moment.spec?.content?.raw ?: moment.spec?.content}"></div>
|
||||||
|
<div class="wi-moments-page__media" th:if="${moment.spec.content.medium != null and !#lists.isEmpty(moment.spec.content.medium)}">
|
||||||
|
<th:block th:each="media : ${moment.spec.content.medium}">
|
||||||
|
<img
|
||||||
|
th:if="${media.type.name == 'PHOTO'}"
|
||||||
|
th:src="${media.url}"
|
||||||
|
th:alt="''"
|
||||||
|
class="wi-moments-page__image"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
<div class="wi-moments-page__footer">
|
||||||
|
<time
|
||||||
|
class="wi-moments-page__date"
|
||||||
|
th:text="${#dates.format(moment.spec?.releaseTime ?: moment.metadata?.creationTimestamp, 'yyyy-MM-dd HH:mm')}"
|
||||||
|
></time>
|
||||||
|
<div class="wi-moments-page__actions">
|
||||||
|
<button
|
||||||
|
class="wi-moments-page__like-btn"
|
||||||
|
type="button"
|
||||||
|
th:attr="data-moment-name=${moment.metadata.name}"
|
||||||
|
>
|
||||||
|
<svg class="wi-moments-page__like-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg>
|
||||||
|
<span class="wi-moments-page__like-count" th:text="${moment.stats?.upvote ?: 0}">0</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="wi-moments-page__comment-btn"
|
||||||
|
type="button"
|
||||||
|
th:attr="data-moment-name=${moment.metadata.name}"
|
||||||
|
>
|
||||||
|
<svg class="wi-moments-page__comment-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||||
|
<span class="wi-moments-page__comment-count" th:text="${moment.stats?.approvedComment ?: 0}">0</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wi-moments-page__comment-area" th:attr="data-moment-name=${moment.metadata.name}" style="display:none;">
|
||||||
|
<halo:comment
|
||||||
|
group="moment.halo.run"
|
||||||
|
kind="Moment"
|
||||||
|
th:attr="name=${moment.metadata.name}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
<th:block th:if="${momentsResult == null or #lists.isEmpty(momentsResult.items)}">
|
||||||
|
<div class="wi-moments-page__empty">
|
||||||
|
<p>还没有瞬间,快去记录当下的心情吧 🌙</p>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wi-moments-page__cards" th:if="${theme.config?.moments?.moments_style == 'cards'}">
|
||||||
|
<th:block th:if="${momentsResult != null and not #lists.isEmpty(momentsResult.items)}">
|
||||||
|
<div class="wi-moments-page__card-item" th:each="moment : ${momentsResult.items}">
|
||||||
|
<div class="wi-moments-page__card-text" th:utext="${moment.spec?.content?.html ?: moment.spec?.content?.raw ?: moment.spec?.content}"></div>
|
||||||
|
<div class="wi-moments-page__card-media" th:if="${moment.spec.content.medium != null and !#lists.isEmpty(moment.spec.content.medium)}">
|
||||||
|
<th:block th:each="media : ${moment.spec.content.medium}">
|
||||||
|
<img
|
||||||
|
th:if="${media.type.name == 'PHOTO'}"
|
||||||
|
th:src="${media.url}"
|
||||||
|
th:alt="''"
|
||||||
|
class="wi-moments-page__card-image"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
<div class="wi-moments-page__card-footer">
|
||||||
|
<time
|
||||||
|
class="wi-moments-page__card-date"
|
||||||
|
th:text="${#dates.format(moment.spec?.releaseTime ?: moment.metadata?.creationTimestamp, 'yyyy-MM-dd HH:mm')}"
|
||||||
|
></time>
|
||||||
|
<div class="wi-moments-page__actions">
|
||||||
|
<button
|
||||||
|
class="wi-moments-page__like-btn"
|
||||||
|
type="button"
|
||||||
|
th:attr="data-moment-name=${moment.metadata.name}"
|
||||||
|
>
|
||||||
|
<svg class="wi-moments-page__like-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg>
|
||||||
|
<span class="wi-moments-page__like-count" th:text="${moment.stats?.upvote ?: 0}">0</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="wi-moments-page__comment-btn"
|
||||||
|
type="button"
|
||||||
|
th:attr="data-moment-name=${moment.metadata.name}"
|
||||||
|
>
|
||||||
|
<svg class="wi-moments-page__comment-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||||
|
<span class="wi-moments-page__comment-count" th:text="${moment.stats?.approvedComment ?: 0}">0</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wi-moments-page__comment-area" th:attr="data-moment-name=${moment.metadata.name}" style="display:none;">
|
||||||
|
<halo:comment
|
||||||
|
group="moment.halo.run"
|
||||||
|
kind="Moment"
|
||||||
|
th:attr="name=${moment.metadata.name}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
<th:block th:if="${momentsResult == null or #lists.isEmpty(momentsResult.items)}">
|
||||||
|
<div class="wi-moments-page__empty">
|
||||||
|
<p>还没有瞬间,快去记录当下的心情吧 🌙</p>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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)}">
|
||||||
|
<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-media" th:if="${moment.spec.content.medium != null and !#lists.isEmpty(moment.spec.content.medium)}">
|
||||||
|
<th:block th:each="media : ${moment.spec.content.medium}">
|
||||||
|
<img
|
||||||
|
th:if="${media.type.name == 'PHOTO'}"
|
||||||
|
th:src="${media.url}"
|
||||||
|
th:alt="''"
|
||||||
|
class="wi-moments-page__masonry-image"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
<div class="wi-moments-page__masonry-footer">
|
||||||
|
<time
|
||||||
|
class="wi-moments-page__masonry-date"
|
||||||
|
th:text="${#dates.format(moment.spec?.releaseTime ?: moment.metadata?.creationTimestamp, 'yyyy-MM-dd HH:mm')}"
|
||||||
|
></time>
|
||||||
|
<div class="wi-moments-page__actions">
|
||||||
|
<button
|
||||||
|
class="wi-moments-page__like-btn"
|
||||||
|
type="button"
|
||||||
|
th:attr="data-moment-name=${moment.metadata.name}"
|
||||||
|
>
|
||||||
|
<svg class="wi-moments-page__like-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg>
|
||||||
|
<span class="wi-moments-page__like-count" th:text="${moment.stats?.upvote ?: 0}">0</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="wi-moments-page__comment-btn"
|
||||||
|
type="button"
|
||||||
|
th:attr="data-moment-name=${moment.metadata.name}"
|
||||||
|
>
|
||||||
|
<svg class="wi-moments-page__comment-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||||
|
<span class="wi-moments-page__comment-count" th:text="${moment.stats?.approvedComment ?: 0}">0</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wi-moments-page__comment-area" th:attr="data-moment-name=${moment.metadata.name}" style="display:none;">
|
||||||
|
<halo:comment
|
||||||
|
group="moment.halo.run"
|
||||||
|
kind="Moment"
|
||||||
|
th:attr="name=${moment.metadata.name}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
<th:block th:if="${momentsResult == null or #lists.isEmpty(momentsResult.items)}">
|
||||||
|
<div class="wi-moments-page__empty">
|
||||||
|
<p>还没有瞬间,快去记录当下的心情吧 🌙</p>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</th:block>
|
||||||
|
<th:block th:if="${momentFinder == null}">
|
||||||
|
<div class="wi-moments-page__empty">
|
||||||
|
<p>瞬间插件加载异常,请检查插件状态</p>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
|
||||||
|
<th:block th:unless="${pluginFinder.available('PluginMoments')}">
|
||||||
|
<div class="wi-moments-page__empty">
|
||||||
|
<p>瞬间功能需要安装「瞬间」插件,<a href="https://www.halo.run/store/apps/app-hqbe">前往应用市场安装</a></p>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<LightGallery />
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
(function () {
|
||||||
|
document.addEventListener("click", function (e) {
|
||||||
|
var likeBtn = e.target.closest(".wi-moments-page__like-btn");
|
||||||
|
if (likeBtn) {
|
||||||
|
handleLike(likeBtn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var commentBtn = e.target.closest(".wi-moments-page__comment-btn");
|
||||||
|
if (commentBtn) {
|
||||||
|
toggleComment(commentBtn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleLike(btn) {
|
||||||
|
var momentName = btn.getAttribute("data-moment-name");
|
||||||
|
var likedKey = "wi-moment-liked-" + momentName;
|
||||||
|
var countEl = btn.querySelector(".wi-moments-page__like-count");
|
||||||
|
|
||||||
|
if (localStorage.getItem(likedKey)) return;
|
||||||
|
if (btn.classList.contains("wi-moments-page__like-btn--liked")) return;
|
||||||
|
|
||||||
|
fetch("/apis/api.halo.run/v1alpha1/trackers/upvote", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json;charset=UTF-8" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
group: "moment.halo.run",
|
||||||
|
plural: "moments",
|
||||||
|
name: momentName,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then(function (res) {
|
||||||
|
if (!res.ok) throw new Error("Like failed");
|
||||||
|
btn.classList.add("wi-moments-page__like-btn--liked");
|
||||||
|
if (countEl) {
|
||||||
|
countEl.textContent = parseInt(countEl.textContent || "0") + 1;
|
||||||
|
}
|
||||||
|
localStorage.setItem(likedKey, "1");
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
console.error("Like error:", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleComment(btn) {
|
||||||
|
var momentName = btn.getAttribute("data-moment-name");
|
||||||
|
var commentArea = document.querySelector(
|
||||||
|
'.wi-moments-page__comment-area[data-moment-name="' + momentName + '"]'
|
||||||
|
);
|
||||||
|
if (!commentArea) return;
|
||||||
|
|
||||||
|
if (commentArea.style.display === "none") {
|
||||||
|
commentArea.style.display = "block";
|
||||||
|
btn.classList.add("wi-moments-page__comment-btn--active");
|
||||||
|
} else {
|
||||||
|
commentArea.style.display = "none";
|
||||||
|
btn.classList.remove("wi-moments-page__comment-btn--active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll(".wi-moments-page__like-btn").forEach(function (btn) {
|
||||||
|
var momentName = btn.getAttribute("data-moment-name");
|
||||||
|
var likedKey = "wi-moment-liked-" + momentName;
|
||||||
|
if (localStorage.getItem(likedKey)) {
|
||||||
|
btn.classList.add("wi-moments-page__like-btn--liked");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadMomentStats() {
|
||||||
|
fetch("/apis/api.moment.halo.run/v1alpha1/moments")
|
||||||
|
.then(function (res) {
|
||||||
|
if (!res.ok) return null;
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(function (data) {
|
||||||
|
if (!data || !data.items) return;
|
||||||
|
data.items.forEach(function (moment) {
|
||||||
|
var name = moment.metadata.name;
|
||||||
|
var stats = moment.stats || {};
|
||||||
|
|
||||||
|
document.querySelectorAll(".wi-moments-page__like-btn").forEach(function (btn) {
|
||||||
|
if (btn.getAttribute("data-moment-name") === name) {
|
||||||
|
var countEl = btn.querySelector(".wi-moments-page__like-count");
|
||||||
|
if (countEl) countEl.textContent = stats.upvote || 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll(".wi-moments-page__comment-btn").forEach(function (btn) {
|
||||||
|
if (btn.getAttribute("data-moment-name") === name) {
|
||||||
|
var countEl = btn.querySelector(".wi-moments-page__comment-count");
|
||||||
|
if (countEl) countEl.textContent = stats.approvedComment || 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(function () {});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMomentStats();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wi-moments-page {
|
||||||
|
padding-block: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--space-3xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__title {
|
||||||
|
font-size: var(--text-4xl);
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-page-accent {
|
||||||
|
display: block;
|
||||||
|
width: 48px;
|
||||||
|
height: 3px;
|
||||||
|
margin: var(--space-sm) auto 0;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-4xl) var(--space-xl);
|
||||||
|
color: var(--ink-3);
|
||||||
|
font-size: var(--text-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__empty a {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__timeline {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-xl);
|
||||||
|
padding-left: var(--space-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__item {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__dot {
|
||||||
|
position: absolute;
|
||||||
|
left: calc(-1 * var(--space-2xl) - 5px);
|
||||||
|
top: 10px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--accent);
|
||||||
|
box-shadow: 0 0 0 3px var(--bg), 0 0 0 4px var(--accent);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__line {
|
||||||
|
position: absolute;
|
||||||
|
left: calc(-1 * var(--space-2xl));
|
||||||
|
top: 20px;
|
||||||
|
bottom: calc(-1 * var(--space-xl));
|
||||||
|
width: 1px;
|
||||||
|
background: var(--rule);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__item:last-child .wi-moments-page__line {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__card {
|
||||||
|
background: var(--bg-raised);
|
||||||
|
border: 1px solid var(--rule);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: var(--space-xl);
|
||||||
|
transition:
|
||||||
|
box-shadow var(--duration-normal) var(--ease-out-expo),
|
||||||
|
transform var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__card:hover {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__text {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
color: var(--ink);
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__media {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__image {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__image:hover {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__media:has(.wi-moments-page__image:only-child) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__media:has(.wi-moments-page__image:only-child) .wi-moments-page__image {
|
||||||
|
aspect-ratio: auto;
|
||||||
|
max-height: 300px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__media:has(.wi-moments-page__image:nth-child(2):last-child) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__media:has(.wi-moments-page__image:nth-child(4)) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__date {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--ink-3);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__like-btn,
|
||||||
|
.wi-moments-page__comment-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border: 1px solid var(--rule);
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ink-3);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-family: var(--font-body);
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
color 0.2s ease,
|
||||||
|
border-color 0.2s ease,
|
||||||
|
background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__like-btn:hover,
|
||||||
|
.wi-moments-page__comment-btn:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__like-btn--liked {
|
||||||
|
color: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: color-mix(in srgb, var(--accent) 8%, var(--bg));
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__like-btn--liked .wi-moments-page__like-icon {
|
||||||
|
fill: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__comment-btn--active {
|
||||||
|
color: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: color-mix(in srgb, var(--accent) 8%, var(--bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__like-icon,
|
||||||
|
.wi-moments-page__comment-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__like-count,
|
||||||
|
.wi-moments-page__comment-count {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__comment-area {
|
||||||
|
margin-top: var(--space-md);
|
||||||
|
padding-top: var(--space-md);
|
||||||
|
border-top: 1px solid var(--rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__comment-area halo\:comment {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__card-item {
|
||||||
|
background: var(--glass-bg);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: var(--space-xl);
|
||||||
|
transition:
|
||||||
|
box-shadow var(--duration-normal) var(--ease-out-expo),
|
||||||
|
transform var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__card-item:hover {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__card-text {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
color: var(--ink);
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__card-media {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__card-image {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__card-image:hover {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__card-media:has(.wi-moments-page__card-image:only-child) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__card-media:has(.wi-moments-page__card-image:only-child) .wi-moments-page__card-image {
|
||||||
|
aspect-ratio: auto;
|
||||||
|
max-height: 300px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__card-media:has(.wi-moments-page__card-image:nth-child(2):last-child) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__card-media:has(.wi-moments-page__card-image:nth-child(4)) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__card-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__card-date {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--ink-3);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__masonry {
|
||||||
|
columns: 2;
|
||||||
|
column-gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__masonry-item {
|
||||||
|
break-inside: avoid;
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
background: var(--bg-raised);
|
||||||
|
border: 1px solid var(--rule);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: var(--space-xl);
|
||||||
|
transition:
|
||||||
|
box-shadow var(--duration-normal) var(--ease-out-expo),
|
||||||
|
transform var(--duration-normal) var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__masonry-item:hover {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__masonry-text {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
color: var(--ink);
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__masonry-media {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__masonry-image {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__masonry-image:hover {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__masonry-media:has(.wi-moments-page__masonry-image:only-child) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__masonry-media:has(.wi-moments-page__masonry-image:only-child) .wi-moments-page__masonry-image {
|
||||||
|
aspect-ratio: auto;
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__masonry-media:has(.wi-moments-page__masonry-image:nth-child(2):last-child) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__masonry-media:has(.wi-moments-page__masonry-image:nth-child(4)) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__masonry-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__masonry-date {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--ink-3);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.wi-moments-page__timeline {
|
||||||
|
padding-left: var(--space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__dot {
|
||||||
|
left: calc(-1 * var(--space-xl) - 5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__line {
|
||||||
|
left: calc(-1 * var(--space-xl));
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__masonry {
|
||||||
|
columns: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__cards {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.wi-moments-page__timeline {
|
||||||
|
padding-left: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__dot {
|
||||||
|
left: calc(-1 * var(--space-lg) - 5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__line {
|
||||||
|
left: calc(-1 * var(--space-lg));
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-moments-page__media,
|
||||||
|
.wi-moments-page__card-media,
|
||||||
|
.wi-moments-page__masonry-media {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+126
-9
@@ -1,25 +1,142 @@
|
|||||||
---
|
---
|
||||||
import Layout from "../layouts/Layout.astro";
|
import Layout from "../layouts/Layout.astro";
|
||||||
|
import LightGallery from "../components/LightGallery.astro";
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout contentClass="prose">
|
<Layout>
|
||||||
<Fragment slot="head">
|
<Fragment slot="head">
|
||||||
<title th:text="|${singlePage.spec.title} - ${site.title}|"></title>
|
<title th:text="|${singlePage.spec.title} - ${site.title}|"></title>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|
||||||
<div class="page-heading">
|
<article class="wi-page">
|
||||||
<h1 th:text="${singlePage.spec.title}"></h1>
|
<header class="wi-page__header">
|
||||||
<p class="page-meta">
|
<h1
|
||||||
<span
|
class="wi-page__title"
|
||||||
|
th:text="${singlePage.spec.title}"
|
||||||
|
></h1>
|
||||||
|
<span class="wi-page-accent"></span>
|
||||||
|
<div class="wi-page__meta">
|
||||||
|
<time
|
||||||
|
th:datetime="${singlePage.spec.publishTime}"
|
||||||
th:text="${#dates.format(singlePage.spec.publishTime, 'yyyy-MM-dd')}"
|
th:text="${#dates.format(singlePage.spec.publishTime, 'yyyy-MM-dd')}"
|
||||||
></span>
|
></time>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div th:utext="${singlePage.content.content}"></div>
|
<div
|
||||||
|
class="wi-page__body"
|
||||||
|
th:utext="${singlePage.content.content}"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div class="wi-page__comment">
|
||||||
<halo:comment
|
<halo:comment
|
||||||
|
th:if="${haloCommentEnabled}"
|
||||||
group="content.halo.run"
|
group="content.halo.run"
|
||||||
kind="SinglePage"
|
kind="SinglePage"
|
||||||
th:attr="name=${singlePage.metadata.name}"></halo:comment>
|
th:attr="name=${singlePage.metadata.name}"
|
||||||
|
></halo:comment>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<LightGallery />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wi-page {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
padding-block: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-page__header {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-md);
|
||||||
|
padding-bottom: var(--space-lg);
|
||||||
|
border-bottom: 1px solid var(--rule);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-page__title {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: clamp(1.8rem, 4vw, 2.8rem);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: var(--leading-tight);
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: var(--ink);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-page-accent {
|
||||||
|
display: block;
|
||||||
|
width: 48px;
|
||||||
|
height: 3px;
|
||||||
|
margin: var(--space-sm) auto 0;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-page__meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
color: var(--ink-3);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-page__body {
|
||||||
|
max-width: var(--content-max);
|
||||||
|
margin-inline: auto;
|
||||||
|
font-size: var(--text-md);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-page__body :is(h1, h2, h3, h4, h5, h6) {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
margin-top: 2em;
|
||||||
|
margin-bottom: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-page__body p {
|
||||||
|
margin-block-end: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-page__body img {
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-block: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-page__body a {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
text-underline-offset: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-page__body ul,
|
||||||
|
.wi-page__body ol {
|
||||||
|
padding-inline-start: 1.5em;
|
||||||
|
margin-block-end: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-page__body li {
|
||||||
|
margin-block-end: 0.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wi-page__comment {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
halo\:comment {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.wi-page__header {
|
||||||
|
padding-bottom: var(--space-xl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user