【图片预览】怎么在 Markdown 文档解析的 html 中实现图片预览功能?

问题背景:大模型的回答都是流式输出,输出的语料大都是 md 格式,前端拿到这些 md 数据后解析为 html 用于流式显示。现在在模型回答的内容中新增一个图片点击预览功能,方便用户查看图片。

解决原理:事件循环机制。

基于事件循环机制和事件委托,我们可以实现一个高效的图片点击预览功能,即使图片是动态加载的。以下是实现原理:

事件委托:利用事件冒泡机制,在父容器上监听点击事件,而不是直接给每个图片绑定事件

动态检测:无论图片何时加载完成,都能通过事件目标判断是否点击了图片

懒处理:不需要预先扫描DOM中的图片,只在点击发生时检查目标元素

示例代码:

import {
  createElement,
  CSSProperties,
  ReactHTML,
  Fragment,
  useMemo,
  useEffect,
  useState,
  SetStateAction,
} from 'react';
import classNames from 'classnames';
import { parse } from '@sobot/utils/markdown';
import { addTargetToHTMLLinks } from '@/utils';
import { MessageSide, IMessage } from '../types';
import styles from './index.less';
import { Image } from '@sobot/soil-ui';

interface CursorProps {
  side?: MessageSide;
  status?: string;
}

// 光标效果
const renderCursor = ({ status, side }: CursorProps): string => {
  const dom =
    status === 'pending' && side !== 'right'
      ? `<span class=${styles.cursor} />`
      : '';
  return dom;
};

export interface RichTextProps extends CursorProps {
  tag?: keyof ReactHTML;
  content?: string;
  className?: string;
  style?: CSSProperties;
  msgType?: IMessage['msgType'];
  previewImg?: boolean;
}

export default function RichText({
  status,
  side,
  tag = 'span',
  className,
  content,
  msgType,
  previewImg = false, // 是否预览图片
  ...props
}: RichTextProps) {
  const [viewImgSrc, setViewImgSrc] = useState<string | undefined>();

  useEffect(() => {
    return () => {
      setViewImgSrc(undefined);
    };
  }, []);
  const onClickImg = (e: React.MouseEvent<HTMLElement>) => {
    const target = e.target as HTMLElement;
    if (target.nodeName === 'IMG') {
      setViewImgSrc((target as HTMLImageElement).src);
    }
  };

  const html = useMemo(() => {
    switch (msgType) {
      // 图片
      case 'IMG': {
        const str = `<img src="${content}" />`;
        return parse(str);
      }
      default:
        const cursorStr = renderCursor({ status, side });
        const _html = parse((content || '') + cursorStr);
        return addTargetToHTMLLinks(_html);
    }
  }, [content, status]);

  const renderNode = createElement(tag, {
    ...props,
    className: classNames(styles.html, className),
    dangerouslySetInnerHTML: { __html: html },
    onClick: onClickImg,
  });

  return (
    <>
      {renderNode}
      {previewImg ? (
        <div style={{ display: 'none' }}>
          <Image
            width={0}
            preview={{
              visible: !!viewImgSrc,
              src: viewImgSrc,
              onVisibleChange: (value) => {
                setViewImgSrc(undefined);
              },
            }}
          />
        </div>
      ) : null}
    </>
  );
}

posted @ 2025-07-01 11:47  muling9955  阅读(109)  评论(0)    收藏  举报