awesome/src/routes/pages.routes.js
cheney c09857cc81
All checks were successful
Build and Deploy / build (push) Successful in 56s
Build and Deploy / deploy (push) Successful in 28s
feat: 登录记住本机+密码可见、内容分简介与主体、Markdown 渲染、首页随机条目
2026-09-01 10:53:17 +08:00

192 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Router } from "express";
import * as searchService from "../services/search.service.js";
import * as entryService from "../services/entry.service.js";
import * as tagService from "../services/tag.service.js";
import * as commentService from "../services/comment.service.js";
import * as sourceService from "../services/source.service.js";
import * as userService from "../services/user.service.js";
import { ENTRY_TYPES } from "../services/entry.service.js";
export const pagesRouter = Router();
function csv(v) {
return String(v || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}
pagesRouter.get("/healthz", (_req, res) => {
res.json({ ok: true, uptime: process.uptime() });
});
pagesRouter.get("/", async (_req, res, next) => {
try {
const [stats, recent, hero] = await Promise.all([
searchService.stats(),
entryService.listRecent(4),
(async () => {
const rand = await searchService.randomActive();
return rand ? entryService.getById(rand.id) : null;
})(),
]);
res.render("index", {
title: "Awesome Index · 把网上牛逼的东西收进一个库",
stats,
recent,
hero,
});
} catch (e) {
next(e);
}
});
pagesRouter.get("/entries", async (req, res, next) => {
try {
const filters = {
q: req.query.q ?? "",
types: csv(req.query.type),
tagIds: csv(req.query.tags).map(Number).filter(Number.isFinite),
sort: req.query.sort ?? "updated",
page: Math.max(1, Number(req.query.page) || 1),
status: "active",
};
const [result, tagTree, typeCounts] = await Promise.all([
searchService.searchEntries({ ...filters, perPage: 10 }),
tagService.attachCounts(await tagService.buildTree()),
searchService.typeCounts(),
]);
const totalPages = Math.max(1, Math.ceil(result.total / result.perPage));
res.render("list", {
title: `搜索${filters.q ? `${filters.q}` : "全部"} · Awesome Index`,
result,
filters,
totalPages,
types: ENTRY_TYPES,
tagTree,
typeCounts,
});
} catch (e) {
next(e);
}
});
pagesRouter.get("/entries/:slug", async (req, res, next) => {
try {
const entry = await entryService.getBySlug(req.params.slug);
if (!entry || entry.status === "greyed") {
return res.status(404).send("条目不存在或已下架");
}
const comments = await commentService.listForEntry(entry.id);
const machine = {
id: entry.slug,
type: entry.type,
name: entry.title,
summary: entry.summary,
description: entry.description_md,
url: entry.url,
tags: entry.tags,
license: entry.license,
stars: Number(entry.stars),
updated_at: entry.updated_at,
status: entry.status,
};
res.render("detail", {
title: `${entry.title} · Awesome Index`,
entry,
comments,
machineJson: JSON.stringify(machine, null, 2),
});
} catch (e) {
next(e);
}
});
pagesRouter.get("/random", async (_req, res, next) => {
try {
const entry = await searchService.randomActive();
if (!entry) return res.redirect("/entries");
res.redirect(`/entries/${entry.slug}`);
} catch (e) {
next(e);
}
});
pagesRouter.get("/login", (_req, res) => {
res.render("login", { title: "管理台登录 · Awesome Index" });
});
pagesRouter.get("/admin", async (req, res, next) => {
try {
if (!req.session.userId) return res.redirect("/login");
const isAdmin = req.session.role === "admin";
const [sources, users, comments, entries, tagTreeAdmin] = await Promise.all([
sourceService.listSources(),
isAdmin ? userService.listUsers() : Promise.resolve([]),
commentService.listAll({}),
searchService.searchEntries({ status: "all", perPage: 50 }),
tagService.attachCounts(await tagService.buildTree()),
]);
res.render("admin", {
title: "管理台 · Awesome Index",
user: { username: req.session.username, role: req.session.role },
sources,
users,
comments,
entries: entries.items,
isAdmin,
tagTreeAdmin,
});
} catch (e) {
next(e);
}
});
pagesRouter.get("/admin/entries/:id/edit", async (req, res, next) => {
try {
if (!req.session.userId) return res.redirect("/login");
const entry = await entryService.getById(Number(req.params.id));
if (!entry) throw Object.assign(new Error("条目不存在"), { status: 404 });
const tagTreeAdmin = await tagService.buildTree();
res.render("admin-entry-edit", {
title: `编辑 · ${entry.title} · Awesome Index`,
user: { username: req.session.username, role: req.session.role },
isAdmin: req.session.role === "admin",
entry,
tagTreeAdmin,
types: ENTRY_TYPES,
});
} catch (e) {
next(e);
}
});
pagesRouter.post("/admin/entries/:id/edit", async (req, res, next) => {
try {
if (!req.session.userId) return res.redirect("/login");
const id = Number(req.params.id);
const tags = Array.isArray(req.body.tags) ? req.body.tags : req.body.tags ? [req.body.tags] : [];
const newTags = String(req.body.new_tags || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
const patch = {
title: String(req.body.title || "").trim(),
type: req.body.type,
summary: String(req.body.summary || "").trim(),
description_md: String(req.body.description_md || ""),
url: String(req.body.url || "").trim(),
license: String(req.body.license || "").trim(),
stars: Number(req.body.stars) || 0,
tags: [...new Set([...tags, ...newTags])],
};
await entryService.updateEntry(id, patch, { isHumanEditor: true });
const status = req.body.status;
if (status && entryService.STATUSES.includes(status)) {
await entryService.setStatusDirect(id, status);
}
res.redirect("/admin#sec-content");
} catch (e) {
next(e);
}
});