📦 朋友圈配置记录与前端美化

📋 准备工作

  • 服务器已安装 1Panel(我是用这个,按照您的实际情况来,这里只给出演示)
  • 博客已配置好友链页面(link 页面),数据文件为 source/_data/link.yml
  • 确保服务器能访问外网(用于抓取友链文章)

一、生成友链数据文件 friend.json

Friend-Circle-Lite 需要一个 JSON 格式的友链列表。我们从你的 link.yml 生成。

1.1 安装依赖(如果未安装 yamljs)

在博客根目录执行:

1
npm install yamljs --save-dev

在博客根目录创建 link.js,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const YML = require('yamljs');
const fs = require('fs');

// 黑名单:不想显示的友链名称(请根据实际情况修改)
const blacklist = [];

let friends = [];
let data_f = YML.parse(fs.readFileSync('source/_data/link.yml').toString().replace(/(?<=rss:)\s*\n/g, ' ""\n'));

// 默认取前两个分类(可根据你的 link.yml 结构调整)
data_f.forEach((entry, index) => {
// 限制分类数量,通常只取主要分类(如“友情链接”)
if (index < 2) {
const filtered = entry.link_list.filter(item => !blacklist.includes(item.name));
friends = friends.concat(filtered);
}
});

// 构建 Friend-Circle-Lite 需要的格式
const friendData = {
friends: friends.map(item => [item.name, item.link, item.avatar])
};

// 写入到 source/ 下,生成后会被 hexo 复制到 public
fs.writeFileSync('./source/friend.json', JSON.stringify(friendData, null, 2));
console.log('✅ friend.json 已生成');

1.3 执行生成

1
node link.js

此时会在 source/ 目录下生成 friend.json

1.4 将 friend.json 部署到可访问的 URL

你需要让这个文件能被 Friend-Circle-Lite 后端访问到。有两种方式:

  • 方式A(推荐):直接放在你的博客网站目录下,例如 https://你的域名/friend.json(通过 Hexo 生成后自动存在于 public 根目录)。访问 https://你的域名/friend.json 应能看到 JSON 数据。
  • 方式B:在 1Panel 管理的网站中创建一个 API 路径,手动放置文件。

我们采用方式A,因为最简单。执行 hexo g 后,friend.json 会被复制到 public 目录。


二、在服务器部署 Friend-Circle-Lite 后端

2.1 通过 SSH 登录服务器

1
ssh root@你的服务器IP

2.2 克隆项目

1
2
git clone https://github.com/willow-god/Friend-Circle-Lite.git
cd Friend-Circle-Lite

2.3 安装 Python 依赖

1
2
pip install -r requirements.txt
pip install -r server/requirements-server.txt

如果系统没有 pip,先安装 python3-pip。

2.4 修改配置文件 conf.yaml

1
vim conf.yaml

修改关键部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
spider_settings:
enable: true
json_url: "https://你的域名/friend.json" # 换成你的实际 URL
article_count: 5 # 每个博客获取最新几篇文章
merge_result:
enable: false
merge_json_url: ""

email_push:
enable: false

rss_subscribe:
enable: false

保存退出。

2.5 测试服务

运行后端服务:

1
python3 server.py

默认监听 0.0.0.0:1223。访问 http://你的服务器IP:1223/all,如果返回 JSON 数据(可能为空),说明服务启动正常。按 Ctrl+C 停止。

2.6 配置为系统服务(可选但推荐)

创建 systemd 服务文件 /etc/systemd/system/fcircle.service

1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=Friend Circle Lite Service
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/root/Friend-Circle-Lite
ExecStart=/usr/bin/python3 /root/Friend-Circle-Lite/server.py
Restart=always

[Install]
WantedBy=multi-user.target

启动并设置开机自启:

1
2
3
systemctl daemon-reload
systemctl start fcircle
systemctl enable fcircle

2.7 验证服务

1
curl http://127.0.0.1:1223/all

应返回 JSON。


三、在 1Panel 中配置反向代理

