AI正在绞尽脑汁想思路ING···
BiのAI摘要
HunYuan-Lite

又开始折腾啦，这次把说说页面加上，改用moments项目的api

🪧引用站外地址，不保证站点的可用性和安全性
Moments 极简朋友圈
githun@kingwrcy

# 前期要求

## 硬件要求

1. 一台服务器
2. 一个可自主解析的域名

## 软件要求

1. docker环境
2. 反向代理工具（本文以Nginx为例）

# 介绍与展示

这里先给大家展示一下最终的效果，注意该教程可能仅适合部分主题，如果出现主题不适配的情况请自行适配，这里以本站主题 `Hexo-theme-butterfly`为基础进行修改：

1. 说说页面：
   ✅来自本站，本站可确保其安全性，请放心点击跳转
   我的说说
   胡言乱语ing。。。
2. 轻量朋友圈
   🪧引用站外地址，不保证站点的可用性和安全性
   朋友圈
   依旧胡言乱语
3. 功能说明

Moments作为一个轻量朋友圈，其功能都是分享上的部分，如下所示：

- 分享：链接，图片，音乐，视频，书籍，电影
- 信息：自定义位置，自定义标签，是否公开
- 页面：Markdown渲染，编辑说说，删除说说，暗夜模式，自定义图标，信息，CSS及JS代码
- 功能：S3存储，文件查询，多用户注册，点赞，评论，API

简单介绍完毕，下面我就来教大家如何进行部署！

# 部署教程

## Moments部署

### Compose部署

官方给予了很完善的教程，这里我仅仅简单介绍一下 `docker-compose`部署的方式，如果你想以源码等其他方式进行部署，请查看文章开头部分的 `github`地址进行查阅。

首先，在服务器任意位置创建文件：`docker-compose.yaml`，填入以下内容：

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    version: '3'
    services:
      moments:
        image: kingwrcy/moments:latest
        container_name: moments
        restart: always
        environment:
          port: 3000
          JWT_KEY: "自己随便生成点字符串"
          ENABLE_SWAGGER: "true"
          CORS_ORIGIN: # 填写跨域域名
        ports:
          - "3000:3000"  # 自行换端口，换前面的，后面的3000不要动
        volumes:
          - ./opt/data:/app/data
        #   - ./data/localtime:/etc/localtime:ro
        #   - ./data/timezone:/etc/timezone:ro

注意文件，我将 `/opt/`文件夹(当然可以改成其他的)下的 `/data`文件夹挂载了进去，数据都会在里面，迁移时仅需整体打包到新服务器即可。然后执行以下两条命令，后续需要升级也仅需要执行这两个命令：

    1
    2
    docker-compose pull
    docker-compose up -d

如果网络环境不佳，可尝试替换 `docker`源，可以自行查找

通过反向代理将其添加到某个域名中，这里就不再多说了，各大面板都有极其完备的反代文档。

### Nginx修改

`Moments`在跨域 `docker-compose`文件中可以配置，所以不必进行此步

网站目录

返回到上一级目录，也就是域名名称的文件夹下，找到 `Proxy`文件夹，编辑里面的 `root.conf`文件为如下内容：

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    #代理配置

    location / {
        # 跨域设置
        add_header Access-Control-Allow-Origin *;  # 允许所有域名访问，你也可以指定具体域名
        add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS';  # 允许的 HTTP 方法
        add_header Access-Control-Allow-Headers 'Origin, X-Requested-With, Content-Type, Accept, Authorization';  # 允许的请求头

        # 处理 OPTIONS 请求，预检请求
        if ($request_method = 'OPTIONS') {
            add_header Access-Control-Allow-Origin *;
            add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS';
            add_header Access-Control-Allow-Headers 'Origin, X-Requested-With, Content-Type, Accept, Authorization';
            add_header Access-Control-Max-Age 1728000;
            add_header Content-Type 'text/plain charset=UTF-8';
            add_header Content-Length 0;
            return 204;
        }

        # 原代理设置
        proxy_pass http://127.0.0.1:3003;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header REMOTE-HOST $remote_addr;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $http_connection;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        add_header X-Cache $upstream_cache_status;
        add_header Cache-Control no-cache;
        proxy_ssl_server_name off;
        proxy_ssl_name $proxy_host;
        add_header Strict-Transport-Security "max-age=31536000";
    }

下面是原代理设置，可以仅仅复制上面部分内容，下面保持不变，注意端口不要出问题。

## 前端实现

