manual save(2026-01-27 16:39)
This commit is contained in:
@@ -7,6 +7,7 @@ import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
globalIgnores(['src/clientsdk/**/*']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
|
||||
@@ -28,9 +28,9 @@ const BookSpine: React.FC<{
|
||||
selected: boolean
|
||||
onSelect: (event?: React.PointerEvent | React.MouseEvent) => void
|
||||
}> = ({ book, index, selected, onSelect }) => {
|
||||
const tilt = (index % 2 === 0 ? 1 : -1) * 0.9
|
||||
const height = 148 + (index % 4) * 14
|
||||
const width = 46 + (index % 3) * 6
|
||||
const tilt = (index % 2 === 0 ? 1 : -1) * 0.35
|
||||
const height = 168 + (index % 4) * 12
|
||||
const width = 52 + (index % 3) * 6
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -42,32 +42,32 @@ const BookSpine: React.FC<{
|
||||
if (e.key === 'Enter' || e.key === ' ') onSelect()
|
||||
}}
|
||||
className={
|
||||
'relative select-none rounded-lg cursor-pointer outline-none transition-transform duration-300 ' +
|
||||
'focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent focus-visible:ring-white/70'
|
||||
'group relative select-none rounded-[14px] cursor-pointer outline-none transition-transform duration-300 ' +
|
||||
'focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent focus-visible:ring-black/15'
|
||||
}
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
transform: selected
|
||||
? 'translateY(-10px) rotateZ(0deg)'
|
||||
? 'translateY(-12px) rotateZ(0deg)'
|
||||
: `translateY(0px) rotateZ(${tilt}deg)`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'absolute inset-0 rounded-lg shadow-[0_18px_40px_rgba(0,0,0,0.45)] ' +
|
||||
(selected ? 'ring-2 ring-white/60' : 'ring-1 ring-white/10')
|
||||
'absolute inset-0 rounded-[14px] shadow-[0_22px_60px_rgba(0,0,0,0.18)] ' +
|
||||
(selected ? 'ring-1 ring-black/12' : 'ring-1 ring-black/8')
|
||||
}
|
||||
style={{
|
||||
background: `linear-gradient(180deg, ${book.spineColor} 0%, rgba(0,0,0,0.2) 100%)`,
|
||||
background: `linear-gradient(180deg, ${book.spineColor} 0%, rgba(0,0,0,0.16) 100%)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="absolute inset-[1px] rounded-[7px]"
|
||||
className="absolute inset-[1px] rounded-[13px]"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(90deg, rgba(255,255,255,0.10), rgba(255,255,255,0.02) 38%, rgba(0,0,0,0.18) 74%, rgba(255,255,255,0.04))',
|
||||
'linear-gradient(90deg, rgba(255,255,255,0.12), rgba(255,255,255,0.03) 44%, rgba(0,0,0,0.14) 76%, rgba(255,255,255,0.05))',
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -76,13 +76,13 @@ const BookSpine: React.FC<{
|
||||
style={{
|
||||
height: 3,
|
||||
background: `linear-gradient(90deg, transparent, ${book.accentColor}, transparent)`,
|
||||
opacity: 0.9,
|
||||
opacity: selected ? 0.86 : 0.72,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="absolute inset-x-2 bottom-3">
|
||||
<div
|
||||
className="text-[11px] leading-tight text-white/90 font-semibold"
|
||||
className="text-[11px] leading-tight text-white/95 font-semibold"
|
||||
style={{
|
||||
writingMode: 'vertical-rl',
|
||||
textOrientation: 'mixed',
|
||||
@@ -92,7 +92,7 @@ const BookSpine: React.FC<{
|
||||
{book.title}
|
||||
</div>
|
||||
<div
|
||||
className="mt-2 text-[10px] text-white/70"
|
||||
className="mt-2 text-[10px] text-white/75"
|
||||
style={{ writingMode: 'vertical-rl', textOrientation: 'mixed' }}
|
||||
>
|
||||
{book.author}
|
||||
@@ -104,10 +104,19 @@ const BookSpine: React.FC<{
|
||||
style={{
|
||||
width: 10,
|
||||
background:
|
||||
'linear-gradient(180deg, rgba(255,255,255,0.16), rgba(0,0,0,0.2)), linear-gradient(90deg, rgba(0,0,0,0.4), transparent)',
|
||||
opacity: selected ? 0.8 : 0.45,
|
||||
'linear-gradient(180deg, rgba(255,255,255,0.14), rgba(0,0,0,0.16)), linear-gradient(90deg, rgba(0,0,0,0.26), transparent)',
|
||||
opacity: selected ? 0.72 : 0.40,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 rounded-[14px] opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||
style={{
|
||||
background:
|
||||
'radial-gradient(120px 160px at 30% 16%, rgba(255,255,255,0.26), transparent 62%), radial-gradient(120px 160px at 70% 90%, rgba(0,0,0,0.18), transparent 55%)',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -145,14 +154,23 @@ export const Bookshelf: React.FC<Props> = ({ books }) => {
|
||||
|
||||
return (
|
||||
<section aria-label="书架" className="w-full">
|
||||
<div className="surface relative overflow-hidden rounded-2xl">
|
||||
<div className="grain absolute inset-0 opacity-30" aria-hidden="true" />
|
||||
<div className="surface relative overflow-hidden rounded-2xl text-[color:var(--text)]">
|
||||
<div className="grain absolute inset-0 opacity-[0.22]" aria-hidden="true" />
|
||||
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{
|
||||
background:
|
||||
'radial-gradient(900px 520px at 18% 0%, rgba(10,132,255,0.18), transparent 60%), radial-gradient(760px 520px at 88% 12%, rgba(88,86,214,0.12), transparent 58%), linear-gradient(180deg, rgba(255,255,255,0.66), rgba(255,255,255,0.46))',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div className="relative px-5 py-5 sm:px-8 sm:py-7">
|
||||
<div className="flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-white/70 text-sm">在书脊上点一下,把书抽出来</p>
|
||||
<h2 className="font-display text-[28px] sm:text-[34px] leading-tight text-white">
|
||||
<p className="text-[color:var(--muted)] text-sm">点一下书脊,抽出一本书</p>
|
||||
<h2 className="font-display text-[28px] sm:text-[34px] leading-tight">
|
||||
从书架拿书
|
||||
</h2>
|
||||
</div>
|
||||
@@ -160,7 +178,7 @@ export const Bookshelf: React.FC<Props> = ({ books }) => {
|
||||
{selectedBook && (
|
||||
<Link
|
||||
to={`/books/${selectedBook.slug}`}
|
||||
className="btn-ghost inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm text-white/90 transition-colors"
|
||||
className="btn-ghost inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm text-[color:var(--text)] transition-colors"
|
||||
aria-label={`打开《${selectedBook.title}》`}
|
||||
>
|
||||
<span className="font-semibold">翻开阅读</span>
|
||||
@@ -171,17 +189,17 @@ export const Bookshelf: React.FC<Props> = ({ books }) => {
|
||||
|
||||
<div
|
||||
ref={shelfRef}
|
||||
className="relative mt-5 rounded-xl p-4 sm:p-5 shelf-wood"
|
||||
className="relative mt-5 rounded-2xl p-4 sm:p-5 shelf-tray"
|
||||
style={{
|
||||
boxShadow:
|
||||
'inset 0 2px 0 rgba(255,255,255,0.08), inset 0 -10px 30px rgba(0,0,0,0.55), 0 40px 90px rgba(0,0,0,0.45)',
|
||||
'inset 0 1px 0 rgba(255,255,255,0.85), inset 0 -22px 40px rgba(0,0,0,0.08), var(--shadow-2)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-x-3 bottom-[10px] h-[10px] rounded-full opacity-70"
|
||||
className="absolute inset-x-4 bottom-[10px] h-[10px] rounded-full opacity-[0.6]"
|
||||
style={{
|
||||
background:
|
||||
'radial-gradient(80% 120% at 20% 10%, rgba(255,255,255,0.12), transparent 60%), radial-gradient(90% 130% at 90% 30%, rgba(0,0,0,0.65), transparent 55%)',
|
||||
'radial-gradient(80% 120% at 20% 10%, rgba(0,0,0,0.10), transparent 60%), radial-gradient(90% 130% at 90% 30%, rgba(0,0,0,0.16), transparent 58%)',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
@@ -209,7 +227,17 @@ export const Bookshelf: React.FC<Props> = ({ books }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={scrollerRef} dir="ltr" className="flex items-end gap-3 overflow-x-auto pb-3 pt-2">
|
||||
<div
|
||||
ref={scrollerRef}
|
||||
dir="ltr"
|
||||
className="hide-scrollbar flex items-end gap-3 overflow-x-auto pb-4 pt-2"
|
||||
style={{
|
||||
WebkitMaskImage:
|
||||
'linear-gradient(90deg, transparent, #000 22px, #000 calc(100% - 22px), transparent)',
|
||||
maskImage:
|
||||
'linear-gradient(90deg, transparent, #000 22px, #000 calc(100% - 22px), transparent)',
|
||||
}}
|
||||
>
|
||||
{books.map((book, i) => (
|
||||
<div key={book.id} className="flex-none">
|
||||
<BookSpine book={book} index={i} selected={book.slug === selectedSlug} onSelect={(e) => onSelect(book, e)} />
|
||||
@@ -220,19 +248,19 @@ export const Bookshelf: React.FC<Props> = ({ books }) => {
|
||||
|
||||
{selectedBook && (
|
||||
<div
|
||||
className="mt-5 rounded-xl border border-white/12 bg-white/5 p-4 sm:p-5"
|
||||
className="mt-5 rounded-2xl border border-[color:var(--border)] bg-white/60 p-4 sm:p-5"
|
||||
style={{
|
||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.10)',
|
||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.75)',
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-white/60 text-xs">已选中</p>
|
||||
<h3 className="font-display text-[22px] text-white leading-tight">
|
||||
<p className="text-[color:var(--muted)] text-xs">已选中</p>
|
||||
<h3 className="font-display text-[22px] leading-tight">
|
||||
{selectedBook.title}
|
||||
</h3>
|
||||
{selectedBook.subtitle && (
|
||||
<p className="text-white/70 text-sm mt-1">{selectedBook.subtitle}</p>
|
||||
<p className="text-[color:var(--muted)] text-sm mt-1">{selectedBook.subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -240,19 +268,19 @@ export const Bookshelf: React.FC<Props> = ({ books }) => {
|
||||
{selectedBook.tags.slice(0, 3).map((t) => (
|
||||
<span
|
||||
key={t}
|
||||
className="rounded-full px-3 py-1 text-xs text-white/80"
|
||||
className="rounded-full px-3 py-1 text-xs text-[color:var(--text)]"
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.08)',
|
||||
border: '1px solid rgba(255,255,255,0.12)',
|
||||
background: 'rgba(255,255,255,0.55)',
|
||||
border: '1px solid rgba(0,0,0,0.10)',
|
||||
}}
|
||||
>
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
<span className="text-white/55 text-xs">{selectedBook.author} · {selectedBook.year}</span>
|
||||
<span className="text-[color:var(--muted)] text-xs">{selectedBook.author} · {selectedBook.year}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-3 text-white/80 leading-relaxed">{selectedBook.blurb}</p>
|
||||
<p className="mt-3 text-[color:var(--text)]/90 leading-relaxed">{selectedBook.blurb}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,13 +4,13 @@ export const Footer: React.FC = () => {
|
||||
return (
|
||||
<footer className="mt-10 pb-10">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="surface rounded-2xl px-6 py-6 text-white">
|
||||
<div className="surface rounded-2xl px-6 py-6">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="font-display text-lg">书架</div>
|
||||
<div className="text-white/70 text-sm mt-1">挑一本书,翻开阅读。</div>
|
||||
<div className="text-[color:var(--muted)] text-sm mt-1">挑一本书,翻开阅读。</div>
|
||||
</div>
|
||||
<div className="text-white/60 text-xs">
|
||||
<div className="text-[color:var(--muted)] text-xs">
|
||||
Demo UI · 2026-01-01
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,11 +10,11 @@ export const Header: React.FC = () => {
|
||||
<div className="relative">
|
||||
<div
|
||||
className={
|
||||
(isShelf ? 'bg-transparent' : 'bg-white') +
|
||||
' border-b border-white/10'
|
||||
(isShelf ? 'bg-transparent' : 'bg-transparent') +
|
||||
' border-b border-black/5'
|
||||
}
|
||||
style={{
|
||||
backdropFilter: 'blur(10px)',
|
||||
backdropFilter: 'blur(14px)',
|
||||
}}
|
||||
>
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
@@ -22,13 +22,13 @@ export const Header: React.FC = () => {
|
||||
<Link
|
||||
to="/"
|
||||
className={
|
||||
(isShelf ? 'text-white' : 'text-gray-900') +
|
||||
(isShelf ? 'text-[color:var(--text)]' : 'text-[color:var(--text)]') +
|
||||
' text-xl sm:text-2xl font-bold tracking-tight'
|
||||
}
|
||||
aria-label="返回书架"
|
||||
>
|
||||
<span className="font-display">书架</span>
|
||||
<span className={(isShelf ? 'text-white/70' : 'text-gray-500') + ' ml-2 text-sm font-semibold'}>
|
||||
<span className={'text-[color:var(--muted)] ml-2 text-sm font-semibold'}>
|
||||
Library
|
||||
</span>
|
||||
</Link>
|
||||
@@ -37,8 +37,10 @@ export const Header: React.FC = () => {
|
||||
<Link
|
||||
to="/"
|
||||
className={
|
||||
(isShelf ? 'text-white/80 hover:text-white' : 'text-gray-700 hover:text-gray-900') +
|
||||
' text-sm font-semibold'
|
||||
(isShelf
|
||||
? 'text-[color:var(--muted)] hover:text-[color:var(--text)]'
|
||||
: 'text-[color:var(--muted)] hover:text-[color:var(--text)]') +
|
||||
' text-sm font-semibold transition-colors'
|
||||
}
|
||||
>
|
||||
书架
|
||||
@@ -46,8 +48,10 @@ export const Header: React.FC = () => {
|
||||
<Link
|
||||
to="/categories"
|
||||
className={
|
||||
(isShelf ? 'text-white/80 hover:text-white' : 'text-gray-700 hover:text-gray-900') +
|
||||
' text-sm font-semibold'
|
||||
(isShelf
|
||||
? 'text-[color:var(--muted)] hover:text-[color:var(--text)]'
|
||||
: 'text-[color:var(--muted)] hover:text-[color:var(--text)]') +
|
||||
' text-sm font-semibold transition-colors'
|
||||
}
|
||||
>
|
||||
分类
|
||||
|
||||
@@ -17,20 +17,20 @@ export const PostCard: React.FC<PostCardProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<article
|
||||
className="border border-gray-200 rounded-lg p-6 shadow-sm hover:shadow-md transition-shadow cursor-pointer bg-white"
|
||||
className="surface rounded-2xl p-6 hover:shadow-[0_24px_70px_rgba(0,0,0,0.14)] transition-shadow cursor-pointer"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{category && (
|
||||
<span className="px-2 py-1 text-xs font-medium bg-blue-100 text-blue-700 rounded-full">
|
||||
<span className="px-2 py-1 text-xs font-medium rounded-full border border-[color:var(--border)] bg-white/60">
|
||||
{category}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-sm text-gray-500">{date}</span>
|
||||
<span className="text-sm text-[color:var(--muted)]">{date}</span>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">{title}</h2>
|
||||
<p className="text-gray-600 line-clamp-3">{excerpt}</p>
|
||||
<div className="mt-4 text-blue-600 text-sm font-medium flex items-center gap-1">
|
||||
<h2 className="text-xl font-semibold mb-2">{title}</h2>
|
||||
<p className="text-[color:var(--muted)] line-clamp-3">{excerpt}</p>
|
||||
<div className="mt-4 text-[color:var(--accent)] text-sm font-medium flex items-center gap-1">
|
||||
阅读更多
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
|
||||
@@ -2,18 +2,18 @@ import React from 'react'
|
||||
|
||||
export const PostCardSkeleton: React.FC = () => {
|
||||
return (
|
||||
<div className="border border-gray-200 rounded-lg p-6 shadow-sm bg-white animate-pulse">
|
||||
<div className="surface rounded-2xl p-6 animate-pulse">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-16 h-5 bg-gray-200 rounded-full"></div>
|
||||
<div className="w-20 h-4 bg-gray-200 rounded"></div>
|
||||
<div className="w-16 h-5 bg-black/10 rounded-full"></div>
|
||||
<div className="w-20 h-4 bg-black/10 rounded"></div>
|
||||
</div>
|
||||
<div className="h-6 bg-gray-200 rounded w-3/4 mb-3"></div>
|
||||
<div className="h-6 bg-black/10 rounded w-3/4 mb-3"></div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 bg-gray-200 rounded w-full"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-5/6"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-4/6"></div>
|
||||
<div className="h-4 bg-black/10 rounded w-full"></div>
|
||||
<div className="h-4 bg-black/10 rounded w-5/6"></div>
|
||||
<div className="h-4 bg-black/10 rounded w-4/6"></div>
|
||||
</div>
|
||||
<div className="mt-4 h-4 bg-gray-200 rounded w-20"></div>
|
||||
<div className="mt-4 h-4 bg-black/10 rounded w-20"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -14,13 +14,13 @@ export const BookDetail: React.FC = () => {
|
||||
<div className="min-h-screen">
|
||||
<Header />
|
||||
<main className="container mx-auto px-4 py-10">
|
||||
<div className="surface rounded-2xl p-6 sm:p-8 text-white">
|
||||
<div className="surface rounded-2xl p-6 sm:p-8">
|
||||
<h1 className="font-display text-2xl">未找到这本书</h1>
|
||||
<p className="text-white/80 mt-2">它可能被借走了,或者书名写错了。</p>
|
||||
<p className="text-[color:var(--muted)] mt-2">它可能被借走了,或者书名写错了。</p>
|
||||
<div className="mt-6">
|
||||
<Link
|
||||
to="/"
|
||||
className="btn-ghost inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm text-white/90"
|
||||
className="btn-ghost inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm"
|
||||
>
|
||||
回到书架 <span aria-hidden="true">→</span>
|
||||
</Link>
|
||||
@@ -37,23 +37,23 @@ export const BookDetail: React.FC = () => {
|
||||
<Header />
|
||||
<main className="container mx-auto px-4 py-10">
|
||||
<article className="max-w-4xl mx-auto">
|
||||
<header className="surface rounded-2xl p-6 sm:p-9 text-white relative overflow-hidden">
|
||||
<div className="grain absolute inset-0 opacity-25" aria-hidden="true" />
|
||||
<header className="surface rounded-2xl p-6 sm:p-9 relative overflow-hidden">
|
||||
<div className="grain absolute inset-0 opacity-[0.18]" aria-hidden="true" />
|
||||
|
||||
<div className="relative">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<Link
|
||||
to="/"
|
||||
className="btn-ghost inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm text-white/90"
|
||||
className="btn-ghost inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm"
|
||||
>
|
||||
<span aria-hidden="true">←</span> 书架
|
||||
</Link>
|
||||
|
||||
<span
|
||||
className="rounded-full px-3 py-1 text-xs text-white/85"
|
||||
className="rounded-full px-3 py-1 text-xs text-[color:var(--text)]"
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.08)',
|
||||
border: '1px solid rgba(255,255,255,0.12)',
|
||||
background: 'rgba(255,255,255,0.60)',
|
||||
border: '1px solid rgba(0,0,0,0.10)',
|
||||
}}
|
||||
>
|
||||
{book.author} · {book.year}
|
||||
@@ -64,18 +64,18 @@ export const BookDetail: React.FC = () => {
|
||||
{book.title}
|
||||
</h1>
|
||||
{book.subtitle && (
|
||||
<p className="text-white/80 mt-3 text-lg">{book.subtitle}</p>
|
||||
<p className="text-[color:var(--muted)] mt-3 text-lg">{book.subtitle}</p>
|
||||
)}
|
||||
<p className="text-white/85 mt-4 leading-relaxed">{book.blurb}</p>
|
||||
<p className="text-[color:var(--text)]/90 mt-4 leading-relaxed">{book.blurb}</p>
|
||||
|
||||
<div className="mt-6 flex flex-wrap gap-2">
|
||||
{book.tags.map((t) => (
|
||||
<span
|
||||
key={t}
|
||||
className="rounded-full px-3 py-1 text-xs text-white/80"
|
||||
className="rounded-full px-3 py-1 text-xs text-[color:var(--text)]"
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.07)',
|
||||
border: '1px solid rgba(255,255,255,0.12)',
|
||||
background: 'rgba(255,255,255,0.55)',
|
||||
border: '1px solid rgba(0,0,0,0.10)',
|
||||
}}
|
||||
>
|
||||
{t}
|
||||
@@ -93,24 +93,24 @@ export const BookDetail: React.FC = () => {
|
||||
/>
|
||||
</header>
|
||||
|
||||
<section className="mt-8 surface rounded-2xl p-6 sm:p-9 text-white">
|
||||
<section className="mt-8 surface rounded-2xl p-6 sm:p-9">
|
||||
<h2 className="font-display text-2xl">目录</h2>
|
||||
<ol className="mt-4 grid gap-3">
|
||||
{book.chapters.map((c, idx) => (
|
||||
<li
|
||||
key={c.title}
|
||||
className="rounded-xl border border-white/12 bg-white/5 p-4"
|
||||
className="rounded-xl border border-[color:var(--border)] bg-white/60 p-4"
|
||||
>
|
||||
<div className="text-white/70 text-xs">第 {idx + 1} 章</div>
|
||||
<div className="text-white text-lg mt-1">{c.title}</div>
|
||||
<div className="text-[color:var(--muted)] text-xs">第 {idx + 1} 章</div>
|
||||
<div className="text-[color:var(--text)] text-lg mt-1">{c.title}</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section className="mt-8 surface rounded-2xl p-6 sm:p-9 text-white">
|
||||
<section className="mt-8 surface rounded-2xl p-6 sm:p-9">
|
||||
<h2 className="font-display text-2xl">试读</h2>
|
||||
<div className="mt-5 prose prose-invert max-w-none">
|
||||
<div className="mt-5 prose max-w-none">
|
||||
{book.chapters[0]?.content.split('\n\n').map((p, i) => (
|
||||
<p key={i}>{p}</p>
|
||||
))}
|
||||
|
||||
@@ -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<any[]>([])
|
||||
const [categories, setCategories] = useState<Category[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="min-h-screen">
|
||||
<Header />
|
||||
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
<section className="mb-12">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-2">📂 文章分类</h2>
|
||||
<p className="text-gray-600">浏览所有分类</p>
|
||||
<h2 className="font-display text-3xl font-bold mb-2">文章分类</h2>
|
||||
<p className="text-[color:var(--muted)]">浏览所有分类</p>
|
||||
</section>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6">
|
||||
<div className="surface border border-red-200/60 text-red-700 px-4 py-3 rounded-2xl mb-6">
|
||||
<strong>错误:</strong> {error}
|
||||
</div>
|
||||
)}
|
||||
@@ -64,26 +65,26 @@ export const CategoriesPage: React.FC = () => {
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{loading
|
||||
? Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="bg-white p-6 rounded-lg shadow-sm animate-pulse">
|
||||
<div className="h-6 bg-gray-200 rounded w-3/4 mb-2"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
|
||||
<div key={i} className="surface p-6 rounded-2xl animate-pulse">
|
||||
<div className="h-6 bg-black/10 rounded w-3/4 mb-2"></div>
|
||||
<div className="h-4 bg-black/10 rounded w-1/2"></div>
|
||||
</div>
|
||||
))
|
||||
: categories.map((category) => (
|
||||
<a
|
||||
key={category.id}
|
||||
href={`/categories/${category.slug}`}
|
||||
className="bg-white p-6 rounded-lg shadow-sm hover:shadow-md transition-shadow"
|
||||
className="surface p-6 rounded-2xl hover:shadow-[0_24px_70px_rgba(0,0,0,0.14)] transition-shadow"
|
||||
>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">{category.title}</h3>
|
||||
<p className="text-sm text-gray-600">查看该分类下的所有文章</p>
|
||||
<h3 className="text-xl font-semibold mb-2">{category.title}</h3>
|
||||
<p className="text-sm text-[color:var(--muted)]">查看该分类下的所有文章</p>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!loading && categories.length === 0 && !error && (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500 text-lg">暂无分类</p>
|
||||
<p className="text-[color:var(--muted)] text-lg">暂无分类</p>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
@@ -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<any[]>([])
|
||||
const [category, setCategory] = useState<any>(null)
|
||||
const [posts, setPosts] = useState<Post[]>([])
|
||||
const [category, setCategory] = useState<Category | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="min-h-screen">
|
||||
<Header />
|
||||
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
<section className="mb-12">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
📂 {category?.title || '分类'}
|
||||
<h2 className="font-display text-3xl font-bold mb-2">
|
||||
{category?.title || '分类'}
|
||||
</h2>
|
||||
<p className="text-gray-600">探索该分类下的所有内容</p>
|
||||
<p className="text-[color:var(--muted)]">探索该分类下的所有内容</p>
|
||||
</section>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6">
|
||||
<div className="surface border border-red-200/60 text-red-700 px-4 py-3 rounded-2xl mb-6">
|
||||
<strong>错误:</strong> {error}
|
||||
</div>
|
||||
)}
|
||||
@@ -127,7 +133,7 @@ export const CategoryDetail: React.FC = () => {
|
||||
<PostCard
|
||||
key={post.id}
|
||||
title={post.title}
|
||||
excerpt={stripHtml(post.content_html || post.content?.root?.children?.[0]?.children?.[0]?.text || post.title)}
|
||||
excerpt={getExcerpt(post)}
|
||||
category={getCategoryTitle(post)}
|
||||
date={formatDate(post.createdAt)}
|
||||
onClick={() => handlePostClick(post.slug)}
|
||||
@@ -137,7 +143,7 @@ export const CategoryDetail: React.FC = () => {
|
||||
|
||||
{!loading && posts.length === 0 && !error && (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500 text-lg">该分类下暂无文章</p>
|
||||
<p className="text-[color:var(--muted)] text-lg">该分类下暂无文章</p>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
@@ -15,12 +15,12 @@ export const Home: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<section className="mt-10 max-w-5xl mx-auto">
|
||||
<div className="surface relative rounded-2xl p-5 sm:p-7 text-white">
|
||||
<div className="grain absolute inset-0 opacity-20" aria-hidden="true" />
|
||||
<div className="surface relative rounded-2xl p-5 sm:p-7">
|
||||
<div className="grain absolute inset-0 opacity-[0.18]" aria-hidden="true" />
|
||||
<h3 className="font-display text-2xl">提示</h3>
|
||||
<p className="text-white/80 mt-2 leading-relaxed">
|
||||
点击书脊选中,右上角会出现“翻开阅读”。每本书都有一个独立分页面:
|
||||
<span className="font-mono text-white/90">/books/:slug</span>。
|
||||
<p className="text-[color:var(--muted)] mt-2 leading-relaxed">
|
||||
点击书脊选中,右上角会出现“翻开阅读”。每本书都有一个独立分页:
|
||||
<span className="font-mono text-[color:var(--text)]">/books/:slug</span>。
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -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<any>(null)
|
||||
const [post, setPost] = useState<Post | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="min-h-screen">
|
||||
<Header />
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
<div className="animate-pulse">
|
||||
<div className="h-8 bg-gray-200 rounded w-1/2 mb-4"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-1/4 mb-6"></div>
|
||||
<div className="h-8 bg-black/10 rounded w-1/2 mb-4"></div>
|
||||
<div className="h-4 bg-black/10 rounded w-1/4 mb-6"></div>
|
||||
<div className="space-y-3">
|
||||
<div className="h-4 bg-gray-200 rounded"></div>
|
||||
<div className="h-4 bg-gray-200 rounded"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
||||
<div className="h-4 bg-black/10 rounded"></div>
|
||||
<div className="h-4 bg-black/10 rounded"></div>
|
||||
<div className="h-4 bg-black/10 rounded w-3/4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -92,14 +106,14 @@ export const PostDetail: React.FC = () => {
|
||||
|
||||
if (error || !post) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="min-h-screen">
|
||||
<Header />
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
<div className="text-center py-12">
|
||||
<p className="text-red-600 text-lg">{error || '文章不存在'}</p>
|
||||
<button
|
||||
onClick={() => window.location.href = '/'}
|
||||
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
className="mt-4 btn-ghost px-4 py-2 rounded-full"
|
||||
>
|
||||
返回首页
|
||||
</button>
|
||||
@@ -111,27 +125,27 @@ export const PostDetail: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="min-h-screen">
|
||||
<Header />
|
||||
|
||||
<main className="container mx-auto px-4 py-8 max-w-4xl">
|
||||
<article>
|
||||
<header className="mb-8">
|
||||
{getCategoryTitle(post) && (
|
||||
<span className="inline-block px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm mb-4">
|
||||
<span className="inline-block px-3 py-1 rounded-full text-sm mb-4 border border-[color:var(--border)] bg-white/60">
|
||||
{getCategoryTitle(post)}
|
||||
</span>
|
||||
)}
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">{post.title}</h1>
|
||||
<div className="flex items-center text-gray-600 text-sm">
|
||||
<h1 className="font-display text-4xl font-bold mb-4">{post.title}</h1>
|
||||
<div className="flex items-center text-[color:var(--muted)] text-sm">
|
||||
<span>{formatDate(post.createdAt)}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{post.heroImage && (
|
||||
{hero && (
|
||||
<img
|
||||
src={post.heroImage.url}
|
||||
alt={post.heroImage.alt || post.title}
|
||||
src={hero.url}
|
||||
alt={hero.alt}
|
||||
className="w-full h-auto rounded-lg mb-8"
|
||||
/>
|
||||
)}
|
||||
@@ -141,10 +155,10 @@ export const PostDetail: React.FC = () => {
|
||||
dangerouslySetInnerHTML={{ __html: post.content_html || '' }}
|
||||
/>
|
||||
|
||||
<div className="mt-12 pt-8 border-t border-gray-200">
|
||||
<div className="mt-12 pt-8 border-t border-black/10">
|
||||
<button
|
||||
onClick={() => window.location.href = '/'}
|
||||
className="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300"
|
||||
className="btn-ghost px-5 py-2 rounded-full"
|
||||
>
|
||||
返回首页
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user