为了通过域名访问后端接口(例如 https://friends.你的域名.com),并避免跨域问题。

3.1 添加反向代理

  1. 登录 1Panel,进入「网站」→「创建反向代理」。
  2. 填写:
    • 域名friends.你的域名.com(需提前解析到服务器)
    • 目标地址http://127.0.0.1:1223
    • 其他默认即可。
  3. 提交后,等待 SSL 证书申请(可选)。

3.2 测试代理

访问 https://friends.你的域名.com/all,应该返回和后端一样的 JSON 数据。

3.3 我遇到的问题

我部署的时候python安装异常,所以我用的是虚拟环境,过程如下:

🐍 解决 Python 包安装错误:externally-managed-environment

问题原因

较新的 Linux 发行版(如 Ubuntu 22.04+、Debian 12+)默认启用了 Python 的 EXTERNALLY-MANAGED 机制,防止用 pip 直接安装包到系统环境中,以免破坏系统依赖。执行 pip install 时会报该错误。

解决方案
使用虚拟环境

虚拟环境能隔离依赖,不影响系统,且便于管理。

  1. 安装 python3-venv(如果尚未安装)
1
apt update && apt install python3-venv -y
  1. 进入 Friend-Circle-Lite 目录,创建虚拟环境
1
2
cd ~/Friend-Circle-Lite
python3 -m venv venv
  1. 激活虚拟环境
1
source venv/bin/activate

激活后,命令行提示符会显示 (venv)

  1. 安装依赖(在虚拟环境中)
1
2
pip install -r requirements.txt
pip install -r server/requirements-server.txt

此时安装的包都在虚拟环境中,不会影响系统。

  1. 运行服务
    在虚拟环境中执行:
1
python server.py

如果成功运行,那么你就可以进行下一步了。

定时任务
  1. 进入计划任务,点击「添加任务」。

  2. 任务类型:Shell脚本。

  3. 任务名称:如 friend-circle-crawl

  4. 执行周期:按需设置(例如每6小时)。

  5. 脚本内容:填入你的命令

    1
    2
    cd /root/Friend-Circle-Lite
    /root/Friend-Circle-Lite/venv/bin/python run.py
  6. 保存

那么一个每天定时抓取的朋友圈后端就完成了

🎨 美化版朋友圈前端代码

  • 统计信息置顶:将订阅数(友链总数)、成功数(文章总数)、总数(总抓取量)移到页面最上方,以卡片形式展示。
  • 动态获取:自动从后端 API 获取实时统计数据。
  • 完整兼容:保留原朋友圈全部功能,布局不变。
  • 谨慎复制:请谨慎复制,将我的链接换成你自己的!

📁 完整代码(替换你的 source/fcircle/index.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
---
title: 朋友圈
date: 2024-01-01 00:00:00
comments: false
aside: false
top_img: false
---

<style>
/* ===== 头部区域 ===== */
.fcircle-header {
max-width: 1200px;
margin: 30px auto 20px;
padding: 0 20px;
display: flex;
flex-wrap: wrap;
align-items: flex-end;
justify-content: space-between;
}

.header-title {
flex: 1;
}

.header-title h1 {
font-size: 2.8rem;
font-weight: 700;
color: #fff;
margin: 0 0 5px;
text-shadow: 0 2px 10px rgba(0,0,0,0.3);
letter-spacing: 1px;
}

.header-title .subtitle {
font-size: 1.1rem;
color: #ccc;
border-left: 4px solid #b87333;
padding-left: 15px;
margin: 0;
}

.header-meta {
display: flex;
align-items: center;
gap: 20px;
background: rgba(20,20,20,0.5);
backdrop-filter: blur(8px);
border-radius: 50px;
padding: 12px 25px;
border: 1px solid rgba(255,255,255,0.1);
margin-top: 15px;
}

.update-time {
color: #ccc;
font-size: 0.95rem;
display: flex;
align-items: center;
gap: 6px;
}
.update-time i {
color: #b87333;
}

.refresh-btn {
background: rgba(184,115,51,0.15);
border: 1px solid #b87333;
color: #b87333;
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
font-size: 1.2rem;
}
.refresh-btn:hover {
background: #b87333;
color: #111;
transform: rotate(180deg);
}

/* ===== 统计卡片样式(保持不变) ===== */
.stats-container {
display: flex;
justify-content: center;
gap: 25px;
margin: 20px auto 40px;
max-width: 900px;
padding: 0 20px;
flex-wrap: wrap;
}

.stat-card {
flex: 1;
min-width: 150px;
background: rgba(20, 20, 20, 0.65);
backdrop-filter: blur(12px) saturate(180%);
-webkit-backdrop-filter: blur(12px) saturate(180%);
border-radius: 32px;
padding: 28px 15px;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 25px 45px -15px rgba(0, 0, 0, 0.4);
transition: transform 0.25s ease, border-color 0.25s;
}

.stat-card:hover {
transform: translateY(-6px);
border-color: #b87333;
}

.stat-number {
font-size: 3.2rem;
font-weight: 800;
color: #b87333;
line-height: 1.2;
text-shadow: 0 0 15px rgba(184, 115, 51, 0.3);
letter-spacing: 1px;
}

.stat-label {
font-size: 1rem;
color: #ccc;
letter-spacing: 1.5px;
text-transform: uppercase;
margin-top: 10px;
font-weight: 500;
}

/* ===== 文章卡片列表(与之前相同) ===== */
.articles-container {
max-width: 1200px;
margin: 30px auto;
padding: 0 20px;
}

.article-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 25px;
margin-bottom: 40px;
}

.article-card {
background: rgba(20, 20, 20, 0.6);
backdrop-filter: blur(12px) saturate(180%);
-webkit-backdrop-filter: blur(12px) saturate(180%);
border-radius: 28px;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: transform 0.3s ease, border-color 0.3s;
display: flex;
flex-direction: column;
}

.article-card:hover {
transform: translateY(-6px);
border-color: #b87333;
}

.article-header {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 15px;
}

.article-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
overflow: hidden;
border: 2px solid rgba(255, 255, 255, 0.2);
flex-shrink: 0;
}
.article-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}

