乐呵呵、欢的博客

lehhair's Blog

用rss订阅给你的博客加上热图

2024-05-28

用rss订阅给你的博客加上热图

代码是抄的蜗牛大佬的感谢大佬的分享

1. 封装后的js代码如下

class HeatmapCard extends HTMLElement {
    constructor() {
        super();
        this.rss_url = '';
        this.attachShadow({ mode: 'open' });

        // 监听系统颜色方案的变化
        this.colorSchemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
        this.colorSchemeMediaQuery.addEventListener('change', this.handleColorSchemeChange.bind(this));
    }

    connectedCallback() {
        this.shadowRoot.innerHTML = `
            <style>
                :host {
                    --ht-main: #334155;
                    --ht-tooltip: #24292f;
                    --ht-tooltip-bg: #fff;
                    --ht-lv-0: #ebedf0;
                    --ht-lv-1: #9be9a8;
                    --ht-lv-2: #40c463;
                    --ht-lv-3: #30a14e;
                    --ht-lv-4: #216e39;
                }
                :host([data-theme="dark"]) {
                    --ht-main: #94a3b8;
                    --ht-tooltip: #24292f;
                    --ht-tooltip-bg: #fff;
                    --ht-lv-0: #161b22;
                    --ht-lv-1: #0e4429;
                    --ht-lv-2: #006d32;
                    --ht-lv-3: #26a641;
                    --ht-lv-4: #39d353;
                }
                .heatmap_container {
                    display: flex;
                    flex-direction: column;
                    align-items: center;
                    font-size: 10px;
                    line-height: 12px;
                    color: var(--ht-main);
                    background-color: transparent;
                }
                .heatmap_content {
                    display: flex;
                    flex-direction: row;
                    align-items: flex-end;
                    background-color: transparent;
                }
                .heatmap_week {
                    display: flex;
                    margin-top: 0.25rem;
                    margin-right: 0.25rem;
                    flex-direction: column;
                    justify-content: flex-start;
                    align-items: flex-end;
                    text-align: right;
                    background-color: transparent;
                }
                .heatmap_main {
                    display: flex;
                    flex-direction: column;
                    background-color: transparent;
                }
                .heatmap_month {
                    display: flex;
                    margin-top: 0.25rem;
                    margin-right: 0.25rem;
                    flex-direction: row;
                    justify-content: space-around;
                    align-items: flex-end;
                    text-align: right;
                }
                .heatmap {
                    display: flex;
                    flex-direction: row;
                    height: 84px;
                    background-color: transparent;
                }
                .heatmap_footer {
                    display: flex;
                    margin-top: 0.5rem;
                    background-color: transparent;
                }
                .heatmap_level {
                    display: flex;
                    gap: 2px;
                    margin: 0 0.25rem;
                    flex-direction: row;
                    width: max-content;
                    height: 10px;
                    background-color: transparent;
                }
                .heatmap_level_item {
                    display: block;
                    border-radius: 0.125rem;
                    width: 10px;
                    height: 10px;
                }
                .heatmap_level_0 {
                    background: var(--ht-lv-0);
                }
                .heatmap_level_1 {
                    background: var(--ht-lv-1);
                }
                .heatmap_level_2 {
                    background: var(--ht-lv-2);
                }
                .heatmap_level_3 {
                    background: var(--ht-lv-3);
                }
                .heatmap_level_4 {
                    background: var(--ht-lv-4);
                }
                .heatmap_day {
                    width: 10px;
                    height: 10px;
                    background-color: transparent;
                    margin: 1px;
                    border-radius: 2px;
                    display: inline-block;
                    position: relative;
                }
                .heatmap_tooltip {
                    position: absolute;
                    bottom: 12px;
                    left: 50%;
                    width: max-content;
                    color: var(--ht-tooltip);
                    background-color: var(--ht-tooltip-bg);
                    font-size: 12px;
                    line-height: 16px;
                    padding: 8px;
                    border-radius: 3px;
                    white-space: pre-wrap;
                    opacity: 1;
                    transition: 0.3s;
                    z-index: 1000;
                    text-align: right;
                    transform: translateX(-50%);
                }
                .heatmap_tooltip_count,
                .heatmap_tooltip_post {
                    display: inline-block;
                }
                .heatmap_tooltip_title,
                .heatmap_tooltip_date {
                    display: block;
                }
                .heatmap_tooltip_date {
                    margin: 0 0.25rem;
                }
                .heatmap_day_level_0 {
                    background-color: var(--ht-lv-0);
                }
                .heatmap_day_level_1 {
                    background-color: var(--ht-lv-1);
                }
                .heatmap_day_level_2 {
                    background-color: var(--ht-lv-2);
                }
                .heatmap_day_level_3 {
                    background-color: var(--ht-lv-3);
                }
                .heatmap_day_level_4 {
                    background-color: var(--ht-lv-4);
                }
            </style>
            <div class="heatmap_container">
                <div class="heatmap_content">
                    <div class="heatmap_week">
                        <span>Mon</span>
                        <span>&nbsp;</span>
                        <span>Wed</span>
                        <span>&nbsp;</span>
                        <span>Fri</span>
                        <span>&nbsp;</span>
                        <span>Sun</span>
                    </div>
                    <div class="heatmap_main">
                        <div class="month heatmap_month"></div>
                        <div id="heatmap" class="heatmap"></div>
                    </div>
                </div>
                <div class="heatmap_footer">
                    <div class="heatmap_less">Less</div>
                    <div class="heatmap_level">
                        <span class="heatmap_level_item heatmap_level_0"></span>
                        <span class="heatmap_level_item heatmap_level_1"></span>
                        <span class="heatmap_level_item heatmap_level_2"></span>
                        <span class="heatmap_level_item heatmap_level_3"></span>
                        <span class="heatmap_level_item heatmap_level_4"></span>
                    </div>
                    <div class="heatmap_more">More</div>
                </div>
            </div>
        `;

        this.generateMonthLabels();
        this.fetchData();

        // 初始化主题
        this.updateTheme();
    }

