跳过正文
Background Image

Memos 配置 Artalk 评论系统

·3742 字·8 分钟·
作者
大大的小蜗牛
机会总是垂青于有准备的人!
目录
memos - 这篇文章属于一个选集。
§ 6: 本文

更新:2023.10.20 此脚本只适配到 Memos v0.14.0

TODO:

在木木老师《 Memos x Twikoo》和拾月老师《 单页 Memos 添加 Artalk 评论,无限接近微博》的启发之下。
Memos 嘀咕页面Memos 应用 添加了 Artalk 评论系统。

自定义样式
#

登录 Memos 应用后台,在 设置 系统 自定义样式 中添加 CSS 代码:

a.time-text:after { content: ' 评论 💬 '; }
.atk-main-editor { margin-top: 20px; }
.dark .artalk{
  --at-color-font: #fff;
  --at-color-deep: #e7e7e7;
  --at-color-sub: #e7e7e7;
  --at-color-grey: #fff;
  --at-color-meta: #fff;
  --at-color-border: #2d3235;
  --at-color-light: #687a86;
  --at-color-bg: #1e2224;
  --at-color-bg-transl: rgba(30, 34, 36, .95);
  --at-color-bg-grey: #46494e;
  --at-color-bg-grey-transl: rgba(8, 8, 8, .95);
  --at-color-bg-light: rgba(29, 161, 242, .1);
  --at-color-main: #0083ff;
  --at-color-red: #ff5652;
  --at-color-pink: #fa5a57;
  --at-color-yellow: #ff7c37;
  --at-color-green: #4caf50;
  --at-color-gradient: linear-gradient(180deg, transparent, rgba(30, 34, 36, 1))
}

自定义脚本
#

登录 Memos 应用后台,在 设置 系统 自定义脚本 中添加 CSS 代码:

// Artalk comments
// 用 JS 向页面中插入 JS
function addArtalkJS() { 
    var memosArtalk = document.createElement("script");
    memosArtalk.src = `https://cdn.staticfile.org/artalk/2.5.5/Artalk.min.js`;
    var artakPos = document.getElementsByTagName("script")[0];
    artakPos.parentNode.insertBefore(memosArtalk, artakPos);
};
// div
function startArtalk() {
    start = setInterval(function(){
        var artalkDom = document.getElementById('Comments') || '';
        var memoAt = document.querySelector('.memo-wrapper') || '';
        var memoLoading = document.querySelector('.action-button-container') || '';
        var memoLoadingA = document.querySelector('.action-button-container a') || '';
        if(window.location.href.replace(/^.*\/(m)\/.*$/,'$1') == "m" && memoLoadingA){
        memoLoading.innerHTML = "评论加载中……"
        }
        if(window.location.href.replace(/^.*\/(m)\/.*$/,'$1') == "m" && !artalkDom){
            addArtalkJS()
            if(memoAt){
                clearInterval(start)
                var cssLink = document.createElement("link");
                cssLink.rel = "stylesheet";
                cssLink.href = "https://cdn.staticfile.org/artalk/2.5.5/Artalk.min.css";
                document.head.appendChild(cssLink);
                memoAt.insertAdjacentHTML('afterend', '<div id="Comments"></div>');
                setTimeout(function() {
                    Artalk.init({
                        el: '#Comments',
                        pageKey: location.pathname,
                        pageTitle: document.title,
                        server: 'https://artalk.at.your.server.com/',
                        site: 'memos',
                        darkMode: 'auto'
                    });
                    Artalk.on('list-loaded', function() {
                        // console.log('评论加载完成');
                        memoLoading.innerHTML = ''
                        startArtalk();
                    });
                }, 1000);
            }
        }
        //console.log(window.location.href);
    }, 1000)
}
startArtalk();

需要修改的内容:

Memos 独立页面配置 Artalk 评论系统
#

部署方式请参考:《 Memos API 调用渲染页面》一文。
在原来的基本上 assets/memos.js
添加 JS 代码:

