Files
halo-theme-whitespace/src/links.html
T

540 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<include src="partials/layout.html">
<template name="head">
<title th:text="|${linksTitle} - ${site.title}|">链接</title>
<style>
.links-nav {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin: 1rem 0 1.5rem;
}
.links-nav a {
color: var(--color-link);
}
.links-nav a:hover,
.links-nav a.active {
color: var(--color-link-hover);
font-weight: 600;
}
.links-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(18rem, 100%), 1fr));
gap: 1rem;
}
.links-list li {
margin: 0;
display: flex;
}
.links-list a {
display: flex;
align-items: flex-start;
gap: 0.8rem;
padding: 1.1rem 1.25rem;
width: 100%;
box-sizing: border-box;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg, 0.75rem);
background: var(--color-bg-secondary);
color: var(--color-text);
transition:
border-color var(--transition-fast),
box-shadow var(--transition-fast);
}
.links-list a:hover {
border-color: var(--color-primary);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
}
.links-avatar {
position: relative;
display: block;
flex-shrink: 0;
}
.links-avatar img {
display: block;
width: 2.75rem;
height: 2.75rem;
border-radius: var(--radius-md);
object-fit: cover;
background: var(--color-bg);
}
/* 头像右下角状态圆点 */
.links-avatar::after {
content: "";
position: absolute;
right: -0.1rem;
bottom: -0.1rem;
width: 0.55rem;
height: 0.55rem;
border-radius: 50%;
background: #2ea043;
border: 2px solid var(--color-bg-secondary);
}
.links-list .links-body {
display: flex;
flex-direction: column;
min-width: 0;
padding-top: 0.1rem;
}
.links-list .links-name {
font-weight: 600;
line-height: 1.4;
}
.links-list .links-desc {
margin: 0.3rem 0 0;
font-size: 0.85rem;
line-height: 1.55;
color: var(--color-text-secondary);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.links-group-title {
font-size: 1.1rem;
margin: 2rem 0 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-border);
}
.links-section:first-of-type .links-group-title {
margin-top: 0;
}
/* 申请友链按钮 */
.links-apply {
margin-top: 2.5rem;
text-align: center;
}
.btn-apply {
display: inline-block;
padding: 0.6rem 1.75rem;
border: 1px solid var(--color-primary);
border-radius: var(--radius-full);
background: var(--color-primary);
color: #fff;
font: inherit;
font-size: 0.95rem;
cursor: pointer;
transition:
opacity var(--transition-fast),
transform var(--transition-fast);
}
.btn-apply:hover {
opacity: 0.88;
}
.btn-apply:active {
transform: scale(0.97);
}
/* 申请弹窗(运行时移至 body 下,避免被父级 stacking context 遮挡) */
.link-apply-modal[hidden] {
display: none;
}
.link-apply-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 1000;
}
.link-apply-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: min(26rem, calc(100vw - 2rem));
max-height: min(85vh, 44rem);
overflow-y: auto;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg, 0.75rem);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
padding: 1.5rem;
z-index: 1001;
}
.link-apply-close {
position: absolute;
top: 0.75rem;
right: 0.75rem;
width: 2rem;
height: 2rem;
border: none;
border-radius: 50%;
background: transparent;
color: var(--color-text-muted);
font-size: 1.4rem;
line-height: 1;
cursor: pointer;
}
.link-apply-close:hover {
color: var(--color-text);
background: var(--color-bg-secondary);
}
.link-apply-title {
margin: 0 0 1rem;
font-size: 1.125rem;
padding-right: 2rem;
}
.apply-form .form-row {
margin-bottom: 1rem;
}
.apply-form label {
display: block;
margin-bottom: 0.35rem;
font-weight: 600;
color: var(--color-text);
}
.apply-form input,
.apply-form textarea {
width: 100%;
padding: 0.6rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-bg);
color: var(--color-text);
font: inherit;
}
.apply-form textarea {
min-height: 5rem;
resize: vertical;
}
.apply-captcha {
display: flex;
gap: 0.75rem;
align-items: center;
}
.apply-captcha img {
height: 2.75rem;
border-radius: var(--radius-sm);
border: 1px solid var(--color-border);
cursor: pointer;
}
.apply-status {
margin-top: 1rem;
padding: 0.75rem 1rem;
border-radius: var(--radius-sm);
display: none;
}
.apply-status.ok {
display: block;
background: rgba(46, 160, 67, 0.12);
color: #2ea043;
border: 1px solid rgba(46, 160, 67, 0.4);
}
.apply-status.err {
display: block;
background: rgba(229, 57, 53, 0.12);
color: #e53935;
border: 1px solid rgba(229, 57, 53, 0.4);
}
</style>
</template>
<div class="container">
<section class="links">
<h1 th:text="${linksTitle}">链接</h1>
<nav class="links-nav" aria-label="链接分组">
<a th:href="@{/links}" th:classappend="${group == null} ? 'active'">全部</a>
<a
th:each="item : ${simpleGroups}"
th:href="@{/links(group=${item.metadata.name})}"
th:classappend="${group == item.metadata.name} ? 'active'"
th:text="${item.spec.displayName}"
></a>
</nav>
<th:block th:if="${group == null}">
<section class="links-section" th:each="grp : ${simpleGroups}">
<h2 class="links-group-title" th:text="${grp.spec.displayName}">
分组
</h2>
<ul class="links-list">
<li
th:each="link : ${links}"
th:if="${link.spec.groupName == grp.metadata.name}"
>
<a
th:href="@{${link.spec.url}}"
target="_blank"
rel="noopener noreferrer"
>
<span class="links-avatar">
<img
th:if="${not #strings.isEmpty(link.spec.logo)}"
th:src="@{${link.spec.logo}}"
alt=""
loading="lazy"
/>
</span>
<span class="links-body">
<span class="links-name" th:text="${link.spec.displayName}"
>链接名称</span
>
<p
class="links-desc"
th:if="${not #strings.isEmpty(link.spec.description)}"
th:text="${link.spec.description}"
></p>
</span>
</a>
</li>
</ul>
</section>
<section class="links-section">
<h2 class="links-group-title">未分组</h2>
<ul class="links-list">
<li
th:each="link : ${links}"
th:if="${#strings.isEmpty(link.spec.groupName)}"
>
<a
th:href="@{${link.spec.url}}"
target="_blank"
rel="noopener noreferrer"
>
<span class="links-avatar">
<img
th:if="${not #strings.isEmpty(link.spec.logo)}"
th:src="@{${link.spec.logo}}"
alt=""
loading="lazy"
/>
</span>
<span class="links-body">
<span class="links-name" th:text="${link.spec.displayName}"
>链接名称</span
>
<p
class="links-desc"
th:if="${not #strings.isEmpty(link.spec.description)}"
th:text="${link.spec.description}"
></p>
</span>
</a>
</li>
</ul>
</section>
</th:block>
<th:block th:unless="${group == null}">
<ul th:if="${not #lists.isEmpty(links)}" class="links-list">
<li th:each="link : ${links}">
<a
th:href="@{${link.spec.url}}"
target="_blank"
rel="noopener noreferrer"
>
<span class="links-avatar">
<img
th:if="${not #strings.isEmpty(link.spec.logo)}"
th:src="@{${link.spec.logo}}"
alt=""
loading="lazy"
/>
</span>
<span class="links-body">
<span class="links-name" th:text="${link.spec.displayName}"
>链接名称</span
>
<p
class="links-desc"
th:if="${not #strings.isEmpty(link.spec.description)}"
th:text="${link.spec.description}"
></p>
</span>
</a>
</li>
</ul>
<p th:if="${#lists.isEmpty(links)}">暂无链接</p>
</th:block>
<div class="links-apply">
<button
class="btn-apply"
id="link-apply-toggle"
type="button"
th:text="#{links.apply}"
>申请友链</button>
</div>
</section>
<!-- 申请友链弹窗:脚本运行时移至 body 下 -->
<div class="link-apply-modal" id="link-apply-modal" hidden>
<div class="link-apply-backdrop" data-close></div>
<div
class="link-apply-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="link-apply-title"
>
<button
class="link-apply-close"
type="button"
data-close
aria-label="关闭"
>
×
</button>
<h2 class="link-apply-title" id="link-apply-title" th:text="#{links.apply}">
申请友链
</h2>
<form class="apply-form" id="link-apply-form" novalidate>
<div class="form-row">
<label for="f-url">网站地址 *</label>
<input id="f-url" name="url" type="url" required placeholder="https://example.com" />
</div>
<div class="form-row">
<label for="f-name">网站名称 *</label>
<input id="f-name" name="displayName" type="text" required placeholder="我的站点" />
</div>
<div class="form-row">
<label for="f-logo">Logo 地址</label>
<input id="f-logo" name="logo" type="url" placeholder="https://example.com/logo.png" />
</div>
<div class="form-row">
<label for="f-desc">网站描述</label>
<input id="f-desc" name="description" type="text" placeholder="一句话介绍你的站点" />
</div>
<div class="form-row">
<label for="f-email">联系邮箱</label>
<input id="f-email" name="email" type="email" placeholder="you@example.com" />
</div>
<div class="form-row">
<label for="f-backlink">反链页面</label>
<input id="f-backlink" name="backlink" type="url" placeholder="含本站链接的页面地址" />
</div>
<div class="form-row">
<label for="f-feeds">RSS / Atom 地址(每行一个)</label>
<textarea id="f-feeds" name="feedUrls" placeholder="https://example.com/feed.xml"></textarea>
</div>
<div class="form-row">
<label for="f-captcha">验证码 *</label>
<div class="apply-captcha">
<img id="captcha-img" alt="验证码" title="点击刷新" />
<input id="f-captcha" name="captchaCode" type="text" maxlength="5" required autocomplete="off" />
</div>
</div>
<button class="btn-apply" type="submit">提交申请</button>
</form>
<div class="apply-status" id="apply-status" role="alert"></div>
</div>
</div>
<div class="comments-section" th:if="${haloCommentEnabled}">
<halo:comment group="plugin.halo.run" kind="Plugin" th:attr="name=${pluginName}" />
</div>
</div>
<script th:inline="none">
(function () {
// 收起没有链接的分组区块
document.querySelectorAll(".links-section").forEach(function (sec) {
if (!sec.querySelector(".links-list li")) {
sec.remove();
}
});
var API = "/apis/api.link.halo.run/v1alpha1/link-applications";
var CAPTCHA = "/apis/api.link.halo.run/v1alpha1/link-applications/captcha";
var toggle = document.getElementById("link-apply-toggle");
var modal = document.getElementById("link-apply-modal");
var form = document.getElementById("link-apply-form");
var img = document.getElementById("captcha-img");
var status = document.getElementById("apply-status");
var challengeId = null;
// 弹窗移至 body 下,避免被页面父级 stacking context 遮挡
document.body.appendChild(modal);
function openModal() {
modal.hidden = false;
document.body.style.overflow = "hidden";
if (!challengeId) loadCaptcha();
}
function closeModal() {
modal.hidden = true;
document.body.style.overflow = "";
}
toggle.addEventListener("click", openModal);
modal.addEventListener("click", function (e) {
if (e.target.closest("[data-close]")) closeModal();
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && !modal.hidden) closeModal();
});
function loadCaptcha() {
fetch(CAPTCHA, { method: "POST" })
.then(function (r) {
if (!r.ok) throw new Error("captcha");
return r.json();
})
.then(function (data) {
challengeId = data.challengeId;
img.src = data.image;
})
.catch(function () {
status.className = "apply-status err";
status.textContent = "验证码加载失败,请刷新页面重试。";
});
}
img.addEventListener("click", loadCaptcha);
form.addEventListener("submit", function (e) {
e.preventDefault();
status.className = "apply-status";
status.textContent = "";
if (!challengeId) {
status.className = "apply-status err";
status.textContent = "请先点击「申请友链」并等待验证码加载。";
return;
}
var fd = new FormData(form);
var feeds = (fd.get("feedUrls") || "")
.split(/\r?\n/)
.map(function (s) { return s.trim(); })
.filter(Boolean);
var payload = {
url: fd.get("url"),
displayName: fd.get("displayName"),
logo: fd.get("logo") || undefined,
description: fd.get("description") || undefined,
email: fd.get("email") || undefined,
backlink: fd.get("backlink") || undefined,
feedUrls: feeds,
challengeId: challengeId,
captchaCode: fd.get("captchaCode")
};
fetch(API, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
})
.then(function (r) {
if (r.status === 201) {
form.reset();
challengeId = null;
loadCaptcha();
status.className = "apply-status ok";
status.textContent = "申请已提交,等待管理员审核。";
setTimeout(closeModal, 1500);
} else {
return r.json().then(function (err) {
throw err;
});
}
})
.catch(function () {
loadCaptcha();
status.className = "apply-status err";
status.textContent = "提交失败,请检查信息后重试(验证码已刷新)。";
});
});
})();
</script>
</include>