    fetchData() {
        fetch(this.rss_url)
            .then(response => response.text())
            .then(str => new window.DOMParser().parseFromString(str, "text/xml"))
            .then(data => {
                var items = data.querySelectorAll("item");
                var blogInfo = {
                    "pages": []
                };

                items.forEach(item => {
                    var page = {
                        "title": item.querySelector("title").textContent,
                        "link": item.querySelector("link").textContent,
                        "date": new Date(item.querySelector("pubDate").textContent).toISOString().split('T')[0],
                        "word_count": new DOMParser().parseFromString(item.querySelector("description").textContent, "text/html").body.textContent.trim().length.toString()
                    };
                    blogInfo.pages.push(page);
                });

                this.createHeatmap(blogInfo);
            })
            .catch(error => {
                console.log('Request failed: ' + error.message);
            });
    }

    generateMonthLabels() {
        const monthDiv = this.shadowRoot.querySelector('.month');
        const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

        let numMonths;
        if (window.innerWidth < 768) {
            numMonths = 5;
        } else {
            numMonths = 12;
        }

        const currentDate = new Date();
        const startMonthIndex = (currentDate.getMonth() - (numMonths - 1) + 12) % 12;

        for (let i = startMonthIndex; i < startMonthIndex + numMonths; i++) {
            const monthSpan = document.createElement('span');
            const monthIndex = i % 12;
            monthSpan.textContent = monthNames[monthIndex];
            monthDiv.appendChild(monthSpan);
        }
    }

