清羽AI正在绞尽脑汁想思路ING···
清羽のAI摘要
GLM-4-Flash

碎碎念

上班1个月!感觉灵魂都被抽干了,已经彻底成为合格牛马的形状了。每天睁眼就是工作,闭眼就是盼着周末,结果真到了周末,可能还得面对加班的召唤。这谁顶得住,总之就是累累的,困困的,呜呜呜……

前些天的月度答辩,那叫一个紧张,PPT还没讲,我人都要裂开了。脑子里预演了一万遍被怼到怀疑人生的场景。索性我们组的哥姐们都超nice,没有为难我这个新人,知道我第一个月基本属于啥也不会的废物状态。后面跟着做了一些业务,刚开始上手确实感觉有点难度,但沉下心来搞,发现其实也并没有想象中的那么复杂。总之,第一个月总算是有惊无险地正常度过啦!

回到正题,之前朋友写了个程序叫Ech0。一开始因为他的设计理念,界面做得特别简约,甚至从程序底层就限制了一些花里胡哨的操作(没错,说的就是想搞事的我)。所以当时测试了一个周,感觉不太符合我的折腾欲,就选择了暂时观望。后来嘛,忙入职、忙培训,这事儿就暂时搁置了。结果前阵子偶然逛到他博客,发现Ech0更新了好几个版本,加的新功能简直正中我的下怀!再加上它那一直很戳我的美观界面,我当场就心动了,于是稍微捣鼓了一下,火速迁移过来啦!更棒的是,结合PWA技术,现在能直接像手机App一样发说说了!这体验感,绝了!很不错!超喜欢!

现在就盼着作者大大后面能加上多标签功能了!到时候我想把标签整成定位,比如#摸鱼#发呆之类的,人主打一个花里胡哨,记录生活嘛!

介绍

刚开始,我其实用的是Memos。该程序设计完善,功能花里胡哨,资源占用还低,所以吸引了大量用户,生态也相当全面。不过嘛,作者更新实在太频繁了,导致API时不时就不兼容,我那套定制的前端维护起来就有点麻烦了,具体原因可以看我之前写的这篇文章:

后来我换到了Moments,这绝对是个非常优秀的程序,功能和设计上都完全满足我的要求。但美中不足的是,它没有适配PWA。当然,这不算什么大问题,纯粹是我的个人需求。正巧那段时间,有个朋友在开发他的新项目Ech0,我一眼就被它那简约又美观的界面给吸引了,立马上手体验了一段时间。不过那时候的Ech0还比较早期,功能上不太全面:没有标签系统,图片不能上传到S3,也没有我喜欢的Meting音乐插件,甚至连视频分享都没有。这对于追求花里胡哨的我来说,肯定是不够用的,所以当时就没切换。

再后来,朋友很给力,Ech0项目逐渐完善,之前缺少的功能一个个都补上了。我再次体验的时候,发现已经十分好用,于是就果断迁移了过来,还顺手把我之前那套前端给适配上了。目前使用很爽,下面是几个程序的对比:

特性MemosMomentsEch0
PWA 适配✅ 支持❌ 不支持✅ 支持
标签系统✅ 完善✅ 完善✅ 支持
S3 图床✅ 支持✅ 支持✅ 支持
Meting 音乐❌ 不支持✅ 支持✅ 支持
视频分享✅ 支持✅ 支持✅ 支持
设计风格功能导向,略显臃肿功能全面,类似后台简约美观,轻量
开发活跃度非常活跃,API易变相对稳定活跃,快速迭代
个人评价功能强大,但更新太折腾,生态不稳定。功能完美,但没PWA对我是减分项界面戳我,功能追上来了,PWA是加分项,目前的最爱

话不多说,开始教程!

教程

部署要求

硬件要求

  • 一台服务器
  • 一个可自主解析的域名
  • 一个已经部署好的类ButterflyHexo博客

软件要求

  • docker环境
  • 反向代理工具(本文以Nginx为例)

效果展示

  1. 说说页面:

    由于前端才是最主要的展示区域,在这里我尽可能做了最多的适配,适配了图片分享,链接分享,音乐分享,Bilibili分享,Youtube分享,黑夜模式等多种适配,这里仅展示部分功能,yoputube由于拉低网络加载速度,不予展示。其他的具体效果可以上网站我的说说页面自行查看。

    亮色模式

    暗色模式

    音乐分享

    图片分享

    链接分享

  2. Ech0

    这里也没什么过多可以展示的,因为网站功能太多,无法一一展示,建议大家直接进入网站自行查看。

    Ech0

程序部署

程序的Github地址如下:

下面介绍两种主要部署方式,分别为Docker,应用商店。

Docker

