/* =====================================================================
* 文章视图模块(article.js)
* 首页文章列表(分页)/ 分区 / 项目 / 简介 / 文章详情 / 点赞。
* ===================================================================== */
import { ABOUT_DEFAULT, ARTICLE_CATEGORIES, DONATE_IMAGE, PARTITIONS, SOCIAL_LINKS, state } from "./state.js";
import { $, escapeHtml, formatDate, openModal, showToast } from "./utils.js";
import { api } from "./api.js";
import { renderMarkdown } from "./markdown.js";
import { openLoginModal } from "./auth.js";
import { applyFriend } from "./friend.js";
import { renderComments } from "./comment.js";
/* ---------------- 首页(简介 + 社交链接 + 全部文章,分页加载) ---------------- */
export async function renderHome(app) {
document.title = "首页 - 孤竹居士的博客";
app.innerHTML = `
最新文章
`;
await renderHomeAbout($("#about-card", app));
await renderArticleList($("#article-feed", app), 6);
}
/** 主页简介卡片:头像、名字、简介(线上可改)、社交账号链接 */
async function renderHomeAbout(el) {
let profile = null;
try {
profile = await api("/api/user/blogger");
} catch (err) { /* 简介加载失败不阻塞页面 */ }
const name = (profile && profile.username) || ABOUT_DEFAULT.name;
const avatar = (profile && profile.avatar) || "";
const bio = (profile && profile.bio)
? profile.bio.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)
: ABOUT_DEFAULT.bio;
const avatarHtml = avatar
? `
`
: `${escapeHtml(name.slice(0, 1))}
`;
el.innerHTML = `
${avatarHtml}
${escapeHtml(name)}
${escapeHtml(ABOUT_DEFAULT.tagline)}
${bio.map((p) => `
${escapeHtml(p)}
`).join("")}
${state.user && state.user.role === "blogger" ? '' : ""}
${(ABOUT_DEFAULT.tags || []).map((t) => `${escapeHtml(t)}`).join("")}
`;
// 绑定按钮事件:打赏弹窗(所有人可见);编辑简介(仅博主,展示在主页简介处)
$("#btn-donate", el).addEventListener("click", openDonateModal);
const bioBtn = $("#btn-edit-bio", el);
if (bioBtn) bioBtn.addEventListener("click", () => openBioEditor(el, profile && profile.bio));
}
/** 打赏弹窗:展示收款二维码;点击图片外任意位置关闭(按 Esc 也可关闭) */
function openDonateModal() {
const overlay = document.createElement("div");
overlay.className = "modal-overlay";
overlay.innerHTML = `
感谢打赏
`;
const close = () => {
overlay.remove();
document.removeEventListener("keydown", onKey);
};
const onKey = (e) => { if (e.key === "Escape") close(); };
document.addEventListener("keydown", onKey);
// 点击图片本身不关闭(方便长按识别),点击其他任意位置关闭
overlay.addEventListener("click", (e) => {
if (e.target.closest("img")) return;
close();
});
document.body.appendChild(overlay);
}
/** 简介编辑弹窗(仅博主):入口在主页简介处,保存后立即刷新简介卡片 */
async function openBioEditor(aboutEl, currentBio) {
const form = document.createElement("div");
form.className = "form";
form.innerHTML = `
`;
const modal = openModal({ title: "编辑简介", content: form });
$("#btn-bio-save", form).addEventListener("click", async () => {
const btn = $("#btn-bio-save", form);
const bio = $("#bio-edit-input", form).value.trim();
btn.disabled = true; btn.textContent = "保存中…";
try {
await api("/api/user/profile", { method: "PUT", body: { bio } });
modal.close();
showToast("简介已保存", "success");
await renderHomeAbout(aboutEl); // 重新拉取最新简介并重绘
} catch (err) {
showToast(err.message, "error");
btn.disabled = false; btn.textContent = "保存简介";
}
});
}
export function articleCard(article, compact = false) {
const card = document.createElement("a");
card.className = "article-card";
// 文章详情地址按分区决定前缀:/life/1 或 /study/1
const category = article.category || "life";
card.href = `/${category}/${article.id}`;
card.setAttribute("data-link", "");
const isFriendOnly = article.visibility === "friend";
// 游客视角的好友文章:后端仅返回 id/title/cover/visibility(无 author_username 字段)
const locked = isFriendOnly && typeof article.author_username === "undefined";
const coverHtml = article.cover
? `})
${isFriendOnly ? '
好友专属' : ""}
`
: `${isFriendOnly ? "🔒" : ""}
`;
// 分区页(compact)只展示封面 + 标题;首页展示完整元信息
const metaHtml = compact ? "" : `
${isFriendOnly ? '好友专属' : '公开'}
${locked
? '仅好友可见全文'
: `${escapeHtml(article.author_username || "")}${formatDate(article.created_time)}`}
`;
card.innerHTML = `
${coverHtml}
${escapeHtml(article.title)}
${metaHtml}
`;
return card;
}
/* ---------------- 文章列表分页加载(首页 / 分区复用) ---------------- */
export async function renderArticleList(container, pageSize = 6, category = "") {
container.innerHTML = "";
const list = document.createElement("div");
list.className = "article-list";
container.appendChild(list);
const moreBtn = document.createElement("button");
moreBtn.className = "btn btn-outline btn-block load-more-btn";
moreBtn.textContent = "加载更多";
moreBtn.hidden = true;
moreBtn.addEventListener("click", () => loadPage());
container.appendChild(moreBtn);
let page = 1;
let loaded = 0;
let total = 0;
async function loadPage() {
moreBtn.disabled = true;
moreBtn.textContent = "加载中…";
try {
// category 为空 = 首页全部文章;传入分区则按分区过滤
const categoryQuery = category ? `&category=${encodeURIComponent(category)}` : "";
const data = await api(`/api/article/list?page=${page}&page_size=${pageSize}${categoryQuery}`);
total = data.total;
data.items.forEach((a) => list.appendChild(articleCard(a, Boolean(category))));
loaded += data.items.length;
page += 1;
if (loaded === 0 && total === 0) {
list.innerHTML = '暂无文章
';
moreBtn.hidden = true;
moreBtn.textContent = "加载更多";
return;
}
moreBtn.hidden = loaded >= total;
moreBtn.textContent = "加载更多";
} catch (err) {
moreBtn.textContent = "加载更多";
showToast(err.message, "error");
} finally {
moreBtn.disabled = false;
}
}
await loadPage();
}
/* ---------------- 分区 ---------------- */
export async function renderPartition(app, slug) {
if (slug === "projects") return renderProjects(app);
const partition = PARTITIONS.find((p) => p.slug === slug);
if (!partition) return renderNotFound(app);
document.title = `${partition.name} - 孤竹居士的博客`;
const sub = slug === "life" ? "生活点滴记录" : "学习笔记与总结";
app.innerHTML = `
${escapeHtml(partition.name)}
${sub}
`;
// 分区页只展示本分区的文章卡片(封面 + 标题)
await renderArticleList($("#article-feed", app), 6, slug);
}
export async function renderProjects(app) {
document.title = "我的项目 - 孤竹居士的博客";
app.innerHTML = `
我的项目
在线体验与源码下载
`;
const grid = $(".project-grid", app);
let items = [];
try { items = await api("/api/project/list"); }
catch (err) { showToast(err.message, "error"); }
if (!items.length) { grid.innerHTML = '暂无项目
'; return; }
items.forEach((project) => {
const card = document.createElement("a");
card.className = "project-card";
card.href = `/projects/${project.id}`;
card.setAttribute("data-link", "");
const badge = project.project_type === "static"
? '在线演示'
: 'GitHub 链接';
const visBadge = project.visibility === "friend"
? '🔒 好友专属'
: '公开';
const lockedIcon = project.visibility === "friend" ? " 🔒" : "";
card.innerHTML = `
${escapeHtml(project.name)}${lockedIcon}
${escapeHtml(project.description || "")}
${(project.tech || "").split(",").map((t) => t.trim()).filter(Boolean).map((t) => `${escapeHtml(t)}`).join("")}
${badge}
${visBadge}
查看详情`;
grid.appendChild(card);
});
}
/* ---------------- 项目详情 ---------------- */
export async function renderProjectDetail(app, id) {
let project = null;
try { project = await api(`/api/project/${id}`); }
catch (err) { return renderNotFound(app); }
if (!project) return renderNotFound(app);
document.title = `${project.name} - 孤竹居士的博客`;
const demoBtn = project.demo_url
? `🚀 在线运行`
: "";
const downloadBtn = project.download_url
? `⬇ 下载项目`
: "";
app.innerHTML = `
${escapeHtml(project.name)}
${escapeHtml(project.description || "")}
${(project.tech || "").split(",").map((t) => t.trim()).filter(Boolean).map((t) => `${escapeHtml(t)}`).join("")}
${demoBtn}
${downloadBtn}
`;
}
/* 简介已并入首页(renderHomeAbout),不再单独成页 */
export function renderNotFound(app) {
document.title = "页面不存在 - 孤竹居士的博客";
app.innerHTML = `
`;
}
/* ---------------- 文章详情(含点赞) ---------------- */
export async function renderArticle(app, articleId) {
const article = await api(`/api/article/${articleId}`);
const isFriendOnly = article.visibility === "friend";
const locked = isFriendOnly && article.content == null; // 游客视角:正文为 null
const category = article.category || "life";
document.title = `${article.title} - 孤竹居士的博客`;
app.innerHTML = `
${escapeHtml(article.title)}
${escapeHtml(article.author_username || "")}
${formatDate(article.created_time)}
${isFriendOnly ? '好友专属' : ""}
${article.cover ? `` : ""}
${locked ? lockedBox() : renderMarkdown(article.content)}
`;
if (locked) bindLockedApply();
await renderLikeBar($("#like-bar", app), articleId);
await renderComments($("#comments", app), articleId);
}
function lockedBox() {
let actionHtml = "";
if (!state.user) {
actionHtml = '';
} else if (state.user.role === "visitor") {
if (state.friendStatus === "pending") actionHtml = '申请已提交,等待博主审批
';
else if (state.friendStatus === "rejected") actionHtml = '';
else actionHtml = '';
} else {
actionHtml = '登录好友账号后即可查看全文
';
}
return `
`;
}
function bindLockedApply() {
const btn = $("#btn-locked-apply");
if (btn) {
btn.addEventListener("click", () => {
if (!state.user) openLoginModal();
else applyFriend();
});
}
}
export async function renderLikeBar(el, articleId) {
// 后端只返回数量与“当前用户是否已点赞”,不返回点赞名单(防用户名枚举)
let likes = { count: 0, includes_me: false };
try {
likes = await api(`/api/like/list?article_id=${articleId}`);
} catch (err) { /* 好友文章对游客不可见点赞,忽略 */ }
const liked = !!likes.includes_me;
el.innerHTML = `
${likes.count ? `共 ${likes.count} 人点赞` : "还没有人点赞"}
`;
const btn = $("#btn-like", el);
if (!btn) return;
btn.addEventListener("click", async () => {
if (!state.user) {
openLoginModal();
showToast("请先登录后点赞", "info");
return;
}
if (state.user.role === "visitor") {
if (state.friendStatus === "pending") {
showToast("好友申请审批通过后即可点赞", "info");
return;
}
showToast("申请成为好友,审批通过后即可点赞", "info");
applyFriend();
return;
}
if (liked) {
showToast("您已经点过赞了", "info");
return;
}
btn.disabled = true;
try {
await api("/api/like/add", { method: "POST", body: { article_id: articleId } });
showToast("点赞成功", "success");
await renderLikeBar(el, articleId);
} catch (err) {
showToast(err.message, "error");
}
});
}
评论