    createHeatmap(blogInfo) {
        const container = this.shadowRoot.getElementById('heatmap');
        const startDate = this.getStartDate();
        const endDate = new Date();

        let currentWeek = this.createWeek();
        container.appendChild(currentWeek);

        let currentDate = startDate;

        while (currentDate <= endDate) {
            const dateString = `${currentDate.getFullYear()}-${("0" + (currentDate.getMonth() + 1)).slice(-2)}-${("0" + (currentDate.getDate())).slice(-2)}`;

            const articleDataList = blogInfo.pages.filter(page => page.date === dateString);

            if (articleDataList.length > 0) {
                const titles = articleDataList.map(data => data.title);
                const title = titles.map(t => `《${t}》`).join('<br />');

                let count = 0;
                let post = articleDataList.length;

                articleDataList.forEach(data => {
                    count += parseInt(data.word_count, 10);
                });

                const day = this.createDay(dateString, title, count, post);
                currentWeek.appendChild(day);
            } else {
                const day = this.createDay(dateString, '', '0', '0');
                currentWeek.appendChild(day);
            }

            if (currentDate.getDay() === 0) {
                currentWeek = this.createWeek();
                container.appendChild(currentWeek);
            }

            currentDate.setDate(currentDate.getDate() + 1);
        }
    }

    createDay(date, title, count, post) {
        const day = document.createElement("div");
        day.className = "heatmap_day";
        day.setAttribute("data-title", title);
        day.setAttribute("data-count", count);
        day.setAttribute("data-post", post);
        day.setAttribute("data-date", date);

        day.addEventListener("mouseenter", function () {
            const tooltip = document.createElement("div");
            tooltip.className = "heatmap_tooltip";

            let tooltipContent = "";

            if (post && parseInt(post, 10) !== 0) {
                tooltipContent += '<span class="heatmap_tooltip_post">' + '共 ' + post + ' 篇' + '</span>';
            }

            if (count && parseInt(count, 10) !== 0) {
                tooltipContent += '<span class="heatmap_tooltip_count">' + ' ' + count + ' 字;' + '</span>';
            }

            if (title && parseInt(title, 10) !== 0) {
                tooltipContent += '<span class="heatmap_tooltip_title">' + title + '</span>';
            }

            if (date) {
                tooltipContent += '<span class="heatmap_tooltip_date">' + date + '</span>';
            }

            tooltip.innerHTML = tooltipContent;
            day.appendChild(tooltip);
        });

        day.addEventListener("mouseleave", function () {
            const tooltip = day.querySelector(".heatmap_tooltip");
            if (tooltip) {
                day.removeChild(tooltip);
            }
        });

        if (count == 0) {
            day.classList.add("heatmap_day_level_0");
        } else if (count > 0 && count < 1000) {
            day.classList.add("heatmap_day_level_1");
        } else if (count >= 1000 && count < 2000) {
            day.classList.add("heatmap_day_level_2");
        } else if (count >= 2000 && count < 3000) {
            day.classList.add("heatmap_day_level_3");
        } else {
            day.classList.add("heatmap_day_level_4");
        }

        return day;
    }

    createWeek() {
        const week = document.createElement('div');
        week.className = 'heatmap_week';
        return week;
    }

    getStartDate() {
        const today = new Date();
        let numMonths;
        if (window.innerWidth < 768) {
            numMonths = 4;
        } else {
            numMonths = 12;
        }

        const startDate = new Date(today.getFullYear(), today.getMonth() - numMonths + 1, 1, today.getHours(), today.getMinutes(), today.getSeconds());

        while (startDate.getDay() !== 1) {
            startDate.setDate(startDate.getDate() + 1);
        }

        return startDate;
    }

    setTheme(theme) {
        if (theme === 'light') {
            this.setAttribute('data-theme', 'light');
        } else {
            this.setAttribute('data-theme', 'dark');
        }
    }

    handleColorSchemeChange(event) {
        if (event.matches) {
            this.setTheme('dark');
        } else {
            this.setTheme('light');
        }
    }

    updateTheme() {
        const systemPrefersDark = this.colorSchemeMediaQuery.matches;
        const documentTheme = document.documentElement.getAttribute('data-theme');
        if (documentTheme) {
            this.setTheme(documentTheme);
        } else {
            this.setTheme(systemPrefersDark ? 'dark' : 'light');
        }
    }
}

customElements.define('heatmap-card', HeatmapCard);

