Files
xk-admin/apps/web-antd/src/views/business/chat/components/CustomTextarea.vue
2025-12-23 11:05:12 +08:00

294 lines
5.4 KiB
Vue

<template>
<div
class="custom-textarea-container bg-white dark:bg-gray-700 border-2 border-gray-200 dark:border-gray-600"
:class="{ 'focused': isFocused, 'disabled': disabled }"
@drop="handleDrop"
@dragover="handleDragOver"
@dragenter="handleDragEnter"
@dragleave="handleDragLeave"
>
<textarea
ref="textareaRef"
:value="modelValue"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:maxlength="maxlength"
:rows="rows"
class="custom-textarea"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@keydown="handleKeydown"
@paste="handlePaste"
></textarea>
<div class="textarea-actions" v-if="clearable || $slots.actions">
<button
v-if="clearable && modelValue && !disabled"
class="clear-btn"
@click="handleClear"
type="button"
>
<i class="fas fa-times"></i>
</button>
<slot name="actions"></slot>
</div>
</div>
</template>
<script setup>
import {ref, nextTick, watch} from 'vue';
const props = defineProps({
modelValue: {
type: String,
default: ''
},
placeholder: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
},
readonly: {
type: Boolean,
default: false
},
clearable: {
type: Boolean,
default: false
},
maxlength: {
type: Number,
default: undefined
},
rows: {
type: Number,
default: 3
},
autoResize: {
type: Boolean,
default: false
},
maxRows: {
type: Number,
default: 6
},
detectPaste: {
type: Boolean,
default: false
}
});
const emit = defineEmits(['update:modelValue', 'focus', 'blur', 'clear', 'keydown', 'paste-file']);
const textareaRef = ref(null);
const isFocused = ref(false);
const handleInput = (event) => {
emit('update:modelValue', event.target.value);
if (props.autoResize) {
autoResize();
}
};
const handleFocus = (event) => {
isFocused.value = true;
emit('focus', event);
};
const handleBlur = (event) => {
isFocused.value = false;
emit('blur', event);
};
const handleClear = () => {
emit('update:modelValue', '');
emit('clear');
textareaRef.value?.focus();
if (props.autoResize) {
autoResize();
}
};
const handleKeydown = (event) => {
emit('keydown', event);
};
const handlePaste = (event) => {
if (!props.detectPaste) return;
const items = event.clipboardData?.items;
if (!items) return;
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.kind === 'file') {
event.preventDefault();
const file = item.getAsFile();
if (file) {
emit('paste-file', file);
}
break;
}
}
};
const handleDrop = (event) => {
event.preventDefault();
const files = event.dataTransfer?.files;
if (files && files.length > 0) {
emit('paste-file', files[0]);
}
};
const handleDragOver = (event) => {
event.preventDefault();
};
const handleDragEnter = (event) => {
event.preventDefault();
};
const handleDragLeave = (event) => {
event.preventDefault();
};
const autoResize = () => {
nextTick(() => {
if (!textareaRef.value) return;
const textarea = textareaRef.value;
textarea.style.height = 'auto';
const lineHeight = parseInt(getComputedStyle(textarea).lineHeight);
const maxHeight = lineHeight * props.maxRows;
const scrollHeight = textarea.scrollHeight;
textarea.style.height = Math.min(scrollHeight, maxHeight) + 'px';
});
};
const focus = () => {
textareaRef.value?.focus();
};
const blur = () => {
textareaRef.value?.blur();
};
// 监听内容变化自动调整高度
watch(() => props.modelValue, () => {
if (props.autoResize) {
autoResize();
}
});
defineExpose({
focus,
blur
});
</script>
<style scoped>
.custom-textarea-container {
position: relative;
border-radius: 12px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
.custom-textarea-container:hover {
border-color: #cbd5e1;
}
.dark .custom-textarea-container:hover {
border-color: #6b7280;
}
.custom-textarea-container.focused {
border-color: #4361ee;
box-shadow: 0 0 0 4px rgba(67, 97, 238, 0.1);
}
.dark .custom-textarea-container.focused {
box-shadow: 0 0 0 4px rgba(67, 97, 238, 0.2);
}
.custom-textarea-container.disabled {
background: #f8fafc;
border-color: #e2e8f0;
cursor: not-allowed;
}
.dark .custom-textarea-container.disabled {
background: #1f2937;
border-color: #374151;
}
.custom-textarea {
width: 100%;
border: none;
outline: none;
background: transparent;
padding: 12px 16px;
font-size: 14px;
line-height: 1.5;
resize: none;
font-family: inherit;
}
.custom-textarea::placeholder {
color: #94a3b8;
}
.dark .custom-textarea::placeholder {
color: #6b7280;
}
.custom-textarea:disabled {
cursor: not-allowed;
color: #94a3b8;
}
.dark .custom-textarea:disabled {
color: #6b7280;
}
.textarea-actions {
position: absolute;
top: 12px;
right: 12px;
display: flex;
gap: 8px;
}
.clear-btn {
background: none;
border: none;
color: #94a3b8;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.clear-btn:hover {
color: #64748b;
background: #f1f5f9;
}
.dark .clear-btn:hover {
color: #d1d5db;
background: #4b5563;
}
</style>