feat: 输入框允许粘贴文件

master
LCJ-MinYa 7 months ago
parent f1fb378d1e
commit afdb1de5ff

@ -0,0 +1,344 @@
<template>
<div class="rich-input-container">
<!-- 内容显示区域 -->
<div
ref="editableDiv"
class="rich-input-editable"
contenteditable="true"
@input="handleInput"
@paste="handlePaste"
@dragover.prevent="handleDragOver"
@drop.prevent="handleDrop"
@click="focusEditableArea"
:placeholder="placeholder"
></div>
<!-- 文件/图片预览区域 -->
<div
v-if="attachments.length > 0"
class="preview-area mt-2"
>
<div
v-for="(item, index) in attachments"
:key="item.id"
class="preview-item"
>
<!-- 图片预览 -->
<el-image
v-if="item.type.startsWith('image/')"
:src="item.url"
fit="cover"
class="preview-image"
:preview-src-list="[item.url]"
>
<template #error>
<div class="image-error">图片加载失败</div>
</template>
</el-image>
<!-- 文件预览 -->
<div
v-else
class="file-preview"
>
<el-icon
:size="40"
class="file-icon"
>
<Document />
</el-icon>
<div class="file-name">{{ item.name }}</div>
</div>
<!-- 删除按钮 -->
<el-icon
class="remove-icon"
@click="removeAttachment(index)"
>
<Close />
</el-icon>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons mt-2">
<el-button
type="primary"
size="small"
@click="handleSubmit"
>提交</el-button
>
<el-button
size="small"
@click="clearAll"
>清空</el-button
>
<el-upload
v-model:file-list="fileList"
action="#"
:auto-upload="false"
:show-file-list="false"
:multiple="true"
:on-change="handleFileChange"
class="ml-2"
>
<template #trigger>
<el-button size="small">选择文件</el-button>
</template>
</el-upload>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue';
import { Document, Close } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
const props = defineProps({
placeholder: {
type: String,
default: '请输入内容...',
},
modelValue: {
type: String,
default: '',
},
});
const emit = defineEmits(['update:modelValue', 'submit']);
const editableDiv = ref(null);
const attachments = ref([]);
const fileList = ref([]);
// ID
const generateId = () => {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
};
//
const handleInput = () => {
emit('update:modelValue', editableDiv.value.innerHTML);
};
const handlePaste = async (e) => {
const items = e.clipboardData?.items;
if (!items) return;
let hasFile = false;
//
e.preventDefault();
//
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.kind === 'string' && item.type === 'text/plain') {
item.getAsString((text) => {
document.execCommand('insertText', false, text);
});
continue;
}
}
//
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.kind === 'file') {
hasFile = true;
const file = item.getAsFile();
await processFile(file);
}
}
};
//
const handleDragOver = (e) => {
e.dataTransfer.dropEffect = 'copy';
};
//
const handleDrop = async (e) => {
const files = e.dataTransfer.files;
if (files.length > 0) {
for (let i = 0; i < files.length; i++) {
await processFile(files[i]);
}
focusEditableArea();
}
};
//
const handleFileChange = (file) => {
processFile(file.raw);
};
//
const processFile = (file) => {
return new Promise((resolve) => {
const fileType = file.type;
const fileId = generateId();
if (fileType.startsWith('image/')) {
//
const reader = new FileReader();
reader.onload = (e) => {
attachments.value.push({
id: fileId,
type: fileType,
name: file.name,
file: file,
url: e.target.result,
});
insertPlaceholder(fileId);
resolve();
};
reader.readAsDataURL(file);
} else {
//
attachments.value.push({
id: fileId,
type: fileType,
name: file.name,
file: file,
url: null,
});
insertPlaceholder(fileId);
resolve();
}
});
};
//
const insertPlaceholder = (fileId) => {
const placeholder = `[file:${fileId}]`;
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(document.createTextNode(placeholder));
//
const newRange = document.createRange();
newRange.setStartAfter(range.endContainer);
newRange.collapse(true);
selection.removeAllRanges();
selection.addRange(newRange);
}
emit('update:modelValue', editableDiv.value.innerHTML);
};
//
const removeAttachment = (index) => {
const removed = attachments.value.splice(index, 1)[0];
if (removed) {
//
const content = editableDiv.value.innerHTML;
const updatedContent = content.replace(`[file:${removed.id}]`, '');
editableDiv.value.innerHTML = updatedContent;
emit('update:modelValue', updatedContent);
}
};
//
const focusEditableArea = () => {
if (editableDiv.value) {
editableDiv.value.focus();
}
};
//
const handleSubmit = () => {
if (!editableDiv.value.textContent.trim() && attachments.value.length === 0) {
ElMessage.warning('请输入内容或添加文件');
return;
}
const content = {
text: editableDiv.value.innerHTML,
attachments: [...attachments.value],
};
emit('submit', content);
};
//
const clearAll = () => {
editableDiv.value.innerHTML = '';
attachments.value = [];
emit('update:modelValue', '');
};
//
onMounted(() => {
if (props.modelValue) {
editableDiv.value.innerHTML = props.modelValue;
}
});
</script>
<style lang="scss" scoped>
.rich-input-container {
@apply border border-gray-300 rounded p-3 bg-white;
.rich-input-editable {
@apply min-h-[100px] p-2 outline-none;
&:empty::before {
content: attr(placeholder);
@apply text-gray-400;
}
&:focus {
@apply ring-1 ring-blue-500;
}
}
.preview-area {
@apply flex flex-wrap gap-2;
.preview-item {
@apply relative border border-gray-200 rounded p-1 w-[100px] h-[100px] flex items-center justify-center;
.preview-image {
@apply w-full h-full object-cover;
}
.file-preview {
@apply flex flex-col items-center;
.file-icon {
@apply text-gray-400;
}
.file-name {
@apply text-xs text-center text-gray-600 truncate w-full px-1;
}
}
.remove-icon {
@apply absolute top-0 right-0 bg-white rounded-full p-1 cursor-pointer opacity-0 transition-opacity;
&:hover {
@apply text-red-500;
}
}
&:hover .remove-icon {
@apply opacity-100;
}
}
}
.action-buttons {
@apply flex items-center;
}
.image-error {
@apply w-full h-full flex items-center justify-center text-xs text-gray-500 bg-gray-100;
}
}
</style>
Loading…
Cancel
Save