首页 前端知识 ChatGPT输入框以及文件上传框

ChatGPT输入框以及文件上传框

2024-11-05 23:11:11 前端知识 前端哥 180 702 我要收藏

封装的仿制chatgpt的文件的输入框,支持文件上传显示,多行文本自动宽高,上传部分需要有AntUI的一些组件或者自己更换为其他UI组件也行,本组件只适合有 tailwindcss ,Ts ,AntUi 的Vue3项目

组件传值:

传值:

绑定的value值
    modelValue: string;
  height
    height?: number;
width值
    width?: number;
  输入框最高高度
    maxHeight?: number;
placeholder提示
    placeholder?: string;

组件  inputBorder

    inputBorder?: string

  输入框的圆角值
    radius?: number;
  icon 在多行情况下的位置
    iconAlign?: "center" | "flex-end" | "flex-start"
 placeholder 文本是否居中
    placeholderCenter?: boolean;
  回车回调函数
    onEnter?: (params?: onEnterParams) => Promise<any> | any;
输入回调函数
    onInput?: (text?: string) => void;
是否开启多模态上传功能 :multifunctionIcon 与multifunctionCallbackSet只有开启多模态功能才会生效
    multifunction?: boolean;
    多模态功能图标size与tile,详细查看代码里type
    multifunctionIcon?: multifunctionIconParams;
  多模态CallbackSet 回调
    multifunctionCallbackSet?: multifunctioncaCallback[]
    上传完成的列表
    onUploadProgressList?: uploadFileListParams[]
  取消某一个上传完成的回调
    onUploadProgressListClose?: (item: uploadFileListParams, index: number) => void

组件代码

<template>
  <div class="flex w-full items-center">
    <a-popover v-if="multifunction" :title="multifunctionIcon.title" trigger="focus">
      <template #content>
        <p v-for="(item,index) in multifunctionCallbackSet" :key="index" @click="item.cb" class="cursor-pointer">
          {{ item.label }}</p>

      </template>
      <PaperClipOutlined :style="{color:multifunctionIcon.color,fontSize:multifunctionIcon.size +'px'}"
                         class=" cursor-pointer mr-2"/>
    </a-popover>


    <div class="inline-block relative  input"
         :style="{ borderRadius: `${props.radius}px`,border:inputBorder}">
      <div v-if="onUploadProgressList?.length"
           class="min-h-[70px] w-full grid  grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 justify-items-center  gap-4 p-4 "
           :style="{borderBottom:inputBorder}">

        <div v-for="(item,index) in onUploadProgressList" :key="index"
             class="w-full h-[70px] px-5 relative   flex  bg-[#f2f2f2]  items-center rounded-[8px]">
          <div class="relative  w-[30px] h-[30px]">
            <FileOutlined class="text-[20px] progress"/>
            <a-progress :key="index" v-if="item.schedule<99" :stroke-color="'#4CADAB'" :format="() => ''"
                        class="progress" :size="30" type="circle"
                        :percent="item.schedule"/>
          </div>
          <div>
            <div class=" overflow-hidden text-[1rem]  w-[70%] ml-6  text-ellipsis  text-left">
              {{ item.name }}
            </div>
          </div>
          <CloseCircleOutlined @click="onUploadProgressListClose?.(item,index)"
                               class="absolute right-0 top-0 cursor-pointer"/>

        </div>


      </div>
      <div class="input-container"

           :style="{ borderRadius: `${props.radius}px` ,alignItems:iconEndOrTop() }">
        <div class="icon">
          <slot name="prefix"></slot>
        </div>

        <textarea
            ref="textarea"
            v-model="inputValue"
            :class="placeholderCenter && 'atextarea'"
            :style="textareaStyle"
            @input="handleInput"
            @compositionstart="handleCompositionStart"
            @compositionend="handleCompositionEnd"
            @keydown.enter.prevent="onSpaceDown"
            @keyup.enter.prevent="onEnter"
            :placeholder="placeholder"
            rows="1"
        ></textarea>

        <div class="icon_suffix">
          <slot class="ml-4" name="suffix"></slot>
        </div>
      </div>
    </div>

  </div>
</template>


<script lang="ts" setup>
/**
 * TODO
 * @author  Mr.fengjun
 * @date    2024/7/9
 * @time    17:09
 * @version 1.0
 * @mali 3278440884@qq.com
 * @phone 17301529005
 */

import {computed, nextTick, onMounted, ref, watch} from 'vue';
import {withDefaults, defineProps, defineEmits} from 'vue';
import {CustomTextareaProps} from './type/index.ts';
import {PaperClipOutlined, FileOutlined, CloseCircleOutlined} from "@ant-design/icons-vue";

const props = withDefaults(defineProps<CustomTextareaProps>(), {
  modelValue: '',
  height: 40,
  maxHeight: 200,
  placeholder: 'Enter text here...',
  radius: 8,
  placeholderCenter: false,
  iconAlign: "center",
  multifunction: false,
  inputBorder: "1px solid #000",
  multifunctionIcon: {size: 30, color: "#000", title: "文件上传"},
});

const emit = defineEmits(['update:modelValue']);

const inputValue = ref(props.modelValue);
const textarea = ref<HTMLTextAreaElement | null>(null);
const textareaHeight = ref(props.height);

const compositionStartFlag = ref(false)

const handleCompositionStart = () => {
  compositionStartFlag.value = true;
};


const handleCompositionEnd = () => {
  // 这里是要做一个延时,监听输入法输入完成 两次回车,为了防止第二次回车直接发送所以等待100毫秒
  setTimeout(() => {
    compositionStartFlag.value = false;
  }, 100)
};
// 字符串清空方法
const clearContent = () => {
  inputValue.value = '';
  nextTick(() => {
    adjustHeight()
  })
};

//判断当前是一行的话,保持当前高度,当前是多行的话拉升当前高度,高度不能超过maxHeight,textarea需要设置为1行多行的话auto后的默认值是66px
const adjustHeight = () => {
  const el = textarea.value;
  if (el) {
    el.style.height = 'auto';
    requestAnimationFrame(() => {
      const newHeight = el.scrollHeight;
      textareaHeight.value = newHeight < props.height ? props.height : (newHeight < props.maxHeight ? newHeight : props.maxHeight);
      el.style.height = `${textareaHeight.value}px`;
    });
  }
};

const handleInput = () => {
  adjustHeight();

  if (props.onInput) {
    props.onInput(inputValue.value);
  }
};

const textareaStyle = computed(() => ({
  height: `${textareaHeight.value}px`,
  width: props.width ? `${props.width}px` : '100%',
}));

const onSpaceDown = (event: Event) => {
  event.preventDefault();
  event.stopPropagation();
}

// 监听enter事件
const onEnter = (event: Event) => {
  event.preventDefault();
  event.stopPropagation();

  if (compositionStartFlag.value) return;

  if (props.onEnter) {
    props.onEnter({e: event, text: inputValue.value, clearContent});
  }

};

onMounted(() => {
  adjustHeight();
});


const iconEndOrTop = () => {
  // 如果位置不是center则需要做优化只有在内容大于一行时再做位置变化
  if (props.iconAlign === "center") {
    return 'center'
  }
  return inputValue.value !== '' && textareaHeight.value - 2 > props.height ? props.iconAlign : 'center'

}
const inputSetValue = (val: string) => {
  inputValue.value = val
}


watch(inputValue, (newValue) => {
  emit('update:modelValue', newValue);
});


defineExpose({
  inputValue,
  clearContent,
  inputSetValue
})


</script>

<style scoped lang="scss">
@import "../../styles/variable.scss";

.input-container {
  display: flex;
  line-height: 2;
  height: fit-content;
  padding: 2px;
  background: #ffffff;
}

.icon {
  width: fit-content;
  position: relative;
  left: 10px;
  z-index: 999;
}

.icon_suffix {
  width: 20px;
  position: relative;
  right: 10px;
  z-index: 999;
  cursor: pointer;
  height: 100%;
  bottom: 0;
}

.icon i {
  font-size: 16px;
  color: #aaa;
}

.atextarea::placeholder {
  text-align: center; /* 水平居中 */
  color: #999; /* placeholder 颜色 */
  /* 其他样式,如 font-size 等 */
}

textarea {
  width: 100%;
  box-sizing: border-box;
  border: none;
  resize: none;
  padding: 12px 10px;
  display: flex;
  align-items: center;
  outline: none;
}

textarea:focus {
  border: none; /* 更改为你想要的颜色 */
  outline: none;
}

.input {
  width: 100%;
}

.progress {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
</style>

Ts类型

export interface CustomTextareaProps {
    // 绑定的value值
    modelValue: string;
    // height值
    height?: number;
    //width值
    width?: number;
    // 输入框最高高度
    maxHeight?: number;
    // placeholder提示
    placeholder?: string;

    inputBorder?: string

    // 输入框的圆角值
    radius?: number;
    // icon 在多行情况下的位置
    iconAlign?: "center" | "flex-end" | "flex-start"
    // placeholder 文本是否居中
    placeholderCenter?: boolean;
    // 回车回调函数
    onEnter?: (params?: onEnterParams) => Promise<any> | any;
    // 输入回调函数
    onInput?: (text?: string) => void;
    // 是否开启多模态功能 multifunctionIcon 与multifunctionCallbackSet只有开启多模态功能才会生效
    multifunction?: boolean;
    // 多模态功能图标size与tile
    multifunctionIcon?: multifunctionIconParams;
    // 多模态CallbackSet 回调
    multifunctionCallbackSet?: multifunctioncaCallback[]
    // 上传完成的列表
    onUploadProgressList?: uploadFileListParams[]
    // 取消某一个上传完成的回调
    onUploadProgressListClose?: (item: uploadFileListParams, index: number) => void

}

export interface onEnterParams {
    e: Event;
    text: string;
    clearContent: () => void
}

export interface multifunctionIconParams {
    size: number,
    color: string,
    title: string
}

export interface multifunctioncaCallback {
    label: string,
    cb: (data: any) => any
}

export interface uploadFileListParams {
    name: string,
    type: string,
    // 进度条
    schedule: any
    taskId: number | null

}

使用示例

<script lang="ts" setup>
     <AutoResizeInput
              ref="AutoInput"
              v-model="messageText"
              :height="50"
              :icon-align="'flex-end'"
              :input-border="'1px solid #4CADAB'"
              :multifunction="true"
              :multifunction-callback-set="uploadFnCbSet"
              :multifunction-icon="{size:30,color:'#4CADABFF',title:'文件上传'}"
              :on-enter="inputEnter as any"
              :on-upload-progress-list="onUploadProgressList"
              :on-upload-progress-list-close="progressListClose"
              class="w-full"
              placeholder="输入你想问的问题吧">
            <template #suffix>

              <img v-if="!modelRecovery" alt="" class="w-[20px] h-[20px] cursor-pointer "
                   src="../../assets/images/icon/send.png" @click="send">


              <img v-if="modelRecovery" alt="" class="w-[20px] h-[20px] object-cover cursor-pointer "
                   src="../../assets/images/icon/stop.png"
                   @click="sendStop">

            </template>

          </AutoResizeInput>

const messageText = ref()
// 多模态功能回调
const uploadFnCbSet: multifunctioncaCallback[] = [
  {
    label: "上传视频",
    cb: videoUpdate
  },
  {
    label: "上传文档",
    cb: documentUpdate
  }
]

const inputEnter = async (): Promise<any> => {

  // 当前模型如果正在输出,那么不能再使用input enter
  if (!modelRecovery.value) {
    await sendChat()
  }

}

export interface uploadFileListParams {
    // 文件名称
    name: string,
    // 文件类型
    type: string,
    // 进度条
    schedule: any
    // 文件上传任务id
    taskId: number | null

}

const onUploadProgressList = ref<uploadFileListParams[]>([]);
const progressListClose = (item: uploadFileListParams, index: number) => {
  onUploadProgressList.value.splice(index, 1)
}
</script>

转载请注明出处或者链接地址:https://www.qianduange.cn//article/19991.html
标签
chatgpt
评论
发布的文章
大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!