294 lines
5.4 KiB
Vue
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>
|