feat: 首页改版

This commit is contained in:
2026-07-20 22:14:20 +08:00
parent 95320835b6
commit a5f4d0a28d
16 changed files with 359 additions and 130 deletions

View File

@@ -17,5 +17,10 @@ export default defineConfig({
}), }),
vite: { vite: {
plugins: [], plugins: [],
server: {
watch: {
usePolling: true,
},
},
}, },
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
src/assets/icon/eye.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
src/assets/icon/folder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -0,0 +1,87 @@
---
import Folder from "@/assets/icon/folder.png?url";
import type {CategoriesStats} from "@/types";
interface Props {
categories: CategoriesStats[]
}
const {categories} = Astro.props
---
<aside class="sidebar">
<div class="widget">
<h3 class="widget-title">
<img src={Folder} alt="Folder">
文章分类
</h3>
<ul class="category-list">
{
categories.map((category) => (
<li>
<a>{category.name}</a><span class="count">{category.post_count}</span>
</li>
))
}
</ul>
</div>
</aside>
<style>
.sidebar {
width: 300px;
height: 100%;
}
.widget {
background: var(--card-bg);
border-radius: 12px;
box-shadow: var(--shadow);
padding: 22px;
}
.widget-title {
font-size: 16px;
font-weight: 700;
margin-bottom: 16px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border);
display: flex;
gap: 8px;
}
.widget-title img {
width: 18px;
height: 18px;
}
/* 分类 & 标签云 */
.category-list {
list-style: none;
}
.category-list li {
display: flex;
justify-content: space-between;
padding: 8px 4px;
font-size: 14px;
border-bottom: 1px dashed var(--border);
}
.category-list li:last-child {
border-bottom: none;
}
.category-list a:hover {
color: var(--primary);
}
.category-list .count {
color: var(--text-secondary);
font-size: 13px;
}
@media (max-width: 860px) {
.sidebar{
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,35 @@
---
import type {Post, CategoriesStats} from "@/types/post";
import IndexSidebar from "@/components/IndexSidebar.astro";
import PostList from '@/components/PostList.astro'
interface Props {
posts: Post[]
page: number
totalPage: number
categories: CategoriesStats[]
}
const {posts, page, totalPage, categories} = Astro.props
---
<div class="container">
<PostList posts={posts} page={page} totalPage={totalPage}/>
<IndexSidebar categories={categories}/>
</div>
<style>
.container {
width: 100%;
display: flex;
gap: 28px;
}
@media (max-width: 860px) {
.container{
flex-direction: column-reverse;
gap: 16px;
}
}
</style>

View File

@@ -1,4 +1,7 @@
--- ---
import Eye from "@/assets/icon/eye.png?url";
import Folder from "@/assets/icon/folder.png?url";
import Calendar from "@/assets/icon/calendar.png?url";
import type {Post} from "@/types/post"; import type {Post} from "@/types/post";
import {formatDatetime} from "@/lib/format.ts"; import {formatDatetime} from "@/lib/format.ts";
@@ -11,155 +14,232 @@ interface Props {
const {posts, page, totalPage} = Astro.props const {posts, page, totalPage} = Astro.props
posts.map((post) => { posts.map((post) => {
post.published_at = formatDatetime(post.published_at, 'YYYY 年 M 月 D 日') post.published_at = formatDatetime(post.published_at, 'YYYY-MM-DD')
}) })
--- ---
<section class="section">
<div class="posts"> <div class="post-list">
{ {posts.map(post => (
posts.map((post) => ( <a href={`/post/${post.slug}`}>
<a class="post" href={`/posts/${post.slug}`}> <article class="post-card">
<div class="published-at">{post.published_at}</div> <div class="post-cover">
<div class="category-name">{post.category_name}</div> <img src={post.cover} alt={post.title} loading="lazy"/>
<div class="post-title">{post.title}</div> </div>
<div class="link"> <div class="post-body">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" <div class="post-meta">
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" <span>
class="lucide lucide-arrow-up-right-icon lucide-arrow-up-right"> <img src={Calendar} alt="Calendar"/>
<path d="M7 7h10v10"/> {post.published_at}
<path d="M7 17 17 7"/> </span>
</svg> <span>
</div> <img src={Folder} alt="Folder"/>
{post.category_name}
</span>
<span>
<img src={Eye} alt="Eye"/>
{post.view}
</span>
</div>
<h3 class="post-title">
{post.title}
</h3>
<div class="post-summary">
{post.summary}
</div>
<!--<div class="tags">-->
<!-- <div class="tag">HTML</div>-->
<!-- <div class="tag">CSS</div>-->
<!-- <div class="tag">响应式</div>-->
<!--</div>-->
</div>
</article>
</a> </a>
)) ))}
} </div>
</div>
<!--分页器--> <!--分页器-->
{totalPage>1&& {totalPage > 1 &&
<div class="pagination"> <nav class="pagination" aria-label="分页">
{page > 1 ? (<a href={`/page/${page - 1}`}>上一页</a>) : ( {page > 1 ? <a href={`/page/${page - 1}`} class="link">« 上一页</a> : <span class="link disabled">« 上一页</span>}
<span class="disabled" aria-disabled="true">上一页</span>)} {Array.from({length: totalPage}).map((_, index) => {
if (page === index + 1) {
{Array.from({length: totalPage}).map((_, index) => { return <span class="link current">{index + 1}</span>
if (page === index + 1) { }
return <span class="pagination-number disabled" aria-disabled="true">{index + 1}</span> return <a class="link" href={`/page/${index + 1}`}>{index + 1}</a>
} })}
return <a class="pagination-number" href={`/page/${index + 1}`}>{index + 1}</a> {page < totalPage ? <a href={`/page/${page + 1}`} class="link">下一页 »</a> :
})} <span class="link disabled">下一页 »</span>}
</nav>}
{page < totalPage ? (<a href={`/page/${page + 1}`}>下一页</a>) : ( </section>
<span class="disabled" aria-disabled="true">下一页</span>)}
</div>}
<style> <style>
.posts { .section {
flex: 1;
height: 100%;
}
.post-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
} }
.post { .post-card {
padding: 16px; background: var(--card-bg);
border-radius: 12px; border-radius: 12px;
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px; box-shadow: var(--shadow);
padding: 20px;
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
gap: 22px;
}
.post-card:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-hover);
}
.post-cover {
flex-shrink: 0;
width: 160px;
height: 110px;
border-radius: 8px;
display: flex; display: flex;
align-items: center; align-items: center;
transition: transform 0.2s linear; justify-content: center;
background: #fff; font-size: 40px;
} color: #fff;
.post:hover {
box-shadow: rgba(33, 35, 38, 0.1) 0px 10px 10px -10px;
}
.published-at {
white-space: nowrap;
font-size: 14px;
color: #4b5563;
min-width: 130px;
margin-right: 20px;
}
.category-name {
border-radius: 999px;
background: #fceeda;
padding: 4px 12px;
font-size: 14px;
margin-right: 24px;
}
.post-title {
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
} }
.link { .post-cover img {
width: 100%;
height: 100%;
object-fit: cover;
} }
.link svg { .post-body {
width: 16px; flex: 1;
height: 16px; min-width: 0;
color: #6b7280;
} }
.pagination { .post-meta {
margin: 24px 0; font-size: 13px;
color: var(--text-secondary);
margin-bottom: 6px;
display: flex; display: flex;
justify-content: flex-end; gap: 14px;
gap: 12px; flex-wrap: wrap;
align-items: center;
font-size: 14px;
} }
.pagination-number { .post-meta span {
display: flex;
align-items: center;
gap: 4px;
}
.post-meta span img {
width: 14px;
height: 14px;
display: block;
}
.post-title {
font-size: 20px;
font-weight: 700;
margin-bottom: 8px;
line-height: 1.4;
}
.post-summary {
font-size: 14px;
color: var(--text-secondary);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-bottom: 12px;
}
.tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.tag {
font-size: 12px;
color: #4f6df5;
background: #eef1fe;
padding: 3px 10px;
border-radius: 999px; border-radius: 999px;
color: #fafafa; transition: background 0.2s;
background: #1f2937; }
width: 20px;
height: 20px; .tag:hover {
background: #dfe6fd;
}
/* ========== 分页 ========== */
.pagination {
display: flex; display: flex;
justify-content: center; justify-content: center;
gap: 8px;
margin-top: 32px;
}
.link {
display: inline-flex;
align-items: center; align-items: center;
justify-content: center;
min-width: 38px;
height: 38px;
padding: 0 12px;
border-radius: 8px;
background: var(--card-bg);
border: 1px solid var(--border);
font-size: 14px;
color: var(--text-secondary);
transition: all 0.2s;
} }
.pagination .disabled { .link:hover,
color: #9ca3af; .link.current {
background: var(--primary);
border-color:var(--primary);
color: #fff;
}
.link.disabled {
color: var(--text-secondary);
background: var(--card-bg);
border-color: var(--border);
cursor: not-allowed; cursor: not-allowed;
pointer-events: none;
text-decoration: none;
} }
.pagination-number.disabled {
background: #d1d5db;
}
@media (max-width: 768px) { @media (max-width: 860px) {
.posts { .post-card {
gap: 12px; padding: 16px;
} }
.published-at { .post-cover {
margin-right: 8px; width: 128px;
} height: 88px;
.category-name {
display: none;
} }
} }
:global(.dark) .post { @media (max-width: 560px) {
background: #171717; .post-card {
padding: 12px;
}
.post-title {
font-size: 16px;
}
} }
:global(.dark) .published-at {
color: #ffffff;
}
:global(.dark) .category-name {
background: #e5e5e5;
color: #0a0a0a;
}
</style> </style>

