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