Skip to content

Commit c475936

Browse files
committed
统一影视详情页 Hero 文案展示
1 parent bece153 commit c475936

13 files changed

Lines changed: 173 additions & 57 deletions

web/src/components/CommentSection.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useTranslation } from '@/i18n'
77
import type { Comment } from '@/types'
88
import { ChevronDown, ChevronUp, MessageSquare, Send, Star, Trash2 } from 'lucide-react'
99
import { Button, EmptyState, Input, Surface, Tag } from '@/components/design-system'
10+
import DetailSectionTitle from '@/components/media/DetailSectionTitle'
1011

1112
interface CommentSectionProps {
1213
mediaId: string
@@ -111,11 +112,10 @@ export default function CommentSection({ mediaId }: CommentSectionProps) {
111112
onClick={() => setExpanded((value) => !value)}
112113
>
113114
<span className="nv-comment-summary-main">
114-
<span className="nv-comment-summary-icon" aria-hidden="true">
115-
<MessageSquare size={17} />
116-
</span>
117115
<span className="nv-comment-summary-copy">
118-
<span id="comment-section-title" className="nv-comment-summary-title">评价</span>
116+
<DetailSectionTitle as="span" id="comment-section-title" icon={<MessageSquare size={17} />}>
117+
评价
118+
</DetailSectionTitle>
119119
<span className="nv-comment-summary-meta">{summaryText}</span>
120120
</span>
121121
</span>

web/src/components/media/CastGrid.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { EmptyState } from '@/components/design-system'
66
import { PersonCard } from '@/ui'
77
import { Users } from 'lucide-react'
88
import { useTranslation } from '@/i18n'
9+
import DetailSectionTitle from './DetailSectionTitle'
910

1011
interface CastGridProps {
1112
persons: MediaPerson[]
@@ -66,10 +67,9 @@ export default function CastGrid({ persons }: CastGridProps) {
6667
return (
6768
<section className="nv-cast-grid-section" aria-labelledby="cast-grid-title">
6869
<div className="nv-cast-grid-header">
69-
<h2 id="cast-grid-title" className="nv-section-title inline-flex items-center gap-2">
70-
<Users size={17} className="text-[var(--nv-action-primary)]" aria-hidden="true" />
70+
<DetailSectionTitle id="cast-grid-title" icon={<Users size={17} />}>
7171
{t('castGrid.title')}
72-
</h2>
72+
</DetailSectionTitle>
7373
<span>{sortedPersons.length}</span>
7474
</div>
7575

web/src/components/media/CollectionCarousel.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { collectionApi, streamApi } from '@/api'
1616
import { Button, Surface, Tag } from '@/components/design-system'
1717
import type { CollectionMediaItem, CollectionWithMedia } from '@/types'
1818
import { groupByMovie, versionLabel, type GroupedMovieItem } from '@/utils/collectionGroup'
19+
import DetailSectionTitle from './DetailSectionTitle'
1920

2021
interface CollectionCarouselProps {
2122
mediaId: string
@@ -71,10 +72,9 @@ export default function CollectionCarousel({ mediaId }: CollectionCarouselProps)
7172
<section className="mt-6" aria-labelledby="collection-carousel-title">
7273
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
7374
<div className="flex min-w-0 flex-wrap items-center gap-2">
74-
<h3 id="collection-carousel-title" className="inline-flex items-center gap-2 text-base font-semibold text-[var(--nv-text-primary)]">
75-
<Layers size={17} className="text-[var(--nv-action-primary)]" aria-hidden="true" />
75+
<DetailSectionTitle as="h3" id="collection-carousel-title" icon={<Layers size={17} />}>
7676
系列合集
77-
</h3>
77+
</DetailSectionTitle>
7878

7979
<Link
8080
to={`/collections/${collection.id}`}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { ReactNode } from 'react'
2+
import clsx from 'clsx'
3+
4+
type DetailSectionTitleElement = 'h2' | 'h3' | 'span'
5+
6+
interface DetailSectionTitleProps {
7+
as?: DetailSectionTitleElement
8+
id?: string
9+
icon: ReactNode
10+
children: ReactNode
11+
className?: string
12+
}
13+
14+
export default function DetailSectionTitle({
15+
as: Element = 'h2',
16+
id,
17+
icon,
18+
children,
19+
className,
20+
}: DetailSectionTitleProps) {
21+
return (
22+
<Element id={id} className={clsx('nv-detail-section-title', className)}>
23+
<span className="nv-detail-section-title-icon" aria-hidden="true">{icon}</span>
24+
<span>{children}</span>
25+
</Element>
26+
)
27+
}

web/src/components/media/HeroSection.tsx

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useToast } from '@/components/Toast'
77
import { Button, Tag, buttonClassName } from '@/components/design-system'
88
import { HeroContent, MediaArtwork } from '@/ui'
99
import { useTranslation } from '@/i18n'
10-
import { formatDuration, formatDurationShort } from '@/utils/format'
10+
import { formatDuration, formatDurationShort, formatTime } from '@/utils/format'
1111
import type { Media, MediaPlayInfo, Playlist, WatchHistory } from '@/types'
1212
import {
1313
Check,
@@ -209,6 +209,10 @@ export default function HeroSection({
209209
: media.title
210210

211211
const isResume = !!watchProgress && !watchProgress.completed && watchProgress.position > 0
212+
const watchDuration = watchProgress?.duration || media.duration || 0
213+
const watchPercent = watchProgress && watchDuration > 0
214+
? Math.min(100, Math.max(0, Math.round((watchProgress.position / watchDuration) * 100)))
215+
: 0
212216
const playLabel = isResume
213217
? t('hero.continuePlayAt', { time: formatDurationShort(watchProgress.position) })
214218
: t('media.play')
@@ -416,7 +420,6 @@ export default function HeroSection({
416420
</div>
417421

418422
<HeroContent
419-
compact
420423
className="pb-1"
421424
eyebrow={episodeEyebrow}
422425
title={title}
@@ -431,22 +434,32 @@ export default function HeroSection({
431434
)}
432435
{media.year > 0 && <span>{media.year}</span>}
433436
{media.duration > 0 && <span>{formatDuration(media.duration)}</span>}
434-
{media.country && <span>{media.country}</span>}
437+
{media.resolution && <Tag tone="quality">{media.resolution}</Tag>}
435438
{media.genres && media.genres.split(',').slice(0, 3).map((genre) => (
436-
<Link key={genre} to={`/search?q=${encodeURIComponent(genre.trim())}`} className="transition-colors hover:text-[var(--nv-action-muted-hover)]">
437-
{genre.trim()}
439+
<Link
440+
key={genre}
441+
to={`/search?q=${encodeURIComponent(genre.trim())}`}
442+
className="transition-opacity hover:opacity-80"
443+
>
444+
<Tag>{genre.trim()}</Tag>
438445
</Link>
439446
))}
440-
</>
441-
)}
442-
badges={(
443-
<>
444-
{media.resolution && <Tag tone="quality">{media.resolution}</Tag>}
445447
{media.video_codec && <Tag>{media.video_codec}</Tag>}
446448
{playStatus && <Tag tone={playStatus.tone}>{playStatus.label}</Tag>}
449+
{media.country && <span>{media.country}</span>}
447450
</>
448451
)}
449452
overview={media.overview || undefined}
453+
supplemental={watchProgress && watchDuration > 0 ? (
454+
<div className="nv-detail-hero-watch-progress" aria-label={`已观看 ${watchPercent}%`}>
455+
<div className="nv-detail-hero-watch-progress-label">
456+
上次观看至 {formatTime(watchProgress.position)} / {formatTime(watchDuration)}
457+
</div>
458+
<div className="nv-detail-hero-watch-progress-track">
459+
<span style={{ width: `${watchPercent}%` }} />
460+
</div>
461+
</div>
462+
) : undefined}
450463
actions={heroActions}
451464
/>
452465
</div>

web/src/components/media/MediaDetailTechOverview.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type { ReactNode } from 'react'
22
import type { FileDetail, Media, TechSpecs } from '@/types'
33
import { formatSize } from '@/utils/format'
4-
import { AudioLines, Gauge, HardDrive, Monitor, Palette, PanelsTopLeft, ScanLine, Video } from 'lucide-react'
4+
import { AudioLines, Cpu, Gauge, HardDrive, Monitor, Palette, PanelsTopLeft, ScanLine, Video } from 'lucide-react'
5+
import DetailSectionTitle from './DetailSectionTitle'
56

67
interface MediaDetailTechOverviewProps {
78
media: Media
@@ -126,7 +127,9 @@ export default function MediaDetailTechOverview({ media, techSpecs, fileInfo }:
126127
<div className="nv-detail-section-heading">
127128
<div>
128129
<span className="nv-detail-section-eyebrow">Technical</span>
129-
<h2 id="detail-tech-overview-title">技术规格</h2>
130+
<DetailSectionTitle id="detail-tech-overview-title" icon={<Cpu size={17} />}>
131+
技术规格
132+
</DetailSectionTitle>
130133
</div>
131134
<span className="nv-detail-section-hint">主播放文件</span>
132135
</div>

web/src/components/media/MediaHighlightsPanel.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { mediaAnalysisApi, type MediaAnalysisTask, type MediaHighlight } from '@
88
import { useWebSocket, WS_EVENTS, type MediaAnalysisProgressData } from '@/hooks/useWebSocket'
99
import { bumpPosterVersion } from '@/stores/mediaRefresh'
1010
import { formatErrMsg } from '@/utils/error'
11+
import DetailSectionTitle from './DetailSectionTitle'
1112

1213
interface MediaHighlightsPanelProps {
1314
mediaId: string
@@ -329,8 +330,8 @@ export default function MediaHighlightsPanel({ mediaId, isAdmin }: MediaHighligh
329330
return (
330331
<div ref={sectionRef} className="nv-highlights-panel space-y-4">
331332
<div className="nv-highlights-header flex items-center justify-between gap-3">
332-
<div className="flex items-baseline gap-2">
333-
<h2 className="text-lg font-semibold text-[var(--nv-text-primary)]">精彩片段</h2>
333+
<div className="flex items-center gap-2">
334+
<DetailSectionTitle icon={<Clapperboard size={17} />}>精彩片段</DetailSectionTitle>
334335
<span className="text-xs text-[var(--nv-text-tertiary)]">{orderedHighlights.length}</span>
335336
</div>
336337

web/src/components/media/MediaInfoSection.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import { useState, type ReactNode } from 'react'
22
import { Link } from 'react-router-dom'
33
import type { Media, MediaPlayInfo, MediaPerson } from '@/types'
44
import { Button, EmptyState, Tag } from '@/components/design-system'
5-
import { ChevronDown, ChevronUp, ExternalLink, FileText } from 'lucide-react'
5+
import { ChevronDown, ChevronUp, ExternalLink, FileText, Info, Shapes, Tags } from 'lucide-react'
66
import clsx from 'clsx'
77
import { useTranslation } from '@/i18n'
8+
import DetailSectionTitle from './DetailSectionTitle'
89

910
interface MediaInfoSectionProps {
1011
media: Media
@@ -103,7 +104,7 @@ export default function MediaInfoSection({ media, playInfo: _playInfo, persons }
103104
{hasIntro ? (
104105
<section className="nv-media-info-intro py-5 sm:py-6">
105106
<div className="mb-4">
106-
<h2 className="text-sm font-semibold text-[var(--nv-text-primary)]">影片简介</h2>
107+
<DetailSectionTitle icon={<FileText size={17} />}>影片简介</DetailSectionTitle>
107108
{media.orig_title && media.orig_title !== media.title && (
108109
<p className="mt-1 text-xs italic text-[var(--nv-text-tertiary)]">{media.orig_title}</p>
109110
)}
@@ -167,7 +168,9 @@ export default function MediaInfoSection({ media, playInfo: _playInfo, persons }
167168
<section className="nv-media-info-classifications grid gap-5 py-5 sm:grid-cols-2 sm:py-6">
168169
{genreList.length > 0 && (
169170
<div>
170-
<SectionLabel>{t('mediaInfo.genres').replace(/[:]\s*$/, '')}</SectionLabel>
171+
<DetailSectionTitle as="h3" icon={<Shapes size={17} />} className="mb-2">
172+
{t('mediaInfo.genres').replace(/[:]\s*$/, '')}
173+
</DetailSectionTitle>
171174
<div className="flex flex-wrap gap-1.5">
172175
{genreList.map((genre) => (
173176
<MetadataLink key={`g-${genre}`} to={`/search?q=${encodeURIComponent(genre)}`}>{genre}</MetadataLink>
@@ -178,7 +181,9 @@ export default function MediaInfoSection({ media, playInfo: _playInfo, persons }
178181

179182
{tagList.length > 0 && tagList.join(',') !== genreList.join(',') && (
180183
<div>
181-
<SectionLabel>{t('mediaInfo.tags').replace(/[:]\s*$/, '')}</SectionLabel>
184+
<DetailSectionTitle as="h3" icon={<Tags size={17} />} className="mb-2">
185+
{t('mediaInfo.tags').replace(/[:]\s*$/, '')}
186+
</DetailSectionTitle>
182187
<div className="flex flex-wrap gap-1.5">
183188
{tagList.map((tag) => (
184189
<MetadataLink key={`t-${tag}`} to={`/search?q=${encodeURIComponent(tag)}`}>#{tag}</MetadataLink>
@@ -192,7 +197,7 @@ export default function MediaInfoSection({ media, playInfo: _playInfo, persons }
192197
{hasMetaTable && (
193198
<section className="nv-media-info-metadata py-5 sm:py-6">
194199
<div className="mb-2 flex items-center justify-between gap-3">
195-
<h2 className="text-sm font-semibold text-[var(--nv-text-primary)]">影片详情</h2>
200+
<DetailSectionTitle icon={<Info size={17} />}>影片详情</DetailSectionTitle>
196201
<span className="text-[10px] uppercase tracking-[0.1em] text-[var(--nv-text-tertiary)]">Metadata</span>
197202
</div>
198203

web/src/components/media/RecommendationCarousel.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import MediaCard from '@/components/MediaCard'
44
import { EmptyState } from '@/components/design-system'
55
import { MediaRail } from '@/ui'
66
import { ChevronRight, Film } from 'lucide-react'
7+
import DetailSectionTitle from './DetailSectionTitle'
78

89
interface RecommendationCarouselProps {
910
recommendations: RecommendedMedia[]
@@ -25,10 +26,9 @@ export default function RecommendationCarousel({ recommendations }: Recommendati
2526
<section className="nv-recommendation-section nv-detail-recommendation-section" aria-labelledby="recommendation-title">
2627
<MediaRail
2728
title={(
28-
<span className="inline-flex items-center gap-2">
29-
<Film size={16} className="text-[var(--nv-action-primary)]" aria-hidden="true" />
30-
<span id="recommendation-title">相似推荐</span>
31-
</span>
29+
<DetailSectionTitle as="span" id="recommendation-title" icon={<Film size={17} />}>
30+
相似推荐
31+
</DetailSectionTitle>
3232
)}
3333
ariaLabel="相似推荐"
3434
itemCount={recommendations.length}

web/src/components/media/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export { default as RecommendationCarousel } from './RecommendationCarousel'
77
export { default as TrailerModal } from './TrailerModal'
88
export { default as CastGrid } from './CastGrid'
99
export { default as CollectionCarousel } from './CollectionCarousel'
10+
export { default as DetailSectionTitle } from './DetailSectionTitle'

0 commit comments

Comments
 (0)