import { renderRichTextToReact } from '@kkk/richtext' import { differenceInSeconds, format, formatDistanceToNow, fromUnixTime } from 'date-fns' import { zhCN } from 'date-fns/locale' import React, { type ReactNode, useEffect, useState } from 'react' import { cn } from '../../../../utils/cn' import { isDark } from '../../../../utils/theme' import { DefaultLayout } from '../../../components/DefaultLayout' import { QRCodeWithAvatar } from '../../../components/QRCodeWithAvatar' import type { PosterProps } from '../../../types/ctx' import { ThumbUpIcon } from '../../components/Icons' import type { FansDetail, QRCodeSectionProps } from '../../components/types' import type { BilibiliCommentData } from './types' const bilibiliMentionClassName = 'text-[#006A9E] dark:text-[#58B0D5]' /** * 将 B 站 32 位 ARGB 整数转为 rgba 字符串 */ const intToRgba = (color: number): string => { const a = ((color >>> 24) & 0xff) / 255 const r = (color >>> 16) & 0xff const g = (color >>> 8) & 0xff const b = color & 0xff return `rgba(${r}, ${g}, ${b}, ${a})` } /** * 粉丝勋章组件 */ const FansMedal: React.FC<{ detail: FansDetail }> = ({ detail }) => { const bgStart = intToRgba(detail.medal_color) const bgEnd = intToRgba(detail.medal_color_end) const borderColor = intToRgba(detail.medal_color_border) const nameColor = intToRgba(detail.medal_color_name) const levelColor = intToRgba(detail.medal_color_level) return (
{detail.first_icon && ( )}
{detail.medal_name} {detail.level}
) } /** * 置顶标签组件 */ const TopBadge: React.FC = () => { return ( 置顶 ) } /** * 带有加载状态的图片组件 * @param props 图片组件属性 * @returns JSX元素 */ interface ImageWithSkeletonProps { /** 图片源地址 */ src: string /** 替代文本 */ alt: string /** CSS类名 */ className?: string /** 占位符文本 */ placeholder?: string /** 是否为圆形 */ isCircular?: boolean } const ImageWithSkeleton: React.FC = ({ src, alt, className = '', placeholder }) => { const [hasError, setHasError] = useState(false) /** * 处理图片加载失败 */ const handleError = () => { setHasError(true) } // 重置状态当src改变时 useEffect(() => { setHasError(false) }, [src]) if (hasError) { return (
{placeholder || '图片加载失败'}
) } return ( {alt} ) } const renderBilibiliCommentRichText = (content: BilibiliCommentData['CommentsData'][number]['message']): ReactNode => { return renderRichTextToReact(content, { mention: { className: bilibiliMentionClassName } }) } const formatBilibiliCommentTime = (timestamp: number): string => { if (!timestamp) { return '' } const commentDate = fromUnixTime(timestamp) const diffSeconds = differenceInSeconds(new Date(), commentDate) if (diffSeconds < 30) { return '刚刚' } if (diffSeconds < 7776000) { return formatDistanceToNow(commentDate, { locale: zhCN, addSuffix: true }) } return format(commentDate, 'yyyy-MM-dd') } const formatBilibiliLikeCount = (count: number): string => { if (count > 10000) { return `${(count / 10000).toFixed(1)}w` } return String(count) } const renderBilibiliUserName = (uname: string, unameColor: string | null | undefined, vipstatus?: number) => { return ( {uname} ) } const BilibiliLogo: React.FC = () => { const [hasError, setHasError] = useState(false) if (hasError) { return
哔哩哔哩
} return ( B站Logo setHasError(true)} /> ) } /** * 二维码组件 * @param props 组件属性 * @returns JSX元素 */ const QRCodeSection: React.FC = ({ share_url, useDarkTheme, avatarUrl }) => { return (
) } /** * 视频信息头部组件 * @param props 组件属性 * @returns JSX元素 */ const VideoInfoHeader: React.FC & { useDarkTheme: boolean }> = (props) => { return (
{/* 左侧信息区域 */}
{/* Logo 和分辨率区域 */}
{/* Logo */}
{/* 分辨率信息 - 仅视频类型显示 */} {props.Type === '视频' && props.Resolution && (
分辨率(px) {props.Resolution}
)}
{/* 信息列表 */}
类型 {props.Type}
评论 {props.CommentLength}条
{props.Type === '视频' ? ( <>
大小 {props.VideoSize}
画质 {props.Clarity}
) : ( <>
图片 {props.ImageLength}张
)}
{/* 右侧二维码区域 */}
) } /** * 评论项组件 * @param props 组件属性 * @returns JSX元素 */ const CommentItemComponent: React.FC = ({ isLast = false, ...props }) => { return (
{/* 用户头像区域 */}
{/* 主头像 */} {/* 头像框 */} {props.frame && ( )} {/* 大会员标识 */} {props.vipstatus === 1 && (
大会员
)}
{/* 评论内容区域 */}
{/* 用户信息 */}
{/* 用户名区域 */}
{renderBilibiliUserName(props.uname, props.unameColor, props.vipstatus)} {/* 等级图标 */} {props.level !== undefined && props.level >= 0 && props.level <= 7 && ( {`等级${props.level}`} )} {/* UP主标签 */} {props.isUP && UP主标签} {/* 粉丝勋章 */} {props.fansDetail && }
{/* 粉丝卡片 - 绝对定位在右侧,不会被挤出 */} {props.fanCard && props.fanCard.image && (
粉丝卡片
{props.fanCard.numPrefix} {props.fanCard.numDesc}
)} {/* 作者标签 */} {props.label_type === 1 && (
作者
)} {/* 状态标签 */} {props.status_label && (
{props.status_label}
)}
{/* 评论文本 */}
{props.isTop && } {renderBilibiliCommentRichText(props.message)}
{/* 评论图片 */} {props.pictures && props.pictures.length > 0 && (
{props.pictures.length === 1 ? ( // 只有一张图片时,完整显示
) : ( // 多张图片时,显示两张正方形缩略图 props.pictures.slice(0, 2).map((picUrl, idx) => (
{/* 如果是第二张图且总数超过2张,显示+x标志 */} {idx === 1 && props.pictures.length > 2 && (
{/* 悬浮按钮背景 */}
{/* +x 文字 */} + {props.pictures.length - 2}
)}
)) )}
)} {/* 点赞区域 */}
{formatBilibiliCommentTime(props.ctime)} · {props.location} {props.replylength > 0 ? ( {props.replylength}回复 ) : ( 回复 )}
{formatBilibiliLikeCount(props.like)}
{/* 二级评论 */} {props.replies && props.replies.length > 0 && (
{props.replies.map((subReply, index) => (
{/* 二级评论头像区域 */}
{subReply.frame && ( )} {/* 大会员标识 */} {subReply.vipstatus === 1 && (
大会员
)}
{/* 二级评论内容 */}
{/* 用户信息 */}
{renderBilibiliUserName(subReply.uname, subReply.unameColor, subReply.vipstatus)} {subReply.level !== undefined && subReply.level >= 0 && subReply.level <= 7 && ( {`等级${subReply.level}`} )} {subReply.isUP && ( UP主标签 )} {/* 粉丝勋章 */} {subReply.fansDetail && }
{/* 二级评论粉丝卡片 */} {subReply.fanCard && subReply.fanCard.image && (
粉丝卡片
{subReply.fanCard.numPrefix} {subReply.fanCard.numDesc}
)}
{/* 评论文本 */}
{renderBilibiliCommentRichText(subReply.message)}
{/* 二级评论图片 */} {subReply.pictures && subReply.pictures.length > 0 && (
{subReply.pictures.length === 1 ? ( // 只有一张图片时,完整显示
) : ( // 多张图片时,显示两张正方形缩略图 subReply.pictures.slice(0, 2).map((picUrl, idx) => (
{/* 如果是第二张图且总数超过2张,显示+x标志 */} {idx === 1 && subReply.pictures.length > 2 && (
{/* 悬浮按钮背景 */}
{/* +x 文字 */} +{subReply.pictures.length - 2}
)}
)) )}
)} {/* 二级评论底部信息 */}
{formatBilibiliCommentTime(subReply.ctime)} · {subReply.location}
{formatBilibiliLikeCount(subReply.like)}
))}
)}
) } export const BilibiliComment: React.FC> = React.memo((props) => { return (
{/* 视频信息头部 */} {/* 评论列表 */}
{props.data.CommentsData.length > 0 ? ( props.data.CommentsData.map((comment, index) => ( )) ) : (

暂无评论数据

)}
) }) export default BilibiliComment