feat: connect friend links and friend circle to RSS API

- Friend links page now fetches from /api/links with avatar cards
- Friend circle fetches from /api/articles with real-time data

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
zqlitandClaude Opus 4.7 committed 2026-05-19 19:30:34 +08:00
1 parent 35f3105ae1
commit 23c0657ffc
2 files changed
+196 -252

No files matched your search

+111 -187
View File
@@ -15,9 +15,9 @@
.friend-circle-list { .friend-circle-list {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 12px; /* Tighter gap to make cards wider */ gap: 12px;
margin-top: 20px; margin-top: 20px;
padding: 0; /* Remove padding to maximize width */ padding: 0;
width: 100%; width: 100%;
} }
@@ -29,7 +29,7 @@
} }
} }
/* Card Style - Refined based on User Request */ /* Card Style */
.fc-item { .fc-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -39,89 +39,57 @@
height: 160px; height: 160px;
justify-content: space-between; justify-content: space-between;
background: rgba(255, 255, 255, 0.4); background: rgba(255, 255, 255, 0.4);
/* Enhanced Glassmorphism for non-mobile */
backdrop-filter: blur(12px) saturate(120%); backdrop-filter: blur(12px) saturate(120%);
-webkit-backdrop-filter: blur(12px) saturate(120%); -webkit-backdrop-filter: blur(12px) saturate(120%);
border: 1px solid #111827; border: 1px solid #111827;
box-shadow: none; box-shadow: none;
transition: transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1),
box-shadow 0.3s ease,
background-color 0.3s ease,
border-color 0.3s ease;
will-change: transform; will-change: transform;
text-decoration: none; text-decoration: none;
transform: translateZ(0); /* Enable GPU acceleration */ transform: translateZ(0);
overflow: hidden; /* 防止背景图溢出 */ overflow: hidden;
} }
/* Mobile Optimization: Remove expensive effects */
@media (max-width: 767px) { @media (max-width: 767px) {
.fc-item { .fc-item {
backdrop-filter: none !important; backdrop-filter: none !important;
-webkit-backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
background: rgba(255, 255, 255, 0.8); /* Higher opacity fallback */ background: rgba(255, 255, 255, 0.8);
} }
[data-theme="dark"] .fc-item { [data-theme="dark"] .fc-item {
background: rgba(30, 30, 30, 0.8); background: rgba(30, 30, 30, 0.8);
} }
} }
.friend-circle-list a { .friend-circle-list a {
text-decoration: none; text-decoration: none;
} }
/* Light Mode Override - if theme is light, keep it readable but consistent layout */
/* Assuming the user wants the dark look as shown in the reference, we prioritize that feel
or make it adapt. Let's make it adapt but keep the structure. */
:root { :root {
/* --fc-bg: rgba(255, 255, 255, 0.8); */
--fc-text: #333; --fc-text: #333;
--fc-meta-bg: rgba(0, 0, 0, 0.05); --fc-meta-bg: rgba(0, 0, 0, 0.05);
/* --fc-border: rgba(0, 0, 0, 0.05); */
} }
[data-theme="dark"] { [data-theme="dark"] {
/* --fc-bg: rgba(35, 35, 35, 0.8); */
--fc-text: #fff; --fc-text: #fff;
--fc-meta-bg: rgba(255, 255, 255, 0.1); --fc-meta-bg: rgba(255, 255, 255, 0.1);
/* --fc-border: rgba(255, 255, 255, 0.1); */
} }
/*
.fc-item {
background: var(--fc-bg);
border-color: var(--fc-border);
}
*/
/* UK Style Animation & Hover */
/* 优化后的 hover 效果 */
.fc-item:hover { .fc-item:hover {
/* 位移+缩放:微调数值,更克制不夸张 */
transform: translateY(-6px) scale(1.015); transform: translateY(-6px) scale(1.015);
/* 阴影:分层阴影,更有立体感(主阴影+柔和光晕) */ box-shadow: 0 12px 28px rgba(0, 0, 0, 0.1), 0 2px 8px rgba(0, 0, 0, 0.06);
box-shadow:
0 12px 28px rgba(0, 0, 0, 0.1),
/* 主阴影:缩小范围,降低不透明度 */ 0 2px 8px rgba(0, 0, 0, 0.06); /* 次级光晕:增加柔和感 */
/* 背景:轻微提亮,保留通透感 */
background: rgba(255, 255, 255, 0.7); background: rgba(255, 255, 255, 0.7);
/* 边框:轻微加亮,避免纯黑太突兀 */
border-color: #1a1a1a; border-color: #1a1a1a;
/* 层级:保持原有值 */
z-index: 10; z-index: 10;
/* 光标:交互提示 */
cursor: pointer; cursor: pointer;
} }
/* 可选:点击回弹效果,增强交互反馈 */
.fc-item:active { .fc-item:active {
transform: translateY(-3px) scale(1.005); transform: translateY(-3px) scale(1.005);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
transition: all 0.15s ease; transition: all 0.15s ease;
} }
/* Top: Title */
.fc-title { .fc-title {
font-size: 18px; font-size: 18px;
font-weight: 700; font-weight: 700;
@@ -133,14 +101,13 @@
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
margin-right: 60px; /* Space for the image */ margin-right: 60px;
} }
.fc-item:hover .fc-title { .fc-item:hover .fc-title {
color: #000000; color: #000000;
} }
/* Background Image (Decorative) */
.fc-bg-image { .fc-bg-image {
position: absolute; position: absolute;
right: -20px; right: -20px;
@@ -162,7 +129,6 @@
filter: grayscale(0%); filter: grayscale(0%);
} }
/* Bottom Row: Author Pill & Date */
.fc-bottom { .fc-bottom {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -170,7 +136,6 @@
z-index: 2; z-index: 2;
} }
/* Author Pill */
.fc-author-pill { .fc-author-pill {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -200,7 +165,6 @@
opacity: 0.9; opacity: 0.9;
} }
/* Date */
.fc-date { .fc-date {
font-size: 12px; font-size: 12px;
color: var(--fc-text); color: var(--fc-text);
@@ -214,14 +178,12 @@
font-size: 11px; font-size: 11px;
} }
/* FontAwesome fallback if not available, simple calendar icon via CSS or Unicode */
.fc-icon-calendar::before { .fc-icon-calendar::before {
content: "\f133"; content: "\f133";
font-family: "Font Awesome 5 Free", "FontAwesome"; font-family: "Font Awesome 5 Free", "FontAwesome";
font-weight: 900; font-weight: 900;
} }
/* Loading Animation */
.loading-container { .loading-container {
grid-column: 1 / -1; grid-column: 1 / -1;
display: flex; display: flex;
@@ -241,14 +203,8 @@
margin-bottom: 15px; margin-bottom: 15px;
} }
/* Dark Mode Specific Adjustments to match the reference exactly */
[data-theme="dark"] .fc-item { [data-theme="dark"] .fc-item {
background: rgba( background: rgba(35, 35, 35, 0.65);
35,
35,
35,
0.65
); /* More transparent for glass effect */
box-shadow: none; box-shadow: none;
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
} }
@@ -271,19 +227,15 @@
color: #ffffff; color: #ffffff;
} }
/* Removed unused fadeInUp to keep clean, utilizing ukFadeInUp instead */
@keyframes spin { @keyframes spin {
to { to { transform: rotate(360deg); }
transform: rotate(360deg);
}
} }
/* Load More Button */
.fc-load-more-btn { .fc-load-more-btn {
grid-column: 1 / -1; grid-column: 1 / -1;
padding: 5px 30px; padding: 5px 30px;
margin: 30px auto 10px; margin: 30px auto 10px;
background: #000; /* Black background */ background: #000;
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 30px; border-radius: 30px;
color: #fff; color: #fff;
@@ -310,12 +262,10 @@
background: #222; background: #222;
} }
/* Artalk Dark Mode Adjustment */
[data-theme="dark"] .artalk .atk-main-editor { [data-theme="dark"] .artalk .atk-main-editor {
border: 1px solid rgba(255, 255, 255, 0.1) !important; border: 1px solid rgba(255, 255, 255, 0.1) !important;
} }
/* Friend Circle Tooltip Card */
.fc-tooltip { .fc-tooltip {
position: fixed; position: fixed;
z-index: 9999; z-index: 9999;
@@ -334,7 +284,7 @@
font-size: 14px; font-size: 14px;
line-height: 1.6; line-height: 1.6;
color: #333; color: #333;
display: none; /* Hidden by default */ display: none;
} }
.fc-tooltip.is-visible { .fc-tooltip.is-visible {
@@ -356,7 +306,7 @@
color: #555; color: #555;
text-align: justify; text-align: justify;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 6; /* Limit lines */ -webkit-line-clamp: 6;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
} }
@@ -383,7 +333,6 @@
</div> </div>
<script> <script>
// 使用全局对象管理友圈逻辑
window.FriendCircle = { window.FriendCircle = {
allPosts: [], allPosts: [],
currentIndex: 0, currentIndex: 0,
@@ -394,7 +343,7 @@
const year = date.getFullYear(); const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0"); const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`; return year + "-" + month + "-" + day;
}, },
renderPosts: function (posts) { renderPosts: function (posts) {
@@ -407,29 +356,21 @@
let html = ""; let html = "";
posts.forEach((post, index) => { posts.forEach((post, index) => {
const dateStr = this.timeAgo(post.date); const dateStr = this.timeAgo(post.date);
// 背景图固定使用头像,以获得一致的装饰效果
const bgImageSrc = post.avatar; const bgImageSrc = post.avatar;
// Escape quotes for data attribute
const description = post.description ? post.description.replace(/"/g, '&quot;') : ''; const description = post.description ? post.description.replace(/"/g, '&quot;') : '';
// Calculate delay for staggered animation
const delay = index * 100; const delay = index * 100;
html += ` html += '<a href="' + post.link + '" target="_blank" class="fc-item" data-desc="' + description + '" data-title="' + post.title + '" uk-scrollspy="cls: uk-animation-slide-bottom-small; delay: ' + delay + '">'
<a href="${post.link}" target="_blank" class="fc-item" data-desc="${description}" data-title="${post.title}" uk-scrollspy="cls: uk-animation-slide-bottom-small; delay: ${delay}"> + '<div class="fc-title" title="' + post.title + '">' + post.title + '</div>'
<div class="fc-title" title="${post.title}">${post.title}</div> + '<img src="' + bgImageSrc + '" class="fc-bg-image" onerror="this.style.opacity=0">'
<img src="${bgImageSrc}" class="fc-bg-image" onerror="this.style.opacity=0"> + '<div class="fc-bottom">'
<div class="fc-bottom"> + '<div class="fc-author-pill">'
<div class="fc-author-pill"> + '<img src="' + post.avatar + '" class="fc-avatar" onerror="this.src=\'https://cravatar.cn/avatar/default?s=50\'">'
<img src="${post.avatar}" class="fc-avatar" onerror="this.src='https://cravatar.cn/avatar/default?s=50'"> + '<span class="fc-author-name">' + post.author + '</span>'
<span class="fc-author-name">${post.author}</span> + '</div>'
</div> + '<div class="fc-date"><i class="ri-calendar-line"></i> ' + dateStr + '</div>'
<div class="fc-date"> + '</div>'
<i class="ri-calendar-line"></i> ${dateStr} + '</a>';
</div>
</div>
</a>
`;
}); });
container.insertAdjacentHTML("beforeend", html); container.insertAdjacentHTML("beforeend", html);
@@ -439,14 +380,13 @@
btn.id = "fc-load-more-btn"; btn.id = "fc-load-more-btn";
btn.className = "fc-load-more-btn"; btn.className = "fc-load-more-btn";
btn.innerText = "加载更多"; btn.innerText = "加载更多";
btn.onclick = () => this.loadMore(); btn.onclick = function() { window.FriendCircle.loadMore(); };
container.appendChild(btn); container.appendChild(btn);
} else { } else {
if (window.fcUpdateTime) { if (window.fcUpdateTime) {
const footer = document.createElement("div"); const footer = document.createElement("div");
footer.style.cssText = footer.style.cssText = "grid-column: 1 / -1; text-align: center; margin-top: 20px; font-size: 12px; color: #999; opacity: 0.6;";
"grid-column: 1 / -1; text-align: center; margin-top: 20px; font-size: 12px; color: #999; opacity: 0.6;"; footer.innerText = "最近更新: " + window.fcUpdateTime;
footer.innerText = `最近更新: ${window.fcUpdateTime}`;
container.appendChild(footer); container.appendChild(footer);
} }
} }
@@ -455,12 +395,11 @@
loadMore: function () { loadMore: function () {
const nextBatch = this.allPosts.slice( const nextBatch = this.allPosts.slice(
this.currentIndex, this.currentIndex,
this.currentIndex + this.BATCH_SIZE, this.currentIndex + this.BATCH_SIZE
); );
this.currentIndex += this.BATCH_SIZE; this.currentIndex += this.BATCH_SIZE;
// 使用 requestAnimationFrame 优化渲染性能 requestAnimationFrame(function() {
requestAnimationFrame(() => { window.FriendCircle.renderPosts(nextBatch);
this.renderPosts(nextBatch);
}); });
}, },
@@ -475,130 +414,115 @@
if (!tooltip || !container) return; if (!tooltip || !container) return;
// Move tooltip to body to prevent clipping by overflow:hidden or transforms
if (tooltip.parentNode !== document.body) { if (tooltip.parentNode !== document.body) {
document.body.appendChild(tooltip); document.body.appendChild(tooltip);
} }
// Cleanup tooltip when navigating away (PJAX) const cleanup = function() {
const cleanup = () => { if (tooltip && tooltip.parentNode === document.body) {
if (tooltip && tooltip.parentNode === document.body) { tooltip.remove();
tooltip.remove(); }
} document.removeEventListener('pjax:send', cleanup);
document.removeEventListener('pjax:send', cleanup);
}; };
document.addEventListener('pjax:send', cleanup); document.addEventListener('pjax:send', cleanup);
const updateTooltip = (e) => { const updateTooltip = function(e) {
// Disable tooltip on mobile devices if (window.innerWidth <= 768) return;
if (window.innerWidth <= 768) return; const item = e.target.closest('.fc-item');
if (item) {
const item = e.target.closest('.fc-item'); const desc = item.getAttribute('data-desc');
if (item) { const title = item.getAttribute('data-title');
const desc = item.getAttribute('data-desc'); if (desc) {
const title = item.getAttribute('data-title'); titleEl.innerText = title;
contentEl.innerText = desc;
if (desc) { tooltip.classList.add('is-visible');
titleEl.innerText = title; const tooltipWidth = tooltip.offsetWidth;
contentEl.innerText = desc; const tooltipHeight = tooltip.offsetHeight;
tooltip.classList.add('is-visible'); const winWidth = window.innerWidth;
const winHeight = window.innerHeight;
const tooltipWidth = tooltip.offsetWidth; const gap = 15;
const tooltipHeight = tooltip.offsetHeight; let left = e.clientX + gap;
const winWidth = window.innerWidth; let top = e.clientY + gap;
const winHeight = window.innerHeight; if (left + tooltipWidth > winWidth - gap) {
const gap = 15; // Gap between cursor and tooltip left = e.clientX - tooltipWidth - gap;
if (left < gap) left = gap;
let left = e.clientX + gap; }
let top = e.clientY + gap; if (top + tooltipHeight > winHeight - gap) {
top = e.clientY - tooltipHeight - gap;
// Horizontal Logic if (top < gap) top = gap;
if (left + tooltipWidth > winWidth - gap) { }
// Flip to left tooltip.style.left = left + 'px';
left = e.clientX - tooltipWidth - gap; tooltip.style.top = top + 'px';
// If flipping makes it go off-screen to the left (e.g. wide tooltip, cursor near left)
if (left < gap) {
// Clamp to left edge
left = gap;
}
}
// Vertical Logic
if (top + tooltipHeight > winHeight - gap) {
// Flip to top
top = e.clientY - tooltipHeight - gap;
// If flipping makes it go off-screen to the top
if (top < gap) {
// Clamp to top edge
top = gap;
}
}
tooltip.style.left = `${left}px`;
tooltip.style.top = `${top}px`;
} else {
tooltip.classList.remove('is-visible');
}
} else { } else {
tooltip.classList.remove('is-visible'); tooltip.classList.remove('is-visible');
} }
} else {
tooltip.classList.remove('is-visible');
}
}; };
container.addEventListener('mousemove', updateTooltip); container.addEventListener('mousemove', updateTooltip);
container.addEventListener('mouseleave', () => { container.addEventListener('mouseleave', function() {
tooltip.classList.remove('is-visible'); tooltip.classList.remove('is-visible');
}); });
}, },
init: function () { init: function () {
const container = document.getElementById("friend-circle-container"); const container = document.getElementById("friend-circle-container");
if (!container) return; if (!container) return;
this.initTooltip(); this.initTooltip();
// 防止重复初始化导致掉帧
if (this.isFetching) return; if (this.isFetching) return;
this.isFetching = true; this.isFetching = true;
fetch("/json/friend_circle_data.json?t=" + new Date().getTime()) // 从 RSS API 拉取按时间排序的文章
.then((res) => { fetch("https://rssapi.usj.cc/api/articles?limit=50&t=" + new Date().getTime())
.then(function(res) {
if (!res.ok) throw new Error("Network response was not ok"); if (!res.ok) throw new Error("Network response was not ok");
return res.json(); return res.json();
}) })
.then((data) => { .then(function(apiData) {
// 将 API 格式转换为友圈格式
const posts = (apiData.articles || []).map(function(a) {
return {
title: a.title || '',
link: a.link || '',
date: a.pubDate || '',
author: a.feedName || a.author || '',
avatar: a.favicon || '',
blogUrl: a.siteUrl || '',
image: null,
description: ''
};
});
const data = { posts: posts, updated: apiData.timestamp || new Date().toISOString() };
if (!data.posts || data.posts.length === 0) { if (!data.posts || data.posts.length === 0) {
container.innerHTML = container.innerHTML = '<div class="empty" style="grid-column: 1/-1; text-align:center; padding:20px;">暂无动态</div>';
'<div class="empty" style="grid-column: 1/-1; text-align:center; padding:20px;">暂无动态</div>';
return; return;
} }
this.allPosts = data.posts; window.FriendCircle.allPosts = data.posts;
window.fcUpdateTime = new Date(data.updated).toLocaleString(); window.fcUpdateTime = new Date(data.updated).toLocaleString();
this.currentIndex = 0; window.FriendCircle.currentIndex = 0;
container.innerHTML = ""; container.innerHTML = "";
this.loadMore(); window.FriendCircle.loadMore();
}) })
.catch((err) => { .catch(function(err) {
console.error("Friend Circle Error:", err); console.error("Friend Circle Error:", err);
container.innerHTML = container.innerHTML = '<div class="error" style="grid-column: 1/-1; text-align:center; padding:20px; color:#d9534f;">加载失败,请稍后刷新重试</div>';
'<div class="error" style="grid-column: 1/-1; text-align:center; padding:20px; color:#d9534f;">加载失败,请稍后刷新重试</div>';
}) })
.finally(() => { .finally(function() {
this.isFetching = false; window.FriendCircle.isFetching = false;
}); });
}, }
}; };
// 移除多余的 pjax:complete 监听,因为脚本在 PJAX 容器内,每次导航都会重新执行
// 避免了重复触发 init 导致的性能问题
// 立即执行初始化
if (document.readyState === "loading") { if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => document.addEventListener("DOMContentLoaded", function() {
window.FriendCircle.init(), window.FriendCircle.init();
); });
} else { } else {
window.FriendCircle.init(); window.FriendCircle.init();
} }
+85 -65
View File
@@ -1,84 +1,104 @@
{{ define "main" }} {{ define "main" }}
<div id="pjax-container"> <div id="pjax-container">
<style> <style>
.links ul>li { .friend-links {
position: relative; display: grid;
padding-left: 1.75em; grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-top: 16px;
} }
@media (max-width: 768px) {
.links a { .friend-links { grid-template-columns: 1fr; }
color: var(--link-color-light); }
font-weight: 500; .fl-card {
display: flex;
align-items: center;
gap: 14px;
padding: 16px 18px;
border-radius: 10px;
background: rgba(255,255,255,0.4);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(0,0,0,0.08);
text-decoration: none; text-decoration: none;
transition: transform 0.25s ease, box-shadow 0.25s ease;
} }
.fl-card:hover {
.links li { transform: translateY(-3px);
margin-top: .5em; box-shadow: 0 8px 24px rgba(0,0,0,0.08);
margin-bottom: .5em;
} }
.fl-avatar {
.links ul>li:before { width: 48px; height: 48px;
content: "";
position: absolute;
background-color: #d1d5db;
border-radius: 50%; border-radius: 50%;
width: .375em; object-fit: cover;
height: .375em; flex-shrink: 0;
top: .6875em; background: #eee;
left: .25em;
} }
.fl-info { flex: 1; min-width: 0; }
.url-name { .fl-name {
position: relative; font-size: 15px; font-weight: 600;
display: inline-block; color: var(--fc-text, #333);
line-height: 1.3;
} }
.fl-desc {
.url-name::before { font-size: 12px;
content: ""; color: var(--fc-text, #666);
position: absolute; opacity: 0.65;
height: 12px; margin-top: 3px;
bottom: 3px; white-space: nowrap;
left: -4px; overflow: hidden;
z-index: -1; text-overflow: ellipsis;
background-color: #5d5d5d;
opacity: .3;
transform: skew(-35deg);
transition: opacity .2s ease;
border-radius: 3px 8px 10px 6px;
transition: 0.1s ease background-color;
width: 112%;
/* 宽度根据a标签的内容动态变化 */
} }
[data-theme="dark"] .fl-card {
.url-name:hover::before { background: rgba(35,35,35,0.65);
opacity: 0.5; border-color: rgba(255,255,255,0.1);
/* 鼠标悬浮时可以改变不透明度 */
} }
[data-theme="dark"] .fl-name { color: #e5e7eb; }
[data-theme="dark"] .links a { [data-theme="dark"] .fl-desc { color: #9ca3af; }
color: var(--link-color-dark); .links-loading {
} grid-column: 1 / -1;
text-align: center;
[data-theme="dark"] .url-name::before { padding: 40px;
background-color: #e5e7eb; color: #888;
opacity: 0.18;
}
[data-theme="dark"] .url-name:hover::before {
opacity: 0.35;
} }
</style> </style>
<div class="post-content" itemprop="articleBody"> <div class="post-content" itemprop="articleBody">
<div class="links"> <div class="friend-links" id="friend-links-list">
<ul id="links-list" uk-scrollspy="target: > li; cls: uk-animation-slide-bottom-small; delay: 50"> <div class="links-loading">加载友链中...</div>
<!-- 静态生成的内容作为 SEO 兜底或构建时数据 -->
{{ range site.Data.links.blogs }}
<li><a href="{{ .url }}" class="url-name" target="_blank">{{ .name }}</a><span style="margin-left: 13px;">{{ .description }}</span></li>
{{ end }}
</ul>
</div> </div>
</div> </div>
<!-- 移除动态加载脚本,仅保留静态生成内容 --> <script>
(function() {
var container = document.getElementById('friend-links-list');
if (!container) return;
fetch('https://rssapi.usj.cc/api/links?t=' + new Date().getTime())
.then(function(res) { return res.json(); })
.then(function(data) {
var links = data.links || [];
if (links.length === 0) {
container.innerHTML = '<div class="links-loading">暂无友链</div>';
return;
}
var html = '';
links.forEach(function(l, i) {
var img = l.image || '';
html += '<a href="' + l.url + '" target="_blank" class="fl-card"' +
' uk-scrollspy="cls: uk-animation-slide-bottom-small; delay: ' + (i * 60) + '">' +
(img ? '<img src="' + img + '" class="fl-avatar" onerror="this.style.opacity=0">' : '') +
'<div class="fl-info">' +
'<div class="fl-name">' + l.name + '</div>' +
'<div class="fl-desc">' + (l.description || '') + '</div>' +
'</div></a>';
});
container.innerHTML = html;
})
.catch(function() {
container.innerHTML = '<div class="links-loading">加载失败,请刷新重试</div>';
});
})();
</script>
</div> </div>
{{ end }} {{ end }}