| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- <script setup lang="ts">
- import { ref, watch, onMounted, nextTick } from 'vue';
- const props = defineProps<{
- modelValue?: string;
- }>();
- const emits = defineEmits<{
- 'update:modelValue': [value: string];
- change: [value: string];
- }>();
- const editorRef = ref<HTMLDivElement>();
- const content = ref(props.modelValue || '');
- // 监听外部值变化
- watch(
- () => props.modelValue,
- (newVal) => {
- if (newVal !== content.value && editorRef.value) {
- editorRef.value.innerHTML = newVal || '';
- content.value = newVal || '';
- }
- }
- );
- // 内容变化处理
- const handleInput = () => {
- if (editorRef.value) {
- const html = editorRef.value.innerHTML;
- content.value = html;
- emits('update:modelValue', html);
- emits('change', html);
- }
- };
- // 工具栏命令
- const execCommand = (command: string, value?: string | boolean) => {
- document.execCommand(command, false, value as string);
- editorRef.value?.focus();
- handleInput();
- };
- // 格式化按钮
- const formatBold = () => execCommand('bold');
- const formatItalic = () => execCommand('italic');
- const formatUnderline = () => execCommand('underline');
- // 对齐
- const alignLeft = () => execCommand('justifyLeft');
- const alignCenter = () => execCommand('justifyCenter');
- const alignRight = () => execCommand('justifyRight');
- const alignJustify = () => execCommand('justifyFull');
- // 列表
- const insertUnorderedList = () => execCommand('insertUnorderedList');
- const insertOrderedList = () => execCommand('insertOrderedList');
- // 其他
- const formatBlockquote = () => execCommand('formatBlock', 'blockquote');
- const indent = () => execCommand('indent');
- const outdent = () => execCommand('outdent');
- const insertLink = () => {
- const url = prompt('请输入链接地址:');
- if (url) {
- execCommand('createLink', url);
- }
- };
- const insertCode = () => execCommand('formatBlock', 'pre');
- // 撤销/重做
- const undo = () => execCommand('undo');
- const redo = () => execCommand('redo');
- // 插入图片
- const insertImage = () => {
- const url = prompt('请输入图片地址:');
- if (url) {
- execCommand('insertImage', url);
- }
- };
- // 插入视频
- const insertVideo = () => {
- const url = prompt('请输入视频地址:');
- if (url && editorRef.value) {
- const video = document.createElement('video');
- video.controls = true;
- video.src = url;
- video.style.maxWidth = '100%';
- editorRef.value.appendChild(video);
- handleInput();
- }
- };
- // 段落格式
- const formatParagraph = (tag: string) => {
- execCommand('formatBlock', tag);
- };
- onMounted(() => {
- if (editorRef.value && props.modelValue) {
- editorRef.value.innerHTML = props.modelValue;
- }
- });
- </script>
- <template>
- <div class="rich-text-editor">
- <!-- 工具栏第一行 -->
- <div class="toolbar toolbar-row-1">
- <a-select
- :value="'p'"
- style="width: 100px"
- @change="(val) => formatParagraph(val)"
- placeholder="段落"
- >
- <a-select-option value="p">段落</a-select-option>
- <a-select-option value="h1">标题1</a-select-option>
- <a-select-option value="h2">标题2</a-select-option>
- <a-select-option value="h3">标题3</a-select-option>
- </a-select>
- <div class="toolbar-divider"></div>
- <a-button-group>
- <a-button @click="formatBold" title="粗体" size="small">
- <strong style="font-weight: bold">B</strong>
- </a-button>
- <a-button @click="formatItalic" title="斜体" size="small">
- <em style="font-style: italic">I</em>
- </a-button>
- <a-button @click="formatUnderline" title="下划线" size="small">
- <u style="text-decoration: underline">U</u>
- </a-button>
- </a-button-group>
- <div class="toolbar-divider"></div>
- <a-button-group>
- <a-button @click="alignLeft" title="左对齐" size="small">⬅</a-button>
- <a-button @click="alignCenter" title="居中" size="small">⬍</a-button>
- <a-button @click="alignRight" title="右对齐" size="small">➡</a-button>
- <a-button @click="alignJustify" title="两端对齐" size="small">⬌</a-button>
- </a-button-group>
- <div class="toolbar-divider"></div>
- <a-button-group>
- <a-button @click="insertUnorderedList" title="无序列表" size="small">•</a-button>
- <a-button @click="insertOrderedList" title="有序列表" size="small">1.</a-button>
- </a-button-group>
- <div class="toolbar-divider"></div>
- <a-button @click="formatBlockquote" title="引用" size="small">"</a-button>
- <a-button @click="outdent" title="减少缩进" size="small">◀</a-button>
- <a-button @click="indent" title="增加缩进" size="small">▶</a-button>
- <a-button @click="insertLink" title="链接" size="small">🔗</a-button>
- <a-button @click="formatBlockquote" title="引用块" size="small">[]</a-button>
- <a-button @click="insertCode" title="代码块" size="small">{}</a-button>
- </div>
- <!-- 工具栏第二行 -->
- <div class="toolbar toolbar-row-2">
- <a-button @click="undo" title="撤销" size="small">↶</a-button>
- <a-button @click="redo" title="重做" size="small">↷</a-button>
- <a-button @click="insertImage" title="图片" size="small">🖼️</a-button>
- <a-button @click="insertVideo" title="视频" size="small">📹</a-button>
- <a-button title="计数" size="small" class="counter-btn">1</a-button>
- </div>
- <!-- 编辑区域 -->
- <div
- ref="editorRef"
- class="editor-content"
- contenteditable="true"
- @input="handleInput"
- @paste="handleInput"
- placeholder="请输入文字"
- ></div>
- </div>
- </template>
- <style scoped lang="scss">
- .rich-text-editor {
- border: 1px solid #d9d9d9;
- border-radius: 4px;
- background: #fff;
- display: flex;
- flex-direction: column;
- }
- .toolbar {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 8px 12px;
- border-bottom: 1px solid #d9d9d9;
- background: #fafafa;
- flex-wrap: wrap;
- }
- .toolbar-row-1 {
- border-bottom: 1px solid #e8e8e8;
- }
- .toolbar-row-2 {
- border-top: 1px solid #e8e8e8;
- }
- .toolbar-divider {
- width: 1px;
- height: 20px;
- background: #d9d9d9;
- margin: 0 4px;
- }
- .editor-content {
- min-height: 300px;
- padding: 12px;
- outline: none;
- overflow-y: auto;
- line-height: 1.6;
- color: #000;
- &:empty::before {
- content: attr(placeholder);
- color: #bfbfbf;
- }
- :deep(img) {
- max-width: 100%;
- height: auto;
- }
- :deep(video) {
- max-width: 100%;
- height: auto;
- }
- }
- :deep(.ant-btn) {
- border-color: #d9d9d9;
- color: #000;
- height: 28px;
- padding: 0 8px;
- &:hover {
- border-color: #40a9ff;
- color: #40a9ff;
- }
- }
- .counter-btn {
- background: #ffd700 !important;
- border-color: #ffd700 !important;
- color: #000 !important;
- }
- </style>
|