这里我推荐使用docker-compose,原生docker不太方便,不太好记录挂载目录等,官方给出了详细的文档,以下为compose文件:

1
2
3
4
5
6
7
8
9
10
11
12
services:
ech0:
image: sn0wl1n/ech0:latest
container_name: ${容器名称
environment:
- JWT_SECRET=${随便写}
ports:
- "${PANEL_APP_PORT_HTTP}:6277"
volumes:
- ./data/ech0-data:/app/data
- ./data/backup:/app/backup
restart: always

将以上内容写入任意文件内,命名为docker-compose.yml,在当前目录下执行:

1
docker compose up -d

如果不出意外,容器已经启动,自行实现反向代理即可。

应用商店

如果你为1Panel用户,可以选择直接通过我个人维护的第三方应用商店实现安装,

在以前的文章曾介绍了如何添加三方应用商店,这里就不重复了,首先按照以下教程安装三方应用商店:

安装好后,在应用商店直接搜索安装即可,更加快捷,更好维护!

前端魔改

说说页面

由于该项目利用了MetingJSAPlayer,所以请提前引入这两个包,Hexo-theme-butterfly中虽然有内置的两个包,仅需修改配置文件即可开启,但是版本比较老,这里我建议自行引入最新版本,在配置中引入以下文件,注意cssjs应该是分开引入的,在主题hexo-theme-butterfly中,仅需要修改配置文件即可。

1
2
3
<link rel="stylesheet" href="https://fastly.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.css" media="all" onload="this.media=&quot;all&quot;">
<script src="https://fastly.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.js"></script>
<script src="https://fastly.jsdelivr.net/npm/meting@2.0.1/dist/Meting.min.js"></script>

新建页面shuoshuo,在文件内写入以下内容:

1
2
3
4
5
6
7
---
title: 日常哔哔,键盘侠的日常吐槽
aside: false
---
<div id="talk"></div>
<div class="limit">- 只展示最近30条说说 -</div>
<script src="/js/shuoshuo.js" no-pjax></script>

其中的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
function renderTalks() {
const talkContainer = document.querySelector('#talk');
if (!talkContainer) return;
talkContainer.innerHTML = '';
const generateIconSVG = () => {
return `<svg viewBox="0 0 512 512"xmlns="http://www.w3.org/2000/svg"class="is-badge icon"><path d="m512 268c0 17.9-4.3 34.5-12.9 49.7s-20.1 27.1-34.6 35.4c.4 2.7.6 6.9.6 12.6 0 27.1-9.1 50.1-27.1 69.1-18.1 19.1-39.9 28.6-65.4 28.6-11.4 0-22.3-2.1-32.6-6.3-8 16.4-19.5 29.6-34.6 39.7-15 10.2-31.5 15.2-49.4 15.2-18.3 0-34.9-4.9-49.7-14.9-14.9-9.9-26.3-23.2-34.3-40-10.3 4.2-21.1 6.3-32.6 6.3-25.5 0-47.4-9.5-65.7-28.6-18.3-19-27.4-42.1-27.4-69.1 0-3 .4-7.2 1.1-12.6-14.5-8.4-26-20.2-34.6-35.4-8.5-15.2-12.8-31.8-12.8-49.7 0-19 4.8-36.5 14.3-52.3s22.3-27.5 38.3-35.1c-4.2-11.4-6.3-22.9-6.3-34.3 0-27 9.1-50.1 27.4-69.1s40.2-28.6 65.7-28.6c11.4 0 22.3 2.1 32.6 6.3 8-16.4 19.5-29.6 34.6-39.7 15-10.1 31.5-15.2 49.4-15.2s34.4 5.1 49.4 15.1c15 10.1 26.6 23.3 34.6 39.7 10.3-4.2 21.1-6.3 32.6-6.3 25.5 0 47.3 9.5 65.4 28.6s27.1 42.1 27.1 69.1c0 12.6-1.9 24-5.7 34.3 16 7.6 28.8 19.3 38.3 35.1 9.5 15.9 14.3 33.4 14.3 52.4zm-266.9 77.1 105.7-158.3c2.7-4.2 3.5-8.8 2.6-13.7-1-4.9-3.5-8.8-7.7-11.4-4.2-2.7-8.8-3.6-13.7-2.9-5 .8-9 3.2-12 7.4l-93.1 140-42.9-42.8c-3.8-3.8-8.2-5.6-13.1-5.4-5 .2-9.3 2-13.1 5.4-3.4 3.4-5.1 7.7-5.1 12.9 0 5.1 1.7 9.4 5.1 12.9l58.9 58.9 2.9 2.3c3.4 2.3 6.9 3.4 10.3 3.4 6.7-.1 11.8-2.9 15.2-8.7z"fill="#1da1f2"></path></svg>`;
}
const waterfall = (a) => {
function b(a, b) {
var c = window.getComputedStyle(b);
return parseFloat(c["margin" + a]) || 0
}

function c(a) {
return a + "px"
}

function d(a) {
return parseFloat(a.style.top)
}

function e(a) {
return parseFloat(a.style.left)
}

function f(a) {
return a.clientWidth
}

function g(a) {
return a.clientHeight
}

function h(a) {
return d(a) + g(a) + b("Bottom", a)
}

function i(a) {
return e(a) + f(a) + b("Right", a)
}

function j(a) {
a = a.sort(function (a, b) {
return h(a) === h(b) ? e(b) - e(a) : h(b) - h(a)
})
}

function k(b) {
f(a) != t && (b.target.removeEventListener(b.type, arguments.callee), waterfall(a))
}
"string" == typeof a && (a = document.querySelector(a));
var l = [].map.call(a.children, function (a) {
return a.style.position = "absolute", a
});
a.style.position = "relative";
var m = [];
l.length && (l[0].style.top = "0px", l[0].style.left = c(b("Left", l[0])), m.push(l[0]));
for (var n = 1; n < l.length; n++) {
var o = l[n - 1],
p = l[n],
q = i(o) + f(p) <= f(a);
if (!q) break;
p.style.top = o.style.top, p.style.left = c(i(o) + b("Left", p)), m.push(p)
}
for (; n < l.length; n++) {
j(m);
var p = l[n],
r = m.pop();
p.style.top = c(h(r) + b("Top", p)), p.style.left = c(e(r)), m.push(p)
}
j(m);
var s = m[0];
a.style.height = c(h(s) + b("Bottom", s));
var t = f(a);
window.addEventListener ? window.addEventListener("resize", k) : document.body.onresize = k
};

const fetchAndRenderTalks = () => {
const url = 'https://mm.liushen.fun/api/echo/page';
const cacheKey = 'talksCache';
const cacheTimeKey = 'talksCacheTime';
const cacheDuration = 30 * 60 * 1000;
const cachedData = localStorage.getItem(cacheKey);
const cachedTime = localStorage.getItem(cacheTimeKey);
const now = Date.now();

if (cachedData && cachedTime && (now - cachedTime < cacheDuration)) {
renderTalksList(JSON.parse(cachedData));
} else {
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ page: 1, pageSize: 30 })
})
.then(res => res.json())
.then(data => {
if (data.code === 1 && data.data && Array.isArray(data.data.items)) {
localStorage.setItem(cacheKey, JSON.stringify(data.data.items));
localStorage.setItem(cacheTimeKey, now.toString());
renderTalksList(data.data.items);
}
})
.catch(err => console.error('Error fetching:', err));
}
};

