IM 聊天组件

3,181 阅读3分钟

IM 消息通常分为文本、图片、文件等 3 类,会对应不同的展示

im_3.png

传入参数

自定义内容:标题(title)、内容(children)、底部(footer)

弹框组件显隐控制:

一般通过一个变量控制显示或隐藏(visible);

并且暴露出一个事件,控制该变量(setVisible)

interface iProps {
    title?: string // 标题
    maskClose?: boolean // 点击 x 或 mask 回调
    visible?: boolean // 是否显示
    setVisible: (args) => void // 设置是否显示
    children?: React.ReactNode | Array<React.ReactNode> // 自定义内容
    footer?: React.ReactNode | Array<React.ReactNode> // 自定义底部
}

基础结构

IM 聊天组件基础结构包含:头部、内容区、尾部

function wsDialog(prop: iProps) {
  const wsContentRef = useRef(null); // 消息区
  const { title = "消息", maskClose, visible, setVisible } = prop; // 传入参数
  const [message, setMessage] = useState(""); // 当前消息
  const imMessage = useSelector(
    (state: rootState) => state.mediaReducer.imMessage
  ); // 消息列表 全局管理

  return (
    <Modal
      className={styles.ws_modal}
      visible={visible}
      transparent
      onClose={handleMaskClose}
      popup
      animationType="slide-up"
    >
      <div className={styles.ws_modal_widget}>
        {/* 头部 */}
        <div className={styles.ws_header}></div>
        {/* 内容区 */}
        <div ref={wsContentRef} className={styles.ws_content}></div>
        {/* 尾部区域 */}
        <div className={styles.ws_footer}></div>
      </div>
    </Modal>
  );
}

头部区

头部区域主要展示标题和关闭图标

标题内容可以自定义

不仅可以点击“右上角关闭图标”进行关闭

也可以通过点击“遮罩”进行关闭

// 头部关闭事件
function handleClose() {
  slLog.log("[wsDialog]点击了关闭按钮");
  setVisible(false);
}

// 弹框遮罩关闭事件
function handleMaskClose() {
  if (maskClose) {
    slLog.log("[wsDialog]点击了遮罩关闭");
    setVisible(false);
  }
}

// 头部区域
<div className={styles.ws_header}>
  <div>{title}</div>
  <div className={styles.ws_header_close} onClick={handleClose}>
    <Icon type="cross" color="#999" size="lg" />
  </div>
</div>;

内容区

消息内容分类展示:

  1. 文本:直接展示内容
  2. 图片:通过 a 标签包裹展示,可以在新标签页中打开,通过target="_blank"控制
  3. 文件:不同类型文件展示不同的图标,包括 zip、rar、doc、docx、xls、xlsx、pdf、txt 等;文件还可以进行下载
<div ref={wsContentRef} className={styles.ws_content}>
  {imMessage &&
    imMessage.length &&
    imMessage.map((o, index) => {
      return (
        <div
          key={index}
          className={`${styles.item} ${
            o.category === "send" ? styles.self_item : ""
          }`}
        >
          <div className={styles.title}>{o.showName + " " + o.showNum}</div>
          {/* 消息为图片 */}
          {o.desc === "img" ? (
            <a
              className={`${styles.desc} ${styles.desc_image}`}
              href={o.fileUrl}
              title={o.fileName}
              target="_blank"
            >
              <img src={o.fileUrl} />
            </a>
          ) : o.desc === "file" ? (
            // 消息为文件
            <div className={`${styles.desc} ${styles.desc_file}`}>
              <img
                className={styles.file_icon}
                src={handleSuffix(o.fileSuffix)}
              />
              <div className={styles.file_content}>
                <a title={o.fileName}>{o.fileName}</a>
                <div>{o.fileSize}</div>
              </div>
              <img
                className={styles.down_icon}
                src={downIcon}
                onClick={() => handleDownload(o)}
              />
            </div>
          ) : (
            // 消息为文本
            <div className={`${styles.desc} ${styles.desc_message}`}>
              {o.message}
            </div>
          )}
        </div>
      );
    })}
</div>

文件下载通过 a 标签模拟实现

// 下载文件
function handleDownload(o) {
  slLog.log("[SLIM]下载消息文件", o.fileUrl);
  const a = document.createElement("a");
  a.href = o.fileUrl;
  a.download = o.fileName;
  document.body.appendChild(a);
  a.target = "_blank";
  a.click();
  a.remove();
}

监听消息内容,自动滚动到最底部处理

useEffect(() => {
  if (visible && imMessage && imMessage.length) {
    // 滚动到底部
    wsContentRef.current.scrollTop = wsContentRef.current.scrollHeight;
  }
}, [visible, imMessage]);

尾部区

主要是操作区,用于展示和发送文本、图片、文件等消息。

图片和文件通过原生input实现,通过accept属性控制文件类型

<div className={styles.ws_footer}>
  <div className={styles.tools_panel}>
    {/* 上传图片 */}
    <div className={styles.tool}>
      <img src={imageIcon} />
      <input type="file" accept="image/*" onChange={handleChange("img")} />
    </div>
    {/* 上传文件 */}
    <div className={styles.tool}>
      <img src={fileIcon} />
      <input
        type="file"
        accept=".doc,.docx,.pdf,.txt,.xls,.xlsx,.zip,.rar"
        onChange={handleChange("file")}
      />
    </div>
  </div>
  <div className={styles.input_panel}>
    {/* 输入框,上传文本 */}
    <input
      placeholder="输入文本"
      value={message}
      onChange={handleInputChange}
      className={`${styles.message} ${styles.mMessage}`}
      onKeyUp={handleKeyUp}
    />
    {/* 消息发送按钮 */}
    <div onClick={handleMessage} className={styles.btn}>
      发送
    </div>
  </div>
</div>

获取图片、文件信息:

// 消息处理
function handleChange(type) {
  return (ev) => {
    switch (type) {
      case "img":
      case "file":
        msgObj.type = type === "img" ? 4 : 7;
        const e = window.event || ev;
        const files = e.target.files || e.dataTransfer.files;
        const file = files[0];
        msgObj.content = file;
        break;
    }
  };
}

实现回车键发送消息:

通过输入框,发送文本消息时,一般需要监听回车事件(onKeyUp 事件中的 event.keyCode 为 13),也能发送消息

// 回车事件
function handleKeyUp(event) {
  const value = event.target.value;
  if (event.keyCode === 13) {
    slLog.log("[wsDialog]onKeyUp", value, event.keyCode);
    handleInputChange(event);
    handleMessage();
  }
}

组件封装

组件级别:公司级、系统级、业务级

组件封装优势:

  1. 提升开发效率,组件化、统一化管理
  2. 考虑发布成 npm 形式,远程发布通用

组件封装考虑点:

  1. 组件的分层和分治
  2. 设置扩展性(合理预留插槽)
  3. 兼容性考虑(向下兼容)
  4. 使用对象考虑
  5. 适用范围考虑

组件封装步骤:

  1. 建立组件的模板:基础架子,UI 样式,基本逻辑
  2. 定义数据输入:分析逻辑,定义 props 里面的数据、类型
  3. 定义数据输出:根据组件逻辑,定义要暴露出来的方法,$emit 实现等
  4. 完成组件内部的逻辑,考虑扩展性和维护性
  5. 编写详细的说明文档