feat: plugin flexible layout (#1219)

This commit is contained in:
lisong 2025-03-20 10:29:17 +08:00 committed by GitHub
parent 20c3b071bd
commit 8d31bd3ed4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 2257 additions and 842 deletions

View File

@ -1,5 +1,5 @@
<template>
<component :is="CanvasLayout">
<component :is="CanvasLayout" :class="{ 'not-selected': getMoveDragBarState() }">
<template #header>
<component v-if="!isBlock()" :is="CanvasRouteBar"></component>
</template>
@ -67,6 +67,8 @@ export default {
const { canvasSrc = '' } = getOptions(meta.id) || {}
const canvasSrcDoc = ref('')
const { getMoveDragBarState, getFixedPanelsStatus, closePlugin, closeSetting } = useLayout()
useMessage().subscribe({
topic: 'init_canvas_deps',
subscriber: 'canvas_design_canvas',
@ -151,9 +153,17 @@ export default {
)
const nodeSelected = (node, parent, type, id) => {
const { leftPanelFixed, rightPanelFixed } = getFixedPanelsStatus()
const { toolbars } = useLayout().layoutState
if (type !== 'clickTree') {
useLayout().closePlugin()
if (!leftPanelFixed) {
closePlugin()
}
if (!rightPanelFixed) {
closeSetting(true)
}
}
const { getSchema, getNodePath } = useCanvas()
@ -275,6 +285,7 @@ export default {
useNotify
},
isBlock,
getMoveDragBarState,
CanvasLayout,
canvasRef,
CanvasContainer,
@ -284,3 +295,10 @@ export default {
}
}
</script>
<style lang="less" scoped>
.not-selected {
pointer-events: none;
user-select: none;
}
</style>

View File

@ -92,7 +92,7 @@ export default {
watch(() => useLayout().getDimension().width, setScale, { flush: 'post', immediate: true })
watch(() => useLayout().getPluginState().fixedPanels, setScale, { flush: 'post' })
watch(() => useLayout().leftFixedPanelsStorage.value, setScale, { flush: 'post' })
watch(
() => useLayout().getPluginState().render,
@ -106,6 +106,20 @@ export default {
{ flush: 'post' }
)
watch(() => useLayout().rightFixedPanelsStorage.value, setScale, { flush: 'post' })
watch(
() => useLayout().getSettingState().render,
(value) => {
const currentFixed = useLayout().rightFixedPanelsStorage.value.includes(value)
if (!value || currentFixed) {
setScale()
}
},
{ flush: 'post' }
)
watch(
() => sizeStyle.value,
() => {

View File

@ -1,6 +1,6 @@
<template>
<div class="plugin-panel">
<div class="plugin-panel-header">
<div class="plugin-panel" ref="panel" :style="{ width: panelWidth + 'px' }">
<div :class="['plugin-panel-header', headerBottomLine]">
<div class="plugin-panel-title">
<span class="title"
>{{ title }}<link-button class="link" v-if="isShowDocsIcon" :href="docsUrl"></link-button
@ -9,22 +9,53 @@
</div>
<div class="plugin-panel-icon">
<slot name="header"></slot>
<tiny-tooltip
v-if="isShowCollapseIcon"
effect="light"
:content="isCollapsed ? '展开' : '折叠'"
placement="top"
:visible-arrow="false"
>
<template #default>
<svg-button :name="settingIcon" @click="clickCollapseIcon"></svg-button>
</template>
</tiny-tooltip>
<svg-button
class="item icon-sidebar"
:name="fixedPanels?.includes(fixedName) ? 'fixed-solid' : 'fixed'"
:tips="!fixedPanels?.includes(fixedName) ? '固定面板' : '解除固定面板'"
@click="fixPanel"
></svg-button>
<close-icon v-if="!isCloseLeft" :name="name" @close="closePanel"></close-icon>
</div>
</div>
<slot name="content"></slot>
<div class="scroll-content">
<slot name="content"></slot>
</div>
<div v-if="isWidthResizable">
<div class="resizer-right" v-if="isLeftResizer" @mousedown="onMouseDownRight"></div>
<div class="resizer-left" v-if="isRightResizer" @mousedown="onMouseDownLeft"></div>
</div>
</div>
</template>
<script>
import { useThrottleFn } from '@vueuse/core'
import { inject, ref, computed, onMounted, provide } from 'vue'
import { useLayout } from '@opentiny/tiny-engine-meta-register'
import { SvgButton } from '@opentiny/tiny-engine-common'
import { constants } from '@opentiny/tiny-engine-utils'
import LinkButton from './LinkButton.vue'
import CloseIcon from './CloseIcon.vue'
import { Tooltip } from '@opentiny/vue'
export default {
components: {
TinyTooltip: Tooltip,
LinkButton,
CloseIcon
CloseIcon,
SvgButton
},
props: {
/**
@ -52,18 +83,151 @@ export default {
isShowDocsIcon: {
type: Boolean,
default: false
},
/**
* 固定面板插件数组
*/
fixedPanels: {
type: Array
},
/**
* 固定面板标识
*/
fixedName: {
type: String
},
/**
* 是否展示标题下边线
*/
showBottomBorder: {
type: Boolean,
default: false
},
/**
* 是否展示折叠按钮
*/
isShowCollapseIcon: {
type: Boolean,
default: false
}
},
emits: ['close'],
emits: ['close', 'updateCollapseStatus'],
setup(props, { emit }) {
const closePanel = () => {
useLayout().closePlugin()
emit('close')
}
const { PLUGIN_DEFAULT_WIDTH } = constants
const MIN_WIDTH = PLUGIN_DEFAULT_WIDTH //
const MAX_WIDTH = 1000 //
const panel = ref(null)
let startX = 0
let startWidth = 0
const isCollapsed = ref(false)
const settingIcon = computed(() => (isCollapsed.value ? 'collapse_all' : 'expand_all'))
provide('isCollapsed', isCollapsed)
const panelState = inject('panelState')
const fixPanel = () => {
panelState.emitEvent('fixPanel', props.fixedName)
}
const headerBottomLine = computed(() => (props.showBottomBorder ? 'header-bottom-line' : ''))
const { getPluginWidth, changePluginWidth, getPluginByLayout, changeMoveDragBarState, isPanelWidthResizable } =
useLayout()
const align = ref(getPluginByLayout(props.fixedName)) //
const panelWidth = ref(getPluginWidth(props.fixedName)) // 使
const isLeftResizer = ref(align.value.includes('left'))
const isRightResizer = ref(align.value.includes('right'))
const isWidthResizable = computed(() => isPanelWidthResizable(props.fixedName))
const onMouseMoveRight = (event) => {
const newWidth = startWidth + (event.clientX - startX)
panelWidth.value = Math.max(MIN_WIDTH, Math.min(newWidth, MAX_WIDTH))
changePluginWidth(props.fixedName, panelWidth.value)
}
const onMouseMoveLeft = (event) => {
const newWidth = startWidth - (event.clientX - startX)
panelWidth.value = Math.max(MIN_WIDTH, Math.min(newWidth, MAX_WIDTH))
changePluginWidth(props.fixedName, panelWidth.value)
}
const throttledMouseMoveRight = useThrottleFn(onMouseMoveRight, 50)
const throttledMouseMoveLeft = useThrottleFn(onMouseMoveLeft, 50)
const leftResizer = ref(null)
const rightResizer = ref(null)
const onMouseUpRight = () => {
changeMoveDragBarState(false)
document.removeEventListener('mousemove', throttledMouseMoveRight)
document.removeEventListener('mouseup', onMouseUpRight)
rightResizer.value.style.cursor = ''
rightResizer.value.classList.remove('dragging')
}
const onMouseDownRight = (event) => {
changeMoveDragBarState(true)
startX = event.clientX
startWidth = panel.value.offsetWidth
document.addEventListener('mousemove', throttledMouseMoveRight)
document.addEventListener('mouseup', onMouseUpRight)
rightResizer.value.style.cursor = 'ew-resize'
rightResizer.value.classList.add('dragging')
}
const onMouseUpLeft = () => {
changeMoveDragBarState(false)
document.removeEventListener('mousemove', throttledMouseMoveLeft)
document.removeEventListener('mouseup', onMouseUpLeft)
leftResizer.value.style.cursor = ''
leftResizer.value.classList.remove('dragging')
}
const onMouseDownLeft = (event) => {
changeMoveDragBarState(true)
startX = event.clientX
startWidth = panel.value.offsetWidth
document.addEventListener('mousemove', throttledMouseMoveLeft)
document.addEventListener('mouseup', onMouseUpLeft)
leftResizer.value.style.cursor = 'ew-resize'
leftResizer.value.classList.add('dragging')
}
const initResizerDOM = () => {
leftResizer.value = document.querySelector('.resizer-left')
rightResizer.value = document.querySelector('.resizer-right')
}
const clickCollapseIcon = () => {
isCollapsed.value = !isCollapsed.value
emit('updateCollapseStatus', isCollapsed.value)
}
onMounted(() => {
initResizerDOM()
})
return {
closePanel
isWidthResizable,
headerBottomLine,
clickCollapseIcon,
isCollapsed,
settingIcon,
closePanel,
fixPanel,
panel,
panelWidth,
onMouseDownRight,
onMouseDownLeft,
isLeftResizer,
isRightResizer
}
}
}
@ -88,12 +252,15 @@ export default {
padding: 12px;
color: var(--te-component-common-text-color-primary);
font-weight: var(--te-base-font-weight-7);
.plugin-panel-title {
display: flex;
align-items: center;
.title + .icon-wrap {
margin-left: 10px;
}
.title {
display: flex;
align-items: center;
@ -109,10 +276,65 @@ export default {
:deep(.svg-button + .svg-button) {
margin-left: 4px;
}
:deep(.svg-button + .icon-wrap) {
margin-left: 4px;
}
}
}
}
// 线
.resizer-right {
position: absolute;
top: 0;
right: 0;
width: 1px;
height: 100%;
cursor: ew-resize;
background-color: rgba(0, 0, 0, 0.1);
transition: width 0.3s ease;
}
.header-bottom-line {
border-bottom: 1px solid var(--te-common-border-divider);
}
.dragging {
width: 2px !important;
background-color: var(--te-component-common-resizer-border-color) !important;
}
.resizer-right:hover {
width: 2px;
background-color: var(--te-component-common-resizer-border-color);
}
// 线
.resizer-left {
position: absolute;
top: 0;
left: 0;
width: 1px;
height: 100%;
cursor: ew-resize;
background-color: rgba(0, 0, 0, 0.1);
transition: width 0.3s ease;
}
.resizer-left:hover {
width: 2px;
background-color: var(--te-component-common-resizer-border-color);
}
.scroll-content {
height: 100%;
overflow: auto;
scrollbar-width: none;
-ms-overflow-style: none;
}
.scroll-content::-webkit-scrollbar {
display: none;
}
</style>

View File

@ -0,0 +1,179 @@
<template>
<ul v-if="false" class="plugin-context-menu" :style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }">
<li v-if="contextMenu.type" @click="hidePlugin">隐藏 "{{ contextMenu.item.title }}"</li>
<li v-if="contextMenu.type" class="bottom-li" @click="switchAlign">
切换到{{ align.includes('right') ? '左侧' : '右侧' }}
</li>
<li
v-for="(item, index) in list"
:key="index"
@click.stop="changeShowState(item.id)"
class="menu-item-wrapper"
:class="{
'bottom-li': index === list.length - 1
}"
>
<span class="check-mark">
<span v-show="getPluginShown(item.id)"></span>
</span>
<span>{{ item.title }}</span>
</li>
</ul>
</template>
<script>
import { reactive, onMounted, onBeforeUnmount, nextTick, ref } from 'vue'
import { useLayout } from '@opentiny/tiny-engine-meta-register'
export default {
props: {
list: {
type: Array
},
align: {
type: String,
default: 'left'
}
},
emits: ['close', 'switchAlign'],
setup(props, { emit }) {
const { getPluginShown, changePluginShown, changeMenuShown } = useLayout()
const pluginGroupPosition = ref('')
const contextMenu = reactive({
type: true,
visible: false,
x: 0,
y: 0,
item: null,
index: null,
list: null
})
//
const contextMenuWidth = props.align.includes('right') ? 130 : 0
const showContextMenu = (x, y, type, item, index, position) => {
const windowHeight = window.innerHeight
contextMenu.type = type
contextMenu.visible = true
nextTick(() => {
const pluginMenuPanel = document.querySelector('.plugin-context-menu')
const menuHeight = pluginMenuPanel?.offsetHeight
const spaceBelow = windowHeight - y - 20
if (menuHeight && spaceBelow < menuHeight) {
contextMenu.y = y - menuHeight
} else {
contextMenu.y = y
}
contextMenu.x = x - contextMenuWidth
})
if (type) {
contextMenu.item = item
contextMenu.index = index
pluginGroupPosition.value = position
}
}
//
const hideContextMenu = () => {
contextMenu.visible = false
}
//
const hidePlugin = () => {
emit('close')
changePluginShown(contextMenu.item.id)
hideContextMenu()
}
//
const switchAlign = () => {
emit('close')
emit('switchAlign', contextMenu.index, contextMenu.item.id, pluginGroupPosition.value)
hideContextMenu()
}
//
const hideSidebar = () => {
const align = props.align.includes('right') ? 'right' : 'left'
changeMenuShown(align)
hideContextMenu()
}
//
const changeShowState = (pluginName) => {
changePluginShown(pluginName)
}
const handleClickOutside = (event) => {
if (!event.target.closest('.plugin-context-menu')) {
hideContextMenu()
}
}
onMounted(() => {
document.addEventListener('click', handleClickOutside)
})
onBeforeUnmount(() => {
document.removeEventListener('click', handleClickOutside)
})
return {
contextMenu,
showContextMenu,
changeShowState,
getPluginShown,
hidePlugin,
switchAlign,
hideSidebar
}
}
}
</script>
<style scoped>
/* 引入B的CSS样式 */
.plugin-context-menu {
position: absolute;
background: white;
border: 1px solid #ccc;
list-style: none;
width: 135px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 1000;
}
.plugin-context-menu-header {
padding: 8px 12px;
font-weight: bold;
cursor: default;
background: #f5f5f5;
border-bottom: 1px solid #ccc;
}
.bottom-li {
border-bottom: 1px solid #ccc;
}
.plugin-context-menu li {
padding: 8px 12px;
cursor: pointer;
}
.plugin-context-menu li:hover {
background: #f0f0f0;
}
.menu-item-wrapper {
display: flex;
flex-grow: 1;
}
.check-mark {
width: 20px;
text-align: left;
}
</style>

View File

@ -1,6 +1,14 @@
<template>
<div
:class="['plugin-setting', { 'second-panel': isSecond }, { 'full-screen': state.isFullScreen }]"
id="panel-setting"
:class="[
'plugin-setting',
{ 'second-panel': isSecond },
{ 'full-screen': state.isFullScreen },
{ 'align-right': align.includes('right') },
shadowClass
]"
:style="alignStyle"
@click="$emit('click')"
>
<div class="plugin-setting-header">
@ -31,11 +39,12 @@
</template>
<script>
import { reactive, watchEffect } from 'vue'
import { nextTick, reactive, watchEffect, computed } from 'vue'
import { Button } from '@opentiny/vue'
import { iconPlus } from '@opentiny/vue-icon'
import ButtonGroup from './ButtonGroup.vue'
import SvgButton from './SvgButton.vue'
import { useLayout } from '@opentiny/tiny-engine-meta-register'
const EVENTS = {
FULL_SCREEN_CHANGE: 'fullScreenChange',
@ -91,6 +100,14 @@ export default {
icon: {
type: Object,
default: iconPlus()
},
fixedName: {
type: String,
default: ''
},
align: {
type: String,
default: 'leftTop'
}
},
emits: [EVENTS.FULL_SCREEN_CHANGE, EVENTS.SAVE, EVENTS.CANCEL, EVENTS.ADD, EVENTS.CLICK],
@ -99,6 +116,28 @@ export default {
isFullScreen: false
})
const { getPluginWidth } = useLayout()
const firstPanelOffset = computed(() => {
return getPluginWidth(props.fixedName) + 1
})
const secondPanelAlign = computed(() => {
return props.align.includes('left') ? 'left' : 'right'
})
const alignStyle = computed(() => {
return `${secondPanelAlign.value} : ${firstPanelOffset.value}px`
})
watchEffect(() => {
//
const secondPanelOffset = document.querySelector('.plugin-setting')?.clientWidth + firstPanelOffset.value
nextTick(() => {
document.querySelector('.second-panel')?.style.setProperty(secondPanelAlign.value, `${secondPanelOffset}px`)
})
})
watchEffect(() => {
state.isFullScreen = props.isFullScreen
})
@ -112,7 +151,16 @@ export default {
return isFullScreen ? '收起' : '全屏查看'
}
//
const shadowClass = computed(() => {
if (props.isSecond) return ''
return props.align.includes('right') ? 'shadow-right' : 'shadow-left'
})
return {
alignStyle,
shadowClass,
firstPanelOffset,
state,
fullScreen,
getFullScreenLabel
@ -124,7 +172,6 @@ export default {
<style lang="less" scoped>
.plugin-setting {
position: absolute;
left: var(--base-left-panel-width);
top: 0;
width: var(--base-collection-panel-width);
height: 100%;
@ -132,17 +179,24 @@ export default {
background: var(--te-component-common-bg-color);
overflow: hidden;
border-left: 1px solid var(--te-component-common-border-color-divider);
&:not(.second-panel) {
box-shadow: 6px 0px 3px 0px var(--te-component-common-shadow-color);
border-right: none;
border-left: none;
&.shadow-left {
box-shadow: 6px 0px 3px 0px var(--te-component-common-shadow-color);
border-right: none;
}
&.shadow-right {
box-shadow: -6px 0px 3px 0px var(--te-component-common-shadow-color);
border-left: none;
}
}
&.full-screen {
width: var(--base-collection-panel-full-screen-width);
}
&.second-panel {
left: calc(var(--base-left-panel-width) + var(--base-collection-panel-width));
z-index: 1;
}
@ -167,6 +221,7 @@ export default {
color: var(--te-component-common-text-color-primary);
padding: 0 12px;
border-bottom: 1px solid var(--te-component-common-border-color-divider);
.plugin-setting-header-title {
font-size: 12px;
font-weight: 700;
@ -175,9 +230,11 @@ export default {
text-overflow: ellipsis;
white-space: nowrap;
}
:deep(.svg-button + .svg-button) {
margin: 0;
}
:deep(.tiny-button.tiny-button) {
margin-right: 0;
}
@ -195,4 +252,8 @@ export default {
align-items: center;
}
}
.align-right {
right: 0;
}
</style>

View File

@ -14,6 +14,7 @@ import ConfigGroup from './ConfigGroup.vue'
import ConfigItem from './ConfigItem.vue'
export { default as PluginSetting } from './PluginSetting.vue'
export { default as PluginPanel } from './PluginPanel.vue'
export { default as PluginRightMenu } from './PluginRightMenu.vue'
export { default as SvgButton } from './SvgButton.vue'
export { default as LinkButton } from './LinkButton.vue'
export { default as ConfigCollapse } from './ConfigCollapse.vue'

View File

@ -44,13 +44,14 @@
"eslint-linter-browserify": "8.57.0",
"monaco-editor": "0.51.0",
"prettier": "2.7.1",
"vue-draggable-next": "2.1.0"
"vue-draggable-next": "2.2.1"
},
"devDependencies": {
"@opentiny/tiny-engine-vite-plugin-meta-comments": "workspace:*",
"@vitejs/plugin-vue": "^5.1.2",
"@vitejs/plugin-vue-jsx": "^4.0.1",
"glob": "^10.3.4",
"@vueuse/core": "^9.6.0",
"vite": "^5.4.2"
},
"peerDependencies": {

View File

@ -28,6 +28,8 @@
--te-component-common-border-color-transparent: var(--te-base-color-transparent); // 透明色直接使用base的变量
--te-component-common-button-border-color: var(--te-common-border-secondary);
--te-component-common-resizer-border-color: var(--te-base-blue-70);
--te-component-common-shadow-color: var(--te-common-shadow-panel);
--te-component-config-item-bind-bg-color: var(--te-common-bg-info);

View File

@ -32,7 +32,7 @@
"@opentiny/tiny-engine-utils": "workspace:*",
"@vue/shared": "^3.3.4",
"monaco-editor": "0.51.0",
"vue-draggable-next": "2.1.0"
"vue-draggable-next": "2.2.1"
},
"publishConfig": {
"access": "public"

View File

@ -1,18 +1,26 @@
<svg
data-icon="DisplayInline"
aria-hidden="true"
focusable="false"
width="16"
height="16"
viewBox="0 0 16 16"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
style="display: block"
>
<path opacity=".6" fill-rule="evenodd" clip-rule="evenodd" d="M1 2h1v12H1V2zm14 0h-1v12h1V2z"></path>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7.25 3L3.5 13h2l1-3h3l1 3h2L8.75 3h-1.5zm1.917 6L8 5.5 6.833 9h2.334z"
></path>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 132 132" style="enable-background:new 0 0 132 132;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:currentColor;}
</style>
<rect id="size" class="st0" width="132" height="132"/>
<path id="矢量_28" class="st1" d="M8.2,66C8.2,34.1,34.1,8.2,66,8.2c31.9,0,57.8,25.8,57.8,57.8c0,15.9-35.8,0.9-39.9,19.2
c-0.7,3,1.1,8.8,3,14.8c2.7,8.6,5.4,17.6,1.3,19.3c-6.8,2.8-14.3,4.4-22.1,4.4C34.1,123.8,8.2,97.9,8.2,66z M46.7,20.4
c2.7-1.1,5.4-2,8.2-2.6c3.6-0.8,7.3-1.2,11.1-1.2c3.8,0,7.5,0.4,11.1,1.2c2.8,0.6,5.5,1.5,8.2,2.6c2.8,1.2,5.3,2.6,7.8,4.2
c2.8,1.8,5.5,4,7.9,6.4s4.6,5.1,6.5,7.9v0c1.6,2.5,3,5.1,4.2,7.8c1.1,2.7,2,5.4,2.7,8.2l0,0c0.8,3.4,1.2,6.8,1.2,10.4
c-0.8,0.3-2,0.5-3.6,0.7c-1.2,0.1-3.2,0.3-6.1,0.4c-3.4,0.2-6,0.4-7.8,0.6c-3.2,0.4-6.1,1-8.5,1.8c-3.4,1.1-6.2,2.8-8.4,5
c-0.9,0.9-1.7,1.9-2.4,2.9c-1.4,2-2.3,4.3-2.9,6.8c-0.3,1.6-0.4,3.3-0.2,5.3c0.1,1.3,0.4,2.9,0.8,4.6c0.4,1.9,1.3,5,2.6,9.1
c0.9,2.8,1.5,4.8,1.8,6.1c0.5,1.7,0.8,3.1,1,4.3c-1.6,0.5-3.2,1-4.8,1.4c-3.5,0.8-7.2,1.2-10.9,1.2c-3.8,0-7.5-0.4-11.1-1.2
c-2.8-0.6-5.5-1.5-8.2-2.7c-2.8-1.2-5.3-2.6-7.8-4.2c-2.8-1.9-5.5-4-7.9-6.5s-4.6-5.1-6.5-7.9c-1.6-2.5-3-5.1-4.2-7.8
c-1.1-2.7-2-5.4-2.6-8.2c-0.8-3.6-1.2-7.3-1.2-11.1c0-3.8,0.4-7.5,1.2-11.1c0.6-2.8,1.5-5.5,2.6-8.2c1.2-2.8,2.6-5.3,4.2-7.8
c1.8-2.8,4-5.5,6.4-7.9s5.1-4.6,7.9-6.4l0,0l0,0l0,0C41.4,22.9,44,21.5,46.7,20.4z M33,45.4C33,38.5,38.5,33,45.4,33
s12.4,5.5,12.4,12.4s-5.5,12.4-12.4,12.4S33,52.2,33,45.4z M86.6,33c-6.8,0-12.4,5.5-12.4,12.4s5.5,12.4,12.4,12.4
c6.8,0,12.4-5.5,12.4-12.4S93.4,33,86.6,33z M41.2,45.4c0-2.3,1.8-4.1,4.1-4.1c2.3,0,4.1,1.8,4.1,4.1c0,2.3-1.8,4.1-4.1,4.1
C43.1,49.5,41.2,47.7,41.2,45.4z M86.6,41.2c-2.3,0-4.1,1.8-4.1,4.1c0,2.3,1.8,4.1,4.1,4.1c2.3,0,4.1-1.8,4.1-4.1
C90.8,43.1,88.9,41.2,86.6,41.2z M45.4,74.2c-6.8,0-12.4,5.5-12.4,12.4C33,93.4,38.5,99,45.4,99s12.4-5.6,12.4-12.4
C57.8,79.8,52.2,74.2,45.4,74.2z M45.4,82.5c-2.3,0-4.1,1.8-4.1,4.1c0,2.3,1.8,4.1,4.1,4.1c2.3,0,4.1-1.8,4.1-4.1
C49.5,84.3,47.7,82.5,45.4,82.5z"/>
</svg>

Before

Width:  |  Height:  |  Size: 459 B

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1,17 +1,17 @@
<svg version="1.1" id="图层_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 64 64" style="enable-background:new 0 0 64 64;" xml:space="preserve">
<style type="text/css">
.st0{fill:currentcolor;}
</style>
<g>
<path class="st0" d="M57.5,9.2h-51c-1.1,0-2,0.9-2,2v39.7c0,1.1,0.9,2,2,2h51.1c1.1,0,2-0.9,2-2V11.2C59.5,10.1,58.7,9.2,57.5,9.2z
M55.5,48.9h-47V13.2h47.1L55.5,48.9L55.5,48.9z"/>
<rect x="14.3" y="20.6" class="st0" width="5.3" height="3"/>
<rect x="25.1" y="20.6" class="st0" width="24.5" height="3"/>
<rect x="14.3" y="29.6" class="st0" width="5.3" height="3"/>
<rect x="25.1" y="29.6" class="st0" width="24.5" height="3"/>
<rect x="14.3" y="38.5" class="st0" width="5.3" height="3"/>
<rect x="25.1" y="38.5" class="st0" width="24.5" height="3"/>
</g>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 132 132" style="enable-background:new 0 0 132 132;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
.st1{fill:currentColor;}
</style>
<rect id="size" class="st0" width="132" height="132"/>
<path id="形状结合" class="st1" d="M119.6,33h-12.9c-2.1-9.6-10.3-16.5-20.1-16.5c-9.9,0-18.2,6.9-20.2,16.5h-54
c-2.3,0-4.1,1.8-4.1,4.1c0,2.2,1.8,4.1,4.1,4.1h54c2.1,9.5,10.3,16.5,20.2,16.5c9.8,0,18.1-7,20.1-16.5h12.9c2.2,0,4.1-1.9,4.1-4.1
C123.8,34.8,121.8,33,119.6,33z M86.6,49.5c-7,0-12.4-5.5-12.4-12.4c0-7,5.4-12.4,12.4-12.4c6.9,0,12.4,5.4,12.4,12.4
C99,44,93.6,49.5,86.6,49.5z M12.4,99h12.8c2.1,9.5,10.3,16.5,20.2,16.5c9.8,0,18.1-7,20.1-16.5h54.1c2.2,0,4.1-1.9,4.1-4.1
c0-2.3-1.9-4.1-4.1-4.1H65.5c-2.1-9.6-10.3-16.5-20.1-16.5c-9.9,0-18.1,6.9-20.2,16.5H12.4c-2.3,0-4.1,1.8-4.1,4.1
C8.2,97.1,10.1,99,12.4,99z M45.4,82.5c6.9,0,12.4,5.4,12.4,12.4c0,6.9-5.5,12.4-12.4,12.4c-7,0-12.4-5.4-12.4-12.4
C33,87.9,38.4,82.5,45.4,82.5z"/>
</svg>

Before

Width:  |  Height:  |  Size: 877 B

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,3 +1,25 @@
<svg data-icon="Target" aria-hidden="true" focusable="false" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 2H7v2H4v3H2v2h2v3h3v2h2v-2h3V9h2V7h-2V4H9V2zm1 5V6H6v4h4V7z" fill="currentColor"></path>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 132 132" style="enable-background:new 0 0 132 132;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:currentColor;}
</style>
<rect id="size" class="st0" width="132" height="132"/>
<path id="矢量_29" class="st1" d="M79.1,13.5C76.7,9.4,72.6,7,68,7H53.3l-1.5,0.2l-1.3,0.2c-3.9,1.1-6.9,3.7-8.5,7.3l-3.5,8
l-1.7,1l-8.7-1l-1.2-0.1c-4.7,0-8.9,2.3-11.2,6.3L8.2,41.7L7.6,43l-0.4,1.2c-1,3.9-0.2,7.8,2.1,11l5.3,7v1.9l-5.3,7l-0.8,1.2
C6.2,76.4,6.2,81.1,8.4,85l7.4,12.8l0.8,1.1l0.9,1.1c2.8,2.8,6.7,4.1,10.5,3.6l8.7-1.1l1.7,1.1l3.5,8l0.6,1.1
c2.4,4.1,6.5,6.5,11,6.5h7.3v-8.2h-7.3c-1.6,0-3.4-1.5-4.1-2.9l-3.9-9.1c-0.4-0.8-1-1.5-1.7-2l-4.1-2.3c-0.8-0.5-1.6-0.7-2.6-0.6
L27,95.5c-1.5,0.2-3.4-1.1-4.3-2.2l-7.3-12.4c-0.8-1.3-0.2-3.6,0.5-5l5.9-7.9c0.5-0.7,0.8-1.6,0.8-2.5v-4.7c0-0.9-0.3-1.7-0.8-2.5
l-6.1-8.1c-0.9-1.2-0.8-3.5-0.2-4.9L22.8,33c0.8-1.3,3-2.1,4.5-2.1l9.9,1.2c0.9,0.1,1.7-0.1,2.6-0.6l4.1-2.3c0.7-0.5,1.3-1.2,1.7-2
l4-9.3c0.6-1.3,2.6-2.4,4.1-2.6H68c1.6,0,3.4,1.5,4.1,2.9l3.9,9.1c0.3,0.8,1,1.5,1.7,2l4.1,2.3c0.8,0.5,1.7,0.7,2.6,0.6l10.1-1.2
c1.5-0.2,3.3,1.1,4.3,2.2l7.3,12.4c0.8,1.3,0.2,3.6-0.5,5l-5.9,7.9c-0.6,0.7-0.8,1.6-0.8,2.5v4.7c0,0.9,0.2,1.8,0.8,2.5l2.6,3.5
h10.3c-0.1-0.1-0.2-0.2-0.2-0.3l-5.3-7v-1.9l5.3-7l0.8-1.2c2.3-4.1,2.3-8.8,0.1-12.7l-7.4-12.8l-0.8-1.2l-0.9-1.1
c-2.8-2.8-6.7-4.1-10.5-3.6l-8.7,1l-1.8-1l-3.4-8L79.1,13.5z M82.7,63.2c0,2.9-0.6,5.7-1.6,8.2h-9.5c1.8-2.3,2.9-5.1,2.9-8.2
c0-7.4-6-13.4-13.4-13.4s-13.3,5.9-13.3,13.4c0,7.3,5.8,13.2,13,13.3v8.2C49.1,84.6,39.6,75,39.6,63.2c0-12,9.7-21.6,21.5-21.6
C73.1,41.6,82.7,51.3,82.7,63.2z M126.9,83.8c0-2.3-1.9-4.1-4.1-4.1H73.2c-2.3,0-4.1,1.8-4.1,4.1c0,2.2,1.8,4.1,4.1,4.1h49.5
C125,88,126.9,86.1,126.9,83.8z M122.7,96.2c2.2,0,4.1,1.8,4.1,4.1c0,2.2-1.9,4.1-4.1,4.1H73.2c-2.3,0-4.1-1.9-4.1-4.1
c0-2.3,1.8-4.1,4.1-4.1H122.7z M126.9,116.8c0-2.3-1.9-4.1-4.1-4.1H73.2c-2.3,0-4.1,1.8-4.1,4.1c0,2.2,1.8,4.1,4.1,4.1h49.5
C125,121,126.9,119.1,126.9,116.8z"/>
</svg>

Before

Width:  |  Height:  |  Size: 280 B

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -26,7 +26,8 @@
"@opentiny/tiny-engine-common": "workspace:*",
"@opentiny/tiny-engine-meta-register": "workspace:*",
"@opentiny/tiny-engine-utils": "workspace:*",
"@vueuse/core": "^9.6.0"
"@vueuse/core": "^9.6.0",
"vue-draggable-next": "2.2.1"
},
"devDependencies": {
"@opentiny/tiny-engine-vite-plugin-meta-comments": "workspace:*",

View File

@ -1,7 +1,15 @@
<!-- 左侧插件栏-->
<template>
<div id="tiny-engine-nav-panel" :style="{ 'pointer-events': pluginState.pluginEvent }">
<ul class="nav-panel-lists top">
<li
<vue-draggable-next
v-model="state.topNavLists"
filter="EditorHelp"
class="nav-panel-lists top"
id="leftTop"
group="plugins"
@end="onEnd"
>
<div
v-for="(item, index) in state.topNavLists"
:key="index"
:class="{
@ -12,8 +20,9 @@
}"
:title="item.title"
@click="clickMenu({ item, index })"
@contextmenu.prevent="showContextMenu($event, true, item, index, PLUGIN_POSITION.leftTop)"
>
<div>
<div v-if="getPluginShown(item.id)">
<span class="item-icon">
<svg-icon
v-if="typeof iconComponents[item.id] === 'string'"
@ -23,65 +32,80 @@
<component v-else :is="iconComponents[item.id]" class="panel-icon"></component>
</span>
</div>
</li>
</ul>
<ul class="nav-panel-lists bottom">
<li style="flex: 1" class="list-item"></li>
<li
v-for="(item, index) in state.bottomNavLists"
:key="index"
:class="[
'list-item',
{ active: renderPanel === item.id, prev: state.prevIdex - 1 === index, 'first-item': index === 0 }
]"
:title="item.title"
@click="clickMenu({ item, index })"
>
<div :class="{ 'is-show': renderPanel }">
<span class="item-icon">
<public-icon
v-if="typeof iconComponents[item.id] === 'string'"
:name="iconComponents[item.id]"
class="panel-icon"
svgClass="panel-svg"
></public-icon>
<component v-else :is="iconComponents[item.id]" class="panel-icon"></component>
</span>
</div>
</li>
</ul>
</div>
</div>
</vue-draggable-next>
<div
v-show="renderPanel && components[renderPanel]"
id="tiny-engine-left-panel"
:class="[renderPanel, { 'is-fixed': pluginState.fixedPanels.includes(renderPanel) }]"
>
<div class="left-panel-wrap">
<keep-alive>
<component
:is="components[renderPanel]"
ref="pluginRef"
:fixed-panels="pluginState.fixedPanels"
@close="close"
@fixPanel="fixPanel"
></component>
</keep-alive>
<!-- 图标菜单下侧区域附加icon -->
<div class="nav-panel-lists bottom">
<div style="flex: 1" class="list-item" @contextmenu.prevent="showContextMenu($event, false)" />
<vue-draggable-next id="leftBottom" v-model="state.bottomNavLists" group="plugins" @end="onEnd">
<div
v-for="(item, index) in state.bottomNavLists"
:key="index"
:class="[
'list-item',
{ active: renderPanel === item.id, prev: state.prevIdex - 1 === index, 'first-item': index === 0 }
]"
:title="item.title"
@click="clickMenu({ item, index })"
@contextmenu.prevent="showContextMenu($event, true, item, index, PLUGIN_POSITION.leftBottom)"
>
<div :class="{ 'is-show': renderPanel }" v-if="getPluginShown(item.id)">
<span class="item-icon">
<public-icon
v-if="typeof iconComponents[item.id] === 'string'"
:name="iconComponents[item.id]"
class="panel-icon"
svgClass="panel-svg"
></public-icon>
<component v-else :is="iconComponents[item.id]" class="panel-icon"></component>
</span>
</div>
</div>
</vue-draggable-next>
</div>
</div>
<div :class="{ 'not-selected': getMoveDragBarState() }">
<!-- 插件面板 -->
<div
v-show="renderPanel && components[renderPanel]"
id="tiny-engine-left-panel"
:class="[renderPanel, { 'is-fixed': leftFixedPanelsStorage.includes(renderPanel) }]"
>
<div class="left-panel-wrap">
<keep-alive>
<component
ref="pluginRef"
:is="currentComponent"
:fixed-panels="leftFixedPanelsStorage"
@close="close"
@fixPanel="fixPanel"
></component>
</keep-alive>
</div>
</div>
</div>
<plugin-right-menu
ref="rightMenu"
:list="[...state.topNavLists, ...state.bottomNavLists]"
:align="left"
@switchAlign="switchAlign"
/>
</template>
<script>
import { reactive, ref, watch } from 'vue'
import { reactive, ref, watch, computed } from 'vue'
import { Popover, Tooltip } from '@opentiny/vue'
import { useLayout, usePage, useModal, META_APP } from '@opentiny/tiny-engine-meta-register'
import { PublicIcon } from '@opentiny/tiny-engine-common'
import { constants } from '@opentiny/tiny-engine-utils'
const { STORAGE_KEY_FIXED_PANELS } = constants
import { VueDraggableNext } from 'vue-draggable-next'
import { useLayout, usePage, META_APP } from '@opentiny/tiny-engine-meta-register'
import { PublicIcon, PluginRightMenu } from '@opentiny/tiny-engine-common'
export default {
components: {
PluginRightMenu,
VueDraggableNext,
TinyPopover: Popover,
TinyTooltip: Tooltip,
PublicIcon
@ -93,6 +117,10 @@ export default {
plugins: {
type: Array,
default: () => []
},
pluginList: {
type: Array,
default: () => []
}
},
emits: ['click', 'node-click'],
@ -101,18 +129,56 @@ export default {
const iconComponents = {}
const pluginRef = ref(null)
const { isTemporaryPage } = usePage()
const { message } = useModal()
const pluginState = useLayout().getPluginState()
props.plugins.forEach(({ id, entry, icon }) => {
const {
changeLeftFixedPanels,
leftFixedPanelsStorage,
getPluginById,
getPluginShown,
PLUGIN_POSITION,
getMoveDragBarState,
isSameSide,
dragPluginLayout,
getPluginsByPosition
} = useLayout()
const rightMenu = ref(null)
const showContextMenu = (event, type, item, index, align) => {
if (!type) {
rightMenu.value.showContextMenu(event.clientX, event.clientY, type)
} else {
rightMenu.value.showContextMenu(event.clientX, event.clientY, type, item, index, align)
}
}
const state = reactive({
prevIdex: -2,
topNavLists: getPluginsByPosition(PLUGIN_POSITION.leftTop, props.pluginList),
bottomNavLists: getPluginsByPosition(PLUGIN_POSITION.leftBottom, props.pluginList)
})
const changeAlign = (pluginId) => {
const item = getPluginById(props.pluginList, pluginId)
const existingItemIndex = state.topNavLists.findIndex((plugin) => plugin.id === item.id)
if (existingItemIndex !== -1) {
state.topNavLists.splice(existingItemIndex, 1)
}
state.topNavLists.unshift(item)
}
props.pluginList.forEach(({ id, entry, icon }) => {
components[id] = entry
iconComponents[id] = icon
})
const state = reactive({
prevIdex: -2,
topNavLists: props.plugins.filter((item) => item.align === 'top'),
bottomNavLists: props.plugins.filter((item) => item.align === 'bottom')
const currentComponent = computed(() => {
const isExistedComponent = [...state.topNavLists, ...state.bottomNavLists].some(
(item) => item.id === props.renderPanel
)
return isExistedComponent ? components[props.renderPanel] : null
})
const clickMenu = ({ item, index }) => {
@ -127,14 +193,14 @@ export default {
result &&
emit('click', {
item,
navLists: item.align === 'top' ? state.topNavLists[index] : state.bottomNavLists[index]
navLists: item.align === 'leftTop' ? state.topNavLists[index] : state.bottomNavLists[index]
})
pluginRef.value?.[lastPlugin.confirm](confirmCallback)
} else {
emit('click', {
item,
navLists: item.align === 'top' ? state.topNavLists[index] : state.bottomNavLists[index]
navLists: item.align === 'leftTop' ? state.topNavLists[index] : state.bottomNavLists[index]
})
}
}
@ -154,35 +220,43 @@ export default {
useLayout().closePlugin(true)
}
/**
* @param index 组件索引
* @param id 组件 ID, 类似于 'engine.plugins.*'
* @param from 组件来源
*/
const switchAlign = (index, id, from) => {
if (from === PLUGIN_POSITION.leftTop) {
state.topNavLists.splice(index, 1)
} else {
state.bottomNavLists.splice(index, 1)
}
emit('changeLeftAlign', id)
if (!isSameSide(index, 0)) close()
dragPluginLayout(from, PLUGIN_POSITION.rightTop, index, 0)
}
const fixPanel = (pluginName) => {
pluginState.fixedPanels = pluginState.fixedPanels?.includes(pluginName)
? pluginState.fixedPanels?.filter((item) => item !== pluginName)
: [...pluginState.fixedPanels, pluginName]
try {
localStorage.setItem(STORAGE_KEY_FIXED_PANELS, JSON.stringify(pluginState.fixedPanels))
} catch (error) {
message({ message: `'存储固定面板数据失败:'${error}`, status: 'error' })
}
changeLeftFixedPanels(pluginName)
}
const restoreFixedPanels = () => {
try {
const storedPanels = localStorage.getItem(STORAGE_KEY_FIXED_PANELS)
pluginState.fixedPanels = storedPanels ? JSON.parse(storedPanels) : []
if (!Array.isArray(pluginState.fixedPanels)) {
pluginState.fixedPanels = []
}
} catch (error) {
message({ message: `'读取固定面板数据失败:'${error}`, status: 'error' })
pluginState.fixedPanels = []
}
//
const onEnd = (e) => {
if (!isSameSide(e.from.id, e.to.id)) close()
dragPluginLayout(e.from.id, e.to.id, e.oldIndex, e.newIndex)
}
restoreFixedPanels()
return {
leftFixedPanelsStorage,
currentComponent,
changeAlign,
rightMenu,
PLUGIN_POSITION,
showContextMenu,
switchAlign,
getPluginShown,
onEnd,
state,
clickMenu,
pluginRef,
@ -190,6 +264,7 @@ export default {
fixPanel,
pluginState,
components,
getMoveDragBarState,
iconComponents
}
}
@ -198,7 +273,7 @@ export default {
<style lang="less" scoped>
#tiny-engine-left-panel {
width: var(--base-left-panel-width);
width: auto !important;
height: calc(100vh - var(--base-top-panel-height));
border-right: 1px solid var(--te-layout-common-border-color);
background: var(--te-layout-common-bg-color);
@ -209,10 +284,6 @@ export default {
left: var(--base-nav-panel-width);
z-index: 999;
&[class~='engine.plugins.i18n'] {
width: auto;
}
&.is-fixed {
position: relative;
top: 0;
@ -324,4 +395,9 @@ export default {
:deep(.svg-icon.icon-plugin-icon-plugin-help) {
font-size: 18px;
}
.not-selected {
pointer-events: none;
user-select: none;
}
</style>

View File

@ -1,130 +1,328 @@
<!-- 右侧插件栏 -->
<template>
<div id="tiny-right-panel">
<tiny-tabs v-model="layoutState.settings.render">
<tiny-tab-item v-for="(setting, index) in settings" :key="index" :title="setting.title" :name="setting.name">
<component :is="setting.entry"></component>
<div v-show="activating" class="active"></div>
</tiny-tab-item>
</tiny-tabs>
<div v-if="layoutState.settings.render === 'style'" class="tabs-setting">
<tiny-tooltip effect="light" :content="isCollapsed ? '展开' : '折叠'" placement="top" :visible-arrow="false">
<template #default> <svg-icon :name="settingIcon" @click="isCollapsed = !isCollapsed"></svg-icon> </template>
</tiny-tooltip>
<div :class="{ 'not-selected': getMoveDragBarState() }">
<div
v-show="renderPanel && components[renderPanel]"
id="tiny-engine-right-panel"
:class="[renderPanel, { 'is-fixed': rightFixedPanelsStorage.includes(renderPanel) }]"
>
<div class="right-panel-wrap">
<component
:is="currentComponent"
:fixed-panels="rightFixedPanelsStorage"
@close="close"
@fixPanel="fixPanel"
></component>
<div v-show="activating" class="active2" />
</div>
</div>
</div>
<div id="tiny-engine-nav-panel">
<vue-draggable-next id="rightTop" v-model="settingPlugins" class="nav-panel-lists" group="plugins" @end="onEnd">
<div
v-for="(item, index) in settingPlugins"
:key="index"
:class="['list-item', { 'first-item': item === settingPlugins[0], active: item.id === renderPanel }]"
:title="item.title"
@click="clickMenu({ item, index })"
@contextmenu.prevent="showContextMenu($event, true, item, index, PLUGIN_POSITION.rightTop)"
>
<span class="item-icon" v-if="getPluginShown(item.id)">
<svg-icon v-if="iconComponents[item.id]" :name="iconComponents[item.id]" class="panel-icon"></svg-icon>
<component v-else :is="iconComponents[item.id]" class="panel-icon"></component>
</span>
</div>
<div style="flex: 1" class="list-item" @contextmenu.prevent="showContextMenu($event, false)"></div>
</vue-draggable-next>
</div>
<plugin-right-menu
ref="rightMenu"
:list="settingPlugins"
:align="PLUGIN_POSITION.rightTop"
@switchAlign="switchAlign"
/>
</template>
<script>
import { computed, provide, ref } from 'vue'
import { Tabs, TabItem, Tooltip } from '@opentiny/vue'
import { computed, ref, watch, toRefs } from 'vue'
import { Tabs, TabItem } from '@opentiny/vue'
import { useLayout } from '@opentiny/tiny-engine-meta-register'
import { VueDraggableNext } from 'vue-draggable-next'
import { PluginRightMenu } from '@opentiny/tiny-engine-common'
export default {
components: {
PluginRightMenu,
TinyTabs: Tabs,
TinyTabItem: TabItem,
TinyTooltip: Tooltip
VueDraggableNext
},
props: {
settings: {
type: Array,
default: () => []
},
renderPanel: {
type: String
},
pluginList: {
type: Array,
default: () => []
}
},
setup() {
const { layoutState } = useLayout()
const activating = computed(() => layoutState.settings.activating)
const showMask = ref(true)
const isCollapsed = ref(false)
const settingIcon = computed(() => (isCollapsed.value ? 'collapse_all' : 'expand_all'))
setup(props, { emit }) {
const components = {}
const iconComponents = {}
provide('isCollapsed', isCollapsed)
const {
getPluginsByPosition,
getPluginById,
PLUGIN_POSITION,
rightFixedPanelsStorage,
changeRightFixedPanels,
dragPluginLayout,
isSameSide,
getPluginShown,
getMoveDragBarState,
layoutState: { settings: settingsState }
} = useLayout()
const rightMenu = ref(null)
const { renderPanel } = toRefs(props)
const showContextMenu = (event, type, item, index, align) => {
if (!type) {
rightMenu.value.showContextMenu(event.clientX, event.clientY, type)
} else {
rightMenu.value.showContextMenu(event.clientX, event.clientY, type, item, index, align)
}
}
props.pluginList.forEach(({ id, entry, icon }) => {
components[id] = entry
iconComponents[id] = icon
})
const settingPlugins = ref(getPluginsByPosition(PLUGIN_POSITION.rightTop, props.pluginList))
const currentComponent = computed(() => {
const isExistedComponent = settingPlugins.value.some((item) => item.id === renderPanel.value)
return isExistedComponent ? components[renderPanel.value] : null
})
const close = () => {
useLayout().closeSetting(true)
}
const switchAlign = (index, id, from) => {
settingPlugins.value.splice(index, 1)
emit('changeRightAlign', id)
if (!isSameSide(index, 0)) close()
dragPluginLayout(from, PLUGIN_POSITION.leftTop, index, 0)
}
const changeAlign = (pluginId) => {
const item = getPluginById(props.pluginList, pluginId)
const existingItemIndex = settingPlugins.value.findIndex((plugin) => plugin.id === item.id)
if (existingItemIndex !== -1) {
settingPlugins.value.splice(existingItemIndex, 1)
}
settingPlugins.value.unshift(item)
}
const setRender = (curId) => {
settingsState.render = curId
}
//icon
const clickMenu = ({ item }) => {
if (settingsState.render == item.id) {
useLayout().closeSetting(true)
return
}
setRender(item.id)
}
watch(renderPanel, (n) => {
setRender(n)
})
//
const fixPanel = (pluginName) => {
changeRightFixedPanels(pluginName)
}
//
const onEnd = (e) => {
if (!isSameSide(e.from.id, e.to.id)) close()
dragPluginLayout(e.from.id, e.to.id, e.oldIndex, e.newIndex)
}
const activating = computed(() => settingsState.activating)
const showMask = ref(true)
return {
currentComponent,
changeAlign,
showMask,
isCollapsed,
activating,
settingIcon,
layoutState
settingsState,
settingPlugins,
components,
iconComponents,
clickMenu,
close,
fixPanel,
rightFixedPanelsStorage,
onEnd,
showContextMenu,
PLUGIN_POSITION,
getPluginShown,
switchAlign,
rightMenu,
getMoveDragBarState
}
}
}
</script>
<style lang="less" scoped>
#tiny-right-panel {
width: var(--base-right-panel-width);
height: 100%;
transition: 0.3s linear;
position: relative;
#tiny-engine-right-panel {
height: calc(100vh - var(--base-top-panel-height));
border-left: 1px solid var(--te-layout-common-border-color);
padding-top: 12px;
background-color: var(--te-layout-common-bg-color);
background: var(--ti-lowcode-common-component-bg);
display: flex;
flex-direction: column;
position: absolute;
top: var(--base-top-panel-height);
right: var(--base-nav-panel-width);
z-index: 999;
.tabs-setting {
position: absolute;
top: 9px;
right: 18px;
line-height: 26px;
color: var(--te-layout-common-icon-color);
cursor: pointer;
&.I18n {
width: auto;
}
.tiny-tabs {
&.is-fixed {
position: relative;
top: 0;
right: 0;
}
.right-panel-wrap {
width: 100%;
height: 100%;
}
:deep(.tiny-tabs) {
display: flex;
flex-direction: column;
.tiny-tabs__header .tiny-tabs__nav {
width: 60%;
background-color: var(--te-layout-common-bg-color);
position: relative;
:deep(.tiny-tabs__nav.is-show-active-bar) .tiny-tabs__item {
margin-right: 0;
}
.tiny-tabs__nav-scroll {
margin-left: 12px;
.tiny-tabs__active-bar {
height: 3px;
background-color: var(--te-layout-common-text-color-active);
}
}
.tiny-tabs__content {
flex: 1;
overflow-y: auto;
padding: 0;
margin: 0;
}
.tiny-tabs__nav.is-show-active-bar .tiny-tabs__item {
margin-right: 8px;
}
.tiny-tabs__item {
flex: 1;
background-color: var(--te-layout-common-bg-color);
color: var(--te-layout-common-text-color-secondary);
margin-right: 5px;
&:hover {
color: var(--te-layout-common-text-color-hover);
}
&.is-active {
color: var(--te-layout-common-text-color-active);
border: none;
}
.tiny-tabs__item__title {
padding-bottom: 6px;
}
}
.tiny-tabs__nav-wrap-not-separator::after {
z-index: 2;
}
}
:deep(.tiny-collapse-item__content) {
padding: 0 8px 12px 12px; // bottom4px + 8px = 12px
}
}
.active {
#tiny-engine-nav-panel {
display: none;
width: var(--base-nav-panel-width);
display: flex;
flex-direction: column;
justify-content: space-between;
background: var(--te-layout-common-bg-color);
box-sizing: border-box;
z-index: 1000;
border-left: 1px solid var(--te-layout-common-border-color);
&.completed {
display: block;
}
.nav-panel-lists {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
height: 100vh;
&.bottom {
flex: 1;
padding-bottom: 28px;
}
.list-item {
width: 100%;
padding: 3px 0;
&:first-child {
padding-top: 12px;
}
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
&:hover,
&.active {
.item-icon {
background: var(--te-layout-common-bg-color-hover);
border-radius: 4px;
}
}
&.active {
position: relative;
.item-icon {
color: var(--te-layout-common-text-color-secondary-checked);
}
}
&.prev {
border-bottom-color: var(--te-layout-common-border-color);
}
}
.item-icon {
display: flex;
justify-content: center;
align-items: center;
color: var(--te-layout-common-text-color);
font-size: 22px;
width: 26px;
height: 26px;
svg {
font-size: 18px;
}
.public-icon {
display: flex;
justify-content: center;
align-items: center;
width: 26px;
height: 26px;
}
}
}
}
:deep(.panel-svg) {
font-size: 18px;
}
:deep(.svg-icon.icon-plugin-icon-plugin-help) {
font-size: 18px;
}
.not-selected {
pointer-events: none;
user-select: none;
}
:deep(.svg-icon.icon-plugin-icon-plugin-help) {
font-size: 22px;
}
//
.active2 {
width: 100%;
height: 100%;
position: absolute;
@ -135,10 +333,10 @@ export default {
@keyframes glow {
0% {
box-shadow: inset 0px 0px 4px var(--te-layout-setting-bg-color-hover);
box-shadow: inset 0px 0px 4px var(--ti-lowcode-canvas-handle-hover-bg);
}
100% {
box-shadow: inset 0px 0px 14px var(--te-layout-setting-bg-color-hover);
box-shadow: inset 0px 0px 14px var(--ti-lowcode-canvas-handle-hover-bg);
}
}
</style>

View File

@ -6,8 +6,12 @@
<div class="tiny-engine-left-wrap">
<div class="tiny-engine-content-wrap">
<design-plugins
v-if="leftMenuShownStorage"
ref="left"
:plugins="registry.plugins"
:plugin-list="pluginList"
:render-panel="plugins.render"
@changeLeftAlign="changeLeftAlign"
@click="toggleNav"
></design-plugins>
<component :is="registry.canvas.entry"></component>
@ -15,9 +19,13 @@
</div>
<div class="tiny-engine-right-wrap">
<design-settings
:settings="registry.settings"
v-show="layoutState.settings.showDesignSettings"
v-if="rightMenuShownStorage"
ref="right"
:settings="registry.settings"
:render-panel="settings.render"
:plugin-list="pluginList"
v-show="layoutState.settings.showDesignSettings"
@changeRightAlign="changeRightAlign"
></design-settings>
</div>
</div>
@ -27,10 +35,12 @@
<script>
import { useLayout, getMergeRegistry } from '@opentiny/tiny-engine-meta-register'
import { constants } from '@opentiny/tiny-engine-utils'
import DesignToolbars from './DesignToolbars.vue'
import DesignPlugins from './DesignPlugins.vue'
import DesignSettings from './DesignSettings.vue'
import meta from '../meta'
import { ref } from 'vue'
export default {
name: 'TinyLowCode',
@ -49,24 +59,82 @@ export default {
type: Object
}
},
setup() {
setup(props) {
const layoutRegistry = getMergeRegistry(meta.type)
const configProvider = layoutRegistry.options.configProvider
const configProviderDesign = layoutRegistry.options.configProviderDesign
const { layoutState } = useLayout()
const { plugins } = layoutState
const { layoutState, leftMenuShownStorage, rightMenuShownStorage, initPluginStorageReactive } = useLayout()
const { plugins, settings } = layoutState
const toggleNav = ({ item }) => {
if (!item.id) return
plugins.render = plugins.render === item.id ? null : item.id
}
const left = ref(null)
const right = ref(null)
const changeLeftAlign = (pluginId) => {
right.value?.changeAlign(pluginId)
}
const changeRightAlign = (pluginId) => {
left.value?.changeAlign(pluginId)
}
//
const pluginList = [...props.registry.plugins, ...props.registry.settings]
// align
const alignGroups = {}
const plugin = {}
const { PLUGIN_DEFAULT_WIDTH } = constants
pluginList.forEach((item) => {
if (item.id) {
const align = item?.align || 'leftTop'
// alignGroups[align]
if (!alignGroups[align]) {
alignGroups[align] = []
}
// item.id alignGroups
alignGroups[align].push(item.id)
// index
const index = alignGroups[align].indexOf(item.id)
const widthResizable = item?.widthResizable ?? false
plugin[item.id] = {
width: item?.width || PLUGIN_DEFAULT_WIDTH,
align: align,
index: index,
isShow: true,
entry: item.entry,
id: item.id,
icon: item.icon,
widthResizable
}
}
})
localStorage.setItem('plugin', JSON.stringify(plugin))
initPluginStorageReactive(plugin)
return {
left,
right,
changeLeftAlign,
changeRightAlign,
leftMenuShownStorage,
rightMenuShownStorage,
pluginList,
layoutRegistry,
configProvider,
configProviderDesign,
plugins,
settings,
toggleNav,
layoutState
}
@ -98,7 +166,8 @@ export default {
}
}
.tiny-engine-right-wrap {
position: relative;
display: flex;
flex-flow: row nowrap;
z-index: 4;
}
:deep(.monaco-editor .suggest-widget) {

View File

@ -11,12 +11,27 @@
*/
import { reactive, nextTick } from 'vue'
import { useStorage } from '@vueuse/core'
import { constants } from '@opentiny/tiny-engine-utils'
import { META_APP as PLUGIN_NAME, getMetaApi } from '@opentiny/tiny-engine-meta-register'
const { PAGE_STATUS } = constants
const { PAGE_STATUS, STORAGE_KEY_LEFT_FIXED_PANELS, STORAGE_KEY_RIGHT_FIXED_PANELS, PLUGIN_DEFAULT_WIDTH } = constants
const PLUGIN_POSITION = {
leftTop: 'leftTop',
leftBottom: 'leftBottom',
independence: 'independence',
rightTop: 'rightTop',
rightBottom: 'rightBottom',
fixed: 'fixed'
}
const pluginState = reactive({
pluginEvent: 'all'
})
const layoutState = reactive({
isMoveDragBar: false,
dimension: {
deviceType: 'desktop',
width: '',
@ -26,12 +41,17 @@ const layoutState = reactive({
height: '100%'
},
plugins: {
isShow: true,
fixedPanels: [PLUGIN_NAME.Materials],
render: null,
pluginEvent: 'all'
render: PLUGIN_NAME.Materials,
pluginEvent: 'all',
activating: false, // 右侧面版激活提示状态
showDesignSettings: true
},
settings: {
render: 'props',
isShow: true,
fixedPanels: [PLUGIN_NAME.Props, PLUGIN_NAME.Styles, PLUGIN_NAME.Event],
render: PLUGIN_NAME.Props,
api: null,
activating: false, // 右侧面版激活提示状态
showDesignSettings: true
@ -41,10 +61,44 @@ const layoutState = reactive({
},
pageStatus: ''
})
const getMoveDragBarState = () => {
return layoutState.isMoveDragBar
}
const changeMoveDragBarState = (state) => {
layoutState.isMoveDragBar = state
}
const leftMenuShownStorage = useStorage('leftMenuShown', layoutState.plugins.isShow)
const rightMenuShownStorage = useStorage('rightMenuShown', layoutState.settings.isShow)
const changeMenuShown = (menuName) => {
switch (menuName) {
case 'left': {
leftMenuShownStorage.value = !leftMenuShownStorage.value
break
}
case 'right': {
rightMenuShownStorage.value = !rightMenuShownStorage.value
break
}
}
}
const leftFixedPanelsStorage = useStorage(STORAGE_KEY_LEFT_FIXED_PANELS, layoutState.plugins.fixedPanels)
const rightFixedPanelsStorage = useStorage(STORAGE_KEY_RIGHT_FIXED_PANELS, layoutState.settings.fixedPanels)
const changeLeftFixedPanels = (pluginName) => {
leftFixedPanelsStorage.value = leftFixedPanelsStorage.value?.includes(pluginName)
? leftFixedPanelsStorage.value?.filter((item) => item !== pluginName)
: [...leftFixedPanelsStorage.value, pluginName]
}
const changeRightFixedPanels = (pluginName) => {
rightFixedPanelsStorage.value = rightFixedPanelsStorage.value?.includes(pluginName)
? rightFixedPanelsStorage.value?.filter((item) => item !== pluginName)
: [...rightFixedPanelsStorage.value, pluginName]
}
const getScale = () => layoutState.dimension.scale
const getPluginState = () => layoutState.plugins
const getSettingState = () => layoutState.settings
const getDimension = () => layoutState.dimension
@ -66,6 +120,22 @@ const activeSetting = (name) => {
})
}
/**
* 两侧面板的固定状态
*/
const getFixedPanelsStatus = () => {
const leftPanelFixed = leftFixedPanelsStorage.value.includes(layoutState.plugins.render)
const rightPanelFixed = rightFixedPanelsStorage.value.includes(layoutState.settings.render)
return { leftPanelFixed, rightPanelFixed }
}
const closeSetting = (forceClose) => {
const { settings } = layoutState
if (!settings.fixedPanels.includes(settings.render) || forceClose) {
settings.render = null
}
}
// 激活plugin面板并返回当前插件注册的Api
const activePlugin = (name, noActiveRender) => {
const { plugins } = layoutState
@ -90,16 +160,191 @@ const closePlugin = (forceClose) => {
const isEmptyPage = () => layoutState.pageStatus?.state === PAGE_STATUS.Empty
export default () => {
let plugin = []
try {
const storedPlugin = localStorage.getItem('plugin')
if (storedPlugin) {
plugin = JSON.parse(storedPlugin)
}
} catch (error) {
throw new Error(error)
}
// 如果 plugin 不是一个数组,则将其重置为默认值
if (!Array.isArray(plugin)) {
plugin = []
}
const pluginStorageReactive = useStorage('plugin', plugin)
// 获取插件宽度
const getPluginWidth = (name) => pluginStorageReactive.value[name]?.width || PLUGIN_DEFAULT_WIDTH
// 修改插件宽度
const changePluginWidth = (name, width, offset) => {
if (Object.prototype.hasOwnProperty.call(pluginStorageReactive.value, name)) {
pluginStorageReactive.value[name].width = width
pluginStorageReactive.value[name].offset = offset
} else {
pluginStorageReactive.value[name] = {
width
}
}
}
// 获取插件布局
const getPluginByLayout = (name) => pluginStorageReactive.value[name]?.align || 'leftTop'
// 获取某个布局(左上/左下/右上/右下)的插件名称列表
const getPluginsByLayout = (layout = 'all') => {
// 筛选出符合布局条件的插件名称
const pluginNames = Object.keys(pluginStorageReactive.value).filter(
(key) => pluginStorageReactive.value[key].align === layout || layout === 'all'
)
pluginNames.sort((a, b) => pluginStorageReactive.value[a].index - pluginStorageReactive.value[b].index)
return pluginNames
}
const getPluginById = (pluginList, pluginId) => {
return pluginList.find((item) => item.id === pluginId)
}
const getPluginsByPosition = (position, pluginList) => {
return getPluginsByLayout(position).map((pluginId) => getPluginById(pluginList, pluginId))
}
// 修改某个插件的布局
const changePluginLayout = (name, layout) => {
if (pluginStorageReactive.value[name]) {
pluginStorageReactive.value[name].align = layout
}
}
/**
* 拖拽后改变插件位置
* @param {*} from 插件的起始位置
* @param {*} to 插件的结束位置
* @param {*} oldIndex 插件的起始索引
* @param {*} newIndex 插件的结束索引
* @returns
*/
const dragPluginLayout = (from, to, oldIndex, newIndex) => {
if (from === to && oldIndex === newIndex) return
const items = Object.values(pluginStorageReactive.value)
// 记录拖拽项
const movedItem = items.find((item) => item.align === from && item.index === oldIndex)
// 同一列表中的拖拽
if (from === to) {
if (oldIndex < newIndex) {
//往后移动
items.forEach((item) => {
if (item !== movedItem && item.align === from && item.index > oldIndex && item.index <= newIndex) {
item.index -= 1
}
})
} else {
//往前移动
items.forEach((item) => {
if (item !== movedItem && item.align === from && item.index >= newIndex && item.index < oldIndex) {
item.index += 1
}
})
}
} else {
// 跨列表拖拽
items.forEach((item) => {
if (item !== movedItem && item.align === from && item.index > oldIndex) {
item.index -= 1
}
if (item !== movedItem && item.align === to && item.index >= newIndex) {
item.index += 1
}
})
}
// 更新拖拽项的位置
if (movedItem) {
movedItem.align = to
movedItem.index = newIndex
}
}
//判断是否在同一侧
const isSameSide = (from, to) => {
const leftSide = [PLUGIN_POSITION.leftTop, PLUGIN_POSITION.leftBottom]
const rightSide = [PLUGIN_POSITION.rightTop, PLUGIN_POSITION.rightBottom]
const isLeft = leftSide.includes(from) && leftSide.includes(to)
const isRight = rightSide.includes(from) && rightSide.includes(to)
return isLeft || isRight
}
//获取插件显示状态
const getPluginShown = (name) => pluginStorageReactive.value[name]?.isShow
//修改插件显示状态
const changePluginShown = (name) => {
if (!pluginStorageReactive.value[name]) {
pluginStorageReactive.value[name] = { isShow: true }
}
pluginStorageReactive.value[name].isShow = !pluginStorageReactive.value[name].isShow
}
/**
* 返回面板是否宽度可调
* @param {string} name 插件名称
* @returns
*/
const isPanelWidthResizable = (name) => pluginStorageReactive.value[name]?.widthResizable
const initPluginStorageReactive = (pluginList) => {
if (Object.keys(pluginStorageReactive.value).length) return
pluginStorageReactive.value = pluginList
}
return {
isPanelWidthResizable,
getFixedPanelsStatus,
initPluginStorageReactive,
PLUGIN_NAME,
PLUGIN_POSITION,
activeSetting,
closeSetting,
activePlugin,
closePlugin,
layoutState,
getScale,
setDimension,
getDimension,
getPluginById,
pluginState,
getPluginState,
isEmptyPage
getSettingState,
isEmptyPage,
getPluginWidth,
changePluginWidth,
leftFixedPanelsStorage,
rightFixedPanelsStorage,
leftMenuShownStorage,
rightMenuShownStorage,
changeLeftFixedPanels,
changeRightFixedPanels,
getPluginsByLayout,
changePluginLayout,
getPluginByLayout,
dragPluginLayout,
isSameSide,
getPluginShown,
changePluginShown,
changeMenuShown,
getMoveDragBarState,
changeMoveDragBarState,
getPluginsByPosition
}
}

View File

@ -2,6 +2,6 @@ export default {
id: 'engine.plugins.blockmanage',
title: '区块管理',
type: 'plugins',
align: 'top',
align: 'leftTop',
icon: 'plugin-icon-symbol'
}

View File

@ -3,6 +3,8 @@
v-if="isOpen"
class="plugin-block-setting"
title="区块设置"
:align="align"
:fixed-name="PLUGIN_NAME.BlockManage"
@mouseleave="onMouseLeave"
@click="handleClick"
>
@ -86,7 +88,14 @@
<script lang="jsx">
import { reactive, ref, watch, watchEffect, computed } from 'vue'
import { Button as TinyButton, Collapse as TinyCollapse, CollapseItem as TinyCollapseItem } from '@opentiny/vue'
import { useModal, getMergeMeta, useBlock, getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register'
import {
useLayout,
useModal,
getMergeMeta,
useBlock,
getMetaApi,
META_SERVICE
} from '@opentiny/tiny-engine-meta-register'
import { BlockHistoryList, PluginSetting, CloseIcon, SvgButton, ButtonGroup } from '@opentiny/tiny-engine-common'
import { previewBlock } from '@opentiny/tiny-engine-common/js/preview'
import { LifeCycles } from '@opentiny/tiny-engine-common'
@ -152,6 +161,9 @@ export default {
})
const blockConfigForm = ref(null)
const { PLUGIN_NAME, getPluginByLayout } = useLayout()
const align = computed(() => getPluginByLayout(PLUGIN_NAME.BlockManage))
const state = reactive({
activeName: ['base', 'attribute', 'event', 'lifeCycle', 'history'],
backupList: [],
@ -277,6 +289,8 @@ export default {
}
return {
align,
PLUGIN_NAME,
state,
isOpen,
showDeployBlockDialog,

View File

@ -1,129 +1,128 @@
<template>
<div class="plugin-block">
<plugin-panel
class="block-manage"
title="区块管理"
:docsUrl="docsUrl"
:isShowDocsIcon="true"
:isCloseLeft="false"
@close="closePanel"
>
<template #header>
<svg-button name="add-page" placement="bottom" tips="新建区块" @click="openBlockAdd"></svg-button>
</template>
<template #content>
<div class="app-manage-type">
<tiny-select
ref="groupSelect"
v-model="state.categoryId"
popper-class="block-popper"
:placeholder="groupLabels.selectPlaceholder"
filterable
:filter-method="categoryFilter"
clearable
top-create
@top-create-click="createCategory"
@change="changeCategory"
@clear="changeCategory"
@visible-change="handleSelectVisibleChange"
class="search-select"
<plugin-panel
title="区块管理"
class="plugin-block"
:fixed-name="PLUGIN_NAME.BlockManage"
:fixedPanels="fixedPanels"
:docsUrl="docsUrl"
:isShowDocsIcon="true"
@close="close"
>
<template #header>
<svg-button name="add-page" placement="bottom" tips="新建区块" @click="openBlockAdd"></svg-button>
</template>
<template #content>
<div class="app-manage-type">
<tiny-select
ref="groupSelect"
v-model="state.categoryId"
popper-class="block-popper"
:placeholder="groupLabels.selectPlaceholder"
filterable
:filter-method="categoryFilter"
clearable
top-create
@top-create-click="createCategory"
@change="changeCategory"
@clear="changeCategory"
@visible-change="handleSelectVisibleChange"
class="search-select"
>
<tiny-option
v-for="item in categoryList"
:key="item.id"
:label="item.name"
:value="item.id"
class="block-group-option-item"
>
<tiny-option
v-for="item in categoryList"
:key="item.id"
:label="item.name"
:value="item.id"
class="block-group-option-item"
>
<div class="block-item">
<span>{{ item.name }}</span>
<div class="item-btns">
<svg-button
class="item-icon"
name="to-edit"
:hoverBgColor="false"
@click.stop="editCategory(item)"
></svg-button>
<tiny-popover
:modelValue="state.currentDeleteGroupId === item.id"
placement="right"
trigger="manual"
popper-class="block-category-option-popper-wrapper"
@update:modelValue="handleChangeDeletePopoverVisible"
>
<div class="popper-confirm" @mousedown.stop="">
<div class="popper-confirm-header">删除</div>
<div class="popper-confirm-content">
<span class="title">{{ groupLabels.deletePrompt }}</span>
</div>
<div class="popper-confirm-footer">
<tiny-button class="cancel-btn" size="small" @click="handleShowDeleteModal(null)"
>取消</tiny-button
>
<tiny-button class="confirm-btn" size="small" type="primary" @click="delCategory(item.id)"
>确定</tiny-button
>
</div>
<div class="block-item">
<span>{{ item.name }}</span>
<div class="item-btns">
<svg-button
class="item-icon"
name="to-edit"
:hoverBgColor="false"
@click.stop="editCategory(item)"
></svg-button>
<tiny-popover
:modelValue="state.currentDeleteGroupId === item.id"
placement="right"
trigger="manual"
popper-class="block-category-option-popper-wrapper"
@update:modelValue="handleChangeDeletePopoverVisible"
>
<div class="popper-confirm" @mousedown.stop="">
<div class="popper-confirm-header">删除</div>
<div class="popper-confirm-content">
<span class="title">{{ groupLabels.deletePrompt }}</span>
</div>
<template #reference>
<svg-button
v-if="!item.blocks.length"
class="item-icon"
name="delete"
:hoverBgColor="false"
@click.stop="handleShowDeleteModal(item.id)"
></svg-button>
</template>
</tiny-popover>
</div>
<div class="popper-confirm-footer">
<tiny-button class="cancel-btn" size="small" @click="handleShowDeleteModal(null)"
>取消</tiny-button
>
<tiny-button class="confirm-btn" size="small" type="primary" @click="delCategory(item.id)"
>确定</tiny-button
>
</div>
</div>
<template #reference>
<svg-button
v-if="!item.blocks.length"
class="item-icon"
name="delete"
:hoverBgColor="false"
@click.stop="handleShowDeleteModal(item.id)"
></svg-button>
</template>
</tiny-popover>
</div>
</tiny-option>
</tiny-select>
</div>
<div class="app-manage-search">
<tiny-search v-model="state.searchKey" placeholder="搜索">
<template #prefix>
<tiny-icon-search />
</template>
</tiny-search>
</div>
<div class="plugin-block-list">
<plugin-block-list
:data="state.blockList"
:isBlockManage="true"
:showBlockShot="true"
:blockStyle="state.layout"
default-icon-tip="查看区块"
:externalBlock="externalBlock"
@editBlock="editBlock"
@iconClick="openSettingPanel"
></plugin-block-list>
</div>
<block-setting></block-setting>
<div class="block-footer">
<tiny-dropdown trigger="click" @item-click="changeType">
<span>
<span>{{ state.sortTypeLabel }}</span>
</span>
<template #dropdown>
<tiny-dropdown-menu
popper-class="my-class"
placement="top"
:options="state.sortOptions"
></tiny-dropdown-menu>
</template>
</tiny-dropdown>
<block-group-arrange v-model="state.layout" :arrangeList="state.arrangeList"></block-group-arrange>
</div>
</template>
</plugin-panel>
<category-edit v-model="state.editVisible" :initialValue="state.groupInitialValue"></category-edit>
<save-new-block :boxVisibility="boxVisibility" @close="close"></save-new-block>
</div>
</div>
</tiny-option>
</tiny-select>
</div>
<div class="app-manage-search">
<tiny-search v-model="state.searchKey" placeholder="搜索">
<template #prefix>
<tiny-icon-search />
</template>
</tiny-search>
</div>
<div class="plugin-block-list">
<plugin-block-list
:data="state.blockList"
:isBlockManage="true"
:showBlockShot="true"
:blockStyle="state.layout"
default-icon-tip="查看区块"
:externalBlock="externalBlock"
@editBlock="editBlock"
@iconClick="openSettingPanel"
></plugin-block-list>
</div>
<block-setting></block-setting>
<div class="block-footer">
<tiny-dropdown trigger="click" @item-click="changeType">
<span>
<span>{{ state.sortTypeLabel }}</span>
</span>
<template #dropdown>
<tiny-dropdown-menu
popper-class="my-class"
placement="top"
:options="state.sortOptions"
></tiny-dropdown-menu>
</template>
</tiny-dropdown>
<block-group-arrange v-model="state.layout" :arrangeList="state.arrangeList"></block-group-arrange>
</div>
</template>
</plugin-panel>
<category-edit v-model="state.editVisible" :initialValue="state.groupInitialValue"></category-edit>
<save-new-block :boxVisibility="boxVisibility" @close="close"></save-new-block>
</template>
<script lang="jsx">
import { ref, reactive, computed, watch } from 'vue'
import { ref, reactive, computed, watch, provide } from 'vue'
import {
Search as TinySearch,
Select as TinySelect,
@ -217,8 +216,13 @@ export default {
TinyButton,
TinyIconSearch: IconSearch()
},
setup() {
props: {
fixedPanels: {
type: Array
}
},
emits: ['close'],
setup(props, { emit }) {
const docsUrl = useHelp().getDocsUrl('block')
const { getBlockList, sort } = useBlock()
const { isSaved } = useCanvas()
@ -266,6 +270,14 @@ export default {
const groupSelect = ref(null)
const { PLUGIN_NAME } = useLayout()
const panelState = reactive({
emitEvent: emit
})
provide('panelState', panelState)
watch(
() => [getBlockList(), state.searchKey, state.publishFilterType],
() => {
@ -304,15 +316,15 @@ export default {
}
const close = () => {
boxVisibility.value = false
emit('close')
closePanel()
}
const editBlock = async (block) => {
const isEdit = true
if (isSaved()) {
await refreshBlockData(block)
useBlock().initBlock(block, {}, isEdit)
useLayout().closePlugin()
closePanel()
getMetaApi(META_SERVICE.GlobalService).updateBlockId(block.id)
} else {
confirm({
@ -320,8 +332,6 @@ export default {
exec: async () => {
await refreshBlockData(block)
useBlock().initBlock(block, {}, isEdit)
useLayout().closePlugin()
closePanel()
}
})
}
@ -423,6 +433,7 @@ export default {
}
return {
PLUGIN_NAME,
state,
groupSelect,
categoryList,
@ -458,7 +469,7 @@ export default {
}
.app-manage-type {
padding: 0 10px;
margin: 12px 0;
margin-bottom: 12px;
display: flex;
.search-select {
flex: 1;

View File

@ -2,6 +2,6 @@ export default {
id: 'engine.plugins.bridge',
title: '资源管理',
type: 'plugins',
align: 'top',
align: 'leftTop',
icon: 'plugin-icon-sresources'
}

View File

@ -1,5 +1,5 @@
<template>
<plugin-setting v-if="isOpen">
<plugin-setting v-if="isOpen" :align="align" :fixed-name="PLUGIN_NAME.Bridge">
<template #title>
<div class="title-wrap">
<span>{{ state.title }}</span>
@ -112,7 +112,7 @@ import {
getResourceNamesByType
} from './js/resource'
import { VueMonaco as MonacoEditor, PluginSetting, SvgButton, ButtonGroup } from '@opentiny/tiny-engine-common'
import { useModal, useNotify, getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register'
import { useLayout, useModal, useNotify, getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register'
import { getMergeMeta } from '@opentiny/tiny-engine-meta-register'
const isOpen = ref(false)
@ -151,6 +151,9 @@ export default {
}
const { confirm } = useModal()
const { PLUGIN_NAME, getPluginByLayout } = useLayout()
const align = computed(() => getPluginByLayout(PLUGIN_NAME.Bridge))
const state = reactive({
resource: computed(() => getResource()),
name: '',
@ -273,6 +276,8 @@ export default {
}
return {
align,
PLUGIN_NAME,
rules,
resourceForm,
editor,

View File

@ -1,5 +1,11 @@
<template>
<plugin-panel title="资源管理" class="plugin-bridge" :isCloseLeft="false" @close="closePanel">
<plugin-panel
title="资源管理"
class="plugin-bridge"
:fixed-name="PLUGIN_NAME.Bridge"
:fixedPanels="fixedPanels"
@close="closePanel"
>
<template #header>
<svg-button name="add-utils" placement="left" :tips="tips" @click="addResource('npm')"></svg-button>
</template>
@ -11,8 +17,9 @@
</template>
<script>
import { ref, computed } from 'vue'
import { ref, reactive, computed, provide } from 'vue'
import { PluginPanel, SvgButton } from '@opentiny/tiny-engine-common'
import { useLayout } from '@opentiny/tiny-engine-meta-register'
import { RESOURCE_TYPE } from './js/resource'
import BridgeManage from './BridgeManage.vue'
import BridgeSetting, { openPanel, closePanel } from './BridgeSetting.vue'
@ -25,11 +32,23 @@ export default {
BridgeManage,
BridgeSetting
},
setup() {
props: {
fixedPanels: {
type: Array
}
},
setup(props, { emit }) {
const activedName = ref(RESOURCE_TYPE.Util)
const utilsRef = ref(null)
const tips = computed(() => RESOURCE_TIP[activedName.value])
const { PLUGIN_NAME } = useLayout()
const panelState = reactive({
emitEvent: emit
})
provide('panelState', panelState)
const openBridgePanel = () => {
openPanel()
}
@ -43,6 +62,7 @@ export default {
}
return {
PLUGIN_NAME,
addResource,
RESOURCE_TYPE,
activedName,

View File

@ -2,6 +2,6 @@ export default {
id: 'engine.plugins.collections',
title: '数据源',
type: 'plugins',
align: 'top',
align: 'leftTop',
icon: 'plugin-icon-data'
}

View File

@ -1,5 +1,11 @@
<template>
<plugin-setting v-if="isOpen" title="设置数据源" class="data-source-form">
<plugin-setting
v-if="isOpen"
title="设置数据源"
class="data-source-form plugin-datasource"
:fixed-name="PLUGIN_NAME.Collections"
:align="align"
>
<template #header>
<button-group>
<tiny-button class="field-save" type="primary" @click="save">保存</tiny-button>
@ -27,7 +33,7 @@
</template>
<script lang="jsx">
import { reactive, ref, watch } from 'vue'
import { reactive, ref, watch, computed } from 'vue'
import { Form, Button } from '@opentiny/vue'
import { ButtonGroup, PluginSetting, SvgButton } from '@opentiny/tiny-engine-common'
import DataSourceType from './DataSourceType.vue'
@ -41,7 +47,14 @@ import {
requestDeleteDataSource,
requestGenerateDataSource
} from './js/http'
import { useModal, useDataSource, useNotify, getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register'
import {
useLayout,
useModal,
useDataSource,
useNotify,
getMetaApi,
META_SERVICE
} from '@opentiny/tiny-engine-meta-register'
import { extend } from '@opentiny/vue-renderless/common/object'
const isOpen = ref(false)
@ -84,6 +97,9 @@ export default {
dataSource: {}
})
const { PLUGIN_NAME, getPluginByLayout } = useLayout()
const align = computed(() => getPluginByLayout(PLUGIN_NAME.Collections))
watch(
() => state.dataSource.name,
(value) => {
@ -131,7 +147,8 @@ export default {
dataSourceState.dataSourceColumn = { name, type: type || 'array', columns: filterColumns }
dataSourceState.dataSourceColumnCopies = extend(true, {}, dataSourceState.dataSourceColumn)
}
},
{ immediate: true }
)
const closeAllPanel = () => {
@ -240,6 +257,8 @@ export default {
}
return {
align,
PLUGIN_NAME,
state,
isOpen,
save,

View File

@ -1,28 +1,34 @@
<template>
<div v-if="isOpen" class="global-data-handler">
<plugin-setting title="全局设置" @cancel="close" @save="saveGlobalDataHandle">
<template #content>
<tiny-collapse v-model="activeNames">
<tiny-collapse-item title="请求参数处理函数willFetch" name="willFetch">
<data-handler-editor v-model="state.willFetchValue"></data-handler-editor>
</tiny-collapse-item>
<tiny-collapse-item title="请求完成回调函数dataHandler" name="dataHandler">
<data-handler-editor v-model="state.dataHandlerValue"></data-handler-editor>
</tiny-collapse-item>
<tiny-collapse-item title="请求失败后的回调函数errorHandler" name="errorHandler">
<data-handler-editor v-model="state.errorHandlerValue"></data-handler-editor>
</tiny-collapse-item>
</tiny-collapse>
</template>
</plugin-setting>
</div>
<plugin-setting
v-if="isOpen"
title="全局设置"
class="plugin-datasource global-data-handler"
:align="align"
:fixed-name="PLUGIN_NAME.Collections"
@cancel="close"
@save="saveGlobalDataHandle"
>
<template #content>
<tiny-collapse v-model="activeNames">
<tiny-collapse-item title="请求参数处理函数willFetch" name="willFetch">
<data-handler-editor v-model="state.willFetchValue"></data-handler-editor>
</tiny-collapse-item>
<tiny-collapse-item title="请求完成回调函数dataHandler" name="dataHandler">
<data-handler-editor v-model="state.dataHandlerValue"></data-handler-editor>
</tiny-collapse-item>
<tiny-collapse-item title="请求失败后的回调函数errorHandler" name="errorHandler">
<data-handler-editor v-model="state.errorHandlerValue"></data-handler-editor>
</tiny-collapse-item>
</tiny-collapse>
</template>
</plugin-setting>
</template>
<script>
import DataHandlerEditor from './RemoteDataAdapterForm.vue'
import { watch, ref, nextTick, reactive } from 'vue'
import { watch, ref, nextTick, reactive, computed } from 'vue'
import { requestGlobalDataHandler } from './js/http'
import { useModal, useResource, getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register'
import { useLayout, useModal, useResource, getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register'
import { PluginSetting } from '@opentiny/tiny-engine-common'
import { Collapse, CollapseItem } from '@opentiny/vue'
import { constants } from '@opentiny/tiny-engine-utils'
@ -48,6 +54,9 @@ export default {
setup() {
const { confirm } = useModal()
const { PLUGIN_NAME, getPluginByLayout } = useLayout()
const align = computed(() => getPluginByLayout(PLUGIN_NAME.Collections))
const state = reactive({
dataHandlerValue: useResource().appSchemaState?.dataHandler?.value,
willFetchValue: useResource().appSchemaState.willFetch?.value,
@ -88,6 +97,8 @@ export default {
)
return {
align,
PLUGIN_NAME,
isOpen,
close,
saveGlobalDataHandle,

View File

@ -3,6 +3,8 @@
v-if="isOpen"
:is-icon-button="false"
:showIfFullScreen="true"
:fixed-name="PLUGIN_NAME.Collections"
:align="align"
title="静态数据管理"
class="datasource-record-list"
@cancel="closeRecordList"
@ -122,7 +124,8 @@ export default {
const grid = ref(null)
const { confirm } = useModal()
const { toClipboard } = useClipboard()
const { layoutState } = useLayout()
const { layoutState, PLUGIN_NAME, getPluginByLayout } = useLayout()
const align = computed(() => getPluginByLayout(PLUGIN_NAME.Collections))
const state = reactive({
totalData: [],
@ -559,6 +562,8 @@ export default {
}
return {
align,
PLUGIN_NAME,
isOpen,
state,
grid,

View File

@ -2,8 +2,9 @@
<div class="remote">
<plugin-setting
title="获取远程字段"
class="remote-setting"
class="remote-setting plugin-datasource"
:isSecond="true"
:align="align"
@cancel="closePanel"
@save="saveRemote"
>
@ -52,7 +53,7 @@
</template>
<script>
import { reactive, watch, ref } from 'vue'
import { reactive, watch, ref, computed } from 'vue'
import { Collapse, CollapseItem, Tabs, TabItem, Button } from '@opentiny/vue'
import { PluginSetting } from '@opentiny/tiny-engine-common'
import DataSourceRemoteForm, { getServiceForm } from './DataSourceRemoteForm.vue'
@ -61,7 +62,7 @@ import DataSourceRemoteAutoload from './DataSourceRemoteAutoload.vue'
import DataSourceRemoteAdapter from './DataSourceRemoteDataAdapter.vue'
import DataSrouceRemoteDataResult, { getResponseData } from './DataSourceRemoteDataResult.vue'
import { open as openRemoteMapping } from './DataSourceRemoteMapping.vue'
import { useDataSource, useNotify } from '@opentiny/tiny-engine-meta-register'
import { useLayout, useDataSource, useNotify } from '@opentiny/tiny-engine-meta-register'
import { isEmptyObject } from '@opentiny/vue-renderless/common/type'
import { utils } from '@opentiny/tiny-engine-utils'
import { getRequest } from './js/datasource'
@ -114,6 +115,9 @@ export default {
const dataSourceRemoteAdapteRef = ref(null)
const { dataSourceState } = useDataSource()
const { PLUGIN_NAME, getPluginByLayout } = useLayout()
const align = computed(() => getPluginByLayout(PLUGIN_NAME.Collections))
const state = reactive({
remoteData: { options: {} },
activeName: ['excute', 'result'],
@ -221,6 +225,7 @@ export default {
}
return {
align,
state,
dataSourceRemoteAdapteRef,
closePanel: close,

View File

@ -1,62 +1,67 @@
<template>
<div class="plugin-datasource">
<plugin-panel title="数据源">
<template #header>
<link-button :href="docsUrl"></link-button>
<svg-button
class="set-data-source"
tips="全局设置"
name="global-setting"
@click="openGlobalDataHanderPanel"
></svg-button>
<svg-button
class="refresh-data-source"
tips="刷新数据源"
name="flow-refresh"
@click="refreshDataSource"
></svg-button>
</template>
<template #content>
<tiny-button class="add-data-source" @click="openDataSourceFormPanel()">
<svg-icon name="add"></svg-icon>
</tiny-button>
<data-source-list @edit="openDataSourceFormPanel"></data-source-list>
</template>
</plugin-panel>
<data-source-remote-panel
v-if="isOpenRemotePanel"
v-model="state.currentDataSource.data"
:editable="state.editable"
@confirm="getRomoteReponseData"
></data-source-remote-panel>
<data-source-form
v-model="state.currentDataSource"
:editable="state.editable"
@save="refreshDataSource"
></data-source-form>
<data-source-remote-mapping
v-if="isOpenSourceRemoteMapping"
v-model="state.remoteFields"
:data="state.remoteResponData"
></data-source-remote-mapping>
<data-source-global-data-handler></data-source-global-data-handler>
</div>
<plugin-panel
title="数据源"
class="plugin-datasource"
:fixed-name="PLUGIN_NAME.Collections"
:fixedPanels="fixedPanels"
:docsUrl="docsUrl"
:isShowDocsIcon="true"
@close="$emit('close')"
>
<template #header>
<svg-button
class="set-data-source"
tips="全局设置"
name="global-setting"
@click="openGlobalDataHanderPanel"
></svg-button>
<svg-button
class="refresh-data-source"
tips="刷新数据源"
name="flow-refresh"
@click="refreshDataSource"
></svg-button>
</template>
<template #content>
<tiny-button class="add-data-source" @click="openDataSourceFormPanel()">
<svg-icon name="add"></svg-icon>
</tiny-button>
<data-source-list @edit="openDataSourceFormPanel"></data-source-list>
</template>
</plugin-panel>
<data-source-remote-panel
v-if="isOpenRemotePanel"
v-model="state.currentDataSource.data"
:editable="state.editable"
@confirm="getRomoteReponseData"
></data-source-remote-panel>
<data-source-form
v-model="state.currentDataSource"
:editable="state.editable"
@save="refreshDataSource"
></data-source-form>
<data-source-remote-mapping
v-if="isOpenSourceRemoteMapping"
v-model="state.remoteFields"
:data="state.remoteResponData"
></data-source-remote-mapping>
<data-source-global-data-handler></data-source-global-data-handler>
</template>
<script>
import { reactive, watch } from 'vue'
import { reactive, watch, provide } from 'vue'
import { Button } from '@opentiny/vue'
import DataSourceList, { refresh as refreshDataSourceList, clearActive } from './DataSourceList.vue'
import DataSourceRemotePanel, {
close as closeRemotePanel,
isOpen as isOpenRemotePanel
} from './DataSourceRemotePanel.vue'
import { PluginPanel, SvgButton, LinkButton } from '@opentiny/tiny-engine-common'
import { PluginPanel, SvgButton } from '@opentiny/tiny-engine-common'
import DataSourceForm, { open as openDataSourceForm, close as closeDataSourceForm } from './DataSourceForm.vue'
import { close as closeRecordList } from './DataSourceRecordList.vue'
import { close as closeRecordForm } from './DataSourceRecordForm.vue'
import DataSourceRemoteMapping, { isOpen as isOpenSourceRemoteMapping } from './DataSourceRemoteMapping.vue'
import { useDataSource, useHelp } from '@opentiny/tiny-engine-meta-register'
import { useDataSource, useHelp, useLayout } from '@opentiny/tiny-engine-meta-register'
import { requestUpdateDataSource } from './js/http'
import DataSourceGlobalDataHandler, {
open as openGlobalDataHander,
@ -72,10 +77,14 @@ export default {
DataSourceGlobalDataHandler,
PluginPanel,
DataSourceForm,
SvgButton,
LinkButton
SvgButton
},
setup() {
props: {
fixedPanels: {
type: Array
}
},
setup(props, { emit }) {
const docsUrl = useHelp().getDocsUrl('datasource')
const state = reactive({
editable: true,
@ -84,6 +93,14 @@ export default {
remoteResponData: {}
})
const { PLUGIN_NAME } = useLayout()
const panelState = reactive({
emitEvent: emit
})
provide('panelState', panelState)
const { dataSourceState, saveDataSource } = useDataSource()
watch(
@ -134,6 +151,7 @@ export default {
}
return {
PLUGIN_NAME,
state,
open,
isOpenRemotePanel,
@ -153,6 +171,7 @@ export default {
}
.add-data-source {
margin: 0 12px 12px 12px;
width: calc(100% - 24px);
}
:deep(.help-box) {
position: absolute;

View File

@ -2,8 +2,8 @@ import HelpIcon from './src/HelpIcon.vue'
export default {
id: 'engine.plugins.editorhelp',
title: '',
title: '帮助',
type: 'plugins',
icon: HelpIcon,
align: 'bottom'
align: 'leftBottom'
}

View File

@ -154,7 +154,7 @@ export default {
{
title: '组件设置',
text: '拖拽至画布中的组件或区块,在这里进行属性、样式、事件绑定等众多高级配置;',
domElement: '#tiny-right-panel',
domElement: '#rightTop',
classes: 'lwocode-guide-toolbar-right',
popPosition: 'left',
button: [

View File

@ -2,6 +2,7 @@ export default {
id: 'engine.plugins.i18n',
title: '国际化',
type: 'plugins',
align: 'top',
align: 'leftTop',
width: 600,
icon: 'plugin-icon-i18n'
}

View File

@ -1,5 +1,12 @@
<template>
<plugin-panel title="国际化资源" :docsUrl="docsUrl" :isShowDocsIcon="true" :isCloseLeft="false" class="plugin-i18n">
<plugin-panel
title="国际化资源"
class="plugin-i18n"
:fixed-name="PLUGIN_NAME.I18n"
:fixedPanels="fixedPanels"
:docsUrl="docsUrl"
:isShowDocsIcon="true"
>
<template #content>
<div class="language-search-box">
<tiny-select v-model="currentSearchType" :options="i18nSearchTypes"></tiny-select>
@ -113,12 +120,19 @@
</template>
<script lang="jsx">
import { computed, ref, watchEffect, reactive, onMounted, nextTick, resolveComponent, watch } from 'vue'
import { computed, ref, watchEffect, reactive, onMounted, nextTick, resolveComponent, watch, provide } from 'vue'
import useClipboard from 'vue-clipboard3'
import { Grid, GridColumn, Input, Popover, Button, FileUpload, Loading, Tooltip, Select } from '@opentiny/vue'
import { iconLoadingShadow, iconUpload } from '@opentiny/vue-icon'
import { PluginPanel, SearchEmpty } from '@opentiny/tiny-engine-common'
import { useTranslate, useModal, useHelp, getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register'
import {
useTranslate,
useModal,
useHelp,
getMetaApi,
META_SERVICE,
useLayout
} from '@opentiny/tiny-engine-meta-register'
import { getMergeMeta } from '@opentiny/tiny-engine-meta-register'
import { utils, constants } from '@opentiny/tiny-engine-utils'
import { BASE_URL } from '@opentiny/tiny-engine-common/js/environments'
@ -138,7 +152,12 @@ export default {
SearchEmpty,
IconUpload: iconUpload()
},
setup() {
props: {
fixedPanels: {
type: Array
}
},
setup(props, { emit }) {
// iconLoadingShadowicon
const SvgIcon = resolveComponent('SvgIcon')
const lightSpinnerIcon = iconLoadingShadow()
@ -146,6 +165,13 @@ export default {
const isLightTheme = getMergeMeta('engine.config').theme === 'light'
const { getLangs, i18nResource, currentLanguage, getI18nData } = useTranslate()
const { toClipboard } = useClipboard()
const { PLUGIN_NAME } = useLayout()
const panelState = reactive({
emitEvent: emit
})
provide('panelState', panelState)
const fullLangList = computed(() => {
const langs = getLangs()
@ -392,6 +418,7 @@ export default {
}
return {
PLUGIN_NAME,
sortTypeChanges,
currentSearchType,
i18nSearchTypes,

View File

@ -3,5 +3,5 @@ export default {
title: '物料',
type: 'plugins',
icon: 'plugin-icon-materials',
align: 'top'
align: 'leftTop'
}

View File

@ -1,5 +1,12 @@
<template>
<plugin-setting v-if="panel.show" :title="validGroup.groupName" @cancel="closeGroupPanel" @save="addBlocks">
<plugin-setting
v-if="panel.show"
:align="align"
:title="validGroup.groupName"
:fixed-name="PLUGIN_NAME.Materials"
@cancel="closeGroupPanel"
@save="addBlocks"
>
<template #content>
<div class="block-add-content">
<div class="block-add-content-title">区块列表</div>
@ -27,12 +34,13 @@
</plugin-setting>
</template>
<script>
import { reactive, watch, provide, inject, ref } from 'vue'
import { reactive, watch, provide, inject, ref, computed } from 'vue'
import { Search } from '@opentiny/vue'
import { iconSearch } from '@opentiny/vue-icon'
import { PluginSetting } from '@opentiny/tiny-engine-common'
import {
useBlock,
useLayout,
useModal,
useResource,
useNotify,
@ -117,6 +125,9 @@ export default {
const validGroup = ref({ ...selectedGroup.value })
const { PLUGIN_NAME, getPluginByLayout } = useLayout()
const align = computed(() => getPluginByLayout(PLUGIN_NAME.Materials))
watch(
() => selectedGroup.value.groupId,
(groupId) => {
@ -271,12 +282,14 @@ export default {
})
return {
align,
validGroup,
state,
panel,
closeGroupPanel,
addBlocks,
searchBlocks
searchBlocks,
PLUGIN_NAME
}
}
}

View File

@ -1,13 +1,10 @@
<template>
<plugin-panel :title="shortcut ? '' : title" @close="$emit('close')">
<template #header>
<component
v-if="!onlyShowDefault"
:is="registryData?.components?.header"
:fixedPanels="fixedPanels"
@fix-panel="(id) => $emit('fix-panel', id)"
></component>
</template>
<plugin-panel
:title="shortcut ? '' : title"
:fixed-name="PLUGIN_NAME.Materials"
:fixedPanels="fixedPanels"
@close="$emit('close')"
>
<template #content>
<tiny-tabs v-model="activeName" tab-style="button-card" class="full-width-tabs" v-if="!onlyShowDefault">
<tiny-tab-item :key="item.id" v-for="item in tabComponents" :title="item.title" :name="item.id">
@ -23,7 +20,7 @@
<script>
import { reactive, provide, ref, computed } from 'vue'
import { Tabs, TabItem } from '@opentiny/vue'
import { getMergeMeta } from '@opentiny/tiny-engine-meta-register'
import { META_APP as PLUGIN_NAME, getMergeMeta } from '@opentiny/tiny-engine-meta-register'
import { PluginPanel } from '@opentiny/tiny-engine-common'
export default {
@ -83,6 +80,7 @@ export default {
const title = ref(props.registryData?.title)
return {
PLUGIN_NAME,
title,
activeName,
defaultComponent,

View File

@ -3,5 +3,5 @@ export default {
title: '页面',
type: 'plugins',
icon: 'plugin-icon-page',
align: 'top'
align: 'leftTop'
}

View File

@ -1,6 +1,13 @@
<template>
<div class="plugin-page">
<plugin-panel :title="title" @close="pluginPanelClosed" :docsUrl="docsUrl" :isShowDocsIcon="true">
<plugin-panel
:title="title"
:fixed-name="PLUGIN_NAME.AppManage"
:fixedPanels="fixedPanels"
@close="pluginPanelClosed"
:docsUrl="docsUrl"
:isShowDocsIcon="true"
>
<template #header>
<svg-button
class="add-folder-icon"
@ -36,7 +43,7 @@
<script lang="jsx">
import { reactive, ref, watchEffect, provide } from 'vue'
import { useCanvas, usePage, useHelp, useModal, useNotify } from '@opentiny/tiny-engine-meta-register'
import { useCanvas, usePage, useHelp, useModal, useNotify, useLayout } from '@opentiny/tiny-engine-meta-register'
import { PluginPanel, SvgButton } from '@opentiny/tiny-engine-common'
import { extend } from '@opentiny/vue-renderless/common/object'
import PageSetting, { openPageSettingPanel, closePageSettingPanel } from './PageSetting.vue'
@ -67,13 +74,25 @@ export default {
title: {
type: String,
default: '页面'
},
fixedPanels: {
type: Array
}
},
setup() {
emits: ['close'],
setup(props, { emit }) {
const { confirm } = useModal()
const { pageState } = useCanvas()
const { pageSettingState, getDefaultPage, isTemporaryPage, initCurrentPageData } = usePage()
const { PLUGIN_NAME } = useLayout()
const panelState = reactive({
emitEvent: emit
})
provide('panelState', panelState)
const pageTreeRef = ref(null)
const ROOT_ID = pageSettingState.ROOT_ID
const docsUrl = useHelp().getDocsUrl('page')
@ -169,6 +188,7 @@ export default {
provide('openSettingPanel', openSettingPanel)
const pluginPanelClosed = () => {
emit('close')
closePageSettingPanel()
closeFolderSettingPanel()
}
@ -178,6 +198,7 @@ export default {
}
return {
PLUGIN_NAME,
state,
pageState,
openNewPage,

View File

@ -1,5 +1,11 @@
<template>
<plugin-setting v-if="isShow" :title="state.title" class="pageFolder-plugin-setting">
<plugin-setting
v-if="isShow"
:fixed-name="PLUGIN_NAME.AppManage"
:align="align"
:title="state.title"
class="pageFolder-plugin-setting"
>
<template #header>
<button-group>
<tiny-button type="primary" @click="saveFolderSetting">保存</tiny-button>
@ -27,11 +33,12 @@
</template>
<script>
import { reactive, ref } from 'vue'
import { reactive, ref, computed } from 'vue'
import { Button, Collapse, CollapseItem } from '@opentiny/vue'
import { PluginSetting, SvgButton, ButtonGroup } from '@opentiny/tiny-engine-common'
import {
usePage,
useLayout,
useModal,
useNotify,
getMergeRegistry,
@ -83,6 +90,9 @@ export default {
const pageGeneral = registry.components.PageGeneral
const folderGeneralRef = ref(null)
const { PLUGIN_NAME, getPluginByLayout } = useLayout()
const align = computed(() => getPluginByLayout(PLUGIN_NAME.AppManage))
const closeFolderSetting = () => {
if (isEqual(pageSettingState.currentPageData, pageSettingState.currentPageDataCopy)) {
closeFolderSettingPanel()
@ -197,6 +207,8 @@ export default {
}
return {
align,
PLUGIN_NAME,
saveFolderSetting,
deleteFolder: throttle(5000, true, deleteFolder),
pageGeneral,

View File

@ -1,5 +1,11 @@
<template>
<plugin-setting v-if="isShow" :title="state.title" class="page-plugin-setting">
<plugin-setting
v-if="isShow"
:fixed-name="PLUGIN_NAME.AppManage"
:align="align"
:title="state.title"
class="page-plugin-setting"
>
<template #header>
<button-group>
<tiny-button type="primary" @click="savePageSetting">保存</tiny-button>
@ -54,7 +60,7 @@
</template>
<script lang="jsx">
import { reactive, ref } from 'vue'
import { reactive, ref, computed } from 'vue'
import { Button, Collapse, CollapseItem, Input } from '@opentiny/vue'
import { PluginSetting, ButtonGroup, SvgButton, LifeCycles } from '@opentiny/tiny-engine-common'
import {
@ -136,6 +142,9 @@ export default {
const beforeCreatePage = registry?.options?.beforeCreatePage
const pageGeneralRef = ref(null)
const { PLUGIN_NAME, getPluginByLayout } = useLayout()
const align = computed(() => getPluginByLayout(PLUGIN_NAME.AppManage))
const state = reactive({
activeName: Object.values(PAGE_SETTING_SESSION),
title: '页面设置',
@ -398,6 +407,8 @@ export default {
}
return {
align,
PLUGIN_NAME,
state,
isShow,
savePageSetting,

View File

@ -5,5 +5,5 @@ export default {
title: 'AI对话框',
type: 'plugins',
icon: RobotIcon,
align: 'bottom'
align: 'leftBottom'
}

View File

@ -3,5 +3,7 @@ export default {
title: '页面 Schema',
type: 'plugins',
icon: 'plugin-icon-page-schema',
align: 'bottom'
align: 'leftBottom',
widthResizable: true,
width: 600
}

View File

@ -1,45 +1,49 @@
<template>
<div id="source-code" class="plugin-schema">
<div class="source-code-header">
<div class="title">页面Schema</div>
<div class="header-title">
<!-- 暂时放开schema录入功能等画布功能完善后再打开下面一行的注释 -->
<!-- <tiny-popover v-if="isEdit" placement="bottom" trigger="hover" append-to-body content="保存"> -->
<span class="icon-wrap" @click="saveSchema">
<i v-show="!showRed" class="red"></i>
<tiny-button type="primary">保存</tiny-button>
</span>
<tiny-popover v-show="false" placement="bottom" trigger="hover" append-to-body content="导入 Schema">
<template #reference>
<span class="icon-wrap">
<icon-download-link></icon-download-link>
</span>
</template>
</tiny-popover>
<close-icon @close="close"></close-icon>
<plugin-panel
id="source-code"
title="页面 Schema"
class="plugin-schema"
:fixed-name="PLUGIN_NAME.Schema"
:fixedPanels="fixedPanels"
@close="close"
>
<template #header>
<span class="icon-wrap">
<i v-show="!showRed" class="red"></i>
<tiny-button type="primary" @click="saveSchema">保存</tiny-button>
</span>
<tiny-popover v-show="false" placement="bottom" trigger="hover" append-to-body content="导入 Schema">
<template #reference>
<span class="icon-wrap">
<icon-download-link></icon-download-link>
</span>
</template>
</tiny-popover>
</template>
<template #content>
<div class="source-code-content">
<monaco-editor
ref="container"
class="code-edit-content"
:value="state.pageData"
:options="options"
@change="editorChange"
@shortcutSave="saveSchema"
></monaco-editor>
</div>
</div>
<div class="source-code-content">
<monaco-editor
ref="container"
class="code-edit-content"
:value="state.pageData"
:options="options"
@change="editorChange"
@shortcutSave="saveSchema"
></monaco-editor>
</div>
<div class="source-code-footer">
<button>导入 Schema</button>
</div>
</div>
<div class="source-code-footer">
<button>导入 Schema</button>
</div>
</template>
</plugin-panel>
</template>
<script lang="jsx">
import { nextTick, reactive, getCurrentInstance, onActivated, ref, onDeactivated } from 'vue'
import { nextTick, reactive, getCurrentInstance, onActivated, ref, onDeactivated, provide } from 'vue'
import { Popover, Button } from '@opentiny/vue'
import { VueMonaco, CloseIcon } from '@opentiny/tiny-engine-common'
import { useCanvas, useModal, useNotify, useMessage } from '@opentiny/tiny-engine-meta-register'
import { VueMonaco, PluginPanel } from '@opentiny/tiny-engine-common'
import { useCanvas, useModal, useNotify, useMessage, useLayout } from '@opentiny/tiny-engine-meta-register'
import { utils } from '@opentiny/tiny-engine-utils'
import { iconDownloadLink } from '@opentiny/vue-icon'
import { useThrottleFn } from '@vueuse/core'
@ -51,9 +55,14 @@ export default {
MonacoEditor: VueMonaco,
TinyPopover: Popover,
TinyButton: Button,
CloseIcon,
PluginPanel,
IconDownloadLink: iconDownloadLink()
},
props: {
fixedPanels: {
type: Array
}
},
setup(props, { emit }) {
const app = getCurrentInstance()
const { pageState } = useCanvas()
@ -63,6 +72,13 @@ export default {
})
const { subscribe, unsubscribe } = useMessage()
const { PLUGIN_NAME } = useLayout()
const panelState = reactive({
emitEvent: emit
})
provide('panelState', panelState)
const isEdit = false
const showRed = ref(true)
@ -148,6 +164,7 @@ export default {
})
return {
PLUGIN_NAME,
state,
isEdit,
saveSchema,
@ -169,57 +186,35 @@ export default {
<style lang="less" scoped>
#source-code {
width: 50vw;
height: calc(100% - var(--base-top-panel-height));
padding: 12px 0;
position: fixed;
top: var(--base-top-panel-height);
left: 41px;
background: var(--te-schema-panel-bg-color);
box-shadow: 6px 0px 3px 0px var(--te-schema-panel-shadow-color);
z-index: 1000;
.source-code-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--te-schema-common-border-color);
margin-bottom: 12px;
padding: 0 12px 12px;
}
.title {
color: var(--te-schema-panel-title-text-color);
font-weight: var(--te-base-font-weight-bold);
}
.header-title {
display: flex;
justify-content: flex-end;
align-items: center;
.icon-wrap {
position: relative;
.tiny-button {
min-width: 40px;
margin-right: 2px;
height: 24px;
line-height: 24px;
}
.red {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: var(--te-schema-dot-color);
display: block;
z-index: 100;
position: absolute;
top: -3px;
right: -4px;
}
.icon-wrap {
position: relative;
margin-right: 6px;
.tiny-button {
min-width: 40px;
margin-right: 2px;
height: 24px;
line-height: 24px;
}
& > span:not(:last-child) {
margin-right: 8px;
.red {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: var(--te-schema-dot-color);
display: block;
z-index: 100;
position: absolute;
top: -3px;
right: -1px;
}
}
.source-code-content {
height: calc(100% - 42px);
height: calc(100% - 12px);
border: 1px solid var(--te-schema-common-border-color);
border-radius: 4px;
margin: 0 12px;

View File

@ -3,6 +3,8 @@ export default {
title: '页面 JS',
type: 'plugins',
icon: 'plugin-icon-js',
align: 'top',
align: 'leftTop',
width: 600,
widthResizable: true,
confirm: 'close' // 当点击插件栏切换或关闭前是否需要确认, 会调用插件中confirm值指定的方法e.g. 此处指向 close方法会调用插件的close方法执行确认逻辑
}

View File

@ -1,36 +1,39 @@
<template>
<div class="plugin-page-js-container plugin-script">
<div class="code-edit-head">
<div class="head-left">
<span class="title">页面 JS</span>
<link-button :href="docsUrl"></link-button>
<plugin-panel
title="页面 JS"
:fixed-name="PLUGIN_NAME.Page"
:fixedPanels="fixedPanels"
:docsUrl="docsUrl"
:isShowDocsIcon="true"
@close="$emit('close')"
class="plugin-page-js-container plugin-script"
>
<template #header>
<span class="icon-wrap">
<i v-show="state.isChanged" class="red"></i>
<tiny-button type="primary" @click="saveMethods">保存</tiny-button>
</span>
</template>
<template #content>
<div class="code-edit-content">
<monaco-editor
ref="monaco"
:value="state.script"
:options="options"
@change="change"
@editorDidMount="editorDidMount"
@shortcutSave="saveMethods"
></monaco-editor>
</div>
<div class="head-right">
<tiny-button type="primary" class="save-btn" @click="saveMethods">
<span>保存</span>
<span v-show="state.isChanged" class="dots"></span>
</tiny-button>
<close-icon @close="close"></close-icon>
</div>
</div>
<div class="code-edit-content">
<monaco-editor
ref="monaco"
:value="state.script"
:options="options"
@change="change"
@editorDidMount="editorDidMount"
@shortcutSave="saveMethods"
></monaco-editor>
</div>
</div>
</template>
</plugin-panel>
</template>
<script>
import { onBeforeUnmount } from 'vue'
import { onBeforeUnmount, reactive, provide } from 'vue'
import { Button } from '@opentiny/vue'
import { VueMonaco, CloseIcon, LinkButton } from '@opentiny/tiny-engine-common'
import { useHelp } from '@opentiny/tiny-engine-meta-register'
import { VueMonaco, PluginPanel } from '@opentiny/tiny-engine-common'
import { useHelp, useLayout } from '@opentiny/tiny-engine-meta-register'
import { initCompletion } from '@opentiny/tiny-engine-common/js/completion'
import { initLinter } from '@opentiny/tiny-engine-common/js/linter'
import useMethod, { saveMethod, highlightMethod, getMethodNameList, getMethods } from './js/method'
@ -46,14 +49,25 @@ export default {
components: {
MonacoEditor: VueMonaco,
TinyButton: Button,
CloseIcon,
LinkButton
PluginPanel
},
props: {
fixedPanels: {
type: Array
}
},
emits: ['close'],
setup(props, { emit }) {
const docsUrl = useHelp().getDocsUrl('script')
const { state, monaco, change, close, saveMethods } = useMethod({ emit })
const { PLUGIN_NAME } = useLayout()
const panelState = reactive({
emitEvent: emit
})
provide('panelState', panelState)
const options = {
language: 'javascript',
minimap: {
@ -105,6 +119,7 @@ export default {
})
return {
PLUGIN_NAME,
state,
monaco,
options,
@ -120,59 +135,36 @@ export default {
<style lang="less" scoped>
.plugin-page-js-container {
width: 50vw;
height: 100%;
background: var(--te-plugin-js-panel-bg-color);
box-shadow: 6px 0px 3px 0px var(--te-plugin-js-panel-shadow-color);
position: absolute;
left: 0;
top: 0;
z-index: 999;
box-sizing: border-box;
.code-edit-head {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--te-plugin-js-common-border-color);
padding: 12px 0;
.icon-wrap {
position: relative;
margin-right: 6px;
.head-left {
padding-left: 12px;
display: flex;
align-items: center;
.title {
color: var(--te-plugin-js-panel-title-text-color);
font-weight: var(--te-base-font-weight-bold);
}
.tiny-button {
min-width: 40px;
margin-right: 2px;
height: 24px;
line-height: 24px;
}
.head-right {
margin-right: 12px;
display: flex;
align-items: center;
.save-btn {
min-width: 40px;
margin-right: 8px;
height: 24px;
line-height: 24px;
.dots {
width: 6px;
height: 6px;
background: var(--te-plugin-js-dot-color);
border-radius: 50%;
position: absolute;
top: 9px;
right: 34px;
}
}
.red {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--te-plugin-js-dot-color);
display: block;
z-index: 100;
position: absolute;
top: -3px;
right: -1px;
}
}
.code-edit-content {
padding: 12px;
height: calc(100% - 54px);
padding: 0 12px;
height: calc(100% - 12px);
& > div {
border: 1px solid var(--te-plugin-js-common-border-color);
@ -181,6 +173,7 @@ export default {
}
}
}
:deep(.help-box) {
height: auto;
#help-icon {

View File

@ -2,6 +2,6 @@ export default {
id: 'engine.plugins.state',
title: '状态管理',
type: 'plugins',
align: 'top',
align: 'leftTop',
icon: 'plugin-icon-var'
}

View File

@ -1,78 +1,84 @@
<template>
<div id="data-source" class="plugin-state">
<div class="data-source-left-panel">
<div class="title">
<span>状态管理</span>
<link-button :href="docsUrl"></link-button>
<close-icon @close="closePanel"></close-icon>
<plugin-panel
id="data-source"
title="状态管理"
class="plugin-state"
:fixed-name="PLUGIN_NAME.State"
:fixedPanels="fixedPanels"
:docsUrl="docsUrl"
:isShowDocsIcon="true"
@close="closePanel"
>
<template #content>
<div class="data-source-left-panel">
<tiny-tabs v-model="activeName" @click="tabClick" tab-style="button-card">
<tiny-tab-item :name="STATE.CURRENT_STATE" :title="isBlock ? '区块状态' : '页面状态'"></tiny-tab-item>
<tiny-tab-item :name="STATE.GLOBAL_STATE" title="应用状态"></tiny-tab-item>
</tiny-tabs>
<tiny-search
:modelValue="query"
class="left-filter"
placeholder="请输入搜索条件"
clearable
@update:modelValue="search"
>
<template #prefix>
<tiny-icon-search />
</template>
</tiny-search>
<div class="add-btn">
<tiny-button @click="openPanel(OPTION_TYPE.ADD)">
<svg-icon name="add" class="add-btn-icon"></svg-icon>
<span class="add-btn-text">{{ activeName === STATE.CURRENT_STATE ? '添加变量' : '添加全局变量' }}</span>
</tiny-button>
</div>
<data-source-list
:modelValue="Object.keys(state.dataSource)"
:stateScope="activeName"
:query="query"
:selectedKey="selectedKey"
@openPanel="openPanel"
@remove="remove"
@removeStore="removeStore"
/>
</div>
<tiny-tabs v-model="activeName" @click="tabClick" tab-style="button-card">
<tiny-tab-item :name="STATE.CURRENT_STATE" :title="isBlock ? '区块状态' : '页面状态'"></tiny-tab-item>
<tiny-tab-item :name="STATE.GLOBAL_STATE" title="应用状态"></tiny-tab-item>
</tiny-tabs>
<tiny-search
:modelValue="query"
class="left-filter"
placeholder="请输入搜索条件"
clearable
@update:modelValue="search"
>
<template #prefix>
<tiny-icon-search />
</template>
</tiny-search>
<div class="add-btn">
<tiny-button @click="openPanel(OPTION_TYPE.ADD)">
<svg-icon name="add" class="add-btn-icon"></svg-icon>
<span class="add-btn-text">{{ activeName === STATE.CURRENT_STATE ? '添加变量' : '添加全局变量' }}</span>
</tiny-button>
<div class="data-source-right-panel" v-if="isPanelShow" :style="alignStyle">
<div class="header">
<span>{{ addDataSource }}</span>
<span class="options-wrap">
<tiny-button type="primary" @click="confirm">保存</tiny-button>
<close-icon @close="cancel"></close-icon>
</span>
</div>
<create-variable
v-if="activeName === STATE.CURRENT_STATE"
ref="variableRef"
:dataSource="state.dataSource"
:flag="flag"
:updateKey="updateKey"
:createData="state.createData"
@nameInput="updateName"
@close="cancel"
@mouseleave="onMouseLeaveVariable"
/>
<create-store
v-if="activeName === STATE.GLOBAL_STATE"
ref="storeRef"
:dataSource="state.dataSource"
:flag="flag"
:updateKey="updateKey"
:storeData="state.createData"
@nameInput="validName"
@close="cancel"
@mouseleave="onMouseLeaveStore"
/>
</div>
<data-source-list
:modelValue="Object.keys(state.dataSource)"
:stateScope="activeName"
:query="query"
:selectedKey="selectedKey"
@openPanel="openPanel"
@remove="remove"
@removeStore="removeStore"
/>
</div>
<div v-if="isPanelShow" class="data-source-right-panel">
<div class="header">
<span>{{ addDataSource }}</span>
<span class="options-wrap">
<tiny-button type="primary" @click="confirm">保存</tiny-button>
<close-icon @close="cancel"></close-icon>
</span>
</div>
<create-variable
v-if="activeName === STATE.CURRENT_STATE"
ref="variableRef"
:dataSource="state.dataSource"
:flag="flag"
:updateKey="updateKey"
:createData="state.createData"
@nameInput="updateName"
@close="cancel"
@mouseleave="onMouseLeaveVariable"
/>
<create-store
v-if="activeName === STATE.GLOBAL_STATE"
ref="storeRef"
:dataSource="state.dataSource"
:flag="flag"
:updateKey="updateKey"
:storeData="state.createData"
@nameInput="validName"
@close="cancel"
@mouseleave="onMouseLeaveStore"
/>
</div>
</div>
</template>
</plugin-panel>
</template>
<script>
import { reactive, ref, computed, onActivated, watch } from 'vue'
import { reactive, ref, computed, onActivated, watch, provide } from 'vue'
import { Button, Search, Tabs, TabItem } from '@opentiny/vue'
import {
useCanvas,
@ -80,13 +86,14 @@ import {
useResource,
useNotify,
useHelp,
useLayout,
getMetaApi,
META_APP,
META_SERVICE
} from '@opentiny/tiny-engine-meta-register'
import { getCommentByKey } from '@opentiny/tiny-engine-common/js/comment'
import { iconSearch } from '@opentiny/vue-icon'
import { CloseIcon, LinkButton } from '@opentiny/tiny-engine-common'
import { CloseIcon, PluginPanel } from '@opentiny/tiny-engine-common'
import DataSourceList from './DataSourceList.vue'
import CreateVariable from './CreateVariable.vue'
import CreateStore from './CreateStore.vue'
@ -104,9 +111,14 @@ export default {
TinyTabs: Tabs,
TinyTabItem: TabItem,
CreateStore,
LinkButton,
PluginPanel,
TinyIconSearch: iconSearch()
},
props: {
fixedPanels: {
type: Array
}
},
setup(props, { emit }) {
const variableRef = ref(null)
const storeRef = ref(null)
@ -131,6 +143,24 @@ export default {
})
const selectedKey = ref(null)
const { PLUGIN_NAME, getPluginWidth, getPluginByLayout } = useLayout()
const firstPanelOffset = computed(() => {
return getPluginWidth(PLUGIN_NAME.State)
})
const alignStyle = computed(() => {
const panelAlign = getPluginByLayout(PLUGIN_NAME.State)
const align = panelAlign.includes('left') ? 'left' : 'right'
return `${align}: ${firstPanelOffset.value}px`
})
const panelState = reactive({
emitEvent: emit
})
provide('panelState', panelState)
watch(activeName, () => {
selectedKey.value = null
})
@ -355,6 +385,9 @@ export default {
})
return {
alignStyle,
firstPanelOffset,
PLUGIN_NAME,
isBlock,
isPanelShow,
errorMessage,
@ -420,18 +453,6 @@ export default {
}
}
.title {
padding: 10px;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
'Helvetica Neue', sans-serif;
color: var(--te-state-common-text-color);
font-weight: var(--te-base-font-weight-bold);
border-bottom: 1px solid var(--te-state-common-border-color-divider);
display: flex;
justify-content: space-between;
align-items: center;
}
.left-filter {
margin-top: 4px;
padding: 0 8px;
@ -459,7 +480,6 @@ export default {
border-right: 1px solid var(--te-state-common-border-color-divider);
background: var(--te-state-common-bg-color);
position: absolute;
left: var(--base-left-panel-width);
top: 0;
.header {
@ -490,7 +510,7 @@ export default {
}
:deep(.tiny-tabs__header) {
padding: 8px;
padding: 0 8px 8px 8px;
}
:deep(.tiny-tabs__header .tiny-tabs__active-bar) {

View File

@ -3,5 +3,6 @@ export default {
title: '大纲树',
type: 'plugins',
icon: 'plugin-icon-tree',
align: 'top'
widthResizable: true,
align: 'leftTop'
}

View File

@ -1,13 +1,13 @@
<template>
<plugin-panel class="outlinebox plugin-tree" title="大纲树" @close="$emit('close')" ref="panelRef" tabindex="0">
<template #header>
<svg-button
class="item icon-sidebar"
:name="panelFixed ? 'fixed-solid' : 'fixed'"
:tips="panelFixed ? '解除固定面板' : '固定面板'"
@click="$emit('fix-panel', PLUGIN_NAME.OutlineTree)"
></svg-button>
</template>
<plugin-panel
tabindex="0"
title="大纲树"
ref="panelRef"
class="outlinebox plugin-tree"
:fixed-name="PLUGIN_NAME.OutlineTree"
:fixedPanels="fixedPanels"
@close="$emit('close')"
>
<template #content>
<draggable-tree
label-key="componentName"
@ -38,8 +38,19 @@
</template>
<script>
import { reactive, watch, computed, onActivated, onDeactivated, onMounted, onBeforeUnmount, nextTick, ref } from 'vue'
import { PluginPanel, SvgButton } from '@opentiny/tiny-engine-common'
import {
reactive,
watch,
computed,
onActivated,
onDeactivated,
provide,
onMounted,
onBeforeUnmount,
nextTick,
ref
} from 'vue'
import { PluginPanel } from '@opentiny/tiny-engine-common'
import { constants } from '@opentiny/tiny-engine-utils'
import {
useCanvas,
@ -56,7 +67,6 @@ const { PAGE_STATUS } = constants
export default {
components: {
PluginPanel,
SvgButton,
DraggableTree
},
props: {
@ -65,7 +75,7 @@ export default {
}
},
emits: ['close', 'fix-panel'],
setup(props) {
setup(props, { emit }) {
const { pageState } = useCanvas()
const { getMaterial } = useMaterial()
const { PLUGIN_NAME } = useLayout()
@ -76,6 +86,11 @@ export default {
const selectedIds = computed(() => useMultiSelect().multiSelectedStates.value.map((state) => state.id))
const panelState = reactive({
emitEvent: emit
})
provide('panelState', panelState)
const filterSchema = (data) => {
const translateChild = (data) => {
data.forEach((item) => {

View File

@ -3,5 +3,5 @@ export default {
title: 'TinyEngine 教程',
type: 'plugins',
icon: 'plugin-icon-tutorial',
align: 'bottom'
align: 'leftBottom'
}

View File

@ -9,7 +9,7 @@
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { isRef } from 'vue'
import { initHook } from './hooks'
const vueLifeHook = [
@ -161,7 +161,7 @@ export const preprocessRegistry = (registry) => {
export const generateRegistry = (registry) => {
Object.entries(registry).forEach(([key, value]) => {
if (typeof value === 'object' && value) {
if (typeof value === 'object' && value && !isRef(value)) {
const { id } = value
// 如果匹配到了id说明是元服务配置对元服务配置做读取和写入
if (id && key !== 'metaData') {

View File

@ -20,9 +20,7 @@ import './src/styles/vars.less'
export default {
...metaData,
entry,
options: {
commonEvents
},
options: { commonEvents },
components: {
BindEventsDialogSidebar,
BindEventsDialogContent

View File

@ -2,7 +2,7 @@ export default {
id: 'engine.setting.event',
title: '高级',
type: 'setting',
align: 'left',
align: 'rightTop',
name: 'event',
icon: ''
icon: 'target'
}

View File

@ -1,19 +1,44 @@
<template>
<tiny-collapse v-model="activeNames">
<tiny-collapse-item title="事件绑定" name="bindEvent">
<bind-events></bind-events>
</tiny-collapse-item>
<tiny-collapse-item title="高级配置" name="advancedConfig">
<advance-config></advance-config>
</tiny-collapse-item>
</tiny-collapse>
<plugin-panel
title="高级"
:fixed-panels="fixedPanels"
:fixed-name="PLUGIN_NAME.Event"
:header-margin-bottom="0"
@close="$emit('close')"
>
<template #content>
<tiny-collapse v-model="activeNames">
<tiny-collapse-item title="事件绑定" name="bindEvent">
<bind-events></bind-events>
</tiny-collapse-item>
<tiny-collapse-item title="高级配置" name="advancedConfig">
<advance-config></advance-config>
</tiny-collapse-item>
</tiny-collapse>
</template>
</plugin-panel>
</template>
<script setup>
import { ref } from 'vue'
import { ref, reactive, provide, defineProps, defineEmits } from 'vue'
import { Collapse as TinyCollapse, CollapseItem as TinyCollapseItem } from '@opentiny/vue'
import BindEvents from './components/BindEvents.vue'
import AdvanceConfig from './components/AdvanceConfig.vue'
import { PluginPanel } from '@opentiny/tiny-engine-common'
import { useLayout } from '@opentiny/tiny-engine-meta-register'
const activeNames = ref(['bindEvent', 'advancedConfig'])
const { PLUGIN_NAME } = useLayout()
defineProps({
fixedPanels: Array
})
const emit = defineEmits([])
const panelState = reactive({
emitEvent: emit
})
provide('panelState', panelState)
</script>

View File

@ -2,7 +2,7 @@ export default {
id: 'engine.setting.props',
title: '属性',
type: 'setting',
align: 'left',
align: 'rightTop',
name: 'props',
icon: ''
icon: 'form'
}

View File

@ -1,17 +1,27 @@
<template>
<config-render :data="properties">
<template #prefix="{ data }">
<block-link-field v-if="isBlock" :data="data"></block-link-field>
<plugin-panel
title="属性"
:fixed-panels="fixedPanels"
:fixed-name="PLUGIN_NAME.Props"
:show-bottom-border="showEmptyTips"
@close="$emit('close')"
>
<template #content>
<config-render :data="properties">
<template #prefix="{ data }">
<block-link-field v-if="isBlock" :data="data"></block-link-field>
</template>
</config-render>
<block-description v-if="isBlock" class="block-description"> </block-description>
<empty :showEmptyTips="showEmptyTips"></empty>
</template>
</config-render>
<block-description v-if="isBlock" class="block-description"> </block-description>
<empty :showEmptyTips="showEmptyTips"></empty>
</plugin-panel>
</template>
<script>
import { computed, watchEffect, ref } from 'vue'
import { ConfigRender, BlockDescription, BlockLinkField } from '@opentiny/tiny-engine-common'
import { useCanvas, useProperty } from '@opentiny/tiny-engine-meta-register'
import { computed, watchEffect, ref, reactive, provide } from 'vue'
import { ConfigRender, BlockDescription, BlockLinkField, PluginPanel } from '@opentiny/tiny-engine-common'
import { useCanvas, useProperty, useLayout } from '@opentiny/tiny-engine-meta-register'
import Empty from './components/Empty.vue'
export default {
@ -19,13 +29,21 @@ export default {
ConfigRender,
BlockLinkField,
BlockDescription,
Empty
Empty,
PluginPanel
},
setup() {
setup(props, { emit }) {
const { pageState } = useCanvas()
const { properties } = useProperty().getProperty({ pageState })
const showEmptyTips = ref(false)
const { PLUGIN_NAME } = useLayout()
const panelState = reactive({
emitEvent: emit
})
provide('panelState', panelState)
const isBlock = computed(() => pageState.isBlock)
watchEffect(() => {
@ -33,6 +51,7 @@ export default {
})
return {
PLUGIN_NAME,
isBlock,
properties,
showEmptyTips

View File

@ -3,6 +3,6 @@ export default {
title: '样式',
name: 'style',
type: 'setting',
align: 'left',
icon: ''
align: 'rightTop',
icon: 'display-inline'
}

View File

@ -1,75 +1,87 @@
<template>
<div class="style-editor">
<div class="line-style">
<span class="line-text"> 行内样式 </span>
<div class="inline-style">
<component
:is="CodeConfigurator"
v-if="state.lineStyleDisable"
:buttonShowContent="true"
:modelValue="state.styleContent"
title="编辑行内样式"
:button-text="state.inlineBtnText"
language="css"
single
@save="save"
></component>
<div v-if="!state.lineStyleDisable">
<tiny-input v-model="state.propertiesList" class="inline-bind-style"> </tiny-input>
<plugin-panel
title="样式"
:fixed-panels="fixedPanels"
:fixed-name="PLUGIN_NAME.Styles"
:is-show-collapse-icon="true"
:show-bottom-border="true"
@updateCollapseStatus="updateCollapseStatus"
@close="$emit('close')"
>
<template #content>
<div class="style-editor">
<div class="line-style">
<span class="line-text"> 行内样式 </span>
<div class="inline-style">
<component
:is="CodeConfigurator"
v-if="state.lineStyleDisable"
:buttonShowContent="true"
:modelValue="state.styleContent"
title="编辑行内样式"
:button-text="state.inlineBtnText"
language="css"
single
@save="save"
></component>
<div v-if="!state.lineStyleDisable">
<tiny-input v-model="state.propertiesList" class="inline-bind-style"> </tiny-input>
</div>
<component
:is="VariableConfigurator"
ref="bindVariable"
:model-value="state.bindModelValue"
name="advance"
@update:modelValue="setConfig"
>
</component>
</div>
</div>
<component
:is="VariableConfigurator"
ref="bindVariable"
:model-value="state.bindModelValue"
name="advance"
@update:modelValue="setConfig"
>
</component>
</div>
</div>
</div>
<class-names-container></class-names-container>
<tiny-collapse v-model="activeNames" @change="handoverGroup">
<tiny-collapse-item title="布局" name="layout">
<layout-group :display="state.style.display" @update="updateStyle" />
<flex-box v-if="state.style.display === 'flex'" :style="state.style" @update="updateStyle"></flex-box>
<grid-box v-if="state.style.display === 'grid'" :style="state.style" @update="updateStyle"></grid-box>
</tiny-collapse-item>
<class-names-container></class-names-container>
<tiny-collapse v-model="activeNames" @change="handoverGroup">
<tiny-collapse-item title="布局" name="layout">
<layout-group :display="state.style.display" @update="updateStyle" />
<flex-box v-if="state.style.display === 'flex'" :style="state.style" @update="updateStyle"></flex-box>
<grid-box v-if="state.style.display === 'grid'" :style="state.style" @update="updateStyle"></grid-box>
</tiny-collapse-item>
<tiny-collapse-item title="间距" name="spacing">
<spacing-group :style="state.style" @update="updateStyle" />
</tiny-collapse-item>
<tiny-collapse-item title="间距" name="spacing">
<spacing-group :style="state.style" @update="updateStyle" />
</tiny-collapse-item>
<tiny-collapse-item title="尺寸" name="size">
<size-group :style="state.style" @update="updateStyle" />
</tiny-collapse-item>
<tiny-collapse-item title="尺寸" name="size">
<size-group :style="state.style" @update="updateStyle" />
</tiny-collapse-item>
<tiny-collapse-item title="定位" name="position">
<position-group :style="state.style" @update="updateStyle" />
</tiny-collapse-item>
<tiny-collapse-item title="定位" name="position">
<position-group :style="state.style" @update="updateStyle" />
</tiny-collapse-item>
<tiny-collapse-item title="文本" name="typography">
<typography-group :style="state.style" @update="updateStyle" />
</tiny-collapse-item>
<tiny-collapse-item title="文本" name="typography">
<typography-group :style="state.style" @update="updateStyle" />
</tiny-collapse-item>
<tiny-collapse-item title="背景" name="backgrounds">
<background-group :style="state.style" @update="updateStyle" />
</tiny-collapse-item>
<tiny-collapse-item title="背景" name="backgrounds">
<background-group :style="state.style" @update="updateStyle" />
</tiny-collapse-item>
<tiny-collapse-item title="边框" name="borders">
<border-group :style="state.style" @update="updateStyle" />
</tiny-collapse-item>
<tiny-collapse-item title="边框" name="borders">
<border-group :style="state.style" @update="updateStyle" />
</tiny-collapse-item>
<tiny-collapse-item title="效果" name="effects" class="effects-style">
<effect-group :style="state.style" @update="updateStyle" />
</tiny-collapse-item>
</tiny-collapse>
<tiny-collapse-item title="效果" name="effects" class="effects-style">
<effect-group :style="state.style" @update="updateStyle" />
</tiny-collapse-item>
</tiny-collapse>
</template>
</plugin-panel>
</template>
<script>
import { watch, inject, ref } from 'vue'
import { watch, ref, reactive, provide } from 'vue'
import { Collapse, CollapseItem, Input } from '@opentiny/vue'
import { useHistory, useCanvas, useProperties, getConfigurator } from '@opentiny/tiny-engine-meta-register'
import { useLayout, useHistory, useCanvas, useProperties, getConfigurator } from '@opentiny/tiny-engine-meta-register'
import {
SizeGroup,
LayoutGroup,
@ -85,10 +97,12 @@ import {
} from './components'
import { CSS_TYPE } from './js/cssType'
import useStyle from './js/useStyle'
import { PluginPanel } from '@opentiny/tiny-engine-common'
import { styleStrRemoveRoot } from './js/cssConvert'
export default {
components: {
PluginPanel,
SizeGroup,
LayoutGroup,
FlexBox,
@ -104,7 +118,12 @@ export default {
TinyCollapseItem: CollapseItem,
TinyInput: Input
},
setup() {
props: {
fixedPanels: {
type: Array
}
},
setup(props, { emit }) {
const CodeConfigurator = getConfigurator('CodeConfigurator')
const VariableConfigurator = getConfigurator('VariableConfigurator')
const styleCategoryGroup = [
@ -117,13 +136,19 @@ export default {
'borders',
'effects'
]
const isCollapsed = inject('isCollapsed')
const isCollapsed = ref(false)
const activeNames = ref(styleCategoryGroup)
const { getCurrentSchema } = useCanvas()
// style
const { state, updateStyle } = useStyle() // updateStyle
const { addHistory } = useHistory()
const { getSchema, setProp } = useProperties()
const { PLUGIN_NAME } = useLayout()
const panelState = reactive({
emitEvent: emit
})
provide('panelState', panelState)
const handoverGroup = (actives) => {
if (isCollapsed.value) {
@ -196,6 +221,10 @@ export default {
}
)
const updateCollapseStatus = (val) => {
isCollapsed.value = val
}
watch(
() => isCollapsed.value,
() => {
@ -208,6 +237,8 @@ export default {
)
return {
updateCollapseStatus,
PLUGIN_NAME,
CodeConfigurator,
VariableConfigurator,
state,
@ -228,7 +259,7 @@ export default {
<style lang="less" scoped>
.style-editor {
justify-content: space-around;
padding: 12px 0 0;
margin-top: 12px;
column-gap: 8px;
.line-style {
padding: 0 8px 0 12px;

View File

@ -40,7 +40,7 @@ body {
--base-bottom-panel-height: 30px;
--base-nav-panel-width: 40px;
--base-collection-panel-width: calc(
(100vw - (var(--base-left-panel-width) + var(--base-right-panel-width) + var(--base-nav-panel-width) - 1px)) / 2
(99vw - (var(--base-left-panel-width) + var(--base-right-panel-width) + var(--base-nav-panel-width) - 1px)) / 2
);
--base-collection-panel-full-screen-width: calc(
(100vw - (var(--base-left-panel-width) + var(--base-right-panel-width) + var(--base-nav-panel-width) - 1px))

View File

@ -81,7 +81,7 @@ export default {
const showpopover = ref(false)
const themeShowType = computed(() => {
let filterList = THEME_DATA.value.filter((item) => ['light', 'dark'].includes(item.type)) || []
const filterList = THEME_DATA.value.filter((item) => ['light', 'dark'].includes(item.type)) || []
return THEME_DATA.value.length === filterList.length
})

View File

@ -97,7 +97,9 @@ export const BROADCAST_CHANNEL = {
CanvasRouterViewSetting: `tiny-lowcode-canvas-router-view-setting-${CHANNEL_UID}`
}
export const STORAGE_KEY_FIXED_PANELS = `tiny-engine-fixed-panels-${CHANNEL_UID}`
export const STORAGE_KEY_LEFT_FIXED_PANELS = `tiny-engine-left-fixed-panels`
export const STORAGE_KEY_RIGHT_FIXED_PANELS = `tiny-engine-right-fixed-panels`
export const CANVAS_ROUTER_VIEW_SETTING_VIEW_MODE_KEY = `tiny-engine-canvas-router-view-setting-view-mode`
export const AUTO_SAVED = 'tiny-engine-auto-saved'
@ -161,3 +163,6 @@ export const SORT_TYPE = {
// 字母倒序
alphabetDesc: 'alphabetDesc'
}
// 插件面板默认宽度
export const PLUGIN_DEFAULT_WIDTH = 280