Greasy Fork is available in English.
Yamibo漫画区帖子列表实现卡片风格瀑布流展示
// ==UserScript==
// @name Yamibo漫区封面卡片
// @namespace https://bbs.yamibo.com/
// @version 1.1
// @description Yamibo漫画区帖子列表实现卡片风格瀑布流展示
// @author hitori酱
// @match https://bbs.yamibo.com/forum.php*fid=30*
// @match https://bbs.yamibo.com/forum-30-*
// @match https://bbs.yamibo.com/forum-37-*
// @icon https://www.yamibo.com/favicon.ico
// @grant none
// @run-at document-end
// @noframes
// @license MIT License
// @updateURL
// @downloadURL
// ==/UserScript==
(function() {
'use strict';
const DOMAIN = 'https://bbs.yamibo.com/';
const PLACEHOLDER = 'https://s2.loli.net/2025/11/10/nTuPOVqdFQHLosz.jpg';
// Base64 SVG 图标
const ICONS = {
view: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjNjY2NjY2Ij48cGF0aCBkPSJNNDgwLTMxMnE3MCAwIDExOS00OXQ0OS0xMTlxMC03MC00OS0xMTl0LTExOS00OXEtNzAgMC0xMTkgNDl0LTQ5IDExOXEwIDcwIDQ5IDExOXQxMTkgNDlabTAtNzJxLTQwIDAtNjgtMjh0LTI4LTY4cTAtNDAgMjgtNjh0NjgtMjhxNDAgMCA2OCAyOHQyOCA2OHEwIDQwLTI4IDY4dC02OCAyOFptMCAxOTJxLTE0Mi42IDAtMjU5LjgtNzguNVExMDMtMzQ5IDQ4LTQ4MHE1NS0xMzEgMTcyLjItMjA5LjVRMzM3LjQtNzY4IDQ4MC03NjhxMTQyLjYgMCAyNTkuOCA3OC41UTg1Ny02MTEgOTEyLTQ4MHEtNTUgMTMxLTE3Mi4yIDIwOS41UTYyMi42LTE5MiA0ODAtMTkyWiIvPjwvc3ZnPg==',
reply: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjNjY2NjY2Ij48cGF0aCBkPSJNMjQwLTM4NGg0ODB2LTcySDI0MHY3MlptMC0xMzJoNDgwdi03MkgyNDB2NzJabTAtMTMyaDQ4MHYtNzJIMjQwdjcyWm0tNzIgNDA4cS0yOS43IDAtNTAuODUtMjEuMTVROTYtMjgyLjMgOTYtMzEydi00ODBxMC0yOS43IDIxLjE1LTUwLjg1UTEzOC4zLTg2NCAxNjgtODY0aDYyNHEyOS43IDAgNTAuODUgMjEuMTVRODY0LTgyMS43IDg2NC03OTJ2Njk2TDcyMC0yNDBIMTY4WiIvPjwvc3ZnPg==',
favorite: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRUEzMzIzIj48cGF0aCBkPSJtNDgwLTE0NC01MC00NXEtMTAwLTg5LTE2NS0xNTIuNXQtMTAyLjUtMTEzUTEyNS01MDQgMTEwLjUtNTQ1VDk2LTYyOXEwLTg5IDYxLTE1MHQxNTAtNjFxNDkgMCA5NSAyMXQ3OCA1OXEzMi0zOCA3OC01OXQ5NS0yMXE4OSAwIDE1MCA2MXQ2MSAxNTBxMCA0My0xNCA4M3QtNTEuNSA4OXEtMzcuNSA0OS0xMDMgMTEzLjVUNTI4LTE4N2wtNDggNDNaIi8+PC9zdmc+'
};
function setRealImage(card, imgUrl) {
const img = card.querySelector('img');
if (img) {
img.src = imgUrl;
img.removeAttribute('data-src');
}
}
function createCard(threadData) {
const { title, url, category, heat, readPerm, views, replies, favorite } = threadData;
const card = document.createElement('div');
card.className = 'comic-card';
// 图片
const imgWrapper = document.createElement('div');
imgWrapper.className = 'img-wrapper';
const img = document.createElement('img');
img.src = PLACEHOLDER;
img.alt = title;
imgWrapper.appendChild(img);
// 浮层
const tagCategory = document.createElement('div');
tagCategory.className = 'tag-category';
tagCategory.innerText = category;
imgWrapper.appendChild(tagCategory);
// 热门标签
if (heat && (heat.includes('热') || heat.includes('火'))) {
const hotTag = document.createElement('div');
hotTag.className = 'tag-hot';
hotTag.innerText = '火';
imgWrapper.appendChild(hotTag);
}
const tagRead = document.createElement('div');
tagRead.className = 'tag-readperm';
tagRead.innerText = readPerm ? `[阅读权限 ${readPerm}]` : '';
imgWrapper.appendChild(tagRead);
card.appendChild(imgWrapper);
// 数据行
const dataRow = document.createElement('div');
dataRow.className = 'data-row';
const viewDiv = document.createElement('div');
viewDiv.innerHTML = `<img src="${ICONS.view}" class="icon"> ${views||0}`;
dataRow.appendChild(viewDiv);
const replyDiv = document.createElement('div');
replyDiv.innerHTML = `<img src="${ICONS.reply}" class="icon"> ${replies||0}`;
dataRow.appendChild(replyDiv);
const favDiv = document.createElement('div');
favDiv.innerHTML = `<img src="${ICONS.favorite}" class="icon fav-icon"> ${favorite||0}`;
dataRow.appendChild(favDiv);
card.appendChild(dataRow);
// 标题
const titleDiv = document.createElement('div');
titleDiv.className = 'title';
titleDiv.innerText = title;
card.appendChild(titleDiv);
card.onclick = () => window.open(url, '_blank');
return card;
}
function initGrid() {
const threadList = document.querySelector('#threadlist');
if (!threadList) return;
// 默认收起标签区域
const threadTypes = document.querySelector('#thread_types');
if (threadTypes) {
threadTypes.style.height = '';
const foldLi = threadTypes.querySelector('li.fold');
if (foldLi && foldLi.onclick) {
foldLi.onclick();
}
}
const oldTable = document.querySelector('#threadlisttableid');
if (oldTable) oldTable.style.display = 'none';
const stickyThreads = document.querySelectorAll('tbody[id^="stickthread_"]');
stickyThreads.forEach(tbody => tbody.style.display = 'none');
const nextPageBtn = document.querySelector('#autopbn');
if (nextPageBtn) nextPageBtn.style.display = 'none';
// 隐藏列表标题框右侧的表头文字标签
const threadHeader = document.querySelector('#threadlist .th');
if (threadHeader) {
const headerCells = threadHeader.querySelectorAll('td, th');
headerCells.forEach((cell, index) => {
// 隐藏作者、回复/查看、最后发表等标签列
if (index > 0) { // 保留第一列(主题标题),隐藏其他列
cell.style.display = 'none';
}
});
}
const grid = document.createElement('div');
grid.id = 'comic-grid';
threadList.appendChild(grid);
const style = document.createElement('style');
style.innerHTML = `
#comic-grid {
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: center;
padding: 10px;
}
.comic-card {
width: calc(16.66% - 15px);
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
overflow: visible;
cursor: pointer;
transition: transform 0.3s, box-shadow 0.3s;
}
.comic-card:hover {
transform: translateY(-6px);
box-shadow: 0 8px 20px rgba(0,0,0,0.2);
}
.img-wrapper {
position: relative;
width: 100%;
aspect-ratio: 4/5;
background: #f8f8f8;
overflow: hidden;
flex-shrink: 0;
}
.img-wrapper img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: top;
display: block;
border-radius: 8px 8px 0 0;
}
.tag-category {
position: absolute;
top: 5px; left: 5px;
background: rgba(153,153,153,0.6);
padding: 2px 5px;
border-radius: 3px;
font-size: 12px;
z-index: 10;
color: #a0522d;
}
.tag-hot {
position: absolute;
top: 5px; right: 5px;
background: rgba(255, 107, 53, 0.9);
color: #fff;
padding: 2px 5px;
border-radius: 3px;
font-size: 12px;
z-index: 10;
}
.tag-readperm {
position: absolute;
bottom: 5px; right: 5px;
background: rgba(153,153,153,0.6);
color: #a0522d;
padding: 1px 6px;
border-radius: 3px;
font-size: 12px;
z-index: 10;
}
.data-row {
display: flex;
justify-content: space-around;
padding: 5px 0;
font-size: 12px;
color: #666;
}
.data-row .icon {
width: 16px;
height: 16px;
vertical-align: middle;
margin-right: 3px;
}
.data-row .fav-icon {
width: 16px;
height: 16px;
vertical-align: middle;
margin-right: 4px;
object-fit: contain;
}
.title {
padding: 8px;
font-size: 15px;
line-height: 1.4;
text-align: left;
word-break: break-word;
overflow-wrap: break-word;
min-height: 1.4em;
overflow: visible;
display: block;
flex: 1 1 auto;
}
`;
document.head.appendChild(style);
// 等高逻辑:基于标题实际需要高度计算
function equalizeRowHeights() {
const cards = Array.from(grid.children);
let rows = {};
cards.forEach(card => {
card.style.height = '';
const top = card.offsetTop;
if (!rows[top]) rows[top] = [];
rows[top].push(card);
});
Object.values(rows).forEach(rowCards => {
// 计算固定部分高度:图片 + 数据行
const fixedHeight = rowCards[0].querySelector('.img-wrapper').offsetHeight +
rowCards[0].querySelector('.data-row').offsetHeight;
// 找出本行标题需要的最大高度
const maxTitleHeight = Math.max(...rowCards.map(c => {
const title = c.querySelector('.title');
return title.scrollHeight + 16; // 16px是padding
}));
const finalHeight = fixedHeight + maxTitleHeight;
rowCards.forEach(c => {
c.style.height = finalHeight + 'px';
});
});
}
loadThreads(grid, equalizeRowHeights);
window.addEventListener('resize', () => setTimeout(equalizeRowHeights, 300));
}
function getThreads() {
return document.querySelectorAll('tbody[id^="normalthread_"]');
}
function findFirstImage(doc) {
let img;
// 优先查找 ignore_js_op 内的图片(通常是漫画内容)
img = doc.querySelector('ignore_js_op img');
if (img && (img.getAttribute('file') || img.getAttribute('zoomfile'))) return img;
// 查找带有 aid 属性的附件图片(支持多种容器)
img = doc.querySelector('img[aid]');
if (img && (img.getAttribute('file') || img.getAttribute('zoomfile'))) {
return img; // 有file或zoomfile属性的aid图片直接返回
}
// 查找 zoom 类的图片
img = doc.querySelector('img.zoom');
if (img && (img.getAttribute('file') || img.getAttribute('zoomfile'))) return img;
// 在 postmessage 容器中查找图片
img = doc.querySelector('.postmessage img');
if (img && (img.getAttribute('src') || img.getAttribute('file') || img.getAttribute('zoomfile'))) {
const src = img.getAttribute('src') || '';
// 过滤表情图片和占位图片
if (!src.includes('static/image/smiley/') && !src.includes('static/image/common/none.gif')) {
return img;
}
}
// 最后尝试第一个帖子内容区的图片,但要过滤表情图片
img = doc.querySelector('td.t_f img');
if (img && (img.getAttribute('src') || img.getAttribute('file') || img.getAttribute('zoomfile'))) {
const src = img.getAttribute('src') || '';
// 过滤表情图片路径
if (!src.includes('static/image/smiley/') && !src.includes('static/image/common/none.gif')) {
return img;
}
}
// 尝试后续楼层的内容(第2-5楼)- 支持多种容器格式
const postContainers = doc.querySelectorAll('.postmessage, td.t_f');
for (let i = 1; i < Math.min(postContainers.length, 5); i++) {
const post = postContainers[i];
// 优先查找 ignore_js_op 内的图片
img = post.querySelector('ignore_js_op img');
if (img && (img.getAttribute('file') || img.getAttribute('zoomfile'))) {
return img;
}
// 查找带 aid 属性的图片
img = post.querySelector('img[aid]');
if (img && (img.getAttribute('file') || img.getAttribute('zoomfile'))) {
return img; // 有file或zoomfile属性的aid图片直接返回
}
// 然后查找普通图片,但过滤表情图片
img = post.querySelector('img');
if (img && (img.getAttribute('src') || img.getAttribute('file') || img.getAttribute('zoomfile'))) {
const src = img.getAttribute('src') || '';
if (!src.includes('static/image/smiley/') && !src.includes('static/image/common/none.gif')) {
return img;
}
}
}
// 如果楼层查找失败,尝试其他备用方式
// 5. 查找保存照片区域的图片
img = doc.querySelector('div.savephotop img');
if (img && (img.getAttribute('src') || img.getAttribute('file') || img.getAttribute('zoomfile'))) {
const src = img.getAttribute('src') || '';
if (!src.includes('static/image/common/none.gif')) return img;
}
// 6. 查找嵌套在各种容器内的 savephotop 图片
img = doc.querySelector('.pcb .savephotop img');
if (img && (img.getAttribute('src') || img.getAttribute('file') || img.getAttribute('zoomfile'))) {
const src = img.getAttribute('src') || '';
if (!src.includes('static/image/common/none.gif')) return img;
}
// 7. 查找 pattl 内的图片
img = doc.querySelector('.pattl img');
if (img && (img.getAttribute('src') || img.getAttribute('file') || img.getAttribute('zoomfile'))) {
const src = img.getAttribute('src') || '';
if (!src.includes('static/image/common/none.gif')) return img;
}
// 8. 查找 tattl attm 内的图片
img = doc.querySelector('.tattl.attm img');
if (img && (img.getAttribute('src') || img.getAttribute('file') || img.getAttribute('zoomfile'))) {
const src = img.getAttribute('src') || '';
if (!src.includes('static/image/common/none.gif')) return img;
}
// 9. 最后尝试任何带有 file 或 zoomfile 属性的图片(优先使用这些属性)
const fileImgs = doc.querySelectorAll('img[file], img[zoomfile]');
for (const img of fileImgs) {
if (img.getAttribute('file') || img.getAttribute('zoomfile')) {
const src = img.getAttribute('src') || '';
if (!src.includes('static/image/common/none.gif')) return img;
}
}
return null;
}
async function fetchFavorite(url) {
try {
const resp = await fetch(url);
const html = await resp.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const fav = doc.querySelector('#favoritenumber');
return fav ? parseInt(fav.innerText) : 0;
} catch(e) {
return 0;
}
}
async function loadThreads(grid, equalizeRowHeights) {
const threads = getThreads();
let loadedCount = 0;
for (const tbody of threads) {
const link = tbody.querySelector('th a.s.xst');
if (!link) continue;
const title = link.innerText;
const url = link.href;
const categoryElem = tbody.querySelector('th em a');
const category = categoryElem ? categoryElem.innerText : '';
const readPermElem = tbody.querySelector('th span.xw1');
const readPerm = readPermElem ? readPermElem.innerText : '';
const heatElem = tbody.querySelector('th .tbox.theatlevel');
const heat = heatElem ? heatElem.innerText : '';
const repliesElem = tbody.querySelector('td.num a.xi2');
const replies = repliesElem ? repliesElem.innerText : '';
const viewsElem = tbody.querySelector('td.num em');
const views = viewsElem ? viewsElem.innerText : '';
// 初始 favorite 0,异步更新
let threadData = { title, url, category, heat, readPerm, views, replies, favorite: 0 };
const card = createCard(threadData);
grid.appendChild(card);
fetch(url).then(resp => resp.text()).then(async html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const firstImg = findFirstImage(doc);
if (firstImg) {
let srcRel = firstImg.getAttribute('file') || firstImg.getAttribute('zoomfile') || firstImg.getAttribute('src');
if (srcRel) {
const imgUrl = srcRel.startsWith('http') ? srcRel : new URL(srcRel, DOMAIN).href;
if (/\.(jpe?g|png|webp)$/i.test(imgUrl)) {
setRealImage(card, imgUrl);
}
}
}
// 更新收藏
const favElem = doc.querySelector('#favoritenumber');
if (favElem) {
const favNum = parseInt(favElem.innerText) || 0;
const favDiv = card.querySelector('.fav-icon').parentNode;
favDiv.innerHTML = `<img src="${ICONS.favorite}" class="icon fav-icon"> ${favNum}`;
}
// 每次更新后重新计算等高
loadedCount++;
if (loadedCount === threads.length) {
setTimeout(equalizeRowHeights, 100);
}
});
}
}
window.addEventListener('load', initGrid);
})();