document.addEventListener('DOMContentLoaded', () => {
    const initialTheme = document.documentElement.getAttribute('data-theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
    document.querySelectorAll('heatmap-card').forEach(card => card.setTheme(initialTheme));

    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            if (mutation.type === 'attributes' && mutation.attributeName === 'data-theme') {
                const newTheme = document.documentElement.getAttribute('data-theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
                document.querySelectorAll('heatmap-card').forEach(card => card.setTheme(newTheme));
            }
        });
    });

    observer.observe(document.documentElement, { attributes: true });
});

将其保存在你的博客路径
然后像这样嵌入

    <script src="/heatmap/rss_heatmap.js"></script>
    <p>这是我的瞬间。</p>
    <div class="heatmap-container" data-rss-url="https://blog.lehhair.net/moments/rss.xml"></div>
    
    <p>这才是我的博客。</p>
    <div class="heatmap-container" data-rss-url="https://blog.lehhair.net/rss.xml"></div>

    <script>
        document.addEventListener('DOMContentLoaded', function() {
            var containers = document.querySelectorAll('.heatmap-container');
            containers.forEach(function(container) {
                var card = document.createElement('heatmap-card');
                card.rss_url = container.getAttribute('data-rss-url'); // 从 data-rss-url 属性中获取 RSS 订阅链接
                container.appendChild(card);
            });
        });
    </script>

效果看乐呵呵的关于页面|

2. 新增一个版本是小尺寸设备出现滚动条,依旧显示12个月内容

class HeatmapCard extends HTMLElement {
    constructor() {
        super();
        this.rss_url = '';
        this.attachShadow({ mode: 'open' });

        // 监听系统颜色方案的变化
        this.colorSchemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
        this.colorSchemeMediaQuery.addEventListener('change', this.handleColorSchemeChange.bind(this));
    }

