| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- import { Card, Tag, Typography } from 'antd'
- import { PlayCircleFilled } from '@ant-design/icons'
- import type { VideoMatchEnrichedVO } from '../api/types'
- import { toHttps } from '../utils/url'
- const { Text, Paragraph } = Typography
- interface VideoCardProps {
- item: VideoMatchEnrichedVO
- }
- export default function VideoCard({ item }: VideoCardProps) {
- const score = item.score != null ? item.score.toFixed(4) : '--'
- const scoreStyle = getScoreStyle(item.score)
- return (
- <Card
- size="small"
- hoverable
- style={{ width: '100%' }}
- cover={renderCover(item)}
- >
- <div
- style={{
- display: 'flex',
- alignItems: 'baseline',
- gap: 8,
- padding: '8px 10px',
- marginBottom: 8,
- background: scoreStyle.bg,
- border: `1px solid ${scoreStyle.border}`,
- borderRadius: 6,
- }}
- >
- <span style={{ fontSize: 12, color: 'rgba(0,0,0,0.55)' }}>相似度</span>
- <span style={{ fontSize: 22, fontWeight: 700, color: scoreStyle.text, lineHeight: 1 }}>
- {score}
- </span>
- </div>
- {item.configCode && (
- <div style={{ marginBottom: 8 }}>
- <Tag>{item.configCode}</Tag>
- </div>
- )}
- <Paragraph
- ellipsis={{ rows: 2, tooltip: item.title ?? '' }}
- style={{ marginBottom: 6, fontWeight: 500, minHeight: 44 }}
- >
- {item.title || <Text type="secondary">(无标题)</Text>}
- </Paragraph>
- <div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)' }}>
- ID: {item.id}
- </div>
- {renderMetrics(item)}
- </Card>
- )
- }
- function getScoreStyle(score: number | null | undefined) {
- if (score == null) {
- return { bg: '#fafafa', border: '#e8e8e8', text: 'rgba(0,0,0,0.45)' }
- }
- if (score >= 0.85) return { bg: '#f6ffed', border: '#b7eb8f', text: '#389e0d' }
- if (score >= 0.75) return { bg: '#fcffe6', border: '#eaff8f', text: '#7cb305' }
- if (score >= 0.65) return { bg: '#fff7e6', border: '#ffd591', text: '#d46b08' }
- return { bg: '#fff1f0', border: '#ffa39e', text: '#cf1322' }
- }
- function renderCover(item: VideoMatchEnrichedVO) {
- let imgSrc: string | null = null
- if (item.cover) {
- imgSrc = toHttps(item.cover)
- } else if (item.imageList && item.imageList.length > 0) {
- imgSrc = toHttps(item.imageList[0])
- }
- const playable = !!item.videoUrl
- const handleClick = () => {
- if (playable) {
- window.open(toHttps(item.videoUrl!), '_blank', 'noopener,noreferrer')
- }
- }
- // 5:4 宽高比 (宽=5, 高=4) → paddingTop = 4/5 = 80%
- const containerStyle: React.CSSProperties = {
- position: 'relative',
- width: '100%',
- paddingTop: '80%',
- background: '#fafafa',
- overflow: 'hidden',
- cursor: playable ? 'pointer' : 'default',
- }
- return (
- <div style={containerStyle} onClick={handleClick}>
- {imgSrc ? (
- <img
- src={imgSrc}
- alt="cover"
- style={{
- position: 'absolute',
- inset: 0,
- width: '100%',
- height: '100%',
- objectFit: 'cover',
- display: 'block',
- }}
- onError={(e) => {
- ;(e.currentTarget as HTMLImageElement).style.visibility = 'hidden'
- }}
- />
- ) : (
- <div
- style={{
- position: 'absolute',
- inset: 0,
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- color: '#bfbfbf',
- }}
- >
- 无封面
- </div>
- )}
- {playable && <PlayOverlay />}
- </div>
- )
- }
- function PlayOverlay() {
- return (
- <div
- style={{
- position: 'absolute',
- inset: 0,
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- pointerEvents: 'none',
- }}
- >
- <div
- style={{
- width: 60,
- height: 60,
- borderRadius: '50%',
- background: 'rgba(0,0,0,0.45)',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- backdropFilter: 'blur(2px)',
- }}
- >
- <PlayCircleFilled style={{ fontSize: 36, color: '#fff' }} />
- </div>
- </div>
- )
- }
- function renderMetrics(item: VideoMatchEnrichedVO) {
- const items: Array<[string, string]> = []
- if (item.modality === 'VIDEO') {
- items.push(['播放量', item.playCount])
- items.push(['ROV', item.rov])
- } else if (item.modality === 'MATERIAL') {
- items.push(['曝光', item.exposure])
- items.push(['CTR', item.ctr])
- } else {
- items.push(['阅读', item.readCount])
- }
- return (
- <div style={{ marginTop: 6, fontSize: 12, color: 'rgba(0,0,0,0.65)' }}>
- {items.map(([k, v]) => (
- <span key={k} style={{ marginRight: 12 }}>
- {k}: {v}
- </span>
- ))}
- </div>
- )
- }
|