import { createRichTextDocument, renderRichTextToReact } from '@kkk/richtext' import { PlayIcon } from '@phosphor-icons/react' import { differenceInSeconds, format, formatDistanceToNow, fromUnixTime } from 'date-fns' import { zhCN } from 'date-fns/locale' import { Search } from 'lucide-react' import React, { type ReactNode } 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 { DouyinCommentIcon, DouyinFavoriteIcon, DouyinLikeIcon, DouyinMoreIcon, DouyinShareIcon } from '../../components/Icons' import type { DouyinSubComment } from '../../components/types' import type { DouyinCommentData } from './types' const douyinMentionClassName = 'text-[#04498d] dark:text-[#face15]' const douyinSearchKeywordClassName = 'font-medium text-[#04498d] dark:text-[#face15]' const douyinSearchKeywordIconClassName = 'opacity-90' // 注意不能提升到模块顶层调用:打包后注册表与共享 chunk 存在循环引用, // 模块求值期调用会在「未初始化」的绑定上炸出 is not a function,必须惰性创建。 const createEmptyDouyinRichText = () => createRichTextDocument([], { platform: 'douyin' }) const renderDouyinCommentRichText = (content: DouyinSubComment['text'] | DouyinCommentData['CommentsData'][number]['text']): ReactNode => { return renderRichTextToReact(content, { mention: { className: douyinMentionClassName }, searchKeyword: { className: douyinSearchKeywordClassName, iconClassName: douyinSearchKeywordIconClassName } }) } const formatDouyinCommentTime = (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 formatDouyinCommentDiggCount = (count: number): string => { if (count >= 10000) { return `${(count / 10000).toFixed(1)}w` } return String(count) } const formatPublishTime = (timestamp: number): string => { if (!timestamp) return '' return format(fromUnixTime(timestamp), 'yyyy年MM月dd日 HH:mm', { locale: zhCN }) } // const DouyinLogo: React.FC<{ useDarkTheme?: boolean }> = ({ useDarkTheme }) => { // const [hasError, setHasError] = React.useState(false) // if (hasError) { // return ( //
// 抖音 //
// ) // } // return ( // 抖音Logo setHasError(true)} // /> // ) // } /** * 二维码组件 * @param props 组件属性 * @returns JSX元素 */ const QRCodeSection: React.FC & { useDarkTheme?: boolean }> = (props) => { return (
) } /** * 视频信息头部组件 * @param props 组件属性 * @returns JSX元素 */ const VideoInfoHeader: React.FC & { useDarkTheme?: boolean }> = (props) => { return (
{/* 左侧内容区 */}
{/* 作者信息 + Logo */}
{/* 作者头像 */}
{props.Author}
{/* 作者名 + 时间 */}
{props.Author}
{formatPublishTime(props.CreateTime)}
{/* 抖音 Logo */} {/*
*/}
{/* 数据统计 */}
{formatDouyinCommentDiggCount(props.Statistics.digg_count)}
{formatDouyinCommentDiggCount(props.Statistics.comment_count)}
{formatDouyinCommentDiggCount(props.Statistics.share_count)}
{formatDouyinCommentDiggCount(props.Statistics.collect_count)}
{/* 规格信息 */}
{props.Type} {props.Type === '视频' && props.Resolution && ( <> · {props.Resolution} )} {props.Type === '视频' ? ( <> · {props.VideoSize}MB · {props.VideoFPS}Hz ) : ( <> · {props.Region} · {props.ImageLength}张 )}
{/* 右侧二维码 */}
) } interface ReplyNode extends DouyinSubComment { children: ReplyNode[] hiddenCount?: number } const organizeReplies = (replies: DouyinSubComment[], rootCid: string, maxDepth: number = 6): ReplyNode[] => { const map = new Map() const roots: ReplyNode[] = [] // 第一遍:创建节点 replies.forEach((r) => { map.set(r.cid, { ...r, children: [] }) }) // 第二遍:构建树 replies.forEach((r) => { const node = map.get(r.cid)! const parentId = r.reply_to_reply_id if (parentId && map.has(parentId) && parentId !== rootCid && parentId !== '0') { map.get(parentId)!.children.push(node) } else { roots.push(node) } }) // 计算深度并修剪树的函数 const pruneTree = (nodes: ReplyNode[], currentDepth: number): ReplyNode[] => { if (currentDepth > maxDepth) { const count = nodes.length + nodes.reduce((acc, node) => acc + countChildren(node), 0) if (count > 0) { return [ { cid: `more-${Date.now()}-${Math.random()}`, text: createEmptyDouyinRichText(), digg_count: 0, create_time: 0, nickname: '', userimageurl: '', ip_label: '', text_extra: [], label_text: '', image_list: null, reply_to_reply_id: '', reply_to_username: '', children: [], hiddenCount: count } ] } return [] } return nodes.map((node) => { node.children = pruneTree(node.children, currentDepth + 1) return node }) } const countChildren = (node: ReplyNode): number => { return node.children.length + node.children.reduce((acc, child) => acc + countChildren(child), 0) } return pruneTree(roots, 1) } const ReplyItemComponent: React.FC<{ reply: ReplyNode; depth?: number; isLast?: boolean; maxDepth?: number }> = ({ reply, depth = 0, isLast, maxDepth = 6 }) => { const nicknameLength = reply.nickname.length const replyToLength = reply.reply_to_username?.length || 0 const isNicknameLonger = nicknameLength >= replyToLength if (reply.hiddenCount) { return (
{/* 外部网格:处理缩进和树连接 第1列:父级线程线(脊柱) + 连接到此评论的线(曲线) 第2列:评论本身 */}
{/* 第1列:树连接区域 */}
{/* 1. 脊柱:来自父级的垂直线 */} {/* 如果不是最后一个子节点则穿过 */} {!isLast &&
} {/* 脊柱延伸用于边距间隙 */} {/* 连接到下一个兄弟节点的 mb-6 间隙 */} {!isLast &&
} {/* 2. 曲线:L形连接到当前评论 */}
{/* 第2列:显示更多文本 */}
另外 {reply.hiddenCount} 条回复
) } return (
{/* 外部网格:处理缩进和树连接 第1列:父级线程线(脊柱) + 连接到此评论的线(曲线) 第2列:评论本身 */}
{/* 第1列:树连接区域 */}
{/* 1. 脊柱:来自父级的垂直线 */} {/* 如果不是最后一个子节点则穿过 */} {!isLast &&
} {/* 脊柱延伸用于边距间隙 */} {/* 连接到下一个兄弟节点的 mb-6 间隙 */} {!isLast &&
} {/* 2. 曲线:L形连接到当前评论 */}
{/* 第2列:评论主体(六宫格) */}
{/* 内部网格:头像 | 内容 */}
{/* 内部第1列:头像 & 子线程线 */} {/* 添加 h-full 以确保其拉伸以匹配内容高度 */}
{/* 头像 - 固定高度 */}
用户头像
{/* 子线程线 - 从头像下方开始并延伸到此单元格底部 */} {reply.children.length > 0 &&
}
{/* 内部第2列:头部、内容、操作 */}
{/* 第1行:头部 */}
{reply.nickname} {reply.label_text !== '' && (
{reply.label_text}
)} {reply.reply_to_username && (
{reply.reply_to_username}
)}
{/* 第2行:内容 */}
{renderDouyinCommentRichText(reply.text)}
{reply.image_list && reply.image_list.length > 0 && reply.image_list.filter(Boolean).map((img, idx) => (
评论图片
))}
{/* 第3行:操作 */}
{formatDouyinCommentTime(reply.create_time)} {reply.ip_label}
{formatDouyinCommentDiggCount(reply.digg_count)}
{/* 子容器 - 递归 */} {reply.children.length > 0 && (
{reply.children.map((child, index) => ( ))}
)}
) } /** * 单个评论组件 * @param props 组件属性 * @returns JSX元素 * @description Root comment component */ const CommentItemComponent: React.FC = (props) => { return (
{/* 根网格 - 单列主体(根节点没有连接列) */}
{/* 内部网格:头像 | 内容 */}
{/* 内部第1列:头像 & 子线程线 */}
{/* 头像 - 根节点更大 */}
用户头像
{/* 子线程线 */} {props.replyComment && props.replyComment.length > 0 && (
)}
{/* 内部第2列:内容 */}
{/* 头部 */}
{props.nickname} {props.label_type === 1 && (
作者
)} {props.is_author_digged && props.status_label !== '作者赞过' && (
作者赞过
)} {props.status_label && (
{props.status_label}
)}
{renderDouyinCommentRichText(props.text)}
{/* 评论图片 */} {(props.commentimage || props.sticker) && (
评论图片
)}
{formatDouyinCommentTime(props.create_time)} {props.ip_label}
{formatDouyinCommentDiggCount(props.digg_count)}
{/* 回复容器 */} {/* 根缩进调整: 根头像列为 140px。中心点为 70px。 子外部列为 100px。中心点为 50px。 我们需要子中心点(50px)与根中心点(70px)对齐。 所以我们需要将子元素向右推 20px。 容器上使用 pl-5。 */} {props.replyComment && props.replyComment.length > 0 && (
{organizeReplies(props.replyComment, props.cid || '', props.maxDepth).map((reply, index, arr) => ( ))}
)}
) } /** * 抖音评论组件 * @param props 组件属性 * @returns JSX元素 */ export const DouyinComment: React.FC> = React.memo((props) => { // 随机选择一个搜索词 const randomSuggestWord = () => { if (props.data.suggestWrod && props.data.suggestWrod.length > 0) { const randomIndex = Math.floor(Math.random() * props.data.suggestWrod.length) return props.data.suggestWrod[randomIndex] } return null } return (
{/* 视频信息头部 */} {/* 推荐搜索词 */} {randomSuggestWord() && (
大家都在搜: {randomSuggestWord()}
)} {/* {randomSuggestWord && (
相关搜索 {randomSuggestWord}
)} */} {/* 评论列表 */}
{props.data.CommentsData.length > 0 ? ( <> {props.data.CommentsData.map((comment, index) => ( ))} ) : (

暂无评论数据

)}
) }) export default DouyinComment