- Hero:背景图 opacity 0.15 -> 1 完整显示(此前近乎透明的背景图是"蒙灰"观感的 主因);标题/副标题/一言按明暗模式补轻投影保证复杂图片上的可读性 - 导航栏二级菜单:桌面端纯 CSS 悬停/聚焦展开(零 JS),带悬停桥防误关、 下拉箭头旋转反馈;移动端菜单以缩进子项形式呈现 - 瞬间页朋友圈化:头像(moment.owner.avatar,缺省回退站点名首字占位)+ 昵称 + 正文 + 九宫格图片 + 时间操作;九宫格遵循微信规则(单图原比例、2/4 图两列、 其余三列) - 友情链接页适配 plugin-links 友链申请:同源表单(POST /links/apply/submit + csrfToken + GET /links/apply/captcha 验证码图,点击刷新),支持名称/链接/ Logo/描述/邮箱/反链/RSS 字段、提交结果与错误提示;插件未开放申请时验证码 加载失败自动隐藏表单;新增 links_apply_form_enabled 开关(默认开启)
609 lines
18 KiB
Plaintext
609 lines
18 KiB
Plaintext
---
|
|
import Layout from "../layouts/Layout.astro";
|
|
---
|
|
|
|
<Layout wide>
|
|
<Fragment slot="head">
|
|
<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'},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 ?: '友链'}"
|
|
>
|
|
友情链接
|
|
</h1>
|
|
<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}">
|
|
<h2
|
|
class="wi-links__group"
|
|
th:if="${group.spec.displayName != null and !#strings.isEmpty(group.spec.displayName)}"
|
|
th:text="${group.spec.displayName}"
|
|
></h2>
|
|
<div class="wi-links__grid">
|
|
<a
|
|
th:each="link : ${group.links}"
|
|
th:href="${link.spec.url}"
|
|
th:title="${link.spec.displayName}"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="wi-links__card"
|
|
>
|
|
<div class="wi-links__card-glow"></div>
|
|
<div class="wi-links__card-inner">
|
|
<img
|
|
th:if="${link.spec.logo}"
|
|
th:src="${link.spec.logo}"
|
|
th:alt="${link.spec.displayName}"
|
|
class="wi-links__avatar"
|
|
/>
|
|
<div
|
|
th:unless="${link.spec.logo}"
|
|
class="wi-links__avatar wi-links__avatar--placeholder"
|
|
>
|
|
<span
|
|
th:text="${#strings.substring(link.spec.displayName, 0, 1)}"
|
|
></span>
|
|
</div>
|
|
<div class="wi-links__info">
|
|
<span
|
|
class="wi-links__name"
|
|
th:text="${link.spec.displayName}"
|
|
></span>
|
|
<span
|
|
class="wi-links__desc"
|
|
th:if="${link.spec.description}"
|
|
th:text="${link.spec.description}"
|
|
></span>
|
|
</div>
|
|
<svg class="wi-links__arrow" 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="M7 17L17 7"/><path d="M7 7h10v10"/></svg>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
</th:block>
|
|
|
|
<div
|
|
class="wi-links__apply"
|
|
th:if="${theme.config?.links?.links_apply_form_enabled != false}"
|
|
th:with="applied=${param.applied != null and !#lists.isEmpty(param.applied) and param.applied[0] == 'true'},
|
|
applyMsg=${param.message != null and !#lists.isEmpty(param.message) ? param.message[0] : null}"
|
|
>
|
|
<h2 class="wi-links__apply-title" th:text="'申请友链'">申请友链</h2>
|
|
<p
|
|
class="wi-links__apply-result"
|
|
th:if="${applied}"
|
|
th:text="'申请已提交,等待站长审核 🌙'"
|
|
role="status"
|
|
></p>
|
|
<p
|
|
class="wi-links__apply-result wi-links__apply-result--error"
|
|
th:if="${not applied and applyMsg != null}"
|
|
th:text="${applyMsg}"
|
|
role="alert"
|
|
></p>
|
|
|
|
<form
|
|
class="wi-links__apply-form"
|
|
method="post"
|
|
th:action="@{/links/apply/submit}"
|
|
>
|
|
<input type="hidden" name="_csrf" th:value="${csrfToken}" />
|
|
<div class="wi-links__apply-row">
|
|
<label class="wi-links__apply-field">
|
|
<span th:text="'站点名称 *'">站点名称 *</span>
|
|
<input type="text" name="displayName" required maxlength="50" placeholder="你的站点名称" th:attr="value=${param.value != null and !#lists.isEmpty(param.value) ? param.value[0] : ''}" />
|
|
</label>
|
|
<label class="wi-links__apply-field">
|
|
<span th:text="'站点链接 *'">站点链接 *</span>
|
|
<input type="url" name="url" required placeholder="https://" />
|
|
</label>
|
|
</div>
|
|
<div class="wi-links__apply-row">
|
|
<label class="wi-links__apply-field">
|
|
<span th:text="'图标地址'">图标地址</span>
|
|
<input type="url" name="logo" placeholder="https://" />
|
|
</label>
|
|
<label class="wi-links__apply-field">
|
|
<span th:text="'联系邮箱'">联系邮箱</span>
|
|
<input type="email" name="email" placeholder="用于通知审核结果" />
|
|
</label>
|
|
</div>
|
|
<label class="wi-links__apply-field">
|
|
<span th:text="'站点描述'">站点描述</span>
|
|
<input type="text" name="description" maxlength="120" placeholder="一句话介绍你的站点" />
|
|
</label>
|
|
<label class="wi-links__apply-field">
|
|
<span th:text="'反链页面'">反链页面</span>
|
|
<input type="url" name="backlink" placeholder="https://" />
|
|
</label>
|
|
<label class="wi-links__apply-field">
|
|
<span th:text="'RSS / Atom 地址(每行一个)'">RSS / Atom 地址(每行一个)</span>
|
|
<textarea name="feedUrls" rows="2" placeholder="https://"></textarea>
|
|
</label>
|
|
<div class="wi-links__apply-row">
|
|
<label class="wi-links__apply-field">
|
|
<span th:text="'验证码 *'">验证码 *</span>
|
|
<input type="text" name="captchaCode" required maxlength="8" autocomplete="off" placeholder="图中五位字符" />
|
|
</label>
|
|
<div class="wi-links__apply-captcha">
|
|
<img
|
|
class="wi-links__apply-captcha-img"
|
|
th:src="@{/links/apply/captcha}"
|
|
alt="验证码"
|
|
width="120"
|
|
height="44"
|
|
loading="lazy"
|
|
title="点击刷新验证码"
|
|
onclick="this.src=this.src.split('?')[0]+'?ts='+Date.now()"
|
|
th:onerror="|this.closest('.wi-links__apply').classList.add('wi-links__apply--unavailable')|"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<button class="wi-links__apply-submit" type="submit" th:text="'提交申请'">提交申请</button>
|
|
</form>
|
|
<p
|
|
class="wi-links__apply-unavailable-tip"
|
|
th:text="'友链申请暂未开放。'"
|
|
hidden
|
|
></p>
|
|
</div>
|
|
|
|
<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>
|
|
.wi-links {
|
|
padding-block: 1rem;
|
|
}
|
|
|
|
.wi-links__header {
|
|
text-align: center;
|
|
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__apply {
|
|
margin-top: var(--space-3xl);
|
|
padding: var(--space-xl);
|
|
border: 1px solid var(--rule);
|
|
border-radius: var(--radius-lg, 16px);
|
|
background: var(--bg-raised);
|
|
}
|
|
|
|
.wi-links__apply-title {
|
|
text-align: center;
|
|
font-size: var(--text-xl);
|
|
font-weight: 700;
|
|
color: var(--ink);
|
|
margin-bottom: var(--space-lg);
|
|
}
|
|
|
|
.wi-links__apply-result {
|
|
text-align: center;
|
|
font-size: var(--text-sm);
|
|
color: var(--accent);
|
|
margin-bottom: var(--space-md);
|
|
}
|
|
|
|
.wi-links__apply-result--error {
|
|
color: #e6162d;
|
|
}
|
|
|
|
html.dark .wi-links__apply-result--error {
|
|
color: #ff6b6b;
|
|
}
|
|
|
|
.wi-links__apply-form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-md);
|
|
}
|
|
|
|
.wi-links__apply-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: var(--space-md);
|
|
}
|
|
|
|
.wi-links__apply-field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.35rem;
|
|
font-size: var(--text-sm);
|
|
color: var(--ink-2);
|
|
}
|
|
|
|
.wi-links__apply-field input,
|
|
.wi-links__apply-field textarea {
|
|
padding: 0.6rem 0.8rem;
|
|
border: 1px solid var(--rule);
|
|
border-radius: 10px;
|
|
background: var(--bg);
|
|
color: var(--ink);
|
|
font-family: inherit;
|
|
font-size: var(--text-sm);
|
|
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
resize: vertical;
|
|
}
|
|
|
|
.wi-links__apply-field input:focus,
|
|
.wi-links__apply-field textarea:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent);
|
|
}
|
|
|
|
.wi-links__apply-captcha {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
padding-bottom: 2px;
|
|
}
|
|
|
|
.wi-links__apply-captcha-img {
|
|
border-radius: 8px;
|
|
border: 1px solid var(--rule);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.wi-links__apply-submit {
|
|
align-self: center;
|
|
padding: 0.65rem 2.2rem;
|
|
min-height: 44px;
|
|
border: none;
|
|
border-radius: 9999px;
|
|
font-size: var(--text-sm);
|
|
font-weight: 600;
|
|
font-family: inherit;
|
|
color: #fff;
|
|
background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 70%, #000));
|
|
cursor: pointer;
|
|
transition: transform var(--duration-fast) var(--ease-out-quart), opacity 0.15s ease;
|
|
}
|
|
|
|
.wi-links__apply-submit:hover {
|
|
transform: translateY(-1px);
|
|
opacity: 0.95;
|
|
}
|
|
|
|
.wi-links__apply-submit:active {
|
|
transform: translateY(0) scale(0.98);
|
|
}
|
|
|
|
/* 插件未开放申请时(验证码 404)自动隐藏表单并提示 */
|
|
.wi-links__apply--unavailable .wi-links__apply-form {
|
|
display: none;
|
|
}
|
|
|
|
.wi-links__apply--unavailable .wi-links__apply-unavailable-tip {
|
|
display: block !important;
|
|
text-align: center;
|
|
color: var(--ink-3);
|
|
font-size: var(--text-sm);
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.wi-links__apply-row {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
.wi-links__title {
|
|
font-size: var(--text-4xl);
|
|
margin-bottom: var(--space-xs);
|
|
}
|
|
|
|
.wi-links__subtitle {
|
|
font-size: var(--text-sm);
|
|
color: var(--ink-3);
|
|
letter-spacing: var(--tracking-wide);
|
|
margin-block-end: 0;
|
|
}
|
|
|
|
.wi-page-accent {
|
|
display: block;
|
|
width: 48px;
|
|
height: 3px;
|
|
margin: var(--space-md) auto 0;
|
|
border-radius: 9999px;
|
|
background: var(--accent);
|
|
}
|
|
|
|
.wi-links__group {
|
|
font-size: var(--text-lg);
|
|
color: var(--ink-2);
|
|
margin-bottom: var(--space-lg);
|
|
padding-bottom: var(--space-sm);
|
|
border-bottom: 1px solid var(--rule);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.wi-links__grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: var(--space-md);
|
|
margin-bottom: var(--space-2xl);
|
|
}
|
|
|
|
.wi-links[data-style="list"] .wi-links__grid {
|
|
grid-template-columns: 1fr;
|
|
max-width: 720px;
|
|
}
|
|
|
|
.wi-links[data-style="list"] .wi-links__card-inner {
|
|
border-radius: 12px;
|
|
padding: var(--space-md) var(--space-lg);
|
|
}
|
|
|
|
.wi-links[data-style="grid"] .wi-links__grid {
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: var(--space-sm);
|
|
}
|
|
|
|
.wi-links[data-style="grid"] .wi-links__card-inner {
|
|
flex-direction: column;
|
|
align-items: center;
|
|
text-align: center;
|
|
padding: var(--space-xl) var(--space-lg);
|
|
}
|
|
|
|
.wi-links[data-style="grid"] .wi-links__arrow {
|
|
display: none;
|
|
}
|
|
|
|
.wi-links__card {
|
|
position: relative;
|
|
border-radius: 16px;
|
|
text-decoration: none;
|
|
color: inherit;
|
|
overflow: hidden;
|
|
display: flex;
|
|
}
|
|
|
|
.wi-links__card-glow {
|
|
position: absolute;
|
|
inset: 0;
|
|
border-radius: 16px;
|
|
opacity: 0;
|
|
background: linear-gradient(
|
|
135deg,
|
|
color-mix(in srgb, var(--accent) 8%, transparent) 0%,
|
|
transparent 60%
|
|
);
|
|
transition: opacity var(--duration-normal) var(--ease-out-expo);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.wi-links__card:hover .wi-links__card-glow {
|
|
opacity: 1;
|
|
}
|
|
|
|
.wi-links__card-inner {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-md);
|
|
padding: var(--space-lg);
|
|
background: var(--glass-bg);
|
|
backdrop-filter: blur(16px);
|
|
-webkit-backdrop-filter: blur(16px);
|
|
border: 1px solid var(--glass-border);
|
|
border-radius: 16px;
|
|
flex: 1;
|
|
transition:
|
|
transform var(--duration-normal) var(--ease-out-expo),
|
|
box-shadow var(--duration-normal) var(--ease-out-expo),
|
|
border-color var(--duration-normal) var(--ease-out-expo);
|
|
}
|
|
|
|
.wi-links__card:hover .wi-links__card-inner {
|
|
transform: translateY(-4px);
|
|
box-shadow: var(--shadow-lg);
|
|
border-color: color-mix(in srgb, var(--accent) 40%, var(--glass-border));
|
|
}
|
|
|
|
.wi-links__avatar {
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: 12px;
|
|
object-fit: cover;
|
|
flex-shrink: 0;
|
|
box-shadow: var(--shadow-sm);
|
|
transition: transform var(--duration-normal) var(--ease-out-expo);
|
|
}
|
|
|
|
.wi-links__card:hover .wi-links__avatar {
|
|
transform: scale(1.08);
|
|
}
|
|
|
|
.wi-links__avatar--placeholder {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--accent-bg);
|
|
color: var(--accent);
|
|
font-family: var(--font-sans);
|
|
font-size: var(--text-lg);
|
|
font-weight: 700;
|
|
box-shadow: none;
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.wi-links__info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
min-width: 0;
|
|
flex: 1;
|
|
}
|
|
|
|
.wi-links__name {
|
|
font-size: var(--text-md);
|
|
font-weight: 600;
|
|
color: var(--ink);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
transition: color var(--duration-fast) var(--ease-out-quart);
|
|
}
|
|
|
|
.wi-links__card:hover .wi-links__name {
|
|
color: var(--accent);
|
|
}
|
|
|
|
.wi-links__desc {
|
|
font-size: var(--text-sm);
|
|
color: var(--ink-3);
|
|
line-height: var(--leading-snug);
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.wi-links__arrow {
|
|
flex-shrink: 0;
|
|
color: var(--ink-3);
|
|
opacity: 0;
|
|
transform: translate(-4px, 4px);
|
|
transition:
|
|
opacity var(--duration-normal) var(--ease-out-expo),
|
|
transform var(--duration-normal) var(--ease-out-expo),
|
|
color var(--duration-fast) var(--ease-out-quart);
|
|
}
|
|
|
|
.wi-links__card:hover .wi-links__arrow {
|
|
opacity: 1;
|
|
transform: translate(0, 0);
|
|
color: var(--accent);
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.wi-links__grid {
|
|
grid-template-columns: 1fr;
|
|
gap: var(--space-sm);
|
|
}
|
|
|
|
.wi-links__card-inner {
|
|
padding: var(--space-md);
|
|
}
|
|
|
|
.wi-links__avatar {
|
|
width: 42px;
|
|
height: 42px;
|
|
}
|
|
}
|
|
</style>
|