feat: 完善主题功能并适配插件 (v1.1.5)
功能新增 - 新增页面布局契约 templates/layout.html,插件前台页面可复用主题外观,避免回退到 Halo fallback 布局 - 新增微信读书页面模板 (weread.html),集成 halo-weread-plugin 书架 - 新增订阅页面模板 (page_subscriptions.html) - 新增欢迎弹窗组件(IP 地理定位,多 API 容错) - 新增点击特效组件(9 种效果) - 页脚新增全站统计,支持开关与统计项配置,默认开启 插件适配 - 适配 extra-api:文章字数改服务端统计、正文可选中英混排与服务端代码高亮 - 修正各页面插件应用市场链接与插件名 修复 - 修复文章正文图片/视频无法居中(正文样式移至全局,scoped 样式对 Thymeleaf 注入内容无效) - 修复正文 H1-H6 字号过大,重排层级与移动端断点 - 修复 th: 属性中文字符串缺少引号导致的 Thymeleaf 解析异常(/links、/moments 500) - 修复 JS 内联表达式与 Thymeleaf 内联语法冲突,全部内联脚本加 th:inline="none" - 修复移除 i18n 后 steam 页 Elvis 表达式回归问题 - 修正订阅页 th:attr 引号嵌套错误 优化 - Hero 移除灰色遮罩,下滑箭头改为可点击平滑滚动 - 优化目录样式:移除圆点与下拉箭头,支持两行显示,修复长目录截断 - 移除 i18n 多语言支持 清理 - 移除未使用的 Vue/图标依赖与废弃脚本、重复资源
This commit is contained in:
1 parent
9d0bfcfb4f
commit
430d107d44
78 files changed
+2959
-36075
No files matched your search
@@ -0,0 +1,243 @@
|
||||
---
|
||||
---
|
||||
|
||||
<div id="wi-click-effect" th:if="${theme.config?.style?.click_effect_enabled == true}" th:attr="data-effect=${theme.config?.style?.click_effect_type}">
|
||||
|
||||
<script is:inline th:inline="none">
|
||||
(function () {
|
||||
var container = document.getElementById("wi-click-effect");
|
||||
if (!container) return;
|
||||
var effect = container.getAttribute("data-effect") || "off";
|
||||
if (effect === "off" || !effect) return;
|
||||
|
||||
/* 社会主义核心价值观 */
|
||||
var CORE_VALUES = [
|
||||
"富强", "民主", "文明", "和谐",
|
||||
"自由", "平等", "公正", "法治",
|
||||
"爱国", "敬业", "诚信", "友善"
|
||||
];
|
||||
|
||||
var RANDOM_COLORS = [
|
||||
"#ff6b6b", "#f06595", "#cc5de8", "#845ef7",
|
||||
"#5c7cfa", "#339af0", "#22b8cf", "#20c997",
|
||||
"#51cf66", "#94d82d", "#fcc419", "#ff922b"
|
||||
];
|
||||
|
||||
/* 创建单个 DOM 元素并加入舞台 */
|
||||
function spawn(el, x, y) {
|
||||
el.style.left = x + "px";
|
||||
el.style.top = y + "px";
|
||||
container.appendChild(el);
|
||||
}
|
||||
|
||||
/* ========== 1. 社会主义核心价值观 ========== */
|
||||
function spawnValueText(x, y) {
|
||||
var el = document.createElement("span");
|
||||
el.className = "wi-click-fx wi-click-fx--text";
|
||||
el.textContent = CORE_VALUES[Math.floor(Math.random() * CORE_VALUES.length)];
|
||||
el.style.color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
|
||||
spawn(el, x, y);
|
||||
setTimeout(function () {
|
||||
if (el.parentNode) el.parentNode.removeChild(el);
|
||||
}, 1200);
|
||||
}
|
||||
|
||||
/* ========== 2. 随机彩色爱心 ========== */
|
||||
function spawnHeart(x, y) {
|
||||
var el = document.createElement("span");
|
||||
el.className = "wi-click-fx wi-click-fx--heart";
|
||||
el.innerHTML = "❤";
|
||||
el.style.color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
|
||||
var dx = (Math.random() - 0.5) * 80;
|
||||
spawn(el, x, y);
|
||||
requestAnimationFrame(function () {
|
||||
el.style.transform = "translate(" + dx + "px, -80px) scale(0.2)";
|
||||
el.style.opacity = "0";
|
||||
});
|
||||
setTimeout(function () {
|
||||
if (el.parentNode) el.parentNode.removeChild(el);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/* ========== 3. 烟花 ========== */
|
||||
function spawnFirework(x, y) {
|
||||
var count = 16;
|
||||
var color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
|
||||
for (var i = 0; i < count; i++) {
|
||||
var el = document.createElement("span");
|
||||
el.className = "wi-click-fx wi-click-fx--spark";
|
||||
el.style.background = color;
|
||||
el.style.boxShadow = "0 0 6px " + color;
|
||||
var angle = (Math.PI * 2 * i) / count;
|
||||
var dist = 40 + Math.random() * 30;
|
||||
var dx = Math.cos(angle) * dist;
|
||||
var dy = Math.sin(angle) * dist;
|
||||
spawn(el, x, y);
|
||||
requestAnimationFrame(function (e, tx, ty) {
|
||||
return function () {
|
||||
e.style.transform = "translate(" + tx + "px, " + ty + "px) scale(0)";
|
||||
e.style.opacity = "0";
|
||||
};
|
||||
}(el, dx, dy));
|
||||
(function (e) {
|
||||
setTimeout(function () {
|
||||
if (e.parentNode) e.parentNode.removeChild(e);
|
||||
}, 800);
|
||||
})(el);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 4. 烟花波纹 ========== */
|
||||
function spawnRipple(x, y) {
|
||||
var color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
|
||||
for (var r = 0; r < 2; r++) {
|
||||
var el = document.createElement("span");
|
||||
el.className = "wi-click-fx wi-click-fx--ripple";
|
||||
el.style.borderColor = color;
|
||||
spawn(el, x, y);
|
||||
(function (e, delay) {
|
||||
setTimeout(function () {
|
||||
requestAnimationFrame(function () {
|
||||
e.style.transform = "translate(-50%, -50%) scale(4)";
|
||||
e.style.opacity = "0";
|
||||
});
|
||||
}, delay);
|
||||
setTimeout(function () {
|
||||
if (e.parentNode) e.parentNode.removeChild(e);
|
||||
}, delay + 700);
|
||||
})(el, r * 150);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 5. 粒子 ========== */
|
||||
function spawnParticles(x, y, n) {
|
||||
n = n || 8;
|
||||
for (var i = 0; i < n; i++) {
|
||||
var el = document.createElement("span");
|
||||
el.className = "wi-click-fx wi-click-fx--particle";
|
||||
el.style.background = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
|
||||
var dx = (Math.random() - 0.5) * 60;
|
||||
var dy = (Math.random() - 0.5) * 60 - 20;
|
||||
spawn(el, x, y);
|
||||
requestAnimationFrame(function (e, tx, ty) {
|
||||
return function () {
|
||||
e.style.transform = "translate(" + tx + "px, " + ty + "px) scale(0)";
|
||||
e.style.opacity = "0";
|
||||
};
|
||||
}(el, dx, dy));
|
||||
(function (e) {
|
||||
setTimeout(function () {
|
||||
if (e.parentNode) e.parentNode.removeChild(e);
|
||||
}, 700);
|
||||
})(el);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 6. 星星 ========== */
|
||||
function spawnStar(x, y) {
|
||||
var el = document.createElement("span");
|
||||
el.className = "wi-click-fx wi-click-fx--star";
|
||||
el.innerHTML = "★";
|
||||
el.style.color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
|
||||
var dx = (Math.random() - 0.5) * 50;
|
||||
var dy = (Math.random() - 0.5) * 50 - 30;
|
||||
spawn(el, x, y);
|
||||
requestAnimationFrame(function () {
|
||||
el.style.transform = "translate(" + dx + "px, " + dy + "px) scale(1.6)";
|
||||
el.style.opacity = "0";
|
||||
});
|
||||
setTimeout(function () {
|
||||
if (el.parentNode) el.parentNode.removeChild(el);
|
||||
}, 800);
|
||||
}
|
||||
|
||||
/* 点击分发 */
|
||||
var clickHandlers = {
|
||||
"values": spawnValueText,
|
||||
"hearts": spawnHeart,
|
||||
"fireworks": spawnFirework,
|
||||
"ripples": spawnRipple,
|
||||
"particles": function (x, y) { spawnParticles(x, y, 10); },
|
||||
"stars": spawnStar
|
||||
};
|
||||
|
||||
/* 拖尾效果 */
|
||||
var trailHandlers = {
|
||||
"star-trail": function (x, y) {
|
||||
var el = document.createElement("span");
|
||||
el.className = "wi-click-fx wi-click-fx--trail-star";
|
||||
el.innerHTML = "★";
|
||||
el.style.color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
|
||||
spawn(el, x, y);
|
||||
setTimeout(function () {
|
||||
if (el.parentNode) el.parentNode.removeChild(el);
|
||||
}, 600);
|
||||
},
|
||||
"particle-trail": function (x, y) {
|
||||
var el = document.createElement("span");
|
||||
el.className = "wi-click-fx wi-click-fx--trail-particle";
|
||||
el.style.background = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
|
||||
spawn(el, x, y);
|
||||
setTimeout(function () {
|
||||
if (el.parentNode) el.parentNode.removeChild(el);
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
var isTrail = effect.indexOf("trail") !== -1;
|
||||
|
||||
if (isTrail) {
|
||||
var handler = trailHandlers[effect];
|
||||
if (!handler) return;
|
||||
var lastX = 0, lastY = 0;
|
||||
var throttle = false;
|
||||
document.addEventListener("mousemove", function (e) {
|
||||
lastX = e.clientX;
|
||||
lastY = e.clientY;
|
||||
if (throttle) return;
|
||||
throttle = true;
|
||||
requestAnimationFrame(function () {
|
||||
throttle = false;
|
||||
handler(lastX, lastY);
|
||||
});
|
||||
}, { passive: true });
|
||||
} else {
|
||||
var handler = clickHandlers[effect];
|
||||
if (!handler) return;
|
||||
document.addEventListener("click", function (e) {
|
||||
handler(e.clientX, e.clientY);
|
||||
}, { passive: true });
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style is:global>
|
||||
.wi-click-fx{position:fixed;pointer-events:none;will-change:transform,opacity
|
||||
}
|
||||
|
||||
.wi-click-fx--text{font-size:18px;font-weight:700;transform:translate(-50%,-50%);opacity:0;animation:wi-fx-text 1.2s ease-out forwards
|
||||
}
|
||||
|
||||
.wi-click-fx--heart{font-size:20px;transform:translate(-50%,-50%) scale(1);opacity:1;transition:transform 1s cubic-bezier(.16,1,.3,1),opacity 1s ease-out
|
||||
}
|
||||
|
||||
.wi-click-fx--spark{width:6px;height:6px;border-radius:50%;transform:translate(-50%,-50%) scale(1);opacity:1;transition:transform .8s cubic-bezier(.16,1,.3,1),opacity .8s ease-out
|
||||
}
|
||||
|
||||
.wi-click-fx--ripple{width:30px;height:30px;border:2px solid;border-radius:50%;transform:translate(-50%,-50%) scale(.5);opacity:1;transition:transform .7s ease-out,opacity .7s ease-out
|
||||
}
|
||||
|
||||
.wi-click-fx--particle{width:8px;height:8px;border-radius:50%;transform:translate(-50%,-50%) scale(1);opacity:1;transition:transform .7s cubic-bezier(.16,1,.3,1),opacity .7s ease-out
|
||||
}
|
||||
|
||||
.wi-click-fx--star{font-size:16px;transform:translate(-50%,-50%) scale(1);opacity:1;transition:transform .8s cubic-bezier(.16,1,.3,1),opacity .8s ease-out
|
||||
}
|
||||
|
||||
.wi-click-fx--trail-star{font-size:12px;transform:translate(-50%,-50%) scale(1);opacity:.8;transition:opacity .6s ease-out,transform .6s ease-out
|
||||
}
|
||||
|
||||
.wi-click-fx--trail-particle{width:6px;height:6px;border-radius:50%;transform:translate(-50%,-50%) scale(1);opacity:.8;transition:opacity .5s ease-out,transform .5s ease-out
|
||||
}
|
||||
|
||||
@keyframes wi-fx-text{0%{opacity:0;transform:translate(-50%,-50%) scale(.5)}20%{opacity:1;transform:translate(-50%,-80%) scale(1.1)}to{opacity:0;transform:translate(-50%,-120%) scale(.9)}}
|
||||
</style>
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<div class="wi-cursor-glow" id="wi-cursor-glow"></div>
|
||||
|
||||
<script is:inline>
|
||||
<script is:inline th:inline="none">
|
||||
(function(){
|
||||
var glowEl = document.getElementById("wi-cursor-glow");
|
||||
if (!glowEl) return;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
>
|
||||
<div class="wi-container">
|
||||
<div class="wi-section__header">
|
||||
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_featured_title ?: #messages.msg('common.featured')}">精选</h2>
|
||||
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_featured_title ?: '精选'}">精选</h2>
|
||||
</div>
|
||||
<div class="wi-featured">
|
||||
<a
|
||||
@@ -32,7 +32,7 @@
|
||||
<span
|
||||
class="wi-featured__pinned"
|
||||
th:if="${post.spec.pinned}"
|
||||
th:text="#{hero.pinned}">Pinned</span>
|
||||
th:text="'置顶'">置顶</span>
|
||||
</div>
|
||||
<h3
|
||||
class="wi-featured__title"
|
||||
|
||||
+107
-6
@@ -1,19 +1,42 @@
|
||||
---
|
||||
const THEME_VERSION = "1.0.0";
|
||||
const today = new Date();
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
// 版本号直接读取 theme.yaml,避免与包版本不同步
|
||||
function readThemeVersion() {
|
||||
try {
|
||||
const yaml = readFileSync(resolve(process.cwd(), "theme.yaml"), "utf-8");
|
||||
return yaml.match(/^\s*version:\s*"?([^"\s]+)"?/m)?.[1] ?? "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
const THEME_VERSION = readThemeVersion();
|
||||
---
|
||||
|
||||
<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}">
|
||||
<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}, icpText=${theme.config?.footer?.footer_icp}, mpsText=${theme.config?.footer?.footer_mps}, showIcp=${theme.config?.footer?.footer_show_icp == true and not #strings.isEmpty(#strings.trim(icpText ?: ''))}, showMps=${theme.config?.footer?.footer_show_mps == true and not #strings.isEmpty(#strings.trim(mpsText ?: ''))}"
|
||||
>
|
||||
<div class="footer-inner">
|
||||
<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="${#dates.format(#dates.createNow(), 'yyyy')}">2026</th:block>
|
||||
<th:block th:text="${theme.config?.basic?.owner_name ?: 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 class="footer-icp" th:if="${showIcp}">
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer" th:text="${#strings.trim(icpText)}"></a>
|
||||
</span>
|
||||
<span class="footer-mps" th:if="${showMps}" th:with="mpsUrl=${theme.config?.footer?.footer_mps_url ?: 'https://beian.mps.gov.cn/'}">
|
||||
<a th:href="${mpsUrl}" target="_blank" rel="noopener noreferrer">
|
||||
<svg class="footer-mps__icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12 2L2 7v6c0 5.5 3.8 10.7 10 12 6.2-1.3 10-6.5 10-12V7l-10-5z"></path>
|
||||
<path d="M9.5 12.5l2 2 3.5-4" stroke="#fff" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
<span th:text="${#strings.trim(mpsText)}"></span>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="footer-socials" th:if="${theme.config?.footer?.footer_socials}">
|
||||
@@ -34,6 +57,39 @@ const today = new Date();
|
||||
<span th:if="${showTheme}">WarmIsland v{THEME_VERSION}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="footer-stats"
|
||||
th:if="${theme.config?.footer?.footer_show_stats != false}"
|
||||
th:with="stats=${siteStatsFinder.getStats()}, items=${theme.config?.footer?.footer_stats_items ?: {'post','category','comment','visit'}}"
|
||||
>
|
||||
<span class="footer-stat" th:if="${#lists.contains(items, 'post')}">
|
||||
<span class="footer-stat__value" th:text="${stats.post ?: 0}">0</span>
|
||||
<span class="footer-stat__label">文章</span>
|
||||
</span>
|
||||
<span class="footer-stat" th:if="${#lists.contains(items, 'category')}">
|
||||
<span class="footer-stat__value" th:text="${stats.category ?: 0}">0</span>
|
||||
<span class="footer-stat__label">分类</span>
|
||||
</span>
|
||||
<span class="footer-stat" th:if="${#lists.contains(items, 'comment')}">
|
||||
<span class="footer-stat__value" th:text="${stats.comment ?: 0}">0</span>
|
||||
<span class="footer-stat__label">评论</span>
|
||||
</span>
|
||||
<span class="footer-stat" th:if="${#lists.contains(items, 'visit')}">
|
||||
<span class="footer-stat__value" th:text="${stats.visit ?: 0}">0</span>
|
||||
<span class="footer-stat__label">访问</span>
|
||||
</span>
|
||||
<span class="footer-stat" th:if="${#lists.contains(items, 'upvote')}">
|
||||
<span class="footer-stat__value" th:text="${stats.upvote ?: 0}">0</span>
|
||||
<span class="footer-stat__label">点赞</span>
|
||||
</span>
|
||||
<span
|
||||
class="footer-stat"
|
||||
th:if="${#lists.contains(items, 'word') and pluginFinder.available('extra-api')}"
|
||||
>
|
||||
<span class="footer-stat__value" th:text="${extraApiStatsFinder.getPostWordCount()}">0</span>
|
||||
<span class="footer-stat__label">字数</span>
|
||||
</span>
|
||||
</div>
|
||||
<div th:if="${theme.config?.footer?.footer_custom_html}" th:utext="${theme.config?.footer?.footer_custom_html}" class="footer-custom"></div>
|
||||
<div class="footer-injected">
|
||||
<halo:footer />
|
||||
@@ -104,6 +160,51 @@ const today = new Date();
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.footer-mps a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
color: var(--ink-3);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.footer-mps a:hover {
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.footer-mps__icon {
|
||||
flex: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.footer-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem 1.25rem;
|
||||
width: 100%;
|
||||
font-size: 0.8rem;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
.footer-stat {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.footer-stat__value {
|
||||
font-weight: 600;
|
||||
color: var(--ink-2);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.footer-stat__label {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.footer-socials {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
---
|
||||
---
|
||||
|
||||
<style is:inline th:if="${theme.config?.hero?.hero_title_color_light != null and theme.config?.hero?.hero_title_color_light != ''}" th:utext="${' :root { --hero-title-text: ' + theme.config?.hero?.hero_title_color_light + '; } '}">/*hero-title-light*/</style>
|
||||
<style is:inline th:if="${theme.config?.hero?.hero_title_color_dark != null and theme.config?.hero?.hero_title_color_dark != ''}" th:utext="${' html.dark { --hero-title-text: ' + theme.config?.hero?.hero_title_color_dark + '; } '}">/*hero-title-dark*/</style>
|
||||
<style is:inline th:if="${theme.config?.hero?.hero_subtitle_color_light != null and theme.config?.hero?.hero_subtitle_color_light != ''}" th:utext="${' :root { --hero-subtitle-text: ' + theme.config?.hero?.hero_subtitle_color_light + '; } '}">/*hero-subtitle-light*/</style>
|
||||
<style is:inline th:if="${theme.config?.hero?.hero_subtitle_color_dark != null and theme.config?.hero?.hero_subtitle_color_dark != ''}" th:utext="${' html.dark { --hero-subtitle-text: ' + theme.config?.hero?.hero_subtitle_color_dark + '; } '}">/*hero-subtitle-dark*/</style>
|
||||
|
||||
<section
|
||||
class="hero"
|
||||
id="wi-hero"
|
||||
@@ -13,9 +18,24 @@
|
||||
<div class="hero__orb hero__orb--3"></div>
|
||||
<div
|
||||
class="hero__bg-image"
|
||||
th:if="${theme.config?.hero?.hero_background_image}"
|
||||
th:if="${!#strings.isEmpty(theme.config?.hero?.hero_background_image) and #strings.isEmpty(theme.config?.hero?.hero_background_image_dark)}"
|
||||
th:style="'background-image: url(' + ${theme.config?.hero?.hero_background_image} + ')'"
|
||||
></div>
|
||||
<div
|
||||
class="hero__bg-image"
|
||||
th:if="${#strings.isEmpty(theme.config?.hero?.hero_background_image) and !#strings.isEmpty(theme.config?.hero?.hero_background_image_dark)}"
|
||||
th:style="'background-image: url(' + ${theme.config?.hero?.hero_background_image_dark} + ')'"
|
||||
></div>
|
||||
<div
|
||||
class="hero__bg-image hero__bg-image--light"
|
||||
th:if="${!#strings.isEmpty(theme.config?.hero?.hero_background_image) and !#strings.isEmpty(theme.config?.hero?.hero_background_image_dark)}"
|
||||
th:style="'background-image: url(' + ${theme.config?.hero?.hero_background_image} + ')'"
|
||||
></div>
|
||||
<div
|
||||
class="hero__bg-image hero__bg-image--dark"
|
||||
th:if="${!#strings.isEmpty(theme.config?.hero?.hero_background_image) and !#strings.isEmpty(theme.config?.hero?.hero_background_image_dark)}"
|
||||
th:style="'background-image: url(' + ${theme.config?.hero?.hero_background_image_dark} + ')'"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="hero__blur-overlay" id="wi-hero-blur-overlay"></div>
|
||||
@@ -37,7 +57,7 @@
|
||||
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}"
|
||||
th:attr="data-api=${theme.config?.hero?.hero_hitokoto_api ?: 'https://v1.hitokoto.cn/'},data-categories=${theme.config?.hero?.hero_hitokoto_categories},data-auto-refresh=${theme.config?.hero?.hero_hitokoto_auto_refresh == true ? 'true' : 'false'},data-refresh-interval=${theme.config?.hero?.hero_hitokoto_refresh_interval ?: 10000}"
|
||||
></p>
|
||||
<p
|
||||
class="hero__description"
|
||||
@@ -46,7 +66,12 @@
|
||||
></p>
|
||||
</div>
|
||||
|
||||
<div class="hero__scroll-indicator" id="wi-hero-scroll-indicator" aria-hidden="true">
|
||||
<button
|
||||
class="hero__scroll-indicator"
|
||||
id="wi-hero-scroll-indicator"
|
||||
type="button"
|
||||
aria-label="向下浏览"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
@@ -56,53 +81,106 @@
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<script is:inline>
|
||||
<script is:inline th:inline="none">
|
||||
/* Hero 下滑箭头:平滑滚动到首屏内容区 */
|
||||
(function () {
|
||||
var indicator = document.getElementById("wi-hero-scroll-indicator");
|
||||
if (!indicator) return;
|
||||
|
||||
indicator.addEventListener("click", function () {
|
||||
var content = document.querySelector(".wi-home-content");
|
||||
var hero = document.getElementById("wi-hero");
|
||||
var top;
|
||||
|
||||
if (content) {
|
||||
top = content.getBoundingClientRect().top + window.pageYOffset;
|
||||
} else if (hero) {
|
||||
top = hero.offsetTop + hero.offsetHeight;
|
||||
} else {
|
||||
top = window.innerHeight;
|
||||
}
|
||||
|
||||
/* 预留固定导航栏的高度,避免内容被顶栏遮住 */
|
||||
var navbar = document.getElementById("wi-navbar");
|
||||
var offset = navbar ? navbar.offsetHeight + 8 : 8;
|
||||
|
||||
var reduce =
|
||||
window.matchMedia &&
|
||||
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
||||
window.scrollTo({
|
||||
top: Math.max(0, top - offset),
|
||||
behavior: reduce ? "auto" : "smooth",
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
(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";
|
||||
var autoRefresh = el.getAttribute("data-auto-refresh") === "true";
|
||||
var refreshInterval = parseInt(el.getAttribute("data-refresh-interval"), 10) || 10000;
|
||||
|
||||
if (categories) {
|
||||
try {
|
||||
var cats = JSON.parse(categories);
|
||||
if (Array.isArray(cats)) {
|
||||
cats.forEach(function (c) {
|
||||
url += "&c=" + encodeURIComponent(c);
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
function buildUrl() {
|
||||
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) {}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
el.style.opacity = "0.5";
|
||||
el.textContent = "\u2026";
|
||||
function load(initial) {
|
||||
if (initial) {
|
||||
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 = "";
|
||||
});
|
||||
return fetch(buildUrl())
|
||||
.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";
|
||||
// 自动刷新失败时保留原文案,避免闪烁成空白
|
||||
if (initial) el.textContent = "";
|
||||
});
|
||||
}
|
||||
|
||||
load(true);
|
||||
|
||||
// 自动刷新:仅在开启且间隔不小于 3 秒时启用,避免请求过于频繁
|
||||
if (autoRefresh && refreshInterval >= 3000) {
|
||||
setInterval(function () {
|
||||
load(false);
|
||||
}, refreshInterval);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -199,6 +277,16 @@
|
||||
opacity: 0.15;
|
||||
}
|
||||
|
||||
/* 深色模式背景图切换:未配置双图时不生效,保持单一背景图 */
|
||||
.hero__bg-image--dark,
|
||||
html.dark .hero__bg-image--light {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html.dark .hero__bg-image--dark {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hero__content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
@@ -216,14 +304,14 @@
|
||||
font-size: clamp(3rem, 8vw, 6rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.03em;
|
||||
color: var(--ink);
|
||||
color: var(--hero-title-text, var(--ink));
|
||||
line-height: var(--leading-tight);
|
||||
}
|
||||
|
||||
.hero__subtitle {
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-xl);
|
||||
color: var(--ink-2);
|
||||
color: var(--hero-subtitle-text, var(--ink-2));
|
||||
letter-spacing: 0.05em;
|
||||
line-height: var(--leading-normal);
|
||||
}
|
||||
@@ -243,9 +331,29 @@
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
color: var(--ink-3);
|
||||
cursor: pointer;
|
||||
animation: scrollHint 2s ease-in-out infinite;
|
||||
transition: opacity 0.4s ease;
|
||||
transition: opacity 0.4s ease, color 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.hero__scroll-indicator:hover {
|
||||
color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 10%, transparent);
|
||||
}
|
||||
|
||||
.hero__scroll-indicator:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.hero--no-breath .hero__orb {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
>
|
||||
<div class="wi-container">
|
||||
<div class="wi-section__header">
|
||||
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_latest_title ?: #messages.msg('common.latestPosts')}">Latest Posts</h2>
|
||||
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_latest_title ?: '最新文章'}">最新文章</h2>
|
||||
</div>
|
||||
<div
|
||||
class="wi-latest"
|
||||
@@ -58,7 +58,7 @@
|
||||
th:if="${posts.hasPrevious()}"
|
||||
th:href="@{${posts.prevUrl}}"
|
||||
class="wi-pagination__prev"
|
||||
th:text="${theme.config?.home?.home_label_newer ?: #messages.msg('common.newer')}"
|
||||
th:text="${theme.config?.home?.home_label_newer ?: '较新'}"
|
||||
>← Newer</a>
|
||||
<span
|
||||
class="wi-pagination__info"
|
||||
@@ -68,7 +68,7 @@
|
||||
th:if="${posts.hasNext()}"
|
||||
th:href="@{${posts.nextUrl}}"
|
||||
class="wi-pagination__next"
|
||||
th:text="${theme.config?.home?.home_label_older ?: #messages.msg('common.older')}"
|
||||
th:text="${theme.config?.home?.home_label_older ?: '较旧'}"
|
||||
>Older →</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
>
|
||||
<div class="wi-container">
|
||||
<div class="wi-section__header">
|
||||
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_message_wall_title ?: #messages.msg('messageboard.title')}">留言墙</h2>
|
||||
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_message_wall_title ?: '留言板'}">留言墙</h2>
|
||||
<span class="wi-section__accent"></span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -35,19 +35,19 @@
|
||||
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 ?: #messages.msg('common.search')}">Search</span>
|
||||
<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 ?: #messages.msg('common.themeSwitch')}">Switch Theme</span>
|
||||
<span class="wi-mobile-menu__theme-label" th:text="${theme.config?.basic?.label_theme_switch ?: '切换主题'}">切换主题</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script is:inline>
|
||||
<script is:inline th:inline="none">
|
||||
const menu = document.getElementById("wi-mobile-menu");
|
||||
const backdrop = menu?.querySelector(".wi-mobile-menu__backdrop");
|
||||
const closeBtn = menu?.querySelector(".wi-mobile-menu__close");
|
||||
|
||||
@@ -9,7 +9,7 @@ const pageConfig = findPageBySlug("moments");
|
||||
<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 ?: #messages.msg('moments.title')}">瞬间</h2>
|
||||
<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'}"
|
||||
@@ -29,7 +29,7 @@ const pageConfig = findPageBySlug("moments");
|
||||
</th:block>
|
||||
</div>
|
||||
<a th:href="@{'/moments'}" class="wi-moments-section__more">
|
||||
<span th:text="${theme.config?.home?.home_label_view_all ?: #messages.msg('common.viewAll')}">查看全部</span>
|
||||
<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>
|
||||
|
||||
@@ -4,18 +4,22 @@ import ThemeSwitcher from "./ThemeSwitcher.astro";
|
||||
|
||||
<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="@{/}">
|
||||
<a
|
||||
class="wi-navbar__brand"
|
||||
th:href="@{/}"
|
||||
th:with="brandType=${theme.config?.navbar?.navbar_brand_type}, logoUrl=${!#strings.isEmpty(theme.config?.navbar?.navbar_logo) ? theme.config?.navbar?.navbar_logo : theme.config?.basic?.logo}, logoRadius=${theme.config?.navbar?.navbar_logo_radius}"
|
||||
>
|
||||
<img
|
||||
th:if="${theme.config?.basic?.logo}"
|
||||
th:src="${theme.config?.basic?.logo}"
|
||||
th:if="${brandType == 'logo' and !#strings.isEmpty(logoUrl)}"
|
||||
th:src="${logoUrl}"
|
||||
alt=""
|
||||
class="wi-navbar__logo"
|
||||
th:class="${'wi-navbar__logo wi-navbar__logo--' + (logoRadius != null ? logoRadius : 'rounded')}"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
onerror="this.style.display='none';this.nextElementSibling&&this.nextElementSibling.classList.add('wi-navbar__title')"
|
||||
onerror="this.style.display='none'"
|
||||
/>
|
||||
<span
|
||||
th:if="${#strings.isEmpty(theme.config?.basic?.logo)}"
|
||||
th:if="${brandType != 'logo' or #strings.isEmpty(logoUrl)}"
|
||||
class="wi-navbar__title"
|
||||
th:text="${site.title}"
|
||||
></span>
|
||||
@@ -47,14 +51,14 @@ import ThemeSwitcher from "./ThemeSwitcher.astro";
|
||||
th:if="${pluginFinder.available('PluginSearchWidget') and theme.config?.navbar?.navbar_show_search != false}"
|
||||
class="wi-navbar__action-btn wi-navbar__search-btn"
|
||||
type="button"
|
||||
th:aria-label="${theme.config?.basic?.label_search ?: #messages.msg('common.search')}"
|
||||
th:aria-label="${theme.config?.basic?.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}">
|
||||
<span th:attr="data-label=${theme.config?.basic?.label_theme_switch ?: #messages.msg('common.themeSwitch')}" style="display:inline-flex">
|
||||
<span th:attr="data-label=${theme.config?.basic?.label_theme_switch ?: '切换主题'}" style="display:inline-flex">
|
||||
<ThemeSwitcher />
|
||||
</span>
|
||||
</th:block>
|
||||
@@ -70,7 +74,7 @@ import ThemeSwitcher from "./ThemeSwitcher.astro";
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<script is:inline>
|
||||
<script is:inline th:inline="none">
|
||||
const navbar = document.getElementById("wi-navbar");
|
||||
let lastScrollY = window.scrollY;
|
||||
let ticking = false;
|
||||
@@ -218,6 +222,18 @@ import ThemeSwitcher from "./ThemeSwitcher.astro";
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.wi-navbar__logo--square {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.wi-navbar__logo--rounded {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.wi-navbar__logo--circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.wi-navbar__title {
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 700;
|
||||
|
||||
@@ -9,7 +9,7 @@ const pageConfig = findPageBySlug("photos");
|
||||
<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 ?: #messages.msg('photos.title')}">图库</h2>
|
||||
<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}"
|
||||
@@ -37,7 +37,7 @@ const pageConfig = findPageBySlug("photos");
|
||||
</th:block>
|
||||
</div>
|
||||
<a th:href="@{'/photos'}" class="wi-photos-section__more">
|
||||
<span th:text="${theme.config?.home?.home_label_view_all ?: #messages.msg('common.viewAll')}">查看全部</span>
|
||||
<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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
---
|
||||
|
||||
<script is:inline>
|
||||
<script is:inline th:inline="none">
|
||||
(function(){
|
||||
var enabled = !document.documentElement.classList.contains("wi-no-animations");
|
||||
if (!enabled) {
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
<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>
|
||||
@@ -24,7 +24,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<script is:inline>
|
||||
<script is:inline th:inline="none">
|
||||
(function(){
|
||||
var btn = document.getElementById("wi-theme-toggle");
|
||||
if (!btn) return;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
>
|
||||
<div class="wi-container">
|
||||
<div class="wi-section__header">
|
||||
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_timeline_title ?: #messages.msg('common.timeline')}">时间线</h2>
|
||||
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_timeline_title ?: '时间线'}">时间线</h2>
|
||||
<span class="wi-section__accent"></span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
---
|
||||
---
|
||||
|
||||
<section
|
||||
class="wi-welcome-popup"
|
||||
id="wi-welcome-popup"
|
||||
th:if="${theme.config?.welcome_popup?.welcome_popup_enabled == true}"
|
||||
th:attr="data-position=${theme.config?.welcome_popup?.welcome_popup_position ?: 'top-center'},data-duration=${theme.config?.welcome_popup?.welcome_popup_duration ?: 5},data-api=${theme.config?.welcome_popup?.welcome_popup_api},data-fallback-api=${theme.config?.welcome_popup?.welcome_popup_fallback_api},data-show-once=${theme.config?.welcome_popup?.welcome_popup_show_once != false ? 'true' : 'false'}"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>
|
||||
<div class="wi-welcome-popup__inner">
|
||||
<div class="wi-welcome-popup__icon" aria-hidden="true">
|
||||
<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">
|
||||
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"></path>
|
||||
<circle cx="12" cy="10" r="3"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="wi-welcome-popup__content" id="wi-welcome-popup-text">加载中…</div>
|
||||
<button class="wi-welcome-popup__close" type="button" aria-label="关闭">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 6 6 18"></path>
|
||||
<path d="m6 6 12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="wi-welcome-popup__progress" id="wi-welcome-popup-progress" aria-hidden="true"></div>
|
||||
</section>
|
||||
|
||||
<script is:inline th:inline="none">
|
||||
(function () {
|
||||
var popup = document.getElementById("wi-welcome-popup");
|
||||
if (!popup) return;
|
||||
|
||||
/* 同会话已显示过则不再弹出(可通过"同会话仅显示一次"开关关闭) */
|
||||
var sessionKey = "wi-welcome-shown";
|
||||
var showOnce = popup.getAttribute("data-show-once") !== "false";
|
||||
try {
|
||||
if (showOnce && sessionStorage.getItem(sessionKey)) {
|
||||
popup.style.display = "none";
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
var textEl = document.getElementById("wi-welcome-popup-text");
|
||||
var closeBtn = popup.querySelector(".wi-welcome-popup__close");
|
||||
var progress = document.getElementById("wi-welcome-popup-progress");
|
||||
var duration = parseInt(popup.getAttribute("data-duration"), 10) || 5;
|
||||
var api = popup.getAttribute("data-api") || "https://myip.ipip.net/json";
|
||||
var fallbackApi = popup.getAttribute("data-fallback-api") || "https://r.inews.qq.com/api/ip2city?otype=json";
|
||||
|
||||
var hideTimer = null;
|
||||
var progressRaf = null;
|
||||
var progressStart = 0;
|
||||
var isHidden = false;
|
||||
|
||||
function hidePopup() {
|
||||
if (isHidden) return;
|
||||
isHidden = true;
|
||||
popup.classList.remove("wi-welcome-popup--visible");
|
||||
popup.classList.add("wi-welcome-popup--leaving");
|
||||
if (hideTimer) {
|
||||
clearTimeout(hideTimer);
|
||||
hideTimer = null;
|
||||
}
|
||||
if (progressRaf) {
|
||||
cancelAnimationFrame(progressRaf);
|
||||
progressRaf = null;
|
||||
}
|
||||
try {
|
||||
if (showOnce) sessionStorage.setItem(sessionKey, "1");
|
||||
} catch (e) {}
|
||||
setTimeout(function () {
|
||||
popup.style.display = "none";
|
||||
}, 400);
|
||||
}
|
||||
|
||||
function startAutoHide() {
|
||||
progressStart = performance.now();
|
||||
var step = function (now) {
|
||||
var elapsed = (now - progressStart) / 1000;
|
||||
var ratio = Math.min(elapsed / duration, 1);
|
||||
if (progress) {
|
||||
progress.style.transform = "scaleX(" + (1 - ratio) + ")";
|
||||
}
|
||||
if (ratio < 1 && !isHidden) {
|
||||
progressRaf = requestAnimationFrame(step);
|
||||
}
|
||||
};
|
||||
progressRaf = requestAnimationFrame(step);
|
||||
hideTimer = setTimeout(hidePopup, duration * 1000);
|
||||
}
|
||||
|
||||
function renderMessage(location) {
|
||||
var loc = (location || "").trim();
|
||||
if (!loc) loc = "未知星球";
|
||||
if (textEl) {
|
||||
textEl.textContent = "欢迎来自 " + loc + " 的朋友";
|
||||
}
|
||||
popup.classList.add("wi-welcome-popup--visible");
|
||||
startAutoHide();
|
||||
}
|
||||
|
||||
function fetchLocation() {
|
||||
if (textEl) {
|
||||
textEl.textContent = "加载中…";
|
||||
}
|
||||
popup.classList.add("wi-welcome-popup--visible");
|
||||
|
||||
/* 按顺序尝试主 API -> 备选 API */
|
||||
var endpoints = [];
|
||||
if (api) endpoints.push(api);
|
||||
if (fallbackApi && fallbackApi !== api) endpoints.push(fallbackApi);
|
||||
|
||||
if (endpoints.length === 0) {
|
||||
renderMessage("");
|
||||
return;
|
||||
}
|
||||
|
||||
var idx = 0;
|
||||
|
||||
function tryNext() {
|
||||
if (idx >= endpoints.length) {
|
||||
renderMessage("");
|
||||
return;
|
||||
}
|
||||
var url = endpoints[idx++];
|
||||
fetchSingle(url, 6000)
|
||||
.then(function (location) {
|
||||
if (location) {
|
||||
renderMessage(location);
|
||||
} else {
|
||||
tryNext();
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
tryNext();
|
||||
});
|
||||
}
|
||||
|
||||
tryNext();
|
||||
}
|
||||
|
||||
function fetchSingle(url, timeoutMs) {
|
||||
var controller = null;
|
||||
var timeoutId = null;
|
||||
|
||||
if (window.AbortController) {
|
||||
controller = new AbortController();
|
||||
timeoutId = setTimeout(function () {
|
||||
controller.abort();
|
||||
}, timeoutMs);
|
||||
}
|
||||
|
||||
return fetch(url, controller ? { signal: controller.signal } : {})
|
||||
.then(function (res) {
|
||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||
return res.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
return parseLocation(data);
|
||||
})
|
||||
.catch(function (err) {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
/* 解析多种 IP API 的返回格式:
|
||||
- IPIP: {"ret":"ok","data":{"location":["中国","河北","张家口","","联通"]}}
|
||||
- 腾讯 ip2city: {"ret":0,"province":"河北","city":"张家口",...}
|
||||
- ipapi.co / ip-api.com: {"country":"China","regionName":"Hebei","city":"Zhangjiakou"}
|
||||
- 太平洋: {"pro":"广东省","city":"广州市",...} */
|
||||
function parseLocation(data) {
|
||||
if (!data) return "";
|
||||
|
||||
/* IPIP: data.data.location 数组 */
|
||||
if (data.data && Array.isArray(data.data.location)) {
|
||||
var loc = data.data.location
|
||||
.filter(function (s) {
|
||||
return s && String(s).trim();
|
||||
})
|
||||
.map(function (s) {
|
||||
return String(s).trim();
|
||||
});
|
||||
/* 去掉末尾的运营商字段(如"联通""电信""移动") */
|
||||
var ispKeywords = ["联通", "电信", "移动", "铁通", "广电", "教育网", "长城"];
|
||||
while (loc.length > 1 && ispKeywords.indexOf(loc[loc.length - 1]) !== -1) {
|
||||
loc.pop();
|
||||
}
|
||||
return loc.length ? loc.join("·") : "";
|
||||
}
|
||||
|
||||
var country = data.country || data.country_name || data.nation || "";
|
||||
var region = data.regionName || data.region || data.province || data.pro || "";
|
||||
var city = data.city || data.cityName || "";
|
||||
var district = data.district || data.area || data.info3 || "";
|
||||
|
||||
/* 短英文国家代码(如 CN)舍弃,避免显示无意义的两字母 */
|
||||
if (country && country.length <= 3 && !/[一-龥]/.test(country)) {
|
||||
country = "";
|
||||
}
|
||||
|
||||
var parts = [];
|
||||
if (country) parts.push(country);
|
||||
if (region && region !== country) parts.push(region);
|
||||
if (city && city !== region) parts.push(city);
|
||||
if (district && district !== city) parts.push(district);
|
||||
|
||||
return parts.length ? parts.join("·") : "";
|
||||
}
|
||||
|
||||
/* 交互:悬停/触摸暂停倒计时,离开恢复 */
|
||||
function pauseTimer() {
|
||||
if (hideTimer) {
|
||||
clearTimeout(hideTimer);
|
||||
hideTimer = null;
|
||||
}
|
||||
if (progressRaf) {
|
||||
cancelAnimationFrame(progressRaf);
|
||||
progressRaf = null;
|
||||
}
|
||||
}
|
||||
|
||||
function resumeTimer() {
|
||||
var remaining = duration;
|
||||
if (progress) {
|
||||
var matrix = progress.style.transform;
|
||||
var match = matrix && matrix.match(/scaleX\(([\d.]+)\)/);
|
||||
if (match) {
|
||||
var ratio = parseFloat(match[1]);
|
||||
remaining = duration * ratio;
|
||||
}
|
||||
}
|
||||
progressStart = performance.now() - (duration - remaining) * 1000;
|
||||
var step = function (now) {
|
||||
var elapsed = (now - progressStart) / 1000;
|
||||
var ratio = Math.min(elapsed / duration, 1);
|
||||
if (progress) {
|
||||
progress.style.transform = "scaleX(" + (1 - ratio) + ")";
|
||||
}
|
||||
if (ratio < 1 && !isHidden) {
|
||||
progressRaf = requestAnimationFrame(step);
|
||||
}
|
||||
};
|
||||
progressRaf = requestAnimationFrame(step);
|
||||
hideTimer = setTimeout(hidePopup, remaining * 1000);
|
||||
}
|
||||
|
||||
popup.addEventListener("mouseenter", pauseTimer);
|
||||
popup.addEventListener("mouseleave", resumeTimer);
|
||||
popup.addEventListener("touchstart", pauseTimer, { passive: true });
|
||||
popup.addEventListener("touchend", resumeTimer, { passive: true });
|
||||
|
||||
if (closeBtn) closeBtn.addEventListener("click", hidePopup);
|
||||
|
||||
/* 延迟 800ms 显示,避免与首屏渲染争抢资源 */
|
||||
setTimeout(fetchLocation, 800);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.wi-welcome-popup {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
max-width: min(380px, calc(100vw - 32px));
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transform: translateY(-12px);
|
||||
transition: opacity 0.4s var(--ease-out-expo), transform 0.4s var(--ease-out-expo);
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
.wi-welcome-popup[data-position="top-left"] {
|
||||
top: 88px;
|
||||
left: 24px;
|
||||
}
|
||||
|
||||
.wi-welcome-popup[data-position="top-right"] {
|
||||
top: 88px;
|
||||
right: 24px;
|
||||
}
|
||||
|
||||
.wi-welcome-popup[data-position="bottom-left"] {
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
|
||||
.wi-welcome-popup[data-position="bottom-right"] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
|
||||
.wi-welcome-popup[data-position="top-center"] {
|
||||
top: 88px;
|
||||
left: 50%;
|
||||
transform: translate(-50%) translateY(-12px);
|
||||
}
|
||||
|
||||
.wi-welcome-popup--visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.wi-welcome-popup--visible[data-position="top-left"],
|
||||
.wi-welcome-popup--visible[data-position="top-right"],
|
||||
.wi-welcome-popup--visible[data-position="bottom-left"],
|
||||
.wi-welcome-popup--visible[data-position="bottom-right"] {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.wi-welcome-popup--visible[data-position="top-center"] {
|
||||
transform: translate(-50%) translateY(0);
|
||||
}
|
||||
|
||||
.wi-welcome-popup--leaving {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wi-welcome-popup--leaving[data-position^="top"] {
|
||||
transform: translateY(-12px);
|
||||
}
|
||||
|
||||
.wi-welcome-popup--leaving[data-position="bottom-left"],
|
||||
.wi-welcome-popup--leaving[data-position="bottom-right"] {
|
||||
transform: translateY(12px);
|
||||
}
|
||||
|
||||
.wi-welcome-popup--leaving[data-position="top-center"] {
|
||||
transform: translate(-50%) translateY(-12px);
|
||||
}
|
||||
|
||||
.wi-welcome-popup__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 9999px;
|
||||
background: color-mix(in srgb, var(--bg-raised) 92%, transparent);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid var(--rule);
|
||||
box-shadow: 0 8px 24px -8px #0000001f, 0 2px 8px -2px #0000000f;
|
||||
color: var(--ink);
|
||||
font-size: var(--text-sm);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.wi-welcome-popup__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.wi-welcome-popup__icon svg {
|
||||
animation: wi-welcome-popup-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.wi-welcome-popup__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wi-welcome-popup__close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
color: var(--ink-3);
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s ease, background 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.wi-welcome-popup__close:hover {
|
||||
opacity: 1;
|
||||
background: color-mix(in srgb, var(--ink-3) 12%, transparent);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.wi-welcome-popup__progress {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
right: 14px;
|
||||
bottom: 4px;
|
||||
height: 2px;
|
||||
border-radius: 1px;
|
||||
background: color-mix(in srgb, var(--accent) 30%, transparent);
|
||||
transform-origin: left center;
|
||||
transform: scaleX(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.wi-welcome-popup--visible .wi-welcome-popup__progress {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes wi-welcome-popup-pulse {
|
||||
0%,
|
||||
to {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.12);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.wi-welcome-popup[data-position] {
|
||||
max-width: calc(100vw - 24px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wi-welcome-popup,
|
||||
.wi-welcome-popup--visible,
|
||||
.wi-welcome-popup--leaving,
|
||||
.wi-welcome-popup__icon svg {
|
||||
transition: opacity 0.2s ease !important;
|
||||
transform: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+28
-38
@@ -5,19 +5,30 @@ import Footer from "../components/Footer.astro";
|
||||
import ScrollReveal from "../components/ScrollReveal.astro";
|
||||
import CursorGlow from "../components/CursorGlow.astro";
|
||||
import MobileMenu from "../components/MobileMenu.astro";
|
||||
import ClickEffect from "../components/ClickEffect.astro";
|
||||
|
||||
interface Props {
|
||||
pageTitle?: string;
|
||||
contentClass?: string;
|
||||
wide?: boolean;
|
||||
home?: boolean;
|
||||
/** 渲染为 Halo 页面布局契约(templates/layout.html 的 html(head, content) 片段) */
|
||||
layoutFragment?: boolean;
|
||||
}
|
||||
|
||||
const { pageTitle, contentClass, wide, home } = Astro.props;
|
||||
const { pageTitle, contentClass, wide, home, layoutFragment = false } = Astro.props;
|
||||
const fragmentAttr = layoutFragment ? "html (head, content)" : undefined;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html class={home ? 'wi-home-snap' : ''} style="overflow-x:hidden" th:lang="${#locale.toLanguageTag()}" th:classappend="${theme.config?.animation?.animation_enabled == false} ? 'wi-no-animations'" th:attr="data-lang=${theme.config?.basic?.language}">
|
||||
<html
|
||||
lang="zh-CN"
|
||||
class={home ? 'wi-home-snap' : ''}
|
||||
style="overflow-x:hidden"
|
||||
th:classappend="${theme.config?.animation?.animation_enabled == false} ? 'wi-no-animations'"
|
||||
xmlns:th={layoutFragment ? "https://www.thymeleaf.org" : undefined}
|
||||
th:fragment={fragmentAttr}
|
||||
>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
@@ -73,47 +84,31 @@ const { pageTitle, contentClass, wide, home } = Astro.props;
|
||||
<style is:inline th:if="${theme.config?.style?.layout_container_width == 'medium' or theme.config?.style?.layout_container_width == null}" th:utext="${'.wi-content-wrap { max-width: 800px; } .wi-content-wrap--wide { max-width: 1200px; } .wi-main--wide { max-width: 1400px; }'}">/*width*/</style>
|
||||
<style is:inline th:if="${theme.config?.style?.layout_container_width == 'wide'}" th:utext="${'.wi-content-wrap { max-width: 900px; } .wi-content-wrap--wide { max-width: 1400px; } .wi-main--wide { max-width: 1600px; }'}">/*width*/</style>
|
||||
<style is:inline th:if="${theme.config?.style?.custom_css}" th:utext="${theme.config?.style?.custom_css}">/*custom*/</style>
|
||||
{
|
||||
layoutFragment && (
|
||||
<title th:if="${head == null}" th:text="${site.title}"></title>
|
||||
<th:block th:if="${head != null}">
|
||||
<th:block th:replace="${head}" />
|
||||
</th:block>
|
||||
)
|
||||
}
|
||||
<slot name="head" />
|
||||
</head>
|
||||
<body class="wi-body">
|
||||
<script is:inline>
|
||||
<script is:inline th:inline="none">
|
||||
window.addEventListener("unhandledrejection", function (e) {
|
||||
if (e.reason && e.reason.message && e.reason.message.indexOf("Could not establish connection") !== -1) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script is:inline>
|
||||
(function(){
|
||||
try{
|
||||
var lang=document.documentElement.getAttribute("data-lang");
|
||||
if(!lang||lang==="auto"||lang==="null"||lang===""){
|
||||
var cookies=document.cookie.split(";");
|
||||
for(var i=0;i<cookies.length;i++){
|
||||
var c=cookies[i].trim();
|
||||
if(c.indexOf("language=")===0){
|
||||
document.cookie="language=;path=/;max-age=0;SameSite=Lax";
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
var tag=lang.replace(/_/g,"-");
|
||||
var cookies=document.cookie.split(";");
|
||||
var current="";
|
||||
for(var i=0;i<cookies.length;i++){
|
||||
var c=cookies[i].trim();
|
||||
if(c.indexOf("language=")===0){current=c.substring(9);break;}
|
||||
}
|
||||
if(current!==tag){
|
||||
document.cookie="language="+tag+";path=/;max-age=31536000;SameSite=Lax";
|
||||
}
|
||||
}catch(e){}
|
||||
})();
|
||||
</script>
|
||||
<Navbar />
|
||||
<main class:list={["wi-main", { "wi-main--wide": wide || home, "wi-main--home": home }]}>
|
||||
{home ? (
|
||||
{layoutFragment ? (
|
||||
<div class:list={["wi-content-wrap", { "wi-content-wrap--wide": wide }]}>
|
||||
<th:block th:replace="${content}" />
|
||||
</div>
|
||||
) : home ? (
|
||||
<slot />
|
||||
) : (
|
||||
<div class:list={["wi-content-wrap", { "wi-content-wrap--wide": wide }]}>
|
||||
@@ -125,6 +120,7 @@ const { pageTitle, contentClass, wide, home } = Astro.props;
|
||||
<MobileMenu />
|
||||
<ScrollReveal th:if="${theme.config?.animation?.animation_enabled != false and theme.config?.animation?.animation_scroll_reveal != false}" />
|
||||
<CursorGlow th:if="${theme.config?.animation?.animation_enabled != false and theme.config?.animation?.animation_cursor_glow == true}" />
|
||||
<ClickEffect />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -169,10 +165,4 @@ html.wi-no-animations .wi-reveal {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.wi-post__content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -4,14 +4,14 @@ import Layout from "../layouts/Layout.astro";
|
||||
|
||||
<Layout>
|
||||
<Fragment slot="head">
|
||||
<title th:text="|${theme.config?.basic?.label_archives_title ?: #messages.msg('archives.title')} - ${site.title}|"></title>
|
||||
<title th:text="|${theme.config?.basic?.label_archives_title ?: '归档'} - ${site.title}|"></title>
|
||||
</Fragment>
|
||||
|
||||
<div class="wi-archives">
|
||||
<header class="wi-archives__header">
|
||||
<h1 class="wi-archives__title" th:text="${theme.config?.basic?.label_archives_title ?: #messages.msg('archives.title')}">归档</h1>
|
||||
<h1 class="wi-archives__title" th:text="${theme.config?.basic?.label_archives_title ?: '归档'}">归档</h1>
|
||||
<span class="wi-page-accent"></span>
|
||||
<p class="wi-archives__subtitle" th:text="${#strings.replace(#messages.msg('common.postCount'), '{0}', #strings.toString(archives.total))}"></p>
|
||||
<p class="wi-archives__subtitle" th:text="|共 ${archives.total} 篇文章|">共 0 篇文章</p>
|
||||
</header>
|
||||
|
||||
<section class="wi-archives__timeline" aria-label="Archive timeline">
|
||||
@@ -57,7 +57,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
<div
|
||||
th:if="${archives.total == 0}"
|
||||
class="wi-archives__empty"
|
||||
th:text="${theme.config?.home?.home_label_no_posts ?: #messages.msg('common.noPosts')}"
|
||||
th:text="${theme.config?.home?.home_label_no_posts ?: '暂无文章'}"
|
||||
>No posts yet.</div>
|
||||
</section>
|
||||
|
||||
@@ -70,7 +70,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
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 ?: #messages.msg('common.newer')}"
|
||||
th:text="${theme.config?.home?.home_label_newer ?: '较新'}"
|
||||
>← Newer</a>
|
||||
<span
|
||||
class="wi-archives__page-info"
|
||||
@@ -80,7 +80,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
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 ?: #messages.msg('common.older')}"
|
||||
th:text="${theme.config?.home?.home_label_older ?: '较旧'}"
|
||||
>Older →</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -4,14 +4,14 @@ import Layout from "../layouts/Layout.astro";
|
||||
|
||||
<Layout>
|
||||
<Fragment slot="head">
|
||||
<title th:text="|#{categories.title} - ${site.title}|"></title>
|
||||
<title th:text="|分类 - ${site.title}|"></title>
|
||||
</Fragment>
|
||||
|
||||
<div class="wi-categories">
|
||||
<header class="wi-categories__header">
|
||||
<h1 class="wi-categories__title" th:text="#{categories.title}">分类</h1>
|
||||
<h1 class="wi-categories__title" th:text="'分类'">分类</h1>
|
||||
<span class="wi-page-accent"></span>
|
||||
<p class="wi-categories__subtitle" th:text="${#messages.msg('common.categoryCount', categories.size())}"></p>
|
||||
<p class="wi-categories__subtitle" th:text="|共 ${categories.size()} 个分类|">共 0 个分类</p>
|
||||
</header>
|
||||
|
||||
<nav class="wi-categories__list" aria-label="Category list">
|
||||
|
||||
@@ -14,7 +14,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
th:text="${category.spec.displayName}"
|
||||
></h1>
|
||||
<span class="wi-page-accent"></span>
|
||||
<p class="wi-category__subtitle" th:text="#{common.postCount(${posts.total})}"></p>
|
||||
<p class="wi-category__subtitle" th:text="|共 ${posts.total} 篇文章|">共 0 篇文章</p>
|
||||
<p
|
||||
th:if="${not #strings.isEmpty(category.spec.description)}"
|
||||
class="wi-category__description"
|
||||
@@ -58,7 +58,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
<div
|
||||
th:if="${posts.total == 0}"
|
||||
class="wi-category__empty"
|
||||
th:text="${theme.config?.home?.home_label_no_posts ?: #messages.msg('common.noPosts')}"
|
||||
th:text="${theme.config?.home?.home_label_no_posts ?: '暂无文章'}"
|
||||
>No posts yet.</div>
|
||||
|
||||
<nav
|
||||
@@ -70,7 +70,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
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 ?: #messages.msg('common.newer')}"
|
||||
th:text="${theme.config?.home?.home_label_newer ?: '较新'}"
|
||||
>← Newer</a>
|
||||
<span
|
||||
class="wi-category__page-info"
|
||||
@@ -80,7 +80,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
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 ?: #messages.msg('common.older')}"
|
||||
th:text="${theme.config?.home?.home_label_older ?: '较旧'}"
|
||||
>Older →</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
+13
-13
@@ -10,7 +10,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
<section class="wi-douban">
|
||||
<div class="wi-container">
|
||||
<div class="wi-douban__header">
|
||||
<h1 class="wi-douban__title" th:text="${title ?: #messages.msg('douban.title')}">Douban</h1>
|
||||
<h1 class="wi-douban__title" th:text="${title ?: '豆瓣'}">豆瓣</h1>
|
||||
<span class="wi-page-accent"></span>
|
||||
</div>
|
||||
|
||||
@@ -32,63 +32,63 @@ import Layout from "../layouts/Layout.astro";
|
||||
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
|
||||
th:href="@{/douban(type=${currentType},status='wish')}"
|
||||
th:classappend="${hasStatus and currentStatus == 'wish'} ? 'wi-douban__filter-tag--active' : ''"
|
||||
th:text="#{douban.wish}">Wish</a>
|
||||
th:text="'想看'">想看</a>
|
||||
<a
|
||||
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
|
||||
th:href="@{/douban(type=${currentType},status='done')}"
|
||||
th:classappend="${(!hasStatus and isDefaultType) or (hasStatus and currentStatus == 'done')} ? 'wi-douban__filter-tag--active' : ''"
|
||||
th:text="#{douban.done}">Done</a>
|
||||
th:text="'看过'">看过</a>
|
||||
</th:block>
|
||||
<th:block th:if="${currentType == 'book'}">
|
||||
<a
|
||||
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
|
||||
th:href="@{/douban(type='book',status='wish')}"
|
||||
th:classappend="${hasStatus and currentStatus == 'wish'} ? 'wi-douban__filter-tag--active' : ''"
|
||||
th:text="#{douban.wishRead}">Wish</a>
|
||||
th:text="'想读'">想读</a>
|
||||
<a
|
||||
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
|
||||
th:href="@{/douban(type='book',status='doing')}"
|
||||
th:classappend="${hasStatus and currentStatus == 'doing'} ? 'wi-douban__filter-tag--active' : ''"
|
||||
th:text="#{douban.reading}">Reading</a>
|
||||
th:text="'在读'">在读</a>
|
||||
<a
|
||||
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
|
||||
th:href="@{/douban(type='book',status='done')}"
|
||||
th:classappend="${(!hasStatus and isDefaultType) or (hasStatus and currentStatus == 'done')} ? 'wi-douban__filter-tag--active' : ''"
|
||||
th:text="#{douban.done}">Done</a>
|
||||
th:text="'看过'">看过</a>
|
||||
</th:block>
|
||||
<th:block th:if="${currentType == 'music'}">
|
||||
<a
|
||||
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
|
||||
th:href="@{/douban(type='music',status='wish')}"
|
||||
th:classappend="${hasStatus and currentStatus == 'wish'} ? 'wi-douban__filter-tag--active' : ''"
|
||||
th:text="#{douban.wishListen}">Wish</a>
|
||||
th:text="'想听'">想听</a>
|
||||
<a
|
||||
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
|
||||
th:href="@{/douban(type='music',status='doing')}"
|
||||
th:classappend="${hasStatus and currentStatus == 'doing'} ? 'wi-douban__filter-tag--active' : ''"
|
||||
th:text="#{douban.listening}">Listening</a>
|
||||
th:text="'在听'">在听</a>
|
||||
<a
|
||||
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
|
||||
th:href="@{/douban(type='music',status='done')}"
|
||||
th:classappend="${(!hasStatus and isDefaultType) or (hasStatus and currentStatus == 'done')} ? 'wi-douban__filter-tag--active' : ''"
|
||||
th:text="#{douban.listened}">Listened</a>
|
||||
th:text="'听过'">听过</a>
|
||||
</th:block>
|
||||
<th:block th:if="${currentType == 'game'}">
|
||||
<a
|
||||
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
|
||||
th:href="@{/douban(type='game',status='wish')}"
|
||||
th:classappend="${hasStatus and currentStatus == 'wish'} ? 'wi-douban__filter-tag--active' : ''"
|
||||
th:text="#{douban.wishPlay}">Wish</a>
|
||||
th:text="'想玩'">想玩</a>
|
||||
<a
|
||||
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
|
||||
th:href="@{/douban(type='game',status='doing')}"
|
||||
th:classappend="${hasStatus and currentStatus == 'doing'} ? 'wi-douban__filter-tag--active' : ''"
|
||||
th:text="#{douban.playing}">Playing</a>
|
||||
th:text="'在玩'">在玩</a>
|
||||
<a
|
||||
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
|
||||
th:href="@{/douban(type='game',status='done')}"
|
||||
th:classappend="${(!hasStatus and isDefaultType) or (hasStatus and currentStatus == 'done')} ? 'wi-douban__filter-tag--active' : ''"
|
||||
th:text="#{douban.played}">Played</a>
|
||||
th:text="'玩过'">玩过</a>
|
||||
</th:block>
|
||||
</div>
|
||||
|
||||
@@ -162,7 +162,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
class="wi-douban__my-score"
|
||||
th:if="${!#strings.isEmpty(item.faves?.score)}"
|
||||
>
|
||||
<span th:text="#{douban.myRating}">My:</span><th:block th:text="${item.faves?.score}"></th:block>
|
||||
<span th:text="'我的:'">My:</span><th:block th:text="${item.faves?.score}"></th:block>
|
||||
</span>
|
||||
<span
|
||||
class="wi-douban__year"
|
||||
|
||||
@@ -4,7 +4,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
|
||||
<Layout wide>
|
||||
<Fragment slot="head">
|
||||
<title th:text="|${theme.config?.friends?.friends_page_title ?: #messages.msg('friends.title')} - ${site.title}|"></title>
|
||||
<title th:text="|${theme.config?.friends?.friends_page_title ?: '友人圈'} - ${site.title}|"></title>
|
||||
</Fragment>
|
||||
|
||||
<section
|
||||
@@ -15,7 +15,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
<div class="wi-friends-page__header">
|
||||
<h1
|
||||
class="wi-friends-page__title"
|
||||
th:text="${theme.config?.friends?.friends_page_title ?: #messages.msg('friends.title')}"
|
||||
th:text="${theme.config?.friends?.friends_page_title ?: '友人圈'}"
|
||||
>
|
||||
Friends Circle
|
||||
</h1>
|
||||
@@ -87,7 +87,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
th:if="${friends.hasPrevious()}"
|
||||
th:href="@{${friends.prevUrl}}"
|
||||
class="wi-friends-page__page-link"
|
||||
th:text="#{common.newer}"
|
||||
th:text="'较新'"
|
||||
>
|
||||
← Newer
|
||||
</a>
|
||||
@@ -99,7 +99,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
th:if="${friends.hasNext()}"
|
||||
th:href="@{${friends.nextUrl}}"
|
||||
class="wi-friends-page__page-link"
|
||||
th:text="#{common.older}"
|
||||
th:text="'较旧'"
|
||||
>
|
||||
Older →
|
||||
</a>
|
||||
@@ -113,11 +113,11 @@ import Layout from "../layouts/Layout.astro";
|
||||
>
|
||||
<div class="wi-container">
|
||||
<div class="wi-friends-page__header">
|
||||
<h1 class="wi-friends-page__title" th:text="#{friends.title}">Friends Circle</h1>
|
||||
<h1 class="wi-friends-page__title" th:text="'友人圈'">友人圈</h1>
|
||||
<span class="wi-page-accent"></span>
|
||||
</div>
|
||||
<div class="wi-friends-page__empty">
|
||||
<p th:utext="#{common.installPlugin('Friends Circle', 'https://www.halo.run/store/apps/app-friends')}">This feature requires the Friends Circle plugin</p>
|
||||
<p>该功能需要安装「朋友圈」插件,<a href="https://www.halo.run/store/apps/app-yISsV" target="_blank" rel="noopener noreferrer">前往应用市场安装</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+18
-15
@@ -1,6 +1,7 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import HeroSection from "../components/HeroSection.astro";
|
||||
import WelcomePopup from "../components/WelcomePopup.astro";
|
||||
---
|
||||
|
||||
<Layout home>
|
||||
@@ -8,12 +9,13 @@ import HeroSection from "../components/HeroSection.astro";
|
||||
<title th:text="${site.title}"></title>
|
||||
</Fragment>
|
||||
|
||||
<WelcomePopup />
|
||||
|
||||
<HeroSection />
|
||||
|
||||
<div
|
||||
class="wi-home-content"
|
||||
th:classappend="${theme.config?.hero?.hero_enabled != false} ? 'wi-home-content--with-hero'"
|
||||
th:attr="data-i18n-all-loaded=#{common.allLoaded},data-i18n-loading=#{common.loading},data-i18n-words=#{common.words},data-i18n-read-time=#{common.readTime},data-i18n-views=#{common.views}"
|
||||
>
|
||||
<section
|
||||
class="wi-home-pinned"
|
||||
@@ -21,7 +23,7 @@ import HeroSection from "../components/HeroSection.astro";
|
||||
>
|
||||
<div class="wi-container">
|
||||
<div class="wi-section__header">
|
||||
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_pinned_title ?: #messages.msg('hero.pinned')}">置顶</h2>
|
||||
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_pinned_title ?: '置顶'}">置顶</h2>
|
||||
</div>
|
||||
<div class="wi-pinned">
|
||||
<a
|
||||
@@ -66,7 +68,7 @@ import HeroSection from "../components/HeroSection.astro";
|
||||
<section class="wi-home-posts" id="wi-home-posts">
|
||||
<div class="wi-container">
|
||||
<div class="wi-section__header">
|
||||
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_posts_title ?: #messages.msg('hero.posts')}">文章</h2>
|
||||
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_posts_title ?: '文章'}">文章</h2>
|
||||
</div>
|
||||
<div class="wi-flow" id="wi-post-grid">
|
||||
<article
|
||||
@@ -129,7 +131,7 @@ import HeroSection from "../components/HeroSection.astro";
|
||||
class="wi-flow__stat"
|
||||
th:if="${theme.config?.home?.home_card_show_visit ?: true}"
|
||||
>
|
||||
<th:block th:text="${post.stats?.visit ?: 0}"></th:block> <th:block th:text="#{common.views}">阅读</th:block>
|
||||
<th:block th:text="${post.stats?.visit ?: 0}"></th:block> <th:block th:text="'阅读'">阅读</th:block>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -153,7 +155,7 @@ import HeroSection from "../components/HeroSection.astro";
|
||||
th:if="${posts.hasPrevious()}"
|
||||
th:href="@{${posts.prevUrl}}"
|
||||
class="wi-pagination__prev"
|
||||
th:text="${theme.config?.home?.home_label_newer ?: #messages.msg('common.newer')}"
|
||||
th:text="${theme.config?.home?.home_label_newer ?: '较新'}"
|
||||
>← 较新</a>
|
||||
<span
|
||||
class="wi-pagination__info"
|
||||
@@ -163,18 +165,18 @@ import HeroSection from "../components/HeroSection.astro";
|
||||
th:if="${posts.hasNext()}"
|
||||
th:href="@{${posts.nextUrl}}"
|
||||
class="wi-pagination__next"
|
||||
th:text="${theme.config?.home?.home_label_older ?: #messages.msg('common.older')}"
|
||||
th:text="${theme.config?.home?.home_label_older ?: '较旧'}"
|
||||
>较旧 →</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 ?: #messages.msg('common.allLoaded')}"
|
||||
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 ?: #messages.msg('common.loading')}">加载中...</span>
|
||||
<span th:text="${theme.config?.home?.home_label_loading ?: '加载中...'}">加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -182,23 +184,22 @@ import HeroSection from "../components/HeroSection.astro";
|
||||
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 ?: #messages.msg('common.allLoaded')}">已加载全部文章</span>
|
||||
<span th:text="${theme.config?.home?.home_label_all_loaded ?: '已加载全部文章'}">已加载全部文章</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<script is:inline>
|
||||
<script is:inline th:inline="none">
|
||||
(function () {
|
||||
var sentinel = document.getElementById("wi-infinite-scroll-sentinel");
|
||||
if (!sentinel) return;
|
||||
|
||||
var grid = document.getElementById("wi-post-grid");
|
||||
var i18nEl = document.querySelector(".wi-home-content");
|
||||
var loading = false;
|
||||
var nextUrl = sentinel.getAttribute("data-next-url");
|
||||
var allLoadedText = sentinel.getAttribute("data-all-loaded-text") || (i18nEl && i18nEl.getAttribute("data-i18n-all-loaded")) || "All posts loaded";
|
||||
var allLoadedText = sentinel.getAttribute("data-all-loaded-text") || "已加载全部文章";
|
||||
|
||||
var observer = new IntersectionObserver(
|
||||
function (entries) {
|
||||
@@ -240,7 +241,9 @@ import HeroSection from "../components/HeroSection.astro";
|
||||
sentinel.style.display = "none";
|
||||
var endMsg = document.createElement("div");
|
||||
endMsg.className = "wi-infinite-scroll__end";
|
||||
endMsg.innerHTML = "<span>" + allLoadedText + "</span>";
|
||||
var endText = document.createElement("span");
|
||||
endText.textContent = allLoadedText;
|
||||
endMsg.appendChild(endText);
|
||||
sentinel.parentNode.appendChild(endMsg);
|
||||
}
|
||||
|
||||
@@ -260,7 +263,7 @@ import HeroSection from "../components/HeroSection.astro";
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script is:inline>
|
||||
<script is:inline th:inline="none">
|
||||
(function () {
|
||||
var hero = document.getElementById("wi-hero");
|
||||
var blurOverlay = document.getElementById("wi-hero-blur-overlay");
|
||||
@@ -304,7 +307,7 @@ import HeroSection from "../components/HeroSection.astro";
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script is:inline>
|
||||
<script is:inline th:inline="none">
|
||||
(function () {
|
||||
var hero = document.getElementById("wi-hero");
|
||||
var content = document.querySelector(".wi-home-content--with-hero");
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
---
|
||||
|
||||
<!--
|
||||
页面布局契约(Halo 2.26+)
|
||||
供插件前台页面通过 ~{layout :: html(head = ..., content = ...)} 复用主题外观,
|
||||
避免这些页面回退到 Halo 内置的 fallback 布局导致风格不一致。
|
||||
|
||||
本文件只负责声明契约:实际结构、配色、导航、页脚均由 Layout 组件提供,
|
||||
因此修改主题外观时无需同步改这里。
|
||||
-->
|
||||
<Layout layoutFragment wide />
|
||||
+108
-4
@@ -4,24 +4,39 @@ import Layout from "../layouts/Layout.astro";
|
||||
|
||||
<Layout wide>
|
||||
<Fragment slot="head">
|
||||
<title th:text="|${theme.config?.links?.links_page_title ?: #messages.msg('links.title')} - ${site.title}|"></title>
|
||||
<title th:text="|${theme.config?.links?.links_page_title ?: '友链'} - ${site.title}|"></title>
|
||||
</Fragment>
|
||||
|
||||
<section
|
||||
class="wi-links"
|
||||
th:if="${pluginFinder.available('PluginLinks')}"
|
||||
th:attr="data-style=${theme.config?.links?.links_style ?: 'cards'}"
|
||||
th:attr="data-style=${theme.config?.links?.links_style ?: 'cards'},data-random=${theme.config?.links?.links_random_order == true ? 'true' : 'false'}"
|
||||
>
|
||||
<div class="wi-container">
|
||||
<div class="wi-links__header">
|
||||
<h1
|
||||
class="wi-links__title"
|
||||
th:text="${theme.config?.links?.links_page_title ?: #messages.msg('links.title')}"
|
||||
th:text="${theme.config?.links?.links_page_title ?: '友链'}"
|
||||
>
|
||||
友情链接
|
||||
</h1>
|
||||
<p class="wi-links__subtitle" th:text="#{links.subtitle}">Mountains and rivers, a journey of a thousand miles</p>
|
||||
<p class="wi-links__subtitle" th:text="'山水一程,三生有幸'">山水一程,三生有幸</p>
|
||||
<span class="wi-page-accent"></span>
|
||||
<button
|
||||
type="button"
|
||||
class="wi-links__portal"
|
||||
id="wi-links-portal"
|
||||
th:if="${theme.config?.links?.links_random_portal != false}"
|
||||
title="随机传送"
|
||||
aria-label="随机传送"
|
||||
>
|
||||
<svg 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 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96" transform="translate(0 8)"></polyline>
|
||||
</svg>
|
||||
<span class="wi-links__portal-text">随机传送</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<th:block th:each="group : ${groups}">
|
||||
@@ -71,8 +86,50 @@ import Layout from "../layouts/Layout.astro";
|
||||
</a>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<div class="wi-links__comment" th:if="${theme.config?.links?.links_show_comment == true}">
|
||||
<halo:comment group="plugin.halo.run" kind="Plugin" name="PluginLinks"></halo:comment>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script is:inline th:inline="none">
|
||||
(function () {
|
||||
var section = document.querySelector(".wi-links");
|
||||
if (!section) return;
|
||||
|
||||
var cards = Array.prototype.slice.call(section.querySelectorAll(".wi-links__card"));
|
||||
if (!cards.length) return;
|
||||
|
||||
// 随机排列:Fisher-Yates 打乱每个分组内的卡片顺序
|
||||
if (section.getAttribute("data-random") === "true") {
|
||||
Array.prototype.forEach.call(section.querySelectorAll(".wi-links__grid"), function (grid) {
|
||||
for (var i = grid.children.length; i >= 0; i--) {
|
||||
var j = Math.floor(Math.random() * i);
|
||||
grid.appendChild(grid.children[j]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 随机传送:从当前页所有友链中随机打开一个
|
||||
var portal = document.getElementById("wi-links-portal");
|
||||
if (portal) {
|
||||
portal.addEventListener("click", function () {
|
||||
var links = Array.prototype.slice
|
||||
.call(section.querySelectorAll(".wi-links__card"))
|
||||
.map(function (a) {
|
||||
return a.getAttribute("href");
|
||||
})
|
||||
.filter(function (h) {
|
||||
return h && h !== "#";
|
||||
});
|
||||
if (!links.length) return;
|
||||
var target = links[Math.floor(Math.random() * links.length)];
|
||||
window.open(target, "_blank", "noopener");
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
@@ -85,6 +142,53 @@ import Layout from "../layouts/Layout.astro";
|
||||
margin-bottom: var(--space-3xl);
|
||||
}
|
||||
|
||||
.wi-links__portal {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.6rem 1.1rem;
|
||||
min-height: 44px;
|
||||
margin-top: var(--space-md);
|
||||
border-radius: 9999px;
|
||||
font-size: var(--text-sm);
|
||||
color: #fff;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--accent),
|
||||
color-mix(in srgb, var(--accent) 70%, #000)
|
||||
);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 8px color-mix(in srgb, var(--accent) 30%, transparent);
|
||||
transition: transform var(--duration-fast) var(--ease-out-quart),
|
||||
box-shadow var(--duration-fast) var(--ease-out-quart),
|
||||
opacity var(--duration-fast) var(--ease-out-quart);
|
||||
}
|
||||
|
||||
.wi-links__portal:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 14px color-mix(in srgb, var(--accent) 40%, transparent);
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.wi-links__portal:active {
|
||||
transform: translateY(0) scale(0.97);
|
||||
}
|
||||
|
||||
.wi-links__portal svg {
|
||||
transition: transform var(--duration-normal) var(--ease-out-expo);
|
||||
}
|
||||
|
||||
.wi-links__portal:hover svg {
|
||||
transform: rotate(180deg) scale(1.1);
|
||||
}
|
||||
|
||||
.wi-links__comment {
|
||||
margin-top: var(--space-3xl);
|
||||
}
|
||||
|
||||
.wi-links__title {
|
||||
font-size: var(--text-4xl);
|
||||
margin-bottom: var(--space-xs);
|
||||
|
||||
+40
-14
@@ -5,13 +5,22 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
|
||||
<Layout>
|
||||
<Fragment slot="head">
|
||||
<title th:text="|${theme.config?.moments?.moments_page_title ?: #messages.msg('moments.title')} - ${site.title}|"></title>
|
||||
<title th:text="|${theme.config?.moments?.moments_page_title ?: '动态'} - ${site.title}|"></title>
|
||||
</Fragment>
|
||||
|
||||
<section class="wi-moments-page" th:attr="data-i18n-all-moments-loaded=#{common.allMomentsLoaded}">
|
||||
<section class="wi-moments-page">
|
||||
<div class="wi-moments-page__hero" th:if="${theme.config?.moments?.moments_cover}">
|
||||
<img
|
||||
class="wi-moments-page__cover"
|
||||
th:src="${theme.config?.moments?.moments_cover}"
|
||||
alt=""
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="wi-container">
|
||||
<div class="wi-moments-page__header">
|
||||
<h1 class="wi-moments-page__title" th:text="${theme.config?.moments?.moments_page_title ?: #messages.msg('moments.title')}">瞬间</h1>
|
||||
<div class="wi-moments-page__header" th:unless="${theme.config?.moments?.moments_cover}">
|
||||
<h1 class="wi-moments-page__title" th:text="${theme.config?.moments?.moments_page_title ?: '动态'}">瞬间</h1>
|
||||
<span class="wi-page-accent"></span>
|
||||
</div>
|
||||
|
||||
@@ -70,7 +79,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
</th:block>
|
||||
<th:block th:if="${momentsResult == null or #lists.isEmpty(momentsResult.items)}">
|
||||
<div class="wi-moments-page__empty">
|
||||
<p th:text="#{moments.noMoments}">No moments yet</p>
|
||||
<p th:text="'还没有瞬间,快去记录当下的心情吧 🌙'">还没有瞬间,快去记录当下的心情吧 🌙</p>
|
||||
</div>
|
||||
</th:block>
|
||||
</div>
|
||||
@@ -126,7 +135,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
</th:block>
|
||||
<th:block th:if="${momentsResult == null or #lists.isEmpty(momentsResult.items)}">
|
||||
<div class="wi-moments-page__empty">
|
||||
<p th:text="#{moments.noMoments}">No moments yet</p>
|
||||
<p th:text="'还没有瞬间,快去记录当下的心情吧 🌙'">还没有瞬间,快去记录当下的心情吧 🌙</p>
|
||||
</div>
|
||||
</th:block>
|
||||
</div>
|
||||
@@ -141,13 +150,13 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
class="wi-moments-page__page-btn"
|
||||
th:if="${momentsResult.hasPrevious()}"
|
||||
th:href="${momentsResult.prevUrl}"
|
||||
th:text="#{common.newer}">Newer</a>
|
||||
th:text="'较新'">较新</a>
|
||||
<span class="wi-moments-page__page-spacer" th:if="${momentsResult.hasPrevious() and momentsResult.hasNext()}"></span>
|
||||
<a
|
||||
class="wi-moments-page__page-btn"
|
||||
th:if="${momentsResult.hasNext()}"
|
||||
th:href="${momentsResult.nextUrl}"
|
||||
th:text="#{common.older}">Older</a>
|
||||
th:text="'较旧'">较旧</a>
|
||||
</nav>
|
||||
|
||||
<div
|
||||
@@ -157,7 +166,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
></div>
|
||||
<th:block th:if="${momentFinder == null}">
|
||||
<div class="wi-moments-page__empty">
|
||||
<p th:text="#{common.pluginError}">Plugin error</p>
|
||||
<p th:text="'插件加载异常,请检查插件状态'">插件加载异常,请检查插件状态</p>
|
||||
</div>
|
||||
</th:block>
|
||||
</div>
|
||||
@@ -165,7 +174,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
|
||||
<th:block th:unless="${pluginFinder.available('PluginMoments')}">
|
||||
<div class="wi-moments-page__empty">
|
||||
<p th:utext="#{common.installPlugin('Moments', 'https://www.halo.run/store/apps/app-hqbe')}">This feature requires the Moments plugin</p>
|
||||
<p>该功能需要安装「瞬间」插件,<a href="https://www.halo.run/store/apps/app-SnwWD" target="_blank" rel="noopener noreferrer">前往应用市场安装</a></p>
|
||||
</div>
|
||||
</th:block>
|
||||
</div>
|
||||
@@ -174,7 +183,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
<LightGallery />
|
||||
</Layout>
|
||||
|
||||
<script is:inline>
|
||||
<script is:inline th:inline="none">
|
||||
(function () {
|
||||
document.addEventListener("click", function (e) {
|
||||
var likeBtn = e.target.closest(".wi-moments-page__like-btn");
|
||||
@@ -350,9 +359,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
sentinel.style.display = "none";
|
||||
var endMsg = document.createElement("div");
|
||||
endMsg.className = "wi-moments-page__empty";
|
||||
var momentsSection = document.querySelector(".wi-moments-page");
|
||||
var allMomentsText = momentsSection ? momentsSection.getAttribute("data-i18n-all-moments-loaded") : "All moments loaded";
|
||||
endMsg.innerHTML = "<p>" + allMomentsText + "</p>";
|
||||
endMsg.innerHTML = "<p>已加载全部瞬间</p>";
|
||||
sentinel.parentNode.appendChild(endMsg);
|
||||
}
|
||||
|
||||
@@ -376,6 +383,21 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
padding-block: 1rem;
|
||||
}
|
||||
|
||||
.wi-moments-page__hero {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: clamp(220px, 30vw, 380px);
|
||||
overflow: hidden;
|
||||
background: var(--bg-raised);
|
||||
}
|
||||
|
||||
.wi-moments-page__cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wi-moments-page__header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--space-3xl);
|
||||
@@ -678,6 +700,10 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.wi-moments-page__hero {
|
||||
height: clamp(180px, 36vw, 260px);
|
||||
}
|
||||
|
||||
.wi-moments-page__masonry {
|
||||
columns: 1;
|
||||
}
|
||||
|
||||
@@ -92,21 +92,6 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
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;
|
||||
|
||||
@@ -5,7 +5,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
<Layout>
|
||||
<Fragment slot="head">
|
||||
<title
|
||||
th:text="|${theme.config?.messageboard?.messageboard_title ?: #messages.msg('messageboard.title')} - ${site.title}|"
|
||||
th:text="|${theme.config?.messageboard?.messageboard_title ?: '留言板'} - ${site.title}|"
|
||||
></title>
|
||||
</Fragment>
|
||||
|
||||
@@ -14,12 +14,12 @@ import Layout from "../layouts/Layout.astro";
|
||||
<span class="wi-messageboard__deco">"</span>
|
||||
<h1
|
||||
class="wi-messageboard__title"
|
||||
th:text="${theme.config?.messageboard?.messageboard_title ?: #messages.msg('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 ?: #messages.msg('messageboard.subtitle')}"
|
||||
th:text="${theme.config?.messageboard?.messageboard_subtitle ?: '在这里留下你的足迹 🌙'}"
|
||||
></p>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
---
|
||||
|
||||
<Layout wide>
|
||||
<Fragment slot="head">
|
||||
<title th:text="|${theme.config?.subscriptions?.subscriptions_page_title ?: '订阅'} - ${site.title}|"></title>
|
||||
</Fragment>
|
||||
|
||||
<div class="wi-content-wrap wi-content-wrap--wide"> <section class="wi-subs" th:if="${pluginFinder.available('PluginLinks')}" th:attr="data-layout=${theme.config?.subscriptions?.subscriptions_layout ?: 'list'}" th:with="
|
||||
cursorDate=${param.beforePublishedAt != null and param.beforePublishedAt.length > 0 ? param.beforePublishedAt[0] : null},
|
||||
cursorId=${param.beforeId != null and param.beforeId.length > 0 ? param.beforeId[0] : null},
|
||||
pageSize=${theme.config?.subscriptions?.subscriptions_page_size ?: 20},
|
||||
feedPage=${linkFeedFinder.list({limit: pageSize, beforePublishedAt: cursorDate, beforeId: cursorId})},
|
||||
hasCursor=${cursorDate != null},
|
||||
loadingMode=${theme.config?.subscriptions?.subscriptions_loading_mode ?: 'pagination'}
|
||||
"> <div class="wi-container"> <div class="wi-subs__header"> <h1 class="wi-subs__title" th:text="${theme.config?.subscriptions?.subscriptions_page_title ?: '订阅'}">
|
||||
订阅
|
||||
</h1> <p class="wi-subs__subtitle" th:text="${theme.config?.subscriptions?.subscriptions_subtitle ?: '博友们的最新文章'}">
|
||||
博友们的最新文章
|
||||
</p> <span class="wi-page-accent"></span> </div> <th:block th:if="${feedPage != null and !#lists.isEmpty(feedPage.items)}"> <div class="wi-subs__flow"> <article class="wi-subs__card" th:each="item : ${feedPage.items}" th:with="authorName=${item.author ?: '匿名'}"> <div class="wi-subs__author"> <img th:if="${item.authorLogo}" th:src="${item.authorLogo}" th:alt="${authorName}" class="wi-subs__avatar" loading="lazy" decoding="async"> <div th:unless="${item.authorLogo}" class="wi-subs__avatar wi-subs__avatar--placeholder"> <span th:text="${#strings.substring(authorName, 0, 1)}"></span> </div> <div class="wi-subs__author-info"> <a th:if="${item.authorUrl}" th:href="${item.authorUrl}" target="_blank" rel="noopener noreferrer" class="wi-subs__author-name" th:text="${authorName}"></a> <span th:unless="${item.authorUrl}" class="wi-subs__author-name" th:text="${authorName}"></span> <time class="wi-subs__date" th:if="${item.publishedAt}" th:text="${#temporals.format(item.publishedAt, 'yyyy-MM-dd')}"></time> </div> </div> <div class="wi-subs__body"> <a th:href="${item.url}" target="_blank" rel="noopener noreferrer" class="wi-subs__title-link"> <h2 class="wi-subs__post-title" th:text="${item.title}"></h2> </a> <p class="wi-subs__summary" th:if="${item.summary}" th:text="${item.summary}"></p> </div> </article> </div> <nav class="wi-subs__pagination" th:if="${loadingMode != 'infinite_scroll' and (hasCursor or feedPage.hasNext)}" th:with="permalink=${singlePage.status.permalink}"> <a th:if="${hasCursor}" th:href="@{${permalink}}" class="wi-subs__page-link" th:text="'较新'">
|
||||
较新
|
||||
</a> <span th:unless="${hasCursor}" class="wi-subs__page-link wi-subs__page-link--placeholder" aria-hidden="true"></span> <span class="wi-subs__page-info"></span> <a th:if="${feedPage.hasNext}" th:href="@{|${permalink}?beforePublishedAt=${feedPage.nextBeforePublishedAt}&beforeId=${feedPage.nextBeforeId}|}" class="wi-subs__page-link" th:text="'较旧'">
|
||||
较旧
|
||||
</a> <span th:unless="${feedPage.hasNext}" class="wi-subs__page-link wi-subs__page-link--placeholder" aria-hidden="true"></span> </nav> <div class="wi-subs__infinite-sentinel" th:if="${loadingMode == 'infinite_scroll' and feedPage.hasNext}" th:with="permalink=${singlePage.status.permalink}" th:attr="data-next-url=@{|${permalink}?beforePublishedAt=${feedPage.nextBeforePublishedAt}&beforeId=${feedPage.nextBeforeId}|}, data-loading='加载中...', data-no-more='没有更多了'"></div> <div class="wi-subs__infinite-status" th:if="${loadingMode == 'infinite_scroll'}" aria-live="polite"></div> </th:block> <div class="wi-subs__empty" th:if="${feedPage == null or #lists.isEmpty(feedPage.items)}"> <p th:utext="'暂无订阅动态,安装并开启「链接管理」插件的 RSS 订阅后将在此展示'">暂无订阅动态</p> </div> </div> </section> <section class="wi-subs" th:unless="${pluginFinder.available('PluginLinks')}"> <div class="wi-container"> <div class="wi-subs__header"> <h1 class="wi-subs__title" th:text="'订阅'">订阅</h1> <span class="wi-page-accent"></span> </div> <div class="wi-subs__empty"> <p>该功能需要安装「链接管理」插件,<a href="https://www.halo.run/store/apps/app-hfbQg" target="_blank" rel="noopener noreferrer">前往应用市场安装</a></p> </div> </div> </section> <script is:inline th:inline="none">
|
||||
(function () {
|
||||
var sentinel = document.querySelector(".wi-subs__infinite-sentinel");
|
||||
if (!sentinel) return;
|
||||
|
||||
var infiniteLoading = false;
|
||||
var infiniteRetryCount = 0;
|
||||
var infiniteMaxRetries = 3;
|
||||
var infiniteNextUrl = sentinel.getAttribute("data-next-url");
|
||||
var loadingText = sentinel.getAttribute("data-loading") || "Loading...";
|
||||
var noMoreText = sentinel.getAttribute("data-no-more") || "No more items";
|
||||
var statusEl = document.querySelector(".wi-subs__infinite-status");
|
||||
|
||||
var infiniteObserver = new IntersectionObserver(
|
||||
function (entries) {
|
||||
if (entries[0].isIntersecting && !infiniteLoading && infiniteNextUrl) {
|
||||
infiniteLoading = true;
|
||||
if (statusEl) {
|
||||
statusEl.innerHTML = '<p class="wi-subs__infinite-loading">' + loadingText + "</p>";
|
||||
}
|
||||
fetch(infiniteNextUrl)
|
||||
.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 container = doc.querySelector(".wi-subs__flow");
|
||||
var currentContainer = document.querySelector(".wi-subs__flow");
|
||||
if (!container || !currentContainer) {
|
||||
infiniteLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var newItems = container.querySelectorAll(".wi-subs__card");
|
||||
newItems.forEach(function (item) {
|
||||
var clone = item.cloneNode(true);
|
||||
clone.style.opacity = "0";
|
||||
clone.style.transform = "translateY(20px)";
|
||||
currentContainer.appendChild(clone);
|
||||
(function (el) {
|
||||
requestAnimationFrame(function () {
|
||||
el.style.transition = "opacity 0.4s ease, transform 0.4s ease";
|
||||
el.style.opacity = "1";
|
||||
el.style.transform = "translateY(0)";
|
||||
setTimeout(function () {
|
||||
el.style.transition = "";
|
||||
el.style.opacity = "";
|
||||
el.style.transform = "";
|
||||
}, 450);
|
||||
});
|
||||
})(clone);
|
||||
});
|
||||
|
||||
var newSentinel = doc.querySelector(".wi-subs__infinite-sentinel");
|
||||
if (newSentinel && newSentinel.getAttribute("data-next-url")) {
|
||||
infiniteNextUrl = newSentinel.getAttribute("data-next-url");
|
||||
sentinel.setAttribute("data-next-url", infiniteNextUrl);
|
||||
infiniteRetryCount = 0;
|
||||
if (statusEl) {
|
||||
statusEl.innerHTML = "";
|
||||
}
|
||||
} else {
|
||||
infiniteNextUrl = null;
|
||||
infiniteObserver.disconnect();
|
||||
sentinel.style.display = "none";
|
||||
if (statusEl) {
|
||||
statusEl.innerHTML = '<p class="wi-subs__infinite-end">' + noMoreText + "</p>";
|
||||
}
|
||||
}
|
||||
|
||||
infiniteLoading = false;
|
||||
})
|
||||
.catch(function () {
|
||||
infiniteRetryCount++;
|
||||
if (infiniteRetryCount >= infiniteMaxRetries) {
|
||||
infiniteObserver.disconnect();
|
||||
sentinel.style.display = "none";
|
||||
return;
|
||||
}
|
||||
infiniteLoading = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
{ rootMargin: "300px" }
|
||||
);
|
||||
|
||||
infiniteObserver.observe(sentinel);
|
||||
})();
|
||||
</script> </div>
|
||||
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.wi-subs{padding-block:1rem}.wi-subs__header{text-align:center;margin-bottom:var(--space-3xl)}.wi-subs__title{font-size:var(--text-4xl);margin-bottom:var(--space-sm)}.wi-subs__subtitle{font-size:var(--text-sm);color:var(--ink-3);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-subs__flow{display:flex;flex-direction:column;gap:var(--space-lg);max-width:720px;margin-inline:auto}.wi-subs[data-layout=grid] .wi-subs__flow{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--space-lg);max-width:1200px}.wi-subs[data-layout=grid] .wi-subs__card{padding:var(--space-lg);display:flex;flex-direction:column;gap:var(--space-md)}.wi-subs[data-layout=grid] .wi-subs__author{flex-direction:column;align-items:center;text-align:center;gap:var(--space-xs);margin-bottom:0}.wi-subs[data-layout=grid] .wi-subs__avatar{width:44px;height:44px}.wi-subs[data-layout=grid] .wi-subs__author-info{align-items:center;gap:2px}.wi-subs[data-layout=grid] .wi-subs__author-name{font-size:var(--text-sm);white-space:normal;text-align:center;line-height:var(--leading-tight);-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.wi-subs[data-layout=grid] .wi-subs__date{font-size:var(--text-xs)}.wi-subs[data-layout=grid] .wi-subs__body{margin-bottom:0;flex:1;display:flex;flex-direction:column;gap:var(--space-xs)}.wi-subs[data-layout=grid] .wi-subs__post-title{font-size:var(--text-md);line-height:var(--leading-snug);-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden;margin:0}.wi-subs[data-layout=grid] .wi-subs__summary{font-size:var(--text-sm);-webkit-line-clamp:3;line-clamp:3}@media(max-width:768px){.wi-subs[data-layout=grid] .wi-subs__flow{grid-template-columns:repeat(2,1fr)}}@media(max-width:480px){.wi-subs[data-layout=grid] .wi-subs__flow{grid-template-columns:1fr}.wi-subs[data-layout=grid] .wi-subs__card{padding:var(--space-md)}.wi-subs[data-layout=grid] .wi-subs__author{flex-direction:row;text-align:left}.wi-subs[data-layout=grid] .wi-subs__author-info{align-items:flex-start}.wi-subs[data-layout=grid] .wi-subs__author-name{text-align:left}}.wi-subs__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-subs__card:hover{box-shadow:var(--shadow-md);transform:translateY(-2px);border-color:var(--accent)}.wi-subs__author{display:flex;align-items:center;gap:var(--space-md);margin-bottom:var(--space-md)}.wi-subs__avatar{width:44px;height:44px;border-radius:9999px;object-fit:cover;flex-shrink:0;box-shadow:var(--shadow-sm)}.wi-subs__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-subs__author-info{display:flex;flex-direction:column;gap:2px;min-width:0}.wi-subs__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)}a.wi-subs__author-name:hover{color:var(--accent)}.wi-subs__date{font-size:var(--text-xs);color:var(--ink-3);letter-spacing:.02em}.wi-subs__body{margin-bottom:var(--space-sm)}.wi-subs__title-link{text-decoration:none;display:block}.wi-subs__post-title{font-size:var(--text-lg);font-weight:600;color:var(--ink);line-height:var(--leading-snug);margin:0 0 var(--space-xs);transition:color var(--duration-fast) var(--ease-out-quart)}.wi-subs__title-link:hover .wi-subs__post-title{color:var(--accent)}.wi-subs__summary{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-subs__pagination{display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-top:var(--space-2xl);padding-top:var(--space-xl);border-top:1px solid var(--rule)}.wi-subs__page-link{display:inline-flex;align-items:center;padding:var(--space-sm) var(--space-md);min-height:44px;color:var(--accent);text-decoration:none;font-size:var(--text-sm);font-weight:500;border-radius:var(--radius-sm);transition:opacity var(--duration-fast) var(--ease-out-quart),background var(--duration-fast) var(--ease-out-quart)}.wi-subs__page-link:hover{opacity:.8;background:var(--bg-raised)}.wi-subs__page-link--placeholder{visibility:hidden;pointer-events:none}.wi-subs__page-info{color:var(--ink-3);font-size:var(--text-sm)}.wi-subs__empty{text-align:center;padding:var(--space-4xl) var(--space-xl);color:var(--ink-3);font-size:var(--text-md)}.wi-subs__empty a{color:var(--accent)}.wLine truncated
|
||||
</style>
|
||||
@@ -5,7 +5,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
|
||||
<Layout wide>
|
||||
<Fragment slot="head">
|
||||
<title th:text="|${title ?: (theme.config?.photos?.photos_page_title ?: #messages.msg('photos.title'))} - ${site.title}|"></title>
|
||||
<title th:text="|${title ?: (theme.config?.photos?.photos_page_title ?: '相册')} - ${site.title}|"></title>
|
||||
</Fragment>
|
||||
|
||||
<section class="wi-photos-page">
|
||||
@@ -13,7 +13,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
<div class="wi-photos-page__header">
|
||||
<h1
|
||||
class="wi-photos-page__title"
|
||||
th:text="${theme.config?.photos?.photos_page_title ?: #messages.msg('photos.title')}"
|
||||
th:text="${theme.config?.photos?.photos_page_title ?: '相册'}"
|
||||
>
|
||||
Gallery
|
||||
</h1>
|
||||
@@ -30,7 +30,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
th:href="@{${photoUrl.list()}}"
|
||||
class="wi-photos-page__group-link"
|
||||
th:classappend="${#strings.isEmpty(currentGroup)} ? 'wi-photos-page__group-link--active' : ''"
|
||||
th:text="#{common.all}"
|
||||
th:text="'全部'"
|
||||
>
|
||||
All
|
||||
</a>
|
||||
@@ -49,7 +49,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
<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" th:text="#{common.all}">All</h2>
|
||||
<h2 class="wi-photos-page__section-title" th:text="'全部'">全部</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}"
|
||||
@@ -241,7 +241,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
</div>
|
||||
</div>
|
||||
<div class="wi-photos-page__empty" th:if="${#lists.isEmpty(group.photos)}">
|
||||
<p th:text="#{common.noPhotosInGroup}">No photos in this group.</p>
|
||||
<p th:text="'该分组暂无图片'">该分组暂无图片</p>
|
||||
</div>
|
||||
</th:block>
|
||||
</th:block>
|
||||
@@ -249,20 +249,20 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
</th:block>
|
||||
|
||||
<div class="wi-photos-page__empty" th:if="${#lists.isEmpty(groups) and !(showUngrouped and photos != null and !#lists.isEmpty(photos.items))}">
|
||||
<p th:text="#{common.noPhotos}">No photos yet.</p>
|
||||
<p th:text="'暂无图片'">暂无图片</p>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<th:block th:if="${groups == null}">
|
||||
<div class="wi-photos-page__empty">
|
||||
<p th:text="#{common.pluginError}">Plugin error</p>
|
||||
<p th:text="'插件加载异常,请检查插件状态'">插件加载异常,请检查插件状态</p>
|
||||
</div>
|
||||
</th:block>
|
||||
</th:block>
|
||||
|
||||
<th:block th:unless="${pluginFinder.available('PluginPhotos')}">
|
||||
<div class="wi-photos-page__empty">
|
||||
<p th:utext="#{common.installPlugin('Gallery', 'https://www.halo.run/store/apps/app-PgHgw')}">This feature requires the Gallery plugin</p>
|
||||
<p>该功能需要安装「相册」插件,<a href="https://www.halo.run/store/apps/app-BmQJW" target="_blank" rel="noopener noreferrer">前往应用市场安装</a></p>
|
||||
</div>
|
||||
</th:block>
|
||||
</div>
|
||||
@@ -547,7 +547,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
}
|
||||
</style>
|
||||
|
||||
<script is:inline>
|
||||
<script is:inline th:inline="none">
|
||||
(function () {
|
||||
var rafPending = false;
|
||||
|
||||
|
||||
+89
-238
@@ -38,7 +38,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
|
||||
<div class="wi-reading-progress" id="wi-reading-progress"></div>
|
||||
|
||||
<article class="wi-post" th:attr="data-i18n-words=#{common.words},data-i18n-read-time=#{common.readTime}">
|
||||
<article class="wi-post">
|
||||
<header class="wi-post__header">
|
||||
<div
|
||||
th:if="${theme.config?.article?.article_show_cover != false and !#strings.isEmpty(post.spec.cover)}"
|
||||
@@ -111,12 +111,14 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
th:if="${theme.config?.article?.article_show_word_count ?: true}"
|
||||
class="wi-post__stat-item"
|
||||
id="wi-word-count"
|
||||
>-- words</span>
|
||||
th:with="serverWords=${pluginFinder.available('extra-api') ? extraApiStatsFinder.getPostWordCount({name: post.metadata.name}) : 0}"
|
||||
th:text="${serverWords > 0 ? serverWords + ' 字' : ''}"
|
||||
></span>
|
||||
<span
|
||||
th:if="${theme.config?.article?.article_show_visit_count ?: true}"
|
||||
class="wi-post__stat-item"
|
||||
>
|
||||
<th:block th:text="${post.stats?.visit ?: 0}"></th:block> <th:block th:text="#{common.views}"></th:block>
|
||||
<th:block th:text="${post.stats?.visit ?: 0}"></th:block> <th:block th:text="'阅读'"></th:block>
|
||||
</span>
|
||||
<span
|
||||
th:if="${theme.config?.article?.article_show_read_time ?: false}"
|
||||
@@ -131,7 +133,10 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
<div
|
||||
class="wi-post__body"
|
||||
id="wi-post-body"
|
||||
th:utext="${post.content.content}"
|
||||
th:with="hasExtra=${pluginFinder.available('extra-api')},
|
||||
spaced=${hasExtra and (theme.config?.article?.article_extra_spacing ?: true) ? extraApiRenderFinder.applySpacingInHtml(post.content.content) : post.content.content},
|
||||
rendered=${hasExtra and (theme.config?.article?.article_extra_highlight ?: false) and extraApiPluginInfoFinder.isFullVersion() ? extraApiJsRenderFinder.highlightCodeInHtml(spaced) : spaced}"
|
||||
th:utext="${rendered}"
|
||||
></div>
|
||||
|
||||
<aside
|
||||
@@ -141,7 +146,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
th:attr="data-position=${theme.config?.article?.article_toc_position ?: 'left'},data-expand=${theme.config?.article?.article_toc_expand ?: 'active'}"
|
||||
>
|
||||
<div class="wi-toc__header">
|
||||
<span class="wi-toc__title" th:text="#{article.toc}">目录</span>
|
||||
<span class="wi-toc__title" th:text="'目录'">目录</span>
|
||||
</div>
|
||||
<nav class="wi-toc__nav" id="wi-toc-nav"></nav>
|
||||
</aside>
|
||||
@@ -158,7 +163,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
</div>
|
||||
|
||||
<div class="wi-post__share" th:if="${theme.config?.article?.article_show_share != false}" th:with="shareUrl=${post.status.permalink}, shareTitle=${post.spec.title}, shareChannels=${theme.config?.article?.article_share_channels ?: 'qq,wechat,weibo'}">
|
||||
<span class="wi-post__share-label" th:text="${theme.config?.article?.article_share_title ?: #messages.msg('share.title')}">分享</span>
|
||||
<span class="wi-post__share-label" th:text="${theme.config?.article?.article_share_title ?: '分享'}">分享</span>
|
||||
<div class="wi-post__share-buttons">
|
||||
<button th:if="${#strings.contains(shareChannels, 'qq')}" class="wi-post__share-btn wi-post__share-btn--qq" type="button" th:attr="data-url=${shareUrl},data-title=${shareTitle}" title="QQ">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width="18" height="18" fill="currentColor"><!--!Font Awesome Free v7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M530.1 484.4C518.6 485.8 485.2 431.7 485.2 431.7C485.2 463 469.1 503.9 434.2 533.5C451 538.7 489 552.7 480 567.9C472.7 580.2 354.5 575.8 320.4 571.9C286.3 575.7 168.1 580.2 160.8 567.9C151.8 552.7 189.7 538.7 206.6 533.5C171.7 504 155.5 463.1 155.5 431.7C155.5 431.7 122.2 485.8 110.6 484.4C105.2 483.8 98.2 454.8 119.9 384.7C130.2 351.7 141.9 324.2 160 278.9C156.9 162 205.2 63.9 320.3 63.9C434 63.9 483.5 160 480.6 278.9C498.7 324.1 510.5 351.8 520.7 384.7C542.5 454.8 535.4 483.8 530 484.4z"/></svg>
|
||||
@@ -189,9 +194,9 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
<button class="wi-wechat-qr__close" type="button" aria-label="Close">
|
||||
<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"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
<div class="wi-wechat-qr__title" th:text="#{share.wechatQR ?: '微信扫码分享'}">微信扫码分享</div>
|
||||
<div class="wi-wechat-qr__title" th:text="'微信扫码分享'">微信扫码分享</div>
|
||||
<canvas class="wi-wechat-qr__canvas" id="wi-wechat-qr-canvas"></canvas>
|
||||
<div class="wi-wechat-qr__hint" th:text="#{share.wechatHint ?: '打开微信,扫描二维码即可分享'}">打开微信,扫描二维码即可分享</div>
|
||||
<div class="wi-wechat-qr__hint" th:text="'打开微信,扫描二维码即可分享'">打开微信,扫描二维码即可分享</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -205,7 +210,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
th:href="@{${cursor.previous.status.permalink}}"
|
||||
class="wi-post__nav-link wi-post__nav-link--prev"
|
||||
>
|
||||
<span class="wi-post__nav-label" th:text="#{article.previousPost}">上一篇</span>
|
||||
<span class="wi-post__nav-label" th:text="'上一篇'">上一篇</span>
|
||||
<span
|
||||
class="wi-post__nav-title"
|
||||
th:text="${cursor.previous.spec.title}"
|
||||
@@ -220,7 +225,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
th:href="@{${cursor.next.status.permalink}}"
|
||||
class="wi-post__nav-link wi-post__nav-link--next"
|
||||
>
|
||||
<span class="wi-post__nav-label" th:text="#{article.nextPost}">下一篇</span>
|
||||
<span class="wi-post__nav-label" th:text="'下一篇'">下一篇</span>
|
||||
<span
|
||||
class="wi-post__nav-title"
|
||||
th:text="${cursor.next.spec.title}"
|
||||
@@ -236,7 +241,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
|
||||
<th:block th:if="${theme.config?.article?.article_show_related == true}">
|
||||
<section class="wi-post__related" th:with="relatedCat=${post.categories != null and !post.categories.isEmpty()} ? ${post.categories[0].metadata.name} : null, relatedPosts=${relatedCat != null} ? ${postFinder.list({size: 3, categoryName: relatedCat})} : ${postFinder.list({size: 3})}">
|
||||
<h2 class="wi-post__related-title" th:text="${theme.config?.article?.article_related_title ?: #messages.msg('article.relatedPosts')}">相关推荐</h2>
|
||||
<h2 class="wi-post__related-title" th:text="${theme.config?.article?.article_related_title ?: '相关推荐'}">相关推荐</h2>
|
||||
<div class="wi-post__related-grid">
|
||||
<a th:each="rp : ${relatedPosts.items}" th:if="${rp.metadata.name != post.metadata.name}" th:href="@{${rp.status.permalink}}" class="wi-post__related-card">
|
||||
<div class="wi-post__related-cover" th:if="${!#strings.isEmpty(rp.spec.cover)}">
|
||||
@@ -266,7 +271,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
class="wi-toc-fab"
|
||||
id="wi-toc-fab"
|
||||
type="button"
|
||||
th:aria-label="#{common.openToc}"
|
||||
th:aria-label="'打开目录'"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg>
|
||||
</button>
|
||||
@@ -279,8 +284,8 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
<div class="wi-toc-drawer__overlay"></div>
|
||||
<div class="wi-toc-drawer__panel">
|
||||
<div class="wi-toc-drawer__header">
|
||||
<span class="wi-toc-drawer__title" th:text="#{article.toc}">目录</span>
|
||||
<button class="wi-toc-drawer__close" type="button" th:aria-label="#{common.closeToc}">
|
||||
<span class="wi-toc-drawer__title" th:text="'目录'">目录</span>
|
||||
<button class="wi-toc-drawer__close" type="button" th:aria-label="'关闭目录'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -305,7 +310,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
|
||||
<script is:inline defer src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"></script>
|
||||
<script is:inline defer src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
|
||||
<script is:inline>
|
||||
<script is:inline th:inline="none">
|
||||
(function () {
|
||||
if (typeof Prism !== "undefined") {
|
||||
Prism.plugins.autoloader.languages_path = "https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/";
|
||||
@@ -321,7 +326,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script is:inline>
|
||||
<script is:inline th:inline="none">
|
||||
(function () {
|
||||
var progressBar = document.getElementById("wi-reading-progress");
|
||||
if (progressBar) {
|
||||
@@ -347,6 +352,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
var body = document.getElementById("wi-post-body");
|
||||
if (!body) return;
|
||||
|
||||
/* 图片:补齐懒加载属性 + 加载前的模糊占位 */
|
||||
body.querySelectorAll("img").forEach(function(img) {
|
||||
if (!img.hasAttribute("loading")) {
|
||||
img.setAttribute("loading", "lazy");
|
||||
@@ -354,21 +360,63 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
if (!img.hasAttribute("decoding")) {
|
||||
img.setAttribute("decoding", "async");
|
||||
}
|
||||
if (img.complete && img.naturalWidth > 0) {
|
||||
img.classList.add("wi-media-loaded");
|
||||
} else {
|
||||
img.classList.add("wi-media-loading");
|
||||
img.addEventListener("load", function () {
|
||||
img.classList.remove("wi-media-loading");
|
||||
img.classList.add("wi-media-loaded");
|
||||
});
|
||||
img.addEventListener("error", function () {
|
||||
img.classList.remove("wi-media-loading");
|
||||
img.classList.add("wi-media-loaded");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/* 嵌入媒体(B站 / 腾讯视频 / YouTube 等 iframe):
|
||||
按原始 width/height 生成 aspect-ratio 容器,保证居中且自适应不形变 */
|
||||
body.querySelectorAll("iframe").forEach(function(frame) {
|
||||
if (frame.closest(".wi-embed")) return;
|
||||
var w = parseInt(frame.getAttribute("width"), 10);
|
||||
var h = parseInt(frame.getAttribute("height"), 10);
|
||||
var wrap = document.createElement("div");
|
||||
wrap.className = "wi-embed";
|
||||
if (w > 0 && h > 0) {
|
||||
wrap.style.aspectRatio = w + " / " + h;
|
||||
} else {
|
||||
wrap.style.aspectRatio = "16 / 9";
|
||||
}
|
||||
frame.parentNode.insertBefore(wrap, frame);
|
||||
wrap.appendChild(frame);
|
||||
frame.removeAttribute("width");
|
||||
frame.removeAttribute("height");
|
||||
});
|
||||
|
||||
var showWordCount = document.getElementById("wi-word-count");
|
||||
var showReadTime = document.getElementById("wi-read-time");
|
||||
var articleEl = document.querySelector(".wi-post");
|
||||
var wordsLabel = articleEl ? articleEl.getAttribute("data-i18n-words") : "words";
|
||||
var readTimeLabel = articleEl ? articleEl.getAttribute("data-i18n-read-time") : "min read";
|
||||
var wordsLabel = "字";
|
||||
var readTimeLabel = "分钟";
|
||||
|
||||
if (showWordCount || showReadTime) {
|
||||
var text = (body.textContent || body.innerText || "").trim();
|
||||
var charCount = text.replace(/\s/g, "").length;
|
||||
var readMinutes = Math.max(1, Math.ceil(charCount / 500));
|
||||
|
||||
/* 服务端已统计字数时(API 扩展包插件)直接复用,避免重复计算 */
|
||||
if (showWordCount) {
|
||||
showWordCount.textContent = charCount + " " + wordsLabel;
|
||||
if (showWordCount.textContent.trim()) {
|
||||
var servered = parseInt(showWordCount.textContent.replace(/\D/g, ""), 10);
|
||||
if (servered > 0) {
|
||||
charCount = servered;
|
||||
readMinutes = Math.max(1, Math.ceil(charCount / 500));
|
||||
} else {
|
||||
showWordCount.textContent = charCount + " " + wordsLabel;
|
||||
}
|
||||
} else {
|
||||
showWordCount.textContent = charCount + " " + wordsLabel;
|
||||
}
|
||||
}
|
||||
if (showReadTime) {
|
||||
showReadTime.textContent = readMinutes + " " + readTimeLabel;
|
||||
@@ -727,20 +775,6 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
});
|
||||
});
|
||||
|
||||
var lazyImgs = document.querySelectorAll(".wi-post__content img[loading='lazy']");
|
||||
lazyImgs.forEach(function (img) {
|
||||
if (img.complete && img.naturalWidth > 0) {
|
||||
img.setAttribute("data-blur", "loaded");
|
||||
} else {
|
||||
img.setAttribute("data-blur", "true");
|
||||
img.addEventListener("load", function () {
|
||||
img.setAttribute("data-blur", "loaded");
|
||||
});
|
||||
img.addEventListener("error", function () {
|
||||
img.setAttribute("data-blur", "loaded");
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</Layout>
|
||||
@@ -874,109 +908,9 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
color: #d4cdc4;
|
||||
}
|
||||
|
||||
.wi-post__body * {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.wi-post__body :is(pre, code, table) {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.wi-post__body pre {
|
||||
margin-block: 1.5em;
|
||||
padding: 1.25em;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--rule);
|
||||
background: var(--bg-raised);
|
||||
font-size: 0.875em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.wi-post__body pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.wi-post__body code {
|
||||
background: var(--bg-raised);
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875em;
|
||||
border: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
.wi-post__body :is(h1, h2, h3, h4, h5, h6) {
|
||||
font-family: var(--font-sans);
|
||||
margin-top: 1.8em;
|
||||
margin-bottom: 0.5em;
|
||||
scroll-margin-top: 80px;
|
||||
}
|
||||
|
||||
.wi-post__body h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-top: 2em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.wi-post__body h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.wi-post__body .ai-summary,
|
||||
.wi-post__body [class*="ai-summary"],
|
||||
.wi-post__body [class*="ai-abstract"],
|
||||
.wi-post__body .abstract,
|
||||
.wi-post__body [class*="likcc-article-summary"] {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.wi-post__body p {
|
||||
margin-block-end: 1.2em;
|
||||
}
|
||||
|
||||
.wi-post__body img {
|
||||
border-radius: 8px;
|
||||
margin-block: 1.5em;
|
||||
}
|
||||
|
||||
.wi-post__content img {
|
||||
transition: filter 0.4s ease, opacity 0.4s ease;
|
||||
}
|
||||
|
||||
.wi-post__content img[data-blur="true"] {
|
||||
filter: blur(20px);
|
||||
opacity: 0.6;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.wi-post__content img[data-blur="loaded"] {
|
||||
filter: blur(0);
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.wi-post__body a {
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 0.2em;
|
||||
}
|
||||
|
||||
.wi-post__body ul,
|
||||
.wi-post__body ol {
|
||||
padding-inline-start: 1.5em;
|
||||
margin-block-end: 1.2em;
|
||||
}
|
||||
|
||||
.wi-post__body li {
|
||||
margin-block-end: 0.6em;
|
||||
}
|
||||
|
||||
.wi-post__body blockquote {
|
||||
margin-block: 2em;
|
||||
}
|
||||
/* 正文本身及内部元素样式统一放在全局样式 src/styles/_content.scss,
|
||||
因为正文由 Thymeleaf 在运行时注入,不会带 Astro 的 data-astro-cid 作用域属性,
|
||||
写在组件 scoped 样式里对正文内容完全无效。 */
|
||||
|
||||
.wi-toc {
|
||||
display: none;
|
||||
@@ -1135,12 +1069,6 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.wi-toc-drawer__nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.wi-post__like {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -1558,51 +1486,10 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
line-height: 1.82;
|
||||
}
|
||||
|
||||
.wi-post__body p {
|
||||
margin-block-end: 1.4em;
|
||||
}
|
||||
|
||||
.wi-post__body h2 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.wi-post__body h3 {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
.wi-post__body h4 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.wi-post__body pre {
|
||||
font-size: 0.9em;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.wi-post__body code {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style is:global>
|
||||
.wi-post__body figure {
|
||||
margin-block: 1.5em;
|
||||
}
|
||||
|
||||
.wi-post__body figcaption {
|
||||
color: #545164;
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 1.2em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
html.dark .wi-post__body figcaption {
|
||||
color: #8a8494;
|
||||
}
|
||||
|
||||
.wi-toc__group {
|
||||
position: relative;
|
||||
}
|
||||
@@ -1615,36 +1502,38 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
}
|
||||
|
||||
.wi-toc__group--active .wi-toc__sub {
|
||||
max-height: 600px;
|
||||
max-height: 1600px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.wi-toc__group--expanded .wi-toc__sub {
|
||||
max-height: 600px;
|
||||
max-height: 1600px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.wi-toc[data-expand="all"] .wi-toc__sub {
|
||||
max-height: 600px;
|
||||
max-height: 1600px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.wi-toc__link {
|
||||
display: block;
|
||||
padding: 4px 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
padding: 5px 8px;
|
||||
font-size: var(--text-xs);
|
||||
line-height: var(--leading-snug);
|
||||
color: var(--ink-3);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-radius: 6px;
|
||||
border-left: 2px solid transparent;
|
||||
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.wi-toc__link:hover {
|
||||
color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 5%, transparent);
|
||||
}
|
||||
|
||||
.wi-toc__link--active {
|
||||
@@ -1655,56 +1544,23 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
}
|
||||
|
||||
.wi-toc__link--group {
|
||||
padding-left: 0;
|
||||
padding-right: 4px;
|
||||
font-weight: 500;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
font-weight: 600;
|
||||
color: var(--ink-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wi-toc__link--group::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: var(--ink-3);
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.wi-toc__link--group::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-right: 1.5px solid var(--ink-3);
|
||||
border-bottom: 1.5px solid var(--ink-3);
|
||||
transform: rotate(-45deg);
|
||||
margin-left: 6px;
|
||||
vertical-align: middle;
|
||||
transition: transform 0.2s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.wi-toc__group--expanded > .wi-toc__link--group::after,
|
||||
.wi-toc__group--active > .wi-toc__link--group::after {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.wi-toc__link--active.wi-toc__link--group::before {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.wi-toc__link--h3 {
|
||||
padding-left: 16px;
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.wi-toc__link--h4 {
|
||||
padding-left: 32px;
|
||||
padding-left: 36px;
|
||||
}
|
||||
|
||||
.wi-toc__link--h5 {
|
||||
padding-left: 48px;
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
.wi-toc__link--h6 {
|
||||
@@ -1715,11 +1571,6 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
border-left-color: var(--accent);
|
||||
}
|
||||
|
||||
.wi-toc__sub .wi-toc__link--h3.wi-toc__link--active { padding-left: 14px; }
|
||||
.wi-toc__sub .wi-toc__link--h4.wi-toc__link--active { padding-left: 30px; }
|
||||
.wi-toc__sub .wi-toc__link--h5.wi-toc__link--active { padding-left: 46px; }
|
||||
.wi-toc__sub .wi-toc__link--h6.wi-toc__link--active { padding-left: 62px; }
|
||||
|
||||
.wi-toc-drawer__nav .wi-toc__link {
|
||||
font-size: var(--text-sm);
|
||||
padding: 6px 0;
|
||||
|
||||
+13
-13
@@ -55,35 +55,35 @@ import Layout from "../layouts/Layout.astro";
|
||||
th:href="${profile.summary?.profileUrl}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
th:text="#{steam.homepage}">Steam Homepage →</a>
|
||||
th:text="'Steam 主页'">Steam Homepage →</a>
|
||||
</div>
|
||||
<div class="wi-steam__level" th:if="${profile.steamLevel != null}">
|
||||
<span class="wi-steam__level-num" th:text="${profile.steamLevel}"></span>
|
||||
<span class="wi-steam__level-label" th:text="#{steam.level}">Level</span>
|
||||
<span class="wi-steam__level-label" th:text="'等级'">等级</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wi-steam__stats" th:if="${stats != null}">
|
||||
<div class="wi-steam__stat-card">
|
||||
<span class="wi-steam__stat-value" th:text="${stats.totalGames} ?: 0">0</span>
|
||||
<span class="wi-steam__stat-label" th:text="#{steam.ownedGames}">Owned Games</span>
|
||||
<span class="wi-steam__stat-label" th:text="'拥有游戏'">拥有游戏</span>
|
||||
</div>
|
||||
<div class="wi-steam__stat-card">
|
||||
<span class="wi-steam__stat-value" th:text="${stats.totalPlaytimeFormatted} ?: '0 hours'">0 hours</span>
|
||||
<span class="wi-steam__stat-label" th:text="#{steam.totalPlaytime}">Total Playtime</span>
|
||||
<span class="wi-steam__stat-label" th:text="'总游玩时长'">总游玩时长</span>
|
||||
</div>
|
||||
<div class="wi-steam__stat-card">
|
||||
<span class="wi-steam__stat-value" th:text="${stats.recentPlaytimeFormatted} ?: '0 hours'">0 hours</span>
|
||||
<span class="wi-steam__stat-label" th:text="#{steam.recentPlaytime}">Recent 2 Weeks</span>
|
||||
<span class="wi-steam__stat-label" th:text="'近两周'">近两周</span>
|
||||
</div>
|
||||
<div class="wi-steam__stat-card">
|
||||
<span class="wi-steam__stat-value" th:text="${badges != null and badges.totalBadges != null ? badges.totalBadges : 0}">0</span>
|
||||
<span class="wi-steam__stat-label" th:text="#{steam.badges}">Badges</span>
|
||||
<span class="wi-steam__stat-label" th:text="'徽章'">徽章</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wi-steam__section" th:if="${recentGames != null and !recentGames.isEmpty()}">
|
||||
<h3 class="wi-steam__section-title" th:text="#{steam.recentlyPlayed}">Recently Played</h3>
|
||||
<h3 class="wi-steam__section-title" th:text="'最近游玩'">最近游玩</h3>
|
||||
<div class="wi-steam__recent-scroll">
|
||||
<article class="wi-steam__recent-card" th:each="game : ${recentGames}">
|
||||
<a
|
||||
@@ -107,7 +107,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
<div class="wi-steam__recent-body">
|
||||
<h4 class="wi-steam__recent-name" th:text="${game.name}"></h4>
|
||||
<div class="wi-steam__recent-meta">
|
||||
<span class="wi-steam__recent-time" th:text="${game.playtime2WeeksFormatted} ?: #{steam.hours}"></span>
|
||||
<span class="wi-steam__recent-time" th:text="${game.playtime2WeeksFormatted} ?: '小时'"></span>
|
||||
<span
|
||||
class="wi-steam__recent-achievements"
|
||||
th:if="${game.achievementProgressText != null}"
|
||||
@@ -122,7 +122,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
</th:block>
|
||||
|
||||
<div class="wi-steam__section">
|
||||
<h3 class="wi-steam__section-title" th:text="#{steam.gameLibrary}">Game Library</h3>
|
||||
<h3 class="wi-steam__section-title" th:text="'游戏库'">游戏库</h3>
|
||||
<div class="wi-steam__games-grid">
|
||||
<article class="wi-steam__game-card" th:each="game : ${games.items}">
|
||||
<a
|
||||
@@ -146,11 +146,11 @@ import Layout from "../layouts/Layout.astro";
|
||||
<div class="wi-steam__game-body">
|
||||
<h4 class="wi-steam__game-name" th:text="${game.name}"></h4>
|
||||
<div class="wi-steam__game-meta">
|
||||
<span class="wi-steam__game-time" th:text="${game.playtimeFormatted} ?: #{steam.hours}"></span>
|
||||
<span class="wi-steam__game-time" th:text="${game.playtimeFormatted} ?: '小时'"></span>
|
||||
<span
|
||||
class="wi-steam__game-last"
|
||||
th:if="${!#strings.isEmpty(game.lastPlayedFormatted)}"
|
||||
th:text="#{steam.lastPlayed(${game.lastPlayedFormatted})}"
|
||||
th:text="|最后游玩 ${game.lastPlayedFormatted}|"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -166,13 +166,13 @@ import Layout from "../layouts/Layout.astro";
|
||||
class="wi-steam__page-btn"
|
||||
th:if="${games.hasPrevious()}"
|
||||
th:href="${games.prevUrl}"
|
||||
th:text="#{common.newer}">Previous</a>
|
||||
th:text="'较新'">较新</a>
|
||||
<span class="wi-steam__page-spacer" th:if="${games.hasPrevious() and games.hasNext()}"></span>
|
||||
<a
|
||||
class="wi-steam__page-btn"
|
||||
th:if="${games.hasNext()}"
|
||||
th:href="${games.nextUrl}"
|
||||
th:text="#{common.older}">Next</a>
|
||||
th:text="'较旧'">较旧</a>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+4
-4
@@ -11,7 +11,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
<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="#{common.postCount(${posts.total})}"></p>
|
||||
<p class="wi-tag__subtitle" th:text="|共 ${posts.total} 篇文章|">共 0 篇文章</p>
|
||||
<p
|
||||
th:if="${not #strings.isEmpty(tag.spec.description)}"
|
||||
class="wi-tag__description"
|
||||
@@ -55,7 +55,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
<div
|
||||
th:if="${posts.total == 0}"
|
||||
class="wi-tag__empty"
|
||||
th:text="${theme.config?.home?.home_label_no_posts ?: #messages.msg('common.noPosts')}"
|
||||
th:text="${theme.config?.home?.home_label_no_posts ?: '暂无文章'}"
|
||||
>No posts yet.</div>
|
||||
|
||||
<nav
|
||||
@@ -67,7 +67,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
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 ?: #messages.msg('common.newer')}"
|
||||
th:text="${theme.config?.home?.home_label_newer ?: '较新'}"
|
||||
>← Newer</a>
|
||||
<span
|
||||
class="wi-tag__page-info"
|
||||
@@ -77,7 +77,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
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 ?: #messages.msg('common.older')}"
|
||||
th:text="${theme.config?.home?.home_label_older ?: '较旧'}"
|
||||
>Older →</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -4,14 +4,14 @@ import Layout from "../layouts/Layout.astro";
|
||||
|
||||
<Layout>
|
||||
<Fragment slot="head">
|
||||
<title th:text="|#{tags.title} - ${site.title}|"></title>
|
||||
<title th:text="|标签 - ${site.title}|"></title>
|
||||
</Fragment>
|
||||
|
||||
<div class="wi-tags">
|
||||
<header class="wi-tags__header">
|
||||
<h1 class="wi-tags__title" th:text="#{tags.title}">标签</h1>
|
||||
<h1 class="wi-tags__title" th:text="'标签'">标签</h1>
|
||||
<span class="wi-page-accent"></span>
|
||||
<p class="wi-tags__subtitle" th:text="${#messages.msg('common.tagCount', tags.size())}"></p>
|
||||
<p class="wi-tags__subtitle" th:text="|共 ${tags.size()} 个标签|">共 0 个标签</p>
|
||||
</header>
|
||||
|
||||
<div class="wi-tags__cloud" role="list" aria-label="Tag list">
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
---
|
||||
|
||||
<Layout wide>
|
||||
<Fragment slot="head">
|
||||
<title th:text="|${theme.config?.weread?.weread_page_title ?: '书架'} - ${site.title}|"></title>
|
||||
</Fragment>
|
||||
|
||||
<section class="wi-weread">
|
||||
<div class="wi-container">
|
||||
<div class="wi-weread__header">
|
||||
<h1
|
||||
class="wi-weread__title"
|
||||
th:text="${theme.config?.weread?.weread_page_title ?: '书架'}"
|
||||
>
|
||||
书架
|
||||
</h1>
|
||||
<p
|
||||
class="wi-weread__subtitle"
|
||||
th:if="${theme.config?.weread?.weread_page_subtitle}"
|
||||
th:text="${theme.config?.weread?.weread_page_subtitle}"
|
||||
>
|
||||
我在读的书
|
||||
</p>
|
||||
<span class="wi-page-accent"></span>
|
||||
</div>
|
||||
|
||||
<th:block th:if="${pluginFinder.available('halo-weread-plugin')}">
|
||||
<div
|
||||
class="wi-weread__shelf"
|
||||
id="weread-shelf-wrapper"
|
||||
th:attr="data-endpoint=@{/halo-weread-plugin/shelf-html}"
|
||||
>
|
||||
<p class="wi-weread__loading">正在展开书架…</p>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<div
|
||||
class="wi-weread__empty"
|
||||
th:unless="${pluginFinder.available('halo-weread-plugin')}"
|
||||
>
|
||||
<p>
|
||||
该功能需要安装「微信读书(WeRead)信息展示」插件,<a
|
||||
href="https://github.com/yeora26/halo-weread-plugin"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>前往插件仓库了解</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script is:inline th:inline="none">
|
||||
(function () {
|
||||
var container = document.getElementById("weread-shelf-wrapper");
|
||||
if (!container) return;
|
||||
|
||||
var endpoint =
|
||||
container.getAttribute("data-endpoint") ||
|
||||
"/halo-weread-plugin/shelf-html";
|
||||
|
||||
fetch(endpoint)
|
||||
.then(function (res) {
|
||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||
return res.text();
|
||||
})
|
||||
.then(function (html) {
|
||||
container.innerHTML = html;
|
||||
|
||||
/* innerHTML 插入的脚本不会执行,需要重建后才会运行 */
|
||||
var oldScripts = container.querySelectorAll("script");
|
||||
Array.prototype.forEach.call(oldScripts, function (oldScript) {
|
||||
var script = document.createElement("script");
|
||||
script.textContent = oldScript.textContent;
|
||||
oldScript.parentNode.replaceChild(script, oldScript);
|
||||
});
|
||||
})
|
||||
.catch(function () {
|
||||
container.innerHTML =
|
||||
'<p class="wi-weread__loading">书架暂时无法展开,请稍后再试。</p>';
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.wi-weread {
|
||||
padding-block: 2rem 3rem;
|
||||
}
|
||||
|
||||
.wi-weread__header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--space-2xl);
|
||||
}
|
||||
|
||||
.wi-weread__title {
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--text-3xl);
|
||||
font-weight: 700;
|
||||
color: var(--ink);
|
||||
letter-spacing: var(--tracking-tight);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.wi-weread__subtitle {
|
||||
font-size: var(--text-base);
|
||||
color: var(--ink-3);
|
||||
line-height: var(--leading-relaxed);
|
||||
}
|
||||
|
||||
.wi-weread__shelf {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.wi-weread__loading {
|
||||
text-align: center;
|
||||
color: var(--ink-3);
|
||||
font-size: var(--text-sm);
|
||||
padding: var(--space-2xl) 0;
|
||||
}
|
||||
|
||||
.wi-weread__empty {
|
||||
text-align: center;
|
||||
padding: var(--space-2xl) var(--space-lg);
|
||||
border: 1px dashed var(--rule);
|
||||
border-radius: var(--radius-lg, 16px);
|
||||
color: var(--ink-3);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.wi-weread__empty a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.wi-weread__empty a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 插件返回的书架内容:收敛为与主题一致的卡片观感 */
|
||||
.wi-weread__shelf :global(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.wi-weread__shelf :global(a) {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.wi-weread__title {
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,331 @@
|
||||
@use 'variables' as *;
|
||||
|
||||
/* ==========================================================================
|
||||
正文排版(文章页 .wi-post__body / 单页 .wi-page__body 共用)
|
||||
--------------------------------------------------------------------------
|
||||
目标:
|
||||
1. 标题层级清晰、字号克制,移动端逐级收敛(而不是反向放大)
|
||||
2. 图片 / 视频 / 嵌入 iframe / figure 一律水平居中且不破坏响应式
|
||||
========================================================================== */
|
||||
|
||||
/* 正文容器:运行时由 Thymeleaf 注入内容,因此这里的规则必须是全局的,
|
||||
不能写进组件的 scoped <style>(那样不会命中注入的内容)。 */
|
||||
.wi-post__body,
|
||||
.wi-page__body,
|
||||
.wi-messageboard__body {
|
||||
/* ---------- 标题层级 ---------- */
|
||||
:is(h1, h2, h3, h4, h5, h6) {
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--ink);
|
||||
scroll-margin-top: 88px;
|
||||
text-wrap: balance;
|
||||
/* 覆盖全局标题装饰(左侧竖线 / 圆点),正文内保持干净 */
|
||||
padding-left: 0;
|
||||
border-left: 0;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(1.45rem, 1.3rem + 0.7vw, 1.75rem);
|
||||
margin-top: 2em;
|
||||
margin-bottom: 0.7em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: clamp(1.3rem, 1.2rem + 0.5vw, 1.5rem);
|
||||
margin-top: 1.9em;
|
||||
margin-bottom: 0.65em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: clamp(1.15rem, 1.08rem + 0.35vw, 1.28rem);
|
||||
margin-top: 1.7em;
|
||||
margin-bottom: 0.6em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: clamp(1.05rem, 1rem + 0.25vw, 1.13rem);
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1rem;
|
||||
margin-top: 1.4em;
|
||||
margin-bottom: 0.45em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--ink-3);
|
||||
margin-top: 1.4em;
|
||||
margin-bottom: 0.45em;
|
||||
}
|
||||
|
||||
/* 标题紧跟在另一个标题后时去掉多余上间距 */
|
||||
:is(h1, h2, h3, h4, h5, h6) + :is(h1, h2, h3, h4, h5, h6) {
|
||||
margin-top: 0.6em;
|
||||
}
|
||||
|
||||
/* 段落内首个元素不需要上间距 */
|
||||
> :is(h1, h2, h3, h4, h5, h6):first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-block-end: 1.2em;
|
||||
}
|
||||
|
||||
/* ---------- 代码块 / 引用 / 列表 ---------- */
|
||||
|
||||
* {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
:is(pre, code, table) {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-block: 1.5em;
|
||||
padding: 1.25em;
|
||||
border-radius: $border-radius-md;
|
||||
border: 1px solid var(--rule);
|
||||
background: var(--bg-raised);
|
||||
font-size: 0.875em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
code {
|
||||
background: var(--bg-raised);
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: $border-radius-sm;
|
||||
font-size: 0.875em;
|
||||
border: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin-block: 2em;
|
||||
}
|
||||
|
||||
:is(ul, ol) {
|
||||
padding-inline-start: 1.5em;
|
||||
margin-block-end: 1.2em;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-block-end: 0.6em;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 0.2em;
|
||||
}
|
||||
|
||||
/* 第三方摘要插件容器 */
|
||||
:is(.ai-summary, .abstract),
|
||||
[class*='ai-summary'],
|
||||
[class*='ai-abstract'],
|
||||
[class*='likcc-article-summary'] {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* ---------- 媒体居中 ---------- */
|
||||
|
||||
/* 图片 / 视频:块级 + 自动外边距实现居中,max-width 保证不溢出容器 */
|
||||
img,
|
||||
video {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
border-radius: $border-radius-md;
|
||||
margin-block: 1.5em;
|
||||
}
|
||||
|
||||
/* 行内小图标 / 表情不受居中规则影响 */
|
||||
img[role='presentation'],
|
||||
img[aria-hidden='true'],
|
||||
:is(.emoji, .icon, .avatar, .badge) {
|
||||
display: inline;
|
||||
margin-inline: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* 编辑器可能输出 align="center|left|right" 或 text-align 样式,保持一致居中 */
|
||||
[align='center'],
|
||||
[style*='text-align:center'],
|
||||
[style*='text-align: center'] {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
iframe {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
margin-inline: auto;
|
||||
border: none;
|
||||
}
|
||||
|
||||
figure {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
margin-inline: auto;
|
||||
margin-block: 1.5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
figure :is(img, video) {
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
color: var(--ink-3);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
margin-top: 0.6em;
|
||||
}
|
||||
|
||||
/* lightgallery 注入的 <a> 包裹层:收缩到图片宽度并居中 */
|
||||
.wi-lightgallery-item {
|
||||
display: block;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
margin-inline: auto;
|
||||
border-radius: $border-radius-md;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.wi-lightgallery-item > img {
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
/* 响应式嵌入容器(由脚本按 width/height 生成 aspect-ratio) */
|
||||
.wi-embed {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-inline: auto;
|
||||
margin-block: 1.5em;
|
||||
overflow: hidden;
|
||||
border-radius: $border-radius-md;
|
||||
background: var(--bg-raised);
|
||||
}
|
||||
|
||||
.wi-embed > :is(iframe, video, embed, object) {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* 图片懒加载占位模糊 */
|
||||
.wi-media-loading {
|
||||
filter: blur(20px);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.wi-media-loaded {
|
||||
filter: blur(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端:标题逐级收敛,正文行高放宽 */
|
||||
@media (max-width: 768px) {
|
||||
.wi-post__body,
|
||||
.wi-page__body,
|
||||
.wi-messageboard__body {
|
||||
h1 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.28rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.16rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-block-end: 1.35em;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 0.9em;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
margin-block: 1.2em;
|
||||
}
|
||||
|
||||
/* 窄屏图片允许撑满内容宽度,避免留白过大 */
|
||||
img,
|
||||
video,
|
||||
.wi-lightgallery-item,
|
||||
figure {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.wi-post__body,
|
||||
.wi-page__body,
|
||||
.wi-messageboard__body {
|
||||
h1 {
|
||||
font-size: 1.32rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.22rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.12rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,19 +39,19 @@ h1, h2, h3, h4, h5, h6 {
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--text-4xl);
|
||||
font-size: var(--text-3xl);
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--text-3xl);
|
||||
font-size: var(--text-2xl);
|
||||
padding-left: var(--space-md);
|
||||
border-left: 3px solid var(--accent);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--text-2xl);
|
||||
font-size: var(--text-xl);
|
||||
position: relative;
|
||||
padding-left: var(--space-lg);
|
||||
|
||||
@@ -71,16 +71,16 @@ h3 {
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: var(--text-xl);
|
||||
font-size: var(--text-md);
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: var(--text-lg);
|
||||
font-size: var(--text-base);
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: var(--text-base);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--ink-3);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
@use 'mixins';
|
||||
@use 'colors';
|
||||
@use 'typography';
|
||||
@use 'content';
|
||||
@use 'spacing';
|
||||
@use 'animations';
|
||||
@use 'comment';
|
||||
|
||||
Reference in new issue
Block a user