refactor: dashboard add item

This commit is contained in:
wangjiahao
2025-05-21 17:40:05 +08:00
parent 7786ff10f0
commit 611313f54e
28 changed files with 1059 additions and 153 deletions
-26
View File
@@ -8,32 +8,6 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElStep: typeof import('element-plus/es')['ElStep']
ElSteps: typeof import('element-plus/es')['ElSteps']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
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']
+1
View File
@@ -10,6 +10,7 @@
},
"dependencies": {
"lodash": "^4.17.21",
"snowflake-id": "^1.1.0",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"web-storage-cache": "^1.1.1"
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747819208465" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5002" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M948.560332 281.179984c-13.765515-13.833053-36.127825-13.833053-49.89334 0L511.991302 668.591431 125.313565 281.179984c-13.763468-13.798261-36.093033-13.798261-49.856501 0-13.799284 13.798261-13.799284 36.161594 0 49.993624l410.857439 411.674037c7.067976 7.085372 16.402575 10.521634 25.675776 10.331299 9.274224 0.191358 18.608823-3.245927 25.677822-10.331299l410.891208-411.708829c6.863315-6.89913 10.331299-15.940041 10.331299-24.979928S955.423647 288.078091 948.560332 281.179984z" p-id="5003"></path></svg>

After

Width:  |  Height:  |  Size: 844 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="16px" height="16.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M393.390114 512.023536l347.948667-336.348468c20.50808-19.85828 20.50808-51.997258 0-71.792093-20.507056-19.826558-53.778834-19.826558-74.28589 0L281.990954 476.135164c-20.476357 19.826558-20.476357 51.981908 0 71.746044l385.061936 372.236839c10.285251 9.91379 23.728424 14.869662 37.173644 14.869662 13.446243 0 26.889417-4.956895 37.112246-14.901385 20.50808-19.826558 20.50808-51.919487 0-71.746044L393.390114 512.023536" /></svg>

After

Width:  |  Height:  |  Size: 697 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.833984 2.49935C0.833984 2.03911 1.20708 1.66602 1.66732 1.66602H4.16732V6.66602H19.1673V17.4993C19.1673 17.9596 18.7942 18.3327 18.334 18.3327H1.66732C1.20708 18.3327 0.833984 17.9596 0.833984 17.4993V2.49935ZM19.1673 4.99935V2.49935C19.1673 2.03911 18.7942 1.66602 18.334 1.66602H13.334V4.99935H19.1673ZM11.6673 1.66602H5.83398V4.99935H11.6673V1.66602Z"/>
</svg>

After

Width:  |  Height:  |  Size: 500 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.0893 1.91058C17.933 1.7543 17.721 1.6665 17.5 1.6665H2.50002C2.27901 1.6665 2.06705 1.7543 1.91076 1.91058C1.75448 2.06686 1.66669 2.27882 1.66669 2.49984V17.4998C1.66669 17.7209 1.75448 17.9328 1.91076 18.0891C2.06705 18.2454 2.27901 18.3332 2.50002 18.3332H17.5C17.721 18.3332 17.933 18.2454 18.0893 18.0891C18.2456 17.9328 18.3334 17.7209 18.3334 17.4998V2.49984C18.3334 2.27882 18.2456 2.06686 18.0893 1.91058ZM10.8334 5.83323H13.75C13.8605 5.83323 13.9665 5.87713 14.0446 5.95527C14.1228 6.03341 14.1667 6.13939 14.1667 6.2499V7.08323C14.1667 7.19374 14.1228 7.29972 14.0446 7.37786C13.9665 7.456 13.8605 7.4999 13.75 7.4999H10.8334V13.7499C10.8334 13.8046 10.8226 13.8588 10.8016 13.9094C10.7807 13.9599 10.75 14.0058 10.7113 14.0445C10.6726 14.0832 10.6267 14.1139 10.5761 14.1348C10.5256 14.1558 10.4714 14.1666 10.4167 14.1666H9.58335C9.52864 14.1666 9.47446 14.1558 9.4239 14.1348C9.37335 14.1139 9.32742 14.0832 9.28873 14.0445C9.25004 14.0058 9.21934 13.9599 9.1984 13.9094C9.17747 13.8588 9.16669 13.8046 9.16669 13.7499V7.4999H6.25002C6.13951 7.4999 6.03353 7.456 5.95539 7.37786C5.87725 7.29972 5.83335 7.19374 5.83335 7.08323V6.2499C5.83335 6.13939 5.87725 6.03341 5.95539 5.95527C6.03353 5.87713 6.13951 5.83323 6.25002 5.83323H9.16669C9.16669 5.83323 10.8334 5.85657 10.8334 5.83323Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.71952 18.0526C13.8471 18.0526 17.2237 14.8374 17.4815 10.7749C17.499 10.4994 17.2734 10.2748 16.9973 10.2748C14.5714 10.2748 12.1454 10.2748 9.71952 10.2748C9.71952 7.40872 9.71952 4.98267 9.71952 2.99669C9.71952 2.72055 9.49501 2.4954 9.21942 2.51289C5.15692 2.77069 1.94174 6.1473 1.94174 10.2748C1.94174 14.5704 5.42397 18.0526 9.71952 18.0526Z" fill="#3370FF"/>
<path d="M12.1306 8.60807C11.7192 8.60807 11.3857 8.2746 11.3857 7.86325V2.49351C11.3857 2.19784 11.6417 1.96535 11.9329 2.01629C15.011 2.55462 17.4392 4.9828 17.9775 8.0609C18.0285 8.35215 17.796 8.60807 17.5003 8.60807H12.1306Z" fill="#00D6B9"/>
</svg>

After

Width:  |  Height:  |  Size: 729 B

+16 -2
View File
@@ -4,7 +4,8 @@ import Layout from '@/components/layout/index.vue'
import login from '@/views/login/index.vue'
import chat from '@/views/chat/index.vue'
import ds from '@/views/ds/index.vue'
import DashboardEditor from '@/views/dashboard/DashboardEditor.vue'
import DashboardEditor from '@/views/dashboard/editor/index.vue'
import Dashboard from '@/views/dashboard/index.vue'
import setting from '@/views/setting/index.vue'
import { watchRouter } from './watch'
const router = createRouter({
@@ -55,11 +56,24 @@ const router = createRouter({
{
path: 'index',
name: 'dashboard',
component: DashboardEditor,
component: Dashboard,
meta: { title: 'Dashboard', icon: 'dashboard' }
}
]
},
{
path: '/dashboard-edit',
redirect: '/dashboard-edit/index',
component: Layout,
children: [
{
path: 'index',
name: 'dashboard-edit',
component: DashboardEditor,
meta: { title: 'dashboard-edit', icon: 'dashboard' }
}
]
},
{
path: '/setting',
component: Layout,
+20 -1
View File
@@ -4,7 +4,19 @@ import {store} from "@/stores";
export const dashboardStore = defineStore('dashboard', {
state: () => {
return {
curComponent: null
curComponent: null,
canvasStyle: {},
componentData: [],
dashboardInfo: {
id: null,
name: 'New Dashboard',
pid: null,
dataState: null,
optType: null,
status: null,
type: null,
mobileLayout: false
}
}
},
getters: {
@@ -15,7 +27,14 @@ export const dashboardStore = defineStore('dashboard', {
actions: {
setCurComponent(value: any) {
this.curComponent = value
},
setComponentData(value: any) {
this.componentData = value
},
setCanvasStyle(value: any) {
this.canvasStyle = value
}
}
})
+49
View File
@@ -0,0 +1,49 @@
// @ts-ignore
import SnowflakeID from 'snowflake-id';
const snowflake = new SnowflakeID({
mid: 42,
offset: (2010 - 1970) * 365 * 24 * 3600 * 1000
});
export const guid = () => {
return snowflake.generate();
}
export interface CanvasItem {
_dragId: string | number
x: number
y: number
sizeX: number
sizeY: number
[key: string]: any
}
export type CanvasCoord = {
x1: number
y1: number
x2: number
y2: number
c1: number
c2: number
el: {
x: number
y: number
sizeX: number
sizeY: number
_dragId: string | number
[key: string]: any
}
}
export type DashboardInfo = {
dataState: string,
optType: string,
id: number,
name: string,
pid: number,
status: number,
type: string,
mobileLayout: boolean,
}
@@ -1,5 +1,5 @@
<script setup lang="ts">
import DsCanvasCore from '@/views/dashboard/canvas/DsCanvasCore.vue'
import CanvasCore from '@/views/dashboard/canvas/CanvasCore.vue'
import { nextTick, onMounted, ref } from 'vue'
const cyGridster = ref(null)
const baseWidth = ref(0)
@@ -56,14 +56,14 @@ onMounted(() => {
</script>
<template>
<DsCanvasCore
<CanvasCore
ref="cyGridster"
:base-width="baseWidth"
:base-height="baseHeight"
:base-margin-left="baseMarginLeft"
:base-margin-top="baseMarginTop"
:canvas-component-data="componentData"
></DsCanvasCore>
></CanvasCore>
</template>
<style scoped lang="less"></style>
@@ -2,8 +2,9 @@
import {ref, nextTick, toRefs, type PropType} from 'vue'
import _ from 'lodash'
import {dashboardStoreWithOut} from '@/stores/dashboard/dashboard'
import {type CanvasCoord, type CanvasItem} from '../utils/CanvasUtils'
import {type CanvasCoord, type CanvasItem} from '@/utils/canvas.ts'
import CanvasShape from './CanvasShape.vue'
import {findComponent} from "@/views/dashboard/components/component-list.ts";
const dashboardStore = dashboardStoreWithOut()
@@ -218,9 +219,19 @@ function recomputeCellWidth() {
maxCell.value = Math.round(containerRef.value.offsetWidth / cellWidth.value)
}
function init() {
function sizeInit() {
cellsInit()
recomputeCellWidth()
itemMaxX = maxCell.value
}
function cellsInit() {
cellWidth.value = baseWidth.value + baseMarginLeft.value
cellHeight.value = baseHeight.value + baseMarginTop.value
}
function init() {
cellsInit()
positionBox.value = []
coordinates.value = []
@@ -790,7 +801,7 @@ function startMove(e: MouseEvent, item: CanvasItem, index: number) {
moveYSize % cellHeight.value > cellHeight.value / 4
? parseInt(String(moveYSize / cellHeight.value + 1))
: parseInt(String(moveYSize / cellHeight.value))
// 根据坐标点 判断resize方向
// Determine the resizing direction based on the coordinate points
const {
nowSizeX,
nowSizeY,
@@ -943,6 +954,7 @@ function moving() {
defineExpose({
getRenderState,
init,
sizeInit,
afterInitOk,
addItemBox,
getMaxCell,
@@ -959,14 +971,19 @@ defineExpose({
<template>
<div class="dragAndResize" ref="containerRef" @mousedown="containerMouseDown($event)" @mouseup="endMove()"
@mousemove="moving()">
<div v-if="renderOk">
<CanvasShape v-for="(item, index) in canvasComponentData" :config-item="item" :draggable="draggable"
<template v-if="renderOk">
<CanvasShape v-for="(item, index) in canvasComponentData"
:config-item="item"
:draggable="draggable"
:item-index="index"
:move-animate="moveAnimate" :start-move="startMove" :start-resize="startResize" :key="'item' + index"
:move-animate="moveAnimate"
:start-move="startMove"
:start-resize="startResize"
:key="'item' + index"
:style="nowItemStyle(item)">
<!-- {{ item }}-->
<component class="slot-component" :is="findComponent(item.component)" :config-item="item"></component>
</CanvasShape>
</div>
</template>
</div>
</template>
@@ -1,104 +1,56 @@
<script setup lang="ts">
import { type CanvasItem } from '../utils/CanvasUtils'
import { ref, toRefs, type PropType } from 'vue'
import {type CanvasItem} from '@/utils/canvas.ts'
import {ref, toRefs, type PropType} from 'vue'
import ResizeHandle from "@/views/dashboard/canvas/ResizeHandle.vue";
const shapeRef = ref(null)
// Props
const props = defineProps({
configItem: {
type: Object as PropType<CanvasItem>,
required: true
},
itemIndex: {
type: Number,
required: true
},
moveAnimate: {
type: Boolean,
required: true
},
draggable: {
type: Boolean,
required: true
},
startMove: {
type: Function,
default: () => {
return {}
}
},
startResize: {
type: Function,
default: () => {
return {}
}
configItem: {
type: Object as PropType<CanvasItem>,
required: true
},
itemIndex: {
type: Number,
required: true
},
moveAnimate: {
type: Boolean,
required: true
},
draggable: {
type: Boolean,
required: true
},
startMove: {
type: Function,
default: () => {
return {}
}
},
startResize: {
type: Function,
default: () => {
return {}
}
}
})
const {
draggable
} = toRefs(props)
const cursors = {
lt: 'nw',
t: 'n',
rt: 'ne',
r: 'e',
rb: 'se',
b: 's',
lb: 'sw',
l: 'w'
}
function getPointStyle(point: string) {
const hasT = /t/.test(point)
const hasB = /b/.test(point)
const hasL = /l/.test(point)
const hasR = /r/.test(point)
let newLeft = '0px'
let newTop = '0px'
// Points at the four corners
if (point.length === 2) {
newLeft = hasL ? '0px' : '100%'
newTop = hasT ? '0px' : '100%'
} else {
// The point between the upper and lower points, with a width centered
if (hasT || hasB) {
newLeft = '50%'
newTop = hasT ? '0px' : '100%'
}
// The points on both sides are centered in height
if (hasL || hasR) {
newLeft = hasL ? '0px' : '100%'
newTop ='50%'
}
}
return {
marginLeft: '-4px',
marginTop: '-4px',
left: `${newLeft}`,
top: `${newTop}`,
// @ts-expect-error
cursor: `${cursors[point]}-resize`
}
}
const pointList = ['lt', 't', 'rt', 'r', 'rb', 'b', 'lb', 'l']
const { draggable } = toRefs(props)
</script>
<template>
<div :class="{
<div :class="{
item: true,
moveAnimation: moveAnimate,
movingItem: configItem.isPlayer,
canNotDrag: !draggable
}" @mousedown="startMove($event, configItem, itemIndex)" ref="shapeRef">
<slot></slot>
<div v-for="point in pointList" :key="point" class="resizeHandle" :style="getPointStyle(point)"
@mousedown="startResize($event, point, configItem, itemIndex)"></div>
</div>
<slot></slot>
<resize-handle
:start-resize="(event : MouseEvent, point: string) => startResize(event, point, configItem, itemIndex)">
</resize-handle>
</div>
</template>
<style scoped lang="less">
@@ -0,0 +1,57 @@
<script setup lang="ts">
import type {PropType} from "vue";
defineProps({
sizeList: {
type: Array as PropType<number[]>,
required: false,
default: [12, 12, 12, 12]
}
})
const shadowStyle = (size: number, index: number) => {
if (index === 0) {
return {
top: 0,
left: 0,
width: '100%',
height: `${size}px`
}
} else if (index === 1) {
return {
top: 0,
right: 0,
width: `${size}px`,
height: '100%'
}
} else if (index === 2) {
return {
bottom: 0,
left: 0,
width: '100%',
height: `${size}px`
}
} else if (index === 3) {
return {
top: 0,
left: 0,
width: `${size}px`,
height: '100%'
}
}
}
</script>
<template>
<div class="dragHandle" v-for="(size,index) in sizeList" :style="shadowStyle(size,index)"></div>
</template>
<style scoped lang="less">
.dragHandle {
background: #c1e2e8;
position: absolute;
cursor: move;
z-index: 3;
}
</style>
@@ -0,0 +1,69 @@
<script setup lang="ts">
defineProps({
startResize: {
type: Function,
default: () => {
return {}
}
}
})
const cursors = {
lt: 'nw',
t: 'n',
rt: 'ne',
r: 'e',
rb: 'se',
b: 's',
lb: 'sw',
l: 'w'
}
function getPointStyle(point: string) {
const hasT = /t/.test(point)
const hasB = /b/.test(point)
const hasL = /l/.test(point)
const hasR = /r/.test(point)
let newLeft = '0px'
let newTop = '0px'
// Points at the four corners
if (point.length === 2) {
newLeft = hasL ? '0px' : '100%'
newTop = hasT ? '0px' : '100%'
} else {
// The point between the upper and lower points, with a width centered
if (hasT || hasB) {
newLeft = '50%'
newTop = hasT ? '0px' : '100%'
}
// The points on both sides are centered in height
if (hasL || hasR) {
newLeft = hasL ? '0px' : '100%'
newTop ='50%'
}
}
return {
marginLeft: '-4px',
marginTop: '-4px',
left: `${newLeft}`,
top: `${newTop}`,
// @ts-expect-error
cursor: `${cursors[point]}-resize`
}
}
const pointList = ['lt', 't', 'rt', 'r', 'rb', 'b', 'lb', 'l']
</script>
<template>
<div v-for="point in pointList"
:key="point" class="resizeHandle"
:style="getPointStyle(point)"
@mousedown="startResize($event, point)"></div>
</template>
<style scoped lang="less">
@import '../css/CanvasStyle.less';
</style>
@@ -0,0 +1,77 @@
<script setup lang="ts">
import {toRefs} from 'vue'
const props = defineProps({
title: {type: String, required: false},
tips: {type: String, required: false},
iconName: {type: String, required: false},
showSplitLine: {type: Boolean, required: false, default: false},
active: {type: Boolean, required: false, default: false},
})
const {title, tips, iconName, showSplitLine} = toRefs(props)
const emits = defineEmits(['customClick'])
</script>
<template>
<div class="flex-align-center">
<el-row class="group_icon" :title="tips" @click="emits('customClick')">
<el-col :span="24" class="group_inner" :class="{ 'inner-active': active }">
<component class="svg-icon toolbar-icon" :is="iconName"></component>
<span>{{ title }}</span>
</el-col>
</el-row>
<el-divider class="group-right-border" v-if="showSplitLine" direction="vertical"/>
</div>
</template>
<style lang="less" scoped>
.flex-align-center {
& + & {
margin-left: 4px;
}
}
.group_inner {
padding: 8px;
display: flex;
cursor: pointer;
flex-direction: column;
align-items: center;
border-radius: 4px;
color: #a6a6a6;
span {
float: left;
font-size: 12px;
margin-top: 8px;
line-height: 12px;
}
&:hover {
background: rgba(235, 235, 235, 0.1);
color: #fff;
}
&:active {
color: #fff;
background: rgba(235, 235, 235, 0.2);
}
}
.group-right-border {
border-color: rgba(255, 255, 255, 0.15);
margin: 0 2px 0 6px;
height: 40px;
}
.inner-active {
border: 1px solid var(--ed-color-primary);
background: rgba(255, 255, 255, 0.2);
}
.toolbar-icon {
float: left;
width: 20px;
height: 20px;
}
</style>
@@ -0,0 +1,67 @@
import SQTab from '@/views/dashboard/components/sq-tab/index.vue'
import SQText from '@/views/dashboard/components/sq-text/index.vue'
import SQView from '@/views/dashboard/components/sq-view/index.vue'
import SQEmpty from '@/views/dashboard/components/sq-empty/index.vue'
const COMPONENT_LIST = [
{
id: 1001,
component: 'SQView',
name: 'new-view',
propValue: '&nbsp;',
icon: 'icon_graphical',
innerType: 'bar',
x: 1,
y: 1,
sizeX: 36,
sizeY: 14,
style: {}
},
{
id: 1002,
component: 'SQTab',
name: 'new-tabs',
propValue: [
{
name: 'tab',
title: 'new_tab',
componentData: [],
closable: true
}
],
canvasActive: false,
x: 1,
y: 1,
sizeX: 36,
sizeY: 14,
style: {}
},
{
id: 1003,
component: 'SQText',
name: 'new text',
propValue: '&nbsp;',
x: 1,
y: 1,
sizeX: 36,
sizeY: 14,
style: {}
}
]
export function findNewComponentFromList(component: string) {
return COMPONENT_LIST.find(item => item.component === component) || null;
}
export const componentsMap = {
SQTab: SQTab,
SQText: SQText,
SQView: SQView
}
export function findComponent(key = 'SQEmpty') {
//@ts-ignore
return componentsMap[key] ? componentsMap[key] : SQEmpty
}
export default COMPONENT_LIST
@@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div> no component can be find </div>
</template>
<style scoped lang="less">
</style>
@@ -0,0 +1,194 @@
<template>
<el-tabs :class="['de-tabs', ...tabClassName]" :style="tabStyle" v-bind="$attrs">
<slot></slot>
</el-tabs>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps({
hideTitle: Boolean,
/* 颜色可以单词,如red;也可以是颜色值 */
// 字体颜色
fontColor: String,
// 激活字体颜色
activeColor: String,
// 边框颜色 如果是none就无边框 如果是none Card类型激活的下滑线也消失
borderColor: String,
// 激活边框颜色 目前只针对card类型
borderActiveColor: String,
// 样式类型 radioGroup只在Card类型有效, 同时必须给borderColor borderActiveColor
styleType: {
type: String,
default: '',
validator: (val: string) => ['', 'radioGroup'].includes(val)
}
})
const tabStyle = computed(() => [
{ '--de-font-color': props.fontColor },
{ '--de-active-color': props.activeColor },
{ '--de-border-color': props.borderColor },
{ '--de-border-active-color': props.borderActiveColor }
])
const tabClassName = computed(() =>
props.styleType
? [props.styleType, props.fontColor && 'fontColor', props.activeColor && 'activeColor']
: [
props.fontColor && 'fontColor',
props.activeColor && 'activeColor',
noBorder.value ? 'noBorder' : props.borderColor && 'borderColor',
props.borderActiveColor && 'borderActiveColor',
props.hideTitle && 'no-header'
]
)
const noBorder = computed(() => props.borderColor === 'none')
</script>
<style lang="less">
.de-tabs {
height: 100%;
&.no-header {
.ed-tabs__header {
display: none;
}
}
&.ed-tabs--card {
> .ed-tabs__header {
height: auto !important;
}
}
&.fontColor {
.ed-tabs__item {
color: var(--de-font-color);
&.is-active {
color: var(--el-color-primary);
}
&:hover {
color: var(--el-color-primary);
}
}
}
&.activeColor {
.ed-tabs__item {
&.is-active {
color: var(--de-active-color);
}
&:hover {
color: var(--de-active-color);
}
}
.ed-tabs__active-bar {
height: 0px;
background-color: var(--de-active-color);
}
}
// card样式的边框
&.noBorder.ed-tabs--card {
> .ed-tabs__header {
border-bottom: none;
.ed-tabs__nav {
border: none;
}
.ed-tabs__item {
border-left: none;
}
.ed-tabs__item.is-active {
border-bottom: none;
}
}
}
&.borderActiveColor.ed-tabs--card {
> .ed-tabs__header .ed-tabs__item.is-active {
border-bottom-color: var(--de-border-active-color);
}
}
&.borderColor.ed-tabs--card {
> .ed-tabs__header {
border-bottom-color: var(--de-border-color);
.ed-tabs__nav {
border-color: var(--de-border-color);
}
.ed-tabs__item {
border-left-color: var(--de-border-color);
}
}
.ed-tabs__item {
&.is-active {
color: var(--de-active-color);
}
&:hover {
color: var(--de-active-color);
}
}
.ed-tabs__active-bar {
height: 0px;
background-color: var(--de-active-color);
}
}
// 简洁样式的边框
&.noBorder {
.ed-tabs__nav-wrap::after {
background: none;
}
}
&.borderColor {
.ed-tabs__nav-wrap::after {
background: var(--de-border-color);
}
}
// radioGroup 类型
&.radioGroup.ed-tabs--card {
> .ed-tabs__header {
border-bottom: none;
.ed-tabs__nav {
border: none;
}
.ed-tabs__item {
border: 1px solid var(--de-border-color);
border-right: 0;
&:first-child {
border-left: 1px solid var(--de-border-color);
border-radius: 4px 0 0 4px;
}
&:last-child {
border-right: 1px solid var(--de-border-color);
border-radius: 0 4px 4px 0;
}
&.is-active {
border: 1px solid var(--de-border-active-color);
& + .ed-tabs__item {
border-left: 0;
}
}
}
}
}
}
</style>
@@ -0,0 +1,167 @@
<script setup lang="ts">
import {computed, type PropType, reactive, ref, toRefs, nextTick} from 'vue'
import CustomTab from "@/views/dashboard/components/sq-tab/CustomTab.vue";
import {guid, type CanvasItem,} from "@/utils/canvas.ts";
import DragHandle from "@/views/dashboard/canvas/DragHandle.vue";
import {ArrowDown} from "@element-plus/icons-vue";
const editableTabsValue = ref(null)
const showTabTitleFlag = ref(true)
const props = defineProps({
configItem: {
type: Object as PropType<CanvasItem>,
required: true
}
})
const {configItem} = toRefs(props)
const state = reactive({
activeTabName: '',
curItem: {},
textarea: '',
dialogVisible: false,
tabShow: true,
hoverFlag: false,
headFontColor: '#OOOOOO',
headFontActiveColor: '#OOOOOO',
headBorderColor: '#OOOOOO',
headBorderActiveColor: '#OOOOOO'
})
function addTab() {
const newName = guid()
const newTab = {
name: newName,
title: 'New Tab',
componentData: [],
closable: true
}
configItem.value.propValue.push(newTab)
editableTabsValue.value = newTab.name
}
function deleteCur(param:any) {
state.curItem = param
let len = configItem.value.propValue.length
while (len--) {
if (configItem.value.propValue[len].name === param.name) {
configItem.value.propValue.splice(len, 1)
const activeIndex =
(len - 1 + configItem.value.propValue.length) % configItem.value.propValue.length
editableTabsValue.value = configItem.value.propValue[activeIndex].name
state.tabShow = false
nextTick(() => {
state.tabShow = true
})
}
}
}
function editCurTitle(param:any) {
state.activeTabName = param.name
state.curItem = param
state.textarea = param.title
state.dialogVisible = true
}
function handleCommand(command:any) {
switch (command.command) {
case 'editTitle':
editCurTitle(command.param)
break
case 'deleteCur':
deleteCur(command.param)
break
}
}
const beforeHandleCommand = (item : any, param : any) => {
return {
command: item,
param: param
}
}
const titleStyle = (itemName: string) => {
if (editableTabsValue.value === itemName) {
return {}
} else {
return {}
}
}
const isEditMode = computed(() => true)
</script>
<template>
<div>
<drag-handle></drag-handle>
<custom-tab
v-model="editableTabsValue"
@tab-add="addTab"
:addable="isEditMode"
:font-color="state.headFontColor"
:active-color="state.headFontActiveColor"
:border-color="state.headBorderColor"
:border-active-color="state.headBorderActiveColor"
:hide-title="!showTabTitleFlag"
>
<template :key="tabItem.name" v-for="tabItem in configItem.propValue">
<el-tab-pane
class="el-tab-pane-custom"
:lazy="isEditMode"
:label="tabItem.title"
:name="tabItem.name"
>
<template #label>
<div class="custom-tab-title" @mousedown.stop>
<span class="title-inner" :style="titleStyle(tabItem.name)"
>{{ tabItem.title }}
<span v-if="isEditMode">
<el-dropdown
popper-class="custom-de-tab-dropdown"
trigger="click"
@command="handleCommand"
>
<span class="el-dropdown-link">
<el-icon v-if="isEditMode"><ArrowDown/></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="beforeHandleCommand('editTitle', tabItem)">
edit
</el-dropdown-item>
<el-dropdown-item :command="beforeHandleCommand('copyCur', tabItem)">
copy
</el-dropdown-item>
<el-dropdown-item
v-if="configItem.propValue.length > 1"
:command="beforeHandleCommand('deleteCur', tabItem)"
>
delete
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</span>
</div>
</template>
</el-tab-pane>
</template>
<div>
this is a tab
</div>
</custom-tab>
</div>
</template>
<style scoped lang="less">
.el-dropdown-link{
margin-top: 3px !important;
}
</style>
@@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped lang="less">
</style>
@@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped lang="less">
</style>
@@ -2,6 +2,7 @@
{
position: relative;
user-select: none;
width: 100%;
*
{
box-sizing: border-box;
@@ -24,7 +25,7 @@
width: 8px;
height: 8px;
border-radius: 50%;
z-index: 0;
z-index: 5;
}
}
.moveAnimation
@@ -65,6 +66,9 @@
{
display: none;
}
.slot-component {
display: none;
}
}
.positionBox
@@ -0,0 +1,113 @@
<script setup lang="ts">
import CanvasCore from '@/views/dashboard/canvas/CanvasCore.vue'
import {nextTick, onMounted, ref} from 'vue'
import type {CanvasItem} from "@/utils/canvas.ts";
const canvasCoreRef = ref(null)
const dashboardEditorRef = ref(null)
const baseWidth = ref(0)
const baseHeight = ref(0)
const baseMarginLeft = ref(0)
const baseMarginTop = ref(0)
const baseMatrixCount = {
x: 72,
y: 36
}
const componentData = ref([
{
id: 4,
x: 1,
y: 1,
sizeX: 20,
sizeY: 10,
_dragId: 0
},
{
id: 10,
x: 2,
y: 1,
sizeX: 20,
sizeY: 10,
_dragId: 1
},
{
id: 7,
x: 1,
y: 2,
sizeX: 20,
sizeY: 10,
_dragId: 2
}
])
const canvasSizeInit = () => {
sizeInit()
if (canvasCoreRef.value) {
//@ts-ignore
canvasCoreRef.value.sizeInit()
}
}
const sizeInit = () => {
if (dashboardEditorRef.value) {
//@ts-ignore
const screenWidth = dashboardEditorRef.value.offsetWidth
//@ts-ignore
const screenHeight = dashboardEditorRef.value.offsetHeight
baseMarginLeft.value = 10
baseMarginTop.value = 10
baseWidth.value = (screenWidth - baseMarginLeft.value) / baseMatrixCount.x - baseMarginLeft.value
baseHeight.value = (screenHeight - baseMarginTop.value) / baseMatrixCount.y - baseMarginTop.value
}
}
const addItemToBox = (item: CanvasItem) => {
// @ts-ignore
console.log(componentData.value.length + '===' + JSON.stringify(componentData.value))
// @ts-ignore
canvasCoreRef.value.addItemBox(item)
}
defineExpose({
canvasSizeInit,
addItemToBox
})
onMounted(() => {
window.addEventListener('resize', canvasSizeInit)
nextTick(() => {
if (dashboardEditorRef.value) {
sizeInit()
// @ts-ignore
nextTick(() => {
if (canvasCoreRef.value) {
// @ts-ignore
canvasCoreRef.value.init()
}
})
}
})
})
</script>
<template>
<div class='dashboard-editor-main' ref="dashboardEditorRef">
<CanvasCore
ref="canvasCoreRef"
:base-width="baseWidth"
:base-height="baseHeight"
:base-margin-left="baseMarginLeft"
:base-margin-top="baseMarginTop"
:canvas-component-data="componentData"
></CanvasCore>
</div>
</template>
<style scoped lang="less">
.dashboard-editor-main {
width: 100%;
height: calc(100% - 56px);
overflow-y: auto;
}
</style>
@@ -0,0 +1,68 @@
<script setup lang="ts">
import {dashboardStoreWithOut} from "@/stores/dashboard/dashboard.ts";
import {storeToRefs} from 'pinia'
import IconBack from '@/assets/svg/common-back.svg'
import ComponentButtonLabel from "@/views/dashboard/components/button-label/ComponentButtonLabel.vue";
import dvTab from '@/assets/svg/dv-tab.svg'
import dvText from '@/assets/svg/dv-text.svg'
import dvView from '@/assets/svg/dv-view.svg'
const dashboardStore = dashboardStoreWithOut()
const {dashboardInfo} = storeToRefs(dashboardStore)
const emits = defineEmits(['addComponent'])
const openViewDialog = () => {
// do addComponent
}
</script>
<template>
<div class="toolbar-main">
<span>
<el-icon><IconBack/></el-icon>
</span>
<span>{{ dashboardInfo.name }}</span>
<div class="core-toolbar">
<component-button-label
:icon-name="dvView"
title="Add View"
themes="light"
is-label
@customClick="openViewDialog"
></component-button-label>
<component-button-label
:icon-name="dvText"
title="Text"
themes="light"
is-label
@customClick="() =>emits('addComponent', 'SQText')"
></component-button-label>
<component-button-label
:icon-name="dvTab"
title="Tab Item"
themes="light"
is-label
@customClick="() =>emits('addComponent', 'SQTab')"
>
</component-button-label>
</div>
</div>
</template>
<style scoped lang="less">
.toolbar-main {
width: 100%;
height: 56px;
display: flex;
align-items: center;
background: #050e21;
padding-left: 8px;
.core-toolbar {
display: flex;
position: absolute;
left: 50%;
transform: translateX(-50%);
}
}
</style>
@@ -0,0 +1,34 @@
<script setup lang="ts">
import {ref} from 'vue'
import Toolbar from "@/views/dashboard/editor/Toolbar.vue";
import DashboardEditor from "@/views/dashboard/editor/DashboardEditor.vue";
import {findNewComponentFromList} from "@/views/dashboard/components/component-list.ts";
import {guid} from "@/utils/canvas.ts";
import cloneDeep from 'lodash/cloneDeep';
const dashboardEditorRef = ref(null);
const addComponent = (componentType : string) => {
const component = cloneDeep(findNewComponentFromList(componentType))
if (component && dashboardEditorRef.value) {
component.id = guid()
//@ts-ignore
dashboardEditorRef.value.addItemToBox(component)
}
}
</script>
<template>
<div class="editor-main">
<Toolbar @add-component="addComponent"></Toolbar>
<DashboardEditor ref="dashboardEditorRef"></DashboardEditor>
</div>
</template>
<style scoped lang="less">
.editor-main {
position: relative;
width: 100vw;
height: calc(100vh - 61px);
}
</style>
@@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped lang="less">
</style>
@@ -1,26 +0,0 @@
export interface CanvasItem {
_dragId: string | number
x: number
y: number
sizeX: number
sizeY: number
[key: string]: any
}
export type CanvasCoord = {
x1: number
y1: number
x2: number
y2: number
c1: number
c2: number
el: {
x: number
y: number
sizeX: number
sizeY: number
_dragId: string | number
[key: string]: any
}
}