feat: 走心评论(精选评论)+ 移动端评论框底栏布局修复
- 底栏:≤480px 压缩按钮内边距,SE 等小屏单行放下;换行时按钮组右对齐 - Worker:comments 加 is_featured;PUT /comments/:id 支持切换;新增公开接口 GET /api/v2/featured/comments(排除悄悄话/待审/已删,服务端渲染 markdown) - 前端:评论区注入「精选」按钮(回复右侧,仅管理员可见) - 关于页:走心评论板块(标题寄语通栏 + 卡片瀑布流),排在热力图上方
This commit is contained in:
1 parent
3d7d7bd396
commit
1b71fba413
10 files changed
+409
-2
No files matched your search
@@ -63,6 +63,7 @@ CREATE TABLE IF NOT EXISTS comments (
|
||||
is_pinned INTEGER NOT NULL DEFAULT 0,
|
||||
is_verified INTEGER NOT NULL DEFAULT 0,
|
||||
is_anonymous INTEGER NOT NULL DEFAULT 0, -- 匿名评论:对外显示「匿名」,真实身份只留后台
|
||||
is_featured INTEGER NOT NULL DEFAULT 0, -- 走心评论:管理员精选,关于页展示(2026-10-05 追加,存量库需 ALTER)
|
||||
vote_up INTEGER NOT NULL DEFAULT 0,
|
||||
vote_down INTEGER NOT NULL DEFAULT 0,
|
||||
ua TEXT NOT NULL DEFAULT '',
|
||||
|
||||
@@ -56,6 +56,7 @@ router.post('/comments', C.createComment);
|
||||
router.get('/comments/:id', C.getComment);
|
||||
router.put('/comments/:id', C.updateComment);
|
||||
router.del('/comments/:id', C.deleteComment);
|
||||
router.get('/featured/comments', C.listFeaturedComments);
|
||||
|
||||
// ---------------------------------------------------------------- 投票 / 浏览量
|
||||
// ★ 投票:Artalk 客户端把「方向」编进 target_name(comment_up / comment_down /
|
||||
|
||||
@@ -245,6 +245,7 @@ export async function cookComments(
|
||||
is_allow_reply: !c.is_collapsed && !c.is_pending,
|
||||
is_verified: user?.is_admin ? true : !!c.is_verified,
|
||||
is_anonymous: isAnon,
|
||||
is_featured: !!c.is_featured,
|
||||
rid: c.rid,
|
||||
badge_name: user?.badge_name ?? '',
|
||||
badge_color: user?.badge_color ?? '',
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
findOrCreatePage,
|
||||
findOrCreateSite,
|
||||
findOrCreateUser,
|
||||
findPagesByKeys,
|
||||
findUserById,
|
||||
hasCaptchaPassed,
|
||||
isCaptchaEnabled,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
import { cookComments, cookPage, pageAccessibleUrl } from '../lib/cook';
|
||||
import { mailEnabled, notifyByEmail } from '../lib/mail';
|
||||
import { isAdminByNameEmail, isAdminRequest } from '../lib/session';
|
||||
import { renderMarkdown } from '../lib/md';
|
||||
import { md5Lower } from '../lib/md5';
|
||||
import {
|
||||
fail,
|
||||
@@ -535,6 +537,70 @@ export async function createComment(ctx: Ctx): Promise<Response> {
|
||||
return ok(cooked);
|
||||
}
|
||||
|
||||
// ============================================================ GET /featured/comments
|
||||
|
||||
/**
|
||||
* 走心评论公开列表:管理员精选的评论,关于页展示用。
|
||||
*
|
||||
* 只给「精选且未删未待审」的;悄悄话评论(is_anonymous)永远不进这里 ——
|
||||
* 它的内容本来就只对博主可见,精选展示等于泄露。
|
||||
* content_html 是服务端渲染好的 markdown(与后台 content_marked 同一渲染器),
|
||||
* 前端直接 innerHTML,不用再带一份 marked。
|
||||
*/
|
||||
export async function listFeaturedComments(ctx: Ctx): Promise<Response> {
|
||||
const { env, url } = ctx;
|
||||
|
||||
const limit = Math.min(Math.max(qInt(url, 'limit', 8), 1), 20);
|
||||
|
||||
const rows = (
|
||||
await env.DB.prepare(
|
||||
`SELECT c.* FROM comments c
|
||||
WHERE c.is_featured = 1 AND c.deleted_at = 0 AND c.is_pending = 0 AND c.is_anonymous = 0
|
||||
ORDER BY c.created_at DESC LIMIT ?`,
|
||||
)
|
||||
.bind(limit)
|
||||
.all<CommentRow>()
|
||||
).results ?? [];
|
||||
|
||||
if (!rows.length) return ok({ comments: [] });
|
||||
|
||||
const cooked = await cookComments(env, rows, { ipRegion: false, includeMarked: false });
|
||||
|
||||
// 页面标题:按 (site_name, page_key) 去重后一次捞齐,避免 N+1
|
||||
const titleMap = new Map<string, string>();
|
||||
const bySite = new Map<string, string[]>();
|
||||
for (const r of rows) {
|
||||
const arr = bySite.get(r.site_name) ?? [];
|
||||
arr.push(r.page_key);
|
||||
bySite.set(r.site_name, arr);
|
||||
}
|
||||
for (const [siteName, keys] of bySite) {
|
||||
for (const [k, v] of await findPagesByKeys(env, keys, siteName)) {
|
||||
titleMap.set(`${siteName}\u0000${k}`, v.title || '');
|
||||
}
|
||||
}
|
||||
|
||||
const out = cooked.map((c, i) => ({
|
||||
id: c.id,
|
||||
nick: c.nick,
|
||||
link: c.link,
|
||||
email_encrypted: c.email_encrypted, // 前端拼 weavatar 头像(与评论区同一套)
|
||||
badge_name: c.badge_name,
|
||||
badge_color: c.badge_color,
|
||||
title_name: c.title_name,
|
||||
rank_name: c.rank_name,
|
||||
rank_color: c.rank_color,
|
||||
rank_bg: c.rank_bg,
|
||||
date: c.date,
|
||||
content_html: renderMarkdown(rows[i].content),
|
||||
page_key: c.page_key,
|
||||
page_url: c.page_url,
|
||||
page_title: titleMap.get(`${c.site_name}\u0000${c.page_key}`) || '',
|
||||
}));
|
||||
|
||||
return ok({ comments: out });
|
||||
}
|
||||
|
||||
// ============================================================ 单条操作
|
||||
|
||||
export async function getComment(ctx: Ctx): Promise<Response> {
|
||||
@@ -585,6 +651,7 @@ export async function updateComment(ctx: Ctx): Promise<Response> {
|
||||
['is_pending', 'is_pending'],
|
||||
['is_pinned', 'is_pinned'],
|
||||
['is_verified', 'is_verified'],
|
||||
['is_featured', 'is_featured'],
|
||||
] as const) {
|
||||
if (body[field] !== undefined) {
|
||||
sets.push(`${col} = ?`);
|
||||
|
||||
@@ -113,6 +113,8 @@ export interface CommentRow {
|
||||
is_pinned: number;
|
||||
is_verified: number;
|
||||
is_anonymous: number;
|
||||
/** 走心评论:管理员手动精选,在关于页展示 */
|
||||
is_featured: number;
|
||||
vote_up: number;
|
||||
vote_down: number;
|
||||
ua: string;
|
||||
@@ -142,6 +144,8 @@ export interface CookedComment {
|
||||
is_verified: boolean;
|
||||
/** 该评论是否匿名发表(管理员请求时字段原样保留,内容是否脱敏看服务端) */
|
||||
is_anonymous: boolean;
|
||||
/** 走心评论(管理员精选),前台据此渲染「精选/取消精选」按钮 */
|
||||
is_featured: boolean;
|
||||
rid: number;
|
||||
badge_name: string;
|
||||
badge_color: string;
|
||||
|
||||
@@ -4641,6 +4641,23 @@ input[type=submit] {
|
||||
.artalk > .atk-list > .atk-list-footer { bottom: auto !important; }
|
||||
.atk-main { margin-left: 34px !important; }
|
||||
.atk-bottom { padding: 5px 10px 10px 5px !important; }
|
||||
/* 底栏右侧(人机验证 + 发送按钮)换行修正:
|
||||
移动端窄屏时右侧组会 flex-wrap 到第二行,space-between 对单元素行不生效,
|
||||
整组会挤在左边、右侧留大片空白(iPhone SE / 16 Pro 实测)。
|
||||
margin-left:auto 单行时与 space-between 等价(仍靠右),换行时把整组推到右边,
|
||||
与桌面端「按钮在右下」的观感一致。 */
|
||||
.atk-main-editor > .atk-bottom > .atk-item:last-child { margin-left: auto; }
|
||||
/* 小屏(≤480,含 iPhone SE 375):底栏压进一行 —— 图标+悄悄话靠左、验证+发送靠右。
|
||||
各元素收一点内边距/间距(375px 下自然宽度 324 vs 可用 316,只差一点就溢出换行)。
|
||||
真放不下(如人机验证失败的长文案)仍按上面的规则优雅换行右对齐。 */
|
||||
@media (max-width: 480px) {
|
||||
.atk-main-editor > .atk-bottom .atk-plug-btn { padding: 0 8px; }
|
||||
.atk-main-editor > .atk-bottom .atk-plug-btn:not(:last-child) { margin-right: 3px; }
|
||||
.atk-whisper-toggle { margin-left: 6px; }
|
||||
/* 提高特异性压过 human.js 动态注入的 @media(max-width:640px) 规则 */
|
||||
.atk-main-editor .atk-human-tf { padding: 0 6px; gap: 5px; margin-right: 5px; }
|
||||
.atk-main-editor > .atk-bottom .atk-send-btn { padding: 0 10px !important; }
|
||||
}
|
||||
.atk-comment-children > .atk-comment-wrap { margin-top: 6px !important; }
|
||||
.atk-comment-children > .atk-comment-wrap > .atk-comment { padding: 0 !important; margin: auto; }
|
||||
.atk-comment .atk-height-limit:after { margin-bottom: 20px; }
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* comment-featured —— 「走心评论」精选按钮(仅管理员可见)
|
||||
*
|
||||
* 管理员在任意评论页,操作条(赞同 / 回复 / 折叠…)的「回复」右边多一个
|
||||
* 「精选」按钮:点了把该评论标记为精选(PUT /comments/:id {is_featured}),
|
||||
* 关于页「走心评论」板块会展示它。已精选的显示「取消精选」,再点一次取消。
|
||||
*
|
||||
* 实现要点:
|
||||
* - 按钮插在「回复」span(纯 <span>回复</span>,无 class)右边,顺序与
|
||||
* 用户要求一致:赞同 → 回复 → 精选 → 折叠/已审/置顶/编辑/删除。
|
||||
* - 只对管理员可见:挂官方同款 atk-only-admin-show 属性,Artalk 内核在
|
||||
* 登录态变化时统一 add/remove .atk-hide,不用自己判断登录态。
|
||||
* - 精选状态从列表响应里收集:fetch 拦截读 GET /comments 的 JSON,
|
||||
* 把 is_featured 的评论 id 记进 Set(单条 PUT 的响应也顺手更新)。
|
||||
* - 编辑器会被 Artalk 反复重建(PJAX / 回复模式),用 MutationObserver 补挂,
|
||||
* 已挂过的评论跳过(data-feat 标记)。
|
||||
*/
|
||||
(function () {
|
||||
if (window.__atkFeatReady) return; // PJAX 重复执行防抖
|
||||
window.__atkFeatReady = true;
|
||||
|
||||
var featured = new Set(); // 已精选的评论 id
|
||||
|
||||
function server() {
|
||||
return String((window.artalkConfig && window.artalkConfig.server) || '').replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
// ── 精选状态收集:拦截 fetch,读评论列表 / 单条响应 ────────────────────
|
||||
// 注意只「读取」,请求原样放行;与 comment-anonymous 的注入拦截互不干扰。
|
||||
var origFetch = window.fetch;
|
||||
if (typeof origFetch === 'function' && !window.__atkFeatFetchHooked) {
|
||||
window.__atkFeatFetchHooked = true;
|
||||
window.fetch = function (input, init) {
|
||||
var p = origFetch.apply(this, [input, init]);
|
||||
try {
|
||||
var url = String(typeof input === 'string' ? input : (input && input.url) || '');
|
||||
if (/\/api\/v2\/comments(\?|$)/.test(url) && !/lookup/.test(url)) {
|
||||
p.then(function (res) {
|
||||
try {
|
||||
if (!res || !res.ok) return;
|
||||
res.clone().json().then(function (data) {
|
||||
var list = (data && data.comments) || [];
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
if (list[i] && list[i].is_featured !== undefined) {
|
||||
if (list[i].is_featured) featured.add(Number(list[i].id));
|
||||
else featured.delete(Number(list[i].id));
|
||||
}
|
||||
}
|
||||
refreshButtons(); // 按钮可能先于响应解析渲染,文案要修正
|
||||
}).catch(function () {});
|
||||
} catch (e) {}
|
||||
}).catch(function () {});
|
||||
}
|
||||
} catch (e) {}
|
||||
return p;
|
||||
};
|
||||
}
|
||||
|
||||
function getToken() {
|
||||
try {
|
||||
var u = JSON.parse(localStorage.getItem('ArtalkUser') || '{}');
|
||||
return u.token || '';
|
||||
} catch (e) { return ''; }
|
||||
}
|
||||
|
||||
function commentIdOf(actions) {
|
||||
var wrap = actions.closest('.atk-comment-wrap');
|
||||
if (!wrap || !wrap.id) return 0;
|
||||
var m = wrap.id.match(/^atk-comment-(\d+)$/);
|
||||
return m ? Number(m[1]) : 0;
|
||||
}
|
||||
|
||||
function toggle(commentId, btn) {
|
||||
if (!commentId) return;
|
||||
var next = !featured.has(commentId);
|
||||
var token = getToken();
|
||||
if (!token) { window.showToast && showToast('请先以管理员身份登录', 'warning'); return; }
|
||||
btn.style.opacity = '0.5';
|
||||
fetch(server() + '/api/v2/comments/' + commentId, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||
body: JSON.stringify({ is_featured: next }),
|
||||
}).then(function (r) {
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
if (next) featured.add(commentId); else featured.delete(commentId);
|
||||
btn.textContent = next ? '取消精选' : '精选';
|
||||
window.showToast && showToast(next ? '已加入走心评论' : '已取消精选', 'success');
|
||||
}).catch(function () {
|
||||
window.showToast && showToast('操作失败,请重试', 'error');
|
||||
}).then(function () { btn.style.opacity = ''; });
|
||||
}
|
||||
|
||||
// 在 .atk-actions 里找到「回复」span:Artalk 渲染的纯 <span>回复</span>
|
||||
function replySpanOf(actions) {
|
||||
var spans = actions.querySelectorAll(':scope > span');
|
||||
for (var i = 0; i < spans.length; i++) {
|
||||
if (spans[i].textContent.trim() === '回复' && !spans[i].classList.contains('atk-common-action-btn')) {
|
||||
return spans[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function refreshButtons() {
|
||||
try {
|
||||
var btns = document.querySelectorAll('.atk-feat-btn');
|
||||
for (var i = 0; i < btns.length; i++) {
|
||||
var id = commentIdOf(btns[i].closest('.atk-actions'));
|
||||
if (id) btns[i].textContent = featured.has(id) ? '取消精选' : '精选';
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function inject(actions) {
|
||||
if (actions.__atkFeat) return;
|
||||
var id = commentIdOf(actions);
|
||||
if (!id) return; // 编辑器回复预览等没有 id 的区域不动
|
||||
var replySpan = replySpanOf(actions);
|
||||
if (!replySpan) return; // 关闭回复的评论没有「回复」按钮,下轮再说
|
||||
|
||||
actions.__atkFeat = true;
|
||||
var btn = document.createElement('span');
|
||||
btn.className = 'atk-common-action-btn atk-feat-btn';
|
||||
btn.setAttribute('atk-only-admin-show', '');
|
||||
btn.textContent = featured.has(id) ? '取消精选' : '精选';
|
||||
// 初始显隐对齐官方 admin 按钮:官方的 [atk-only-admin-show] 显隐同步
|
||||
// 在列表渲染时就跑完了,我们是后注入的赶不上那趟车 —— 抄当前时刻
|
||||
// 官方按钮(如「折叠」)的 atk-hide 状态;之后登录/登出的再同步由
|
||||
// Artalk 内核统一处理(我们挂着同款属性,会被一起切换)。
|
||||
var refAdminBtn = actions.querySelector('[atk-only-admin-show]');
|
||||
if (!refAdminBtn || refAdminBtn.classList.contains('atk-hide')) {
|
||||
btn.classList.add('atk-hide');
|
||||
}
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
toggle(id, btn);
|
||||
});
|
||||
replySpan.after(btn);
|
||||
}
|
||||
|
||||
function scan() {
|
||||
try {
|
||||
var list = document.querySelectorAll('.atk-actions');
|
||||
for (var i = 0; i < list.length; i++) inject(list[i]);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
var timer = null;
|
||||
function scheduleScan() {
|
||||
if (timer) return;
|
||||
timer = setTimeout(function () { timer = null; scan(); }, 150);
|
||||
}
|
||||
|
||||
try {
|
||||
new MutationObserver(scheduleScan).observe(document.body, { childList: true, subtree: true });
|
||||
} catch (e) {}
|
||||
scan();
|
||||
})();
|
||||
@@ -5,6 +5,15 @@
|
||||
|
||||
{{ $sectionCount := 0 }}
|
||||
|
||||
<!-- Featured Comments Section(走心评论:管理员精选的评论,数据来自 Worker) -->
|
||||
{{ if site.Params.about.enablefeatured | default true }}
|
||||
{{ $sectionCount = add $sectionCount 1 }}
|
||||
<div style="margin-top: 40px;">
|
||||
<div class="section-title">{{ printf "%02d" $sectionCount }} / 走心评论 (Featured)</div>
|
||||
{{ partial "featured-comments.html" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<!-- Heatmap Section -->
|
||||
{{ if site.Params.about.enableheatmap }}
|
||||
{{ $sectionCount = add $sectionCount 1 }}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
{{/* 走心评论:管理员在评论区精选的评论,关于页展示。
|
||||
数据走 Worker 公开接口 /api/v2/featured/comments(悄悄话评论永远不进精选列表)。
|
||||
本地预览 / 接口故障时优雅降级:整块隐藏或显示占位文案。
|
||||
调试参数:?featmock=1 用本地 mock 渲染(仅用于排版预览,生产无害)。 */}}
|
||||
<style>
|
||||
.featc-wrap { min-width: 0; }
|
||||
.featc-title {
|
||||
font-size: 1.35rem; font-weight: 700; color: var(--text-title-color);
|
||||
line-height: 1.3; margin: 0 0 10px;
|
||||
}
|
||||
.featc-title .featc-count {
|
||||
font-size: 0.72rem; font-weight: 600; vertical-align: super;
|
||||
margin-left: 3px; color: var(--theme-main, #07c160);
|
||||
}
|
||||
.featc-desc { font-size: 0.86rem; line-height: 1.8; color: var(--text-desc, #777); margin: 0 0 18px; }
|
||||
/* 瀑布流:CSS 多列布局,卡片高度不一时自然错落 */
|
||||
.featc-grid {
|
||||
columns: 3; column-gap: 14px; min-width: 0;
|
||||
}
|
||||
.featc-card {
|
||||
break-inside: avoid; -webkit-column-break-inside: avoid;
|
||||
display: inline-flex; flex-direction: column; gap: 8px; width: 100%;
|
||||
margin: 0 0 14px; box-sizing: border-box;
|
||||
background: var(--bg-card, #fff);
|
||||
border: 1px solid var(--border-main, #eee);
|
||||
border-radius: 10px; padding: 16px 16px 12px;
|
||||
text-decoration: none; color: inherit; min-width: 0;
|
||||
transition: transform .25s ease, box-shadow .25s ease, border-color .25s ease;
|
||||
}
|
||||
.featc-card:hover {
|
||||
transform: translateY(-3px);
|
||||
border-color: var(--theme-main-30, rgba(7,193,96,.3));
|
||||
box-shadow: var(--shadow-popover, 0 4px 20px rgba(0,0,0,.1));
|
||||
text-decoration: none; color: inherit;
|
||||
}
|
||||
.featc-head { display: flex; align-items: center; gap: 9px; min-width: 0; }
|
||||
.featc-avatar { width: 34px; height: 34px; border-radius: 50%; flex: none; object-fit: cover; }
|
||||
.featc-nick {
|
||||
font-size: 0.92rem; font-weight: 700; color: var(--text-title-color, #333);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.featc-date { margin-left: auto; flex: none; font-size: 0.72rem; color: var(--text-muted, #999); }
|
||||
.featc-content {
|
||||
font-size: 0.88rem; line-height: 1.7; color: var(--text-main, #333);
|
||||
margin: 0; word-break: break-word;
|
||||
display: -webkit-box; -webkit-line-clamp: 6; -webkit-box-orient: vertical; overflow: hidden;
|
||||
}
|
||||
.featc-content img { max-height: 1.2em; vertical-align: sub; }
|
||||
.featc-content p { margin: 0; }
|
||||
.featc-foot {
|
||||
font-size: 0.72rem; color: var(--text-muted, #999); line-height: 1.6;
|
||||
padding-left: 8px; border-left: 2px solid var(--border-main, #eee);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.featc-foot a { color: inherit; }
|
||||
.featc-empty { font-size: 0.86rem; color: var(--text-muted, #999); padding: 8px 0; }
|
||||
/* 暗色模式:主题对文字色是「按元素覆写」而不是重定义变量(--text-title-color
|
||||
在暗色下仍是深色字),这里必须显式给出暗色文字色,否则标题会隐身 */
|
||||
[data-theme="dark"] .featc-title,
|
||||
[data-theme="dark"] .featc-nick { color: var(--text-title-dark-color, #e5e7eb); }
|
||||
[data-theme="dark"] .featc-content { color: var(--text-content-dark-color, #d1d5db); }
|
||||
@media (max-width: 1100px) {
|
||||
.featc-grid { columns: 2; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.featc-grid { columns: 1; }
|
||||
.featc-card { margin-bottom: 10px; padding: 13px 14px 10px; }
|
||||
.featc-desc { margin-bottom: 14px; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.featc-card, .featc-card:hover { transition: none; transform: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="featc-wrap">
|
||||
<div class="featc-title">走心评论<span class="featc-count" id="featc-count" hidden></span></div>
|
||||
<p class="featc-desc">当我看到各位在不同的时间和故事中,发出一份份走心的评论时,一份温暖之意即笼罩在我的心头。</p>
|
||||
<div class="featc-grid" id="featc-grid">
|
||||
<div class="featc-empty" id="featc-loading">加载中…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
(function () {
|
||||
var API = (window.artalkConfig && window.artalkConfig.server || 'https://api.200181.xyz').replace(/\/+$/, '') +
|
||||
'/api/v2/featured/comments?limit=8';
|
||||
var grid = document.getElementById('featc-grid');
|
||||
if (!grid) return;
|
||||
|
||||
function esc(s) {
|
||||
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||||
});
|
||||
}
|
||||
|
||||
function render(list) {
|
||||
var countEl = document.getElementById('featc-count');
|
||||
if (countEl && list.length) { countEl.textContent = list.length + '个'; countEl.hidden = false; }
|
||||
if (!list.length) {
|
||||
grid.innerHTML = '<div class="featc-empty">还没有走心评论,去和博主聊聊吧~</div>';
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var c = list[i];
|
||||
var avatar = 'https://weavatar.com/avatar/' + esc(c.email_encrypted) + '?d=mp&s=120';
|
||||
var nick = esc(c.nick);
|
||||
html +=
|
||||
'<a class="featc-card" href="' + esc(c.page_url) + '#atk-comment-' + esc(c.id) + '">' +
|
||||
'<div class="featc-head">' +
|
||||
'<img class="featc-avatar" src="' + avatar + '" alt="" loading="lazy" referrerpolicy="no-referrer">' +
|
||||
'<span class="featc-nick">' + nick + '</span>' +
|
||||
'<span class="featc-date">' + esc(c.date) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="featc-content">' + c.content_html + '</div>' +
|
||||
'<div class="featc-foot">发表在 ' + esc(c.page_title || '页面') + '</div>' +
|
||||
'</a>';
|
||||
}
|
||||
grid.innerHTML = html;
|
||||
}
|
||||
|
||||
function load() {
|
||||
// 排版预览 mock(?featmock=1):接口未部署 / 无精选数据时也能看样式
|
||||
if (/[?&]featmock=1/.test(location.search)) {
|
||||
render([
|
||||
{ id: 1, nick: '森屿', email_encrypted: '897defecd8ecc9ad844d86f615b967d2', link: '', date: '2026-06-27 20:57', content_html: '<p>“被记得”本身就是一件幸福的事</p>', page_url: '#', page_title: '碎碎念|被记得 – 去青岛看一场迟到很久的海' },
|
||||
{ id: 2, nick: '小十', email_encrypted: 'e08b1ed883704d10a4e4b9c05b2bf019', link: '', date: '2026-05-11 17:00', content_html: '<p>哇哦🎉,环境真的太好啦,下次我也想去!</p>', page_url: '#', page_title: '碎碎念|“撩蝉” – 去闯进其他人的生活' },
|
||||
{ id: 3, nick: '群林', email_encrypted: '897defecd8ecc9ad844d86f615b967d2', link: 'https://usj.cc', date: '2026-01-25 13:57', content_html: '<p>欢迎来做客,博客新增了留言板,有问题随时找我~</p>', page_url: '#', page_title: '留言' },
|
||||
{ id: 4, nick: '小柒', email_encrypted: 'e08b1ed883704d10a4e4b9c05b2bf019', link: '', date: '2025-01-21 01:07', content_html: '<p>2025 年新的一年,一起加油,希望一切顺利!</p>', page_url: '#', page_title: '留言' },
|
||||
{ id: 5, nick: '阿澜', email_encrypted: '897defecd8ecc9ad844d86f615b967d2', link: '', date: '2025-11-08 22:14', content_html: '<p>这篇文章我反反复复读了三遍。「把日子过成自己喜欢的样子」说起来简单,做起来才知道有多难。谢谢博主的文字,让我在加班的深夜里被治愈了一下,明天继续加油。</p>', page_url: '#', page_title: '碎碎念|深夜书桌 – 与自己和解的十个瞬间' },
|
||||
{ id: 6, nick: 'Momo', email_encrypted: 'e08b1ed883704d10a4e4b9c05b2bf019', link: '', date: '2025-09-30 09:42', content_html: '<p>签到打卡 ~</p>', page_url: '#', page_title: '碎碎念|秋日漫步 – 路过巷口的桂花香' },
|
||||
{ id: 7, nick: '北屿', email_encrypted: '897defecd8ecc9ad844d86f615b967d2', link: '', date: '2025-08-12 19:03', content_html: '<p>从友链跳转过来的,一口气读了五篇,博客的排版和摄影都太舒服了。已经加入 RSS 阅读器,期待更新!</p>', page_url: '#', page_title: '碎碎念|山海之间 – 一次说走就走的旅行' }
|
||||
]);
|
||||
return;
|
||||
}
|
||||
fetch(API)
|
||||
.then(function (r) { return r.ok ? r.json() : Promise.reject(r.status); })
|
||||
.then(function (data) { render((data && data.comments) || []); })
|
||||
.catch(function () {
|
||||
if (grid) grid.innerHTML = '<div class="featc-empty">还没有走心评论,去和博主聊聊吧~</div>';
|
||||
});
|
||||
}
|
||||
|
||||
load();
|
||||
})();
|
||||
</script>
|
||||
@@ -146,8 +146,10 @@
|
||||
{{/* 匿名评论开关 + 表情包最近使用 */}}
|
||||
{{ $anon := resources.Get "js/modules/comment-anonymous.js" }}
|
||||
{{ $emoRecent := resources.Get "js/modules/emoticon-recent.js" }}
|
||||
{{/* 走心评论:管理员「精选」按钮 */}}
|
||||
{{ $feat := resources.Get "js/modules/comment-featured.js" }}
|
||||
|
||||
{{ $pageScripts := slice $friendlink $autofill $anon $emoRecent $artalkModule $human $paragraphComments $reward | resources.Concat "js/page-only.js" | resources.Minify | resources.Fingerprint }}
|
||||
{{ $pageScripts := slice $friendlink $autofill $anon $emoRecent $feat $artalkModule $human $paragraphComments $reward | resources.Concat "js/page-only.js" | resources.Minify | resources.Fingerprint }}
|
||||
|
||||
<script defer src="{{ $pageScripts.RelPermalink }}"></script>
|
||||
<script>window._pageOnlyScriptUrl = '{{ $pageScripts.RelPermalink }}';</script>
|
||||
@@ -162,8 +164,9 @@
|
||||
{{ $autofill := resources.Get "js/modules/comment-autofill.js" }}
|
||||
{{ $anon := resources.Get "js/modules/comment-anonymous.js" }}
|
||||
{{ $emoRecent := resources.Get "js/modules/emoticon-recent.js" }}
|
||||
{{ $feat := resources.Get "js/modules/comment-featured.js" }}
|
||||
|
||||
{{ $pageScripts := slice $friendlink $autofill $anon $emoRecent $artalkModule $human $paragraphComments $reward | resources.Concat "js/page-only.js" | resources.Minify | resources.Fingerprint }}
|
||||
{{ $pageScripts := slice $friendlink $autofill $anon $emoRecent $feat $artalkModule $human $paragraphComments $reward | resources.Concat "js/page-only.js" | resources.Minify | resources.Fingerprint }}
|
||||
|
||||
<script>window._pageOnlyScriptUrl = '{{ $pageScripts.RelPermalink }}';</script>
|
||||
{{ end }}
|
||||
|
||||
Reference in new issue
Block a user