function loadArtalk(memo_id) {
    const commentDiv = document.getElementById('memo_' + memo_id);
    const commentBtn = document.getElementById('btn_memo_' + memo_id);
    if (commentDiv.classList.contains('hidden')) {
        commentDiv.classList.remove('hidden');
        commentBtn.innerHTML = '收起评论<i class="fas fa-level-up-alt"></i>';
        const artalk = new Artalk({
            el: '#memo_' + memo_id,
            pageKey: '/m/' + memo_id,
            pageTitle: '',
            server: 'https://artalk.at.your.server.com/',
            site: 'memos',
            darkMode: 'auto'
        });
        } else {
        commentDiv.classList.add('hidden');
        commentBtn.innerHTML = '评论';
        }
}

并且修改下面的内容:

memoResult +=
        '<li id="' +
        data[i].id +
        '" class="timeline"><div class="talks__content"><div class="talks__text"><div class="talks__userinfo"><div>Charles Chin</div><div><svg viewBox="0 0 24 24" aria-label="认证账号" class="talks__verify"><g><path d="M22.5 12.5c0-1.58-.875-2.95-2.148-3.6.154-.435.238-.905.238-1.4 0-2.21-1.71-3.998-3.818-3.998-.47 0-.92.084-1.336.25C14.818 2.415 13.51 1.5 12 1.5s-2.816.917-3.437 2.25c-.415-.165-.866-.25-1.336-.25-2.11 0-3.818 1.79-3.818 4 0 .494.083.964.237 1.4-1.272.65-2.147 2.018-2.147 3.6 0 1.495.782 2.798 1.942 3.486-.02.17-.032.34-.032.514 0 2.21 1.708 4 3.818 4 .47 0 .92-.086 1.335-.25.62 1.334 1.926 2.25 3.437 2.25 1.512 0 2.818-.916 3.437-2.25.415.163.865.248 1.336.248 2.11 0 3.818-1.79 3.818-4 0-.174-.012-.344-.033-.513 1.158-.687 1.943-1.99 1.943-3.484zm-6.616-3.334l-4.334 6.5c-.145.217-.382.334-.625.334-.143 0-.288-.04-.416-.126l-.115-.094-2.415-2.415c-.293-.293-.293-.768 0-1.06s.768-.294 1.06 0l1.77 1.767 3.825-5.74c.23-.345.696-.436 1.04-.207.346.23.44.696.21 1.04z"></path></g></svg></div><div class="talks__id">@eallion · </div><small class="talks__date"><a href="https://memos.eallion.com/m/' +
        data[i].id +
        '" target="_blank">' +
        moment(data[i].createdTs * 1000).twitterLong() +
        "</a></small></div><p>" +
        memoContREG +
        "</p></div></div></li>";

修改为:

memoResult +=
        '<li id="' +
        memo_id +
        '" class="timeline"><div class="talks__content"><div class="talks__text"><div class="talks__userinfo"><div>Charles Chin</div><div><svg viewBox="0 0 24 24" aria-label="认证账号" class="talks__verify"><g><path d="M22.5 12.5c0-1.58-.875-2.95-2.148-3.6.154-.435.238-.905.238-1.4 0-2.21-1.71-3.998-3.818-3.998-.47 0-.92.084-1.336.25C14.818 2.415 13.51 1.5 12 1.5s-2.816.917-3.437 2.25c-.415-.165-.866-.25-1.336-.25-2.11 0-3.818 1.79-3.818 4 0 .494.083.964.237 1.4-1.272.65-2.147 2.018-2.147 3.6 0 1.495.782 2.798 1.942 3.486-.02.17-.032.34-.032.514 0 2.21 1.708 4 3.818 4 .47 0 .92-.086 1.335-.25.62 1.334 1.926 2.25 3.437 2.25 1.512 0 2.818-.916 3.437-2.25.415.163.865.248 1.336.248 2.11 0 3.818-1.79 3.818-4 0-.174-.012-.344-.033-.513 1.158-.687 1.943-1.99 1.943-3.484zm-6.616-3.334l-4.334 6.5c-.145.217-.382.334-.625.334-.143 0-.288-.04-.416-.126l-.115-.094-2.415-2.415c-.293-.293-.293-.768 0-1.06s.768-.294 1.06 0l1.77 1.767 3.825-5.74c.23-.345.696-.436 1.04-.207.346.23.44.696.21 1.04z"></path></g></svg></div><div class="talks__id">@eallion · </div><small class="talks__date"><a href="https://memos.eallion.com/m/' +
        memo_id +
        '" target="_blank">' +
        moment(data[i].createdTs * 1000).twitterLong() +
        "</a></small></div><p>" +
        memoContREG +
        "</p><div class='talks_comments'><a onclick=\"loadArtalk(\'" +
        memo_id +
        "\')\"><i class='fas fa-comment-dots fa-fw'></i><span id='btn_memo_" +
        memo_id +
        "'>评论</span> (<span id='ArtalkCount' data-page-key='/m/" +
        memo_id +
        "'>0</span>)</a></div><div id='memo_" +
        memo_id +
        "' class='artalk hidden'></div></div></li>";