由于该项目利用了MetingJS和APlayer，所以请提前引入这两个包，Hexo-theme-butterfly中虽然有内置的两个包，仅需修改配置文件即可开启，但是版本比较老，这里我建议自行引入最新版本，在配置中引入以下文件，注意css和js应该是分开引入的：

    1
    2
    3
    <link rel="stylesheet" href="https://fastly.jsdelivr.net/npm/aplayer/dist/APlayer.min.css" media="all" onload="this.media="all"">
    <script src="https://fastly.jsdelivr.net/npm/aplayer/dist/APlayer.min.js"></script>
    <script src="https://fastly.jsdelivr.net/npm/meting/dist/Meting.min.js"></script>

新建页面shuoshuo，在文件内写入一下内容：

    1
    2
    3
    4
    5
    6
    7
    ---
    title: 日常哔哔，键盘侠的日常吐槽
    aside: false
    ---
    <div id="talk"></div>
    <div class="limit">- 只展示最近30条说说 -</div>
    <script src="/js/shuoshuo.js" no-pjax></script>

其中的JS文件地址清自行修改，放在主题目录 `/script/`目录，自行创建，并写入以下内容：

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    function renderTalks() {
        const talkContainer = document.querySelector('#talk');
        const domain = 'https://mm.biss.click';
        if (!talkContainer) return;
        talkContainer.innerHTML = '';
        const generateIconSVG = () => {
            return `<svg viewBox="0 0 512 512"xmlns="http://www.w3.org/2000/svg"class="is-badge icon"><path d="m512 268c0 17.9-4.3 34.5-12.9 49.7s-20.1 27.1-34.6 35.4c.4 2.7.6 6.9.6 12.6 0 27.1-9.1 50.1-27.1 69.1-18.1 19.1-39.9 28.6-65.4 28.6-11.4 0-22.3-2.1-32.6-6.3-8 16.4-19.5 29.6-34.6 39.7-15 10.2-31.5 15.2-49.4 15.2-18.3 0-34.9-4.9-49.7-14.9-14.9-9.9-26.3-23.2-34.3-40-10.3 4.2-21.1 6.3-32.6 6.3-25.5 0-47.4-9.5-65.7-28.6-18.3-19-27.4-42.1-27.4-69.1 0-3 .4-7.2 1.1-12.6-14.5-8.4-26-20.2-34.6-35.4-8.5-15.2-12.8-31.8-12.8-49.7 0-19 4.8-36.5 14.3-52.3s22.3-27.5 38.3-35.1c-4.2-11.4-6.3-22.9-6.3-34.3 0-27 9.1-50.1 27.4-69.1s40.2-28.6 65.7-28.6c11.4 0 22.3 2.1 32.6 6.3 8-16.4 19.5-29.6 34.6-39.7 15-10.1 31.5-15.2 49.4-15.2s34.4 5.1 49.4 15.1c15 10.1 26.6 23.3 34.6 39.7 10.3-4.2 21.1-6.3 32.6-6.3 25.5 0 47.3 9.5 65.4 28.6s27.1 42.1 27.1 69.1c0 12.6-1.9 24-5.7 34.3 16 7.6 28.8 19.3 38.3 35.1 9.5 15.9 14.3 33.4 14.3 52.4zm-266.9 77.1 105.7-158.3c2.7-4.2 3.5-8.8 2.6-13.7-1-4.9-3.5-8.8-7.7-11.4-4.2-2.7-8.8-3.6-13.7-2.9-5 .8-9 3.2-12 7.4l-93.1 140-42.9-42.8c-3.8-3.8-8.2-5.6-13.1-5.4-5 .2-9.3 2-13.1 5.4-3.4 3.4-5.1 7.7-5.1 12.9 0 5.1 1.7 9.4 5.1 12.9l58.9 58.9 2.9 2.3c3.4 2.3 6.9 3.4 10.3 3.4 6.7-.1 11.8-2.9 15.2-8.7z"fill="#1da1f2"></path></svg>`;
        }
        const waterfall = (a) => {
            function b(a, b) {
                var c = window.getComputedStyle(b);
                return parseFloat(c["margin" + a]) || 0
            }

            function c(a) {
                return a + "px"
            }

            function d(a) {
                return parseFloat(a.style.top)
            }

            function e(a) {
                return parseFloat(a.style.left)
            }

            function f(a) {
                return a.clientWidth
            }

            function g(a) {
                return a.clientHeight
            }

            function h(a) {
                return d(a) + g(a) + b("Bottom", a)
            }

            function i(a) {
                return e(a) + f(a) + b("Right", a)
            }

            function j(a) {
                a = a.sort(function (a, b) {
                    return h(a) === h(b) ? e(b) - e(a) : h(b) - h(a)
                })
            }

            function k(b) {
                f(a) != t && (b.target.removeEventListener(b.type, arguments.callee), waterfall(a))
            }
            "string" == typeof a && (a = document.querySelector(a));
            var l = [].map.call(a.children, function (a) {
                return a.style.position = "absolute", a
            });
            a.style.position = "relative";
            var m = [];
            l.length && (l[0].style.top = "0px", l[0].style.left = c(b("Left", l[0])), m.push(l[0]));
            for (var n = 1; n < l.length; n++) {
                var o = l[n - 1],
                    p = l[n],
                    q = i(o) + f(p) <= f(a);
                if (!q) break;
                p.style.top = o.style.top, p.style.left = c(i(o) + b("Left", p)), m.push(p)
            }
            for (; n < l.length; n++) {
                j(m);
                var p = l[n],
                    r = m.pop();
                p.style.top = c(h(r) + b("Top", p)), p.style.left = c(e(r)), m.push(p)
            }
            j(m);
            var s = m[0];
            a.style.height = c(h(s) + b("Bottom", s));
            var t = f(a);
            window.addEventListener ? window.addEventListener("resize", k) : document.body.onresize = k
        };

        const fetchAndRenderTalks = () => {
            const url = 'https://mm.biss.click/api/memo/list';
            const cacheKey = 'talksCache';
            const cacheTimeKey = 'talksCacheTime';
            const cacheDuration = 30 * 60 * 1000; // 半个小时 (30 分钟)

            const cachedData = localStorage.getItem(cacheKey);
            const cachedTime = localStorage.getItem(cacheTimeKey);
            const currentTime = new Date().getTime();

            // 判断缓存是否有效
            if (cachedData && cachedTime && (currentTime - cachedTime < cacheDuration)) {
                const data = JSON.parse(cachedData);
                renderTalks(data); // 使用缓存渲染数据
            } else {
                if (talkContainer) {
                    talkContainer.innerHTML = '';
                    fetch(url, {
                            method: 'POST',
                            headers: {
                                'Content-Type': 'application/json',
                            },
                            body: JSON.stringify({
                                size: 30
                            })
                        })
                        .then(res => res.json())
                        .then(data => {
                            if (data.code === 0 && data.data && Array.isArray(data.data.list)) {
                                // 缓存数据
                                localStorage.setItem(cacheKey, JSON.stringify(data.data.list));
                                localStorage.setItem(cacheTimeKey, currentTime.toString());
                                renderTalks(data.data.list); // 渲染数据
                            }
                        })
                        .catch(error => {
                            console.error('Error fetching data:', error);
                        });
                }
            }

            // 渲染函数
            function renderTalks(list) {
                // 确保 data 是一个数组
                if (Array.isArray(list)) {
                    let items = list.map(item => formatTalk(item, url));
                    items.forEach(item => talkContainer.appendChild(generateTalkElement(item)));
                    waterfall('#talk');
                } else {
                    console.error('Data is not an array:', list);
                }
            }
        };

        const formatTalk = (item, url) => {
            let date = formatTime(new Date(item.createdAt).toString());
            let content = item.content;
            let imgs = item.imgs ? item.imgs.split(',') : [];
            let text = content;
            content = text.replace(/\[(.*?)\]\((.*?)\)/g, `<a href="$2">@$1</a>`)
                .replace(/- \[ \]/g, '⚪')
                .replace(/- \[x\]/g, '⚫');
            // 保留换行符，转换 \n 为 <br>
    		content = content.replace(/\n/g, '<br>');
            // 将content用一个类包裹，便于后续处理
            content = `<div class="talk_content_text">${content}</div>`;
            if (imgs.length > 0) {
                const imgDiv = document.createElement('div');
                imgDiv.className = 'zone_imgbox';
                imgs.forEach(e => {
                    const imgLink = document.createElement('a');
                    const imgUrl = domain + e;
                    imgLink.href = imgUrl;
                    imgLink.setAttribute('data-fancybox', 'gallery');
                    imgLink.className = 'fancybox';
                    imgLink.setAttribute('data-thumb', e);
                    const imgTag = document.createElement('img');
                    imgTag.src = domain + e;
                    imgLink.appendChild(imgTag);
                    imgDiv.appendChild(imgLink);
                });
                content += imgDiv.outerHTML;
            }

            // 外链分享功能
            if (item.externalUrl) {
                const externalUrl = item.externalUrl;
                const externalTitle = item.externalTitle;
                const externalFavicon = item.externalFavicon;

                const externalContainer = `
                <div class="shuoshuo-external-link">
                    <a class="external-link" href="${externalUrl}" target="_blank" rel="external nofollow noopener noreferrer">
                        <div class="external-link-left" style="background-image: url(${externalFavicon})"></div>
                        <div class="external-link-right">
                            <div class="external-link-title">${externalTitle}</div>
                            <div>点击跳转<i class="fa-solid fa-angle-right"></i></div>
                        </div>
                    </a>
                </div>`;

                content += externalContainer;
            }

            const ext = JSON.parse(item.ext || '{}');

            if (ext.music && ext.music.id) {
                const music = ext.music;
                const musicUrl = music.api.replace(':server', music.server)
                    .replace(':type', music.type)
                    .replace(':id', music.id);
                content += `
                <meting-js server="${music.server}" type="${music.type}" id="${music.id}" api="${music.api}"></meting-js>
            `;
            }

            if (ext.doubanMovie && ext.doubanMovie.id) {
                const doubanMovie = ext.doubanMovie;
                const doubanMovieUrl = doubanMovie.url;
                const doubanTitle = doubanMovie.title;
                // const doubanDesc = doubanMovie.desc || '暂无描述';
                const doubanImage = doubanMovie.image;
                const doubanDirector = doubanMovie.director || '未知导演';
                const doubanRating = doubanMovie.rating || '暂无评分';
                // const doubanReleaseDate = doubanMovie.releaseDate || '未知上映时间';
                // const doubanActors = doubanMovie.actors || '未知演员';
                const doubanRuntime = doubanMovie.runtime || '未知时长';

                content += `
                    <a class="douban-card" href="${doubanMovieUrl}" target="_blank">
                        <div class="douban-card-bgimg" style="background-image: url('${doubanImage}');"></div>
                        <div class="douban-card-left">
                            <div class="douban-card-img" style="background-image: url('${doubanImage}');"></div>
                        </div>
                        <div class="douban-card-right">
                            <div class="douban-card-item"><span>电影名: </span><strong>${doubanTitle}</strong></div>
                            <div class="douban-card-item"><span>导演: </span><span>${doubanDirector}</span></div>
                            <div class="douban-card-item"><span>评分: </span><span>${doubanRating}</span></div>
                            <div class="douban-card-item"><span>时长: </span><span>${doubanRuntime}</span></div>
                        </div>
                    </a>
                `;
            }

            if (ext.doubanBook && ext.doubanBook.id) {
                const doubanBook = ext.doubanBook;
                const bookUrl = doubanBook.url;
                const bookTitle = doubanBook.title;
                // const bookDesc = doubanBook.desc;
                const bookImage = doubanBook.image;
                const bookAuthor = doubanBook.author;
                const bookRating = doubanBook.rating;
                const bookPubDate = doubanBook.pubDate;

                const bookTemplate = `
                    <a class="douban-card" href="${bookUrl}" target="_blank">
                        <div class="douban-card-bgimg" style="background-image: url('${bookImage}');"></div>
                            <div class="douban-card-left">
                                <div class="douban-card-img" style="background-image: url('${bookImage}');"></div>
                            </div>
                            <div class="douban-card-right">
                                <div class="douban-card-item">
                                    <span>书名: </span><strong>${bookTitle}</strong>
                                </div>
                                <div class="douban-card-item">
                                    <span>作者: </span><span>${bookAuthor}</span>
                                </div>
                                <div class="douban-card-item">
                                    <span>出版年份: </span><span>${bookPubDate}</span>
                                </div>
                                <div class="douban-card-item">
                                    <span>评分: </span><span>${bookRating}</span>
                                </div>
                            </div>
                    </a>
                `;

                content += bookTemplate;
            }

            if (ext.video && ext.video.type) {
                const videoType = ext.video.type;
                const videoUrl = ext.video.value;
                if (videoType === 'bilibili') {
                    // Bilibili 视频模板
                    // 从形如https://www.bilibili.com/video/BV1VGAPeAEMQ/?vd_source=91b3158d27d98ff41f842508c3794a13 的链接中提取视频 BV1VGAPeAEMQ
                    const biliTemplate = `
                    <div style="position: relative; padding: 30% 45%; margin-top: 10px;">
                        <iframe
                            style="position: absolute; width: 100%; height: 100%; left: 0; top: 0; border-radius: 12px;"
                            src="${videoUrl}&autoplay=0"
                            scrolling="no"
                            frameborder="no"
                            allowfullscreen>
                        </iframe>
                    </div>
                `;
                    // 将模板插入到 DOM 中
                    content += biliTemplate;

                } else if (videoType === 'youtube') {
                    // YouTube 视频模板
                    // 从形如https://youtu.be/2V6lvCUPT8I?si=DVhUas6l6qlAr6Ru的链接中提取视频 ID2V6lvCUPT8I
                    const youtubeTemplate = `
                    <div style="position: relative; padding: 30% 45%; margin-top: 10px;">
                        <iframe width="100%"
                            style="position: absolute; width: 100%; height: 100%; left: 0; top: 0; border-radius: 12px;"
                            src="${videoUrl}"
                            title="YouTube video player"
                            frameborder="0"
                            allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
                            referrerpolicy="strict-origin-when-cross-origin"
                            allowfullscreen>
                        </iframe>
                    </div>
                `;
                    // 将模板插入到 DOM 中
                    content += youtubeTemplate;
                }
            }

            return {
                content: content,
                user: item.user.nickname || '匿名',
                avatar: item.user.avatarUrl || 'https://p.liiiu.cn/i/2024/03/29/66061417537af.png',
                date: date,
                location: item.location || '山西',
                tags: item.tags ? item.tags.split(',').filter(tag => tag.trim() !== '') : ['无标签'],
                text: content.replace(/\[(.*?)\]\((.*?)\)/g, '[链接]' + `${imgs.length ? '[图片]' : ''}`)
            };
        };

        const generateTalkElement = (item) => {
            const talkItem = document.createElement('div');
            talkItem.className = 'talk_item';

            const talkMeta = document.createElement('div');
            talkMeta.className = 'talk_meta';

            const avatar = document.createElement('img');
            avatar.className = 'no-lightbox avatar';
            avatar.src = item.avatar;

            const info = document.createElement('div');
            info.className = 'info';

            const talkNick = document.createElement('span');
            talkNick.className = 'talk_nick';
            talkNick.innerHTML = `${item.user} ${generateIconSVG()}`;

            const talkDate = document.createElement('span');
            talkDate.className = 'talk_date';
            talkDate.textContent = item.date;

            const talkContent = document.createElement('div');
            talkContent.className = 'talk_content';
            talkContent.innerHTML = item.content;

            const talkBottom = document.createElement('div');
            talkBottom.className = 'talk_bottom';

            const TagContainer = document.createElement('div');

            const talkTag = document.createElement('span');
            talkTag.className = 'talk_tag';
            talkTag.textContent = `🏷️${item.tags}`;

            const locationTag = document.createElement('span');
            locationTag.className = 'location_tag';
            locationTag.textContent = `🌍${item.location}`;

            TagContainer.appendChild(talkTag);
            TagContainer.appendChild(locationTag);

            const commentLink = document.createElement('a');
            commentLink.href = 'javascript:;';
            commentLink.onclick = () => goComment(item.text);
            const commentIcon = document.createElement('span');
            commentIcon.className = 'icon';
            const commentIconInner = document.createElement('i');
            commentIconInner.className = 'fa-solid fa-message fa-fw';
            commentIcon.appendChild(commentIconInner);
            commentLink.appendChild(commentIcon);

            talkMeta.appendChild(avatar);
            info.appendChild(talkNick);
            info.appendChild(talkDate);
            talkMeta.appendChild(info);
            talkItem.appendChild(talkMeta);
            talkItem.appendChild(talkContent);
            talkBottom.appendChild(TagContainer);
            talkBottom.appendChild(commentLink);
            talkItem.appendChild(talkBottom);

            return talkItem;
        };

        const goComment = (e) => {
            const match = e.match(/<div class="talk_content_text">([\s\S]*?)<\/div>/);
            const textContent = match ? match[1] : "";
            const n = document.querySelector(".atk-textarea");
            n.value = `> ${textContent}\n\n`;
            n.focus();
            btf.snackbarShow("已为您引用该说说，不删除空格效果更佳");
            // const n = document.querySelector(".atk-textarea");
            // n.value = `> ${e}\n\n`;
            // n.focus();
            // btf.snackbarShow("已为您引用该说说，不删除空格效果更佳");
        };

        const formatTime = (time) => {
            const d = new Date(time);
            const ls = [
                d.getFullYear(),
                d.getMonth() + 1,
                d.getDate(),
                d.getHours(),
                d.getMinutes(),
                d.getSeconds(),
            ];
            const r = ls.map((a) => (a.toString().length === 1 ? '0' + a : a));
            return `${r[0]}-${r[1]}-${r[2]} ${r[3]}:${r[4]}`;
        };

        fetchAndRenderTalks();
    }

    renderTalks();

    // function whenDOMReady() {
    //     const talkContainer = document.querySelector('#talk');
    //     talkContainer.innerHTML = '';
    //     fetchAndRenderTalks();
    // }
    // whenDOMReady();
    // document.addEventListener("pjax:complete", whenDOMReady);

