约 1810 字
预计阅读 4 分钟
Hugo博客迁移日志(4)
2024-07-01
博客
AI 摘要
109酱

芜湖,各位老友们好啊,我是 Chlorine。接着上一回,继续为您说。

下面的目标是为主题添加侧边栏组件。经过观察,这部分代码位于 主题/layouts/partials/sidebar主题/layouts/partials/sidebar.html 中。不过使用循环来简化代码似乎有那么亿点点麻烦,所以还是先不管可扩展性,直接硬上吧。

sidebar 文件夹下添加一个 announcement.html

{{ $announcement := .Site.GetPage "announcement" }}
{{ with $announcement }}
<div class="markdown-body text-neutral-900 dark:text-neutral-100 text-center">
    {{ .Content | safeHTML }}
</div>
{{ end }}

sidebar.html 下添加:

{{ if .Site.Params.Basic.announcement }}
<widget-layout id="announcement-widget" class="pb-4 card-base">
    <div
        class="font-bold transition text-lg text-neutral-900 dark:text-neutral-100 relative ml-8 mt-4 mb-2 before:content-[''] before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)] before:absolute before:left-[-16px] before:top-[5.5px]">
        公告
    </div>
    <div id="announcement-content" class="collapse-wrapper px-4 overflow-hidden">
        {{ partial "sidebar/announcement.html" . }}
    </div>
</widget-layout>
{{ end }}

这样直接编辑 announcement.md 就可以编辑公告了。当然,还需要配置 .Site.Params.Basic.announcement 参数。

目录和公告大差不差:

<div id="toc-container" class="
    toc text-neutral-900 dark:text-neutral-100 
    p-4 rounded-lg shadow-lg overflow-auto max-h-96 
    transition-all duration-300 ease-in-out hover:shadow-2xl
">
    <div id="toc-content" class="transition-all duration-500 ease-in-out">
        {{ .Page.TableOfContents }}
    </div>
</div>

就是这个样式不大好看,凑合用吧。

这个相对麻烦。我不想在 HTML 里硬编码,于是想了半天后,决定使用 TOML 配置,大概就是:

[[params.social]]
    name = "Home"
    url = ""
    icon = "i-carbon-home"
    enable = true
[[params.social]]
    name = "Email"
    url = "mailto:your@email.com"
    icon = "i-carbon-email"
    enable = true

然后改一下 profile 的逻辑:

<div class="card-base">
    <a aria-label="Go to About Page" href="{{ relURL "about/" }}" class="group block relative mx-auto mt-4 lg:mx-3 lg:mt-3 mb-3
       max-w-[240px] lg:max-w-none overflow-hidden rounded-xl active:scale-95">
        <div class="absolute transition pointer-events-none group-hover:bg-black/30 group-active:bg-black/50
        w-full h-full z-50 flex items-center justify-center">
            <div
                class="transition opacity-0 group-hover:opacity-100 text-white text-5xl i-mdi-card-account-details-outline">
            </div>
        </div>
        <div class="mx-auto lg:w-full h-full lg:mt-0 overflow-hidden relative">
            <div class="transition absolute inset-0 dark:bg-black/10 bg-opacity-50 pointer-events-none"></div>
            <img src="{{ relURL "/img/avatar.webp" }}" alt="Profile Image of the Author"
                class="w-full h-full object-center object-cover mx-auto lg:w-full h-full lg:mt-0" />
        </div>
    </a>
    <div class="font-bold text-xl text-center mb-1 dark:text-neutral-50 transition">{{ .Site.Params.Author.name }}</div>
    <div class="h-1 w-5 bg-[var(--primary)] mx-auto rounded-full mb-2 transition"></div>
    <div class="text-center text-neutral-400 mb-2.5 transition">{{ .Site.Params.Author.description }}</div>
    {{/* <div class="flex gap-2 mx-2 justify-center mb-4">
        <a aria-label="Home" href="" target="_blank" class="btn-regular rounded-lg h-10 w-10 active:scale-90">
            <div class="i-carbon-home text-xl"></div>
        </a>

    </div> */}}
    <div class="icons-container">
        {{ range .Site.Params.social }}
        {{ if .enable }}
        <a aria-label="{{ .name }}" href="{{ .url }}" target="_blank"
            class="btn-regular rounded-lg h-10 w-10 active:scale-90">
            <div class="{{ .icon }} text-xl"></div>
        </a>
        {{ end }}
        {{ end }}
    </div>

</div>

再写点美化的 CSS:

.icon svg {
    height: 1em;
    width: 1em
}

.icons-container {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
}

.icons-container a {
    margin: 5px;
    /* 调整图标之间的间距 */
    width: 40px;
    height: 40px;
    display: flex;
    justify-content: center;
    align-items: center;
}