并且在 HTML 文件中合适的位置引入 Artalk 的 JS 和 CSS 资源文件

<!-- CSS -->
<link href="http://artalk.at.your.server.com:8080/dist/Artalk.css" rel="stylesheet">

<!-- JS -->
<script src="http://artalk.at.your.server.com:8080/dist/Artalk.js"></script>

<!-- Artalk 评论数统计 -->
<script>
    window.onload = function() {
        Artalk.loadCountWidget({
            server: 'https://artalk.at.your.server.com/',
            site: 'memos',
            pvEl: '#ArtalkPV',
            countEl: '#ArtalkCount',
        });
    }
</script>
        document.querySelector("button.button-load").remove()
        return
    }
})

}
// 插入 html
function updateHTMl(data) {
var memoResult = “”, resultAll = “”;

// 解析 TAG 标签,添加样式
const TAG_REG = /#([^\s#]+?) /g;

// 解析 BiliBili
const BILIBILI_REG = /<a\shref="https:\/\/www\.bilibili\.com\/video\/((av[\d]{1,10})|(BV([\w]{10})))\/?">.*<\/a>/g;
// 解析网易云音乐
const NETEASE_MUSIC_REG = /<a\shref="https:\/\/music\.163\.com\/.*id=([0-9]+)".*?>.*<\/a>/g;
// 解析 QQ 音乐
const QQMUSIC_REG = /<a\shref="https\:\/\/y\.qq\.com\/.*(\/[0-9a-zA-Z]+)(\.html)?".*?>.*?<\/a>/g;
// 解析腾讯视频
const QQVIDEO_REG = /<a\shref="https:\/\/v\.qq\.com\/.*\/([a-z|A-Z|0-9]+)\.html".*?>.*<\/a>/g;
// 解析 Spotify
const SPOTIFY_REG = /<a\shref="https:\/\/open\.spotify\.com\/(track|album)\/([\s\S]+)".*?>.*<\/a>/g;
// 解析优酷视频
const YOUKU_REG = /<a\shref="https:\/\/v\.youku\.com\/.*\/id_([a-z|A-Z|0-9|==]+)\.html".*?>.*<\/a>/g;
//解析 Youtube
const YOUTUBE_REG = /<a\shref="https:\/\/www\.youtube\.com\/watch\?v\=([a-z|A-Z|0-9]{11})\".*?>.*<\/a>/g;

// Marked Options
marked.setOptions({
    breaks: true,
    smartypants: true,
    langPrefix: 'language-',
    highlight: function (code, lang) {
        const language = hljs.getLanguage(lang) ? lang : 'plaintext';
        return hljs.highlight(code, { language }).value;
    },
});

// Memos Content
for (var i = 0; i < data.length; i++) {
    var memoContREG = data[i].content
        .replace(TAG_REG, "<span class='tag-span'><a rel='noopener noreferrer' href='#'>#$1</a></span> ")

    // For CJK language users
    // 用 PanguJS 自动处理中英文混合排版
    // 在 index.html 引入 JS:<script type="text/javascript" src="assets/js/pangu.min.js?v=4.0.7"></script>
    // 把下面的 memoContREG = marked.parse(memoContREG) 改为:memoContREG = marked.parse(pangu.spacing(memoContREG))

    memoContREG = marked.parse(memoContREG)
        .replace(BILIBILI_REG, "<div class='video-wrapper'><iframe src='//player.bilibili.com/player.html?bvid=$1&as_wide=1&high_quality=1&danmaku=0' scrolling='no' border='0' frameborder='no' framespacing='0' allowfullscreen='true' style='position:absolute;height:100%;width:100%;'></iframe></div>")
        .replace(YOUTUBE_REG, "<div class='video-wrapper'><iframe src='https://www.youtube.com/embed/$1' title='YouTube video player' frameborder='0' allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture' allowfullscreen title='YouTube Video'></iframe></div>")
        .replace(NETEASE_MUSIC_REG, "<meting-js auto='https://music.163.com/#/song?id=$1'></meting-js>")
        .replace(QQMUSIC_REG, "<meting-js auto='https://y.qq.com/n/yqq/song$1.html'></meting-js>")
        .replace(QQVIDEO_REG, "<div class='video-wrapper'><iframe src='//v.qq.com/iframe/player.html?vid=$1' allowFullScreen='true' frameborder='no'></iframe></div>")
        .replace(SPOTIFY_REG, "<div class='spotify-wrapper'><iframe style='border-radius:12px' src='https://open.spotify.com/embed/$1/$2?utm_source=generator&theme=0' width='100%' frameBorder='0' allowfullscreen='' allow='autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture' loading='lazy'></iframe></div>")
        .replace(YOUKU_REG, "<div class='video-wrapper'><iframe src='https://player.youku.com/embed/$1' frameborder=0 'allowfullscreen'></iframe></div>")
        .replace(YOUTUBE_REG, "<div class='video-wrapper'><iframe src='https://www.youtube.com/embed/$1' title='YouTube video player' frameborder='0' allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture' allowfullscreen title='YouTube Video'></iframe></div>")

    // 解析内置资源文件 
    if (data[i].resourceList && data[i].resourceList.length > 0) {
        var resourceList = data[i].resourceList;
        var imgUrl = '', resUrl = '', resImgLength = 0;
        for (var j = 0; j < resourceList.length; j++) {
            var resType = resourceList[j].type.slice(0, 5);
            var resexlink = resourceList[j].externalLink;
            var resLink = ''
            if (resexlink) {
                resLink = resexlink
            } else {
                resLink = memos + 'o/r/' + resourceList[j].id + '/' + resourceList[j].filename
            }
            if (resType == 'image') {
                imgUrl += '<img loading="lazy" src="    ' + resLink + '"/>'
                resImgLength = resImgLength + 1
            }
            if (resType !== 'image') {
                resUrl += '<a target="_blank" rel="noreferrer" href="' + resLink + '">' + resourceList[j].filename + '</a>'
            }
        }
        if (imgUrl) {
            var resImgGrid = ""
            if (resImgLength !== 1) { var resImgGrid = "grid grid-" + resImgLength }
            memoContREG += '<div class="resimg ' + resImgGrid + '">' + imgUrl + '</div>'
        }
        if (resUrl) {
            memoContREG += '<p class="datasource">' + resUrl + '</p>'
        }
    }
    memoResult += '<li class="timeline"><div class="memos__content"><div class="memos__text"><div class="memos__userinfo"><div>' + memo.name + '</div><div><svg viewBox="0 0 24 24" aria-label="认证账号" class="memos__verify"><g><path d="M22.5 12.5c0-1.58-.875-2.95-2.148-3.6.154-.435.238-.905.238-1.4 0-2.21-1.71-3.998-3.818-3.998-.47 0-.92.084-1.336.25C14.818 2.415 13.51 1.5 12 1.5s-2.816.917-3.437 2.25c-.415-.165-.866-.25-1.336-.25-2.11 0-3.818 1.79-3.818 4 0 .494.083.964.237 1.4-1.272.65-2.147 2.018-2.147 3.6 0 1.495.782 2.798 1.942 3.486-.02.17-.032.34-.032.514 0 2.21 1.708 4 3.818 4 .47 0 .92-.086 1.335-.25.62 1.334 1.926 2.25 3.437 2.25 1.512 0 2.818-.916 3.437-2.25.415.163.865.248 1.336.248 2.11 0 3.818-1.79 3.818-4 0-.174-.012-.344-.033-.513 1.158-.687 1.943-1.99 1.943-3.484zm-6.616-3.334l-4.334 6.5c-.145.217-.382.334-.625.334-.143 0-.288-.04-.416-.126l-.115-.094-2.415-2.415c-.293-.293-.293-.768 0-1.06s.768-.294 1.06 0l1.77 1.767 3.825-5.74c.23-.345.696-.436 1.04-.207.346.23.44.696.21 1.04z"></path></g></svg></div><div class="memos__id">@' + memo.username + '</div></div><p>' + memoContREG + '</p></div><div class="memos__meta"><small class="memos__date">' + moment(data[i].createdTs * 1000).twitter() + ' • 来自「<a href="' + memo.host + 'm/' + data[i].id + '" target="_blank">Memos</a>」</small></div></div></li>'
}
var memoBefore = '<ul class="">'
var memoAfter = '</ul>'
resultAll = memoBefore + memoResult + memoAfter
memoDom.insertAdjacentHTML('beforeend', resultAll);
//取消这行注释解析豆瓣电影和豆瓣阅读
// fetchDB()
document.querySelector('button.button-load').textContent = '加载更多';

}
// Memos End

