Zuletzt aktiv 1 week ago

将ech0说说展示到前端网站页面

Änderung 31660901590511a6a17658815e6fc034cf116013

ech0-shuoshuo.js Originalformat
1(function () {
2const TALK_API_URL = 'https://mm.liushen.fun/api/echo/page';
3const TALK_CACHE_KEY = 'liushenEchoCacheV2';
4const TALK_CACHE_TIME_KEY = 'liushenEchoCacheTimeV2';
5const TALK_CACHE_DURATION = 30 * 60 * 1000;
6const TALK_AVATAR = 'https://p.liiiu.cn/i/2025/03/13/67d2fc82d329c.webp';
7const shuoshuoState = window.__liushenShuoshuoState || (window.__liushenShuoshuoState = {
8 resizeHandler: null,
9 afterRenderTimer: null,
10 listenersBound: false
11});
12
13function cleanupShuoshuo() {
14 if (shuoshuoState.afterRenderTimer) {
15 window.clearTimeout(shuoshuoState.afterRenderTimer);
16 shuoshuoState.afterRenderTimer = null;
17 }
18
19 if (shuoshuoState.resizeHandler) {
20 window.removeEventListener('resize', shuoshuoState.resizeHandler);
21 shuoshuoState.resizeHandler = null;
22 }
23}
24
25function renderTalks() {
26 cleanupShuoshuo();
27
28 const talkContainer = document.querySelector('#talk');
29 if (!talkContainer) return;
30
31 talkContainer.innerHTML = '';
32
33 const generateIconSVG = () => {
34 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>';
35 };
36
37 const waterfall = (container) => {
38 function getMargin(side, element) {
39 const styles = window.getComputedStyle(element);
40 return parseFloat(styles[`margin${side}`]) || 0;
41 }
42
43 function toPx(value) {
44 return `${value}px`;
45 }
46
47 function getTop(element) {
48 return parseFloat(element.style.top);
49 }
50
51 function getLeft(element) {
52 return parseFloat(element.style.left);
53 }
54
55 function getWidth(element) {
56 return element.clientWidth;
57 }
58
59 function getHeight(element) {
60 return element.clientHeight;
61 }
62
63 function getBottom(element) {
64 return getTop(element) + getHeight(element) + getMargin('Bottom', element);
65 }
66
67 function getRight(element) {
68 return getLeft(element) + getWidth(element) + getMargin('Right', element);
69 }
70
71 function sortColumns(elements) {
72 elements.sort((left, right) => {
73 return getBottom(left) === getBottom(right)
74 ? getLeft(right) - getLeft(left)
75 : getBottom(right) - getBottom(left);
76 });
77 }
78
79 if (typeof container === 'string') {
80 container = document.querySelector(container);
81 }
82
83 if (!container) return;
84
85 const items = Array.from(container.children).map(item => {
86 item.style.position = 'absolute';
87 return item;
88 });
89
90 container.style.position = 'relative';
91
92 const columns = [];
93 if (items.length) {
94 items[0].style.top = '0px';
95 items[0].style.left = toPx(getMargin('Left', items[0]));
96 columns.push(items[0]);
97 }
98
99 let index = 1;
100 for (; index < items.length; index += 1) {
101 const previous = items[index - 1];
102 const current = items[index];
103 const fits = getRight(previous) + getWidth(current) <= getWidth(container);
104
105 if (!fits) break;
106
107 current.style.top = previous.style.top;
108 current.style.left = toPx(getRight(previous) + getMargin('Left', current));
109 columns.push(current);
110 }
111
112 for (; index < items.length; index += 1) {
113 sortColumns(columns);
114 const current = items[index];
115 const column = columns.pop();
116
117 current.style.top = toPx(getBottom(column) + getMargin('Top', current));
118 current.style.left = toPx(getLeft(column));
119 columns.push(current);
120 }
121
122 sortColumns(columns);
123 const tallestColumn = columns[0];
124 container.style.height = tallestColumn ? toPx(getBottom(tallestColumn) + getMargin('Bottom', tallestColumn)) : '0px';
125
126 const currentWidth = getWidth(container);
127 shuoshuoState.resizeHandler = () => {
128 const currentContainer = document.querySelector('#talk');
129 if (!currentContainer || !document.body.contains(currentContainer)) {
130 cleanupShuoshuo();
131 return;
132 }
133
134 if (getWidth(currentContainer) !== currentWidth) {
135 waterfall(currentContainer);
136 }
137 };
138
139 window.addEventListener('resize', shuoshuoState.resizeHandler);
140 };
141
142 const parseMaybeJson = (value) => {
143 return value && typeof value === 'object' ? value : null;
144 };
145
146 const getEchoExtension = (item) => {
147 return parseMaybeJson(item?.extension);
148 };
149
150 const getEchoExtensionType = (item) => {
151 return getEchoExtension(item)?.type || '';
152 };
153
154 const getEchoExtensionPayload = (item) => {
155 const extension = getEchoExtension(item);
156 return extension?.payload || null;
157 };
158
159 const getEchoImages = (item) => {
160 if (!Array.isArray(item?.echo_files)) return [];
161
162 return item.echo_files
163 .map(entry => entry?.file || entry)
164 .filter(file => {
165 const category = String(file?.category || '').toLowerCase();
166 const contentType = String(file?.content_type || '').toLowerCase();
167 return category === 'image' || contentType.startsWith('image/');
168 })
169 .map(file => {
170 let url = file?.url;
171 if (!url) return null;
172
173 // 如果是相对路径,补全为完整 URL
174 if (url.startsWith('/')) {
175 url = 'https://mm.liushen.fun' + url;
176 } else if (!url.startsWith('http://') && !url.startsWith('https://')) {
177 url = 'https://mm.liushen.fun/' + url;
178 }
179
180 return url;
181 })
182 .filter(Boolean);
183 };
184
185 const getEchoTags = (item) => {
186 if (!Array.isArray(item?.tags) || !item.tags.length) return ['无标签'];
187 return item.tags.map(tag => tag?.name || tag).filter(Boolean);
188 };
189
190 const formatTime = (time) => {
191 // 如果是 Unix 时间戳(秒),转换为毫秒
192 const timestamp = time < 10000000000 ? time * 1000 : time;
193 const date = new Date(timestamp);
194 const pad = value => String(value).padStart(2, '0');
195 return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
196 };
197
198 const renderTextContent = (content) => {
199 return (content || '')
200 .replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="nofollow noopener">@$1</a>')
201 .replace(/- \[ \]/g, '[]')
202 .replace(/- \[x\]/gi, '[x]')
203 .replace(/\n/g, '<br>');
204 };
205
206 const buildImageHtml = (images) => {
207 if (!images.length) return '';
208
209 const imageLinks = images.map(url => {
210 const safeUrl = `${url}?fmt=webp&q=75`;
211 return `<a href="${safeUrl}" data-fancybox="gallery" class="fancybox"><img src="${safeUrl}" loading="lazy"></a>`;
212 }).join('');
213
214 return `<div class="zone_imgbox">${imageLinks}</div>`;
215 };
216
217 const getGithubTitle = (repoUrl) => {
218 if (!repoUrl) return '';
219
220 const match = repoUrl.match(/^https?:\/\/github\.com\/[^/]+\/([^/?#]+)/i);
221 if (match) return match[1];
222
223 try {
224 const parts = new URL(repoUrl).pathname.split('/').filter(Boolean);
225 return parts.pop() || repoUrl;
226 } catch (error) {
227 return repoUrl;
228 }
229 };
230
231 const buildExternalHtml = (type, payload) => {
232 if (!payload) return '';
233
234 let siteUrl = '';
235 let title = '';
236 let background = 'https://p.liiiu.cn/i/2024/07/27/66a4632bbf06e.webp';
237
238 if (type === 'WEBSITE') {
239 siteUrl = payload.site || payload.url || '';
240 title = payload.title || siteUrl;
241 }
242
243 if (type === 'GITHUBPROJ') {
244 siteUrl = payload.repoUrl || payload.url || '';
245 title = payload.title || getGithubTitle(siteUrl);
246 background = 'https://p.liiiu.cn/i/2024/07/27/66a461a3098aa.webp';
247 }
248
249 if (!siteUrl) return '';
250
251 return `
252 <div class="shuoshuo-external-link">
253 <a class="external-link" href="${siteUrl}" target="_blank" rel="nofollow noopener">
254 <div class="external-link-left" style="background-image:url(${background})"></div>
255 <div class="external-link-right">
256 <div class="external-link-title">${title}</div>
257 <div>点击跳转<i class="fa-solid fa-angle-right"></i></div>
258 </div>
259 </a>
260 </div>
261 `;
262 };
263
264 const getMusicInfo = (payload) => {
265 const link = payload?.url;
266 if (!link) return null;
267
268 let server = '';
269 if (link.includes('music.163.com')) server = 'netease';
270 if (link.includes('y.qq.com')) server = 'tencent';
271
272 const idMatch = link.match(/id=(\d+)/);
273 if (!server || !idMatch) return null;
274
275 return { server, id: idMatch[1] };
276 };
277
278 const buildMusicHtml = (payload) => {
279 const music = getMusicInfo(payload);
280 if (!music) return '';
281
282 return `<meting-js server="${music.server}" type="song" id="${music.id}" api="https://met.liiiu.cn/meting/api?server=:server&type=:type&id=:id&auth=:auth&r=:r"></meting-js>`;
283 };
284
285 const getYoutubeVideoId = (value) => {
286 if (!value) return '';
287 if (/^[a-zA-Z0-9_-]{11}$/.test(value)) return value;
288
289 try {
290 const url = new URL(value);
291 if (url.hostname.includes('youtu.be')) return url.pathname.replace('/', '');
292 if (url.hostname.includes('youtube.com')) {
293 return url.searchParams.get('v') || url.pathname.split('/').filter(Boolean).pop() || '';
294 }
295 } catch (error) {
296 return '';
297 }
298
299 return '';
300 };
301
302 const buildVideoHtml = (payload) => {
303 const rawValue = payload?.videoId || payload?.url || '';
304
305 if (!rawValue) return '';
306
307 let embedUrl = '';
308
309 if (/^BV[0-9A-Za-z]+$/i.test(rawValue)) {
310 embedUrl = `https://www.bilibili.com/blackboard/html5mobileplayer.html?bvid=${rawValue}&as_wide=1&high_quality=1&danmaku=0`;
311 } else {
312 const youtubeId = getYoutubeVideoId(rawValue);
313 if (youtubeId) {
314 embedUrl = `https://www.youtube.com/embed/${youtubeId}`;
315 }
316 }
317
318 if (!embedUrl) return '';
319
320 return `
321 <div style="position: relative; padding: 30% 45%; margin-top: 10px;">
322 <iframe
323 style="position:absolute;width:100%;height:100%;left:0;top:0;border-radius:12px;"
324 src="${embedUrl}"
325 frameborder="0"
326 allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
327 allowfullscreen
328 loading="lazy">
329 </iframe>
330 </div>
331 `;
332 };
333
334 const normalizeTalk = (item) => {
335 const extensionType = getEchoExtensionType(item);
336 const extensionPayload = getEchoExtensionPayload(item);
337 const textContent = item?.content || '';
338 const images = getEchoImages(item);
339
340 let content = `<div class="talk_content_text">${renderTextContent(textContent)}</div>`;
341 content += buildImageHtml(images);
342
343 if (extensionType === 'WEBSITE' || extensionType === 'GITHUBPROJ') {
344 content += buildExternalHtml(extensionType, extensionPayload);
345 }
346
347 if (extensionType === 'MUSIC') {
348 content += buildMusicHtml(extensionPayload);
349 }
350
351 if (extensionType === 'VIDEO') {
352 content += buildVideoHtml(extensionPayload);
353 }
354
355 return {
356 content,
357 user: item?.username || '匿名',
358 avatar: TALK_AVATAR,
359 date: formatTime(item?.created_at),
360 tags: getEchoTags(item),
361 quoteText: textContent
362 };
363 };
364
365 const generateTalkElement = (item) => {
366 const talkItem = document.createElement('div');
367 talkItem.className = 'talk_item';
368
369 const talkMeta = document.createElement('div');
370 talkMeta.className = 'talk_meta';
371
372 const avatar = document.createElement('img');
373 avatar.className = 'no-lightbox avatar';
374 avatar.src = item.avatar;
375
376 const info = document.createElement('div');
377 info.className = 'info';
378
379 const nick = document.createElement('span');
380 nick.className = 'talk_nick';
381 nick.innerHTML = `${item.user} ${generateIconSVG()}`;
382
383 const date = document.createElement('span');
384 date.className = 'talk_date';
385 date.textContent = item.date;
386
387 info.appendChild(nick);
388 info.appendChild(date);
389 talkMeta.appendChild(avatar);
390 talkMeta.appendChild(info);
391
392 const talkContent = document.createElement('div');
393 talkContent.className = 'talk_content';
394 talkContent.innerHTML = item.content;
395
396 const talkBottom = document.createElement('div');
397 talkBottom.className = 'talk_bottom';
398
399 const tags = document.createElement('div');
400 const tag = document.createElement('span');
401 tag.className = 'talk_tag';
402 tag.textContent = `# ${item.tags.join(' / ')}`;
403 tags.appendChild(tag);
404
405 const commentLink = document.createElement('a');
406 commentLink.href = 'javascript:;';
407 commentLink.addEventListener('click', () => goComment(item.quoteText));
408
409 const icon = document.createElement('span');
410 icon.className = 'icon';
411 icon.innerHTML = '<i class="fa-solid fa-message fa-fw"></i>';
412 commentLink.appendChild(icon);
413
414 talkBottom.appendChild(tags);
415 talkBottom.appendChild(commentLink);
416
417 talkItem.appendChild(talkMeta);
418 talkItem.appendChild(talkContent);
419 talkItem.appendChild(talkBottom);
420
421 return talkItem;
422 };
423
424 const goComment = (text) => {
425 const textarea = document.querySelector('.atk-textarea');
426 if (!textarea) return;
427
428 textarea.value = `> ${text || ''}\n\n`;
429 textarea.focus();
430
431 if (window.btf?.snackbarShow) {
432 btf.snackbarShow('已为您引用该说说,删除空格效果更佳');
433 }
434 };
435
436 const afterRender = () => {
437 waterfall('#talk');
438
439 if (window.btf?.loadLightbox) {
440 btf.loadLightbox(document.querySelectorAll('#talk img:not(.no-lightbox)'));
441 }
442
443 if (window.lazyLoadInstance?.update) {
444 lazyLoadInstance.update();
445 }
446 };
447
448 const renderTalksList = (list) => {
449 list.map(normalizeTalk).forEach(item => talkContainer.appendChild(generateTalkElement(item)));
450 afterRender();
451
452 const media = talkContainer.querySelectorAll('img, iframe, meting-js');
453 media.forEach(element => {
454 element.addEventListener('load', afterRender, { once: true });
455 });
456
457 shuoshuoState.afterRenderTimer = window.setTimeout(afterRender, 300);
458 };
459
460 const fetchTalks = () => {
461 const cachedData = localStorage.getItem(TALK_CACHE_KEY);
462 const cachedTime = Number(localStorage.getItem(TALK_CACHE_TIME_KEY));
463 const now = Date.now();
464
465 if (cachedData && cachedTime && now - cachedTime < TALK_CACHE_DURATION) {
466 renderTalksList(JSON.parse(cachedData));
467 return;
468 }
469
470 fetch(TALK_API_URL, {
471 method: 'POST',
472 headers: { 'Content-Type': 'application/json' },
473 body: JSON.stringify({ page: 1, pageSize: 30, search: '' })
474 })
475 .then(response => response.json())
476 .then(data => {
477 if (data?.code !== 1 || !Array.isArray(data?.data?.items)) {
478 console.warn('Unexpected API response format:', data);
479 renderTalksList([]);
480 return;
481 }
482
483 localStorage.setItem(TALK_CACHE_KEY, JSON.stringify(data.data.items));
484 localStorage.setItem(TALK_CACHE_TIME_KEY, now.toString());
485 renderTalksList(data.data.items);
486 })
487 .catch(error => console.error('Error fetching data:', error));
488 };
489
490 fetchTalks();
491}
492
493function initShuoshuoPage() {
494 renderTalks();
495}
496
497window.initShuoshuoPage = initShuoshuoPage;
498
499if (!shuoshuoState.listenersBound) {
500 document.addEventListener('pjax:send', cleanupShuoshuo);
501 document.addEventListener('pjax:complete', initShuoshuoPage);
502 shuoshuoState.listenersBound = true;
503}
504
505initShuoshuoPage();
506})();
507
twikoo-comments.js Originalformat
1const goComment = (e) => {
2 const n = document.querySelector(".el-textarea__inner");
3 n.value = `> ${e}\n\n`;
4 n.focus();
5 btf.snackbarShow("已为您引用该说说,不删除空格效果更佳");
6 };
7
8// 如果是twikoo评论区,请自行替换goComment函数,注意,如果评论区开了懒加载可能导致无法找到元素。