优化Jeditor
This commit is contained in:
parent
e382297495
commit
ada36b3cf0
@ -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>
|
||||
@ -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,
|
||||
|
||||
@ -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...": "请输入内容..."
|
||||
}
|
||||
|
||||
@ -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 "请输入内容..."
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user