.article-author {
font-size: 1.1rem;
font-weight: 600;
color: #fff;
line-height: 1.3;
}
.article-author small {
display: block;
font-size: 0.8rem;
color: #aaa;
font-weight: 400;
margin-top: 4px;
}

.article-title {
font-size: 1.2rem;
font-weight: 600;
color: #fff;
margin-bottom: 12px;
line-height: 1.4;
word-break: break-word;
}
.article-title a {
color: inherit;
text-decoration: none;
}
.article-title a:hover {
color: #b87333;
}

.article-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
font-size: 0.9rem;
color: #aaa;
}

.article-time {
display: flex;
align-items: center;
gap: 5px;
}
.article-time i {
font-size: 0.8rem;
}

/* 分页控件 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
margin: 40px 0;
}

.page-btn {
background: rgba(184, 115, 51, 0.2);
border: 1px solid #b87333;
color: #b87333;
width: 44px;
height: 44px;
border-radius: 50%;
font-size: 1.4rem;
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.page-btn:hover:not(:disabled) {
background: #b87333;
color: #111;
}
.page-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
border-color: #666;
}

.page-info {
color: #ccc;
font-size: 1.1rem;
}

/* 加载中/无数据提示 */
.loading, .no-data {
text-align: center;
padding: 60px 20px;
color: #ccc;
font-size: 1.2rem;
background: rgba(20,20,20,0.4);
backdrop-filter: blur(8px);
border-radius: 40px;
}

/* 移动端优化 */
@media (max-width: 768px) {
.fcircle-header {
flex-direction: column;
align-items: flex-start;
}
.header-meta {
width: 100%;
justify-content: space-between;
}
.article-grid {
grid-template-columns: 1fr;
}
}
</style>

<!-- ===== 新增头部:标题、副标题、更新时间、刷新按钮 ===== -->
<div class="fcircle-header">
<div class="header-title">
<h1>📡 朋友圈</h1>
<p class="subtitle">捕捉来自朋友们的最新动态</p>
</div>
<div class="header-meta" id="header-meta">
<span class="update-time" id="update-time">
<i class="far fa-clock"></i> 最后更新: 加载中...
</span>
<button class="refresh-btn" id="refresh-btn" title="刷新缓存">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</div>

