封装的仿制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>
复制