/* 移动端样式 */
@media (max-width: 600px) {
    .icons-container a {
        margin: 4px;
        width: 30px;
        height: 30px;
    }
}

@media (max-width: 600px) {
    .card-base {
        flex-direction: column;
        padding: 0 1rem;
        gap: 0.5rem;
    }
}

就可以渲染社交图标了。

当初整这个的时候费了不少劲,主要的问题居然是我不知道图标不经过重新 UnoCSS 构建是不生效的(因此我安装了 UnoCSS 🤣)。以及,UnoCSS 不能检测没有在 HTML 中直接使用的图标,因此开了个 utility.html 来专门引入图标。

这个我在网上找了许多代码,最终在 Claude 等 AI 伙伴的帮助下写了出来。

新建一个 candy.js

// 糖果雨效果,一定要在 footer 中引入,否则由于页面未加载完成,无法获取到 body 元素

class Circle {
    constructor ({ origin, speed, color, angle, context }) {
        this.origin = origin
        this.position = { ...this.origin }
        this.color = color
        this.speed = speed
        this.angle = angle
        this.context = context
        this.renderCount = 0
    }

    draw() {
        this.context.fillStyle = this.color
        this.context.beginPath()
        this.context.arc(this.position.x, this.position.y, 2, 0, Math.PI * 2)
        this.context.fill()
    }

    move() {
        this.position.x = (Math.sin(this.angle) * this.speed) + this.position.x
        this.position.y = (Math.cos(this.angle) * this.speed) + this.position.y + (this.renderCount * 0.3)
        this.renderCount++
    }
}

class Boom {
    constructor ({ origin, context, circleCount = 10, area }) {
        this.origin = origin
        this.context = context
        this.circleCount = circleCount
        this.area = area
        this.stop = false
        this.circles = []
    }

    randomArray(range) {
        const length = range.length
        const randomIndex = Math.floor(length * Math.random())
        return range[randomIndex]
    }

    randomColor() {
        const range = ['8', '9', 'A', 'B', 'C', 'D', 'E', 'F']
        return '#' + this.randomArray(range) + this.randomArray(range) + this.randomArray(range) + this.randomArray(range) + this.randomArray(range) + this.randomArray(range)
    }

    randomRange(start, end) {
        return (end - start) * Math.random() + start
    }

    init() {
        for (let i = 0; i < this.circleCount; i++) {
            const circle = new Circle({
                context: this.context,
                origin: this.origin,
                color: this.randomColor(),
                angle: this.randomRange(Math.PI - 1, Math.PI + 1),
                speed: this.randomRange(1, 6)
            })
            this.circles.push(circle)
        }
    }

    move() {
        this.circles.forEach((circle, index) => {
            if (circle.position.x > this.area.width || circle.position.y > this.area.height) {
                return this.circles.splice(index, 1)
            }
            circle.move()
        })
        if (this.circles.length == 0) {
            this.stop = true
        }
    }

    draw() {
        this.circles.forEach(circle => circle.draw())
    }
}

class CursorSpecialEffects {
    constructor () {
        this.computerCanvas = document.createElement('canvas')
        this.renderCanvas = document.createElement('canvas')

        this.computerContext = this.computerCanvas.getContext('2d')
        this.renderContext = this.renderCanvas.getContext('2d')

        this.globalWidth = window.innerWidth
        this.globalHeight = window.innerHeight

        this.booms = []
        this.running = false
    }

    handleMouseDown(e) {
        const boom = new Boom({
            origin: { x: e.clientX, y: e.clientY },
            context: this.computerContext,
            area: {
                width: this.globalWidth,
                height: this.globalHeight
            }
        })
        boom.init()
        this.booms.push(boom)
        this.running || this.run()
    }

    handlePageHide() {
        this.booms = []
        this.running = false
    }

    init() {
        const style = this.renderCanvas.style
        style.position = 'fixed'
        style.top = style.left = 0
        style.zIndex = '999999999999999999999999999999999999999999'
        style.pointerEvents = 'none'

        style.width = this.renderCanvas.width = this.computerCanvas.width = this.globalWidth
        style.height = this.renderCanvas.height = this.computerCanvas.height = this.globalHeight

        document.body.append(this.renderCanvas)

        window.addEventListener('mousedown', this.handleMouseDown.bind(this))
        window.addEventListener('pagehide', this.handlePageHide.bind(this))
    }

    run() {
        this.running = true
        if (this.booms.length == 0) {
            return this.running = false
        }

        requestAnimationFrame(this.run.bind(this))

        this.computerContext.clearRect(0, 0, this.globalWidth, this.globalHeight)
        this.renderContext.clearRect(0, 0, this.globalWidth, this.globalHeight)

        this.booms.forEach((boom, index) => {
            if (boom.stop) {
                return this.booms.splice(index, 1)
            }
            boom.move()
            boom.draw()
        })
        this.renderContext.drawImage(this.computerCanvas, 0, 0, this.globalWidth, this.globalHeight)
    }
}