View File

@@ -1,5 +1,5 @@
export const metadata = { export const metadata = {
siteTitle: "Uroboros", siteTitle: "Uroboros",
postPageSize: 12 postPageSize: 6
} }

View File

@@ -12,13 +12,12 @@ import Layout from "@/layouts/Layout.astro";
<style> <style>
.not-found{ .not-found{
flex: 1; flex: 1;
height: 100%; width: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
margin-top: -100px;
} }
.back-home{ .back-home{

View File

@@ -1,7 +1,7 @@
--- ---
import Layout from "@/layouts/Layout.astro"; import Layout from "@/layouts/Layout.astro";
import PostList from "@/components/PostList.astro"; import IndexView from "@/components/IndexView.astro";
import type {Post} from "@/types/post"; import type {Post, CategoriesStats} from "@/types/post";
import {createHttp} from "@/lib/request"; import {createHttp} from "@/lib/request";
import {metadata} from "@/consts.ts"; import {metadata} from "@/consts.ts";
@@ -13,10 +13,11 @@ const {list, total} = await http.get<ApiPageResponse<Post>>("post", {
page_size: metadata.postPageSize page_size: metadata.postPageSize
} }
}); });
const totalPage = Math.ceil(total / metadata.postPageSize) const totalPage = Math.ceil(total / metadata.postPageSize)
const {data: categories} = await http.get<ApiPageResponse<CategoriesStats>>("category/stats");
--- ---
<Layout> <Layout>
<PostList posts={list} page={1} totalPage={totalPage}/> <IndexView categories={categories} posts={list} page={1} totalPage={totalPage}/>
</Layout> </Layout>

