碎碎念 上班1个月!感觉灵魂都被抽干了,已经彻底成为合格牛马的形状了。每天睁眼就是工作,闭眼就是盼着周末,结果真到了周末,可能还得面对加班的召唤。这谁顶得住,总之就是累累的,困困的,呜呜呜……
前些天的月度答辩,那叫一个紧张,PPT还没讲,我人都要裂开了。脑子里预演了一万遍被怼到怀疑人生的场景。索性我们组的哥姐们都超nice,没有为难我这个新人,知道我第一个月基本属于啥也不会的废物状态。后面跟着做了一些业务,刚开始上手确实感觉有点难度,但沉下心来搞,发现其实也并没有想象中的那么复杂。总之,第一个月总算是有惊无险地正常度过啦!
回到正题,之前朋友写了个程序叫Ech0。一开始因为他的设计理念,界面做得特别简约,甚至从程序底层就限制了一些花里胡哨的操作(没错,说的就是想搞事的我)。所以当时测试了一个周,感觉不太符合我的折腾欲,就选择了暂时观望。后来嘛,忙入职、忙培训,这事儿就暂时搁置了。结果前阵子偶然逛到他博客,发现Ech0更新了好几个版本,加的新功能简直正中我的下怀!再加上它那一直很戳我的美观界面,我当场就心动了,于是稍微捣鼓了一下,火速迁移过来啦!更棒的是,结合PWA技术,现在能直接像手机App一样发说说了!这体验感,绝了!很不错!超喜欢!
现在就盼着作者大大后面能加上多标签功能了!到时候我想把标签整成定位,比如#摸鱼、#发呆之类的,人主打一个花里胡哨,记录生活嘛!
介绍 刚开始,我其实用的是Memos。该程序设计完善,功能花里胡哨,资源占用还低,所以吸引了大量用户,生态也相当全面。不过嘛,作者更新实在太频繁了,导致API时不时就不兼容,我那套定制的前端维护起来就有点麻烦了,具体原因可以看我之前写的这篇文章:
后来我换到了Moments,这绝对是个非常优秀的程序,功能和设计上都完全满足我的要求。但美中不足的是,它没有适配PWA。当然,这不算什么大问题,纯粹是我的个人需求。正巧那段时间,有个朋友在开发他的新项目Ech0,我一眼就被它那简约又美观的界面给吸引了,立马上手体验了一段时间。不过那时候的Ech0还比较早期,功能上不太全面:没有标签系统,图片不能上传到S3,也没有我喜欢的Meting音乐插件,甚至连视频分享都没有。这对于追求花里胡哨的我来说,肯定是不够用的,所以当时就没切换。
再后来,朋友很给力,Ech0项目逐渐完善,之前缺少的功能一个个都补上了。我再次体验的时候,发现已经十分好用,于是就果断迁移了过来,还顺手把我之前那套前端给适配上了。目前使用很爽,下面是几个程序的对比:
特性 MemosMomentsEch0PWA 适配✅ 支持 ❌ 不支持 ✅ 支持 标签系统 ✅ 完善 ✅ 完善 ✅ 支持 S3 图床✅ 支持 ✅ 支持 ✅ 支持 Meting 音乐❌ 不支持 ✅ 支持 ✅ 支持 视频分享 ✅ 支持 ✅ 支持 ✅ 支持 设计风格 功能导向,略显臃肿 功能全面,类似后台 简约美观,轻量 开发活跃度 非常活跃,API易变 相对稳定 活跃,快速迭代 个人评价 功能强大,但更新太折腾,生态不稳定。 功能完美,但没PWA对我是减分项 界面戳我,功能追上来了,PWA是加分项,目前的最爱
话不多说,开始教程!
教程 部署要求 硬件要求
一台服务器 一个可自主解析的域名 一个已经部署好的类Butterfly的Hexo博客 软件要求
docker环境反向代理工具(本文以Nginx为例) 效果展示 说说页面:
由于前端才是最主要的展示区域,在这里我尽可能做了最多的适配,适配了图片分享,链接分享,音乐分享,Bilibili分享,Youtube分享,黑夜模式等多种适配,这里仅展示部分功能,yoputube由于拉低网络加载速度,不予展示。其他的具体效果可以上网站我的说说页面自行查看。
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,在当前目录下执行:
如果不出意外,容器已经启动,自行实现反向代理即可。
应用商店 如果你为1Panel用户,可以选择直接通过我个人维护的第三方应用商店实现安装,
在以前的文章曾介绍了如何添加三方应用商店,这里就不重复了,首先按照以下教程安装三方应用商店:
安装好后,在应用商店直接搜索安装即可,更加快捷,更好维护!
前端魔改 说说页面 由于该项目利用了MetingJS和APlayer,所以请提前引入这两个包,Hexo-theme-butterfly中虽然有内置的两个包,仅需修改配置文件即可开启,但是版本比较老,这里我建议自行引入最新版本,在配置中引入以下文件,注意css和js应该是分开引入的,在主题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=" all" " > <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 ; } if (['WEBSITE' , 'GITHUBPROJ' ].includes (item.extension_type )) { let siteUrl = '' , title = '' ; let extensionBack = "https://p.liiiu.cn/i/2024/07/27/66a4632bbf06e.webp" ; 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; } if (item.extension_type === 'GITHUBPROJ' ) { extensionBack = "https://p.liiiu.cn/i/2024/07/27/66a461a3098aa.webp" ; const match = siteUrl.match (/^https?:\/\/github\.com\/[^/]+\/([^/?#]+)/i ); if (match) { title = match[1 ]; } else { try { const parts = new URL (siteUrl).pathname .split ('/' ).filter (Boolean ); title = parts.pop () || siteUrl; } catch { } } } 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} ` ; tags.appendChild (tag); 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 ();
自行修改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 ; 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 => { 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)); } } 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 #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 { box-sizing : border-box; 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 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吧,希望能做到。
也希望各位朋友都能坚持下去,我们一起加油,山顶见!
每日一图 图片来自哲风壁纸