481 lines
10 KiB
Vue
481 lines
10 KiB
Vue
<template>
|
|
<div class="quill-editor-container">
|
|
<div ref="editorContainer" class="editor-content"></div>
|
|
|
|
<!-- 文件列表区域 -->
|
|
<div class="file-list-section" v-if="uploadedFiles.length > 0">
|
|
<div class="file-list">
|
|
<div v-for="(file, index) in uploadedFiles" :key="index" class="file-item">
|
|
<div class="file-icon">
|
|
<img src="/images/profile/file.png" alt="文件图标" />
|
|
</div>
|
|
<div class="file-info">
|
|
<div class="file-name">{{ file.name }}</div>
|
|
<div class="file-size">{{ file.size }}</div>
|
|
</div>
|
|
<button class="file-delete" @click="removeFile(index)" title="删除文件">
|
|
<img src="/images/profile/del-black.png" alt="删除" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="showColorPicker" class="color-picker-popup" @click.stop>
|
|
<div class="color-grid">
|
|
<div v-for="color in colors" :key="color" class="color-item" :style="{ backgroundColor: color }" @click="selectColor(color)"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, watch, nextTick } from 'vue'
|
|
import Quill from 'quill'
|
|
import 'quill/dist/quill.snow.css'
|
|
|
|
interface Props {
|
|
modelValue?: string
|
|
placeholder?: string
|
|
height?: string
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
modelValue: '',
|
|
placeholder: '请输入内容...',
|
|
height: '500px'
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: string]
|
|
'change': [value: string]
|
|
}>()
|
|
|
|
const editorContainer = ref<HTMLElement>()
|
|
const quill = ref<Quill>()
|
|
// const fontSize = ref(14)
|
|
const isBold = ref(false)
|
|
const isList = ref(false)
|
|
const showColorPicker = ref(false)
|
|
const uploadedFiles = ref<Array<{name: string, size: string}>>([])
|
|
|
|
const colors = [
|
|
'#000000', '#FF0000', '#00FF00', '#0000FF', '#FFFF00',
|
|
'#FF00FF', '#00FFFF', '#FFA500', '#800080', '#008000',
|
|
'#FFC0CB', '#A52A2A', '#808080', '#C0C0C0', '#FFFFFF'
|
|
]
|
|
|
|
const initQuill = () => {
|
|
if (!editorContainer.value) return
|
|
|
|
const toolbarOptions = [
|
|
['bold', 'underline', 'italic'],
|
|
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
|
[{ 'color': [] }, { 'background': [] }],
|
|
[{ 'size': ['12', '14', '16', '18'] }],
|
|
['image'],
|
|
['clean']
|
|
]
|
|
|
|
quill.value = new Quill(editorContainer.value, {
|
|
theme: 'snow',
|
|
modules: {
|
|
toolbar: {
|
|
container: toolbarOptions,
|
|
handlers: {
|
|
image: () => insertImage()
|
|
}
|
|
}
|
|
},
|
|
placeholder: props.placeholder
|
|
})
|
|
|
|
// 添加自定义文件上传按钮
|
|
addCustomFileButton()
|
|
|
|
if (props.modelValue) {
|
|
quill.value.root.innerHTML = props.modelValue
|
|
}
|
|
|
|
quill.value.on('text-change', () => {
|
|
const content = quill.value?.root.innerHTML || ''
|
|
emit('update:modelValue', content)
|
|
emit('change', content)
|
|
})
|
|
|
|
quill.value.on('selection-change', (range) => {
|
|
if (range) {
|
|
const formats = quill.value?.getFormat(range)
|
|
isBold.value = !!formats?.bold
|
|
isList.value = !!formats?.list
|
|
}
|
|
})
|
|
}
|
|
|
|
// const changeFontSize = () => {
|
|
// if (quill.value) {
|
|
// quill.value.format('size', fontSize.value)
|
|
// }
|
|
// }
|
|
|
|
// const toggleBold = () => {
|
|
// if (quill.value) {
|
|
// quill.value.format('bold', !isBold.value)
|
|
// }
|
|
// }
|
|
|
|
// const toggleFontStyle = () => {
|
|
// if (quill.value) {
|
|
// quill.value.format('italic', !quill.value.getFormat().italic)
|
|
// }
|
|
// }
|
|
|
|
// const toggleColorPicker = () => {
|
|
// showColorPicker.value = !showColorPicker.value
|
|
// }
|
|
|
|
const selectColor = (color: string) => {
|
|
if (quill.value) {
|
|
quill.value.format('color', color)
|
|
}
|
|
showColorPicker.value = false
|
|
}
|
|
|
|
// const toggleList = () => {
|
|
// if (quill.value) {
|
|
// const format = quill.value.getFormat()
|
|
// if (format.list) {
|
|
// quill.value.format('list', false)
|
|
// } else {
|
|
// quill.value.format('list', 'bullet')
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
const insertImage = () => {
|
|
const input = document.createElement('input')
|
|
input.setAttribute('type', 'file')
|
|
input.setAttribute('accept', 'image/*')
|
|
input.click()
|
|
|
|
input.onchange = () => {
|
|
const file = input.files?.[0]
|
|
if (file && quill.value) {
|
|
const reader = new FileReader()
|
|
reader.onload = (e) => {
|
|
const range = quill.value?.getSelection()
|
|
if (range) {
|
|
quill.value?.insertEmbed(range.index, 'image', e.target?.result)
|
|
}
|
|
}
|
|
reader.readAsDataURL(file)
|
|
}
|
|
}
|
|
}
|
|
|
|
const insertFile = () => {
|
|
const input = document.createElement('input')
|
|
input.setAttribute('type', 'file')
|
|
input.setAttribute('accept', '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt')
|
|
input.click()
|
|
|
|
input.onchange = () => {
|
|
const file = input.files?.[0]
|
|
if (file) {
|
|
const fileSize = (file.size / 1024).toFixed(2) + 'KB'
|
|
uploadedFiles.value.push({
|
|
name: file.name,
|
|
size: fileSize
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
const removeFile = (index: number) => {
|
|
uploadedFiles.value.splice(index, 1)
|
|
}
|
|
|
|
const addCustomFileButton = () => {
|
|
if (!quill.value) return
|
|
|
|
// 获取工具栏
|
|
const toolbar = quill.value.getModule('toolbar') as any
|
|
const toolbarElement = toolbar.container
|
|
|
|
// 查找最后一个格式组
|
|
const lastFormatGroup = toolbarElement.querySelector('.ql-formats:last-child')
|
|
|
|
if (lastFormatGroup) {
|
|
// 创建文件按钮
|
|
const fileButton = document.createElement('button')
|
|
fileButton.type = 'button'
|
|
fileButton.className = 'ql-file'
|
|
fileButton.setAttribute('aria-label', 'file')
|
|
fileButton.setAttribute('title', '上传文件')
|
|
fileButton.innerHTML = `
|
|
<svg viewBox="0 0 18 18">
|
|
<path class="ql-stroke" d="M6,4V2A1,1,0,0,1,7,1H11A1,1,0,0,1,12,2V4"></path>
|
|
<path class="ql-stroke" d="M6,4H4A1,1,0,0,0,3,5V15A1,1,0,0,0,4,16H14A1,1,0,0,0,15,15V5A1,1,0,0,0,14,4H12"></path>
|
|
<line class="ql-stroke" x1="6" x2="12" y1="8" y2="8"></line>
|
|
<line class="ql-stroke" x1="6" x2="12" y1="11" y2="11"></line>
|
|
<line class="ql-stroke" x1="6" x2="10" y1="14" y2="14"></line>
|
|
</svg>
|
|
`
|
|
|
|
// 添加点击事件
|
|
fileButton.addEventListener('click', insertFile)
|
|
|
|
// 将按钮添加到工具栏
|
|
lastFormatGroup.appendChild(fileButton)
|
|
}
|
|
}
|
|
|
|
watch(() => props.modelValue, (newValue: string) => {
|
|
if (quill.value && newValue !== quill.value.root.innerHTML) {
|
|
quill.value.root.innerHTML = newValue
|
|
}
|
|
})
|
|
|
|
onMounted(() => {
|
|
nextTick(() => {
|
|
initQuill()
|
|
})
|
|
})
|
|
|
|
defineExpose({
|
|
getContent: () => quill.value?.root.innerHTML || '',
|
|
setContent: (content: string) => {
|
|
if (quill.value) {
|
|
quill.value.root.innerHTML = content
|
|
}
|
|
},
|
|
focus: () => quill.value?.focus()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.quill-editor-container {
|
|
border: 1px solid #F7F7F7;
|
|
overflow: hidden;
|
|
width: 100%;
|
|
}
|
|
|
|
.editor-toolbar {
|
|
background: #f8f9fa;
|
|
border-bottom: 1px solid #F7F7F7;
|
|
padding: 8px 16px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.toolbar-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.font-size-select {
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
font-size: 12px;
|
|
background: white;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.toolbar-btn {
|
|
background: #f8f9fa;
|
|
border-radius: 4px;
|
|
padding: 6px 10px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
min-width: 32px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.toolbar-btn:hover {
|
|
background: #e9ecef;
|
|
border-color: #adb5bd;
|
|
}
|
|
|
|
.toolbar-btn.active {
|
|
background: #007bff;
|
|
color: white;
|
|
border-color: #007bff;
|
|
}
|
|
|
|
.toolbar-btn.bold {
|
|
font-weight: bold;
|
|
}
|
|
|
|
.toolbar-btn.font-style {
|
|
font-style: italic;
|
|
}
|
|
|
|
.toolbar-btn.color {
|
|
position: relative;
|
|
}
|
|
|
|
.toolbar-btn.color::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: 2px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
width: 12px;
|
|
height: 2px;
|
|
background: #ff0000;
|
|
border-radius: 1px;
|
|
}
|
|
|
|
.editor-content {
|
|
min-height: v-bind(height);
|
|
background: white;
|
|
width: 100%;
|
|
}
|
|
|
|
.editor-content :deep(.ql-editor) {
|
|
min-height: v-bind(height);
|
|
padding: 16px;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
line-height: 1.6;
|
|
color: #333;
|
|
width: 100%;
|
|
}
|
|
|
|
.editor-content :deep(.ql-toolbar) {
|
|
display: block;
|
|
background: white;
|
|
border-bottom: 1px solid #E6E6E6;
|
|
padding: 12px 16px;
|
|
}
|
|
|
|
.editor-content :deep(.ql-container) {
|
|
border-bottom-left-radius: 4px;
|
|
border-bottom-right-radius: 4px;
|
|
}
|
|
|
|
.editor-content :deep(.ql-editor) {
|
|
min-height: v-bind(height);
|
|
padding: 16px;
|
|
padding-bottom: 80px;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
line-height: 1.6;
|
|
color: #333;
|
|
width: 100%;
|
|
border-bottom-left-radius: 4px;
|
|
border-bottom-right-radius: 4px;
|
|
}
|
|
|
|
.color-picker-popup {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
background: white;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
padding: 8px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
z-index: 1000;
|
|
}
|
|
|
|
.color-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(5, 1fr);
|
|
gap: 4px;
|
|
}
|
|
|
|
.color-item {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 2px;
|
|
cursor: pointer;
|
|
border: 1px solid #ddd;
|
|
transition: transform 0.1s;
|
|
}
|
|
|
|
.color-item:hover {
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
/* 文件列表样式 */
|
|
.file-list-section {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 20px;
|
|
right: 20px;
|
|
z-index: 10;
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
|
|
.file-list {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
}
|
|
|
|
.file-item {
|
|
display: flex;
|
|
align-items: center;
|
|
background: #F5F8FB;
|
|
padding: 8px 12px;
|
|
width: 187px;
|
|
min-height: 84px;
|
|
position: relative;
|
|
}
|
|
|
|
.file-icon {
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.file-icon img {
|
|
width: 49px;
|
|
height: 49px;
|
|
}
|
|
|
|
.file-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.file-name {
|
|
font-size: 10px;
|
|
color: #000;
|
|
font-weight: 500;
|
|
margin-bottom: 2px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.file-size {
|
|
font-size: 8px;
|
|
color: #999;
|
|
}
|
|
|
|
.file-delete {
|
|
position: absolute;
|
|
top: 8px;
|
|
right: 8px;
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: 4px;
|
|
transition: opacity 0.2s;
|
|
z-index: 5;
|
|
}
|
|
|
|
.file-delete img {
|
|
width: 12px;
|
|
height: 12px;
|
|
opacity: 0.6;
|
|
transition: opacity 0.2s;
|
|
}
|
|
|
|
.file-delete:hover img {
|
|
opacity: 1;
|
|
}
|
|
</style>
|