feat: 初始化 Halo 暗色模式插件

- Halo Plugin 后端(Java/Gradle),含 DarkModePlugin 主类和测试
- Vue 3 + TypeScript 前端 UI,包含主题切换组件和设置页面
- 暗色模式 CSS 变量和覆盖样式(布局/编辑器/表单/滚动条等)
- 设计文档和调查文档
- Halo 插件/主题开发 Agent Skills

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-06 21:03:00 +08:00
commit edbf78f236
115 changed files with 14745 additions and 0 deletions
@@ -0,0 +1,32 @@
<include src="layout.html">
<template name="head">
<title th:text="|Archives - ${site.title}|">Archives</title>
</template>
<h1>Archives</h1>
<th:block th:each="archive : ${archives.items}">
<div class="archive-year">
<h2 th:text="${archive.year}"></h2>
<th:block th:each="month : ${archive.months}">
<h3 th:text="|${archive.year}-${month.month}|"></h3>
<ul class="post-list">
<li class="post-item" th:each="post : ${month.posts}">
<h2 class="post-title">
<a th:href="@{${post.status.permalink}}" th:text="${post.spec.title}"></a>
</h2>
<p class="post-meta">
<time th:text="${#temporals.format(post.spec.publishTime, 'yyyy-MM-dd')}"></time>
</p>
</li>
</ul>
</th:block>
</div>
</th:block>
<nav class="pagination" th:if="${archives.hasPrevious() || archives.hasNext()}">
<a th:if="${archives.hasPrevious()}" th:href="@{${archives.prevUrl}}">Previous</a>
<span th:text="|${archives.page} / ${archives.totalPages}|"></span>
<a th:if="${archives.hasNext()}" th:href="@{${archives.nextUrl}}">Next</a>
</nav>
</include>
@@ -0,0 +1,16 @@
<include src="layout.html">
<template name="head">
<title th:text="|${author.spec.displayName} - ${site.title}|">Author</title>
</template>
<section>
<h1 th:text="${author.spec.displayName}"></h1>
<p th:if="${not #strings.isEmpty(author.spec.bio)}" th:text="${author.spec.bio}"></p>
<ul class="post-list">
<include src="post-card.html"></include>
</ul>
<include src="pagination.html"></include>
</section>
</include>
@@ -0,0 +1,22 @@
<include src="layout.html">
<template name="head">
<title th:text="|Categories - ${site.title}|">Categories</title>
</template>
<h1>Categories</h1>
<ul class="category-list">
<li th:each="cat : ${categories.items}">
<a
th:href="@{${cat.status.permalink}}"
th:text="|${cat.spec.displayName} (${cat.postCount})|"
class="category"
></a>
</li>
</ul>
<nav class="pagination" th:if="${categories.hasPrevious() || categories.hasNext()}">
<a th:if="${categories.hasPrevious()}" th:href="@{${categories.prevUrl}}">Previous</a>
<span th:text="|${categories.page} / ${categories.totalPages}|"></span>
<a th:if="${categories.hasNext()}" th:href="@{${categories.nextUrl}}">Next</a>
</nav>
</include>
@@ -0,0 +1,14 @@
<include src="layout.html">
<template name="head">
<title th:text="|${category.spec.displayName} - ${site.title}|">Category Archive</title>
</template>
<h1 th:text="|Category: ${category.spec.displayName}|"></h1>
<p th:if="${category.spec.description}" th:text="${category.spec.description}"></p>
<ul class="post-list">
<include src="post-card.html"></include>
</ul>
<include src="pagination.html"></include>
</include>
@@ -0,0 +1,167 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family:
system-ui,
-apple-system,
sans-serif;
line-height: 1.6;
color: #333;
}
a {
color: #0070f3;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 0 1rem;
}
.site-header {
border-bottom: 1px solid #eee;
padding: 1rem 0;
}
.site-header .container {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5rem;
}
.site-title {
font-size: 1.25rem;
font-weight: 700;
color: #333;
}
.site-nav ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
gap: 1rem;
}
.site-main {
padding: 2rem 0;
}
.post-list {
list-style: none;
margin: 0;
padding: 0;
}
.post-item {
padding: 1.5rem 0;
border-bottom: 1px solid #eee;
}
.post-item:last-child {
border-bottom: none;
}
.post-title {
margin: 0 0 0.25rem;
font-size: 1.25rem;
}
.post-meta {
color: #666;
font-size: 0.875rem;
margin: 0 0 0.5rem;
}
.post-excerpt {
color: #555;
margin: 0;
}
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 2rem 0;
}
.article-header {
margin-bottom: 2rem;
}
.article-title {
margin: 0 0 0.5rem;
font-size: 2rem;
}
.article-meta {
color: #666;
font-size: 0.875rem;
}
.article-content {
max-width: 100%;
line-height: 1.8;
}
.article-content img {
max-width: 100%;
}
.article-tags,
.article-categories {
margin-top: 1.5rem;
}
.tag,
.category {
display: inline-block;
padding: 0.125rem 0.5rem;
background: #f0f0f0;
border-radius: 4px;
font-size: 0.875rem;
margin: 0.25rem;
}
.post-nav {
display: flex;
justify-content: space-between;
padding: 2rem 0;
border-top: 1px solid #eee;
margin-top: 2rem;
}
.site-footer {
border-top: 1px solid #eee;
padding: 1.5rem 0;
text-align: center;
color: #666;
font-size: 0.875rem;
}
.archive-year {
margin-top: 2rem;
}
.tag-list,
.category-list {
list-style: none;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
@@ -0,0 +1,12 @@
<include src="layout.html">
<template name="head">
<title th:text="${site.title}">Home</title>
<script type="module" src="./js/index.ts"></script>
</template>
<ul class="post-list">
<include src="post-card.html"></include>
</ul>
<include src="pagination.html"></include>
</include>
@@ -0,0 +1 @@
console.log("Hello, Halo Theme!");
@@ -0,0 +1 @@
import "../css/main.css";
@@ -0,0 +1 @@
console.log("Hello, Halo Theme Post!");
@@ -0,0 +1,20 @@
<include src="layout.html">
<template name="head">
<title th:text="|${singlePage.spec.title} - ${site.title}|">Page Title</title>
</template>
<article>
<header class="article-header">
<h1 class="article-title" th:text="${singlePage.spec.title}"></h1>
</header>
<div class="article-content" th:utext="${singlePage.content.content}"></div>
</article>
<div th:if="${haloCommentEnabled}">
<halo:comment
group="content.halo.run"
kind="SinglePage"
th:attr="name=${singlePage.metadata.name}"
/>
</div>
</include>
@@ -0,0 +1,42 @@
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<slot name="head">
<title th:text="${site.title}">Site Title</title>
</slot>
<script type="module" src="./js/main.ts"></script>
</head>
<body>
<header class="site-header">
<div class="container">
<a class="site-title" th:href="@{/}" th:text="${site.title}">Site Title</a>
<nav class="site-nav" th:with="menu = ${menuFinder.getPrimary()}">
<ul>
<li th:each="item : ${menu.menuItems}">
<a
th:href="@{${item.status.href}}"
th:text="${item.status.displayName}"
th:target="${item.spec.target?.value}"
></a>
</li>
</ul>
</nav>
</div>
</header>
<main class="site-main">
<div class="container">
<slot />
</div>
</main>
<footer class="site-footer">
<div class="container">
<p th:text="${theme.config.basic.custom_footer ?: site.title}"></p>
<halo:footer />
</div>
</footer>
</body>
</html>
@@ -0,0 +1,5 @@
<nav class="pagination" th:if="${posts.hasPrevious() || posts.hasNext()}">
<a th:if="${posts.hasPrevious()}" th:href="@{${posts.prevUrl}}">Previous</a>
<span th:text="|${posts.page} / ${posts.totalPages}|"></span>
<a th:if="${posts.hasNext()}" th:href="@{${posts.nextUrl}}">Next</a>
</nav>
@@ -0,0 +1,18 @@
<li class="post-item" th:each="post : ${posts.items}">
<h2 class="post-title">
<a th:href="@{${post.status.permalink}}" th:text="${post.spec.title}"></a>
</h2>
<p class="post-meta">
<time th:text="${#temporals.format(post.spec.publishTime, 'yyyy-MM-dd')}"></time>
<span th:if="${not #lists.isEmpty(post.categories)}">
&nbsp;·&nbsp;
<a
th:each="cat : ${post.categories}"
th:href="@{${cat.status.permalink}}"
th:text="${cat.spec.displayName}"
class="category"
></a>
</span>
</p>
<p class="post-excerpt" th:text="${post.status.excerpt}"></p>
</li>
@@ -0,0 +1,56 @@
<include src="layout.html">
<template name="head">
<title th:text="|${post.spec.title} - ${site.title}|">Post Title</title>
<script type="module" src="./js/post.ts"></script>
</template>
<article>
<header class="article-header">
<h1 class="article-title" th:text="${post.spec.title}"></h1>
<div class="article-meta">
<time th:text="${#temporals.format(post.spec.publishTime, 'yyyy-MM-dd')}"></time>
<span th:if="${not #lists.isEmpty(post.categories)}">
&nbsp;·&nbsp;
<a
th:each="cat : ${post.categories}"
th:href="@{${cat.status.permalink}}"
th:text="${cat.spec.displayName}"
class="category"
></a>
</span>
</div>
</header>
<div class="article-content" th:utext="${post.content.content}"></div>
<div class="article-tags" th:if="${not #lists.isEmpty(post.tags)}">
<a
th:each="tag : ${post.tags}"
th:href="@{${tag.status.permalink}}"
th:text="${tag.spec.displayName}"
class="tag"
></a>
</div>
<nav class="post-nav" th:with="cursor = ${postFinder.cursor(post.metadata.name)}">
<span>
<a
th:if="${cursor.hasPrevious()}"
th:href="@{${cursor.previous.status.permalink}}"
th:text="|← ${cursor.previous.spec.title}|"
></a>
</span>
<span>
<a
th:if="${cursor.hasNext()}"
th:href="@{${cursor.next.status.permalink}}"
th:text="|${cursor.next.spec.title} →|"
></a>
</span>
</nav>
</article>
<div th:if="${haloCommentEnabled}">
<halo:comment group="content.halo.run" kind="Post" th:attr="name=${post.metadata.name}" />
</div>
</include>
@@ -0,0 +1,13 @@
<include src="layout.html">
<template name="head">
<title th:text="|${tag.spec.displayName} - ${site.title}|">Tag Archive</title>
</template>
<h1 th:text="|Tag: ${tag.spec.displayName}|"></h1>
<ul class="post-list">
<include src="post-card.html"></include>
</ul>
<include src="pagination.html"></include>
</include>
@@ -0,0 +1,18 @@
<include src="layout.html">
<template name="head">
<title th:text="|Tags - ${site.title}|">Tags</title>
</template>
<h1>Tags</h1>
<ul class="tag-list">
<li th:each="tag : ${tags.items}">
<a th:href="@{${tag.status.permalink}}" th:text="${tag.spec.displayName}" class="tag"></a>
</li>
</ul>
<nav class="pagination" th:if="${tags.hasPrevious() || tags.hasNext()}">
<a th:if="${tags.hasPrevious()}" th:href="@{${tags.prevUrl}}">Previous</a>
<span th:text="|${tags.page} / ${tags.totalPages}|"></span>
<a th:if="${tags.hasNext()}" th:href="@{${tags.nextUrl}}">Next</a>
</nav>
</include>