优化Jeditor

This commit is contained in:
jingrow 2026-05-30 01:03:25 +08:00
parent e382297495
commit ada36b3cf0
4 changed files with 397 additions and 95 deletions

View File

@ -182,6 +182,7 @@ onMounted(() => {
},
})
editorRef.value = _editor
document.addEventListener('mousedown', handleMousedown)
})
function focusEditor() {
@ -189,6 +190,7 @@ function focusEditor() {
}
onBeforeUnmount(() => {
document.removeEventListener('mousedown', handleMousedown)
_editor?.destroy()
_editor = null
editorRef.value = null
@ -237,9 +239,91 @@ function exec(name: string, attrs?: Record<string, any>) {
}
case 'undo': chain.undo().run(); break
case 'redo': chain.redo().run(); break
case 'codeBlock': chain.toggleCodeBlock().run(); break
case 'taskList': chain.toggleTaskList().run(); break
case 'subscript': chain.toggleSubscript().run(); break
case 'superscript': chain.toggleSuperscript().run(); break
case 'image': openImagePicker(ed); break
case 'hr': chain.setHorizontalRule().run(); break
}
}
// ===== Dropdown state =====
const activeDropdown = ref<string | null>(null)
function toggleDropdown(name: string | null) {
activeDropdown.value = activeDropdown.value === name ? null : name
}
function handleMousedown(e: MouseEvent) {
if (!(e.target as HTMLElement).closest('.be-tb-dropdown')) {
activeDropdown.value = null
}
}
// ===== Font sizes =====
const fontSizes = [8, 10, 12, 14, 16, 18, 20, 24, 28, 32, 36, 48, 64]
function setFontSize(sz: number) {
_editor?.chain().focus().setMark('textStyle', { fontSize: `${sz}px` }).run()
activeDropdown.value = null
}
function resetFontSize() {
_editor?.chain().focus().unsetMark('textStyle', { extendEmptyMarkRange: true }).run()
activeDropdown.value = null
}
// ===== Text align =====
function setTextAlign(align: string) {
_editor?.chain().focus().setTextAlign(align).run()
activeDropdown.value = null
}
// ===== Table commands =====
function insertTable() {
_editor?.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()
activeDropdown.value = null
}
function deleteTable() {
_editor?.chain().focus().deleteTable().run()
activeDropdown.value = null
}
function addRowAfter() {
_editor?.chain().focus().addRowAfter().run()
activeDropdown.value = null
}
function addColumnAfter() {
_editor?.chain().focus().addColumnAfter().run()
activeDropdown.value = null
}
function deleteRow() {
_editor?.chain().focus().deleteRow().run()
activeDropdown.value = null
}
function deleteColumn() {
_editor?.chain().focus().deleteColumn().run()
activeDropdown.value = null
}
// ===== Text color =====
const TEXT_COLORS = ['#000000','#444444','#666666','#999999','#b45309','#d97706','#059669','#047857','#1d4ed8','#2563eb','#7c3aed','#db2777']
const HIGHLIGHT_COLORS = ['#ffff00','#ffedd5','#fef3c7','#fce7f3','#dbeafe','#e0e7ff','#dcfce7','#d1fae5','#f3e8ff','#fce4ec','#fff3e0','transparent']
function setTextColor(color: string) {
_editor?.chain().focus().setColor(color).run()
activeDropdown.value = null
}
function setHighlightColor(color: string) {
if (color === 'transparent') {
_editor?.chain().focus().unsetHighlight().run()
} else {
_editor?.chain().focus().setHighlight({ color }).run()
}
activeDropdown.value = null
}
// ===== Block Handle =====
const currentBlockPos = ref<number | null>(null)
const showBlockMenu = ref(false)
@ -411,6 +495,32 @@ const ICONS: Record<string, string> = {
h3: '<text x="3" y="18" font-family="system-ui,-apple-system,sans-serif" font-size="16" font-weight="700" fill="currentColor" stroke="none">H3</text>',
link: '<path d="m9 15l6-6m-4-3l.463-.536a5 5 0 0 1 7.071 7.072L18 13m-5 5l-.397.534a5.07 5.07 0 0 1-7.127 0a4.97 4.97 0 0 1 0-7.071L6 11"/>',
drag: '<circle cx="8" cy="7" r="1.5" fill="currentColor"/><circle cx="16" cy="7" r="1.5" fill="currentColor"/><circle cx="8" cy="12" r="1.5" fill="currentColor"/><circle cx="16" cy="12" r="1.5" fill="currentColor"/><circle cx="8" cy="17" r="1.5" fill="currentColor"/><circle cx="16" cy="17" r="1.5" fill="currentColor"/>',
// - "Aa"
format_size: '<text x="0.5" y="19" font-family="system-ui,-apple-system,sans-serif" font-size="20" font-weight="700" fill="currentColor" stroke="none">A</text><text x="13" y="19" font-family="system-ui,-apple-system,sans-serif" font-size="14" font-weight="600" fill="currentColor" stroke="none">a</text>',
// - tabler:table
table_menu: '<path d="M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2zm0 5h18M10 3v18"/>',
// - tabler:align-left
align_left: '<path d="M4 6h16M4 12h10M4 18h14"/>',
// - tabler:align-center
align_center: '<path d="M4 6h16M8 12h8M6 18h12"/>',
// - tabler:align-right
align_right: '<path d="M4 6h16m-10 6h10M6 18h14"/>',
// - tabler:source-code
code_block: '<path d="M14.5 4H17a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-5m2-7L4 7l2 2"/><path d="m10 9l2-2l-2-2"/>',
// - tabler:list-check
task_list: '<path d="M3.5 5.5L5 7l2.5-2.5m-4 7L5 13l2.5-2.5m-4 7L5 19l2.5-2.5M11 6h9m-9 6h9m-9 6h9"/>',
// - tabler:subscript
subscript: '<path d="m5 7l8 10m-8 0l8-10m8 13h-4l3.5-4a1.73 1.73 0 0 0-3.5-2"/>',
// - tabler:superscript
superscript: '<path d="m5 7l8 10m-8 0l8-10m8 4h-4l3.5-4A1.73 1.73 0 0 0 17 5"/>',
// - A +
text_color: '<text x="12" y="9.5" text-anchor="middle" dy="0.35em" font-family="system-ui,-apple-system,sans-serif" font-size="15" font-weight="700" fill="currentColor" stroke="none">A</text><rect x="4.5" y="17.5" width="15" height="3" rx="1.5" fill="currentColor" stroke="none"/>',
// - A +
highlight: '<rect x="3" y="6" width="18" height="8" rx="2" fill="currentColor" opacity="0.25" stroke="none"/><text x="12" y="9.5" text-anchor="middle" dy="0.35em" font-family="system-ui,-apple-system,sans-serif" font-size="15" font-weight="700" fill="currentColor" stroke="none">A</text>',
// - tabler:photo
image: '<path d="M15 8h.01M3 6a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3z"/><path d="m3 16l5-5c.928-.893 2.072-.893 3 0l5 5"/><path d="m14 14l1-1c.928-.893 2.072-.893 3 0l3 3"/>',
// 线 - tabler:minus
hr_line: '<path d="M5 12h14"/>',
}
function icon(name: string): string {
@ -427,23 +537,97 @@ function icon(name: string): string {
<div class="blockeditor-container" :class="{ 'blockeditor-ready': editorRef }">
<!-- 🔧 Toolbar visible when editor is created -->
<div v-if="editorRef && isEditable" class="blockeditor-toolbar">
<!-- Undo / Redo -->
<button class="be-tb-btn" :title="ctx?.t ? ctx.t('Undo') : 'Undo'" @click="exec('undo')" v-html="icon('undo')"></button>
<button class="be-tb-btn" :title="ctx?.t ? ctx.t('Redo') : 'Redo'" @click="exec('redo')" v-html="icon('redo')"></button>
<span class="be-tb-divider"></span>
<!-- Font Size -->
<div class="be-tb-dropdown">
<button class="be-tb-btn" :title="ctx?.t ? ctx.t('Font Size') : 'Font Size'" @click="toggleDropdown('fontSize')" v-html="icon('format_size')"></button>
<div v-if="activeDropdown === 'fontSize'" class="be-tb-dropdown-menu">
<div class="be-tb-dropdown-item" v-for="sz in fontSizes" :key="sz" @mousedown.prevent="setFontSize(sz)">{{ sz }}px</div>
<div class="be-tb-dropdown-divider"></div>
<div class="be-tb-dropdown-item" @mousedown.prevent="resetFontSize()">{{ ctx?.t ? ctx.t('Reset') : 'Reset' }}</div>
</div>
</div>
<!-- Headings H1/H2/H3 -->
<button class="be-tb-btn" :class="{ active: isActive('heading', { level: 1 }) }" :title="ctx?.t ? ctx.t('Heading 1') : 'Heading 1'" @click="exec('h1')" v-html="icon('h1')"></button>
<button class="be-tb-btn" :class="{ active: isActive('heading', { level: 2 }) }" :title="ctx?.t ? ctx.t('Heading 2') : 'Heading 2'" @click="exec('h2')" v-html="icon('h2')"></button>
<button class="be-tb-btn" :class="{ active: isActive('heading', { level: 3 }) }" :title="ctx?.t ? ctx.t('Heading 3') : 'Heading 3'" @click="exec('h3')" v-html="icon('h3')"></button>
<span class="be-tb-divider"></span>
<!-- Bold / Italic / Underline / Strike / Inline Code -->
<button class="be-tb-btn" :class="{ active: isActive('bold') }" :title="ctx?.t ? ctx.t('Bold') : 'Bold'" @click="exec('bold')" v-html="icon('bold')"></button>
<button class="be-tb-btn" :class="{ active: isActive('italic') }" :title="ctx?.t ? ctx.t('Italic') : 'Italic'" @click="exec('italic')" v-html="icon('italic')"></button>
<button class="be-tb-btn" :class="{ active: isActive('underline') }" :title="ctx?.t ? ctx.t('Underline') : 'Underline'" @click="exec('underline')" v-html="icon('underline')"></button>
<button class="be-tb-btn" :class="{ active: isActive('strike') }" :title="ctx?.t ? ctx.t('Strikethrough') : 'Strikethrough'" @click="exec('strike')" v-html="icon('strike')"></button>
<button class="be-tb-btn" :class="{ active: isActive('code') }" :title="ctx?.t ? ctx.t('Inline Code') : 'Inline Code'" @click="exec('code')" v-html="icon('code')"></button>
<span class="be-tb-divider"></span>
<button class="be-tb-btn" :class="{ active: isActive('heading', { level: 1 }) }" :title="ctx?.t ? ctx.t('Heading 1') : 'Heading 1'" @click="exec('h1')" v-html="icon('h1')"></button>
<button class="be-tb-btn" :class="{ active: isActive('heading', { level: 2 }) }" :title="ctx?.t ? ctx.t('Heading 2') : 'Heading 2'" @click="exec('h2')" v-html="icon('h2')"></button>
<button class="be-tb-btn" :class="{ active: isActive('heading', { level: 3 }) }" :title="ctx?.t ? ctx.t('Heading 3') : 'Heading 3'" @click="exec('h3')" v-html="icon('h3')"></button>
<span class="be-tb-divider"></span>
<!-- Code Block / Blockquote / Subscript / Superscript -->
<button class="be-tb-btn" :class="{ active: isActive('codeBlock') }" :title="ctx?.t ? ctx.t('Code Block') : 'Code Block'" @click="exec('codeBlock')" v-html="icon('code_block')"></button>
<button class="be-tb-btn" :class="{ active: isActive('blockquote') }" :title="ctx?.t ? ctx.t('Blockquote') : 'Blockquote'" @click="exec('quote')" v-html="icon('quote')"></button>
<button class="be-tb-btn" :class="{ active: isActive('subscript') }" :title="ctx?.t ? ctx.t('Subscript') : 'Subscript'" @click="exec('subscript')" v-html="icon('subscript')"></button>
<button class="be-tb-btn" :class="{ active: isActive('superscript') }" :title="ctx?.t ? ctx.t('Superscript') : 'Superscript'" @click="exec('superscript')" v-html="icon('superscript')"></button>
<span class="be-tb-divider"></span>
<!-- Bullet List / Numbered List / Task List -->
<button class="be-tb-btn" :class="{ active: isActive('bulletList') }" :title="ctx?.t ? ctx.t('Bullet List') : 'Bullet List'" @click="exec('bulletList')" v-html="icon('ulist')"></button>
<button class="be-tb-btn" :class="{ active: isActive('orderedList') }" :title="ctx?.t ? ctx.t('Numbered List') : 'Numbered List'" @click="exec('orderedList')" v-html="icon('olist')"></button>
<button class="be-tb-btn" :class="{ active: isActive('taskList') }" :title="ctx?.t ? ctx.t('Task List') : 'Task List'" @click="exec('taskList')" v-html="icon('task_list')"></button>
<span class="be-tb-divider"></span>
<!-- Link / Image / Divider -->
<button class="be-tb-btn" :class="{ active: isActive('link') }" :title="ctx?.t ? ctx.t('Link') : 'Link'" @click="exec('link')" v-html="icon('link')"></button>
<button class="be-tb-btn" :title="ctx?.t ? ctx.t('Image') : 'Image'" @click="exec('image')" v-html="icon('image')"></button>
<button class="be-tb-btn" :title="ctx?.t ? ctx.t('Divider') : 'Divider'" @click="exec('hr')" v-html="icon('hr_line')"></button>
<span class="be-tb-divider"></span>
<!-- Align Dropdown -->
<div class="be-tb-dropdown">
<button class="be-tb-btn" :title="ctx?.t ? ctx.t('Align') : 'Align'" @click="toggleDropdown('align')" v-html="icon('align_left')"></button>
<div v-if="activeDropdown === 'align'" class="be-tb-dropdown-menu">
<div class="be-tb-dropdown-item" @mousedown.prevent="setTextAlign('left')"><span v-html="icon('align_left')"></span>{{ ctx?.t ? ctx.t('Align Left') : 'Align Left' }}</div>
<div class="be-tb-dropdown-item" @mousedown.prevent="setTextAlign('center')"><span v-html="icon('align_center')"></span>{{ ctx?.t ? ctx.t('Align Center') : 'Align Center' }}</div>
<div class="be-tb-dropdown-item" @mousedown.prevent="setTextAlign('right')"><span v-html="icon('align_right')"></span>{{ ctx?.t ? ctx.t('Align Right') : 'Align Right' }}</div>
</div>
</div>
<!-- Table Dropdown -->
<div class="be-tb-dropdown">
<button class="be-tb-btn" :title="ctx?.t ? ctx.t('Table') : 'Table'" @click="toggleDropdown('table')" v-html="icon('table_menu')"></button>
<div v-if="activeDropdown === 'table'" class="be-tb-dropdown-menu">
<div class="be-tb-dropdown-item" @mousedown.prevent="insertTable()">{{ ctx?.t ? ctx.t('Insert Table') : 'Insert Table' }}</div>
<div class="be-tb-dropdown-item" @mousedown.prevent="deleteTable()">{{ ctx?.t ? ctx.t('Delete Table') : 'Delete Table' }}</div>
<div class="be-tb-dropdown-divider"></div>
<div class="be-tb-dropdown-item" @mousedown.prevent="addRowAfter()">{{ ctx?.t ? ctx.t('Insert Row Below') : 'Insert Row Below' }}</div>
<div class="be-tb-dropdown-item" @mousedown.prevent="addColumnAfter()">{{ ctx?.t ? ctx.t('Insert Column Right') : 'Insert Column Right' }}</div>
<div class="be-tb-dropdown-item" @mousedown.prevent="deleteRow()">{{ ctx?.t ? ctx.t('Delete Row') : 'Delete Row' }}</div>
<div class="be-tb-dropdown-item" @mousedown.prevent="deleteColumn()">{{ ctx?.t ? ctx.t('Delete Column') : 'Delete Column' }}</div>
</div>
</div>
<span class="be-tb-divider"></span>
<!-- Text Color / Highlight -->
<div class="be-tb-dropdown">
<button class="be-tb-btn" :title="ctx?.t ? ctx.t('Text Color') : 'Text Color'" @click="toggleDropdown('textColor')" v-html="icon('text_color')"></button>
<div v-if="activeDropdown === 'textColor'" class="be-tb-dropdown-menu be-tb-dropdown-menu-right">
<div class="be-tb-color-grid">
<div v-for="c in TEXT_COLORS" :key="c" class="be-tb-color-swatch" :style="{ background: c }" @mousedown.prevent="setTextColor(c)"></div>
</div>
</div>
</div>
<div class="be-tb-dropdown">
<button class="be-tb-btn" :title="ctx?.t ? ctx.t('Highlight') : 'Highlight'" @click="toggleDropdown('highlight')" v-html="icon('highlight')"></button>
<div v-if="activeDropdown === 'highlight'" class="be-tb-dropdown-menu be-tb-dropdown-menu-right">
<div class="be-tb-color-grid">
<div v-for="c in HIGHLIGHT_COLORS" :key="c" class="be-tb-color-swatch" :class="{ 'be-tb-color-swatch-transparent': c === 'transparent' }" :style="{ background: c === 'transparent' ? '#fff' : c }" :title="c === 'transparent' ? (ctx?.t ? ctx.t('Remove') : 'Remove') : ''" @mousedown.prevent="setHighlightColor(c)"></div>
</div>
</div>
</div>
</div>
<!-- Block Handle -->
@ -713,4 +897,66 @@ function icon(name: string): string {
.blockeditor-slash-info { flex: 1; min-width: 0; }
.blockeditor-slash-label { font-size: 13px; font-weight: 500; color: #111827; line-height: 1.3; }
.blockeditor-slash-desc { font-size: 11px; color: #9ca3af; line-height: 1.2; }
/* ===== Toolbar Dropdown ===== */
.be-tb-dropdown { position: relative; display: inline-flex; }
.be-tb-dropdown-menu {
position: absolute;
top: 100%;
left: 0;
min-width: 140px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 6px 20px rgba(0,0,0,0.12);
padding: 4px;
z-index: 40;
margin-top: 4px;
}
.be-tb-dropdown-menu-right { left: auto; right: 0; }
.be-tb-dropdown-item {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
font-size: 13px;
cursor: pointer;
border-radius: 4px;
white-space: nowrap;
color: #1f2937;
transition: background 0.1s;
}
.be-tb-dropdown-item:hover { background: #f3f4f6; }
.be-tb-dropdown-item svg { width: 16px; height: 16px; flex-shrink: 0; }
.be-tb-dropdown-divider { height: 1px; background: #e5e7eb; margin: 4px 0; }
/* ===== Color Grid ===== */
.be-tb-color-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 4px;
padding: 8px;
}
.be-tb-color-swatch {
width: 24px;
height: 24px;
border-radius: 4px;
cursor: pointer;
border: 1px solid #d1d5db;
transition: transform 0.1s, border-color 0.1s;
box-sizing: border-box;
}
.be-tb-color-swatch:hover {
border-color: #2383e2;
transform: scale(1.15);
}
.be-tb-color-swatch-transparent {
position: relative;
overflow: hidden;
}
.be-tb-color-swatch-transparent::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, #fff 45%, #ef4444 45%, #ef4444 55%, #fff 55%);
}
</style>

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref, watch, computed, useSlots } from 'vue'
import { t } from '@/shared/i18n'
import { Editor, Extension, Node } from '@tiptap/core'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
@ -287,7 +288,7 @@ function showLinkPanel(toolbar: HTMLElement, e: MouseEvent, ed: Editor) {
input.placeholder = 'https://...'
input.style.width = '240px'
const apply = document.createElement('button')
apply.textContent = '应用'
apply.textContent = t('Apply')
apply.className = 'tiptap-toolbar-btn'
apply.onmousedown = (ev) => { ev.preventDefault(); const url = input.value.trim(); if (url) ed.chain().focus().setLink({ href: url }).run(); panel.remove() }
panel.appendChild(input)
@ -394,93 +395,93 @@ function buildToolbar(container: HTMLElement, ed: Editor) {
bar.className = 'tiptap-toolbar'
// H1-H6
bar.appendChild(dropdown('标题', 'format_paragraph', [
{ label: '标题1', onClick: () => ed.chain().focus().toggleHeading({ level: 1 }).run() },
{ label: '标题2', onClick: () => ed.chain().focus().toggleHeading({ level: 2 }).run() },
{ label: '标题3', onClick: () => ed.chain().focus().toggleHeading({ level: 3 }).run() },
{ label: '标题4', onClick: () => ed.chain().focus().toggleHeading({ level: 4 }).run() },
{ label: '标题5', onClick: () => ed.chain().focus().toggleHeading({ level: 5 }).run() },
{ label: '标题6', onClick: () => ed.chain().focus().toggleHeading({ level: 6 }).run() },
{ label: '正文', onClick: () => ed.chain().focus().setParagraph().run() },
bar.appendChild(dropdown(t('Heading'), 'format_paragraph', [
{ label: t('Heading 1'), onClick: () => ed.chain().focus().toggleHeading({ level: 1 }).run() },
{ label: t('Heading 2'), onClick: () => ed.chain().focus().toggleHeading({ level: 2 }).run() },
{ label: t('Heading 3'), onClick: () => ed.chain().focus().toggleHeading({ level: 3 }).run() },
{ label: t('Heading 4'), onClick: () => ed.chain().focus().toggleHeading({ level: 4 }).run() },
{ label: t('Heading 5'), onClick: () => ed.chain().focus().toggleHeading({ level: 5 }).run() },
{ label: t('Heading 6'), onClick: () => ed.chain().focus().toggleHeading({ level: 6 }).run() },
{ label: t('Paragraph'), onClick: () => ed.chain().focus().setParagraph().run() },
]))
// jingrow TextStyle + setMark/unsetMark
bar.appendChild(dropdown('字号', 'format_size', [
bar.appendChild(dropdown(t('Font Size'), 'format_size', [
...[8,10,12,14,16,18,20,24,28,32,36,48,64].map(sz => ({
label: `${sz}px`,
onClick: () => ed.chain().focus().setMark('textStyle', { fontSize: `${sz}px` }).run(),
})),
{ label: '重置', onClick: () => ed.chain().focus().unsetMark('textStyle', { extendEmptyMarkRange: true }).run() }
{ label: t('Reset'), onClick: () => ed.chain().focus().unsetMark('textStyle', { extendEmptyMarkRange: true }).run() }
]))
//
bar.appendChild(divider())
// & jingrow
bar.appendChild(dropdown('表格', 'table', [
{ label: '插入表格', onClick: () => ed.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run() },
{ label: '删除表格', onClick: () => ed.chain().focus().deleteTable().run() },
{ label: '插入行(下)', onClick: () => ed.chain().focus().addRowAfter().run() },
{ label: '插入列(右)', onClick: () => ed.chain().focus().addColumnAfter().run() },
{ label: '删除行', onClick: () => ed.chain().focus().deleteRow().run() },
{ label: '删除列', onClick: () => ed.chain().focus().deleteColumn().run() },
bar.appendChild(dropdown(t('Table'), 'table', [
{ label: t('Insert Table'), onClick: () => ed.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run() },
{ label: t('Delete Table'), onClick: () => ed.chain().focus().deleteTable().run() },
{ label: t('Insert Row Below'), onClick: () => ed.chain().focus().addRowAfter().run() },
{ label: t('Insert Column Right'), onClick: () => ed.chain().focus().addColumnAfter().run() },
{ label: t('Delete Row'), onClick: () => ed.chain().focus().deleteRow().run() },
{ label: t('Delete Column'), onClick: () => ed.chain().focus().deleteColumn().run() },
]))
bar.appendChild(dropdown('对齐', 'format_align_left', [
{ label: '左对齐', onClick: () => ed.chain().focus().setTextAlign('left').run() },
{ label: '居中', onClick: () => ed.chain().focus().setTextAlign('center').run() },
{ label: '右对齐', onClick: () => ed.chain().focus().setTextAlign('right').run() },
bar.appendChild(dropdown(t('Align'), 'format_align_left', [
{ label: t('Align Left'), onClick: () => ed.chain().focus().setTextAlign('left').run() },
{ label: t('Align Center'), onClick: () => ed.chain().focus().setTextAlign('center').run() },
{ label: t('Align Right'), onClick: () => ed.chain().focus().setTextAlign('right').run() },
]))
bar.appendChild(divider())
//
bar.appendChild(button('加粗', 'format_bold', () => ed.chain().focus().toggleBold().run()))
bar.appendChild(button('斜体', 'format_italic', () => ed.chain().focus().toggleItalic().run()))
bar.appendChild(button('下划线', 'format_underlined', () => ed.chain().focus().toggleUnderline().run()))
bar.appendChild(button('删除线', 'strikethrough_s', () => ed.chain().focus().toggleStrike().run()))
bar.appendChild(button('行内代码', 'code', () => ed.chain().focus().toggleCode().run()))
bar.appendChild(button(t('Bold'), 'format_bold', () => ed.chain().focus().toggleBold().run()))
bar.appendChild(button(t('Italic'), 'format_italic', () => ed.chain().focus().toggleItalic().run()))
bar.appendChild(button(t('Underline'), 'format_underlined', () => ed.chain().focus().toggleUnderline().run()))
bar.appendChild(button(t('Strikethrough'), 'strikethrough_s', () => ed.chain().focus().toggleStrike().run()))
bar.appendChild(button(t('Inline Code'), 'code', () => ed.chain().focus().toggleCode().run()))
bar.appendChild(divider())
// //
bar.appendChild(button('代码块', 'data_object', () => ed.chain().focus().toggleCodeBlock().run()))
bar.appendChild(button('引用', 'format_quote', () => ed.chain().focus().toggleBlockquote().run()))
bar.appendChild(button('下标', 'subscript', () => ed.chain().focus().toggleSubscript().run()))
bar.appendChild(button('上标', 'superscript', () => ed.chain().focus().toggleSuperscript().run()))
bar.appendChild(button(t('Code Block'), 'data_object', () => ed.chain().focus().toggleCodeBlock().run()))
bar.appendChild(button(t('Blockquote'), 'format_quote', () => ed.chain().focus().toggleBlockquote().run()))
bar.appendChild(button(t('Subscript'), 'subscript', () => ed.chain().focus().toggleSubscript().run()))
bar.appendChild(button(t('Superscript'), 'superscript', () => ed.chain().focus().toggleSuperscript().run()))
bar.appendChild(divider())
//
bar.appendChild(button('无序列表', 'format_list_bulleted', () => ed.chain().focus().toggleBulletList().run()))
bar.appendChild(button('有序列表', 'format_list_numbered', () => ed.chain().focus().toggleOrderedList().run()))
bar.appendChild(button('任务列表', 'checklist', () => ed.chain().focus().toggleTaskList().run()))
bar.appendChild(button(t('Bullet List'), 'format_list_bulleted', () => ed.chain().focus().toggleBulletList().run()))
bar.appendChild(button(t('Numbered List'), 'format_list_numbered', () => ed.chain().focus().toggleOrderedList().run()))
bar.appendChild(button(t('Task List'), 'checklist', () => ed.chain().focus().toggleTaskList().run()))
bar.appendChild(divider())
// //线
bar.appendChild(button('链接', 'link', () => {}))
bar.appendChild(button(t('Link'), 'link', () => {}))
bar.lastElementChild?.addEventListener('mousedown', (e) => { e.preventDefault(); togglePanel(bar, e as MouseEvent, ed, 'link') })
bar.appendChild(button('图片', 'image', () => {}))
bar.appendChild(button(t('Image'), 'image', () => {}))
bar.lastElementChild?.addEventListener('mousedown', (e) => { e.preventDefault(); togglePanel(bar, e as MouseEvent, ed, 'image') })
bar.appendChild(button('分割线', 'horizontal_rule', () => ed.chain().focus().setHorizontalRule().run()))
bar.appendChild(button(t('Divider'), 'horizontal_rule', () => ed.chain().focus().setHorizontalRule().run()))
bar.appendChild(divider())
//
bar.appendChild(button('文字颜色', 'format_color_text', () => {}))
bar.appendChild(button(t('Text Color'), 'format_color_text', () => {}))
bar.lastElementChild?.addEventListener('mousedown', (e) => { e.preventDefault(); togglePanel(bar, e as MouseEvent, ed, 'color') })
bar.appendChild(button('背景色', 'format_color_fill', () => {}))
bar.appendChild(button(t('Highlight'), 'format_color_fill', () => {}))
bar.lastElementChild?.addEventListener('mousedown', (e) => { e.preventDefault(); togglePanel(bar, e as MouseEvent, ed, 'highlight') })
bar.appendChild(divider())
bar.appendChild(button('撤销', 'undo', () => ed.chain().focus().undo().run()))
bar.appendChild(button('重做', 'redo', () => ed.chain().focus().redo().run()))
bar.appendChild(button(t('Undo'), 'undo', () => ed.chain().focus().undo().run()))
bar.appendChild(button(t('Redo'), 'redo', () => ed.chain().focus().redo().run()))
bar.appendChild(divider())
//
const srcBtn = button('显示源代码', 'code_blocks', () => {
const srcBtn = button(t('Show Source Code'), 'code_blocks', () => {
toggleSourceMode()
})
bar.appendChild(srcBtn)
@ -654,7 +655,7 @@ function mountControl() {
Dropcursor,
Gapcursor,
UndoRedo,
Placeholder.configure({ placeholder: props.df?.label || props.df?.fieldname || '请输入内容...' }),
Placeholder.configure({ placeholder: props.df?.label || props.df?.fieldname || t('Enter content...') }),
TextAlign.configure({ types: ['heading', 'paragraph', 'imageResizable'] }),
Color,
TextStyle,

View File

@ -520,7 +520,6 @@
"Yes": "是",
"No": "否",
"Reset to Default": "恢复默认",
"Confirm": "确定",
"Select Menu Type": "选择菜单类型",
"PageType": "页面类型",
"Route": "路由名",
@ -859,7 +858,6 @@
"Uploaded To Google Drive": "已上传到Google Drive",
"View File": "查看文件",
"Download": "下载",
"Optimize": "优化",
"Package Resources": "扩展包资源",
@ -943,8 +941,6 @@
"Node file URL or repository address does not exist": "节点文件URL或仓库地址不存在",
"Node already exists": "节点已存在",
"Node \"{0}\" is already installed, do you want to overwrite?": "节点 \"{0}\" 已安装,是否覆盖安装?",
"Confirm Overwrite": "确认覆盖",
"Preparing installation...": "正在准备安装...",
"Downloading node package...": "正在下载节点包...",
"Installing node...": "正在安装节点...",
"Cloning repository...": "正在克隆仓库...",
@ -1019,7 +1015,6 @@
"Repository Information": "仓库信息",
"No description available": "暂无描述",
"Timestamps": "时间信息",
"Created": "创建时间",
"Back to Marketplace": "返回应用市场",
"Not specified": "未指定",
"Create a new application for the marketplace": "为应用市场创建新应用",
@ -1028,7 +1023,6 @@
"Please enter application description (optional)": "请输入应用描述(可选)",
"App Image": "应用图片",
"New Application": "新应用",
"Upload Image": "上传图片",
"Brief description": "简要描述",
"App title": "应用标题",
"App name": "应用名称",
@ -1084,8 +1078,6 @@
"Untitled Agent": "未命名智能体",
"Failed to load agents": "加载智能体失败",
"Failed to load agent details": "加载智能体详情失败",
"Agent execution started successfully": "智能体执行已启动",
"Execution failed": "执行失败",
"Please save the agent first": "请先保存智能体",
"Are you sure you want to publish agent \"{0}\" to the marketplace?": "确定要将智能体 \"{0}\" 发布到市场吗?",
"Agent flow data or name is missing": "智能体流程数据或名称不存在",
@ -1099,7 +1091,6 @@
"Agent installed successfully!": "智能体安装成功!",
"Agent installed successfully": "智能体安装成功",
"Agent flow data is missing": "智能体流程数据不存在",
"Installation failed": "安装失败",
"Installing Agent": "正在安装智能体",
"Installing Node": "正在安装节点",
@ -1138,7 +1129,6 @@
"Use route name for internal navigation (recommended)": "使用路由名进行内部导航(推荐)",
"Enter URL path": "请输入URL路径",
"Internal path: /example": "内部路径:/example",
"External link: starts with http:// or https://": "外部链接:以 http:// 或 https:// 开头",
"Please enter tool name": "请输入工具名称",
"Please enter tool title": "请输入工具标题",
"Enter tool title": "请输入工具标题",
@ -1148,7 +1138,6 @@
"Tool updated successfully": "工具更新成功",
"Tool added successfully": "工具添加成功",
"Are you sure you want to delete tool": "确定要删除工具",
"Tool deleted successfully": "工具删除成功",
"Route not found: ": "路由未找到:",
"Remove Background": "图片去背景",
"Remove Background - Free AI Background Removal Tool": "图片去背景 - 免费AI背景移除工具",
@ -1211,52 +1200,17 @@
"Tool Marketplace": "工具市场",
"Browse and install tools from Jingrow Tool Marketplace": "浏览和安装来自 Jingrow 工具市场的工具",
"Search tools...": "搜索工具...",
"Sort by": "排序方式",
"Latest": "最新",
"Oldest": "最旧",
"Name A-Z": "名称 A-Z",
"Name Z-A": "名称 Z-A",
"Most Popular": "最受欢迎",
"Installed": "已安装",
"Loading tools...": "正在加载工具...",
"No tools found": "未找到工具",
"Failed to load tools": "加载工具失败",
"Tool Details": "工具详情",
"Loading tool details...": "正在加载工具详情...",
"Failed to load tool details": "加载工具详情失败",
"Untitled Tool": "未命名工具",
"Tool Name": "工具名称",
"Author": "作者",
"Developer": "开发者",
"Route Name": "路由名称",
"URL": "URL",
"Created": "创建时间",
"Last Updated": "最后更新",
"Click image to try": "点击图片快速体验",
"Remove Background": "去除背景",
"Download": "下载",
"Change Image": "更换图片",
"Upload Image": "上传图片",
"or": "或",
"Paste image URL here": "粘贴图片URL",
"Drag and drop your image anywhere, or paste image directly": "拖放图片到任意位置,或直接粘贴图片",
"Supports JPG, PNG, WebP formats": "支持 JPG、PNG、WebP 格式",
"Loading image from URL...": "加载图片URL中...",
"Original": "原图",
"Background Removed": "去背景后",
"Processing...": "处理中...",
"Add new image": "添加新图片",
"Delete": "删除",
"Unsupported image format. Please use JPG, PNG, or WebP": "不支持的图片格式,请使用 JPG、PNG 或 WebP",
"Image size exceeds 10MB limit": "图片大小超过 10MB 限制",
"Please upload an image first": "请先上传图片",
"Failed to download image": "下载失败",
"Please enter an image URL": "请输入图片URL",
"Please enter a valid image URL": "请输入有效的图片URL",
"Image processing failed, please try again": "图片处理失败,请重试",
"Failed to load sample image": "加载示例图片失败",
"Failed to remove background": "去背景失败",
"Failed to load image from URL": "从URL加载图片失败",
"Sign up successful, but auto login failed. Please login manually": "注册成功,但自动登录失败,请手动登录",
"WeChat": "微信",
"Weibo": "微博",
@ -1275,5 +1229,31 @@
"Online Support": "在线客服",
"Feedback & Suggestions": "反馈建议",
"Guangzhou Sunflower Network Information Technology Co., Ltd.": "广州向日葵网络信息技术有限公司",
"All Rights Reserved": "版权所有"
"All Rights Reserved": "版权所有",
"Heading 1": "标题1",
"Heading 2": "标题2",
"Heading 3": "标题3",
"Heading 4": "标题4",
"Heading 5": "标题5",
"Heading 6": "标题6",
"Paragraph": "正文",
"Italic": "斜体",
"Underline": "下划线",
"Strikethrough": "删除线",
"Subscript": "下标",
"Superscript": "上标",
"Code Block": "代码块",
"Blockquote": "引用",
"Inline Code": "行内代码",
"Bullet List": "无序列表",
"Numbered List": "有序列表",
"Task List": "任务列表",
"Insert Table": "插入表格",
"Delete Table": "删除表格",
"Insert Row Below": "在下方插入行",
"Insert Column Right": "在右侧插入列",
"Delete Row": "删除行",
"Show Source Code": "显示源代码",
"Enter content...": "请输入内容..."
}

View File

@ -22103,3 +22103,78 @@ msgstr "新建对话"
msgid "Part of the document failed to render"
msgstr "部分文档内容渲染失败"
msgid "Heading 1"
msgstr "标题1"
msgid "Heading 2"
msgstr "标题2"
msgid "Heading 3"
msgstr "标题3"
msgid "Heading 4"
msgstr "标题4"
msgid "Heading 5"
msgstr "标题5"
msgid "Heading 6"
msgstr "标题6"
msgid "Paragraph"
msgstr "正文"
msgid "Italic"
msgstr "斜体"
msgid "Underline"
msgstr "下划线"
msgid "Strikethrough"
msgstr "删除线"
msgid "Subscript"
msgstr "下标"
msgid "Superscript"
msgstr "上标"
msgid "Code Block"
msgstr "代码块"
msgid "Blockquote"
msgstr "引用"
msgid "Inline Code"
msgstr "行内代码"
msgid "Bullet List"
msgstr "无序列表"
msgid "Numbered List"
msgstr "有序列表"
msgid "Task List"
msgstr "任务列表"
msgid "Insert Table"
msgstr "插入表格"
msgid "Delete Table"
msgstr "删除表格"
msgid "Insert Row Below"
msgstr "在下方插入行"
msgid "Insert Column Right"
msgstr "在右侧插入列"
msgid "Delete Row"
msgstr "删除行"
msgid "Show Source Code"
msgstr "显示源代码"
msgid "Enter content..."
msgstr "请输入内容..."