diff --git a/bun.lockb b/bun.lockb index 98b29ec..965abd3 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/database/development.sqlite3-shm b/database/development.sqlite3-shm index cdf5f27..c28f600 100644 Binary files a/database/development.sqlite3-shm and b/database/development.sqlite3-shm differ diff --git a/database/development.sqlite3-wal b/database/development.sqlite3-wal index e5145a5..fd0f3fc 100644 Binary files a/database/development.sqlite3-wal and b/database/development.sqlite3-wal differ diff --git a/docs/dev.md b/docs/dev.md new file mode 100644 index 0000000..862bc6a --- /dev/null +++ b/docs/dev.md @@ -0,0 +1,6 @@ + +# 开发文档 + +## 记录说明 + +本系统存在token和session两种机制,因此,ctx.session表明session,会影响到客户端的cookie, 由于此机制,ctx.session.user和ctx.state.user都存放了用户信息,注意区别使用。 \ No newline at end of file diff --git a/package.json b/package.json index 8d9da88..e1ddd8d 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "koa-session": "^7.0.2", "lodash": "^4.17.21", "log4js": "^6.9.1", + "marked": "^16.2.1", "minimatch": "^9.0.0", "node-cron": "^4.1.0", "path-to-regexp": "^8.2.0", diff --git a/src/controllers/Page/ArticleController.js b/src/controllers/Page/ArticleController.js new file mode 100644 index 0000000..a0a1fa6 --- /dev/null +++ b/src/controllers/Page/ArticleController.js @@ -0,0 +1,123 @@ +import { ArticleModel } from "../../db/models/ArticleModel.js" +import Router from "utils/router.js" +import { marked } from "marked" + +class ArticleController { + async index(ctx) { + const { page = 1, view = 'grid' } = ctx.query + const limit = 12 // 每页显示的文章数量 + const offset = (page - 1) * limit + + // 获取文章总数 + const total = await ArticleModel.getPublishedArticleCount() + const totalPages = Math.ceil(total / limit) + + // 获取分页文章 + const articles = await ArticleModel.findPublished(offset, limit) + + // 获取所有分类和标签 + const categories = await ArticleModel.getArticleCountByCategory() + const allArticles = await ArticleModel.findPublished() + const tags = new Set() + allArticles.forEach(article => { + if (article.tags) { + article.tags.split(',').forEach(tag => { + tags.add(tag.trim()) + }) + } + }) + + return ctx.render("page/articles/index", { + articles, + categories: categories.map(c => c.category), + tags: Array.from(tags), + currentPage: parseInt(page), + totalPages, + view, + title: "文章列表", + }, { + includeUser: true, + includeSite: true, + }) + } + + async show(ctx) { + const { slug } = ctx.params + console.log(slug); + + const article = await ArticleModel.findBySlug(slug) + + if (!article) { + ctx.throw(404, "文章不存在") + } + + // 增加阅读次数 + await ArticleModel.incrementViewCount(article.id) + + // 将文章内容解析为HTML + article.content = marked(article.content || '') + + // 获取相关文章 + const relatedArticles = await ArticleModel.getRelatedArticles(article.id) + + return ctx.render("page/articles/article", { + article, + relatedArticles, + title: article.title, + }, { + includeUser: true, + }) + } + + async byCategory(ctx) { + const { category } = ctx.params + const articles = await ArticleModel.findByCategory(category) + + return ctx.render("page/articles/category", { + articles, + category, + title: `${category} - 分类文章`, + }, { + includeUser: true, + }) + } + + async byTag(ctx) { + const { tag } = ctx.params + const articles = await ArticleModel.findByTags(tag) + + return ctx.render("page/articles/tag", { + articles, + tag, + title: `${tag} - 标签文章`, + }, { + includeUser: true, + }) + } + + async search(ctx) { + const { q } = ctx.query + const articles = await ArticleModel.searchByKeyword(q) + + return ctx.render("page/articles/search", { + articles, + keyword: q, + title: `搜索:${q}`, + }) + } + + static createRoutes() { + const controller = new ArticleController() + const router = new Router({ auth: true, prefix: "/articles" }) + router.get("", controller.index, { auth: false }) // 允许未登录访问 + router.get("/", controller.index, { auth: false }) // 允许未登录访问 + router.get("/search", controller.search) + router.get("/category/:category", controller.byCategory) + router.get("/tag/:tag", controller.byTag) + router.get("/:slug", controller.show) + return router + } +} + +export default ArticleController +export { ArticleController } diff --git a/src/controllers/Page/PageController.js b/src/controllers/Page/PageController.js index 3deeda6..3c7f94f 100644 --- a/src/controllers/Page/PageController.js +++ b/src/controllers/Page/PageController.js @@ -16,7 +16,7 @@ class PageController { // 首页 async indexGet(ctx) { - const blogs = await this.articleService.getAllArticles() + const blogs = await this.articleService.getPublishedArticles() return await ctx.render( "page/index/index", { @@ -176,8 +176,8 @@ class PageController { // 未授权报错页 router.get("/no-auth", controller.indexNoAuth.bind(controller), { auth: false }) - router.get("/article/:id", controller.pageGet("page/articles/index"), { auth: false }) - router.get("/articles", controller.pageGet("page/articles/index"), { auth: false }) + // router.get("/article/:id", controller.pageGet("page/articles/index"), { auth: false }) + // router.get("/articles", controller.pageGet("page/articles/index"), { auth: false }) router.get("/about", controller.pageGet("page/about/index"), { auth: false }) router.get("/terms", controller.pageGet("page/extra/terms"), { auth: false }) diff --git a/src/db/models/ArticleModel.js b/src/db/models/ArticleModel.js index 4b83535..4bf5fa9 100644 --- a/src/db/models/ArticleModel.js +++ b/src/db/models/ArticleModel.js @@ -5,17 +5,22 @@ class ArticleModel { return db("articles").orderBy("created_at", "desc") } - static async findPublished() { - return db("articles") + static async findPublished(offset, limit) { + let query = db("articles") .where("status", "published") .whereNotNull("published_at") .orderBy("published_at", "desc") + if (typeof offset === "number") { + query = query.offset(offset) + } + if (typeof limit === "number") { + query = query.limit(limit) + } + return query } static async findDrafts() { - return db("articles") - .where("status", "draft") - .orderBy("updated_at", "desc") + return db("articles").where("status", "draft").orderBy("updated_at", "desc") } static async findById(id) { @@ -27,22 +32,16 @@ class ArticleModel { } static async findByAuthor(author) { - return db("articles") - .where("author", author) - .where("status", "published") - .orderBy("published_at", "desc") + return db("articles").where("author", author).where("status", "published").orderBy("published_at", "desc") } static async findByCategory(category) { - return db("articles") - .where("category", category) - .where("status", "published") - .orderBy("published_at", "desc") + return db("articles").where("category", category).where("status", "published").orderBy("published_at", "desc") } static async findByTags(tags) { // 支持多个标签搜索,标签以逗号分隔 - const tagArray = tags.split(',').map(tag => tag.trim()) + const tagArray = tags.split(",").map(tag => tag.trim()) return db("articles") .where("status", "published") .whereRaw("tags LIKE ?", [`%${tagArray[0]}%`]) @@ -52,7 +51,7 @@ class ArticleModel { static async searchByKeyword(keyword) { return db("articles") .where("status", "published") - .where(function() { + .where(function () { this.where("title", "like", `%${keyword}%`) .orWhere("content", "like", `%${keyword}%`) .orWhere("keywords", "like", `%${keyword}%`) @@ -71,7 +70,11 @@ class ArticleModel { // 处理标签,确保格式一致 let tags = data.tags if (tags && typeof tags === "string") { - tags = tags.split(',').map(tag => tag.trim()).filter(tag => tag).join(', ') + tags = tags + .split(",") + .map(tag => tag.trim()) + .filter(tag => tag) + .join(", ") } // 生成slug(如果未提供) @@ -92,17 +95,19 @@ class ArticleModel { excerpt = this.generateExcerpt(data.content) } - return db("articles").insert({ - ...data, - tags, - slug, - reading_time: readingTime, - excerpt, - status: data.status || "draft", - view_count: 0, - created_at: db.fn.now(), - updated_at: db.fn.now(), - }).returning("*") + return db("articles") + .insert({ + ...data, + tags, + slug, + reading_time: readingTime, + excerpt, + status: data.status || "draft", + view_count: 0, + created_at: db.fn.now(), + updated_at: db.fn.now(), + }) + .returning("*") } static async update(id, data) { @@ -114,7 +119,11 @@ class ArticleModel { // 处理标签,确保格式一致 let tags = data.tags if (tags && typeof tags === "string") { - tags = tags.split(',').map(tag => tag.trim()).filter(tag => tag).join(', ') + tags = tags + .split(",") + .map(tag => tag.trim()) + .filter(tag => tag) + .join(", ") } // 生成slug(如果标题改变且未提供slug) @@ -141,15 +150,18 @@ class ArticleModel { publishedAt = db.fn.now() } - return db("articles").where("id", id).update({ - ...data, - tags: tags || current.tags, - slug: slug || current.slug, - reading_time: readingTime || current.reading_time, - excerpt: excerpt || current.excerpt, - published_at: publishedAt || current.published_at, - updated_at: db.fn.now(), - }).returning("*") + return db("articles") + .where("id", id) + .update({ + ...data, + tags: tags || current.tags, + slug: slug || current.slug, + reading_time: readingTime || current.reading_time, + excerpt: excerpt || current.excerpt, + published_at: publishedAt || current.published_at, + updated_at: db.fn.now(), + }) + .returning("*") } static async delete(id) { @@ -183,10 +195,7 @@ class ArticleModel { } static async incrementViewCount(id) { - return db("articles") - .where("id", id) - .increment("view_count", 1) - .returning("*") + return db("articles").where("id", id).increment("view_count", 1).returning("*") } static async findByDateRange(startDate, endDate) { @@ -202,10 +211,7 @@ class ArticleModel { } static async getPublishedArticleCount() { - const result = await db("articles") - .where("status", "published") - .count("id as count") - .first() + const result = await db("articles").where("status", "published").count("id as count").first() return result ? result.count : 0 } @@ -219,33 +225,19 @@ class ArticleModel { } static async getArticleCountByStatus() { - return db("articles") - .select("status") - .count("id as count") - .groupBy("status") - .orderBy("count", "desc") + return db("articles").select("status").count("id as count").groupBy("status").orderBy("count", "desc") } static async getRecentArticles(limit = 10) { - return db("articles") - .where("status", "published") - .orderBy("published_at", "desc") - .limit(limit) + return db("articles").where("status", "published").orderBy("published_at", "desc").limit(limit) } static async getPopularArticles(limit = 10) { - return db("articles") - .where("status", "published") - .orderBy("view_count", "desc") - .limit(limit) + return db("articles").where("status", "published").orderBy("view_count", "desc").limit(limit) } static async getFeaturedArticles(limit = 5) { - return db("articles") - .where("status", "published") - .whereNotNull("featured_image") - .orderBy("published_at", "desc") - .limit(limit) + return db("articles").where("status", "published").whereNotNull("featured_image").orderBy("published_at", "desc").limit(limit) } static async getRelatedArticles(articleId, limit = 5) { @@ -255,12 +247,12 @@ class ArticleModel { return db("articles") .where("status", "published") .where("id", "!=", articleId) - .where(function() { + .where(function () { if (current.category) { this.orWhere("category", current.category) } if (current.tags) { - const tags = current.tags.split(',').map(tag => tag.trim()) + const tags = current.tags.split(",").map(tag => tag.trim()) tags.forEach(tag => { this.orWhereRaw("tags LIKE ?", [`%${tag}%`]) }) @@ -274,9 +266,9 @@ class ArticleModel { static generateSlug(title) { return title .toLowerCase() - .replace(/[^\w\s-]/g, '') - .replace(/\s+/g, '-') - .replace(/-+/g, '-') + .replace(/[^\w\s-]/g, "") + .replace(/\s+/g, "-") + .replace(/-+/g, "-") .trim() } diff --git a/src/views/layouts/empty.pug b/src/views/layouts/empty.pug index 78d79f6..5011437 100644 --- a/src/views/layouts/empty.pug +++ b/src/views/layouts/empty.pug @@ -15,8 +15,7 @@ block $$content #{$site.site_title} // 桌面端菜单 .left.menu.desktop-only - a.menu-item(href="/about") 明月照佳人 - a.menu-item(href="/about") 岁月催人老 + a.menu-item(href="/articles") 所有文章 if !isLogin .right.menu.desktop-only a.menu-item(href="/login") 登录 @@ -35,8 +34,7 @@ block $$content // 移动端菜单内容(与桌面端一致) .mobile-menu.container .left.menu - a.menu-item(href="/about") 明月照佳人 - a.menu-item(href="/about") 岁月催人老 + a.menu-item(href="/articles") 所有文章 if !isLogin .right.menu a.menu-item(href="/login") 登录 diff --git a/src/views/layouts/root.pug b/src/views/layouts/root.pug index ba4c10e..479f568 100644 --- a/src/views/layouts/root.pug +++ b/src/views/layouts/root.pug @@ -12,12 +12,12 @@ html(lang="zh-CN") meta(charset="utf-8") meta(name="viewport" content="width=device-width, initial-scale=1") +css('lib/reset.css') - +css('lib/simplebar.css', true) + +css('lib/simplebar.css') +css('lib/simplebar-shim.css') +css('css/layouts/root.css') +js('lib/htmx.min.js') +js('lib/tailwindcss.3.4.17.js') - +js('lib/simplebar.min.js', true) + +js('lib/simplebar.min.js') body noscript style. diff --git a/src/views/page/articles/article.pug b/src/views/page/articles/article.pug new file mode 100644 index 0000000..a92df10 --- /dev/null +++ b/src/views/page/articles/article.pug @@ -0,0 +1,70 @@ +extends /layouts/empty.pug + +block pageContent + .container.mx-auto.px-4.py-8 + article.max-w-4xl.mx-auto + header.mb-8 + h1.text-4xl.font-bold.mb-4= article.title + .flex.flex-wrap.items-center.text-gray-600.mb-4 + span.mr-4 + i.fas.fa-calendar-alt.mr-1 + = new Date(article.published_at).toLocaleDateString() + span.mr-4 + i.fas.fa-eye.mr-1 + = article.view_count + " 阅读" + if article.reading_time + span.mr-4 + i.fas.fa-clock.mr-1 + = article.reading_time + " 分钟阅读" + if article.category + a.text-blue-600.mr-4(href=`/articles/category/${article.category}` class="hover:text-blue-800") + i.fas.fa-folder.mr-1 + = article.category + if article.status === "draft" + span(class="ml-2 px-2 py-0.5 text-xs bg-yellow-200 text-yellow-800 rounded align-middle") 未发布 + + if article.tags + .flex.flex-wrap.gap-2.mb-4 + each tag in article.tags.split(',') + a.bg-gray-100.text-gray-700.px-3.py-1.rounded-full.text-sm(href=`/articles/tag/${tag.trim()}` class="hover:bg-gray-200") + i.fas.fa-tag.mr-1 + = tag.trim() + + if article.featured_image + .mb-8 + img.w-full.rounded-lg.shadow-lg(src=article.featured_image alt=article.title) + + .prose.prose-lg.max-w-none.mb-8.markdown-content(class="prose-pre:bg-gray-100 prose-pre:p-4 prose-pre:rounded-lg prose-code:text-blue-600 prose-blockquote:border-l-4 prose-blockquote:border-gray-300 prose-blockquote:pl-4 prose-blockquote:italic prose-img:rounded-lg prose-img:shadow-md") + != article.content + + if article.keywords || article.description + .bg-gray-50.rounded-lg.p-6.mb-8 + if article.keywords + .mb-4 + h3.text-lg.font-semibold.mb-2 关键词 + .flex.flex-wrap.gap-2 + each keyword in article.keywords.split(',') + span.bg-white.px-3.py-1.rounded-full.text-sm= keyword.trim() + if article.description + h3.text-lg.font-semibold.mb-2 描述 + p.text-gray-600= article.description + + if relatedArticles && relatedArticles.length + section.border-t.pt-8.mt-8 + h2.text-2xl.font-bold.mb-6 相关文章 + .grid.grid-cols-1.gap-6(class="md:grid-cols-2") + each related in relatedArticles + .bg-white.shadow-md.rounded-lg.overflow-hidden + if related.featured_image + img.w-full.h-48.object-cover(src=related.featured_image alt=related.title) + .p-6 + h3.text-xl.font-semibold.mb-2 + a(href=`/articles/${related.slug}` class="hover:text-blue-600")= related.title + if related.excerpt + p.text-gray-600.text-sm.mb-4= related.excerpt + .flex.justify-between.items-center.text-sm.text-gray-500 + span + i.fas.fa-calendar-alt.mr-1 + = new Date(related.published_at).toLocaleDateString() + if related.category + a.text-blue-600(href=`/articles/category/${related.category}` class="hover:text-blue-800")= related.category diff --git a/src/views/page/articles/category.pug b/src/views/page/articles/category.pug new file mode 100644 index 0000000..fde2be6 --- /dev/null +++ b/src/views/page/articles/category.pug @@ -0,0 +1,29 @@ +extends /layouts/empty.pug + +block pageContent + .container.mx-auto.px-4.py-8 + h1.text-3xl.font-bold.mb-8 + span.text-gray-600 分类: + = category + + .grid.grid-cols-1.gap-6(class="md:grid-cols-2 lg:grid-cols-3") + each article in articles + .bg-white.shadow-md.rounded-lg.overflow-hidden + if article.featured_image + img.w-full.h-48.object-cover(src=article.featured_image alt=article.title) + .p-6 + h2.text-xl.font-semibold.mb-2 + a(href=`/articles/${article.slug}` class="hover:text-blue-600")= article.title + if article.excerpt + p.text-gray-600.mb-4= article.excerpt + .flex.justify-between.items-center.text-sm.text-gray-500 + span + i.fas.fa-calendar-alt.mr-1 + = new Date(article.published_at).toLocaleDateString() + span + i.fas.fa-eye.mr-1 + = article.view_count + " 阅读" + + if !articles.length + .text-center.py-8 + p.text-gray-500 该分类下暂无文章 diff --git a/src/views/page/articles/index.pug b/src/views/page/articles/index.pug index 0c1d940..1f16fe0 100644 --- a/src/views/page/articles/index.pug +++ b/src/views/page/articles/index.pug @@ -1,113 +1,133 @@ -extends /layouts/page.pug +extends /layouts/empty.pug block pageContent - .article-list-container-full - - const articles = [] - - articles.push({ id: 1, title: '文章标题1', author: '作者1', created_at: '2023-08-01', summary: '这是文章摘要...' }) - - articles.push({ id: 2, title: '文章标题2', author: '作者2', created_at: '2023-08-02', summary: '这是另一篇文章摘要...' }) - //- 文章列表 - if articles && articles.length - each article in articles - .article-item-full - h2.article-title-full - a(href=`/articles/${article.id}`) #{article.title} - .article-meta-full - span 作者:#{article.author} | 发布时间:#{article.created_at} - p.article-summary-full #{article.summary} - else - p.no-articles 暂无文章 + .flex.flex-col + .flex-1.p-8.bg-gray-50 + .container.mx-auto + // 页头 + .flex.justify-between.items-center.mb-8 + h1.text-2xl.font-bold 文章列表 + .flex.gap-4 + // 搜索框 + .relative + input#searchInput.w-64.pl-10.pr-4.py-2.border.rounded-lg( + type="text" + placeholder="搜索文章..." + hx-get="/articles/search" + hx-trigger="keyup changed delay:500ms" + hx-target="#articleList" + name="q" + class="focus:outline-none focus:ring-blue-500 focus:ring-2" + ) + i.fas.fa-search.absolute.left-3.top-3.text-gray-400 - //- 分页控件 - if totalPages > 1 - .pagination-full - if page > 1 - a.page-btn-full(href=`?page=${page-1}`) 上一页 - else - span.page-btn-full.disabled 上一页 - span.page-info-full 第 #{page} / #{totalPages} 页 - if page < totalPages - a.page-btn-full(href=`?page=${page+1}`) 下一页 - else - span.page-btn-full.disabled 下一页 - style. - .article-list-container-full { - width: 100%; - max-width: 100%; - margin: 40px 0 0 0; - background: transparent; - border-radius: 0; - box-shadow: none; - padding: 0; - display: flex; - flex-direction: column; - align-items: center; - } - .article-item-full { - width: 90vw; - max-width: 1200px; - background: #fff; - border-radius: 14px; - box-shadow: 0 2px 16px #e0e7ef; - margin-bottom: 28px; - padding: 28px 36px 18px 36px; - border-left: 6px solid #6dd5fa; - transition: box-shadow 0.2s, border-color 0.2s; - } - .article-item-full:hover { - box-shadow: 0 4px 32px #cbe7ff; - border-left: 6px solid #ff6a88; - } - .article-title-full { - margin: 0 0 8px 0; - font-size: 1.6em; - } - .article-title-full a { - color: #2b7cff; - text-decoration: none; - transition: color 0.2s; - } - .article-title-full a:hover { - color: #ff6a88; - } - .article-meta-full { - color: #888; - font-size: 1em; - margin-bottom: 8px; - } - .article-summary-full { - color: #444; - font-size: 1.13em; - } - .no-articles { - text-align: center; - color: #aaa; - margin: 40px 0; - } - .pagination-full { - display: flex; - justify-content: center; - align-items: center; - margin: 32px 0 0 0; - gap: 18px; - } - .page-btn-full { - padding: 7px 22px; - border-radius: 22px; - background: linear-gradient(90deg, #6dd5fa, #ff6a88); - color: #fff; - text-decoration: none; - font-weight: bold; - transition: background 0.2s; - cursor: pointer; - font-size: 1.08em; - } - .page-btn-full.disabled { - background: #eee; - color: #bbb; - cursor: not-allowed; - pointer-events: none; - } - .page-info-full { - color: #666; - font-size: 1.12em; - } \ No newline at end of file + // 视图切换按钮 + .flex.items-center.gap-2.bg-white.p-1.rounded-lg.border + button.p-2.rounded( + class="hover:bg-gray-100" + hx-get="/articles?view=grid" + hx-target="#articleList" + ) + i.fas.fa-th-large + button.p-2.rounded( + class="hover:bg-gray-100" + hx-get="/articles?view=list" + hx-target="#articleList" + ) + i.fas.fa-list + + // 筛选栏 + .bg-white.rounded-lg.shadow-sm.p-4.mb-6 + .flex.flex-wrap.gap-4 + if categories && categories.length + .flex.items-center.gap-2 + span.text-gray-600 分类: + each cat in categories + a.px-3.py-1.rounded-full( + class="hover:bg-blue-50 hover:text-blue-600" + (cat === currentCategory ? " bg-blue-100 text-blue-600" : "") + href=`/articles/category/${cat}` + )= cat + + if tags && tags.length + .flex.items-center.gap-2 + span.text-gray-600 标签: + each tag in tags + a.px-3.py-1.rounded-full( + class="hover:bg-blue-50 hover:text-blue-600" + (tag === currentTag ? " bg-blue-100 text-blue-600" : "") + href=`/articles/tag/${tag}` + )= tag + + // 文章列表 + #articleList.grid.grid-cols-1.gap-6(class="md:grid-cols-2 lg:grid-cols-3") + each article in articles + .bg-white.rounded-lg.shadow-sm.overflow-hidden.transition.duration-300.transform(class="hover:-translate-y-1 hover:shadow-md") + if article.featured_image + .relative.h-48 + img.w-full.h-full.object-cover(src=article.featured_image alt=article.title) + if article.category + a.absolute.top-3.right-3.px-3.py-1.bg-blue-600.text-white.text-sm.rounded-full.opacity-90( + href=`/articles/category/${article.category}` + class="hover:opacity-100" + )= article.category + .p-6 + h2.text-xl.font-bold.mb-3 + a(href=`/articles/${article.slug}` class="hover:text-blue-600")= article.title + if article.excerpt + p.text-gray-600.text-sm.mb-4.line-clamp-2= article.excerpt + + .flex.flex-wrap.gap-2.mb-4 + if article.tags + each tag in article.tags.split(',') + a.text-sm.text-gray-500( + href=`/articles/tag/${tag.trim()}` + class="hover:text-blue-600" + ) + i.fas.fa-tag.mr-1 + = tag.trim() + + .flex.justify-between.items-center.text-sm.text-gray-500 + .flex.items-center.gap-4 + span + i.far.fa-calendar.mr-1 + = new Date(article.published_at).toLocaleDateString() + if article.reading_time + span + i.far.fa-clock.mr-1 + = article.reading_time + "分钟" + span + i.far.fa-eye.mr-1 + = article.view_count + " 阅读" + + if !articles.length + .col-span-full.py-16.text-center + .text-gray-400.mb-4 + i.fas.fa-inbox.text-6xl + p.text-gray-500 暂无文章 + + // 分页 + if totalPages > 1 + .flex.justify-center.mt-8 + nav.flex.items-center.gap-1(aria-label="Pagination") + // 上一页 + if currentPage > 1 + a.px-3.py-1.rounded-md.bg-white.border( + href=`/articles?page=${currentPage - 1}` + class="text-gray-500 hover:text-gray-700 hover:bg-gray-50" + ) 上一页 + + // 页码 + each page in Array.from({length: totalPages}, (_, i) => i + 1) + if page === currentPage + span.px-3.py-1.rounded-md.bg-blue-50.text-blue-600.border.border-blue-200= page + else + a.px-3.py-1.rounded-md.bg-white.border( + href=`/articles?page=${page}` + class="text-gray-500 hover:text-gray-700 hover:bg-gray-50" + )= page + + // 下一页 + if currentPage < totalPages + a.px-3.py-1.rounded-md.bg-white.border( + href=`/articles?page=${currentPage + 1}` + class="text-gray-500 hover:text-gray-700 hover:bg-gray-50" + ) 下一页 diff --git a/src/views/page/articles/search.pug b/src/views/page/articles/search.pug new file mode 100644 index 0000000..555c2e7 --- /dev/null +++ b/src/views/page/articles/search.pug @@ -0,0 +1,34 @@ +extends /layouts/empty.pug + +block pageContent + .container.mx-auto.px-4.py-8 + .mb-8 + h1.text-3xl.font-bold.mb-4 + span.text-gray-600 搜索结果: + = keyword + p.text-gray-500 找到 #{articles.length} 篇相关文章 + + .grid.grid-cols-1.md:grid-cols-2.lg:grid-cols-3.gap-6 + each article in articles + .bg-white.shadow-md.rounded-lg.overflow-hidden + if article.featured_image + img.w-full.h-48.object-cover(src=article.featured_image alt=article.title) + .p-6 + h2.text-xl.font-semibold.mb-2 + a.hover:text-blue-600(href=`/articles/${article.slug}`)= article.title + if article.excerpt + p.text-gray-600.mb-4= article.excerpt + .flex.justify-between.items-center + .text-sm.text-gray-500 + span.mr-4 + i.fas.fa-calendar-alt.mr-1 + = new Date(article.published_at).toLocaleDateString() + span + i.fas.fa-eye.mr-1 + = article.view_count + " 阅读" + if article.category + a.text-sm.bg-blue-100.text-blue-600.px-3.py-1.rounded-full.hover:bg-blue-200(href=`/articles/category/${article.category}`)= article.category + + if !articles.length + .text-center.py-8 + p.text-gray-500 未找到相关文章 diff --git a/src/views/page/articles/tag.pug b/src/views/page/articles/tag.pug new file mode 100644 index 0000000..d52241b --- /dev/null +++ b/src/views/page/articles/tag.pug @@ -0,0 +1,32 @@ +extends /layouts/empty.pug + +block pageContent + .container.mx-auto.px-4.py-8 + h1.text-3xl.font-bold.mb-8 + span.text-gray-600 标签: + = tag + + .grid.grid-cols-1.gap-6(class="md:grid-cols-2 lg:grid-cols-3") + each article in articles + .bg-white.shadow-md.rounded-lg.overflow-hidden + if article.featured_image + img.w-full.h-48.object-cover(src=article.featured_image alt=article.title) + .p-6 + h2.text-xl.font-semibold.mb-2 + a(href=`/articles/${article.slug}` class="hover:text-blue-600")= article.title + if article.excerpt + p.text-gray-600.mb-4= article.excerpt + .flex.justify-between.items-center + .text-sm.text-gray-500 + span.mr-4 + i.fas.fa-calendar-alt.mr-1 + = new Date(article.published_at).toLocaleDateString() + span + i.fas.fa-eye.mr-1 + = article.view_count + " 阅读" + if article.category + a.text-sm.bg-blue-100.text-blue-600.px-3.py-1.rounded-full(href=`/articles/category/${article.category}` class="hover:bg-blue-200")= article.category + + if !articles.length + .text-center.py-8 + p.text-gray-500 该标签下暂无文章 diff --git a/src/views/page/auth/no-auth.pug b/src/views/page/auth/no-auth.pug index 5db22eb..d578636 100644 --- a/src/views/page/auth/no-auth.pug +++ b/src/views/page/auth/no-auth.pug @@ -1,4 +1,4 @@ -extends /layouts/page.pug +extends /layouts/empty.pug block pageContent .no-auth-container diff --git a/src/views/page/index/index.pug b/src/views/page/index/index.pug index 4224ea3..69232cb 100644 --- a/src/views/page/index/index.pug +++ b/src/views/page/index/index.pug @@ -14,7 +14,9 @@ mixin item(url, desc) mixin card(blog) .article-card(class="bg-white rounded-[12px] shadow p-6 transition hover:shadow-lg border border-gray-100") h3.article-title(class="text-lg font-semibold text-gray-900 mb-2") - a(href="/article/1" class="hover:text-blue-600 transition-colors duration-200") #{blog.title} + a(href="/articles/"+blog.slug class="hover:text-blue-600 transition-colors duration-200") #{blog.title} + if blog.status === "draft" + span(class="ml-2 px-2 py-0.5 text-xs bg-yellow-200 text-yellow-800 rounded align-middle") 未发布 p.article-meta(class="text-sm text-gray-400 mb-3 flex") span(class="mr-2 line-clamp-1" title=blog.author) span 作者: @@ -30,7 +32,7 @@ mixin card(blog) style="height: 2.8em; overflow: hidden;" ) | #{blog.description} - a(href="/article/1" class="inline-block text-sm text-blue-600 hover:underline transition-colors duration-200") 阅读全文 → + a(href="/articles/"+blog.slug class="inline-block text-sm text-blue-600 hover:underline transition-colors duration-200") 阅读全文 → mixin empty() .div-placeholder(class="h-[100px] w-full bg-gray-100 text-center flex items-center justify-center text-[16px]") diff --git a/src/views/page/login/index.pug b/src/views/page/login/index.pug index 765ae21..e1ee926 100644 --- a/src/views/page/login/index.pug +++ b/src/views/page/login/index.pug @@ -1,4 +1,4 @@ -extends /layouts/pure.pug +extends /layouts/empty.pug block pageScripts script(src="js/login.js") diff --git a/src/views/page/register/index.pug b/src/views/page/register/index.pug index 96809e3..72b3d67 100644 --- a/src/views/page/register/index.pug +++ b/src/views/page/register/index.pug @@ -1,4 +1,4 @@ -extends /layouts/pure.pug +extends /layouts/empty.pug block pageHead style.