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

main
一杯沧海 12 months ago
parent 48a4b5b452
commit a635eead2a
  1. 222
      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. 189
      src/engineer/render.ts
  14. 76
      src/engineer/types.ts
  15. 3
      src/engineer/utils/align.ts
  16. 2
      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

@ -173,12 +173,20 @@ categories.value.unshift({
type: 'mark',
field: 'style',
label: '指示器样式',
values: [{label: '圆形', value: 'circle'}, {label: '直线', value: 'line'}, {label: '数字', value: 'number'}],
values: [
{ label: '圆形', value: '#circle' },
{ label: '直线', value: '#line' },
{ label: '数字', value: '#number' },
],
}, {
type: 'mark',
field: 'position',
label: '指示器位置',
values: [{label: '居左', value: 'left'}, {label: '居中', value: 'center'}, {label: '居右', value: 'right'}],
values: [
{ label: '居左', value: 'start' },
{ label: '居中', value: 'center' },
{ label: '居右', value: 'end' },
],
},
{
type: 'color',
@ -202,7 +210,8 @@ categories.value.unshift({
features: ['color', 'gradient'],
label: '背景',
}],
}],
}
],
init: {
items: [
{
@ -212,19 +221,67 @@ categories.value.unshift({
}
],
indicator: {
style: 'circle',
color: '#000000',
position: 'left',
style: '#circle',
color: 'green',
position: 'start',
},
background: {
enabled: true,
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: {
position: 'relative',
height: 200,
padding: {
horizontal: 12.0,
vertical: 12,
},
color: 'cyan',
},
@ -233,11 +290,38 @@ categories.value.unshift({
theme: {
width: '100%',
height: '100%',
color: 'white',
radius: 12.0,
color: '@@theme.color',
radius: "@@theme.radius",
textAlign: 'center',
},
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'
}
}],
},
{
@ -282,7 +366,6 @@ categories.value.unshift({
field: 'link',
label: '链接',
help: '请输入链接', // "${label}"
}]
},
{
type: 'text',
@ -296,10 +379,18 @@ categories.value.unshift({
},
]
},
]
},
],
init: {
titleAndDesc: {
mainTitle: '标题',
content: '说明',
},
groups: [
{
productTitle: '111',
}
],
@ -313,7 +404,108 @@ categories.value.unshift({
color: 'pink',
},
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: '产品'
},
]
},
]
}
}
],
@ -322,6 +514,7 @@ categories.value.unshift({
const data = ref<Record<string, Record<string, unknown>>>({
// [blocks.value[0].vid]: {
// textName: ' 2222',
// textAlign: "left"
// },
// [blocks.value[1].vid]: {
// imageKey: 'https://www.w3schools.com/css/paris.jpg',
@ -335,14 +528,7 @@ const data = ref<Record<string, Record<string, unknown>>>({
</script>
<template>
<DddBuilder
:blocks="blocks"
:categories="categories"
:sequence="nextId"
:sources="data"
/>
<DddBuilder :blocks="blocks" :categories="categories" :sequence="nextId" :sources="data" />
</template>
<style scoped>
</style>
<style lang="less" scoped></style>

@ -1,8 +1,8 @@
<script lang="ts" setup>
import { ref, unref } from 'vue'
import { reactive, ref, unref } from 'vue'
import Draggable from 'vuedraggable'
import type { Block, Module } from '../types'
import { useContext } from '../context'
import { eachViewTree, useContext } from '../context'
import { hash } from '../utils'
defineOptions({
@ -28,12 +28,18 @@ function clone(v: any): any {
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 {
// 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)
}
</script>

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

@ -1,8 +1,8 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { computed, ref, watch } from 'vue'
import Draggable from 'vuedraggable'
import { useContext, getModule } from '../context'
import { clone } from '../utils'
import { useContext, getModule, useCurrentViewDOMRect } from '../context'
import { clone, unit } from '../utils'
import { RenderBlock } from '../render'
import { DddView } from '../views'
@ -38,6 +38,19 @@ const measureLine = computed(() => {
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 {
// added?: AddEventInfo
// moved?: SwapEventInfo
@ -75,6 +88,7 @@ function handleDragAdd(e: DraggableEvent): void {
ctx.hoverViewId = '#canvas'
ctx.configurator = 'block'
ctx.sources[block.vid] = module?.init ? clone(module.init) : {}
ctx.views[block.vid] = block
key.value++
}
@ -90,6 +104,7 @@ function handleCanvasClick(e: MouseEvent): void {
<template>
<div
ref="canvasRef"
:class="{
'is-focused': ctx.canvas.focused,
'is-active': ctx.activeViewId === '#canvas',
@ -177,6 +192,13 @@ function handleCanvasClick(e: MouseEvent): void {
</div>
<div class="ddd-canvas-indicator" />
</template>
<!-- 使用全局单例利用 absolute 固定位置 -->
<div
class="ddd-view-current-highlight"
:style="currentViewStyle"
/>
<!-- <div class="ddd-view-parent-highlight" /> -->
</div>
</template>
@ -335,4 +357,12 @@ function handleCanvasClick(e: MouseEvent): void {
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>

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

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

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

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

@ -1,7 +1,7 @@
import type { WritableComputedRef, InjectionKey, UnwrapNestedRefs } from 'vue'
import { computed, inject, isReactive, isRef, provide } from 'vue'
import type { Block, Category, Module, Page } from './types'
import { setValue, valueOf } from './utils'
import { computed, inject, provide, unref } from 'vue'
import type { Block, Category, Module, Page, View, ViewChildren } from './types'
import { isRefer, setValue, valueOf } from './utils'
/** 画布配置 */
export interface CanvasConfig {
@ -24,7 +24,7 @@ export interface Exported {
data: Record<string, Record<string, unknown>>
}
export interface EngineContext {
export interface EngineContextBase {
/** 目标 */
target?: 'app' | 'routine'
/** 当前选中的模板 */
@ -45,26 +45,35 @@ export interface EngineContext {
categories: Category[]
/** 布局视图列表 */
blocks: Block[]
/** 主题列表 */
views: Record<string, View>
/** 内部唯一标识 */
nextId: number
/** 数据源 */
sources: Record<string, Record<string, unknown>>
/** 到处数据 */
export: () => Exported
/** 获取动态数据 */
value: <T>(blockId: string, key: string) => T | undefined
}
export const contextKey: InjectionKey<UnwrapNestedRefs<EngineContext>> = Symbol.for('ddd:engine')
export const parentViewIdKey: InjectionKey<string> = Symbol.for('ddd:view:parent:id')
const blockIdKey: InjectionKey<string> = Symbol.for('ddd:block:id')
export type EngineContext = UnwrapNestedRefs<EngineContextBase>
export const contextKey: InjectionKey<EngineContext> = Symbol.for('ddd:engine')
export function useContext(): UnwrapNestedRefs<EngineContext> {
const config = inject(contextKey)
if (config == null) {
throw new Error('no config found')
const must = <T>(v: T | undefined, error: string): T => {
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 {
return inject(parentViewIdKey)
}
@ -75,15 +84,28 @@ export function provideParentViewId(id: string | undefined): void {
}
}
export function useBlockId(): string | undefined {
return inject(blockIdKey)
const blockIdKey: InjectionKey<string> = Symbol.for('ddd:block:id')
export function useBlockId(): string {
return must(inject(blockIdKey), "no block found")
}
export function provideBlockId(id: string): void {
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 module of category.modules) {
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> {
const ctx = useContext()
const treeData = inject(treeDataKey)
const blockId = useBlockId()
return computed<T | undefined>({
get(): T | undefined {
// if (!blockId) {
// throw new Error('without block')
// }
if (!source) {
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
if (!source || !id) {
if (!id) {
return fallback
}
return valueOf(ctx.sources[id], source) ?? fallback
},
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
// console.log({id, source, value})
if (source && id) {
if (id) {
if (!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 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 {
CanvasConfig,
PageConfig,
@ -9,7 +14,7 @@ export type {
Exported,
} from './context'
export { registerWidget, render } from './render'
export { render } from './render'
export {
useContext,
useBlockId,
@ -21,3 +26,12 @@ export { hash, valueOf } from './utils'
export * from './types'
export default DddBuilder
registerView({
text: DddTextView,
image: DddImageView,
audio: DddAudioView,
video: DddAudioView,
each: DddEachView,
template: RenderTemplate
})

@ -1,55 +1,60 @@
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 type { Axis, Block, DataRefer, Theme, View, ViewChildren, Widget, } from './types'
import { provideBlockId } from './context'
import { align, background, bordering, clip, gap, insets, radii, shadowing, size, unit, } from './utils'
import { provideBlockId, useBlockId, useContext } from './context'
import {
align,
background,
bordering,
clip,
dynamic,
gap,
insets,
isRefer,
radii,
shadowing,
size,
stack,
unit,
} from './utils'
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}`)
}
return func(widget, view, axis)
}
// /**
// * 注册的外部组件渲染函数
// */
// const widgets: Record<string, WidgetRenderFunction> = {}
const isWidget = (s: any): s is Widget => s != null && 'name' in s
const views: Record<string, Component> = {}
function isRefer(s: any): s is DataRefer {
return s != null
&& 'type' in s
&& 'key' in s
&& typeof s.type === 'string'
&& typeof s.key === 'string'
export function registerView(v: Record<string, Component>): void {
for (const [key, comp] of Object.entries(v)) {
views[key] = comp
}
}
// export function registerWidget(some: Record<string, WidgetRenderFunction>): void {
// 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
function renderRefer(refer: DataRefer, axis?: Axis): VNodeChild {
export function renderRefer(refer: DataRefer, axis?: Axis): VNodeChild {
const { key, type, ...attrs } = refer
const component = views[type]
if (component) {
@ -67,15 +72,20 @@ function renderRefer(refer: DataRefer, axis?: Axis): VNodeChild {
}, 'unimplemented')
}
export function isFlexible(theme: Theme) {
return theme.axis != null
|| theme.mainAlign != null
|| theme.crossAlign != null
|| theme.wrap != null
|| theme.gap != null
export function isFlexible(theme: DynamicTheme, value: ToStatic) {
return value(theme.axis) != null
|| value(theme.mainAlign) != null
|| value(theme.crossAlign) != null
|| value(theme.wrap) != 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) {
return null
}
@ -83,55 +93,78 @@ export function render(view?: ViewChildren, axis?: Axis): VNodeChild {
return view
}
if (Array.isArray(view)) {
return view.map(v => render(v, axis))
return view.map(child => render(ctx, blockId, child, axis))
}
if (isRefer(view)) {
return renderRefer(view)
}
if (isWidget(view.theme)) {
return renderWidget(view.theme, view, axis)
}
// if (isWidget(view.theme)) {
// return renderWidget(view.theme, view, axis)
// }
const theme = view.theme
const value = <T>(v: DynamicValue<T>): T => dynamic<T>(ctx, blockId, v)
const css: CSSProperties = {
// boxed & spatial
...insets('padding', theme?.padding),
...insets('margin', theme?.margin),
...size(axis, theme),
...insets('padding', value(theme?.padding)),
...insets('margin', value(theme?.margin)),
...size(axis, theme, value), // todo
// decoration
...background(theme?.color, theme?.image, theme?.gradient),
...bordering(theme?.border),
...radii(theme?.radius),
...shadowing(theme?.shadow),
...background(value(theme?.color), value(theme?.image), value(theme?.gradient)),
...bordering(value(theme?.border)),
...radii(value(theme?.radius)),
...shadowing(value(theme?.shadow)),
// textual
color: theme?.textColor,
fontSize: unit(theme?.fontSize),
textAlign: theme?.textAlign,
lineHeight: theme?.lineHeight,
color: value(theme?.textColor),
fontSize: unit(value(theme?.fontSize)),
textAlign: value(theme?.textAlign),
lineHeight: value(theme?.lineHeight),
// clip
...clip(theme?.clip),
...clip(value(theme?.clip)),
// stack
...stack(theme, value),
}
// note: 在前面初始化 css 时使用了参数 axis,
// 所以在这里复用它用于子视图构建
axis = theme?.axis
if (theme?.flexible || (theme && theme.flexible == null && isFlexible(theme))) {
axis = value(theme?.axis)
const flexible = value(theme?.flexible)
if (flexible || (theme && flexible == null && isFlexible(theme, value))) {
Object.assign(css, {
display: 'flex',
flexDirection: axis === 'y' ? 'column' : undefined,
justifyContent: align(theme?.mainAlign),
alignItems: align(theme?.crossAlign),
flexWrap: theme?.wrap ? 'wrap' : undefined,
...gap(theme?.gap),
justifyContent: align(value(theme?.mainAlign)),
alignItems: align(value(theme?.crossAlign)),
flexWrap: value(theme?.wrap) ? 'wrap' : undefined,
...gap(value(theme?.gap)),
})
}
return h(DddView, {
vid: view.vid,
class: 'ddd-view',
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({
name: 'RenderWidget',
name: 'RenderBlock',
props: {
block: {
@ -141,12 +174,16 @@ export const RenderBlock = defineComponent({
},
setup(props) {
// const ctx = useContext()
provideBlockId(props.block!.vid)
return () => {
if (props.block == 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}%`
@ -50,9 +48,9 @@ export interface Gradient {
/** 边距,用于内外边距 */
export type EdgeInsets =
| Direction<Unit> // 四边
| Symmetric<Unit> // 对称
| Unit // 原语
| Direction<Unit | 'auto'> // 四边
| Symmetric<Unit | 'auto'> // 对称
| Unit | 'auto' // 原语
/** 边框样式 */
export type BorderStyle =
@ -144,6 +142,10 @@ export type TextAlign =
*/
export type Axis = 'x' | 'y'
export type Attach<Data,Append> = {
[p in keyof Data]: Data[p] | Append
}
/**
*
*
@ -238,20 +240,32 @@ export interface Boxed {
height?: number | Percentage | 'auto'
}
/**
*
*
* 使
*/
export interface Widget {
/** 组件名称 */
name: string
/** 组件属性 */
props?: Record<string, unknown>
/** 相关插槽,不包括默认插槽 */
slots?: Omit<Slots, 'default'>
export type ViewPosition =
| "absolute"
| "fixed"
| "relative"
| "static"
| "sticky"
export interface Stacked extends Direction<Unit> {
position?: ViewPosition
zIndex?: number
}
// /**
// * 相关框架组件
// *
// * 建议尽量使用业务组件。
// */
// 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
}
export type DynamicTheme = Attach<Theme, `@@${string}`>
export type DynamicValue<T> = T | `@@${string}`
export type ToStatic = <T>(v: DynamicValue<T>) => T
/** 文本配置引用 */
export interface TextRefer {
/** 标识为文本专用类型 */
@ -340,12 +360,27 @@ export interface AudioRefer {
// todo 其它配置实现,比如自动播放、控件配置等
}
export interface EachRefer {
type: 'each'
/** 循环数据地址键 */
key: string
handle: ViewChildren
}
export interface TemplateRefer {
type: 'template'
/** 模板数据地址键 */
key: string
}
/** 目前支持的 4 中配置引用 */
export type DataRefer =
| TextRefer
| ImageRefer
| VideoRefer
| AudioRefer
| EachRefer
| TemplateRefer
export type ViewChild =
| 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[]
/** 初始化数据 */
init?: Record<string, any>
/** 关联模板 */
templates?: Record<string, ViewChildren>
}
export type ModuleConfig =

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

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

@ -1,7 +1,7 @@
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) {
case 'autoX':
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 './shadow'
export * from './size'
export * from './stack'
export * from './unit'
export {dynamic} from "./dynamic.ts";

@ -1,9 +1,9 @@
import type { CSSProperties } from 'vue'
import type { EdgeInsets } from '../types'
import type { DynamicValue, EdgeInsets } from '../types'
import { isSymmetric } from './is'
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) {
return undefined
}
@ -11,11 +11,20 @@ export function insets(name: string, i: EdgeInsets | undefined): CSSProperties |
return {[name]: `${i}px`}
}
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}
}
if (isSymmetric(i)) {
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 {

@ -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> {
return a != null
@ -23,3 +23,11 @@ export function isCorners(a: any): a is Corners<any> {
|| 'bottomLeft' 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 {
borderRadius: stringify(radius),
borderRadius: stringify(radius as RadiusPrimitive),
}
}

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

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

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

Loading…
Cancel
Save