const renderTalksList = (list) => {
list.map(formatTalk).forEach(item => talkContainer.appendChild(generateTalkElement(item)));
waterfall('#talk');
};

const formatTalk = (item) => {
const date = formatTime(item.created_at);
let content = item.content || '';
content = content.replace(/\[(.*?)\]\((.*?)\)/g, `<a href="$2" target="_blank" rel="nofollow noopener">@$1</a>`)
.replace(/- \[ \]/g, '⚪')
.replace(/- \[x\]/g, '⚫')
.replace(/\n/g, '<br>');
content = `<div class="talk_content_text">${content}</div>`;

// 图片
if (Array.isArray(item.images) && item.images.length > 0) {
const imgDiv = document.createElement('div');
imgDiv.className = 'zone_imgbox';
item.images.forEach(img => {
const link = document.createElement('a');
link.href = img.image_url + "?fmt=webp&q=75";
link.setAttribute('data-fancybox', 'gallery');
link.className = 'fancybox';
const imgTag = document.createElement('img');
imgTag.src = img.image_url + "?fmt=webp&q=75";
link.appendChild(imgTag);
imgDiv.appendChild(link);
});
content += imgDiv.outerHTML;
}

// 外链 / GitHub 项目
// 外链 / GitHub 项目
if (['WEBSITE', 'GITHUBPROJ'].includes(item.extension_type)) {
let siteUrl = '', title = '';
let extensionBack = "https://p.liiiu.cn/i/2024/07/27/66a4632bbf06e.webp";

// 解析 extension 字段
try {
const extObj = typeof item.extension === 'string' ? JSON.parse(item.extension) : item.extension;
siteUrl = extObj.site || extObj.url || item.extension;
title = extObj.title || siteUrl;
} catch {
siteUrl = item.extension;
title = siteUrl;
}

// 特殊处理 GitHub 项目
if (item.extension_type === 'GITHUBPROJ') {
extensionBack = "https://p.liiiu.cn/i/2024/07/27/66a461a3098aa.webp";

// 提取 GitHub 项目名
const match = siteUrl.match(/^https?:\/\/github\.com\/[^/]+\/([^/?#]+)/i);
if (match) {
title = match[1]; // 获取仓库名
} else {
// fallback:从最后一个路径段提取
try {
const parts = new URL(siteUrl).pathname.split('/').filter(Boolean);
title = parts.pop() || siteUrl;
} catch {
// 如果 URL 无效则保留原始
}
}
}

// 输出 HTML 结构
content += `
<div class="shuoshuo-external-link">
<a class="external-link" href="${siteUrl}" target="_blank" rel="nofollow noopener">
<div class="external-link-left" style="background-image:url(${extensionBack})"></div>
<div class="external-link-right">
<div class="external-link-title">${title}</div>
<div>点击跳转<i class="fa-solid fa-angle-right"></i></div>
</div>
</a>
</div>`;
}


// 音乐
if (item.extension_type === 'MUSIC' && item.extension) {
const link = item.extension;
let server = '';
if (link.includes('music.163.com')) server = 'netease';
else if (link.includes('y.qq.com')) server = 'tencent';
const idMatch = link.match(/id=(\d+)/);
const id = idMatch ? idMatch[1] : '';
if (server && id) {
content += `<meting-js server="${server}" type="song" id="${id}" api="https://met.liiiu.cn/meting/api?server=:server&type=:type&id=:id&auth=:auth&r=:r"></meting-js>`;
}
}

// 视频
if (item.extension_type === 'VIDEO' && item.extension) {
const video = item.extension;
if (video.startsWith('BV')) {
const bilibiliUrl = `https://www.bilibili.com/blackboard/html5mobileplayer.html?bvid=${video}&as_wide=1&high_quality=1&danmaku=0`;
content += `
<div style="position: relative; padding: 30% 45%; margin-top: 10px;">
<iframe style="position:absolute;width:100%;height:100%;left:0;top:0;border-radius:12px;"
src="${bilibiliUrl}"
frameborder="no"
allowfullscreen="true"
loading="lazy"></iframe>
</div>`;
} else {
const youtubeUrl = `https://www.youtube.com/embed/${video}`;
content += `
<div style="position: relative; padding: 30% 45%; margin-top: 10px;">
<iframe style="position:absolute;width:100%;height:100%;left:0;top:0;border-radius:12px;"
src="${youtubeUrl}"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen></iframe>
</div>`;
}
}

return {
content,
user: item.username || '匿名',
avatar: 'https://p.liiiu.cn/i/2025/03/13/67d2fc82d329c.webp',
date,
location: '',
tags: Array.isArray(item.tags) && item.tags.length ? item.tags.map(t => t.name) : ['无标签'],
text: content.replace(/\[(.*?)\]\((.*?)\)/g, '[链接]')
};
};

const generateTalkElement = (item) => {
const talkItem = document.createElement('div');
talkItem.className = 'talk_item';

const talkMeta = document.createElement('div');
talkMeta.className = 'talk_meta';
const avatar = document.createElement('img');
avatar.className = 'no-lightbox avatar';
avatar.src = item.avatar;

const info = document.createElement('div');
info.className = 'info';
const nick = document.createElement('span');
nick.className = 'talk_nick';
nick.innerHTML = `${item.user} ${generateIconSVG()}`;
const date = document.createElement('span');
date.className = 'talk_date';
date.textContent = item.date;
info.appendChild(nick);
info.appendChild(date);
talkMeta.appendChild(avatar);
talkMeta.appendChild(info);

const talkContent = document.createElement('div');
talkContent.className = 'talk_content';
talkContent.innerHTML = item.content;

const talkBottom = document.createElement('div');
talkBottom.className = 'talk_bottom';
const tags = document.createElement('div');
const tag = document.createElement('span');
tag.className = 'talk_tag';
tag.textContent = `🏷️${item.tags}`;
//const loc = document.createElement('span');
//loc.className = 'location_tag';
//loc.textContent = `🌍${item.location}`;
tags.appendChild(tag);
//tags.appendChild(loc);

const commentLink = document.createElement('a');
commentLink.href = 'javascript:;';
commentLink.onclick = () => goComment(item.text);
const icon = document.createElement('span');
icon.className = 'icon';
icon.innerHTML = '<i class="fa-solid fa-message fa-fw"></i>';
commentLink.appendChild(icon);

talkBottom.appendChild(tags);
talkBottom.appendChild(commentLink);

talkItem.appendChild(talkMeta);
talkItem.appendChild(talkContent);
talkItem.appendChild(talkBottom);

return talkItem;
};

const goComment = (e) => {
const match = e.match(/<div class="talk_content_text">([\s\S]*?)<\/div>/);
const textContent = match ? match[1] : "";
const textarea = document.querySelector(".atk-textarea");
textarea.value = `> ${textContent}\n\n`;
textarea.focus();
btf.snackbarShow("已为您引用该说说,不删除空格效果更佳");
};

const formatTime = (time) => {
const d = new Date(time);
const pad = (n) => n.toString().padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
};

fetchAndRenderTalks();
}

renderTalks();

// function whenDOMReady() {
// const talkContainer = document.querySelector('#talk');
// talkContainer.innerHTML = '';
// fetchAndRenderTalks();
// }
// whenDOMReady();
// document.addEventListener("pjax:complete", whenDOMReady);

自行修改js文件中的Ech0地址为你的地址,在文件中,有一个gocomment函数,实现的是获取卡片中的文本内容,如果如果出现不匹配的情况,请自行修改一下类名,这里我匹配的是artalk的输入框。

然后引入样式文件,这个文件可以在配置文件中引用,也可以在页面文件中类似于shuoshuo.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
:root {
--liushen-card-bg: #fff;
--liushen-card-border: 1px solid #e3e8f7;
--card-box-shadow: 0 3px 8px 6px rgba(7,17,27,0.09);
--card-hover-box-shadow: 0 3px 8px 6px rgba(7,17,27,0.2);
--liushen-card-secondbg: #f1f3f8;
--liushen-button-hover-bg: #2679cc;
--liushen-text: #4c4948;
--liushen-button-bg: #f1f3f8;
--liushen-fancybox-bg: rgba(255,255,255,0.5);
}

:root, [data-theme=dark] {
--liushen-card-bg: #181818;
--liushen-card-secondbg: #30343f;
--liushen-card-border: 1px solid #42444a;
--card-box-shadow: 0 3px 8px 6px rgba(7,17,27,0.09);
--card-hover-box-shadow: 0 3px 8px 6px rgba(7,17,27,0.2);
--liushen-button-bg: #30343f;
--liushen-button-hover-bg: #2679cc;
--liushen-text: rgba(255,255,255,0.702);
--liushen-fancybox-bg: rgba(0,0,0,0.5);
}

/* 卡片初始化 */
#talk .talk_item {
width: calc(33.333% - 6px);
background: var(--liushen-card-bg);
border: var(--liushen-card-border);
box-shadow: var(--card-box-shadow);
transition: box-shadow .3s ease-in-out;
border-radius: 12px;
display: flex;
flex-direction: column;
padding: 20px;
margin-bottom: 9px;
margin-right: 9px;
}
#talk .talk_item:hover {
box-shadow: var(--card-hover-box-shadow);
}