<!-- ===== 统计卡片容器 ===== -->
<div class="stats-container" id="stats-container">
<div class="stat-card">
<div class="stat-number">--</div>
<div class="stat-label">订阅博客</div>
</div>
<div class="stat-card">
<div class="stat-number">--</div>
<div class="stat-label">文章总数</div>
</div>
<div class="stat-card">
<div class="stat-number">--</div>
<div class="stat-label">活跃站点</div>
</div>
</div>

<!-- ===== 文章卡片容器 ===== -->
<div class="articles-container">
<div id="article-list" class="article-grid"></div>
<div id="pagination" class="pagination"></div>
</div>

<!-- ===== 数据获取与渲染 ===== -->
<script>
(function() {
const friendJsonUrl = 'https://blog.damonz.cn/friend.json';
const articleJsonUrl = 'https://fc.damonz.cn/all.json';
const pageSize = 20; // 每页显示20篇文章
let currentPage = 1;
let articles = []; // 原始文章数据
let friendCount = 0;
let articleCount = 0;
let activeCount = 0;
let lastUpdated = ''; // 最后更新时间

// 获取DOM元素
const statNumbers = document.querySelectorAll('.stat-number');
const articleList = document.getElementById('article-list');
const paginationDiv = document.getElementById('pagination');
const updateTimeSpan = document.getElementById('update-time');
const refreshBtn = document.getElementById('refresh-btn');

// 格式化时间(保留年月日)
function formatDate(dateStr) {
if (!dateStr) return '';
return dateStr.substring(0, 10);
}

// 渲染统计卡片
function updateStats() {
if (statNumbers.length >= 3) {
statNumbers[0].textContent = friendCount;
statNumbers[1].textContent = articleCount;
statNumbers[2].textContent = activeCount;
}
// 更新时间
if (lastUpdated) {
updateTimeSpan.innerHTML = `<i class="far fa-clock"></i> 最后更新: ${lastUpdated}`;
}
}

// 渲染文章卡片
function renderArticles(page) {
const start = (page - 1) * pageSize;
const end = Math.min(start + pageSize, articles.length);
const pageArticles = articles.slice(start, end);

if (pageArticles.length === 0 && articles.length === 0) {
articleList.innerHTML = '<div class="no-data">暂无文章更新</div>';
paginationDiv.innerHTML = '';
return;
}

let html = '';
pageArticles.forEach(item => {
html += `
<div class="article-card">
<div class="article-header">
<div class="article-avatar">
<img src="${item.avatar || 'https://i.p-i.vip/30/20240815-66bced9226a36.webp'}" alt="${item.author}" onerror="this.src='https://i.p-i.vip/30/20240815-66bced9226a36.webp'">
</div>
<div class="article-author">
${item.author}
<small>${item.author}</small>
</div>
</div>
<div class="article-title">
<a href="${item.link}" target="_blank" rel="noopener">${item.title}</a>
</div>
<div class="article-meta">
<span class="article-time">
<i class="far fa-calendar-alt"></i> ${formatDate(item.created)}
</span>
</div>
</div>
`;
});
articleList.innerHTML = html;

// 渲染分页
renderPagination();
}

// 渲染分页控件
function renderPagination() {
const totalPages = Math.ceil(articles.length / pageSize);
if (totalPages <= 1) {
paginationDiv.innerHTML = '';
return;
}

let pagHtml = `
<button class="page-btn prev" ${currentPage === 1 ? 'disabled' : ''} onclick="changePage(${currentPage - 1})"></button>
<span class="page-info">${currentPage} / ${totalPages}</span>
<button class="page-btn next" ${currentPage === totalPages ? 'disabled' : ''} onclick="changePage(${currentPage + 1})"></button>
`;
paginationDiv.innerHTML = pagHtml;
}

// 分页切换函数
window.changePage = function(newPage) {
if (newPage < 1 || newPage > Math.ceil(articles.length / pageSize)) return;
currentPage = newPage;
renderArticles(currentPage);
document.querySelector('.articles-container').scrollIntoView({ behavior: 'smooth', block: 'start' });
};

// 获取数据(支持强制刷新缓存)
async function fetchAllData(forceRefresh = false) {
try {
// 如果强制刷新,给请求添加随机参数避免缓存
const friendUrl = forceRefresh ? friendJsonUrl + '?t=' + Date.now() : friendJsonUrl;
const articleUrl = forceRefresh ? articleJsonUrl + '?t=' + Date.now() : articleJsonUrl;

const [friendRes, articleRes] = await Promise.allSettled([
fetch(friendUrl).then(res => res.ok ? res.json() : Promise.reject('友链获取失败')),
fetch(articleUrl).then(res => res.ok ? res.json() : Promise.reject('文章获取失败'))
]);

// 处理友链数据
if (friendRes.status === 'fulfilled') {
const friendData = friendRes.value;
if (friendData && friendData.friends && Array.isArray(friendData.friends)) {
friendCount = friendData.friends.length;
}
} else {
console.warn('友链获取失败:', friendRes.reason);
}

// 处理文章数据
if (articleRes.status === 'fulfilled') {
const articleData = articleRes.value;
if (articleData && articleData.article_data && Array.isArray(articleData.article_data)) {
articles = articleData.article_data;
articleCount = articles.length;

// 计算活跃站点数
const activeSites = new Set(articles.map(item => item.author));
activeCount = activeSites.size;

// 获取最后更新时间(优先从 statistical_data 获取)
if (articleData.statistical_data && articleData.statistical_data.last_updated_time) {
lastUpdated = articleData.statistical_data.last_updated_time;
} else {
lastUpdated = new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).replace(/\//g, '-');
}
}
} else {
console.warn('文章获取失败:', articleRes.reason);
}

// 更新统计卡片和时间
updateStats();

// 渲染文章列表
currentPage = 1;
renderArticles(currentPage);

} catch (err) {
console.error('数据获取错误:', err);
articleList.innerHTML = '<div class="no-data">数据加载失败</div>';
}
}

// 刷新按钮点击事件
refreshBtn.addEventListener('click', function() {
// 添加旋转动画效果
this.style.transform = 'rotate(180deg)';
setTimeout(() => this.style.transform = '', 300);
// 强制刷新数据
fetchAllData(true);
});

// 初始化加载
fetchAllData();

// PJAX 完成后重新初始化(避免重复绑定刷新按钮事件)
document.addEventListener('pjax:complete', function() {
// 重新获取数据,但不强制刷新(也可强制,看需求)
fetchAllData();
});
})();
</script>

````

但是会发现背景白色不搭,可以导入css:

```css
/* ===== 头部区域(透明背景,橙色文字) ===== */
.fcircle-header {
max-width: 1200px;
margin: 30px auto 20px;
padding: 20px 30px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
background: transparent; /* 完全透明 */
border: none;
box-shadow: none;
}

.header-title h1 {
color: #b87333; /* 橙色主标题 */
margin: 0;
font-size: 2.2rem;
}

.header-title .subtitle {
color: #b87333; /* 橙色副标题 */
margin: 5px 0 0;
font-size: 1rem;
border-left: 4px solid #b87333;
padding-left: 15px;
}

.header-meta {
display: flex;
align-items: center;
gap: 20px;
color: #b87333; /* 橙色文字 */
}

.update-time {
color: #b87333; /* 橙色时间 */
}

.refresh-btn {
background: transparent;
border: 1px solid #b87333;
color: #b87333;
width: 36px;
height: 36px;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s;
font-size: 1.2rem;
display: flex;
align-items: center;
justify-content: center;
}

.refresh-btn:hover {
background: #b87333;
color: #111;
transform: rotate(180deg);
}

📌 其他注意事项

  • 如果页面中使用了 FontAwesome 图标(如刷新按钮的 ``),但图标未显示,请在文件开头添加图标库 CDN 链接(如果还没加的话):

    1
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">