From ecf730bda71a0446494e5898e1d8ba2a214abbb3 Mon Sep 17 00:00:00 2001 From: xy <10816187@qq.com> Date: Wed, 5 Aug 2026 20:38:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=A0=B9=E6=8D=AE=E5=88=86=E7=B1=BB?= =?UTF-8?q?=E5=92=8Ctag=E7=AD=9B=E9=80=89=E6=96=87=E7=AB=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 34 ++ src/components/IndexSidebar.astro | 38 ++- src/components/PostList.astro | 2 +- src/pages/filter.astro | 505 ++++++++++++++++++++++++++++++ src/styles/global.css | 2 + src/types/post.ts | 4 +- 6 files changed, 568 insertions(+), 17 deletions(-) create mode 100644 AGENTS.md create mode 100644 src/pages/filter.astro diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..64aa612 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,34 @@ +# Project Scope Rules + +## 文件范围限制 + +本项目根目录: + +./ + +所有操作必须限制在当前项目目录内。 + +禁止: +- 访问项目外文件 +- 修改父级目录文件 +- 创建项目外文件 +- 修改用户目录中的其他内容 +- 禁止访问服务端仓库 + +## 修改规则 + +修改前必须: +1. 列出将修改的文件 +2. 说明修改原因 +3. 等待确认 + +禁止修改: + +node_modules/ +.git/ +build/ +.env + +## 开发环境 + +默认认为已经启动服务,端口为4321 diff --git a/src/components/IndexSidebar.astro b/src/components/IndexSidebar.astro index 30cd0e2..55134ad 100644 --- a/src/components/IndexSidebar.astro +++ b/src/components/IndexSidebar.astro @@ -1,12 +1,15 @@ --- import Folder from "@/assets/icon/folder.png?url"; -import Tag from "@/assets/icon/tag.png?url"; -import type { CategoriesStats } from "@/types"; +import TagIcon from "@/assets/icon/tag.png?url"; +import type { CategoriesStats,Tag } from "@/types"; import { createHttp } from "@/lib/request"; const http = createHttp(Astro.request.headers); const { data: categories } = await http.get>("categories/stats"); + +const { data: tags } = + await http.get>("posts/tags"); --- @@ -126,7 +134,7 @@ const { data: categories } = } .tag:hover { - background: #dfe6fd; + background: var(--tag-hover-bg); } @media (max-width: 860px) { diff --git a/src/components/PostList.astro b/src/components/PostList.astro index ecd62da..f171f87 100644 --- a/src/components/PostList.astro +++ b/src/components/PostList.astro @@ -177,7 +177,7 @@ posts.map((post) => { } .tag:hover { - background: #dfe6fd; + background: var(--tag-hover-bg); } /* ========== 分页 ========== */ diff --git a/src/pages/filter.astro b/src/pages/filter.astro new file mode 100644 index 0000000..c36d0cc --- /dev/null +++ b/src/pages/filter.astro @@ -0,0 +1,505 @@ +--- +import Layout from "@/layouts/Layout.astro"; +import Calendar from "@/assets/icon/calendar.png?url"; +import Eye from "@/assets/icon/eye.png?url"; +import Folder from "@/assets/icon/folder.png?url"; +import { formatDatetime } from "@/lib/format.ts"; +import { createHttp } from "@/lib/request"; +import type { CategoriesStats, Post, Tag } from "@/types"; + +const http = createHttp(Astro.request.headers); + +const searchParams = Astro.url.searchParams; +const type = searchParams.get("type") ?? ""; +const code = searchParams.get("code") ?? ""; + +/** + * 搜索接口返回结构:tags 为 base64 编码的 JSON 字符串、 + * 阅读数字段为 views,且不含 cover / content,这里做归一化 + */ +type SearchPost = Omit & { + views: number; + tags: string; + cover?: string; + content?: string; +}; + +const decodeTags = (raw: string): Tag[] => { + try { + return JSON.parse(Buffer.from(raw, "base64").toString("utf8")) as Tag[]; + } catch { + return []; + } +}; + +const normalizePost = (post: SearchPost): Post => ({ + ...post, + view: post.views, + tags: decodeTags(post.tags), + cover: post.cover ?? "", + content: post.content ?? "", +}); + +const [{ data: categories }, { data: allTags }, { data: rawPosts }] = await Promise.all([ + http.get>("categories/stats"), + http.get>("posts/tags"), + http.get>("posts/search", { + searchParams: { + category_code: type === "category" ? code : "", + tag_code: type === "tag" ? code : "", + }, + }), +]); + +const searchPosts = rawPosts.map(normalizePost); + +const category = + type === "category" ? categories.find((item) => item.code === code) : undefined; + +/** 展示全部标签,按名称排序 */ +const tags = [...allTags].sort((a, b) => a.name.localeCompare(b.name, "zh-CN")); + +const tag = type === "tag" ? tags.find((item) => item.code === code) : undefined; +const valid = Boolean(category || tag); + +const posts = searchPosts; + +const filterLabel = category ? "分类" : tag ? "标签" : ""; +const filterTitle = category?.name ?? tag?.name ?? ""; +const count = category ? category.post_count : posts.length; + +const pageTitle = valid ? `${filterLabel}:${filterTitle}` : "筛选"; +const pageDescription = valid + ? `${filterLabel}「${filterTitle}」下的文章,共 ${count} 篇` + : "按分类或标签筛选文章"; + +const filterUrl = (type: string, code: string) => + `/filter?type=${type}&code=${encodeURIComponent(code)}`; + +const isActive = (targetType: string, targetCode: string) => + type === targetType && code === targetCode; +--- + + +
+
+
+ { + valid ? ( + <> + {filterLabel} +

{filterTitle}

+ 共 {count} 篇 + + ) : ( + <> +

未找到该筛选条件

+

请从下方分类或标签进入

+ + ) + } + 返回首页 +
+ + + + { + valid && + (posts.length > 0 ? ( + + ) : ( +
+

无数据

+
+ )) + } +
+
+
+ + diff --git a/src/styles/global.css b/src/styles/global.css index 0cf96e8..8a3063e 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -13,6 +13,7 @@ --primary: #4f6df5; --tag-bg: #eef1fe; --tag-text: #4f6df5; + --tag-hover-bg: #dfe6fd; } .dark { @@ -24,6 +25,7 @@ --primary: #1e293b; --tag-bg: #27272a; --tag-text: #f3f4f6; + --tag-hover-bg: #334155; } html, diff --git a/src/types/post.ts b/src/types/post.ts index fdee23d..4f9276c 100644 --- a/src/types/post.ts +++ b/src/types/post.ts @@ -9,6 +9,8 @@ export type Post = { slug: string; cover: string; category_name: string; + category_code: string; + category_id?: number; published_at: string; view: number; content: string; @@ -18,6 +20,6 @@ export type Post = { export type CategoriesStats = { id: number; name: string; + code: string post_count: number; } -