@media (max-width: 900px) {
#talk .talk_item {
width: calc(50% - 5px);
}
}
@media (max-width: 450px) {
#talk .talk_item {
width: calc(100%);
}
}

#talk{
position: relative;
width: 100%;
box-sizing: border-box;
}

#talk .talk_meta .avatar {
margin: 0 !important;
width: 60px;
height: 60px;
border-radius: 12px;
}
#talk .talk_bottom,
#talk .talk_meta {
display: flex;
align-items: center;
}
#talk .talk_meta {
display: flex;
align-items: center;
width: 100%;
padding-bottom: 10px;
border-bottom: 1px dashed grey; /* 添加灰色虚线边框 */
}
#talk .talk_bottom {
margin-top: 15px;
padding-top: 10px;
border-top: 1px dashed grey; /* 添加灰色虚线边框 */
justify-content: space-between;
}
#talk .talk_meta .info {
display: flex;
flex-direction: column;
margin-left: 10px;
}
#talk .talk_meta .info .talk_nick {
color: #6dbdc3;
font-size: 1.2rem;
}
#talk .talk_meta .info svg.is-badge.icon {
width: 15px;
padding-top: 3px;
}
#talk .talk_meta .info span.talk_date {
opacity: .6;
}
#talk .talk_item .talk_content {
margin-top: 10px;
}
#talk .talk_item .talk_content .zone_imgbox {
display: flex;
flex-wrap: wrap;
--w: calc(25% - 8px);
gap: 10px;
margin-top: 10px;
}
#talk .talk_item .talk_content .zone_imgbox a {
display: block;
border-radius: 12px;
width: var(--w);
aspect-ratio: 1/1;
position: relative;
}
#talk .talk_item .talk_content .zone_imgbox a:first-child {
width: 100%;
aspect-ratio: 1.8;
}
#talk .talk_item .talk_content .zone_imgbox img {
border-radius: 10px;
width: 100%;
height: 100%;
margin: 0 !important;
object-fit: cover;
}
/* 底部 */
#talk .talk_item .talk_bottom {
opacity: .9;
}
#talk .talk_item .talk_bottom .icon {
float: right;
transition: all .3s;
}
#talk .talk_item .talk_bottom .icon:hover {
color: #49b1f5;
}
#talk .talk_item .talk_bottom span.talk_tag,
#talk .talk_item .talk_bottom span.location_tag {
font-size: 14px;
background-color: var(--liushen-card-secondbg);
border-radius: 12px;
padding: 3px 15px 3px 10px;
transition: box-shadow 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

