+
)
}
diff --git a/src/index.css b/src/index.css
index f5c83d1..13d3444 100644
--- a/src/index.css
+++ b/src/index.css
@@ -3,29 +3,32 @@
@plugin "@tailwindcss/typography";
:root {
- --ink: #0b0f17;
- --paper: #fbf7ef;
- --paper-2: #fffdf8;
- --wood-1: #2b1c12;
- --wood-2: #3b2417;
- --wood-3: #4a2d1d;
- --accent: #b42318;
- --accent-2: #0ea5a4;
+ --bg: #f5f5f7;
+ --bg-2: #ffffff;
+ --text: #1d1d1f;
+ --muted: rgba(29, 29, 31, 0.62);
+ --card: rgba(255, 255, 255, 0.74);
+ --card-2: rgba(255, 255, 255, 0.58);
+ --border: rgba(0, 0, 0, 0.08);
+ --border-2: rgba(0, 0, 0, 0.12);
+ --shadow: 0 18px 60px rgba(0, 0, 0, 0.12);
+ --shadow-2: 0 40px 120px rgba(0, 0, 0, 0.14);
+ --accent: #0a84ff;
}
html {
- color: var(--ink);
- background: radial-gradient(1200px 800px at 20% 10%, rgba(180, 35, 24, 0.12), transparent 55%),
- radial-gradient(900px 700px at 85% 18%, rgba(14, 165, 164, 0.14), transparent 55%),
- linear-gradient(180deg, #0f172a 0%, #101827 45%, #0b1220 100%);
+ color: var(--text);
+ background:
+ radial-gradient(1200px 800px at 16% -10%, rgba(10, 132, 255, 0.14), transparent 56%),
+ radial-gradient(900px 680px at 92% 0%, rgba(88, 86, 214, 0.10), transparent 55%),
+ linear-gradient(180deg, var(--bg) 0%, var(--bg-2) 64%, #ffffff 100%);
overflow-x: hidden;
}
body {
margin: 0;
min-height: 100vh;
- font-family: "Noto Sans SC", ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial,
- "Apple Color Emoji", "Segoe UI Emoji";
+ font-family: "Noto Sans SC", ui-sans-serif, system-ui, -apple-system, Segoe UI, Helvetica, Arial;
direction: ltr;
text-align: left;
background: transparent;
@@ -37,38 +40,51 @@ body {
}
::selection {
- background: rgba(180, 35, 24, 0.25);
+ background: rgba(10, 132, 255, 0.18);
}
.font-display {
- font-family: "Noto Serif SC", ui-serif, Georgia, "Times New Roman", Times, serif;
+ font-family: "Noto Sans SC", ui-sans-serif, system-ui, -apple-system, Segoe UI, Helvetica, Arial;
+ letter-spacing: -0.02em;
}
.surface {
- background: linear-gradient(180deg, rgba(255, 253, 248, 0.92), rgba(251, 247, 239, 0.88));
- border: 1px solid rgba(255, 255, 255, 0.18);
- box-shadow: 0 30px 90px rgba(0, 0, 0, 0.35);
- backdrop-filter: blur(10px);
+ background: linear-gradient(180deg, var(--card), var(--card-2));
+ border: 1px solid var(--border);
+ box-shadow: var(--shadow);
+ backdrop-filter: blur(14px);
}
.grain {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E");
background-size: 240px 240px;
- mix-blend-mode: overlay;
+ mix-blend-mode: multiply;
pointer-events: none;
}
-.shelf-wood {
- background: linear-gradient(180deg, rgba(74, 45, 29, 0.95), rgba(43, 28, 18, 0.98));
+.shelf-tray {
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.60), rgba(255, 255, 255, 0.36));
+ border: 1px solid var(--border);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75);
+ backdrop-filter: blur(18px);
}
.btn-ghost {
- border: 1px solid rgba(255, 255, 255, 0.18);
- background: rgba(255, 255, 255, 0.06);
+ border: 1px solid var(--border);
+ background: rgba(255, 255, 255, 0.55);
}
.btn-ghost:hover {
- background: rgba(255, 255, 255, 0.11);
+ background: rgba(255, 255, 255, 0.78);
+}
+
+.hide-scrollbar {
+ scrollbar-width: none;
+}
+
+.hide-scrollbar::-webkit-scrollbar {
+ width: 0;
+ height: 0;
}
@media (prefers-reduced-motion: reduce) {
diff --git a/src/pages/BookDetail.tsx b/src/pages/BookDetail.tsx
index 9a0322b..710480e 100644
--- a/src/pages/BookDetail.tsx
+++ b/src/pages/BookDetail.tsx
@@ -14,13 +14,13 @@ export const BookDetail: React.FC = () => {
-
+
未找到这本书
-
它可能被借走了,或者书名写错了。
+
它可能被借走了,或者书名写错了。
回到书架
→
@@ -37,23 +37,23 @@ export const BookDetail: React.FC = () => {
-
-
+
+
← 书架
{book.author} · {book.year}
@@ -64,18 +64,18 @@ export const BookDetail: React.FC = () => {
{book.title}
{book.subtitle && (
- {book.subtitle}
+ {book.subtitle}
)}
- {book.blurb}
+ {book.blurb}
{book.tags.map((t) => (
{t}
@@ -93,24 +93,24 @@ export const BookDetail: React.FC = () => {
/>
-
+
目录
{book.chapters.map((c, idx) => (
-
-
第 {idx + 1} 章
- {c.title}
+ 第 {idx + 1} 章
+ {c.title}
))}
-
+
试读
-
+
{book.chapters[0]?.content.split('\n\n').map((p, i) => (
{p}
))}
diff --git a/src/pages/Categories.tsx b/src/pages/Categories.tsx
index b062f1d..1a2a5ea 100644
--- a/src/pages/Categories.tsx
+++ b/src/pages/Categories.tsx
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'
import { Header } from '../components/Header'
import { Footer } from '../components/Footer'
import { Categories } from '../clientsdk/sdk.gen'
+import type { Category } from '../clientsdk/types.gen'
import { createClient } from '../clientsdk/client'
import { customQuerySerializer } from '../clientsdk/querySerializer'
import { TENANT_SLUG, TENANT_API_KEY, API_URL } from '../config'
@@ -16,7 +17,7 @@ const client = createClient({
})
export const CategoriesPage: React.FC = () => {
- const [categories, setCategories] = useState
([])
+ const [categories, setCategories] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
@@ -33,7 +34,7 @@ export const CategoriesPage: React.FC = () => {
},
})
- setCategories((response as any)?.data?.docs || [])
+ setCategories(response.data?.docs ?? [])
} catch (err) {
setError(err instanceof Error ? err.message : '加载失败')
console.error('获取分类失败:', err)
@@ -46,17 +47,17 @@ export const CategoriesPage: React.FC = () => {
}, [])
return (
-
+
- 📂 文章分类
- 浏览所有分类
+ 文章分类
+ 浏览所有分类
{error && (
-
+
错误: {error}
)}
@@ -64,26 +65,26 @@ export const CategoriesPage: React.FC = () => {
{loading
? Array.from({ length: 6 }).map((_, i) => (
-
{!loading && categories.length === 0 && !error && (
)}
diff --git a/src/pages/CategoryDetail.tsx b/src/pages/CategoryDetail.tsx
index a399d89..83aab56 100644
--- a/src/pages/CategoryDetail.tsx
+++ b/src/pages/CategoryDetail.tsx
@@ -5,6 +5,7 @@ import { Footer } from '../components/Footer'
import { PostCard } from '../components/PostCard'
import { PostCardSkeleton } from '../components/PostCardSkeleton'
import { Posts, Categories } from '../clientsdk/sdk.gen'
+import type { Category, Post } from '../clientsdk/types.gen'
import { createClient } from '../clientsdk/client'
import { customQuerySerializer } from '../clientsdk/querySerializer'
import { TENANT_SLUG, TENANT_API_KEY, API_URL } from '../config'
@@ -20,11 +21,13 @@ const client = createClient({
export const CategoryDetail: React.FC = () => {
const { slug } = useParams<{ slug: string }>()
- const [posts, setPosts] = useState
([])
- const [category, setCategory] = useState(null)
+ const [posts, setPosts] = useState([])
+ const [category, setCategory] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
+ const isCategory = (cat: string | Category): cat is Category => typeof cat !== 'string'
+
useEffect(() => {
if (!slug) return
@@ -55,15 +58,12 @@ export const CategoryDetail: React.FC = () => {
}),
])
- const categoryDocs = (categoriesRes as any)?.data?.docs || []
- if (categoryDocs[0]) {
- setCategory(categoryDocs[0])
- }
+ const categoryDocs = categoriesRes.data?.docs ?? []
+ if (categoryDocs[0]) setCategory(categoryDocs[0])
- const allDocs = (postsRes as any)?.data?.docs || []
- // categories is an array, check if any category in the array matches the slug
- const categoryPosts = allDocs.filter((post: any) =>
- post.categories?.some((cat: any) => cat.slug === slug)
+ const allDocs = postsRes.data?.docs ?? []
+ const categoryPosts = allDocs.filter((post) =>
+ post.categories?.some((cat) => (isCategory(cat) ? cat.slug === slug : false))
)
setPosts(categoryPosts)
@@ -93,9 +93,15 @@ export const CategoryDetail: React.FC = () => {
})
}
- const getCategoryTitle = (post: any): string | undefined => {
- // categories is an array, get the first one
- return post.categories?.[0]?.title
+ const getCategoryTitle = (post: Post): string | undefined => {
+ const first = post.categories?.find((c) => isCategory(c))
+ return first?.title
+ }
+
+ const getExcerpt = (post: Post): string => {
+ const html = post.content_html ?? ''
+ const clean = html ? stripHtml(html) : ''
+ return clean || post.title
}
const handlePostClick = (postSlug: string) => {
@@ -103,19 +109,19 @@ export const CategoryDetail: React.FC = () => {
}
return (
-
+
-
- 📂 {category?.title || '分类'}
+
+ {category?.title || '分类'}
-
探索该分类下的所有内容
+ 探索该分类下的所有内容
{error && (
-
+
错误: {error}
)}
@@ -127,7 +133,7 @@ export const CategoryDetail: React.FC = () => {
handlePostClick(post.slug)}
@@ -137,7 +143,7 @@ export const CategoryDetail: React.FC = () => {
{!loading && posts.length === 0 && !error && (
)}
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx
index acb4fbd..26201a9 100644
--- a/src/pages/Home.tsx
+++ b/src/pages/Home.tsx
@@ -15,12 +15,12 @@ export const Home: React.FC = () => {
-
-
+
+
提示
-
- 点击书脊选中,右上角会出现“翻开阅读”。每本书都有一个独立分页面:
- /books/:slug。
+
+ 点击书脊选中,右上角会出现“翻开阅读”。每本书都有一个独立分页:
+ /books/:slug。
diff --git a/src/pages/PostDetail.tsx b/src/pages/PostDetail.tsx
index fc1c068..a080fc6 100644
--- a/src/pages/PostDetail.tsx
+++ b/src/pages/PostDetail.tsx
@@ -3,6 +3,7 @@ import { useParams } from 'react-router-dom'
import { Header } from '../components/Header'
import { Footer } from '../components/Footer'
import { Posts } from '../clientsdk/sdk.gen'
+import type { Post } from '../clientsdk/types.gen'
import { createClient } from '../clientsdk/client'
import { customQuerySerializer } from '../clientsdk/querySerializer'
import { TENANT_SLUG, TENANT_API_KEY, API_URL } from '../config'
@@ -18,7 +19,7 @@ const client = createClient({
export const PostDetail: React.FC = () => {
const { slug } = useParams<{ slug: string }>()
- const [post, setPost] = useState
(null)
+ const [post, setPost] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
@@ -43,8 +44,8 @@ export const PostDetail: React.FC = () => {
},
})
- const docs = (response as any)?.data?.docs || []
- setPost(docs[0] || null)
+ const docs = response.data?.docs ?? []
+ setPost(docs[0] ?? null)
} catch (err) {
setError(err instanceof Error ? err.message : '加载失败')
console.error('获取文章失败:', err)
@@ -56,11 +57,24 @@ export const PostDetail: React.FC = () => {
fetchPost()
}, [slug])
- const getCategoryTitle = (p: any): string | undefined => {
- // categories is an array, get the first one
- return p?.categories?.[0]?.title
+ const isCategory = (cat: unknown): cat is { title?: string } => typeof cat === 'object' && cat !== null
+
+ const getCategoryTitle = (p: Post): string | undefined => {
+ const first = p.categories?.find((c) => isCategory(c) && typeof (c as { title?: unknown }).title === 'string') as
+ | { title: string }
+ | undefined
+ return first?.title
}
+ const getHeroImage = (p: Post): { url: string; alt: string } | null => {
+ const img = p.heroImage
+ if (!img || typeof img === 'string') return null
+ if (!img.url) return null
+ return { url: img.url, alt: img.alt || p.title }
+ }
+
+ const hero = post ? getHeroImage(post) : null
+
const formatDate = (dateString: string): string => {
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN', {
@@ -72,16 +86,16 @@ export const PostDetail: React.FC = () => {
if (loading) {
return (
-
+
@@ -92,14 +106,14 @@ export const PostDetail: React.FC = () => {
if (error || !post) {
return (
-
+
{error || '文章不存在'}
@@ -111,27 +125,27 @@ export const PostDetail: React.FC = () => {
}
return (
-
+
{getCategoryTitle(post) && (
-
+
{getCategoryTitle(post)}
)}
- {post.title}
-
+
{post.title}
+
{formatDate(post.createdAt)}
- {post.heroImage && (
+ {hero && (

)}
@@ -141,10 +155,10 @@ export const PostDetail: React.FC = () => {
dangerouslySetInnerHTML={{ __html: post.content_html || '' }}
/>
-
+