feat: enhance Milkdown editor and file system functionality
- Normalize line endings in Markdown export for DOCX files. - Improve selection serialization to Markdown with better handling of empty documents. - Add a new `updateFile` function to the file system for updating file properties. - Introduce video transcoding capabilities using FFmpeg, supporting various video formats. - Update AGENTS.md for clearer plugin structure and responsibilities. - Add scoped styles for TreeNodeItem component to improve UI consistency. - Implement cross-origin isolation headers in Vite configuration for enhanced security. - Remove obsolete test_cross.py file.
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
<script setup>
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import 'tui-color-picker/dist/tui-color-picker.css'
|
||||
import 'tui-image-editor/dist/tui-image-editor.css'
|
||||
|
||||
const props = defineProps({
|
||||
imageUrl: { type: String, required: true },
|
||||
imageName: { type: String, default: 'image' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['error'])
|
||||
|
||||
const editorHost = ref(null)
|
||||
const isLoading = ref(false)
|
||||
const isReady = ref(false)
|
||||
|
||||
const editorMenus = ['crop', 'flip', 'rotate', 'draw', 'shape', 'icon', 'text', 'filter']
|
||||
const editorTheme = {
|
||||
'common.bi.image': 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==',
|
||||
'common.backgroundColor': '#08111f',
|
||||
'common.border': '0px',
|
||||
'menu.normalLabel.color': '#9fb0c8',
|
||||
'menu.activeLabel.color': '#f8fafc',
|
||||
'submenu.backgroundColor': 'rgba(8, 17, 31, 0.95)',
|
||||
'submenu.partition.color': 'rgba(159, 176, 200, 0.2)',
|
||||
'submenu.normalLabel.color': '#9fb0c8',
|
||||
'submenu.activeLabel.color': '#f8fafc',
|
||||
'checkbox.backgroundColor': '#f8fafc',
|
||||
'range.pointer.color': '#f8fafc',
|
||||
'range.bar.color': 'rgba(159, 176, 200, 0.3)',
|
||||
'range.subbar.color': '#38bdf8',
|
||||
'range.value.color': '#f8fafc',
|
||||
'range.value.backgroundColor': 'rgba(15, 23, 42, 0.88)',
|
||||
'range.value.border': '1px solid rgba(159, 176, 200, 0.22)',
|
||||
'range.title.color': '#cbd5e1',
|
||||
'colorpicker.button.border': '1px solid rgba(159, 176, 200, 0.2)',
|
||||
'colorpicker.title.color': '#e2e8f0'
|
||||
}
|
||||
|
||||
let editorInstance = null
|
||||
let loadRequestId = 0
|
||||
|
||||
function getErrorMessage(error, fallback) {
|
||||
return error instanceof Error && error.message ? error.message : fallback
|
||||
}
|
||||
|
||||
function dataUrlToBlob(dataUrl) {
|
||||
const [header, body = ''] = String(dataUrl || '').split(',')
|
||||
const mimeMatch = header.match(/data:([^;]+);base64/)
|
||||
const mimeType = mimeMatch ? mimeMatch[1] : 'image/png'
|
||||
const binary = atob(body)
|
||||
const bytes = new Uint8Array(binary.length)
|
||||
|
||||
for (let index = 0; index < binary.length; index += 1) {
|
||||
bytes[index] = binary.charCodeAt(index)
|
||||
}
|
||||
|
||||
return new Blob([bytes], { type: mimeType })
|
||||
}
|
||||
|
||||
async function getImageEditorConstructor() {
|
||||
const module = await import('tui-image-editor')
|
||||
return module.default || module
|
||||
}
|
||||
|
||||
async function initializeEditor() {
|
||||
if (!editorHost.value || !props.imageUrl || editorInstance) return
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const ImageEditor = await getImageEditorConstructor()
|
||||
editorInstance = new ImageEditor(editorHost.value, {
|
||||
includeUI: {
|
||||
loadImage: {
|
||||
path: props.imageUrl,
|
||||
name: props.imageName || 'image'
|
||||
},
|
||||
menu: editorMenus,
|
||||
initMenu: 'draw',
|
||||
menuBarPosition: 'bottom',
|
||||
uiSize: {
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
},
|
||||
theme: editorTheme,
|
||||
usageStatistics: false
|
||||
},
|
||||
cssMaxWidth: 1280,
|
||||
cssMaxHeight: 960,
|
||||
selectionStyle: {
|
||||
cornerSize: 16,
|
||||
rotatingPointOffset: 56
|
||||
},
|
||||
usageStatistics: false
|
||||
})
|
||||
|
||||
isReady.value = true
|
||||
} catch (error) {
|
||||
emit('error', getErrorMessage(error, '图片编辑器加载失败'))
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadImageFromProps() {
|
||||
if (!editorInstance || !props.imageUrl) return
|
||||
|
||||
const currentRequestId = ++loadRequestId
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
await editorInstance.loadImageFromURL(props.imageUrl, props.imageName || 'image')
|
||||
editorInstance.clearUndoStack()
|
||||
editorInstance.clearRedoStack()
|
||||
} catch (error) {
|
||||
if (currentRequestId === loadRequestId) {
|
||||
emit('error', getErrorMessage(error, '重新载入图片失败'))
|
||||
}
|
||||
} finally {
|
||||
if (currentRequestId === loadRequestId) {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function exportImageBlob(options = {}) {
|
||||
if (!editorInstance) {
|
||||
throw new Error('图片编辑器尚未就绪')
|
||||
}
|
||||
|
||||
editorInstance.discardSelection()
|
||||
const dataUrl = editorInstance.toDataURL({
|
||||
format: options.format || 'png',
|
||||
quality: typeof options.quality === 'number' ? options.quality : 0.92
|
||||
})
|
||||
|
||||
return dataUrlToBlob(dataUrl)
|
||||
}
|
||||
|
||||
function destroyEditor() {
|
||||
loadRequestId += 1
|
||||
isReady.value = false
|
||||
|
||||
if (!editorInstance) return
|
||||
|
||||
editorInstance.destroy()
|
||||
editorInstance = null
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.imageUrl,
|
||||
(nextUrl, previousUrl) => {
|
||||
if (!editorInstance || !nextUrl || nextUrl === previousUrl) return
|
||||
reloadImageFromProps()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
destroyEditor()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
exportImageBlob,
|
||||
reloadImageFromProps,
|
||||
isReady
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="image-editor-shell">
|
||||
<div v-if="isLoading && !isReady" class="editor-placeholder">
|
||||
<div class="editor-placeholder-card">
|
||||
<strong>正在装载图片编辑器</strong>
|
||||
<p>首次打开会初始化 TUI Image Editor,时间会略长一些。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="editorHost"
|
||||
class="image-editor-host tui-image-editor-container bottom"
|
||||
:class="{ 'is-initializing': isLoading && !isReady }"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.image-editor-shell {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 640px;
|
||||
height: min(78vh, 920px);
|
||||
border: 1px solid var(--github-border);
|
||||
border-radius: 18px;
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(56, 189, 248, 0.14), transparent 32%),
|
||||
linear-gradient(180deg, #08111f, #0f172a 58%, #121a2c);
|
||||
box-shadow: 0 28px 60px rgba(15, 23, 42, 0.22);
|
||||
}
|
||||
|
||||
.image-editor-host.tui-image-editor-container {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.image-editor-host.is-initializing {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.editor-placeholder {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.editor-placeholder-card {
|
||||
width: min(420px, 100%);
|
||||
padding: 22px 24px;
|
||||
border: 1px solid rgba(159, 176, 200, 0.18);
|
||||
border-radius: 18px;
|
||||
background: rgba(8, 17, 31, 0.82);
|
||||
color: #f8fafc;
|
||||
text-align: center;
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.editor-placeholder-card strong {
|
||||
display: block;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.editor-placeholder-card p {
|
||||
margin: 10px 0 0;
|
||||
color: rgba(226, 232, 240, 0.84);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.image-editor-shell :deep(.tui-image-editor-container) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.image-editor-shell :deep(.tui-image-editor-header) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.image-editor-shell :deep(.tui-image-editor-main-container) {
|
||||
top: 0;
|
||||
bottom: 78px;
|
||||
}
|
||||
|
||||
.image-editor-shell :deep(.tui-image-editor-main) {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.image-editor-shell :deep(.tui-image-editor-wrap) {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.image-editor-shell :deep(.tui-image-editor-controls) {
|
||||
height: 78px;
|
||||
border-top: 1px solid rgba(159, 176, 200, 0.12);
|
||||
background: rgba(8, 17, 31, 0.9);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.image-editor-shell :deep(.tui-image-editor-menu > .tui-image-editor-item) {
|
||||
margin: 0 6px;
|
||||
padding: 10px 10px 4px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.image-editor-shell :deep(.tui-image-editor-menu > .tui-image-editor-item.active) {
|
||||
background: rgba(248, 250, 252, 0.14);
|
||||
}
|
||||
|
||||
.image-editor-shell :deep(.tui-image-editor-submenu) {
|
||||
height: 166px;
|
||||
}
|
||||
|
||||
.image-editor-shell :deep(.tui-image-editor-submenu > div) {
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
.image-editor-shell :deep(.tui-image-editor-canvas-container) {
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.image-editor-shell {
|
||||
min-height: 560px;
|
||||
height: 72vh;
|
||||
}
|
||||
|
||||
.image-editor-shell :deep(.tui-image-editor-main-container) {
|
||||
bottom: 88px;
|
||||
}
|
||||
|
||||
.image-editor-shell :deep(.tui-image-editor-controls) {
|
||||
height: 88px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.image-editor-shell :deep(.tui-image-editor-menu) {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user