Browse Source
feat(editor): replace textarea with Quill.js WYSIWYG editor
feat(editor): replace textarea with Quill.js WYSIWYG editor
- Install quill + @types/quill dependencies - Add QuillEditor.vue component with snow theme, toolbar, image upload - Rebuild ArticleEditorView with left/right split layout (editor + settings panel) - Remove old markdown paste handler, Quill output is HTML directlymain
4 changed files with 388 additions and 163 deletions
-
99package-lock.json
-
3package.json
-
99src/components/QuillEditor.vue
-
318src/views/admin/ArticleEditorView.vue
@ -0,0 +1,99 @@ |
|||
<script setup lang="ts"> |
|||
import { ref, onMounted, watch } from 'vue' |
|||
import Quill from 'quill' |
|||
import 'quill/dist/quill.snow.css' |
|||
import { mediaApi } from '@/api/client' |
|||
|
|||
const props = defineProps<{ |
|||
modelValue: string |
|||
}>() |
|||
const emit = defineEmits<{ |
|||
(e: 'update:modelValue', value: string): void |
|||
}>() |
|||
|
|||
const editorContainer = ref<HTMLDivElement>() |
|||
let quill: Quill | null = null |
|||
|
|||
onMounted(() => { |
|||
if (!editorContainer.value) return |
|||
|
|||
quill = new Quill(editorContainer.value, { |
|||
theme: 'snow', |
|||
placeholder: '开始写作...', |
|||
modules: { |
|||
toolbar: [ |
|||
[{ header: [1, 2, false] }], |
|||
['bold', 'italic', 'underline', 'strike'], |
|||
['blockquote', 'code-block'], |
|||
[{ list: 'ordered' }, { list: 'bullet' }], |
|||
[{ indent: '-1' }, { indent: '+1' }], |
|||
['link', 'image'], |
|||
['clean'] |
|||
] |
|||
} |
|||
}) |
|||
|
|||
quill.root.innerHTML = props.modelValue |
|||
|
|||
quill.on('text-change', () => { |
|||
emit('update:modelValue', quill!.root.innerHTML) |
|||
}) |
|||
|
|||
const toolbar = quill.getModule('toolbar') as any |
|||
toolbar.addHandler('image', () => { |
|||
const input = document.createElement('input') |
|||
input.setAttribute('type', 'file') |
|||
input.setAttribute('accept', 'image/*') |
|||
input.click() |
|||
input.onchange = async () => { |
|||
const file = input.files?.[0] |
|||
if (file) { |
|||
try { |
|||
const resp = await mediaApi.apiMediaUploadPost(file, 'images') |
|||
const stored = resp.data.data |
|||
const url = stored?.url |
|||
if (url && quill) { |
|||
const sel = quill.getSelection() |
|||
const index = sel ? sel.index : 0 |
|||
quill.insertEmbed(index, 'image', url) |
|||
} |
|||
} catch (e) { |
|||
console.error(e) |
|||
} |
|||
} |
|||
} |
|||
}) |
|||
}) |
|||
|
|||
watch(() => props.modelValue, (val) => { |
|||
if (quill && val !== quill.root.innerHTML) { |
|||
quill.root.innerHTML = val |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<template> |
|||
<div ref="editorContainer" class="quill-editor" /> |
|||
</template> |
|||
|
|||
<style scoped> |
|||
.quill-editor { |
|||
min-height: 400px; |
|||
background: var(--ac-bg-card, #fff); |
|||
} |
|||
.quill-editor :deep(.ql-editor) { |
|||
min-height: 400px; |
|||
font-size: 16px; |
|||
line-height: 1.8; |
|||
} |
|||
.quill-editor :deep(.ql-toolbar) { |
|||
border-color: var(--ac-border, #d4c4b0); |
|||
background: var(--ac-surface, #faf8f5); |
|||
} |
|||
.quill-editor :deep(.ql-container) { |
|||
border-color: var(--ac-border, #d4c4b0); |
|||
} |
|||
.quill-editor :deep(.ql-editor.ql-blank::before) { |
|||
color: var(--ac-text-disabled); |
|||
} |
|||
</style> |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue