From a635eead2a6d7bd5834d5e4f4f15aa5956001b79 Mon Sep 17 00:00:00 2001 From: sunlizhou <296190577@qq.com> Date: Wed, 6 Dec 2023 18:33:47 +0800 Subject: [PATCH] =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=95=B0=E6=8D=AE=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E9=85=8D=E7=BD=AE=EF=BC=8C=E8=A7=86=E5=9B=BE=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E5=A2=9E=E5=8A=A0=E5=BE=AA=E7=8E=AF=E4=B8=8E=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E7=BB=91=E5=AE=9A=EF=BC=8C=E9=85=8D=E7=BD=AE=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=A2=9E=E5=8A=A0=E6=A8=A1=E6=9D=BF=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=EF=BC=8C=E8=A7=86=E5=9B=BE=E6=B8=B2=E6=9F=93=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 458 ++++++++++++------ src/engineer/components/Activity.vue | 14 +- src/engineer/components/Builder.vue | 28 +- src/engineer/components/Canvas.vue | 36 +- src/engineer/components/ConfigCurrentView.vue | 33 ++ src/engineer/components/Configurator.vue | 5 +- src/engineer/components/Designer.vue | 14 +- src/engineer/configs/ListConfig.vue | 3 +- src/engineer/configs/ObjectConfig.vue | 3 +- src/engineer/context.ts | 107 +++- src/engineer/emitter.ts | 28 ++ src/engineer/index.ts | 18 +- src/engineer/render.ts | 191 +++++--- src/engineer/types.ts | 78 ++- src/engineer/utils/align.ts | 3 + src/engineer/utils/background.ts | 12 +- src/engineer/utils/clip.ts | 4 +- src/engineer/utils/dynamic.ts | 9 + src/engineer/utils/index.ts | 2 + src/engineer/utils/insets.ts | 15 +- src/engineer/utils/is.ts | 10 +- src/engineer/utils/radii.ts | 2 +- src/engineer/utils/size.ts | 15 +- src/engineer/utils/stack.ts | 20 + src/engineer/utils/unit.ts | 7 + src/engineer/views/AudioView.vue | 1 + src/engineer/views/EachView.vue | 48 ++ src/engineer/views/ImageView.vue | 1 + src/engineer/views/RenderTemplate.vue | 22 + src/engineer/views/TextView.vue | 1 + src/engineer/views/View.vue | 40 +- 31 files changed, 901 insertions(+), 327 deletions(-) create mode 100644 src/engineer/components/ConfigCurrentView.vue create mode 100644 src/engineer/emitter.ts create mode 100644 src/engineer/utils/dynamic.ts create mode 100644 src/engineer/utils/stack.ts create mode 100644 src/engineer/views/EachView.vue create mode 100644 src/engineer/views/RenderTemplate.vue diff --git a/src/App.vue b/src/App.vue index f59fd2a..bc85f84 100644 --- a/src/App.vue +++ b/src/App.vue @@ -6,110 +6,110 @@ import DddBuilder, { hash } from './engineer' let nextId = Date.now() const blocks = ref([ -// { -// vid: hash(`${++nextId}`), -// mid: '', -// theme: { -// radius: 24, -// color: 'yellow', -// }, -// children: { -// type: 'text', -// key: 'textName', -// }, -// }, { -// vid: hash(`${++nextId}`), -// mid: '', -// theme: { -// color: 'pink', -// margin: 12, -// flexible: true, -// }, -// children: { -// type: 'image', -// key: 'imageKey', -// link: 'linkKey', -// radius: 24, -// }, -// }, { -// vid: hash(`${++nextId}`), -// mid: '', -// theme: { -// color: '#fafbfc', -// margin: 12, -// padding: 24, -// mainAlign: 'center', -// crossAlign: 'center', -// flexible: true, -// }, -// children: { -// type: 'audio', -// key: 'audioKey', -// title: 'title', -// }, -// }, { -// vid: hash(`${++nextId}`), -// mid: '', -// theme: { -// radius: 24, -// color: 'teal', -// }, -// children: [{ -// theme: { -// flexible: true, -// gap: 12, -// crossAlign: 'center', -// }, -// children: [{ -// theme: { -// padding: 12, -// margin: 12, -// grow: 1, -// }, -// children: { -// vid: hash(`${++nextId}`), -// theme: { -// fontSize: 32, -// }, -// children: 'left', -// }, -// }, { -// vid: hash(`${++nextId}`), -// theme: { -// width: 'auto', -// padding: { -// vertical: 4, -// horizontal: 12, -// }, -// color: 'pink', -// radius: 16, -// }, -// children: 'right222', -// }, { -// theme: { -// width: 48, -// }, -// } -// ], -// } -// ], -// }, ...Array.from(Array.from({length: 3})).map((_, i) => ({ -// vid: hash(`${++nextId}`), -// mid: '', -// children: [{ -// vid: hash(`${++nextId}`), -// theme: { -// fontSize: 22, -// }, -// children: `测试${i}`, -// }], -// })) + // { + // vid: hash(`${++nextId}`), + // mid: '', + // theme: { + // radius: 24, + // color: 'yellow', + // }, + // children: { + // type: 'text', + // key: 'textName', + // }, + // }, { + // vid: hash(`${++nextId}`), + // mid: '', + // theme: { + // color: 'pink', + // margin: 12, + // flexible: true, + // }, + // children: { + // type: 'image', + // key: 'imageKey', + // link: 'linkKey', + // radius: 24, + // }, + // }, { + // vid: hash(`${++nextId}`), + // mid: '', + // theme: { + // color: '#fafbfc', + // margin: 12, + // padding: 24, + // mainAlign: 'center', + // crossAlign: 'center', + // flexible: true, + // }, + // children: { + // type: 'audio', + // key: 'audioKey', + // title: 'title', + // }, + // }, { + // vid: hash(`${++nextId}`), + // mid: '', + // theme: { + // radius: 24, + // color: 'teal', + // }, + // children: [{ + // theme: { + // flexible: true, + // gap: 12, + // crossAlign: 'center', + // }, + // children: [{ + // theme: { + // padding: 12, + // margin: 12, + // grow: 1, + // }, + // children: { + // vid: hash(`${++nextId}`), + // theme: { + // fontSize: 32, + // }, + // children: 'left', + // }, + // }, { + // vid: hash(`${++nextId}`), + // theme: { + // width: 'auto', + // padding: { + // vertical: 4, + // horizontal: 12, + // }, + // color: 'pink', + // radius: 16, + // }, + // children: 'right222', + // }, { + // theme: { + // width: 48, + // }, + // } + // ], + // } + // ], + // }, ...Array.from(Array.from({length: 3})).map((_, i) => ({ + // vid: hash(`${++nextId}`), + // mid: '', + // children: [{ + // vid: hash(`${++nextId}`), + // theme: { + // fontSize: 22, + // }, + // 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, - modules: Array.from(Array.from({length: 102})).map((_, i) => ({ + modules: Array.from(Array.from({ length: 102 })).map((_, i) => ({ vid: hash(`${++nextId}`), mid: hash(`${++nextId}`), title: `${c.text}${i + 1}`, @@ -122,7 +122,7 @@ const categories = ref([{icon: 'trash', text: '媒体'}, {icon: 'trash', text: ' theme: { height: 68, width: 375, - border: {color: '#eee'}, + border: { color: '#eee' }, }, children: [String(`${c.text}${i + 1}`)], }], @@ -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' + } }], }, { @@ -254,8 +338,8 @@ categories.value.unshift({ label: '产品标题及说明', configs: [ { - type:'text', - label:'产品标题', + type: 'text', + label: '产品标题', field: 'mainTitle' }, { @@ -273,33 +357,40 @@ categories.value.unshift({ label: '产品组列表', configs: [ { - type:'image', - label:'产品图片', + type: 'image', + label: '产品图片', field: 'productPhoto', // 表示内联数据 - inlines: [ { + inlines: [{ type: 'text', field: 'link', label: '链接', help: '请输入链接', // 自动生成:"请输入${label}" - }] - }, - { - type:'text', - label:'自定义标题', - field: 'productTitle', - }, - { - type:'text', - label:'自定义说明', - field: 'productDesc', + }, + { + type: 'text', + label: '自定义标题', + field: 'productTitle', + }, + { + type: 'text', + label: '自定义说明', + field: 'productDesc', + }, + ] }, + ] }, ], init: { + titleAndDesc: { + mainTitle: '标题', + content: '说明', + }, groups: [ { + productTitle: '111', } ], @@ -313,15 +404,117 @@ 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: '产品' + }, + ] + }, + ] } } -], + ], }) const data = ref>>({ // [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>>({ - + diff --git a/src/engineer/components/Activity.vue b/src/engineer/components/Activity.vue index 3dd8ab3..e0d9181 100644 --- a/src/engineer/components/Activity.vue +++ b/src/engineer/components/Activity.vue @@ -1,8 +1,8 @@ diff --git a/src/engineer/components/Builder.vue b/src/engineer/components/Builder.vue index b895441..a7d4072 100644 --- a/src/engineer/components/Builder.vue +++ b/src/engineer/components/Builder.vue @@ -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> }>() -let ctx: UnwrapNestedRefs +let ctx: UnwrapNestedRefs // eslint-disable-next-line prefer-const -ctx = reactive({ +ctx = reactive({ target: 'routine', activeBlockId: undefined, activeViewId: undefined, @@ -59,6 +67,7 @@ ctx = reactive({ }, 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({ const data = unref(ctx.sources) return {attrs, data} }, + value(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({})) diff --git a/src/engineer/components/Designer.vue b/src/engineer/components/Designer.vue index 6e24fa0..d6613e4 100644 --- a/src/engineer/components/Designer.vue +++ b/src/engineer/components/Designer.vue @@ -1,6 +1,6 @@ @@ -82,6 +91,7 @@ function onMouseover(e: MouseEvent): void { > } -export interface EngineContext { +export interface EngineContextBase { /** 目标 */ target?: 'app' | 'routine' /** 当前选中的模板 */ @@ -45,26 +45,35 @@ export interface EngineContext { categories: Category[] /** 布局视图列表 */ blocks: Block[] + /** 主题列表 */ + views: Record /** 内部唯一标识 */ nextId: number /** 数据源 */ sources: Record> /** 到处数据 */ export: () => Exported + /** 获取动态数据 */ + value: (blockId: string, key: string) => T | undefined } -export const contextKey: InjectionKey> = Symbol.for('ddd:engine') -export const parentViewIdKey: InjectionKey = Symbol.for('ddd:view:parent:id') -const blockIdKey: InjectionKey = Symbol.for('ddd:block:id') +export type EngineContext = UnwrapNestedRefs -export function useContext(): UnwrapNestedRefs { - const config = inject(contextKey) - if (config == null) { - throw new Error('no config found') +export const contextKey: InjectionKey = Symbol.for('ddd:engine') + +const must = (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 = 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 = 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, mid: string): Module | undefined { +export const currentViewDOMRectKey: InjectionKey>> = Symbol.for('ddd:view:current:domrect') +export const parentViewDOMRectKey: InjectionKey>> = Symbol.for('ddd:view:parent:domrect') + +export function useCurrentViewDOMRect(): UnwrapNestedRefs> { + return must(inject(currentViewDOMRectKey), 'no DOMReact found') +} + +export function useParentViewDOMRect(): UnwrapNestedRefs> { + return must(inject(parentViewDOMRectKey), 'no DOMReact found') +} + +export function getModule(ctx: UnwrapNestedRefs, 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|WritableComputedRef | null> = Symbol.for('ddd:view:tree:data') + +export function provideTreeData(data: UnwrapNestedRefs | WritableComputedRef | null) { + provide(treeDataKey, data) +} + export function useSource(source: string | undefined, fallback?: T): WritableComputedRef { const ctx = useContext() + const treeData = inject(treeDataKey) const blockId = useBlockId() return computed({ 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(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) +} diff --git a/src/engineer/emitter.ts b/src/engineer/emitter.ts new file mode 100644 index 0000000..46f941a --- /dev/null +++ b/src/engineer/emitter.ts @@ -0,0 +1,28 @@ +const handlers: Record = 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) + } +} diff --git a/src/engineer/index.ts b/src/engineer/index.ts index 9baa062..d3a53fe 100644 --- a/src/engineer/index.ts +++ b/src/engineer/index.ts @@ -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 +}) diff --git a/src/engineer/render.ts b/src/engineer/render.ts index 2ce4250..fa0f634 100644 --- a/src/engineer/render.ts +++ b/src/engineer/render.ts @@ -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 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 = {} - -const views: Record = { - text: DddTextView, - image: DddImageView, - audio: DddAudioView, - video: DddAudioView, -} -export function registerWidget(some: Record): 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 = {} -const isWidget = (s: any): s is Widget => s != null && 'name' in s +const views: Record = {} -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): void { + for (const [key, comp] of Object.entries(v)) { + views[key] = comp + } } -function renderRefer(refer: DataRefer, axis?: Axis): VNodeChild { - const {key, type, ...attrs} = refer +// export function registerWidget(some: Record): 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 + +export function renderRefer(refer: DataRefer, axis?: Axis): VNodeChild { + const { key, type, ...attrs } = refer const component = views[type] if (component) { return h(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 = (v: DynamicValue): T => dynamic(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, + }, + + 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, + }) } }, }) diff --git a/src/engineer/types.ts b/src/engineer/types.ts index 2c7f391..bdf4d40 100644 --- a/src/engineer/types.ts +++ b/src/engineer/types.ts @@ -1,5 +1,3 @@ -import type { Slots } from 'vue' - /** 百分比 */ export type Percentage = `${number}%` @@ -50,9 +48,9 @@ export interface Gradient { /** 边距,用于内外边距 */ export type EdgeInsets = - | Direction // 四边 - | Symmetric // 对称 - | Unit // 原语 + | Direction // 四边 + | Symmetric // 对称 + | Unit | 'auto' // 原语 /** 边框样式 */ export type BorderStyle = @@ -144,6 +142,10 @@ export type TextAlign = */ export type Axis = 'x' | 'y' +export type Attach = { + [p in keyof Data]: Data[p] | Append +} + /** * 文本 * @@ -238,19 +240,31 @@ export interface Boxed { height?: number | Percentage | 'auto' } -/** - * 相关框架组件 - * - * 建议尽量使用业务组件。 - */ -export interface Widget { - /** 组件名称 */ - name: string - /** 组件属性 */ - props?: Record - /** 相关插槽,不包括默认插槽 */ - slots?: Omit -} +export type ViewPosition = + | "absolute" + | "fixed" + | "relative" + | "static" + | "sticky" + +export interface Stacked extends Direction { + position?: ViewPosition + zIndex?: number +} + +// /** +// * 相关框架组件 +// * +// * 建议尽量使用业务组件。 +// */ +// export interface Widget { +// /** 组件名称 */ +// name: string +// /** 组件属性 */ +// props?: Record +// /** 相关插槽,不包括默认插槽 */ +// slots?: Omit +// } /** * 内容超出视图的剪切行为 @@ -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 + +export type DynamicValue = T | `@@${string}` + +export type ToStatic = (v: DynamicValue) => 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 + /** 关联模板 */ + templates?: Record } export type ModuleConfig = diff --git a/src/engineer/utils/align.ts b/src/engineer/utils/align.ts index 0abfe58..c40f788 100644 --- a/src/engineer/utils/align.ts +++ b/src/engineer/utils/align.ts @@ -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 } } diff --git a/src/engineer/utils/background.ts b/src/engineer/utils/background.ts index 98f9f52..9cb91f6 100644 --- a/src/engineer/utils/background.ts +++ b/src/engineer/utils/background.ts @@ -11,12 +11,14 @@ function image(img: string | undefined, g: Gradient | undefined): string | undef if (g == null) { return undefined } - const prefix = g.repeatable ? 'repeating-' : '' - if (g.stops?.length === g.colors.length) { - const colors = g.colors.map((c, i) => `${c} ${g.stops![i]}`).join(', ') - return `${prefix}linear-gradient(${g?.angle ?? '0deg'}, ${colors})` + 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(', ') + 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 { diff --git a/src/engineer/utils/clip.ts b/src/engineer/utils/clip.ts index f7d19b2..0310fed 100644 --- a/src/engineer/utils/clip.ts +++ b/src/engineer/utils/clip.ts @@ -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 | undefined): CSSProperties | undefined { switch (clip) { case 'autoX': return { diff --git a/src/engineer/utils/dynamic.ts b/src/engineer/utils/dynamic.ts new file mode 100644 index 0000000..de00aea --- /dev/null +++ b/src/engineer/utils/dynamic.ts @@ -0,0 +1,9 @@ +import type { DynamicValue } from "../types" +import type { EngineContext } from "../context" + +export function dynamic(ctx: EngineContext, blockId: string, value: DynamicValue): T { + if (typeof value === 'string' && value.startsWith('@@')) { + return ctx.value(blockId, value) as T + } + return value as T +} diff --git a/src/engineer/utils/index.ts b/src/engineer/utils/index.ts index d54e190..da7dbd5 100644 --- a/src/engineer/utils/index.ts +++ b/src/engineer/utils/index.ts @@ -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"; diff --git a/src/engineer/utils/insets.ts b/src/engineer/utils/insets.ts index 5f56359..6ba2b7b 100644 --- a/src/engineer/utils/insets.ts +++ b/src/engineer/utils/insets.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 | 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 { diff --git a/src/engineer/utils/is.ts b/src/engineer/utils/is.ts index 1dc0432..5b4e8f8 100644 --- a/src/engineer/utils/is.ts +++ b/src/engineer/utils/is.ts @@ -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 { return a != null @@ -23,3 +23,11 @@ export function isCorners(a: any): a is Corners { || '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' +} diff --git a/src/engineer/utils/radii.ts b/src/engineer/utils/radii.ts index c2020c3..5cc74a9 100644 --- a/src/engineer/utils/radii.ts +++ b/src/engineer/utils/radii.ts @@ -25,6 +25,6 @@ export function radii(radius: Radius | undefined): CSSProperties | undefined { } } return { - borderRadius: stringify(radius), + borderRadius: stringify(radius as RadiusPrimitive), } } diff --git a/src/engineer/utils/size.ts b/src/engineer/utils/size.ts index a9863ec..5b37ae8 100644 --- a/src/engineer/utils/size.ts +++ b/src/engineer/utils/size.ts @@ -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(), } } diff --git a/src/engineer/utils/stack.ts b/src/engineer/utils/stack.ts new file mode 100644 index 0000000..800d803 --- /dev/null +++ b/src/engineer/utils/stack.ts @@ -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)), + } +} diff --git a/src/engineer/utils/unit.ts b/src/engineer/utils/unit.ts index ce0a38b..e12010b 100644 --- a/src/engineer/utils/unit.ts +++ b/src/engineer/utils/unit.ts @@ -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` diff --git a/src/engineer/views/AudioView.vue b/src/engineer/views/AudioView.vue index 8f62a3c..b580ff1 100644 --- a/src/engineer/views/AudioView.vue +++ b/src/engineer/views/AudioView.vue @@ -3,6 +3,7 @@ import { useSource } from '../context' defineOptions({ name: 'DddAudioView', + inheritAttrs: false, }) const props = defineProps<{ diff --git a/src/engineer/views/EachView.vue b/src/engineer/views/EachView.vue new file mode 100644 index 0000000..59e8939 --- /dev/null +++ b/src/engineer/views/EachView.vue @@ -0,0 +1,48 @@ + + + diff --git a/src/engineer/views/ImageView.vue b/src/engineer/views/ImageView.vue index 8a86036..f50c4f0 100644 --- a/src/engineer/views/ImageView.vue +++ b/src/engineer/views/ImageView.vue @@ -5,6 +5,7 @@ import { radii } from '../utils' defineOptions({ name: 'DddImageView', + inheritAttrs: false, }) const props = defineProps<{ diff --git a/src/engineer/views/RenderTemplate.vue b/src/engineer/views/RenderTemplate.vue new file mode 100644 index 0000000..4e51616 --- /dev/null +++ b/src/engineer/views/RenderTemplate.vue @@ -0,0 +1,22 @@ + + + + diff --git a/src/engineer/views/TextView.vue b/src/engineer/views/TextView.vue index 3a596df..e62259c 100644 --- a/src/engineer/views/TextView.vue +++ b/src/engineer/views/TextView.vue @@ -3,6 +3,7 @@ import { useSource } from '../context' defineOptions({ name: 'DddTextView', + inheritAttrs: false, }) const props = defineProps<{ diff --git a/src/engineer/views/View.vue b/src/engineer/views/View.vue index 830f56e..31460f1 100644 --- a/src/engineer/views/View.vue +++ b/src/engineer/views/View.vue @@ -1,6 +1,6 @@