View File

@@ -1,9 +1,9 @@
--- ---
import Layout from "@/layouts/Layout.astro"; import Layout from "@/layouts/Layout.astro";
import PostList from "@/components/PostList.astro"; import type {CategoriesStats, Post} from "@/types/post";
import type {Post} from "@/types/post";
import {createHttp} from "@/lib/request"; import {createHttp} from "@/lib/request";
import {metadata} from "@/consts.ts"; import {metadata} from "@/consts.ts";
import IndexView from "@/components/IndexView.astro";
const {page} = Astro.params const {page} = Astro.params
@@ -25,8 +25,10 @@ const totalPage = Math.ceil(total / metadata.postPageSize)
if (totalPage < page) { if (totalPage < page) {
return Astro.redirect('/404') return Astro.redirect('/404')
} }
const {data: categories} = await http.get<ApiPageResponse<CategoriesStats>>("category/stats");
--- ---
<Layout> <Layout>
<PostList posts={list} page={Number(page)} totalPage={totalPage}/> <IndexView categories={categories} posts={list} page={Number(page)} totalPage={totalPage}/>
</Layout> </Layout>

View File

@@ -134,7 +134,7 @@ const published_at = formatDatetime(data.published_at, 'YYYY 年 M 月 D 日')
} }
:global(.dark) .post { :global(.dark) .post {
background: #171717; background: var(--card-bg);
box-shadow: none; box-shadow: none;
border: none; border: none;
} }

View File

@@ -1,3 +1,8 @@
* {
margin: 0;
padding: 0;
}
a { a {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;

View File

@@ -2,11 +2,6 @@
font-size: 1.1em; font-size: 1.1em;
} }
h1{
margin-block-start: 0;
margin-block-end: 0;
}
.markdown-body h1, .markdown-body h1,
.markdown-body h2, .markdown-body h2,
.markdown-body h3, .markdown-body h3,

View File

@@ -2,12 +2,24 @@
@import "css-reset.css"; @import "css-reset.css";
:root { :root {
--layout-width: 980px; --layout-width: 1080px;
--background: #ffffff; --background: #f6f7f9;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
--shadow-hover: 0 8px 24px rgba(0, 0, 0, 0.10);
--card-bg: #ffffff;
--text: #2b2f36;
--text-secondary: #6b7280;
--border: #e5e7eb;
--primary: #2b2f36;
} }
.dark { .dark {
--background: #0a0a0a; --background: #0a0a0a;
--card-bg: #161b22;
--text: #2b2f36;
--text-secondary: #e5e7eb;
--border: #e5e7eb;
--primary: #3d414a;
} }
html, html,
@@ -18,7 +30,7 @@ body {
} }
html { html {
background: #f8fafc; background: var(--background);
} }
html.dark { html.dark {
@@ -37,6 +49,8 @@ main {
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
padding: 32px; padding: 32px;
display: flex;
flex-direction: column;
} }
@media (max-width: 768px) { @media (max-width: 768px) {

View File

@@ -1,4 +1,4 @@
export type Post = { export type Post = {
title: string; title: string;
summary: string; summary: string;
slug: string; slug: string;
@@ -8,3 +8,9 @@ export type Post = {
view: number; view: number;
content: string; content: string;
} }
export type CategoriesStats = {
id: number;
name: string;
post_count: number;
}