#talk .talk_item .talk_bottom span.location_tag {
margin-left: 5px;
}

#talk .talk_item .talk_bottom span.talk_tag:hover,
#talk .talk_item .talk_bottom span.location_tag:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
#talk .talk_item .talk_content>a {
margin: 0 3px;
color: #ff7d73 !important;
}
#talk .talk_item .talk_content>a:hover{
text-decoration: none !important;
color: #ff5143 !important
}

@media screen and (max-width: 900px) {
#talk .talk_item .talk_content .zone_imgbox {
--w: calc(33% - 5px);
}
#talk .talk_item #post-comment{
margin: 0 3px
}
}
@media screen and (max-width: 768px) {
.zone_imgbox {
gap: 6px;
}
.zone_imgbox {
--w: calc(50% - 3px);
}
span.talk_date {
font-size: 14px;
}
}

#talk .talk_item .talk_content .douban-card {
margin-top: 10px !important;
text-decoration: none;
align-items: center;
border-radius: 12px;
color: #faebd7;
display: flex;
justify-content: center;
margin: 10px;
max-width: 400px;
overflow: hidden;
padding: 15px;
position: relative;
}

/* 外链卡片 */
#talk .talk_item .talk_content .shuoshuo-external-link {
/* 无下划线 */
width: 100%;
height: 80px;
margin-top: 10px;
border-radius: 12px;
background-color: var(--liushen-card-secondbg);
color: var(--liushen-card-text);
border: var(--liushen-card-border);
transition: background-color .3s ease-in-out;
}

