first commit

This commit is contained in:
2026-07-01 00:22:05 +08:00
commit e4efa4b54d
48 changed files with 7983 additions and 0 deletions

View File

@@ -0,0 +1,336 @@
<script setup lang="ts">
import { watch } from "vue";
import { useEditor, EditorContent } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";
import Placeholder from "@tiptap/extension-placeholder";
const props = withDefaults(
defineProps<{
modelValue?: string;
placeholder?: string;
}>(),
{
modelValue: "",
placeholder: "开始写作...",
},
);
const emit = defineEmits<{
"update:modelValue": [value: string];
}>();
const editor = useEditor({
content: props.modelValue,
extensions: [
StarterKit.configure({
heading: { levels: [1, 2, 3] },
}),
Placeholder.configure({
placeholder: props.placeholder,
}),
],
editorProps: {
attributes: {
class:
"min-h-[320px] px-6 py-4 outline-none focus:outline-none",
},
},
onUpdate: ({ editor }) => {
emit("update:modelValue", editor.getHTML());
},
});
// Sync external modelValue changes back into the editor
watch(
() => props.modelValue,
(val) => {
if (editor.value && val !== editor.value.getHTML()) {
editor.value.commands.setContent(val, { emitUpdate: false });
}
},
);
</script>
<template>
<div
class="flex flex-col rounded-xl border border-gray-700 bg-gray-900 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"
>
<!-- Headings -->
<button
v-for="level in ([1, 2, 3] as const)"
:key="'h' + level"
: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',
]"
@click="editor.chain().focus().toggleHeading({ level }).run()"
>
H{{ level }}
</button>
<span class="w-px h-6 bg-gray-600 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',
]"
@click="editor.chain().focus().toggleBold().run()"
>
<strong>B</strong>
</button>
<button
:class="[
'px-2.5 py-1.5 text-xs font-medium rounded-md transition-colors italic',
editor.isActive('italic')
? 'bg-indigo-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
]"
@click="editor.chain().focus().toggleItalic().run()"
>
I
</button>
<button
:class="[
'px-2.5 py-1.5 text-xs font-medium rounded-md transition-colors line-through',
editor.isActive('strike')
? 'bg-indigo-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
]"
@click="editor.chain().focus().toggleStrike().run()"
>
S
</button>
<button
:class="[
'px-2.5 py-1.5 text-xs font-mono font-medium rounded-md transition-colors',
editor.isActive('code')
? 'bg-indigo-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
]"
@click="editor.chain().focus().toggleCode().run()"
>
&lt;/&gt;
</button>
<span class="w-px h-6 bg-gray-600 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',
]"
@click="editor.chain().focus().toggleBlockquote().run()"
>
"Quote"
</button>
<button
:class="[
'px-2.5 py-1.5 text-xs font-mono font-medium rounded-md transition-colors',
editor.isActive('codeBlock')
? 'bg-indigo-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
]"
@click="editor.chain().focus().toggleCodeBlock().run()"
>
Code
</button>
<span class="w-px h-6 bg-gray-600 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',
]"
@click="editor.chain().focus().toggleBulletList().run()"
>
List
</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',
]"
@click="editor.chain().focus().toggleOrderedList().run()"
>
1. List
</button>
<span class="w-px h-6 bg-gray-600 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"
@click="editor.chain().focus().setHorizontalRule().run()"
>
</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"
:disabled="!editor.can().chain().focus().undo().run()"
@click="editor.chain().focus().undo().run()"
>
</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"
:disabled="!editor.can().chain().focus().redo().run()"
@click="editor.chain().focus().redo().run()"
>
</button>
</div>
<!-- Editor content -->
<EditorContent :editor="editor" class="tiptap-editor" />
</div>
</template>
<style>
/* TipTap / ProseMirror content styling */
.tiptap-editor .ProseMirror {
color: #f3f4f6;
min-height: 320px;
outline: none;
}
.tiptap-editor .ProseMirror p.is-editor-empty:first-child::before {
color: #6b7280;
float: left;
height: 0;
pointer-events: none;
content: attr(data-placeholder);
}
/* Headings */
.tiptap-editor .ProseMirror h1 {
font-size: 1.5rem;
font-weight: 700;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: #fff;
}
.tiptap-editor .ProseMirror h2 {
font-size: 1.25rem;
font-weight: 700;
margin-top: 1.25rem;
margin-bottom: 0.5rem;
color: #fff;
}
.tiptap-editor .ProseMirror h3 {
font-size: 1.125rem;
font-weight: 600;
margin-top: 1rem;
margin-bottom: 0.5rem;
color: #fff;
}
/* Blockquote */
.tiptap-editor .ProseMirror blockquote {
border-left: 4px solid #6366f1;
padding-left: 1rem;
margin-top: 0.75rem;
margin-bottom: 0.75rem;
color: #9ca3af;
font-style: italic;
}
/* Inline code */
.tiptap-editor .ProseMirror code {
background-color: #1f2937;
color: #f472b6;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.875rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
/* Code block */
.tiptap-editor .ProseMirror pre {
background-color: #1f2937;
border-radius: 0.5rem;
padding: 1rem;
margin-top: 0.75rem;
margin-bottom: 0.75rem;
overflow-x: auto;
}
.tiptap-editor .ProseMirror pre code {
background-color: transparent;
color: #e5e7eb;
padding: 0;
}
/* Lists */
.tiptap-editor .ProseMirror ul {
list-style-type: disc;
padding-left: 1.5rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.tiptap-editor .ProseMirror ol {
list-style-type: decimal;
padding-left: 1.5rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.tiptap-editor .ProseMirror li {
margin-top: 0.25rem;
margin-bottom: 0.25rem;
}
/* Horizontal rule */
.tiptap-editor .ProseMirror hr {
border-color: #374151;
margin-top: 1.5rem;
margin-bottom: 1.5rem;
}
/* Links */
.tiptap-editor .ProseMirror a {
color: #818cf8;
text-decoration: underline;
}
/* Bold / Italic / Strike */
.tiptap-editor .ProseMirror strong {
font-weight: 700;
color: #fff;
}
.tiptap-editor .ProseMirror em {
font-style: italic;
}
.tiptap-editor .ProseMirror s {
text-decoration: line-through;
color: #9ca3af;
}
/* Paragraph spacing */
.tiptap-editor .ProseMirror p {
margin-top: 0.375rem;
margin-bottom: 0.375rem;
line-height: 1.625;
}
</style>