chore: 集成 extra-api 统计、页脚备案配置、友链申请内嵌

- extra-api: 文章卡片/详情页展示字数+阅读时长、归档页展示全站总字数、瞬间页展示单条字数(pluginFinder.available 守卫,未安装自动隐藏)
- 页脚新增 ICP 备案号与公网安备号配置(settings.yaml footer 分组,partials/layout.html 渲染并加安全导航守卫)
- 友链申请表单由独立单页改为内嵌 /links 页底部(点击「申请友链」展开),移除独立 links-apply.html 及其自定义模板注册
- 重新打包 dist/whitespace-0.1.0.zip(移除陈旧 1.0.0)
This commit is contained in:
2026-09-28 17:37:16 +08:00
parent 8e6dccbd09
commit 6b8d9ee338
12 changed files with 344 additions and 209 deletions
+204
View File
@@ -56,6 +56,71 @@
font-size: 0.85rem;
color: var(--color-text-secondary);
}
.links-apply {
margin-top: 2rem;
text-align: center;
}
.link-apply-panel {
max-width: 32rem;
margin: 1.25rem auto 0;
text-align: left;
}
.link-apply-title {
margin: 0 0 0.75rem;
font-size: 1.125rem;
}
.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>
@@ -96,10 +161,149 @@
</li>
</ul>
<p th:unless="${not #lists.isEmpty(links)}">暂无链接</p>
<div class="links-apply">
<button
class="btn btn-primary"
id="link-apply-toggle"
type="button"
th:text="#{links.apply}"
>申请友链</button>
<div class="link-apply-panel apply-form" id="link-apply-panel" hidden>
<h2 class="link-apply-title" th:text="#{links.apply}">申请友链</h2>
<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 btn-primary" type="submit">提交申请</button>
</form>
<div class="apply-status" id="apply-status" role="alert"></div>
</div>
</div>
</section>
<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 () {
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 panel = document.getElementById("link-apply-panel");
var form = document.getElementById("link-apply-form");
var img = document.getElementById("captcha-img");
var status = document.getElementById("apply-status");
var challengeId = null;
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 = "验证码加载失败,请刷新页面重试。";
});
}
toggle.addEventListener("click", function () {
panel.hidden = !panel.hidden;
if (!panel.hidden && !challengeId) {
loadCaptcha();
}
});
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 = "申请已提交,等待管理员审核。";
} else {
return r.json().then(function (err) {
throw err;
});
}
})
.catch(function () {
loadCaptcha();
status.className = "apply-status err";
status.textContent = "提交失败,请检查信息后重试(验证码已刷新)。";
});
});
})();
</script>
</include>