.shuoshuo-external-link:hover {
background-color: var(--liushen-button-hover-bg);
}

.shuoshuo-external-link .external-link {
display: flex;
color: var(--liushen-text) !important;
width: 100%;
height: 100%;
}

.shuoshuo-external-link .external-link:hover {
color: white !important;
}

.shuoshuo-external-link .external-link:hover {
text-decoration: none !important;
}

.shuoshuo-external-link .external-link-left {
width: 60px;
height: 60px;
margin: 10px;
border-radius: 12px;
background-size: cover;
background-position: center;
}

.shuoshuo-external-link .external-link-right {
display: flex;
flex-direction: column;
justify-content: center;
width: calc(100% - 80px);
padding: 10px;
}

.shuoshuo-external-link .external-link-right .external-link-title {
font-size: 1.0rem;
font-weight: 800;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.shuoshuo-external-link .external-link-right i {
margin-left: 5px;
}

.limit {
width: 100%;
text-align: center;
margin-top: 30px;
}

如果一切正常,在说说页面下应该就可以显示了,如果仍然有问题可以评论区讨论,我们在这里继续进行下一步:

首页轮播

由于请求的地址一致,这里我直接用了同一个缓存,可以使网站的加载速度更近一步,缓存时间为半个小时,缓存位置为localstorage,缓存数据共30条,由于首页的轮播只需要最新的内容,这里我们取到缓存后节选前五条即可。

在主题配置文件内部引入外部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
let talkTimer = null;

const cacheKey = 'talksCache';
const cacheTimeKey = 'talksCacheTime';
const cacheDuration = 30 * 60 * 1000; // 缓存有效期 30分钟

function indexTalk() {
if (talkTimer) {
clearInterval(talkTimer);
talkTimer = null;
}

if (!document.getElementById('bber-talk')) return;

function toText(ls) {
return ls.map(item => {
let c = item.content || '';

const hasImg = /\!\[.*?\]\(.*?\)/.test(c);
const hasLink = /\[.*?\]\(.*?\)/.test(c);

c = c
.replace(/#(.*?)\s/g, '')
.replace(/\{.*?\}/g, '')
.replace(/\!\[.*?\]\(.*?\)/g, '<i class="fa-solid fa-image"></i>')
.replace(/\[.*?\]\(.*?\)/g, '<i class="fa-solid fa-link"></i>');

const icons = [];

if (item.images?.length && !hasImg) icons.push('fa-solid fa-image');
if (item.extension_type === 'VIDEO') icons.push('fa-solid fa-video');
if (item.extension_type === 'MUSIC') icons.push('fa-solid fa-music');
if (item.extension_type === 'WEBSITE' && !hasLink) icons.push('fa-solid fa-link');
if (item.extension_type === 'GITHUBPROJ' && !hasLink) icons.push('fab fa-github');

if (icons.length) c += ' ' + icons.map(i => `<i class="${i}"></i>`).join(' ');
return c;
});
}

// 渲染与轮播
function talk(ls) {
let html = '';
ls.forEach((item, i) => {
html += `<li class="item item-${i + 1}">${item}</li>`;
});

let box = document.querySelector("#bber-talk .talk-list");
if (!box) return;

box.innerHTML = html;

talkTimer = setInterval(() => {
if (box.children.length > 0) {
box.appendChild(box.children[0]);
}
}, 3000);
}

const cachedData = localStorage.getItem(cacheKey);
const cachedTime = localStorage.getItem(cacheTimeKey);
const currentTime = new Date().getTime();

// 判断缓存是否有效
if (cachedData && cachedTime && (currentTime - cachedTime < cacheDuration)) {
const data = toText(JSON.parse(cachedData));
talk(data.slice(0, 6)); // 使用缓存渲染数据
} else {
fetch('https://mm.liushen.fun/api/echo/page', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ page: 1, pageSize: 30 })
})
.then(res => res.json())
.then(data => {
// 适配新版结构:code=1 且 data.items 存在
if (data.code === 1 && data.data && Array.isArray(data.data.items)) {
localStorage.setItem(cacheKey, JSON.stringify(data.data.items));
localStorage.setItem(cacheTimeKey, currentTime.toString());

const formattedData = toText(data.data.items);
talk(formattedData.slice(0, 6));
} else {
console.warn('Unexpected API response format:', data);
}
})
.catch(error => console.error('Error fetching data:', error));
}
}

// pjax 支持
function whenDOMReady() {
indexTalk();
}

whenDOMReady();
document.addEventListener("pjax:complete", whenDOMReady);

注意自行修改其中第69行的API地址为你自己的。

然后在主题文件中添加以下的样式:

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
/* maintop */

#main_top {
display: flex;
justify-content: center;
z-index: 1;
max-width: 1200px;
margin: 20px auto;
width: 100%;
padding: 0 15px;
margin-top: 40px;
margin-bottom: 0px;
}

.hide-aside #main_top {
width: 80%;
}

.hide-aside #main_top #bber-talk {
max-width: 936px;
}

@media screen and (min-width: 2000px) {
.hide-aside #main_top #bber-talk {
max-width: 80%;
}

#main_top {
max-width: 70%;
}
}

@media screen and (max-width: 1210px) {
.hide-aside #main_top {
padding: 0 12px;
}
}

@media screen and (max-width: 900px) {
.hide-aside #main_top {
width: 100%;
padding: 0 15px;
}
}

@media screen and (max-width: 768px) {
.hide-aside #main_top {
padding: 0 5px;
}

div#main_top {
margin-top: 20px;
padding: 0 5px;
}
}

#bber-talk {
/* border-radius: 8px; */
/* background: var(--card-bg); */
/* box-shadow: none; */
box-sizing: border-box;
/* transition: all .3s ease-in-out; */
cursor: pointer;
width: 100%;
min-height: 50px;
padding: .5rem 1rem;
display: flex;
align-items: center;
overflow: hidden;
font-weight: 700;
}

#bber-talk,
#bber-talk a {
color: var(--font-color);
}

#bber-talk svg.icon {
width: 1em;
height: 1em;
vertical-align: -.15em;
fill: currentColor;
overflow: hidden;
font-size: 20px;
}

#bber-talk .item i {
margin-left: 5px;
}

#bber-talk > i {
font-size: 1.1rem;
}

#bber-talk .talk-list {
flex: 1;
max-height: 32px;
font-size: 16px;
padding: 0;
margin: 0;
overflow: hidden;
}

#bber-talk .talk-list:hover {
color: var(--default-bg-color);
transition: all .2s ease-in-out;
}

#bber-talk .talk-list li {
list-style: none;
width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
margin-left: 10px;
}

@media screen and (min-width: 770px) {
#bber-talk .talk-list {
text-align: center;
margin-right: 20px;
}
}

此时我们还缺少一个插入点,我们需要确保我们的轮播条在最顶部的位置轮播,创建文件themes\butterfly\layout\includes\others\memos_home.pug,写入以下内容:

