实现RAGFlow-0.14.1的输入框多行输入和消息框的多行显示

news2025/1/20 3:34:36

 一、Chat页面输入框的修改


1. macOS配置

我使用MacBook Pro,chip 是 Apple M3 Pro,Memory是18GB,macOS是 Sonoma 14.6.1。

2. 修改chat输入框代码

目前RAGFlow前端的chat功能,输入的内容是单行的,不能主动使用Shift+Enter实现分行。根据 src/pages/chat/index.tsx 文件,可以看出该文件是聊天页面的主入口,整体结构是将聊天内容通过 <ChatContainer /> 组件呈现。因此,如果要实现多行文本框功能,主要修改点会在 ChatContainer 组件的实现中。

chat/chat-container/index.tsx 中,可以看到消息输入功能是通过 <MessageInput /> 组件实现的。如果需要将单行输入框改为支持多行输入的 TextArea,需要修改 MessageInput 组件的实现。

修改src/components/message-input/index.tsx的代码如下:

return (
    <Flex
      className={styles.messageInputWrapper}
      style={{
        backgroundColor: '#f7f8fa', // 淡灰色背景
        border: '1px solid #e0e0e0', // 外部边框颜色
        borderRadius: '12px', // 圆角增加为原来的 1.5 倍
        padding: '10px 12px', // 内边距
        boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', // 添加阴影
      }}
      vertical
    >
      {/* 输入框 */}
      <Input.TextArea
        size="large"
        placeholder={t('sendPlaceholder')}
        value={value}
        disabled={disabled}
        autoSize={{ minRows: 1, maxRows: 6 }} // 默认一行,自动调整至 6 行
        style={{
          flex: 1,
          border: 'none', // 禁用自带边框
          outline: 'none', // 去掉选中高亮
          boxShadow: 'none', // 禁用焦点样式
          resize: 'none', // 禁用用户手动调整大小
          fontSize: '14px',
          lineHeight: '20px', // 行高,保证单行内容视觉效果
          padding: '0', // 去掉多余的填充
          // overflow: 'hidden', // 禁止滚动条显示
          backgroundColor: '#f7f8fa', // 与外层背景色一致
        }}
        onPressEnter={(e) => {
          if (!e.shiftKey) {
            e.preventDefault();
            handlePressEnter();
          }
        }}
        onChange={onInputChange as ChangeEventHandler<HTMLTextAreaElement>}
      />

      {/* 按钮区域 */}
      <Flex
        justify="space-between"
        align="center"
        style={{
          marginTop: '8px',
        }}
      >
        {showUploadIcon && (
          <Upload
            onPreview={handlePreview}
            onChange={handleChange}
            multiple={false}
            onRemove={handleRemove}
            showUploadList={false}
            beforeUpload={() => {
              return false;
            }}
          >
            <Button
              type={'text'}
              disabled={disabled}
              icon={
                <SvgIcon
                  name="paper-clip"
                  width={18}
                  height={22}
                  disabled={disabled}
                ></SvgIcon>
              }
            ></Button>
          </Upload>
        )}
        <Button
          type="primary"
          onClick={handlePressEnter}
          loading={sendLoading}
          disabled={sendDisabled || isUploadingFile}
          style={{
            height: '40px',
            borderRadius: '12px', // 按钮圆角同步调整
            padding: '0 16px',
          }}
        >
          {t('send')}
        </Button>
      </Flex>

 实际页面输入效果如下:

2.1 替换Input为 Input.TextArea

Input 替换为 Input.TextArea,并添加 autoSize 属性,以实现多行输入框的自动伸缩功能。

2.2 修改发送逻辑

在原有逻辑中,按 Enter 会直接触发消息发送。对于多行输入框,需要支持:

  • Shift + Enter 换行。

  • Enter 发送消息。

上面代码中,onPressEnter 事件已经处理了此逻辑。

  

二、Agent Flow页面中输入框的修改

项目的Agent页面上还有chat,改了component下的message-input,对这个chat不起作用。修改src/pages/flow/box.tsx,关键点说明:

  • Input.TextArea 的使用

    • 替换了原来的 Input,支持多行输入。
    • autoSize 参数允许输入框高度根据内容自动扩展。
  • Shift + Enter 处理

    • 检测 e.shiftKey 是否被按下。
    • Shift 被按下时,不触发消息发送,只换行。
    • 当未按下 Shift 时,发送消息并阻止默认行为。
  • suffix 按钮

    • 保留了发送按钮的逻辑,用户也可以点击按钮发送消息。
return (
    <>
      <Flex flex={1} className={styles.chatContainer} vertical>
        <Flex flex={1} vertical className={styles.messageContainer}>
          <div>
            <Spin spinning={loading}>
              {derivedMessages?.map((message, i) => {
                return (
                  <MessageItem
                    loading={
                      message.role === MessageType.Assistant &&
                      sendLoading &&
                      derivedMessages.length - 1 === i
                    }
                    key={message.id}
                    nickname={userInfo.nickname}
                    avatar={userInfo.avatar}
                    item={message}
                    reference={buildMessageItemReference(
                      { message: derivedMessages, reference },
                      message,
                    )}
                    clickDocumentButton={clickDocumentButton}
                    index={i}
                    showLikeButton={false}
                    sendLoading={sendLoading}
                  ></MessageItem>
                );
              })}
            </Spin>
          </div>
          <div ref={ref} />
        </Flex>
        <Flex
          align="flex-start" // 改为 flex-start,使内容顶部对齐
          style={{
            padding: '12px 20px',
            backgroundColor: '#ffffff', // 白色背景
            borderTop: '1px solid #e8e8e8', // 分割线颜色
            position: 'sticky', // 固定在底部
            bottom: 0,
            zIndex: 100, // 确保浮于内容上方
          }}
        >
          <Input.TextArea
            placeholder={t('sendPlaceholder')}
            value={value}
            autoSize={{ minRows: 1, maxRows: 6 }} // 自动调整高度
            onChange={handleInputChange as React.ChangeEventHandler<HTMLTextAreaElement>}
            onPressEnter={(e) => {
              if (!e.shiftKey) { // Shift+Enter 换行
                e.preventDefault();
                handlePressEnter();
              }
            }}
            style={{
              flex: 1,
              border: '1px solid #e0e0e0', // 边框颜色
              borderRadius: '8px', // 圆角边框
              padding: '10px 12px',
              fontSize: '14px',
              lineHeight: '20px',
              boxShadow: 'none', // 去除阴影
              resize: 'none', // 禁止拖动调整大小
            }}
          />
          <Button
            type="primary"
            onClick={handlePressEnter}
            loading={sendLoading}
            style={{
              marginLeft: '10px',
              borderRadius: '8px',
              padding: '0 16px',
              height: '40px',
              fontSize: '14px',
              display: 'flex',
              alignItems: 'center', // 保持内容居中
              justifyContent: 'center',
              marginTop: 'auto', // 自动保持按钮与输入框底部对齐
            }}
          >
            {t('send')}
          </Button>
        </Flex>
      </Flex>
      <PdfDrawer
        visible={visible}
        hideModal={hideModal}
        documentId={documentId}
        chunk={selectedChunk}
      ></PdfDrawer>
    </>
  );

实际页面输入效果如下:

三、消息框中的显示内容的修改

虽然对话的多行输入没有问题了,对话chat上的消息显示没有跟随输入分行,只是将分行的地方加了一个空格,显得很怪异,现在将chat的消息显示也适配一下多行。


1. 修改src/components/message-item/index.tsx:

要实现消息内容中的换行处理,确保用户输入的内容能够正确地显示多行,我们需要确保在 MessageItem 组件中渲染消息文本时能够正确处理换行符。

修改目标:

  1. 支持多行显示:当用户发送多行消息时,确保文本能够按行显示,而不仅仅是将换行符替换为空格。
  2. CSS 样式处理:通过合适的 CSS 属性(如 white-space: pre-line)来保留换行符。

主要改动:

  • MessageItem 组件中确保显示消息的部分使用正确的 white-space 样式。
  • 如果 item.content 包含换行符,它们将被正确处理并显示为多行。
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
import { MessageType } from '@/constants/chat';
import { useSetModalState } from '@/hooks/common-hooks';
import { IReference } from '@/interfaces/database/chat';
import { IChunk } from '@/interfaces/database/knowledge';
import classNames from 'classnames';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';

import {
  useFetchDocumentInfosByIds,
  useFetchDocumentThumbnailsByIds,
} from '@/hooks/document-hooks';
import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks';
import { IMessage } from '@/pages/chat/interface';
import MarkdownContent from '@/pages/chat/markdown-content';
import { getExtension, isImage } from '@/utils/document-util';
import { Avatar, Button, Flex, List, Space, Typography } from 'antd';
import FileIcon from '../file-icon';
import IndentedTreeModal from '../indented-tree/modal';
import NewDocumentLink from '../new-document-link';
import { AssistantGroupButton, UserGroupButton } from './group-button';
import styles from './index.less';

const { Text } = Typography;

interface IProps extends Partial<IRemoveMessageById>, IRegenerateMessage {
  item: IMessage;
  reference: IReference;
  loading?: boolean;
  sendLoading?: boolean;
  nickname?: string;
  avatar?: string;
  clickDocumentButton?: (documentId: string, chunk: IChunk) => void;
  index: number;
  showLikeButton?: boolean;
}

const MessageItem = ({
  item,
  reference,
  loading = false,
  avatar = '',
  sendLoading = false,
  clickDocumentButton,
  index,
  removeMessageById,
  regenerateMessage,
  showLikeButton = true,
}: IProps) => {
  const isAssistant = item.role === MessageType.Assistant;
  const isUser = item.role === MessageType.User;
  const { data: documentList, setDocumentIds } = useFetchDocumentInfosByIds();
  const { data: documentThumbnails, setDocumentIds: setIds } =
    useFetchDocumentThumbnailsByIds();
  const { visible, hideModal, showModal } = useSetModalState();
  const [clickedDocumentId, setClickedDocumentId] = useState('');

  const referenceDocumentList = useMemo(() => {
    return reference?.doc_aggs ?? [];
  }, [reference?.doc_aggs]);

  const handleUserDocumentClick = useCallback(
    (id: string) => () => {
      setClickedDocumentId(id);
      showModal();
    },
    [showModal],
  );

  const handleRegenerateMessage = useCallback(() => {
    regenerateMessage?.(item);
  }, [regenerateMessage, item]);

  useEffect(() => {
    const ids = item?.doc_ids ?? [];
    if (ids.length) {
      setDocumentIds(ids);
      const documentIds = ids.filter((x) => !(x in documentThumbnails));
      if (documentIds.length) {
        setIds(documentIds);
      }
    }
  }, [item.doc_ids, setDocumentIds, setIds, documentThumbnails]);

  return (
    <div
      className={classNames(styles.messageItem, {
        [styles.messageItemLeft]: item.role === MessageType.Assistant,
        [styles.messageItemRight]: item.role === MessageType.User,
      })}
    >
      <section
        className={classNames(styles.messageItemSection, {
          [styles.messageItemSectionLeft]: item.role === MessageType.Assistant,
          [styles.messageItemSectionRight]: item.role === MessageType.User,
        })}
      >
        <div
          className={classNames(styles.messageItemContent, {
            [styles.messageItemContentReverse]: item.role === MessageType.User,
          })}
        >
          {item.role === MessageType.User ? (
            <Avatar
              size={40}
              src={
                avatar ??
                'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
              }
            />
          ) : (
            <AssistantIcon></AssistantIcon>
          )}
          <Flex vertical gap={8} flex={1}>
            <Space>
              {isAssistant ? (
                index !== 0 && (
                  <AssistantGroupButton
                    messageId={item.id}
                    content={item.content}
                    prompt={item.prompt}
                    showLikeButton={showLikeButton}
                    audioBinary={item.audio_binary}
                  ></AssistantGroupButton>
                )
              ) : (
                <UserGroupButton
                  content={item.content}
                  messageId={item.id}
                  removeMessageById={removeMessageById}
                  regenerateMessage={
                    regenerateMessage && handleRegenerateMessage
                  }
                  sendLoading={sendLoading}
                ></UserGroupButton>
              )}

              {/* <b>{isAssistant ? '' : nickname}</b> */}
            </Space>
            <div
              className={
                isAssistant ? styles.messageText : styles.messageUserText
              }
              style={{ whiteSpace: 'pre-line' }} // 保留换行符并自动换行
            >
              <MarkdownContent
                loading={loading}
                content={item.content}
                reference={reference}
                clickDocumentButton={clickDocumentButton}
              ></MarkdownContent>
            </div>
            {isAssistant && referenceDocumentList.length > 0 && (
              <List
                bordered
                dataSource={referenceDocumentList}
                renderItem={(item) => {
                  return (
                    <List.Item>
                      <Flex gap={'small'} align="center">
                        <FileIcon
                          id={item.doc_id}
                          name={item.doc_name}
                        ></FileIcon>

                        <NewDocumentLink
                          documentId={item.doc_id}
                          documentName={item.doc_name}
                          prefix="document"
                        >
                          {item.doc_name}
                        </NewDocumentLink>
                      </Flex>
                    </List.Item>
                  );
                }}
              />
            )}
            {isUser && documentList.length > 0 && (
              <List
                bordered
                dataSource={documentList}
                renderItem={(item) => {
                  // TODO:
                  const fileThumbnail =
                    documentThumbnails[item.id] || documentThumbnails[item.id];
                  const fileExtension = getExtension(item.name);
                  return (
                    <List.Item>
                      <Flex gap={'small'} align="center">
                        <FileIcon id={item.id} name={item.name}></FileIcon>

                        {isImage(fileExtension) ? (
                          <NewDocumentLink
                            documentId={item.id}
                            documentName={item.name}
                            prefix="document"
                          >
                            {item.name}
                          </NewDocumentLink>
                        ) : (
                          <Button
                            type={'text'}
                            onClick={handleUserDocumentClick(item.id)}
                          >
                            <Text
                              style={{ maxWidth: '40vw' }}
                              ellipsis={{ tooltip: item.name }}
                            >
                              {item.name}
                            </Text>
                          </Button>
                        )}
                      </Flex>
                    </List.Item>
                  );
                }}
              />
            )}
          </Flex>
        </div>
      </section>
      {visible && (
        <IndentedTreeModal
          visible={visible}
          hideModal={hideModal}
          documentId={clickedDocumentId}
        ></IndentedTreeModal>
      )}
    </div>
  );
};

export default memo(MessageItem);

 

2. 修改src/components/message-item/index.less:

要确保文本内容(特别是多行消息)能够正确显示换行符并且样式合理,我们可以对现有的 .messageText.messageUserText 样式做一些调整。以下是针对 index.less 样式的改进:

关键改动:

  1. 保留换行符: 使用 white-space: pre-line 来保留文本中的换行符(\n),并且自动换行。
  2. 避免内容溢出: 适当设置 word-breakoverflow-wrap 属性,以确保长单词或无空格的长文本能够正确换行,避免溢出。
  3. 简化重复的 .messageText.messageUserText 样式: 让这两者有一个统一的基础样式,便于管理。
.messageItem {
  padding: 24px 0;
  .messageItemSection {
    display: inline-block;
  }
  .messageItemSectionLeft {
    width: 80%;
  }
  .messageItemSectionRight {
    // width: 80%;
    // max-width: 50vw;
  }
  .messageItemContent {
    display: inline-flex;
    gap: 20px;
    flex-wrap: wrap;  // 允许内容换行
  }
  .messageItemContentReverse {
    flex-direction: row-reverse;
  }
  .messageText {
    .chunkText();
    padding: 0 14px;
    background-color: rgba(249, 250, 251, 1);
    word-break: break-all;
  }
  /* 共同的文本样式基础 */
  .messageTextBase {
    padding: 6px 10px;
    border-radius: 8px;
    word-wrap: break-word;  // 强制长单词换行
    overflow-wrap: break-word;  // 强制长单词换行
    white-space: pre-line;  // 保留换行符并换行
  }

  /* Assistant 消息文本样式 */
  .messageText {
    .chunkText();
    .messageTextBase();
    background-color: #e6f4ff;
    word-break: break-word;  // 自动换行
  }

  /* User 消息文本样式 */
  .messageUserText {
    .chunkText();
    .messageTextBase();
    background-color: rgb(248, 247, 247);
    word-break: break-word;  // 自动换行
    text-align: justify;  // 用户消息文本两端对齐
  }
  
  .messageEmpty {
    width: 300px;
  }

  .thumbnailImg {
    max-width: 20px;
  }
}

.messageItemLeft {
  text-align: left;
}

.messageItemRight {
  text-align: right;
}

实际对话消息,显示如下:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2255841.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

电子商务人工智能指南 4/6 - 内容理解

介绍 81% 的零售业高管表示&#xff0c; AI 至少在其组织中发挥了中等至完全的作用。然而&#xff0c;78% 的受访零售业高管表示&#xff0c;很难跟上不断发展的 AI 格局。 近年来&#xff0c;电子商务团队加快了适应新客户偏好和创造卓越数字购物体验的需求。采用 AI 不再是一…

基于STM32F103RCT6的PS2手柄控制舵机转向小车

一、PS2 &#xff08;1&#xff09;当接收器上的绿灯常亮时&#xff0c;证明手柄和接收器配对成功&#xff0c;可以正常进行数据通讯。如果手柄和接收器断开了&#xff0c;按手柄上的START键即可恢复配对&#xff1b; &#xff08;2&#xff09;当手柄上的MODE指示灯没有点亮的…

电脑投屏到电脑:Windows,macOS及Linux系统可以相互投屏!

本篇其实是电脑远程投屏到另一台电脑的操作介绍。本篇文章的方法可用于Windows&#xff0c;macOS及Linux系统的相互投屏。 为了避免介绍过程中出现“这台电脑”投屏到“那台电脑”的混乱表述&#xff0c;假定当前屏幕投出端是Windows系统电脑&#xff0c;屏幕接收端是Linux系统…

软件测试环境搭建与测试流程

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 1.软件测试环境搭建 思考&#xff1a; 在什么条件下做软件测试&#xff1f;怎么做软件测试&#xff1f; 1.1 搭建测试环境前 确定测试目的 功能测试&#xff…

实战 | C# 中使用YOLOv11实现实例分割 (步骤 + 源码)

导 读 本文主要介绍在C#中使用YOLOv11实现实例分割,并给详细步骤和源码。 C# YOLO11实例分割——本文实现效果:

C#窗体程序学生管理

代码如下&#xff1a; public static string constr "Data SourceFUSHUAI;Initial Catalogproduct;Integrated SecurityTrue"; public static SqlConnection con new SqlConnection(constr); private void Form1_Load(object sender, EventArgs e) { gettable…

特朗普画像

任务内容 Description 特朗普当选了&#xff0c;网上流传着很多段子&#xff0c;也出了特朗普的头像。有人说&#xff0c;特朗普 的头像像一团云。所以今年马云去了美国和特朗普谈中美企业的发展。那么你能帮 忙打印出特朗普的头像吗&#xff1f; 抽象派认为&#xff0c;特朗普…

【Linux 篇】Docker 启动和停止的精准掌舵:操控指南

文章目录 【Linux篇】Docker 启动和停止的精准掌舵&#xff1a;操控指南前言docker基本命令1. 帮助手册 2. 指令介绍 常用命令1. 查看镜像2. 搜索镜像3. 拉取镜像4. 删除镜像5. 从Docker Hub拉取 容器的相关命令1. 查看容器2. 创建与启动容器3. 查看镜像4. 启动容器5. 查看容器…

Android环境搭建

Android环境搭建 第一步&#xff1a;安装 Homebrew 执行以下命令来安装 Homebrew&#xff1a; /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"检测是否安装成功&#xff1a; brew --version第二步&#xff1a;安装 No…

【调试工具】USB 转 UART 适配器(USB 转 TTL)

「USB 转 TTL 转换器」是错误的叫法&#xff0c;正确的叫法应该为 「USB 转 UART 适配器」。 Device connection 注意端口的交叉连接&#xff0c;Device1_TX<---->Device2_RX USB-to-UART adapter GND 记得接地。 使用&#xff1a; 当 TX,RX 需要电平为 0-3.3V 时&am…

Stable Diffusion核心网络结构——U-Net

本文详细详细介绍Stable Diffusion核心网络结构——U-Net&#xff0c;作用&#xff0c;架构&#xff0c;加噪去噪过程损失函数等。 目录 Stable Diffusion核心网络结构 SD模型整体架构初识 U-Net模型 【1】U-Net的核心作用 【2】U-Net模型的完整结构图 &#xff08;1&#xff0…

利用【AOP+自定义注解】实现项目中【日志记录】

利用AOP自定义注解实现日志记录 需求: 日志记录 操作日志记录,类似如下 思路:AOP自定义注解 AOP面向切面编程,利用 一种称为"横切"的技术&#xff0c;剖开封装的对象内部&#xff0c;并将那些影响了 多个类的公共行为抽取出封装到一个可重用模块&#xff0c;并将其…

JAVA-二叉树的概念和性质

目录 一.树形结构 1.1 概念 1.2 树的概念(重要)​编辑 补充&#xff1a;高度和深度的区别 1.3 树的应用 二. 二叉树&#xff08;重点&#xff09; 2.1 概念 2.2 两种特殊的二叉树 2.3 二叉树的性质 2.4 选择题 一.树形结构 1.1 概念 树是一种 非线性 的数据结构&…

SSM虾米音乐项目2--分页查询

1.分页查询的底层逻辑 首先根据用户输入的流派&#xff0c;进行模糊查询根据查询的数据进行分页需要前端用户提供pageNo(当前页数)和pageSize(每页的数据量)并且要从后端计算count(总数据量)和totalPage(总页数)&#xff0c;以及startNum(每页开始的记录)从而将对应的页面数据…

debian编译失败

A、缘由和分析 debian的代码在删除该路径下的2个包后&#xff0c; 重新全编&#xff0c;编译不过的问题。 至于我为什么删除这2个包&#xff0c;这是因为在sdk第一次编译时一些文件已经打包进去了&#xff0c;我现在的修改无法更新进img中&#xff0c;而现在我的项目中不需要…

Thonny IDE + MicroPython + ESP32 + A9G 发短信打电话

A9G模块的使用说明 详见该博客&#xff1a;a9gdfgdfhguyiuh-CSDN博客 接线 ESP32 DEVKIT_C A9G GND GND D23 RX A9G开发板用板载MiniUSB&#xff08;安卓口&#xff09;供电 代码 from machine import UART # 导入串口模块 # import timeUART0 UART1 UART2 TX …

在GITHUB上传本地文件指南(详细图文版)

这份笔记简述了如何在GITHUB上上传文件夹的详细策略。 既是对自己未来的一个参考&#xff0c;又希望能给各位读者带来帮助。 详细步骤 打开目标文件夹&#xff08;想要上传的文件夹&#xff09; 右击点击git bash打开 GitHub创立新的仓库后&#xff0c;点击右上方CODE绿色按…

沈阳工业大学《2024年827自动控制原理真题》 (完整版)

本文内容&#xff0c;全部选自自动化考研联盟的&#xff1a;《沈阳工业大学827自控考研资料》的真题篇。后续会持续更新更多学校&#xff0c;更多年份的真题&#xff0c;记得关注哦~ 目录 2024年真题 Part1&#xff1a;2024年完整版真题 2024年真题

初始化webpack应用示例

1、初始化npm mkdir webpack_test cd webpack_test npm init 2、安装webpack依赖 npm install webpack webpack-cli -D 3、添加文件夹&#xff0c;入口文件 mkdir src touch index.js touch add-content.js 文件夹结构 index.js import addContent from "./add-cont…

重邮+数字信号处理实验三:z变换及离散LTI系统的z域分析

实验目的&#xff1a; &#xff08; 1 &#xff09;学会运用 Matlab 求离散时间信号的有理函数 z 变换的部分分式展开&#xff1b; &#xff08; 2 &#xff09;学会运用 Matlab 分析离散时间系统的系统函数的零极点&#xff1b; &#xff08; 3 &#xff09;学会运用 …