feat(chat): add custom drag-resize handle on input top border (#725)

* feat(chat): add custom drag-resize handle on input top border

* fix(chat): skip auto-resize when user has manually set height via drag handle
This commit is contained in:
memeflyfly
2026-05-14 21:02:44 +08:00
committed by GitHub
parent d551b2d6db
commit 7420f7aad5
2 changed files with 107 additions and 10 deletions
@@ -20,6 +20,37 @@ const isDragging = ref(false)
const dragCounter = ref(0)
const isComposing = ref(false)
// 自定义高度拖拽
const textareaHeight = ref<number | null>(null) // null = auto
function startResize(e: MouseEvent) {
e.preventDefault()
const el = textareaRef.value
if (!el) return
// 如果当前是 auto,用实际 clientHeight 作为起始值
const startHeight = el.clientHeight
const startY = e.clientY
function onMouseMove(e: MouseEvent) {
const deltaY = e.clientY - startY
// 往上拖 (deltaY < 0) → 高度增加
const newHeight = startHeight - deltaY
textareaHeight.value = Math.max(20, Math.min(400, Math.round(newHeight)))
}
function onMouseUp() {
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
document.body.style.cursor = ''
document.body.style.userSelect = ''
}
document.body.style.cursor = 'row-resize'
document.body.style.userSelect = 'none'
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
}
// 自动播放语音开关
const autoPlaySpeech = ref(false)
@@ -229,6 +260,8 @@ function handleKeydown(e: KeyboardEvent) {
}
function handleInput(e: Event) {
// 用户手动拖拽自定义高度时,不覆盖
if (textareaHeight.value !== null) return
const el = e.target as HTMLTextAreaElement
el.style.height = 'auto'
el.style.height = Math.min(el.scrollHeight, 100) + 'px'
@@ -349,10 +382,12 @@ function isImage(type: string): boolean {
class="file-input-hidden"
@change="handleFileChange"
/>
<div class="resize-handle" @mousedown="startResize"></div>
<textarea
ref="textareaRef"
v-model="inputText"
class="input-textarea"
:style="textareaHeight ? { height: textareaHeight + 'px' } : {}"
:placeholder="t('chat.inputPlaceholder')"
rows="1"
@keydown="handleKeydown"
@@ -594,6 +629,7 @@ function isImage(type: string): boolean {
border: 1px solid $border-color;
border-radius: $radius-md;
padding: 10px 12px;
position: relative;
transition: border-color $transition-fast, background-color $transition-fast;
&:focus-within {
@@ -605,6 +641,21 @@ function isImage(type: string): boolean {
}
}
.resize-handle {
position: absolute;
top: -4px;
left: 0;
right: 0;
height: 8px;
cursor: row-resize;
z-index: 2;
&:hover {
background: rgba($accent-primary, 0.15);
border-radius: 4px;
}
}
.input-textarea {
flex: 1;
background: none;
@@ -615,7 +666,7 @@ function isImage(type: string): boolean {
font-size: 14px;
line-height: 1.5;
resize: none;
max-height: 100px;
max-height: 400px;
min-height: 20px;
overflow-y: auto;
@@ -13,6 +13,35 @@ const textareaRef = ref<HTMLTextAreaElement>()
const dropdownRef = ref<HTMLDivElement>()
const isComposing = ref(false)
// 自定义高度拖拽
const textareaHeight = ref<number | null>(null)
function startResize(e: MouseEvent) {
e.preventDefault()
const el = textareaRef.value
if (!el) return
const startHeight = el.clientHeight
const startY = e.clientY
function onMouseMove(e: MouseEvent) {
const deltaY = e.clientY - startY
const newHeight = startHeight - deltaY
textareaHeight.value = Math.max(20, Math.min(400, Math.round(newHeight)))
}
function onMouseUp() {
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
document.body.style.cursor = ''
document.body.style.userSelect = ''
}
document.body.style.cursor = 'row-resize'
document.body.style.userSelect = 'none'
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
}
// ─── Mention State ───────────────────────────────────────
const mentionActive = ref(false)
@@ -125,8 +154,10 @@ function selectMention(name: string) {
const newPos = before.length + name.length + 2
el.setSelectionRange(newPos, newPos)
el.focus()
el.style.height = 'auto'
el.style.height = Math.min(el.scrollHeight, 100) + 'px'
if (textareaHeight.value === null) {
el.style.height = 'auto'
el.style.height = Math.min(el.scrollHeight, 100) + 'px'
}
}
})
}
@@ -173,15 +204,12 @@ function handleSend() {
emit('send', content)
inputText.value = ''
mentionActive.value = false
nextTick(() => {
if (textareaRef.value) {
textareaRef.value.style.height = 'auto'
}
})
// 发送后重置到自定义高度(不清除拖拽状态)
}
function handleInput(e: Event) {
// 用户手动拖拽自定义高度时,不覆盖
if (textareaHeight.value !== null) return
store.emitTyping()
const el = e.target as HTMLTextAreaElement
el.style.height = 'auto'
@@ -233,10 +261,12 @@ function handleCompositionEnd() {
<template>
<div class="chat-input-area">
<div class="input-wrapper">
<div class="resize-handle" @mousedown="startResize"></div>
<textarea
ref="textareaRef"
v-model="inputText"
class="input-textarea"
:style="textareaHeight ? { height: textareaHeight + 'px' } : {}"
:placeholder="t('groupChat.inputPlaceholder')"
rows="1"
@keydown="handleKeydown"
@@ -326,6 +356,7 @@ function handleCompositionEnd() {
border: 1px solid $border-color;
border-radius: $radius-md;
padding: 10px 12px;
position: relative;
transition: border-color $transition-fast, background-color $transition-fast;
&:focus-within {
@@ -337,6 +368,21 @@ function handleCompositionEnd() {
}
}
.resize-handle {
position: absolute;
top: -4px;
left: 0;
right: 0;
height: 8px;
cursor: row-resize;
z-index: 2;
&:hover {
background: rgba($accent-primary, 0.15);
border-radius: 4px;
}
}
.input-textarea {
flex: 1;
background: none;
@@ -347,7 +393,7 @@ function handleCompositionEnd() {
font-size: 14px;
line-height: 1.5;
resize: none;
max-height: 100px;
max-height: 400px;
min-height: 20px;
overflow-y: auto;