feat: dashboard preview

This commit is contained in:
wangjiahao
2025-06-03 18:36:01 +08:00
parent 7ba78ded01
commit 866344b26b
23 changed files with 1530 additions and 5 deletions
+2 -1
View File
@@ -26,7 +26,8 @@ fileConfig(config.config_file_name)
# from apps.system.models.user import SQLModel # noqa
# from apps.settings.models.setting_models import SQLModel
from apps.chat.models.chat_model import SQLModel
#from apps.chat.models.chat_model import SQLModel
from apps.dashboard.models.dashboard import SQLModel
from common.core.config import settings # noqa
target_metadata = SQLModel.metadata
@@ -0,0 +1,56 @@
"""core_dashboard
Revision ID: 804b08ac329d
Revises: 1f077c30e476
Create Date: 2025-06-03 14:40:25.419409
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '804b08ac329d'
down_revision = '1f077c30e476'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('core_dashboard',
sa.Column('id', sa.String(length=50), nullable=False),
sa.Column('name', sa.String(length=255), nullable=True),
sa.Column('pid', sa.String(length=50), nullable=True),
sa.Column('workspace_id', sa.String(length=50), nullable=True),
sa.Column('org_id', sa.String(length=50), nullable=True),
sa.Column('level', sa.Integer(), nullable=True),
sa.Column('node_type', sa.String(length=255), nullable=True),
sa.Column('type', sa.String(length=50), nullable=True),
sa.Column('canvas_style_data', sa.Text(), nullable=True),
sa.Column('component_data', sa.Text(), nullable=True),
sa.Column('mobile_layout', sa.SmallInteger(), nullable=True),
sa.Column('status', sa.Integer(), nullable=True),
sa.Column('self_watermark_status', sa.Integer(), nullable=True),
sa.Column('sort', sa.Integer(), nullable=True),
sa.Column('create_time', sa.BigInteger(), nullable=True),
sa.Column('create_by', sa.String(length=255), nullable=True),
sa.Column('update_time', sa.BigInteger(), nullable=True),
sa.Column('update_by', sa.String(length=255), nullable=True),
sa.Column('remark', sa.String(length=255), nullable=True),
sa.Column('source', sa.String(length=255), nullable=True),
sa.Column('delete_flag', sa.SmallInteger(), nullable=True),
sa.Column('delete_time', sa.BigInteger(), nullable=True),
sa.Column('delete_by', sa.String(length=255), nullable=True),
sa.Column('version', sa.Integer(), nullable=True),
sa.Column('content_id', sa.String(length=50), nullable=True),
sa.Column('check_version', sa.String(length=50), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
op.drop_table('core_dashboard')
# ### end Alembic commands ###
View File
@@ -0,0 +1,18 @@
import hashlib
import os
import uuid
from typing import List
from fastapi import APIRouter, File, UploadFile, HTTPException
from apps.dashboard.crud.dashboard_service import get_dashboard_list, preview_with_id
from common.core.deps import SessionDep
router = APIRouter(tags=["dashboard"], prefix="/dashboard")
@router.get("/list")
async def datasource_list(session: SessionDep):
return get_dashboard_list(session=session)
@router.get("/preview_dashboard/{id}")
async def preview_dashboard(session: SessionDep,id:str):
return preview_with_id(session=session,dashboard_id=id)
@@ -0,0 +1,13 @@
from select import select
from apps.dashboard.models.dashboard_model import CoreDashboard
from common.core.deps import SessionDep
def get_dashboard_list(session: SessionDep):
statement = select(CoreDashboard).order_by(CoreDashboard.create_time.desc())
dashboard_list = session.exec(statement).fetchall()
return dashboard_list
def preview_with_id(session: SessionDep, dashboard_id: str):
return session.query(CoreDashboard).filter(CoreDashboard.id == id).first()
@@ -0,0 +1,123 @@
from sqlmodel import SQLModel, Field
from sqlalchemy import String, Column, Text, SmallInteger, BigInteger, Integer
class CoreDashboard(SQLModel, table=True):
__tablename__ = "core_dashboard"
id: str = Field(
sa_column=Column(String(50), nullable=False, primary_key=True)
)
name: str = Field(
default=None,
max_length=255,
sa_column=Column(String(255), nullable=True)
)
pid: str = Field(
default=None,
max_length=50,
sa_column=Column(String(50), nullable=True)
)
workspace_id: str = Field(
default=None,
max_length=50,
sa_column=Column(String(50), nullable=True)
)
org_id: str = Field(
default=None,
max_length=50,
sa_column=Column(String(50), nullable=True)
)
level: int = Field(
default=None,
sa_column=Column(Integer, nullable=True)
)
node_type: str = Field(
default=None,
max_length=255,
sa_column=Column(String(255), nullable=True)
)
type: str = Field(
default=None,
max_length=50,
sa_column=Column(String(50), nullable=True)
)
canvas_style_data: str = Field(
default=None,
sa_column=Column(Text, nullable=True)
)
component_data: str = Field(
default=None,
sa_column=Column(Text, nullable=True)
)
mobile_layout: int = Field(
default=0,
sa_column=Column(SmallInteger, nullable=True)
)
status: int = Field(
default=1,
sa_column=Column(Integer, nullable=True)
)
self_watermark_status: int = Field(
default=0,
sa_column=Column(Integer, nullable=True)
)
sort: int = Field(
default=0,
sa_column=Column(Integer, nullable=True)
)
create_time: int = Field(
default=None,
sa_column=Column(BigInteger, nullable=True)
)
create_by: str = Field(
default=None,
max_length=255,
sa_column=Column(String(255), nullable=True)
)
update_time: int = Field(
default=None,
sa_column=Column(BigInteger, nullable=True)
)
update_by: str = Field(
default=None,
max_length=255,
sa_column=Column(String(255), nullable=True)
)
remark: str = Field(
default=None,
max_length=255,
sa_column=Column(String(255), nullable=True)
)
source: str = Field(
default=None,
max_length=255,
sa_column=Column(String(255), nullable=True)
)
delete_flag: int = Field(
default=0,
sa_column=Column(SmallInteger, nullable=True)
)
delete_time: int = Field(
default=None,
sa_column=Column(BigInteger, nullable=True)
)
delete_by: str = Field(
default=None,
max_length=255,
sa_column=Column(String(255), nullable=True)
)
version: int = Field(
default=3,
sa_column=Column(Integer, nullable=True)
)
content_id: str = Field(
default='0',
max_length=50,
sa_column=Column(String(50), nullable=True)
)
check_version: str = Field(
default='1',
max_length=50,
sa_column=Column(String(50), nullable=True)
)
+8
View File
@@ -0,0 +1,8 @@
import { request } from '@/utils/request'
export const dashboardApi = {
list: () => request.get('/dashboard/list'),
getDashboardInfo: (params:any) => request.post(`/dashboard/get_dashboard/${params.id}`,params),
sqNameCheck: (data: any) => request.post('/dashboard/name_check',data),
moveResource: (data: any) => request.post('/dashboard/move',data),
}
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M11 11V2.64097C11 2.41809 11.0232 2.33727 11.0668 2.25579C11.1104 2.17431 11.1743 2.11036 11.2558 2.06678C11.3373 2.02321 11.4181 2 11.641 2H12.359C12.5819 2 12.6627 2.02321 12.7442 2.06678C12.8257 2.11036 12.8896 2.17431 12.9332 2.25579C12.9768 2.33727 13 2.41809 13 2.64097V11H21.359C21.5819 11 21.6627 11.0232 21.7442 11.0668C21.8257 11.1104 21.8896 11.1743 21.9332 11.2558C21.9768 11.3373 22 11.4181 22 11.641V12.359C22 12.5819 21.9768 12.6627 21.9332 12.7442C21.8896 12.8257 21.8257 12.8896 21.7442 12.9332C21.6627 12.9768 21.5819 13 21.359 13H13V21.359C13 21.5819 12.9768 21.6627 12.9332 21.7442C12.8896 21.8257 12.8257 21.8896 12.7442 21.9332C12.6627 21.9768 12.5819 22 12.359 22H11.641C11.4181 22 11.3373 21.9768 11.2558 21.9332C11.1743 21.8896 11.1104 21.8257 11.0668 21.7442C11.0232 21.6627 11 21.5819 11 21.359V13H2.64097C2.41809 13 2.33727 12.9768 2.25579 12.9332C2.17431 12.8896 2.11036 12.8257 2.06678 12.7442C2.02321 12.6627 2 12.5819 2 12.359V11.641C2 11.4181 2.02321 11.3373 2.06678 11.2558C2.11036 11.1743 2.17431 11.1104 2.25579 11.0668C2.33727 11.0232 2.41809 11 2.64097 11H11Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+14 -4
View File
@@ -10,15 +10,19 @@ export const dashboardStore = defineStore('dashboard', {
curComponentId: null,
canvasStyle: {},
componentData: [],
fullscreenFlag: false,
dashboardInfo: {
id: null,
name: 'New Dashboard',
name: null,
pid: null,
dataState: null,
optType: null,
workspaceId: null,
status: null,
type: null,
mobileLayout: false
creatorName: null,
updateName: null,
createTime: null,
updateTime: null,
contentId: null
}
}
},
@@ -32,6 +36,9 @@ export const dashboardStore = defineStore('dashboard', {
this.curComponent = value
this.curComponentId = value && value.id ? value.id : null
},
setDashboardInfo(value: any) {
this.dashboardInfo = value
},
setComponentData(value: any) {
this.componentData = value
},
@@ -44,6 +51,9 @@ export const dashboardStore = defineStore('dashboard', {
setTabMoveInActiveId(tabId: any) {
this.tabMoveInActiveId = tabId
},
canvasDataInit(){
}
}
})
@@ -0,0 +1,58 @@
<template>
<div class="info-card">
<div class="info-title">
Dashboard ID
</div>
<div class="info-content">{{ dashboardInfo.id }}</div>
<div v-if="dashboardInfo.creatorName" class="info-title">Creator</div>
<div v-if="dashboardInfo.creatorName" class="info-content">Creator Name</div>
<div class="info-title">Create Time</div>
<div class="info-content">{{ timestampFormatDate(dashboardInfo.createTime) }}</div>
<div v-if="dashboardInfo.updateName" class="info-title">Updator</div>
<div v-if="dashboardInfo.updateName" class="info-content">Updator Name</div>
<div class="info-title">Update Time</div>
<div v-if="dashboardInfo.updateTime" class="info-content">
{{ timestampFormatDate(dashboardInfo.updateTime) }}
</div>
<div v-if="!dashboardInfo.updateTime" class="info-content">N/A</div>
</div>
</template>
<script lang="ts" setup>
import {dashboardStoreWithOut} from "@/stores/dashboard/dashboard.ts";
import {storeToRefs} from "pinia";
const dashboardStore = dashboardStoreWithOut()
const {dashboardInfo} = storeToRefs(dashboardStore)
const timestampFormatDate = (value:any) => {
if (!value) {
return '-'
}
return new Date(value).toLocaleString()
}
</script>
<style lang="less" scoped>
.info-card {
font-style: normal;
padding-left: 4px;
font-weight: 400;
line-height: 22px;
.info-title {
color: #646a73;
font-size: 14px;
margin-bottom: 4px;
}
.info-content {
color: #1f2329;
font-size: 14px;
margin-bottom: 12px;
}
:last-child {
margin-bottom: 0;
}
}
</style>
@@ -0,0 +1,62 @@
<script lang="ts" setup>
import { type PropType } from 'vue'
import nothingInput from '@/assets/img/nothing-input.png'
import nothingSelect from '@/assets/img/nothing-select.png'
import nothingTable from '@/assets/img/nothing-table.png'
import none from '@/assets/img/none.png'
import error from '@/assets/img/error.png'
import nothingTree from '@/assets/img/nothing-tree.png'
import nothingNone from '@/assets/img/nothing-none.png'
defineProps({
imgType: {
type: String as PropType<
'input' | 'select' | 'table' | 'none' | 'noneWhite' | 'tree' | 'error'
>,
default: 'table'
},
imageSize: {
type: Number,
default: 125
},
description: {
type: String,
default: ''
}
})
const getAssetsFile = {
input: nothingInput,
select: nothingSelect,
table: nothingTable,
noneWhite: nothingNone,
tree: nothingTree,
error,
none
}
</script>
<template>
<el-empty
class="empty-info"
:imageSize="imageSize"
:description="description"
:image="getAssetsFile[imgType]"
>
<slot></slot>
</el-empty>
</template>
<style lang="less" scoped>
.empty-info {
height: 100%;
}
:deep(.ed-empty__description) {
margin-top: 8px;
color: var(--N600, #646a73);
text-align: center;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
}
</style>
@@ -0,0 +1,80 @@
<script lang="ts" setup>
import {Icon} from '@/components/icon-custom'
import icon_more_outlined from '@/assets/svg/icon_more_outlined.svg'
import type {Placement} from 'element-plus-secondary'
export interface Menu {
svgName?: string
label?: string
command: string
divided?: boolean
disabled?: boolean
}
withDefaults(
defineProps<{
menuList: Menu[]
placement?: Placement
iconName?: any
iconSize?: string
inTable?: boolean
}>(),
{
placement: 'bottom-end',
iconSize: '16px',
inTable: false
}
)
const handleCommand = (command: string | number | object) => {
emit('handleCommand', command)
}
const emit = defineEmits(['handleCommand'])
</script>
<template>
<el-dropdown
popper-class="menu-more_popper"
:placement="placement"
:persistent="false"
trigger="click"
@command="handleCommand"
>
<el-icon class="hover-icon" :class="inTable && 'hover-icon-in-table'" @click.stop>
<Icon>
<component class="svg-icon" :is="iconName || icon_more_outlined"></component>
</Icon>
</el-icon>
<template #dropdown>
<el-dropdown-menu :persistent="false">
<template v-for="ele in menuList"
>
<el-dropdown-item
:divided="ele.divided"
:command="ele.command"
:disabled="ele.disabled"
>
<el-icon class="handle-icon" :style="{ fontSize: iconSize }" v-if="ele.svgName">
<Icon>
<component class="svg-icon" :is="ele.svgName"></component>
</Icon>
</el-icon>
{{ ele.label }}
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<style lang="less">
.menu-more_popper {
margin-top: -2px !important;
.handle-icon {
color: #646a73;
}
}
</style>
@@ -0,0 +1,478 @@
<script setup lang="ts">
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
import { treeDraggableChart } from '@/views/dashboard/utils/treeDraggableChart'
import dvNewFolder from '@/assets/svg/dv-new-folder.svg'
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
import dvFolder from '@/assets/svg/dv-folder.svg'
import icon_operationAnalysis_outlined from '@/assets/svg/icon_operation-analysis_outlined.svg'
import icon_edit_outlined from '@/assets/svg/icon_edit_outlined.svg'
import { onMounted, reactive, ref, watch, nextTick, computed } from 'vue'
import { ElIcon, ElScrollbar } from 'element-plus-secondary'
import { Icon } from '@/components/icon-custom'
import { type SQTreeNode } from '@/views/dashboard/utils/treeNode'
import _ from 'lodash'
import { treeParentWeight } from '@/views/dashboard/utils/treeSortUtils'
import router from '@/router'
import {dashboardStoreWithOut} from "@/stores/dashboard/dashboard.ts";
import HandleMore from "@/views/dashboard/common/HandleMore.vue";
const dashboardStore = dashboardStoreWithOut()
defineProps({
curCanvasType: {
type: String,
required: true
},
showPosition: {
required: false,
type: String,
default: 'preview'
},
resourceTable: {
required: false,
type: String,
default: 'core'
}
})
const defaultProps = {
children: 'children',
label: 'name',
disabled: (data: any) => data.extraFlag1 === 0
}
const mounted = ref(false)
const rootManage = ref(false)
const selectedNodeKey:any = ref(null)
const filterText = ref(null)
const expandedArray = ref([])
const resourceListTree = ref()
const returnMounted = ref(false)
const state = reactive({
pWeightMap: {},
curSortType: 'time_desc',
resourceTree: [] as SQTreeNode[],
originResourceTree: [] as SQTreeNode[],
folderMenuList: [
],
sortType: [
],
templateCreatePid: 0
})
const resourceTypeList = computed(() => {
return []
})
// @ts-ignore
const { handleDrop, allowDrop, handleDragStart } = treeDraggableChart(
state,
'resourceTree',
'dashboard'
)
const routerDashboardId = router.currentRoute.value.query.dashboardId
if (routerDashboardId) {
selectedNodeKey.value = routerDashboardId
returnMounted.value = true
}
const nodeExpand = (data:any) => {
if (data.id) {
// @ts-ignore
expandedArray.value.push(data.id)
}
}
const nodeCollapse = (data:any) => {
if (data.id) {
// @ts-ignore
expandedArray.value.splice(expandedArray.value.indexOf(data.id), 1)
}
}
const filterNode = (value: string, data: SQTreeNode) => {
if (!value) return true
return data.name?.toLocaleLowerCase().includes(value.toLocaleLowerCase())
}
const nodeClick = (data: SQTreeNode, node:any) => {
dashboardStore.setCurComponent({ component: null, index: null })
if (node.disabled) {
nextTick(() => {
const currentNode = resourceListTree.value.$el.querySelector('.is-current')
if (currentNode) {
currentNode.classList.remove('is-current')
}
return
})
} else {
selectedNodeKey.value = data.id
if (data.leaf) {
emit('nodeClick', data)
} else {
resourceListTree.value.setCurrentKey(null)
}
}
}
const getTree = async () => {
state.originResourceTree = []
afterTreeInit()
}
// @ts-ignore
const flattedTree = computed<SQTreeNode[]>(() => {
return _.filter(flatTree(state.resourceTree), node => node.leaf)
})
const hasData = computed<boolean>(() => flattedTree.value.length > 0)
function flatTree(tree: SQTreeNode[]) {
let result = _.cloneDeep(tree)
_.forEach(tree, node => {
if (node.children && node.children.length > 0) {
result = _.union(result, flatTree(node.children))
}
})
return result
}
const afterTreeInit = () => {
state.pWeightMap = treeParentWeight(state.originResourceTree, rootManage.value ? 9 : 0)
mounted.value = true
if (selectedNodeKey.value && returnMounted.value) {
// @ts-ignore
expandedArray.value = getDefaultExpandedKeys()
returnMounted.value = false
}
nextTick(() => {
resourceListTree.value.setCurrentKey(selectedNodeKey.value)
resourceListTree.value.filter(filterText.value)
nextTick(() => {
// @ts-ignore
document.querySelector('.is-current')?.firstChild?.click()
})
})
}
const copyLoading = ref(false)
const emit = defineEmits(['nodeClick'])
function createNewObject() {
}
// @ts-ignore
const resourceEdit = resourceId => {
}
// @ts-ignore
const getParentKeys = (tree:any, targetKey:any, parentKeys = []) => {
for (const node of tree) {
if (node.id === targetKey) {
return parentKeys
}
if (node.children) {
const newParentKeys = [...parentKeys, node.id]
// @ts-ignore
const result = getParentKeys(node.children, targetKey, newParentKeys)
if (result) {
return result
}
}
}
return null
}
const getDefaultExpandedKeys = () => {
const parentKeys = getParentKeys(state.resourceTree, selectedNodeKey.value)
if (parentKeys) {
return [selectedNodeKey.value, ...parentKeys]
} else {
return []
}
}
watch(filterText, val => {
resourceListTree.value.filter(val)
})
const loadInit = () => {}
onMounted(() => {
loadInit()
getTree()
})
// @ts-ignore
const addOperation = ( cmd: string, data?: SQTreeNode, nodeType?: string, parentSelect?: boolean) => {
}
defineExpose({
rootManage,
hasData,
createNewObject,
mounted
})
</script>
<template>
<div class="resource-tree">
<div class="tree-header">
<div class="icon-methods" v-show="showPosition === 'preview'">
<span class="title"> Dashboard </span>
<div v-if="rootManage" class="flex-align-center">
<el-tooltip :content="'New Folder'" placement="top" effect="dark">
<el-icon
class="custom-icon btn"
style="margin-right: 20px"
>
<Icon name="dv-new-folder"><dvNewFolder class="svg-icon" /></Icon>
</el-icon>
</el-tooltip>
</div>
</div>
<el-input
:placeholder="'Search'"
v-model="filterText"
clearable
class="search-bar"
>
<template #prefix>
<el-icon>
<Icon name="icon_search-outline_outlined"
><icon_searchOutline_outlined class="svg-icon"
/></Icon>
</el-icon>
</template>
</el-input>
</div>
<el-scrollbar class="custom-tree" v-loading="copyLoading">
<el-tree
menu
ref="resourceListTree"
:default-expanded-keys="expandedArray"
:data="state.resourceTree"
:props="defaultProps"
node-key="id"
highlight-current
:expand-on-click-node="true"
:filter-node-method="filterNode"
@node-expand="nodeExpand"
@node-collapse="nodeCollapse"
@node-click="nodeClick"
@node-drag-start="handleDragStart"
@node-drop="handleDrop"
draggable
>
<template #default="{ data }">
<span class="custom-tree-node" :class="{ 'node-disabled-custom': data.extraFlag1 === 0 }">
<el-icon style="font-size: 18px" v-if="!data.leaf">
<Icon name="dv-folder"><dvFolder class="svg-icon" /></Icon>
</el-icon>
<el-icon
class="icon-screen-new color-dataV"
:class="{ 'color-dataV': data.extraFlag1, 'color-dataV-disabled': !data.extraFlag1 }"
style="font-size: 18px"
v-else
>
<Icon name="icon_operation-analysis_outlined"
><icon_operationAnalysis_outlined class="svg-icon"
/></Icon>
</el-icon>
<div class="icon-more">
<el-icon
v-on:click.stop
v-if="data.leaf"
class="hover-icon"
@click="resourceEdit(data.id)"
>
<Icon><icon_edit_outlined class="svg-icon" /></Icon>
</el-icon>
<HandleMore
@handle-command="
cmd => addOperation(cmd, data, cmd === 'newFolder' ? 'folder' : 'leaf')
"
:menu-list="resourceTypeList"
:icon-name="icon_add_outlined"
placement="bottom-start"
v-if="!data.leaf"
></HandleMore>
</div>
</span>
</template>
</el-tree>
</el-scrollbar>
</div>
</template>
<style lang="less" scoped>
.filter-icon-span {
border: 1px solid #bbbfc4;
width: 32px;
height: 32px;
border-radius: 4px;
color: #1f2329;
padding: 8px;
margin-left: 8px;
font-size: 16px;
cursor: pointer;
.opt-icon:focus {
outline: none !important;
}
&:hover {
background: #f5f6f7;
}
&:active {
background: #eff0f1;
}
}
.resource-tree {
padding: 16px 0 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.tree-header {
padding: 0 16px;
}
.icon-methods {
display: flex;
align-items: center;
justify-content: flex-end;
font-size: 20px;
font-weight: 500;
color: var(--TextPrimary, #1f2329);
padding-bottom: 16px;
.title {
margin-right: auto;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 24px;
}
.custom-icon {
font-size: 20px;
&.btn {
color: var(--ed-color-primary);
}
&:hover {
cursor: pointer;
}
}
}
.search-bar {
padding-bottom: 10px;
width: calc(100% - 40px);
}
}
.title-area {
margin-left: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.title-area-outer {
display: flex;
flex: 1 1 0%;
width: 0px;
}
.custom-tree-node-list {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding: 0 8px;
}
.father .child {
visibility: hidden;
}
.father:hover .child {
visibility: visible;
}
:deep(.ed-input__wrapper) {
width: 80px;
}
.custom-tree {
height: calc(100vh - 148px);
padding: 0 8px;
}
.custom-tree-node {
width: calc(100% - 30px);
display: flex;
align-items: center;
box-sizing: content-box;
padding-right: 4px;
.label-tooltip {
width: 100%;
margin-left: 8.75px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.icon-more {
margin-left: auto;
display: none;
}
&:hover {
.label-tooltip {
width: calc(100% - 78px);
}
.icon-more {
display: inline-flex;
}
}
.icon-screen-new {
border-radius: 4px;
color: #fff;
padding: 3px;
}
}
</style>
<style lang="less">
.menu-outer-dv_popper {
min-width: 140px;
margin-top: -2px !important;
.ed-icon {
border-radius: 4px;
}
}
.sort-type-normal {
i {
display: none;
}
}
.sort-type-checked {
color: var(--ed-color-primary);
i {
display: block;
}
}
.node-disabled-custom {
color: rgba(187, 191, 196, 1);
cursor: not-allowed;
}
.color-dataV-disabled {
background: #bbbfc4 !important;
}
</style>
@@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped lang="less">
</style>
@@ -0,0 +1,134 @@
<script setup lang="ts">
import dvInfoSvg from '@/assets/svg/dv-info.svg'
import icon_pc_outlined from '@/assets/svg/icon_pc_outlined.svg'
import icon_edit_outlined from '@/assets/svg/icon_edit_outlined.svg'
import DashboardDetailInfo from "@/views/dashboard/common/DashboardDetailInfo.vue";
import {dashboardStoreWithOut} from "@/stores/dashboard/dashboard.ts";
import {storeToRefs} from "pinia";
const emit = defineEmits(['reload', 'download', 'downloadAsAppTemplate'])
const dashboardStore = dashboardStoreWithOut()
const {dashboardInfo} = storeToRefs(dashboardStore)
const preview = () => {}
const edit = () =>{}
</script>
<template>
<div class="preview-head flex-align-center">
<div class="canvas-name ellipsis">{{ dashboardInfo.name }}</div>
<el-divider style="margin: 0 16px 0 7px" direction="vertical" />
<div class="create-area flex-align-center">
<span style="line-height: 22px"
>Creator:{{ dashboardInfo.creatorName }}</span
>
<el-popover show-arrow :offset="8" placement="bottom" width="400" trigger="hover">
<template #reference>
<el-icon class="info-tips"
><Icon name="dv-info"><dvInfoSvg class="svg-icon" /></Icon
></el-icon>
</template>
<DashboardDetailInfo></DashboardDetailInfo>
</el-popover>
</div>
<div class="canvas-opt-button">
<el-button secondary @click="preview">
<template #icon>
<icon name="icon_pc_outlined"><icon_pc_outlined class="svg-icon" /></icon>
</template>
Preview
</el-button>
<el-button class="custom-button" type="primary" @click="edit">
<template #icon>
<icon name="icon_edit_outlined"><icon_edit_outlined class="svg-icon" /></icon>
</template>
Edit</el-button
>
</div>
</div>
</template>
<style lang="less">
.pad12 {
.ed-dropdown-menu__item {
padding: 5px 36px 5px 12px !important;
.ed-icon {
margin-right: 8px;
}
.arrow-right_icon {
position: absolute;
right: 12px;
margin-right: 0;
}
&:has(.arrow-right_icon) {
width: 100%;
}
}
}
.preview-head {
width: 100%;
min-width: 300px;
height: 56px;
padding: 16px 24px;
border-bottom: 1px solid rgba(31, 35, 41, 0.15);
.canvas-name {
max-width: 200px;
font-size: 16px;
font-weight: 500;
}
.canvas-have-update {
background-color: rgba(52, 199, 36, 0.2);
color: rgba(44, 169, 31, 1);
font-weight: 400;
font-size: 12px;
line-height: 20px;
vertical-align: middle;
padding: 0 4px;
margin-left: 8px;
}
.custom-icon {
cursor: pointer;
margin-left: 8px;
}
.create-area {
color: #646a73;
font-weight: 400;
font-size: 14px;
}
.canvas-opt-button {
display: flex;
justify-content: right;
align-items: center;
flex: 1;
.head-more-icon {
color: #1f2329;
margin-left: 12px;
cursor: pointer;
font-size: 20px;
border-radius: 4px;
position: relative;
&:hover {
&::after {
content: '';
position: absolute;
top: -4px;
left: -4px;
border-radius: 4px;
height: 28px;
width: 28px;
background: #1f23291a;
}
}
}
}
}
.info-tips {
margin-left: 4px;
font-size: 16px;
color: #646a73;
}
.custom-button {
margin-left: 12px;
}
</style>
@@ -0,0 +1,246 @@
<script setup lang="ts">
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
import {reactive, ref, toRefs, onBeforeMount, computed} from 'vue'
import {initCanvasData} from '@/views/dashboard/utils/canvasUtils'
import {Icon} from '@/components/icon-custom'
import ResourceTree from "@/views/dashboard/common/ResourceTree.vue";
import SQPreview from "@/views/dashboard/preview/SQPreview.vue";
import SQPreviewHead from "@/views/dashboard/preview/SQPreviewHead.vue";
import EmptyBackground from "@/views/dashboard/common/EmptyBackground.vue";
import {dashboardStoreWithOut} from "@/stores/dashboard/dashboard.ts";
const dashboardStore = dashboardStoreWithOut()
const previewCanvasContainer = ref(null)
const dashboardPreview = ref(null)
const slideShow = ref(true)
const dataInitState = ref(true)
const downloadStatus = ref(false)
const state = reactive({
canvasDataPreview: null,
canvasStylePreview: null,
canvasViewInfoPreview: null,
dashboardInfo: null,
showOffset: {
top: 110,
left: 280
}
})
const props = defineProps({
showPosition: {
required: false,
type: String,
default: 'preview'
},
noClose: {
required: false,
type: Boolean,
default: false
}
})
const {showPosition} = toRefs(props)
const resourceTreeRef = ref()
const hasTreeData = computed(() => {
return resourceTreeRef.value?.hasData
})
const rootManage = computed(() => {
return resourceTreeRef.value?.rootManage
})
const mounted = computed(() => {
return resourceTreeRef.value?.mounted
})
function createNew() {
resourceTreeRef.value?.createNewObject()
}
const loadCanvasData = (dashboardId: string) => {
dataInitState.value = false
//@ts-ignore
initCanvasData({dashboardId},function ({dashboardInfo, canvasDataResult, canvasStyleResult, canvasViewInfoPreview }) {
state.canvasDataPreview = canvasDataResult
state.canvasStylePreview = canvasStyleResult
state.canvasViewInfoPreview = canvasViewInfoPreview
state.dashboardInfo = dashboardInfo
dataInitState.value = true
}
)
}
// @ts-ignore
const slideOpenChange = () => {
slideShow.value = !slideShow.value
}
const getPreviewStateInfo = () => {
return state
}
const reload = (dashboardId:string) => {
loadCanvasData(dashboardId)
}
const resourceNodeClick = (data:any) => {
loadCanvasData(data.id)
}
const previewShowFlag = computed(() => !!dashboardStore.dashboardInfo?.name)
onBeforeMount(() => {
if (showPosition.value === 'preview') {
dashboardStore.canvasDataInit()
}
})
const sideTreeStatus = ref(true)
// @ts-ignore
const changeSideTreeStatus = val => {
sideTreeStatus.value = val
}
const freezeStyle = computed(() => [
{'--top-show-offset': state.showOffset.top},
{'--left-show-offset': state.showOffset.left}
])
defineExpose({
getPreviewStateInfo
})
</script>
<template>
<div class="dv-preview dv-teleport-query" :style="freezeStyle">
<el-aside
class="resource-area"
:class="{ 'close-side': !slideShow, retract: !sideTreeStatus }"
ref="node"
style="width: 280px"
>
<resource-tree
ref="resourceTreeRef"
v-show="slideShow"
:cur-canvas-type="'dashboard'"
:show-position="showPosition"
@node-click="resourceNodeClick"
/>
</el-aside>
<el-container
class="preview-area"
:class="{ 'no-data': !state.dashboardInfo }"
v-loading="!dataInitState"
>
<template v-if="previewShowFlag">
<SQPreviewHead
v-if="showPosition === 'preview'"
@reload="reload"
/>
<div
ref="previewCanvasContainer"
class="content"
id="sq-preview-content"
>
<SQPreview
ref="dashboardPreview"
v-if="state.canvasStylePreview && dataInitState"
:dv-info="state.dashboardInfo"
:component-data="state.canvasDataPreview"
:canvas-style-data="state.canvasStylePreview"
:canvas-view-info="state.canvasViewInfoPreview"
:show-position="showPosition"
:download-status="downloadStatus"
:show-linkage-button="false"
></SQPreview>
</div>
</template>
<template v-else-if="hasTreeData && mounted">
<EmptyBackground :description="'Please Select Resource'" img-type="select"/>
</template>
<template v-else-if="mounted">
<EmptyBackground :description="'No Resource'" img-type="none">
<el-button v-if="rootManage" @click="createNew" type="primary">
<template #icon>
<Icon name="icon_add_outlined">
<icon_add_outlined class="svg-icon"/>
</Icon>
</template>
Create Dashboard
</el-button>
</EmptyBackground>
</template>
</el-container>
</div>
</template>
<style lang="less">
.dv-preview {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
background: #ffffff;
position: relative;
.resource-area {
position: relative;
height: 100%;
width: 279px;
padding: 0;
border-right: 1px solid #d7d7d7;
overflow: visible;
&.retract {
display: none;
}
}
.preview-area {
flex: 1;
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
position: relative;
//transition: 0.5s;
&.no-data {
background-color: rgba(245, 246, 247, 1);
}
.content {
position: relative;
display: flex;
width: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
align-items: center;
}
}
}
.close-side {
width: 0px !important;
padding: 0px !important;
}
.flexible-button-area {
position: absolute;
height: 60px;
width: 16px;
left: 0;
top: calc(50% - 30px);
background-color: #ffffff;
border-radius: 0 4px 4px 0;
cursor: pointer;
z-index: 10;
display: flex;
align-items: center;
border-top: 1px solid #d7d7d7;
border-right: 1px solid #d7d7d7;
border-bottom: 1px solid #d7d7d7;
}
</style>
@@ -0,0 +1,24 @@
import {dashboardApi} from "@/api/dashboard.ts";
export const initCanvasData = (params: any, callBack: Function) => {
dashboardApi.getDashboardInfo(params).then((res: any) => {
const canvasInfo = res.data;
const dashboardInfo = {
id: canvasInfo.id,
name: canvasInfo.name,
pid: canvasInfo.pid,
workspaceId: canvasInfo.workspaceId,
status: canvasInfo.status,
type: canvasInfo.type,
creatorName: canvasInfo.creatorName,
updateName: canvasInfo.updateName,
createTime: canvasInfo.createTime,
updateTime: canvasInfo.updateTime,
contentId: canvasInfo.contentId
}
const canvasDataResult = JSON.parse(canvasInfo.componentData)
const canvasStyleResult = JSON.parse(canvasInfo.canvasStyleData)
const canvasViewInfoPreview = canvasInfo.canvasViewInfo
callBack({dashboardInfo, canvasDataResult, canvasStyleResult, canvasViewInfoPreview})
})
}
@@ -0,0 +1,139 @@
import { cloneDeep } from 'lodash-es'
import {dashboardApi} from "@/api/dashboard.ts";
const treeDraggableChart = (state:any, key:any, type:any) => {
let dragNodeParentId = ''
let dragNodeId = ''
let dragNodeIndex = 0
let rawData:any[] = []
const dfsTreeNode = (arr:any, parentId:any) => {
arr.forEach((element:any, index:any) => {
if (element.id === dragNodeId) {
dragNodeIndex = index
dragNodeParentId = parentId
}
if (element.children?.length) {
dfsTreeNode(element.children, element.id)
}
})
}
const dfsTreeNodeSaveLevel = (arr:any[], idArr:any) => {
arr.forEach((element:any) => {
const index = idArr.findIndex((ele:any) => {
return ele === element.id
})
if (index !== -1) {
idArr.splice(index, 1)
}
if (element.children?.length && idArr.length === 2) {
dfsTreeNodeSaveLevel(element.children, idArr)
}
})
}
const dfsTreeNodeBack = (arr:any, parentId:any, params:any) => {
arr.forEach((element:any) => {
if (element.id === params.id) {
params.pid = parentId
}
if (element.children?.length) {
dfsTreeNodeBack(element.children, element.id, params)
}
})
}
const dfsTreeNodeReset = (arr:any, node:any) => {
arr.forEach((element:any) => {
if (element.id === dragNodeParentId) {
element.children.splice(dragNodeIndex, 0, node)
}
if (element.children?.length) {
dfsTreeNodeReset(element.children, node)
}
})
}
const dfsTreeNodeDel = (arr:any, node:any) => {
arr.forEach((element:any, index:any) => {
if (element.id === node.id) {
arr.splice(index, 1)
}
if (element.children?.length) {
dfsTreeNodeDel(element.children, node)
}
})
}
const handleDragStart = (node:any) => {
dragNodeId = node.data.id
rawData = cloneDeep(state[key])
dfsTreeNode(state[key], '0')
}
const allowDrop = (dropNode:any) => {
return !dropNode.data?.leaf
}
const handleDrop = async (draggingNode:any, dropNode:any, dropType:any) => {
const params = {
id: draggingNode.data?.id,
name: draggingNode.data?.name,
nodeType: draggingNode.data?.leaf ? 'leaf' : 'folder',
pid: '0',
opt: 'move',
type
}
if (dropType !== 'inner') {
const idArr = [params.id, dropNode.data?.id]
dfsTreeNodeSaveLevel(rawData, idArr)
if (idArr.length === 0) {
dfsTreeNodeDel(state[key], draggingNode.data)
setTimeout(() => {
if (dragNodeParentId === '0') {
state[key].splice(dragNodeIndex, 0, draggingNode.data)
} else {
dfsTreeNodeReset(state[key], draggingNode.data)
}
}, 0)
return
}
}
try {
await dashboardApi.sqNameCheck(params)
} catch (error) {
console.error(error)
}
// @ts-ignore
delete params.opt
if (dropType === 'inner') {
params.pid = dropNode.data?.id
} else {
dfsTreeNodeBack(state[key], '0', params)
}
dashboardApi.moveResource(params)
.then(() => {
state.originResourceTree = cloneDeep(state[key])
})
.catch(() => {
if (dragNodeParentId === '0') {
state[key].splice(dragNodeIndex, 0, draggingNode.data)
return
}
dfsTreeNodeReset(state[key], draggingNode.data)
})
}
return {
handleDrop,
allowDrop,
handleDragStart
}
}
export { treeDraggableChart }
@@ -0,0 +1,19 @@
export interface SQTreeNode {
id: string | number
pid: string | number
name: string
leaf?: boolean
weight: number
ext?: number
extraFlag: number
extraFlag1: number
children?: SQTreeNode[]
}
export interface SQTreeRequest {
treeFlag?: string
leaf?: boolean
weight?: number
sortType?: string
resourceTable?: string
}
@@ -0,0 +1,42 @@
import _ from 'lodash'
import type {SQTreeNode} from "@/views/dashboard/utils/treeNode";
export function treeParentWeight(tree: SQTreeNode[], pWeight:any) {
const pWeightResult = {}
weightCheckCircle(tree, pWeightResult, pWeight)
return pWeightResult
}
export function weightCheckCircle(tree: SQTreeNode[], pWeightResult:any, pWeight:any) {
_.forEach(tree, node => {
pWeightResult[node.id] = pWeight
if (node.children && node.children.length > 0) {
weightCheckCircle(node.children, pWeightResult, node.weight)
}
})
}
export default function treeSort(tree: SQTreeNode[], sortType: string) {
const result = _.cloneDeep(tree)
sortCircle(result, sortType)
return result
}
export function sortCircle(tree: SQTreeNode[], sortType: string) {
sortPer(tree, sortType)
_.forEach(tree, node => {
if (node.children && node.children.length > 0) {
sortCircle(node.children, sortType)
}
})
}
export const sortPer = (subTree: SQTreeNode[], sortType: string) => {
if (sortType === 'name_desc') {
subTree.sort((a, b) => b.name.localeCompare(a.name, 'zh-Hans-CN', { sensitivity: 'accent' }))
} else if (sortType === 'name_asc') {
subTree.sort((a, b) => a.name.localeCompare(b.name, 'zh-Hans-CN', { sensitivity: 'accent' }))
} else if (sortType === 'time_asc') {
subTree.reverse()
}
}