first commit

This commit is contained in:
nxxy335top committed 2026-05-18 17:29:18 +08:00
1 parent e85fbd62f4
commit a0c64736eb
119 files changed
+13527 -1228

No files matched your search

+68
View File
@@ -0,0 +1,68 @@
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from "vue";
const x = ref(0);
const y = ref(0);
const visible = ref(false);
function handleMouseMove(e: MouseEvent) {
x.value = e.clientX;
y.value = e.clientY;
visible.value = true;
}
function handleMouseLeave() {
visible.value = false;
}
onMounted(() => {
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseleave", handleMouseLeave);
});
onUnmounted(() => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseleave", handleMouseLeave);
});
</script>
<template>
<div
class="wi-cursor-glow"
:class="{ 'wi-cursor-glow--visible': visible }"
:style="{ transform: `translate(calc(${x}px - 50%), calc(${y}px - 50%))` }"
/>
</template>
<style scoped>
.wi-cursor-glow {
position: fixed;
top: 0;
left: 0;
width: 400px;
height: 400px;
border-radius: 50%;
background: radial-gradient(
circle,
rgba(212, 118, 78, 0.06) 0%,
transparent 70%
);
pointer-events: none;
z-index: 9999;
opacity: 0;
transition: opacity 0.3s ease;
will-change: transform;
}
.wi-cursor-glow--visible {
opacity: 1;
}
html.dark .wi-cursor-glow {
background: radial-gradient(
circle,
rgba(232, 149, 95, 0.04) 0%,
transparent 70%
);
}
</style>
+178
View File
@@ -0,0 +1,178 @@
---
---
<section
class="wi-section"
th:if="${theme.config?.home?.home_featured_enabled != false}"
th:with="featuredPosts = ${postFinder.list({page: 1, size: theme.config?.home?.home_featured_count ?: 3})}"
>
<div class="wi-container">
<div class="wi-section__header">
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_featured_title ?: '精选'}">精选</h2>
</div>
<div class="wi-featured">
<a
th:each="post,iterStat : ${featuredPosts.items}"
th:href="@{${post.status.permalink}}"
class="wi-featured__card"
th:classappend="${iterStat.index == 0} ? 'wi-featured__card--hero' : ''"
>
<div
class="wi-featured__cover"
th:if="${!#strings.isEmpty(post.spec.cover)}"
>
<img th:src="${post.spec.cover}" th:alt="${post.spec.title}" class="wi-featured__image" />
</div>
<div class="wi-featured__body">
<time
class="wi-featured__date"
th:text="${#dates.format(post.spec.publishTime, 'yyyy-MM-dd')}"
></time>
<h3
class="wi-featured__title"
th:text="${post.spec.title}"
></h3>
<p
class="wi-featured__excerpt"
th:if="${post.status.excerpt}"
th:text="${post.status.excerpt}"
></p>
</div>
</a>
</div>
</div>
</section>
<style>
.wi-section__header {
margin-bottom: var(--space-xl);
}
.wi-section__title {
font-family: var(--font-sans);
font-size: var(--text-2xl);
font-weight: 700;
color: var(--ink);
letter-spacing: var(--tracking-tight);
display: flex;
align-items: center;
gap: 0.5rem;
}
.wi-section__title::before {
content: '';
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent);
flex-shrink: 0;
}
.wi-featured {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-lg);
}
.wi-featured__card {
display: flex;
flex-direction: column;
border-radius: 16px;
overflow: hidden;
background: var(--bg-raised);
box-shadow: var(--shadow-sm);
text-decoration: none;
color: inherit;
transition:
transform var(--duration-normal) var(--ease-out-expo),
box-shadow var(--duration-normal) var(--ease-out-expo);
}
.wi-featured__card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
.wi-featured__card--hero {
grid-column: 1 / -1;
}
.wi-featured__card--hero .wi-featured__cover {
aspect-ratio: 21 / 9;
}
.wi-featured__card--hero .wi-featured__title {
font-size: var(--text-2xl);
}
.wi-featured__cover {
aspect-ratio: 16 / 10;
overflow: hidden;
}
.wi-featured__image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform var(--duration-normal) var(--ease-out-expo);
border-radius: 0;
}
.wi-featured__card:hover .wi-featured__image {
transform: scale(1.03);
}
.wi-featured__body {
padding: var(--space-lg);
display: flex;
flex-direction: column;
gap: var(--space-sm);
flex: 1;
}
.wi-featured__date {
font-size: var(--text-xs);
color: var(--ink-3);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
}
.wi-featured__title {
font-family: var(--font-sans);
font-size: var(--text-xl);
font-weight: 700;
color: var(--ink);
line-height: var(--leading-tight);
letter-spacing: var(--tracking-tight);
transition: color var(--duration-fast) var(--ease-out-quart);
}
.wi-featured__card:hover .wi-featured__title {
color: var(--accent);
}
.wi-featured__excerpt {
font-size: var(--text-sm);
color: var(--ink-2);
line-height: var(--leading-relaxed);
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
@media (max-width: 768px) {
.wi-featured {
grid-template-columns: 1fr;
}
.wi-featured__card--hero .wi-featured__cover {
aspect-ratio: 16 / 9;
}
.wi-featured__card--hero .wi-featured__title {
font-size: var(--text-xl);
}
}
</style>
+144 -12
View File
@@ -1,33 +1,165 @@
---
// TODO: Only execute when compiling Astro templates, so it will not be dynamically updated
const THEME_VERSION = "1.0.0";
const today = new Date();
---
<footer>
<footer th:with="showPowered=${theme.config?.footer?.footer_show_powered != false}, showTheme=${theme.config?.footer?.footer_show_theme != false}, hideRight=${showPowered == false and showTheme == false}">
<div class="footer-inner">
<span>
&copy; {today.getFullYear()}
<th:block th:text="${site.title}"></th:block>.
</span>
<div class="footer-bottom" th:classappend="${hideRight} ? 'footer-bottom--center' : ''">
<div class="footer-bottom__left">
<span class="footer-copyright" th:if="${theme.config?.footer?.footer_copyright}" th:text="${theme.config?.footer?.footer_copyright}"></span>
<span class="footer-copyright" th:unless="${theme.config?.footer?.footer_copyright}">
© {today.getFullYear()}
<th:block th:text="${site.title}"></th:block>
</span>
<span class="footer-icp" th:if="${theme.config?.footer?.footer_icp}">
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer" th:text="${theme.config?.footer?.footer_icp}"></a>
</span>
</div>
<div class="footer-socials" th:if="${theme.config?.footer?.footer_socials}">
<a
th:each="social : ${theme.config?.footer?.footer_socials}"
th:href="${social.url}"
th:title="${social.platform}"
target="_blank"
rel="noopener noreferrer"
>
<i th:if="${social.icon}" th:class="${social.icon}"></i>
<span th:if="${#strings.isEmpty(social.icon)}" th:text="${social.platform}"></span>
</a>
</div>
<div class="footer-bottom__right" th:if="${showPowered or showTheme}">
<a th:if="${showPowered}" href="https://halo.run" target="_blank" rel="noopener noreferrer">Powered by Halo</a>
<span class="footer-separator" th:if="${showPowered and showTheme}">·</span>
<span th:if="${showTheme}">WarmIsland v{THEME_VERSION}</span>
</div>
</div>
<div th:if="${theme.config?.footer?.footer_custom_html}" th:utext="${theme.config?.footer?.footer_custom_html}" class="footer-custom"></div>
<halo:footer />
</div>
</footer>
<style>
footer {
border-top: 1px solid var(--rule);
background: var(--bg);
background: linear-gradient(
to bottom,
var(--bg),
color-mix(in srgb, var(--bg) 97%, var(--accent) 3%)
);
}
.footer-inner {
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 0 1.25rem;
min-height: 50px;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.footer-bottom {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 1rem;
width: 820px;
max-width: calc(100% - 2.5rem);
margin: 0 auto;
padding: 1.5rem 0 2.5rem;
font-size: 0.85rem;
color: var(--ink-3);
font-size: 0.88rem;
width: 100%;
}
.footer-bottom--center {
justify-content: center;
}
.footer-bottom--center .footer-bottom__left {
flex: none;
}
.footer-bottom__left {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.footer-copyright {
color: var(--ink-3);
}
.footer-icp a {
color: var(--ink-3);
text-decoration: none;
transition: color 0.15s ease;
}
.footer-icp a:hover {
color: var(--ink-2);
}
.footer-socials {
display: inline-flex;
align-items: center;
gap: 0.75rem;
}
.footer-socials a {
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--ink-3);
text-decoration: none;
font-size: 1.05rem;
transition: color 0.15s ease;
}
.footer-socials a:hover {
color: var(--ink-2);
}
.footer-bottom__right {
display: flex;
align-items: center;
gap: 0.5rem;
}
.footer-bottom__right a {
color: var(--ink-3);
text-decoration: none;
transition: color 0.15s ease;
}
.footer-bottom__right a:hover {
color: var(--ink-2);
}
.footer-separator {
color: var(--ink-3);
opacity: 0.5;
}
.footer-custom {
margin-left: auto;
font-size: 0.85rem;
color: var(--ink-3);
}
@media (max-width: 768px) {
.footer-bottom {
flex-direction: column;
align-items: center;
text-align: center;
}
.footer-bottom__left {
justify-content: center;
}
.footer-bottom__right {
justify-content: center;
}
}
</style>
+129
View File
@@ -0,0 +1,129 @@
---
---
<section
class="wi-section wi-friends"
th:if="${theme.config?.home?.home_friends_enabled == true and pluginFinder.available('PluginLinks')}"
>
<div class="wi-container">
<div class="wi-section__header">
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_friends_title ?: '友链'}">友链</h2>
<span class="wi-section__accent"></span>
</div>
<div class="wi-friends__grid">
<a
th:each="link : ${linkFinder.list()}"
th:href="${link.spec.url}"
th:title="${link.spec.displayName}"
target="_blank"
rel="noopener noreferrer"
class="wi-friends__card"
>
<img
th:if="${link.spec.logo}"
th:src="${link.spec.logo}"
th:alt="${link.spec.displayName}"
class="wi-friends__avatar"
/>
<div
th:unless="${link.spec.logo}"
class="wi-friends__avatar wi-friends__avatar--placeholder"
>
<span th:text="${#strings.substring(link.spec.displayName, 0, 1)}"></span>
</div>
<div class="wi-friends__info">
<span class="wi-friends__name" th:text="${link.spec.displayName}"></span>
<span
class="wi-friends__desc"
th:if="${link.spec.description}"
th:text="${link.spec.description}"
></span>
</div>
</a>
</div>
</div>
</section>
<style>
.wi-friends__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: var(--space-lg);
}
.wi-friends__card {
display: flex;
align-items: center;
gap: var(--space-md);
padding: var(--space-lg);
background: var(--glass-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--glass-border);
border-radius: 12px;
text-decoration: none;
color: inherit;
transition:
transform var(--duration-normal) var(--ease-out-expo),
box-shadow var(--duration-normal) var(--ease-out-expo),
border-color var(--duration-normal) var(--ease-out-expo);
}
.wi-friends__card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: var(--accent);
color: inherit;
}
.wi-friends__avatar {
width: 48px;
height: 48px;
border-radius: 9999px;
object-fit: cover;
flex-shrink: 0;
}
.wi-friends__avatar--placeholder {
display: flex;
align-items: center;
justify-content: center;
background: var(--accent-bg);
color: var(--accent);
font-family: var(--font-sans);
font-size: var(--text-lg);
font-weight: 700;
}
.wi-friends__info {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.wi-friends__name {
font-size: var(--text-md);
font-weight: 600;
color: var(--ink);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wi-friends__desc {
font-size: var(--text-sm);
color: var(--ink-3);
line-height: var(--leading-snug);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 640px) {
.wi-friends__grid {
grid-template-columns: 1fr;
gap: var(--space-md);
}
}
</style>
-172
View File
@@ -1,172 +0,0 @@
---
import ThemeSwitcher from "./ThemeSwitcher.vue";
import UserButton from "./UserButton.vue";
---
<header>
<div class="header-inner">
<a class="brand" href="/" th:text="${site.title}"></a>
<nav
th:with="menu = ${menuFinder.getPrimary()}"
aria-label="Main navigation"
>
<a
th:each="menuItem : ${menu.menuItems}"
th:href="@{${menuItem.status.href}}"
th:target="${menuItem.spec.target?.value}"
th:text="${menuItem.status.displayName}"
>
</a>
</nav>
<div class="header-actions">
<ThemeSwitcher client:load />
<UserButton client:load>
<div slot="fallback" class="user-button-fallback" aria-hidden="true">
<span class="user-button-fallback__avatar"></span>
<span class="user-button-fallback__text"></span>
</div>
</UserButton>
</div>
</div>
</header>
<style>
header {
background: var(--bg);
border-bottom: 1px solid var(--rule);
position: sticky;
top: 0;
z-index: 100;
}
.header-inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 2rem;
width: 820px;
max-width: calc(100% - 2.5rem);
margin: 0 auto;
height: 58px;
}
.brand {
font-size: 1rem;
font-weight: 700;
color: var(--ink);
text-decoration: none;
letter-spacing: -0.02em;
flex-shrink: 0;
}
.brand:hover {
color: var(--accent);
}
nav {
display: flex;
align-items: center;
gap: 0.2rem;
flex: 1;
}
.header-actions {
display: flex;
align-items: center;
gap: 0.35rem;
margin-left: auto;
}
.user-button-fallback {
display: inline-flex;
align-items: center;
gap: 0.55rem;
box-sizing: border-box;
flex-shrink: 0;
width: 112px;
height: 34px;
padding: 0 0.7rem;
border-radius: 8px;
pointer-events: none;
}
.user-button-fallback__avatar,
.user-button-fallback__text {
display: block;
background: linear-gradient(
90deg,
var(--bg-raised) 0%,
color-mix(in srgb, var(--bg-raised) 78%, var(--ink) 22%) 50%,
var(--bg-raised) 100%
);
background-size: 200% 100%;
animation: user-button-fallback-shimmer 1.2s ease-in-out infinite;
}
.user-button-fallback__avatar {
width: 20px;
height: 20px;
border-radius: 999px;
flex-shrink: 0;
}
.user-button-fallback__text {
width: 100%;
height: 0.72rem;
border-radius: 999px;
}
nav :global(a) {
display: inline-block;
padding: 0.3rem 0.65rem;
border-radius: 6px;
font-size: 0.9rem;
color: var(--ink-2);
text-decoration: none;
transition:
color 0.15s ease,
background 0.15s ease;
}
nav :global(a:hover) {
color: var(--ink);
background: var(--bg-raised);
}
nav :global(a.active) {
color: var(--ink);
background: var(--bg-raised);
font-weight: 600;
}
@media (max-width: 680px) {
.header-inner {
flex-direction: column;
align-items: flex-start;
height: auto;
padding: 1rem 0;
gap: 0.75rem;
}
nav {
flex-wrap: wrap;
gap: 0.1rem;
margin-left: -0.65rem;
flex: initial;
}
.header-actions {
margin-left: 0;
}
}
@keyframes user-button-fallback-shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style>
+303
View File
@@ -0,0 +1,303 @@
---
---
<section
class="hero"
th:if="${theme.config?.hero?.hero_enabled != false}"
th:classappend="${theme.config?.animation?.animation_breath == false} ? 'hero--no-breath'"
>
<div class="hero__bg">
<div class="hero__orb hero__orb--1"></div>
<div class="hero__orb hero__orb--2"></div>
<div class="hero__orb hero__orb--3"></div>
<div
class="hero__bg-image"
th:if="${theme.config?.hero?.hero_background_image}"
th:style="'background-image: url(' + ${theme.config?.hero?.hero_background_image} + ')'"
></div>
</div>
<div class="hero__content">
<h1
class="hero__title"
th:text="${theme.config?.hero?.hero_title != null and !#strings.isEmpty(theme.config?.hero?.hero_title) ? theme.config?.hero?.hero_title : site.title}"
>
暖屿
</h1>
<p
class="hero__subtitle"
th:text="${theme.config?.hero?.hero_subtitle != null and !#strings.isEmpty(theme.config?.hero?.hero_subtitle) ? theme.config?.hero?.hero_subtitle : site.subtitle}"
>
深夜里温暖的小岛
</p>
<p
class="hero__description"
th:if="${theme.config?.hero?.hero_description_mode == 'hitokoto'}"
id="wi-hitokoto"
th:attr="data-api=${theme.config?.hero?.hero_hitokoto_api ?: 'https://v1.hitokoto.cn/'},data-categories=${theme.config?.hero?.hero_hitokoto_categories}"
></p>
<p
class="hero__description"
th:if="${theme.config?.hero?.hero_description_mode == 'custom'}"
th:text="${theme.config?.hero?.hero_custom_description}"
></p>
</div>
<div class="hero__scroll-indicator" aria-hidden="true">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
</section>
<script is:inline>
(function () {
var el = document.getElementById("wi-hitokoto");
if (!el) return;
var api = el.getAttribute("data-api") || "https://v1.hitokoto.cn/";
var categories = el.getAttribute("data-categories");
var url = api + "?encode=json";
if (categories) {
try {
var cats = JSON.parse(categories);
if (Array.isArray(cats)) {
cats.forEach(function (c) {
url += "&c=" + encodeURIComponent(c);
});
}
} catch (e) {}
}
el.style.opacity = "0.5";
el.textContent = "\u2026";
fetch(url)
.then(function (res) {
if (!res.ok) throw new Error("HTTP " + res.status);
return res.json();
})
.then(function (data) {
el.style.opacity = "1";
var text = data.hitokoto || "";
var from = data.from || data.from_who || "";
if (from) {
text += " \u2014\u2014 " + from;
}
el.textContent = text;
})
.catch(function () {
el.style.opacity = "1";
el.textContent = "";
});
})();
</script>
<style>
.hero {
position: relative;
height: calc(100vh + 80px);
height: calc(100dvh + 80px);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: linear-gradient(
160deg,
var(--bg) 0%,
color-mix(in srgb, var(--bg) 85%, var(--accent) 15%) 40%,
color-mix(in srgb, var(--bg) 90%, var(--mist-pink) 10%) 70%,
var(--bg) 100%
);
margin-top: -80px;
padding-top: 80px;
width: 100%;
}
.hero__bg {
position: absolute;
inset: 0;
pointer-events: none;
}
.hero__orb {
position: absolute;
border-radius: 50%;
filter: blur(80px);
animation: breathe 5s ease-in-out infinite;
}
.hero__orb--1 {
width: 420px;
height: 420px;
top: -10%;
right: -5%;
background: radial-gradient(
circle,
rgba(212, 118, 78, 0.4) 0%,
transparent 70%
);
animation-delay: 0s;
animation-duration: 5s;
}
.hero__orb--2 {
width: 350px;
height: 350px;
bottom: -8%;
left: -5%;
background: radial-gradient(
circle,
rgba(220, 140, 160, 0.3) 0%,
transparent 70%
);
animation-delay: -1.8s;
animation-duration: 6s;
}
.hero__orb--3 {
width: 300px;
height: 300px;
top: 40%;
left: 50%;
transform: translateX(-50%);
background: radial-gradient(
circle,
rgba(180, 150, 200, 0.2) 0%,
transparent 70%
);
animation-delay: -3.2s;
animation-duration: 4.5s;
}
.hero__bg-image {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
opacity: 0.15;
}
.hero__content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: var(--space-xl);
gap: var(--space-lg);
}
.hero__title {
font-family: var(--font-sans);
font-size: clamp(3rem, 8vw, 6rem);
font-weight: 700;
letter-spacing: -0.03em;
color: var(--ink);
line-height: var(--leading-tight);
}
.hero__subtitle {
font-family: var(--font-body);
font-size: var(--text-xl);
color: var(--ink-2);
letter-spacing: 0.05em;
line-height: var(--leading-normal);
}
.hero__description {
font-size: var(--text-lg);
color: var(--ink-2);
line-height: var(--leading-relaxed);
max-width: 560px;
text-align: center;
transition: opacity 0.3s ease;
}
.hero__scroll-indicator {
position: absolute;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
color: var(--ink-3);
animation: scrollHint 2s ease-in-out infinite;
}
.hero--no-breath .hero__orb {
animation: none;
}
html.dark .hero {
background: linear-gradient(
160deg,
var(--bg) 0%,
color-mix(in srgb, var(--bg) 85%, var(--accent) 12%) 40%,
var(--bg) 100%
);
}
html.dark .hero__orb--1 {
background: radial-gradient(
circle,
rgba(232, 149, 95, 0.25) 0%,
transparent 70%
);
}
html.dark .hero__orb--2 {
background: radial-gradient(
circle,
rgba(200, 120, 140, 0.18) 0%,
transparent 70%
);
}
html.dark .hero__orb--3 {
background: radial-gradient(
circle,
rgba(160, 130, 180, 0.12) 0%,
transparent 70%
);
}
@media (max-width: 768px) {
.hero__title {
font-size: clamp(2.25rem, 10vw, 3.5rem);
}
.hero__orb--1 {
width: 260px;
height: 260px;
}
.hero__orb--2 {
width: 220px;
height: 220px;
}
.hero__orb--3 {
width: 180px;
height: 180px;
}
.hero__content {
padding: var(--space-lg);
gap: var(--space-md);
}
.hero__description {
max-width: 90vw;
}
}
</style>
+307
View File
@@ -0,0 +1,307 @@
---
---
<section
class="wi-section"
th:if="${theme.config?.home?.home_latest_enabled != false}"
>
<div class="wi-container">
<div class="wi-section__header">
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_latest_title ?: '最新文章'}">最新文章</h2>
</div>
<div
class="wi-latest"
th:classappend="|wi-latest--${theme.config?.home?.home_latest_style ?: 'magazine'}|"
>
<a
th:each="post,iterStat : ${posts.items}"
th:href="@{${post.status.permalink}}"
class="wi-latest__card"
th:classappend="${iterStat.index lt 2} ? 'wi-latest__card--hero' : ''"
>
<div
class="wi-latest__cover"
th:if="${!#strings.isEmpty(post.spec.cover)}"
>
<img th:src="${post.spec.cover}" th:alt="${post.spec.title}" class="wi-latest__image" />
</div>
<div class="wi-latest__body">
<div class="wi-latest__meta">
<span
class="wi-latest__category"
th:if="${!#lists.isEmpty(post.categories)}"
th:text="${post.categories[0].spec.displayName}"
></span>
<time
class="wi-latest__date"
th:text="${#dates.format(post.spec.publishTime, 'yyyy-MM-dd')}"
></time>
</div>
<h3
class="wi-latest__title"
th:text="${post.spec.title}"
></h3>
<p
class="wi-latest__excerpt"
th:if="${post.status.excerpt}"
th:text="${post.status.excerpt}"
></p>
</div>
</a>
</div>
<nav
th:if="${posts.totalPages gt 1}"
class="wi-pagination"
aria-label="Pagination"
>
<a
th:if="${posts.hasPrevious()}"
th:href="@{${posts.prevUrl}}"
class="wi-pagination__prev"
th:text="${theme.config?.home?.home_label_newer ?: '较新'}"
>&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>
</section>
<style>
.wi-section__header {
margin-bottom: var(--space-xl);
}
.wi-section__title {
font-family: var(--font-sans);
font-size: var(--text-2xl);
font-weight: 700;
color: var(--ink);
letter-spacing: var(--tracking-tight);
display: flex;
align-items: center;
gap: 0.5rem;
}
.wi-section__title::before {
content: '';
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent);
flex-shrink: 0;
}
.wi-latest {
display: grid;
gap: var(--space-lg);
}
.wi-latest--magazine {
grid-template-columns: repeat(6, 1fr);
}
.wi-latest--magazine .wi-latest__card {
grid-column: span 2;
}
.wi-latest--magazine .wi-latest__card--hero {
grid-column: span 3;
}
.wi-latest--magazine .wi-latest__card--hero .wi-latest__cover {
aspect-ratio: 16 / 9;
}
.wi-latest--magazine .wi-latest__card--hero .wi-latest__title {
font-size: var(--text-xl);
}
.wi-latest--grid {
grid-template-columns: repeat(3, 1fr);
}
.wi-latest--list .wi-latest__card {
display: flex;
flex-direction: row;
align-items: stretch;
}
.wi-latest--list .wi-latest__cover {
width: 240px;
flex-shrink: 0;
aspect-ratio: 4 / 3;
}
.wi-latest--list .wi-latest__body {
justify-content: center;
}
.wi-latest__card {
display: flex;
flex-direction: column;
border-radius: 16px;
overflow: hidden;
background: var(--bg-raised);
box-shadow: var(--shadow-sm);
text-decoration: none;
color: inherit;
transition:
transform var(--duration-normal) var(--ease-out-expo),
box-shadow var(--duration-normal) var(--ease-out-expo);
}
.wi-latest__card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
.wi-latest__cover {
aspect-ratio: 4 / 3;
overflow: hidden;
}
.wi-latest__image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform var(--duration-normal) var(--ease-out-expo);
border-radius: 0;
}
.wi-latest__card:hover .wi-latest__image {
transform: scale(1.03);
}
.wi-latest__body {
padding: var(--space-lg);
display: flex;
flex-direction: column;
gap: var(--space-sm);
flex: 1;
}
.wi-latest__meta {
display: flex;
align-items: center;
gap: var(--space-sm);
flex-wrap: wrap;
}
.wi-latest__category {
font-size: var(--text-xs);
color: var(--accent);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
font-weight: 600;
}
.wi-latest__category::after {
content: '·';
margin-left: var(--space-sm);
color: var(--ink-3);
}
.wi-latest__date {
font-size: var(--text-xs);
color: var(--ink-3);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
}
.wi-latest__title {
font-family: var(--font-sans);
font-size: var(--text-lg);
font-weight: 700;
color: var(--ink);
line-height: var(--leading-tight);
letter-spacing: var(--tracking-tight);
transition: color var(--duration-fast) var(--ease-out-quart);
}
.wi-latest__card:hover .wi-latest__title {
color: var(--accent);
}
.wi-latest__excerpt {
font-size: var(--text-sm);
color: var(--ink-2);
line-height: var(--leading-relaxed);
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.wi-pagination {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding-top: var(--space-xl);
margin-top: var(--space-xl);
border-top: 1px solid var(--rule);
font-size: var(--text-sm);
}
.wi-pagination__prev,
.wi-pagination__next {
color: var(--ink-2);
text-decoration: none;
transition: color var(--duration-fast) var(--ease-out-quart);
}
.wi-pagination__prev:hover,
.wi-pagination__next:hover {
color: var(--accent);
}
.wi-pagination__info {
color: var(--ink-3);
}
@media (max-width: 1023px) {
.wi-latest--magazine {
grid-template-columns: repeat(2, 1fr);
}
.wi-latest--magazine .wi-latest__card {
grid-column: span 1;
}
.wi-latest--magazine .wi-latest__card--hero {
grid-column: span 2;
}
.wi-latest--grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.wi-latest--magazine,
.wi-latest--grid {
grid-template-columns: 1fr;
}
.wi-latest--magazine .wi-latest__card--hero {
grid-column: span 1;
}
.wi-latest--list .wi-latest__card {
flex-direction: column;
}
.wi-latest--list .wi-latest__cover {
width: 100%;
}
}
</style>
+255
View File
@@ -0,0 +1,255 @@
---
---
<script>
import 'lightgallery/css/lightgallery-bundle.css';
import lightGallery from 'lightgallery';
import lgZoom from 'lightgallery/plugins/zoom';
import lgThumbnail from 'lightgallery/plugins/thumbnail';
import lgFullscreen from 'lightgallery/plugins/fullscreen';
import lgRotate from 'lightgallery/plugins/rotate';
import lgAutoplay from 'lightgallery/plugins/autoplay';
var _initialized = new WeakSet();
function isInitialized(el) {
if (_initialized.has(el)) return true;
return false;
}
function markInitialized(el) {
_initialized.add(el);
}
function buildExifHtml(exifStr, caption) {
if (!exifStr) return caption || '';
var parts = exifStr.split('|');
if (parts.length < 8) return caption || '';
var make = parts[0], model = parts[1], lensModel = parts[2];
var fNumber = parts[3], exposureTime = parts[4], iso = parts[5];
var focalLength = parts[6], focalLengthIn35mm = parts[7];
var hasAnyExif = make || model || lensModel || fNumber || exposureTime || iso || focalLength;
if (!hasAnyExif) return caption || '';
var html = '<div class="wi-lg-caption">' + (caption || '') + '</div>';
html += '<div class="wi-lg-exif">';
if (make || model) {
html += '<span class="wi-lg-exif__item wi-lg-exif__camera">' + (make ? make + ' ' : '') + model + '</span>';
}
if (lensModel) {
html += '<span class="wi-lg-exif__item wi-lg-exif__lens">' + lensModel + '</span>';
}
html += '<div class="wi-lg-exif__params">';
if (fNumber) html += '<span class="wi-lg-exif__item wi-lg-exif__aperture">f/' + fNumber + '</span>';
if (exposureTime) html += '<span class="wi-lg-exif__item wi-lg-exif__shutter">' + exposureTime + 's</span>';
if (iso) html += '<span class="wi-lg-exif__item wi-lg-exif__iso">ISO ' + iso + '</span>';
if (focalLength) {
var focalText = focalLength + 'mm';
if (focalLengthIn35mm && focalLengthIn35mm !== focalLength) {
focalText += ' (eq. ' + focalLengthIn35mm + 'mm)';
}
html += '<span class="wi-lg-exif__item wi-lg-exif__focal">' + focalText + '</span>';
}
html += '</div></div>';
return html;
}
function preprocessExif(el) {
var items = el.querySelectorAll('.wi-lightgallery-item[data-exif]');
items.forEach(function(item) {
var exifStr = item.getAttribute('data-exif');
if (!exifStr) return;
var caption = item.getAttribute('data-sub-html') || '';
var html = buildExifHtml(exifStr, caption);
if (html && html !== caption) {
item.setAttribute('data-sub-html', html);
}
});
}
function createLgConfig() {
return {
selector: '.wi-lightgallery-item',
plugins: [lgZoom, lgThumbnail, lgFullscreen, lgRotate, lgAutoplay],
speed: 280,
mode: 'lg-fade',
licenseKey: '0000-0000-0000-0000',
download: false,
counter: true,
preload: 2,
thumbWidth: 80,
thumbHeight: '60px',
thumbMargin: 4,
zoomFromOrigin: true,
actualSize: false,
showZoomInOutIcons: true,
toggleThumb: true,
allowMediaOverlap: true,
autoplay: false,
slideDelay: 5000,
progressBar: true,
rotate: true,
flipHorizontal: false,
flipVertical: false,
mobileSettings: {
controls: true,
showCloseIcon: true,
download: false,
},
};
}
function initLg(el) {
if (isInitialized(el)) return;
var items = el.querySelectorAll('.wi-lightgallery-item');
if (items.length === 0) return;
preprocessExif(el);
lightGallery(el, createLgConfig());
markInitialized(el);
}
function initLightGallery() {
initContentPages();
initPhotosPage();
initMomentsPage();
document.addEventListener('click', function(e) {
var toggleBtn = e.target.closest('.lg-toggle-thumb');
if (toggleBtn) {
e.stopPropagation();
var lgOuter = toggleBtn.closest('.lg-outer');
if (lgOuter) {
lgOuter.classList.toggle('wi-thumb-shown');
}
}
}, true);
}
function initContentPages() {
var selectors = ['.wi-post__body', '.wi-page__body'];
selectors.forEach(function(sel) {
var els = document.querySelectorAll(sel);
els.forEach(function(el) {
processContentImages(el);
initLg(el);
});
});
}
function initPhotosPage() {
var allGroups = document.querySelector('.wi-photos-page__all-groups');
if (allGroups) {
var links = allGroups.querySelectorAll('.wi-photos-page__link');
links.forEach(function(link) {
link.classList.add('wi-lightgallery-item');
});
initLg(allGroups);
return;
}
var singleGrid = document.querySelector('.wi-photos-page__grid:not(.wi-photos-page__all-groups .wi-photos-page__grid)');
if (singleGrid) {
var links2 = singleGrid.querySelectorAll('.wi-photos-page__link');
links2.forEach(function(link) {
link.classList.add('wi-lightgallery-item');
});
initLg(singleGrid);
return;
}
var grids = document.querySelectorAll('.wi-photos-page__grid');
grids.forEach(function(el) {
processPhotosGrid(el);
initLg(el);
});
}
function initMomentsPage() {
var selectors = ['.wi-moments-page__media', '.wi-moments-page__card-media', '.wi-moments-page__masonry-media'];
selectors.forEach(function(sel) {
var els = document.querySelectorAll(sel);
els.forEach(function(el) {
processMomentsMedia(el);
initLg(el);
});
});
}
function processContentImages(container) {
var images = container.querySelectorAll('img');
images.forEach(function(img) {
var src = img.getAttribute('src');
if (!src) return;
var w = img.getAttribute('width');
var h = img.getAttribute('height');
if ((w && parseInt(w) < 50) || (h && parseInt(h) < 50)) return;
var className = (img.getAttribute('class') || '').toLowerCase();
if (/emoji|icon|avatar|logo|badge|smiley/.test(className)) return;
if (img.getAttribute('role') === 'presentation') return;
if (img.getAttribute('aria-hidden') === 'true') return;
var parentA = img.closest('a');
if (parentA) {
if (parentA.getAttribute('target') === '_blank') return;
parentA.classList.add('wi-lightgallery-item');
if (!parentA.getAttribute('data-src')) {
var href = parentA.getAttribute('href') || src;
parentA.setAttribute('data-src', href);
}
var alt = img.getAttribute('alt') || '';
if (alt && !parentA.getAttribute('data-sub-html')) {
parentA.setAttribute('data-sub-html', alt);
}
} else {
var a = document.createElement('a');
a.setAttribute('href', src);
a.setAttribute('data-src', src);
a.classList.add('wi-lightgallery-item');
var alt2 = img.getAttribute('alt') || '';
if (alt2) {
a.setAttribute('data-sub-html', alt2);
}
img.parentNode.insertBefore(a, img);
a.appendChild(img);
}
});
}
function processPhotosGrid(container) {
var links = container.querySelectorAll('.wi-photos-page__link');
links.forEach(function(link) {
link.classList.add('wi-lightgallery-item');
});
}
function processMomentsMedia(container) {
var images = container.querySelectorAll('img');
images.forEach(function(img) {
var src = img.getAttribute('src');
if (!src) return;
var parentA = img.closest('a');
if (parentA) {
parentA.classList.add('wi-lightgallery-item');
if (!parentA.getAttribute('data-src')) {
parentA.setAttribute('data-src', src);
}
} else {
var a = document.createElement('a');
a.setAttribute('href', src);
a.setAttribute('data-src', src);
a.classList.add('wi-lightgallery-item');
img.parentNode.insertBefore(a, img);
a.appendChild(img);
}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initLightGallery);
} else {
initLightGallery();
}
</script>
+153
View File
@@ -0,0 +1,153 @@
---
---
<section
class="wi-section wi-message-wall"
th:if="${theme.config?.home?.home_message_wall_enabled == true}"
>
<div class="wi-container">
<div class="wi-section__header">
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_message_wall_title ?: '留言墙'}">留言墙</h2>
<span class="wi-section__accent"></span>
</div>
<div class="wi-message-wall__grid">
<div
class="wi-message-wall__card"
th:each="comment : ${commentFinder.list()}"
>
<div class="wi-message-wall__head">
<img
th:if="${comment.spec.owner.avatar}"
th:src="${comment.spec.owner.avatar}"
th:alt="${comment.spec.owner.displayName}"
class="wi-message-wall__avatar"
/>
<div
th:unless="${comment.spec.owner.avatar}"
class="wi-message-wall__avatar wi-message-wall__avatar--placeholder"
>
<span th:text="${#strings.substring(comment.spec.owner.displayName, 0, 1)}"></span>
</div>
<div class="wi-message-wall__meta">
<span class="wi-message-wall__name" th:text="${comment.spec.owner.displayName}"></span>
<time
class="wi-message-wall__date"
th:text="${#dates.format(comment.metadata.creationTimestamp, 'yyyy-MM-dd')}"
></time>
</div>
</div>
<p class="wi-message-wall__text" th:text="${comment.spec.content}"></p>
</div>
</div>
</div>
</section>
<style>
.wi-message-wall__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: var(--space-md);
}
.wi-message-wall__card {
padding: var(--space-lg);
border-radius: 12px;
border: 1px solid var(--rule);
transition:
transform var(--duration-normal) var(--ease-out-expo),
box-shadow var(--duration-normal) var(--ease-out-expo);
}
.wi-message-wall__card:nth-child(6n + 1) {
background: var(--accent-bg);
}
.wi-message-wall__card:nth-child(6n + 2) {
background: var(--mist-pink);
}
.wi-message-wall__card:nth-child(6n + 3) {
background: var(--bg-raised);
}
.wi-message-wall__card:nth-child(6n + 4) {
background: color-mix(in srgb, var(--bg) 85%, var(--accent) 15%);
}
.wi-message-wall__card:nth-child(6n + 5) {
background: color-mix(in srgb, var(--bg) 90%, var(--caramel) 10%);
}
.wi-message-wall__card:nth-child(6n + 6) {
background: var(--sea-salt);
}
.wi-message-wall__card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.wi-message-wall__head {
display: flex;
align-items: center;
gap: var(--space-sm);
margin-bottom: var(--space-sm);
}
.wi-message-wall__avatar {
width: 32px;
height: 32px;
border-radius: 9999px;
object-fit: cover;
flex-shrink: 0;
}
.wi-message-wall__avatar--placeholder {
display: flex;
align-items: center;
justify-content: center;
background: var(--accent-bg);
color: var(--accent);
font-size: var(--text-sm);
font-weight: 700;
}
.wi-message-wall__meta {
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
}
.wi-message-wall__name {
font-size: var(--text-sm);
font-weight: 600;
color: var(--ink);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wi-message-wall__date {
font-size: var(--text-xs);
color: var(--ink-3);
letter-spacing: var(--tracking-wide);
}
.wi-message-wall__text {
font-size: var(--text-sm);
color: var(--ink-2);
line-height: var(--leading-relaxed);
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
@media (max-width: 640px) {
.wi-message-wall__grid {
grid-template-columns: 1fr;
}
}
</style>
+274
View File
@@ -0,0 +1,274 @@
---
---
<div class="wi-mobile-menu" id="wi-mobile-menu" aria-hidden="true">
<div class="wi-mobile-menu__backdrop"></div>
<div class="wi-mobile-menu__panel">
<div class="wi-mobile-menu__header">
<button class="wi-mobile-menu__close" type="button" aria-label="Close menu">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</div>
<nav class="wi-mobile-menu__nav" aria-label="Mobile navigation">
<th:block th:if="${menuFinder != null}">
<th:block th:with="menu = ${menuFinder.getPrimary()}">
<th:block th:if="${menu != null and menu.menuItems != null and not #lists.isEmpty(menu.menuItems)}">
<a
th:each="menuItem : ${menu.menuItems}"
th:href="@{${menuItem.status.href}}"
th:target="${menuItem.spec.target}"
th:text="${menuItem.status.displayName}"
class="wi-mobile-menu__link"
th:data-href="${menuItem.status.href}"
></a>
</th:block>
</th:block>
</th:block>
</nav>
<div class="wi-mobile-menu__footer">
<button
th:if="${pluginFinder.available('PluginSearchWidget')}"
class="wi-mobile-menu__action-btn"
type="button"
aria-label="Search"
onclick="SearchWidget.open()"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
<span th:text="${theme.config?.basic?.label_search ?: '搜索'}">搜索</span>
</button>
<button class="wi-mobile-menu__action-btn wi-mobile-menu__theme-btn" type="button" aria-label="Toggle theme">
<svg class="wi-mobile-menu__icon-moon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
<svg class="wi-mobile-menu__icon-sun" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
<span class="wi-mobile-menu__theme-label" th:text="${theme.config?.basic?.label_theme_switch ?: '切换主题'}">切换主题</span>
</button>
</div>
</div>
</div>
<script is:inline>
const menu = document.getElementById("wi-mobile-menu");
const backdrop = menu?.querySelector(".wi-mobile-menu__backdrop");
const closeBtn = menu?.querySelector(".wi-mobile-menu__close");
const themeBtn = menu?.querySelector(".wi-mobile-menu__theme-btn");
let isOpen = false;
function openMenu() {
isOpen = true;
menu?.classList.add("wi-mobile-menu--open");
menu?.setAttribute("aria-hidden", "false");
document.body.style.overflow = "hidden";
}
function closeMenu() {
isOpen = false;
menu?.classList.remove("wi-mobile-menu--open");
menu?.setAttribute("aria-hidden", "true");
document.body.style.overflow = "";
}
window.addEventListener("wi:toggle-mobile-menu", () => {
if (isOpen) {
closeMenu();
} else {
openMenu();
}
});
closeBtn?.addEventListener("click", closeMenu);
backdrop?.addEventListener("click", closeMenu);
themeBtn?.addEventListener("click", () => {
document.documentElement.classList.add("wi-theme-transition");
const isDark = document.documentElement.classList.toggle("dark");
localStorage.setItem("wi-theme", isDark ? "dark" : "light");
document.documentElement.setAttribute("data-color-scheme", isDark ? "dark" : "light");
setTimeout(() => {
document.documentElement.classList.remove("wi-theme-transition");
}, 300);
});
menu?.querySelectorAll(".wi-mobile-menu__link").forEach((link) => {
link.addEventListener("click", closeMenu);
});
menu?.querySelectorAll('.wi-mobile-menu__link[data-href]').forEach(function(link) {
try {
if (new URL(link.href).pathname === window.location.pathname) {
link.classList.add('wi-mobile-menu__link--active');
}
} catch(e) {}
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && isOpen) {
closeMenu();
}
});
</script>
<style>
.wi-mobile-menu {
position: fixed;
inset: 0;
z-index: 200;
pointer-events: none;
visibility: hidden;
}
.wi-mobile-menu--open {
pointer-events: auto;
visibility: visible;
}
.wi-mobile-menu__backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.4);
opacity: 0;
transition: opacity var(--duration-normal) var(--ease-out-expo);
}
.wi-mobile-menu--open .wi-mobile-menu__backdrop {
opacity: 1;
}
.wi-mobile-menu__panel {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: min(85vw, 360px);
display: flex;
flex-direction: column;
background: var(--glass-bg);
backdrop-filter: blur(24px) saturate(180%);
-webkit-backdrop-filter: blur(24px) saturate(180%);
border-left: 1px solid var(--glass-border);
box-shadow: var(--shadow-lg);
transform: translateX(100%);
transition: transform var(--duration-normal) var(--ease-out-expo);
}
.wi-mobile-menu--open .wi-mobile-menu__panel {
transform: translateX(0);
}
.wi-mobile-menu__header {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--glass-border);
}
.wi-mobile-menu__close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
padding: 0;
border: none;
border-radius: 9999px;
background: transparent;
color: var(--ink-2);
cursor: pointer;
transition:
background var(--duration-fast) var(--ease-out-quart),
color var(--duration-fast) var(--ease-out-quart);
}
.wi-mobile-menu__close:hover {
background: var(--bg-raised);
color: var(--ink);
}
.wi-mobile-menu__close:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.wi-mobile-menu__nav {
flex: 1;
overflow-y: auto;
padding: 1rem 1.25rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.wi-mobile-menu__link {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
border-radius: 12px;
font-size: var(--text-lg);
font-weight: 500;
color: var(--ink-2);
text-decoration: none;
transition:
color var(--duration-fast) var(--ease-out-quart),
background var(--duration-fast) var(--ease-out-quart);
}
.wi-mobile-menu__link:hover {
color: var(--accent);
background: var(--bg-raised);
}
.wi-mobile-menu__link--active {
color: var(--accent);
background: var(--bg-raised);
font-weight: 600;
}
.wi-mobile-menu__footer {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem 1.25rem;
border-top: 1px solid var(--glass-border);
}
.wi-mobile-menu__action-btn {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
padding: 0.75rem 1rem;
border: none;
border-radius: 12px;
background: transparent;
color: var(--ink-2);
font-size: var(--text-md);
font-family: var(--font-sans);
cursor: pointer;
transition:
color var(--duration-fast) var(--ease-out-quart),
background var(--duration-fast) var(--ease-out-quart);
}
.wi-mobile-menu__action-btn:hover {
color: var(--ink);
background: var(--bg-raised);
}
.wi-mobile-menu__action-btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.wi-mobile-menu__icon-sun {
display: none;
}
:global(.dark) .wi-mobile-menu__icon-moon {
display: none;
}
:global(.dark) .wi-mobile-menu__icon-sun {
display: block;
}
</style>
+156
View File
@@ -0,0 +1,156 @@
---
import { findPageBySlug } from "../lib/pages";
const pageConfig = findPageBySlug("moments");
---
<Fragment>
<th:block th:if="${pluginFinder.available('PluginMoments')}">
<section class="wi-moments-section">
<div class="wi-container">
<h2 class="wi-moments-section__title" th:text="${pageConfig?.title ?: theme.config?.moments?.moments_title ?: '瞬间'}">瞬间</h2>
<div
class="wi-moments-section__list"
th:attr="data-style=${theme.config?.moments?.moments_style ?: 'timeline'}"
>
<th:block th:with="limit = ${theme.config?.moments?.moments_count ?: 5}, momentsResult = ${momentFinder.list(1, limit)}">
<div
class="wi-moments-section__item"
th:each="moment : ${momentsResult.items}"
>
<div class="wi-moments-section__dot" th:if="${theme.config?.moments?.moments_style != 'cards'}"></div>
<div class="wi-moments-section__content" th:utext="${moment.spec?.content?.html ?: moment.spec?.content?.raw ?: moment.spec?.content}"></div>
<time
class="wi-moments-section__date"
th:text="${#dates.format(moment.spec?.releaseTime ?: moment.metadata?.creationTimestamp, 'yyyy-MM-dd HH:mm')}"
></time>
</div>
</th:block>
</div>
<a th:href="@{'/moments'}" class="wi-moments-section__more">
<span th:text="${theme.config?.home?.home_label_view_all ?: '查看全部'}">查看全部</span>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</a>
</div>
</section>
</th:block>
</Fragment>
<style>
.wi-moments-section {
padding-block: var(--section-spacing);
}
.wi-moments-section__title {
font-size: var(--text-2xl);
text-align: center;
margin-bottom: var(--space-2xl);
position: relative;
}
.wi-moments-section__title::after {
content: "";
display: block;
width: 48px;
height: 3px;
margin-inline: auto;
border-radius: 9999px;
background: var(--accent);
margin-top: var(--space-sm);
}
.wi-moments-section__list {
display: flex;
flex-direction: column;
gap: var(--space-xl);
}
.wi-moments-section__list[data-style="timeline"] {
padding-left: var(--space-2xl);
}
.wi-moments-section__item {
position: relative;
background: var(--bg-raised);
border: 1px solid var(--rule);
border-radius: 16px;
padding: var(--space-xl);
transition:
box-shadow var(--duration-normal) var(--ease-out-expo),
transform var(--duration-normal) var(--ease-out-expo);
}
.wi-moments-section__item:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.wi-moments-section__list[data-style="timeline"] .wi-moments-section__item {
margin-left: var(--space-lg);
}
.wi-moments-section__list[data-style="timeline"] .wi-moments-section__dot {
position: absolute;
left: calc(-1 * var(--space-2xl) - 5px);
top: 28px;
width: 10px;
height: 10px;
border-radius: 9999px;
background: var(--accent);
box-shadow: 0 0 0 3px var(--bg), 0 0 0 4px var(--accent);
}
.wi-moments-section__content {
font-size: var(--text-base);
line-height: var(--leading-relaxed);
color: var(--ink);
margin-bottom: var(--space-sm);
}
.wi-moments-section__date {
display: block;
font-size: var(--text-xs);
color: var(--ink-3);
letter-spacing: var(--tracking-wide);
}
.wi-moments-section__more {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
margin-top: var(--space-xl);
margin-inline: auto;
font-size: var(--text-sm);
color: var(--ink-2);
transition: color var(--duration-normal) var(--ease-out-expo);
}
.wi-moments-section__more:hover {
color: var(--accent);
}
.wi-moments-section__more svg {
transition: transform var(--duration-normal) var(--ease-out-expo);
}
.wi-moments-section__more:hover svg {
transform: translateX(4px);
}
@media (max-width: 768px) {
.wi-moments-section__list[data-style="timeline"] {
padding-left: var(--space-xl);
}
.wi-moments-section__list[data-style="timeline"] .wi-moments-section__item {
margin-left: var(--space-md);
}
.wi-moments-section__list[data-style="timeline"] .wi-moments-section__dot {
left: calc(-1 * var(--space-xl) - 5px);
}
}
</style>
+324
View File
@@ -0,0 +1,324 @@
---
import ThemeSwitcher from "./ThemeSwitcher.vue";
---
<header class="wi-navbar-wrapper" id="wi-navbar" th:classappend="${theme.config?.navbar?.navbar_style == 'minimal'} ? 'wi-navbar--minimal' : (${theme.config?.navbar?.navbar_style == 'float'} ? 'wi-navbar--float' : 'wi-navbar--glass')">
<div class="wi-navbar__capsule">
<a class="wi-navbar__brand" th:href="@{/}">
<img
th:if="${theme.config?.basic?.logo}"
th:src="${theme.config?.basic?.logo}"
alt=""
class="wi-navbar__logo"
/>
<span
th:if="${#strings.isEmpty(theme.config?.basic?.logo)}"
class="wi-navbar__title"
th:text="${site.title}"
></span>
</a>
<nav
class="wi-navbar__nav"
aria-label="Main navigation"
>
<th:block th:if="${menuFinder != null}">
<th:block th:with="menu = ${menuFinder.getPrimary()}">
<th:block th:if="${menu != null and menu.menuItems != null and not #lists.isEmpty(menu.menuItems)}">
<a
th:each="menuItem : ${menu.menuItems}"
th:href="@{${menuItem.status.href}}"
th:target="${menuItem.spec.target}"
th:text="${menuItem.status.displayName}"
class="wi-navbar__link"
th:data-href="${menuItem.status.href}"
>
</a>
</th:block>
</th:block>
</th:block>
</nav>
<div class="wi-navbar__actions">
<button
th:if="${pluginFinder.available('PluginSearchWidget') and theme.config?.navbar?.navbar_show_search != false}"
class="wi-navbar__action-btn"
type="button"
aria-label="Search"
onclick="SearchWidget.open()"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
</button>
<th:block th:if="${theme.config?.navbar?.navbar_show_theme_switch != false}">
<ThemeSwitcher client:load />
</th:block>
<button
class="wi-navbar__action-btn wi-navbar__menu-btn"
type="button"
aria-label="Toggle menu"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
</button>
</div>
</div>
</header>
<script is:inline>
const navbar = document.getElementById("wi-navbar");
let lastScrollY = window.scrollY;
let ticking = false;
function onScroll() {
if (!ticking) {
requestAnimationFrame(() => {
const currentScrollY = window.scrollY;
if (currentScrollY > 20) {
navbar?.classList.add("wi-navbar--scrolled");
} else {
navbar?.classList.remove("wi-navbar--scrolled");
}
if (currentScrollY > lastScrollY && currentScrollY > 80) {
navbar?.classList.add("wi-navbar--hidden");
} else if (currentScrollY < lastScrollY) {
navbar?.classList.remove("wi-navbar--hidden");
}
lastScrollY = currentScrollY;
ticking = false;
});
ticking = true;
}
}
window.addEventListener("scroll", onScroll, { passive: true });
document.querySelectorAll('.wi-navbar__link[data-href]').forEach(function(link) {
try {
if (new URL(link.href).pathname === window.location.pathname) {
link.classList.add('wi-navbar__link--active');
}
} catch(e) {}
});
document.querySelector(".wi-navbar__menu-btn")?.addEventListener("click", () => {
window.dispatchEvent(new CustomEvent("wi:toggle-mobile-menu"));
});
</script>
<style>
.wi-navbar-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
display: flex;
justify-content: center;
padding: 0.75rem 1rem;
pointer-events: none;
transition:
transform 0.35s cubic-bezier(0.16, 1, 0.3, 1),
padding var(--duration-normal) var(--ease-out-expo);
}
.wi-navbar--hidden {
transform: translateY(-100%);
}
.wi-navbar__capsule {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
height: 56px;
padding: 0 1.5rem;
margin: 0 auto;
pointer-events: auto;
transition:
background var(--duration-normal) var(--ease-out-expo),
box-shadow var(--duration-normal) var(--ease-out-expo),
transform var(--duration-normal) var(--ease-out-expo);
}
.wi-navbar--glass .wi-navbar__capsule {
border-radius: 9999px;
background: var(--glass-bg);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid var(--glass-border);
box-shadow: var(--shadow-md);
max-width: 900px;
}
.wi-navbar--glass.wi-navbar--scrolled .wi-navbar__capsule {
background: var(--bg-overlay);
box-shadow: var(--shadow-lg);
transform: scale(0.98);
}
.wi-navbar--minimal {
padding: 0;
}
.wi-navbar--minimal .wi-navbar__capsule {
border-radius: 0;
background: transparent;
backdrop-filter: none;
-webkit-backdrop-filter: none;
border: none;
border-bottom: 1px solid transparent;
box-shadow: none;
max-width: 100%;
padding: 0 2rem;
transition: background 0.3s ease, border-color 0.3s ease;
}
.wi-navbar--minimal.wi-navbar--scrolled .wi-navbar__capsule {
background: var(--bg);
border-bottom-color: var(--rule);
}
.wi-navbar--float .wi-navbar__capsule {
border-radius: 16px;
background: var(--glass-bg);
backdrop-filter: blur(24px) saturate(180%);
-webkit-backdrop-filter: blur(24px) saturate(180%);
border: 1px solid var(--glass-border);
box-shadow: var(--shadow-lg);
max-width: 800px;
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), background 0.3s ease, box-shadow 0.3s ease;
}
.wi-navbar--float.wi-navbar--scrolled .wi-navbar__capsule {
transform: scale(0.96);
box-shadow: var(--shadow-md);
}
.wi-navbar__brand {
display: flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
flex-shrink: 0;
}
.wi-navbar__logo {
height: 32px;
width: auto;
border-radius: 8px;
object-fit: contain;
}
.wi-navbar__title {
font-family: var(--font-sans);
font-weight: 700;
font-size: var(--text-md);
color: var(--ink);
letter-spacing: var(--tracking-tight);
white-space: nowrap;
transition: color var(--duration-fast) var(--ease-out-quart);
}
.wi-navbar__brand:hover .wi-navbar__title {
color: var(--accent);
}
.wi-navbar__nav {
display: flex;
align-items: center;
gap: 0.25rem;
flex: 1;
justify-content: center;
min-width: 0;
}
.wi-navbar__link {
display: inline-flex;
align-items: center;
padding: 0.3rem 0.7rem;
border-radius: 9999px;
font-size: var(--text-sm);
color: var(--ink-2);
text-decoration: none;
white-space: nowrap;
transition:
color var(--duration-fast) var(--ease-out-quart),
background var(--duration-fast) var(--ease-out-quart);
}
.wi-navbar__link:hover {
color: var(--accent);
background: var(--bg-raised);
}
.wi-navbar__link--active {
color: var(--ink);
background: var(--bg-raised);
font-weight: 600;
}
.wi-navbar__actions {
display: flex;
align-items: center;
gap: 0.25rem;
flex-shrink: 0;
margin-left: auto;
}
.wi-navbar__action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
padding: 0;
border: none;
border-radius: 9999px;
background: transparent;
color: var(--ink-2);
cursor: pointer;
transition:
background var(--duration-fast) var(--ease-out-quart),
color var(--duration-fast) var(--ease-out-quart);
}
.wi-navbar__action-btn:hover {
background: var(--bg-raised);
color: var(--ink);
}
.wi-navbar__action-btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.wi-navbar__menu-btn {
display: none;
}
@media (max-width: 768px) {
.wi-navbar-wrapper {
padding: 0.5rem 0.75rem;
}
.wi-navbar__capsule {
padding: 0 1rem;
}
.wi-navbar__nav {
display: none;
}
.wi-navbar__action-btn[aria-label="Search"] {
display: none;
}
.wi-navbar__menu-btn {
display: inline-flex;
}
}
</style>
+170
View File
@@ -0,0 +1,170 @@
---
import { findPageBySlug } from "../lib/pages";
const pageConfig = findPageBySlug("photos");
---
<Fragment>
<th:block th:if="${pluginFinder.available('PluginPhotos')}">
<section class="wi-photos-section">
<div class="wi-container">
<h2 class="wi-photos-section__title" th:text="${pageConfig?.title ?: theme.config?.photos?.photos_title ?: '图库'}">图库</h2>
<div
class="wi-photos-section__grid"
th:attr="data-style=${theme.config?.photos?.photos_style ?: 'masonry'}, data-columns=${theme.config?.photos?.photos_columns ?: 3}"
>
<th:block th:with="limit = ${theme.config?.photos?.photos_count ?: 6}">
<th:block th:each="group : ${photoFinder.groupBy()}">
<div
class="wi-photos-section__item"
th:each="photo, stat : ${group.photos}"
th:if="${stat.index < limit}"
>
<a th:href="@{'/photos'}" class="wi-photos-section__link">
<div class="wi-photos-section__wrap">
<img
th:src="${photo.spec.url}"
th:alt="${photo.spec.displayName ?: ''}"
class="wi-photos-section__image"
loading="lazy"
/>
</div>
</a>
</div>
</th:block>
</th:block>
</div>
<a th:href="@{'/photos'}" class="wi-photos-section__more">
<span th:text="${theme.config?.home?.home_label_view_all ?: '查看全部'}">查看全部</span>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</a>
</div>
</section>
</th:block>
</Fragment>
<style>
.wi-photos-section {
padding-block: var(--section-spacing);
}
.wi-photos-section__title {
font-size: var(--text-2xl);
text-align: center;
margin-bottom: var(--space-2xl);
position: relative;
}
.wi-photos-section__title::after {
content: "";
display: block;
width: 48px;
height: 3px;
margin-inline: auto;
border-radius: 9999px;
background: var(--accent);
margin-top: var(--space-sm);
}
.wi-photos-section__grid[data-style="masonry"] {
columns: 3;
column-gap: var(--space-lg);
}
.wi-photos-section__grid[data-style="masonry"][data-columns="2"] {
columns: 2;
}
.wi-photos-section__grid[data-style="grid"] {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-lg);
}
.wi-photos-section__grid[data-style="grid"][data-columns="2"] {
grid-template-columns: repeat(2, 1fr);
}
.wi-photos-section__grid[data-style="masonry"] .wi-photos-section__item {
break-inside: avoid;
margin-bottom: var(--space-lg);
}
.wi-photos-section__wrap {
position: relative;
border-radius: 16px;
overflow: hidden;
box-shadow: var(--shadow-sm);
transition:
box-shadow var(--duration-normal) var(--ease-out-expo),
transform var(--duration-normal) var(--ease-out-expo);
}
.wi-photos-section__wrap:hover {
box-shadow: var(--shadow-lg);
transform: scale(1.02);
}
.wi-photos-section__link {
display: block;
}
.wi-photos-section__image {
display: block;
width: 100%;
height: auto;
border-radius: 0;
transition: transform var(--duration-slow) var(--ease-out-expo);
}
.wi-photos-section__grid[data-style="grid"] .wi-photos-section__image {
aspect-ratio: 1;
object-fit: cover;
}
.wi-photos-section__wrap:hover .wi-photos-section__image {
transform: scale(1.06);
}
.wi-photos-section__more {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
margin-top: var(--space-xl);
margin-inline: auto;
font-size: var(--text-sm);
color: var(--ink-2);
transition: color var(--duration-normal) var(--ease-out-expo);
}
.wi-photos-section__more:hover {
color: var(--accent);
}
.wi-photos-section__more svg {
transition: transform var(--duration-normal) var(--ease-out-expo);
}
.wi-photos-section__more:hover svg {
transform: translateX(4px);
}
@media (max-width: 768px) {
.wi-photos-section__grid[data-style="masonry"] {
columns: 2;
column-gap: var(--space-md);
}
.wi-photos-section__grid[data-style="masonry"] .wi-photos-section__item {
margin-bottom: var(--space-md);
}
.wi-photos-section__grid[data-style="grid"] {
grid-template-columns: repeat(2, 1fr);
gap: var(--space-md);
}
}
</style>
+248
View File
@@ -0,0 +1,248 @@
---
interface Props {
variant?: "default" | "featured" | "compact";
}
const { variant = "default" } = Astro.props;
---
<a
class:list={["post-card", `post-card--${variant}`]}
th:href="@{${post.status.permalink}}"
>
<div
th:unless="${#strings.isEmpty(post.spec.cover)}"
class="post-card__image-wrap"
>
<img
class="post-card__image"
th:src="${post.spec.cover}"
th:srcset="|${thumbnail.gen(post.spec.cover, 's')} 400w,
${thumbnail.gen(post.spec.cover, 'm')} 800w,
${thumbnail.gen(post.spec.cover, 'l')} 1200w|"
th:alt="${post.spec.title}"
loading="lazy"
decoding="async"
/>
<div class="post-card__overlay"></div>
</div>
<div class="post-card__content">
<time
class="post-card__date"
th:text="${#dates.format(post.spec.publishTime, 'yyyy-MM-dd')}"
></time>
<h3 class="post-card__title" th:text="${post.spec.title}"></h3>
<p
th:if="${variant != 'compact'}"
class="post-card__excerpt"
th:text="${post.status.excerpt}"
></p>
<div
th:if="${variant != 'compact' and !#lists.isEmpty(post.tags)}"
class="post-card__tags"
>
<span
th:each="tag : ${post.tags}"
class="post-card__tag"
th:text="${tag.spec.displayName}"
></span>
</div>
</div>
</a>
<style>
.post-card {
display: flex;
align-items: stretch;
gap: var(--space-lg);
background: var(--bg-raised);
border-radius: var(--border-radius-lg);
overflow: hidden;
text-decoration: none;
color: inherit;
box-shadow: var(--shadow-sm);
transition:
transform 400ms var(--ease-out-expo),
box-shadow 400ms var(--ease-out-expo);
}
.post-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
.post-card__image-wrap {
flex-shrink: 0;
overflow: hidden;
border-radius: var(--border-radius-lg);
}
.post-card__image {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 400ms var(--ease-out-expo);
}
.post-card:hover .post-card__image {
transform: scale(1.03);
}
.post-card__overlay {
display: none;
}
.post-card__content {
display: flex;
flex-direction: column;
gap: var(--space-sm);
padding: var(--space-lg);
min-width: 0;
}
.post-card__date {
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: var(--tracking-wider);
color: var(--ink-3);
line-height: var(--leading-none);
}
.post-card__title {
font-family: var(--font-sans);
font-weight: 700;
font-size: clamp(1.1rem, 2vw, 1.5rem);
line-height: var(--leading-tight);
color: var(--ink);
margin: 0;
transition: color 400ms var(--ease-out-expo);
}
.post-card:hover .post-card__title {
color: var(--accent);
}
.post-card__excerpt {
color: var(--ink-2);
font-size: var(--text-sm);
line-height: var(--leading-normal);
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
margin: 0;
}
.post-card__tags {
display: flex;
flex-wrap: wrap;
gap: var(--space-xs);
margin-top: auto;
}
.post-card__tag {
display: inline-flex;
align-items: center;
padding: 0.2em 0.6em;
border-radius: var(--border-radius-sm);
background: var(--bg-raised);
border: 1px solid var(--rule);
font-size: var(--text-xs);
color: var(--ink-2);
line-height: var(--leading-none);
}
.post-card--default {
flex-direction: row;
}
.post-card--default .post-card__image-wrap {
width: 40%;
aspect-ratio: 4 / 3;
}
.post-card--featured {
flex-direction: column;
}
.post-card--featured .post-card__image-wrap {
width: 100%;
aspect-ratio: 16 / 9;
position: relative;
}
.post-card--featured .post-card__overlay {
display: block;
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 50%;
background: linear-gradient(
to top,
var(--glass-bg),
transparent
);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
pointer-events: none;
}
.post-card--featured .post-card__content {
padding: var(--space-xl);
gap: var(--space-md);
}
.post-card--featured .post-card__title {
font-size: clamp(1.5rem, 3vw, 2rem);
}
.post-card--featured .post-card__excerpt {
-webkit-line-clamp: 3;
}
.post-card--compact {
flex-direction: row;
align-items: center;
background: transparent;
box-shadow: none;
gap: var(--space-md);
}
.post-card--compact:hover {
box-shadow: none;
}
.post-card--compact .post-card__image-wrap {
width: 60px;
height: 60px;
flex-shrink: 0;
border-radius: var(--border-radius-md);
}
.post-card--compact .post-card__image-wrap img {
aspect-ratio: 1 / 1;
}
.post-card--compact .post-card__content {
padding: var(--space-sm) 0;
gap: 0.2rem;
}
.post-card--compact .post-card__title {
font-size: var(--text-md);
}
@media (max-width: 768px) {
.post-card--default {
flex-direction: column;
}
.post-card--default .post-card__image-wrap {
width: 100%;
aspect-ratio: 16 / 9;
}
}
</style>
+85
View File
@@ -0,0 +1,85 @@
---
---
<section
class="wi-section wi-quote"
th:if="${theme.config?.home?.home_quote_enabled == true and theme.config?.home?.home_quote_content}"
>
<div class="wi-container">
<div class="wi-quote__inner">
<span class="wi-quote__mark wi-quote__mark--open" aria-hidden="true">"</span>
<blockquote
class="wi-quote__text"
th:text="${theme.config?.home?.home_quote_content}"
></blockquote>
<span class="wi-quote__mark wi-quote__mark--close" aria-hidden="true">"</span>
</div>
</div>
</section>
<style>
.wi-quote__inner {
position: relative;
max-width: 680px;
margin: 0 auto;
padding: var(--space-2xl) var(--space-xl);
background: var(--mist-pink);
border-radius: 16px;
text-align: center;
border: 1px solid var(--rule);
}
.wi-quote__mark {
font-family: Georgia, 'Times New Roman', serif;
font-size: 5rem;
line-height: 1;
color: var(--accent);
opacity: 0.25;
position: absolute;
user-select: none;
}
.wi-quote__mark--open {
top: -0.1em;
left: var(--space-lg);
}
.wi-quote__mark--close {
bottom: -0.5em;
right: var(--space-lg);
}
.wi-quote__text {
margin: 0;
padding: 0;
border: none;
background: none;
font-family: var(--font-sans);
font-size: var(--text-xl);
font-style: italic;
line-height: var(--leading-relaxed);
color: var(--ink);
letter-spacing: var(--tracking-normal);
}
@media (max-width: 640px) {
.wi-quote__inner {
padding: var(--space-xl) var(--space-lg);
}
.wi-quote__mark {
font-size: 3.5rem;
}
.wi-quote__mark--open {
left: var(--space-md);
}
.wi-quote__mark--close {
right: var(--space-md);
}
.wi-quote__text {
font-size: var(--text-lg);
}
}
</style>
+36
View File
@@ -0,0 +1,36 @@
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from "vue";
const observer = ref<IntersectionObserver | null>(null);
onMounted(() => {
const animationEnabled = !document.documentElement.classList.contains(
"wi-no-animations"
);
if (!animationEnabled) return;
observer.value = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("wi-revealed");
observer.value?.unobserve(entry.target);
}
});
},
{ threshold: 0.1, rootMargin: "0px 0px -40px 0px" }
);
document.querySelectorAll(".wi-reveal").forEach((el) => {
observer.value?.observe(el);
});
});
onUnmounted(() => {
observer.value?.disconnect();
});
</script>
<template>
<slot />
</template>
+56
View File
@@ -0,0 +1,56 @@
<script setup lang="ts">
import { onMounted, onUnmounted } from "vue";
function handleKeydown(e: KeyboardEvent) {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
const searchWidget = (window as any).SearchWidget;
if (searchWidget && typeof searchWidget.open === "function") {
searchWidget.open();
}
}
}
onMounted(() => {
document.addEventListener("keydown", handleKeydown);
});
onUnmounted(() => {
document.removeEventListener("keydown", handleKeydown);
});
</script>
<template>
<span class="wi-search-hint" aria-hidden="true">
<kbd class="wi-search-hint__key">⌘</kbd>
<kbd class="wi-search-hint__key">K</kbd>
</span>
</template>
<style scoped>
.wi-search-hint {
display: inline-flex;
align-items: center;
gap: 4px;
pointer-events: none;
user-select: none;
}
.wi-search-hint__key {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 22px;
height: 22px;
padding: 0 5px;
font-family: var(--font-body);
font-size: 11px;
font-weight: 500;
line-height: 1;
color: var(--ink-3);
background: var(--bg-raised);
border: 1px solid var(--rule);
border-radius: 6px;
box-shadow: 0 1px 0 var(--rule);
}
</style>
+5 -1
View File
@@ -10,9 +10,13 @@ onMounted(() => {
});
function toggle() {
document.documentElement.classList.add("wi-theme-transition");
isDark.value = !isDark.value;
document.documentElement.classList.toggle("dark", isDark.value);
localStorage.setItem("theme", isDark.value ? "dark" : "light");
localStorage.setItem("wi-theme", isDark.value ? "dark" : "light");
setTimeout(() => {
document.documentElement.classList.remove("wi-theme-transition");
}, 300);
}
</script>
+158
View File
@@ -0,0 +1,158 @@
---
---
<section
class="wi-section wi-timeline"
th:if="${theme.config?.home?.home_timeline_enabled == true}"
>
<div class="wi-container">
<div class="wi-section__header">
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_timeline_title ?: '时间线'}">时间线</h2>
<span class="wi-section__accent"></span>
</div>
<div class="wi-timeline__body">
<th:block th:with="postsResult = ${postFinder.list({page: 1, size: 50})}">
<th:block th:each="post : ${postsResult.items}">
<div class="wi-timeline__year" th:text="${#dates.format(post.spec.publishTime, 'yyyy')}"></div>
<div class="wi-timeline__entries">
<a
th:href="@{${post.status.permalink}}"
class="wi-timeline__entry"
>
<span class="wi-timeline__dot"></span>
<span class="wi-timeline__line"></span>
<time
class="wi-timeline__date"
th:text="${#dates.format(post.spec.publishTime, 'MM-dd')}"
></time>
<div class="wi-timeline__content">
<span class="wi-timeline__title" th:text="${post.spec.title}"></span>
<span
class="wi-timeline__excerpt"
th:if="${post.status.excerpt}"
th:text="${post.status.excerpt}"
></span>
</div>
</a>
</div>
</th:block>
</th:block>
</div>
</div>
</section>
<style>
.wi-timeline__body {
position: relative;
display: flex;
flex-direction: column;
gap: var(--space-2xl);
}
.wi-timeline__year {
font-family: var(--font-sans);
font-size: var(--text-xl);
font-weight: 700;
color: var(--ink-2);
letter-spacing: var(--tracking-tight);
padding-bottom: var(--space-sm);
border-bottom: 1px solid var(--rule);
}
.wi-timeline__entries {
display: flex;
flex-direction: column;
}
.wi-timeline__entry {
position: relative;
display: grid;
grid-template-columns: 48px 56px minmax(0, 1fr);
gap: var(--space-sm);
align-items: baseline;
padding: var(--space-sm) 0;
text-decoration: none;
color: inherit;
transition: color var(--duration-fast) var(--ease-out-quart);
}
.wi-timeline__entry:hover {
color: var(--accent);
}
.wi-timeline__dot {
position: absolute;
left: 19px;
top: 10px;
width: 8px;
height: 8px;
border-radius: 9999px;
background: var(--accent);
z-index: 2;
}
.wi-timeline__line {
position: absolute;
left: 22px;
top: 18px;
bottom: calc(-1 * var(--space-sm));
width: 1px;
background: var(--rule);
z-index: 1;
}
.wi-timeline__entry:last-child .wi-timeline__line {
display: none;
}
.wi-timeline__date {
font-size: var(--text-sm);
color: var(--ink-3);
letter-spacing: var(--tracking-wide);
padding-top: 1px;
}
.wi-timeline__content {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.wi-timeline__title {
font-size: var(--text-base);
font-weight: 600;
color: var(--ink);
line-height: var(--leading-snug);
transition: color var(--duration-fast) var(--ease-out-quart);
}
.wi-timeline__entry:hover .wi-timeline__title {
color: var(--accent);
}
.wi-timeline__excerpt {
font-size: var(--text-sm);
color: var(--ink-3);
line-height: var(--leading-normal);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 640px) {
.wi-timeline__entry {
grid-template-columns: 36px 48px minmax(0, 1fr);
}
.wi-timeline__dot {
left: 13px;
width: 6px;
height: 6px;
}
.wi-timeline__line {
left: 15px;
}
}
</style>
-185
View File
@@ -1,185 +0,0 @@
<script lang="ts" setup>
import type { DetailedUser } from "@halo-dev/api-client";
import ky from "ky";
import { computed, onMounted, ref } from "vue";
const user = ref<DetailedUser | null>(null);
const isLoading = ref(true);
const isAnonymous = computed(() => {
return user.value?.user.metadata.name === "anonymousUser";
});
const displayName = computed(() => {
return (
user.value?.user.spec.displayName ||
user.value?.user.metadata.name ||
"Account"
);
});
const avatarSrc = computed(() => {
return user.value?.user.spec.avatar || "";
});
const avatarLabel = computed(() => {
if (isAnonymous.value) {
return "?";
}
return displayName.value.charAt(0).toUpperCase();
});
onMounted(async () => {
try {
user.value = await ky
.get<DetailedUser>(`/apis/api.console.halo.run/v1alpha1/users/-`)
.json();
} catch {
user.value = null;
} finally {
isLoading.value = false;
}
});
</script>
<template>
<div
v-if="isLoading"
class="user-button-shell user-button-shell--loading"
aria-busy="true"
aria-live="polite"
>
<span
class="user-button-avatar user-button-avatar--skeleton"
aria-hidden="true"
></span>
<span class="user-button-text-skeleton" aria-hidden="true"></span>
<span class="sr-only">Loading user state</span>
</div>
<a
href="/uc"
v-else-if="user && !isAnonymous"
class="user-button-shell"
:title="displayName"
>
<span class="user-button-avatar" aria-hidden="true">
<img
v-if="avatarSrc"
class="user-button-avatar__image"
:src="avatarSrc"
:alt="displayName"
/>
<span v-else>{{ avatarLabel }}</span>
</span>
<span class="user-button-text">{{ displayName }}</span>
</a>
<a v-else class="user-button-shell user-button-shell--link" href="/login">
<span class="user-button-avatar" aria-hidden="true">{{ avatarLabel }}</span>
<span class="user-button-text">Login</span>
</a>
</template>
<style scoped>
.user-button-shell {
display: inline-flex;
align-items: center;
gap: 0.55rem;
box-sizing: border-box;
flex-shrink: 0;
width: 112px;
height: 34px;
padding: 0 0.7rem;
border-radius: 8px;
color: var(--ink-2);
background: transparent;
}
.user-button-shell--link {
text-decoration: none;
transition:
background 0.15s ease,
color 0.15s ease;
}
.user-button-shell--link:hover {
background: var(--bg-raised);
color: var(--ink);
}
.user-button-shell--loading {
pointer-events: none;
}
.user-button-avatar {
display: inline-flex;
align-items: center;
justify-content: center;
overflow: hidden;
width: 20px;
height: 20px;
border-radius: 999px;
background: var(--bg-raised);
color: var(--ink);
font-size: 0.72rem;
font-weight: 700;
line-height: 1;
flex-shrink: 0;
}
.user-button-avatar__image {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.user-button-avatar--skeleton,
.user-button-text-skeleton {
background: linear-gradient(
90deg,
var(--bg-raised) 0%,
color-mix(in srgb, var(--bg-raised) 78%, var(--ink) 22%) 50%,
var(--bg-raised) 100%
);
background-size: 200% 100%;
animation: shimmer 1.2s ease-in-out infinite;
}
.user-button-text {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.9rem;
line-height: 1;
}
.user-button-text-skeleton {
display: block;
width: 100%;
height: 0.72rem;
border-radius: 999px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style>