Appearance
dataviewjs
// === 配置区 ===
const coverField = "banner_img"; // 封面图属性名
const cardWidth = "220px"; // 卡片宽度
// 1. 获取当前页面的出链
const outlinks = dv.current().file.outlinks;
const currentFile = app.vault.getAbstractFileByPath(dv.current().file.path);
if (currentFile && outlinks.length > 0) {
const fileContent = await app.vault.read(currentFile);
// 2. 排序:按链接在正文中的出现位置
// 不能拿 link.display 去找:[别名]() 的 display 是"别名",正文里根本没有「[[别名」,
// indexOf 会返回 -1,这张卡就被过滤掉了。这里改成用"链接目标"匹配,
// 并要求后面紧跟 | # ^ ] 之一,避免 [笔记]() 误命中 [笔记2]()。
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const linkPosition = (content, link) => {
const base = link.path.split("/").pop().replace(/\.md$/, "");
const targets = [link.path, link.path.replace(/\.md$/, ""), base].filter(Boolean);
let pos = -1;
for (const t of new Set(targets)) {
const re = new RegExp(`\\[\\[\\s*${escapeRe(t)}\\s*(?=[|#^\\]])`);
const m = re.exec(content);
if (m && (pos === -1 || m.index < pos)) pos = m.index;
}
return pos;
};
const selfPath = dv.current().file.path;
const seenPaths = new Set();
const sortedLinks = [];
for (const link of outlinks) {
// 先解析成真实文件路径:[名称]()、[路径/名称]()、[别名]()、[名称#标题]() 都会落到同一篇
const dest = app.metadataCache.getFirstLinkpathDest(link.path, selfPath);
const path = dest ? dest.path : link.path;
if (!path || path === selfPath) continue; // 跳过 [#本页小标题]() 这类自链接
if (seenPaths.has(path)) continue; // 同一篇被链接多次,只出一张卡
seenPaths.add(path);
sortedLinks.push({ link, path, index: linkPosition(fileContent, link) });
}
// 找不到出现位置的排到最后:宁可多显示一张,也别让它莫名消失
sortedLinks.sort((a, b) => {
const x = a.index === -1 ? Number.MAX_SAFE_INTEGER : a.index;
const y = b.index === -1 ? Number.MAX_SAFE_INTEGER : b.index;
return x - y;
});
// 3. 创建网格容器
// 【关键】不能写 dv.el("div", ""):空字符串会走 Dataview 的 renderValue,
// 在容器里塞一个没有内容的 <span>。它也是网格项,会独占第一行第一列,
// 于是整排卡片右移一格、最后一张掉到第二行,左上角留出一块空白。
const container = dv.el("div");
container.empty(); // 双保险:清掉任何非卡片子节点
container.style.display = "grid";
container.style.gridTemplateColumns = `repeat(auto-fill, minmax(${cardWidth}, 1fr))`;
container.style.gap = "16px";
container.style.marginLeft = "0";
container.style.paddingLeft = "0";
container.style.width = "100%";
// 4. 遍历生成卡片
for (const { link, path } of sortedLinks) {
const page = dv.page(path);
if (!page) continue;
// 处理封面图
const cover = page[coverField];
let imgSrc = "";
if (cover) {
if (cover.startsWith("[[")) {
const imgName = cover.replace(/\[\[|\]\]/g, '');
const imgFile = app.metadataCache.getFirstLinkpathDest(imgName, page.file.path);
if (imgFile) imgSrc = app.vault.getResourcePath(imgFile);
} else {
imgSrc = cover;
}
}
// 创建卡片外壳
const card = container.createEl("a", { href: page.file.path, cls: "internal-link" });
card.style.textDecoration = "none";
card.style.color = "inherit";
const cardInner = card.createEl("div");
cardInner.style.background = "var(--background-secondary)";
cardInner.style.borderRadius = "8px";
cardInner.style.overflow = "hidden";
cardInner.style.boxShadow = "0 2px 4px rgba(0,0,0,0.1)";
cardInner.style.transition = "transform 0.2s";
// 封面图区域
if (imgSrc) {
const img = cardInner.createEl("img");
img.src = imgSrc;
img.style.width = "100%";
img.style.height = "140px";
img.style.objectFit = "cover";
img.style.display = "block";
} else {
const noCover = cardInner.createEl("div");
noCover.style.height = "140px";
noCover.style.display = "flex";
noCover.style.alignItems = "center";
noCover.style.justifyContent = "center";
noCover.style.background = "var(--background-modifier-border)";
noCover.style.color = "var(--text-muted)";
noCover.textContent = "无封面";
}
// 文字信息区域
const textDiv = cardInner.createEl("div");
textDiv.style.padding = "12px";
// 标题:优先用链接里写的别名([别名]()),没写别名就用文件名。
// link.display 只有 [别名]() 这种写法才有值,[文件]() 时是 undefined。
const linkAlias = (link.display || "").trim();
const titleDiv = textDiv.createEl("div");
titleDiv.textContent = linkAlias || page.file.name;
titleDiv.title = page.file.name; // 悬停显示真实文件名,别名太抽象也能对回去
titleDiv.style.fontWeight = "bold";
titleDiv.style.marginBottom = "4px";
titleDiv.style.whiteSpace = "nowrap";
titleDiv.style.overflow = "hidden";
titleDiv.style.textOverflow = "ellipsis";
const folderDiv = textDiv.createEl("div");
// 优化显示:只显示最后一级文件夹,避免太长
const folderName = page.file.folder;
folderDiv.textContent = folderName ? folderName.split('/').pop() : "根目录";
folderDiv.style.fontSize = "12px";
folderDiv.style.color = "var(--text-muted)";
}
} else if (outlinks.length === 0) {
dv.paragraph("当前笔记没有出链");
}后面可能是大幅度改动相关内容,暂时先不动了,之前的问题是维护难度很大,层级关系写的挺好的,但是自己自己后面难以梳理,而且很多东西,的确是有概念就行,还是需要实操和理解,编程能力和细节等。
什么阶段做什么事情。