提交
This commit is contained in:
1 parent
d2290eb92c
commit
a2d4d3b0d5
320 files changed
+125011
-26951
No files matched your search
@@ -0,0 +1,316 @@
|
||||
<template>
|
||||
<div class="tinymce-editor-container">
|
||||
<div v-if="showPrint" class="tinymce-editor-extra-bar">
|
||||
<el-button size="small" @click="handlePrint">
|
||||
<el-icon><Printer /></el-icon>
|
||||
打印
|
||||
</el-button>
|
||||
</div>
|
||||
<Editor
|
||||
:id="editorId"
|
||||
v-model="contentValue"
|
||||
:init="editorInit"
|
||||
:disabled="disabled"
|
||||
tinymce-script-src="/tinymce/tinymce.min.js"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import Editor from '@tinymce/tinymce-vue'
|
||||
import { Printer } from '@element-plus/icons-vue'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import {
|
||||
printSopHtml,
|
||||
setupSopEditorInteractions,
|
||||
SOP_EDITOR_CONTENT_STYLE,
|
||||
getSopA4EditorHeightPx,
|
||||
resolveSopPageCount
|
||||
} from '@/utils/sop-print'
|
||||
|
||||
defineOptions({ name: 'TinyMceEditor' })
|
||||
|
||||
interface Props {
|
||||
modelValue?: string
|
||||
disabled?: boolean
|
||||
height?: number
|
||||
toolbar?: boolean
|
||||
placeholder?: string
|
||||
/** 附件上传 folder,与门户附件接口一致 */
|
||||
uploadFolder?: string
|
||||
/** 是否显示 SOP 打印按钮 */
|
||||
showPrint?: boolean
|
||||
/** 打印页标题 */
|
||||
printTitle?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: '',
|
||||
disabled: false,
|
||||
height: 400,
|
||||
toolbar: true,
|
||||
placeholder: '请输入内容...',
|
||||
uploadFolder: 'production/process-sop',
|
||||
showPrint: false,
|
||||
printTitle: 'SOP'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
}>()
|
||||
|
||||
const TINYMCE_Z_INDEX_FLOOR = 10000
|
||||
|
||||
const resolveTinyMceBaseZIndex = (): number => {
|
||||
let maxZ = 3000
|
||||
document.querySelectorAll('.el-overlay, .el-dialog, .el-popper').forEach((el) => {
|
||||
const z = Number.parseInt(window.getComputedStyle(el).zIndex || '0', 10)
|
||||
if (!Number.isNaN(z) && z > maxZ) maxZ = z
|
||||
})
|
||||
return Math.max(maxZ + 200, TINYMCE_Z_INDEX_FLOOR)
|
||||
}
|
||||
|
||||
const syncTinyMceFloatingZIndex = (baseZ: number) => {
|
||||
const z = String(baseZ)
|
||||
const backdropZ = String(baseZ - 1)
|
||||
document.querySelectorAll('.tox-tinymce-aux').forEach((el) => {
|
||||
;(el as HTMLElement).style.zIndex = z
|
||||
})
|
||||
document.querySelectorAll('.tox-dialog-wrap').forEach((el) => {
|
||||
;(el as HTMLElement).style.zIndex = z
|
||||
})
|
||||
document.querySelectorAll('.tox-dialog').forEach((el) => {
|
||||
;(el as HTMLElement).style.zIndex = String(baseZ + 1)
|
||||
})
|
||||
document.querySelectorAll('.tox-dialog__backdrop').forEach((el) => {
|
||||
;(el as HTMLElement).style.zIndex = backdropZ
|
||||
})
|
||||
document.querySelectorAll('.tox-menu, .tox-pop, .tox-toolbar__overflow, .tox-collection').forEach(
|
||||
(el) => {
|
||||
;(el as HTMLElement).style.zIndex = String(baseZ + 2)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const editorId = `tinymce-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
||||
const contentValue = ref(props.modelValue)
|
||||
const editorInstance = ref<{
|
||||
getContent: (args?: { format?: string }) => string
|
||||
} | null>(null)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal !== contentValue.value) {
|
||||
contentValue.value = newVal
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(contentValue, (newVal) => {
|
||||
emit('update:modelValue', newVal)
|
||||
})
|
||||
|
||||
async function handlePrint() {
|
||||
// 从编辑器实时取 HTML,避免勾选态只在 iframe DOM 中、v-model 未同步
|
||||
const liveHtml = editorInstance.value?.getContent() || contentValue.value || props.modelValue || ''
|
||||
if (editorInstance.value) {
|
||||
contentValue.value = liveHtml
|
||||
}
|
||||
await printSopHtml(liveHtml, props.printTitle)
|
||||
}
|
||||
|
||||
const useSopA4Layout = computed(() => props.showPrint)
|
||||
|
||||
const sopPageCount = computed(() =>
|
||||
useSopA4Layout.value
|
||||
? resolveSopPageCount(contentValue.value || props.modelValue || '')
|
||||
: 1
|
||||
)
|
||||
|
||||
const editorInit = computed(() => {
|
||||
const baseZIndex = resolveTinyMceBaseZIndex()
|
||||
const sopA4 = useSopA4Layout.value
|
||||
|
||||
return {
|
||||
skin_url: '/tinymce/skins/ui/oxide',
|
||||
language: 'zh_CN',
|
||||
language_url: '/tinymce/langs/zh_CN.js',
|
||||
height: sopA4 ? getSopA4EditorHeightPx(sopPageCount.value) : props.height,
|
||||
resize: !sopA4,
|
||||
menubar: false,
|
||||
statusbar: false,
|
||||
readonly: props.disabled,
|
||||
placeholder: props.placeholder,
|
||||
plugins: props.toolbar
|
||||
? [
|
||||
'advlist',
|
||||
'autolink',
|
||||
'lists',
|
||||
'link',
|
||||
'image',
|
||||
'charmap',
|
||||
'searchreplace',
|
||||
'visualblocks',
|
||||
'code',
|
||||
'fullscreen',
|
||||
'insertdatetime',
|
||||
'table',
|
||||
'wordcount'
|
||||
]
|
||||
: [],
|
||||
toolbar: props.toolbar
|
||||
? 'undo redo | bold italic underline | alignleft aligncenter alignright | bullist numlist | table | link image | forecolor backcolor | removeformat | code fullscreen'
|
||||
: false,
|
||||
content_style: sopA4
|
||||
? SOP_EDITOR_CONTENT_STYLE
|
||||
: `
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
|
||||
'Microsoft YaHei', sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #334155;
|
||||
padding: 16px;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
table td,
|
||||
table th {
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 8px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
table th {
|
||||
background-color: #f8fafc;
|
||||
font-weight: 600;
|
||||
}
|
||||
`,
|
||||
extended_valid_elements: '*[*]',
|
||||
valid_elements: '*[*]',
|
||||
table_toolbar:
|
||||
'tableprops tablecellprops tabledelete | tableinsertrowbefore tableinsertrowafter tabledeleterow | tableinsertcolbefore tableinsertcolafter tabledeletecol',
|
||||
table_appearance_options: true,
|
||||
table_grid: true,
|
||||
table_resize_bars: true,
|
||||
branding: false,
|
||||
promotion: false,
|
||||
base_z_index: baseZIndex,
|
||||
images_upload_handler: (blobInfo: { blob: () => Blob; filename: () => string }, progress: (n: number) => void) => {
|
||||
return new Promise<string>(async (resolve, reject) => {
|
||||
try {
|
||||
progress(10)
|
||||
const file = new File([blobInfo.blob()], blobInfo.filename(), {
|
||||
type: blobInfo.blob().type || 'image/jpeg'
|
||||
})
|
||||
const formData = new FormData()
|
||||
formData.append('folder', props.uploadFolder)
|
||||
formData.append('file', file)
|
||||
progress(40)
|
||||
const res = await fetchUploadAttachment(formData)
|
||||
const fileUrl = res?.fileUrl
|
||||
if (!fileUrl) {
|
||||
reject('上传失败')
|
||||
return
|
||||
}
|
||||
progress(100)
|
||||
resolve(fileUrl)
|
||||
} catch (error: unknown) {
|
||||
const err = error as { message?: string }
|
||||
reject(err?.message || '上传失败')
|
||||
}
|
||||
})
|
||||
},
|
||||
paste_data_images: true,
|
||||
automatic_uploads: true,
|
||||
file_picker_types: 'image',
|
||||
images_file_types: 'jpg,jpeg,png,gif,bmp,webp',
|
||||
image_advtab: true,
|
||||
setup: (editor: {
|
||||
on: (name: string, fn: ((e?: Event) => void) | (() => void)) => void
|
||||
getContainer: () => HTMLElement
|
||||
getBody: () => HTMLElement
|
||||
getContent: (args?: { format?: string }) => string
|
||||
undoManager: { transact: (fn: () => void) => void }
|
||||
nodeChanged: () => void
|
||||
fire: (name: string) => void
|
||||
setDirty?: (state: boolean) => void
|
||||
}) => {
|
||||
const refreshFloatingLayers = () => {
|
||||
const z = resolveTinyMceBaseZIndex()
|
||||
syncTinyMceFloatingZIndex(z)
|
||||
}
|
||||
|
||||
editor.on('init', refreshFloatingLayers)
|
||||
editor.on('OpenWindow', refreshFloatingLayers)
|
||||
editor.on('CloseWindow', refreshFloatingLayers)
|
||||
editor.on('ShowMenu', refreshFloatingLayers)
|
||||
editor.on('focus', refreshFloatingLayers)
|
||||
|
||||
editor.on('init', () => {
|
||||
editorInstance.value = editor as { getContent: (args?: { format?: string }) => string }
|
||||
if (props.disabled) {
|
||||
const toolbar = editor.getContainer().querySelector('.tox-toolbar')
|
||||
if (toolbar) {
|
||||
toolbar.style.display = 'none'
|
||||
}
|
||||
}
|
||||
if (sopA4) {
|
||||
setupSopEditorInteractions(editor)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tinymce-editor-container {
|
||||
:deep(.tox-tinymce) {
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:deep(.tox-editor-header) {
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
}
|
||||
|
||||
.tinymce-editor-extra-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.tox-tinymce-aux {
|
||||
z-index: 10000 !important;
|
||||
}
|
||||
|
||||
.tox-dialog-wrap {
|
||||
z-index: 10000 !important;
|
||||
}
|
||||
|
||||
.tox-dialog {
|
||||
z-index: 10001 !important;
|
||||
}
|
||||
|
||||
.tox-dialog__backdrop {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
.tox-menu,
|
||||
.tox-pop,
|
||||
.tox-toolbar__overflow,
|
||||
.tox-collection,
|
||||
.tox-swatches,
|
||||
.tox-selector {
|
||||
z-index: 10002 !important;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user