    connectedCallback() {
        this.shadowRoot.innerHTML = `
            <style>
            :host {
                    --ht-main: #334155;
                    --ht-tooltip: #24292f;
                    --ht-tooltip-bg: #fff;
                    --ht-lv-0: #ebedf0;
                    --ht-lv-1: #9be9a8;
                    --ht-lv-2: #40c463;
                    --ht-lv-3: #30a14e;
                    --ht-lv-4: #216e39;
                }
                :host([data-theme="dark"]) {
                    --ht-main: #94a3b8;
                    --ht-tooltip: #24292f;
                    --ht-tooltip-bg: #fff;
                    --ht-lv-0: #161b22;
                    --ht-lv-1: #0e4429;
                    --ht-lv-2: #006d32;
                    --ht-lv-3: #26a641;
                    --ht-lv-4: #39d353;
                }
            .heatmap_container {
                display: flex;
                flex-direction: column;
                font-size: 10px;
                line-height: 12px;
                color: var(--ht-main);
                background-color: transparent;
                align-items: center;
            }
            .heatmap_content {
                display: flex;
                flex-direction: row;
                align-items: flex-end;
                background-color: transparent;
                width: 860px; /* 固定宽度 */
                overflow-x: auto; /* 当内容超出容器时出现横向滚动条 */
                overflow-y: hidden; /* 隐藏纵向滚动条 */

            }

                .heatmap_week {
                    display: flex;
                    margin-top: 0.25rem;
                    margin-right: 0.25rem;
                    flex-direction: column;
                    justify-content: flex-start;
                    align-items: flex-end;
                    text-align: right;
                    background-color: transparent;
                }
                .heatmap_content > .heatmap_week span {
                    min-width: 22px; /* 确保文字不被压缩 */
                    white-space: nowrap; /* 防止文字换行 */
                }
                .heatmap_main {
                    display: flex;
                    flex-direction: column;
                    background-color: transparent;
                }
                /* 媒体查询,确保在屏幕宽度小于860px时出现横向滚动条 */
                @media (max-width: 860px) {
                    .heatmap_content {
                        width: 100%; /* 设置宽度为100%,以适应较小的屏幕 */
                    }
                }
                
                .heatmap_month {
                    display: flex;
                    margin-top: 0.25rem;
                    margin-right: 0.25rem;
                    flex-direction: row;
                    justify-content: space-around;
                    align-items: flex-end;
                    text-align: right;
                }
                .heatmap {
                    display: flex;
                    flex-direction: row;
                    height: 84px;
                    background-color: transparent;
                }
                .heatmap_footer {
                    display: flex;
                    margin-top: 0.5rem;
                    background-color: transparent;
                    align-self: flex-end;
                    min-width:113px;
                    white-space: nowrap;
                    width: 18%; /* 宽度设置为100%,占满容器 */
                }
                .heatmap_level {
                    display: flex;
                    gap: 2px;
                    margin: 0 0.25rem;
                    flex-direction: row;
                    width: max-content;
                    height: 10px;
                    background-color: transparent;
                    align-self: flex-end;
                }

                .heatmap_level_item {
                    display: block;
                    border-radius: 0.125rem;
                    width: 10px;
                    height: 10px;
                }
                .heatmap_level_0 {
                    background: var(--ht-lv-0);
                }
                .heatmap_level_1 {
                    background: var(--ht-lv-1);
                }
                .heatmap_level_2 {
                    background: var(--ht-lv-2);
                }
                .heatmap_level_3 {
                    background: var(--ht-lv-3);
                }
                .heatmap_level_4 {
                    background: var(--ht-lv-4);
                }
                .heatmap_day {
                    width: 10px;
                    height: 10px;
                    background-color: transparent;
                    margin: 1px;
                    border-radius: 2px;
                    display: inline-block;
                    position: relative;
                }
                .heatmap_tooltip {
                    position: absolute;
                    color: var(--ht-tooltip);
                    background-color: var(--ht-tooltip-bg);
                    font-size: 12px;
                    line-height: 16px;
                    padding: 8px;
                    border-radius: 3px;
                    white-space: pre-wrap;
                    opacity: 1;
                    transition: 0.3s;
                    z-index: 1000;
                    text-align: right;
                }

                .heatmap_tooltip_count,
                .heatmap_tooltip_post {
                    display: inline-block;
                }
                .heatmap_tooltip_title,
                .heatmap_tooltip_date {
                    display: block;
                }
                .heatmap_tooltip_date {
                    margin: 0 0.25rem;
                }
                .heatmap_day_level_0 {
                    background-color: var(--ht-lv-0);
                }
                .heatmap_day_level_1 {
                    background-color: var(--ht-lv-1);
                }
                .heatmap_day_level_2 {
                    background-color: var(--ht-lv-2);
                }
                .heatmap_day_level_3 {
                    background-color: var(--ht-lv-3);
                }
                .heatmap_day_level_4 {
                    background-color: var(--ht-lv-4);
                }
            </style>
            <div class="heatmap_container">
                <div class="heatmap_content">
                    <div class="heatmap_week">
                        <span>Mon</span>
                        <span>&nbsp;</span>
                        <span>Wed</span>
                        <span>&nbsp;</span>
                        <span>Fri</span>
                        <span>&nbsp;</span>
                        <span>Sun</span>
                    </div>
                    <div class="heatmap_main">
                        <div class="month heatmap_month"></div>
                        <div id="heatmap" class="heatmap"></div>
                    </div>
                </div>
                <div class="heatmap_footer">
                    <div class="heatmap_less">Less</div>
                    <div class="heatmap_level">
                        <span class="heatmap_level_item heatmap_level_0"></span>
                        <span class="heatmap_level_item heatmap_level_1"></span>
                        <span class="heatmap_level_item heatmap_level_2"></span>
                        <span class="heatmap_level_item heatmap_level_3"></span>
                        <span class="heatmap_level_item heatmap_level_4"></span>
                    </div>
                    <div class="heatmap_more">More</div>
                </div>
                <div class="heatmap_tooltip_container"></div>
            </div>
        `;

        this.generateMonthLabels();
        this.fetchData();

        // 初始化主题
        this.updateTheme();
    }

