配置数据动态配置,视图组件增加循环与动态绑定,配置数据增加模板配置,视图渲染模板配置

main
一杯沧海 12 months ago
parent 48a4b5b452
commit a635eead2a
  1. 458
      src/App.vue
  2. 14
      src/engineer/components/Activity.vue
  3. 28
      src/engineer/components/Builder.vue
  4. 36
      src/engineer/components/Canvas.vue
  5. 33
      src/engineer/components/ConfigCurrentView.vue
  6. 5
      src/engineer/components/Configurator.vue
  7. 14
      src/engineer/components/Designer.vue
  8. 3
      src/engineer/configs/ListConfig.vue
  9. 3
      src/engineer/configs/ObjectConfig.vue
  10. 107
      src/engineer/context.ts
  11. 28
      src/engineer/emitter.ts
  12. 18
      src/engineer/index.ts
  13. 191
      src/engineer/render.ts
  14. 78
      src/engineer/types.ts
  15. 3
      src/engineer/utils/align.ts
  16. 12
      src/engineer/utils/background.ts
  17. 4
      src/engineer/utils/clip.ts
  18. 9
      src/engineer/utils/dynamic.ts
  19. 2
      src/engineer/utils/index.ts
  20. 15
      src/engineer/utils/insets.ts
  21. 10
      src/engineer/utils/is.ts
  22. 2
      src/engineer/utils/radii.ts
  23. 15
      src/engineer/utils/size.ts
  24. 20
      src/engineer/utils/stack.ts
  25. 7
      src/engineer/utils/unit.ts
  26. 1
      src/engineer/views/AudioView.vue
  27. 48
      src/engineer/views/EachView.vue
  28. 1
      src/engineer/views/ImageView.vue
  29. 22
      src/engineer/views/RenderTemplate.vue
  30. 1
      src/engineer/views/TextView.vue
  31. 40
      src/engineer/views/View.vue