自行修改js文件中的Moments地址为你的地址，在文件中，有一个gocomment函数，实现的是获取卡片中的文本内容，如果如果出现不匹配的情况，请自行修改一下类名，这里我匹配的是artalk的输入框。在这里我把代码做了修改，因为我不想使用s3，结果导致说说图片无法加载。。。
修改历程：日后再写

然后引入样式文件，这个文件可以在配置文件中引用，也可以在页面文件中类似于shuoshuo.js一样引用，样式内容如下：

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    :root {
        --liushen-card-bg: #fff;
        --liushen-card-border: 1px solid #e3e8f7;
        --card-box-shadow: 0 3px 8px 6px rgba(7,17,27,0.09);
        --card-hover-box-shadow: 0 3px 8px 6px rgba(7,17,27,0.2);
        --liushen-card-secondbg: #f1f3f8;
        --liushen-button-hover-bg: #2679cc;
        --liushen-text: #4c4948;
        --liushen-button-bg: #f1f3f8;
        --liushen-fancybox-bg: rgba(255,255,255,0.5);
    }

    :root, [data-theme=dark] {
        --liushen-card-bg: #181818;
        --liushen-card-secondbg: #30343f;
        --liushen-card-border: 1px solid #42444a;
        --card-box-shadow: 0 3px 8px 6px rgba(7,17,27,0.09);
        --card-hover-box-shadow: 0 3px 8px 6px rgba(7,17,27,0.2);
        --liushen-button-bg: #30343f;
        --liushen-button-hover-bg: #2679cc;
        --liushen-text: rgba(255,255,255,0.702);
        --liushen-fancybox-bg: rgba(0,0,0,0.5);
    }

    /* 卡片初始化 */
    #talk .talk_item {
        width: calc(33.333% - 6px);
        background: var(--liushen-card-bg);
        border: var(--liushen-card-border);
        box-shadow: var(--card-box-shadow);
        transition: box-shadow .3s ease-in-out;
        border-radius: 12px;
        display: flex;
        flex-direction: column;
        padding: 20px;
        margin-bottom: 9px;
        margin-right: 9px;
    }
    #talk .talk_item:hover {
        box-shadow: var(--card-hover-box-shadow);
    }

    @media (max-width: 900px) {
        #talk .talk_item {
          width: calc(50% - 5px);
        }
    }
    @media (max-width: 450px) {
        #talk .talk_item {
          width: calc(100%);
        }
    }

    #talk{
        position: relative;
        width: 100%;
        box-sizing: border-box;
    }

    #talk .talk_meta .avatar {
        margin: 0 !important;
        width: 60px;
        height: 60px;
        border-radius: 12px;
    }
    #talk .talk_bottom,
    #talk .talk_meta {
        display: flex;
        align-items: center;
    }
    #talk .talk_meta {
        display: flex;
        align-items: center;
        width: 100%;
        padding-bottom: 10px;
        border-bottom: 1px dashed grey; /* 添加灰色虚线边框 */
    }
    #talk .talk_bottom {
        margin-top: 15px;
        padding-top: 10px;
        border-top: 1px dashed grey; /* 添加灰色虚线边框 */
        justify-content: space-between;
    }
    #talk .talk_meta .info {
        display: flex;
        flex-direction: column;
        margin-left: 10px;
    }
    #talk .talk_meta .info .talk_nick {
        color: #6dbdc3;
        font-size: 1.2rem;
    }
    #talk .talk_meta .info svg.is-badge.icon {
        width: 15px;
        padding-top: 3px;
    }
    #talk .talk_meta .info span.talk_date {
        opacity: .6;
    }
    #talk .talk_item .talk_content {
        margin-top: 10px;
    }
    #talk .talk_item .talk_content .zone_imgbox {
        display: flex;
        flex-wrap: wrap;
        --w: calc(25% - 8px);
        gap: 10px;
        margin-top: 10px;
    }
    #talk .talk_item .talk_content .zone_imgbox a {
        display: block;
        border-radius: 12px;
        width: var(--w);
        aspect-ratio: 1/1;
        position: relative;
    }
    #talk .talk_item .talk_content .zone_imgbox a:first-child {
        width: 100%;
        aspect-ratio: 1.8;
    }
    #talk .talk_item .talk_content .zone_imgbox img {
        border-radius: 10px;
        width: 100%;
        height: 100%;
        margin: 0 !important;
        object-fit: cover;
    }
    /* 底部 */
    #talk .talk_item .talk_bottom {
        opacity: .9;
    }
    #talk .talk_item .talk_bottom .icon {
        float: right;
        transition: all .3s;
    }
    #talk .talk_item .talk_bottom .icon:hover {
        color: #49b1f5;
    }
    #talk .talk_item .talk_bottom span.talk_tag,
    #talk .talk_item .talk_bottom span.location_tag {
        font-size: 14px;
        background-color: var(--liushen-card-secondbg);
        border-radius: 12px;
        padding: 3px 15px 3px 10px;
        transition: box-shadow 0.3s ease;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }

    #talk .talk_item .talk_bottom span.location_tag {
        margin-left: 5px;
    }

    #talk .talk_item .talk_bottom span.talk_tag:hover,
    #talk .talk_item .talk_bottom span.location_tag:hover {
        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
    }
    #talk .talk_item .talk_content>a {
        margin: 0 3px;
        color: #ff7d73 !important;
    }
    #talk .talk_item .talk_content>a:hover{
        text-decoration: none !important;
        color: #ff5143 !important
    }

    @media screen and (max-width: 900px) {
        #talk .talk_item .talk_content .zone_imgbox {
            --w: calc(33% - 5px);
        }
        #talk .talk_item #post-comment{
            margin: 0 3px
        }
    }
    @media screen and (max-width: 768px) {
        .zone_imgbox {
            gap: 6px;
        }
        .zone_imgbox {
            --w: calc(50% - 3px);
        }
        span.talk_date {
            font-size: 14px;
        }
    }

    #talk .talk_item .talk_content .douban-card {
        margin-top: 10px !important;
        text-decoration: none;
        align-items: center;
        border-radius: 12px;
        color: #faebd7;
        display: flex;
        justify-content: center;
        margin: 10px;
        max-width: 400px;
        overflow: hidden;
        padding: 15px;
        position: relative;
    }

    .douban-card .douban-card-bgimg {
        background-position: 50%;
        background-repeat: no-repeat;
        background-size: 100%;
        filter: blur(15px) brightness(.6);
        height: 115%;
        position: absolute;
        width: 115%;
    }

    .douban-card .douban-card-left {
        align-items: center;
        display: flex;
        flex-direction: column;
        position: relative;
    }

    .douban-card .douban-card-left .douban-card-img {
        transition: all .5s ease;
        height: 130px;
        position: relative;
        width: 80px;
        background-position: 50%;
        background-repeat: no-repeat;
        background-size: 100%;
    }

    .douban-card .douban-card-left:hover .douban-card-img {
        filter: blur(5px) brightness(.6);
        transform: perspective(800px) rotateX(180deg);
    }

    .douban-card .douban-card-right {
        color: #faebd7;
        display: flex;
        flex-direction: column;
        font-size: 14px;
        line-height: 1.5;
        margin-left: 12px;
        position: relative;
    }

    .douban-card .douban-card-right .douban-card-item  {
        margin-top: 4px;
        max-width: 95%;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* 外链卡片 */
    #talk .talk_item .talk_content .shuoshuo-external-link {
        /* 无下划线 */
        width: 100%;
        height: 80px;
        margin-top: 10px;
        border-radius: 12px;
        background-color: var(--liushen-card-secondbg);
        color: var(--liushen-card-text);
        border: var(--liushen-card-border);
        transition: background-color .3s ease-in-out;
    }

    .shuoshuo-external-link:hover {
        background-color: var(--liushen-button-hover-bg);
    }

    .shuoshuo-external-link .external-link {
        display: flex;
        color: var(--liushen-text) !important;
        width: 100%;
        height: 100%;
    }

    .shuoshuo-external-link .external-link:hover {
        color: white !important;
    }

    .shuoshuo-external-link .external-link:hover {
        text-decoration: none !important;
    }

    .shuoshuo-external-link .external-link-left {
        width: 60px;
        height: 60px;
        margin: 10px;
        border-radius: 12px;
        background-size: cover;
        background-position: center;
    }

    .shuoshuo-external-link .external-link-right {
        display: flex;
        flex-direction: column;
        justify-content: center;
        width: calc(100% - 80px);
        padding: 10px;
    }

    .shuoshuo-external-link .external-link-right .external-link-title {
        font-size: 1.0rem;
        font-weight: 800;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    .shuoshuo-external-link .external-link-right i {
        margin-left: 5px;
    }

    .limit {
        width: 100%;
        text-align: center;
        margin-top: 30px;
    }

如果一切正常，应该就可以显示了，但是我在使用时发现卡片是黑色的，把前面的root内容删除之后就好了。

# 参考内容

🪧引用站外地址，不保证站点的可用性和安全性
liushen的博客
liushen

当然还有chatgpt的支持

文章作者: biss
文章链接: https://blog.biss.click/posts/ad244066/
版权声明: 本博客所有文章除特别声明外，均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Bi's Blog！
网站
相关推荐
2025-08-02
部署历程
前提条件在开始之前，请确保你已满足以下条件： 服务器: 安装了 1Panel 面板的 Linux 服务器 拥有服务器的 SSH 访问权限（本文示例将使用 root 用户，你可以根据实际情况替换为其他有权限的用户）。 服务器上已安装 Git。 1Panel 配置: 已通过 1Panel 创建了一个网站（例如 blog.yourdomain.com），并记下该网站的 根目录（Document Root）。通常在 1Panel 中，路径类似于 /opt/1panel/apps/openresty/openresty/www/sites/你的网站目录/index。请务必替换为你的实际路径。本地环境: 你的电脑上已安装并配置好 Hexo 博客环境。 你的电脑上已安装 Git。 已安装 Hexo Git 部署插件： 1npm install hexo-deployer-git --save SSH 密钥认证 (关键): 你需要在本地电脑生成 SSH 密钥对（如果还没有的话）。 必须将你的本地电脑的 SSH 公钥 添加到服务器上对应用户（如 root）的 ~/.ssh...
2025-08-12
博客添加AI总结
之前在wordpress中看到过ai插件，现在使用hexo好像有洪墨AI，但是收费，有点负担不了，于是寻找代替品，真的找到了 🪧引用站外地址，不保证站点的可用性和安全性 liushen开发的插件 ai-summary 首先，安装插件： 1npm install hexo-ai-summary-liushen --save 安装额外插件： 1npm install axios p-limit node-fetch --save 安装后，在Hexo配置文件 _config.yml任意位置添加以下配置： 1234567891011121314151617181920212223242526# hexo-ai-summary-liushen# docs on : https://github.com/willow-god/hexo-ai-summaryaisummary: # 基本控制 enable: true ...
2025-08-10
在侧边栏中添加欢迎信息
效果展示 开始折腾申请腾讯地图的api key 🪧引用站外地址，不保证站点的可用性和安全性 腾讯地图 Tencent 自行查看文档,要注意在填写信任域名时不填 https:// 添加js在主题 /source/js/目录下添加 txmap.js 有一个更完善的js脚本 🪧引用站外地址，不保证站点的可用性和安全性 txmap.js github 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858...
2026-02-10
添加网站左上角菜单
看到柳神的网站有这种菜单，但是没有写魔改教程，只好自己慢慢摸索了。 🪧引用站外地址，不保证站点的可用性和安全性 liushen liushen 预计做好后是这种效果： 首先，修改\themes\butterfly\layout\includes\header\nav.pug: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051nav#nav //- 左侧区域：包含指纹菜单和网站名 span#blog-info #ls-menu-container i.fas.fa-fingerprint #ls-menu-panel .ls-section .ls-title 😀 个人网站 .ls-grid ...
2025-08-14
自定义页脚（新）
添加CSS修改有风险，注意备份 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196...
2026-01-16
在侧边栏添加日历和倒计时
突然看到某个网站侧边栏有日历和倒计时，就研究了一下，抄下来了（🤭）效果图： 添加js123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111document.addEventListener("DOMContentLoaded", () => { initializeCard();});document.addEventListener("pjax:complete", () => { initializeCard();});function initializeCard() ...

评论
biss
文章
34
标签
11
分类
5
Follow Me
公告
系列文章
添加网站左上角菜单 2026-02-10
添加typesense搜索 2026-02-05
自定义字体 2026-01-16
在侧边栏添加日历和倒计时 2026-01-16
使用GitHub推送Hexo到服务器 2025-08-18
自定义页脚（新）2025-08-14
自定义分类条 2025-08-14
自定义导航栏 2025-08-13
利用 SiteMap 随机访问站内页面 2025-08-12
博客添加AI总结 2025-08-12
利用插件自定义页脚菜单 2025-08-12
自定义右键菜单 2025-08-11
在侧边栏中添加欢迎信息 2025-08-10
配置说说页面 2025-08-10
最新文章
opencv基础操作 2026-03-15
配置opencv 2026-03-14
自建bitwarden服务 2026-02-25
自建renovate-bot 2026-02-23
新年快乐！2026-02-14
随便逛逛昼夜切换繁简转换阅读模式隐私声明 Cookie协议版权声明