Commit fb7284af by pangchong

refactor: 代码优化

parent 8a5c5306
import { IModuleConf, SlateElement } from '@wangeditor/editor'
import { nodeSet } from '@/views/editor/constants/nodeParsed.ts'
import { nodeSet } from '@/configs/node.config'
const elemToHtml = (type: string) => {
return (elem: SlateElement, childrenHtml: string): string => {
......
import { IDomEditor, IModuleConf, SlateDescendant, SlateElement } from '@wangeditor/editor'
import { nodeSet } from '@/views/editor/constants/nodeParsed.ts'
import { nodeSet } from '@/configs/node.config'
const parseElemHtml = (type: string) => {
return (domElem: Element, children: SlateDescendant[], editor: IDomEditor): SlateElement => {
......
import { h, VNode } from 'snabbdom'
import { IDomEditor, IModuleConf, SlateElement } from '@wangeditor/editor'
import { nodeSet } from '@/views/editor/constants/nodeParsed.ts'
import { nodeSet } from '@/configs/node.config'
const renderElem = (type: string, style = { display: 'block' }) => {
return (elem: SlateElement, children: VNode[] | null, editor: IDomEditor): VNode => {
......
// 引入 TreeRenderResult 类型,该类型定义在 @/lib/XMLProcessor/src/typing 模块中
import { NewTreeModification, OldTreeModification, TreeRenderResult, TreeRenderResultFlatted } from '@/lib/XMLProcessor/src/typing'
// 引入 UUID 类用于生成唯一标识符
import { UUID } from 'uuidjs'
import { md5 } from 'js-md5'
import { getUUID } from '@/utils'
// 定义 Processing 类,用于处理 XML 数据
export class Processing {
......@@ -185,7 +184,7 @@ export class Processing {
// 生成唯一的 key
let targetKey = domNode.getAttribute('data-key')
if (!targetKey) {
targetKey = 'g-' + UUID.generate()
targetKey = getUUID()
}
// 创建树形数据项
const treeItem: TreeRenderResult = {
......@@ -227,7 +226,7 @@ export class Processing {
// 生成唯一的 key
let targetKey = node.getAttribute('data-key')
if (!targetKey) {
targetKey = 'g-' + UUID.generate()
targetKey = getUUID()
}
// 创建树形数据项
const treeItem: TreeRenderResult = {
......
import {Processing} from '@/lib/XMLProcessor/src/core/Processing.ts'
import {Plugin, getCurrentInstance} from 'vue'
import TextA from "@/assets/file/CES-QEC-V250-A.xml?raw"
import TextB from "@/assets/file/Trans-Convert.xml?raw"
import {nodeSet} from '@/views/editor/constants/nodeParsed.ts'
import { Processing } from '@/lib/XMLProcessor/src/core/Processing.ts'
import { Plugin, getCurrentInstance } from 'vue'
import TextA from '@/assets/file/CES-QEC-V250-A.xml?raw'
import TextB from '@/assets/file/Trans-Convert.xml?raw'
import { nodeSet } from '@/configs/node.config'
const p = new Processing()
// @ts-ignore
window.$p = function() {
let nodeV = p.dualCompareFromString(
TextA,
TextB,
nodeSet
)
window.$p = function () {
let nodeV = p.dualCompareFromString(TextA, TextB, nodeSet)
console.log('v-h', nodeV)
}
export const XMLProcessing: Plugin = function(app) {
app.provide('xmlProcessing', p);
export const XMLProcessing: Plugin = function (app) {
app.provide('xmlProcessing', p)
}
export function useXMLProcessing(): Processing {
......
import { UUID } from 'uuidjs'
export const getUUID = () => {
return 'g-' + UUID.generate()
}
<template>
<div class="z-10 h-full flex flex-col">
<!-- 工具栏 -->
<div class="flex">
<div class="flex-1">
<Toolbar :editor="editorRef" :editorId="editorId" class="border-b border-solid border-borderColor" />
</div>
<n-button @click="uploadXml" class="h-full">上传XML</n-button>
</div>
<!-- 编辑器 -->
<div class="p-[15px] flex flex-1 overflow-hidden">
<slot name="left"></slot>
<n-card class="flex-1" content-class="h-full !p-0">
<Editor
v-model="valueHtml"
:defaultConfig="editorConfig"
:editorId="editorId"
:style="editorStyle"
@on-change="handleChange"
@on-created="handleCreated"
@click="handleClick"
@contextmenu.prevent="handleContextMenu"
/>
<n-dropdown
placement="bottom-start"
trigger="manual"
:x="dropdownConfig.x"
:y="dropdownConfig.y"
:options="dropdownConfig.options"
:show="dropdownConfig.show"
:on-clickoutside="onClickoutside"
@select="handleSelect"
/>
</n-card>
</div>
</div>
</template>
<script setup lang="ts">
import { IDomEditor, IEditorConfig } from '@wangeditor/editor'
// @ts-ignore
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { isNumber } from 'lodash'
import { Boot, IModuleConf } from '@wangeditor/editor'
import renderElemConf from '../functions/render-elem'
import elemToHtmlConf from '../functions/elem-to-html'
import parseHtmlConf from '../functions/parse-elem-html'
import { uploadXml } from '../functions'
import { ArrowLeft24Filled, ArrowRight24Filled, Book20Filled, TableInsertColumn24Filled } from '@vicons/fluent'
const module: Partial<IModuleConf> = {
renderElems: renderElemConf,
elemsToHtml: elemToHtmlConf,
parseElemsHtml: parseHtmlConf
}
Boot.registerModule(module)
//菜单配置
const toolbarConfig = {
toolbarKeys: [
'headerSelect',
'bold',
'italic',
'underline',
'through',
'color',
'fontSize',
'lineHeight',
'bulletedList',
'numberedList',
'justifyLeft',
'justifyCenter',
'justifyRight',
'undo',
'redo',
'uploadImage',
'insertLink',
'fullScreen',
'clearStyle'
]
}
type InsertFnType = (url: string, alt: string, href: string) => void
const ps = defineProps({
modelValue: {
type: String,
default: ''
},
editorId: {
type: String,
default: 'wangeEditor-1'
},
url: {
type: String,
default: ''
},
height: {
type: [String, Number],
default: '100%'
},
editorConfig: {
type: Object,
default: () => {
return undefined
}
},
readonly: {
type: Boolean,
default: false
}
})
const es = defineEmits(['change', 'created', 'update:modelValue', 'handleClick', 'handleContextSelect'])
// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef<IDomEditor>()
const valueHtml = ref('')
watch(
() => ps.modelValue,
(val: string) => {
if (val === unref(valueHtml)) return
valueHtml.value = val
},
{
immediate: true
}
)
// 监听
watch(
() => valueHtml.value,
(val: string) => {
es('update:modelValue', val)
}
)
const handleCreated = (editor: IDomEditor) => {
editorRef.value = editor
es('created', editor)
}
//点击
const handleClick = (event: any) => {
const key = getKey(event.target)
es('handleClick', key)
}
//获取key
const getKey = (elem: any): string => {
if (!elem) return ''
const key = elem.getAttribute('data-key')
if (key && elem != null) {
return key
} else {
elem = elem.parentElement
return getKey(elem)
}
}
//右键菜单
const dropdownConfig = reactive({
show: false,
x: 0,
y: 0,
options: [
{
label: '左缩进',
key: 'left',
icon() {
return h(NIcon, null, {
default: () => h(ArrowLeft24Filled)
})
}
},
{
label: '右缩进',
key: 'right',
icon() {
return h(NIcon, null, {
default: () => h(ArrowRight24Filled)
})
}
},
{
label: '插入',
key: 'insert',
icon() {
return h(NIcon, null, {
default: () => h(TableInsertColumn24Filled)
})
},
children: [
{
label: 'TaskTitle',
key: 'TaskTitle',
icon() {
return h(NIcon, null, {
default: () => h(Book20Filled)
})
}
},
{
label: 'TopicTitle',
key: 'TopicTitle',
icon() {
return h(NIcon, null, {
default: () => h(Book20Filled)
})
}
}
]
}
]
})
const handleContextMenu = (event: MouseEvent) => {
dropdownConfig.show = false
nextTick(() => {
dropdownConfig.show = true
dropdownConfig.x = event.clientX
dropdownConfig.y = event.clientY
})
}
const handleSelect = (key: string | number) => {
dropdownConfig.show = false
editorRef.value?.restoreSelection()
es('handleContextSelect', key)
}
const onClickoutside = () => {
dropdownConfig.show = false
}
// 编辑器配置
const message = useMessage()
const editorConfig = computed((): IEditorConfig => {
return Object.assign(
{
placeholder: '请输入内容...',
readOnly: ps.readonly,
customAlert: (s: string, t: string) => {
switch (t) {
case 'success':
message.success(s)
break
case 'info':
message.info(s)
break
case 'warning':
message.warning(s)
break
case 'error':
message.error(s)
break
default:
message.info(s)
break
}
},
autoFocus: false,
scroll: true,
MENU_CONF: {
['uploadImage']: {
server: ps.url,
// 单个文件的最大体积限制,默认为 2M
maxFileSize: 5 * 1024 * 1024,
// 最多可上传几个文件,默认为 100
maxNumberOfFiles: 10,
// 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
allowedFileTypes: ['image/*'],
// 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。
meta: { updateSupport: 0 },
// 将 meta 拼接到 url 参数中,默认 false
metaWithUrl: true,
// 自定义增加 http header
headers: {
Accept: '*',
Authorization: `Bearer`
},
// 跨域是否传递 cookie ,默认为 false
withCredentials: true,
// 超时时间,默认为 10 秒
timeout: 5 * 1000, // 5 秒
// form-data fieldName,后端接口参数名称,默认值wangeditor-uploaded-image
fieldName: 'file',
// 上传之前触发
onBeforeUpload(file: File) {
console.log(file)
return file
},
// 上传进度的回调函数
onProgress(progress: number) {
// progress 是 0-100 的数字
console.log('progress', progress)
},
onSuccess(file: File, res: any) {
console.log('onSuccess', file, res)
},
onFailed(file: File, res: any) {
alert(res.message)
console.log('onFailed', file, res)
},
onError(file: File, err: any, res: any) {
alert(err.message)
console.error('onError', file, err, res)
},
// 自定义插入图片
customInsert(res: any, insertFn: InsertFnType) {
insertFn(res.data, 'image', res.data)
}
}
},
uploadImgShowBase64: true
},
ps.editorConfig || {}
)
})
const editorStyle = computed(() => {
return {
height: isNumber(ps.height) ? `${ps.height}px` : ps.height
}
})
// 回调函数
const handleChange = (editor: IDomEditor) => {
es('change', editor)
}
// 组件销毁时,及时销毁编辑器
onBeforeUnmount(() => {
const editor = unref(editorRef.value)
// 销毁,并移除 editor
editor?.destroy()
})
defineExpose({
editorRef
})
</script>
<style src="@wangeditor/editor/dist/css/style.css"></style>
......@@ -19,22 +19,20 @@
</template>
<script lang="ts" setup>
import { realComposableData, searchKey, treeSelectedKeys, treeRef } from '../constants'
import type { TreeOption } from 'naive-ui'
import { nodeProps } from '../functions'
import { treeSelectedKeys, treeRef, searchKey, expandedKeys, treeData } from '../constants/tree'
import { buildFilteredTree, getAllKeys, nodeProps } from '../functions/tree'
const expandedKeys = ref<string[]>([])
function getAllKeys(item: TreeOption[]) {
return item.reduce(function (q, w) {
q.push(w.key as string)
if (w.children) {
q.push(...getAllKeys(w.children))
const realComposableData = computed(function () {
if (!searchKey.value) {
return treeData.value
}
return q
}, [] as string[])
}
// @ts-ignore
const result = buildFilteredTree(treeData.value[0], searchKey.value)
if (!result) {
return []
}
return [result]
})
watch(
realComposableData,
function () {
......
// @ts-nocheck
import type { TreeOption } from 'naive-ui'
import { TreeRenderResult } from '@/lib/XMLProcessor/src/typing'
import { ArrowLeft24Filled, ArrowRight24Filled, Book20Filled, TableInsertColumn24Filled } from '@vicons/fluent'
import { IDomEditor } from '@wangeditor/editor'
export const showLoading = ref(false)
// 菜单相关
export const searchKey = ref('')
export const treeData: Ref<TreeOption[]> = ref([])
export const xmlDOM: Ref<Document> = ref()
export const treeSelectedKeys = ref<string[]>([])
export const xmlContent: Ref<string> = ref('')
//编辑器相关
export const editorRef = ref()
export const formData = reactive({
html: ''
})
export const treeRef = ref()
// 递归函数:检查树中是否存在满足条件的节点
// 递归函数:检查树中是否存在满足条件的节点,并构建新的树结构
function buildFilteredTree(tree: TreeRenderResult, searchString: string): TreeRenderResult | null {
// 如果当前节点的 label 包含指定字符串,直接返回当前节点
if (tree.label.includes(searchString)) {
return { ...tree } // 返回当前节点的副本
}
// 如果当前节点有子节点,递归检查每个子节点
if (tree.children) {
const filteredChildren: TreeRenderResult[] = []
for (let child of tree.children) {
const result = buildFilteredTree(child, searchString)
if (result) {
filteredChildren.push(result) // 如果子节点符合条件,加入到当前节点的子节点列表
//编辑器相关配置
export const editorRef = shallowRef<IDomEditor>()
export const editorHtml = ref('')
//菜单配置
export const toolbarConfig = {
toolbarKeys: [
'headerSelect',
'bold',
'italic',
'underline',
'through',
'color',
'fontSize',
'lineHeight',
'bulletedList',
'numberedList',
'justifyLeft',
'justifyCenter',
'justifyRight',
'undo',
'redo',
'uploadImage',
'insertLink',
'fullScreen',
'clearStyle'
]
}
//右键菜单
export const dropdownConfig = reactive({
show: false,
x: 0,
y: 0,
options: [
{
label: '左缩进',
key: 'left',
icon() {
return h(NIcon, null, {
default: () => h(ArrowLeft24Filled)
})
}
},
{
label: '右缩进',
key: 'right',
icon() {
return h(NIcon, null, {
default: () => h(ArrowRight24Filled)
})
}
// 如果有符合条件的子节点,返回当前节点,并更新子节点列表
if (filteredChildren.length > 0) {
return { ...tree, children: filteredChildren }
},
{
label: '插入',
key: 'insert',
icon() {
return h(NIcon, null, {
default: () => h(TableInsertColumn24Filled)
})
},
children: [
{
label: 'TaskTitle',
key: 'TaskTitle',
icon() {
return h(NIcon, null, {
default: () => h(Book20Filled)
})
}
},
{
label: 'TopicTitle',
key: 'TopicTitle',
icon() {
return h(NIcon, null, {
default: () => h(Book20Filled)
})
}
// 如果当前节点及其子节点都不满足条件,返回 null
return null
}
export const realComposableData = computed(function () {
if (!searchKey.value) {
return treeData.value
}
// @ts-ignore
const result = buildFilteredTree(treeData.value[0], searchKey.value)
if (!result) {
return []
]
}
// @ts-ignore
return [result]
]
})
import { TreeOption } from 'naive-ui'
export const expandedKeys = ref<string[]>([])
export const treeData: Ref<TreeOption[]> = ref([])
export const treeSelectedKeys = ref<string[]>([])
export const treeRef = ref()
export const searchKey = ref('')
import { showLoading } from '../constants'
import { nodeSet } from '@/configs/node.config'
import { IDomEditor } from '@wangeditor/editor'
import { editorRef, formData, showLoading, treeData, treeRef, treeSelectedKeys, xmlContent, xmlDOM } from '../constants'
import { TreeOption } from 'naive-ui'
import { nodeSet } from '../constants/nodeParsed'
import { treeData, treeRef, treeSelectedKeys } from '../constants/tree'
import { dropdownConfig, editorRef, editorHtml } from '../constants'
import { UUID } from 'uuidjs'
import { getUUID } from '@/utils'
export const handleEditor = (editor: IDomEditor) => {
if (editor.getHtml() == '<p><br></p>') return
const xmlProcessing = useXMLProcessing()
const res = xmlProcessing.processXML(editor.getHtml().replace(/<p><br><\/p>/g, ''), nodeSet)
treeData.value = res.treeData
export const handleCreated = (editor: IDomEditor) => {
editorRef.value = editor
handleChange(editor)
}
export const handleClick = (key: string) => {
export const handleClick = (event: any) => {
const key = getKey(event.target)
treeRef.value?.scrollTo({ key })
treeSelectedKeys.value = [key]
setEditorActive(key)
}
export const handleContextSelect = (key: string) => {
if (key == 'TaskTitle') {
const node = { type: 'TaskTitle', dataKey: 'g-' + UUID.generate(), children: [{ text: 'TaskTitle' }] }
editorRef.value?.editorRef?.insertNode(node)
} else if (key == 'TopicTitle') {
const node = { type: 'TopicTitle', dataKey: 'g-' + UUID.generate(), children: [{ text: 'TopicTitle' }] }
editorRef.value?.editorRef?.insertNode(node)
}
}
export const nodeProps = ({ option }: { option: TreeOption }) => {
return {
onClick() {
setEditorActive(option.key as string, (id: string) => {
editorRef.value?.editorRef.scrollToElem([id])
})
}
}
}
let lastFocusedId = ''
export const setEditorActive = (key: string, callBack?: Function) => {
const container = editorRef.value?.editorRef.getEditableContainer()
const id = container.querySelector(`[data-key="${key}"]`)?.getAttribute('id')
const container = editorRef.value?.getEditableContainer()
const id = container!.querySelector(`[data-key="${key}"]`)?.getAttribute('id') as string
if (container) {
if (lastFocusedId) {
container.querySelector(`#${lastFocusedId}`)?.classList.remove('bg-primaryColorHover')
......@@ -51,6 +31,49 @@ export const setEditorActive = (key: string, callBack?: Function) => {
if (callBack) callBack(id)
}
//获取节点的key
export const getKey = (elem: any): string => {
if (!elem) return ''
const key = elem.getAttribute('data-key')
if (key && elem != null) {
return key
} else {
elem = elem.parentElement
return getKey(elem)
}
}
// 右键
export const handleContextMenu = (event: MouseEvent) => {
dropdownConfig.show = false
nextTick(() => {
dropdownConfig.show = true
dropdownConfig.x = event.clientX
dropdownConfig.y = event.clientY
})
}
// 右键选择
export const handleSelect = (key: string | number) => {
dropdownConfig.show = false
editorRef.value?.restoreSelection()
if (key == 'TaskTitle') {
const node = { type: 'TaskTitle', dataKey: getUUID(), children: [{ text: 'TaskTitle' }] }
editorRef.value?.insertNode(node)
} else if (key == 'TopicTitle') {
const node = { type: 'TopicTitle', dataKey: getUUID(), children: [{ text: 'TopicTitle' }] }
editorRef.value?.insertNode(node)
}
}
export const onClickoutside = () => {
dropdownConfig.show = false
}
// 编辑器内容变化
export const handleChange = (editor: IDomEditor) => {
if (editor.getHtml() == '<p><br></p>') return
const xmlProcessing = useXMLProcessing()
const res = xmlProcessing.processXML(editor.getHtml().replace(/<p><br><\/p>/g, ''), nodeSet)
treeData.value = res.treeData
}
// 上传xml
export const uploadXml = async () => {
const input = document.createElement('input')
input.type = 'file'
......@@ -65,7 +88,7 @@ export const uploadXml = async () => {
showLoading.value = true
const res = await xmlProcessing.processFile(file, nodeSet)
showLoading.value = false
formData.html = res.xmlContent
editorHtml.value = res.xmlContent
}
})
}
import { TreeRenderResult } from '@/lib/XMLProcessor/src/typing'
import { setEditorActive } from '.'
import { editorRef } from '../constants'
import { TreeOption } from 'naive-ui'
//点击节点
export const nodeProps = ({ option }: { option: TreeOption }) => {
return {
onClick() {
setEditorActive(option.key as string, (id: string) => {
editorRef.value?.scrollToElem(id)
})
}
}
}
// 递归函数:检查树中是否存在满足条件的节点,并构建新的树结构
export const buildFilteredTree = (tree: TreeRenderResult, searchString: string): TreeRenderResult | null => {
// 如果当前节点的 label 包含指定字符串,直接返回当前节点
if (tree.label.includes(searchString)) {
return { ...tree } // 返回当前节点的副本
}
// 如果当前节点有子节点,递归检查每个子节点
if (tree.children) {
const filteredChildren: TreeRenderResult[] = []
for (let child of tree.children) {
const result = buildFilteredTree(child, searchString)
if (result) {
filteredChildren.push(result) // 如果子节点符合条件,加入到当前节点的子节点列表
}
}
// 如果有符合条件的子节点,返回当前节点,并更新子节点列表
if (filteredChildren.length > 0) {
return { ...tree, children: filteredChildren }
}
}
// 如果当前节点及其子节点都不满足条件,返回 null
return null
}
export const getAllKeys = (item: TreeOption[]) => {
return item.reduce(function (q, w) {
q.push(w.key as string)
if (w.children) {
q.push(...getAllKeys(w.children))
}
return q
}, [] as string[])
}
<template>
<n-spin class="h-full w-full" content-class="h-full w-full" :show="showLoading" description="加载中...">
<ContentEditor
ref="editorRef"
v-model="formData.html"
@change="handleEditor"
@created="handleEditor"
@handleClick="handleClick"
@handleContextSelect="handleContextSelect"
>
<template #left>
<div class="z-10 h-full flex flex-col">
<!-- 工具栏 -->
<div class="flex">
<div class="flex-1">
<Toolbar :editor="editorRef" editorId="wangeEditor-1" class="border-b border-solid border-borderColor" />
</div>
<n-button @click="uploadXml" class="h-full">上传XML</n-button>
</div>
<!-- 编辑器 -->
<div class="p-[15px] flex flex-1 overflow-hidden">
<n-card class="h-full min-w-[300px] max-w-[300px] mr-[15px]" content-class="bg-baseColor !p-0 overflow-hidden">
<ContentTree></ContentTree>
<Tree />
</n-card>
</template>
</ContentEditor>
</n-spin>
<n-card class="flex-1" content-class="h-full !p-0">
<Editor
v-model="editorHtml"
editorId="wangeEditor-1"
:style="{ height: '100%' }"
@on-change="handleChange"
@on-created="handleCreated"
@click="handleClick"
@contextmenu.prevent="handleContextMenu"
/>
<n-dropdown
placement="bottom-start"
trigger="manual"
:x="dropdownConfig.x"
:y="dropdownConfig.y"
:options="dropdownConfig.options"
:show="dropdownConfig.show"
:on-clickoutside="onClickoutside"
@select="handleSelect"
/>
</n-card>
</div>
</div>
</template>
<script setup lang="ts">
import ContentEditor from './components/ContentEditor.vue'
import ContentTree from './components/ContentTree.vue'
import { editorRef, formData, showLoading, xmlContent } from './constants'
import { handleClick, handleEditor, handleContextSelect } from './functions'
// @ts-ignore
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { Boot, IModuleConf } from '@wangeditor/editor'
import renderElemConf from '@/configs/render-elem'
import elemToHtmlConf from '@/configs/elem-to-html'
import parseHtmlConf from '@/configs/parse-elem-html'
import { dropdownConfig, editorRef, editorHtml } from './constants/'
import { handleChange, handleClick, handleContextMenu, handleCreated, handleSelect, onClickoutside, uploadXml } from './functions/'
import Tree from './components/Tree.vue'
onMounted(() => {
// nextTick(() => {
// editorRef.value?.editorRef?.setHtml(xmlContent.value)
// })
const module: Partial<IModuleConf> = {
renderElems: renderElemConf,
elemsToHtml: elemToHtmlConf,
parseElemsHtml: parseHtmlConf
}
Boot.registerModule(module)
// 组件销毁时,及时销毁编辑器
onBeforeUnmount(() => {
const editor = unref(editorRef.value)
// 销毁,并移除 editor
editor?.destroy()
})
</script>
<style lang="less" scoped></style>
<style src="@wangeditor/editor/dist/css/style.css"></style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment