AI正在绞尽脑汁想思路ING···
BiのAI摘要
HunYuan-Lite

又开始折腾啦,这次把说说页面加上,改用moments项目的api

前期要求

硬件要求

  1. 一台服务器
  2. 一个可自主解析的域名

软件要求

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

介绍与展示

这里先给大家展示一下最终的效果,注意该教程可能仅适合部分主题,如果出现主题不适配的情况请自行适配,这里以本站主题 Hexo-theme-butterfly为基础进行修改:

  1. 说说页面:
  2. 轻量朋友圈
  3. 功能说明

Moments作为一个轻量朋友圈,其功能都是分享上的部分,如下所示:

  • 分享:链接,图片,音乐,视频,书籍,电影
  • 信息:自定义位置,自定义标签,是否公开
  • 页面:Markdown渲染,编辑说说,删除说说,暗夜模式,自定义图标,信息,CSS及JS代码
  • 功能:S3存储,文件查询,多用户注册,点赞,评论,API

简单介绍完毕,下面我就来教大家如何进行部署!

部署教程

Moments部署

Compose部署

官方给予了很完善的教程,这里我仅仅简单介绍一下 docker-compose部署的方式,如果你想以源码等其他方式进行部署,请查看文章开头部分的 github地址进行查阅。

首先,在服务器任意位置创建文件:docker-compose.yaml,填入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
version: '3'
services:
moments:
image: kingwrcy/moments:latest
container_name: moments
restart: always
environment:
port: 3000
JWT_KEY: "自己随便生成点字符串"
ENABLE_SWAGGER: "true"
CORS_ORIGIN: # 填写跨域域名
ports:
- "3000:3000" # 自行换端口,换前面的,后面的3000不要动
volumes:
- ./opt/data:/app/data
# - ./data/localtime:/etc/localtime:ro
# - ./data/timezone:/etc/timezone:ro

注意文件,我将 /opt/文件夹(当然可以改成其他的)下的 /data文件夹挂载了进去,数据都会在里面,迁移时仅需整体打包到新服务器即可。然后执行以下两条命令,后续需要升级也仅需要执行这两个命令:

1
2
docker-compose pull
docker-compose up -d

如果网络环境不佳,可尝试替换 docker源,可以自行查找

通过反向代理将其添加到某个域名中,这里就不再多说了,各大面板都有极其完备的反代文档。

Nginx修改

Moments在跨域 docker-compose文件中可以配置,所以不必进行此步

网站目录

返回到上一级目录,也就是域名名称的文件夹下,找到 Proxy文件夹,编辑里面的 root.conf文件为如下内容:

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
#代理配置

location / {
# 跨域设置
add_header Access-Control-Allow-Origin *; # 允许所有域名访问,你也可以指定具体域名
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS'; # 允许的 HTTP 方法
add_header Access-Control-Allow-Headers 'Origin, X-Requested-With, Content-Type, Accept, Authorization'; # 允许的请求头

# 处理 OPTIONS 请求,预检请求
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS';
add_header Access-Control-Allow-Headers 'Origin, X-Requested-With, Content-Type, Accept, Authorization';
add_header Access-Control-Max-Age 1728000;
add_header Content-Type 'text/plain charset=UTF-8';
add_header Content-Length 0;
return 204;
}

# 原代理设置
proxy_pass http://127.0.0.1:3003;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
add_header X-Cache $upstream_cache_status;
add_header Cache-Control no-cache;
proxy_ssl_server_name off;
proxy_ssl_name $proxy_host;
add_header Strict-Transport-Security "max-age=31536000";
}

下面是原代理设置,可以仅仅复制上面部分内容,下面保持不变,注意端口不要出问题。

前端实现

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

1
2
3
<link rel="stylesheet" href="https://fastly.jsdelivr.net/npm/aplayer/dist/APlayer.min.css" media="all" onload="this.media="all"">
<script src="https://fastly.jsdelivr.net/npm/aplayer/dist/APlayer.min.js"></script>
<script src="https://fastly.jsdelivr.net/npm/meting/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文件地址清自行修改,放在主题目录 /script/目录,自行创建,并写入以下内容:

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
function renderTalks() {
const talkContainer = document.querySelector('#talk');
const domain = 'https://mm.biss.click';
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.biss.click/api/memo/list';
const cacheKey = 'talksCache';
const cacheTimeKey = 'talksCacheTime';
const cacheDuration = 30 * 60 * 1000; // 半个小时 (30 分钟)

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

// 判断缓存是否有效
if (cachedData && cachedTime && (currentTime - cachedTime < cacheDuration)) {
const data = JSON.parse(cachedData);
renderTalks(data); // 使用缓存渲染数据
} else {
if (talkContainer) {
talkContainer.innerHTML = '';
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
size: 30
})
})
.then(res => res.json())
.then(data => {
if (data.code === 0 && data.data && Array.isArray(data.data.list)) {
// 缓存数据
localStorage.setItem(cacheKey, JSON.stringify(data.data.list));
localStorage.setItem(cacheTimeKey, currentTime.toString());
renderTalks(data.data.list); // 渲染数据
}
})
.catch(error => {
console.error('Error fetching data:', error);
});
}
}

// 渲染函数
function renderTalks(list) {
// 确保 data 是一个数组
if (Array.isArray(list)) {
let items = list.map(item => formatTalk(item, url));
items.forEach(item => talkContainer.appendChild(generateTalkElement(item)));
waterfall('#talk');
} else {
console.error('Data is not an array:', list);
}
}
};


const formatTalk = (item, url) => {
let date = formatTime(new Date(item.createdAt).toString());
let content = item.content;
let imgs = item.imgs ? item.imgs.split(',') : [];
let text = content;
content = text.replace(/\[(.*?)\]\((.*?)\)/g, `<a href="$2">@$1</a>`)
.replace(/- \[ \]/g, '⚪')
.replace(/- \[x\]/g, '⚫');
// 保留换行符,转换 \n 为 <br>
content = content.replace(/\n/g, '<br>');
// 将content用一个类包裹,便于后续处理
content = `<div class="talk_content_text">${content}</div>`;
if (imgs.length > 0) {
const imgDiv = document.createElement('div');
imgDiv.className = 'zone_imgbox';
imgs.forEach(e => {
const imgLink = document.createElement('a');
const imgUrl = domain + e;
imgLink.href = imgUrl;
imgLink.setAttribute('data-fancybox', 'gallery');
imgLink.className = 'fancybox';
imgLink.setAttribute('data-thumb', e);
const imgTag = document.createElement('img');
imgTag.src = domain + e;
imgLink.appendChild(imgTag);
imgDiv.appendChild(imgLink);
});
content += imgDiv.outerHTML;
}

// 外链分享功能
if (item.externalUrl) {
const externalUrl = item.externalUrl;
const externalTitle = item.externalTitle;
const externalFavicon = item.externalFavicon;

const externalContainer = `
<div class="shuoshuo-external-link">
<a class="external-link" href="${externalUrl}" target="_blank" rel="external nofollow noopener noreferrer">
<div class="external-link-left" style="background-image: url(${externalFavicon})"></div>
<div class="external-link-right">
<div class="external-link-title">${externalTitle}</div>
<div>点击跳转<i class="fa-solid fa-angle-right"></i></div>
</div>
</a>
</div>`;

content += externalContainer;
}

const ext = JSON.parse(item.ext || '{}');

if (ext.music && ext.music.id) {
const music = ext.music;
const musicUrl = music.api.replace(':server', music.server)
.replace(':type', music.type)
.replace(':id', music.id);
content += `
<meting-js server="${music.server}" type="${music.type}" id="${music.id}" api="${music.api}"></meting-js>
`;
}

if (ext.doubanMovie && ext.doubanMovie.id) {
const doubanMovie = ext.doubanMovie;
const doubanMovieUrl = doubanMovie.url;
const doubanTitle = doubanMovie.title;
// const doubanDesc = doubanMovie.desc || '暂无描述';
const doubanImage = doubanMovie.image;
const doubanDirector = doubanMovie.director || '未知导演';
const doubanRating = doubanMovie.rating || '暂无评分';
// const doubanReleaseDate = doubanMovie.releaseDate || '未知上映时间';
// const doubanActors = doubanMovie.actors || '未知演员';
const doubanRuntime = doubanMovie.runtime || '未知时长';

content += `
<a class="douban-card" href="${doubanMovieUrl}" target="_blank">
<div class="douban-card-bgimg" style="background-image: url('${doubanImage}');"></div>
<div class="douban-card-left">
<div class="douban-card-img" style="background-image: url('${doubanImage}');"></div>
</div>
<div class="douban-card-right">
<div class="douban-card-item"><span>电影名: </span><strong>${doubanTitle}</strong></div>
<div class="douban-card-item"><span>导演: </span><span>${doubanDirector}</span></div>
<div class="douban-card-item"><span>评分: </span><span>${doubanRating}</span></div>
<div class="douban-card-item"><span>时长: </span><span>${doubanRuntime}</span></div>
</div>
</a>
`;
}

if (ext.doubanBook && ext.doubanBook.id) {
const doubanBook = ext.doubanBook;
const bookUrl = doubanBook.url;
const bookTitle = doubanBook.title;
// const bookDesc = doubanBook.desc;
const bookImage = doubanBook.image;
const bookAuthor = doubanBook.author;
const bookRating = doubanBook.rating;
const bookPubDate = doubanBook.pubDate;

const bookTemplate = `
<a class="douban-card" href="${bookUrl}" target="_blank">
<div class="douban-card-bgimg" style="background-image: url('${bookImage}');"></div>
<div class="douban-card-left">
<div class="douban-card-img" style="background-image: url('${bookImage}');"></div>
</div>
<div class="douban-card-right">
<div class="douban-card-item">
<span>书名: </span><strong>${bookTitle}</strong>
</div>
<div class="douban-card-item">
<span>作者: </span><span>${bookAuthor}</span>
</div>
<div class="douban-card-item">
<span>出版年份: </span><span>${bookPubDate}</span>
</div>
<div class="douban-card-item">
<span>评分: </span><span>${bookRating}</span>
</div>
</div>
</a>
`;

content += bookTemplate;
}

if (ext.video && ext.video.type) {
const videoType = ext.video.type;
const videoUrl = ext.video.value;
if (videoType === 'bilibili') {
// Bilibili 视频模板
// 从形如https://www.bilibili.com/video/BV1VGAPeAEMQ/?vd_source=91b3158d27d98ff41f842508c3794a13 的链接中提取视频 BV1VGAPeAEMQ
const biliTemplate = `
<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="${videoUrl}&autoplay=0"
scrolling="no"
frameborder="no"
allowfullscreen>
</iframe>
</div>
`;
// 将模板插入到 DOM 中
content += biliTemplate;

} else if (videoType === 'youtube') {
// YouTube 视频模板
// 从形如https://youtu.be/2V6lvCUPT8I?si=DVhUas6l6qlAr6Ru的链接中提取视频 ID2V6lvCUPT8I
const youtubeTemplate = `
<div style="position: relative; padding: 30% 45%; margin-top: 10px;">
<iframe width="100%"
style="position: absolute; width: 100%; height: 100%; left: 0; top: 0; border-radius: 12px;"
src="${videoUrl}"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen>
</iframe>
</div>
`;
// 将模板插入到 DOM 中
content += youtubeTemplate;
}
}

return {
content: content,
user: item.user.nickname || '匿名',
avatar: item.user.avatarUrl || 'https://p.liiiu.cn/i/2024/03/29/66061417537af.png',
date: date,
location: item.location || '山西',
tags: item.tags ? item.tags.split(',').filter(tag => tag.trim() !== '') : ['无标签'],
text: content.replace(/\[(.*?)\]\((.*?)\)/g, '[链接]' + `${imgs.length ? '[图片]' : ''}`)
};
};

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 talkNick = document.createElement('span');
talkNick.className = 'talk_nick';
talkNick.innerHTML = `${item.user} ${generateIconSVG()}`;

