style: 优化书本封面显示样式

This commit is contained in:
xiamuceer
2026-03-16 16:50:15 +08:00
parent 17ecf34565
commit 577b48285b
+281 -32
View File
@@ -60,8 +60,8 @@ export default function BookshelfPage({
const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`; const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
const [flippedProjectIds, setFlippedProjectIds] = useState<Record<string, boolean>>({}); const [flippedProjectIds, setFlippedProjectIds] = useState<Record<string, boolean>>({});
const [coverGeneratingIds, setCoverGeneratingIds] = useState<Record<string, boolean>>({}); const [coverGeneratingIds, setCoverGeneratingIds] = useState<Record<string, boolean>>({});
const mobileBookHeight = 460; const mobileBookHeight = 520;
const desktopBookHeight = 430; const desktopBookHeight = 520;
const mobileSpineWidth = 32; const mobileSpineWidth = 32;
const serialBookPalettes = [ const serialBookPalettes = [
@@ -365,13 +365,14 @@ export default function BookshelfPage({
const coverReady = project.cover_status === 'ready' && !!project.cover_image_url; const coverReady = project.cover_status === 'ready' && !!project.cover_image_url;
const coverGenerating = project.cover_status === 'generating' || coverActionLoading; const coverGenerating = project.cover_status === 'generating' || coverActionLoading;
const coverFailed = project.cover_status === 'failed' && !coverActionLoading; const coverFailed = project.cover_status === 'failed' && !coverActionLoading;
const showCoverFace = coverReady && !isFlipped;
return ( return (
<div key={project.id} style={{ position: 'relative', width: '100%', minWidth: 0, minHeight: isMobile ? mobileBookHeight : desktopBookHeight }}> <div key={project.id} style={{ position: 'relative', width: '100%', minWidth: 0, height: isMobile ? mobileBookHeight : desktopBookHeight }}>
<Card <Card
hoverable hoverable
style={{ ...bookshelfCardStyles.projectCard, minHeight: isMobile ? mobileBookHeight : desktopBookHeight }} style={{ ...bookshelfCardStyles.projectCard, height: isMobile ? mobileBookHeight : desktopBookHeight }}
styles={{ body: { padding: 0, flex: 1, display: 'flex', flexDirection: 'column' } }} styles={{ body: { padding: 0, height: '100%', flex: 1, display: 'flex', flexDirection: 'column' } }}
{...bookshelfCardHoverHandlers} {...bookshelfCardHoverHandlers}
onClick={() => !isFlipped && onEnterProject(project)} onClick={() => !isFlipped && onEnterProject(project)}
data-card-style="bookshelf-book" data-card-style="bookshelf-book"
@@ -414,50 +415,273 @@ export default function BookshelfPage({
zIndex: 2, zIndex: 2,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
minHeight: isMobile ? mobileBookHeight : desktopBookHeight, flex: 1,
padding: isMobile ? '18px 16px 14px 38px' : '26px 24px 18px 42px', height: '100%',
padding: isMobile ? '16px 16px 14px 38px' : '20px 20px 18px 42px',
overflow: 'hidden',
width: '100%',
boxSizing: 'border-box',
}}> }}>
<div style={{ display: 'flex', justifyContent: 'flex-start', marginBottom: 8 }}> {showCoverFace && (
<>
<div style={{
position: 'absolute',
inset: 0,
backgroundImage: `url(${project.cover_image_url})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}} />
<div style={{
position: 'absolute',
inset: 0,
background: isDark
? 'linear-gradient(180deg, rgba(7,10,18,0.18) 0%, rgba(7,10,18,0.38) 24%, rgba(7,10,18,0.72) 62%, rgba(7,10,18,0.88) 100%)'
: 'linear-gradient(180deg, rgba(255,255,255,0.08) 0%, rgba(16,24,40,0.22) 24%, rgba(16,24,40,0.62) 62%, rgba(16,24,40,0.82) 100%)',
}} />
</>
)}
<div style={{ display: 'flex', justifyContent: 'flex-start', marginBottom: 8, position: 'relative', zIndex: 2 }}>
<Button <Button
type="text" type="text"
size="small" size="small"
icon={isFlipped ? <BookOutlined /> : <SwapOutlined />} icon={showCoverFace ? <BookOutlined /> : <SwapOutlined />}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
toggleProjectFace(project.id); toggleProjectFace(project.id);
}} }}
style={showCoverFace ? {
color: token.colorWhite,
borderRadius: 999,
background: 'rgba(255,255,255,0.18)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.22)',
} : undefined}
> >
{isFlipped ? '返回书本' : '查看封面'} {showCoverFace ? '切换回详情' : (coverReady ? '查看封面' : '封面操作')}
</Button> </Button>
</div> </div>
{isFlipped ? (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 12, justifyContent: 'center' }}> {showCoverFace ? (
{coverReady ? ( <div style={{
<> position: 'relative',
<div style={{ flex: 1, minHeight: 240, borderRadius: 12, overflow: 'hidden', background: alphaColor(token.colorText, 0.06), border: `1px solid ${alphaColor(token.colorText, 0.08)}` }}> zIndex: 2,
<img src={project.cover_image_url} alt={`${project.title} cover`} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} /> flex: 1,
display: 'flex',
flexDirection: 'column',
minHeight: 0,
width: '100%',
}}>
<div style={{
minHeight: isMobile ? 82 : 90,
width: '100%',
marginBottom: isMobile ? 10 : 12,
}}>
<div style={{
fontSize: isMobile ? 18 : 22,
fontWeight: 700,
color: token.colorWhite,
lineHeight: 1.3,
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
wordBreak: 'break-word',
fontFamily: 'Georgia, "Times New Roman", "Noto Serif SC", serif',
textShadow: '0 2px 12px rgba(0,0,0,0.35)',
}}>
{project.title}
</div> </div>
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: 6,
marginTop: 10,
minHeight: isMobile ? 20 : 22,
alignItems: 'flex-start',
}}>
{tags.length > 0 ? tags.slice(0, 3).map((tag: string, idx: number) => (
<Tag key={idx} style={{
margin: 0,
padding: isMobile ? '0 7px' : '0 8px',
borderRadius: 999,
border: '1px solid rgba(255,255,255,0.24)',
background: 'rgba(255,255,255,0.14)',
color: token.colorWhite,
fontSize: isMobile ? 10 : 11,
lineHeight: isMobile ? '18px' : '20px',
fontWeight: 500,
backdropFilter: 'blur(8px)',
}}>
{tag}
</Tag>
)) : (
<Tag style={{
margin: 0,
padding: isMobile ? '0 7px' : '0 8px',
borderRadius: 999,
border: '1px solid rgba(255,255,255,0.24)',
background: 'rgba(255,255,255,0.14)',
color: token.colorWhite,
fontSize: isMobile ? 10 : 11,
lineHeight: isMobile ? '18px' : '20px',
fontWeight: 500,
backdropFilter: 'blur(8px)',
}}>
</Tag>
)}
</div>
</div>
<div style={{ flex: 1, minHeight: isMobile ? 54 : 72 }} />
<div style={{
display: 'flex',
flexDirection: 'column',
gap: 10,
width: '100%',
}}>
<div style={{
padding: isMobile ? '12px 10px' : '14px 12px',
borderRadius: 10,
background: 'rgba(10,18,32,0.34)',
border: '1px solid rgba(255,255,255,0.16)',
boxShadow: '0 8px 24px rgba(0,0,0,0.18)',
backdropFilter: 'blur(12px)',
minHeight: isMobile ? 74 : 82,
}}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 6,
fontSize: isMobile ? 11 : 12,
color: 'rgba(255,255,255,0.82)',
}}>
<span></span>
<span style={{ color: token.colorWhite, fontWeight: 700 }}>{progress}%</span>
</div>
<div style={{
height: 6,
width: '100%',
borderRadius: 999,
overflow: 'hidden',
background: 'rgba(255,255,255,0.18)',
marginBottom: 12,
}}>
<div style={{
width: `${progress}%`,
height: '100%',
borderRadius: 999,
background: progressColor,
transition: 'width 0.3s ease',
}} />
</div>
<div style={{ display: 'flex', alignItems: 'stretch', textAlign: 'center' }}>
<div style={{ flex: 1 }}>
<div style={{
fontSize: isMobile ? 22 : 26,
fontWeight: 700,
color: token.colorWhite,
lineHeight: 1.1,
fontFamily: 'Georgia, "Times New Roman", serif',
}}>
{formatWordCount(project.current_words || 0)}
</div>
<div style={{
fontSize: isMobile ? 10 : 11,
color: 'rgba(255,255,255,0.72)',
marginTop: 4,
}}>
</div>
</div>
<div style={{
width: 1,
margin: '0 12px',
background: 'rgba(255,255,255,0.16)',
}} />
<div style={{ flex: 1 }}>
<div style={{
fontSize: isMobile ? 22 : 26,
fontWeight: 700,
color: progress >= 100 ? '#7CFFB2' : token.colorWhite,
lineHeight: 1.1,
fontFamily: 'Georgia, "Times New Roman", serif',
}}>
{formatWordCount(project.target_words || 0)}
</div>
<div style={{
fontSize: isMobile ? 10 : 11,
color: 'rgba(255,255,255,0.72)',
marginTop: 4,
}}>
</div>
</div>
</div>
</div>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: 10,
flexWrap: 'wrap',
paddingTop: isMobile ? 10 : 12,
borderTop: '1px solid rgba(255,255,255,0.18)',
minHeight: isMobile ? 42 : 46,
}}>
<Space size={4} style={{ fontSize: isMobile ? 11 : 12, color: 'rgba(255,255,255,0.76)' }}>
<CalendarOutlined style={{ fontSize: isMobile ? 10 : 12 }} />
{formatDate(project.updated_at)}
</Space>
<Space wrap> <Space wrap>
<Button icon={<DownloadOutlined />} onClick={(e) => { e.stopPropagation(); onDownloadCover(project); }} disabled={coverActionLoading}></Button>
<Button <Button
size="small"
icon={<DownloadOutlined />}
onClick={(e) => { e.stopPropagation(); onDownloadCover(project); }}
disabled={coverActionLoading}
style={{
color: token.colorWhite,
background: 'rgba(255,255,255,0.14)',
borderColor: 'rgba(255,255,255,0.22)',
backdropFilter: 'blur(8px)',
}}
>
</Button>
<Button
size="small"
icon={<ReloadOutlined />} icon={<ReloadOutlined />}
loading={coverActionLoading} loading={coverActionLoading}
onClick={(e) => void handleGenerateCoverClick(e, project, true)} onClick={(e) => void handleGenerateCoverClick(e, project, true)}
style={{
color: token.colorWhite,
background: 'rgba(255,255,255,0.14)',
borderColor: 'rgba(255,255,255,0.22)',
backdropFilter: 'blur(8px)',
}}
> >
{coverActionLoading ? '重新生成中...' : '重新生成'} {coverActionLoading ? '重新生成中...' : '重新生成'}
</Button> </Button>
</Space> </Space>
</> </div>
) : coverGenerating ? ( </div>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12 }}> </div>
) : (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 12, justifyContent: 'space-between', minHeight: 0, width: '100%' }}>
{isFlipped && coverGenerating ? (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12, minHeight: 0, width: '100%' }}>
<LoadingOutlined spin style={{ fontSize: 28, color: token.colorPrimary }} /> <LoadingOutlined spin style={{ fontSize: 28, color: token.colorPrimary }} />
<div style={{ color: token.colorTextSecondary }}>...</div> <div style={{ color: token.colorTextSecondary }}>...</div>
</div> </div>
) : ( ) : isFlipped && coverFailed ? (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12, textAlign: 'center' }}> <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12, textAlign: 'center', minHeight: 0 }}>
<PictureOutlined style={{ fontSize: 36, color: token.colorTextTertiary }} /> <PictureOutlined style={{ fontSize: 36, color: token.colorTextTertiary }} />
{coverFailed ? (
<>
<div style={{ color: token.colorError }}></div> <div style={{ color: token.colorError }}></div>
<div style={{ color: token.colorTextSecondary, fontSize: 12 }}>{project.cover_error || '请稍后重试'}</div> <div style={{ color: token.colorTextSecondary, fontSize: 12 }}>{project.cover_error || '请稍后重试'}</div>
<Button <Button
@@ -468,8 +692,11 @@ export default function BookshelfPage({
> >
{coverActionLoading ? '重新生成中...' : '重新生成'} {coverActionLoading ? '重新生成中...' : '重新生成'}
</Button> </Button>
</> </div>
) : ( ) : isFlipped && !coverReady ? (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12, textAlign: 'center', minHeight: 0 }}>
<PictureOutlined style={{ fontSize: 36, color: token.colorTextTertiary }} />
<div style={{ color: token.colorTextSecondary }}></div>
<Button <Button
type="primary" type="primary"
icon={<PictureOutlined />} icon={<PictureOutlined />}
@@ -478,9 +705,6 @@ export default function BookshelfPage({
> >
{coverActionLoading ? '生成中...' : '生成封面'} {coverActionLoading ? '生成中...' : '生成封面'}
</Button> </Button>
)}
</div>
)}
</div> </div>
) : ( ) : (
<> <>
@@ -648,19 +872,41 @@ export default function BookshelfPage({
</div> </div>
<div style={{ <div style={{
display: 'flex', display: 'grid',
justifyContent: 'space-between', gridTemplateColumns: '1fr auto 1fr',
alignItems: 'center', alignItems: 'center',
gap: 10,
paddingTop: isMobile ? 10 : 12, paddingTop: isMobile ? 10 : 12,
borderTop: `1px solid ${alphaColor(token.colorText, 0.06)}`, borderTop: `1px solid ${alphaColor(token.colorText, 0.06)}`,
color: token.colorTextTertiary, color: token.colorTextTertiary,
marginTop: 'auto', marginTop: 'auto',
}}> }}>
<Space size={4} style={{ fontSize: isMobile ? 11 : 12, color: token.colorTextTertiary }}> <Space size={4} style={{ fontSize: isMobile ? 11 : 12, color: token.colorTextTertiary, justifySelf: 'start' }}>
<CalendarOutlined style={{ fontSize: isMobile ? 10 : 12 }} /> <CalendarOutlined style={{ fontSize: isMobile ? 10 : 12 }} />
{formatDate(project.updated_at)} {formatDate(project.updated_at)}
</Space> </Space>
<div style={{ justifySelf: 'center' }}>
{!coverReady && (
<Button
size="small"
icon={<PictureOutlined />}
loading={coverActionLoading}
onClick={(e) => void handleGenerateCoverClick(e, project, true)}
style={{
color: token.colorWhite,
background: 'rgba(255,255,255,0.14)',
borderColor: 'rgba(255,255,255,0.22)',
backdropFilter: 'blur(8px)',
minWidth: isMobile ? 112 : 124,
}}
>
{coverActionLoading ? '生成中...' : '生成封面'}
</Button>
)}
</div>
<div style={{ justifySelf: 'end' }}>
<Button <Button
type="text" type="text"
size="small" size="small"
@@ -676,9 +922,12 @@ export default function BookshelfPage({
}} }}
/> />
</div> </div>
</div>
</> </>
)} )}
</div> </div>
)}
</div>
</Card> </Card>
</div> </div>
); );