    fetchData() {
        fetch(this.rss_url)
            .then(response => response.text())
            .then(str => new window.DOMParser().parseFromString(str, "text/xml"))
            .then(data => {
                var items = data.querySelectorAll("item");
                var blogInfo = {
                    "pages": []
                };

                items.forEach(item => {
                    var page = {
                        "title": item.querySelector("title").textContent,
                        "link": item.querySelector("link").textContent,
                        "date": new Date(item.querySelector("pubDate").textContent).toISOString().split('T')[0],
                        "word_count": new DOMParser().parseFromString(item.querySelector("description").textContent, "text/html").body.textContent.trim().length.toString()
                    };
                    blogInfo.pages.push(page);
                });

                this.createHeatmap(blogInfo);
            })
            .catch(error => {
                console.log('Request failed: ' + error.message);
            });
    }

    generateMonthLabels() {
        const monthDiv = this.shadowRoot.querySelector('.month');
        const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

        const numMonths = 12; // 统一生成12个月的标签

        const currentDate = new Date();
        const startMonthIndex = (currentDate.getMonth() - (numMonths - 1) + 12) % 12;

        for (let i = startMonthIndex; i < startMonthIndex + numMonths; i++) {
            const monthSpan = document.createElement('span');
            const monthIndex = i % 12;
            monthSpan.textContent = monthNames[monthIndex];
            monthDiv.appendChild(monthSpan);
        }
    }

    createHeatmap(blogInfo) {
        const container = this.shadowRoot.getElementById('heatmap');
        const startDate = this.getStartDate();
        const endDate = new Date();

        let currentWeek = this.createWeek();
        container.appendChild(currentWeek);

        let currentDate = startDate;

        while (currentDate <= endDate) {
            const dateString = `${currentDate.getFullYear()}-${("0" + (currentDate.getMonth() + 1)).slice(-2)}-${("0" + (currentDate.getDate())).slice(-2)}`;

            const articleDataList = blogInfo.pages.filter(page => page.date === dateString);

            if (articleDataList.length > 0) {
                const titles = articleDataList.map(data => data.title);
                const title = titles.map(t => `《${t}》`).join('<br />');

                let count = 0;
                let post = articleDataList.length;

                articleDataList.forEach(data => {
                    count += parseInt(data.word_count, 10);
                });

                const day = this.createDay(dateString, title, count, post);
                currentWeek.appendChild(day);
            } else {
                const day = this.createDay(dateString, '', '0', '0');
                currentWeek.appendChild(day);
            }

            if (currentDate.getDay() === 0) {
                currentWeek = this.createWeek();
                container.appendChild(currentWeek);
            }

            currentDate.setDate(currentDate.getDate() + 1);
        }
    }

    createDay(date, title, count, post) {
        const day = document.createElement("div");
        day.className = "heatmap_day";
        day.setAttribute("data-title", title);
        day.setAttribute("data-count", count);
        day.setAttribute("data-post", post);
        day.setAttribute("data-date", date);

        day.addEventListener("mouseenter", this.showTooltip.bind(this, day));
        day.addEventListener("mouseleave", this.hideTooltip.bind(this));

        if (count == 0) {
            day.classList.add("heatmap_day_level_0");
        } else if (count > 0 && count < 1000) {
            day.classList.add("heatmap_day_level_1");
        } else if (count >= 1000 && count < 2000) {
            day.classList.add("heatmap_day_level_2");
        } else if (count >= 2000 && count < 3000) {
            day.classList.add("heatmap_day_level_3");
        } else {
            day.classList.add("heatmap_day_level_4");
        }
        

        return day;
    }

