This commit is contained in:
2026-07-03 00:56:13 +08:00
parent 82a64787c3
commit bc96296670
13 changed files with 611 additions and 378 deletions

View File

@@ -3,6 +3,7 @@ import { watch } from "vue";
import { useEditor, EditorContent } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";
import Placeholder from "@tiptap/extension-placeholder";
import { Markdown } from "@tiptap/markdown";
const props = withDefaults(
defineProps<{
@@ -29,6 +30,7 @@ const editor = useEditor({
Placeholder.configure({
placeholder: props.placeholder,
}),
Markdown,
],
editorProps: {
attributes: {
@@ -45,7 +47,7 @@ const editor = useEditor({
},
},
onUpdate: ({ editor }) => {
emit("update:modelValue", editor.getHTML());
emit("update:modelValue", editor.getMarkdown());
},
});
@@ -53,8 +55,8 @@ const editor = useEditor({
watch(
() => props.modelValue,
(val) => {
if (editor.value && val !== editor.value.getHTML()) {
editor.value.commands.setContent(val, { emitUpdate: false });
if (editor.value && val !== editor.value.getMarkdown()) {
editor.value.commands.setContent(val, { contentType: "markdown", emitUpdate: false });
}
},
);
@@ -62,12 +64,12 @@ watch(
<template>
<div
class="flex flex-col flex-1 w-full border border-gray-700 bg-gray-900 shadow-2xl overflow-hidden"
class="flex flex-col flex-1 w-full border border-[#e0dbcf] bg-white shadow-2xl overflow-hidden"
>
<!-- Toolbar -->
<div
v-if="editor"
class="flex flex-wrap items-center gap-0.5 px-3 py-2 border-b border-gray-700 bg-gray-800/80 backdrop-blur"
class="flex flex-wrap items-center gap-0.5 px-3 py-2 border-b border-[#e0dbcf] bg-[#f4f1eb]/80 backdrop-blur"
>
<!-- Headings -->
<button
@@ -76,140 +78,140 @@ watch(
:class="[
'px-2.5 py-1.5 text-xs font-semibold rounded-md transition-colors',
editor.isActive('heading', { level })
? 'bg-indigo-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
? 'bg-[#bf6a3b] text-white'
: 'text-[#5c574e] hover:bg-[#e0dbcf] hover:text-[#38342e]',
]"
@click="editor.chain().focus().toggleHeading({ level }).run()"
>
H{{ level }}
</button>
<span class="w-px h-6 bg-gray-600 mx-1" />
<span class="w-px h-6 bg-[#d4cfc4] mx-1" />
<!-- Inline formatting -->
<button
:class="[
'px-2.5 py-1.5 text-xs font-medium rounded-md transition-colors',
editor.isActive('bold')
? 'bg-indigo-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
? 'bg-[#bf6a3b] text-white'
: 'text-[#5c574e] hover:bg-[#e0dbcf] hover:text-[#38342e]',
]"
@click="editor.chain().focus().toggleBold().run()"
>
<strong>B</strong>
<i class="ri-bold"></i>
</button>
<button
:class="[
'px-2.5 py-1.5 text-xs font-medium rounded-md transition-colors italic',
'px-2.5 py-1.5 text-xs font-medium rounded-md transition-colors',
editor.isActive('italic')
? 'bg-indigo-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
? 'bg-[#bf6a3b] text-white'
: 'text-[#5c574e] hover:bg-[#e0dbcf] hover:text-[#38342e]',
]"
@click="editor.chain().focus().toggleItalic().run()"
>
I
<i class="ri-italic"></i>
</button>
<button
:class="[
'px-2.5 py-1.5 text-xs font-medium rounded-md transition-colors line-through',
'px-2.5 py-1.5 text-xs font-medium rounded-md transition-colors',
editor.isActive('strike')
? 'bg-indigo-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
? 'bg-[#bf6a3b] text-white'
: 'text-[#5c574e] hover:bg-[#e0dbcf] hover:text-[#38342e]',
]"
@click="editor.chain().focus().toggleStrike().run()"
>
S
<i class="ri-strikethrough"></i>
</button>
<button
:class="[
'px-2.5 py-1.5 text-xs font-mono font-medium rounded-md transition-colors',
'px-2.5 py-1.5 text-xs font-medium rounded-md transition-colors',
editor.isActive('code')
? 'bg-indigo-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
? 'bg-[#bf6a3b] text-white'
: 'text-[#5c574e] hover:bg-[#e0dbcf] hover:text-[#38342e]',
]"
@click="editor.chain().focus().toggleCode().run()"
>
&lt;/&gt;
<i class="ri-code-line"></i>
</button>
<span class="w-px h-6 bg-gray-600 mx-1" />
<span class="w-px h-6 bg-[#d4cfc4] mx-1" />
<!-- Block elements -->
<button
:class="[
'px-2.5 py-1.5 text-xs font-medium rounded-md transition-colors',
editor.isActive('blockquote')
? 'bg-indigo-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
? 'bg-[#bf6a3b] text-white'
: 'text-[#5c574e] hover:bg-[#e0dbcf] hover:text-[#38342e]',
]"
@click="editor.chain().focus().toggleBlockquote().run()"
>
"Quote"
<i class="ri-double-quotes-l"></i>
</button>
<button
:class="[
'px-2.5 py-1.5 text-xs font-mono font-medium rounded-md transition-colors',
'px-2.5 py-1.5 text-xs font-medium rounded-md transition-colors',
editor.isActive('codeBlock')
? 'bg-indigo-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
? 'bg-[#bf6a3b] text-white'
: 'text-[#5c574e] hover:bg-[#e0dbcf] hover:text-[#38342e]',
]"
@click="editor.chain().focus().toggleCodeBlock().run()"
>
Code
<i class="ri-code-box-line"></i>
</button>
<span class="w-px h-6 bg-gray-600 mx-1" />
<span class="w-px h-6 bg-[#d4cfc4] mx-1" />
<!-- Lists -->
<button
:class="[
'px-2.5 py-1.5 text-xs font-medium rounded-md transition-colors',
editor.isActive('bulletList')
? 'bg-indigo-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
? 'bg-[#bf6a3b] text-white'
: 'text-[#5c574e] hover:bg-[#e0dbcf] hover:text-[#38342e]',
]"
@click="editor.chain().focus().toggleBulletList().run()"
>
List
<i class="ri-list-unordered"></i>
</button>
<button
:class="[
'px-2.5 py-1.5 text-xs font-medium rounded-md transition-colors',
editor.isActive('orderedList')
? 'bg-indigo-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
? 'bg-[#bf6a3b] text-white'
: 'text-[#5c574e] hover:bg-[#e0dbcf] hover:text-[#38342e]',
]"
@click="editor.chain().focus().toggleOrderedList().run()"
>
1. List
<i class="ri-list-ordered"></i>
</button>
<span class="w-px h-6 bg-gray-600 mx-1" />
<span class="w-px h-6 bg-[#d4cfc4] mx-1" />
<!-- Horizontal rule -->
<button
class="px-2.5 py-1.5 text-xs font-medium rounded-md text-gray-300 hover:bg-gray-700 hover:text-white transition-colors"
class="px-2.5 py-1.5 text-xs font-medium rounded-md text-[#5c574e] hover:bg-[#e0dbcf] hover:text-[#38342e] transition-colors"
@click="editor.chain().focus().setHorizontalRule().run()"
>
<i class="ri-separator"></i>
</button>
<span class="flex-1" />
<!-- Undo / Redo -->
<button
class="px-2.5 py-1.5 text-xs font-medium rounded-md text-gray-300 hover:bg-gray-700 hover:text-white transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
class="px-2.5 py-1.5 text-xs font-medium rounded-md text-[#5c574e] hover:bg-[#e0dbcf] hover:text-[#38342e] transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
:disabled="!editor.can().chain().focus().undo().run()"
@click="editor.chain().focus().undo().run()"
>
<i class="ri-arrow-go-back-line"></i>
</button>
<button
class="px-2.5 py-1.5 text-xs font-medium rounded-md text-gray-300 hover:bg-gray-700 hover:text-white transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
class="px-2.5 py-1.5 text-xs font-medium rounded-md text-[#5c574e] hover:bg-[#e0dbcf] hover:text-[#38342e] transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
:disabled="!editor.can().chain().focus().redo().run()"
@click="editor.chain().focus().redo().run()"
>
<i class="ri-arrow-go-forward-line"></i>
</button>
</div>
@@ -221,14 +223,15 @@ watch(
<style>
/* TipTap / ProseMirror content styling */
.tiptap-editor .ProseMirror {
color: #f3f4f6;
color: #38342e;
min-height: 320px;
flex: 1;
outline: none;
overflow-y: auto;
}
.tiptap-editor .ProseMirror p.is-editor-empty:first-child::before {
color: #6b7280;
color: #b8b3a8;
float: left;
height: 0;
pointer-events: none;
@@ -241,37 +244,37 @@ watch(
font-weight: 700;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: #fff;
color: #2d2a26;
}
.tiptap-editor .ProseMirror h2 {
font-size: 1.25rem;
font-weight: 700;
margin-top: 1.25rem;
margin-bottom: 0.5rem;
color: #fff;
color: #2d2a26;
}
.tiptap-editor .ProseMirror h3 {
font-size: 1.125rem;
font-weight: 600;
margin-top: 1rem;
margin-bottom: 0.5rem;
color: #fff;
color: #2d2a26;
}
/* Blockquote */
.tiptap-editor .ProseMirror blockquote {
border-left: 4px solid #6366f1;
border-left: 4px solid #bf6a3b;
padding-left: 1rem;
margin-top: 0.75rem;
margin-bottom: 0.75rem;
color: #9ca3af;
color: #8c877d;
font-style: italic;
}
/* Inline code */
.tiptap-editor .ProseMirror code {
background-color: #1f2937;
color: #f472b6;
background-color: #f4f1ea;
color: #bf6a3b;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.875rem;
@@ -280,7 +283,7 @@ watch(
/* Code block */
.tiptap-editor .ProseMirror pre {
background-color: #1f2937;
background-color: #f4f1ea;
border-radius: 0.5rem;
padding: 1rem;
margin-top: 0.75rem;
@@ -289,7 +292,7 @@ watch(
}
.tiptap-editor .ProseMirror pre code {
background-color: transparent;
color: #e5e7eb;
color: #5c574e;
padding: 0;
}
@@ -313,28 +316,28 @@ watch(
/* Horizontal rule */
.tiptap-editor .ProseMirror hr {
border-color: #374151;
border-color: #e8e4da;
margin-top: 1.5rem;
margin-bottom: 1.5rem;
}
/* Links */
.tiptap-editor .ProseMirror a {
color: #818cf8;
color: #bf6a3b;
text-decoration: underline;
}
/* Bold / Italic / Strike */
.tiptap-editor .ProseMirror strong {
font-weight: 700;
color: #fff;
color: #2d2a26;
}
.tiptap-editor .ProseMirror em {
font-style: italic;
}
.tiptap-editor .ProseMirror s {
text-decoration: line-through;
color: #9ca3af;
color: #8c877d;
}
/* Paragraph spacing */