refactor: dashboard rich text

This commit is contained in:
wangjiahao
2025-05-30 17:41:51 +08:00
parent d4982ef8d1
commit 6de49c87aa
10 changed files with 227 additions and 81 deletions
+8
View File
@@ -13,6 +13,8 @@ declare module '@vue/runtime-core' {
ElAvatar: typeof import('element-plus-secondary/es')['ElAvatar']
ElButton: typeof import('element-plus-secondary/es')['ElButton']
ElCheckbox: typeof import('element-plus-secondary/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus-secondary/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus-secondary/es')['ElCol']
ElContainer: typeof import('element-plus-secondary/es')['ElContainer']
ElDialog: typeof import('element-plus-secondary/es')['ElDialog']
ElDivider: typeof import('element-plus-secondary/es')['ElDivider']
@@ -30,15 +32,21 @@ declare module '@vue/runtime-core' {
ElMenuItem: typeof import('element-plus-secondary/es')['ElMenuItem']
ElOption: typeof import('element-plus-secondary/es')['ElOption']
ElPagination: typeof import('element-plus-secondary/es')['ElPagination']
ElRadio: typeof import('element-plus-secondary/es')['ElRadio']
ElRadioGroup: typeof import('element-plus-secondary/es')['ElRadioGroup']
ElRow: typeof import('element-plus-secondary/es')['ElRow']
ElScrollbar: typeof import('element-plus-secondary/es')['ElScrollbar']
ElSelect: typeof import('element-plus-secondary/es')['ElSelect']
ElSlider: typeof import('element-plus-secondary/es')['ElSlider']
ElStep: typeof import('element-plus-secondary/es')['ElStep']
ElSteps: typeof import('element-plus-secondary/es')['ElSteps']
ElSwitch: typeof import('element-plus-secondary/es')['ElSwitch']
ElTable: typeof import('element-plus-secondary/es')['ElTable']
ElTableColumn: typeof import('element-plus-secondary/es')['ElTableColumn']
ElTabPane: typeof import('element-plus-secondary/es')['ElTabPane']
ElTabs: typeof import('element-plus-secondary/es')['ElTabs']
ElTooltip: typeof import('element-plus-secondary/es')['ElTooltip']
ElUpload: typeof import('element-plus-secondary/es')['ElUpload']
Icon: typeof import('./src/components/icon-custom/src/Icon.vue')['default']
Layout: typeof import('./src/components/layout/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
@@ -7,6 +7,7 @@ export const dashboardStore = defineStore('dashboard', {
tabCollisionActiveId: null,
tabMoveInActiveId: null,
curComponent: null,
curComponentId: null,
canvasStyle: {},
componentData: [],
dashboardInfo: {
@@ -29,6 +30,7 @@ export const dashboardStore = defineStore('dashboard', {
actions: {
setCurComponent(value: any) {
this.curComponent = value
this.curComponentId = value && value.id ? value.id : null
},
setComponentData(value: any) {
this.componentData = value
@@ -5,10 +5,12 @@ import {dashboardStoreWithOut} from '@/stores/dashboard/dashboard'
import {type CanvasCoord, type CanvasItem} from '@/utils/canvas.ts'
import CanvasShape from './CanvasShape.vue'
import {findComponent} from "@/views/dashboard/components/component-list.ts";
import {storeToRefs} from "pinia";
const dashboardStore = dashboardStoreWithOut()
const canvasLocked = ref(false) // Is the canvas movement locked, Default false
const emits = defineEmits(["parentAddItemBox"]);
const {curComponentId, curComponent} = storeToRefs(dashboardStore)
// @ts-ignore
let currentInstance
// Props
@@ -411,7 +413,7 @@ function getNextDragId() {
if (validIds.length === 0) {
return 0;
}
//@ts-ignore
//@ts-ignore
const maxDragId = Math.max(...validIds);
return maxDragId + 1;
}
@@ -420,7 +422,7 @@ function addItem(item: CanvasItem, index: any) {
if (index < 0) {
index = canvasComponentData.value.length
}
item._dragId = getNextDragId()
item._dragId = item.id || getNextDragId()
checkItemPosition(item, {x: item.x, y: item.y})
emptyTargetCell(item)
addItemToPositionBox(item)
@@ -702,15 +704,23 @@ function startResize(e: MouseEvent, point: string, item: CanvasItem, index: numb
infoBox.value.point = point
}
function containerClick() {
// remove current component info
dashboardStore.setCurComponent(null)
}
function containerMouseDown(e: MouseEvent) {
// e.preventDefault();
if (!infoBox.value) {
infoBox.value = {} // Reinitialize
}
infoBox.value.startX = e.pageX
infoBox.value.startY = e.pageY
e.preventDefault()
e.stopPropagation()
// @ts-ignore
if (curComponent.value?.component !== 'SQText') {
e.preventDefault()
e.stopPropagation()
}
}
function getNowPosition(addSizeX: number, addSizeY: number, moveXSize: number, moveYSize: number) {
@@ -959,6 +969,7 @@ function startMove(e: MouseEvent, item: CanvasItem, index: number) {
window.removeEventListener('mouseup', itemMouseUp)
}
// This will prevent click events from being passed to the parent level
window.addEventListener('mouseup', itemMouseUp)
}
@@ -1147,10 +1158,12 @@ defineExpose({
</script>
<template>
<div class="dragAndResize" ref="containerRef" @mousedown="containerMouseDown($event)" @mouseup="endMove()"
<div class="dragAndResize" ref="containerRef" @click="containerClick" @mousedown="containerMouseDown($event)"
@mouseup="endMove()"
@mousemove="moving()">
<template v-if="renderOk">
<CanvasShape v-for="(item, index) in canvasComponentData"
:active="curComponentId === item.id"
:config-item="item"
:draggable="draggable"
:item-index="index"
@@ -10,6 +10,10 @@ const props = defineProps({
type: Object as PropType<CanvasItem>,
required: true
},
active: {
type: Boolean,
default: false
},
itemIndex: {
type: Number,
required: true
@@ -37,18 +41,23 @@ const props = defineProps({
})
const { draggable } = toRefs(props)
const shapeClick = (e: MouseEvent) => {
e.stopPropagation()
e.preventDefault()
}
</script>
<template>
<div :class="{
item: true,
itemActive: active,
itemCursorDefault: configItem.component === 'SQTab',
moveAnimation: moveAnimate,
movingItem: configItem.isPlayer,
canNotDrag: !draggable
}" @mousedown="startMove($event, configItem, itemIndex)" ref="shapeRef">
}" @click="shapeClick" @mousedown="startMove($event, configItem, itemIndex)" ref="shapeRef">
<slot></slot>
<resize-handle
<resize-handle v-if="active"
:start-resize="(event : MouseEvent, point: string) => startResize(event, point, configItem, itemIndex)">
</resize-handle>
</div>
@@ -12,6 +12,7 @@ const COMPONENT_LIST = [
icon: 'icon_graphical',
innerType: 'bar',
locked: false,
editing: false,
x: 1,
y: 1,
sizeX: 36,
@@ -36,6 +37,7 @@ const COMPONENT_LIST = [
}
],
canvasActive: false,
editing: false,
x: 1,
y: 1,
sizeX: 36,
@@ -47,7 +49,8 @@ const COMPONENT_LIST = [
component: 'SQText',
name: 'new text',
locked: false,
propValue: '&nbsp;',
propValue: 'Double click to edit text',
editing: false,
x: 1,
y: 1,
sizeX: 36,
@@ -220,6 +220,7 @@ defineExpose({
position: absolute;
width: 100%;
height: 100%;
margin: 2px!important;// border size
}
.tab-dashboard-editor-main{
@@ -1,60 +0,0 @@
<template>
<editor v-model="myValue" :init="init" :id="tinymceId"></editor>
</template>
<script setup lang="ts">
import tinymce from 'tinymce/tinymce'
import Editor from '@tinymce/tinymce-vue'
import 'tinymce/themes/silver'
import 'tinymce/themes/silver/theme'
import 'tinymce/icons/default'
import 'tinymce/icons/default/icons'
import 'tinymce/plugins/table'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/wordcount'
import 'tinymce/plugins/code'
import '@npkg/tinymce-plugins/letterspacing'
import {reactive, ref} from 'vue'
import {onMounted} from 'vue'
defineProps({
value: {
type: String,
default: () => {
return ''
}
},
baseUrl: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
},
plugins: {
type: [String, Array],
default: 'lists table'
}
})
const myValue = ref('this is a rich text')
const tinymceId = ref('vue-tinymce-1')
const init = reactive({
selector: '#vue-tinymce-1',
language_url: '/tinymce-sqlbot-private/langs/zh_CN.js',
language: 'zh_CN',
skin_url: '/tinymce-sqlbot-private/skins/ui/oxide',
content_css: '/tinymce-sqlbot-private/skins/content/default/content.css',
toolbar: false,
menubar: false,
inline: true,
base_url: '/node_modules/tinymce',
plugins: ['link', 'table'],
quickbars_selection_toolbar: 'bold italic forecolor | link blockquote quickimage',
})
onMounted(() => {
tinymce.init({})
})
</script>
<style scoped lang="less"></style>
@@ -1,11 +1,177 @@
<script setup lang="ts">
import SQRichText from "@/views/dashboard/components/sq-text/SQRichText.vue";
</script>
<template>
<SQRichText></SQRichText>
<div
class="rich-main-class"
:class="{ 'edit-model': configItem.editing }"
@keydown.stop
@keyup.stop
@dblclick="setEdit"
>
<editor :class="{ 'custom-text-content':true,'dragHandle':isDisabled}"
v-model="configItem.propValue"
:disabled="isDisabled" :init="init"
:id="tinymceId"></editor>
</div>
</template>
<style scoped lang="less">
<script setup lang="ts">
import tinymce from 'tinymce/tinymce'
import Editor from '@tinymce/tinymce-vue'
import 'tinymce/themes/silver'
import 'tinymce/themes/silver/theme'
import 'tinymce/icons/default'
</style>
import 'tinymce/plugins/advlist'
import 'tinymce/plugins/autolink'
import 'tinymce/plugins/link'
import 'tinymce/plugins/image'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/charmap'
import 'tinymce/plugins/media'
import 'tinymce/plugins/wordcount'
import 'tinymce/plugins/contextmenu'
import 'tinymce/plugins/directionality'
import 'tinymce/plugins/nonbreaking'
import 'tinymce/plugins/pagebreak'
import '@npkg/tinymce-plugins/letterspacing'
import {computed, nextTick, type PropType, reactive, toRefs} from 'vue'
import {onMounted} from 'vue'
import type {CanvasItem} from "@/utils/canvas.ts";
const props = defineProps({
configItem: {
type: Object as PropType<CanvasItem>,
required: true
},
disabled: {
type: Boolean,
default: false
}
})
const {configItem} = toRefs(props)
const tinymceId = 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
const init = reactive({
selector: tinymceId,
language_url: '/tinymce-sqlbot-private/langs/zh_CN.js',
language: 'zh_CN',
skin_url: '/tinymce-sqlbot-private/skins/ui/oxide',
content_css: '/tinymce-sqlbot-private/skins/content/default/content.css',
plugins:
'advlist autolink link image lists charmap media wordcount contextmenu directionality pagebreak letterspacing', // 插件
// 工具栏
toolbar:
'undo redo | fontselect fontsizeselect |forecolor backcolor bold italic letterspacing |underline strikethrough link lineheight| formatselect |' +
'top-align center-align bottom-align | alignleft aligncenter alignright | bullist numlist |' +
' blockquote subscript superscript removeformat | image ',
toolbar_location: '/',
font_formats:
'微软雅黑=Microsoft YaHei;宋体=SimSun;黑体=SimHei;仿宋=FangSong;华文黑体=STHeiti;华文楷体=STKaiti;华文宋体=STSong;华文仿宋=STFangsong;Andale Mono=andale mono,times;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier;Georgia=georgia,palatino;Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco;Times New Roman=times new roman,times;Trebuchet MS=trebuchet ms,geneva;Verdana=verdana,geneva;Webdings=webdings;Wingdings=wingdings',
fontsize_formats:
'12px 14px 16px 18px 20px 22px 24px 28px 32px 36px 42px 48px 56px 72px 80px 90px 100px 110px 120px 140px 150px 170px 190px 210px', // 字体大小
menubar: false,
placeholder: '',
inline: true,
setup: function (editor : any) {
editor.on('blur', function () {
configItem.value.editing = false
});
}
})
const isDisabled = computed(() => props.disabled || !configItem.value.editing)
const setEdit = () => {
configItem.value.editing = true
// @ts-ignore
const ed = tinymce.editors[tinymceId]
ed.setContent(configItem.value.propValue)
nextTick(() => {
editCursor()
})
}
const editCursor = () => {
setTimeout(() => {
const myDiv = document.getElementById(tinymceId)
// Focus the cursor on the end of the text
const range = document.createRange()
const sel = window.getSelection()
if (myDiv && myDiv.childNodes) {
range.setStart(myDiv.childNodes[myDiv.childNodes.length - 1], 1)
range.collapse(false)
if (sel) {
sel.removeAllRanges()
sel.addRange(range)
}
}
// For some browsers, it may be necessary to set the cursor to the end in another way
if (myDiv && myDiv.focus) {
myDiv.focus()
}
}, 100)
}
onMounted(() => {
tinymce.init({})
})
</script>
<style scoped lang="less">
.edit-model {
cursor: text;
}
.rich-main-class {
display: flex;
font-size: initial;
width: 100%;
height: 100%;
overflow-y: auto !important;
position: relative;
div::-webkit-scrollbar {
width: 0px !important;
height: 0px !important;
}
:deep(.ol) {
display: block !important;
list-style-type: decimal;
margin-block-start: 1em !important;
margin-block-end: 1em !important;
margin-inline-start: 0px !important;
margin-inline-end: 0px !important;
padding-inline-start: 40px !important;
}
:deep(ul) {
display: block !important;
list-style-type: disc;
margin-block-start: 1em !important;
margin-block-end: 1em !important;
margin-inline-start: 0px !important;
margin-inline-end: 0px !important;
padding-inline-start: 40px !important;
}
:deep(li) {
margin-left: 20px;
display: list-item !important;
text-align: -webkit-match-parent !important;
}
}
.custom-text-content {
width: 100%;
overflow-y: auto;
outline: none !important;
border: none !important;
margin: 2px;
ol {
list-style-type: decimal;
}
}
</style>
@@ -10,19 +10,23 @@
}
.itemCursorDefault {
cursor: default!important;
cursor: default !important;
}
.itemActive {
outline: 1px solid #59c7f9;
}
.item {
position: absolute;
width: 100px;
height: 100px;
border: 1px solid #59c7f9;
background-color: #fff;
cursor: move;
.resizeHandle {
position: absolute;
background: #fff;
border: 1px solid #59c7f9;
outline: 1px solid #59c7f9;
width: 8px;
height: 8px;
border-radius: 50%;
@@ -114,7 +114,7 @@ const emits = defineEmits(["parentAddItemBox"]);
}
.move-in-active {
border: 2px dotted transparent;
border-color: blueviolet;
border: 2px dotted blueviolet;
margin: -2px;
}
</style>