const talkDate = document.createElement('span');
talkDate.className = 'talk_date';
talkDate.textContent = item.date;

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

const talkBottom = document.createElement('div');
talkBottom.className = 'talk_bottom';

const TagContainer = document.createElement('div');

const talkTag = document.createElement('span');
talkTag.className = 'talk_tag';
talkTag.textContent = `🏷️${item.tags}`;

const locationTag = document.createElement('span');
locationTag.className = 'location_tag';
locationTag.textContent = `🌍${item.location}`;

TagContainer.appendChild(talkTag);
TagContainer.appendChild(locationTag);

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

talkMeta.appendChild(avatar);
info.appendChild(talkNick);
info.appendChild(talkDate);
talkMeta.appendChild(info);
talkItem.appendChild(talkMeta);
talkItem.appendChild(talkContent);
talkBottom.appendChild(TagContainer);
talkBottom.appendChild(commentLink);
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 n = document.querySelector(".atk-textarea");
n.value = `> ${textContent}\n\n`;
n.focus();
btf.snackbarShow("已为您引用该说说,不删除空格效果更佳");
// const n = document.querySelector(".atk-textarea");
// n.value = `> ${e}\n\n`;
// n.focus();
// btf.snackbarShow("已为您引用该说说,不删除空格效果更佳");
};

const formatTime = (time) => {
const d = new Date(time);
const ls = [
d.getFullYear(),
d.getMonth() + 1,
d.getDate(),
d.getHours(),
d.getMinutes(),
d.getSeconds(),
];
const r = ls.map((a) => (a.toString().length === 1 ? '0' + a : a));
return `${r[0]}-${r[1]}-${r[2]} ${r[3]}:${r[4]}`;
};

fetchAndRenderTalks();
}

renderTalks();

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

自行修改js文件中的Moments地址为你的地址,在文件中,有一个gocomment函数,实现的是获取卡片中的文本内容,如果如果出现不匹配的情况,请自行修改一下类名,这里我匹配的是artalk的输入框。在这里我把代码做了修改,因为我不想使用s3,结果导致说说图片无法加载。。。
修改历程:日后再写

然后引入样式文件,这个文件可以在配置文件中引用,也可以在页面文件中类似于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
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
: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;
}

.douban-card .douban-card-bgimg {
background-position: 50%;
background-repeat: no-repeat;
background-size: 100%;
filter: blur(15px) brightness(.6);
height: 115%;
position: absolute;
width: 115%;
}

.douban-card .douban-card-left {
align-items: center;
display: flex;
flex-direction: column;
position: relative;
}

.douban-card .douban-card-left .douban-card-img {
transition: all .5s ease;
height: 130px;
position: relative;
width: 80px;
background-position: 50%;
background-repeat: no-repeat;
background-size: 100%;
}

.douban-card .douban-card-left:hover .douban-card-img {
filter: blur(5px) brightness(.6);
transform: perspective(800px) rotateX(180deg);
}

.douban-card .douban-card-right {
color: #faebd7;
display: flex;
flex-direction: column;
font-size: 14px;
line-height: 1.5;
margin-left: 12px;
position: relative;
}

.douban-card .douban-card-right .douban-card-item {
margin-top: 4px;
max-width: 95%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

/* 外链卡片 */
#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;
}

如果一切正常,应该就可以显示了,但是我在使用时发现卡片是黑色的,把前面的root内容删除之后就好了。

参考内容

当然还有chatgpt的支持