1
2
3
4
5
6
7
if (is_home())
#main_top
#bber-talk.cardHover.bb_talk_swipper(onclick=`pjax.loadUrl("/shuoshuo/")`)
svg.icon(t='1660960757124', viewBox='0 0 1024 1024', version='1.1', xmlns='http://www.w3.org/2000/svg', p-id='3946', width='200', height='200')
path(d='M526.432 924.064c-20.96 0-44.16-12.576-68.96-37.344L274.752 704H192c-52.928 0-96-43.072-96-96V416c0-52.928 43.072-96 96-96h82.752l182.624-182.624c24.576-24.576 47.744-37.024 68.864-37.024C549.184 100.352 576 116 576 160v704c0 44.352-26.72 60.064-49.568 60.064zM192 384c-17.632 0-32 14.368-32 32v192c0 17.664 14.368 32 32 32h96c8.48 0 16.64 3.36 22.624 9.376l192.064 192.096c3.392 3.36 6.496 6.208 9.312 8.576V174.016a145.824 145.824 0 0 0-9.376 8.608l-192 192C304.64 380.64 296.48 384 288 384h-96zM687.584 730.368a31.898 31.898 0 0 1-18.656-6.016c-14.336-10.304-17.632-30.304-7.328-44.672l12.672-17.344C707.392 617.44 736 578.624 736 512c0-69.024-25.344-102.528-57.44-144.928-5.664-7.456-11.328-15.008-16.928-22.784-10.304-14.336-7.04-34.336 7.328-44.672 14.368-10.368 34.336-7.04 44.672 7.328 5.248 7.328 10.656 14.464 15.968 21.504C764.224 374.208 800 421.504 800 512c0 87.648-39.392 141.12-74.144 188.32l-12.224 16.736c-6.272 8.704-16.064 13.312-26.048 13.312z', p-id='3947')
path(d='M796.448 839.008a31.906 31.906 0 0 1-21.088-7.936c-13.28-11.648-14.624-31.872-2.976-45.152C836.608 712.672 896 628.864 896 512s-59.392-200.704-123.616-273.888c-11.648-13.312-10.304-33.504 2.976-45.184 13.216-11.648 33.44-10.336 45.152 2.944C889.472 274.56 960 373.6 960 512s-70.528 237.472-139.488 316.096c-6.368 7.232-15.2 10.912-24.064 10.912z', p-id='3948')
ul.talk-list 说说加载中。。。

创建了元素,我们需要通过一个注入点引入这个文件,将其插入到主页中,打开文件themes\butterfly\layout\includes\layout.pug,按照下面的位置插入我们的文件引用:

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
……
doctype html
html(lang=config.language data-theme=theme.display_mode class=htmlClassHideAside)
head
include ./head.pug
body
if theme.preloader.enable
!=partial('includes/loading/index', {}, {cache: true})

if theme.background
#web_bg

!=partial('includes/sidebar', {}, {cache: true})

#body-wrap(class=pageType)
include ./header/index.pug
+ include ./others/memos_home.pug

main#content-inner.layout(class=hideAside)
if body
div!= body
else
block content
if theme.aside.enable && page.aside !== false
include widget/index.pug

- const footerBg = theme.footer_img
- const footer_bg = footerBg ? footerBg === true ? bg_img : getBgPath(footerBg) : ''
footer#footer(style=footer_bg)
!=partial('includes/footer', {}, {cache: true})

include ./rightside.pug
!=partial('includes/rightmenu', {}, {cache: true})
include ./additional-js.pug

添加第22行,注意不要抄前面的加号,缩进与上面的include对齐即可。这样我们的首页轮播也实现了,如果有样式不对的地方请自行微调。

如果一切正常,魔改应该就结束啦!以后就可以直接在Ech0发表说说,前端可以直接获取哦!

总结

Ech0可以直接安装到桌面上,这点我很满意。就像装了个专门的说说软件,想发东西的时候点一下就行,很方便,看着也舒服。

工作的洪流,足以冲刷掉大部分的热情与精力,它让人疲惫,催人懒惰,也让人渐渐疏远了那些曾视若珍宝的热爱。我知道在日复一日的奔波中,维护这精神自留地是不容易的,每个月都有成本,且没有回报。但我依然选择坚持,并非为了向谁证明什么,而是想守护这份曾经的热爱,抵消自己的逐渐老去。总有人说,兴趣不过是一时兴起,三分钟热度。而我想用行动去打破这句断言,告诉他,兴趣并非转瞬即逝的烟火,它也可以是恒久燃烧的,只要我们愿意为之添柴。

后面更新可能没那么勤快了,但我会尽量保证每个月都有点东西发出来。给自己立个flag吧,希望能做到。

也希望各位朋友都能坚持下去,我们一起加油,山顶见!

每日一图

图片来自哲风壁纸

孤独的猫