// 全局可调配置(改这里即可) ====================================
const CONFIG = {
excludedFolders: ["Template"],
excludedTags: new Set(["mcl/list-card", "mcl/list-grid", "mcl", "算法", "蓝桥", "excalidraw"]),
useTagCountCache: false, // 关掉即每次实时计算,避免旧标签残留
minTagCount: 1, // 小于该计数的标签不显示
defaultVisibleTags: 10, // 初始展示的标签数量
debounceMs: 400 // 输入防抖间隔
};
// 主题/样式配置 ================================================
const STYLE = {
headerBg: "linear-gradient(135deg, var(--interactive-accent), #4B50E6)",
cardBg: "var(--background-secondary)",
cardHoverBg: "var(--background-secondary-alt)",
textColor: "hsl(220, 25%, 85%)",
mutedText: "var(--text-muted)",
borderColor: "var(--background-modifier-border)",
hoverShadow: "0 8px 24px rgba(75, 80, 230, 0.3)",
inputBg: "var(--background-primary)",
inputBorder: "var(--background-modifier-border)",
inputFocusBorder: "var(--interactive-accent)",
tagSaturation: "55%",
tagLightness: "85%",
tagHoverLightness: "80%",
breakpoints: { mobile: 600, tablet: 900 },
globalTagsBg: "var(--background-secondary-alt)",
globalTagsBorder: "1px solid var(--background-modifier-border)",
};
// 标签计数缓存(可选)
const tagCountCache = CONFIG.useTagCountCache ? new Map() : null;
// 获取文件列表
const currentFile = app.workspace.getActiveFile();
const files = app.vault.getMarkdownFiles()
.filter(f => f.path !== currentFile?.path)
.filter(f => !CONFIG.excludedFolders.some(folder => f.path.startsWith(folder)));
// 辅助函数:设置样式
function setStyles(element, styles) {
Object.assign(element.style, styles);
}
// 色相计算函数
function getTagHue(tag) {
const hash = Array.from(tag).reduce((acc, char) => acc + char.charCodeAt(0), 0);
return (hash * 137.508) % 360;
}
// 创建搜索框
async function createSearchBox() {
const container = dv.el("div", "");
setStyles(container, {
marginBottom: "24px",
position: "sticky",
top: "0",
zIndex: "100",
padding: "12px 0",
backdropFilter: "blur(8px)",
background: "var(--background-primary)",
borderRadius: "12px",
boxShadow: "0 4px 12px rgba(0,0,0,0.05)",
transition: "all 0.3s ease",
});
const inputGroup = dv.el("div", "");
setStyles(inputGroup, {
display: "flex",
gap: "12px",
alignItems: "center",
padding: "0 16px",
});
const input = dv.el("input", "");
input.placeholder = "🔍 输入标签(例:#项目/进行中)";
setStyles(input, {
flex: "1",
padding: "14px 20px",
borderRadius: "15px",
border: `2px solid ${STYLE.inputBorder}`,
background: STYLE.inputBg,
fontSize: "1em",
outline: "none",
transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
boxShadow: "0 2px 8px rgba(0,0,0,0.05)",
fontFamily: "var(--font-editor)",
color: STYLE.textColor,
});
input.addEventListener("focus", () => {
setStyles(input, {
boxShadow: `0 4px 16px ${STYLE.headerBg}40, inset 0 0 0 1px ${STYLE.inputFocusBorder}`,
borderColor: STYLE.inputFocusBorder,
});
});
input.addEventListener("blur", () => {
setStyles(input, {
boxShadow: "0 2px 8px rgba(0,0,0,0.05)",
borderColor: STYLE.inputBorder,
});
});
// 刷新按钮
const refreshBtn = dv.el("button", "🔄");
setStyles(refreshBtn, {
padding: "8px 12px",
borderRadius: "15px",
border: `1px solid ${STYLE.inputBorder}`,
background: STYLE.inputBg,
cursor: "pointer",
transition: "all 0.2s ease",
color: STYLE.textColor,
});
refreshBtn.addEventListener("mouseenter", () => {
setStyles(refreshBtn, {
background: STYLE.inputBorder,
color: "var(--background-primary)",
});
});
refreshBtn.addEventListener("mouseleave", () => {
setStyles(refreshBtn, {
background: STYLE.inputBg,
color: STYLE.textColor,
});
});
inputGroup.appendChild(input);
inputGroup.appendChild(refreshBtn);
container.append(inputGroup);
// 全局标签区域
let globalTags = await createGlobalTags(input);
container.appendChild(globalTags);
// 刷新缓存功能
refreshBtn.addEventListener("click", async () => {
if (tagCountCache) tagCountCache.clear();
const newGlobalTags = await createGlobalTags(input);
container.replaceChild(newGlobalTags, globalTags);
globalTags = newGlobalTags;
});
return { container, input };
}
// 创建全局标签
async function createGlobalTags(inputRef) {
const container = dv.el("div", "");
setStyles(container, {
display: "flex",
alignItems: "baseline",
gap: "8px",
flexWrap: "wrap",
margin: "12px 0 0",
padding: "12px 18px",
background: STYLE.globalTagsBg,
borderRadius: "8px",
border: STYLE.globalTagsBorder,
transition: "all 0.3s ease",
});
const label = dv.el("span", "🏷️ 知识图谱标签:");
setStyles(label, {
fontSize: "0.85em",
whiteSpace: "nowrap",
fontWeight: "500",
color: STYLE.textColor,
});
container.appendChild(label);
// 加载状态
const loadingEl = dv.el("span", "标签加载中...");
setStyles(loadingEl, {
marginLeft: "8px",
color: STYLE.mutedText,
});
container.appendChild(loadingEl);
try {
const tagsData = await getAllTags();
container.removeChild(loadingEl);
const [visibleTags, hiddenTags] = [
tagsData.slice(0, CONFIG.defaultVisibleTags),
tagsData.slice(CONFIG.defaultVisibleTags)
];
// 创建标签元素(确保同一标签只生成一次)
const createTagElement = (tagData) => {
const tagEl = dv.el("span", `${tagData.tag}(${tagData.count})`);
tagEl.classList.add('tag');
tagEl.dataset.tag = tagData.tag; // 给标签加唯一标识,便于排查
setStyles(tagEl, {
'--hue': getTagHue(tagData.tag),
'--tag-saturation': STYLE.tagSaturation,
'--tag-lightness': STYLE.tagLightness,
padding: "4px 12px",
margin: "2px",
borderRadius: "15px",
cursor: "pointer",
transition: "transform 0.2s ease, background 0.2s ease",
background: `hsl(var(--hue), var(--tag-saturation), var(--tag-lightness))`,
color: "#2c2c2c",
});
tagEl.addEventListener("mouseenter", () => {
setStyles(tagEl, {
transform: "scale(1.05)",
background: `hsl(var(--hue), var(--tag-saturation), ${STYLE.tagHoverLightness})`,
});
});
tagEl.addEventListener("mouseleave", () => {
setStyles(tagEl, {
transform: "none",
background: `hsl(var(--hue), var(--tag-saturation), var(--tag-lightness))`,
});
});
tagEl.addEventListener("click", (e) => {
e.stopPropagation();
inputRef.value = `#${tagData.tag}`;
inputRef.dispatchEvent(new Event("input"));
});
return tagEl;
};
// 添加可见标签(直接遍历去重后的tagsData切片)
visibleTags.forEach(tagData => {
container.appendChild(createTagElement(tagData));
});
// 折叠/展开功能
if (hiddenTags.length > 0) {
let isExpanded = false;
const toggleBtn = dv.el("button", `展开更多 (${hiddenTags.length}+)`);
setStyles(toggleBtn, {
border: `1px solid ${STYLE.headerBg}30`,
color: STYLE.headerBg,
padding: "4px 12px",
borderRadius: "15px",
fontSize: "0.8em",
cursor: "pointer",
transition: "all 0.2s ease",
order: 999,
background: "transparent",
});
toggleBtn.addEventListener("mouseenter", () => {
setStyles(toggleBtn, {
background: `${STYLE.headerBg}10`,
});
});
toggleBtn.addEventListener("mouseleave", () => {
setStyles(toggleBtn, {
background: "transparent",
});
});
// 添加隐藏标签(只添加tagsData中未显示的)
hiddenTags.forEach(tagData => {
const tagEl = createTagElement(tagData);
tagEl.style.display = "none";
container.appendChild(tagEl);
});
// 切换显示状态
toggleBtn.addEventListener("click", () => {
isExpanded = !isExpanded;
const tagElements = Array.from(container.children).filter(el => el.classList.contains('tag'));
const hiddenTagElements = tagElements.slice(CONFIG.defaultVisibleTags);
hiddenTagElements.forEach(el => {
el.style.display = isExpanded ? "inline-block" : "none";
});
toggleBtn.textContent = isExpanded ?
`收起标签 (${hiddenTags.length}-)` :
`展开更多 (${hiddenTags.length}+)`;
});
container.appendChild(toggleBtn);
}
} catch (error) {
container.removeChild(loadingEl);
const errEl = dv.el("span", "标签加载失败");
setStyles(errEl, { color: "var(--text-error)" });
container.appendChild(errEl);
console.error("创建全局标签失败:", error);
}
return container;
}
// 创建结果容器
function createResultsContainer() {
const container = dv.el("div", "");
setStyles(container, {
display: "grid",
gap: "20px",
gridTemplateColumns: "repeat(auto-fit, minmax(320px, 1fr))",
margin: "20px 0",
});
// 响应式样式
const style = dv.el("style", `
@media (max-width: ${STYLE.breakpoints.mobile}px) {
#${container.id} {
grid-template-columns: 1fr !important;
}
}
`);
dv.container.appendChild(style);
return container;
}
// 查找最近的标题
function findNearestHeader(headers, targetLine) {
return headers.reduce((nearest, header) =>
(header.line < targetLine && (!nearest || header.line > nearest.line))
? header
: nearest, null);
}
// 生成锚点
function generateAnchor(text) {
return encodeURIComponent(
text.toLowerCase()
.replace(/[^\w\u4e00-\u9fa5]+/g, "-")
.replace(/^-+|-+$/g, "")
);
}
// 创建卡片
function createCard(file, { header, anchor, preview }) {
const card = dv.el("div", "");
setStyles(card, {
background: STYLE.cardBg,
borderRadius: "12px",
padding: "30px",
margin: "5px",
border: `1px solid ${STYLE.borderColor}`,
boxShadow: "0 3px 6px rgba(0,0,0,0.05)",
// 拆分过渡:不同属性用不同时长/曲线,让动画更细腻
transition: `
transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1),
box-shadow 0.3s ease,
background 0.3s ease
`,
cursor: "pointer",
});
card.addEventListener("mouseenter", () => {
setStyles(card, {
transform: "translateY(-5px) scale(1.02)", // 微调放大比例,更自然
boxShadow: STYLE.hoverShadow,
background: STYLE.cardHoverBg,
});
});
card.addEventListener("mouseleave", () => {
setStyles(card, {
transform: "none",
boxShadow: "0 3px 6px rgba(0,0,0,0.05)",
background: STYLE.cardBg,
});
});
// Wiki链接
const wikiLinkText = `[[${file.basename}#${header}]]`;
// Wiki链接(新增颜色过渡)
const wikiLink = dv.el("div", wikiLinkText);
setStyles(wikiLink, {
color: STYLE.headerBg,
fontWeight: "600",
marginBottom: "10px",
fontSize: "1.05em",
textDecoration: "none",
transition: "color 0.2s ease", // 单独为颜色变化添加过渡
});
wikiLink.addEventListener("mouseenter", () => {
setStyles(wikiLink, {
color: "var(--interactive-accent-hover)",
});
});
wikiLink.addEventListener("mouseleave", () => {
setStyles(wikiLink, {
color: STYLE.headerBg,
});
});
// 点击跳转
card.onclick = () => {
app.workspace.openLinkText(file.basename, file.path, true, {
fragment: generateAnchor(header)
});
};
// 标题
const headerEl = dv.el("div", header);
setStyles(headerEl, {
color: STYLE.textColor,
marginBottom: "12px",
fontSize: "0.95em",
fontWeight: "500",
});
// 预览文本
const previewEl = dv.el("div", preview);
setStyles(previewEl, {
fontSize: "0.9em",
lineHeight: "1.5",
maxHeight: "4em",
overflow: "hidden",
textOverflow: "ellipsis",
webkitLineClamp: "2",
webkitBoxOrient: "vertical",
display: "-webkit-box",
color: STYLE.mutedText,
});
card.append(wikiLink, headerEl, previewEl);
return card;
}
// 搜索功能
async function searchFiles(searchTerm) {
if (!searchTerm || !searchTerm.trim()) return [];
const terms = searchTerm.trim().split(/\s+/)
.filter(t => t.startsWith("#"))
.map(t =>
t.slice(1)
.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
.replace(/(\w+)\/(\w+)/g, "$1\\/$2")
);
if (terms.length === 0) return [];
const results = [];
const regex = new RegExp(`#(${terms.join("|")})`, "i");
for (const file of files) {
try {
const content = await app.vault.cachedRead(file);
const lines = content.split("\n");
const headers = lines.map((line, i) => {
const match = line.match(/^(#+)\s(.+)/);
return match ? {
level: match[1].length,
text: match[2].trim(),
line: i,
anchor: generateAnchor(match[2].trim())
} : null;
}).filter(Boolean);
lines.forEach((line, i) => {
if (terms.every(term => new RegExp(`#${term}`, "i").test(line))) {
const nearestHeader = findNearestHeader(headers, i);
results.push({
file,
header: nearestHeader?.text || "无标题内容",
anchor: nearestHeader
? `${file.basename}#${nearestHeader.anchor}`
: file.basename,
preview: line.replace(regex, "").trim().slice(0, 150)
});
}
});
} catch (error) {
console.error("文件读取失败:", file.path, error);
}
}
// 结果去重
return [...new Map(results.map(item =>
[item.file.path + item.anchor, item]
)).values()];
}
// 更新结果
let resultsContainer, countInfo;
async function updateResults(searchTerm) {
const loading = dv.el("div", "🔍 搜索中...");
setStyles(loading, {
textAlign: "center",
padding: "30px",
color: STYLE.textColor,
});
resultsContainer.replaceChildren(loading);
try {
const start = Date.now();
const results = await searchFiles(searchTerm);
const duration = Date.now() - start;
resultsContainer.replaceChildren();
if (results.length === 0) {
const empty = dv.el("div", "🎯 未找到匹配内容");
setStyles(empty, {
textAlign: "center",
padding: "30px",
color: STYLE.mutedText,
});
resultsContainer.appendChild(empty);
} else {
resultsContainer.append(...results.map(r => createCard(r.file, r)));
}
countInfo.textContent = `找到 ${results.length} 个结果 | 耗时 ${duration}ms`;
setStyles(countInfo, {
margin: "20px 0",
fontSize: "0.9em",
textAlign: "right",
color: STYLE.mutedText,
});
} catch (error) {
const errMsg = dv.el("div", "⚠️ 搜索出错,请检查控制台");
setStyles(errMsg, { color: "var(--text-error)" });
resultsContainer.replaceChildren(errMsg);
console.error("搜索失败:", error);
}
}
// 关键修改:获取所有标签并强制去重(确保标签唯一)
async function getAllTags() {
const tagsArray = [];
const indexMap = {};
// 1. 初步收集标签(基于page.file.tags去重)
const pages = dv.pages()
.filter(page => !CONFIG.excludedFolders.some(folder =>
page.file.path.startsWith(folder)));
for (const page of pages) {
const pageTags = page.file.tags || [];
for (const tag of pageTags) {
// 统一标签格式:移除#、trim空格,避免格式差异导致的重复
const cleanTag = tag.replace(/^#+/, "").trim();
// 排除不需要的标签 + 已存在的标签
if (!cleanTag || CONFIG.excludedTags.has(cleanTag) || indexMap[cleanTag]) continue;
indexMap[cleanTag] = tagsArray.length;
tagsArray.push({ tag: cleanTag, count: 0 });
}
}
// 2. 为每个标签计算count(可选缓存)
for (const tagItem of tagsArray) {
const tagName = tagItem.tag;
if (CONFIG.useTagCountCache && tagCountCache?.has(tagName)) {
tagItem.count = tagCountCache.get(tagName);
continue;
}
try {
const searchResults = await searchFiles(`#${tagName}`);
const resultCount = searchResults.length;
tagItem.count = resultCount;
if (CONFIG.useTagCountCache && tagCountCache) {
tagCountCache.set(tagName, resultCount);
}
} catch (error) {
console.error(`计算标签 [${tagName}] 结果数失败:`, error);
tagItem.count = 0;
}
}
// 3. 兜底去重:基于tag字段唯一化(即使前面有遗漏,这里也能确保唯一)
// 使用Map的键唯一性,以tag为键,过滤重复标签
const uniqueTags = [...new Map(tagsArray.map(item => [item.tag, item])).values()];
// 4. 过滤计数过小的标签 + 按count降序排序
return uniqueTags
.filter(item => item.count >= CONFIG.minTagCount)
.sort((a, b) => b.count - a.count);
}
// 初始化函数
async function initialize() {
try {
const { container: searchBox, input } = await createSearchBox();
resultsContainer = createResultsContainer();
countInfo = dv.el("div", "");
setStyles(countInfo, {
margin: "20px 0",
fontSize: "0.9em",
textAlign: "right",
});
dv.container.append(
searchBox,
resultsContainer,
countInfo
);
// 防抖搜索
let timeoutId;
input.addEventListener("input", (e) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => updateResults(e.target.value), CONFIG.debounceMs);
});
} catch (error) {
console.error("程序初始化失败:", error);
const errEl = dv.el("div", "⚠️ 标签搜索功能初始化出错,请检查控制台");
setStyles(errEl, { color: "var(--text-error)", margin: "20px 0" });
dv.container.appendChild(errEl);
}
}
// 全局标签样式
dv.el("style", `
.tag {
background: hsl(var(--hue), var(--tag-saturation), var(--tag-lightness));
color: #2c2c2c;
border-radius: 15px;
transition: transform 0.2s ease, background 0.2s ease;
}
.tag:hover {
transform: scale(1.05);
background: hsl(var(--hue), var(--tag-saturation), ${STYLE.tagHoverLightness});
}
`);
// 启动程序
initialize();