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 (
)
}
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 (
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 && (

)}
{/* UP主标签 */}
{props.isUP &&

}
{/* 粉丝勋章 */}
{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.isUP && (

)}
{/* 粉丝勋章 */}
{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