修改编辑器和树的显示
This commit is contained in:
parent
394cb3cc18
commit
bbcb734f72
|
|
@ -1,5 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||||
|
import { asBlob } from 'html-docx-js-typescript'
|
||||||
|
import { saveAs } from 'file-saver'
|
||||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||||
import StarterKit from '@tiptap/starter-kit'
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
// Extensions used in content.vue
|
// Extensions used in content.vue
|
||||||
|
|
@ -275,6 +277,296 @@ const convertTableToSQL = () => {
|
||||||
sqlContent.value = sqlText
|
sqlContent.value = sqlText
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 导出 Word 功能 ==========
|
||||||
|
const exportToWord = async () => {
|
||||||
|
if (!editor.value) return
|
||||||
|
|
||||||
|
const htmlContent = editor.value.getHTML()
|
||||||
|
|
||||||
|
// 定义 Word 导出专用样式 (High Fidelity for Word)
|
||||||
|
// 使用 pt 单位,显式字体,兼容 Word 渲染机制
|
||||||
|
const exportCSS = `
|
||||||
|
body {
|
||||||
|
font-family: 'SimSun', 'Times New Roman', serif;
|
||||||
|
font-size: 10.5pt; /* 五号字 */
|
||||||
|
line-height: 1.5; /* 1.5倍行距 */
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: 'SimHei', 'Arial', sans-serif; /* 标题用黑体 */
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 12pt;
|
||||||
|
margin-bottom: 6pt;
|
||||||
|
}
|
||||||
|
h1 { font-size: 16pt; } /* 三号 */
|
||||||
|
h2 { font-size: 15pt; } /* 小三 */
|
||||||
|
h3 { font-size: 14pt; } /* 四号 */
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 6pt 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
/* width: 100%; REMOVED: Allow absolute pixel widths */
|
||||||
|
table-layout: fixed; /* 强制固定布局,防止 Word 自动缩列宽 */
|
||||||
|
word-wrap: break-word; /* 防止长单词撑开 */
|
||||||
|
margin-bottom: 12pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #000000; /* 纯黑边框 */
|
||||||
|
padding: 5pt;
|
||||||
|
vertical-align: top;
|
||||||
|
font-size: 10.5pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul, ol {
|
||||||
|
margin: 0 0 6pt 0;
|
||||||
|
padding-left: 18pt;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
// 构建完整的 HTML 文档
|
||||||
|
const fullHtml = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
${exportCSS}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<body>
|
||||||
|
${(() => {
|
||||||
|
// 使用 DOMParser 处理 HTML,比正则更稳健
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const doc = parser.parseFromString(htmlContent, 'text/html')
|
||||||
|
|
||||||
|
// 1. 处理空段落
|
||||||
|
doc.querySelectorAll('p').forEach(p => {
|
||||||
|
if (p.textContent?.trim() === '' && p.children.length === 0) {
|
||||||
|
p.innerHTML = ' '
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. 字体单位转换 (pt -> px for export lib)
|
||||||
|
// Input 12pt -> /0.75 -> 16px. Library sees 16px -> converts to 12pt for Word.
|
||||||
|
doc.querySelectorAll('*[style]').forEach(el => {
|
||||||
|
const style = el.getAttribute('style')
|
||||||
|
if (style?.includes('font-size')) {
|
||||||
|
const newStyle = style.replace(/font-size:\s*(\d+(\.\d+)?)pt/gi, (match, val) => {
|
||||||
|
const px = parseFloat(val) / 0.75
|
||||||
|
return `font-size: ${px}px`
|
||||||
|
})
|
||||||
|
el.setAttribute('style', newStyle)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. 图片缩放 (Scaling Factor 0.75)
|
||||||
|
// Editor (96dpi) vs Word (72dpi)
|
||||||
|
// 400px in Editor = 4.16in.
|
||||||
|
// 4.16in in Word (72pt/in) = 300pt.
|
||||||
|
// So convert 400 -> 300. Factor = 0.75.
|
||||||
|
doc.querySelectorAll('img').forEach(img => {
|
||||||
|
// 优先取 style 宽度,其次取 width 属性
|
||||||
|
let widthVal = 0
|
||||||
|
// 解析 style 字符串而不是依赖 img.style 对象,有些环境 DOMParser 解析出的 style 对象可能不全
|
||||||
|
const styleStr = img.getAttribute('style') || ''
|
||||||
|
const widthMatch = styleStr.match(/width:\s*(\d+)px/)
|
||||||
|
|
||||||
|
if (widthMatch) {
|
||||||
|
widthVal = parseInt(widthMatch[1])
|
||||||
|
} else if (img.getAttribute('width')) {
|
||||||
|
widthVal = parseInt(img.getAttribute('width') || '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widthVal > 0) {
|
||||||
|
// 用户反馈 0.75 偏小,导致图片只占一半宽度。调整为 1.0 (不缩放),这可能意味着 html-docx-js 或 Word 内部默认 1px = 1px (96dpi)
|
||||||
|
// 或者 1px (editor) = 1pt (word) 的假设在某些情况下更符合视觉预期
|
||||||
|
const scaleFactor = 1.0
|
||||||
|
const scaledWidth = Math.round(widthVal * scaleFactor)
|
||||||
|
img.setAttribute('width', scaledWidth.toString())
|
||||||
|
img.style.width = `${scaledWidth}px`
|
||||||
|
|
||||||
|
// 处理高度,防止变形
|
||||||
|
let heightVal = 0
|
||||||
|
const heightMatch = styleStr.match(/height:\s*(\d+)px/)
|
||||||
|
if (heightMatch) {
|
||||||
|
heightVal = parseInt(heightMatch[1])
|
||||||
|
} else if (img.getAttribute('height')) {
|
||||||
|
const h = img.getAttribute('height') || ''
|
||||||
|
if (h && h !== 'auto') {
|
||||||
|
heightVal = parseInt(h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heightVal > 0) {
|
||||||
|
const scaledHeight = Math.round(heightVal * scaleFactor)
|
||||||
|
img.setAttribute('height', scaledHeight.toString())
|
||||||
|
img.style.height = `${scaledHeight}px`
|
||||||
|
} else {
|
||||||
|
// 如果没有明确高度,设为 auto 让 Word 自动计算
|
||||||
|
img.style.height = 'auto'
|
||||||
|
img.removeAttribute('height')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. 表格宽度修复
|
||||||
|
// Word 导出库通常不支持 style="width: 600px",需要 width="600"
|
||||||
|
doc.querySelectorAll('table').forEach(table => {
|
||||||
|
// 处理 Table
|
||||||
|
let tableWidth = 0
|
||||||
|
const styleStr = table.getAttribute('style') || ''
|
||||||
|
const widthMatch = styleStr.match(/width:\s*(\d+)px/)
|
||||||
|
if (widthMatch) {
|
||||||
|
tableWidth = parseInt(widthMatch[1])
|
||||||
|
}
|
||||||
|
if (tableWidth > 0) {
|
||||||
|
table.setAttribute('width', tableWidth.toString())
|
||||||
|
// 移除 style 中的 width 防止冲突? 不,通常保留没事,但为了保险
|
||||||
|
// table.style.width = ''
|
||||||
|
} else {
|
||||||
|
// 如果没抓到 pixel width,尝试抓百分比或者默认为 600 (A4 content width)?
|
||||||
|
// 用户前面说 "width greatly reduced",说明可能变成 auto 了。
|
||||||
|
// 如果我们 insertTable 只有 style,那必须转 attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 TD/TH
|
||||||
|
table.querySelectorAll('td, th').forEach(cell => {
|
||||||
|
let cellWidth = 0
|
||||||
|
const cellStyle = cell.getAttribute('style') || ''
|
||||||
|
const cellWidthMatch = cellStyle.match(/width:\s*(\d+)px/)
|
||||||
|
if (cellWidthMatch) {
|
||||||
|
cellWidth = parseInt(cellWidthMatch[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tiptap resizable columns use data-colwidth
|
||||||
|
if (cellWidth === 0) {
|
||||||
|
const colwidth = cell.getAttribute('data-colwidth')
|
||||||
|
if (colwidth) {
|
||||||
|
// data-colwidth="100" or "[100]"
|
||||||
|
const match = colwidth.match(/(\d+)/)
|
||||||
|
if (match) cellWidth = parseInt(match[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cellWidth > 0) {
|
||||||
|
cell.setAttribute('width', cellWidth.toString())
|
||||||
|
// IMPORTANT: clean up style width if it exists to avoid conflicts?
|
||||||
|
// actually keeping style width is fine, but attribute width is key for Word table-layout:fixed
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 5. 重新生成 colgroup 以确保 Word 严格遵循列宽
|
||||||
|
// Tiptap 的 colgroup 有时会丢失或不含宽度,我们需要根据第一行的 cell 宽度重建
|
||||||
|
const firstRow = table.querySelector('tr')
|
||||||
|
if (firstRow) {
|
||||||
|
const cells = firstRow.querySelectorAll('td, th')
|
||||||
|
let colgroupHtml = '<colgroup>'
|
||||||
|
let hasExplicitWidth = false
|
||||||
|
|
||||||
|
cells.forEach(c => {
|
||||||
|
const cell = c as HTMLElement
|
||||||
|
let width = 0
|
||||||
|
const colwidth = cell.getAttribute('data-colwidth')
|
||||||
|
if (colwidth && colwidth.match(/(\d+)/)) {
|
||||||
|
width = parseInt(colwidth.match(/(\d+)/)![1])
|
||||||
|
} else {
|
||||||
|
// [NEW] Check for 'colwidth' attribute
|
||||||
|
const rawColwidth = cell.getAttribute('colwidth')
|
||||||
|
if (rawColwidth) {
|
||||||
|
const match = rawColwidth.match(/(\d+)/)
|
||||||
|
if (match) width = parseInt(match[1])
|
||||||
|
} else if (cell.style.width) {
|
||||||
|
width = parseInt(cell.style.width) || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width > 0) {
|
||||||
|
// Word HTML behavior:
|
||||||
|
// width="120" might be interpreted as "120/50 inch" (twips) or similar?
|
||||||
|
// User feedback: "px became too wide". Reverting to 0.75 scaling (pt).
|
||||||
|
// 43px * 0.75 = 32.25pt.
|
||||||
|
const ptWidth = width * 0.75
|
||||||
|
colgroupHtml += `<col width="${ptWidth}pt">`
|
||||||
|
hasExplicitWidth = true
|
||||||
|
} else {
|
||||||
|
colgroupHtml += '<col>'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
colgroupHtml += '</colgroup>'
|
||||||
|
|
||||||
|
// 如果找到了宽度,就插入 colgroup
|
||||||
|
if (hasExplicitWidth) {
|
||||||
|
// 移除旧的 colgroup
|
||||||
|
const oldColgroup = table.querySelector('colgroup')
|
||||||
|
if (oldColgroup) {
|
||||||
|
oldColgroup.remove()
|
||||||
|
}
|
||||||
|
// 插入新的
|
||||||
|
table.insertAdjacentHTML('afterbegin', colgroupHtml)
|
||||||
|
}
|
||||||
|
|
||||||
|
// [NEW] Force apply width to first row cells (TD/TH) because Word/html-docx-js might ignore colgroup
|
||||||
|
// Apply both width attribute (pt) and style width (pt)
|
||||||
|
// Apply both width attribute (pt) and style width (pt)
|
||||||
|
cells.forEach((c) => {
|
||||||
|
const cell = c as HTMLElement
|
||||||
|
let width = 0
|
||||||
|
const colwidth = cell.getAttribute('data-colwidth')
|
||||||
|
if (colwidth && colwidth.match(/(\d+)/)) {
|
||||||
|
width = parseInt(colwidth.match(/(\d+)/)![1])
|
||||||
|
} else {
|
||||||
|
// [NEW] Check for 'colwidth' attribute
|
||||||
|
const rawColwidth = cell.getAttribute('colwidth')
|
||||||
|
if (rawColwidth) {
|
||||||
|
const match = rawColwidth.match(/(\d+)/)
|
||||||
|
if (match) width = parseInt(match[1])
|
||||||
|
} else if (cell.style.width) {
|
||||||
|
width = parseInt(cell.style.width) || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width > 0) {
|
||||||
|
// Reverting to pt (0.75 scaling)
|
||||||
|
const ptWidth = width * 0.75
|
||||||
|
cell.style.width = `${ptWidth}pt`
|
||||||
|
cell.setAttribute('width', `${ptWidth}pt`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return doc.body.innerHTML
|
||||||
|
})()}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
|
||||||
|
try {
|
||||||
|
const buffer = await asBlob(fullHtml, {
|
||||||
|
orientation: 'portrait',
|
||||||
|
margins: { top: 1440, right: 1440, bottom: 1440, left: 1440 } // TWIPS units (1440 = 1 inch)
|
||||||
|
})
|
||||||
|
saveAs(buffer, `export_${new Date().toISOString().slice(0, 10)}.docx`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Export failed:', error)
|
||||||
|
alert('导出失败,请检查控制台错误信息。')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const currentDisplayContent = computed(() => {
|
const currentDisplayContent = computed(() => {
|
||||||
if (displayMode.value === 'json') return jsonContent.value
|
if (displayMode.value === 'json') return jsonContent.value
|
||||||
if (displayMode.value === 'html') return htmlContent.value
|
if (displayMode.value === 'html') return htmlContent.value
|
||||||
|
|
@ -462,16 +754,30 @@ const insertTable = () => {
|
||||||
const inputRows = tableRows.value
|
const inputRows = tableRows.value
|
||||||
|
|
||||||
// 构造 HTML 字符串插入
|
// 构造 HTML 字符串插入
|
||||||
let tableHtml = '<table><thead><tr>'
|
// A4 Content Width: ~600px (15.92cm)
|
||||||
|
// Use explicit pixel widths for table and columns
|
||||||
|
const tableWidth = 600
|
||||||
|
const colWidth = Math.floor(tableWidth / 5) // 120px each
|
||||||
|
|
||||||
|
let tableHtml = `<table style="width: ${tableWidth}px">`
|
||||||
|
|
||||||
|
// Add colgroup for better Tiptap resizing support
|
||||||
|
tableHtml += '<colgroup>'
|
||||||
|
for(let k=0; k<5; k++) {
|
||||||
|
tableHtml += `<col style="width: ${colWidth}px">`
|
||||||
|
}
|
||||||
|
tableHtml += '</colgroup>'
|
||||||
|
|
||||||
|
tableHtml += '<thead><tr>'
|
||||||
fixedHeaders.forEach(header => {
|
fixedHeaders.forEach(header => {
|
||||||
tableHtml += `<th><p>${header}</p></th>`
|
tableHtml += `<th style="width: ${colWidth}px"><p>${header}</p></th>`
|
||||||
})
|
})
|
||||||
tableHtml += '</tr></thead><tbody>'
|
tableHtml += '</tr></thead><tbody>'
|
||||||
|
|
||||||
for (let i = 0; i < inputRows; i++) {
|
for (let i = 0; i < inputRows; i++) {
|
||||||
tableHtml += '<tr>'
|
tableHtml += '<tr>'
|
||||||
for (let j = 0; j < 5; j++) {
|
for (let j = 0; j < 5; j++) {
|
||||||
tableHtml += '<td><p></p></td>'
|
tableHtml += `<td style="width: ${colWidth}px"><p></p></td>`
|
||||||
}
|
}
|
||||||
tableHtml += '</tr>'
|
tableHtml += '</tr>'
|
||||||
}
|
}
|
||||||
|
|
@ -501,6 +807,9 @@ const insertTable = () => {
|
||||||
<button class="action-btn convert-btn" @click="convertTableToSQL">
|
<button class="action-btn convert-btn" @click="convertTableToSQL">
|
||||||
表格转 SQL
|
表格转 SQL
|
||||||
</button>
|
</button>
|
||||||
|
<button class="action-btn export-btn" @click="exportToWord">
|
||||||
|
导出 Word
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 选中节点操作栏 -->
|
<!-- 选中节点操作栏 -->
|
||||||
|
|
@ -547,15 +856,20 @@ const insertTable = () => {
|
||||||
</select>
|
</select>
|
||||||
<select class="toolbar-select" @change="setFontSize" title="字号" style="width: 70px;">
|
<select class="toolbar-select" @change="setFontSize" title="字号" style="width: 70px;">
|
||||||
<option value="" selected>默认</option>
|
<option value="" selected>默认</option>
|
||||||
<option value="12px">12px</option>
|
<option value="9pt">9pt</option>
|
||||||
<option value="14px">14px</option>
|
<option value="10pt">10pt</option>
|
||||||
<option value="16px">16px</option>
|
<option value="10.5pt">10.5pt (五号)</option>
|
||||||
<option value="18px">18px</option>
|
<option value="11pt">11pt</option>
|
||||||
<option value="20px">20px</option>
|
<option value="12pt">12pt (小四)</option>
|
||||||
<option value="24px">24px</option>
|
<option value="14pt">14pt (四号)</option>
|
||||||
<option value="30px">30px</option>
|
<option value="15pt">15pt (小三)</option>
|
||||||
<option value="36px">36px</option>
|
<option value="16pt">16pt (三号)</option>
|
||||||
<option value="48px">48px</option>
|
<option value="18pt">18pt (小二)</option>
|
||||||
|
<option value="22pt">22pt (二号)</option>
|
||||||
|
<option value="24pt">24pt (小一)</option>
|
||||||
|
<option value="26pt">26pt (一号)</option>
|
||||||
|
<option value="36pt">36pt</option>
|
||||||
|
<option value="42pt">42pt</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="toolbar-btn" @click="clearFormat" title="清除格式">
|
<button class="toolbar-btn" @click="clearFormat" title="清除格式">
|
||||||
<Eraser :size="16" />
|
<Eraser :size="16" />
|
||||||
|
|
@ -743,7 +1057,14 @@ const insertTable = () => {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.tiptap-editor {
|
.tiptap-editor {
|
||||||
overflow-y: auto;
|
flex: 1; /* Occupy remaining vertical space */
|
||||||
|
overflow: auto; /* Scroll the "desk" */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center; /* Center the page horizontally */
|
||||||
|
align-items: flex-start; /* Don't stretch page to full height - let it grow naturally */
|
||||||
|
padding: 32px 0; /* Vertical breathing room */
|
||||||
|
background-color: #f0f0f0; /* Desk color */
|
||||||
|
cursor: text;
|
||||||
}
|
}
|
||||||
.button-group {
|
.button-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -817,7 +1138,7 @@ const insertTable = () => {
|
||||||
.editor-container {
|
.editor-container {
|
||||||
border: 1px solid #d0d0d0;
|
border: 1px solid #d0d0d0;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden; /* Allow content to flow, removed hidden */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
@ -828,27 +1149,56 @@ const insertTable = () => {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TipTap 编辑器样式 */
|
/* TipTap 编辑器样式 - A4 Paper Simulation */
|
||||||
:deep(.tiptap) {
|
:deep(.tiptap) {
|
||||||
flex: 1;
|
/* Paper Dimensions (A4) */
|
||||||
padding: 16px;
|
width: 21cm;
|
||||||
|
min-height: 29.7cm;
|
||||||
|
|
||||||
|
/* Paper Appearance */
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
border: 1px solid #d0d0d0;
|
||||||
|
|
||||||
|
/* Layout & Spacing */
|
||||||
|
margin: 0 auto; /* Centered in flex container */
|
||||||
|
padding: 2.54cm; /* 1 inch margins */
|
||||||
|
|
||||||
|
/* Behavior */
|
||||||
outline: none;
|
outline: none;
|
||||||
overflow-y: auto;
|
flex: none; /* Don't stretch */
|
||||||
|
overflow: visible; /* Let content grow */
|
||||||
|
|
||||||
|
/* WYSIWYG Styling - Matching Export CSS */
|
||||||
|
font-family: 'SimSun', 'Times New Roman', serif;
|
||||||
|
font-size: 14px; /* Approx 10.5pt */
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.tiptap p) {
|
:deep(.tiptap p) {
|
||||||
margin: 0 0 12px 0;
|
margin: 0 0 8px 0; /* Approx 6pt */
|
||||||
line-height: 1.6;
|
line-height: 1.5;
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.tiptap p:last-child) {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.tiptap h1) {
|
:deep(.tiptap h1) {
|
||||||
font-size: 28px;
|
font-family: 'SimHei', 'Arial', sans-serif;
|
||||||
|
font-size: 21px; /* Approx 16pt */
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 0 0 16px 0;
|
margin: 16px 0 8px 0; /* Approx 12pt top, 6pt bottom */
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.tiptap h2) {
|
||||||
|
font-family: 'SimHei', 'Arial', sans-serif;
|
||||||
|
font-size: 20px; /* Approx 15pt */
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 16px 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.tiptap h3) {
|
||||||
|
font-family: 'SimHei', 'Arial', sans-serif;
|
||||||
|
font-size: 18.7px; /* Approx 14pt */
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 16px 0 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.tiptap h2) {
|
:deep(.tiptap h2) {
|
||||||
|
|
@ -924,22 +1274,20 @@ const insertTable = () => {
|
||||||
:deep(.tiptap table) {
|
:deep(.tiptap table) {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 12px 0;
|
margin: 12px 0; /* Approx 12pt */
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
/* 强制表格宽度固定,不随内容撑开 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.tiptap table th),
|
:deep(.tiptap table th),
|
||||||
:deep(.tiptap table td) {
|
:deep(.tiptap table td) {
|
||||||
border: 1px solid #d0d0d0;
|
border: 1px solid #000000; /* Black border for fidelity */
|
||||||
padding: 8px 12px;
|
padding: 6.7px 8px; /* Approx 5pt vertical padding */
|
||||||
text-align: left;
|
text-align: left;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
/* 允许长单词换行 */
|
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
/* 允许长单词换行 */
|
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
/* 强制数字/字母换行防止撑开 */
|
font-size: 14px; /* Approx 10.5pt */
|
||||||
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.tiptap table th) {
|
:deep(.tiptap table th) {
|
||||||
|
|
@ -947,6 +1295,21 @@ const insertTable = () => {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Table Column Resizing */
|
||||||
|
:deep(.tiptap .column-resize-handle) {
|
||||||
|
position: absolute;
|
||||||
|
right: -2px;
|
||||||
|
top: 0;
|
||||||
|
bottom: -2px;
|
||||||
|
width: 4px;
|
||||||
|
background-color: #adf;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.tiptap.resize-cursor) {
|
||||||
|
cursor: col-resize;
|
||||||
|
}
|
||||||
|
|
||||||
/* ProseMirror 选中样式 */
|
/* ProseMirror 选中样式 */
|
||||||
:deep(.ProseMirror-selectednode) {
|
:deep(.ProseMirror-selectednode) {
|
||||||
outline: 2px solid #0078d4;
|
outline: 2px solid #0078d4;
|
||||||
|
|
@ -1326,4 +1689,14 @@ const insertTable = () => {
|
||||||
.convert-btn:hover {
|
.convert-btn:hover {
|
||||||
background: #218838 !important;
|
background: #218838 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.export-btn {
|
||||||
|
background: #0078d4 !important;
|
||||||
|
border-color: #0078d4 !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-btn:hover {
|
||||||
|
background: #106ebe !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1323,34 +1323,64 @@ defineExpose({
|
||||||
<span class="tree-col-status">Status</span>
|
<span class="tree-col-status">Status</span>
|
||||||
<span class="tree-col-version">Version</span>
|
<span class="tree-col-version">Version</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tree-nodes">
|
<transition-group name="tree-row" tag="div" class="tree-nodes">
|
||||||
<div v-for="{ node, level } in flattenedNodes" :key="node.id"
|
<div
|
||||||
:class="['tree-node-wrapper', { 'has-children': hasChildren(node) }]">
|
v-for="{ node, level } in flattenedNodes"
|
||||||
<div :class="['tree-node', { selected: isSelected(node.id) }]" :style="{ paddingLeft: `${level * 16 + 4}px` }"
|
:key="node.id"
|
||||||
@click="handleSelect(node.id, node)" @dblclick="handleDoubleClick(node.id, node)"
|
:class="['tree-node-wrapper', { 'has-children': hasChildren(node) }]"
|
||||||
@contextmenu="handleContextMenu($event, node.id)">
|
>
|
||||||
<span class="tree-node-toggle" @click.stop="handleToggle(node.id)">
|
<div
|
||||||
<Minus v-if="hasChildren(node) && isExpanded(node.id)" :size="12" />
|
:class="['tree-node', { selected: isSelected(node.id) }]"
|
||||||
<Plus v-else-if="hasChildren(node)" :size="12" />
|
@click="handleSelect(node.id, node)"
|
||||||
<span v-else class="tree-node-spacer" />
|
@dblclick="handleDoubleClick(node.id, node)"
|
||||||
</span>
|
@contextmenu="handleContextMenu($event, node.id)"
|
||||||
<span class="tree-node-icon">
|
>
|
||||||
<Link v-if="isMappedNode(node)" :size="14" class="icon-mapped" />
|
<div
|
||||||
<FileText v-else-if="isTerminalNode(node)" :size="14" class="icon-terminal" />
|
class="tree-node-main"
|
||||||
<FolderOpen v-else-if="isExpanded(node.id)" :size="14" class="icon-folder-open" />
|
:style="{ paddingLeft: `${level * 16 + 4}px` }"
|
||||||
<Folder v-else :size="14" class="icon-folder" />
|
>
|
||||||
</span>
|
<span class="tree-node-toggle" @click.stop="handleToggle(node.id)">
|
||||||
<span class="tree-node-label" :title="node.label">
|
<Minus v-if="hasChildren(node) && isExpanded(node.id)" :size="12" />
|
||||||
{{ node.label }}
|
<Plus v-else-if="hasChildren(node)" :size="12" />
|
||||||
<span v-if="node.department" class="dept-badge">[{{ node.department }}]</span>
|
<span v-else class="tree-node-spacer" />
|
||||||
<span v-if="node.label === 'Inbox' && inboxTotalCount > 0" class="inbox-count">({{ inboxTotalCount }})</span>
|
</span>
|
||||||
<span v-if="node.label === 'Map' && mappedCount > 0" class="inbox-count">({{ mappedCount }})</span>
|
<span class="tree-node-icon">
|
||||||
<span v-if="node.label === 'Review' && pendingReviewCount > 0" class="inbox-count">({{ pendingReviewCount }})</span>
|
<Link v-if="isMappedNode(node)" :size="14" class="icon-mapped" />
|
||||||
<span v-if="node.isClone" class="clone-badge">(Clone)</span>
|
<FileText v-else-if="isTerminalNode(node)" :size="14" class="icon-terminal" />
|
||||||
</span>
|
<FolderOpen v-else-if="isExpanded(node.id)" :size="14" class="icon-folder-open" />
|
||||||
|
<Folder v-else :size="14" class="icon-folder" />
|
||||||
|
</span>
|
||||||
|
<span class="tree-node-label" :title="node.label">
|
||||||
|
{{ node.label }}
|
||||||
|
<span v-if="node.department" class="dept-badge">[{{ node.department }}]</span>
|
||||||
|
<span v-if="node.label === 'Inbox' && inboxTotalCount > 0" class="inbox-count">
|
||||||
|
({{ inboxTotalCount }})
|
||||||
|
</span>
|
||||||
|
<span v-if="node.label === 'Map' && mappedCount > 0" class="inbox-count">
|
||||||
|
({{ mappedCount }})
|
||||||
|
</span>
|
||||||
|
<span v-if="node.label === 'Review' && pendingReviewCount > 0" class="inbox-count">
|
||||||
|
({{ pendingReviewCount }})
|
||||||
|
</span>
|
||||||
|
<span v-if="node.isClone" class="clone-badge">(Clone)</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="tree-node-status-col">
|
||||||
|
<span
|
||||||
|
v-if="node.status"
|
||||||
|
:class="['status-badge', `status-${(node.status || '').replace(/\\s+/g, '').toLowerCase()}`]"
|
||||||
|
>
|
||||||
|
{{ node.status }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="tree-node-version-col">
|
||||||
|
<span class="version-text">
|
||||||
|
{{ node.version }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右键菜单 -->
|
<!-- 右键菜单 -->
|
||||||
|
|
@ -1539,6 +1569,15 @@ defineExpose({
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tree-node {
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
.tree-node:hover {
|
||||||
|
/* Darken the background on hover for better focus on hovered node */
|
||||||
|
background-color: rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.tree-view {
|
.tree-view {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -1546,6 +1585,12 @@ defineExpose({
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
background: linear-gradient(180deg, #f5f5f8, #e1e1eb);
|
||||||
|
border: 1px solid #b5b5c5;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.8) inset,
|
||||||
|
0 10px 24px rgba(15, 23, 42, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-header {
|
.tree-header {
|
||||||
|
|
@ -1553,8 +1598,11 @@ defineExpose({
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
background: linear-gradient(to bottom, #f5f5f5, #e8e8e8);
|
background: linear-gradient(180deg, #fdfdfd, #e9e9f2);
|
||||||
border-bottom: 1px solid #a0a0a0;
|
border-bottom: 1px solid #b0b0c0;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 0 rgba(255, 255, 255, 0.8) inset,
|
||||||
|
0 1px 0 rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-header-title {
|
.tree-header-title {
|
||||||
|
|
@ -1595,13 +1643,32 @@ defineExpose({
|
||||||
.tree-content {
|
.tree-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
background: radial-gradient(circle at top left, #ffffff 0, #f5f5f8 45%, #ebebf2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-content::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-content::-webkit-scrollbar-track {
|
||||||
|
background: #f0f0f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-content::-webkit-scrollbar-thumb {
|
||||||
|
background: linear-gradient(180deg, #c1c1cf, #9d9db3);
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 2px solid #f0f0f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-content::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: linear-gradient(180deg, #a3a3ba, #7f7fa0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-column-headers {
|
.tree-column-headers {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
background: #e8e8e8;
|
background: linear-gradient(180deg, #f4f4f8, #e3e3ee);
|
||||||
border-bottom: 1px solid #a0a0a0;
|
border-bottom: 1px solid #c0c0d0;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #666;
|
color: #666;
|
||||||
|
|
@ -1633,20 +1700,45 @@ defineExpose({
|
||||||
.tree-node {
|
.tree-node {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 4px 8px;
|
padding: 2px 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.1s;
|
transition:
|
||||||
|
background-color 220ms ease-out,
|
||||||
|
box-shadow 220ms ease-out,
|
||||||
|
color 220ms ease-out;
|
||||||
border-bottom: 1px solid transparent;
|
border-bottom: 1px solid transparent;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
font-size: 12px;
|
||||||
|
will-change: background-color, box-shadow, color;
|
||||||
.tree-node:hover {
|
|
||||||
background: #d0d0d0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-node.selected {
|
.tree-node.selected {
|
||||||
background: #cde8ff;
|
background: linear-gradient(180deg, #c9e1ff, #b3d6ff);
|
||||||
border-bottom-color: #0078d4;
|
border-bottom-color: #0078d4;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(0, 120, 212, 0.25),
|
||||||
|
0 0 0 2px rgba(255, 255, 255, 0.6) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-wrapper:nth-child(odd) .tree-node:not(.selected) {
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-wrapper:nth-child(even) .tree-node:not(.selected) {
|
||||||
|
background: rgba(244, 244, 248, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node:hover {
|
||||||
|
/* 基础 hover,主要由下面更高优先级规则覆盖 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-wrapper:nth-child(odd) .tree-node:hover,
|
||||||
|
.tree-node-wrapper:nth-child(even) .tree-node:hover {
|
||||||
|
background:rgb(226, 234, 246);
|
||||||
|
color: #123a6b;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(0, 120, 212, 0.18),
|
||||||
|
0 0 0 3px rgba(0, 120, 212, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-node-toggle {
|
.tree-node-toggle {
|
||||||
|
|
@ -1658,6 +1750,35 @@ defineExpose({
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #666;
|
color: #666;
|
||||||
|
transition:
|
||||||
|
background-color 160ms ease-out,
|
||||||
|
color 160ms ease-out,
|
||||||
|
transform 220ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node:hover .tree-node-toggle {
|
||||||
|
color: #005a9e;
|
||||||
|
transform: scale(1.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-status-col,
|
||||||
|
.tree-node-version-col {
|
||||||
|
width: 70px;
|
||||||
|
flex: 0 0 70px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-version-col {
|
||||||
|
padding-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-node-spacer,
|
.tree-node-spacer,
|
||||||
|
|
@ -1676,6 +1797,11 @@ defineExpose({
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
|
transition: transform 220ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node:hover .tree-node-icon {
|
||||||
|
transform: scale(1.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-folder,
|
.icon-folder,
|
||||||
|
|
@ -1739,6 +1865,79 @@ defineExpose({
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 52px;
|
||||||
|
padding: 0 6px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 9px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
background: #f3f3f7;
|
||||||
|
color: #444;
|
||||||
|
transition:
|
||||||
|
background-color 220ms ease-out,
|
||||||
|
color 220ms ease-out,
|
||||||
|
box-shadow 220ms ease-out,
|
||||||
|
transform 220ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-work {
|
||||||
|
background: #e6f2ff;
|
||||||
|
color: #004578;
|
||||||
|
border-color: rgba(0, 120, 212, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-released {
|
||||||
|
background: #e9f7ec;
|
||||||
|
color: #0b6a2b;
|
||||||
|
border-color: rgba(15, 123, 52, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-approved {
|
||||||
|
background: #e9f7ec;
|
||||||
|
color: #0b6a2b;
|
||||||
|
border-color: rgba(15, 123, 52, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-rejected {
|
||||||
|
background: #fdecea;
|
||||||
|
color: #a4262c;
|
||||||
|
border-color: rgba(164, 38, 44, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pending {
|
||||||
|
background: #fff8e5;
|
||||||
|
color: #8a5a00;
|
||||||
|
border-color: rgba(138, 90, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-csreleased {
|
||||||
|
background: #efe9ff;
|
||||||
|
color: #4b0082;
|
||||||
|
border-color: rgba(64, 34, 117, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-mapped {
|
||||||
|
background: #e6f4ff;
|
||||||
|
color: #005a9e;
|
||||||
|
border-color: rgba(0, 90, 158, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node:hover .status-badge {
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-text {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #444;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
.inbox-count {
|
.inbox-count {
|
||||||
color: #dc3545;
|
color: #dc3545;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
@ -1761,10 +1960,14 @@ defineExpose({
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #c0c0c0;
|
border: 1px solid #c0c0c0;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.9) inset,
|
||||||
|
0 8px 20px rgba(0, 0, 0, 0.2);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
min-width: 160px;
|
min-width: 160px;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
|
animation: contextMenuIn 140ms ease-out;
|
||||||
|
transform-origin: top left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-group {
|
.menu-group {
|
||||||
|
|
@ -1779,11 +1982,14 @@ defineExpose({
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #333;
|
color: #333;
|
||||||
transition: background 0.15s;
|
transition:
|
||||||
|
background-color 140ms ease-out,
|
||||||
|
transform 120ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item:hover:not(.disabled) {
|
.menu-item:hover:not(.disabled) {
|
||||||
background: #f0f8ff;
|
background: #f0f8ff;
|
||||||
|
transform: translateX(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item.disabled {
|
.menu-item.disabled {
|
||||||
|
|
@ -1814,6 +2020,7 @@ defineExpose({
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
|
animation: dialogFadeIn 160ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.operation-dialog {
|
.operation-dialog {
|
||||||
|
|
@ -1824,6 +2031,7 @@ defineExpose({
|
||||||
max-height: 85vh;
|
max-height: 85vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
animation: dialogScaleIn 160ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-header {
|
.dialog-header {
|
||||||
|
|
@ -2045,6 +2253,52 @@ defineExpose({
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 行进出动效(展开/收起) */
|
||||||
|
.tree-row-enter-active,
|
||||||
|
.tree-row-leave-active {
|
||||||
|
transition:
|
||||||
|
opacity 240ms ease-out,
|
||||||
|
transform 240ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-row-enter-from,
|
||||||
|
.tree-row-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右键菜单与对话框动效 */
|
||||||
|
@keyframes contextMenuIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.96) translateY(-4px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dialogFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dialogScaleIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(6px) scale(0.97);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 添加节点对话框 */
|
/* 添加节点对话框 */
|
||||||
.add-node-dialog {
|
.add-node-dialog {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
|
@ -2054,6 +2308,7 @@ defineExpose({
|
||||||
max-height: 85vh;
|
max-height: 85vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
animation: dialogScaleIn 160ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-group {
|
.input-group {
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ export const CustomVideo = Node.create({
|
||||||
atom: true,
|
atom: true,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
|
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
src: { default: null },
|
src: { default: null },
|
||||||
|
|
@ -78,7 +78,7 @@ export const CustomVideo = Node.create({
|
||||||
controls: { default: true },
|
controls: { default: true },
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
@ -86,11 +86,11 @@ export const CustomVideo = Node.create({
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
renderHTML({ HTMLAttributes }) {
|
||||||
return ['div', { 'data-custom-video': '', ...HTMLAttributes }]
|
return ['div', { 'data-custom-video': '', ...HTMLAttributes }]
|
||||||
},
|
},
|
||||||
|
|
||||||
addNodeView() {
|
addNodeView() {
|
||||||
return VueNodeViewRenderer(VideoPlayerComponent)
|
return VueNodeViewRenderer(VideoPlayerComponent)
|
||||||
},
|
},
|
||||||
|
|
@ -99,7 +99,7 @@ export const CustomVideo = Node.create({
|
||||||
// ========== 自定义图片扩展(支持缩放) ==========
|
// ========== 自定义图片扩展(支持缩放) ==========
|
||||||
export const ResizableImage = Image.extend({
|
export const ResizableImage = Image.extend({
|
||||||
name: 'resizableImage',
|
name: 'resizableImage',
|
||||||
|
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
...this.parent?.(),
|
...this.parent?.(),
|
||||||
|
|
@ -108,9 +108,13 @@ export const ResizableImage = Image.extend({
|
||||||
style: { default: null },
|
style: { default: null },
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addNodeView() {
|
addNodeView() {
|
||||||
return VueNodeViewRenderer(ImageResizeComponent)
|
return VueNodeViewRenderer(ImageResizeComponent)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return ['img', HTMLAttributes]
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue