更新日志

2026

05-14

重新部署后端,支持AI摘要生成

05-13

重构前端:移除第三方 CDN 依赖(Vue + Element Plus),使用纯原生 JavaScript 重写友圈前端;新增排序切换功能(最新发布/最近更新);优化卡片样式和弹窗交互

2024

07-15

修复友链地址不一致导致 API 查询失败的问题

03-05

初始搭建

前言

美化之前

美化的时候需要修改网站的源文件、添加样式、js之类的,需要一点基础,可以参考

千万别忘了

在添加完js、css后,一定要记得在_config.butterfly.ymlinject里引用

效果

参考链接

文档更全面哦

不懂的先去文档里找一找,里面都很详细

后端搭建

因为我没有服务器,所以采用的是云函数部署,数据库用的是之前搭建Twikoo评论的Mongodb,具体怎么搭建数据库可以查看云函数部署 | Twikoo 文档,视频教程里很详细。

前端搭建

本方案使用纯原生 JavaScript 实现,不依赖任何第三方 CDN(Vue、Element Plus 等),代码清晰可读,方便自定义修改。

1. 新增 fcircle 页面

新建[blogRoot]/source/fcircle/index.md

1
2
3
4
5
6
---
title: 朋友圈
date: 2024-03-03 20:41:11
comments: true
aside: false
---

2. 添加 fcircle.pug

新建[blogRoot]/themes/butterfly/layout/includes/page/fcircle.pug

记得将private_api_url改为你自己后端的接口

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
.fcircle_page
.title-h2-a
.title-h2-a-left
h2(style='padding-top:0;margin:.6rem 0 .6rem') 🎣 钓鱼
a.random-post-start(href='javascript:fetchRandomPost();')
i.fa.fa-refresh
.title-h2-a-right
a.random-post-all(href='/link/') 全部友链
#random-post

.title-h2-a
.title-h2-a-left
h2(style='padding-top:0;margin:.6rem 0 .6rem') 🐟 鱼塘

#hexo-circle-of-friends-root
script.
var UserConfig = {
// 填写你的api地址
private_api_url: "https://fcircle.june-pj.cn/",
// 点击加载更多时,一次最多加载几篇文章,默认12
page_turning_number: 12,
// 头像加载失败时,默认头像地址
error_img: 'https://img.june-pj.cn/loadding.svg',
// 进入页面时第一次的排序规则
sort_rule: 'created'
}
script(type='text/javascript' src=url_for("/static/js/random_friends_post.js"))
script(type='text/javascript' src=url_for("/static/js/fcircle.js"))

3. 修改 page.pug

修改[blogRoot]/themes/butterfly/layout/page.pug,添加 fcircle 页面类型:

1
2
3
case page.type
when 'fcircle'
include includes/page/fcircle.pug

4. 添加 fcircle.js

新建[blogRoot]/source/static/js/fcircle.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
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
/**
* 友链朋友圈(FriendCircle)纯原生 JavaScript 实现
* 替代原 Vue 前端,保持完全一致的 DOM 结构和 class 名
*
* 依赖:页面中需要存在 UserConfig 全局配置对象
* 容器:页面中需要存在 #hexo-circle-of-friends-root 元素
*/

;(function () {
'use strict'

// ==================== 配置读取(延迟到 init 时读取) ====================

let API_URL = ''
let PAGE_SIZE = 12
let ERROR_IMG = ''
let SORT_RULE = 'created'

function loadConfig() {
var config = window.UserConfig || {}
API_URL = (config.private_api_url || '').replace(/\/$/, '') + '/'
PAGE_SIZE = config.page_turning_number || 12
ERROR_IMG = config.error_img || ''
SORT_RULE = config.sort_rule || 'created'
}

// ==================== 状态管理 ====================

let allArticles = [] // 所有文章数据
let currentIndex = 0 // 当前已显示的文章数量
let statisticalData = null // 统计数据
let currentRule = '' // 当前排序规则

// ==================== 工具函数 ====================

/**
* 发起 GET 请求并返回 JSON
* @param {string} url 请求地址
* @returns {Promise<object>}
*/
function fetchJSON(url) {
return fetch(url)
.then(function (res) {
if (!res.ok) throw new Error('请求失败: ' + res.status)
return res.json()
})
}

/**
* 创建 DOM 元素的简便方法
* @param {string} tag 标签名
* @param {object} attrs 属性对象
* @param {string|Node|Array} children 子元素
* @returns {HTMLElement}
*/
function createElement(tag, attrs, children) {
var el = document.createElement(tag)
if (attrs) {
Object.keys(attrs).forEach(function (key) {
if (key === 'className') {
el.className = attrs[key]
} else if (key === 'style' && typeof attrs[key] === 'object') {
Object.assign(el.style, attrs[key])
} else if (key.startsWith('on')) {
el.addEventListener(key.slice(2).toLowerCase(), attrs[key])
} else {
el.setAttribute(key, attrs[key])
}
})
}
if (children !== undefined && children !== null) {
if (Array.isArray(children)) {
children.forEach(function (child) {
if (child) el.appendChild(typeof child === 'string' ? document.createTextNode(child) : child)
})
} else if (typeof children === 'string') {
el.textContent = children
} else {
el.appendChild(children)
}
}
return el
}

/**
* 头像加载失败时使用默认图片
* @param {HTMLImageElement} img
*/
function handleImgError(img) {
if (ERROR_IMG) {
img.src = ERROR_IMG
}
}

// ==================== 渲染函数 ====================

/**
* 渲染统计栏
* @returns {HTMLElement}
*/
function renderStateBox() {
var stateBox = createElement('div', { id: 'cf-state-box' })
var state = createElement('div', { id: 'cf-state', className: 'cf-new-add' })

var stateData = createElement('div', { className: 'cf-state-data' }, [
createStatItem('cf-data-friends', '好友', statisticalData.friends_num),
createStatItem('cf-data-active', '活跃', statisticalData.active_num),
createStatItem('cf-data-article', '动态', statisticalData.article_num)
])

// 排序切换按钮
var sortBtns = createElement('div', { className: 'cf-sort-btns' })

var btnCreated = createElement('span', {
className: 'cf-sort-btn pointer' + (currentRule === 'created' ? ' cf-sort-active' : ''),
onClick: function () { switchSort('created') }
}, '最新发布')

var btnUpdated = createElement('span', {
className: 'cf-sort-btn pointer' + (currentRule === 'updated' ? ' cf-sort-active' : ''),
onClick: function () { switchSort('updated') }
}, '最近更新')

sortBtns.appendChild(btnCreated)
sortBtns.appendChild(btnUpdated)

state.appendChild(stateData)
state.appendChild(sortBtns)
stateBox.appendChild(state)
return stateBox
}

/**
* 创建单个统计项
* @param {string} className 类名
* @param {string} label 标签文字
* @param {number|string} value 数值
* @returns {HTMLElement}
*/
function createStatItem(className, label, value) {
return createElement('div', { className: className }, [
createElement('span', { className: 'cf-label' }, label),
createElement('span', { className: 'cf-message' }, String(value || 0))
])
}

/**
* 渲染单篇文章卡片
* @param {object} article 文章数据
* @returns {HTMLElement}
*/
function renderArticleItem(article) {
var item = createElement('div', { className: 'cf-article-item' })
var articleDiv = createElement('div', { className: 'cf-article' })

// 文章标题链接
var titleLink = createElement('a', {
className: 'cf-article-title',
href: article.link,
target: '_blank',
rel: 'noopener nofollow',
'data-title': article.title
}, article.title)

// 文章摘要
var descDiv = null
if (article.summary) {
descDiv = createElement('div', {
className: 'cf-article-summary',
title: article.summary
}, article.summary)
}

// 头像区域
var avatarDiv = createElement('div', {
className: 'cf-article-avatar no-lightbox flink-item-icon'
})

// 点击弹窗的处理函数
var onAvatarClick = function (e) {
e.preventDefault()
e.stopPropagation()
showOverlay(article.author, article.avatar, article.link)
}

// 头像图片(纯装饰,不可点击)
var avatarImg = createElement('img', {
className: 'cf-img-avatar avatar no-lightbox',
src: article.avatar || ERROR_IMG,
alt: 'avatar'
})
avatarImg.onerror = function () { handleImgError(this) }

// 作者名(可点击弹出弹窗)
var authorSpan = createElement('span', {
className: 'cf-article-author pointer',
onClick: onAvatarClick
}, article.author)

// 时间(不可点击)
var timeDate = (currentRule === 'updated') ? (article.updated || article.created || '') : (article.created || '')
var timePrefix = (currentRule === 'updated') ? '更新于 ' : ''
var timeSpan = createElement('span', { className: 'cf-article-time' }, [
createElement('span', { className: 'cf-time-created' }, [
createElement('i', { className: 'far fa-calendar-alt' }),
document.createTextNode(timePrefix + timeDate)
])
])

avatarDiv.appendChild(avatarImg)
avatarDiv.appendChild(authorSpan)
avatarDiv.appendChild(timeSpan)

articleDiv.appendChild(titleLink)
if (descDiv) {
articleDiv.appendChild(descDiv)
}
articleDiv.appendChild(avatarDiv)
item.appendChild(articleDiv)

return item
}

/**
* 渲染底部区域(加载更多按钮 + 页脚信息)
* @returns {HTMLElement}
*/
function renderFooter() {
var footer = createElement('div', { id: 'cf-footer' })

// 加载更多按钮
var moreBtn = createElement('div', { id: 'cf-more', className: 'cf-new-add pointer', onClick: loadMore })
moreBtn.innerHTML = '<i class="fas fa-infinity"></i>'

footer.appendChild(moreBtn)

return footer
}

/**
* 渲染弹窗覆盖层容器(初始隐藏)
* @returns {HTMLElement}
*/
function renderOverlayGroup() {
var group = createElement('div', { id: 'cf-overlay-group' })
group.style.display = 'none'

// 遮罩层(点击关闭弹窗)
var overlay = createElement('div', { id: 'cf-overlay', onClick: closeOverlay })

// 弹窗内容区
var overshow = createElement('div', { className: 'cf-overshow' })

group.appendChild(overlay)
group.appendChild(overshow)

return group
}

// ==================== 核心逻辑 ====================

/**
* 初始化:获取数据并渲染页面
*/
function init() {
loadConfig()
currentRule = SORT_RULE
var root = document.getElementById('hexo-circle-of-friends-root')
if (!root) return

// 清空容器
root.innerHTML = ''

// 获取文章数据
fetchJSON(API_URL + 'all?rule=' + SORT_RULE + '&num=1000')
.then(function (data) {
statisticalData = data.statistical_data || {}
allArticles = data.article_data || []
currentIndex = 0

render(root)
})
.catch(function (err) {
console.error('[FriendCircle] 数据加载失败:', err)
root.innerHTML = '<div style="text-align:center;padding:20px;color:#999;">友圈数据加载失败,请稍后刷新重试</div>'
})
}

/**
* 主渲染函数
* @param {HTMLElement} root 根容器
*/
function render(root) {
root.innerHTML = ''

// 弹窗覆盖层(放在最前面,与原 Vue 版本一致)
var overlayGroup = renderOverlayGroup()

// 主内容容器
var wrapper = createElement('div')
var container = createElement('div', { id: 'cf-container' })

// 统计栏
container.appendChild(renderStateBox())

// 文章列表容器
var articleGroup = createElement('div', { className: 'cf-article-group' })
container.appendChild(articleGroup)

wrapper.appendChild(container)
root.appendChild(overlayGroup)
root.appendChild(wrapper)

// 渲染第一批文章
loadArticles(articleGroup)
}

/**
* 加载文章到列表中
* @param {HTMLElement} articleGroup 文章列表容器
*/
function loadArticles(articleGroup) {
// 移除旧的 footer(如果存在)
var oldFooter = articleGroup.querySelector('#cf-footer')
if (oldFooter) {
articleGroup.removeChild(oldFooter)
}

// 计算本次要显示的文章范围
var end = Math.min(currentIndex + PAGE_SIZE, allArticles.length)

// 逐个添加文章
for (var i = currentIndex; i < end; i++) {
articleGroup.appendChild(renderArticleItem(allArticles[i]))
}

currentIndex = end

// 添加底部区域
var footer = renderFooter()
articleGroup.appendChild(footer)

// 如果所有文章已加载完,隐藏"加载更多"按钮
var moreBtn = footer.querySelector('#cf-more')
if (currentIndex >= allArticles.length) {
moreBtn.style.display = 'none'
}
}

/**
* 加载更多文章(点击按钮触发)
*/
function loadMore() {
var root = document.getElementById('hexo-circle-of-friends-root')
if (!root) return

var articleGroup = root.querySelector('.cf-article-group')
if (!articleGroup) return

loadArticles(articleGroup)
}

/**
* 显示友链文章弹窗
* @param {string} author 作者名
* @param {string} avatar 头像 URL
* @param {string} articleLink 文章链接(用于查询该友链的文章)
*/
function showOverlay(author, avatar, articleLink) {
var root = document.getElementById('hexo-circle-of-friends-root')
if (!root) return

var overlayGroup = root.querySelector('#cf-overlay-group')
if (!overlayGroup) return

var overshow = overlayGroup.querySelector('.cf-overshow')
overshow.innerHTML = '<div style="text-align:center;padding:20px;">加载中...</div>'
overlayGroup.style.display = ''

// 通过文章链接提取友链主页地址(取域名部分)
var friendLink = ''
try {
var urlObj = new URL(articleLink)
friendLink = urlObj.origin + '/'
} catch (e) {
friendLink = articleLink
}

// 请求该友链的最近文章
fetchJSON(API_URL + 'post?num=5&link=' + encodeURIComponent(friendLink))
.then(function (data) {
renderOvershowContent(overshow, data)
})
.catch(function (err) {
console.error('[FriendCircle] 弹窗数据加载失败:', err)
overshow.innerHTML = '<div style="text-align:center;padding:20px;color:#999;">加载失败</div>'
})
}

/**
* 渲染弹窗内容
* @param {HTMLElement} overshow 弹窗容器
* @param {object} data API 返回数据
*/
function renderOvershowContent(overshow, data) {
overshow.innerHTML = ''

var stat = data.statistical_data || {}
var articles = data.article_data || []

// 弹窗头部:头像 + 友链名
var head = createElement('div', { className: 'cf-overshow-head' })

var avatarLink = createElement('a', {
href: stat.link || '#',
target: '_blank',
rel: 'noopener nofollow',
className: 'pointer'
})
var avatarImg = createElement('img', {
className: 'cf-img-avatar avatar',
src: stat.avatar || ERROR_IMG,
alt: 'avatar'
})
avatarImg.onerror = function () { handleImgError(this) }
avatarLink.appendChild(avatarImg)

var nameLink = createElement('a', {
target: '_blank',
rel: 'noopener nofollow',
href: stat.link || '#',
className: 'pointer'
}, stat.name || '未知')

head.appendChild(avatarLink)
head.appendChild(nameLink)
overshow.appendChild(head)

// 弹窗文章列表
var contentWrapper = createElement('div')

articles.forEach(function (article, index) {
// 最后一篇使用 cf-overshow-content-tail 类名
var isLast = (index === articles.length - 1)
var contentClass = isLast ? 'cf-overshow-content-tail' : 'cf-overshow-content'

var contentDiv = createElement('div', { className: contentClass })
var p = createElement('p', null, [
createElement('a', {
className: 'cf-article-title',
href: article.link,
target: '_blank',
rel: 'noopener nofollow',
'data-title': article.title
}, article.title),
createElement('span', null, article.created || '')
])

contentDiv.appendChild(p)
contentWrapper.appendChild(contentDiv)
})

overshow.appendChild(contentWrapper)
}

/**
* 关闭弹窗
*/
function closeOverlay() {
var root = document.getElementById('hexo-circle-of-friends-root')
if (!root) return

var overlayGroup = root.querySelector('#cf-overlay-group')
if (overlayGroup) {
overlayGroup.style.display = 'none'
}
}

/**
* 切换排序规则并重新加载数据
* @param {string} rule 排序规则:'created' 或 'updated'
*/
function switchSort(rule) {
if (rule === currentRule) return
currentRule = rule

var root = document.getElementById('hexo-circle-of-friends-root')
if (!root) return

var container = root.querySelector('#cf-container')
if (!container) return

// 重新请求数据
fetchJSON(API_URL + 'all?rule=' + currentRule)
.then(function (data) {
statisticalData = data.statistical_data || {}
allArticles = data.article_data || []
currentIndex = 0
render(root)
})
.catch(function (err) {
console.error('[FriendCircle] 切换排序失败:', err)
})
}

// 暴露关闭方法到全局(兼容原版 onclick="closeShow()" 的调用方式)
window.closeShow = closeOverlay

// ==================== 启动 ====================

// DOM 加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init)
} else {
init()
}

// 支持 PJAX 重新加载(Butterfly 主题使用 PJAX)
document.addEventListener('pjax:complete', init)

})()

5. 添加 random_friends_post.js

新建[blogRoot]/source/static/js/random_friends_post.js

apiurl改为你自己后端的接口

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
(() => {
const DEFAULT_CONFIG = {
apiurl: 'https://fcircle.june-pj.cn/',
defaultFish: 100,
hungryFish: 100,
};

const CONFIG = { ...DEFAULT_CONFIG, ...(window.fdataUser || {}) };

const RANDOM_POST_TIPS = [
"钓到了绝世好文!", "在河边打了个喷嚏,吓跑了", "你和小伙伴抢夺着",
"你击败了巨龙,在巢穴中发现了", "挖掘秦始皇坟时找到了", "在路边闲逛的时候随手买了一个",
"从学校班主任那拿来了孩子上课偷偷看的", "你的同桌无情的从你的语文书中撕下了那篇你最喜欢的",
"考古学家近日发现了", "外星人降临地球学习地球文化,落地时被你塞了",
"从图书馆顶层的隐秘角落里发现了闪着金光的", "徒弟修炼走火入魔,为师立刻掏出了",
"在大山中唱山歌,隔壁的阿妹跑来了,带着", "隔壁家的孩子数学考了满分,都是因为看了",
"隔壁家的孩子英语考了满分,都是因为看了", "小米研发了全新一代MIX手机,据说灵感",
"修炼渡劫成功,还好提前看了", "库克坐上了苹果CEO的宝座,因为他面试的时候看了",
"阿里巴巴大喊芝麻开门,映入眼帘的就是", "师傅说练武要先炼心,然后让我好生研读",
"科考队在南极大陆发现了被冰封的", "飞机窗户似乎被一张纸糊上了,仔细一看是",
"历史上满写的仁义道德四个字,透过字缝里却全是", "十几年前的录音机似乎还能够使用,插上电发现正在播的是",
"新版语文书拟增加一篇熟读并背诵的", "经调查,99%的受访者都没有背诵过",
"今年的高考满分作文是", "唐僧揭开了佛祖压在五指山上的",
"科学家发现能够解决衰老的秘密,就是每日研读", "英特尔发布了全新的至强处理器,其芯片的制造原理都是",
"新的iPhone产能很足,新的进货渠道是", "今年亩产突破了八千万斤,多亏了",
"陆隐一统天上宗,在无数祖境高手的目光下宣读了", "黑钻风跟白钻风说道,吃了唐僧肉能长生不老,他知道是因为看了",
"上卫生间没带纸,直接提裤跑路也不愿意玷污手中", "种下一篇文章就会产生很多很多文章,我种下了",
"三十年河东,三十年河西,莫欺我没有看过", "踏破铁血无觅处,得来全靠",
"今日双色球中了两千万,预测全靠", "因为卷子上没写名字,老师罚抄",
"为了抗议世间的不公,割破手指写下了", "在艺术大街上被贴满了相同的纸,走近一看是",
"这区区迷阵岂能难得住我?其实能走出来多亏了", "今日被一篇文章顶上了微博热搜,它是",
"你送给乞丐一个暴富秘籍,它是", "UZI一个走A拿下五杀,在事后采访时说他当时回想起了",
"科学家解刨了第一个感染丧尸病毒的人,发现丧尸抗体存在于", "如果你有梦想的话,就要努力去看",
"决定我们成为什么样人的,不是我们的能力,而是是否看过", "有信心不一定会成功,没信心就去看",
"你真正是谁并不重要,重要的是你看没看过", "玄天境重要的是锻体,为师赠你此书,好好修炼去吧,这是",
"上百祖境高手在天威湖大战三天三夜为了抢夺", "这化仙池水乃上古真仙对后人的考校,要求熟读并背诵",
"庆氏三千年根基差点竟被你小子毁于一旦,能够被我拯救全是因为我看了", "我就是神奇宝贝大师!我这只皮卡丘可是",
"我就是神奇宝贝大师!我这只小火龙可是", "我就是神奇宝贝大师!我这只可达鸭可是",
"我就是神奇宝贝大师!我这只杰尼龟可是", "上古遗迹中写道,只要习得此书,便得成功。你定睛一看,原来是",
"奶奶的,玩阴的是吧,我就是双料特工代号穿山甲,", "你的背景太假了,我的就逼真多了,学到这个技术全是因为看了",
"我是云南的,云南怒江的,怒江芦水市,芦水市六库,六库傈僳族,傈僳族是", "我真的栓Q了,我真的会谢如果你看",
"你已经习得退退退神功,接下来的心法已经被记录在", "人生无常大肠包小肠,小肠包住了",
"你抽到了普通文章,它是", "你收到了稀有文章,它是",
"你抽到了金色普通文章,它是", "你抽到了金色稀有文章,它是",
"你抽到了传说文章!它是", "哇!金色传说!你抽到了金色传说文章,它是",
"报告!侦察兵说在前往300米有一个男子在偷偷看一本书,上面赫然写着", "芷莲姑娘大摆擂台,谁若是能读完此书,便可娶了她。然后从背后掏出了",
"请问你的梦想是什么?我的梦想是能读到", "读什么才能增智慧?当然是读",
"纳兰嫣然掏出了退婚书,可是发现出门带错了,结果拿出了一本", "你要尽全力保护你的梦想。那些嘲笑你的人,他们必定会失败,他们想把你变成和他们一样的人。如果你有梦想的话,就要努力去读",
"走人生的路就像爬山一样,看起来走了许多冤枉的路,崎岖的路,但终究需要读完", "游戏的规则就是这么的简单,你听懂了吗?管你听没听懂,快去看"
];

let randomPostTimes = 0;
let randomPostClick = 0;
let isWorking = false;

const randomNum = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;

const getLevel = (times) => {
if (times > 10000) return "愿者上钩";
if (times > 1000) return "超越神了"; // Note: Logic in original was >1000 twice
if (times > 100) return "绝世渔夫";
if (times > 75) return "钓鱼王者";
if (times > 50) return "钓鱼宗师";
if (times > 20) return "钓鱼专家";
if (times > 5) return "钓鱼高手";
return "钓鱼新手";
};

const fetchRandomPost = () => {
const el = document.getElementById("random-post");
if (!el) return;

if (isWorking) return;
isWorking = true;

const tip = RANDOM_POST_TIPS[Math.floor(Math.random() * RANDOM_POST_TIPS.length)];
const level = getLevel(randomPostTimes);

el.innerHTML = randomPostTimes >= 5
? `钓鱼中... (Lv.${randomPostTimes} 当前称号:${level})`
: `钓鱼中...`;

const randomTime = randomPostTimes === 0 ? 0 : randomNum(1000, 3000);

const startBtn = document.querySelector(".random-post-start");
if (startBtn) {
Object.assign(startBtn.style, {
opacity: "0.2",
transitionDuration: "0.3s",
transform: `rotate(${randomPostTimes * 360}deg)`
});
}

// Hunger check
const isHungry = (randomPostClick * CONFIG.hungryFish + CONFIG.defaultFish) < randomPostTimes;
if (isHungry && Math.round(Math.random()) === 0) {
el.innerHTML = "因为只钓鱼不吃鱼,过分饥饿导致本次钓鱼失败...(点击任意一篇钓鱼获得的文章即可恢复)";
isWorking = false;
return;
}

fetch(CONFIG.apiurl + "randompost")
.then(res => res.json())
.then(data => {
const json = Array.isArray(data) ? data[0] : data;
if (document.getElementById("random-post") && json) {
setTimeout(() => {
el.innerHTML = `${tip}来自友链 <b>${json.author}</b> 的文章:<a class="random-friends-post" target="_blank" href="${json.link}" rel="external nofollow">${json.title}</a>`;

// Add click listener dynamically
el.querySelector('.random-friends-post').onclick = () => {
randomPostClick++;
localStorage.setItem("randomPostClick", randomPostClick);
};

randomPostTimes++;
localStorage.setItem("randomPostTimes", randomPostTimes);

if (startBtn) startBtn.style.opacity = "1";
}, randomTime);
}
})
.catch(e => console.error(e))
.finally(() => {
isWorking = false;
});
};

const init = () => {
if (localStorage.randomPostTimes) {
randomPostTimes = parseInt(localStorage.randomPostTimes);
randomPostClick = parseInt(localStorage.randomPostClick || 0);

const startBtn = document.querySelector(".random-post-start");
if (startBtn) {
startBtn.style.transitionDuration = "0.3s";
startBtn.style.transform = `rotate(${360 * randomPostTimes}deg)`;
}
}
fetchRandomPost();
};

// Expose fetchRandomPost if needed globally (e.g. for button click)
window.fetchRandomPost = fetchRandomPost;

// 初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}

// PJAX 兼容
document.addEventListener('pjax:complete', init);
})();

6. 添加 fcircle.css

新建[blogRoot]/themes/butterfly/source/css/_custom/pages/fcircle/fcircle.css

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
/* ====================================================
友链朋友圈 - 完整样式
==================================================== */

/* ==================== 页面基础 ==================== */
body[data-type="fcircle"] #web_bg {
background-color: #f7f9fe;
}

[data-theme="dark"] body[data-type="fcircle"] #web_bg {
background-color: #000;
}

body[data-type="fcircle"] #page .page-title {
display: none;
}

body[data-type="fcircle"] #page {
border: 0;
box-shadow: none !important;
padding: 0 !important;
background: transparent !important;
}

.fcircle_page a {
border-bottom: none !important;
}

.fcircle_page .cf-article-avatar a:hover {
background: none !important;
color: var(--june-fontcolor) !important;
}

.fcircle_page .author-content.fcirclePage {
height: 19rem;
color: var(--june-white);
overflow: hidden;
margin-top: 0px;
}

::selection {
color: #fff !important;
background: var(--june) !important;
}

@media screen and (max-width: 768px) {
#page .fcircle_page .author-content-item .card-content .banner-button-group .banner-button i,
#page .fcircle_page .author-content-item .card-content .banner-button-group .banner-button svg {
margin-left: 140px;
margin-top: 15px;
}
}

/* ==================== 钓鱼 / 随机文章 ==================== */
.title-h2-a {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 0.5rem;
}

.title-h2-a-left {
display: flex;
align-items: center;
}

.title-h2-a-left h2 {
margin-top: 0 !important;
margin-bottom: 0 !important;
}

.title-h2-a a {
margin-left: 0.5rem;
color: var(--june-fontcolor);
font-weight: 700;
}

#random-post {
min-height: 32px;
background: var(--card-bg);
border: var(--style-border-always);
box-shadow: var(--june-shadow-border);
padding: 15px 30px;
border-radius: 12px;
margin-top: 8px;
}

#random-post .random-friends-post {
text-decoration: none;
border-bottom: 2px solid var(--june-lighttext) !important;
color: var(--june-fontcolor);
font-weight: 700;
padding: 0 4px;
}

#random-post .random-friends-post:hover {
text-decoration: none;
border-bottom: 2px solid var(--june-none) !important;
color: var(--june-white) !important;
background: var(--june) !important;
border-radius: 4px;
box-shadow: var(--june-shadow-main);
}

#article-container .title-h2-a a:hover {
color: var(--june-hovertext) !important;
}

.title-h2-a-right a.random-post-all {
color: var(--june-fontcolor);
}

#cf-overshow.cf-show-now p a.cf-article-title:hover,
.fcircle_page #fcircleContainer .cf-article a.cf-article-title:hover,
.fcircle_page .title-h2-a-right a.random-post-all:hover,
.fcircle_page .title-h2-a-left a.random-post-start:hover {
background: none;
box-shadow: none;
color: var(--june);
}

/* ==================== 容器 ==================== */
#cf-container {
padding: 0;
}

#cf-container a:hover {
color: var(--june) !important;
}

/* ==================== 统计栏 ==================== */
#cf-state-box {
margin-bottom: 12px;
}

#cf-state {
display: flex;
align-items: center;
justify-content: space-between;
}

.cf-state-data {
display: flex;
gap: 12px;
}

.cf-state-data .cf-data-friends,
.cf-state-data .cf-data-active,
.cf-state-data .cf-data-article {
display: flex;
align-items: center;
gap: 4px;
}

.cf-state-data .cf-label {
font-size: 13px;
color: var(--june-secondtext);
}

.cf-state-data .cf-message {
font-size: 13px;
font-weight: 700;
color: var(--june-fontcolor);
}

/* ==================== 排序切换 ==================== */
.cf-sort-btns {
display: flex;
gap: 12px;
}

.cf-sort-btn {
font-size: 13px;
padding: 0;
border-radius: 0;
color: var(--june-secondtext);
background: transparent;
transition: all 0.2s;
user-select: none;
font-weight: 700;
border-bottom: 2px solid transparent;
padding-bottom: 2px;
}

.cf-sort-btn:hover {
color: var(--june-fontcolor);
}

.cf-sort-btn.cf-sort-active {
color: var(--june-fontcolor);
background: transparent;
border-bottom-color: var(--june);
}

/* ==================== 文章网格 ==================== */
.cf-article-group {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}

@media screen and (max-width: 1200px) {
.cf-article-group {
grid-template-columns: repeat(3, 1fr);
}
}

@media screen and (max-width: 900px) {
.cf-article-group {
grid-template-columns: repeat(2, 1fr);
}
}

@media screen and (max-width: 500px) {
.cf-article-group {
grid-template-columns: 1fr;
}
}

/* ==================== 文章卡片 ==================== */
.cf-article-item {
border: 1px solid var(--june-card-border);
border-radius: 12px;
background: var(--june-card-bg);
overflow: hidden;
transition: all 0.3s ease;
position: relative;
}

.cf-article-item:hover {
box-shadow: var(--june-shadow-border);
border-color: var(--june);
}

.cf-article {
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: auto auto 1fr;
padding: 16px;
height: 100%;
box-sizing: border-box;
gap: 4px;
}

.cf-article:hover {
border: none !important;
}

/* 标题 */
.cf-article .cf-article-title {
grid-column: 1;
grid-row: 1;
text-decoration: none;
font-size: 14px;
font-weight: 700;
color: var(--june-fontcolor);
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
transition: color 0.2s;
align-self: start;
}

.cf-article .cf-article-title:hover {
color: var(--june);
}

/* 文章摘要 */
.cf-article-summary {
font-size: 13px;
color: var(--june-secondtext);
line-height: 1.6;
margin: 6px 0 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
grid-column: 1 / -1;
}

/* 头像区域:底部 */
.cf-article-avatar {
grid-column: 1 / -1;
grid-row: -1;
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
}

/* 大头像作为背景装饰,右下角 */
.cf-article-avatar .cf-img-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
transition: opacity 0.3s;
order: 0;
position: absolute;
right: 12px;
bottom: 12px;
opacity: 0.2;
}

.cf-article-item:hover .cf-img-avatar {
opacity: 0.7;
}

/* 作者名 */
.cf-article-avatar .cf-article-author {
font-size: 11px;
font-weight: 700;
color: var(--june-fontcolor);
white-space: nowrap;
order: 1;
background: var(--june-secondbg);
padding: 2px 8px;
border-radius: 14px;
transition: all 0.2s;
}

.cf-article-avatar .cf-article-author:hover {
background: var(--june);
color: #fff;
}

/* 日期 */
.cf-article-avatar .cf-article-time {
font-size: 12px;
font-weight: 700;
color: var(--june-fontcolor);
white-space: nowrap;
margin-left: auto;
order: 2;
transition: color 0.3s;
}

.cf-article-item:hover .cf-article-time {
color: var(--june-secondtext);
}

.cf-article-avatar .cf-time-created i {
margin-right: 3px;
}

/* ==================== 加载更多 ==================== */
#cf-footer {
grid-column: 1 / -1;
text-align: center;
padding: 8px 0;
}

#cf-more {
padding: 8px 0;
color: var(--june-secondtext);
font-size: 14px;
transition: all 0.3s ease;
border: 1px solid var(--june-card-border);
border-radius: 40px;
width: 40%;
margin: 8px auto;
background: transparent;
display: flex;
align-items: center;
justify-content: center;
}

#cf-more:hover {
background: var(--june);
color: #fff;
width: 60%;
border-color: var(--june);
box-shadow: 0 4px 12px var(--june-theme-op, rgba(66, 90, 239, 0.2));
}

/* ==================== 弹窗 ==================== */
#cf-overlay-group {
display: flex;
position: fixed;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
z-index: 1000;
top: 0;
left: 0;
}

#cf-overlay {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: var(--june-maskbgdeep, rgba(255, 255, 255, 0.85));
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
z-index: 998;
animation: cf-show 0.3s ease-in-out;
}

@keyframes cf-show {
0% { opacity: 0; }
100% { opacity: 1; }
}

@keyframes cf-show-move {
0% { opacity: 0; transform: translateY(20px); }
100% { opacity: 1; transform: translateY(0); }
}

.cf-overshow {
text-align: center;
border-radius: 12px;
width: 360px;
max-width: 90vw;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.12);
background: var(--june-card-bg);
z-index: 999;
padding: 24px;
border: var(--style-border-always);
animation: cf-show-move 0.3s ease-in-out;
position: relative;
}

.cf-overshow-head {
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 12px;
margin-bottom: 12px;
border-bottom: 1px dashed var(--june-card-border);
}

.cf-overshow-head img.cf-img-avatar {
width: 64px;
height: 64px;
border-radius: 50%;
margin-bottom: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}

.cf-overshow-head a {
color: var(--june-fontcolor);
font-weight: 700;
font-size: 15px;
text-decoration: none;
padding: 4px 8px;
transition: color 0.2s;
}

.cf-overshow-head a:hover {
color: var(--june);
}

.cf-overshow-content,
.cf-overshow-content-tail {
padding: 8px 0;
border-bottom: 1px dashed var(--june-card-border);
}

.cf-overshow-content-tail {
border-bottom: none;
}

.cf-overshow p {
margin: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
}

.cf-overshow p a.cf-article-title {
text-decoration: none;
text-align: left;
font-size: 14px;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
color: var(--june-fontcolor);
font-weight: 700;
transition: color 0.2s;
}

.cf-overshow p a.cf-article-title:hover {
color: var(--june);
}

.cf-overshow p span {
font-size: 12px;
margin-top: 4px;
color: var(--june-secondtext);
}

@media screen and (max-width: 768px) {
.cf-overshow {
width: 90%;
}
}