    showTooltip(day, event) {
    const title = day.getAttribute("data-title");
    const count = day.getAttribute("data-count");
    const post = day.getAttribute("data-post");
    const date = day.getAttribute("data-date");

    const tooltip = document.createElement("div");
    tooltip.className = "heatmap_tooltip";

    let tooltipContent = "";

    if (post && parseInt(post, 10) !== 0) {
        tooltipContent += '<span class="heatmap_tooltip_post">' + '共 ' + post + ' 篇' + '</span>';
    }

    if (count && parseInt(count, 10) !== 0) {
        tooltipContent += '<span class="heatmap_tooltip_count">' + ' ' + count + ' 字;' + '</span>';
    }

    if (title && parseInt(title, 10) !== 0) {
        tooltipContent += '<span class="heatmap_tooltip_title">' + title + '</span>';
    }

    if (date) {
        tooltipContent += '<span class="heatmap_tooltip_date">' + date + '</span>';
    }

    tooltip.innerHTML = tooltipContent;

    const tooltipContainer = this.shadowRoot.querySelector(".heatmap_tooltip_container");
    tooltipContainer.appendChild(tooltip);

    const updateTooltipPosition = (event) => {
        const tooltipRect = tooltip.getBoundingClientRect();
        const left = event.pageX - tooltipRect.width / 2;
        const top = event.pageY - tooltipRect.height - 10;

        tooltip.style.left = `${left}px`;
        tooltip.style.top = `${top}px`;
    };

    updateTooltipPosition(event);

    const mouseMoveHandler = (event) => {
        updateTooltipPosition(event);
    };

    day.addEventListener("mousemove", mouseMoveHandler);

    day.addEventListener("mouseleave", () => {
        day.removeEventListener("mousemove", mouseMoveHandler);
        this.hideTooltip();
    });
}

    

    hideTooltip() {
        const tooltipContainer = this.shadowRoot.querySelector(".heatmap_tooltip_container");
        const tooltip = tooltipContainer.querySelector(".heatmap_tooltip");
        if (tooltip) {
            tooltipContainer.removeChild(tooltip);
        }
    }

    createWeek() {
        const week = document.createElement('div');
        week.className = 'heatmap_week';
        return week;
    }

    getStartDate() {
        const today = new Date();
        const numMonths = 12; // 统一生成12个月的数据

        const startDate = new Date(today.getFullYear(), today.getMonth() - numMonths + 1, 1, today.getHours(), today.getMinutes(), today.getSeconds());

        while (startDate.getDay() !== 1) {
            startDate.setDate(startDate.getDate() + 1);
        }

        return startDate;
    }

    setTheme(theme) {
        if (theme === 'light') {
            this.setAttribute('data-theme', 'light');
        } else {
            this.setAttribute('data-theme', 'dark');
        }
    }

    handleColorSchemeChange(event) {
        if (event.matches) {
            this.setTheme('dark');
        } else {
            this.setTheme('light');
        }
    }

    updateTheme() {
        const systemPrefersDark = this.colorSchemeMediaQuery.matches;
        const documentTheme = document.documentElement.getAttribute('data-theme');
        if (documentTheme) {
            this.setTheme(documentTheme);
        } else {
            this.setTheme(systemPrefersDark ? 'dark' : 'light');
        }
    }
}

customElements.define('heatmap-card', HeatmapCard);

document.addEventListener('DOMContentLoaded', () => {
    const initialTheme = document.documentElement.getAttribute('data-theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
    document.querySelectorAll('heatmap-card').forEach(card => card.setTheme(initialTheme));

    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            if (mutation.type === 'attributes' && mutation.attributeName === 'data-theme') {
                const newTheme = document.documentElement.getAttribute('data-theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
                document.querySelectorAll('heatmap-card').forEach(card => card.setTheme(newTheme));
            }
        });
    });

    observer.observe(document.documentElement, { attributes: true });
});

3. 直接引用

    <script src="/heatmap/heatmap-rss.js"></script>
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            var containers = document.querySelectorAll('.heatmap-container');
            containers.forEach(function(container) {
                var card = document.createElement('heatmap-card');
                card.rss_url = container.getAttribute('data-rss-url');
                container.appendChild(card);
            });
        });
    </script>
<div class="heatmap-container" data-rss-url="https://blog.lehhair.net/moments/rss.xml"></div>