目 录CONTENT

文章目录

halo纯前端字数统计

突发奇想想给自己的文章加个字数统计,试图用纯前端的方式实现,在ai的帮助下我这个前端菜鸡总算是勉勉强强实现了orz。

<script>
class CharCounter {
    constructor(article) {
        this.article = article;
        this.timers = new Set();
        this.animationFrames = new Set();
        this.isAnimating = false;
        this.mainInterval = null;
        this.isDestroyed = false; // 添加销毁标记
        
        this.init();
    }

    init() {
        const lastP = this.article.getElementsByTagName('p')[this.article.getElementsByTagName('p').length-1];
        this.finalCount = (this.article.innerText.match(/[\u4e00-\u9fff]/g)||[]).length;
        
        lastP.insertAdjacentHTML('afterend', 
            `<div id="charCounter" style="
                text-align: center;
                font-size: 10px;
                margin: 32px auto;
                color: currentColor;
                opacity: 0.7;
                font-family: system-ui;
                position: relative;
            ">本文共计 <span id="charCountWrapper" style="
                display: inline-flex;
                font-weight: 700;
                font-size: 1.3em;
                filter: contrast(1.7) brightness(1.3);
                text-shadow: 0 0 0.5px rgba(0,0,0,0.15);
                font-variant-numeric: tabular-nums;
                font-feature-settings: 'tnum';
                letter-spacing: 1px;
            "></span> 字</div>`
        );

        this.wrapper = document.getElementById('charCountWrapper');
        this.digits = String(this.finalCount).split('');
        this.slots = [];
        
        this.digits.forEach(() => {
            const slot = this.createDigitSlot();
            this.slots.push(slot);
            this.wrapper.appendChild(slot);
        });

        // 绑定清理方法到页面卸载事件
        window.addEventListener('unload', () => this.destroy());
    }

    createDigitSlot() {
        const slot = document.createElement('span');
        slot.style.cssText = `
            display: inline-block;
            width: 0.6em;
            height: 1.2em;
            overflow: hidden;
            position: relative;
        `;
        return slot;
    }

    destroy() {
        this.isDestroyed = true;
        this.cleanup();
    }

    cleanup() {
        // 清理所有定时器
        this.timers.forEach(timer => {
            clearTimeout(timer);
            clearInterval(timer);
        });
        this.timers.clear();
        
        // 清理所有动画帧
        this.animationFrames.forEach(frame => cancelAnimationFrame(frame));
        this.animationFrames.clear();
        
        // 清理主循环定时器
        if (this.mainInterval) {
            clearInterval(this.mainInterval);
            this.mainInterval = null;
        }
        
        this.isAnimating = false;
    }

    wait(ms) {
        return new Promise(resolve => {
            if (this.isDestroyed) {
                resolve();
                return;
            }
            const timer = setTimeout(() => {
                this.timers.delete(timer);
                resolve();
            }, ms);
            this.timers.add(timer);
        });
    }

    getRandomDigit(lastNumber) {
        let newNumber;
        do {
            newNumber = Math.floor(Math.random() * 10);
        } while (newNumber === lastNumber);
        return newNumber;
    }

    startRandomizing(startIndex) {
        if (this.isDestroyed) return null;
        
        const interval = setInterval(() => {
            if (!this.isAnimating || this.isDestroyed) {
                clearInterval(interval);
                this.timers.delete(interval);
                return;
            }
            for (let i = startIndex; i < this.slots.length; i++) {
                this.slots[i].textContent = this.getRandomDigit(
                    parseInt(this.slots[i].textContent) || 0
                );
            }
        }, 50);
        this.timers.add(interval);
        return interval;
    }

    animateDigit(slot, finalDigit) {
        return new Promise((resolve) => {
            if (this.isDestroyed) {
                resolve();
                return;
            }

            let startTime;
            let lastUpdateTime = 0;
            let lastNumber = null;
            const duration = 2000;
            
            const updateDigit = (timestamp) => {
                if (!this.isAnimating || this.isDestroyed) {
                    resolve();
                    return;
                }

                if (!startTime) startTime = timestamp;
                const elapsed = timestamp - startTime;
                
                const progress = Math.min(elapsed / duration, 1);
                const minUpdateInterval = 50 + (progress * 100);
                
                if (timestamp - lastUpdateTime < minUpdateInterval) {
                    const frame = requestAnimationFrame(updateDigit);
                    this.animationFrames.add(frame);
                    return;
                }
                lastUpdateTime = timestamp;

                if (progress < 1) {
                    const easeOut = t => Math.sin((t * Math.PI) / 2);
                    const slowdown = easeOut(progress);
                    
                    if (Math.random() > slowdown) {
                        let newNumber;
                        const range = Math.max(1, Math.floor((1 - progress) * 10));
                        const finalDigitNum = parseInt(finalDigit);
                        
                        do {
                            const offset = Math.floor(Math.random() * range) - Math.floor(range / 2);
                            newNumber = (finalDigitNum + offset + 10) % 10;
                        } while (newNumber === lastNumber);
                        
                        lastNumber = newNumber;
                        slot.textContent = newNumber;
                    } else {
                        lastNumber = parseInt(finalDigit);
                        slot.textContent = finalDigit;
                    }
                    
                    const frame = requestAnimationFrame(updateDigit);
                    this.animationFrames.add(frame);
                } else {
                    slot.textContent = finalDigit;
                    resolve();
                }
            };
            
            const frame = requestAnimationFrame(updateDigit);
            this.animationFrames.add(frame);
        });
    }

    async startAnimation() {
        if (this.isAnimating || this.isDestroyed) return;
        
        this.cleanup();
        this.isAnimating = true;

        const counter = document.getElementById('charCounter');
        if (!counter || this.isDestroyed) return;
        
        try {
            counter.style.opacity = '0.6';
            
            let randomizeInterval = this.startRandomizing(0);
            await this.wait(1000);
            
            for (let i = 0; i < this.digits.length; i++) {
                if (!this.isAnimating || this.isDestroyed) break;
                
                if (randomizeInterval) {
                    clearInterval(randomizeInterval);
                    this.timers.delete(randomizeInterval);
                }
                
                randomizeInterval = this.startRandomizing(i + 1);
                
                await this.animateDigit(this.slots[i], this.digits[i]);
                
                if (i < this.digits.length - 1) {
                    await this.wait(1000);
                }
            }
            
            if (randomizeInterval) {
                clearInterval(randomizeInterval);
                this.timers.delete(randomizeInterval);
            }
            counter.style.opacity = '0.7';
            
        } finally {
            this.isAnimating = false;
            // 移除循环逻辑,动画只执行一次
        }
    }


    start() {
        if (!this.isDestroyed) {
            this.startAnimation();
        }
    }
}

// 全局实例存储
let counterInstance = null;

function addCounter() {
    const article = document.querySelector('article');
    if (!article?.getElementsByTagName('p').length || document.querySelector('#charCounter')) {
        return;
    }
    
    // 如果已有实例,先销毁
    if (counterInstance) {
        counterInstance.destroy();
    }
    
    counterInstance = new CharCounter(article);
    counterInstance.start();
}

// 只在页面加载完成后执行一次
if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', addCounter);
} else {
    addCounter();
}
</script>

其实后面都是花里胡哨的动画·····

0

评论区