first commit

This commit is contained in:
nxxy335top
2026-05-18 17:29:18 +08:00
parent e85fbd62f4
commit a0c64736eb
119 changed files with 13527 additions and 1228 deletions
+245 -48
View File
@@ -2,64 +2,261 @@
import Layout from "../layouts/Layout.astro";
---
<Layout pageTitle="归档">
<Layout>
<Fragment slot="head">
<title th:text="|归档 - ${site.title}|"></title>
</Fragment>
<section class="archive-list" aria-label="Archive list">
<th:block th:each="archive : ${archives.items}">
<th:block th:each="month : ${archive.months}">
<div class="archive-group">
<h2
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">
<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="month : ${archive.months}">
<div class="wi-archives__group">
<div class="wi-archives__group-label">
<span
class="archive-entry__date"
th:text="${#dates.format(post.spec.publishTime, 'MM/dd')}"
class="wi-archives__dot"
aria-hidden="true"
></span>
<div>
<h3 class="archive-entry__title">
<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')}"
></span>
<div class="wi-archives__entry-body">
<a
class="wi-archives__entry-title"
th:href="@{${post.status.permalink}}"
th:text="${post.spec.title}"></a>
</h3>
<p
class="archive-entry__summary"
th:text="${post.status.excerpt}"
>
</p>
</div>
</li>
</ul>
</div>
th:text="${post.spec.title}"
></a>
<p
class="wi-archives__entry-excerpt"
th:text="${post.status.excerpt}"
></p>
</div>
</li>
</ul>
</div>
</th:block>
</th:block>
</th:block>
<div th:if="${archives.total == 0}" class="archive-empty">暂无文章。</div>
</section>
<div
th:if="${archives.total == 0}"
class="wi-archives__empty"
th:text="${theme.config?.home?.home_label_no_posts ?: '暂无文章。'}"
>暂无文章。</div>
</section>
<nav
th:if="${archives.totalPages gt 1}"
class="pagination"
aria-label="Pagination"
>
<a
th:if="${archives.hasPrevious()}"
th:href="@{${archives.prevUrl}}"
class="pagination__prev">&larr; 较新</a
<nav
th:if="${archives.totalPages gt 1}"
class="wi-archives__pagination"
aria-label="Pagination"
>
<span
class="pagination__info"
th:text="|${archives.page} / ${archives.totalPages}|"></span>
<a
th:if="${archives.hasNext()}"
th:href="@{${archives.nextUrl}}"
class="pagination__next">较旧 &rarr;</a
>
</nav>
<a
th:if="${archives.hasPrevious()}"
th:href="@{${archives.prevUrl}}"
class="wi-archives__page-link wi-archives__page-link--prev"
th:text="${theme.config?.home?.home_label_newer ?: '较新'}"
>&larr; 较新</a>
<span
class="wi-archives__page-info"
th:text="|${archives.page} / ${archives.totalPages}|"
></span>
<a
th:if="${archives.hasNext()}"
th:href="@{${archives.nextUrl}}"
class="wi-archives__page-link wi-archives__page-link--next"
th:text="${theme.config?.home?.home_label_older ?: '较旧'}"
>较旧 &rarr;</a>
</nav>
</div>
</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>
+147 -9
View File
@@ -2,21 +2,159 @@
import Layout from "../layouts/Layout.astro";
---
<Layout pageTitle="分类">
<Layout>
<Fragment slot="head">
<title th:text="|分类 - ${site.title}|"></title>
</Fragment>
<nav class="taxonomy-list" aria-label="Category list">
<div th:each="category : ${categories}" class="taxonomy-list__item">
<div class="wi-categories">
<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
class="taxonomy-list__link"
th:each="category : ${categories}"
class="wi-categories__item"
th:href="@{${category.status.permalink}}"
th:text="${category.spec.displayName}"
>
<div class="wi-categories__item-content">
<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>
<span
class="wi-categories__count"
th:text="${category.postCount}"
></span>
</a>
<span class="taxonomy-list__count" th:text="|${category.postCount} 篇|">
</span>
</div>
</nav>
</nav>
</div>
</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>
+119 -29
View File
@@ -7,34 +7,124 @@ import Layout from "../layouts/Layout.astro";
<title th:text="|${category.spec.displayName} - ${site.title}|"></title>
</Fragment>
<div class="page-heading">
<h1 th:text="${category.spec.displayName}"></h1>
<p class="page-meta">
<span th:text="|${posts.total} 篇文章|"></span>
</p>
<div class="wi-category">
<header class="wi-category__header">
<h1
class="wi-category__title"
th:text="${category.spec.displayName}"
></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>
<div
th:if="${posts.total == 0}"
class="wi-category__empty"
th:text="${theme.config?.home?.home_label_no_posts ?: '暂无文章。'}"
>暂无文章。</div>
<nav
th:if="${posts.totalPages gt 1}"
class="wi-category__pagination"
aria-label="Pagination"
>
<a
th:if="${posts.hasPrevious()}"
th:href="@{${posts.prevUrl}}"
class="wi-category__page-link wi-category__page-link--prev"
th:text="${theme.config?.home?.home_label_newer ?: '较新'}"
>&larr; 较新</a>
<span
class="wi-category__page-info"
th:text="|${posts.page} / ${posts.totalPages}|"
></span>
<a
th:if="${posts.hasNext()}"
th:href="@{${posts.nextUrl}}"
class="wi-category__page-link wi-category__page-link--next"
th:text="${theme.config?.home?.home_label_older ?: '较旧'}"
>较旧 &rarr;</a>
</nav>
</div>
<th:block th:replace="~{fragments/post-list}"></th:block>
<div th:if="${posts.total == 0}" class="feed-empty">暂无文章。</div>
<nav
th:if="${posts.totalPages gt 1}"
class="pagination"
aria-label="Pagination"
>
<a
th:if="${posts.hasPrevious()}"
th:href="@{${posts.prevUrl}}"
class="pagination__prev">&larr; 较新</a
>
<span
class="pagination__info"
th:text="|${posts.page} / ${posts.totalPages}|"></span>
<a
th:if="${posts.hasNext()}"
th:href="@{${posts.nextUrl}}"
class="pagination__next">较旧 &rarr;</a
>
</nav>
</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>
+208
View File
@@ -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>
+310
View File
@@ -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>
+554 -20
View File
@@ -1,31 +1,565 @@
---
import Layout from "../layouts/Layout.astro";
import HeroSection from "../components/HeroSection.astro";
---
<Layout>
<Layout home>
<Fragment slot="head">
<title th:text="${site.title}"></title>
</Fragment>
<th:block th:replace="~{fragments/post-list}"></th:block>
<HeroSection />
<nav
th:if="${posts.totalPages gt 1}"
class="pagination"
aria-label="Pagination"
<section
class="wi-home-pinned"
th:if="${posts != null and !#lists.isEmpty(posts.items)}"
>
<a
th:if="${posts.hasPrevious()}"
th:href="@{${posts.prevUrl}}"
class="pagination__prev">&larr; 较新</a
>
<span
class="pagination__info"
th:text="|${posts.page} / ${posts.totalPages}|"></span>
<a
th:if="${posts.hasNext()}"
th:href="@{${posts.nextUrl}}"
class="pagination__next">较旧 &rarr;</a
>
</nav>
<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
th:if="${posts.totalPages gt 1 and theme.config?.home?.home_post_loading != 'infinite_scroll'}"
class="wi-pagination"
id="wi-post-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 ?: '较新'}"
>&larr; 较新</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 ?: '较旧'}"
>较旧 &rarr;</a>
</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>
<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>
+263
View File
@@ -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>
+735
View File
@@ -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>
+131 -14
View File
@@ -1,25 +1,142 @@
---
import Layout from "../layouts/Layout.astro";
import LightGallery from "../components/LightGallery.astro";
---
<Layout contentClass="prose">
<Layout>
<Fragment slot="head">
<title th:text="|${singlePage.spec.title} - ${site.title}|"></title>
</Fragment>
<div class="page-heading">
<h1 th:text="${singlePage.spec.title}"></h1>
<p class="page-meta">
<span
th:text="${#dates.format(singlePage.spec.publishTime, 'yyyy-MM-dd')}"
></span>
</p>
</div>
<article class="wi-page">
<header class="wi-page__header">
<h1
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')}"
></time>
</div>
</header>
<div th:utext="${singlePage.content.content}"></div>
<div
class="wi-page__body"
th:utext="${singlePage.content.content}"
></div>
<halo:comment
group="content.halo.run"
kind="SinglePage"
th:attr="name=${singlePage.metadata.name}"></halo:comment>
<div class="wi-page__comment">
<halo:comment
th:if="${haloCommentEnabled}"
group="content.halo.run"
kind="SinglePage"
th:attr="name=${singlePage.metadata.name}"
></halo:comment>
</div>
</article>
<LightGallery />
</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>
+140
View File
@@ -0,0 +1,140 @@
---
import Layout from "../layouts/Layout.astro";
---
<Layout>
<Fragment slot="head">
<title
th:text="|${theme.config?.messageboard?.messageboard_title ?: '留言板'} - ${site.title}|"
></title>
</Fragment>
<div class="wi-messageboard">
<header class="wi-messageboard__header">
<span class="wi-messageboard__deco">"</span>
<h1
class="wi-messageboard__title"
th:text="${theme.config?.messageboard?.messageboard_title ?: '留言板'}"
></h1>
<span class="wi-page-accent"></span>
<p
class="wi-messageboard__subtitle"
th:text="${theme.config?.messageboard?.messageboard_subtitle}"
></p>
</header>
<div class="wi-messageboard__content">
<div
class="wi-messageboard__body"
th:utext="${singlePage.content.content}"
></div>
</div>
<div class="wi-messageboard__comment">
<halo:comment
th:if="${haloCommentEnabled}"
group="content.halo.run"
kind="SinglePage"
th:attr="name=${singlePage.metadata.name}"
></halo:comment>
</div>
</div>
</Layout>
<style>
.wi-messageboard {
display: grid;
gap: var(--space-lg);
margin-top: calc(-1 * 100px - var(--space-lg));
padding-top: 100px;
padding-block-end: 1rem;
}
.wi-messageboard__header {
display: grid;
gap: var(--space-md);
padding: var(--space-3xl) var(--space-xl);
text-align: center;
background: var(--bg-raised);
border-radius: 16px;
border: 1px solid var(--rule);
position: relative;
overflow: hidden;
margin-top: 0;
}
.wi-messageboard__deco {
font-family: var(--font-sans);
font-size: clamp(4rem, 10vw, 7rem);
line-height: 1;
color: var(--accent);
opacity: 0.18;
position: absolute;
top: -0.15em;
left: 0.15em;
pointer-events: none;
user-select: none;
}
.wi-messageboard__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;
position: relative;
}
.wi-page-accent {
display: block;
width: 48px;
height: 3px;
margin: var(--space-sm) auto 0;
border-radius: 9999px;
background: var(--accent);
}
.wi-messageboard__subtitle {
font-size: var(--text-md);
color: var(--ink-2);
line-height: var(--leading-relaxed);
margin: 0;
position: relative;
}
.wi-messageboard__content {
max-width: var(--content-max);
margin-inline: auto;
width: 100%;
}
.wi-messageboard__body {
font-size: var(--text-md);
line-height: var(--leading-relaxed);
color: var(--ink);
}
.wi-messageboard__body p {
margin-block-end: 1.2em;
}
.wi-messageboard__comment {
width: 100%;
max-width: 600px;
margin-inline: auto;
}
halo\:comment {
display: block;
width: 100%;
}
@media (max-width: 768px) {
.wi-messageboard__header {
padding: var(--space-2xl) var(--space-lg);
border-radius: 12px;
}
}
</style>
+643
View File
@@ -0,0 +1,643 @@
---
import Layout from "../layouts/Layout.astro";
import LightGallery from "../components/LightGallery.astro";
---
<Layout wide>
<Fragment slot="head">
<title th:text="|${title ?: (theme.config?.photos?.photos_page_title ?: '图库')} - ${site.title}|"></title>
</Fragment>
<section class="wi-photos-page">
<div class="wi-container">
<div class="wi-photos-page__header">
<h1
class="wi-photos-page__title"
th:text="${theme.config?.photos?.photos_page_title ?: '图库'}"
>
图库
</h1>
<span class="wi-page-accent"></span>
</div>
<th:block th:if="${pluginFinder.available('PluginPhotos')}">
<th:block th:if="${groups != null}" th:with="currentGroup=${'' + param.group}, showGroupTitle=${theme.config?.photos?.photos_show_group_title != false}, showUngrouped=${theme.config?.photos?.photos_show_ungrouped == true}">
<nav
class="wi-photos-page__groups"
th:if="${groups != null and !#lists.isEmpty(groups)}"
>
<a
th:href="@{${photoUrl.list()}}"
class="wi-photos-page__group-link"
th:classappend="${#strings.isEmpty(currentGroup)} ? 'wi-photos-page__group-link--active' : ''"
>
全部
</a>
<a
th:each="group : ${groups}"
th:href="@{${photoUrl.list(group.metadata.name)}}"
th:text="${group.spec.displayName}"
class="wi-photos-page__group-link"
th:classappend="${#strings.equals(currentGroup, group.metadata.name)} ? 'wi-photos-page__group-link--active' : ''"
></a>
</nav>
<th:block th:if="${!#lists.isEmpty(groups) or (showUngrouped and photos != null and !#lists.isEmpty(photos.items))}">
<th:block th:if="${#strings.isEmpty(currentGroup)}">
<th:block th:if="${showGroupTitle}">
<div class="wi-photos-page__all-groups">
<th:block th:if="${showUngrouped and photos != null and !#lists.isEmpty(photos.items)}">
<div class="wi-photos-page__section">
<h2 class="wi-photos-page__section-title">全部</h2>
<div
class="wi-photos-page__grid"
th:attr="data-style=${theme.config?.photos?.photos_style ?: 'masonry'}, data-columns=${theme.config?.photos?.photos_columns ?: 3}"
>
<th:block th:each="photo : ${photos.items}">
<div
class="wi-photos-page__item"
th:if="${#strings.isEmpty(photo.spec.groupName)}"
>
<a
th:href="${photo.spec.url}"
class="wi-photos-page__link"
th:attr="data-src=${photo.spec.url}, data-sub-html=${photo.spec.displayName ?: ''}, data-exif=${photo.exif != null ? (photo.exif.make ?: '') + '|' + (photo.exif.model ?: '') + '|' + (photo.exif.lensModel ?: '') + '|' + (photo.exif.fNumber ?: '') + '|' + (photo.exif.exposureTime ?: '') + '|' + (photo.exif.iso ?: '') + '|' + (photo.exif.focalLength ?: '') + '|' + (photo.exif.focalLengthIn35mm ?: '') : ''}"
>
<img
th:src="${photo.spec.cover ?: photo.spec.url}"
th:alt="${photo.spec.displayName ?: ''}"
class="wi-photos-page__image"
loading="lazy"
/>
<div class="wi-photos-page__overlay">
<span
class="wi-photos-page__caption"
th:if="${photo.spec.displayName}"
th:text="${photo.spec.displayName}"
></span>
</div>
</a>
</div>
</th:block>
</div>
</div>
</th:block>
<th:block th:each="group : ${groups}">
<div
class="wi-photos-page__section"
th:if="${!#lists.isEmpty(group.photos)}"
>
<h2
class="wi-photos-page__section-title"
th:if="${group.spec?.displayName != null}"
th:text="${group.spec?.displayName}"
></h2>
<div
class="wi-photos-page__grid"
th:attr="data-style=${theme.config?.photos?.photos_style ?: 'masonry'}, data-columns=${theme.config?.photos?.photos_columns ?: 3}"
>
<div
class="wi-photos-page__item"
th:each="photo : ${group.photos}"
>
<a
th:href="${photo.spec.url}"
class="wi-photos-page__link"
th:attr="data-src=${photo.spec.url}, data-sub-html=${photo.spec.displayName ?: ''}, data-exif=${photo.exif != null ? (photo.exif.make ?: '') + '|' + (photo.exif.model ?: '') + '|' + (photo.exif.lensModel ?: '') + '|' + (photo.exif.fNumber ?: '') + '|' + (photo.exif.exposureTime ?: '') + '|' + (photo.exif.iso ?: '') + '|' + (photo.exif.focalLength ?: '') + '|' + (photo.exif.focalLengthIn35mm ?: '') : ''}"
>
<img
th:src="${photo.spec.cover ?: photo.spec.url}"
th:alt="${photo.spec.displayName ?: ''}"
class="wi-photos-page__image"
loading="lazy"
/>
<div class="wi-photos-page__overlay">
<span
class="wi-photos-page__caption"
th:if="${photo.spec.displayName}"
th:text="${photo.spec.displayName}"
></span>
</div>
</a>
</div>
</div>
</div>
</th:block>
</div>
</th:block>
<th:block th:if="${!showGroupTitle}">
<div
class="wi-photos-page__grid"
th:attr="data-style=${theme.config?.photos?.photos_style ?: 'masonry'}, data-columns=${theme.config?.photos?.photos_columns ?: 3}"
>
<th:block th:if="${showUngrouped and photos != null and !#lists.isEmpty(photos.items)}">
<th:block th:each="photo : ${photos.items}">
<div
class="wi-photos-page__item"
th:if="${#strings.isEmpty(photo.spec.groupName)}"
>
<a
th:href="${photo.spec.url}"
class="wi-photos-page__link"
th:attr="data-src=${photo.spec.url}, data-sub-html=${photo.spec.displayName ?: ''}, data-exif=${photo.exif != null ? (photo.exif.make ?: '') + '|' + (photo.exif.model ?: '') + '|' + (photo.exif.lensModel ?: '') + '|' + (photo.exif.fNumber ?: '') + '|' + (photo.exif.exposureTime ?: '') + '|' + (photo.exif.iso ?: '') + '|' + (photo.exif.focalLength ?: '') + '|' + (photo.exif.focalLengthIn35mm ?: '') : ''}"
>
<img
th:src="${photo.spec.cover ?: photo.spec.url}"
th:alt="${photo.spec.displayName ?: ''}"
class="wi-photos-page__image"
loading="lazy"
/>
<div class="wi-photos-page__overlay">
<span
class="wi-photos-page__caption"
th:if="${photo.spec.displayName}"
th:text="${photo.spec.displayName}"
></span>
</div>
</a>
</div>
</th:block>
</th:block>
<th:block th:each="group : ${groups}">
<div
class="wi-photos-page__item"
th:each="photo : ${group.photos}"
>
<a
th:href="${photo.spec.url}"
class="wi-photos-page__link"
th:attr="data-src=${photo.spec.url}, data-sub-html=${photo.spec.displayName ?: ''}, data-exif=${photo.exif != null ? (photo.exif.make ?: '') + '|' + (photo.exif.model ?: '') + '|' + (photo.exif.lensModel ?: '') + '|' + (photo.exif.fNumber ?: '') + '|' + (photo.exif.exposureTime ?: '') + '|' + (photo.exif.iso ?: '') + '|' + (photo.exif.focalLength ?: '') + '|' + (photo.exif.focalLengthIn35mm ?: '') : ''}"
>
<img
th:src="${photo.spec.cover ?: photo.spec.url}"
th:alt="${photo.spec.displayName ?: ''}"
class="wi-photos-page__image"
loading="lazy"
/>
<div class="wi-photos-page__overlay">
<span
class="wi-photos-page__caption"
th:if="${photo.spec.displayName}"
th:text="${photo.spec.displayName}"
></span>
</div>
</a>
</div>
</th:block>
</div>
</th:block>
</th:block>
<th:block th:if="${!#strings.isEmpty(currentGroup)}">
<th:block th:each="group : ${groups}">
<th:block th:if="${#strings.equals(currentGroup, group.metadata.name)}">
<div
class="wi-photos-page__section"
th:if="${!#lists.isEmpty(group.photos)}"
>
<h2
class="wi-photos-page__section-title"
th:if="${group.spec?.displayName != null}"
th:text="${group.spec?.displayName}"
></h2>
<div
class="wi-photos-page__grid"
th:attr="data-style=${theme.config?.photos?.photos_style ?: 'masonry'}, data-columns=${theme.config?.photos?.photos_columns ?: 3}"
>
<div
class="wi-photos-page__item"
th:each="photo : ${group.photos}"
>
<a
th:href="${photo.spec.url}"
class="wi-photos-page__link"
th:attr="data-src=${photo.spec.url}, data-sub-html=${photo.spec.displayName ?: ''}, data-exif=${photo.exif != null ? (photo.exif.make ?: '') + '|' + (photo.exif.model ?: '') + '|' + (photo.exif.lensModel ?: '') + '|' + (photo.exif.fNumber ?: '') + '|' + (photo.exif.exposureTime ?: '') + '|' + (photo.exif.iso ?: '') + '|' + (photo.exif.focalLength ?: '') + '|' + (photo.exif.focalLengthIn35mm ?: '') : ''}"
>
<img
th:src="${photo.spec.cover ?: photo.spec.url}"
th:alt="${photo.spec.displayName ?: ''}"
class="wi-photos-page__image"
loading="lazy"
/>
<div class="wi-photos-page__overlay">
<span
class="wi-photos-page__caption"
th:if="${photo.spec.displayName}"
th:text="${photo.spec.displayName}"
></span>
</div>
</a>
</div>
</div>
</div>
<div class="wi-photos-page__empty" th:if="${#lists.isEmpty(group.photos)}">
<p>该分组暂无图片</p>
</div>
</th:block>
</th:block>
</th:block>
</th:block>
<div class="wi-photos-page__empty" th:if="${#lists.isEmpty(groups) and !(showUngrouped and photos != null and !#lists.isEmpty(photos.items))}">
<p>暂无图片</p>
</div>
</th:block>
<th:block th:if="${groups == null}">
<div class="wi-photos-page__empty">
<p>图库插件加载异常,请检查插件状态</p>
</div>
</th:block>
</th:block>
<th:block th:unless="${pluginFinder.available('PluginPhotos')}">
<div class="wi-photos-page__empty">
<p>
图库功能需要安装「图库」插件,<a
href="https://www.halo.run/store/apps/app-PgHgw"
>前往应用市场安装</a
>
</p>
</div>
</th:block>
</div>
</section>
<LightGallery />
</Layout>
<style>
.wi-photos-page {
padding-block: 1rem;
}
.wi-photos-page__header {
text-align: center;
margin-bottom: var(--space-3xl);
}
.wi-photos-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-photos-page__groups {
display: flex;
flex-wrap: wrap;
gap: var(--space-sm);
justify-content: center;
margin-bottom: var(--space-2xl);
}
.wi-photos-page__group-link {
display: inline-flex;
align-items: center;
padding: 6px 16px;
border-radius: 9999px;
font-size: var(--text-sm);
font-weight: 500;
color: var(--ink-2);
background: var(--glass-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--glass-border);
text-decoration: none;
transition:
color var(--duration-fast) var(--ease-out-expo),
background var(--duration-fast) var(--ease-out-expo),
border-color var(--duration-fast) var(--ease-out-expo),
transform var(--duration-fast) var(--ease-out-expo);
}
.wi-photos-page__group-link:hover {
color: var(--accent);
border-color: var(--accent);
transform: translateY(-1px);
}
.wi-photos-page__group-link--active {
color: var(--bg);
background: var(--accent);
border-color: var(--accent);
}
.wi-photos-page__group-link--active:hover {
color: var(--bg);
background: var(--accent);
transform: none;
}
.wi-photos-page__section {
margin-bottom: var(--space-2xl);
}
.wi-photos-page__section:last-child {
margin-bottom: 0;
}
.wi-photos-page__section-title {
font-size: var(--text-lg);
font-weight: 600;
color: var(--ink-2);
margin-bottom: var(--space-lg);
padding-bottom: var(--space-sm);
border-bottom: 1px solid var(--rule);
}
.wi-photos-page__empty {
text-align: center;
padding: var(--space-4xl) var(--space-xl);
color: var(--ink-3);
font-size: var(--text-md);
}
.wi-photos-page__empty a {
color: var(--accent);
}
.wi-photos-page__grid {
display: grid;
gap: var(--space-md);
}
.wi-photos-page__grid[data-style="masonry"] {
grid-template-columns: repeat(3, 1fr);
grid-auto-rows: 10px;
}
.wi-photos-page__grid[data-style="masonry"][data-columns="2"] {
grid-template-columns: repeat(2, 1fr);
}
.wi-photos-page__grid[data-style="masonry"][data-columns="4"] {
grid-template-columns: repeat(4, 1fr);
}
.wi-photos-page__grid[data-style="grid"] {
grid-template-columns: repeat(3, 1fr);
}
.wi-photos-page__grid[data-style="grid"][data-columns="2"] {
grid-template-columns: repeat(2, 1fr);
}
.wi-photos-page__grid[data-style="grid"][data-columns="4"] {
grid-template-columns: repeat(4, 1fr);
}
.wi-photos-page__grid[data-style="grid"] .wi-photos-page__image {
aspect-ratio: 1;
object-fit: cover;
}
.wi-photos-page__grid[data-style="carousel"] {
grid-template-columns: unset;
display: flex;
gap: var(--space-md);
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-padding: var(--space-md);
padding-bottom: var(--space-md);
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
scrollbar-color: var(--accent) transparent;
}
.wi-photos-page__grid[data-style="carousel"]::-webkit-scrollbar {
height: 4px;
}
.wi-photos-page__grid[data-style="carousel"]::-webkit-scrollbar-track {
background: transparent;
}
.wi-photos-page__grid[data-style="carousel"]::-webkit-scrollbar-thumb {
background: var(--accent);
border-radius: 9999px;
}
.wi-photos-page__grid[data-style="carousel"] .wi-photos-page__item {
flex: 0 0 auto;
scroll-snap-align: start;
width: calc(100% / 3 - var(--space-md) * 2 / 3);
}
.wi-photos-page__grid[data-style="carousel"][data-columns="2"] .wi-photos-page__item {
width: calc(100% / 2 - var(--space-md) / 2);
}
.wi-photos-page__grid[data-style="carousel"][data-columns="4"] .wi-photos-page__item {
width: calc(100% / 4 - var(--space-md) * 3 / 4);
}
.wi-photos-page__grid[data-style="carousel"] .wi-photos-page__image {
aspect-ratio: 4 / 3;
object-fit: cover;
}
.wi-photos-page__item {
border-radius: 12px;
overflow: hidden;
}
.wi-photos-page__link {
display: block;
position: relative;
text-decoration: none;
border-radius: 12px;
overflow: hidden;
cursor: zoom-in;
}
.wi-photos-page__image {
display: block;
width: 100%;
height: auto;
border-radius: 12px;
transition:
transform var(--duration-slow) var(--ease-out-expo),
filter var(--duration-normal) var(--ease-out-expo);
}
.wi-photos-page__link:hover .wi-photos-page__image {
transform: scale(1.05);
filter: brightness(0.92);
}
.wi-photos-page__overlay {
position: absolute;
inset: 0;
display: flex;
align-items: flex-end;
justify-content: center;
padding: var(--space-lg) var(--space-md);
background: linear-gradient(
to top,
rgba(0, 0, 0, 0.45) 0%,
transparent 50%
);
opacity: 0;
transition: opacity var(--duration-normal) var(--ease-out-expo);
pointer-events: none;
}
.wi-photos-page__link:hover .wi-photos-page__overlay {
opacity: 1;
}
.wi-photos-page__caption {
font-size: var(--text-sm);
color: #fff;
text-align: center;
line-height: var(--leading-snug);
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
}
@media (max-width: 768px) {
.wi-photos-page__grid[data-style="masonry"] {
grid-template-columns: repeat(2, 1fr);
}
.wi-photos-page__grid[data-style="grid"] {
grid-template-columns: repeat(2, 1fr);
}
.wi-photos-page__grid[data-style="carousel"] .wi-photos-page__item {
width: calc(100% / 2 - var(--space-md) / 2);
}
.wi-photos-page__groups {
gap: 6px;
}
.wi-photos-page__group-link {
padding: 4px 12px;
font-size: var(--text-xs);
}
}
@media (max-width: 480px) {
.wi-photos-page__grid[data-style="masonry"] {
grid-template-columns: repeat(2, 1fr);
gap: var(--space-sm);
}
.wi-photos-page__grid[data-style="grid"] {
grid-template-columns: repeat(2, 1fr);
gap: var(--space-sm);
}
.wi-photos-page__grid[data-style="carousel"] .wi-photos-page__item {
width: 75%;
}
}
</style>
<script is:inline>
(function () {
var rafPending = false;
function layoutMasonry() {
if (rafPending) return;
rafPending = true;
requestAnimationFrame(function () {
rafPending = false;
var grids = document.querySelectorAll(
'.wi-photos-page__grid[data-style="masonry"]'
);
grids.forEach(function (grid) {
var items = grid.querySelectorAll('.wi-photos-page__item');
if (items.length === 0) return;
var rowGap = parseInt(getComputedStyle(grid).rowGap) || 8;
var rowHeight = 10;
items.forEach(function (item) {
var img = item.querySelector('.wi-photos-page__image');
if (!img) return;
var ratio = 0;
var w = img.getAttribute('width');
var h = img.getAttribute('height');
if (w && h && parseInt(w) > 0 && parseInt(h) > 0) {
ratio = parseInt(h) / parseInt(w);
} else if (img.naturalWidth > 0 && img.naturalHeight > 0) {
ratio = img.naturalHeight / img.naturalWidth;
}
if (!ratio || ratio <= 0) return;
var itemWidth = item.getBoundingClientRect().width;
var rowSpan = Math.ceil(
(itemWidth * ratio + rowGap) / (rowHeight + rowGap)
);
item.style.gridRowEnd = 'span ' + rowSpan;
});
});
});
}
function observeImages() {
var images = document.querySelectorAll(
'.wi-photos-page__grid[data-style="masonry"] .wi-photos-page__image'
);
if (images.length === 0) return;
images.forEach(function (img) {
if (img.complete && img.naturalHeight > 0) {
layoutMasonry();
} else {
img.addEventListener('load', layoutMasonry);
img.addEventListener('error', layoutMasonry);
}
});
}
if (typeof ResizeObserver !== 'undefined') {
var ro = new ResizeObserver(function () {
layoutMasonry();
});
var observeGrids = function () {
var grids = document.querySelectorAll(
'.wi-photos-page__grid[data-style="masonry"]'
);
grids.forEach(function (grid) {
ro.observe(grid);
});
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () {
observeGrids();
observeImages();
});
} else {
observeGrids();
observeImages();
}
} else {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', observeImages);
} else {
observeImages();
}
}
var resizeTimer;
window.addEventListener('resize', function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(layoutMasonry, 150);
});
})();
</script>
+1041 -14
View File
File diff suppressed because it is too large Load Diff
+242 -48
View File
@@ -7,53 +7,247 @@ import Layout from "../layouts/Layout.astro";
<title th:text="|${tag.spec.displayName} - ${site.title}|"></title>
</Fragment>
<div class="page-heading">
<h1 th:text="${tag.spec.displayName}"></h1>
<p class="page-meta">
<span th:text="|${posts.total} 篇文章|"></span>
</p>
<div class="wi-tag">
<header class="wi-tag__header">
<h1 class="wi-tag__title" th:text="${tag.spec.displayName}"></h1>
<span class="wi-page-accent"></span>
<p class="wi-tag__subtitle" th:text="${theme.config?.home?.home_label_post_count ?: '共 {total} 篇文章}'.replace('{total}', posts.total)}"></p>
<p
th:if="${not #strings.isEmpty(tag.spec.description)}"
class="wi-tag__description"
th:text="${tag.spec.description}"
></p>
</header>
<ul class="wi-tag__list" role="list">
<li
th:each="post : ${posts.items}"
class="wi-tag__entry"
>
<a
class="wi-tag__entry-link"
th:href="@{${post.status.permalink}}"
>
<div class="wi-tag__entry-body">
<time
class="wi-tag__entry-date"
th:text="${#dates.format(post.spec.publishTime, 'yyyy-MM-dd')}"
datetime="${post.spec.publishTime}"
></time>
<h2 class="wi-tag__entry-title" th:text="${post.spec.title}"></h2>
<p
class="wi-tag__entry-excerpt"
th:text="${post.status.excerpt}"
></p>
</div>
<img
th:unless="${#strings.isEmpty(post.spec.cover)}"
class="wi-tag__entry-image"
th:src="${post.spec.cover}"
alt=""
loading="lazy"
/>
</a>
</li>
</ul>
<div
th:if="${posts.total == 0}"
class="wi-tag__empty"
th:text="${theme.config?.home?.home_label_no_posts ?: '暂无文章。'}"
>暂无文章。</div>
<nav
th:if="${posts.totalPages gt 1}"
class="wi-tag__pagination"
aria-label="Pagination"
>
<a
th:if="${posts.hasPrevious()}"
th:href="@{${posts.prevUrl}}"
class="wi-tag__page-link wi-tag__page-link--prev"
th:text="${theme.config?.home?.home_label_newer ?: '较新'}"
>&larr; 较新</a>
<span
class="wi-tag__page-info"
th:text="|${posts.page} / ${posts.totalPages}|"
></span>
<a
th:if="${posts.hasNext()}"
th:href="@{${posts.nextUrl}}"
class="wi-tag__page-link wi-tag__page-link--next"
th:text="${theme.config?.home?.home_label_older ?: '较旧'}"
>较旧 &rarr;</a>
</nav>
</div>
<ul class="post-feed__list" role="list">
<li th:each="post : ${posts.items}" class="post-feed__item">
<a class="post-feed__link" th:href="@{${post.status.permalink}}">
<div class="post-feed__body">
<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
th:unless="${#strings.isEmpty(post.spec.cover)}"
class="post-feed__image"
th:src="${post.spec.cover}"
/>
</a>
</li>
</ul>
<div th:if="${posts.total == 0}" class="feed-empty">暂无文章。</div>
<nav
th:if="${posts.totalPages gt 1}"
class="pagination"
aria-label="Pagination"
>
<a
th:if="${posts.hasPrevious()}"
th:href="@{${posts.prevUrl}}"
class="pagination__prev">&larr; 较新</a
>
<span
class="pagination__info"
th:text="|${posts.page} / ${posts.totalPages}|"></span>
<a
th:if="${posts.hasNext()}"
th:href="@{${posts.nextUrl}}"
class="pagination__next">较旧 &rarr;</a
>
</nav>
</Layout>
<style>
.wi-tag {
display: grid;
gap: var(--space-lg);
padding-block: 1rem;
}
.wi-tag__header {
text-align: center;
padding-bottom: var(--space-lg);
border-bottom: 1px solid var(--rule);
}
.wi-tag__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-tag__subtitle {
font-size: var(--text-sm);
color: var(--ink-3);
letter-spacing: var(--tracking-wide);
margin: 0;
}
.wi-tag__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-tag__list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: var(--space-lg);
}
.wi-tag__entry {
border-bottom: 1px dashed var(--rule);
transition: background var(--duration-fast) var(--ease-out-quart);
}
.wi-tag__entry:last-child {
border-bottom: none;
}
.wi-tag__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-tag__entry-link {
display: grid;
grid-template-columns: minmax(0, 1fr) 200px;
gap: var(--space-lg);
padding: var(--space-lg) 0;
text-decoration: none;
color: inherit;
align-items: start;
}
.wi-tag__entry-body {
display: grid;
gap: 0.35rem;
}
.wi-tag__entry-date {
color: var(--ink-3);
font-size: var(--text-sm);
font-variant-numeric: tabular-nums;
}
.wi-tag__entry-title {
font-family: var(--font-sans);
font-size: var(--text-lg);
font-weight: 600;
color: var(--ink);
line-height: var(--leading-snug);
margin: 0.25rem 0;
transition: color var(--duration-fast) var(--ease-out-quart);
}
.wi-tag__entry-link:hover .wi-tag__entry-title {
color: var(--accent);
}
.wi-tag__entry-excerpt {
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-tag__entry-image {
width: 200px;
height: 140px;
object-fit: cover;
border-radius: $border-radius-sm;
align-self: center;
transition: transform var(--duration-fast) var(--ease-out-quart);
}
.wi-tag__entry:hover .wi-tag__entry-image {
transform: scale(1.02);
}
.wi-tag__empty {
color: var(--ink-3);
font-size: var(--text-md);
padding: var(--space-2xl) 0;
text-align: center;
}
.wi-tag__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-tag__page-link {
color: var(--ink-2);
text-decoration: none;
transition: color var(--duration-fast) var(--ease-out-quart);
}
.wi-tag__page-link:hover {
color: var(--accent);
}
.wi-tag__page-info {
color: var(--ink-3);
}
@media (max-width: 768px) {
.wi-tag__entry-link {
grid-template-columns: 1fr;
}
.wi-tag__entry-image {
width: 100%;
height: 180px;
order: -1;
}
}
</style>
+109 -11
View File
@@ -2,20 +2,118 @@
import Layout from "../layouts/Layout.astro";
---
<Layout pageTitle="标签">
<Layout>
<Fragment slot="head">
<title th:text="|标签 - ${site.title}|"></title>
</Fragment>
<div class="tag-cloud" role="list" aria-label="Tag list">
<a
th:each="tag : ${tags}"
class="tag-pill"
role="listitem"
th:href="@{${tag.status.permalink}}"
>
<span th:text="${tag.spec.displayName}"></span>
<span class="tag-pill__count" th:text="${tag.postCount}"></span>
</a>
<div class="wi-tags">
<header class="wi-tags__header">
<h1 class="wi-tags__title">标签</h1>
<span class="wi-page-accent"></span>
<p class="wi-tags__subtitle" th:text="|共 ${tags.size} 个标签|"></p>
</header>
<div class="wi-tags__cloud" role="list" aria-label="Tag list">
<a
th:each="tag : ${tags}"
class="wi-tags__pill"
role="listitem"
th:href="@{${tag.status.permalink}}"
>
<span class="wi-tags__pill-name" th:text="${tag.spec.displayName}"></span>
<span class="wi-tags__pill-count" th:text="${tag.postCount}"></span>
</a>
</div>
</div>
</Layout>
<style>
.wi-tags {
display: grid;
gap: var(--space-lg);
padding-block: 1rem;
}
.wi-tags__header {
text-align: center;
padding-bottom: var(--space-lg);
border-bottom: 1px solid var(--rule);
}
.wi-tags__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-tags__subtitle {
font-size: var(--text-sm);
color: var(--ink-3);
letter-spacing: var(--tracking-wide);
margin: 0;
}
.wi-tags__cloud {
display: flex;
flex-wrap: wrap;
gap: var(--space-sm);
}
.wi-tags__pill {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 1rem;
border-radius: 999px;
background: var(--bg-raised);
border: 1px solid var(--rule);
color: var(--ink-2);
font-size: var(--text-sm);
text-decoration: none;
transition:
background var(--duration-fast) var(--ease-out-quart),
color var(--duration-fast) var(--ease-out-quart),
border-color var(--duration-fast) var(--ease-out-quart),
transform var(--duration-fast) var(--ease-out-quart);
}
.wi-tags__pill:hover {
background: var(--accent);
color: var(--bg);
border-color: var(--accent);
transform: translateY(-2px);
box-shadow: var(--shadow-sm);
}
.wi-tags__pill-name {
font-weight: 500;
}
.wi-tags__pill-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.5rem;
height: 1.5rem;
padding: 0 0.35rem;
border-radius: 999px;
font-size: 0.8em;
background: var(--bg);
color: var(--ink-3);
font-weight: 600;
}
.wi-tags__pill:hover .wi-tags__pill-count {
background: rgba(255, 255, 255, 0.2);
color: var(--bg);
}
</style>