// 解析豆瓣 Start
// 文章内显示豆瓣条目 https://immmmm.com/post-show-douban-item/
// 解析豆瓣必须要 API,请找朋友要权限,或自己按 https://github.com/eallion/douban-api-rs 这个架设 API,非常简单,资源消耗很少
// 已内置样式,修改 API 即可使用
function fetchDB() {
var dbAPI = “ https://api.example.com/"; // 修改为自己的 API
var dbA = document.querySelectorAll(".timeline a[href*=‘douban.com/subject/’]:not([rel=‘noreferrer’])”) || ‘’;
if (dbA) {
for (var i = 0; i < dbA.length; i++) {
_this = dbA[i]
var dbHref =_this.href
var db_reg = /^https://(movie|book).douban.com/subject/([0-9]+)/?/;
var db_type = dbHref.replace(db_reg, “$1”);
var db_id = dbHref.replace(db_reg, “$2”).toString();
if (db_type == ‘movie’) {
var this_item = ‘movie’ + db_id;
var url = dbAPI + “movies/” + db_id;
if (localStorage.getItem(this_item) == null || localStorage.getItem(this_item) == ‘undefined’) {
fetch(url).then(res => res.json()).then(data => {
let fetch_item = ‘movies’ + data.sid;
let fetch_href = “ https://movie.douban.com/subject/" + data.sid + “/”
localStorage.setItem(fetch_item, JSON.stringify(data));
movieShow(fetch_href, fetch_item)
});
} else {
movieShow(dbHref, this_item)
}
} else if (db_type == ‘book’) {
var this_item = ‘book’ + db_id;
var url = dbAPI + “v2/book/id/” + db_id;
if (localStorage.getItem(this_item) == null || localStorage.getItem(this_item) == ‘undefined’) {
fetch(url).then(res => res.json()).then(data => {
let fetch_item = ‘book’ + data.id;
let fetch_href = “ https://book.douban.com/subject/" + data.id + “/”
localStorage.setItem(fetch_item, JSON.stringify(data));
bookShow(fetch_href, fetch_item)
});
} else {
bookShow(dbHref, this_item)
}
}
}// for end
}
}

function movieShow(fetch_href, fetch_item) {
var storage = localStorage.getItem(fetch_item);
var data = JSON.parse(storage);
var db_star = Math.ceil(data.rating);
var db_html = “

“” + data.name + “”

” + data.rating + “
” + data.intro.replace(/\s*/g, “”) + “

var db_div = document.createElement(“div”);
var qs_href = “.timeline a[href=’” + fetch_href + “’]”
var qs_dom = document.querySelector(qs_href)
qs_dom.parentNode.replaceChild(db_div, qs_dom);
db_div.innerHTML = db_html
}

function bookShow(fetch_href, fetch_item) {
var storage = localStorage.getItem(fetch_item);
var data = JSON.parse(storage);
var db_star = Math.ceil(data.rating.average);
var db_html = “

“” + data.title + “”

” + data.rating.average + “
” + data.summary.replace(/\s*/g, “”) + “

var db_div = document.createElement(“div”);
var qs_href = “.timeline a[href=’” + fetch_href + “’]”
var qs_dom = document.querySelector(qs_href)
qs_dom.parentNode.replaceChild(db_div, qs_dom);
db_div.innerHTML = db_html
}
// 解析豆瓣 End

// Images lightbox
window.ViewImage && ViewImage.init(’.container img’);

// Memos Total Start
// Get Memos total count
function getTotal() {
var totalUrl = memos + “api/memo/stats?creatorId=” + memo.creatorId
fetch(totalUrl).then(res => res.json()).then(resdata => {
if (resdata.data) {
var allnums = resdata.data.length
var memosCount = document.getElementById(’total’);
memosCount.innerHTML = allnums;
}
}).catch(err => {
// Do something for an error here
});
};
window.onload = getTotal();
// Memos Total End


源码在这里:<i class="fab fa-github fa-fw"></i>[memos.top](https://github.com/eallion/memos.top),可能时常会更新变动。

整体样式是自己慢慢捏出来的,大多借鉴了 Twitter 的元素。

相对时间,用的是 [Moment.js](https://github.com/moment/moment/) Twitter 风格的插件:<i class="fab fa-github fa-fw"></i>[moment.twitter.js](https://github.com/eallion/memos.top/blob/main/assets/js/moment.twitter.js)

- 7 天内的发布时间显示为相对时间:`1 天前`
- 本年内的时间不显示年份:`5  20 日,13:14  中午`
- 去年及之前的时间显示为完整时间:`2010  10  10 日,10:10  上午`

全站图片灯箱效果用的是 [view-image.js](https://tokinx.github.io/ViewImage/) 插件:<i class="fab fa-github fa-fw"></i>[view-image.min.js](https://github.com/eallion/memos.top/blob/main/assets/js/view-image.min.js)

3. CSS 参考

> 参考:<i class="fab fa-github fa-fw"></i>[assets/css/style.css](https://github.com/eallion/memos.top/blob/main/assets/css/style.css)

    // 移除之前处于活动状态的 .dvtjjf 元素
    document.querySelector(`.dvtjjf.active[data-search="${e.target.dataset.search}"]`)?.classList.remove('active');

    if (e.target.dataset.value) {
        // 将当前点击的 .dvtjjf 元素设为活动状态
        e.target.classList.add('active');
    }

    // 构建属性选择器数组
    const searchItems = document.querySelectorAll('.dvtjjf.active');
    const attributes = Array.from(searchItems, searchItem => {
        const property = `data-${searchItem.dataset.search}`;
        const logic = searchItem.dataset.method === 'contain' ? '*' : '^';
        const value = searchItem.dataset.method === 'contain' ? `${searchItem.dataset.value}` : searchItem.dataset.value;
        return `[${property}${logic}='${value}']`;
    });

    // 构建选择器字符串
    const selector = `.sorting${attributes.join('')}`;

    // 显示匹配选择器的元素
    document.querySelectorAll(selector).forEach(item => item.classList.remove('hide'));
}

window.addEventListener('click', function (e) {
    if (e.target.classList.contains('sc-gtsrHT')) {
        e.preventDefault();
        search(e);
    }
});

function sort(e) {
    const sortBy = e.target.dataset.order;
    const style = document.createElement('style');
    style.classList.add('sort-order-style');

    // 移除之前的排序样式
    document.querySelector('style.sort-order-style')?.remove();

    // 移除之前处于活动状态的 .sort-by-item 元素
    document.querySelector('.sort-by-item.active')?.classList.remove('active');

    // 将当前点击的 .sort-by-item 元素设为活动状态
    e.target.classList.add('active');

    if (sortBy === 'rating') {
        const movies = Array.from(document.querySelectorAll('.sorting'));

        // 根据评分进行排序
        movies.sort((movieA, movieB) => {
            const ratingA = parseFloat(movieA.dataset.rating) || 0;
            const ratingB = parseFloat(movieB.dataset.rating) || 0;
            if (ratingA === ratingB) {
                return 0;
            }
            return ratingA > ratingB ? -1 : 1;
        });

        // 生成排序样式表
        const stylesheet = movies.map((movie, idx) => `.sorting[data-rating="${movie.dataset.rating}"] { order: ${idx}; }`).join('\r\n');
        style.innerHTML = stylesheet;
        document.body.appendChild(style);
    } else if (sortBy === 'count') {
        const movies = Array.from(document.querySelectorAll('.sorting'));

        // 根据评分人数进行排序
        movies.sort((movieA, movieB) => {
            const countA = parseInt(movieA.dataset.count) || 0;
            const countB = parseInt(movieB.dataset.count) || 0;
            if (countA === countB) {
                return 0;
            }
            return countA > countB ? -1 : 1;
        });

        // 生成排序样式表
        const stylesheet = movies.map((movie, idx) => `.sorting[data-count="${movie.dataset.count}"] { order: ${idx}; }`).join('\r\n');
        style.innerHTML = stylesheet;
        document.body.appendChild(style);
    }
}

window.addEventListener('click', function (e) {
    if (e.target.classList.contains('sort-by-item')) {
        e.preventDefault();
        sort(e);
    }
});

8. 附加 GitHub Actions
#

GitHub Actions 处理 Json 数据的好处是不用每次都手动下载更新,而且 Access Token 可以保存在 GitHub 仓库的 Secrets Setting 里。

然后填入前面步骤得到的 Access Token

  • Name *NEODB_ACCESS_TOKEN
  • Secret *QuhZZpr111111111111111110X2OPaSRKU

下面是具体的 GitHub Actions neodb.yml 代码。不需要用到的步骤直接删除即可。

# .github/workflows/douban.yml
name: Sync NeoDB Data
on:
  schedule:
  - cron: "0 17 * * *"
#  watch:
#    types: [started]

  workflow_dispatch:

jobs:
  douban:
    name: Sync NeoDB Data
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
      uses: actions/checkout@v3

    # 检查是否安装了 JQ
    - name: Check JQ
      run: |
        if ! command -v jq &> /dev/null; then
          echo "jq is not installed. Installing..."
          sudo apt-get update
          sudo apt-get install -y jq
        else
          echo "jq is already installed."
        fi
        # 把当前目录保存到环境变量中
        echo "WORK_DIR=$(pwd)" >> $GITHUB_ENV

    # 获取本地现有文件的标记数
    - name: Get Current Count
      run: |
        CURRENT_COUNT() {
          jq '.count' data/neodb/movie.json
        }
        echo "CURRENT_COUNT=$(CURRENT_COUNT)" >> $GITHUB_ENV

    - name: Get NeoDB JSON and Count
      run: |
        curl -X 'GET' \
        'https://neodb.social/api/me/shelf/complete?category=movie&page=1' \
        -H 'accept: application/json' \
        -H 'Authorization: Bearer ${{ secrets.NEODB_ACCESS_TOKEN }}' > movie1.json

        # 获取 NeoDB 上电影的标记数
        MOVIE_COUNT() {
          jq '.count' movie1.json
        }
        echo "MOVIE_COUNT=$(MOVIE_COUNT)" >> $GITHUB_ENV

        curl -X 'GET' \
        'https://neodb.social/api/me/shelf/complete?category=tv&page=1' \
        -H 'accept: application/json' \
        -H 'Authorization: Bearer ${{ secrets.NEODB_ACCESS_TOKEN }}' > tv1.json

        # 获取 NeoDB 上电视剧的标记数
        TV_COUNT() {
          jq '.count' tv1.json
        }

        REMOTE_COUNT=$(($(MOVIE_COUNT) + $(TV_COUNT)))
        echo "REMOTE_COUNT=$REMOTE_COUNT" >> $GITHUB_ENV

    # 对比本地的标记数和远程标记数,相等就跳过,不相等就下载新数据
    - name: Count Compare
      run: |
        if [ "${{ env.REMOTE_COUNT }}" = "${{ env.CURRENT_COUNT }}" ]; then
          echo "Variables are equal. Skipping the next steps."
          exit 0
        else
          echo "Variables are not equal. Running the next steps."
        fi

    # 下载所有数据
    - name: Get All NeoDB Count
      if: ${{ env.REMOTE_COUNT != env.CURRENT_COUNT }}
      run: |
        #从 json 中提取 pages 字段的值
        pages=$(jq '.pages' movie1.json)
        tv_pages=$(jq '.pages' tv1.json)

        # 个人使用,新建 WorkDIR,排除 vercel.json 和 package.json 等
        mkdir neodb
        cd neodb

        # 下载 Movie 分类
        for ((i=1; i<=$pages; i++)); do
          url="https://neodb.social/api/me/shelf/complete?category=movie&page=$i"
          filename="movie$i.json"

        # 下载文件并保存为对应的文件名
        curl -X 'GET' "$url" \
          -H 'accept: application/json' \
          -H 'Authorization: Bearer ${{ secrets.NEODB_ACCESS_TOKEN }}' > "$filename"
        done

        # 下载 TV 分类
        for ((i=1; i<=$tv_pages; i++)); do
          tv_url="https://neodb.social/api/me/shelf/complete?category=tv&page=$i"
          tv_filename="tv$i.json"

          curl -X 'GET' "$tv_url" \
            -H 'accept: application/json' \
            -H 'Authorization: Bearer ${{ secrets.NEODB_ACCESS_TOKEN }}' > "$tv_filename"
          done

        # 把所有数据合并成一个文件
        jq -c -s '{data: map(.data[]) | unique | sort_by(.created_time) | reverse, pages: map(.pages)[0], count: map(.count)[0]}' *.json > movie.json

        # 更新 NeoDB 数据
        cp -f movie.json ${{ env.WORK_DIR }}/data/neodb/

    - name: Download NeoDB Cover
      run: |
        # 检查 movie 目录是否存在,如果不存在则创建
        if [ ! -d "movie" ]; then
          mkdir movie
        fi

        # 读取本地的 movie.json 文件内容
        json=$(cat data/neodb/movie.json)

        # 提取图片 URL
        image_urls=$(echo "$json" | jq -r '.data[].item.cover_image_url')

        # 遍历图片 URL 并下载图片
        for url in $image_urls; do
          filename=$(basename "$url")
          filepath="data/neodb/cover/$filename"
          # 检查文件是否已存在
          if [ -f "$filepath" ]; then
            echo "Skipping $filename - File already exists"
          else
            # 使用 curl 命令下载图片
            curl -o "$filepath" "$url"
            echo "Downloaded $filename"
            echo "REMOTE_COUNT=''" >> $GITHUB_ENV
          fi
        done

    # 把修改后的数据提交到 GitHub 仓库
    - name: Git Add and Commit
      if: ${{ env.REMOTE_COUNT != env.CURRENT_COUNT }}
      uses: EndBug/add-and-commit@v9
      with:
        message: 'chore(data): update neodb data'
        add: './data/neodb'

    # 调用另外的 GitHub Actions 构建 Hugo
    - name: Build Hugo and Deploy
      if: ${{ env.REMOTE_COUNT != env.CURRENT_COUNT }}
      uses: peter-evans/repository-dispatch@v2
      with:
          event-type: "Build Hugo and Deploy"

    # 把海报上传到腾讯云
    - name: Upload Cover to Tencent COS
      if: ${{ env.REMOTE_COUNT != env.CURRENT_COUNT }}
      uses: zkqiang/tencent-cos-action@v0.1.0
      with:
        args: upload -rs ./data/neodb/cover/ /images/neodb/
        secret_id: ${{ secrets.SECRET_COS_ID }}
        secret_key: ${{ secrets.SECRET_COS_KEY }}
        bucket: ${{ secrets.COS_CDN_BUCKET }}
        region: ap-shanghai
memos - 这篇文章属于一个选集。
§ 6: 本文

Click to load Giscus
or