style: 优化书本封面显示样式
This commit is contained in:
@@ -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,110 +415,89 @@ 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' }}>
|
|
||||||
{coverReady ? (
|
|
||||||
<>
|
|
||||||
<div style={{ flex: 1, minHeight: 240, borderRadius: 12, overflow: 'hidden', background: alphaColor(token.colorText, 0.06), border: `1px solid ${alphaColor(token.colorText, 0.08)}` }}>
|
|
||||||
<img src={project.cover_image_url} alt={`${project.title} cover`} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
|
|
||||||
</div>
|
|
||||||
<Space wrap>
|
|
||||||
<Button icon={<DownloadOutlined />} onClick={(e) => { e.stopPropagation(); onDownloadCover(project); }} disabled={coverActionLoading}>下载封面</Button>
|
|
||||||
<Button
|
|
||||||
icon={<ReloadOutlined />}
|
|
||||||
loading={coverActionLoading}
|
|
||||||
onClick={(e) => void handleGenerateCoverClick(e, project, true)}
|
|
||||||
>
|
|
||||||
{coverActionLoading ? '重新生成中...' : '重新生成'}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</>
|
|
||||||
) : coverGenerating ? (
|
|
||||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12 }}>
|
|
||||||
<LoadingOutlined spin style={{ fontSize: 28, color: token.colorPrimary }} />
|
|
||||||
<div style={{ color: token.colorTextSecondary }}>封面生成中,请稍候...</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12, textAlign: 'center' }}>
|
|
||||||
<PictureOutlined style={{ fontSize: 36, color: token.colorTextTertiary }} />
|
|
||||||
{coverFailed ? (
|
|
||||||
<>
|
|
||||||
<div style={{ color: token.colorError }}>封面生成失败</div>
|
|
||||||
<div style={{ color: token.colorTextSecondary, fontSize: 12 }}>{project.cover_error || '请稍后重试'}</div>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<ReloadOutlined />}
|
|
||||||
loading={coverActionLoading}
|
|
||||||
onClick={(e) => void handleGenerateCoverClick(e, project, true)}
|
|
||||||
>
|
|
||||||
{coverActionLoading ? '重新生成中...' : '重新生成'}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<PictureOutlined />}
|
|
||||||
loading={coverActionLoading}
|
|
||||||
onClick={(e) => void handleGenerateCoverClick(e, project, true)}
|
|
||||||
>
|
|
||||||
{coverActionLoading ? '生成中...' : '生成封面'}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div style={{ marginBottom: isMobile ? 10 : 12, paddingRight: isMobile ? 18 : 30 }}>
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
gap: 8,
|
|
||||||
marginBottom: isMobile ? 8 : 10,
|
|
||||||
minHeight: isMobile ? 50 : 58,
|
|
||||||
}}>
|
|
||||||
<BookOutlined style={{
|
|
||||||
fontSize: isMobile ? 14 : 16,
|
|
||||||
color: alphaColor(token.colorText, 0.4),
|
|
||||||
marginTop: 2,
|
|
||||||
flexShrink: 0,
|
|
||||||
}} />
|
|
||||||
<div style={{
|
|
||||||
fontSize: isMobile ? 18 : 22,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: token.colorText,
|
|
||||||
lineHeight: 1.3,
|
|
||||||
display: '-webkit-box',
|
|
||||||
WebkitLineClamp: 2,
|
|
||||||
WebkitBoxOrient: 'vertical',
|
|
||||||
overflow: 'hidden',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
fontFamily: 'Georgia, "Times New Roman", "Noto Serif SC", serif',
|
|
||||||
}}>
|
|
||||||
{project.title}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{showCoverFace ? (
|
||||||
|
<div style={{
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 2,
|
||||||
|
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 style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
gap: 6,
|
gap: 6,
|
||||||
|
marginTop: 10,
|
||||||
minHeight: isMobile ? 20 : 22,
|
minHeight: isMobile ? 20 : 22,
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
}}>
|
}}>
|
||||||
@@ -525,13 +505,14 @@ export default function BookshelfPage({
|
|||||||
<Tag key={idx} style={{
|
<Tag key={idx} style={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: isMobile ? '0 7px' : '0 8px',
|
padding: isMobile ? '0 7px' : '0 8px',
|
||||||
borderRadius: 4,
|
borderRadius: 999,
|
||||||
border: `1px solid ${alphaColor(token.colorSuccess, 0.18)}`,
|
border: '1px solid rgba(255,255,255,0.24)',
|
||||||
background: alphaColor(token.colorSuccess, 0.08),
|
background: 'rgba(255,255,255,0.14)',
|
||||||
color: token.colorSuccess,
|
color: token.colorWhite,
|
||||||
fontSize: isMobile ? 10 : 11,
|
fontSize: isMobile ? 10 : 11,
|
||||||
lineHeight: isMobile ? '18px' : '20px',
|
lineHeight: isMobile ? '18px' : '20px',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
|
backdropFilter: 'blur(8px)',
|
||||||
}}>
|
}}>
|
||||||
{tag}
|
{tag}
|
||||||
</Tag>
|
</Tag>
|
||||||
@@ -539,13 +520,14 @@ export default function BookshelfPage({
|
|||||||
<Tag style={{
|
<Tag style={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: isMobile ? '0 7px' : '0 8px',
|
padding: isMobile ? '0 7px' : '0 8px',
|
||||||
borderRadius: 4,
|
borderRadius: 999,
|
||||||
border: `1px solid ${alphaColor(token.colorSuccess, 0.18)}`,
|
border: '1px solid rgba(255,255,255,0.24)',
|
||||||
background: alphaColor(token.colorSuccess, 0.08),
|
background: 'rgba(255,255,255,0.14)',
|
||||||
color: token.colorSuccess,
|
color: token.colorWhite,
|
||||||
fontSize: isMobile ? 10 : 11,
|
fontSize: isMobile ? 10 : 11,
|
||||||
lineHeight: isMobile ? '18px' : '20px',
|
lineHeight: isMobile ? '18px' : '20px',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
|
backdropFilter: 'blur(8px)',
|
||||||
}}>
|
}}>
|
||||||
未分类
|
未分类
|
||||||
</Tag>
|
</Tag>
|
||||||
@@ -553,130 +535,397 @@ export default function BookshelfPage({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Paragraph
|
<div style={{ flex: 1, minHeight: isMobile ? 54 : 72 }} />
|
||||||
ellipsis={{ rows: isMobile ? 3 : 3 }}
|
|
||||||
style={{
|
<div style={{
|
||||||
fontSize: isMobile ? 12 : 13,
|
display: 'flex',
|
||||||
color: token.colorTextSecondary,
|
flexDirection: 'column',
|
||||||
marginBottom: isMobile ? 12 : 16,
|
gap: 10,
|
||||||
lineHeight: 1.7,
|
width: '100%',
|
||||||
flexGrow: 1,
|
}}>
|
||||||
}}
|
<div style={{
|
||||||
>
|
padding: isMobile ? '12px 10px' : '14px 12px',
|
||||||
{project.description || '暂无描述...'}
|
borderRadius: 10,
|
||||||
</Paragraph>
|
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={{ marginBottom: isMobile ? 14 : 18 }}>
|
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 6,
|
gap: 10,
|
||||||
fontSize: isMobile ? 11 : 12,
|
flexWrap: 'wrap',
|
||||||
|
paddingTop: isMobile ? 10 : 12,
|
||||||
|
borderTop: '1px solid rgba(255,255,255,0.18)',
|
||||||
|
minHeight: isMobile ? 42 : 46,
|
||||||
}}>
|
}}>
|
||||||
<span style={{ color: token.colorTextTertiary }}>完成进度</span>
|
<Space size={4} style={{ fontSize: isMobile ? 11 : 12, color: 'rgba(255,255,255,0.76)' }}>
|
||||||
<span style={{ color: progressColor, fontWeight: 700 }}>{progress}%</span>
|
<CalendarOutlined style={{ fontSize: isMobile ? 10 : 12 }} />
|
||||||
</div>
|
{formatDate(project.updated_at)}
|
||||||
<div style={{
|
</Space>
|
||||||
height: 6,
|
|
||||||
width: '100%',
|
<Space wrap>
|
||||||
borderRadius: 999,
|
<Button
|
||||||
overflow: 'hidden',
|
size="small"
|
||||||
background: alphaColor(token.colorText, 0.06),
|
icon={<DownloadOutlined />}
|
||||||
}}>
|
onClick={(e) => { e.stopPropagation(); onDownloadCover(project); }}
|
||||||
<div style={{
|
disabled={coverActionLoading}
|
||||||
width: `${progress}%`,
|
style={{
|
||||||
height: '100%',
|
color: token.colorWhite,
|
||||||
borderRadius: 999,
|
background: 'rgba(255,255,255,0.14)',
|
||||||
background: progressColor,
|
borderColor: 'rgba(255,255,255,0.22)',
|
||||||
transition: 'width 0.3s ease',
|
backdropFilter: 'blur(8px)',
|
||||||
}} />
|
}}
|
||||||
|
>
|
||||||
|
下载封面
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
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)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{coverActionLoading ? '重新生成中...' : '重新生成'}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div style={{
|
) : (
|
||||||
marginBottom: isMobile ? 12 : 16,
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 12, justifyContent: 'space-between', minHeight: 0, width: '100%' }}>
|
||||||
padding: isMobile ? '12px 10px' : '14px 12px',
|
{isFlipped && coverGenerating ? (
|
||||||
background: `linear-gradient(180deg, ${alphaColor(token.colorBgContainer, 0.94)} 0%, ${alphaColor(token.colorFillSecondary, 0.78)} 100%)`,
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12, minHeight: 0, width: '100%' }}>
|
||||||
borderRadius: 10,
|
<LoadingOutlined spin style={{ fontSize: 28, color: token.colorPrimary }} />
|
||||||
border: `1px solid ${alphaColor(token.colorText, 0.06)}`,
|
<div style={{ color: token.colorTextSecondary }}>封面生成中,请稍候...</div>
|
||||||
boxShadow: `inset 0 1px 2px ${alphaColor(token.colorText, 0.08)}`,
|
</div>
|
||||||
}}>
|
) : isFlipped && coverFailed ? (
|
||||||
<div style={{ display: 'flex', alignItems: 'stretch', textAlign: 'center' }}>
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12, textAlign: 'center', minHeight: 0 }}>
|
||||||
<div style={{ flex: 1 }}>
|
<PictureOutlined style={{ fontSize: 36, color: token.colorTextTertiary }} />
|
||||||
|
<div style={{ color: token.colorError }}>封面生成失败</div>
|
||||||
|
<div style={{ color: token.colorTextSecondary, fontSize: 12 }}>{project.cover_error || '请稍后重试'}</div>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
loading={coverActionLoading}
|
||||||
|
onClick={(e) => void handleGenerateCoverClick(e, project, true)}
|
||||||
|
>
|
||||||
|
{coverActionLoading ? '重新生成中...' : '重新生成'}
|
||||||
|
</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
|
||||||
|
type="primary"
|
||||||
|
icon={<PictureOutlined />}
|
||||||
|
loading={coverActionLoading}
|
||||||
|
onClick={(e) => void handleGenerateCoverClick(e, project, true)}
|
||||||
|
>
|
||||||
|
{coverActionLoading ? '生成中...' : '生成封面'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div style={{ marginBottom: isMobile ? 10 : 12, paddingRight: isMobile ? 18 : 30 }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: isMobile ? 22 : 26,
|
display: 'flex',
|
||||||
fontWeight: 700,
|
alignItems: 'flex-start',
|
||||||
color: token.colorText,
|
gap: 8,
|
||||||
lineHeight: 1.1,
|
marginBottom: isMobile ? 8 : 10,
|
||||||
fontFamily: 'Georgia, "Times New Roman", serif',
|
minHeight: isMobile ? 50 : 58,
|
||||||
}}>
|
}}>
|
||||||
{formatWordCount(project.current_words || 0)}
|
<BookOutlined style={{
|
||||||
|
fontSize: isMobile ? 14 : 16,
|
||||||
|
color: alphaColor(token.colorText, 0.4),
|
||||||
|
marginTop: 2,
|
||||||
|
flexShrink: 0,
|
||||||
|
}} />
|
||||||
|
<div style={{
|
||||||
|
fontSize: isMobile ? 18 : 22,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: token.colorText,
|
||||||
|
lineHeight: 1.3,
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitLineClamp: 2,
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
overflow: 'hidden',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
fontFamily: 'Georgia, "Times New Roman", "Noto Serif SC", serif',
|
||||||
|
}}>
|
||||||
|
{project.title}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: isMobile ? 10 : 11,
|
display: 'flex',
|
||||||
color: token.colorTextTertiary,
|
flexWrap: 'wrap',
|
||||||
marginTop: 4,
|
gap: 6,
|
||||||
|
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: 4,
|
||||||
|
border: `1px solid ${alphaColor(token.colorSuccess, 0.18)}`,
|
||||||
|
background: alphaColor(token.colorSuccess, 0.08),
|
||||||
|
color: token.colorSuccess,
|
||||||
|
fontSize: isMobile ? 10 : 11,
|
||||||
|
lineHeight: isMobile ? '18px' : '20px',
|
||||||
|
fontWeight: 500,
|
||||||
|
}}>
|
||||||
|
{tag}
|
||||||
|
</Tag>
|
||||||
|
)) : (
|
||||||
|
<Tag style={{
|
||||||
|
margin: 0,
|
||||||
|
padding: isMobile ? '0 7px' : '0 8px',
|
||||||
|
borderRadius: 4,
|
||||||
|
border: `1px solid ${alphaColor(token.colorSuccess, 0.18)}`,
|
||||||
|
background: alphaColor(token.colorSuccess, 0.08),
|
||||||
|
color: token.colorSuccess,
|
||||||
|
fontSize: isMobile ? 10 : 11,
|
||||||
|
lineHeight: isMobile ? '18px' : '20px',
|
||||||
|
fontWeight: 500,
|
||||||
|
}}>
|
||||||
|
未分类
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
|
||||||
width: 1,
|
<Paragraph
|
||||||
margin: '0 12px',
|
ellipsis={{ rows: isMobile ? 3 : 3 }}
|
||||||
background: alphaColor(token.colorText, 0.1),
|
style={{
|
||||||
}} />
|
fontSize: isMobile ? 12 : 13,
|
||||||
<div style={{ flex: 1 }}>
|
color: token.colorTextSecondary,
|
||||||
|
marginBottom: isMobile ? 12 : 16,
|
||||||
|
lineHeight: 1.7,
|
||||||
|
flexGrow: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{project.description || '暂无描述...'}
|
||||||
|
</Paragraph>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: isMobile ? 14 : 18 }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: isMobile ? 22 : 26,
|
display: 'flex',
|
||||||
fontWeight: 700,
|
justifyContent: 'space-between',
|
||||||
color: progress >= 100 ? token.colorSuccess : progressColor,
|
alignItems: 'center',
|
||||||
lineHeight: 1.1,
|
marginBottom: 6,
|
||||||
fontFamily: 'Georgia, "Times New Roman", serif',
|
fontSize: isMobile ? 11 : 12,
|
||||||
}}>
|
}}>
|
||||||
{formatWordCount(project.target_words || 0)}
|
<span style={{ color: token.colorTextTertiary }}>完成进度</span>
|
||||||
|
<span style={{ color: progressColor, fontWeight: 700 }}>{progress}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: isMobile ? 10 : 11,
|
height: 6,
|
||||||
color: token.colorTextTertiary,
|
width: '100%',
|
||||||
marginTop: 4,
|
borderRadius: 999,
|
||||||
|
overflow: 'hidden',
|
||||||
|
background: alphaColor(token.colorText, 0.06),
|
||||||
}}>
|
}}>
|
||||||
目标字数
|
<div style={{
|
||||||
|
width: `${progress}%`,
|
||||||
|
height: '100%',
|
||||||
|
borderRadius: 999,
|
||||||
|
background: progressColor,
|
||||||
|
transition: 'width 0.3s ease',
|
||||||
|
}} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
marginBottom: isMobile ? 12 : 16,
|
||||||
justifyContent: 'space-between',
|
padding: isMobile ? '12px 10px' : '14px 12px',
|
||||||
alignItems: 'center',
|
background: `linear-gradient(180deg, ${alphaColor(token.colorBgContainer, 0.94)} 0%, ${alphaColor(token.colorFillSecondary, 0.78)} 100%)`,
|
||||||
paddingTop: isMobile ? 10 : 12,
|
borderRadius: 10,
|
||||||
borderTop: `1px solid ${alphaColor(token.colorText, 0.06)}`,
|
border: `1px solid ${alphaColor(token.colorText, 0.06)}`,
|
||||||
color: token.colorTextTertiary,
|
boxShadow: `inset 0 1px 2px ${alphaColor(token.colorText, 0.08)}`,
|
||||||
marginTop: 'auto',
|
}}>
|
||||||
}}>
|
<div style={{ display: 'flex', alignItems: 'stretch', textAlign: 'center' }}>
|
||||||
<Space size={4} style={{ fontSize: isMobile ? 11 : 12, color: token.colorTextTertiary }}>
|
<div style={{ flex: 1 }}>
|
||||||
<CalendarOutlined style={{ fontSize: isMobile ? 10 : 12 }} />
|
<div style={{
|
||||||
{formatDate(project.updated_at)}
|
fontSize: isMobile ? 22 : 26,
|
||||||
</Space>
|
fontWeight: 700,
|
||||||
|
color: token.colorText,
|
||||||
|
lineHeight: 1.1,
|
||||||
|
fontFamily: 'Georgia, "Times New Roman", serif',
|
||||||
|
}}>
|
||||||
|
{formatWordCount(project.current_words || 0)}
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
fontSize: isMobile ? 10 : 11,
|
||||||
|
color: token.colorTextTertiary,
|
||||||
|
marginTop: 4,
|
||||||
|
}}>
|
||||||
|
已写字数
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
width: 1,
|
||||||
|
margin: '0 12px',
|
||||||
|
background: alphaColor(token.colorText, 0.1),
|
||||||
|
}} />
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{
|
||||||
|
fontSize: isMobile ? 22 : 26,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: progress >= 100 ? token.colorSuccess : progressColor,
|
||||||
|
lineHeight: 1.1,
|
||||||
|
fontFamily: 'Georgia, "Times New Roman", serif',
|
||||||
|
}}>
|
||||||
|
{formatWordCount(project.target_words || 0)}
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
fontSize: isMobile ? 10 : 11,
|
||||||
|
color: token.colorTextTertiary,
|
||||||
|
marginTop: 4,
|
||||||
|
}}>
|
||||||
|
目标字数
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<div style={{
|
||||||
type="text"
|
display: 'grid',
|
||||||
size="small"
|
gridTemplateColumns: '1fr auto 1fr',
|
||||||
danger
|
alignItems: 'center',
|
||||||
icon={<DeleteOutlined style={{ fontSize: isMobile ? 12 : 14 }} />}
|
gap: 10,
|
||||||
onClick={(e) => {
|
paddingTop: isMobile ? 10 : 12,
|
||||||
e.stopPropagation();
|
borderTop: `1px solid ${alphaColor(token.colorText, 0.06)}`,
|
||||||
onDeleteProject(project.id);
|
color: token.colorTextTertiary,
|
||||||
}}
|
marginTop: 'auto',
|
||||||
style={{
|
}}>
|
||||||
padding: isMobile ? '2px 4px' : '4px 8px',
|
<Space size={4} style={{ fontSize: isMobile ? 11 : 12, color: token.colorTextTertiary, justifySelf: 'start' }}>
|
||||||
borderRadius: 8,
|
<CalendarOutlined style={{ fontSize: isMobile ? 10 : 12 }} />
|
||||||
}}
|
{formatDate(project.updated_at)}
|
||||||
/>
|
</Space>
|
||||||
</div>
|
|
||||||
</>
|
<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
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined style={{ fontSize: isMobile ? 12 : 14 }} />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDeleteProject(project.id);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
padding: isMobile ? '2px 4px' : '4px 8px',
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user