Obsidian 标签卡片
显示效果

代码
//在obsidian中使用dataviewjs
// ===== 配置项 =====
const config = {
targetTags: ["#🤔想什么", "#👀看什么", "#🕹️好东西", "#🥕折腾记"],
targetFolder: "0.1-Journal",
timeRange: "week",
// 【仿照添加的自适应布局配置】
minCardWidth: "260px", // 卡片最小宽度
gap: "1.5rem", // 卡片间距
maxWidth: "1600px", // 整体最大宽度(适配大屏)
tagStyles: {
backgroundColor: "#F0F3FC", // 标签背景色
textColor: "#6881E8" // 标签文字色
}
};
// ===== 工具函数 =====
function getWeekStart() {
const now = new Date();
const day = now.getDay() || 7;
const start = new Date(now);
start.setDate(now.getDate() - (day - 1));
start.setHours(0, 0, 0, 0);
return start;
}
function isInTargetFolder(filePath) {
const normalizedPath = filePath.replace(/[\\/]/g, '/');
return normalizedPath.startsWith(`${config.targetFolder}/`) || normalizedPath === config.targetFolder;
}
/**
* 修复内容中的两种Obsidian链接格式
* 1. [[内部链接]] 或 [[内部链接|显示文本]]
* 2. [显示文本](链接地址)
* @param {string} text - 原始内容文本
* @returns {string} 替换链接后的HTML文本
*/
function fixAllLinks(text) {
let fixedText = text;
// 1. 处理Obsidian双链格式:[[链接路径|显示文本]] 或 [[链接路径]]
const doubleBracketRegex = /\[\[(.*?)(\|(.*?))?\]\]/g;
fixedText = fixedText.replace(doubleBracketRegex, (match, path, _, displayText) => {
const linkText = displayText || path.split(/[\\/]/).pop(); // 无显示文本时用路径最后部分
return `<a href="${path}" class="internal-link" style="color: var(--text-accent); text-decoration: none;">${linkText}</a>`;
});
// 2. 处理Markdown链接格式:[显示文本](链接地址)
const markdownLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
fixedText = fixedText.replace(markdownLinkRegex, (match, linkText, url) => {
// 判断是否为内部链接(不含协议的路径)
const isInternal = !url.startsWith('http://') && !url.startsWith('https://');
return `<a href="${url}" class="${isInternal ? 'internal-link' : ''}" style="color: var(--text-accent); text-decoration: none;">${linkText}</a>`;
});
return fixedText;
}
/**
* 格式化块内容(移除标签+修复链接+处理换行)
* @param {string} text - 原始块内容
* @returns {string} 格式化后的HTML文本
*/
function formatBlockContent(text) {
let content = text;
// 1. 移除标签(避免重复显示)
config.targetTags.forEach(tag => {
content = content.replace(tag, '').trim();
});
// 2. 修复所有链接格式(关键步骤)
content = fixAllLinks(content);
// 3. 处理换行,保留格式
return content.replace(/\n/g, '<br>');
}
function formatDate(date) {
if (!(date instanceof Date)) date = new Date(date);
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
/**
* 创建可点击的标签元素
* @param {string} tag - 标签文本(如"#🤔想什么")
* @returns {HTMLAnchorElement} 可点击的标签链接元素
*/
function createClickableTag(tag) {
const tagLink = document.createElement('a');
tagLink.textContent = tag;
tagLink.className = 'tag internal-link';
tagLink.href = `#${tag.substring(1)}`;
// 应用自定义标签样式
tagLink.style.color = config.tagStyles.textColor;
tagLink.style.backgroundColor = config.tagStyles.backgroundColor;
tagLink.style.marginRight = '0.5rem';
tagLink.style.marginBottom = '0.3rem';
tagLink.style.textDecoration = 'none';
tagLink.style.padding = '2px 6px';
tagLink.style.borderRadius = '4px';
// 悬停效果
tagLink.addEventListener('mouseover', () => {
tagLink.style.textDecoration = 'underline';
tagLink.style.backgroundColor = '#E6EBF8';
});
tagLink.addEventListener('mouseout', () => {
tagLink.style.textDecoration = 'none';
tagLink.style.backgroundColor = config.tagStyles.backgroundColor;
});
return tagLink;
}
// ===== 数据筛选 =====
const weekStart = getWeekStart();
const targetBlocks = [];
const validFiles = app.vault.getMarkdownFiles().filter(file => {
return isInTargetFolder(file.path) && new Date(file.stat.mtime) >= weekStart;
});
// ===== 主处理函数 =====
const processFiles = async () => {
// 提取符合条件的块内容
for (const file of validFiles) {
try {
const content = await app.vault.cachedRead(file);
const blocks = content.split(/\n\n+/).filter(block => block.trim() !== '');
blocks.forEach(blockText => {
const hasTargetTag = config.targetTags.some(tag => blockText.includes(tag));
if (hasTargetTag) {
targetBlocks.push({
tags: config.targetTags.filter(tag => blockText.includes(tag)),
content: blockText,
file: file,
mtime: file.stat.mtime
});
}
});
} catch (e) {
console.error(`处理文件 ${file.name} 时出错:`, e);
}
}
targetBlocks.sort((a, b) => new Date(b.mtime) - new Date(a.mtime));
// ===== 创建主容器 =====
const mainContainer = document.createElement('div');
mainContainer.style.cssText = `
width: 100%;
max-width: ${config.maxWidth};
margin: 0 auto;
padding: 1rem;
box-sizing: border-box;
`;
// ===== 核心:卡片网格容器 =====
const gridContainer = document.createElement('div');
gridContainer.style.cssText = `
display: grid;
/* 自适应逻辑:先按最小宽度填充,再限制最大列数 */
grid-template-columns: repeat(auto-fill, minmax(${config.minCardWidth}, 1fr));
grid-template-columns: repeat(min(${config.maxColumns}, auto-fill), minmax(${config.minCardWidth}, 1fr));
gap: ${config.gap};
width: 100%;
align-items: stretch; /* 保证同列卡片高度一致 */
`;
mainContainer.appendChild(gridContainer);
// 无内容提示
if (targetBlocks.length === 0) {
const emptyDiv = document.createElement('div');
emptyDiv.textContent = '未找到符合条件的内容';
emptyDiv.style.cssText = `
grid-column: 1 / -1;
padding: 2rem;
text-align: center;
color: var(--text-muted);
background: var(--background-secondary);
border-radius: 6px;
`;
gridContainer.appendChild(emptyDiv);
} else {
// 渲染每个内容块
targetBlocks.forEach(block => {
const blockContainer = document.createElement('div');
blockContainer.style.cssText = `
background: var(--background-secondary);
border-radius: 6px;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); /* 与字数统计卡片阴影一致 */
box-sizing: border-box;
height: 100%; /* 占满网格项高度,避免高度不一致 */
`;
gridContainer.appendChild(blockContainer);
// 1. 渲染可点击标签
const tagsContainer = document.createElement('div');
tagsContainer.style.cssText = `
margin-bottom: 0.8rem;
display: flex;
flex-wrap: wrap;
`;
blockContainer.appendChild(tagsContainer);
block.tags.forEach(tag => {
const tagElement = createClickableTag(tag);
tagsContainer.appendChild(tagElement);
});
// 2. 渲染内容(链接已修复)
const contentDiv = document.createElement('div');
contentDiv.innerHTML = formatBlockContent(block.content);
contentDiv.style.cssText = `
color: var(--text-normal);
line-height: 1.6;
margin-bottom: 1rem;
font-size: 0.95rem;
`;
blockContainer.appendChild(contentDiv);
// 3. 渲染来源链接
const link = document.createElement('a');
link.textContent = `来自: ${block.file.name} (${formatDate(block.mtime)})`;
link.className = 'internal-link';
link.href = block.file.path;
link.style.cssText = `
font-size: 0.9rem;
color: var(--text-muted);
text-decoration: none;
`;
blockContainer.appendChild(link);
});
}
// ===== 响应式样式 =====
const style = document.createElement('style');
style.textContent = `
.internal-link:hover {
text-decoration: underline !important;
color: var(--text-accent) !important;
}
/* 小屏处理:与字数统计代码一致,≤600px强制1列 */
@media (max-width: 600px) {
[style*="grid-template-columns"] {
grid-template-columns: 1fr !important;
}
}
`;
mainContainer.appendChild(style);
dv.container.appendChild(mainContainer);
};
// 执行处理函数
processFiles();jsObsidian 字数统计
显示效果

代码
//在obsidian中使用dataviewjs
// 统计单个文件的字数(修复中文+英文统计,排除格式内容)
async function countWordsInFile(file) {
const content = await app.vault.cachedRead(file);
// 1. 移除无关内容(frontmatter、代码块、注释)
let text = content
.replace(/^---\s*[\s\S]*?---\s*/, '') // 移除frontmatter
.replace(/```[\s\S]*?```/g, '') // 移除多行代码块
.replace(/`[^`]+`/g, '') // 移除单行代码
.replace(/<!--[\s\S]*?-->/g, '') // 移除HTML注释
.trim();
// 2. 移除Markdown格式符号(保留纯文本内容)
text = text
.replace(/#{1,6}\s+/g, '') // 移除标题符号(#)
.replace(/\[\[(.*?)\]\]/g, '$1') // 保留双链文本([[链接]] → 链接)
.replace(/\[(.*?)\]\(.*?\)/g, '$1') // 保留链接文本([文本](链接) → 文本)
.replace(/\*\*|\*|__|_/g, '') // 移除粗体/斜体符号(**/*/__/_)
.replace(/~~/g, '') // 移除删除线(新增,避免遗漏)
.replace(/\s+/g, ' ') // 多空格合并为单个(优化后续匹配)
.trim();
// 3. 【核心修复】中文+英文+数字混合计数逻辑
// 匹配规则:
// - [\u4e00-\u9fa5]:所有中文字符
// - [a-zA-Z]+:英文单词(大小写)
// - \d+:数字(整数)
// - [\u3040-\u309F\u30A0-\u30FF]:可选,日文(如需可保留)
const wordMatches = text.match(/[\u4e00-\u9fa5]|[a-zA-Z]+|\d+/g);
// 4. 统计有效字数(无匹配时返回0,避免NaN)
return wordMatches ? wordMatches.length : 0;
}
// 统计单个文件夹的字数
async function countFolderWords(folderPath) {
let total = 0;
const files = app.vault.getMarkdownFiles().filter(file =>
file.path.startsWith(`${folderPath}/`) || file.path === folderPath
);
for (let i = 0; i < files.length; i++) {
total += await countWordsInFile(files[i]);
}
return {
total,
fileCount: files.length,
folderPath: folderPath,
folderName: folderPath.split('/').pop() // 提取文件夹名称
};
}
// 统计整个知识库的字数
async function countVaultWords() {
let total = 0;
const files = app.vault.getMarkdownFiles();
for (let i = 0; i < files.length; i++) {
total += await countWordsInFile(files[i]);
}
return {
total,
fileCount: files.length,
folderName: "总共写作"
};
}
// 【可修改】需要统计的文件夹路径
const targetFolders = [
"2023",
"2024",
"2025"
// 可继续添加更多文件夹
];
// 【可修改】卡片背景颜色(支持多个颜色,循环使用)
const cardBackgrounds = [
"rgba(240, 243, 252, 0.8)", // 淡蓝色
"rgba(248, 243, 236, 0.8)", // 淡橙色
"rgba(240, 252, 244, 0.8)", // 淡绿色
"rgba(252, 240, 250, 0.8)", // 淡紫色
"rgba(252, 251, 240, 0.8)", // 淡黄色
"rgba(242, 240, 252, 0.8)", // 淡靛色
"rgba(240, 250, 252, 0.8)", // 淡青色
"rgba(252, 245, 240, 0.8)" // 淡粉色
];
// 执行统计并以卡片形式显示结果
(async () => {
// 创建主容器
const mainContainer = document.createElement('div');
mainContainer.style.cssText = `
width: 100%;
padding: 1rem;
box-sizing: border-box;
`;
// 创建卡片网格容器(4列布局)
const gridContainer = document.createElement('div');
gridContainer.style.cssText = `
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
width: 100%;
`;
mainContainer.appendChild(gridContainer);
// 统计并添加所有卡片
const allStats = [];
// 先添加整个知识库统计
allStats.push(await countVaultWords());
// 再添加各个文件夹统计
for (const folder of targetFolders) {
allStats.push(await countFolderWords(folder));
}
// 生成卡片
allStats.forEach((stats, index) => {
// 循环使用背景颜色
const bgColor = cardBackgrounds[index % cardBackgrounds.length];
// 创建卡片容器
const card = document.createElement('div');
card.style.cssText = `
background: ${bgColor};
border-radius: 8px;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
box-sizing: border-box;
`;
// 1. 文件夹名称(标题,加粗)
const title = document.createElement('div');
title.style.cssText = `
font-size: 1.1rem;
font-weight: bold;
margin-bottom: 0.8rem;
color: var(--text-normal);
`;
title.textContent = stats.folderName;
card.appendChild(title);
// 2. 总字数(强调,加粗)
const wordCount = document.createElement('div');
wordCount.style.cssText = `
font-size: 1rem;
margin-bottom: 0.5rem;
`;
// 字数颜色
wordCount.innerHTML = `<span style=
"font-weight: bold;
color: #E75E58;">
${stats.total.toLocaleString()} 字
</span>`;
card.appendChild(wordCount);
// 3. 文件数量
const fileCount = document.createElement('div');
fileCount.style.cssText = `
font-size: 0.9rem;
color: var(--text-muted);
margin-bottom: 0.3rem;
`;
fileCount.textContent = `文件数量:${stats.fileCount}`;
card.appendChild(fileCount);
// 4. 平均文件字数
const avgWords = document.createElement('div');
avgWords.style.cssText = `
font-size: 0.9rem;
color: var(--text-muted);
`;
const avg = stats.fileCount > 0 ? Math.round(stats.total / stats.fileCount) : 0;
avgWords.textContent = `平均字数:${avg.toLocaleString()}`;
card.appendChild(avgWords);
// 添加到网格
gridContainer.appendChild(card);
});
// 添加响应式样式(小屏幕自动调整列数)
const style = document.createElement('style');
style.textContent = `
@media (max-width: 1200px) {
[style*="grid-template-columns"] {
grid-template-columns: repeat(3, 1fr) !important;
}
}
@media (max-width: 900px) {
[style*="grid-template-columns"] {
grid-template-columns: repeat(2, 1fr) !important;
}
}
@media (max-width: 600px) {
[style*="grid-template-columns"] {
grid-template-columns: 1fr !important;
}
}
`;
mainContainer.appendChild(style);
// 添加到页面
dv.container.appendChild(mainContainer);
})();js声明
本内容完全由作者撰写,无任何 AI 辅助创作,内容仅代表创作者个人观点。