commit
ddf6148d14
@ -0,0 +1 @@ |
|||||||
|
* text=auto eol=lf |
@ -0,0 +1,24 @@ |
|||||||
|
# Logs |
||||||
|
logs |
||||||
|
*.log |
||||||
|
npm-debug.log* |
||||||
|
yarn-debug.log* |
||||||
|
yarn-error.log* |
||||||
|
pnpm-debug.log* |
||||||
|
lerna-debug.log* |
||||||
|
|
||||||
|
node_modules |
||||||
|
dist |
||||||
|
dist-ssr |
||||||
|
*.local |
||||||
|
|
||||||
|
# Editor directories and files |
||||||
|
.vscode/* |
||||||
|
!.vscode/extensions.json |
||||||
|
.idea |
||||||
|
.DS_Store |
||||||
|
*.suo |
||||||
|
*.ntvs* |
||||||
|
*.njsproj |
||||||
|
*.sln |
||||||
|
*.sw? |
@ -0,0 +1,3 @@ |
|||||||
|
{ |
||||||
|
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
# Vue 3 + TypeScript + Vite |
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more. |
||||||
|
|
||||||
|
## Recommended IDE Setup |
||||||
|
|
||||||
|
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). |
||||||
|
|
||||||
|
## Type Support For `.vue` Imports in TS |
||||||
|
|
||||||
|
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types. |
||||||
|
|
||||||
|
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps: |
||||||
|
|
||||||
|
1. Disable the built-in TypeScript Extension |
||||||
|
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette |
||||||
|
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)` |
||||||
|
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette. |
@ -0,0 +1,22 @@ |
|||||||
|
import antfu from '@antfu/eslint-config' |
||||||
|
|
||||||
|
export default await antfu({ |
||||||
|
stylistic: { |
||||||
|
indent: 2, |
||||||
|
quotes: 'single', |
||||||
|
}, |
||||||
|
typescript: true, |
||||||
|
vue: true, |
||||||
|
rules: { |
||||||
|
'vue/singleline-html-element-content-newline': 'off', |
||||||
|
'vue/html-self-closing': ['error', { |
||||||
|
html: { |
||||||
|
void: 'always', |
||||||
|
normal: 'always', |
||||||
|
component: 'always', |
||||||
|
}, |
||||||
|
svg: 'always', |
||||||
|
math: 'always', |
||||||
|
}], |
||||||
|
}, |
||||||
|
}) |
@ -0,0 +1,13 @@ |
|||||||
|
<!doctype html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8" /> |
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
||||||
|
<title>Vite + Vue + TS</title> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div id="app"></div> |
||||||
|
<script type="module" src="/src/main.ts"></script> |
||||||
|
</body> |
||||||
|
</html> |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,27 @@ |
|||||||
|
{ |
||||||
|
"name": "data-driven-design", |
||||||
|
"type": "module", |
||||||
|
"version": "0.0.1", |
||||||
|
"private": true, |
||||||
|
"scripts": { |
||||||
|
"dev": "vite", |
||||||
|
"build": "vue-tsc && vite build", |
||||||
|
"preview": "vite preview", |
||||||
|
"lint": "eslint .", |
||||||
|
"lint:fix": "eslint . --fix" |
||||||
|
}, |
||||||
|
"dependencies": { |
||||||
|
"vue": "^3.3.8", |
||||||
|
"vuedraggable": "^4.1.0" |
||||||
|
}, |
||||||
|
"devDependencies": { |
||||||
|
"@antfu/eslint-config": "^2.1.1", |
||||||
|
"@types/node": "^20.9.4", |
||||||
|
"@vitejs/plugin-vue": "^4.5.0", |
||||||
|
"eslint": "^8.54.0", |
||||||
|
"less": "^4.2.0", |
||||||
|
"typescript": "^5.2.2", |
||||||
|
"vite": "^5.0.0", |
||||||
|
"vue-tsc": "^1.8.22" |
||||||
|
} |
||||||
|
} |
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,296 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { ref } from 'vue' |
||||||
|
import type { Block, Module } from './engineer' |
||||||
|
import DddBuilder, { hash } from './engineer' |
||||||
|
|
||||||
|
let nextId = Date.now() |
||||||
|
|
||||||
|
const blocks = ref<Block[]>([ |
||||||
|
{ |
||||||
|
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<Block>((_, 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 => ({ |
||||||
|
...c, |
||||||
|
modules: Array.from(Array.from({ length: 102 })).map<Module>((_, i) => ({ |
||||||
|
vid: hash(`${++nextId}`), |
||||||
|
mid: hash(`${++nextId}`), |
||||||
|
title: `${c.text}${i + 1}`, |
||||||
|
maxReferenceCount: -1, |
||||||
|
referenceCount: 0, |
||||||
|
image: undefined, |
||||||
|
configs: [], |
||||||
|
children: [ |
||||||
|
{ |
||||||
|
vid: hash(`${++nextId}`), |
||||||
|
theme: { |
||||||
|
height: 68, |
||||||
|
width: 375, |
||||||
|
border: { color: '#eee' }, |
||||||
|
}, |
||||||
|
children: [ |
||||||
|
String(`${c.text}${i + 1}`), |
||||||
|
], |
||||||
|
}, |
||||||
|
], |
||||||
|
})), |
||||||
|
}))) |
||||||
|
|
||||||
|
categories.value.unshift({ |
||||||
|
icon: 'trash', |
||||||
|
text: '基础', |
||||||
|
modules: [ |
||||||
|
{ |
||||||
|
vid: hash(`${++nextId}`), |
||||||
|
mid: hash(`${++nextId}`), |
||||||
|
title: '轮播图', |
||||||
|
maxReferenceCount: -1, |
||||||
|
referenceCount: 0, |
||||||
|
image: undefined, |
||||||
|
configs: [ |
||||||
|
{ |
||||||
|
type: 'list', |
||||||
|
field: 'items', |
||||||
|
label: '', |
||||||
|
help: '最多可添加10张图片,建议宽度750px;鼠标拖拽左侧圆点可调整图片顺序', |
||||||
|
addable: true, |
||||||
|
configs: [ |
||||||
|
{ |
||||||
|
type: 'image', |
||||||
|
field: 'image', |
||||||
|
// 表示内联数据 |
||||||
|
inlines: [ |
||||||
|
{ |
||||||
|
type: 'text', |
||||||
|
field: 'title', |
||||||
|
label: '标题', |
||||||
|
help: '选填,不超过 4 个字', |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: 'text', |
||||||
|
field: 'link', |
||||||
|
label: '链接', |
||||||
|
// help: '请输入链接', // 自动生成:"请输入${label}" |
||||||
|
}, |
||||||
|
], |
||||||
|
label: '图片', |
||||||
|
required: true, |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: 'object', |
||||||
|
field: 'indicator', |
||||||
|
label: '指示器', |
||||||
|
configs: [ |
||||||
|
{ |
||||||
|
type: 'mark', |
||||||
|
field: 'style', |
||||||
|
label: '指示器样式', |
||||||
|
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' }, |
||||||
|
], |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: 'color', |
||||||
|
field: 'color', |
||||||
|
label: '指示器颜色', |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: 'object', |
||||||
|
field: 'background', |
||||||
|
label: '背景', |
||||||
|
configs: [ |
||||||
|
{ |
||||||
|
type: 'bool', |
||||||
|
field: 'enabled', |
||||||
|
label: '是否显示背景色', |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: 'background', |
||||||
|
field: 'value', |
||||||
|
// 背景的不同实现方式,可以在里面添加 image 选项来支持图片 |
||||||
|
// 这里只能支持颜色和渐变, |
||||||
|
features: ['color', 'gradient'], |
||||||
|
label: '背景', |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
], |
||||||
|
theme: { |
||||||
|
height: 200, |
||||||
|
padding: { |
||||||
|
horizontal: 12.0, |
||||||
|
}, |
||||||
|
color: 'cyan', |
||||||
|
}, |
||||||
|
children: [ |
||||||
|
{ |
||||||
|
vid: hash(`${++nextId}`), |
||||||
|
theme: { |
||||||
|
width: '100%', |
||||||
|
height: '100%', |
||||||
|
color: 'white', |
||||||
|
radius: 12.0, |
||||||
|
textAlign: 'center', |
||||||
|
}, |
||||||
|
children: '内容', |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
], |
||||||
|
}) |
||||||
|
|
||||||
|
const data = ref<Record<string, Record<string, unknown>>>({ |
||||||
|
[blocks.value[0].vid]: { |
||||||
|
textName: '测试 2222', |
||||||
|
}, |
||||||
|
[blocks.value[1].vid]: { |
||||||
|
imageKey: 'https://www.w3schools.com/css/paris.jpg', |
||||||
|
linkKey: 'https://docs.taro.zone/canIUse/', |
||||||
|
}, |
||||||
|
[blocks.value[2].vid]: { |
||||||
|
audioKey: 'https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3', |
||||||
|
title: '啊哈哈', |
||||||
|
}, |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<DddBuilder |
||||||
|
:categories="categories" |
||||||
|
:blocks="blocks" |
||||||
|
:sequence="nextId" |
||||||
|
:sources="data" |
||||||
|
/> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
|
||||||
|
</style> |
After Width: | Height: | Size: 496 B |
@ -0,0 +1,229 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { ref, unref } from 'vue' |
||||||
|
import Draggable from 'vuedraggable' |
||||||
|
import type { Block, Module } from '../types' |
||||||
|
import { useContext } from '../context' |
||||||
|
import { hash } from '../utils' |
||||||
|
|
||||||
|
defineOptions({ |
||||||
|
name: 'DddActivity', |
||||||
|
}) |
||||||
|
|
||||||
|
const currentTab = ref(0) |
||||||
|
const ctx = useContext() |
||||||
|
|
||||||
|
// 递归重建所有 ID |
||||||
|
function clone(v: any): any { |
||||||
|
if (v == null || typeof v != 'object') |
||||||
|
return v |
||||||
|
|
||||||
|
if (Array.isArray(v)) |
||||||
|
return v.slice().map(clone) |
||||||
|
|
||||||
|
const s = Object.create(null) |
||||||
|
for (const [key, val] of Object.entries(v)) { |
||||||
|
if (key === 'vid' && typeof val === 'string' && val.length === 8) |
||||||
|
s[key] = hash(`${++ctx.nextId}`) |
||||||
|
else |
||||||
|
s[key] = clone(val) |
||||||
|
} |
||||||
|
return s |
||||||
|
} |
||||||
|
|
||||||
|
function cloneModule(src: Module): Block { |
||||||
|
// eslint-disable-next-line unused-imports/no-unused-vars |
||||||
|
const { maxReferenceCount, referenceCount, image, configs, ...attrs } = unref(src) |
||||||
|
return clone(attrs) |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="ddd-activity"> |
||||||
|
<div class="ddd-activity-header"> |
||||||
|
<slot name="header" /> |
||||||
|
</div> |
||||||
|
<div class="ddd-activity-content"> |
||||||
|
<div class="ddd-activity-tabs"> |
||||||
|
<div |
||||||
|
v-for="(c, i) in ctx.categories" |
||||||
|
:key="i" |
||||||
|
class="ddd-activity-tabs-item" |
||||||
|
:class="{ 'is-active': currentTab === i }" |
||||||
|
@click.stop="currentTab = i" |
||||||
|
> |
||||||
|
<!-- <Icon :name="c.icon" /> --> |
||||||
|
<span>{{ c.text }}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<template v-for="(cat, idx) in ctx.categories" :key="idx"> |
||||||
|
<Draggable |
||||||
|
v-show="currentTab === idx" |
||||||
|
class="ddd-activity-tabs-content" |
||||||
|
:list="cat.modules" |
||||||
|
:group="{ name: 'modules', pull: 'clone' }" |
||||||
|
:clone="cloneModule" |
||||||
|
:component-dat="{ sort: false }" |
||||||
|
:sort="false" |
||||||
|
item-key="id" |
||||||
|
> |
||||||
|
<template #item="{ element }"> |
||||||
|
<div class="ddd-activity-module"> |
||||||
|
<img v-if="element.image" class="ddd-activity-module-image" :src="element.image" /> |
||||||
|
<div v-else class="ddd-activity-module-image">{{ element.title }}</div> |
||||||
|
<div class="ddd-activity-module-title">{{ element.title }}</div> |
||||||
|
<span class="ddd-activity-module-tips">释放鼠标将组件添加到此处</span> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</Draggable> |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
<div class="ddd-activity-footer"> |
||||||
|
<slot name="footer" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="less" scoped> |
||||||
|
.ddd-activity { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
align-items: stretch; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
overflow: hidden; |
||||||
|
font-size: 13px; |
||||||
|
|
||||||
|
&-header, |
||||||
|
&-footer { |
||||||
|
width: 100%; |
||||||
|
flex-shrink: 0; |
||||||
|
} |
||||||
|
|
||||||
|
&-content { |
||||||
|
flex: 1; |
||||||
|
display: flex; |
||||||
|
align-items: stretch; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
|
||||||
|
&-tabs { |
||||||
|
position: relative; |
||||||
|
flex-shrink: 0; |
||||||
|
width: 64px; |
||||||
|
padding: 12px 0; |
||||||
|
|
||||||
|
&::after { |
||||||
|
z-index: 0; |
||||||
|
position: absolute; |
||||||
|
top: 0; |
||||||
|
bottom: 0; |
||||||
|
right: 0; |
||||||
|
content: ""; |
||||||
|
border-right: 1px solid #ddd; |
||||||
|
} |
||||||
|
|
||||||
|
&-item { |
||||||
|
z-index: 1; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
width: 64px; |
||||||
|
height: 64px; |
||||||
|
user-select: none; |
||||||
|
position: relative; |
||||||
|
cursor: pointer; |
||||||
|
gap: 2px; |
||||||
|
transition: all 200ms; |
||||||
|
|
||||||
|
&::after { |
||||||
|
position: absolute; |
||||||
|
content: ""; |
||||||
|
top: 0; |
||||||
|
right: 0; |
||||||
|
bottom: 0; |
||||||
|
width: 2px; |
||||||
|
background: transparent; |
||||||
|
transition: all 200ms; |
||||||
|
} |
||||||
|
|
||||||
|
&:hover { |
||||||
|
background: rgba(#000, 0.08); |
||||||
|
} |
||||||
|
|
||||||
|
&.is-active { |
||||||
|
color: blue; |
||||||
|
background: rgba(2, 86, 255, 0.1); |
||||||
|
|
||||||
|
&::after { |
||||||
|
background: blue; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&-content { |
||||||
|
padding: 12px; |
||||||
|
flex: 1; |
||||||
|
display: flex; |
||||||
|
flex-wrap: wrap; |
||||||
|
gap: 12px; |
||||||
|
justify-items: flex-start; |
||||||
|
align-content: flex-start; |
||||||
|
overflow-y: auto; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&-module { |
||||||
|
// 在左边预览图时的样式 |
||||||
|
&:not(.is-ghost) { |
||||||
|
width: 100px; |
||||||
|
border: 1px solid #eee; |
||||||
|
user-select: none; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
text-align: center; |
||||||
|
background: #fff; |
||||||
|
} |
||||||
|
|
||||||
|
&-image { |
||||||
|
height: 68px; |
||||||
|
width: 98px; |
||||||
|
line-height: 68px; |
||||||
|
color: #eee; |
||||||
|
font-size: 24px; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
|
||||||
|
&-title { |
||||||
|
height: 24px; |
||||||
|
line-height: 24px; |
||||||
|
background: #eee; |
||||||
|
font-size: 13px; |
||||||
|
} |
||||||
|
|
||||||
|
&-tips { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
// 拖拽到模拟器上的样式 |
||||||
|
// 此时,内部内容会变成字符串 “释放鼠标讲组件添加到此处”。 |
||||||
|
&.is-ghost { |
||||||
|
background: rgba(0, 0, 255, 0.1); |
||||||
|
color: blue; |
||||||
|
padding: 8px; |
||||||
|
text-align: center; |
||||||
|
border: 1px dashed blue; |
||||||
|
font-size: 12px; |
||||||
|
} |
||||||
|
|
||||||
|
&.is-ghost &-image, |
||||||
|
&.is-ghost &-title { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
&.is-ghost &-tips { |
||||||
|
display: block; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,77 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
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 DddEngineer from './Engineer.vue' |
||||||
|
|
||||||
|
defineOptions({ |
||||||
|
name: 'DddBuilder', |
||||||
|
}) |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
page?: PageConfig |
||||||
|
categories: Category[] |
||||||
|
blocks: Block[] |
||||||
|
sequence?: number |
||||||
|
sources?: Record<string, Record<string, unknown>> |
||||||
|
}>() |
||||||
|
|
||||||
|
let ctx: UnwrapNestedRefs<EngineContext> |
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-const |
||||||
|
ctx = reactive<EngineContext>({ |
||||||
|
target: 'routine', |
||||||
|
activeBlockId: undefined, |
||||||
|
activeViewId: undefined, |
||||||
|
draggingViewId: undefined, |
||||||
|
page: props.page ?? { |
||||||
|
fullscreen: false, |
||||||
|
brightness: 'light', |
||||||
|
safeAreaInsetTop: true, |
||||||
|
safeAreaInsetBottom: true, |
||||||
|
backgroundColor: '#f1f1f1', |
||||||
|
header: { |
||||||
|
enabled: true, |
||||||
|
custom: undefined, |
||||||
|
title: 'PageTitle', |
||||||
|
centerTitle: false, |
||||||
|
height: 44, |
||||||
|
color: '#ffffff', |
||||||
|
textColor: '#000000', |
||||||
|
bordered: true, |
||||||
|
borderColor: '#ddd', |
||||||
|
}, |
||||||
|
footer: { |
||||||
|
enabled: false, |
||||||
|
height: 44, |
||||||
|
color: '#ffffff', |
||||||
|
textColor: '#000000', |
||||||
|
bordered: true, |
||||||
|
borderColor: '#ddd', // 'rgba(51,51,51,.1)', |
||||||
|
view: undefined, |
||||||
|
}, |
||||||
|
}, |
||||||
|
canvas: { |
||||||
|
focused: false, |
||||||
|
scale: 1, |
||||||
|
}, |
||||||
|
categories: props.categories!, |
||||||
|
blocks: props.blocks, |
||||||
|
nextId: props.sequence ?? Date.now(), |
||||||
|
sources: props.sources ?? Object.create(null), |
||||||
|
export(): Exported { |
||||||
|
const attrs = unref(ctx.page) |
||||||
|
const data = unref(ctx.sources) |
||||||
|
return { attrs, data } |
||||||
|
}, |
||||||
|
}) |
||||||
|
provideBlockId('') |
||||||
|
provide(parentViewIdKey, '') |
||||||
|
provide(contextKey, ctx) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<DddEngineer /> |
||||||
|
</template> |
@ -0,0 +1,335 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { computed, ref } from 'vue' |
||||||
|
import Draggable from 'vuedraggable' |
||||||
|
import { useContext } from '../context' |
||||||
|
import { RenderBlock } from '../render' |
||||||
|
import { DddView } from '../views' |
||||||
|
|
||||||
|
defineOptions({ |
||||||
|
name: 'DddCanvas', |
||||||
|
}) |
||||||
|
|
||||||
|
// const emit = defineEmits<{ |
||||||
|
// (event: 'add', data: AddEventInfo): void |
||||||
|
// (event: 'swap', data: SwapEventInfo): void |
||||||
|
// }>() |
||||||
|
|
||||||
|
// FIXME: 暂时使用 key 强制更新 |
||||||
|
const key = ref(0) |
||||||
|
|
||||||
|
const ctx = useContext() |
||||||
|
|
||||||
|
const headerColor = computed(() => ctx.page.header.color ?? '#fff') |
||||||
|
const headerTextColor = computed(() => ctx.page.header.textColor ?? '#000') |
||||||
|
const headerBorderColor = computed(() => ctx.page.header.borderColor ?? 'rgba(51,51,51,.1)') |
||||||
|
const headerHeight = computed(() => `${ctx.page.header.height ?? 44}px`) |
||||||
|
|
||||||
|
const footerColor = computed(() => ctx.page.footer.color ?? '#fff') |
||||||
|
const footerTextColor = computed(() => ctx.page.footer.textColor ?? '#000') |
||||||
|
const footerBorderColor = computed(() => ctx.page.footer.borderColor ?? 'rgba(51,51,51,.1)') |
||||||
|
const footerHeight = computed(() => `${ctx.page.footer.height ?? 44}px`) |
||||||
|
|
||||||
|
const backgroundColor = computed(() => { |
||||||
|
return ctx.page.backgroundColor |
||||||
|
}) |
||||||
|
|
||||||
|
const measureLine = computed(() => { |
||||||
|
return `${1 / ctx.canvas.scale}px` |
||||||
|
}) |
||||||
|
|
||||||
|
// interface EmulatorChangedEvent { |
||||||
|
// added?: AddEventInfo |
||||||
|
// moved?: SwapEventInfo |
||||||
|
// } |
||||||
|
// |
||||||
|
// const handleDraggingOver = (e: EmulatorChangedEvent): void => { |
||||||
|
// if (e.moved) { |
||||||
|
// const {oldIndex, newIndex, element} = e.moved |
||||||
|
// emit('swap', {oldIndex, newIndex, element}) |
||||||
|
// } |
||||||
|
// if (e.added) { |
||||||
|
// const {newIndex, element} = e.added |
||||||
|
// emit('add', {newIndex, element}) |
||||||
|
// } |
||||||
|
// } |
||||||
|
|
||||||
|
interface DraggableEvent { |
||||||
|
oldDraggableIndex: number |
||||||
|
newDraggableIndex: number |
||||||
|
} |
||||||
|
|
||||||
|
function handleDragStart(e: DraggableEvent): void { |
||||||
|
ctx.draggingViewId = ctx.blocks[e.oldDraggableIndex].vid |
||||||
|
} |
||||||
|
|
||||||
|
function handleDragEnd(): void { |
||||||
|
ctx.draggingViewId = undefined |
||||||
|
} |
||||||
|
|
||||||
|
function handleDragAdd(e: DraggableEvent): void { |
||||||
|
const block = ctx.blocks[e.newDraggableIndex] |
||||||
|
ctx.activeViewId = block.vid |
||||||
|
ctx.activeBlockId = block.vid |
||||||
|
ctx.hoverViewId = '#canvas' |
||||||
|
ctx.configurator = 'block' |
||||||
|
key.value++ |
||||||
|
} |
||||||
|
|
||||||
|
function handleCanvasClick(e: MouseEvent): void { |
||||||
|
if ((e.target as HTMLElement).classList.contains('ddd-canvas')) { |
||||||
|
ctx.canvas.focused = true |
||||||
|
ctx.activeViewId = undefined |
||||||
|
ctx.activeBlockId = undefined |
||||||
|
ctx.configurator = 'page' |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div |
||||||
|
class="ddd-canvas" |
||||||
|
:class="{ |
||||||
|
'is-focused': ctx.canvas.focused, |
||||||
|
'is-active': ctx.activeViewId === '#canvas', |
||||||
|
'is-hover': ctx.hoverViewId === '#canvas', |
||||||
|
}" |
||||||
|
> |
||||||
|
<!-- 中间自定义内容 --> |
||||||
|
<!-- @change="handleDraggingOver" --> |
||||||
|
<Draggable |
||||||
|
:key="key" |
||||||
|
v-model="ctx.blocks" |
||||||
|
class="ddd-canvas-viewer" |
||||||
|
:animation="200" |
||||||
|
:disabled="false" |
||||||
|
:component-data="{ |
||||||
|
tag: 'div', |
||||||
|
type: 'transition-group', |
||||||
|
}" |
||||||
|
:group="{ |
||||||
|
name: 'emulator', |
||||||
|
put: 'modules', |
||||||
|
}" |
||||||
|
ghost-class="is-ghost" |
||||||
|
item-key="id" |
||||||
|
@click="handleCanvasClick" |
||||||
|
@start="handleDragStart" |
||||||
|
@end="handleDragEnd" |
||||||
|
@add="handleDragAdd" |
||||||
|
@change="key++" |
||||||
|
> |
||||||
|
<template #header> |
||||||
|
<div v-show="ctx.page.safeAreaInsetTop && !ctx.page.fullscreen" class="ddd-canvas-safe-area-inset-top" /> |
||||||
|
<div v-show="ctx.page.header.enabled" class="ddd-canvas-safe-area-inset-header" /> |
||||||
|
</template> |
||||||
|
|
||||||
|
<template #footer> |
||||||
|
<div v-show="ctx.page.footer.enabled" class="ddd-canvas-safe-area-inset-footer" /> |
||||||
|
<div v-show="ctx.page.safeAreaInsetBottom && !ctx.page.fullscreen" class="ddd-canvas-safe-area-inset-bottom" /> |
||||||
|
</template> |
||||||
|
|
||||||
|
<template #item="{ element }"> |
||||||
|
<RenderBlock class="ddd-view" :block="element" /> |
||||||
|
</template> |
||||||
|
</Draggable> |
||||||
|
|
||||||
|
<!-- 页面头部 --> |
||||||
|
<DddView |
||||||
|
v-show="ctx.page.header.enabled" |
||||||
|
vid="#header" |
||||||
|
class="ddd-canvas-header" |
||||||
|
:class="{ 'is-bordered': ctx.page.header.bordered ?? true }" |
||||||
|
> |
||||||
|
<div v-show="ctx.page.safeAreaInsetTop && !ctx.page.fullscreen" class="ddd-canvas-safe-area-inset-top" /> |
||||||
|
<div |
||||||
|
class="ddd-canvas-header-title" |
||||||
|
:style="{ textAlign: ctx.page.header.centerTitle ? 'center' : undefined }" |
||||||
|
v-text="ctx.page.header.title" |
||||||
|
/> |
||||||
|
</DddView> |
||||||
|
|
||||||
|
<!-- 页面底部 --> |
||||||
|
<DddView |
||||||
|
v-show="ctx.page.footer.enabled" |
||||||
|
vid="#footer" |
||||||
|
class="ddd-canvas-footer" |
||||||
|
:class="{ 'is-bordered': ctx.page.footer.bordered ?? true }" |
||||||
|
> |
||||||
|
<div class="ddd-canvas-footer-content" /> |
||||||
|
<div v-show="ctx.page.safeAreaInsetBottom && !ctx.page.fullscreen" class="ddd-canvas-safe-area-inset-bottom" /> |
||||||
|
</DddView> |
||||||
|
|
||||||
|
<!-- 顶部状态栏、小程序胶囊按钮、底部操作指示器 --> |
||||||
|
<template v-if="!ctx.page.fullscreen"> |
||||||
|
<div class="ddd-canvas-statusbar"> |
||||||
|
<img style="height:20px" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAXcAAAAUCAYAAAB2132+AAAAAXNSR0IArs4c6QAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAABd6ADAAQAAAABAAAAFAAAAABeOGisAAAGY0lEQVR4Ae2ZCWwVVRSGX6VaRItogShW8xCsilZwQ8QqQRQjilaLEhG1uEWIS9QgrmlFUYILwaIxRuKCCrhgVcANbIjGqMiSWGrSsFTEJSgYLUtww+9v58ab6fS9tjxbXt75k6/33jMz9837e++ZM20sZjIHzAFzwBwwB8wBc8AcMAfMAXPAHDAHzAFzwBwwB8wBc8AcMAfMgVQ4kJWKSWwOc8AcMAfMgUgHehItiDzS9mAtl25KdvleyU5o5fFizt8VoH5z6sWB+2Ax/A7rgnYJbRkcCiZzIAcLepgN5sBuODCBa68OXa9kOwMWwUTw8+DZjGfD63AxOB1C5yYY5AJBeyttbijmhoV0Stwgha3m1NwJ5X+phCe28OAA7zy/74VjMxlUw4MwDPQw6Az/wFlQDt/A02DKTAey+dpPwTZQhbIGToJE0lvoAqj3TtL6vgF2wGgvHtWdS1Br8cLQQRePWs9jgmtUkJj2LAdO5HbK4VGIg9P+dN6BTnAvnAFTQeoPr8AyeBymwQXQBRaC1uLtMAokFaH54K85xZ2U314FrWf1U0H3YM4i2v9Vq5hdG2Jl8CnlwVgx9X3dxWAzaMP+DFUgcy+D00Gb735QXCbKsC2gX4IpsxyYxNfdCefBwfAwbARV8s1pIgf0Fug2mtbNBlgNWo+JkntXjm+HtaCKzZdL7s/6waD/Ca3mLos4ZqGOdUB5ZTrUgP/7KWH8OTgNoaO8pEKgAlyip9twXSWtis4nFUADYVZDLxZ7glbrszkp50laI6ni14YZYzE3dzBs2ugL7Y70pJOiqprGI//93EpX1ZUqdG20obACjoQR0AdkuuKXg879O4DGlEEOnMl3rYL34CfQJlWVNBwOhCWgdeI0mM7N8JALBO0dtC2pcFSJ7QBVZSOhG/jShlKVfoAXLKSvjb7di1l3z3HgNW7lNvgydEt9GasoddID/SBQRRx1rID4pzAMxsMj8BYoXymnaX22p8JrUw+rSLUmuRczg3v6qN9azeQCGZUL6+FreAm0Ie+BKTAHlPD1Gq7Xp6PAlHkO6LW4H+gBL53a2MQOp9XfPlVJnRLE8mi1bq4DvRE6qTBQFa41m0xXcsJ8WARK1peCLyUDrdmrvKA2eiXUezHr7vkOxLlFveE5fU9Ha2Q/iMNv4PQdHcX/gHOhB0yGBaAHh4oO5SkVp/tAR0hFzqCoD06U3JXAy8Al8gHeBH7fCyft/hKc8QHtcSBT9eRUZaRWVf0J8CFIWxob+5lhDrzJ9z0MPgJtJhUG2mDZUANxeAyU/F8EJWa3Zui2SnpgDIF58CdUgpJ9WLMIKKFLKlDGgmKm9HJAD28VBE770tHvXYlcx7qDkxK7Ck1pI2gtLgXlLj0UtA6+glGwHDpDR0hvHk2UKLnr1aMc1KZaquC1KdX2hf5BewztC2AVOyZksPRWVwpd4RyYBFqrP4D0LagQuBZUDLwM2nD5oPPU16Ztia7gpCy4CKaD5iiCOPiazaAPDAVdo8JjMZjSywGtnbh3y+rXwl8QdUzFRFi3EKgAFQEPwDXwBoyE9pb+nPhF1IcmSu5R56cyVspk2jDayKrkq0GvPePAlNkOHM3X10YbCKeB/q6ZDSvAV28GvUDVk9bRZOgS9FUwtETaoGtABcWxwQWq4MYGfddspqOqfgKMh+dBDxhTejnwLrc7GFRYaq3oH5OfgfQ26MGdA/lwPbhjdBukNbkatkIdHA97Qz+og/bWVD5Qa7OJOjK562ZcxaWKXfyooCnjHcjDgY9hBOg1+W6ogrXQE5TIVSVNAR13qKLaFoyV8JPpZE7QursRhnvMo6+kH9ZzBEpAbwZK7qb0c2A9t3wnLAetp8JgTNNQbK6k3QA1sArmgC+tlWeCwHxaFaTVsA6WQZSWRgXbGAvPFZnYk82tL7YL1ErFoLFQX2rJOY1nNv9TDxgZbTIHfAcqGGjhar0pUatCl+KwCUohrHEE6kPBbow1x+hQXMMZoLeCThp4UqLXNfpH7lzQg0bSWq2D98FJ15e5gbVp40Aud3pEM3ertaYiIiytk97hYILxGI4VJTje1kOa85JkF2clO8GOmwMd6IA2Ux4omZvMgXRzIIcbPh8KUnzjtcy3EHameF6bzhwwB8wBc8AcMAfMAXPAHDAHzAFzwBwwB8wBc8AcMAfMAXPAHDAHIhz4FwO/QgbhZ4FRAAAAAElFTkSuQmCC" /> |
||||||
|
</div> |
||||||
|
<div v-if="ctx.target === 'routine'" class="ddd-canvas-routine-button"> |
||||||
|
<svg width="87" height="32" viewBox="0 0 87 32" xmlns="http://www.w3.org/2000/svg"> |
||||||
|
<rect width="87" height="32" rx="16" x="0" y="0" fill="rgba(0,0,0,0.3)" /> |
||||||
|
<rect id="rect5" width="0.5" height="18.5" fill="rgba(255,255,255,0.6)" x="43.5" y="6.5" /> |
||||||
|
<path d="M 19.5,16.25 A 3.25,3.25 0 1 1 22.75,19.5 3.25,3.25 0 0 1 19.5,16.25 Z M 28,16.5 a 2,2 0 1 1 2,2 2,2 0 0 1 -2,-2 z m -14.5,0 a 2,2 0 1 1 2,2 2,2 0 0 1 -2,-2 z" fill="#ffffff" /> |
||||||
|
<path d="m 59.187,21.813 a 8.716,8.716 0 0 1 -2.52,-6.222 8,8 0 0 1 2.52,-5.8 8.915,8.915 0 0 1 6.223,-2.458 8.288,8.288 0 0 1 5.8,2.459 8.18,8.18 0 0 1 2.457,5.799 8.918,8.918 0 0 1 -2.46,6.223 8,8 0 0 1 -5.797,2.519 8.714,8.714 0 0 1 -6.223,-2.52 z m -0.52,-5.739 a 6.521,6.521 0 0 0 6.741,6.259 6.423,6.423 0 0 0 6.259,-6.259 6.521,6.521 0 0 0 -6.259,-6.741 6.593,6.593 0 0 0 -6.741,6.741 z m 3.5,-0.241 a 3,3 0 1 1 3,3 3,3 0 0 1 -3,-3 z" fill="#ffffff" /> |
||||||
|
</svg> |
||||||
|
</div> |
||||||
|
<div class="ddd-canvas-indicator" /> |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped lang="less"> |
||||||
|
.ddd-canvas { |
||||||
|
--canvas-measure-line: v-bind(measureLine); |
||||||
|
|
||||||
|
z-index: var(--emulator-z-index); |
||||||
|
position: absolute; |
||||||
|
top: 0; |
||||||
|
width: 375px; |
||||||
|
min-height: var(--canvas-min-height); |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
align-items: stretch; |
||||||
|
background-color: v-bind(backgroundColor); |
||||||
|
opacity: var(--emulator-opacity, 1); |
||||||
|
user-select: none; |
||||||
|
box-shadow: 0 2px 4px 1px rgba(40, 120, 255, 0.08), 0 0 6px 1px rgba(0, 0, 0, 0.08); |
||||||
|
|
||||||
|
&.is-focused, |
||||||
|
&.is-active { |
||||||
|
outline: var(--canvas-measure-line) solid var(--canvas-active-highlight-color); |
||||||
|
} |
||||||
|
|
||||||
|
&.is-hover { |
||||||
|
outline: var(--canvas-measure-line) solid var(--canvas-hover-highlight-color); |
||||||
|
} |
||||||
|
|
||||||
|
&-viewer { |
||||||
|
z-index: 1; |
||||||
|
position: relative; |
||||||
|
min-height: var(--canvas-min-height); |
||||||
|
//outline: 1px dashed red; |
||||||
|
} |
||||||
|
|
||||||
|
&-header, |
||||||
|
&-footer { |
||||||
|
position: absolute; |
||||||
|
left: 0; |
||||||
|
right: 0; |
||||||
|
overflow: hidden; |
||||||
|
|
||||||
|
&.is-bordered::after { |
||||||
|
z-index: 2; |
||||||
|
position: absolute; |
||||||
|
left: 0; |
||||||
|
right: 0; |
||||||
|
content: ""; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&-header { |
||||||
|
z-index: 2; |
||||||
|
top: 0; |
||||||
|
background-color: v-bind(headerColor); |
||||||
|
color: v-bind(headerTextColor); |
||||||
|
|
||||||
|
&.is-bordered::after { |
||||||
|
bottom: 0; |
||||||
|
border-bottom-style: solid; |
||||||
|
border-bottom-width: 1px; |
||||||
|
border-bottom-color: v-bind(headerBorderColor); |
||||||
|
} |
||||||
|
|
||||||
|
&-title { |
||||||
|
line-height: v-bind(headerHeight); |
||||||
|
height: v-bind(headerHeight); |
||||||
|
text-align: left; |
||||||
|
font-weight: bold; |
||||||
|
padding-left: 12px; |
||||||
|
font-size: 16px; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&-footer { |
||||||
|
z-index: 3; |
||||||
|
bottom: 0; |
||||||
|
background-color: v-bind(footerColor); |
||||||
|
color: v-bind(footerTextColor); |
||||||
|
|
||||||
|
&-content { |
||||||
|
height: v-bind(footerHeight); |
||||||
|
} |
||||||
|
|
||||||
|
&.is-bordered::after { |
||||||
|
top: 0; |
||||||
|
border-top-style: solid; |
||||||
|
border-top-width: 1px; |
||||||
|
border-top-color: v-bind(footerBorderColor); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&-statusbar, |
||||||
|
&-indicator, |
||||||
|
&-routine-button { |
||||||
|
z-index: 3; |
||||||
|
position: absolute; |
||||||
|
left: 0; |
||||||
|
right: 0; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
pointer-events: none; |
||||||
|
//outline: 1px dashed #0256FF; |
||||||
|
} |
||||||
|
|
||||||
|
&-statusbar { |
||||||
|
height: var(--canvas-safe-area-inset-top); |
||||||
|
padding: 0 12px; |
||||||
|
top: 0; |
||||||
|
|
||||||
|
img { |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&-routine-button { |
||||||
|
top: var(--canvas-safe-area-inset-top); |
||||||
|
height: 44px; |
||||||
|
width: 100%; |
||||||
|
align-items: flex-end; |
||||||
|
padding-right: 12px; |
||||||
|
} |
||||||
|
|
||||||
|
&-indicator { |
||||||
|
bottom: 0; |
||||||
|
height: var(--canvas-safe-area-inset-bottom); |
||||||
|
//outline: 1px dashed green; |
||||||
|
|
||||||
|
&::after { |
||||||
|
display: block; |
||||||
|
content: ""; |
||||||
|
width: 100px; |
||||||
|
height: 4px; |
||||||
|
margin: 0 auto; |
||||||
|
border-radius: 2px; |
||||||
|
background: #242424; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&-safe-area-inset-top { |
||||||
|
height: var(--canvas-safe-area-inset-top); |
||||||
|
} |
||||||
|
|
||||||
|
&-safe-area-inset-header { |
||||||
|
height: v-bind(headerHeight); |
||||||
|
} |
||||||
|
|
||||||
|
&-safe-area-inset-bottom { |
||||||
|
height: var(--canvas-safe-area-inset-bottom); |
||||||
|
} |
||||||
|
|
||||||
|
&-safe-area-inset-footer { |
||||||
|
height: v-bind(footerHeight); |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,257 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { ref, watch } from 'vue' |
||||||
|
import type { ConfiguratorType } from '../context' |
||||||
|
import { useContext, useModule } from '../context' |
||||||
|
|
||||||
|
defineOptions({ |
||||||
|
name: 'DddConfigurator', |
||||||
|
}) |
||||||
|
|
||||||
|
interface Tab { |
||||||
|
key: ConfiguratorType |
||||||
|
text: string |
||||||
|
} |
||||||
|
|
||||||
|
const tabs: Tab[] = [ |
||||||
|
{ key: 'page', text: '页面' }, |
||||||
|
{ key: 'block', text: '内容' }, |
||||||
|
{ key: 'view', text: '样式' }, |
||||||
|
] |
||||||
|
|
||||||
|
const currentTab = ref<Tab['key']>() |
||||||
|
const ctx = useContext() |
||||||
|
const module = useModule() |
||||||
|
|
||||||
|
watch(() => ctx.configurator, (v) => { |
||||||
|
currentTab.value = tabs.find(i => i.key === (v ?? 'page'))?.key ?? 'page' |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="ddd-configurator"> |
||||||
|
<div class="ddd-configurator-header"> |
||||||
|
<button |
||||||
|
v-for="(t) in tabs" |
||||||
|
:key="t.key" |
||||||
|
type="button" |
||||||
|
:class="{ 'is-active': currentTab === t.key }" |
||||||
|
@click="currentTab = t.key" |
||||||
|
v-text="t.text" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<div v-show="currentTab === 'page' " class="ddd-configurator-content"> |
||||||
|
<fieldset> |
||||||
|
<legend>应用</legend> |
||||||
|
<div> |
||||||
|
<label> |
||||||
|
<input v-model="ctx.target" type="radio" name="ctx.target" value="routine" /> |
||||||
|
<span>微信小程序</span> |
||||||
|
</label> |
||||||
|
<label> |
||||||
|
<input v-model="ctx.target" type="radio" name="ctx.target" value="app" /> |
||||||
|
<span>手机APP</span> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<label> |
||||||
|
<input v-model="ctx.page.fullscreen" type="checkbox" /> |
||||||
|
<span>全屏模式</span> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<label> |
||||||
|
<input v-model="ctx.page.safeAreaInsetTop" type="checkbox" /> |
||||||
|
<span>开启顶部安全区域</span> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<label> |
||||||
|
<input v-model="ctx.page.safeAreaInsetBottom" type="checkbox" /> |
||||||
|
<span>开启底部安全区域</span> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<label> |
||||||
|
<span>页面背景颜色</span> |
||||||
|
<input v-model="ctx.page.backgroundColor" type="color" /> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
</fieldset> |
||||||
|
|
||||||
|
<fieldset> |
||||||
|
<legend>头部</legend> |
||||||
|
<div> |
||||||
|
<label> |
||||||
|
<input v-model="ctx.page.header.enabled" type="checkbox" /> |
||||||
|
<span>开启头部功能</span> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<label> |
||||||
|
<span>页面标题</span> |
||||||
|
<input v-model="ctx.page.header.title" type="text" placeholder="页面标题" /> |
||||||
|
</label> |
||||||
|
<label> |
||||||
|
<input v-model="ctx.page.header.centerTitle" type="checkbox" /> |
||||||
|
<span>标题居中</span> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<label> |
||||||
|
<input v-model="ctx.page.header.custom" type="checkbox" /> |
||||||
|
<span>内容自定义</span> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<label> |
||||||
|
<span>高度</span> |
||||||
|
<input v-model="ctx.page.header.height" type="number" min="0" placeholder="头部高度,不包括状态栏" /> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<label> |
||||||
|
<span>背景颜色</span> |
||||||
|
<input v-model="ctx.page.header.color" type="color" /> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<label> |
||||||
|
<span>文字颜色</span> |
||||||
|
<input v-model="ctx.page.header.textColor" type="color" /> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<label> |
||||||
|
<input v-model="ctx.page.header.bordered" type="checkbox" /> |
||||||
|
<span>开启下边边框</span> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<label> |
||||||
|
<span>下边边框颜色</span> |
||||||
|
<input v-model="ctx.page.header.borderColor" type="color" /> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
</fieldset> |
||||||
|
|
||||||
|
<fieldset> |
||||||
|
<legend>底部</legend> |
||||||
|
<div> |
||||||
|
<label> |
||||||
|
<input v-model="ctx.page.footer.enabled" type="checkbox" /> |
||||||
|
<span>开启底部功能</span> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<label> |
||||||
|
<span>高度</span> |
||||||
|
<input v-model="ctx.page.footer.height" type="number" min="0" placeholder="头部高度,不包括状态栏" /> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<label> |
||||||
|
<span>背景颜色</span> |
||||||
|
<input v-model="ctx.page.footer.color" type="color" /> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<label> |
||||||
|
<span>文字颜色</span> |
||||||
|
<input v-model="ctx.page.footer.textColor" type="color" /> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<label> |
||||||
|
<input v-model="ctx.page.footer.bordered" type="checkbox" /> |
||||||
|
<span>开启上边框</span> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<label> |
||||||
|
<span>上边框颜色</span> |
||||||
|
<input v-model="ctx.page.footer.borderColor" type="color" /> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
</fieldset> |
||||||
|
</div> |
||||||
|
<div v-show="currentTab === 'block'" class="ddd-configurator-content"> |
||||||
|
<div> |
||||||
|
<pre><code>{{ module }}</code></pre> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div v-show="currentTab === 'view'" class="ddd-configurator-content"> |
||||||
|
<div> |
||||||
|
<pre><code>{{ ctx.blocks }}</code></pre> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="less" scoped> |
||||||
|
.ddd-configurator { |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
|
||||||
|
// fixme: 如果是不使用 fieldset 标签 |
||||||
|
// 记得删除这里的代码 |
||||||
|
fieldset { |
||||||
|
background: transparent; |
||||||
|
border-color: #eee; |
||||||
|
border-radius: 8px; |
||||||
|
} |
||||||
|
|
||||||
|
&-header { |
||||||
|
z-index: 2; |
||||||
|
position: sticky; |
||||||
|
display: flex; |
||||||
|
top: 0; |
||||||
|
width: 100%; |
||||||
|
height: 44px; |
||||||
|
line-height: 44px; |
||||||
|
padding: 0 12px; |
||||||
|
flex-shrink: 0; |
||||||
|
background: #fff; |
||||||
|
border-bottom: 1px solid #ddd; |
||||||
|
|
||||||
|
button { |
||||||
|
position: relative; |
||||||
|
height: 44px; |
||||||
|
padding: 0 16px; |
||||||
|
border: none; |
||||||
|
background: transparent; |
||||||
|
appearance: none; |
||||||
|
box-sizing: border-box; |
||||||
|
transition: all 200ms; |
||||||
|
|
||||||
|
&::after { |
||||||
|
position: absolute; |
||||||
|
content: ""; |
||||||
|
bottom: 0; |
||||||
|
left: 0; |
||||||
|
right: 0; |
||||||
|
height: 2px; |
||||||
|
transition: all 200ms; |
||||||
|
} |
||||||
|
|
||||||
|
&.is-active { |
||||||
|
background: rgba(#0256FF, 0.1); |
||||||
|
color: #0256FF; |
||||||
|
|
||||||
|
&::after { |
||||||
|
background-color: #0256FF; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&-content { |
||||||
|
position: relative; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
flex: 1; |
||||||
|
padding: 12px; |
||||||
|
overflow: auto; |
||||||
|
//background-color: #eee; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,101 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { ref } from 'vue' |
||||||
|
import { useContext, useScale } from '../context' |
||||||
|
import { DddView } from '../views' |
||||||
|
import DddCanvas from './Canvas.vue' |
||||||
|
|
||||||
|
defineOptions({ |
||||||
|
name: 'DddDesigner', |
||||||
|
}) |
||||||
|
|
||||||
|
const ctx = useContext() |
||||||
|
const scale = useScale() |
||||||
|
|
||||||
|
let pageX = 0 |
||||||
|
let pageY = 0 |
||||||
|
|
||||||
|
const canMove = ref(false) |
||||||
|
const x = ref((window.innerWidth - 375 - 324) / 2 + 324) |
||||||
|
const y = ref(24) |
||||||
|
|
||||||
|
function onMousedown(e: MouseEvent): void { |
||||||
|
canMove.value = true |
||||||
|
pageX = e.pageX - x.value |
||||||
|
pageY = e.pageY - y.value |
||||||
|
|
||||||
|
ctx.activeBlockId = undefined |
||||||
|
ctx.activeViewId = undefined |
||||||
|
ctx.configurator = undefined |
||||||
|
ctx.canvas.focused = true |
||||||
|
} |
||||||
|
|
||||||
|
function onMousemove(e: MouseEvent): void { |
||||||
|
if (canMove.value) { |
||||||
|
x.value = e.pageX - pageX! |
||||||
|
y.value = e.pageY - pageY! |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function onMouseup(): void { |
||||||
|
canMove.value = false |
||||||
|
} |
||||||
|
|
||||||
|
function onMousewheel(e: WheelEvent): void { |
||||||
|
if (e.ctrlKey || e.metaKey) { |
||||||
|
if (e.deltaY < 0) |
||||||
|
scale.incr() |
||||||
|
else |
||||||
|
scale.decr() |
||||||
|
} |
||||||
|
else { |
||||||
|
x.value -= e.deltaX |
||||||
|
y.value -= e.deltaY |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TODO(hupeh): 不使用 Vue 更新相关鼠标移入移出的效果,防止频繁更新视图 |
||||||
|
function onMouseover(e: MouseEvent): void { |
||||||
|
if (!ctx.draggingViewId) { |
||||||
|
let el = e.target as HTMLElement | null | undefined |
||||||
|
while (el) { |
||||||
|
const widgetId = (el as HTMLElement)?.dataset?.viewId |
||||||
|
if (widgetId && widgetId !== ctx.activeViewId) { |
||||||
|
ctx.hoverViewId = widgetId |
||||||
|
return |
||||||
|
} |
||||||
|
el = el?.parentNode as HTMLElement |
||||||
|
} |
||||||
|
ctx.hoverViewId = '#canvas' |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div |
||||||
|
class="ddd-designer" |
||||||
|
@mousedown.left="onMousedown" |
||||||
|
@mousemove="onMousemove" |
||||||
|
@mouseup="onMouseup" |
||||||
|
@mouseleave="onMouseup" |
||||||
|
@wheel.passive="onMousewheel" |
||||||
|
> |
||||||
|
<DddView |
||||||
|
:is="DddCanvas" |
||||||
|
:style="`transform: translate(${x}px,${y}px) scale(${ctx.canvas.scale})`" |
||||||
|
vid="#canvas" |
||||||
|
@mouseover="onMouseover" |
||||||
|
@mousedown.stop |
||||||
|
@mousemove.stop |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="less" scoped> |
||||||
|
.ddd-designer { |
||||||
|
z-index: 1; |
||||||
|
position: relative; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
background-color: #eee; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,126 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { useContext, useScale } from '../context' |
||||||
|
import Activity from './Activity.vue' |
||||||
|
import Configurator from './Configurator.vue' |
||||||
|
import Designer from './Designer.vue' |
||||||
|
|
||||||
|
defineOptions({ |
||||||
|
name: 'DddEngineer', |
||||||
|
}) |
||||||
|
|
||||||
|
const ctx = useContext() |
||||||
|
const scale = useScale() |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="ddd-engineer"> |
||||||
|
<div class="ddd-engineer-header"> |
||||||
|
<div> |
||||||
|
<div /> |
||||||
|
</div> |
||||||
|
<div style="display: flex;align-items: center"> |
||||||
|
<button |
||||||
|
type="button" |
||||||
|
:disabled="ctx.canvas.scale <= scale.min" |
||||||
|
@click="scale.decr()" |
||||||
|
v-text="'-'" |
||||||
|
/> |
||||||
|
<span |
||||||
|
style="display: inline-block;width: 5em;text-align: center" |
||||||
|
>{{ Math.ceil(ctx.canvas.scale * 100) }}%</span> |
||||||
|
<button |
||||||
|
type="button" |
||||||
|
:disabled="ctx.canvas.scale >= scale.max" |
||||||
|
@click="scale.incr()" |
||||||
|
v-text="'+'" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<button type="button" @click="ctx.configurator = 'page'"> |
||||||
|
页面配置 |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="ddd-engineer-activity"> |
||||||
|
<Activity /> |
||||||
|
</div> |
||||||
|
<transition name="ddd-engineer-configurator"> |
||||||
|
<div v-show="ctx.configurator" class="ddd-engineer-configurator"> |
||||||
|
<Configurator /> |
||||||
|
</div> |
||||||
|
</transition> |
||||||
|
<Designer class="ddd-engineer-body" /> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped lang="less"> |
||||||
|
.ddd-engineer { |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
|
||||||
|
&-header { |
||||||
|
z-index: 2; |
||||||
|
position: relative; |
||||||
|
width: 100%; |
||||||
|
height: 44px; |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
background: #fff; |
||||||
|
padding: 0 12px; |
||||||
|
box-sizing: border-box; |
||||||
|
|
||||||
|
&::after { |
||||||
|
z-index: 10; |
||||||
|
position: absolute; |
||||||
|
left: 0; |
||||||
|
right: 0; |
||||||
|
bottom: 0; |
||||||
|
content: ""; |
||||||
|
border-bottom: 1px solid #ddd; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&-body { |
||||||
|
z-index: 1; |
||||||
|
position: relative; |
||||||
|
width: 100%; |
||||||
|
height: calc(100% - 44px); |
||||||
|
background: #eee; |
||||||
|
} |
||||||
|
|
||||||
|
&-activity { |
||||||
|
z-index: 3; |
||||||
|
position: absolute; |
||||||
|
left: 0; |
||||||
|
bottom: 0; |
||||||
|
top: 44px; |
||||||
|
width: 324px; |
||||||
|
background: #fff; |
||||||
|
} |
||||||
|
|
||||||
|
&-configurator { |
||||||
|
z-index: 3; |
||||||
|
position: absolute; |
||||||
|
right: 0; |
||||||
|
bottom: 0; |
||||||
|
top: 44px; |
||||||
|
width: 324px; |
||||||
|
background-color: #fff; |
||||||
|
opacity: 1; |
||||||
|
overflow: hidden; |
||||||
|
|
||||||
|
&-enter-from, |
||||||
|
&-leave-to { |
||||||
|
opacity: 0; |
||||||
|
transform: translateX(100%); |
||||||
|
box-shadow: none; |
||||||
|
} |
||||||
|
|
||||||
|
&-enter-active, |
||||||
|
&-leave-active { |
||||||
|
transition: opacity 200ms ease-out, transform 200ms ease-out; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,138 @@ |
|||||||
|
import type { ComputedRef, InjectionKey, UnwrapNestedRefs } from 'vue' |
||||||
|
import { computed, inject, provide } from 'vue' |
||||||
|
import type { Block, Category, Module, Page } from './types' |
||||||
|
import { valueOf } from './utils' |
||||||
|
|
||||||
|
/** 画布配置 */ |
||||||
|
export interface CanvasConfig { |
||||||
|
/** 画布是否聚焦 */ |
||||||
|
focused: boolean |
||||||
|
/** 画布缩放比例 */ |
||||||
|
scale: number |
||||||
|
} |
||||||
|
|
||||||
|
export interface PageConfig extends Omit<Page, 'body'> {} |
||||||
|
|
||||||
|
export type ConfiguratorType = |
||||||
|
| 'page' |
||||||
|
| 'block' |
||||||
|
| 'view' |
||||||
|
|
||||||
|
export interface Exported { |
||||||
|
attrs: PageConfig |
||||||
|
data: Record<string, Record<string, unknown>> |
||||||
|
} |
||||||
|
|
||||||
|
export interface EngineContext { |
||||||
|
/** 目标 */ |
||||||
|
target?: 'app' | 'routine' |
||||||
|
/** 当前选中的模板 */ |
||||||
|
activeBlockId?: string |
||||||
|
/** 当前选中的可编辑组件 */ |
||||||
|
activeViewId?: string |
||||||
|
/** 鼠标进入的可编辑组件 */ |
||||||
|
hoverViewId?: string |
||||||
|
/** 正在被拖拽的模板 */ |
||||||
|
draggingViewId?: string |
||||||
|
/** 当前配置 */ |
||||||
|
configurator?: ConfiguratorType |
||||||
|
/** 页面配置 */ |
||||||
|
page: PageConfig |
||||||
|
/** 画布配置 */ |
||||||
|
canvas: CanvasConfig |
||||||
|
/** 可以分类 */ |
||||||
|
categories: Category[] |
||||||
|
/** 布局视图列表 */ |
||||||
|
blocks: Block[] |
||||||
|
/** 内部唯一标识 */ |
||||||
|
nextId: number |
||||||
|
/** 数据源 */ |
||||||
|
sources: Record<string, Record<string, unknown>> |
||||||
|
/** 到处数据 */ |
||||||
|
export: () => Exported |
||||||
|
} |
||||||
|
|
||||||
|
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 function useContext(): UnwrapNestedRefs<EngineContext> { |
||||||
|
const config = inject(contextKey) |
||||||
|
if (config == null) |
||||||
|
throw new Error('no config found') |
||||||
|
|
||||||
|
return config |
||||||
|
} |
||||||
|
|
||||||
|
export function useParentViewId(): string | undefined { |
||||||
|
return inject(parentViewIdKey) |
||||||
|
} |
||||||
|
|
||||||
|
export function provideParentViewId(id: string | undefined): void { |
||||||
|
if (id) |
||||||
|
provide(parentViewIdKey, id) |
||||||
|
} |
||||||
|
|
||||||
|
export function useBlockId(): string | undefined { |
||||||
|
return inject(blockIdKey) |
||||||
|
} |
||||||
|
|
||||||
|
export function provideBlockId(id: string): void { |
||||||
|
provide(blockIdKey, id) |
||||||
|
} |
||||||
|
|
||||||
|
export function useModule() { |
||||||
|
const ctx = useContext() |
||||||
|
return computed((): Module | undefined => { |
||||||
|
for (const block of ctx.blocks) { |
||||||
|
if (block.vid !== ctx.activeBlockId) |
||||||
|
continue |
||||||
|
|
||||||
|
for (const category of ctx.categories) { |
||||||
|
for (const module of category.modules) { |
||||||
|
if (module.mid === block.mid) |
||||||
|
return module |
||||||
|
} |
||||||
|
} |
||||||
|
break |
||||||
|
} |
||||||
|
return undefined |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export function useScale() { |
||||||
|
const ctx = useContext() |
||||||
|
|
||||||
|
const minScaleValue = 0.6 |
||||||
|
const maxScaleValue = 3.0 |
||||||
|
const scaleStep = 0.2 |
||||||
|
const setScale = (delta: number): void => { |
||||||
|
ctx.canvas.scale = Math.max( |
||||||
|
Math.min( |
||||||
|
ctx.canvas.scale + scaleStep * delta, |
||||||
|
maxScaleValue, |
||||||
|
), |
||||||
|
minScaleValue, |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
min: minScaleValue, |
||||||
|
max: maxScaleValue, |
||||||
|
step: scaleStep, |
||||||
|
incr: () => setScale(1), |
||||||
|
decr: () => setScale(-1), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function useSource<T = unknown>(source: string | undefined, fallback?: T): ComputedRef<T | undefined> { |
||||||
|
const ctx = useContext() |
||||||
|
const blockId = useBlockId() |
||||||
|
return computed<T | undefined>(() => { |
||||||
|
if (!blockId) |
||||||
|
throw new Error('without block') |
||||||
|
if (!source) |
||||||
|
return fallback |
||||||
|
return valueOf(ctx.sources[blockId], source) ?? fallback |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
import DddBuilder from './components/Builder.vue' |
||||||
|
|
||||||
|
export type { WidgetRenderFunction } from './render' |
||||||
|
export type { |
||||||
|
CanvasConfig, |
||||||
|
PageConfig, |
||||||
|
ConfiguratorType, |
||||||
|
EngineContext, |
||||||
|
Exported, |
||||||
|
} from './context' |
||||||
|
|
||||||
|
export { registerWidget, render } from './render' |
||||||
|
export { |
||||||
|
useContext, |
||||||
|
useBlockId, |
||||||
|
useSource, |
||||||
|
useModule, |
||||||
|
useParentViewId, |
||||||
|
} from './context' |
||||||
|
export { hash, valueOf } from './utils' |
||||||
|
export * from './types' |
||||||
|
|
||||||
|
export default DddBuilder |
@ -0,0 +1,175 @@ |
|||||||
|
import type { |
||||||
|
CSSProperties, |
||||||
|
Component, |
||||||
|
Prop, |
||||||
|
VNodeChild, |
||||||
|
} from 'vue' |
||||||
|
import { defineComponent, h } from 'vue' |
||||||
|
import type { |
||||||
|
Axis, |
||||||
|
Block, |
||||||
|
DataRefer, |
||||||
|
View, |
||||||
|
ViewChildren, |
||||||
|
Widget, |
||||||
|
} from './types' |
||||||
|
import { provideBlockId } from './context' |
||||||
|
import { |
||||||
|
align, |
||||||
|
background, |
||||||
|
bordering, |
||||||
|
gap, |
||||||
|
insets, |
||||||
|
radii, |
||||||
|
shadowing, |
||||||
|
size, |
||||||
|
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> = {} |
||||||
|
|
||||||
|
export function registerWidget(some: Record<string, WidgetRenderFunction>): void { |
||||||
|
for (const [key, func] of Object.entries(some)) |
||||||
|
widgets[key] = func |
||||||
|
} |
||||||
|
|
||||||
|
const isWidget = (s: any): s is Widget => s != null && 'name' in s |
||||||
|
|
||||||
|
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 render(view?: ViewChildren, axis?: Axis): VNodeChild { |
||||||
|
if (view == null) |
||||||
|
return null |
||||||
|
|
||||||
|
if (typeof view === 'string' || typeof view === 'number') |
||||||
|
return view |
||||||
|
|
||||||
|
if (Array.isArray(view)) |
||||||
|
return view.map(v => render(v, axis)) |
||||||
|
|
||||||
|
if (isRefer(view)) { |
||||||
|
const { key, ...attrs } = view |
||||||
|
let component: Component | undefined |
||||||
|
switch (view.type) { |
||||||
|
case 'text': |
||||||
|
component = DddTextView |
||||||
|
return h(DddTextView, { |
||||||
|
source: view.key, |
||||||
|
}) |
||||||
|
case 'image': |
||||||
|
component = DddImageView |
||||||
|
return h(DddImageView, { |
||||||
|
source: key, |
||||||
|
...attrs, |
||||||
|
}) |
||||||
|
case 'audio': |
||||||
|
component = DddAudioView |
||||||
|
return h(DddAudioView, { |
||||||
|
source: view.key, |
||||||
|
title: view.title, |
||||||
|
}) |
||||||
|
case 'video': |
||||||
|
// todo ...
|
||||||
|
} |
||||||
|
if (component) { |
||||||
|
return h(component, { |
||||||
|
source: key, |
||||||
|
...attrs, |
||||||
|
}) |
||||||
|
} |
||||||
|
return h('b', { |
||||||
|
style: { |
||||||
|
color: 'red', |
||||||
|
fontSize: '36px', |
||||||
|
}, |
||||||
|
}, 'unimplemented') |
||||||
|
} |
||||||
|
if (isWidget(view.theme)) { |
||||||
|
const func = widgets[view.theme.name] |
||||||
|
if (typeof func !== 'function') |
||||||
|
throw new Error(`unknown widget: ${view.theme.name}`) |
||||||
|
|
||||||
|
const widget = view.theme |
||||||
|
return func(widget, view, axis) |
||||||
|
} |
||||||
|
|
||||||
|
const theme = view.theme |
||||||
|
const css: CSSProperties = { |
||||||
|
// boxed & spatial
|
||||||
|
...insets('padding', theme?.padding), |
||||||
|
...insets('margin', theme?.margin), |
||||||
|
...size(axis, theme), |
||||||
|
// decoration
|
||||||
|
...background(theme?.color, theme?.image, theme?.gradient), |
||||||
|
...bordering(theme?.border), |
||||||
|
...radii(theme?.radius), |
||||||
|
...shadowing(theme?.shadow), |
||||||
|
// textual
|
||||||
|
color: theme?.textColor, |
||||||
|
fontSize: unit(theme?.fontSize), |
||||||
|
textAlign: theme?.textAlign, |
||||||
|
lineHeight: theme?.lineHeight, |
||||||
|
// clip
|
||||||
|
overflow: theme?.clip, |
||||||
|
} |
||||||
|
|
||||||
|
// note: 在前面初始化 css 时使用了参数 axis,
|
||||||
|
// 所以在这里复用它用于子视图构建
|
||||||
|
axis = theme?.axis |
||||||
|
|
||||||
|
if (theme?.flexible) { |
||||||
|
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), |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
return h(DddView, { |
||||||
|
vid: view.vid, |
||||||
|
class: 'ddd-view', |
||||||
|
style: css, |
||||||
|
}, () => render(view.children, axis)) |
||||||
|
} |
||||||
|
|
||||||
|
export const RenderBlock = defineComponent({ |
||||||
|
name: 'RenderWidget', |
||||||
|
|
||||||
|
props: { |
||||||
|
block: { |
||||||
|
type: Object, |
||||||
|
required: true, |
||||||
|
} as Prop<Block>, |
||||||
|
}, |
||||||
|
|
||||||
|
setup(props) { |
||||||
|
provideBlockId(props.block!.vid) |
||||||
|
return () => { |
||||||
|
if (props.block == null) |
||||||
|
return null |
||||||
|
|
||||||
|
return render(props.block) |
||||||
|
} |
||||||
|
}, |
||||||
|
}) |
@ -0,0 +1,558 @@ |
|||||||
|
import type { Slots } from 'vue' |
||||||
|
|
||||||
|
/** 百分比 */ |
||||||
|
export type Percentage = `${number}%` |
||||||
|
|
||||||
|
/** 计量原语 */ |
||||||
|
export type Unit = |
||||||
|
| number // 根据情况,大多数情况下被作为逻辑像素使用
|
||||||
|
| Percentage |
||||||
|
|
||||||
|
/** 对称策略 */ |
||||||
|
export interface Symmetric<T> { |
||||||
|
vertical?: T |
||||||
|
horizontal?: T |
||||||
|
} |
||||||
|
|
||||||
|
/** 四边策略 */ |
||||||
|
export interface Direction<T> { |
||||||
|
left?: T |
||||||
|
right?: T |
||||||
|
top?: T |
||||||
|
bottom?: T |
||||||
|
} |
||||||
|
|
||||||
|
/** 四角策略 */ |
||||||
|
export interface Corners<T> { |
||||||
|
topLeft?: T // 左上角
|
||||||
|
topRight?: T // 右上角
|
||||||
|
bottomLeft?: T // 左下角
|
||||||
|
bottomRight?: T // 右下角
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 线性渐变 |
||||||
|
* |
||||||
|
* 不支持 sweep 和 radial 风格。 |
||||||
|
* |
||||||
|
* 属性 stops 要么不设置,要么其数量必须与 colors 的数量一致。 |
||||||
|
*/ |
||||||
|
export interface Gradient { |
||||||
|
/** 渐变角度 */ |
||||||
|
angle?: string |
||||||
|
/** 颜色组,至少 2 个颜色值 */ |
||||||
|
colors: string[] |
||||||
|
/** 颜色停顿位置组 */ |
||||||
|
stops?: number[] |
||||||
|
/** 是否重复渲染 */ |
||||||
|
repeatable?: boolean |
||||||
|
} |
||||||
|
|
||||||
|
/** 边距,用于内外边距 */ |
||||||
|
export type EdgeInsets = |
||||||
|
| Direction<Unit> // 四边
|
||||||
|
| Symmetric<Unit> // 对称
|
||||||
|
| Unit // 原语
|
||||||
|
|
||||||
|
/** 边框样式 */ |
||||||
|
export type BorderStyle = |
||||||
|
| 'solid' |
||||||
|
| 'dashed' |
||||||
|
| 'dotted' |
||||||
|
| 'double' |
||||||
|
| 'groove' |
||||||
|
| 'ridge' |
||||||
|
| 'outset' |
||||||
|
| 'inset' |
||||||
|
| 'none' |
||||||
|
|
||||||
|
/** 边框的边 */ |
||||||
|
export interface BorderSide { |
||||||
|
color?: string |
||||||
|
style?: BorderStyle |
||||||
|
width?: number |
||||||
|
} |
||||||
|
|
||||||
|
/** 边框 */ |
||||||
|
export type Border = |
||||||
|
| Direction<BorderSide> // 四边
|
||||||
|
| Symmetric<BorderSide> // 对称
|
||||||
|
| BorderSide // 统一设置
|
||||||
|
|
||||||
|
/** 圆角原语 */ |
||||||
|
export type RadiusPrimitive = |
||||||
|
| number |
||||||
|
| [number, number] |
||||||
|
|
||||||
|
/** 圆角双值 */ |
||||||
|
export type RadiusDouble = [ |
||||||
|
RadiusPrimitive, |
||||||
|
RadiusPrimitive, |
||||||
|
] |
||||||
|
|
||||||
|
/** 圆角 */ |
||||||
|
export type Radius = |
||||||
|
| Corners<RadiusPrimitive | RadiusDouble> |
||||||
|
| RadiusPrimitive |
||||||
|
| RadiusDouble |
||||||
|
|
||||||
|
/** 阴影 */ |
||||||
|
export interface Shadow { |
||||||
|
x: number |
||||||
|
y: number |
||||||
|
blur?: number |
||||||
|
spread?: number |
||||||
|
color: string |
||||||
|
inset?: boolean |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 主轴对齐方式(子元素布局方向) |
||||||
|
* |
||||||
|
* 用于 Flex 布局,对应 CSS 属性 justify-content 的值。 |
||||||
|
*/ |
||||||
|
export type MainAlign = |
||||||
|
| 'start' // 左对齐,默认值
|
||||||
|
| 'end' // 右对齐
|
||||||
|
| 'center' // 居中
|
||||||
|
| 'between' // 两端对齐,项目之间的间隔都相等。
|
||||||
|
| 'around' // 每个项目两侧的间隔相等。所以,项目之间的间隔比项目与边框的间隔大一倍。
|
||||||
|
|
||||||
|
/** |
||||||
|
* 交叉轴对齐方式(与主轴垂直的方向) |
||||||
|
* |
||||||
|
* 用于 Flex 布局,对应 CSS 属性 align-items 的值。 |
||||||
|
*/ |
||||||
|
export type CrossAlign = |
||||||
|
| 'start' // 交叉轴的起点对齐。
|
||||||
|
| 'end' // 交叉轴的终点对齐。
|
||||||
|
| 'center' // 交叉轴的中点对齐。
|
||||||
|
| 'baseline' // 项目的第一行文字的基线对齐。
|
||||||
|
| 'stretch' // 默认值):如果项目未设置高度或设为auto,将占满整个容器的高度。
|
||||||
|
|
||||||
|
/** 文本方向 */ |
||||||
|
export type TextAlign = |
||||||
|
| 'left' |
||||||
|
| 'center' |
||||||
|
| 'right' |
||||||
|
|
||||||
|
/** |
||||||
|
* 描述主轴方向 |
||||||
|
* |
||||||
|
* x - 横轴 |
||||||
|
* y - 纵轴 |
||||||
|
*/ |
||||||
|
export type Axis = 'x' | 'y' |
||||||
|
|
||||||
|
/** |
||||||
|
* 文本 |
||||||
|
* |
||||||
|
* 描述与文本外观相关的属性 |
||||||
|
*/ |
||||||
|
export interface Textual { |
||||||
|
/** 文本颜色 */ |
||||||
|
textColor?: string |
||||||
|
/** 字体大小 */ |
||||||
|
fontSize?: number |
||||||
|
/** 文本方向 */ |
||||||
|
textAlign?: TextAlign |
||||||
|
/** 行高,行高比较特殊,需要转换单位 */ |
||||||
|
lineHeight?: number | Percentage | string |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 弹性布局策略 |
||||||
|
* |
||||||
|
* 描述与弹性布局相关的属性 |
||||||
|
*/ |
||||||
|
export interface Flexible { |
||||||
|
/** |
||||||
|
* 主轴方向,对应 CSS 属性 flex-direction。 |
||||||
|
* 如果值为 'x' 表示 `row`;值为 'y' 表示 `column`。 |
||||||
|
*/ |
||||||
|
axis?: Axis |
||||||
|
/** 主轴对齐方式 */ |
||||||
|
mainAlign?: MainAlign |
||||||
|
/** 交叉轴对齐方式 */ |
||||||
|
crossAlign?: CrossAlign |
||||||
|
/** 是否自动换行 */ |
||||||
|
wrap?: boolean |
||||||
|
/** |
||||||
|
* 子组件间隔 |
||||||
|
* |
||||||
|
* 当值未数组时: |
||||||
|
* |
||||||
|
* - 第一个值表示水平方向上子视图的间隔; |
||||||
|
* - 第二个值表示垂直方向上的子视图间隔。 |
||||||
|
*/ |
||||||
|
gap?: number | [number, number] |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 装饰策略 |
||||||
|
* |
||||||
|
* 描述与视图外观相关的属性 |
||||||
|
*/ |
||||||
|
export interface Decoration { |
||||||
|
/** 组件颜色,背景颜色 */ |
||||||
|
color?: string |
||||||
|
/** 组件图片,背景图片 */ |
||||||
|
image?: string |
||||||
|
/** 组件边框 */ |
||||||
|
border?: Border |
||||||
|
/** 组件圆角 */ |
||||||
|
radius?: Radius |
||||||
|
/** 组件阴影效果 */ |
||||||
|
shadow?: Shadow | Shadow[] |
||||||
|
/** 组件渐变,背景渐变 */ |
||||||
|
gradient?: Gradient |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 空间分配策略 |
||||||
|
* |
||||||
|
* 能够用在弹性布局子视图上的属性 |
||||||
|
*/ |
||||||
|
export interface Spatial { |
||||||
|
/** 当组件作为 Flex 的子组件时,视图在主轴方向上的初始大小 */ |
||||||
|
basis?: number |
||||||
|
/** 在 Flex 布局下的增长系数,对应 flex-grow */ |
||||||
|
grow?: number |
||||||
|
/** 在 Flex 布局下的收缩规则 */ |
||||||
|
shrink?: number |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 盒子策略 |
||||||
|
* |
||||||
|
* 描述与盒子模型相关的属性 |
||||||
|
*/ |
||||||
|
export interface Boxed { |
||||||
|
/** 组件内边距 */ |
||||||
|
padding?: EdgeInsets |
||||||
|
/** 组件外边距 */ |
||||||
|
margin?: EdgeInsets |
||||||
|
/** 组件宽度,若在 Flex 布局下可能会被作为主轴方向上的初始大小 */ |
||||||
|
width?: number | Percentage | 'auto' |
||||||
|
/** 组件高度,若在 Flex 布局下可能会被作为主轴方向上的初始大小 */ |
||||||
|
height?: number | Percentage | 'auto' |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 相关框架组件 |
||||||
|
* |
||||||
|
* 建议尽量使用业务组件。 |
||||||
|
*/ |
||||||
|
export interface Widget { |
||||||
|
/** 组件名称 */ |
||||||
|
name: string |
||||||
|
/** 组件属性 */ |
||||||
|
props?: Record<string, unknown> |
||||||
|
/** 相关插槽,不包括默认插槽 */ |
||||||
|
slots?: Omit<Slots, 'default'> |
||||||
|
} |
||||||
|
|
||||||
|
export type ClipBehavior = |
||||||
|
| 'hidden' |
||||||
|
| 'visible' |
||||||
|
| 'scroll' |
||||||
|
| 'auto' |
||||||
|
|
||||||
|
/** |
||||||
|
* 视图数据 |
||||||
|
* |
||||||
|
* 描述了视图的基本布局和外观表现。 |
||||||
|
*/ |
||||||
|
export interface Theme extends Textual, Flexible, Boxed, Decoration, Spatial, Flexible { |
||||||
|
/** 是否开启弹性布局,todo 删除改属性,根据属性判断是否启用 flex 布局 */ |
||||||
|
flexible?: boolean |
||||||
|
/** 超出视图剪切方式,对应 overflow 属性 */ |
||||||
|
clip?: ClipBehavior |
||||||
|
} |
||||||
|
|
||||||
|
/** 文本配置引用 */ |
||||||
|
export interface TextRefer { |
||||||
|
/** 标识为文本专用类型 */ |
||||||
|
type: 'text' |
||||||
|
/** 对应的数据键名称 */ |
||||||
|
key: string |
||||||
|
} |
||||||
|
|
||||||
|
// https://developer.mozilla.org/zh-CN/docs/Web/CSS/object-fit
|
||||||
|
export type ObjectFit = |
||||||
|
| 'fill' |
||||||
|
| 'contain' |
||||||
|
| 'cover' |
||||||
|
| 'none' |
||||||
|
| 'scale-down' |
||||||
|
|
||||||
|
/** 图片配置引用 */ |
||||||
|
export interface ImageRefer { |
||||||
|
/** 图片引用类型专用类型 */ |
||||||
|
type: 'image' |
||||||
|
/** 图片地址键 */ |
||||||
|
key: string |
||||||
|
/** 图片标题键 */ |
||||||
|
title?: string |
||||||
|
/** 图片跳转链接键 */ |
||||||
|
link?: string |
||||||
|
/** 图片填充方式 */ |
||||||
|
fit?: ObjectFit |
||||||
|
/** 图片圆角 */ |
||||||
|
radius?: Radius |
||||||
|
} |
||||||
|
|
||||||
|
/** 视频配置引用 */ |
||||||
|
export interface VideoRefer { |
||||||
|
/** 视频引用类型专用类型 */ |
||||||
|
type: 'video' |
||||||
|
/** 视频地址键 */ |
||||||
|
key: string |
||||||
|
/** 视频封片图片键 */ |
||||||
|
poster?: string |
||||||
|
/** 视频标题键 */ |
||||||
|
title?: string |
||||||
|
/** 视频填充方式 */ |
||||||
|
fit?: ObjectFit |
||||||
|
// todo 其它配置实现,比如自动播放、控件配置等
|
||||||
|
} |
||||||
|
|
||||||
|
/** 音频配置引用 */ |
||||||
|
export interface AudioRefer { |
||||||
|
/** 音频引用类型专用 */ |
||||||
|
type: 'audio' |
||||||
|
/** 音频地址键 */ |
||||||
|
key: string |
||||||
|
/** 音频标题键,todo 是否需要显示?如何显示? */ |
||||||
|
title: string |
||||||
|
// todo 其它配置实现,比如自动播放、控件配置等
|
||||||
|
} |
||||||
|
|
||||||
|
/** 目前支持的 4 中配置引用 */ |
||||||
|
export type DataRefer = |
||||||
|
| TextRefer |
||||||
|
| ImageRefer |
||||||
|
| VideoRefer |
||||||
|
| AudioRefer |
||||||
|
|
||||||
|
export type ViewChild = |
||||||
|
| string |
||||||
|
| number |
||||||
|
| DataRefer |
||||||
|
| View |
||||||
|
|
||||||
|
export type ViewChildren = ViewChild | Array<ViewChild> |
||||||
|
|
||||||
|
/** |
||||||
|
* 视图 |
||||||
|
* |
||||||
|
* 用于构建页面的基本元素 |
||||||
|
*/ |
||||||
|
export interface View { |
||||||
|
/** 视图唯一标识 */ |
||||||
|
vid?: string |
||||||
|
/** |
||||||
|
* 视图数据列表 |
||||||
|
* |
||||||
|
* 如果存在同类型的,属性会根据顺序被覆盖。 |
||||||
|
*/ |
||||||
|
theme?: Theme | Widget |
||||||
|
/** |
||||||
|
* 关联数据源 |
||||||
|
*/ |
||||||
|
source?: string |
||||||
|
/** 子视图 */ |
||||||
|
children?: ViewChildren |
||||||
|
} |
||||||
|
|
||||||
|
/** 装修视图块 */ |
||||||
|
export interface Block extends View { |
||||||
|
/** 视图唯一标识 */ |
||||||
|
vid: string |
||||||
|
/** 模块唯一标识 */ |
||||||
|
mid: string |
||||||
|
} |
||||||
|
|
||||||
|
/** 装修模块 */ |
||||||
|
export interface Module extends Block { |
||||||
|
/** 标题 */ |
||||||
|
title: string |
||||||
|
/** 组件预览图 */ |
||||||
|
image?: string |
||||||
|
/** 允许最大引用次数,如果值为负数则表示没有限制 */ |
||||||
|
maxReferenceCount?: number |
||||||
|
/** 被引用次数,运行时属性 */ |
||||||
|
referenceCount?: number |
||||||
|
/** 模块配置 */ |
||||||
|
configs: ModuleConfig[] |
||||||
|
} |
||||||
|
|
||||||
|
export type ModuleConfig = |
||||||
|
| ListConfig |
||||||
|
| ObjectConfig |
||||||
|
| BackgroundConfig |
||||||
|
| BooleanConfig |
||||||
|
| MarkConfig |
||||||
|
| ImageConfig |
||||||
|
| BaseConfig |
||||||
|
|
||||||
|
export interface BaseConfig { |
||||||
|
type: string |
||||||
|
field: string |
||||||
|
label: string |
||||||
|
help?: string |
||||||
|
required?: boolean |
||||||
|
} |
||||||
|
|
||||||
|
export interface ListConfig extends BaseConfig { |
||||||
|
type: 'list' |
||||||
|
addable: boolean |
||||||
|
min?: number |
||||||
|
max?: number |
||||||
|
configs: ModuleConfig[] |
||||||
|
} |
||||||
|
|
||||||
|
export interface ObjectConfig extends BaseConfig { |
||||||
|
type: 'object' |
||||||
|
configs: ModuleConfig[] |
||||||
|
} |
||||||
|
|
||||||
|
export interface BackgroundConfig extends BaseConfig { |
||||||
|
type: 'background' |
||||||
|
features: Array<'color' | 'gradient' | 'image'> |
||||||
|
} |
||||||
|
|
||||||
|
export interface ConfigValue<T = unknown> { |
||||||
|
label: string |
||||||
|
value: T |
||||||
|
/** 帮助信息 */ |
||||||
|
help?: string |
||||||
|
/** 以图标形式渲染 */ |
||||||
|
icon?: string |
||||||
|
} |
||||||
|
|
||||||
|
/** 针对单选或多选 */ |
||||||
|
export interface MarkConfig extends BaseConfig { |
||||||
|
type: 'mark' |
||||||
|
multiple?: boolean |
||||||
|
values: ConfigValue[] |
||||||
|
} |
||||||
|
|
||||||
|
export interface BooleanConfig extends BaseConfig { |
||||||
|
type: 'bool' |
||||||
|
} |
||||||
|
|
||||||
|
export interface ImageConfig extends BaseConfig { |
||||||
|
type: 'image' |
||||||
|
/** 内联,表示相关联的数据 */ |
||||||
|
inlines?: ModuleConfig[] |
||||||
|
} |
||||||
|
|
||||||
|
/** 模块分类 */ |
||||||
|
export interface Category { |
||||||
|
/** 分类图标 */ |
||||||
|
icon: string |
||||||
|
/** 分类名称 */ |
||||||
|
text: string |
||||||
|
/** 模块列表 */ |
||||||
|
modules: Module[] |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 亮度模式 |
||||||
|
* |
||||||
|
* todo 考虑对比模式 contrast |
||||||
|
*/ |
||||||
|
export type Brightness = |
||||||
|
| 'dark' |
||||||
|
| 'light' |
||||||
|
|
||||||
|
/** 页面头尾固定区域配置 */ |
||||||
|
export interface StickyConfig { |
||||||
|
/** 是否开启 */ |
||||||
|
enabled: boolean |
||||||
|
/** 高度 */ |
||||||
|
height: number |
||||||
|
/** 背景颜色 */ |
||||||
|
color: string |
||||||
|
/** 文本颜色 */ |
||||||
|
textColor: string |
||||||
|
/** 是否开启边框 */ |
||||||
|
bordered: boolean |
||||||
|
/** 边框颜色 */ |
||||||
|
borderColor: string |
||||||
|
} |
||||||
|
|
||||||
|
/** 页面头部配置 */ |
||||||
|
export interface PageHeader extends StickyConfig { |
||||||
|
/** 页面标题 */ |
||||||
|
title?: string |
||||||
|
/** 页面标题是否居中显示,在小程序上非自定义头部时无效 */ |
||||||
|
centerTitle?: boolean |
||||||
|
/** 自定义视图 */ |
||||||
|
custom?: View |
||||||
|
} |
||||||
|
|
||||||
|
/** 页面底部配置 */ |
||||||
|
export interface PageFooter extends StickyConfig { |
||||||
|
/** 自定义视图,todo 小程序底部怎么弄?使用额外配置? */ |
||||||
|
view?: View |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 页面配置 |
||||||
|
* |
||||||
|
* 包含组成页面的视图树和API接口 |
||||||
|
*/ |
||||||
|
export interface Page { |
||||||
|
/** 亮度模式,支持暗模式和亮模式 */ |
||||||
|
brightness?: Brightness |
||||||
|
/** 是否全屏模式 */ |
||||||
|
fullscreen?: boolean |
||||||
|
/** 是否开启顶部安全区域(屏幕状态栏) */ |
||||||
|
safeAreaInsetTop?: boolean |
||||||
|
/** 是否开启底部安全区域(底部操作条) */ |
||||||
|
safeAreaInsetBottom?: boolean |
||||||
|
/** 页面背景颜色 */ |
||||||
|
backgroundColor?: string |
||||||
|
/** 页面头部配置 */ |
||||||
|
header: PageHeader |
||||||
|
/** 页面主体部分 */ |
||||||
|
body: Block[] |
||||||
|
/** 页面底部配置 */ |
||||||
|
footer: PageFooter |
||||||
|
} |
||||||
|
|
||||||
|
// /** 交互类型 */
|
||||||
|
// export type Interaction =
|
||||||
|
// | 'carousel' // 轮播图
|
||||||
|
// | 'collapse' // 折叠面板
|
||||||
|
// | 'dialog' // 弹出层
|
||||||
|
// | 'drawer' // 抽屉
|
||||||
|
// | 'marquee' // 跑马灯(比如滚动通知)
|
||||||
|
// | 'count' // 计数(比如秒杀倒计时、短信验证码倒计时、数字滚动等)
|
||||||
|
// | 'keyboard' // 打开虚拟键盘
|
||||||
|
// | 'picker' // 打开 picker 面板
|
||||||
|
// | 'gesture' // 手势(长按、点击、双击、滑动等)
|
||||||
|
// | 'sticky' // 吸顶
|
||||||
|
//
|
||||||
|
// export interface Interop {
|
||||||
|
// /** 交互方式 */
|
||||||
|
// type: Interaction
|
||||||
|
// /**
|
||||||
|
// * 交互视图数据列表
|
||||||
|
// */
|
||||||
|
// effect?: ViewData[]
|
||||||
|
// /**
|
||||||
|
// * 交互关联视图列表
|
||||||
|
// *
|
||||||
|
// * 如果存在同类型的,属性会根据顺序被覆盖。
|
||||||
|
// */
|
||||||
|
// attach?: ViewData[]
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /** 描述视图动画 */
|
||||||
|
// export interface Motion {
|
||||||
|
// /** 动画类型 */
|
||||||
|
// type: string
|
||||||
|
// }
|
@ -0,0 +1,14 @@ |
|||||||
|
import type { CrossAlign, MainAlign } from '../types' |
||||||
|
|
||||||
|
export function align(s: MainAlign | CrossAlign | undefined): string | undefined { |
||||||
|
switch (s) { |
||||||
|
case 'start': |
||||||
|
case 'end': |
||||||
|
return `flex-${s}` |
||||||
|
case 'between': |
||||||
|
case 'around': |
||||||
|
return `space-${s}` |
||||||
|
default: |
||||||
|
return s |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
import type { CSSProperties } from 'vue' |
||||||
|
import type { Gradient } from '../types' |
||||||
|
|
||||||
|
function image(img: string | undefined, g: Gradient | undefined): string | undefined { |
||||||
|
if (img == null && g == null) |
||||||
|
return undefined |
||||||
|
|
||||||
|
if (img != null) |
||||||
|
return `url(${img})` |
||||||
|
|
||||||
|
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})` |
||||||
|
} |
||||||
|
return `${prefix}linear-gradient(${g?.angle ?? '0deg'}, ${g.colors.join(', ')})` |
||||||
|
} |
||||||
|
|
||||||
|
export function background(color: string | undefined, img: string | undefined, g: Gradient | undefined): CSSProperties { |
||||||
|
return { |
||||||
|
backgroundColor: color, |
||||||
|
backgroundImage: image(img, g), |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,49 @@ |
|||||||
|
import type { CSSProperties } from 'vue' |
||||||
|
import type { Border, BorderSide } from '../types' |
||||||
|
import { unit } from './unit' |
||||||
|
import { isBorderSide, isSymmetric } from './is' |
||||||
|
|
||||||
|
function borderSide(name: string, s: BorderSide | undefined): CSSProperties | undefined { |
||||||
|
if (s == null) |
||||||
|
return undefined |
||||||
|
|
||||||
|
const hasWidth = s.width != null |
||||||
|
const hasColor = s.color != null |
||||||
|
const hasStyle = s.style != null |
||||||
|
const r: Record<string, string | undefined> = {} |
||||||
|
if (hasColor) |
||||||
|
r[`${name}Color`] = s.color |
||||||
|
|
||||||
|
if (hasWidth) |
||||||
|
r[`${name}Width`] = unit(s.width) |
||||||
|
else if (hasColor) |
||||||
|
r[`${name}Width`] = '1px' |
||||||
|
|
||||||
|
if (hasStyle) |
||||||
|
r[`${name}Style`] = s.style |
||||||
|
else if (hasWidth || hasColor) |
||||||
|
r[`${name}Style`] = 'solid' |
||||||
|
|
||||||
|
return r |
||||||
|
} |
||||||
|
|
||||||
|
export function bordering(b: Border | undefined): CSSProperties | undefined { |
||||||
|
if (b == null) |
||||||
|
return undefined |
||||||
|
|
||||||
|
if (isBorderSide(b)) |
||||||
|
return borderSide('border', b) |
||||||
|
|
||||||
|
if (isSymmetric(b)) { |
||||||
|
return { |
||||||
|
...borderSide('borderBlock', b.vertical), |
||||||
|
...borderSide('borderInline', b.horizontal), |
||||||
|
} |
||||||
|
} |
||||||
|
return { |
||||||
|
...borderSide('borderTop', b.top), |
||||||
|
...borderSide('borderBottom', b.bottom), |
||||||
|
...borderSide('borderLeft', b.left), |
||||||
|
...borderSide('borderRight', b.right), |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
import type { CSSProperties } from 'vue' |
||||||
|
import { unit } from './unit' |
||||||
|
|
||||||
|
export function gap(gap: number | [number, number] | undefined): CSSProperties | undefined { |
||||||
|
if (!gap) { |
||||||
|
return undefined |
||||||
|
} |
||||||
|
else if (Array.isArray(gap)) { |
||||||
|
return { |
||||||
|
rowGap: unit(gap[0]), |
||||||
|
columnGap: unit(gap[1]), |
||||||
|
} |
||||||
|
} |
||||||
|
else { |
||||||
|
return { |
||||||
|
gap: unit(gap), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
/** |
||||||
|
* Calculate a 32 bit FNV-1a hash |
||||||
|
* Found here: https://gist.github.com/vaiorabbit/5657561
|
||||||
|
* Ref.: http://isthe.com/chongo/tech/comp/fnv/
|
||||||
|
* |
||||||
|
* @param {string} str the input value |
||||||
|
* @returns {string} return the 8-digit hex string |
||||||
|
*/ |
||||||
|
export function hash(str: string): string { |
||||||
|
let hval = 0x811C9DC5 |
||||||
|
for (let i = 0, l = str.length; i < l; i++) { |
||||||
|
hval ^= str.charCodeAt(i) |
||||||
|
hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24) |
||||||
|
} |
||||||
|
// Convert to 8 digit hex string
|
||||||
|
return (`0000000${(hval >>> 0).toString(16)}`).slice(-8) |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
export * from './align' |
||||||
|
export * from './background' |
||||||
|
export * from './border' |
||||||
|
export * from './gap' |
||||||
|
export * from './hash' |
||||||
|
export * from './insets' |
||||||
|
export * from './is' |
||||||
|
export * from './object' |
||||||
|
export * from './radii' |
||||||
|
export * from './shadow' |
||||||
|
export * from './size' |
||||||
|
export * from './unit' |
@ -0,0 +1,27 @@ |
|||||||
|
import type { CSSProperties } from 'vue' |
||||||
|
import type { EdgeInsets } from '../types' |
||||||
|
import { isSymmetric } from './is' |
||||||
|
import { unit } from './unit' |
||||||
|
|
||||||
|
export function insets(name: string, i: EdgeInsets | undefined): CSSProperties | undefined { |
||||||
|
if (i == null) |
||||||
|
return undefined |
||||||
|
|
||||||
|
if (typeof i === 'number') |
||||||
|
return { [name]: `${i}px` } |
||||||
|
|
||||||
|
if (typeof i === 'string') |
||||||
|
return { [name]: i } |
||||||
|
|
||||||
|
if (isSymmetric(i)) { |
||||||
|
return { |
||||||
|
[name]: [unit(i.vertical), unit(i.horizontal)].join(' '), |
||||||
|
} |
||||||
|
} |
||||||
|
return { |
||||||
|
[`${name}Top`]: unit(i.top), |
||||||
|
[`${name}Bottom`]: unit(i.bottom), |
||||||
|
[`${name}Left`]: unit(i.left), |
||||||
|
[`${name}Right`]: unit(i.right), |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
import type { BorderSide, Corners, Symmetric } from '../types' |
||||||
|
|
||||||
|
export function isSymmetric(a: any): a is Symmetric<any> { |
||||||
|
return a != null |
||||||
|
&& !Array.isArray(a) |
||||||
|
&& typeof a === 'object' |
||||||
|
&& ('vertical' in a || 'horizontal' in a) |
||||||
|
} |
||||||
|
|
||||||
|
export function isBorderSide(a: any): a is BorderSide { |
||||||
|
return a != null |
||||||
|
&& !Array.isArray(a) |
||||||
|
&& typeof a === 'object' |
||||||
|
&& ('color' in a || 'style' in a || 'width' in a) |
||||||
|
} |
||||||
|
|
||||||
|
export function isCorners(a: any): a is Corners<any> { |
||||||
|
return a != null |
||||||
|
&& !Array.isArray(a) |
||||||
|
&& typeof a === 'object' |
||||||
|
&& ('topLeft' in a |
||||||
|
|| 'topRight' in a |
||||||
|
|| 'bottomLeft' in a |
||||||
|
|| 'bottomRight' in a) |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
function find(obj: any, keys: string[]) { |
||||||
|
if (!keys.length) |
||||||
|
return obj |
||||||
|
|
||||||
|
if (obj == null || typeof obj !== 'object') |
||||||
|
return undefined |
||||||
|
|
||||||
|
const key = keys.shift()! |
||||||
|
if (!Array.isArray(obj)) |
||||||
|
return find(obj[key], keys) |
||||||
|
|
||||||
|
const index = Number.parseInt(key) |
||||||
|
if (Number.isNaN(index)) |
||||||
|
return undefined |
||||||
|
|
||||||
|
return find(obj[index], keys) |
||||||
|
} |
||||||
|
|
||||||
|
export function valueOf(obj: any, key: string) { |
||||||
|
if (!key) |
||||||
|
return undefined |
||||||
|
return find(obj, key.split('.')) |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
import type { CSSProperties } from 'vue' |
||||||
|
import type { Radius, RadiusDouble, RadiusPrimitive } from '../types' |
||||||
|
import { isCorners } from './is' |
||||||
|
|
||||||
|
function stringify(radius: RadiusPrimitive | RadiusDouble | undefined): string | undefined { |
||||||
|
if (radius == null) |
||||||
|
return undefined |
||||||
|
|
||||||
|
if (Array.isArray(radius)) |
||||||
|
return radius.map(stringify).join(' ') |
||||||
|
|
||||||
|
return `${radius}px` |
||||||
|
} |
||||||
|
|
||||||
|
export function radii(radius: Radius | undefined): CSSProperties | undefined { |
||||||
|
if (radius == null) |
||||||
|
return undefined |
||||||
|
|
||||||
|
if (isCorners(radius)) { |
||||||
|
return { |
||||||
|
borderTopLeftRadius: stringify(radius.topLeft), |
||||||
|
borderTopRightRadius: stringify(radius.topRight), |
||||||
|
borderBottomLeftRadius: stringify(radius.bottomLeft), |
||||||
|
borderBottomRightRadius: stringify(radius.bottomRight), |
||||||
|
} |
||||||
|
} |
||||||
|
return { |
||||||
|
borderRadius: stringify(radius), |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
import type { CSSProperties } from 'vue' |
||||||
|
import type { Shadow } from '../types' |
||||||
|
|
||||||
|
const unit = (n: number | undefined): string | undefined => n == null ? undefined : n === 0 ? n.toString() : `${n}px` |
||||||
|
|
||||||
|
function stringify(s: Shadow | undefined): string | undefined { |
||||||
|
if (s == null) |
||||||
|
return undefined |
||||||
|
|
||||||
|
return [ |
||||||
|
s.inset ? 'inset' : undefined, |
||||||
|
unit(s.x), |
||||||
|
unit(s.y), |
||||||
|
s.blur != null ? unit(s.blur) : s.spread != null ? '0' : undefined, |
||||||
|
unit(s.spread), |
||||||
|
s.color, |
||||||
|
] |
||||||
|
.filter(n => n != null) |
||||||
|
.join(' ') |
||||||
|
} |
||||||
|
|
||||||
|
export function shadowing(ss: Shadow | Shadow[] | undefined): CSSProperties | undefined { |
||||||
|
if (ss == null) |
||||||
|
return undefined |
||||||
|
|
||||||
|
if (!Array.isArray(ss)) { |
||||||
|
return { |
||||||
|
boxShadow: stringify(ss), |
||||||
|
} |
||||||
|
} |
||||||
|
return { |
||||||
|
boxShadow: ss |
||||||
|
.map(stringify) |
||||||
|
.filter(Boolean) |
||||||
|
.join(', '), |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
import type { CSSProperties } from 'vue' |
||||||
|
import type { Axis, Theme } from '../types' |
||||||
|
import { unit } from './unit' |
||||||
|
|
||||||
|
export function size( |
||||||
|
axis: Axis | undefined, |
||||||
|
theme: Theme | undefined, |
||||||
|
): CSSProperties { |
||||||
|
const width = unit(theme?.width) |
||||||
|
const height = unit(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, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
export function unit(n: number | undefined | string) { |
||||||
|
if (n == null) |
||||||
|
return undefined |
||||||
|
if (typeof n !== 'number') |
||||||
|
return n |
||||||
|
return `${n}px` |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { useSource } from '../context' |
||||||
|
|
||||||
|
defineOptions({ |
||||||
|
name: 'DddAudioView', |
||||||
|
}) |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
source: string |
||||||
|
title?: string |
||||||
|
}>() |
||||||
|
|
||||||
|
const src = useSource<string>(props.source) |
||||||
|
const title = useSource<string>(props.title) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div style="display:inline-block"> |
||||||
|
<div>{{ title }}</div> |
||||||
|
<audio v-if="src" :src="src" controls /> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,38 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import type { ObjectFit, Radius } from '../types' |
||||||
|
import { useSource } from '../context' |
||||||
|
import { radii } from '../utils' |
||||||
|
|
||||||
|
defineOptions({ |
||||||
|
name: 'DddImageView', |
||||||
|
}) |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
source: string |
||||||
|
title?: string |
||||||
|
link?: string |
||||||
|
fit?: ObjectFit |
||||||
|
radius?: Radius |
||||||
|
}>() |
||||||
|
|
||||||
|
// todo 默认图片 |
||||||
|
const src = useSource<string>(props.source, '') |
||||||
|
const title = useSource<string>(props.title) |
||||||
|
// const link = useSource<string>(props.link) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<img |
||||||
|
:src="src" |
||||||
|
:alt="title" |
||||||
|
:style="{ |
||||||
|
objectFit: fit ?? 'contain', |
||||||
|
maxWidth: '100%', |
||||||
|
...radii(radius), |
||||||
|
}" |
||||||
|
/> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped lang="less"> |
||||||
|
|
||||||
|
</style> |
@ -0,0 +1,17 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { useSource } from '../context' |
||||||
|
|
||||||
|
defineOptions({ |
||||||
|
name: 'DddTextView', |
||||||
|
}) |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
source: string |
||||||
|
}>() |
||||||
|
|
||||||
|
const text = useSource<string>(props.source) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
{{ text }} |
||||||
|
</template> |
@ -0,0 +1,116 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import type { Component } from 'vue' |
||||||
|
import { |
||||||
|
provideParentViewId, |
||||||
|
useBlockId, |
||||||
|
useContext, |
||||||
|
useParentViewId, |
||||||
|
} from '../context' |
||||||
|
|
||||||
|
defineOptions({ |
||||||
|
name: 'DddView', |
||||||
|
}) |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
is?: string | Component |
||||||
|
vid?: string |
||||||
|
}>() |
||||||
|
|
||||||
|
const ctx = useContext() |
||||||
|
const parentId = useParentViewId() |
||||||
|
const blockId = useBlockId() |
||||||
|
|
||||||
|
function handleMousedown(e: Event, id: string | undefined): void { |
||||||
|
ctx.activeBlockId = blockId |
||||||
|
ctx.hoverViewId = parentId |
||||||
|
ctx.canvas.focused = false |
||||||
|
if (blockId) |
||||||
|
ctx.configurator = 'block' |
||||||
|
|
||||||
|
if (id) { |
||||||
|
e.stopPropagation() |
||||||
|
ctx!.activeViewId = id |
||||||
|
if (blockId !== id) { |
||||||
|
switch (id) { |
||||||
|
case '#canvas': |
||||||
|
ctx.configurator = 'page' |
||||||
|
break |
||||||
|
case '#header': |
||||||
|
case '#footer': |
||||||
|
ctx.configurator = 'block' |
||||||
|
break |
||||||
|
default: |
||||||
|
ctx.configurator = 'view' |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function handleMouseleave(id: string | undefined): void { |
||||||
|
if (id && ctx.hoverViewId === id && !ctx.draggingViewId) |
||||||
|
ctx.hoverViewId = undefined |
||||||
|
} |
||||||
|
|
||||||
|
provideParentViewId(props.vid) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<component |
||||||
|
:is="is ?? 'div'" |
||||||
|
class="ddd-view" |
||||||
|
:class="{ |
||||||
|
'is-active': vid && ctx.activeViewId === vid, |
||||||
|
'is-hover': vid && ctx.hoverViewId === vid, |
||||||
|
}" |
||||||
|
:data-view-id="vid" |
||||||
|
@mouseleave="handleMouseleave(vid)" |
||||||
|
@mousedown.left="handleMousedown($event, vid)" |
||||||
|
> |
||||||
|
<slot /> |
||||||
|
<!-- 使用全局单例,利用 absolute 固定位置 --> |
||||||
|
<div class="ddd-view-highlight" /> |
||||||
|
</component> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="less" scoped> |
||||||
|
.ddd-view { |
||||||
|
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> |
@ -0,0 +1,4 @@ |
|||||||
|
export { default as DddAudioView } from './AudioView.vue' |
||||||
|
export { default as DddImageView } from './ImageView.vue' |
||||||
|
export { default as DddTextView } from './TextView.vue' |
||||||
|
export { default as DddView } from './View.vue' |
@ -0,0 +1,5 @@ |
|||||||
|
import { createApp } from 'vue' |
||||||
|
import './style.less' |
||||||
|
import App from './App.vue' |
||||||
|
|
||||||
|
createApp(App).mount('#app') |
@ -0,0 +1,40 @@ |
|||||||
|
:root { |
||||||
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; |
||||||
|
line-height: 1.5; |
||||||
|
font-weight: 400; |
||||||
|
|
||||||
|
color-scheme: light; |
||||||
|
//color: rgba(255, 255, 255, 0.87); |
||||||
|
//background-color: #242424; |
||||||
|
|
||||||
|
font-synthesis: none; |
||||||
|
text-rendering: optimizeLegibility; |
||||||
|
-webkit-font-smoothing: antialiased; |
||||||
|
-moz-osx-font-smoothing: grayscale; |
||||||
|
|
||||||
|
|
||||||
|
--canvas-safe-area-inset-top: 36px; |
||||||
|
--canvas-safe-area-inset-bottom: 18px; |
||||||
|
--canvas-min-height: 736px; // 812px; |
||||||
|
--canvas-hover-highlight-color: #0256FF; |
||||||
|
--canvas-active-highlight-color: red; |
||||||
|
} |
||||||
|
|
||||||
|
*, |
||||||
|
*::before, |
||||||
|
*::after { |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
body { |
||||||
|
margin: 0; |
||||||
|
min-width: 320px; |
||||||
|
min-height: 100vh; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
|
||||||
|
#app { |
||||||
|
width: 100vw; |
||||||
|
height: 100vh; |
||||||
|
/*background: #eee;*/ |
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
type Tuple<Len, Elm = unknown, Arr extends Elm[] = Elm[]> = |
||||||
|
Arr['length'] extends Len |
||||||
|
? Arr |
||||||
|
: never |
@ -0,0 +1,25 @@ |
|||||||
|
{ |
||||||
|
"compilerOptions": { |
||||||
|
"target": "ES2020", |
||||||
|
"jsx": "preserve", |
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"], |
||||||
|
"useDefineForClassFields": true, |
||||||
|
"module": "ESNext", |
||||||
|
|
||||||
|
/* Bundler mode */ |
||||||
|
"moduleResolution": "bundler", |
||||||
|
"resolveJsonModule": true, |
||||||
|
"allowImportingTsExtensions": true, |
||||||
|
|
||||||
|
/* Linting */ |
||||||
|
"strict": true, |
||||||
|
"noFallthroughCasesInSwitch": true, |
||||||
|
"noUnusedLocals": true, |
||||||
|
"noUnusedParameters": true, |
||||||
|
"noEmit": true, |
||||||
|
"isolatedModules": true, |
||||||
|
"skipLibCheck": true |
||||||
|
}, |
||||||
|
"references": [{ "path": "./tsconfig.node.json" }], |
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] |
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
{ |
||||||
|
"compilerOptions": { |
||||||
|
"composite": true, |
||||||
|
"module": "ESNext", |
||||||
|
"moduleResolution": "bundler", |
||||||
|
"allowSyntheticDefaultImports": true, |
||||||
|
"skipLibCheck": true |
||||||
|
}, |
||||||
|
"include": ["vite.config.ts"] |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
import { defineConfig } from 'vite' |
||||||
|
import vue from '@vitejs/plugin-vue' |
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({ |
||||||
|
plugins: [vue()], |
||||||
|
}) |
Loading…
Reference in new issue