const cursorSpecialEffects = new CursorSpecialEffects()
cursorSpecialEffects.init()

再添加 CSS:

.candy {
    position: absolute;
    width: 20px;
    height: 20px;
    background-color: #f00;
    border-radius: 50%;
    pointer-events: none;
}

引入即可。以及,点名批评 Hugo 奇葩的构建机制,JavaScript 就摆在那,愣是不加到 public 里,我也是没招了。

这个属实是给我整得心力交瘁。好在最后在强大的 Claude 3.5 的帮助下写出来了。

开一个 clickcopy.js

(function () {
    'use strict';
    if (!navigator.clipboard) {
        return;
    }

    function createSVGIcon(iconName) {
        const svgNS = "http://www.w3.org/2000/svg";
        const svg = document.createElementNS(svgNS, "svg");
        svg.setAttribute("viewBox", "0 0 32 32");

        const path = document.createElementNS(svgNS, "path");
        if (iconName === "copy") {
            path.setAttribute("d", "M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z");
            const path2 = document.createElementNS(svgNS, "path");
            path2.setAttribute("d", "M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z");
            svg.appendChild(path2);
        } else if (iconName === "checkmark") {
            path.setAttribute("d", "M13 24L4 15 5.414 13.586 13 21.171 26.586 7.586 28 9 13 24z");
        }
        svg.appendChild(path);
        return svg;
    }

    function flashCopyMessage(el, msg, iconName) {
        var iconContainer = el.querySelector('.copy-icon');
        var msgContainer = el.querySelector('.copy-msg');

        // 更新图标
        iconContainer.innerHTML = '';
        iconContainer.appendChild(createSVGIcon(iconName));

        // 更新消息
        msgContainer.textContent = msg;

        setTimeout(function () {
            // 2秒后清除消息文本和图标变化
            msgContainer.textContent = '';
            iconContainer.innerHTML = '';
            iconContainer.appendChild(createSVGIcon('copy'));
        }, 2000);
    }

    function addCopyButton(containerEl) {
        if (containerEl.querySelector('.highlight-copy-btn')) {
            return;
        }

        var copyBtn = document.createElement("button");
        copyBtn.className = "highlight-copy-btn";
        copyBtn.setAttribute("aria-label", "Copy to clipboard");
        copyBtn.innerHTML = '<span class="copy-icon"></span><span class="copy-msg"></span>';
        copyBtn.querySelector('.copy-icon').appendChild(createSVGIcon('copy'));
        copyBtn.style.display = "none";

        var codeEl = containerEl.querySelector('code') || containerEl;
        copyBtn.addEventListener('click', function () {
            navigator.clipboard.writeText(codeEl.innerText).then(function () {
                flashCopyMessage(copyBtn, 'Copied!', 'checkmark');
            }, function (err) {
                console.error('Unable to copy: ', err);
                flashCopyMessage(copyBtn, 'Failed :\'(', 'copy');
            });
        });

        containerEl.appendChild(copyBtn);
        containerEl.style.position = 'relative';

        containerEl.addEventListener('mouseenter', function () {
            copyBtn.style.display = "block";
        });
        containerEl.addEventListener('mouseleave', function () {
            copyBtn.style.display = "none";
        });
    }

    // 添加复制按钮到所有代码块
    var codeBlocks = document.querySelectorAll('pre');
    Array.prototype.forEach.call(codeBlocks, addCopyButton);
})();

写 CSS:

.highlight-copy-btn {
    position: absolute;
    top: 7px;
    right: 7px;
    border: 0;
    border-radius: 4px;
    padding: 5px;
    font-size: 0.8em;
    line-height: 1;
    background-color: transparent;
    /* 移除背景色 */
    color: inherit;
    /* 继承父元素的颜色 */
    cursor: pointer;
    opacity: 0.6;
    transition: opacity 0.3s;
}

.highlight-copy-btn:hover {
    opacity: 1;
}

.copy-icon {
    display: inline-flex;
    /* 使用 flex 布局以更好地控制图标 */
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
}

.copy-icon svg {
    width: 16px;
    height: 16px;
    fill: currentColor;
    /* 使用当前文本颜色填充SVG */
}

.copy-msg {
    margin-left: 5px;
    font-size: 12px;
}

然后引入就完事了。不过这个功能有时候加载慢点,需要刷新才能启动。

以及复制之后的按钮和文本有点偏移,但是我没力气调了。

Hugo博客迁移日志(4)
https://www.yoghurtlee.com/migrating-to-hugo-4/
作者
Chlorine
发布于
2024-07-01