@ -6,110 +6,110 @@ import DddBuilder, { hash } from './engineer'
let nextId = Date.now() let nextId = Date.now()
const blocks = ref<Block[]>([ const blocks = ref<Block[]>([
// { // {
// vid: hash(`${++nextId}`), // vid: hash(`${++nextId}`),
// mid: '', // mid: '',
// theme: { // theme: {
// radius: 24, // radius: 24,
// color: 'yellow', // color: 'yellow',
// }, // },
// children: { // children: {
// type: 'text', // type: 'text',
// key: 'textName', // key: 'textName',
// }, // },
// }, { // }, {
// vid: hash(`${++nextId}`), // vid: hash(`${++nextId}`),
// mid: '', // mid: '',
// theme: { // theme: {
// color: 'pink', // color: 'pink',
// margin: 12, // margin: 12,
// flexible: true, // flexible: true,
// }, // },
// children: { // children: {
// type: 'image', // type: 'image',
// key: 'imageKey', // key: 'imageKey',
// link: 'linkKey', // link: 'linkKey',
// radius: 24, // radius: 24,
// }, // },
// }, { // }, {
// vid: hash(`${++nextId}`), // vid: hash(`${++nextId}`),
// mid: '', // mid: '',
// theme: { // theme: {
// color: '#fafbfc', // color: '#fafbfc',
// margin: 12, // margin: 12,
// padding: 24, // padding: 24,
// mainAlign: 'center', // mainAlign: 'center',
// crossAlign: 'center', // crossAlign: 'center',
// flexible: true, // flexible: true,
// }, // },
// children: { // children: {
// type: 'audio', // type: 'audio',
// key: 'audioKey', // key: 'audioKey',
// title: 'title', // title: 'title',
// }, // },
// }, { // }, {
// vid: hash(`${++nextId}`), // vid: hash(`${++nextId}`),
// mid: '', // mid: '',
// theme: { // theme: {
// radius: 24, // radius: 24,
// color: 'teal', // color: 'teal',
// }, // },
// children: [{ // children: [{
// theme: { // theme: {
// flexible: true, // flexible: true,
// gap: 12, // gap: 12,
// crossAlign: 'center', // crossAlign: 'center',
// }, // },
// children: [{ // children: [{
// theme: { // theme: {
// padding: 12, // padding: 12,
// margin: 12, // margin: 12,
// grow: 1, // grow: 1,
// }, // },
// children: { // children: {
// vid: hash(`${++nextId}`), // vid: hash(`${++nextId}`),
// theme: { // theme: {
// fontSize: 32, // fontSize: 32,
// }, // },
// children: 'left', // children: 'left',
// }, // },
// }, { // }, {
// vid: hash(`${++nextId}`), // vid: hash(`${++nextId}`),
// theme: { // theme: {
// width: 'auto', // width: 'auto',
// padding: { // padding: {
// vertical: 4, // vertical: 4,
// horizontal: 12, // horizontal: 12,
// }, // },
// color: 'pink', // color: 'pink',
// radius: 16, // radius: 16,
// }, // },
// children: 'right222', // children: 'right222',
// }, { // }, {
// theme: { // theme: {
// width: 48, // width: 48,
// }, // },
// } // }
// ], // ],
// } // }
// ], // ],
// }, ...Array.from(Array.from({length: 3})).map<Block>((_, i) => ({ // }, ...Array.from(Array.from({length: 3})).map<Block>((_, i) => ({
// vid: hash(`${++nextId}`), // vid: hash(`${++nextId}`),
// mid: '', // mid: '',
// children: [{ // children: [{
// vid: hash(`${++nextId}`), // vid: hash(`${++nextId}`),
// theme: { // theme: {
// fontSize: 22, // fontSize: 22,
// }, // },
// children: `${i}`, // children: `${i}`,
// }], // }],
// })) // }))
]) ])
const categories = ref([{icon: 'trash', text: '媒体'}, {icon: 'trash', text: '图表'}, {icon: 'trash', text: '商品'}, {icon: 'trash', text: '功能'}, {icon: 'trash', text: '素材'}].map(c => ({ const categories = ref([{ icon: 'trash', text: '媒体' }, { icon: 'trash', text: '图表' }, { icon: 'trash', text: '商品' }, { icon: 'trash', text: '功能' }, { icon: 'trash', text: '素材' }].map(c => ({
...c, ...c,
modules: Array.from(Array.from({length: 102})).map<Module>((_, i) => ({ modules: Array.from(Array.from({ length: 102 })).map<Module>((_, i) => ({
vid: hash(`${++nextId}`), vid: hash(`${++nextId}`),
mid: hash(`${++nextId}`), mid: hash(`${++nextId}`),
title: `${c.text}${i + 1}`, title: `${c.text}${i + 1}`,
@ -122,7 +122,7 @@ const categories = ref([{icon: 'trash', text: '媒体'}, {icon: 'trash', text: '
theme: { theme: {
height: 68, height: 68,
width: 375, width: 375,
border: {color: '#eee'}, border: { color: '#eee' },
}, },
children: [String(`${c.text}${i + 1}`)], children: [String(`${c.text}${i + 1}`)],
}], }],
@ -173,12 +173,20 @@ categories.value.unshift({
type: 'mark', type: 'mark',
field: 'style', field: 'style',
label: '指示器样式', label: '指示器样式',
values: [{label: '圆形', value: 'circle'}, {label: '直线', value: 'line'}, {label: '数字', value: 'number'}], values: [
}, { { label: '圆形', value: '#circle' },
{ label: '直线', value: '#line' },
{ label: '数字', value: '#number' },
],
}, {
type: 'mark', type: 'mark',
field: 'position', field: 'position',
label: '指示器位置', label: '指示器位置',
values: [{label: '居左', value: 'left'}, {label: '居中', value: 'center'}, {label: '居右', value: 'right'}], values: [
{ label: '居左', value: 'start' },
{ label: '居中', value: 'center' },
{ label: '居右', value: 'end' },
],
}, },
{ {
type: 'color', type: 'color',
@ -202,7 +210,8 @@ categories.value.unshift({
features: ['color', 'gradient'], features: ['color', 'gradient'],
label: '背景', label: '背景',
}], }],
}], }
],
init: { init: {
items: [ items: [
{ {
@ -212,19 +221,67 @@ categories.value.unshift({
} }
], ],
indicator: { indicator: {
style: 'circle', style: '#circle',
color: '#000000', color: 'green',
position: 'left', position: 'start',
}, },
background: { background: {
enabled: true, enabled: true,
value: "#ffffff", value: "#ffffff",
},
theme: {
color: 'white',
radius: 24
}
},
templates: {
'#circle': {
theme: {
gap: 20,
},
children: {
type: 'each',
key: 'items',
handle: {
theme: {
width: 20,
height: 20,
radius: 10,
color: '@@indicator.color',
},
}
}
},
'#line': {
theme: {
gap: 20,
},
children: {
type: 'each',
key: 'items',
handle: {
theme: {
width: 40,
height: 20,
color: '@@indicator.color',
},
}
}
},
"#number": {
theme: {
color: 'rgba(0,0,0,0.4)',
textColor: '#fff'
},
children: '1/len(items)'
} }
}, },
theme: { theme: {
position: 'relative',
height: 200, height: 200,
padding: { padding: {
horizontal: 12.0, horizontal: 12.0,
vertical: 12,
}, },
color: 'cyan', color: 'cyan',
}, },
@ -233,11 +290,38 @@ categories.value.unshift({
theme: { theme: {
width: '100%', width: '100%',
height: '100%', height: '100%',
color: 'white', color: '@@theme.color',
radius: 12.0, radius: "@@theme.radius",
textAlign: 'center', textAlign: 'center',
}, },
children: '设置轮播图', children: '设置轮播图',
}, {
theme: {
position: 'absolute',
flexible: true,
mainAlign: '@@indicator.position',
bottom: 20,
left: 20,
right: 20,
height: 20,
color: 'red',
},
// children: {
// type: 'each',
// key: 'indicator.position',
// handle: {
// theme: {
// width: 20,
// height: 20,
// radius: 10,
// color: 'black',
// }
// },
// }
children: {
type: 'template',
key: 'indicator.style'
}
}], }],
}, },
{ {
@ -254,8 +338,8 @@ categories.value.unshift({
label: '产品标题及说明', label: '产品标题及说明',
configs: [ configs: [
{ {
type:'text', type: 'text',
label:'产品标题', label: '产品标题',
field: 'mainTitle' field: 'mainTitle'
}, },
{ {
@ -273,33 +357,40 @@ categories.value.unshift({
label: '产品组列表', label: '产品组列表',
configs: [ configs: [
{ {
type:'image', type: 'image',
label:'产品图片', label: '产品图片',
field: 'productPhoto', field: 'productPhoto',
// //
inlines: [ { inlines: [{
type: 'text', type: 'text',
field: 'link', field: 'link',
label: '链接', label: '链接',
help: '请输入链接', // "${label}" help: '请输入链接', // "${label}"
}] },
}, {
{ type: 'text',
type:'text', label: '自定义标题',
label:'自定义标题', field: 'productTitle',
field: 'productTitle', },
}, {
{ type: 'text',
type:'text', label: '自定义说明',
label:'自定义说明', field: 'productDesc',
field: 'productDesc', },
]
}, },
] ]
}, },
], ],
init: { init: {
titleAndDesc: {
mainTitle: '标题',
content: '说明',
},
groups: [ groups: [
{ {
productTitle: '111', productTitle: '111',
} }
], ],
@ -313,15 +404,117 @@ categories.value.unshift({
color: 'pink', color: 'pink',
}, },
children: { children: {
vid: hash(`${++nextId}`),
theme: {
width: '100%',
height: '100%',
color: 'white',
radius: 12.0,
textAlign: 'center',
},
children: [
{
theme: {
flexible: true,
gap: 12,
crossAlign: 'center',
},
children: [
{
theme: {
padding: {
horizontal: 5
},
},
children: [
{
theme: {
flexible: true,
gap: 5,
crossAlign: 'center',
},
children: [
{
theme: {
fontSize: 16,
},
children: '左侧标题',
},
{
theme: {
fontSize: 16,
},
children: '左侧说明',
}
]
},
]
},
{
theme: {
grow: 1,
}
},
{
vid: hash(`${++nextId}`),
theme: {
width: 'auto',
margin: {
horizontal: 5,
},
fontSize: 12,
padding: {
vertical: 1,
horizontal: 5,
},
color: 'pink',
radius: 16,
},
children: '查看更多',
},
],
},
{
vid: hash(`${++nextId}`),
theme: {
width: "94%",
height: "70%",
color: 'pink',
radius: 12.0,
margin: {
top: '1%',
left: '3%',
right: '3%',
bottom: '2%',
},
padding: {
horizontal: 3,
}
},
children: [{
theme: {
width: '35%',
height: '100%',
mainAlign: "center",
color: '#fff',
crossAlign: "center"
},
children: '产品'
},
]
},
]
} }
} }
], ],
}) })
const data = ref<Record<string, Record<string, unknown>>>({ const data = ref<Record<string, Record<string, unknown>>>({
// [blocks.value[0].vid]: { // [blocks.value[0].vid]: {
// textName: ' 2222', // textName: ' 2222',
// textAlign: "left"
// }, // },
// [blocks.value[1].vid]: { // [blocks.value[1].vid]: {
// imageKey: 'https://www.w3schools.com/css/paris.jpg', // imageKey: 'https://www.w3schools.com/css/paris.jpg',
@ -335,14 +528,7 @@ const data = ref<Record<string, Record<string, unknown>>>({
</script> </script>
<template> <template>
<DddBuilder <DddBuilder :blocks="blocks" :categories="categories" :sequence="nextId" :sources="data" />
:blocks="blocks"
:categories="categories"
:sequence="nextId"
:sources="data"
/>
</template> </template>
<style scoped> <style lang="less" scoped></style>
</style>

@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, unref } from 'vue' import { reactive, ref, unref } from 'vue'
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
import type { Block, Module } from '../types' import type { Block, Module } from '../types'
import { useContext } from '../context' import { eachViewTree, useContext } from '../context'
import { hash } from '../utils' import { hash } from '../utils'
defineOptions({ defineOptions({
@ -28,12 +28,18 @@ function clone(v: any): any {
s[key] = clone(val) s[key] = clone(val)
} }
} }
return s const cloned = reactive(s)
eachViewTree(cloned, view => {
if (view.vid) {
ctx.views[view.vid] = view
}
})
return cloned
} }
function cloneModule(src: Module): Block { function cloneModule(src: Module): Block {
// eslint-disable-next-line unused-imports/no-unused-vars // eslint-disable-next-line unused-imports/no-unused-vars
const {maxReferenceCount, referenceCount, image, configs, ...attrs} = unref(src) const {maxReferenceCount, referenceCount, image, configs, init, ...attrs} = unref(src)
return clone(attrs) return clone(attrs)
} }
</script> </script>

@ -2,9 +2,17 @@
import type { UnwrapNestedRefs } from 'vue' import type { UnwrapNestedRefs } from 'vue'
import { provide, reactive, unref } from 'vue' import { provide, reactive, unref } from 'vue'
import type { Block, Category } from '../types' import type { Block, Category } from '../types'
import type { EngineContext, Exported, PageConfig } from '../context' import { EngineContextBase, Exported, PageConfig, provideTreeData } from '../context'
import { contextKey, parentViewIdKey, provideBlockId } from '../context' import {
contextKey,
eachViewTree,
parentViewIdKey,
provideBlockId,
currentViewDOMRectKey,
parentViewDOMRectKey
} from '../context'
import DddEngineer from './Engineer.vue' import DddEngineer from './Engineer.vue'
import { valueOf } from '..'
defineOptions({ defineOptions({
name: 'DddBuilder', name: 'DddBuilder',
@ -18,10 +26,10 @@ const props = defineProps<{
sources?: Record<string, Record<string, unknown>> sources?: Record<string, Record<string, unknown>>
}>() }>()
let ctx: UnwrapNestedRefs<EngineContext> let ctx: UnwrapNestedRefs<EngineContextBase>
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
ctx = reactive<EngineContext>({ ctx = reactive<EngineContextBase>({
target: 'routine', target: 'routine',
activeBlockId: undefined, activeBlockId: undefined,
activeViewId: undefined, activeViewId: undefined,
@ -59,6 +67,7 @@ ctx = reactive<EngineContext>({
}, },
categories: props.categories!, categories: props.categories!,
blocks: props.blocks, blocks: props.blocks,
views: Object.create(null),
nextId: props.sequence ?? Date.now(), nextId: props.sequence ?? Date.now(),
sources: props.sources ?? Object.create(null), sources: props.sources ?? Object.create(null),
export(): Exported { export(): Exported {
@ -66,10 +75,21 @@ ctx = reactive<EngineContext>({
const data = unref(ctx.sources) const data = unref(ctx.sources)
return {attrs, data} return {attrs, data}
}, },
value<T>(blockId: string, key: string): T | undefined {
return valueOf(ctx.sources[blockId], key.slice(2))
}
})
eachViewTree(ctx.blocks, view => {
if (view.vid) {
ctx.views[view.vid] = view
}
}) })
provideBlockId('') provideBlockId('')
provideTreeData(null)
provide(parentViewIdKey, '') provide(parentViewIdKey, '')
provide(contextKey, ctx) provide(contextKey, ctx)
provide(currentViewDOMRectKey, reactive({}))
provide(parentViewDOMRectKey, reactive({}))
</script> </script>
<template> <template>

@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue' import { computed, ref, watch } from 'vue'
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
import { useContext, getModule } from '../context' import { useContext, getModule, useCurrentViewDOMRect } from '../context'
import { clone } from '../utils' import { clone, unit } from '../utils'
import { RenderBlock } from '../render' import { RenderBlock } from '../render'
import { DddView } from '../views' import { DddView } from '../views'
@ -38,6 +38,19 @@ const measureLine = computed(() => {
return `${1 / ctx.canvas.scale}px` return `${1 / ctx.canvas.scale}px`
}) })
const canvasRef = ref<HTMLElement | null>(null)
const currentViewDOMRect = useCurrentViewDOMRect()
const currentViewStyle = computed(() => {
const canvasRect = canvasRef.value?.getBoundingClientRect()
const {left, top, width, height} = currentViewDOMRect
return {
left: left != null ? unit(left - (canvasRect?.left ?? 10000)) : '-1000px',
top: top != null ? unit(top - (canvasRect?.top ?? 10999)) : '-1000px',
width: unit(width),
height: unit(height),
}
})
// interface EmulatorChangedEvent { // interface EmulatorChangedEvent {
// added?: AddEventInfo // added?: AddEventInfo
// moved?: SwapEventInfo // moved?: SwapEventInfo
@ -75,6 +88,7 @@ function handleDragAdd(e: DraggableEvent): void {
ctx.hoverViewId = '#canvas' ctx.hoverViewId = '#canvas'
ctx.configurator = 'block' ctx.configurator = 'block'
ctx.sources[block.vid] = module?.init ? clone(module.init) : {} ctx.sources[block.vid] = module?.init ? clone(module.init) : {}
ctx.views[block.vid] = block
key.value++ key.value++
} }
@ -90,6 +104,7 @@ function handleCanvasClick(e: MouseEvent): void {
<template> <template>
<div <div
ref="canvasRef"
:class="{ :class="{
'is-focused': ctx.canvas.focused, 'is-focused': ctx.canvas.focused,
'is-active': ctx.activeViewId === '#canvas', 'is-active': ctx.activeViewId === '#canvas',
@ -177,6 +192,13 @@ function handleCanvasClick(e: MouseEvent): void {
</div> </div>
<div class="ddd-canvas-indicator" /> <div class="ddd-canvas-indicator" />
</template> </template>
<!-- 使用全局单例利用 absolute 固定位置 -->
<div
class="ddd-view-current-highlight"
:style="currentViewStyle"
/>
<!-- <div class="ddd-view-parent-highlight" /> -->
</div> </div>
</template> </template>
@ -335,4 +357,12 @@ function handleCanvasClick(e: MouseEvent): void {
height: v-bind(footerHeight); height: v-bind(footerHeight);
} }
} }
.ddd-view-current-highlight,
.ddd-view-parent-highlight {
position: absolute;
z-index: 1000;
pointer-events: none;
border: var(--canvas-measure-line) solid var(--canvas-active-highlight-color);
}
</style> </style>

@ -0,0 +1,33 @@
<script lang="ts" setup>
import type { View } from '../types';
import { computed } from 'vue';
import { useContext } from '../context';
import ColorConfig from '../configs/ColorConfig.vue';
defineOptions({
name: 'ConfigCurrentView',
})
const ctx = useContext()
const view = computed<View | undefined>(() => {
return ctx.activeViewId ? ctx.views[ctx.activeViewId] : undefined
})
const styles = computed(() => {
if (!view.value?.theme) {
return undefined
}
return Object.entries(view.value.theme).filter(([, value]) => {
return typeof value === 'string' && value.startsWith('@@theme.')
}) as Array<[string, string]>
})
</script>
<template>
{{ ctx.activeViewId }}
<hr/>
<template v-for="([key, value]) in styles" :key="key">
<ColorConfig v-if="key === 'color'" :field="value.slice(2)" label="背景颜色" />
<div v-else>{{ key }}:{{ value }}</div>
</template>
<pre><code>{{ view }}</code></pre>
</template>

@ -3,6 +3,7 @@ import { ref, watch } from 'vue'
import type { ConfiguratorType } from '../context' import type { ConfiguratorType } from '../context'
import { useContext, useModule } from '../context' import { useContext, useModule } from '../context'
import { RenderConfig } from '../configs/render' import { RenderConfig } from '../configs/render'
import ConfigCurrentView from './ConfigCurrentView.vue'
defineOptions({ defineOptions({
name: 'DddConfigurator', name: 'DddConfigurator',
@ -193,9 +194,7 @@ watch(() => ctx.configurator, (v) => {
</div> </div>
</div> </div>
<div v-show="currentTab === 'view'" class="ddd-configurator-content"> <div v-show="currentTab === 'view'" class="ddd-configurator-content">
<div> <ConfigCurrentView />
<pre><code>{{ ctx.blocks }}</code></pre>
</div>
</div> </div>
</div> </div>
</template> </template>

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { ComponentPublicInstance, ref } from 'vue'
import { useContext, useScale } from '../context' import { useContext, useParentViewDOMRect, useScale } from '../context'
import { DddView } from '../views' import { DddView } from '../views'
import DddCanvas from './Canvas.vue' import DddCanvas from './Canvas.vue'
@ -53,6 +53,13 @@ function onMousewheel(e: WheelEvent): void {
} }
} }
const canvasRef = ref<HTMLElement | null>(null)
const domRect = useParentViewDOMRect()
const setCanvasRef = (a: ComponentPublicInstance) => {
canvasRef.value = a?.$el ?? null
}
// TODO(hupeh): 使 Vue // TODO(hupeh): 使 Vue
function onMouseover(e: MouseEvent): void { function onMouseover(e: MouseEvent): void {
if (!ctx.draggingViewId) { if (!ctx.draggingViewId) {
@ -61,11 +68,13 @@ function onMouseover(e: MouseEvent): void {
const widgetId = (el as HTMLElement)?.dataset?.viewId const widgetId = (el as HTMLElement)?.dataset?.viewId
if (widgetId && widgetId !== ctx.activeViewId) { if (widgetId && widgetId !== ctx.activeViewId) {
ctx.hoverViewId = widgetId ctx.hoverViewId = widgetId
Object.assign(domRect, el.getBoundingClientRect().toJSON())
return return
} }
el = el?.parentNode as HTMLElement el = el?.parentNode as HTMLElement
} }
ctx.hoverViewId = '#canvas' ctx.hoverViewId = '#canvas'
Object.assign(domRect, canvasRef.value!.getBoundingClientRect())
} }
} }
</script> </script>
@ -82,6 +91,7 @@ function onMouseover(e: MouseEvent): void {
<DddView <DddView
:is="DddCanvas" :is="DddCanvas"
:style="`transform: translate(${x}px,${y}px) scale(${ctx.canvas.scale})`" :style="`transform: translate(${x}px,${y}px) scale(${ctx.canvas.scale})`"
:ref="setCanvasRef"
vid="#canvas" vid="#canvas"
@mouseover="onMouseover" @mouseover="onMouseover"
@mousedown.stop @mousedown.stop

@ -4,7 +4,8 @@ import { RenderConfig } from './render';
defineOptions({ defineOptions({
name: 'DddListConfig' name: 'DddListConfig',
inheritAttrs: false,
}) })
const props = defineProps<{ const props = defineProps<{

@ -3,7 +3,8 @@ import { ModuleConfig } from "..";
import { RenderConfig } from './render'; import { RenderConfig } from './render';
defineOptions({ defineOptions({
name: "DddObjectConfig" name: "DddObjectConfig",
inheritAttrs: false,
}) })
defineProps<{ defineProps<{

@ -1,7 +1,7 @@
import type { WritableComputedRef, InjectionKey, UnwrapNestedRefs } from 'vue' import type { WritableComputedRef, InjectionKey, UnwrapNestedRefs } from 'vue'
import { computed, inject, isReactive, isRef, provide } from 'vue' import { computed, inject, provide, unref } from 'vue'
import type { Block, Category, Module, Page } from './types' import type { Block, Category, Module, Page, View, ViewChildren } from './types'
import { setValue, valueOf } from './utils' import { isRefer, setValue, valueOf } from './utils'
/** 画布配置 */ /** 画布配置 */
export interface CanvasConfig { export interface CanvasConfig {
@ -24,7 +24,7 @@ export interface Exported {
data: Record<string, Record<string, unknown>> data: Record<string, Record<string, unknown>>
} }
export interface EngineContext { export interface EngineContextBase {
/** 目标 */ /** 目标 */
target?: 'app' | 'routine' target?: 'app' | 'routine'
/** 当前选中的模板 */ /** 当前选中的模板 */
@ -45,26 +45,35 @@ export interface EngineContext {
categories: Category[] categories: Category[]
/** 布局视图列表 */ /** 布局视图列表 */
blocks: Block[] blocks: Block[]
/** 主题列表 */
views: Record<string, View>
/** 内部唯一标识 */ /** 内部唯一标识 */
nextId: number nextId: number
/** 数据源 */ /** 数据源 */
sources: Record<string, Record<string, unknown>> sources: Record<string, Record<string, unknown>>
/** 到处数据 */ /** 到处数据 */
export: () => Exported export: () => Exported
/** 获取动态数据 */
value: <T>(blockId: string, key: string) => T | undefined
} }
export const contextKey: InjectionKey<UnwrapNestedRefs<EngineContext>> = Symbol.for('ddd:engine') export type EngineContext = UnwrapNestedRefs<EngineContextBase>
export const parentViewIdKey: InjectionKey<string> = Symbol.for('ddd:view:parent:id')
const blockIdKey: InjectionKey<string> = Symbol.for('ddd:block:id')
export function useContext(): UnwrapNestedRefs<EngineContext> { export const contextKey: InjectionKey<EngineContext> = Symbol.for('ddd:engine')
const config = inject(contextKey)
if (config == null) { const must = <T>(v: T | undefined, error: string): T => {
throw new Error('no config found') if (v == null) {
throw new Error(error)
} }
return config return v
}
export function useContext(): EngineContext {
return must(inject(contextKey), 'no context found')
} }
export const parentViewIdKey: InjectionKey<string> = Symbol.for('ddd:view:parent:id')
export function useParentViewId(): string | undefined { export function useParentViewId(): string | undefined {
return inject(parentViewIdKey) return inject(parentViewIdKey)
} }
@ -75,15 +84,28 @@ export function provideParentViewId(id: string | undefined): void {
} }
} }
export function useBlockId(): string | undefined { const blockIdKey: InjectionKey<string> = Symbol.for('ddd:block:id')
return inject(blockIdKey)
export function useBlockId(): string {
return must(inject(blockIdKey), "no block found")
} }
export function provideBlockId(id: string): void { export function provideBlockId(id: string): void {
provide(blockIdKey, id) provide(blockIdKey, id)
} }
export function getModule(ctx: UnwrapNestedRefs<EngineContext>, mid: string): Module | undefined { export const currentViewDOMRectKey: InjectionKey<UnwrapNestedRefs<Partial<DOMRect>>> = Symbol.for('ddd:view:current:domrect')
export const parentViewDOMRectKey: InjectionKey<UnwrapNestedRefs<Partial<DOMRect>>> = Symbol.for('ddd:view:parent:domrect')
export function useCurrentViewDOMRect(): UnwrapNestedRefs<Partial<DOMRect>> {
return must(inject(currentViewDOMRectKey), 'no DOMReact found')
}
export function useParentViewDOMRect(): UnwrapNestedRefs<Partial<DOMRect>> {
return must(inject(parentViewDOMRectKey), 'no DOMReact found')
}
export function getModule(ctx: UnwrapNestedRefs<EngineContextBase>, mid: string): Module | undefined {
for (const category of ctx.categories) { for (const category of ctx.categories) {
for (const module of category.modules) { for (const module of category.modules) {
if (module.mid === mid) { if (module.mid === mid) {
@ -136,25 +158,54 @@ export function useScale() {
} }
} }
export interface TreeData {
key: string | number
value: any
}
const treeDataKey: InjectionKey<UnwrapNestedRefs<TreeData>|WritableComputedRef<TreeData> | null> = Symbol.for('ddd:view:tree:data')
export function provideTreeData(data: UnwrapNestedRefs<TreeData> | WritableComputedRef<TreeData> | null) {
provide(treeDataKey, data)
}
export function useSource<T = unknown>(source: string | undefined, fallback?: T): WritableComputedRef<T | undefined> { export function useSource<T = unknown>(source: string | undefined, fallback?: T): WritableComputedRef<T | undefined> {
const ctx = useContext() const ctx = useContext()
const treeData = inject(treeDataKey)
const blockId = useBlockId() const blockId = useBlockId()
return computed<T | undefined>({ return computed<T | undefined>({
get(): T | undefined { get(): T | undefined {
// if (!blockId) { if (!source) {
// throw new Error('without block') return fallback
// } }
if (source === '$index' || source === '$key') {
console.log(unref(treeData))
return unref(treeData)?.key as T
}
if (source.startsWith('.')) {
return valueOf(unref(treeData)?.value, source.slice(1)) ?? fallback
}
const id = blockId || ctx.activeBlockId const id = blockId || ctx.activeBlockId
if (!source || !id) { if (!id) {
return fallback return fallback
} }
return valueOf(ctx.sources[id], source) ?? fallback return valueOf(ctx.sources[id], source) ?? fallback
}, },
set(value: T | undefined) { set(value: T | undefined) {
if (!source || source.startsWith("$")) {
return
}
if (source.startsWith('.')) {
// if (treeData != null) {
// setValue(unref(treeData)?.value, source.slice(1), value)
// }
console.warn("不可以给 TreeData 赋值")
return
}
const id = blockId || ctx.activeBlockId const id = blockId || ctx.activeBlockId
// console.log({id, source, value}) // console.log({id, source, value})
if (source && id) { if (id) {
if (!ctx.sources[id]) { if (!ctx.sources[id]) {
ctx.sources[id] = {} ctx.sources[id] = {}
} }
@ -163,3 +214,19 @@ export function useSource<T = unknown>(source: string | undefined, fallback?: T)
} }
}) })
} }
export function eachViewTree(view: ViewChildren | undefined, fn: (view: View) => void) {
if (view == null || typeof view === 'string' || typeof view === 'number') {
return
}
if (isRefer(view)) {
// TODO ...
return
}
if (Array.isArray(view)) {
view.map(v => eachViewTree(v, fn))
return
}
fn(view)
eachViewTree(view.children, fn)
}

@ -0,0 +1,28 @@
const handlers: Record<string, Listener[]> = Object.create(null)
interface Listener {
fn: (...args: any[]) => void,
once: boolean
}
export function on(
type: string,
fn: (...args: any[]) => void,
once: boolean = false,
): void {
const listeners = handlers[type] ?? []
listeners.push({fn, once})
handlers[type] = listeners
}
export function emit(type: string, ...args: any[]): void {
const listeners = handlers[type]
if (!listeners?.length) return
for (let i = listeners.length - 1; i >= 0; i--) {
const ln = listeners[i]
if (ln.once) {
handlers[type].splice(i, 1)
}
ln.fn(...args)
}
}

@ -1,6 +1,11 @@
import { registerView } from './render'
import DddBuilder from './components/Builder.vue' import DddBuilder from './components/Builder.vue'
import DddTextView from './views/TextView.vue'
import DddImageView from './views/ImageView.vue'
import DddAudioView from './views/AudioView.vue'
import DddEachView from './views/EachView.vue'
import RenderTemplate from './views/RenderTemplate.vue'
export type { WidgetRenderFunction } from './render'
export type { export type {
CanvasConfig, CanvasConfig,
PageConfig, PageConfig,
@ -9,7 +14,7 @@ export type {
Exported, Exported,
} from './context' } from './context'
export { registerWidget, render } from './render' export { render } from './render'
export { export {
useContext, useContext,
useBlockId, useBlockId,
@ -21,3 +26,12 @@ export { hash, valueOf } from './utils'
export * from './types' export * from './types'
export default DddBuilder export default DddBuilder
registerView({
text: DddTextView,
image: DddImageView,
audio: DddAudioView,
video: DddAudioView,
each: DddEachView,
template: RenderTemplate
})

@ -1,56 +1,61 @@
import type { Component, CSSProperties, Prop, VNodeChild, } from 'vue' import type { Component, CSSProperties, Prop, VNodeChild } from 'vue'
import type { Axis, Block, DataRefer, DynamicTheme, DynamicValue, ToStatic, ViewChildren } from './types'
import type { EngineContext } from './context'
import { defineComponent, h } from 'vue' import { defineComponent, h } from 'vue'
import type { Axis, Block, DataRefer, Theme, View, ViewChildren, Widget, } from './types' import { provideBlockId, useBlockId, useContext } from './context'
import { provideBlockId } from './context' import {
import { align, background, bordering, clip, gap, insets, radii, shadowing, size, unit, } from './utils' align,
background,
bordering,
clip,
dynamic,
gap,
insets,
isRefer,
radii,
shadowing,
size,
stack,
unit,
} from './utils'
import DddView from './views/View.vue' import DddView from './views/View.vue'
import DddTextView from './views/TextView.vue'
import DddImageView from './views/ImageView.vue'
import DddAudioView from './views/AudioView.vue'
/**
*
*/
export type WidgetRenderFunction = (widget: Widget, view: View, axis?: Axis) => VNodeChild
/**
*
*/
const widgets: Record<string, WidgetRenderFunction> = {}
const views: Record<string, Component> = {
text: DddTextView,
image: DddImageView,
audio: DddAudioView,
video: DddAudioView,
}
export function registerWidget(some: Record<string, WidgetRenderFunction>): void { // /**
for (const [key, func] of Object.entries(some)) { // * 相关框架组件渲染函数
widgets[key] = func // */
} // export type WidgetRenderFunction = (widget: Widget, view: View, axis?: Axis) => VNodeChild
}
function renderWidget(widget: Widget, view: View, axis?: Axis): VNodeChild { // /**
const func = widgets[widget.name] // * 注册的外部组件渲染函数
if (typeof func !== 'function') { // */
throw new TypeError(`unknown widget: ${widget.name}`) // const widgets: Record<string, WidgetRenderFunction> = {}
}
return func(widget, view, axis)
}
const isWidget = (s: any): s is Widget => s != null && 'name' in s const views: Record<string, Component> = {}
function isRefer(s: any): s is DataRefer { export function registerView(v: Record<string, Component>): void {
return s != null for (const [key, comp] of Object.entries(v)) {
&& 'type' in s views[key] = comp
&& 'key' in s }
&& typeof s.type === 'string'
&& typeof s.key === 'string'
} }
function renderRefer(refer: DataRefer, axis?: Axis): VNodeChild { // export function registerWidget(some: Record<string, WidgetRenderFunction>): void {
const {key, type, ...attrs} = refer // for (const [key, func] of Object.entries(some)) {
// widgets[key] = func
// }
// }
//
// function renderWidget(widget: Widget, view: View, axis?: Axis): VNodeChild {
// const func = widgets[widget.name]
// if (typeof func !== 'function') {
// throw new TypeError(`unknown widget: ${widget.name}`)
// }
// return func(widget, view, axis)
// }
//
// const isWidget = (s: any): s is Widget => s != null && 'name' in s
export function renderRefer(refer: DataRefer, axis?: Axis): VNodeChild {
const { key, type, ...attrs } = refer
const component = views[type] const component = views[type]
if (component) { if (component) {
return h(component, { return h(component, {
@ -67,15 +72,20 @@ function renderRefer(refer: DataRefer, axis?: Axis): VNodeChild {
}, 'unimplemented') }, 'unimplemented')
} }
export function isFlexible(theme: Theme) { export function isFlexible(theme: DynamicTheme, value: ToStatic) {
return theme.axis != null return value(theme.axis) != null
|| theme.mainAlign != null || value(theme.mainAlign) != null
|| theme.crossAlign != null || value(theme.crossAlign) != null
|| theme.wrap != null || value(theme.wrap) != null
|| theme.gap != null || value(theme.gap) != null
} }
export function render(view?: ViewChildren, axis?: Axis): VNodeChild { export function render(
ctx: EngineContext,
blockId: string,
view?: ViewChildren,
axis?: Axis,
): VNodeChild {
if (view == null) { if (view == null) {
return null return null
} }
@ -83,55 +93,78 @@ export function render(view?: ViewChildren, axis?: Axis): VNodeChild {
return view return view
} }
if (Array.isArray(view)) { if (Array.isArray(view)) {
return view.map(v => render(v, axis)) return view.map(child => render(ctx, blockId, child, axis))
} }
if (isRefer(view)) { if (isRefer(view)) {
return renderRefer(view) return renderRefer(view)
} }
if (isWidget(view.theme)) { // if (isWidget(view.theme)) {
return renderWidget(view.theme, view, axis) // return renderWidget(view.theme, view, axis)
} // }
const theme = view.theme const theme = view.theme
const value = <T>(v: DynamicValue<T>): T => dynamic<T>(ctx, blockId, v)
const css: CSSProperties = { const css: CSSProperties = {
// boxed & spatial // boxed & spatial
...insets('padding', theme?.padding), ...insets('padding', value(theme?.padding)),
...insets('margin', theme?.margin), ...insets('margin', value(theme?.margin)),
...size(axis, theme), ...size(axis, theme, value), // todo
// decoration // decoration
...background(theme?.color, theme?.image, theme?.gradient), ...background(value(theme?.color), value(theme?.image), value(theme?.gradient)),
...bordering(theme?.border), ...bordering(value(theme?.border)),
...radii(theme?.radius), ...radii(value(theme?.radius)),
...shadowing(theme?.shadow), ...shadowing(value(theme?.shadow)),
// textual // textual
color: theme?.textColor, color: value(theme?.textColor),
fontSize: unit(theme?.fontSize), fontSize: unit(value(theme?.fontSize)),
textAlign: theme?.textAlign, textAlign: value(theme?.textAlign),
lineHeight: theme?.lineHeight, lineHeight: value(theme?.lineHeight),
// clip // clip
...clip(theme?.clip), ...clip(value(theme?.clip)),
// stack
...stack(theme, value),
} }
// note: 在前面初始化 css 时使用了参数 axis, // note: 在前面初始化 css 时使用了参数 axis,
// 所以在这里复用它用于子视图构建 // 所以在这里复用它用于子视图构建
axis = theme?.axis axis = value(theme?.axis)
if (theme?.flexible || (theme && theme.flexible == null && isFlexible(theme))) { const flexible = value(theme?.flexible)
if (flexible || (theme && flexible == null && isFlexible(theme, value))) {
Object.assign(css, { Object.assign(css, {
display: 'flex', display: 'flex',
flexDirection: axis === 'y' ? 'column' : undefined, flexDirection: axis === 'y' ? 'column' : undefined,
justifyContent: align(theme?.mainAlign), justifyContent: align(value(theme?.mainAlign)),
alignItems: align(theme?.crossAlign), alignItems: align(value(theme?.crossAlign)),
flexWrap: theme?.wrap ? 'wrap' : undefined, flexWrap: value(theme?.wrap) ? 'wrap' : undefined,
...gap(theme?.gap), ...gap(value(theme?.gap)),
}) })
} }
return h(DddView, { return h(DddView, {
vid: view.vid, vid: view.vid,
class: 'ddd-view', class: 'ddd-view',
style: css, style: css,
}, () => render(view.children, axis)) }, () => render(ctx, blockId, view.children, axis))
} }
export const RenderView = defineComponent({
name: 'RenderView',
props: {
view: {
// type: [Object | Array],
required: true,
} as Prop<ViewChildren>,
},
setup(props) {
const ctx = useContext()
const blockId = useBlockId()
return () => render(ctx, blockId, props.view)
}
})
export const RenderBlock = defineComponent({ export const RenderBlock = defineComponent({
name: 'RenderWidget', name: 'RenderBlock',
props: { props: {
block: { block: {
@ -141,12 +174,16 @@ export const RenderBlock = defineComponent({
}, },
setup(props) { setup(props) {
// const ctx = useContext()
provideBlockId(props.block!.vid) provideBlockId(props.block!.vid)
return () => { return () => {
if (props.block == null) { if (props.block == null) {
return null return null
} }
return render(props.block) // return render(ctx, props.block.vid, props.block)
return h(RenderView, {
view: props.block,
})
} }
}, },
}) })

@ -1,5 +1,3 @@
import type { Slots } from 'vue'
/** 百分比 */ /** 百分比 */
export type Percentage = `${number}%` export type Percentage = `${number}%`
@ -50,9 +48,9 @@ export interface Gradient {
/** 边距,用于内外边距 */ /** 边距,用于内外边距 */
export type EdgeInsets = export type EdgeInsets =
| Direction<Unit> // 四边 | Direction<Unit | 'auto'> // 四边
| Symmetric<Unit> // 对称 | Symmetric<Unit | 'auto'> // 对称
| Unit // 原语 | Unit | 'auto' // 原语
/** 边框样式 */ /** 边框样式 */
export type BorderStyle = export type BorderStyle =
@ -144,6 +142,10 @@ export type TextAlign =
*/ */
export type Axis = 'x' | 'y' export type Axis = 'x' | 'y'
export type Attach<Data,Append> = {
[p in keyof Data]: Data[p] | Append
}
/** /**
* *
* *
@ -238,19 +240,31 @@ export interface Boxed {
height?: number | Percentage | 'auto' height?: number | Percentage | 'auto'
} }
/** export type ViewPosition =
* | "absolute"
* | "fixed"
* 使 | "relative"
*/ | "static"
export interface Widget { | "sticky"
/** 组件名称 */
name: string export interface Stacked extends Direction<Unit> {
/** 组件属性 */ position?: ViewPosition
props?: Record<string, unknown> zIndex?: number
/** 相关插槽,不包括默认插槽 */ }
slots?: Omit<Slots, 'default'>
} // /**
// * 相关框架组件
// *
// * 建议尽量使用业务组件。
// */
// export interface Widget {
// /** 组件名称 */
// name: string
// /** 组件属性 */
// props?: Record<string, unknown>
// /** 相关插槽,不包括默认插槽 */
// slots?: Omit<Slots, 'default'>
// }
/** /**
* *
@ -267,7 +281,7 @@ export type ClipBehavior =
* *
* *
*/ */
export interface Theme extends Textual, Flexible, Boxed, Decoration, Spatial, Flexible { export interface Theme extends Textual, Flexible, Boxed, Decoration, Spatial, Flexible, Stacked {
/** /**
* *
* *
@ -282,6 +296,12 @@ export interface Theme extends Textual, Flexible, Boxed, Decoration, Spatial, Fl
clip?: ClipBehavior clip?: ClipBehavior
} }
export type DynamicTheme = Attach<Theme, `@@${string}`>
export type DynamicValue<T> = T | `@@${string}`
export type ToStatic = <T>(v: DynamicValue<T>) => T
/** 文本配置引用 */ /** 文本配置引用 */
export interface TextRefer { export interface TextRefer {
/** 标识为文本专用类型 */ /** 标识为文本专用类型 */
@ -340,12 +360,27 @@ export interface AudioRefer {
// todo 其它配置实现,比如自动播放、控件配置等 // todo 其它配置实现,比如自动播放、控件配置等
} }
export interface EachRefer {
type: 'each'
/** 循环数据地址键 */
key: string
handle: ViewChildren
}
export interface TemplateRefer {
type: 'template'
/** 模板数据地址键 */
key: string
}
/** 目前支持的 4 中配置引用 */ /** 目前支持的 4 中配置引用 */
export type DataRefer = export type DataRefer =
| TextRefer | TextRefer
| ImageRefer | ImageRefer
| VideoRefer | VideoRefer
| AudioRefer | AudioRefer
| EachRefer
| TemplateRefer
export type ViewChild = export type ViewChild =
| string | string
@ -368,7 +403,8 @@ export interface View {
* *
* *
*/ */
theme?: Theme | Widget // theme?: Theme | Widget
theme?: DynamicTheme
/** /**
* *
*/ */
@ -399,6 +435,8 @@ export interface Module extends Block {
configs: ModuleConfig[] configs: ModuleConfig[]
/** 初始化数据 */ /** 初始化数据 */
init?: Record<string, any> init?: Record<string, any>
/** 关联模板 */
templates?: Record<string, ViewChildren>
} }
export type ModuleConfig = export type ModuleConfig =

@ -9,6 +9,9 @@ export function align(s: MainAlign | CrossAlign | undefined): string | undefined
case 'around': case 'around':
return `space-${s}` return `space-${s}`
default: default:
if (s == null) {
return undefined
}
return s return s
} }
} }

@ -11,12 +11,14 @@ function image(img: string | undefined, g: Gradient | undefined): string | undef
if (g == null) { if (g == null) {
return undefined return undefined
} }
const prefix = g.repeatable ? 'repeating-' : '' if (typeof g === 'object') {
if (g.stops?.length === g.colors.length) { const prefix = g.repeatable ? 'repeating-' : ''
const colors = g.colors.map((c, i) => `${c} ${g.stops![i]}`).join(', ') if (g.stops?.length === g.colors.length) {
return `${prefix}linear-gradient(${g?.angle ?? '0deg'}, ${colors})` const colors = g.colors.map((c, i) => `${c} ${g.stops![i]}`).join(', ')
return `${prefix}linear-gradient(${g?.angle ?? '0deg'}, ${colors})`
}
return `${prefix}linear-gradient(${g?.angle ?? '0deg'}, ${g.colors.join(', ')})`
} }
return `${prefix}linear-gradient(${g?.angle ?? '0deg'}, ${g.colors.join(', ')})`
} }
export function background(color: string | undefined, img: string | undefined, g: Gradient | undefined): CSSProperties { export function background(color: string | undefined, img: string | undefined, g: Gradient | undefined): CSSProperties {

@ -1,7 +1,7 @@
import type { CSSProperties } from 'vue' import type { CSSProperties } from 'vue'
import type { ClipBehavior } from '../types' import type { ClipBehavior,DynamicValue } from '../types'
export function clip(clip: ClipBehavior | undefined): CSSProperties | undefined { export function clip(clip: DynamicValue<ClipBehavior> | undefined): CSSProperties | undefined {
switch (clip) { switch (clip) {
case 'autoX': case 'autoX':
return { return {

@ -0,0 +1,9 @@
import type { DynamicValue } from "../types"
import type { EngineContext } from "../context"
export function dynamic<T>(ctx: EngineContext, blockId: string, value: DynamicValue<T>): T {
if (typeof value === 'string' && value.startsWith('@@')) {
return ctx.value(blockId, value) as T
}
return value as T
}

@ -11,4 +11,6 @@ export * from './object'
export * from './radii' export * from './radii'
export * from './shadow' export * from './shadow'
export * from './size' export * from './size'
export * from './stack'
export * from './unit' export * from './unit'
export {dynamic} from "./dynamic.ts";

@ -1,9 +1,9 @@
import type { CSSProperties } from 'vue' import type { CSSProperties } from 'vue'
import type { EdgeInsets } from '../types' import type { DynamicValue, EdgeInsets } from '../types'
import { isSymmetric } from './is' import { isSymmetric } from './is'
import { unit } from './unit' import { unit } from './unit'
export function insets(name: string, i: EdgeInsets | undefined): CSSProperties | undefined { export function insets(name: string, i: DynamicValue<EdgeInsets> | undefined ): CSSProperties | undefined {
if (i == null) { if (i == null) {
return undefined return undefined
} }
@ -11,11 +11,20 @@ export function insets(name: string, i: EdgeInsets | undefined): CSSProperties |
return {[name]: `${i}px`} return {[name]: `${i}px`}
} }
if (typeof i === 'string') { if (typeof i === 'string') {
if(i.split('@@')[1]){
if(Number.isInteger(i.split('@@')[1])){
return {[name]: `${i.split('@@')[1]}px`}
}
return {[name]: i.split('@@')[1]}
}
return {[name]: i} return {[name]: i}
} }
if (isSymmetric(i)) { if (isSymmetric(i)) {
return { return {
[name]: [unit(i.vertical), unit(i.horizontal)].join(' '), [`${name}Inline`]: i.horizontal != null ? unit(i.horizontal) : undefined,
[`${name}Block`]: i.vertical != null ? unit(i.vertical) : undefined,
} }
} }
return { return {

@ -1,4 +1,4 @@
import type { BorderSide, Corners, Symmetric } from '../types' import type { BorderSide, Corners, DataRefer, Symmetric } from '../types'
export function isSymmetric(a: any): a is Symmetric<any> { export function isSymmetric(a: any): a is Symmetric<any> {
return a != null return a != null
@ -23,3 +23,11 @@ export function isCorners(a: any): a is Corners<any> {
|| 'bottomLeft' in a || 'bottomLeft' in a
|| 'bottomRight' in a) || 'bottomRight' in a)
} }
export function isRefer(s: any): s is DataRefer {
return s != null
&& 'type' in s
&& 'key' in s
&& typeof s.type === 'string'
&& typeof s.key === 'string'
}

@ -25,6 +25,6 @@ export function radii(radius: Radius | undefined): CSSProperties | undefined {
} }
} }
return { return {
borderRadius: stringify(radius), borderRadius: stringify(radius as RadiusPrimitive),
} }
} }

@ -1,18 +1,19 @@
import type { CSSProperties } from 'vue' import type { CSSProperties } from 'vue'
import type { Axis, Theme } from '../types' import type { Axis, DynamicTheme, ToStatic } from '../types'
import { unit } from './unit' import { unit } from './unit'
export function size( export function size(
axis: Axis | undefined, axis: Axis | undefined,
theme: Theme | undefined, theme: DynamicTheme | undefined,
value: ToStatic,
): CSSProperties { ): CSSProperties {
const width = unit(theme?.width) const width = unit(value(theme?.width))
const height = unit(theme?.height) const height = unit(value(theme?.height))
return { return {
width, width,
height, height,
flexBasis: theme?.basis ?? (axis === 'x' ? width : axis === 'y' ? height : undefined), flexBasis: value(theme?.basis) ?? (axis === 'x' ? width : axis === 'y' ? height : undefined),
flexGrow: theme?.grow != null ? `${theme.grow}` : undefined, flexGrow: value(theme?.grow)?.toString(),
flexShrink: theme?.shrink != null ? `${theme.shrink}` : undefined, flexShrink: value(theme?.shrink)?.toString(),
} }
} }

@ -0,0 +1,20 @@
import type { CSSProperties } from "vue";
import type { DynamicTheme, ToStatic } from "../types";
import { unit } from ".";
export function stack(
theme: DynamicTheme | undefined,
value: ToStatic,
): CSSProperties | undefined {
if (theme == null) {
return undefined
}
return {
position: value(theme.position),
zIndex: value(theme.zIndex),
top: unit(value(theme.top)),
bottom: unit(value(theme.bottom)),
left: unit(value(theme.left)),
right: unit(value(theme.right)),
}
}

@ -3,6 +3,13 @@ export function unit(n: number | undefined | string) {
return undefined return undefined
} }
if (typeof n !== 'number') { if (typeof n !== 'number') {
if(n.split('@@')[1]){
if(Number.isInteger(n.split('@@')[1])){
return `${n.split('@@')[1]}px`
}
return n.split('@@')[1]
}
return n return n
} }
return `${n}px` return `${n}px`

@ -3,6 +3,7 @@ import { useSource } from '../context'
defineOptions({ defineOptions({
name: 'DddAudioView', name: 'DddAudioView',
inheritAttrs: false,
}) })
const props = defineProps<{ const props = defineProps<{

@ -0,0 +1,48 @@
<script lang="ts" setup>
import type { ViewChildren } from '../types'
import { provideTreeData, useSource } from '../context'
import { RenderView } from '../render'
import { defineComponent, h } from 'vue'
defineOptions({
name: 'DddEachView',
inheritAttrs: false,
})
const props = defineProps<{
source: string
handle: ViewChildren
}>()
const src = useSource<string>(props.source)
const RenderItem = defineComponent({
props: {
index: {
type: [String, Number],
required: true
},
value: {
required: true
},
view: {
required: true
}
},
setup(props) {
provideTreeData({
key: props.index,
value: props.value,
})
return () => h(RenderView, {
view: props.view,
})
}
})
</script>
<template>
<template v-for="(val, key) in src" :key="key">
<RenderItem :index="key" :value="val" :view="handle" />
</template>
</template>

@ -5,6 +5,7 @@ import { radii } from '../utils'
defineOptions({ defineOptions({
name: 'DddImageView', name: 'DddImageView',
inheritAttrs: false,
}) })
const props = defineProps<{ const props = defineProps<{

@ -0,0 +1,22 @@
<script setup lang="ts">
import { useModule, useSource } from '../context'
import { computed } from 'vue'
import { RenderView } from '../render'
const props = defineProps<{
source: string
}>()
const mod = useModule()
const name = useSource(props.source)
const view = computed(() => {
const ts = mod.value?.templates
const key = name.value
return ts && key ? ts[key] : undefined
})
</script>
<template>
<RenderView :view="view"/>
</template>

@ -3,6 +3,7 @@ import { useSource } from '../context'
defineOptions({ defineOptions({
name: 'DddTextView', name: 'DddTextView',
inheritAttrs: false,
}) })
const props = defineProps<{ const props = defineProps<{

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Component } from 'vue' import type { Component } from 'vue'
import { provideParentViewId, useBlockId, useContext, useParentViewId, } from '../context' import { provideParentViewId, useBlockId, useContext, useCurrentViewDOMRect, useParentViewId } from '../context'
defineOptions({ defineOptions({
name: 'DddView', name: 'DddView',
@ -14,6 +14,7 @@ const props = defineProps<{
const ctx = useContext() const ctx = useContext()
const parentId = useParentViewId() const parentId = useParentViewId()
const blockId = useBlockId() const blockId = useBlockId()
const domRect = useCurrentViewDOMRect()
function handleMousedown(e: Event, id: string | undefined): void { function handleMousedown(e: Event, id: string | undefined): void {
ctx.activeBlockId = blockId ctx.activeBlockId = blockId
@ -26,6 +27,9 @@ function handleMousedown(e: Event, id: string | undefined): void {
if (id) { if (id) {
e.stopPropagation() e.stopPropagation()
ctx!.activeViewId = id ctx!.activeViewId = id
Object.assign(domRect, (e.target as HTMLElement).getBoundingClientRect().toJSON())
if (blockId !== id) { if (blockId !== id) {
switch (id) { switch (id) {
case '#canvas': case '#canvas':
@ -64,8 +68,6 @@ provideParentViewId(props.vid)
@mousedown.left="handleMousedown($event, vid)" @mousedown.left="handleMousedown($event, vid)"
> >
<slot /> <slot />
<!-- 使用全局单例利用 absolute 固定位置 -->
<div class="ddd-view-highlight" />
</component> </component>
</template> </template>
@ -74,40 +76,8 @@ provideParentViewId(props.vid)
z-index: 1; z-index: 1;
position: relative; position: relative;
> .ddd-view-highlight {
z-index: 10;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
content: "";
border: var(--canvas-measure-line) solid transparent;
pointer-events: none;
}
&.is-hover {
z-index: 3;
> .ddd-view-highlight {
border-color: var(--canvas-hover-highlight-color);
}
}
&.is-active {
z-index: 2;
> .ddd-view-highlight {
border-color: var(--canvas-active-highlight-color);
}
}
&.is-ghost { &.is-ghost {
opacity: 0.3; opacity: 0.3;
> .ddd-view-highlight {
border-style: dashed;
}
} }
} }
</style> </style>

Loading…
Cancel
Save