joeytoday

Back

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();
js

Obsidian 字数统计

显示效果

代码

//在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 辅助创作,内容仅代表创作者个人观点。

如何在Obsidian中使用dataview插件实现书籍画廊和卡片效果?
https://www.joeytoday.com/blog/2025/obsidian-use-dataview-to-cards-wordscount
Author joeytoday
Published at August 23, 2025