ech0-shuoshuo.js
· 18 KiB · JavaScript
Bruto
(function () {
const TALK_API_URL = 'https://mm.liushen.fun/api/echo/page';
const TALK_CACHE_KEY = 'liushenEchoCacheV2';
const TALK_CACHE_TIME_KEY = 'liushenEchoCacheTimeV2';
const TALK_CACHE_DURATION = 30 * 60 * 1000;
const TALK_AVATAR = 'https://p.liiiu.cn/i/2025/03/13/67d2fc82d329c.webp';
const shuoshuoState = window.__liushenShuoshuoState || (window.__liushenShuoshuoState = {
resizeHandler: null,
afterRenderTimer: null,
listenersBound: false
});
function cleanupShuoshuo() {
if (shuoshuoState.afterRenderTimer) {
window.clearTimeout(shuoshuoState.afterRenderTimer);
shuoshuoState.afterRenderTimer = null;
}
if (shuoshuoState.resizeHandler) {
window.removeEventListener('resize', shuoshuoState.resizeHandler);
shuoshuoState.resizeHandler = null;
}
}
function renderTalks() {
cleanupShuoshuo();
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 = (container) => {
function getMargin(side, element) {
const styles = window.getComputedStyle(element);
return parseFloat(styles[`margin${side}`]) || 0;
}
function toPx(value) {
return `${value}px`;
}
function getTop(element) {
return parseFloat(element.style.top);
}
function getLeft(element) {
return parseFloat(element.style.left);
}
function getWidth(element) {
return element.clientWidth;
}
function getHeight(element) {
return element.clientHeight;
}
function getBottom(element) {
return getTop(element) + getHeight(element) + getMargin('Bottom', element);
}
function getRight(element) {
return getLeft(element) + getWidth(element) + getMargin('Right', element);
}
function sortColumns(elements) {
elements.sort((left, right) => {
return getBottom(left) === getBottom(right)
? getLeft(right) - getLeft(left)
: getBottom(right) - getBottom(left);
});
}
if (typeof container === 'string') {
container = document.querySelector(container);
}
if (!container) return;
const items = Array.from(container.children).map(item => {
item.style.position = 'absolute';
return item;
});
container.style.position = 'relative';
const columns = [];
if (items.length) {
items[0].style.top = '0px';
items[0].style.left = toPx(getMargin('Left', items[0]));
columns.push(items[0]);
}
let index = 1;
for (; index < items.length; index += 1) {
const previous = items[index - 1];
const current = items[index];
const fits = getRight(previous) + getWidth(current) <= getWidth(container);
if (!fits) break;
current.style.top = previous.style.top;
current.style.left = toPx(getRight(previous) + getMargin('Left', current));
columns.push(current);
}
for (; index < items.length; index += 1) {
sortColumns(columns);
const current = items[index];
const column = columns.pop();
current.style.top = toPx(getBottom(column) + getMargin('Top', current));
current.style.left = toPx(getLeft(column));
columns.push(current);
}
sortColumns(columns);
const tallestColumn = columns[0];
container.style.height = tallestColumn ? toPx(getBottom(tallestColumn) + getMargin('Bottom', tallestColumn)) : '0px';
const currentWidth = getWidth(container);
shuoshuoState.resizeHandler = () => {
const currentContainer = document.querySelector('#talk');
if (!currentContainer || !document.body.contains(currentContainer)) {
cleanupShuoshuo();
return;
}
if (getWidth(currentContainer) !== currentWidth) {
waterfall(currentContainer);
}
};
window.addEventListener('resize', shuoshuoState.resizeHandler);
};
const parseMaybeJson = (value) => {
return value && typeof value === 'object' ? value : null;
};
const getEchoExtension = (item) => {
return parseMaybeJson(item?.extension);
};
const getEchoExtensionType = (item) => {
return getEchoExtension(item)?.type || '';
};
const getEchoExtensionPayload = (item) => {
const extension = getEchoExtension(item);
return extension?.payload || null;
};
const getEchoImages = (item) => {
if (!Array.isArray(item?.echo_files)) return [];
return item.echo_files
.map(entry => entry?.file || entry)
.filter(file => {
const category = String(file?.category || '').toLowerCase();
const contentType = String(file?.content_type || '').toLowerCase();
return category === 'image' || contentType.startsWith('image/');
})
.map(file => {
let url = file?.url;
if (!url) return null;
// 如果是相对路径,补全为完整 URL
if (url.startsWith('/')) {
url = 'https://mm.liushen.fun' + url;
} else if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://mm.liushen.fun/' + url;
}
return url;
})
.filter(Boolean);
};
const getEchoTags = (item) => {
if (!Array.isArray(item?.tags) || !item.tags.length) return ['无标签'];
return item.tags.map(tag => tag?.name || tag).filter(Boolean);
};
const formatTime = (time) => {
// 如果是 Unix 时间戳(秒),转换为毫秒
const timestamp = time < 10000000000 ? time * 1000 : time;
const date = new Date(timestamp);
const pad = value => String(value).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
};
const renderTextContent = (content) => {
return (content || '')
.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="nofollow noopener">@$1</a>')
.replace(/- \[ \]/g, '[]')
.replace(/- \[x\]/gi, '[x]')
.replace(/\n/g, '<br>');
};
const buildImageHtml = (images) => {
if (!images.length) return '';
const imageLinks = images.map(url => {
const safeUrl = `${url}?fmt=webp&q=75`;
return `<a href="${safeUrl}" data-fancybox="gallery" class="fancybox"><img src="${safeUrl}" loading="lazy"></a>`;
}).join('');
return `<div class="zone_imgbox">${imageLinks}</div>`;
};
const getGithubTitle = (repoUrl) => {
if (!repoUrl) return '';
const match = repoUrl.match(/^https?:\/\/github\.com\/[^/]+\/([^/?#]+)/i);
if (match) return match[1];
try {
const parts = new URL(repoUrl).pathname.split('/').filter(Boolean);
return parts.pop() || repoUrl;
} catch (error) {
return repoUrl;
}
};
const buildExternalHtml = (type, payload) => {
if (!payload) return '';
let siteUrl = '';
let title = '';
let background = 'https://p.liiiu.cn/i/2024/07/27/66a4632bbf06e.webp';
if (type === 'WEBSITE') {
siteUrl = payload.site || payload.url || '';
title = payload.title || siteUrl;
}
if (type === 'GITHUBPROJ') {
siteUrl = payload.repoUrl || payload.url || '';
title = payload.title || getGithubTitle(siteUrl);
background = 'https://p.liiiu.cn/i/2024/07/27/66a461a3098aa.webp';
}
if (!siteUrl) return '';
return `
<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(${background})"></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>
`;
};
const getMusicInfo = (payload) => {
const link = payload?.url;
if (!link) return null;
let server = '';
if (link.includes('music.163.com')) server = 'netease';
if (link.includes('y.qq.com')) server = 'tencent';
const idMatch = link.match(/id=(\d+)/);
if (!server || !idMatch) return null;
return { server, id: idMatch[1] };
};
const buildMusicHtml = (payload) => {
const music = getMusicInfo(payload);
if (!music) return '';
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>`;
};
const getYoutubeVideoId = (value) => {
if (!value) return '';
if (/^[a-zA-Z0-9_-]{11}$/.test(value)) return value;
try {
const url = new URL(value);
if (url.hostname.includes('youtu.be')) return url.pathname.replace('/', '');
if (url.hostname.includes('youtube.com')) {
return url.searchParams.get('v') || url.pathname.split('/').filter(Boolean).pop() || '';
}
} catch (error) {
return '';
}
return '';
};
const buildVideoHtml = (payload) => {
const rawValue = payload?.videoId || payload?.url || '';
if (!rawValue) return '';
let embedUrl = '';
if (/^BV[0-9A-Za-z]+$/i.test(rawValue)) {
embedUrl = `https://www.bilibili.com/blackboard/html5mobileplayer.html?bvid=${rawValue}&as_wide=1&high_quality=1&danmaku=0`;
} else {
const youtubeId = getYoutubeVideoId(rawValue);
if (youtubeId) {
embedUrl = `https://www.youtube.com/embed/${youtubeId}`;
}
}
if (!embedUrl) return '';
return `
<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="${embedUrl}"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
loading="lazy">
</iframe>
</div>
`;
};
const normalizeTalk = (item) => {
const extensionType = getEchoExtensionType(item);
const extensionPayload = getEchoExtensionPayload(item);
const textContent = item?.content || '';
const images = getEchoImages(item);
let content = `<div class="talk_content_text">${renderTextContent(textContent)}</div>`;
content += buildImageHtml(images);
if (extensionType === 'WEBSITE' || extensionType === 'GITHUBPROJ') {
content += buildExternalHtml(extensionType, extensionPayload);
}
if (extensionType === 'MUSIC') {
content += buildMusicHtml(extensionPayload);
}
if (extensionType === 'VIDEO') {
content += buildVideoHtml(extensionPayload);
}
return {
content,
user: item?.username || '匿名',
avatar: TALK_AVATAR,
date: formatTime(item?.created_at),
tags: getEchoTags(item),
quoteText: textContent
};
};
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.join(' / ')}`;
tags.appendChild(tag);
const commentLink = document.createElement('a');
commentLink.href = 'javascript:;';
commentLink.addEventListener('click', () => goComment(item.quoteText));
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 = (text) => {
const textarea = document.querySelector('.atk-textarea');
if (!textarea) return;
textarea.value = `> ${text || ''}\n\n`;
textarea.focus();
if (window.btf?.snackbarShow) {
btf.snackbarShow('已为您引用该说说,删除空格效果更佳');
}
};
const afterRender = () => {
waterfall('#talk');
if (window.btf?.loadLightbox) {
btf.loadLightbox(document.querySelectorAll('#talk img:not(.no-lightbox)'));
}
if (window.lazyLoadInstance?.update) {
lazyLoadInstance.update();
}
};
const renderTalksList = (list) => {
list.map(normalizeTalk).forEach(item => talkContainer.appendChild(generateTalkElement(item)));
afterRender();
const media = talkContainer.querySelectorAll('img, iframe, meting-js');
media.forEach(element => {
element.addEventListener('load', afterRender, { once: true });
});
shuoshuoState.afterRenderTimer = window.setTimeout(afterRender, 300);
};
const fetchTalks = () => {
const cachedData = localStorage.getItem(TALK_CACHE_KEY);
const cachedTime = Number(localStorage.getItem(TALK_CACHE_TIME_KEY));
const now = Date.now();
if (cachedData && cachedTime && now - cachedTime < TALK_CACHE_DURATION) {
renderTalksList(JSON.parse(cachedData));
return;
}
fetch(TALK_API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ page: 1, pageSize: 30, search: '' })
})
.then(response => response.json())
.then(data => {
if (data?.code !== 1 || !Array.isArray(data?.data?.items)) {
console.warn('Unexpected API response format:', data);
renderTalksList([]);
return;
}
localStorage.setItem(TALK_CACHE_KEY, JSON.stringify(data.data.items));
localStorage.setItem(TALK_CACHE_TIME_KEY, now.toString());
renderTalksList(data.data.items);
})
.catch(error => console.error('Error fetching data:', error));
};
fetchTalks();
}
function initShuoshuoPage() {
renderTalks();
}
window.initShuoshuoPage = initShuoshuoPage;
if (!shuoshuoState.listenersBound) {
document.addEventListener('pjax:send', cleanupShuoshuo);
document.addEventListener('pjax:complete', initShuoshuoPage);
shuoshuoState.listenersBound = true;
}
initShuoshuoPage();
})();
| 1 | (function () { |
| 2 | const TALK_API_URL = 'https://mm.liushen.fun/api/echo/page'; |
| 3 | const TALK_CACHE_KEY = 'liushenEchoCacheV2'; |
| 4 | const TALK_CACHE_TIME_KEY = 'liushenEchoCacheTimeV2'; |
| 5 | const TALK_CACHE_DURATION = 30 * 60 * 1000; |
| 6 | const TALK_AVATAR = 'https://p.liiiu.cn/i/2025/03/13/67d2fc82d329c.webp'; |
| 7 | const shuoshuoState = window.__liushenShuoshuoState || (window.__liushenShuoshuoState = { |
| 8 | resizeHandler: null, |
| 9 | afterRenderTimer: null, |
| 10 | listenersBound: false |
| 11 | }); |
| 12 | |
| 13 | function 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 | |
| 25 | function 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 | |
| 493 | function initShuoshuoPage() { |
| 494 | renderTalks(); |
| 495 | } |
| 496 | |
| 497 | window.initShuoshuoPage = initShuoshuoPage; |
| 498 | |
| 499 | if (!shuoshuoState.listenersBound) { |
| 500 | document.addEventListener('pjax:send', cleanupShuoshuo); |
| 501 | document.addEventListener('pjax:complete', initShuoshuoPage); |
| 502 | shuoshuoState.listenersBound = true; |
| 503 | } |
| 504 | |
| 505 | initShuoshuoPage(); |
| 506 | })(); |
| 507 |
twikoo-comments.js
· 370 B · JavaScript
Bruto
const goComment = (e) => {
const n = document.querySelector(".el-textarea__inner");
n.value = `> ${e}\n\n`;
n.focus();
btf.snackbarShow("已为您引用该说说,不删除空格效果更佳");
};
// 如果是twikoo评论区,请自行替换goComment函数,注意,如果评论区开了懒加载可能导致无法找到元素。
| 1 | const 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函数,注意,如果评论区开了懒加载可能导致无法找到元素。 |