import { renderRichTextToReact } from '@kkk/richtext' import { PlayIcon } from '@phosphor-icons/react' import { format } from 'date-fns' import { Calendar, Heart, MapPin, MessageCircle, Share2, Star } from 'lucide-react' import React from 'react' import { isDark } from '../../../../utils/theme' import { AmbientCover } from '../../../components/AmbientCover' import { DefaultLayout } from '../../../components/DefaultLayout' import { QRCodeWithAvatar } from '../../../components/QRCodeWithAvatar' import type { PosterProps } from '../../../types/ctx' import type { XiaohongshuNoteInfoData } from './types' /** 小红书品牌红:整图唯一强调色,只用于点赞图标与头像描边 */ const XHS_RED = '#FF2442' const xiaohongshuNoteMentionClassName = 'text-[#13386c] dark:text-[#c7daef]' /** * 格式化互动数:纯数字串过万缩写为 x.x万;API 已格式化的「5.7万」等原样返回 * @param num 数字 * @returns 格式化后的字符串 */ const formatNumber = (num: string | number): string => { if (typeof num === 'string' && !/^\d+$/.test(num.trim())) return num || '0' const numValue = typeof num === 'string' ? parseInt(num, 10) : num if (!Number.isFinite(numValue)) return String(num) || '0' if (numValue >= 10000) { return `${(numValue / 10000).toFixed(1)}万` } return numValue.toLocaleString() } /** * 全局氛围背景层:弥散渐变封面 + 高对比杂色纹理。 * 与抖音/B站同一套骨架,噪点集中在上下两端、中间让位给正文。 */ const NoteDiffuseBackground: React.FC> = ({ data, ctx }) => (
) /** * 顶部系统标签行:海报式技术标签 + 笔记 ID,替代平台 Logo 的顶部占位 */ const NoteSystemLabel: React.FC> = ({ data }) => (
Xiaohongshu Note
ID: {data.note_id}
) /** * 笔记封面:官方分享长图的干净大图样式,圆角 + 大投影,不做溶解遮罩。 * 图集在右下角堆叠预览后续图片,超出部分以 +N 收尾;视频笔记改为白色半透明播放图标。 */ const NoteCover: React.FC> = ({ data }) => { const images = data.image_list?.length ? data.image_list : [data.image_url] const previewImages = data.is_video ? [] : images.slice(1, 4).filter(Boolean) const remainingPreviewCount = Math.max(images.length - previewImages.length - 1, 0) return (
{data.title {data.is_video && ( )} {previewImages.length > 0 && (
{previewImages.map((url, index) => ( 图集预览 ))} {remainingPreviewCount > 0 && (
+{remainingPreviewCount}
)}
)}
) } /** * 作者行:官方长图位置(封面之后、标题之前),头像带品牌红描边 */ const NoteAuthorRow: React.FC> = ({ data }) => (
{data.author.nickname}
{data.author.nickname}
{format(new Date(data.time), 'yyyy-MM-dd HH:mm')} {data.ip_location && ( {data.ip_location} )}
) /** * 标题与正文:标题是唯一大字层级,正文降为次级明度 */ const NoteContent: React.FC> = ({ data }) => (
{data.title && (

{data.title}

)}
{renderRichTextToReact(data.desc, { mention: { className: xiaohongshuNoteMentionClassName } })}
) /** * 互动数据行:大数字做视觉锚点,只有点赞用心红,其余中性色 */ const NoteStatsRow: React.FC> = ({ data }) => { const stats = [ { icon: Heart, value: data.statistics.liked_count, label: '点赞', accent: true }, { icon: MessageCircle, value: data.statistics.comment_count, label: '评论', accent: false }, { icon: Star, value: data.statistics.collected_count, label: '收藏', accent: false }, { icon: Share2, value: data.statistics.share_count, label: '分享', accent: false } ] return (
{stats.map((stat) => { const Icon = stat.icon return (
{stat.label}
{formatNumber(stat.value)}
) })}
) } /** * 底部行动条:官方长图的小红书 Logo + 二维码收尾 */ const NoteFooter: React.FC> = ({ data, ctx }) => (
小红书 {data.share_url && (
扫码查看原笔记
长按识别二维码
)}
) /** * 小红书笔记信息组件 * @param props 组件属性 * @returns JSX元素 */ export const XiaohongshuNoteInfo: React.FC> = React.memo((props) => { return (
) }) XiaohongshuNoteInfo.displayName = 'XiaohongshuNoteInfo' export default XiaohongshuNoteInfo