Merge remote-tracking branch 'upstream/refactor/develop' into resolve-router-page-merge-conflict

This commit is contained in:
rhlin 2025-01-07 09:06:51 -08:00
commit 3bc9d334f2
No known key found for this signature in database
GPG Key ID: 3D103D4C2BA41C8D
309 changed files with 6206 additions and 3242 deletions

View File

@ -4,40 +4,40 @@ on:
push:
branches: []
pull_request:
branches: [develop,main, refactor/develop]
branches: [develop, main, refactor/develop, release/*]
jobs:
push-check:
runs-on: ubuntu-latest # windows-latest || macos-latest
runs-on: ubuntu-latest # windows-latest || macos-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 18
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Install dependencies
run: pnpm i
- name: Install dependencies
run: pnpm i
- name: Get changed files
id: get_changed_files
uses: tj-actions/changed-files@v41
with:
files: |
**.js
**.vue
**.jsx
- name: Run ESLint
run: npx eslint ${{steps.get_changed_files.outputs.all_changed_files}}
- name: Run Build
run: pnpm run build:plugin && pnpm run build:alpha > build-alpha.log 2>&1
- name: Get changed files
id: get_changed_files
uses: tj-actions/changed-files@v41
with:
files: |
**.js
**.vue
**.jsx
- name: Run ESLint
run: npx eslint ${{steps.get_changed_files.outputs.all_changed_files}}
- name: Run Build
run: pnpm run build:plugin && pnpm run build:alpha > build-alpha.log 2>&1
- name: Upload build logs
uses: actions/upload-artifact@v4
with:
name: build-alpha-log
path: build-alpha.log
- name: Upload build logs
uses: actions/upload-artifact@v4
with:
name: build-alpha-log
path: build-alpha.log

View File

@ -1,7 +1,7 @@
{
"name": "designer-demo",
"private": true,
"version": "2.0.0-rc.4",
"version": "2.1.0",
"type": "module",
"scripts": {
"dev": "cross-env VITE_THEME=light vite",

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@opentiny/tiny-engine-mock",
"version": "2.0.0-rc.4",
"version": "2.1.0",
"publishConfig": {
"access": "public"
},

View File

@ -0,0 +1,27 @@
const path = require('path')
const { rules } = require('../../.eslintrc')
module.exports = {
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
projectService: true,
project: [path.join(__dirname, './tsconfig.json') ],
ecmaVersion: 'latest',
},
plugins: ['@typescript-eslint'],
env: {
browser: true,
es2015: true,
node: true
},
rules: {
...rules,
// 允许 @ts-ignore
"@typescript-eslint/ban-ts-comment": "off",
// 允许非空断言
"@typescript-eslint/no-non-null-asserted-optional-chain": "off"
},
ignorePatterns: ['test/sample/*.vue', '.eslintrc.cjs']
}

View File

@ -0,0 +1,2 @@
# @opentiny/tiny-engine 低代码引擎区编译器

View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>block-compiler</title>
<link rel="stylesheet" href="https://unpkg.com/@opentiny/vue-theme@3.14/index.css" />
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3.4.23/dist/vue.runtime.esm-browser.js",
"vue/server-renderer": "https://unpkg.com/@vue/server-renderer@3.4.23/dist/server-renderer.esm-browser.js",
"vue-i18n": "https://unpkg.com/vue-i18n@9.2.0-beta.36/dist/vue-i18n.esm-browser.js",
"vue-router": "https://unpkg.com/vue-router@4.0.16/dist/vue-router.esm-browser.js",
"@vue/devtools-api": "https://unpkg.com/@vue/devtools-api@6.5.1/lib/esm/index.js",
"@vueuse/core": "https://unpkg.com/@vueuse/core@9.6.0/index.mjs",
"@vueuse/shared": "https://unpkg.com/@vueuse/shared@9.6.0/index.mjs",
"axios": "https://unpkg.com/axios@1.0.0/dist/esm/axios.js",
"@opentiny/tiny-engine-i18n-host": "https://unpkg.com/@opentiny/tiny-engine-i18n-host@1/dist/lowcode-design-i18n-host.es.js",
"@opentiny/tiny-engine-builtin-component": "https://unpkg.com/@opentiny/tiny-engine-builtin-component@^2.0.0/dist/index.js",
"pinia": "https://unpkg.com/pinia@2.0.22/dist/pinia.esm-browser.js",
"@opentiny/vue": "https://unpkg.com/@opentiny/vue@3.14/runtime/tiny-vue.mjs",
"@opentiny/vue-icon": "https://unpkg.com/@opentiny/vue@3.14/runtime/tiny-vue-icon.mjs",
"@opentiny/vue-common": "https://unpkg.com/@opentiny/vue@3.14/runtime/tiny-vue-common.mjs",
"@opentiny/vue-locale": "https://unpkg.com/@opentiny/vue@3.14/runtime/tiny-vue-locale.mjs",
"@opentiny/vue-renderless/": "https://unpkg.com/@opentiny/vue-renderless@3.14/"
}
}
</script>
</head>
<body>
<div id="app"></div>
<script type="module" src="./src/dev.ts"></script>
</body>
</html>

View File

@ -0,0 +1,45 @@
{
"name": "@opentiny/tiny-engine-block-compiler",
"version": "2.1.0",
"publishConfig": {
"access": "public"
},
"description": "block runtime compiler",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"files": [
"dist"
],
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/opentiny/tiny-engine",
"directory": "packages/block-compiler"
},
"bugs": {
"url": "https://github.com/opentiny/tiny-engine/issues"
},
"author": "OpenTiny Team",
"license": "MIT",
"homepage": "https://opentiny.design/tiny-engine",
"devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@types/babel__core": "^7.20.5",
"@vitejs/plugin-vue": "^5.1.2",
"typescript": "~5.4.2",
"vite": "^5.4.2",
"vite-plugin-dts": "^4.3.0"
},
"peerDependencies": {
"@babel/core": "^7.26.0",
"@vue/babel-plugin-jsx": "^1.2.5",
"@vue/compiler-sfc": "^3.4.15",
"vue": "^3.4.15"
}
}

View File

@ -0,0 +1,78 @@
// @ts-ignore
import { createApp, defineAsyncComponent, h } from 'https://unpkg.com/vue@3.4.23/dist/vue.runtime.esm-browser.js'
import { compile } from './index'
import BlockFileName from '../test/sample/BlockFileName.vue?raw'
import BlockHead from '../test/sample/BlockHead.vue?raw'
import BlockMenu from '../test/sample/BlockMenu.vue?raw'
import BlockTest from '../test/sample/BlockTest.vue?raw'
import BlockJsxTest from '../test/sample/slotModelValueTest.vue?raw'
const RenderMain = {
setup() {
const componentMap = compile(
[
{
fileName: 'BlockHead',
sourceCode: BlockHead
},
{
fileName: 'BlockFileName',
sourceCode: BlockFileName
},
{
fileName: 'BlockMenu',
sourceCode: BlockMenu
},
{
fileName: 'BlockTest',
sourceCode: BlockTest
},
{
fileName: 'BlockJsxTest',
sourceCode: BlockJsxTest
}
],
{}
)
const blockComponents: { [key: string]: unknown } = {}
// @ts-ignore
window.getBlockComponentBlobUrl = (name) => {
return componentMap?.[name]?.blobURL
}
for (const [fileName, value] of Object.entries(componentMap)) {
blockComponents[fileName] = defineAsyncComponent(() => import(/* @vite-ignore */ value.blobURL))
}
const css = Object.values(componentMap)
.map((item) => item.style)
.join('')
const stylesheet = document.querySelector('#block-stylesheet')
if (stylesheet) {
stylesheet.remove()
} else {
const newStyleSheet = document.createElement('style')
newStyleSheet.innerHTML = css
document.head.appendChild(newStyleSheet)
}
return () =>
h('div', {}, [
h(blockComponents.BlockJsxTest),
h(blockComponents.BlockTest),
h(blockComponents.BlockHead),
h(blockComponents.BlockFileName),
h('span', {}, 'testtest')
])
}
}
const App = createApp(RenderMain)
App.mount(document.querySelector('#app')!)

View File

@ -0,0 +1,308 @@
import { compileScript, compileTemplate, compileStyle, parse, babelParse, MagicString } from '@vue/compiler-sfc'
import type { SFCParseResult, SFCDescriptor, BindingMetadata, CompilerOptions } from '@vue/compiler-sfc'
import { testIsJsx, transformVueJsx } from './transformJsx.ts'
const compileBlockStyle = (descriptor: SFCDescriptor, id: string) => {
const cssResArr = descriptor.styles.map((style) => {
const result = compileStyle({
id,
filename: descriptor.filename,
source: style.content,
scoped: style.scoped
})
return result.code || ''
})
return cssResArr.join('\n')
}
const compileBlockTemplate = (descriptor: SFCDescriptor, id: string, bindingMetadata: BindingMetadata | undefined) => {
const isJsx = testIsJsx(descriptor)
const expressionPlugins: CompilerOptions['expressionPlugins'] = []
if (isJsx) {
expressionPlugins.push('jsx')
}
const compileRes = compileTemplate({
id,
ast: descriptor.template?.ast,
source: descriptor.template?.content!,
filename: descriptor.filename,
scoped: descriptor.styles.some((styleItem) => styleItem.scoped),
slotted: descriptor.slotted,
compilerOptions: {
bindingMetadata,
expressionPlugins
}
})
const { errors } = compileRes
let { code } = compileRes
if (isJsx) {
code = transformVueJsx(code) || ''
}
return { code, errors }
}
interface compiledItem {
js: string
style: string
blobURL: string
}
export interface IResultMap {
[key: string]: compiledItem
}
const resolveRelativeImport = (code: string, globalGetterName = 'loadBlockComponent') => {
const magicStr = new MagicString(code)
const ast = babelParse(code, { sourceType: 'module', plugins: ['jsx'] }).program.body
let vueImportNode = null
let hasDefineAsyncComponent = false
for (const node of ast) {
if (node.type === 'ImportDeclaration') {
const source = node.source.value
if (source === 'vue' && !node.specifiers.find((spec) => spec.type === 'ImportNamespaceSpecifier')) {
vueImportNode = node
}
// 标识相对路径引入的 .vue 文件为区块,使用异步组件替换
if (source.startsWith('./') && node.source.value.endsWith('.vue')) {
hasDefineAsyncComponent = true
const fileName = node.source.value.replace(/^(\.\/+)/, '').slice(0, -4)
// 默认导出名
const defaultImportId = node.specifiers.find((spec) => spec.type === 'ImportDefaultSpecifier')?.local?.name
// 不存在默认导出,跳过
if (!defaultImportId) {
continue
}
// 声明异步组件 const Block = defineAsyncComponent(() => import(getBlockUrl(Block)))
magicStr.appendLeft(
node.start!,
`const ${defaultImportId} = defineAsyncComponent(() => window.${globalGetterName}('${fileName}'))`
)
// 移除 import Block from './Block.vue' 语句
magicStr.remove(node.start!, node.end!)
}
}
}
// TODO: 拿到类型声明,拆分到另一个函数
if (hasDefineAsyncComponent) {
// 存在异步引入组件
if (vueImportNode) {
const asyncSpec = vueImportNode.specifiers.find(
(spec) => spec.type === 'ImportSpecifier' && spec.local.name === 'defineAsyncComponent'
)
if (!asyncSpec) {
const firstRelativeSpec = vueImportNode.specifiers.find((spec) => spec.type === 'ImportSpecifier')
if (firstRelativeSpec) {
magicStr.appendLeft(firstRelativeSpec.start!, 'defineAsyncComponent, ')
} else {
magicStr.appendRight(vueImportNode.specifiers[0].end!, ', { defineAsyncComponent }')
}
}
} else {
magicStr.appendLeft(ast[0].start!, "import { defineAsyncComponent } from 'vue'\n")
}
}
return magicStr.toString()
}
const DEFAULT_COMPONENT_NAME = '__sfc__'
const compileBlockScript = (descriptor: SFCDescriptor, id: string): [string, BindingMetadata | undefined] => {
const isJsx = testIsJsx(descriptor)
const expressionPlugins: CompilerOptions['expressionPlugins'] = []
if (isJsx) {
expressionPlugins.push('jsx')
}
// TODO: try catch
const compiledScript = compileScript(descriptor, {
genDefaultAs: DEFAULT_COMPONENT_NAME,
inlineTemplate: true,
id,
templateOptions: {
compilerOptions: {
expressionPlugins
}
}
})
let code = compiledScript.content
if (isJsx) {
code = transformVueJsx(code) || ''
}
return [code, compiledScript.bindings]
}
interface IParsedFileItem {
fileName: string
sourceCode: string
compilerParseResult: SFCParseResult
importedFiles: string[]
fileNameWithRelativePath: string
}
// 依次构建 script、template、style然后组装成 import
const compileFile = (file: IParsedFileItem): Omit<compiledItem, 'blobURL'> => {
const descriptor = file.compilerParseResult.descriptor
// 编译 script
const [compiledScript, bindings] = compileBlockScript(descriptor, file.fileName)
let componentCode = `${compiledScript}`
// 编译 template
if (!descriptor.scriptSetup && descriptor.template) {
const { code: compiledTemplate } = compileBlockTemplate(descriptor, file.fileName, bindings)
componentCode += `\n ${compiledTemplate} \n ${DEFAULT_COMPONENT_NAME}.render = render`
}
const hasScoped = descriptor.styles.some((styleItem) => styleItem.scoped)
if (hasScoped) {
componentCode += `\n${DEFAULT_COMPONENT_NAME}.__scopeId='data-v-${file.fileName}'`
}
// 编译 style
const styleString = compileBlockStyle(descriptor, file.fileName)
return {
js: `${componentCode}\nexport default ${DEFAULT_COMPONENT_NAME}`,
style: styleString
}
}
// 解析依赖的文件
const parseImportedFiles = (descriptor: SFCDescriptor): string[] => {
let scriptContent = ''
if (descriptor.script) {
scriptContent = descriptor.script.content
} else if (descriptor.scriptSetup) {
scriptContent = descriptor.scriptSetup.content
}
if (!scriptContent) {
return []
}
const ast = babelParse(scriptContent, { sourceFilename: descriptor.filename, sourceType: 'module', plugins: ['jsx'] })
.program.body
const res: string[] = []
for (const node of ast) {
if (node.type === 'ImportDeclaration') {
const source = node.source.value
// 相对路径依赖,区块嵌套的场景
if (source.startsWith('./')) {
res.push(node.source.value)
}
}
}
return res
}
const getJSBlobURL = (str: string) => {
const blob = new Blob([str], { type: 'application/javascript' })
return URL.createObjectURL(blob)
}
export interface IFileItem {
fileName: string
sourceCode: string
}
export type IFileList = IFileItem[]
export interface IConfig {
compileCache?: Map<string, compiledItem>
globalGetterName?: string
}
// TODO: 支持 importMap
export const compile = (fileList: IFileList, config: IConfig) => {
const parsedFileList = fileList.map((fileItem) => {
const { fileName, sourceCode } = fileItem
// FIXME:这里解析的结果不能重复使用,因为可能会涉及修改引入的依赖
const { descriptor, errors } = parse(sourceCode, { filename: fileName })
if (errors) {
// TODO: 抛出错误
}
// TODO: 1. 当前仅支持 vue 文件编译,检查文件后缀,如果不是 .vue 结尾的,抛出错误
// TODO: 2. 检查 style lang仅支持 css
// TODO: 3. 检查 template lang当前不支持任何 template lang
// 解析依赖的文件
const importedFiles = parseImportedFiles(descriptor)
return {
fileName,
sourceCode,
compilerParseResult: {
descriptor,
errors
},
importedFiles,
fileNameWithRelativePath: `./${fileName}.vue`
}
})
const compiledFilesSet: Set<string> = new Set()
const resultMap: IResultMap = {}
const compileCache = config?.compileCache || new Map()
for (const fileItem of parsedFileList) {
const fileName = fileItem.fileName
const cache = compileCache.get(fileName)
let js = ''
let style = ''
// 优先使用缓存
if (cache?.js && cache?.style) {
js = cache.js
style = cache.style
} else {
const compileRes = compileFile(fileItem)
js = compileRes.js
style = compileRes.style
}
const resolvedImportJs = resolveRelativeImport(js, config?.globalGetterName)
resultMap[fileName] = {
js: resolvedImportJs,
style,
blobURL: getJSBlobURL(resolvedImportJs)
}
compileCache.set(fileName, resultMap[fileName])
compiledFilesSet.add(fileItem.fileNameWithRelativePath)
}
return resultMap
}

View File

@ -0,0 +1,18 @@
import { transformSync } from '@babel/core'
import vueJsx from '@vue/babel-plugin-jsx'
import type { SFCDescriptor } from 'vue/compiler-sfc'
export const testIsJsx = (descriptor: SFCDescriptor) => {
const lang = descriptor.script?.lang || descriptor?.scriptSetup?.lang || ''
return /jsx$/.test(lang)
}
export const transformVueJsx = (sourceCode: string) => {
return transformSync(sourceCode, {
babelrc: false,
plugins: [vueJsx],
sourceMaps: false,
configFile: false
})?.code
}

View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,38 @@
<template>
<div>
<tiny-popover class="block-link-field" popper-class="option-popper block-new-attr-popover">
<tiny-input placeholder="请输入字段名称"></tiny-input>
</tiny-popover>
<div>
<tiny-button type="primary">TinyEngine 前端可视化设计器</tiny-button>
<slot name="menu" :title="state.title">
<span>TinyEngine 前端可视化设计器为设计器开发者提供定制服务在线构建出自己专属的设计器</span>
</slot>
</div>
</div>
</template>
<script setup>
import * as vue from 'vue'
import { defineProps, defineEmits } from 'vue'
import { Input as TinyInput, Popover as TinyPopover, Button as TinyButton } from '@opentiny/vue'
const props = defineProps({ testSlot: { type: Object, default: () => ({}) } })
const emit = defineEmits([])
const state = vue.reactive({
title: 'test slot params'
})
</script>
<style scoped>
body {
background-color: #fff;
}
.test {
width: 100px;
padding: 10px;
margin: 10px;
color: #191919;
}
</style>

View File

@ -0,0 +1,21 @@
<template>
<div>
<h1 class="head-title">这里是头部</h1>
<block-menu></block-menu>
</div>
</template>
<script setup>
import { onMounted } from 'vue'
import BlockMenu from './BlockMenu.vue'
onMounted(() => {
console.log('blockhead mounted')
})
</script>
<style scoped>
.head-title {
color: red;
}
</style>

View File

@ -0,0 +1,25 @@
<template>
<div>
<ul class="block-menu">
<li>菜单1</li>
<li>菜单2</li>
<li>菜单3</li>
<li>菜单4</li>
<li>菜单5</li>
</ul>
</div>
</template>
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
console.log('blockMenu mounted')
})
</script>
<style>
.block-menu {
color: red;
}
</style>

View File

@ -0,0 +1,15 @@
<template>
<div>block test aaaa</div>
</template>
<script>
import { onMounted } from 'vue'
export default {
setup() {
onMounted(() => {
console.log('onMounted')
})
}
}
</script>

View File

@ -0,0 +1,89 @@
<template>
<div>
<div>
<tiny-grid
:editConfig="{ trigger: 'click', mode: 'cell', showStatus: true }"
:columns="state.columns"
:data="[
{
id: '1',
name: 'GFD科技有限公司',
city: '福州',
employees: 800,
created_date: '2014-04-30 00:56:00',
boole: false
},
{
id: '2',
name: 'WWW科技有限公司',
city: '深圳',
employees: 300,
created_date: '2016-07-08 12:36:22',
boole: true
}
]"
></tiny-grid>
</div>
</div>
</template>
<script setup lang="jsx">
import { Grid as TinyGrid, Numeric as TinyNumeric, Input as TinyInput } from '@opentiny/vue'
import * as vue from 'vue'
import { defineProps, defineEmits } from 'vue'
const props = defineProps({})
const emit = defineEmits([])
const state = vue.reactive({
columns: [
{ type: 'index', width: 60 },
{ type: 'selection', width: 60 },
{
field: 'employees',
title: '员工数',
slots: {
default: ({ row, column, rowIndex }, h) => (
<div>
<TinyNumeric
allow-empty={true}
placeholder="请输入"
controlsPosition="right"
step={1}
modelValue={row.employees}
onChange={(...eventArgs) => onChangeNumber(eventArgs, row, column, rowIndex)}
onUpdate:modelValue={(value) => (row.employees = value)}
></TinyNumeric>
</div>
)
}
},
{ field: 'created_date', title: '创建日期' },
{
field: 'city',
title: '城市',
slots: {
default: ({ row, column, rowIndex }, h) => (
<div>
<TinyInput
placeholder="请输入"
modelValue={row.city}
onChange={(...eventArgs) => onChangeInput(eventArgs, row, column, rowIndex)}
onUpdate:modelValue={(value) => (row.city = value)}
></TinyInput>
</div>
)
}
}
]
})
const onChangeInput = function onChangeInput(eventArgs, args0, args1, args2) {
console.log('onChangeInput', eventArgs)
}
const onChangeNumber = function onChangeNumber(eventArgs, args0, args1, args2) {
console.log('onChangeNumber', eventArgs)
}
</script>
<style scoped></style>

View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"emitDeclarationOnly": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["./src/**/*.ts", "./src/**/*.tsx", "./src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"emitDeclarationOnly": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts", ".eslintrc.cjs"]
}

View File

@ -0,0 +1,38 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import dts from 'vite-plugin-dts'
import path from 'node:path'
import nodeGlobalsPolyfillPluginCjs from '@esbuild-plugins/node-globals-polyfill'
import nodeModulesPolyfillPluginCjs from '@esbuild-plugins/node-modules-polyfill'
// @ts-ignore
const nodeGlobalsPolyfillPlugin = nodeGlobalsPolyfillPluginCjs.default
// @ts-ignore
const nodeModulesPolyfillPlugin = nodeModulesPolyfillPluginCjs.default
// https://vite.dev/config/
export default defineConfig({
plugins: [dts({ rollupTypes: true, tsconfigPath: './tsconfig.json' }), vue()],
optimizeDeps: {
esbuildOptions: {
plugins: [
nodeGlobalsPolyfillPlugin({
process: true,
buffer: true
}),
nodeModulesPolyfillPlugin()
]
}
},
build: {
lib: {
entry: path.resolve(__dirname, './src/index.ts'),
name: 'block-compiler',
fileName: (_format, entryName) => `${entryName}.js`,
formats: ['es']
},
rollupOptions: {
external: ['@babel/core', '@vue/babel-plugin-jsx', 'vue', 'vue/compiler-sfc']
}
}
})

View File

@ -1,6 +1,6 @@
{
"name": "@opentiny/tiny-engine-block-build",
"version": "2.0.0-rc.4",
"version": "2.1.0",
"description": "translate block to webcomponent template",
"main": "./dist/web-components.es.js",
"type": "module",

View File

@ -1,6 +1,6 @@
{
"name": "@opentiny/tiny-engine-vite-config",
"version": "2.0.0-rc.4",
"version": "2.1.0",
"description": "",
"type": "module",
"main": "./index.js",

View File

@ -61,7 +61,8 @@ const getDevAlias = (useSourceAlias) => {
'@opentiny/tiny-engine-builtin-component': path.resolve(basePath, 'packages/builtinComponent/index.js'),
'@opentiny/tiny-engine-meta-register': path.resolve(basePath, 'packages/register/src/index.js'),
'@opentiny/tiny-engine-layout': path.resolve(basePath, 'packages/layout/index.js'),
'@opentiny/tiny-engine-configurator': path.resolve(basePath, 'packages/configurator/src/index.js')
'@opentiny/tiny-engine-configurator': path.resolve(basePath, 'packages/configurator/src/index.js'),
'@opentiny/tiny-engine-block-compiler': path.resolve(basePath, 'packages/block-compiler/src/index.ts')
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@opentiny/tiny-engine-vite-plugin-meta-comments",
"version": "2.0.0-rc.4",
"version": "2.1.0",
"description": "",
"type": "module",
"main": "dist/index.cjs",

View File

@ -1,4 +1,6 @@
export { default as CanvasCol } from './src/components/CanvasCol.vue'
export { default as CanvasRow } from './src/components/CanvasRow.vue'
export { default as CanvasRowColContainer } from './src/components/CanvasRowColContainer.vue'
export { default as CanvasFlexBox } from './src/components/CanvasFlexBox.vue'
export { default as CanvasSection } from './src/components/CanvasSection.vue'
export { default as meta } from './src/meta'

View File

@ -1,6 +1,6 @@
{
"name": "@opentiny/tiny-engine-builtin-component",
"version": "2.0.0-rc.4",
"version": "2.1.0",
"description": "",
"main": "dist/index.mjs",
"module": "dist/index.mjs",

View File

@ -0,0 +1,55 @@
<template>
<div class="canvas-flex-box">
<slot> </slot>
</div>
</template>
<script setup>
import { computed, defineProps } from 'vue'
import { getStyleValue } from './helper'
const props = defineProps({
flexDirection: {
type: String,
default: 'row'
},
gap: {
type: [String, Number],
default: '8px'
},
padding: {
type: [String, Number],
default: '8px'
},
alignItems: {
type: String,
default: 'center'
},
justifyContent: {
type: String,
default: 'flex-start'
}
})
const styles = computed(() => ({
flexDirection: props.flexDirection,
gap: getStyleValue(props.gap),
padding: getStyleValue(props.padding),
alignItems: props.alignItems,
justifyContent: props.justifyContent
}))
</script>
<style lang="less" scoped>
.canvas-flex-box {
display: flex;
flex: 1 1 0px;
flex-direction: v-bind('styles.flexDirection');
gap: v-bind('styles.gap');
padding: v-bind('styles.padding');
align-items: v-bind('styles.alignItems');
justify-content: v-bind('styles.justifyContent');
:deep(.canvas-container) {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,15 @@
<template>
<div class="canvas-section">
<slot> </slot>
</div>
</template>
<style lang="less" scoped>
.canvas-section {
width: 100%;
display: flex;
flex: 1 1 0;
align-items: center;
justify-content: center;
}
</style>

View File

@ -0,0 +1,221 @@
{
"component": {
"icon": "Box",
"name": {
"zh_CN": "弹性容器"
},
"component": "CanvasFlexBox",
"schema": {
"slots": {},
"properties": [
{
"label": {
"zh_CN": "基础信息"
},
"description": {
"zh_CN": "基础信息"
},
"collapse": {
"number": 6,
"text": {
"zh_CN": "显示更多"
}
},
"content": [
{
"property": "flexDirection",
"type": "String",
"defaultValue": "row",
"bindState": true,
"label": {
"text": {
"zh_CN": "排列方向"
}
},
"cols": 12,
"rules": [],
"widget": {
"component": "SelectConfigurator",
"props": {
"options": [
{
"label": "水平,起点在左端",
"value": "row"
},
{
"label": "水平,起点在右端",
"value": "row-reverse"
},
{
"label": "垂直,起点在上沿",
"value": "column"
},
{
"label": "垂直,起点在下沿",
"value": "column-reverse"
}
]
}
}
},
{
"property": "gap",
"defaultValue": "8px",
"label": {
"text": {
"zh_CN": "间距"
}
},
"widget": {
"component": "InputConfigurator"
},
"description": {
"zh_CN": "控制容器内水平和垂直的间距"
},
"labelPosition": "left"
},
{
"property": "padding",
"defaultValue": "8px",
"label": {
"text": {
"zh_CN": "内边距"
}
},
"widget": {
"component": "InputConfigurator"
},
"labelPosition": "left"
},
{
"property": "justifyContent",
"type": "String",
"defaultValue": "flex-start",
"bindState": true,
"label": {
"text": {
"zh_CN": "水平对齐方式"
}
},
"cols": 12,
"rules": [],
"widget": {
"component": "SelectConfigurator",
"props": {
"options": [
{
"label": "左对齐",
"value": "flex-start"
},
{
"label": "右对齐",
"value": "flex-end"
},
{
"label": "居中",
"value": "center"
},
{
"label": "两端对齐,子元素间隔相等",
"value": "space-between"
},
{
"label": "子元素两侧间隔相等",
"value": "space-around"
}
]
}
}
},
{
"property": "alignItems",
"type": "String",
"defaultValue": "center",
"bindState": true,
"label": {
"text": {
"zh_CN": "垂直对齐方式"
}
},
"cols": 12,
"rules": [],
"widget": {
"component": "SelectConfigurator",
"props": {
"options": [
{
"label": "交叉轴的中点对齐",
"value": "center"
},
{
"label": "交叉轴的起点对齐",
"value": "flex-start"
},
{
"label": "交叉轴的终点对齐",
"value": "flex-end"
},
{
"label": "以子元素第一行文字的基线对齐",
"value": "baseline"
},
{
"label": "占满容器高度",
"value": "stretch"
}
]
}
}
}
]
}
],
"events": {
"onClick": {
"label": {
"zh_CN": "点击事件"
},
"description": {
"zh_CN": "点击时触发的回调函数"
},
"type": "event",
"functionInfo": {
"params": [],
"returns": {}
},
"defaultValue": ""
}
},
"shortcuts": {
"properties": []
},
"contentMenu": {
"actions": []
}
},
"configure": {
"loop": true,
"isContainer": true,
"nestingRule": {
"childWhitelist": [],
"descendantBlacklist": []
}
}
},
"snippet": {
"name": {
"zh_CN": "弹性容器"
},
"screenshot": "",
"snippetName": "CanvasFlexBox",
"icon": "Box",
"schema": {
"componentName": "CanvasFlexBox",
"props": {
"flexDirection": "row",
"gap": "8px",
"padding": "8px"
}
}
}
}

View File

@ -68,21 +68,21 @@
"schema": {
"componentName": "CanvasRowColContainer",
"props": {
"rowGap": "20px"
"rowGap": "16px"
},
"children": [
{
"componentName": "CanvasRow",
"props": {
"rowGap": "20px",
"colGap": "20px"
"rowGap": "16px",
"colGap": "16px"
},
"children": [
{
"componentName": "CanvasCol",
"props": {
"rowGap": "20px",
"colGap": "20px",
"rowGap": "16px",
"colGap": "16px",
"grow": true,
"shrink": true,
"widthType": "auto"

View File

@ -0,0 +1,71 @@
{
"component": {
"icon": "Box",
"name": {
"zh_CN": "全宽居中布局"
},
"component": "CanvasSection",
"schema": {
"slots": {},
"properties": [
{
"label": {
"zh_CN": "基础信息"
},
"description": {
"zh_CN": "基础信息"
},
"collapse": {
"number": 6,
"text": {
"zh_CN": "显示更多"
}
},
"content": []
}
],
"events": {
"onClick": {
"label": {
"zh_CN": "点击事件"
},
"description": {
"zh_CN": "点击时触发的回调函数"
},
"type": "event",
"functionInfo": {
"params": [],
"returns": {}
},
"defaultValue": ""
}
},
"shortcuts": {
"properties": []
},
"contentMenu": {
"actions": []
}
},
"configure": {
"loop": true,
"isContainer": true,
"nestingRule": {
"childWhitelist": [],
"descendantBlacklist": []
}
}
},
"snippet": {
"name": {
"zh_CN": "全宽居中布局"
},
"screenshot": "",
"snippetName": "CanvasSection",
"icon": "Box",
"schema": {
"componentName": "CanvasSection",
"props": {}
}
}
}

View File

@ -1,16 +1,24 @@
import CanvasCol from './CanvasCol.json'
import CanvasRow from './CanvasRow.json'
import CanvasRowColContainer from './CanvasRowColContainer.json'
import CanvasFlexBox from './CanvasFlexBox.json'
import CanvasSection from './CanvasSection.json'
export default {
components: [CanvasCol.component, CanvasRow.component, CanvasRowColContainer.component],
components: [
CanvasCol.component,
CanvasRow.component,
CanvasRowColContainer.component,
CanvasFlexBox.component,
CanvasSection.component
],
snippets: [
{
group: 'layout',
label: {
zh_CN: '布局与容器'
},
children: [CanvasRowColContainer.snippet]
children: [CanvasRowColContainer.snippet, CanvasFlexBox.snippet, CanvasSection.snippet]
}
]
}

View File

@ -0,0 +1,195 @@
# schema 元服务相关APIExperimental
## 直接修改 schema 引用 & 调用通知更新
使用示例:
```javascript
import { useCanvas, useMessage } from '@opentiny/tiny-engine-meta-register'
const pageSchema = useCanvas().getPageSchema()
pageSchema.css = "xxxx"
useMessage().publish({ topic: 'schemaChange' })
```
注意:直接修改 schema 引用当前不能涉及到节点的增加、删除,不然会节点树 nodesMap 无法更新,导致画布无法选中新增的组件。
> 注意:以下所有 API 皆为 Experimental 实验 API请不要用在生产阶段
## 导入/导出 schema
> 这里的导入导出仅包含页面级别,不包含应用级别 schema
**导入 schema:**
```javascript
import { useCanvas } from '@opentiny/tiny-engine-meta-register'
const data = { /*页面/区块 schema*/ }
useCanvas().importSchema(data)
```
**导出 schema:**
```javascript
import { useCanvas } from '@opentiny/tiny-engine-meta-register'
useCanvas().exportSchema()
```
## 页面 schema相关操作
> 主要描述对页面 schema 的增删查改操作
### 获取当前页面/区块 schema:
```javascript
import { useCanvas } from '@opentiny/tiny-engine-meta-register'
useCanvas().getPageSchema()
```
### 获取当前选中节点 schema:
```javascript
import { useProperties } from '@opentiny/tiny-engine-meta-register'
const schema = useProperties().getSchema()
```
### 根据 id 查询对应的 节点schema(schema 片段)
```javascript
import { useCanvas } from '@opentiny/tiny-engine-meta-register'
const schema = useCanvas().getNode('453254', false)
```
类型:
```typescript
/**
* 根据节点 id 获取 schema 片段
* id: schema id
* parent: 是否需要同时获取 parent 节点
*/
type getNode = (id: string, parent: boolean) => INode | { node: INode; parent: INode }
```
### 节点操作
#### 插入节点
使用示例:
```javascript
import { useCanvas } from '@opentiny/tiny-engine-meta-register'
useCanvas().operateNode({
type: 'insert',
parentId: '432423',
newNodeData: { componentName: 'div', props: {}, children: [] },
position: 'after',
referTargetNodeId: '898432'
})
```
类型:
```typescript
interface IInsertOperation {
// 操作类型为 insert
type: 'insert';
// 要插入的节点的 父节点 id
parentId: string;
// 新节点数据
newNodeData: INode;
// 相对节点的 id比如我们想要插入父节点 id 中 第 5 个 children 的后面,或者前面
referTargetNodeId: string;
// 相对节点的位置
position: 'after' | 'before';
}
```
#### 删除节点
使用示例:
```javascript
import { useCanvas } from '@opentiny/tiny-engine-meta-register'
useCanvas().operateNode({
type: 'delete',
id: '432423'
})
```
类型:
```typescript
interface IDeleteOperation {
type: 'delete';
id: string;
}
```
#### 修改节点 props
使用示例:
```javascript
import { useCanvas } from '@opentiny/tiny-engine-meta-register'
useCanvas().operateNode({
type: 'changeProps',
id: '432423',
value: { text: 'TinyEngine' },
option: { overwrite: false }
})
```
类型:
```typescript
interface IChangePropsOperation {
type: 'changeProps';
// 节点 id
id: string;
// 新的 props 值
value: Record<string, any>;
// 操作类型:是否覆写
option: { overwrite: boolean; }
}
```
#### 更新节点属性
使用示例:
```javascript
import { useCanvas } from '@opentiny/tiny-engine-meta-register'
useCanvas().operateNode({
type: 'updateAttributes',
id: '432423',
value: { props: { ... }, loop: { ... } },
overwrite: boolean
})
```
类型:
```typescript
interface IUpdateAttrOperation {
type: 'updateAttributes';
id: string;
// 对节点的属性修改
value: Record<string, any>;
// 是否是直接覆盖
overwrite: boolean;
}
```

View File

@ -36,7 +36,8 @@ import {
getOptions,
getMetaApi,
META_SERVICE,
META_APP
META_APP,
useNotify
} from '@opentiny/tiny-engine-meta-register'
import { constants } from '@opentiny/tiny-engine-utils'
import * as ast from '@opentiny/tiny-engine-common/js/ast'
@ -72,7 +73,7 @@ export default {
const removeNode = (node) => {
const { pageState } = useCanvas()
footData.value = useCanvas().canvasApi.value.getNodePath(node?.id)
footData.value = useCanvas().getNodePath(node?.id)
pageState.currentSchema = {}
pageState.properties = null
}
@ -102,10 +103,11 @@ export default {
// 1.
// 2. ()
// 3.
// 4. 0
const showConfirm = !isSaved || pageSchema !== oldPageSchema
if (!showConfirm || showModal) {
if (!showConfirm || showModal || (useHistory().historyState?.index === 0 && isSaved !== oldIsSaved)) {
return
}
@ -129,7 +131,6 @@ export default {
useModal().confirm({
title: '提示',
message: renderMsg,
status: 'info',
exec: callback,
cancel: callback,
hide: () => {
@ -139,19 +140,21 @@ export default {
}
)
const nodeSelected = (node, parent, type) => {
const nodeSelected = (node, parent, type, id) => {
const { toolbars } = useLayout().layoutState
if (type !== 'clickTree') {
useLayout().closePlugin()
}
const { getSchema, getNodePath } = useCanvas().canvasApi.value
const { getSchema, getNodePath } = useCanvas()
const schemaItem = useCanvas().getNodeById(id)
const pageSchema = getSchema()
const schema = getSchema()
// schema
useProperties().getProps(node || schema, parent)
useCanvas().setCurrentSchema(node || schema)
footData.value = getNodePath(node?.id)
useProperties().getProps(schemaItem || pageSchema, parent)
useCanvas().setCurrentSchema(schemaItem || pageSchema)
footData.value = getNodePath(schemaItem?.id)
toolbars.visiblePopover = false
}
@ -218,13 +221,16 @@ export default {
// canvas/render使
getMaterial: useMaterial().getMaterial,
addHistory: useHistory().addHistory,
registerBlock: useMaterial().registerBlock,
request: getMetaApi(META_SERVICE.Http).getHttp(),
getPageById: getMetaApi(META_APP.AppManage).getPageById,
getPageAncestors: usePage().getAncestors,
getBaseInfo: () => getMetaApi(META_SERVICE.GlobalService).getBaseInfo(),
addHistoryDataChangedCallback,
ast
ast,
getBlockByName: useMaterial().getBlockByName,
useModal,
useMessage,
useNotify
},
isBlock,
CanvasLayout,

View File

@ -11,11 +11,14 @@
*/
/* eslint-disable no-new-func */
import { reactive, ref } from 'vue'
import { constants } from '@opentiny/tiny-engine-utils'
import { useHistory } from '@opentiny/tiny-engine-meta-register'
import { reactive, ref, toRaw } from 'vue'
import * as jsonDiffPatch from 'jsondiffpatch'
import DiffMatchPatch from 'diff-match-patch'
import { constants, utils } from '@opentiny/tiny-engine-utils'
import { useHistory, getMetaApi, useMessage } from '@opentiny/tiny-engine-meta-register'
const { COMPONENT_NAME } = constants
const { deepClone } = utils
const defaultPageState = {
currentVm: null,
@ -53,6 +56,7 @@ const defaultSchema = {
const canvasApi = ref({})
const isCanvasApiReady = ref(false)
const nodesMap = ref(new Map())
const initCanvasApi = (newCanvasApi) => {
canvasApi.value = newCanvasApi
@ -60,11 +64,83 @@ const initCanvasApi = (newCanvasApi) => {
}
const pageState = reactive({ ...defaultPageState, loading: true })
const rootSchema = ref([
{
id: 0,
componentName: 'div',
props: pageState.pageSchema?.props || {},
children: pageState.pageSchema?.children || []
}
])
const generateNodesMap = (nodes, parent) => {
nodes.forEach((nodeItem) => {
if (!nodeItem.id) {
nodeItem.id = utils.guid()
}
nodesMap.value.set(nodeItem.id, {
node: nodeItem,
parent
})
if (Array.isArray(nodeItem.children) && nodeItem.children.length) {
generateNodesMap(nodeItem.children, nodeItem)
}
})
}
const jsonDiffPatchInstance = jsonDiffPatch.create({
objectHash: function (obj, index) {
return obj.fileName || obj.id || `$$index:${index}`
},
arrays: {
detectMove: true,
includeValueOnMove: false
},
textDiff: {
diffMatchPatch: DiffMatchPatch,
minLength: 60
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
propertyFilter: function (name, context) {
return name.slice(0, 1) !== '$'
},
cloneDiffValues: false
})
const { publish } = useMessage()
// 重置画布数据
const resetCanvasState = async (state = {}) => {
const previousSchema = JSON.parse(JSON.stringify(pageState.pageSchema))
Object.assign(pageState, defaultPageState, state)
await canvasApi.value?.setSchema(pageState.pageSchema)
nodesMap.value.clear()
if (pageState.pageSchema) {
if (!pageState.pageSchema.children) {
pageState.pageSchema.children = []
}
rootSchema.value = [
{
id: 0,
componentName: 'div',
props: pageState.pageSchema.props || {},
children: pageState.pageSchema.children
}
]
nodesMap.value.set(0, { node: rootSchema.value, parent: pageState.pageSchema })
generateNodesMap(pageState.pageSchema.children, pageState.pageSchema)
}
const diffPatch = jsonDiffPatchInstance.diff(previousSchema, pageState.pageSchema)
publish({ topic: 'schemaImport', data: { current: pageState.pageSchema, previous: previousSchema, diffPatch } })
}
// 页面重置画布数据
@ -80,11 +156,17 @@ const resetBlockCanvasState = async (state = {}) => {
await resetCanvasState(state)
}
const getDefaultSchema = (componentName = 'Page', fileName = '') => ({
...defaultSchema,
componentName,
fileName
})
const getDefaultSchema = (componentName = 'Page', fileName = '') => {
const DEFAULT_PAGE = getMetaApi('engine.service.page')?.getDefaultPage() || { page_content: { props: {}, css: '' } }
return {
...defaultSchema,
props: DEFAULT_PAGE.page_content?.props || {},
css: DEFAULT_PAGE.page_content?.css || '',
componentName,
fileName
}
}
const setSaved = (flag = false) => {
pageState.isSaved = flag
@ -97,10 +179,13 @@ const clearCanvas = () => {
const { fileName, componentName } = pageState.pageSchema || {}
resetCanvasState({
pageSchema: { ...getDefaultSchema(componentName, fileName) }
pageSchema: { ...deepClone(getDefaultSchema(componentName, fileName)) }
})
setSaved(false)
canvasApi.value?.clearSelect?.()
canvasApi.value?.updateRect?.()
}
const isBlock = () => pageState.isBlock
@ -109,12 +194,12 @@ const isBlock = () => pageState.isBlock
const initData = (schema = { ...defaultSchema }, currentPage) => {
if (schema.componentName === COMPONENT_NAME.Block) {
resetBlockCanvasState({
pageSchema: schema,
pageSchema: toRaw(schema),
loading: false
})
} else {
resetPageCanvasState({
pageSchema: schema,
pageSchema: toRaw(schema),
currentPage,
loading: false
})
@ -145,6 +230,326 @@ const clearCurrentState = () => {
}
const getCurrentPage = () => pageState.currentPage
const getNodeById = (id) => {
return nodesMap.value.get(id)?.node
}
const getNodeWithParentById = (id) => {
return nodesMap.value.get(id)
}
const delNode = (id) => {
nodesMap.value.delete(id)
}
const clearNodes = () => {
nodesMap.value.clear()
}
const setNode = (schema, parent) => {
schema.id = schema.id || utils.guid()
nodesMap.value.set(schema.id, { node: schema, parent })
}
const getNode = (id, parent) => {
return parent ? nodesMap.value.get(id) : nodesMap.value.get(id)?.node
}
const operationTypeMap = {
insert: (operation) => {
const { parentId, newNodeData, position, referTargetNodeId } = operation
const parentNode = getNode(parentId) || pageState.pageSchema
if (!parentNode) {
return {}
}
parentNode.children = parentNode.children || []
if (!newNodeData.id) {
newNodeData.id = utils.guid()
}
if (referTargetNodeId) {
const referenceNode = getNode(referTargetNodeId)
let index = parentNode.children.indexOf(referenceNode)
if (index === -1) {
index = 0
}
index = position === 'before' ? index : index + 1
parentNode.children.splice(index, 0, newNodeData)
setNode(newNodeData, parentNode)
// 递归构建 nodeMap
if (Array.isArray(newNodeData?.children) && newNodeData.children.length) {
const newNode = getNode(newNodeData.id)
generateNodesMap(newNodeData.children, newNode)
}
return {
current: newNodeData,
previous: undefined
}
}
if (position === 'before') {
parentNode.children.unshift(newNodeData)
} else {
parentNode.children.push(newNodeData)
}
setNode(newNodeData, parentNode)
// 递归构建 nodeMap
if (Array.isArray(newNodeData?.children) && newNodeData.children.length) {
const newNode = getNode(newNodeData.id)
generateNodesMap(newNodeData.children, newNode)
}
return {
current: newNodeData,
previous: undefined
}
},
delete: (operation) => {
const { id } = operation
const targetNode = getNode(id, true)
if (!targetNode) {
return
}
const { parent, node } = targetNode
const index = parent.children.indexOf(node)
if (index > -1) {
parent.children.splice(index, 1)
nodesMap.value.delete(node.id)
}
let children = [...(node.children || [])]
// 递归清理 nodesMap
while (children?.length) {
const len = children.length
children.forEach((item) => {
const nodeItem = getNode(item.id)
nodesMap.value.delete(item.id)
if (Array.isArray(nodeItem.children) && nodeItem.children.length) {
children.push(...nodeItem.children)
}
})
children = children.slice(len)
}
return {
current: undefined,
previous: node
}
},
changeProps: (operation) => {
const { id, value, option: changeOption } = operation
let { node } = getNode(id, true) || {}
const previous = deepClone(node)
const { overwrite = false } = changeOption || {}
if (!node) {
node = pageState.pageSchema
}
if (!node.props) {
node.props = {}
}
if (overwrite) {
node.props = value.props
} else {
Object.assign(node.props, value?.props || {})
}
return {
current: node,
previous
}
},
updateAttributes: (operation) => {
const { id, value, overwrite } = operation
const { id: _id, children, ...restAttr } = value
const node = getNode(id)
// 其他属性直接浅 merge
Object.assign(node, restAttr)
// 配置了 overwrite需要将没有传入的属性进行删除不包括 children
if (overwrite) {
const { id: _unUsedId, children: _unUsedChildren, ...restOrigin } = node
const originKeys = Object.keys(restOrigin)
const newKeysSet = new Set(Object.keys(restAttr))
originKeys.forEach((key) => {
if (!newKeysSet.has(key)) {
delete node[key]
}
})
}
if (!Array.isArray(children)) {
// 非数组类型的 children比如是直接的字符串作为 children
if (children || typeof children === 'string') {
node.children = children
}
return
}
const newChildren = children.map((item) => {
if (!item.id) {
item.id = utils.guid()
}
return item
})
// 传了 children 进来,需要找出来被删除的、新增的,剩下的是修改的。
const originChildrenIds = (node.children || []).filter(({ id }) => id).map(({ id }) => id)
const originChildrenSet = new Set(originChildrenIds)
const newChildrenSet = new Set(newChildren.map(({ id }) => id))
// 被删除的项
const deletedIds = originChildrenIds.filter((id) => !newChildrenSet.has(id))
const deletedIdsSet = new Set(deletedIds)
for (const id of deletedIds) {
operationTypeMap.delete({ id })
}
// 筛选出来新增的和修改的
const changedChildren = newChildren.filter(({ id }) => !deletedIdsSet.has(id))
changedChildren.forEach((childItem, index) => {
// 新增
if (!originChildrenSet.has(childItem.id)) {
operationTypeMap.insert({
parentId: id,
newNodeData: childItem,
position: 'after',
referTargetNodeId: changedChildren?.[index]?.id
})
return
}
// 直接改引用插入进来,但是没有构建对应的 Map需要构建Map
if (!getNode(childItem.id)) {
setNode(childItem, node)
// 递归构建 nodeMap
if (Array.isArray(childItem?.children) && childItem.children.length) {
const newNode = getNode(childItem.id)
generateNodesMap(childItem.children, newNode)
}
}
// 递归修改
operationTypeMap.updateAttributes({ id: childItem.id, value: childItem })
})
}
}
const lastUpdateType = ref('')
/**
* @experimental
* @param {*} operation
* @returns
*/
const operateNode = async (operation) => {
if (!operationTypeMap[operation.type]) {
return
}
operationTypeMap[operation.type](operation)
lastUpdateType.value = operation.type
publish({ topic: 'schemaChange', data: { operation } })
if (operation.type !== 'insert') {
// 这里 setTimeout 延时是需要等画布更新渲染完成,然后再更新,才能得到正确的组件 offset
setTimeout(() => {
canvasApi.value.updateRect?.()
}, 0)
}
}
// 获取传入的 schema 与最新 schema 的 diff
const getSchemaDiff = (schema) => {
return jsonDiffPatchInstance.diff(schema, pageState.pageSchema)
}
const patchLatestSchema = (schema) => {
// 这里 pageSchema 需要 deepClone不然 patch 的时候,会 patch 成同一个引用,造成画布无法更新
const diff = jsonDiffPatchInstance.diff(schema, deepClone(pageState.pageSchema))
if (diff) {
jsonDiffPatchInstance.patch(schema, diff)
}
}
const importSchema = (data) => {
let importData = data
if (typeof data === 'string') {
try {
importData = JSON.parse(data)
} catch (error) {
// eslint-disable-next-line no-console
console.error('[useCanvas.importSchema] Invalid data')
}
}
// JSON 格式校验
resetCanvasState({
pageSchema: importData
})
canvasApi.value?.clearSelect?.()
}
const exportSchema = () => {
return JSON.stringify(pageState.pageSchema)
}
const getSchema = () => {
return pageState.pageSchema || {}
}
const getNodePath = (id, nodes = []) => {
const { parent, node } = getNodeWithParentById(id) || {}
node && nodes.unshift({ name: node.componentName, node: id })
if (parent) {
parent && getNodePath(parent.id, nodes)
} else {
nodes.unshift({ name: 'BODY', node: id })
}
return nodes
}
const updateSchema = (data) => {
Object.assign(pageState.pageSchema, data)
publish({ topic: 'schemaChange', data: {} })
}
export default function () {
return {
pageState,
@ -163,6 +568,21 @@ export default function () {
getCurrentPage,
initCanvasApi,
canvasApi,
isCanvasApiReady
isCanvasApiReady,
getNodeById,
getNodeWithParentById,
delNode,
clearNodes,
setNode,
getNode,
operateNode,
lastUpdateType,
getSchemaDiff,
patchLatestSchema,
importSchema,
exportSchema,
getSchema,
getNodePath,
updateSchema
}
}

View File

@ -14,9 +14,14 @@ export function getImportMapData(overrideVersions = {}) {
const blockRequire = {
imports: {
'@opentiny/vue': `${VITE_CDN_DOMAIN}/@opentiny/vue@${importMapVersions.tinyVue}/runtime/tiny-vue.mjs`,
'@opentiny/vue-icon': `${VITE_CDN_DOMAIN}/@opentiny/vue@${importMapVersions.tinyVue}/runtime/tiny-vue-icon.mjs`
'@opentiny/vue-icon': `${VITE_CDN_DOMAIN}/@opentiny/vue@${importMapVersions.tinyVue}/runtime/tiny-vue-icon.mjs`,
'element-plus': `${VITE_CDN_DOMAIN}/element-plus@2.4.2/dist/index.full.mjs`,
'@opentiny/tiny-engine-builtin-component': `${VITE_CDN_DOMAIN}/@opentiny/tiny-engine-builtin-component@^2.0.0/dist/index.mjs`
},
importStyles: [`${VITE_CDN_DOMAIN}/@opentiny/vue-theme@${importMapVersions.tinyVue}/index.css`]
importStyles: [
`${VITE_CDN_DOMAIN}/@opentiny/vue-theme@${importMapVersions.tinyVue}/index.css`,
`${VITE_CDN_DOMAIN}/element-plus@2.4.2/dist/index.css`
]
}
// 以下内容由于物料协议不支持声明子依赖而@opentiny/vue需要依赖所以需要补充

View File

@ -29,9 +29,9 @@
</template>
<script>
import { onMounted, ref, computed, onUnmounted } from 'vue'
import { onMounted, ref, computed, onUnmounted, watch, watchEffect } from 'vue'
import { iframeMonitoring } from '@opentiny/tiny-engine-common/js/monitor'
import { useTranslate, useCanvas, useMaterial } from '@opentiny/tiny-engine-meta-register'
import { useTranslate, useCanvas, useMaterial, useMessage, useResource } from '@opentiny/tiny-engine-meta-register'
import { NODE_UID, NODE_LOOP, DESIGN_MODE } from '../../common'
import { registerHostkeyEvent, removeHostkeyEvent } from './keyboard'
import CanvasMenu, { closeMenu, openMenu } from './components/CanvasMenu.vue'
@ -114,6 +114,25 @@ export default {
if (iframe.value) {
const win = iframe.value.contentWindow
win.thirdPartyDeps = useMaterial().materialState.thirdPartyDeps
const { subscribe, unsubscribe } = useMessage()
const { getSchemaDiff, patchLatestSchema, getSchema, getNode } = useCanvas()
const { appSchemaState } = useResource()
iframe.value.contentWindow.host = {
unsubscribe,
subscribe,
getSchemaDiff,
patchLatestSchema,
watch,
watchEffect,
getSchema,
appSchema: appSchemaState,
schemaUtils: {
getSchema,
getNode
}
}
}
}

View File

@ -124,11 +124,10 @@ import {
updateRect,
copyNode,
getRenderer,
getSchema,
dragStart,
getCurrentElement
} from '../container'
import { useLayout, useMaterial } from '@opentiny/tiny-engine-meta-register'
import { useLayout, useMaterial, useCanvas } from '@opentiny/tiny-engine-meta-register'
import { Popover } from '@opentiny/vue'
import shortCutPopover from './shortCutPopover.vue'
@ -236,7 +235,7 @@ export default {
return !props.resize && parent && parent?.type !== 'JSSlot'
})
const showToParent = computed(() => getCurrent().parent !== getSchema())
const showToParent = computed(() => getCurrent().parent !== useCanvas().getSchema())
const isModal = computed(() => {
const config = useMaterial().getMaterial(props.selectState.componentName)

View File

@ -11,7 +11,7 @@
</template>
<script>
import { ref, computed, watch, nextTick } from 'vue'
import { useLayout } from '@opentiny/tiny-engine-meta-register'
import { useLayout, useCanvas } from '@opentiny/tiny-engine-meta-register'
import { canvasState } from '../container'
export default {
@ -106,6 +106,15 @@ export default {
{ flush: 'post' }
)
watch(
() => sizeStyle.value,
() => {
nextTick(() => {
useCanvas().canvasApi.value?.updateRect?.()
})
}
)
return {
onMouseDown,
onMouseMove,

View File

@ -19,7 +19,8 @@ import {
NODE_TAG,
NODE_LOOP
} from '../../common'
import { useCanvas, useLayout, useResource, useTranslate, useMaterial } from '@opentiny/tiny-engine-meta-register'
import { useCanvas, useLayout, useTranslate, useMaterial } from '@opentiny/tiny-engine-meta-register'
import { utils } from '@opentiny/tiny-engine-utils'
import { isVsCodeEnv } from '@opentiny/tiny-engine-common/js/environments'
import Builtin from '../../render/src/builtin/builtin.json' //TODO 画布内外应该分开
@ -72,15 +73,7 @@ export const getDesignMode = () => getRenderer()?.getDesignMode()
export const setDesignMode = (mode) => getRenderer()?.setDesignMode(mode)
export const getGlobalState = () => getRenderer().getGlobalState()
export const getNode = (id, parent) => getRenderer()?.getNode(id, parent)
export const getSchema = () => getRenderer()?.getSchema()
export const getContext = () => {
return getRenderer().getContext()
}
export const getSchema = () => useCanvas().getPageSchema()
// 记录拖拽状态
export const dragState = reactive({
@ -251,48 +244,51 @@ const getRect = (element) => {
}
const inserAfter = ({ parent, node, data }) => {
const parentChildren = parent.children
const index = parentChildren.indexOf(node)
parent.children.splice(index + 1, 0, data)
if (!data.id) {
data.id = utils.guid()
}
useCanvas().operateNode({
type: 'insert',
parentId: parent.id,
newNodeData: data,
position: 'after',
referTargetNodeId: node.id
})
}
const insertBefore = ({ parent, node, data }) => {
const parentChildren = parent.children
const index = parentChildren.indexOf(node)
parent.children.splice(index, 0, data)
if (!data.id) {
data.id = utils.guid()
}
useCanvas().operateNode({
type: 'insert',
parentId: parent.id,
newNodeData: data,
position: 'before',
referTargetNodeId: node.id
})
}
const insertInner = ({ node, data }, position) => {
node.children = node.children || []
if (position === POSITION.TOP || position === POSITION.LEFT) {
node.children.unshift(data)
} else {
node.children.push(data)
if (!data.id) {
data.id = utils.guid()
}
useCanvas().operateNode({
type: 'insert',
parentId: node.id,
newNodeData: data,
position: [POSITION.TOP, POSITION.LEFT].includes(position) ? 'before' : 'after'
})
}
export const removeNode = ({ parent, node }) => {
const parentChildren = parent.children || parent.value
const index = parentChildren.indexOf(node)
if (index > -1) {
parentChildren.splice(index, 1)
} else {
const templates = parentChildren.filter(({ componentName }) => componentName === 'Template')
templates.forEach((template) => {
const { children } = template
if (children.length) {
children.splice(children.indexOf(node), 1)
}
if (!children.length) {
parentChildren.splice(parentChildren.indexOf(template), 1)
}
})
}
export const removeNode = (id) => {
useCanvas().operateNode({
type: 'delete',
id
})
}
export const removeNodeById = (id) => {
@ -300,7 +296,7 @@ export const removeNodeById = (id) => {
return
}
removeNode(getNode(id, true))
removeNode(id)
clearSelect()
getController().addHistory()
canvasState.emit('remove')
@ -426,13 +422,13 @@ const isAncestor = (ancestor, descendant) => {
let descendantId = typeof descendant === 'string' ? descendant : descendant.id
while (descendantId) {
const { parent } = getNode(descendantId, true) || {}
const { parent } = useCanvas().getNodeWithParentById(descendantId) || {}
if (parent.id === ancestorId) {
if (parent?.id === ancestorId) {
return true
}
descendantId = parent.id
descendantId = parent?.id
}
return false
@ -484,6 +480,7 @@ const setHoverRect = (element, data) => {
const configure = getConfigure(componentName)
const rect = getRect(element)
const { left, height, top, width } = rect
const { getSchema, getNodeWithParentById } = useCanvas()
hoverState.configure = configure
@ -495,7 +492,7 @@ const setHoverRect = (element, data) => {
// 如果拖拽经过的元素是body或者是带有容器属性的盒子并且在元素内部插入,则需要特殊处理
if ((isBodyEl(element) || configure?.isContainer) && rectType === POSITION.IN) {
const { node } = isBodyEl(element) ? { node: getSchema() } : getNode(id, true) || {}
const { node } = isBodyEl(element) ? { node: getSchema() } : getNodeWithParentById(id) || {}
const children = node?.children || []
if (children.length > 0) {
// 如果容器盒子有子节点,则以最后一个子节点为拖拽参照物
@ -649,10 +646,12 @@ export const selectNode = async (id, type) => {
const loopId = type.split('=')[1]
canvasState.loopId = loopId
}
const { node, parent } = getNode(id, true) || {}
const { node, parent } = useCanvas().getNodeWithParentById(id) || {}
let element = querySelectById(id, type)
if (element) {
if (element && node) {
const { rootSelector } = getConfigure(node.componentName)
element = rootSelector ? element.querySelector(rootSelector) : element
}
@ -662,7 +661,7 @@ export const selectNode = async (id, type) => {
await scrollToNode(element)
setSelectRect(element)
canvasState.emit('selected', node, parent, type)
canvasState.emit('selected', node, parent, type, id)
return node
}
@ -674,7 +673,7 @@ export const hoverNode = (id, data) => {
export const insertNode = (node, position = POSITION.IN, select = true) => {
if (!node.parent) {
insertInner({ node: canvasState.schema, data: node.data }, position)
insertInner({ node: useCanvas().pageState.pageSchema, data: node.data }, position)
} else {
switch (position) {
case POSITION.TOP:
@ -709,7 +708,8 @@ export const copyNode = (id) => {
if (!id) {
return
}
const { node, parent } = getNode(id, true)
const { node, parent } = useCanvas().getNodeWithParentById(id)
inserAfter({ parent, node, data: copyObject(node) })
getController().addHistory()
@ -721,15 +721,18 @@ export const onMouseUp = () => {
const absolute = canvasState.type === 'absolute'
const sourceId = data?.id
const lineId = lineState.id
const { getNodeWithParentById, getSchema } = useCanvas()
if (draging && !forbidden) {
const { parent, node } = getNode(lineId, true) || {} // target
const targetNode = { parent, node, data: toRaw(data) }
const { parent, node } = getNodeWithParentById(lineId) || {} // target
const insertData = toRaw(data)
const targetNode = { parent, node, data: { ...insertData, children: insertData.children || [] } }
if (sourceId) {
// 内部拖拽
if (sourceId !== lineId && !absolute) {
removeNode(getNode(sourceId, true))
removeNode(sourceId)
insertNode(targetNode, position)
}
} else {
@ -748,26 +751,6 @@ export const onMouseUp = () => {
dragEnd()
}
export const setPageCss = (css = '') => {
const id = 'page-css'
const document = getDocument()
let element = document.getElementById(id)
const head = document.querySelector('head')
document.body.setAttribute('style', '')
if (!element) {
element = document.createElement('style')
element.setAttribute('type', 'text/css')
element.setAttribute('id', id)
element.innerHTML = css
head.appendChild(element)
} else {
element.innerHTML = css
}
}
export const addStyle = (href) => appendStyle(href, getDocument())
export const addScript = (src) => appendScript(src, getDocument())
@ -786,59 +769,10 @@ export const setLocales = (messages, merge) => {
})
}
export const setState = (state) => {
getRenderer().setState(state)
}
export const setUtils = (utils, clear, isForceRefresh) => {
getRenderer().setUtils(utils, clear, isForceRefresh)
}
export const updateUtils = (utils) => {
getRenderer().updateUtils(utils)
}
export const deleteUtils = (utils) => {
getRenderer().deleteUtils(utils)
}
export const deleteState = (variable) => {
getRenderer().deleteState(variable)
}
export const setGlobalState = (state) => {
useResource().resState.globalState = state
getRenderer().setGlobalState(state)
}
export const getNodePath = (id, nodes = []) => {
const { parent, node } = getNode(id, true) || {}
node && nodes.unshift({ name: node.componentName, node: id })
if (parent) {
parent && getNodePath(parent.id, nodes)
} else {
nodes.unshift({ name: 'BODY', node: id })
}
return nodes
}
export const setSchema = async (schema) => {
clearHover()
clearSelect()
canvasState.schema = await getRenderer()?.setSchema(schema)
return canvasState.schema
}
export const setConfigure = (configure) => {
getRenderer().setConfigure(configure)
}
export const setProps = (data) => getRenderer()?.setProps(data)
export const setI18n = (data) => {
const messages = data || useTranslate().getData()
const i18n = getRenderer().getI18n()
@ -868,12 +802,8 @@ export const canvasDispatch = (name, data, doc = getDocument()) => {
export const canvasApi = {
dragStart,
updateRect,
getContext,
getNodePath,
dragMove,
setLocales,
setState,
deleteState,
getRenderer,
clearSelect,
selectNode,
@ -881,41 +811,26 @@ export const canvasApi = {
insertNode,
removeNode,
addComponent,
setPageCss,
addScript,
addStyle,
getNode,
getCurrent,
setSchema,
setUtils,
updateUtils,
deleteUtils,
getSchema,
setI18n,
getCanvasType,
setCanvasType,
setProps,
setGlobalState,
getGlobalState,
getDesignMode,
setDesignMode,
getDocument,
canvasDispatch,
Builtin,
setDataSourceMap: (...args) => {
return canvasState.renderer.setDataSourceMap(...args)
removeBlockCompsCache: (...args) => {
return canvasState.renderer.removeBlockCompsCache(...args)
},
getDataSourceMap: (...args) => {
return canvasState.renderer.getDataSourceMap(...args)
updateCanvas: (...args) => {
return canvasState.renderer.updateCanvas(...args)
}
}
export const initCanvas = ({ renderer, iframe, emit, controller }) => {
const currentSchema = getSchema()
// 在点击刷新按钮的情况下继续保留最新的schema.json
const schema = currentSchema ? currentSchema : useCanvas().getPageSchema()
canvasState.iframe = iframe
canvasState.emit = emit
// 存放画布外层传进来的插件api
@ -930,11 +845,6 @@ export const initCanvas = ({ renderer, iframe, emit, controller }) => {
senterMessage({ type: 'i18nReady', value: true }, '*')
}
setGlobalState(useResource().resState.globalState)
renderer.setDataSourceMap(useResource().resState.dataSource)
// 设置画布全局的utils工具类上下文环境
setUtils(useResource().resState.utils)
setSchema(schema)
setConfigure(useMaterial().getConfigureMap())
canvasDispatch('updateDependencies', { detail: useMaterial().materialState.thirdPartyDeps })
canvasState.loading = false

View File

@ -14,7 +14,6 @@ import {
getCurrent,
insertNode,
selectNode,
getSchema,
POSITION,
removeNodeById,
allowInsert,
@ -22,7 +21,7 @@ import {
clearHover,
hoverState
} from './container'
import { useHistory } from '@opentiny/tiny-engine-meta-register'
import { useHistory, useCanvas } from '@opentiny/tiny-engine-meta-register'
import { copyObject } from '../../common'
import { getClipboardSchema, setClipboardSchema } from './utils'
@ -63,7 +62,7 @@ const handlerArrow = (keyCode) => {
if (schema) {
index = parent.children.indexOf(schema)
} else {
schema = getSchema()
schema = useCanvas().getSchema()
}
let obj = {

View File

@ -1,6 +1,6 @@
{
"name": "@opentiny/tiny-engine-canvas",
"version": "2.0.0-rc.4",
"version": "2.1.0",
"publishConfig": {
"access": "public"
},
@ -43,9 +43,11 @@
"@opentiny/tiny-engine-meta-register": "workspace:*",
"@opentiny/tiny-engine-utils": "workspace:*",
"@opentiny/tiny-engine-webcomponent-core": "workspace:*",
"@vue/babel-plugin-jsx": "1.1.1",
"@vue/babel-plugin-jsx": "^1.2.5",
"@vue/shared": "^3.3.4",
"@vueuse/core": "^9.6.0",
"diff-match-patch": "^1.0.5",
"jsondiffpatch": "^0.6.0",
"postcss": "^8.4.31",
"postcss-selector-parser": "^7.0.0"
},

View File

@ -0,0 +1,22 @@
<template>
<div class="block-error-container">区块 {{ name }} 加载错误</div>
</template>
<script setup>
import { defineProps } from 'vue'
defineProps({
name: {
type: String,
default: ''
}
})
</script>
<style lang="less" scoped>
.block-error-container {
// 使 css
background-color: #f23030;
padding: 10px;
}
</style>

View File

@ -0,0 +1,14 @@
<template>
<div>区块 {{ name }} 加载中</div>
</template>
<script setup>
import { defineProps } from 'vue'
defineProps({
name: {
type: String,
default: ''
}
})
</script>

View File

@ -11,27 +11,22 @@
*/
import { provide, watch, defineComponent, PropType, ref, inject, onUnmounted, h, Ref } from 'vue'
import { useBroadcastChannel } from '@vueuse/core'
import { constants } from '@opentiny/tiny-engine-utils'
import { getDesignMode, setDesignMode, setController, useCustomRenderer, getController } from './canvas-function'
import { setConfigure } from './material-function'
import { removeBlockCompsCache, setConfigure } from './material-function'
import { useUtils, useBridge, useDataSourceMap, useGlobalState } from './application-function'
import { IPageSchema, useContext, usePageContext, useSchema } from './page-block-function'
import { api, setCurrentApi } from './canvas-function/canvas-api'
import { getPageAncestors } from './material-function/page-getter'
import CanvasEmpty from './canvas-function/CanvasEmpty.vue'
import { setCurrentPage } from './canvas-function/page-switcher'
const { BROADCAST_CHANNEL } = constants
import { useThrottleFn } from '@vueuse/core'
// global-context singleton
const { context: globalContext, setContext: setGlobalContext } = useContext()
const { refreshKey, utils, getUtils, setUtils, updateUtils, deleteUtils } = useUtils(globalContext)
const { bridge, setBridge, getBridge } = useBridge()
const { refreshKey, utils, getUtils, setUtils } = useUtils(globalContext)
const { bridge } = useBridge()
const { getDataSourceMap, setDataSourceMap } = useDataSourceMap()
const { getGlobalState, setGlobalState, stores } = useGlobalState()
const { setGlobalState, stores } = useGlobalState()
const updateGlobalContext = () => {
const context = {
utils,
@ -46,20 +41,12 @@ const updateGlobalContext = () => {
setGlobalContext(context, true)
}
updateGlobalContext()
const activePageContext = usePageContext()
export const activePageContext = usePageContext()
const {
schema: activeSchema,
getSchema,
setSchema,
getState,
setState,
deleteState,
getProps,
setProps,
getMethods,
setMethods,
setPagecss
setPageCss
} = useSchema(activePageContext, {
utils,
bridge,
@ -67,44 +54,31 @@ const {
getDataSourceMap
})
const { getRenderer, setRenderer } = useCustomRenderer()
const getNode = (id, parent) => (id ? activePageContext.getNode(id, parent) : activeSchema)
const { getContext, getRoot, setNode, setCondition, getCondition, getConditions } = activePageContext
const { setCondition } = activePageContext
const updateCanvas = () => {
refreshKey.value++
}
setCurrentApi({
getUtils,
setUtils,
updateUtils,
deleteUtils,
getBridge,
setBridge,
getMethods,
setMethods,
setController,
setConfigure,
getSchema,
setSchema,
getState,
deleteState,
setState,
getProps,
setProps,
getContext,
getNode,
getRoot,
setPagecss,
setCondition,
getCondition,
getConditions,
getGlobalState,
getDataSourceMap,
setDataSourceMap,
setGlobalState,
setNode,
getRenderer,
setRenderer,
getDesignMode,
setDesignMode
setDesignMode,
removeBlockCompsCache,
updateCanvas
})
const throttleUpdateSchema = useThrottleFn(
() => {
window.host.patchLatestSchema(activeSchema)
},
100,
true
)
export default defineComponent({
props: {
entry: {
@ -162,18 +136,83 @@ export default defineComponent({
onUnmounted(() => {
cancel()
})
window.host.subscribe({
topic: 'schemaChange',
subscriber: 'canvasRenderer',
callback: throttleUpdateSchema
})
window.host.subscribe({
topic: 'schemaImport',
subscriber: 'canvasRenderer',
callback: () => {
setSchema(window.host.getSchema())
}
})
watch(
() => activeSchema.css,
(value) => {
setPageCss(value)
}
)
const utilsWatchCanceler = window.host.watch(
() => window.host.appSchema?.utils,
(data) => {
setUtils(data)
},
{
immediate: true,
deep: true
}
)
const dataSourceWatchCanceler = window.host.watch(
() => window.host.appSchema?.dataSource,
(data) => {
setDataSourceMap(data)
},
{
immediate: true,
deep: true
}
)
const globalStateWatchCanceler = window.host.watch(
() => window.host.appSchema?.globalState,
(data) => {
setGlobalState(data)
},
{
immediate: true,
deep: true
}
)
onUnmounted(() => {
window.host.unsubscribe({
topic: 'schemaChange',
subscriber: 'canvasRenderer'
})
window.host.unsubscribe({
topic: 'schemaImport',
subscriber: 'canvasRenderer'
})
utilsWatchCanceler()
dataSourceWatchCanceler()
globalStateWatchCanceler()
})
}
let schema = activeSchema
let setCurrentSchema
let setCurrentMethod = setMethods
if (pageContext.pageId && !props.active && !props.entry) {
// 注意顶层使用activeSchema和对应的api
const {
schema: inActiveSchema,
setSchema: setInactiveSchema,
setMethods: setInactiveMethods
} = useSchema(pageContext, {
const { schema: inActiveSchema, setSchema: setInactiveSchema } = useSchema(pageContext, {
utils,
bridge,
stores,
@ -181,30 +220,10 @@ export default defineComponent({
})
schema = inActiveSchema
setCurrentSchema = setInactiveSchema
setCurrentMethod = setInactiveMethods
}
provide('rootSchema', schema)
const { post } = useBroadcastChannel({ name: BROADCAST_CHANNEL.SchemaLength })
watch(
() => schema?.children?.length,
(length) => {
post(length)
}
)
// 这里监听schema.methods为了保证methods上下文环境始终为最新
watch(
() => schema.methods,
(value) => {
setCurrentMethod(value, true)
},
{
deep: true
}
)
if (!props.entry) {
watch(
[() => props.active, () => props.renderSchema],

View File

@ -1,16 +1,7 @@
import { reset } from '../data-utils'
export function useBridge() {
const bridge = {}
const setBridge = (data, clear = false) => {
clear && reset(bridge)
Object.assign(bridge, data)
}
const getBridge = () => bridge
return {
bridge,
setBridge,
getBridge
bridge
}
}

View File

@ -5,9 +5,6 @@ const Func = Function
export function useGlobalState() {
const globalState = ref([])
const getGlobalState = () => {
return globalState.value
}
const setGlobalState = (data = []) => {
globalState.value = data
@ -28,7 +25,6 @@ export function useGlobalState() {
})
return {
globalState,
getGlobalState,
setGlobalState,
stores
}

View File

@ -18,10 +18,20 @@ export function useUtils(context: Record<string, any>) {
const utils: Record<string, Function | any> = {}
const getUtils = () => utils
const setUtils = (data: Array<IUtil>, clear = false, isForceRefresh = false) => {
if (clear) {
reset(utils)
const setUtils = (data: Array<IUtil>) => {
if (!Array.isArray(data)) {
return
}
// 筛选出来已经被删除的 key
const newKeys = new Set(data.map(({ name }) => name))
const currentKeys = Object.keys(utils)
const deletedUtilsKeys = currentKeys.filter((item) => !newKeys.has(item))
for (const key of deletedUtilsKeys) {
delete utils[key]
}
const utilsCollection = {}
// 目前画布还不具备远程加载utils工具类的功能目前只能加载TinyVue组件库中的组件工具
data?.forEach((item) => {
@ -44,31 +54,13 @@ export function useUtils(context: Record<string, any>) {
})
Object.assign(utils, utilsCollection)
// 因为工具类并不具有响应式行为所以需要通过修改key来强制刷新画布
if (isForceRefresh) {
refreshKey.value++
}
}
const updateUtils = (data: Array<IUtil>) => {
setUtils(data, false, true)
}
const deleteUtils = (data: Array<IUtil>) => {
data?.forEach((item) => {
if (utils[item.name]) {
delete utils[item.name]
}
})
setUtils([], false, true)
refreshKey.value++
}
return {
refreshKey,
utils,
getUtils,
setUtils,
updateUtils,
deleteUtils
setUtils
}
}

View File

@ -11,8 +11,6 @@
*/
import { getController } from '../render'
import { api } from '../RenderMain'
import { useModal } from '@opentiny/tiny-engine-meta-register'
const NAME_PREFIX = {
loop: 'loop',
@ -64,7 +62,6 @@ const removeState = (pageSchema, variableName) => {
}
const setStateWithSourceRef = (pageSchema, variableName, sourceRef, data) => {
api.setState({ [variableName]: data })
pageSchema.state[variableName] = data
if (sourceRef.value.data?.option?.isSync) {
@ -104,7 +101,7 @@ const defaultHandlerTemplate = ({ node, sourceRef, schemaId, pageSchema }) => {
}
}
const generateAssginColumns = (newColumns, oldColumns) => {
const generateAssignColumns = (newColumns, oldColumns) => {
newColumns.forEach((item) => {
const targetColumn = oldColumns.find((value) => value.field === item.field)
if (targetColumn) {
@ -114,22 +111,38 @@ const generateAssginColumns = (newColumns, oldColumns) => {
return newColumns
}
const askShouldImportData = ({ node, sourceRef }) => {
useModal().confirm({
message: '检测到表格存在配置的数据,是否需要引入?',
exec() {
const sourceColums = sourceRef.value?.data?.columns?.map(({ title, field }) => ({ title, field })) || []
// 这里需要找到对应列,然后进行列合并
node.props.columns = generateAssginColumns(sourceColums, node.props.columns)
},
cancel() {
node.props.columns = [...(sourceRef.value.data?.columns || [])]
}
})
const askShouldImportData = ({ node, sourceRef, updateKey }) => {
const { publish } = getController().useMessage()
getController()
.useModal()
.confirm({
message: '检测到表格存在配置的数据,是否需要引入?',
exec() {
try {
const sourceColumns = sourceRef.value?.data?.columns?.map(({ title, field }) => ({ title, field })) || []
// 这里需要找到对应列,然后进行列合并
node.props.columns = generateAssignColumns(sourceColumns, node.props.columns)
publish({ topic: 'schemaChange', data: {} })
updateKey.value++
} catch (error) {
getController().useNotify({
type: 'error',
message: '引入配置数据失败'
})
}
},
cancel() {
node.props.columns = [...(sourceRef.value.data?.columns || [])]
publish({ topic: 'schemaChange', data: {} })
}
})
}
const updateNodeHandler = ({ node, sourceRef, pageSchema, sourceName, methodName }) => {
if (!node || !node.props) {
const updateNodeHandler = ({ node, sourceRef, pageSchema, sourceName, methodName, updateKey }) => {
if (!node || !node.props || !sourceName) {
return
}
@ -137,7 +150,7 @@ const updateNodeHandler = ({ node, sourceRef, pageSchema, sourceName, methodName
delete node?.props?.data
if (node.props.columns.length) {
askShouldImportData({ node, sourceRef })
askShouldImportData({ node, sourceRef, updateKey })
} else {
node.props.columns = [...(sourceRef.value.data?.columns || [])]
}
@ -176,7 +189,7 @@ this.dataSourceMap.${sourceName}.load().then((res) => {
}
const extraHandlerMap = {
TinyGrid: ({ node, sourceRef, schemaId, pageSchema }) => {
TinyGrid: ({ node, sourceRef, schemaId, pageSchema, updateKey }) => {
const sourceName = sourceRef.value?.name
const methodName = `${NAME_PREFIX.table}${schemaId}`
@ -185,7 +198,7 @@ const extraHandlerMap = {
value: `{ api: this.${methodName} }`
}
const updateNode = () => updateNodeHandler({ node, sourceRef, pageSchema, sourceName, methodName })
const updateNode = () => updateNodeHandler({ node, sourceRef, pageSchema, sourceName, methodName, updateKey })
const clearBindVar = () => {
// 当数据源组件children字段为空时及时清空创建的methods
@ -272,7 +285,7 @@ const extraHandlerMap = {
}
}
export const getHandler = ({ node, sourceRef, schemaId, pageSchema }) =>
export const getHandler = ({ node, sourceRef, schemaId, pageSchema, updateKey }) =>
extraHandlerMap[node.componentName]
? extraHandlerMap[node.componentName]({ node, sourceRef, schemaId, pageSchema })
? extraHandlerMap[node.componentName]({ node, sourceRef, schemaId, pageSchema, updateKey })
: defaultHandlerTemplate({ node, sourceRef, schemaId, pageSchema })

View File

@ -1,17 +1,16 @@
<template>
<component :is="tag" v-bind="$attrs">
<component :is="tag" v-bind="$attrs" :key="updateKey">
<slot>
<canvas-placeholder>{{ source?.name || '未选择数据源' }}</canvas-placeholder>
<canvas-placeholder></canvas-placeholder>
</slot>
</component>
</template>
<script>
import { ref, watch, computed, inject } from 'vue'
import { getController } from '../render'
import { ref, watch, computed } from 'vue'
import CanvasPlaceholder from './CanvasPlaceholder.vue'
import { getHandler } from './CanvasCollection.js'
import { getController } from '../render'
import { getHandler } from './CanvasCollection'
export const fetchDataSourceDetail = (dataSourceId) =>
getController().request.get(`/app-center/api/sources/detail/${dataSourceId}`) // TODO:
@ -21,6 +20,7 @@ export default {
CanvasPlaceholder
},
props: {
// TODO: ()
tag: {
type: String,
default: 'div'
@ -33,7 +33,8 @@ export default {
},
setup(props) {
const source = ref(null)
const pageSchema = inject('rootSchema')
// key tiny-grid fetchData
const updateKey = ref(0)
if (props.dataSource) {
fetchDataSourceDetail(props.dataSource).then((res) => {
@ -42,41 +43,45 @@ export default {
}
let handler
watch(
() => props.dataSource,
async (value) => {
if (value) {
source.value = await fetchDataSourceDetail(value)
if (props.schema?.children[0]) {
const { getSchema, getNode } = window.host.schemaUtils
const node = getNode(props.schema?.children?.[0]?.id)
if (node) {
handler = getHandler({
sourceRef: source,
node: props.schema?.children[0],
node,
schemaId: props.schema.id,
pageSchema
pageSchema: getSchema(),
updateKey
})
}
handler?.updateNode()
updateKey.value++
}
}
)
const isEmpty = computed(() => {
const { children } = props.schema || {}
return Array.isArray(children) ? !children.length : !children
})
watch(
() => isEmpty.value,
(value) => {
const { getSchema, getNode } = window.host.schemaUtils
const pageSchema = getSchema()
if (value) {
// state,methodsetup
if (handler) {
handler.clearBindVar()
} else {
const schemaId = props.schema?.id
// handlerschemaId
Object.keys(pageSchema.methods || {})?.some((item) => {
if (item.includes(schemaId)) {
@ -87,20 +92,26 @@ export default {
})
}
} else {
if (props.schema?.children[0]) {
const node = getNode(props.schema?.children?.[0]?.id)
if (node) {
handler = getHandler({
sourceRef: source,
node: props.schema?.children[0],
node,
schemaId: props.schema.id,
pageSchema
pageSchema,
updateKey
})
handler.updateNode()
}
}
const { publish } = getController().useMessage()
publish({ topic: 'schemaChange', data: {} })
updateKey.value++
}
)
return { source }
return { source, updateKey }
}
}
</script>

View File

@ -83,7 +83,7 @@
"content": [
{
"property": "name",
"type": "String",
"type": "string",
"label": {
"text": {
"zh_CN": "插槽名称"
@ -97,7 +97,7 @@
},
{
"property": "params",
"type": "String",
"type": "string",
"defaultValue": "",
"label": {
"text": {
@ -246,7 +246,7 @@
"content": [
{
"property": "condition",
"type": "Boolean",
"type": "boolean",
"defaultValue": true,
"label": {
"text": {
@ -262,7 +262,7 @@
},
{
"property": "style",
"type": "String",
"type": "string",
"defaultValue": "",
"label": {
"text": {
@ -278,7 +278,7 @@
},
{
"property": "dataSource",
"type": "String",
"type": "string",
"defaultValue": "",
"bindState": false,
"label": {
@ -332,7 +332,7 @@
"content": [
{
"property": "text",
"type": "String",
"type": "string",
"defaultValue": "TinyEngine 前端可视化设计器,为设计器开发者提供定制服务,在线构建出自己专属的设计器。",
"label": {
"text": {
@ -404,7 +404,7 @@
"content": [
{
"property": "name",
"type": "String",
"type": "string",
"defaultValue": "IconDel",
"bindState": true,
"label": {
@ -474,7 +474,7 @@
"content": [
{
"property": "src",
"type": "String",
"type": "string",
"defaultValue": "",
"bindState": true,
"label": {
@ -557,6 +557,7 @@
"schema": {
"componentName": "Text",
"props": {
"style": "display: inline-block;",
"text": "TinyEngine 前端可视化设计器,为设计器开发者提供定制服务,在线构建出自己专属的设计器。"
}
}

View File

@ -5,6 +5,7 @@ import renderer from '../render'
function defaultRenderer(schema, refreshKey, entry, active, isPage = true) {
// 渲染画布增加根节点,与出码和预览保持一致
const rootChildrenSchema = {
id: 0,
componentName: 'div',
// 手动添加一个唯一的属性后续在画布选中此节点时方便处理额外的逻辑。由于没有修改schema不会影响出码
props: { ...schema.props, 'data-id': 'root-container', 'data-page-active': active },
@ -24,6 +25,7 @@ function defaultRenderer(schema, refreshKey, entry, active, isPage = true) {
}
return h(
// TODO: 这里顶层的 i18n-host 在不支持 webComponent 的区块之后应该也不需要webComponent 的 i18n provider 了
'tiny-i18n-host',
{
locale: 'zh_CN',

View File

@ -3,7 +3,7 @@ import { transformSync } from '@babel/core'
import i18nHost from '@opentiny/tiny-engine-i18n-host'
import { globalNotify } from '../canvas-function'
import { collectionMethodsMap, customElements, getComponent, getIcon } from '../material-function'
import { collectionMethodsMap, getComponent, getIcon } from '../material-function'
import { newFn } from '../data-utils'
import { renderDefault } from '../render'
@ -61,8 +61,7 @@ const transformJSX = (code) => {
[
babelPluginJSX,
{
pragma: 'h',
isCustomElement: (name) => customElements[name]
pragma: 'h'
}
]
]
@ -152,43 +151,39 @@ const parseJSXFunction = (data, _scope, ctx) => {
export const generateFn = (innerFn, context?) => {
return (...args) => {
// 如果有数据源标识则表格的fetchData返回数据源的静态数据
const sourceId = collectionMethodsMap[innerFn.realName || innerFn.name]
if (sourceId) {
return innerFn.call(context, ...args)
} else {
let result = null
// 这里是为了兼容用户写法报错导致画布异常但无法捕获promise内部的异常
try {
result = innerFn.call(context, ...args)
} catch (error) {
globalNotify({
type: 'warning',
title: `函数:${innerFn.name}执行报错`,
message: error?.message || `函数:${innerFn.name}执行报错,请检查语法`
})
}
let result = null
// 这里注意如果innerFn返回的是一个promise则需要捕获异常重新返回默认一条空数据
if (result.then) {
result = new Promise((resolve) => {
result.then(resolve).catch((error) => {
globalNotify({
type: 'warning',
title: '异步函数执行报错',
message: error?.message || '异步函数执行报错,请检查语法'
})
// 这里需要至少返回一条空数据,方便用户使用表格默认插槽
resolve({
result: [{}],
page: { total: 1 }
})
// 这里是为了兼容用户写法报错导致画布异常但无法捕获promise内部的异常
try {
result = innerFn.call(context, ...args)
} catch (error) {
globalNotify({
type: 'warning',
title: `函数:${innerFn.name}执行报错`,
message: error?.message || `函数:${innerFn.name}执行报错,请检查语法`
})
}
// 这里注意如果innerFn返回的是一个promise则需要捕获异常重新返回默认一条空数据
if (result.then) {
result = new Promise((resolve) => {
result.then(resolve).catch((error) => {
globalNotify({
type: 'warning',
title: '异步函数执行报错',
message: error?.message || '异步函数执行报错,请检查语法'
})
// 这里需要至少返回一条空数据,方便用户使用表格默认插槽
resolve({
result: [{}],
page: { total: 1 }
})
})
}
return result
})
}
return result
}
}
const parseJSFunction = (data, _scope, ctx) => {

View File

@ -10,3 +10,10 @@ export const newFn = (...argv) => {
const Fn = Function
return new Fn(...argv)
}
export const getDeletedKeys = (objA, objB) => {
const keyA = Object.keys(objA)
const keyB = new Set(Object.keys(objB))
return keyA.filter((item) => !keyB.has(item))
}

View File

@ -14,7 +14,6 @@ import { getCurrentInstance, nextTick, provide, inject } from 'vue'
import { I18nInjectionKey } from 'vue-i18n'
import { api } from './RenderMain'
import { globalNotify } from './canvas-function'
import { collectionMethodsMap } from './material-function'
import { generateFn } from './data-function'
export const lowcodeWrap = (props, context) => {
@ -60,13 +59,13 @@ export const lowcodeWrap = (props, context) => {
const wrap = (fn) => {
if (typeof fn === 'function') {
const fnName = fn.name
if (fn.toString().includes('return this')) {
const fnString = fn.toString()
if (fnString.includes('return this')) {
return () => global
} else if (fnName && collectionMethodsMap[fnName.slice(0, -1)]) {
// 这里区块打包的时候会在方法名称后面多加一个字符串,所以此处需要截取下函数名称
fn.realName = fnName.slice(0, -1)
return generateFn(fn)
} else if (/this\.dataSourceMap\.[0-9a-zA-Z_]+\.load\(\)/.test(fnString)) {
const renderContext = (inject('pageContext') as Ref<any>).value
return generateFn(fn, renderContext)
} else if (fn.name === 'setter' || fn.name === 'getter') {
// 这里需要保证在消费区块时,区块中的访问器函数可以正常执行
return (...args) => {

View File

@ -1,8 +1,14 @@
import { h } from 'vue'
import { isHTMLTag, hyphenate } from '@vue/shared'
import { h, defineAsyncComponent } from 'vue'
import { isHTMLTag } from '@vue/shared'
import * as TinyVueIcon from '@opentiny/vue-icon'
import { utils } from '@opentiny/tiny-engine-utils'
import { CanvasRow, CanvasCol, CanvasRowColContainer } from '@opentiny/tiny-engine-builtin-component'
import {
CanvasRow,
CanvasCol,
CanvasRowColContainer,
CanvasFlexBox,
CanvasSection
} from '@opentiny/tiny-engine-builtin-component'
import {
CanvasBox,
CanvasCollection,
@ -15,9 +21,8 @@ import {
CanvasRouterLink
} from '../builtin'
import { getController } from '../canvas-function/controller'
import { generateCollection } from './support-collection'
import BlockLoadError from '../BlockLoadError.vue'
export const customElements = {}
export const Mapper = {
Icon: CanvasIcon,
Text: CanvasText,
@ -27,6 +32,8 @@ export const Mapper = {
slot: CanvasSlot,
Template: CanvasBox,
Img: CanvasImg,
CanvasSection,
CanvasFlexBox,
CanvasRow,
CanvasCol,
CanvasRowColContainer,
@ -42,91 +49,62 @@ const getBlock = (name) => {
return window.blocks?.[name]
}
const { hyphenateRE } = utils
const getPlainProps = (object: Record<string, any> = {}) => {
const { slot, ...rest } = object
const props = {}
const blockComponentsBlobUrlMap = new Map<string, string>()
if (slot) {
rest.slot = slot.name || slot
}
Object.entries(rest).forEach(([key, value]) => {
let renderKey = key
// html 标签属性会忽略大小写,所以传递包含大写的 props 需要转换为 kebab 形式的 props
if (!/on[A-Z]/.test(renderKey) && hyphenateRE.test(renderKey)) {
renderKey = hyphenate(renderKey)
// TODO: 这里的全局 getter 方法名,可以做成配置化
const loadBlockComponent = async (name: string) => {
try {
if (blockComponentsBlobUrlMap.has(name)) {
return import(/* @vite-ignore */ blockComponentsBlobUrlMap.get(name))
}
if (['boolean', 'string', 'number'].includes(typeof value)) {
props[renderKey] = value
} else {
// 如果传给webcomponent标签的是对象或者数组需要使用.prop修饰符转化成h函数就是如下写法
props[`.${renderKey}`] = value
}
})
return props
}
const blocksBlob = (await getController().getBlockByName(name)) as Array<{ blobURL: string; style: string }>
const generateBlockContent = (schema) => {
if (schema?.componentName === 'Collection') {
generateCollection(schema)
}
if (Array.isArray(schema?.children)) {
schema.children.forEach((item) => {
generateBlockContent(item)
})
}
}
const registerBlock = (componentName) => {
getController()
.registerBlock?.(componentName)
.then((res) => {
const blockSchema = res.content
for (const [fileName, value] of Object.entries(blocksBlob)) {
blockComponentsBlobUrlMap.set(fileName, value.blobURL)
// 拿到区块数据,建立区块中数据源的映射关系
generateBlockContent(blockSchema)
// 如果区块的根节点有百分比高度,则需要特殊处理,把高度百分比传递下去,适配大屏应用
if (/height:\s*?[\d|.]+?%/.test(blockSchema?.props?.style)) {
const blockDoms = document.querySelectorAll<HTMLElement>(hyphenate(componentName))
blockDoms.forEach((item) => {
item.style.height = '100%'
})
if (!value.style) {
continue
}
})
// 注册 CSS以区块为维度
const stylesheet = document.querySelector(`#${fileName}`)
if (stylesheet) {
stylesheet.innerHTML = value.style
} else {
const newStylesheet = document.createElement('style')
newStylesheet.innerHTML = value.style
newStylesheet.setAttribute('id', fileName)
document.head.appendChild(newStylesheet)
}
}
return import(/* @vite-ignore */ blockComponentsBlobUrlMap.get(name))
} catch (error) {
// 加载错误提示
return h(BlockLoadError, { name })
}
}
export const wrapCustomElement = (componentName) => {
const material = getController().getMaterial(componentName)
window.loadBlockComponent = loadBlockComponent
if (!Object.keys(material).length) {
registerBlock(componentName)
}
const getBlockComponent = (name) => {
return defineAsyncComponent(() => loadBlockComponent(name))
}
customElements[componentName] = {
name: componentName + '.ce',
render() {
return h(
hyphenate(componentName),
window.parent.TinyGlobalConfig.dslMode === 'Vue' ? getPlainProps(this.$attrs) : this.$attrs,
this.$slots.default?.()
)
}
}
// 移除区块缓存
export const removeBlockCompsCache = () => {
blockComponentsBlobUrlMap.forEach((_, fileName) => {
const stylesheet = document.querySelector(`#${fileName}`)
stylesheet?.remove?.()
})
return customElements[componentName]
blockComponentsBlobUrlMap.clear()
}
export const getIcon = (name) => TinyVueIcon?.[name]?.() || ''
export const getComponent = (name) => {
return (
Mapper[name] ||
getNative(name) ||
getBlock(name) ||
customElements[name] ||
(isHTMLTag(name) ? name : wrapCustomElement(name))
)
return Mapper[name] || getNative(name) || getBlock(name) || (isHTMLTag(name) ? name : getBlockComponent(name))
}

View File

@ -11,7 +11,6 @@
*/
import { shallowReactive } from 'vue'
import { utils } from '@opentiny/tiny-engine-utils'
export function useContext() {
const context = shallowReactive({})
@ -29,38 +28,6 @@ export function useContext() {
}
}
export function useNodes() {
const nodes = {}
const setNode = (schema, parent) => {
schema.id = schema.id || utils.guid()
nodes[schema.id] = { node: schema, parent }
}
const getNode = (id, parent) => {
return parent ? nodes[id] : nodes[id].node
}
const delNode = (id) => delete nodes[id]
const clearNodes = () => {
Object.keys(nodes).forEach(delNode)
}
const getRoot = (id) => {
const { parent } = getNode(id, true)
return parent?.id ? getRoot(parent.id) : parent
}
return {
setNode,
getNode,
delNode,
clearNodes,
getRoot
}
}
export function useCondition() {
// 从大纲树控制隐藏
const conditions = shallowReactive({})
@ -107,13 +74,11 @@ export function useCssScopeId() {
}
export function usePageContext() {
const contextExpose = useContext()
const nodeExpose = useNodes()
const conditionExpose = useCondition()
const contextParentExpose = usePageContextParent()
const cssCopeIdExpose = useCssScopeId()
return {
...contextExpose,
...nodeExpose,
...conditionExpose,
...contextParentExpose,
...cssCopeIdExpose,

View File

@ -1,6 +1,6 @@
import { initStyle } from '../material-function/page-getter'
import { getController } from '../render'
export function setPagecss(css = '', pageId?) {
export function setPageCss(css = '', pageId?) {
const cssPageId = pageId ?? getController().getBaseInfo().pageId
const key = `data-te-page-${cssPageId}`
initStyle(key, css)

View File

@ -8,8 +8,6 @@ export function useProps(generateAccessor: ReturnType<typeof useAccessorMap>['ge
Object.assign(props, data)
}
const getProps = () => props
const initProps = (properties = []) => {
const props: Record<string, any> = {}
const accessorFunctions: Array<ReturnType<typeof generateAccessor>> = []
@ -38,7 +36,6 @@ export function useProps(generateAccessor: ReturnType<typeof useAccessorMap>['ge
return {
props,
initProps,
getProps,
setProps
}
}

View File

@ -1,4 +1,4 @@
import { reactive, watchEffect } from 'vue'
import { reactive, watchEffect, watch } from 'vue'
import { reset } from '../data-utils'
import { useAccessorMap } from './accessor-map'
import { useState } from './state'
@ -6,30 +6,50 @@ import { useProps } from './props'
import { useMethods } from './methods'
import { nextTick } from 'vue'
import { globalNotify } from '../canvas-function'
import { setPagecss } from './css'
import { setPageCss } from './css'
import type { IPageSchema, ISchemaChildrenItem } from '@opentiny/tiny-engine-dsl-vue'
export { IPageSchema, ISchemaChildrenItem }
export function useSchema(
{ context: globalContext, setContext, getContext, clearNodes, getNode },
{ context: globalContext, setContext, getContext },
{ utils, bridge, stores, getDataSourceMap }
) {
const schema = reactive<Partial<IPageSchema>>({})
const { generateAccessor, stateAccessorMap, propsAccessorMap, generateStateAccessors } = useAccessorMap(globalContext)
const { state, getState, setState, deleteState } = useState(schema, {
const { state, setState } = useState(schema, {
getContext,
generateStateAccessors
})
const { props, initProps, getProps, setProps } = useProps(generateAccessor)
const { props, initProps, setProps } = useProps(generateAccessor)
const { methods, getMethods, setMethods } = useMethods({
setContext,
getContext
})
const getSchema = () => schema
watch(
() => schema.state,
(value) => {
setState(value)
},
{
deep: true
}
)
// 这里监听schema.methods为了保证methods上下文环境始终为最新
watch(
() => schema.methods,
(value) => {
setMethods(value, true)
},
{
deep: true
}
)
const setSchema = async (data: IPageSchema, pageId?: string) => {
const newSchema = JSON.parse(JSON.stringify(data || schema))
reset(schema)
@ -71,9 +91,9 @@ export function useSchema(
// 这里setState会触发画布渲染是因为状态管理里面的变量会用到props、utils、bridge、stores、methods
setState(newSchema.state, true)
clearNodes()
await nextTick()
setPagecss(data.css, pageId)
setPageCss(data.css, pageId)
Object.assign(schema, newSchema)
// 当上下文环境设置完成之后再去处理区块属性访问器的watchEffect
@ -99,7 +119,6 @@ export function useSchema(
}
return {
schema,
getSchema,
setSchema,
...{
generateAccessor,
@ -109,14 +128,11 @@ export function useSchema(
},
...{
state,
getState,
setState,
deleteState
setState
},
...{
props,
initProps,
getProps,
setProps
},
...{
@ -125,9 +141,8 @@ export function useSchema(
setMethods
},
...{
getContext,
getNode
getContext
},
setPagecss
setPageCss
}
}

View File

@ -1,20 +1,21 @@
import { shallowReactive } from 'vue'
import { reset } from '../data-utils'
import { getDeletedKeys } from '../data-utils'
import { isStateAccessor, parseData } from '../data-function'
export function useState(schema, { getContext, generateStateAccessors }) {
const state = shallowReactive({})
const getState = () => state
const deleteState = (variable: string) => {
delete state[variable]
}
const setState = (data, clear = false) => {
clear && reset(state)
if (!schema.state) {
schema.state = data
if (typeof data !== 'object' || data === null) {
return
}
const deletedKeys = getDeletedKeys(state, data)
// 同步删除的 key
for (const key of deletedKeys) {
delete state[key]
}
Object.assign(state, parseData(data, {}, getContext()) || {})
@ -35,8 +36,6 @@ export function useState(schema, { getContext, generateStateAccessors }) {
}
return {
state,
getState,
setState,
deleteState
setState
}
}

View File

@ -10,13 +10,14 @@
*
*/
import { defineComponent, h, inject, provide, Ref } from 'vue'
import { defineComponent, h, inject, provide, Ref, Suspense } from 'vue'
import { NODE_UID as DESIGN_UIDKEY, NODE_TAG as DESIGN_TAGKEY, NODE_LOOP as DESIGN_LOOPID } from '../../common'
import { getDesignMode, DESIGN_MODE } from './canvas-function'
import { parseCondition, parseData, parseLoopArgs } from './data-function'
import { blockSlotDataMap, getComponent, generateCollection, Mapper, configure } from './material-function'
import { blockSlotDataMap, getComponent, Mapper, configure } from './material-function'
import { getPage } from './material-function/page-getter'
import BlockLoading from './BlockLoading.vue'
export const renderDefault = (children, scope, parent) =>
children.map?.((child) =>
@ -34,7 +35,7 @@ const stopEvent = (event) => {
return false
}
const generateSlotGroup = (children, isCustomElm, schema) => {
const generateSlotGroup = (children, schema) => {
const slotGroup = {}
children.forEach((child) => {
@ -42,7 +43,6 @@ const generateSlotGroup = (children, isCustomElm, schema) => {
const slot = child.slot || props?.slot?.name || props?.slot || 'default'
const isNotEmptyTemplate = componentName === 'Template' && children.length
isCustomElm && (child.props.slot = 'slot') // CE下需要给子节点加上slot标识
slotGroup[slot] = slotGroup[slot] || {
value: [],
params,
@ -55,9 +55,9 @@ const generateSlotGroup = (children, isCustomElm, schema) => {
return slotGroup
}
const renderSlot = (children, scope, schema, isCustomElm?) => {
const renderSlot = (children, scope, schema) => {
if (children.some((a) => a.componentName === 'Template')) {
const slotGroup = generateSlotGroup(children, isCustomElm, schema)
const slotGroup = generateSlotGroup(children, schema)
const slots = {}
Object.keys(slotGroup).forEach((slotName) => {
@ -150,7 +150,7 @@ const injectPlaceHolder = (componentName, children) => {
return children
}
const renderGroup = (children, scope, parent, pageContext) => {
const renderGroup = (children, scope, pageContext) => {
return children.map?.((schema) => {
const { componentName, children, loop, loopArgs, condition, id } = schema
const loopList = parseData(loop, scope, pageContext.context)
@ -163,8 +163,6 @@ const renderGroup = (children, scope, parent, pageContext) => {
loopArgs
})
pageContext.setNode(schema, parent)
if (pageContext.conditions[id] === false || !parseCondition(condition, mergeScope, pageContext.context)) {
return null
}
@ -190,20 +188,22 @@ const getChildren = (schema, mergeScope, pageContext) => {
const component = getComponent(componentName)
const isNative = typeof component === 'string'
const isCustomElm = customElements[componentName]
const isGroup = checkGroup(componentName)
if (Array.isArray(renderChildren)) {
if (isNative || isCustomElm) {
return renderDefault(renderChildren, mergeScope, schema)
} else {
return isGroup
? renderGroup(renderChildren, mergeScope, schema, pageContext)
: renderSlot(renderChildren, mergeScope, schema, isCustomElm)
// children 空的场景,不能返回空数组,因为有部分组件会误以为使用了自定义插槽,从而无法渲染默认插槽内容,比如 TinyTree 组件
if (!renderChildren.length) {
return null
}
} else {
return parseData(renderChildren, mergeScope, pageContext.context)
if (isNative) {
return renderDefault(renderChildren, mergeScope, schema)
}
return isGroup
? renderGroup(renderChildren, mergeScope, pageContext)
: renderSlot(renderChildren, mergeScope, schema)
}
return parseData(renderChildren, mergeScope, pageContext.context)
}
function getRenderPageId(currentPageId, isPageStart) {
const pagePathFromRoot = (inject('page-ancestors') as Ref<any[]>).value
@ -239,8 +239,6 @@ export const renderer = defineComponent({
const { scope, schema, parent, ancestors } = this
const { componentName, loop, loopArgs, condition } = schema
const pageContext = this.currentPageContext
// 处理数据源和表格fetchData的映射关系
generateCollection(schema)
if (!componentName) {
return parseData(schema, scope, pageContext.context)
@ -280,18 +278,28 @@ export const renderer = defineComponent({
mergeScope = mergeScope ? { ...mergeScope, ...slotData } : slotData
}
// 给每个节点设置schema.id并缓存起来
pageContext.setNode(schema, parent)
if (pageContext.conditions[schema.id] === false || !parseCondition(condition, mergeScope, pageContext.context)) {
return null
}
return h(
const Ele = h(
component,
getBindProps(schema, mergeScope, pageContext.context, pageContext),
getChildren(schema, mergeScope, pageContext)
)
// 区块加上 suspense 渲染,就可以在网络延时的时候显示加载中的字样或者动画,优化体验
if (schema.componentType === 'Block') {
return h(
Suspense,
{},
{
default: () => Ele,
fallback: () => h(BlockLoading, { name: componentName })
}
)
}
return Ele
}
return loopList?.length ? loopList.map(renderElement) : renderElement()

View File

@ -15,7 +15,6 @@ import { addScript, addStyle, dynamicImportComponents, updateDependencies } from
import TinyI18nHost, { I18nInjectionKey } from '@opentiny/tiny-engine-common/js/i18n'
import Main, { api } from './RenderMain'
import lowcode from './lowcode'
import { supportUmdBlock } from './supportUmdBlock'
type ITinyI18nHostI18nHost = typeof TinyI18nHost
interface IExtendsTinyI18nHost extends ITinyI18nHostI18nHost {
@ -33,8 +32,6 @@ const initRenderContext = () => {
window.TinyLowcodeComponent = {}
window.TinyComponentLibs = {}
supportUmdBlock()
document.addEventListener('updateDependencies', updateDependencies)
}

View File

@ -1,71 +0,0 @@
import * as Vue from 'vue'
import * as VueI18n from 'vue-i18n'
import * as TinyWebcomponentCore from '@opentiny/tiny-engine-webcomponent-core'
import * as TinyVueIcon from '@opentiny/vue-icon'
import TinyVue from '@opentiny/vue'
import TinyI18nHost from '@opentiny/tiny-engine-common/js/i18n'
import { camelize, capitalize } from '@vue/shared'
import { blockSlotDataMap, getComponent } from './material-function'
declare global {
interface Window {
Vue: typeof Vue
VueI18n: typeof VueI18n
TinyVue: typeof TinyVue
TinyVueIcon: typeof TinyVueIcon
TinyI18nHost: typeof TinyI18nHost
TinyWebcomponentCore: typeof TinyWebcomponentCore
}
}
// 和 @opentiny/tiny-engine-block-build 打包umd方式相适配
export function supportUmdBlock() {
// 不能采用new Proxy代理Vue的方案在编译后的vue会报错警告采用一下方案扩展用于注入一些区块加载逻辑
window.Vue = {
...Vue,
resolveComponent(...args) {
// 此处先执行vue内部的解析组件的方法如果可以拿到组件对象则直接返回反之则去注册区块
const component = Vue.resolveComponent(args[0])
if (component && typeof component === 'string') {
return getComponent(capitalize(camelize(args[0])))
} else {
return component
}
},
// renderSlot方法第三个参数是作用域插槽传递的数据格式{ data: vue.unref(state).componentData }
renderSlot(...args) {
// 获取当前vue的实例
const instance = Vue.getCurrentInstance()
// 获取当前区块名称
const blockName = instance.attrs.dataTag as string
const [, slotName, slotData] = args
// 如果是作用域插槽,则获取作用域插槽传递过来的参数
if (slotData) {
if (blockSlotDataMap[blockName]) {
blockSlotDataMap[blockName][slotName] = slotData
} else {
blockSlotDataMap[blockName] = { [slotName]: slotData }
}
}
/**
* vue源码中的renderSlot会忽略default插槽的名称args第三个参数的name值
* vue源码如右所示if (name !== 'default') props.name = name; return createVNode('slot', props, fallback && fallback());
**/
if (slotName === 'default') {
args[2] = args[2] || {}
args[2].name = slotName
}
return Vue.renderSlot(...args)
}
}
window.VueI18n = VueI18n
window.TinyVue = TinyVue
window.TinyVueIcon = TinyVueIcon
window.TinyWebcomponentCore = TinyWebcomponentCore
window.TinyI18nHost = TinyI18nHost
}

View File

@ -8,5 +8,7 @@ export declare global {
scripts: Array<any>
}
TinyGlobalConfig: Record<string, any>
loadBlockComponent: (blockName: string) => Promise<any>
host: any
}
}

View File

@ -4,31 +4,29 @@
<tiny-select
ref="selectRef"
v-model="selectValue"
placeholder="请根据显示值搜索"
placeholder="请选择多语言文案"
filterable
is-drop-inherit-width
:filter-method="filterMethod"
@change="selectI18n"
>
<tiny-option v-for="item in langData" :key="item.key" :label="item[currentLang]" :value="item.key">
<div style="display: flex">
<span style="flex: 1">{{ item.key }}</span>
<span style="flex: 1">{{ item[currentLang] }}</span>
</div>
<tiny-option v-for="item in langData" :key="item.key" :label="item.key + item[currentLang]" :value="item.key">
</tiny-option>
</tiny-select>
<div v-if="paramsForm.length" class="params-form">
<label>国际化参数配置</label>
<div class="label">国际化参数配置</div>
<div v-for="param in paramsForm" :key="param.name" class="params-item">
<label>{{ param.name }}</label>
<tiny-input v-model="param.value" @update:modelValue="paramsChange"></tiny-input>
</div>
</div>
<slot name="suffix">
<tiny-button type="info" @click="openCreateForm">
<icon-plus class="icon-plus"></icon-plus>
<span>创建新的多语言文案</span>
</tiny-button>
<tiny-button v-if="isBind" type="info" @click="unbindI18n"> 解除关联 </tiny-button>
<div class="bottom-buttons">
<tiny-button v-if="isBind" @click="unbindI18n">解除关联</tiny-button>
<tiny-button type="primary" @click="openCreateForm">
<span>创建新的多语言文案</span>
</tiny-button>
</div>
</slot>
</div>
<div v-show="showEditItem" class="addNewLanguage">
@ -43,9 +41,9 @@
<input v-model="editForm[locale.lang]" class="tiny-input__inner" />
</div>
</div>
<div class="add-btns">
<tiny-button type="info" @click="activeI18n">国际化管理 </tiny-button>
<tiny-button type="info" @click="addBindI18n">添加并关联 </tiny-button>
<div class="bottom-buttons">
<tiny-button @click="activeI18n">国际化管理</tiny-button>
<tiny-button type="primary" @click="addBindI18n">添加并关联</tiny-button>
</div>
</div>
</div>
@ -57,15 +55,13 @@ import { useLayout, useTranslate } from '@opentiny/tiny-engine-meta-register'
import { PROP_DATA_TYPE } from '../js/constants'
import { utils } from '@opentiny/tiny-engine-utils'
import { Select, Option, Button, Input } from '@opentiny/vue'
import { iconPlus } from '@opentiny/vue-icon'
export default {
components: {
TinySelect: Select,
TinyOption: Option,
TinyButton: Button,
TinyInput: Input,
IconPlus: iconPlus()
TinyInput: Input
},
inheritAttrs: false,
props: {
@ -169,10 +165,7 @@ export default {
<style lang="less" scoped>
.languageContent {
text-align: center;
z-index: 99;
margin-top: 5px;
margin-bottom: 16px;
.tiny-svg {
margin-right: 10px;
@ -182,17 +175,6 @@ export default {
color: #ccc;
}
}
.tiny-button {
max-width: initial;
padding: 0 12px;
background-color: var(--ti-lowcode-tabs-border-color);
border-color: var(--ti-lowcode-tootip-input-border-color);
color: var(--ti-lowcode-toolbar-icon-color);
&:hover {
background-color: var(--ti-lowcode-canvas-wrap-bg);
border-color: var(--ti-lowcode-tabs-border-color);
}
}
.addNewLanguage {
.tiny-input {
display: flex;
@ -209,15 +191,16 @@ export default {
.tiny-input__inner {
flex: 1;
}
.add-btns {
text-align: right;
padding: 0 8px;
}
}
}
.params-form {
& > label {
line-height: 30px;
.label {
margin: 16px 0;
font-size: 12px;
line-height: 18px;
}
.params-item + .params-item {
margin-top: 12px;
}
.params-item {
display: flex;
@ -225,7 +208,16 @@ export default {
label {
width: 80px;
}
margin-bottom: 10px;
}
}
.bottom-buttons {
display: flex;
justify-content: flex-end;
margin-top: 16px;
gap: 8px;
.tiny-button,
.tiny-button.tiny-button--default {
margin: 0;
}
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<tiny-dialog-box
:visible="$attrs.visible"
:visible="visible"
title="发布区块"
width="550px"
append-to-body
@ -10,10 +10,11 @@
<tiny-form
ref="deployBlockRef"
label-position="left"
label-width="100px"
label-width="84px"
label-align
:model="formState"
:rules="formRules"
validate-type="text"
>
<tiny-form-item label="版本号" prop="version">
<tiny-input v-model="formState.version" placeholder="请输入X.Y.Z格式版本号如1.0.0"></tiny-input>
@ -24,7 +25,7 @@
<tiny-form-item label="保存设置" prop="needToSave" class="form-item-save">
<tiny-checkbox v-model="formState.needToSave">发布成功后保存最新数据</tiny-checkbox>
</tiny-form-item>
<tiny-form-item label="schema比对">
<tiny-form-item label="schema比对" class="schema-compare">
<tiny-button class="compare-button" type="text" @click="changeCompare"> 查看本次发布的改动点 </tiny-button>
<tiny-popover
placement="top"
@ -34,7 +35,7 @@
content="区块本地schema和线上新版本schema进行比对"
>
<template #reference>
<icon-help-circle></icon-help-circle>
<svg-icon name="plugin-icon-plugin-help"></svg-icon>
</template>
</tiny-popover>
</tiny-form-item>
@ -71,7 +72,6 @@
<script>
import { reactive, ref, watch } from 'vue'
import { iconHelpCircle } from '@opentiny/vue-icon'
import {
Checkbox as TinyCheckbox,
Input as TinyInput,
@ -81,14 +81,13 @@ import {
Popover as TinyPopover,
FormItem as TinyFormItem
} from '@opentiny/vue'
import { useNotify, useCanvas, getMetaApi, META_APP, useBlock } from '@opentiny/tiny-engine-meta-register'
import { useNotify, getMetaApi, META_APP } from '@opentiny/tiny-engine-meta-register'
import { constants } from '@opentiny/tiny-engine-utils'
import VueMonaco from './VueMonaco.vue'
export default {
components: {
TinyCheckbox,
IconHelpCircle: iconHelpCircle(),
TinyButton,
TinyDialogBox,
TinyForm,
@ -98,13 +97,17 @@ export default {
VueMonaco
},
props: {
nextVersion: {
type: String,
default: ''
block: {
type: Object,
default: () => ({})
},
visible: {
type: Boolean,
default: false
}
},
emits: ['update:visible'],
setup(props, { emit, attrs }) {
emits: ['update:visible', 'changeSchema'],
setup(props, { emit }) {
const { COMPONENT_NAME } = constants
const formState = reactive({
deployInfo: '',
@ -139,12 +142,31 @@ export default {
}
}
watch(
() => attrs.visible,
(visible) => {
if (visible && !formState.version) {
formState.version = props.nextVersion
const getNextVersion = (block) => {
const backupList = block.histories || []
let latestVersion = '1.0.0'
let latestTime = 0
backupList.forEach((v) => {
const vTime = new Date(v.created_at).getTime()
if (vTime > latestTime) {
latestTime = vTime
latestVersion = v.version
}
})
// version X.Y.Z
return latestVersion.replace(/\d+$/, (match) => Number(match) + 1)
}
watch(
() => props.visible,
(visible) => {
if (!visible) {
return
}
formState.version = getNextVersion(props.block)
}
)
@ -152,10 +174,11 @@ export default {
const deployBlock = async () => {
deployBlockRef.value.validate((valid) => {
const { getEditBlock, publishBlock } = getMetaApi(META_APP.BlockManage)
const { publishBlock } = getMetaApi(META_APP.BlockManage)
if (valid) {
const params = {
block: getEditBlock() || useBlock().getCurrentBlock(),
block: props.block,
is_compile: true,
deploy_info: formState.deployInfo,
version: formState.version,
@ -172,21 +195,14 @@ export default {
const changeCompare = async (value) => {
const api = getMetaApi(META_APP.BlockManage)
if (value) {
const block = api.getEditBlock()
const block = props.block
const remote = await api.getBlockById(block?.id)
const originalObj = remote?.content || {}
state.originalCode = JSON.stringify(originalObj, null, 2)
const getSchema = useCanvas().canvasApi.value.getSchema
// 线
const pageSchema = getSchema?.() || {}
if (pageSchema.componentName === 'Block') {
state.code = JSON.stringify(pageSchema, null, 2)
} else {
//
state.code = state.originalCode
}
state.code = JSON.stringify(block?.content || {}, null, 2)
state.compareVisible = true
}
}
@ -209,9 +225,8 @@ export default {
return
}
try {
const pageSchema = JSON.parse(state.newCode)
const setSchema = useCanvas().canvasApi.value.setSchema
setSchema?.({ ...pageSchema, componentName: COMPONENT_NAME.Block })
const newSchema = JSON.parse(state.newCode)
emit('changeSchema', { ...newSchema, componentName: COMPONENT_NAME.Block })
close()
} catch (err) {
useNotify({
@ -254,7 +269,15 @@ export default {
line-height: 0;
}
}
.schema-compare {
.tiny-button.tiny-button {
padding-left: 0;
padding-right: 4px;
font-size: 12px;
}
}
.compare-button {
font-size: 12px;
padding-left: 0;
padding-right: 8px;
line-height: 28px;

View File

@ -38,7 +38,7 @@ export default {
const { addBlockEvent, removeEventLink, getCurrentBlock, appendEventEmit } = useBlock()
const { PLUGIN_NAME, activePlugin } = useLayout()
const { schema } = useCanvas().canvasApi.value?.getSchema?.() || {}
const { schema } = useCanvas()?.getSchema?.() || {}
const events = schema?.events || []
const eventsList = Object.entries(events).map(([key, value]) => {
return {

View File

@ -48,10 +48,10 @@ export default {
},
setup(props) {
const { confirm } = useModal()
const { pageState, canvasApi } = useCanvas()
const { pageState, canvasApi, getSchema } = useCanvas()
const { addBlockProperty, removePropertyLink, getCurrentBlock, editBlockProperty } = useBlock()
const { PLUGIN_NAME, activePlugin } = useLayout()
const { schema } = canvasApi.value.getSchema?.() || {}
const { schema } = getSchema?.() || {}
const state = reactive({
newPropertyName: ''

View File

@ -265,11 +265,11 @@ export default {
const updateValue = (value) => {
const { property, type } = props.property
const { setProp } = useProperties()
const { setProp, getSchema } = useProperties()
//
if (value?.type === SCHEMA_DATA_TYPE.JSExpression) {
const currentComponent = useProperties().getSchema().componentName
const currentComponent = getSchema().componentName
const {
schema: { events = {} }
} = useMaterial().getMaterial(currentComponent)
@ -282,13 +282,13 @@ export default {
}
}
const { operateNode, isSaved } = useCanvas()
if (property === 'children') {
useProperties().getSchema().children = value
const schema = getSchema()
operateNode({ type: 'updateAttributes', id: schema.id, value: { children: value } })
} else {
if (
!useCanvas().isSaved() &&
![PAGE_STATUS.Guest, PAGE_STATUS.Occupy].includes(useLayout().layoutState.pageStatus.state)
) {
if (!isSaved() && ![PAGE_STATUS.Guest, PAGE_STATUS.Occupy].includes(useLayout().layoutState.pageStatus.state)) {
return
}
@ -527,7 +527,7 @@ export default {
}
.item-label {
color: var(--ti-lowcode-meta-config-item-label-color);
color: var(--te-common-text-secondary);
font-size: 12px;
display: flex;
line-height: 18px;

View File

@ -4,13 +4,14 @@
<template #suffix>
<tiny-popover
ref="popoverRef"
width="260"
width="340"
trigger="click"
:visible-arrow="false"
popper-class="lowcode"
popper-class="i18n-input-popover"
@hide="onHide"
>
<div class="popover-content">
<h3 class="title">绑定国际化</h3>
<icon-close class="icon-close" @click="closePopover"></icon-close>
<bind-i18n
ref="addI1i8nRef"
@ -25,7 +26,7 @@
</div>
<template #reference>
<icon-language :class="['icon-language', { isBind }]"></icon-language>
<svg-icon name="internationalization" :class="['icon-language', { isBind }]"></svg-icon>
</template>
</tiny-popover>
</template>
@ -37,7 +38,7 @@
import { computed, ref, watchEffect } from 'vue'
import { useTranslate } from '@opentiny/tiny-engine-meta-register'
import { Input, Popover } from '@opentiny/vue'
import { IconClose, IconLanguage } from '@opentiny/vue-icon'
import { IconClose } from '@opentiny/vue-icon'
import BindI18n from './BindI18n.vue'
export default {
@ -46,8 +47,7 @@ export default {
TinyInput: Input,
BindI18n,
TinyPopover: Popover,
IconClose: IconClose(),
IconLanguage: IconLanguage()
IconClose: IconClose()
},
inheritAttrs: false,
props: {
@ -124,9 +124,27 @@ export default {
}
}
.popover-content {
text-align: right;
position: relative;
.title {
margin-top: 0;
margin-bottom: 16px;
font-size: 14px;
line-height: 20px;
}
.icon-close {
margin-right: 5px;
position: absolute;
top: 0;
right: 0;
}
}
.icon-language {
color: var(--te-common-icon-secondary);
}
</style>
<style>
.tiny-popover.tiny-popover.tiny-popper[x-placement].i18n-input-popover {
padding: 20px;
}
</style>

View File

@ -2,7 +2,7 @@
<div class="life-cycle">
<tiny-popover v-model="state.showPopover" placement="bottom-end" trigger="hover" popperClass="option-popper">
<template #reference>
<tiny-button class="life-cycle-btn"><icon-plus class="icon-plus"></icon-plus> </tiny-button>
<tiny-button class="life-cycle-btn"><svg-icon name="add"></svg-icon> </tiny-button>
</template>
<div class="popover-list">
<ul>
@ -21,13 +21,18 @@
<div class="life-cycle-tips">{{ lifeCycleTips }}</div>
<meta-list-items :optionsList="Object.keys(state.bindLifeCycles)" :draggable="false">
<template #content="{ data }">
<div>
<div class="life-cycle-content-item">
{{ data }}
</div>
</template>
<template #operate="{ data }">
<svg-button name="setting-outline" tips="编辑" placement="top" @click="openLifeCyclesPanel(data)"></svg-button>
<svg-button name="delete" tips="删除" placement="top" @click="deleteLifeCycle(data)"></svg-button>
<svg-button
class="opt-button"
:hoverBgColor="false"
name="text-source-setting"
@click="openLifeCyclesPanel(data)"
></svg-button>
<svg-button class="opt-button" :hoverBgColor="false" name="delete" @click="deleteLifeCycle(data)"></svg-button>
</template>
</meta-list-items>
<tiny-dialog-box v-model:visible="state.showLifeCyclesDialog" fullscreen :title="state.title" :append-to-body="true">
@ -72,7 +77,7 @@ import { Button, DialogBox, Popover, Search } from '@opentiny/vue'
import { useModal, usePage, useNotify, useCanvas } from '@opentiny/tiny-engine-meta-register'
import { getMergeMeta } from '@opentiny/tiny-engine-meta-register'
import MetaListItems from './MetaListItems.vue'
import { iconYes, iconPlus } from '@opentiny/vue-icon'
import { iconYes } from '@opentiny/vue-icon'
import VueMonaco from './VueMonaco.vue'
import { initCompletion } from '../js/completion'
import { initLinter, lint } from '../js/linter'
@ -87,8 +92,7 @@ export default {
MonacoEditor: VueMonaco,
SvgButton,
MetaListItems,
IconYes: iconYes(),
IconPlus: iconPlus()
IconYes: iconYes()
},
props: {
@ -132,7 +136,7 @@ export default {
}
const syncLifeCycle = () => {
const currentSchema = useCanvas().canvasApi.value?.getSchema?.()
const currentSchema = useCanvas().getSchema()
const pageContent = getPageContent()
const { id, fileName } = pageContent
if (id === currentSchema.id || fileName === currentSchema.fileName) {
@ -155,8 +159,7 @@ export default {
const openLifeCyclesPanel = (item) => {
state.title = item
const bindLifeCycleSource =
props.bindLifeCycles?.[item] || useCanvas().canvasApi.value?.getSchema?.()?.lifeCycles?.[item]
const bindLifeCycleSource = props.bindLifeCycles?.[item] || useCanvas().getSchema()?.lifeCycles?.[item]
state.editorValue =
bindLifeCycleSource?.value ||
`function ${item} (${item === 'setup' ? '{ props, state, watch, onMounted }' : ''}) {} `
@ -270,12 +273,20 @@ export default {
margin-left: 20px;
margin-right: 20px;
}
.life-cycle-content-item {
color: var(--te-common-text-primary);
}
.opt-button {
width: auto;
&:last-child {
margin-right: var(--te-base-space-3x);
}
}
.popover-list {
margin: 8px 0;
li {
padding: 0 12px;
margin: 0 -8px;
margin: 0 -16px;
line-height: 24px;
cursor: pointer;
&:hover {

View File

@ -255,7 +255,8 @@ export default {
minimap: {
enabled: false
}
}
},
locale
}
}
}
@ -302,8 +303,9 @@ export default {
width: 100%;
height: 24px;
padding: 4px;
padding-left: 10px;
border: 1px solid var(--ti-lowcode-meta-codeEditor-border-color);
border-radius: 6px;
border-radius: var(--te-base-border-radius-1);
&:hover {
border-color: var(--ti-lowcode-meta-codeEditor-border-hover-color);

View File

@ -16,7 +16,8 @@
:popper-class="['option-popper', { 'fixed-left': expand }]"
:visible-arrow="!expand"
title=""
width="267"
width="295"
height="auto"
trigger="manual"
@hide="hide(item)"
>
@ -295,14 +296,21 @@ export default {
}
}
}
.tiny-popover {
position: relative;
.icon-close {
float: right;
position: absolute;
top: 6px;
right: 6px;
}
}
.add-options {
overflow-y: scroll;
height: 100%;
padding: 20px 0 20px 2px;
&.top {
margin-bottom: 0;
}
&::-webkit-scrollbar-track-piece {
background: var(--ti-lowcode-toolbar-bg);
}

View File

@ -30,31 +30,33 @@
</vue-draggable-next>
</template>
<template v-else>
<div v-for="(item, index) in optionsList" :key="index" class="option-item not-draggable">
<meta-list-item
:item="item"
:index="index"
:itemClick="itemClick"
:dataScource="listItemOption"
@changeItem="changeItem"
@deleteItem="deleteItem"
@editItem="editItem"
>
<template #content>
<slot name="content" :data="item">
<span>{{ item[textField] || formatName(item) }}</span>
</slot>
</template>
<template #operate>
<slot name="operate" :data="item"></slot>
</template>
<template #metaForm>
<slot name="form" :data="item"></slot>
</template>
<template #footer>
<slot name="footer" :data="item"></slot>
</template>
</meta-list-item>
<div>
<div v-for="(item, index) in optionsList" :key="index" class="option-item not-draggable">
<meta-list-item
:item="item"
:index="index"
:itemClick="itemClick"
:dataScource="listItemOption"
@changeItem="changeItem"
@deleteItem="deleteItem"
@editItem="editItem"
>
<template #content>
<slot name="content" :data="item">
<span>{{ item[textField] || formatName(item) }}</span>
</slot>
</template>
<template #operate>
<slot name="operate" :data="item"></slot>
</template>
<template #metaForm>
<slot name="form" :data="item"></slot>
</template>
<template #footer>
<slot name="footer" :data="item"></slot>
</template>
</meta-list-item>
</div>
</div>
</template>
</template>
@ -103,7 +105,7 @@ export default {
},
setup(props, { emit }) {
const listItemOption = computed(() => props)
const { resState } = useResource()
const { appSchemaState } = useResource()
const changeItem = (params) => {
const optionsList = [...props.optionsList]
@ -159,7 +161,7 @@ export default {
if (item[props.textField]) {
if (item[props.textField].i18nKey) {
let i18nKey = item[props.textField].i18nKey
text = resState.langs[i18nKey][resState.currentLang]
text = appSchemaState.langs[i18nKey][appSchemaState.currentLang]
} else {
text = item[props.textField]
}

View File

@ -8,14 +8,19 @@
</div>
<div :class="['buttons', { fullscreen: fullscreen }]" id="icon-buttons">
<slot name="buttons"></slot>
<tiny-tooltip v-if="showFormatBtn && options.language === 'json'" content="格式化" placement="top">
<tiny-tooltip
v-if="showFormatBtn && options.language === 'json'"
content="格式化"
placement="top"
:open-delay="OPEN_DELAY.Default"
>
<public-icon name="json" @click="formatCode"></public-icon>
</tiny-tooltip>
<span v-if="showFullScreenBtn">
<tiny-tooltip v-if="!fullscreen" content="全屏" placement="top">
<tiny-tooltip v-if="!fullscreen" content="全屏" placement="top" :open-delay="OPEN_DELAY.Default">
<public-icon name="full-screen" @click="switchFullScreen(true)"></public-icon>
</tiny-tooltip>
<tiny-tooltip v-else content="退出全屏" placement="top">
<tiny-tooltip v-else content="退出全屏" placement="top" :open-delay="OPEN_DELAY.Default">
<public-icon name="cancel-full-screen" @click="switchFullScreen(false)"></public-icon>
</tiny-tooltip>
</span>
@ -39,6 +44,8 @@ import { computed, ref, onActivated, onDeactivated } from 'vue'
import { Tooltip } from '@opentiny/vue'
import PublicIcon from './PublicIcon.vue'
import VueMonaco from './VueMonaco.vue'
import { constants } from '@opentiny/tiny-engine-utils'
const { OPEN_DELAY } = constants
export default {
components: {
@ -122,7 +129,8 @@ export default {
fullscreen,
switchFullScreen,
getValue,
formatCode
formatCode,
OPEN_DELAY
}
}
}

View File

@ -1,6 +1,8 @@
<template>
<div v-if="blockStyle === BlockStyles.Mini" class="header">
<div class="col-checkbox"></div>
<div v-if="blockStyle === BlockStyles.Mini && showCheckbox" class="header">
<div class="col-checkbox">
<select-all :allItems="data" :selected="checked" :hidden-label="true" @select-all="handleSelectAll"></select-all>
</div>
<div class="col-name">区块名称</div>
<div class="col-time">创建时间</div>
<div class="col-created-by">创建人</div>
@ -16,7 +18,7 @@
@mouseleave="state.hover = false"
>
<li v-if="showAddButton" class="block-item block-plus" @click="$emit('add')">
<span class="block-plus-icon"><icon-plus></icon-plus></span>
<span class="block-plus-icon"><svg-icon name="add"></svg-icon></span>
<div class="item-text">添加区块</div>
</li>
<li
@ -45,41 +47,21 @@
<div v-if="blockStyle === BlockStyles.List" class="item-description">{{ item.description }}</div>
</div>
<div v-if="blockStyle === BlockStyles.Mini" class="cell cell-time">
<span>{{ format(item.created_at, 'yyyy/MM/dd hh:mm:ss') }}</span>
</div>
<div v-if="blockStyle === BlockStyles.Mini" class="cell cell-created-by">
<span>{{ users.find((user) => user.id === item.createdBy)?.name || item.id }}</span>
</div>
<div v-if="item.isShowProgress" class="progress-bar">
<tiny-progress
:text-inside="true"
:stroke-width="8"
:percentage="item.publishProgress"
status="success"
></tiny-progress>
</div>
<div v-if="isBlockManage && !item.is_published" class="publish-flag">未发布</div>
<div v-if="isBlockManage" class="block-detail">
<div class="setting-menu" @mouseover.stop="handleSettingMouseOver" @mouseleave="handleBlockItemLeave">
<ul class="list">
<tiny-tooltip content="编辑" placement="top">
<li class="list-item" @mousedown.stop.left="editBlock({ event: $event, item, index })">
<svg-button class="list-item-svg" name="to-edit"> </svg-button>
</li>
</tiny-tooltip>
<tiny-tooltip content="设置" placement="top">
<li
class="list-item"
@mouseover.stop="iconSettingMove"
@mousedown.stop.prevent="iconClick({ event: $event, item, index })"
>
<svg-button class="list-item-svg" name="text-source-setting"> </svg-button>
</li>
</tiny-tooltip>
<li class="list-item" @mousedown.stop.left="editBlock({ event: $event, item, index })">
<svg-button class="list-item-svg" name="to-edit"> </svg-button>
</li>
<li
class="list-item"
@mouseover.stop="iconSettingMove"
@mousedown.stop.prevent="iconClick({ event: $event, item, index })"
>
<svg-button class="list-item-svg" name="text-source-setting"> </svg-button>
</li>
</ul>
</div>
</div>
@ -103,19 +85,6 @@
</ul>
</div>
</div>
<div
v-if="item.isAnimation"
:class="[
'deploy',
{ success: item.deployStatus === taskStatus.FINISHED },
{
error: item.deployStatus === taskStatus.STOPPED
}
]"
@mouseover.stop="item.isAnimation = false"
>
{{ deployTips[item.deployStatus] }}
</div>
</slot>
</li>
<div v-if="showBlockShot && state.hover && state.currentBlock.screenshot" class="block-shortcut">
@ -138,11 +107,11 @@
<script>
import { computed, watch, inject, reactive } from 'vue'
import { format } from '@opentiny/vue-renderless/common/date'
import { iconPlus } from '@opentiny/vue-icon'
import { Progress, Tooltip } from '@opentiny/vue'
import { Tooltip } from '@opentiny/vue'
import PluginBlockItemImg from './PluginBlockItemImg.vue'
import SearchEmpty from './SearchEmpty.vue'
import SvgButton from './SvgButton.vue'
import SelectAll from './SelectAll.vue'
const BlockStyles = {
Default: 'default',
@ -155,12 +124,11 @@ const defaultImg =
export default {
components: {
TinyProgress: Progress,
IconPlus: iconPlus(),
TinyTooltip: Tooltip,
PluginBlockItemImg,
SvgButton,
SearchEmpty
SearchEmpty,
SelectAll
},
props: {
data: {
@ -234,10 +202,10 @@ export default {
default: 2
}
},
emits: ['click', 'iconClick', 'add', 'deleteBlock', 'openVersionPanel', 'editBlock'],
emits: ['click', 'iconClick', 'add', 'deleteBlock', 'openVersionPanel', 'editBlock', 'checkAll', 'cancelCheckAll'],
setup(props, { emit }) {
const panelState = inject('panelState', {})
const blockUsers = inject('blockUsers')
const blockUsers = inject('blockUsers', [])
const state = reactive({
activeIndex: -1,
data: computed(() => props.data),
@ -298,19 +266,6 @@ export default {
state.activeIndex = -1
}
//
const taskStatus = {
RUNNING: 1,
STOPPED: 2,
FINISHED: 3
}
const deployTips = {
1: '正在发布中',
2: '发布失败,请重新发布',
3: '发布完成'
}
const iconSettingMove = () => {
state.hover = false
}
@ -361,6 +316,14 @@ export default {
}
)
const handleSelectAll = (items) => {
if (Array.isArray(items)) {
emit('checkAll', items)
} else {
emit('cancelCheckAll')
}
}
return {
BlockStyles,
isShortcutPanel: panelState.isShortcutPanel,
@ -370,8 +333,6 @@ export default {
blockClick,
iconClick,
clearActive,
taskStatus,
deployTips,
openBlockShotPanel,
iconSettingMove,
defaultImg,
@ -379,7 +340,8 @@ export default {
handleSettingMouseOver,
handleShowVersionMenu,
editBlock,
format
format,
handleSelectAll
}
}
}
@ -446,6 +408,11 @@ export default {
}
}
.col-checkbox {
display: flex;
flex-direction: column;
align-items: center;
}
.col-checkbox,
.block-item-small-list:deep(.table-selection) {
width: 40px;
@ -485,16 +452,17 @@ export default {
.publish-flag {
position: absolute;
left: 2px;
top: 2px;
left: 4px;
top: 4px;
text-align: center;
display: block;
color: var(--ti-lowcode-common-secondary-text-color);
color: var(--te-common-text-primary);
font-size: 12px;
background-color: var(--ti-lowcode-component-block-list-item-tag-bg);
padding: 2px;
border-radius: 4px 0 4px 0;
background-color: var(--te-common-bg-prompt);
padding: 2px 4px;
border-radius: 2px;
transform: scale(0.9);
min-width: 45px;
}
&.block-item-small-list {
@ -507,7 +475,7 @@ export default {
margin-left: 8px;
}
.item-text {
width: calc(35% - 62px);
width: 50%;
}
.publish-flag {
position: static;
@ -527,6 +495,9 @@ export default {
}
}
}
&:hover {
background-color: var(--te-common-bg-container);
}
}
&:nth-child(even) {
border-right: 0;
@ -584,7 +555,7 @@ export default {
.item-text {
font-size: 12px;
}
.tiny-svg {
.svg-icon {
font-size: 24px;
color: var(--ti-lowcode-component-svg-button-color);
}
@ -770,7 +741,6 @@ export default {
box-sizing: border-box;
cursor: pointer;
&:hover {
background-color: var(--ti-lowcode-component-block-setting-item-hover-bg);
color: var(--ti-lowcode-common-primary-text-color);
}
.list-item-icon {
@ -781,7 +751,7 @@ export default {
color: var(--te-common-icon-secondary);
}
.list-item-svg:hover {
background-color: var(--ti-lowcode-component-block-setting-item-hover-bg);
background-color: transparent;
}
}
}

View File

@ -9,15 +9,14 @@
</slot>
<div class="button-group-wrap">
<slot name="header">
<div v-if="showIfFullScreen" class="cursor" @click="fullScreen">
<icon-fullscreen v-if="!state.isFullScreen" class="icon-fullscreen"></icon-fullscreen>
<icon-minscreen v-if="state.isFullScreen" class="icon-minscreen"></icon-minscreen>
<span class="full-screen-label">{{ getFullScreenLabel(state.isFullScreen) }}</span>
</div>
<tiny-button v-if="!isIconButton" type="info" @click="$emit('save')" class="plugin-save">保存</tiny-button>
<tiny-button v-if="isIconButton" :icon="icon" type="info" @click="$emit('add')">
{{ iconButtonText }}
</tiny-button>
<div v-if="showIfFullScreen" class="cursor" @click="fullScreen">
<svg-icon v-if="!state.isFullScreen" name="full-screen"></svg-icon>
<svg-icon v-if="state.isFullScreen" name="cancel-full-screen"></svg-icon>
</div>
<icon-close class="icon-close close" @click="$emit('cancel')"></icon-close>
</slot>
</div>
@ -32,7 +31,7 @@
<script>
import { reactive, watchEffect } from 'vue'
import { Button } from '@opentiny/vue'
import { iconPlus, iconFullscreen, iconMinscreen, iconClose } from '@opentiny/vue-icon'
import { iconPlus, iconClose } from '@opentiny/vue-icon'
const EVENTS = {
FULL_SCREEN_CHANGE: 'fullScreenChange',
@ -45,8 +44,6 @@ const EVENTS = {
export default {
components: {
TinyButton: Button,
IconFullscreen: iconFullscreen(),
IconMinscreen: iconMinscreen(),
IconClose: iconClose()
},
props: {
@ -114,8 +111,6 @@ export default {
state,
fullScreen,
getFullScreenLabel,
IconFullscreen: iconFullscreen(),
IconMinscreen: iconMinscreen(),
IconClose: iconClose()
}
}
@ -125,7 +120,7 @@ export default {
<style lang="less" scoped>
.plugin-setting {
position: absolute;
left: calc(var(--base-left-panel-width) - 6px);
left: var(--base-left-panel-width);
top: 0;
width: var(--base-collection-panel-width);
height: 100%;
@ -134,8 +129,9 @@ export default {
overflow: hidden;
border-left: 1px solid var(--ti-lowcode-plugin-panel-header-border-bottom-color);
&:not(.second-panel) {
box-shadow: 6px 0px 3px 0px rgba(0, 0, 0, 0.05);
box-shadow: 6px 0px 3px 0px var(--te-base-box-shadow-rgba-3);
border-right: none;
border-left: none;
}
&.full-screen {
width: var(--base-collection-panel-full-screen-width);
@ -154,11 +150,12 @@ export default {
}
.cursor {
margin-left: 12px;
cursor: pointer;
}
.close {
margin-left: 16px;
margin-left: 12px;
cursor: pointer;
}
@ -176,6 +173,10 @@ export default {
.plugin-setting-header-title {
font-size: 12px;
font-weight: 700;
margin-right: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:deep(.svg-button + .svg-button) {
margin: 0;

View File

@ -1,34 +0,0 @@
<template>
<div>
<tiny-progress
v-if="editBlock?.isShowProgress"
class="progress-bar"
:stroke-width="2"
:text-inside="true"
:percentage="editBlock?.publishProgress"
status="success"
></tiny-progress>
</div>
</template>
<script>
import { computed } from 'vue'
import { Progress as TinyProgress } from '@opentiny/vue'
import { getMetaApi, META_APP } from '@opentiny/tiny-engine-meta-register'
export default {
components: {
TinyProgress
},
setup() {
const { getEditBlock } = getMetaApi(META_APP.BlockManage) || {}
const editBlock = getEditBlock ? computed(getEditBlock) : null
return {
editBlock
}
}
}
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,42 @@
<template>
<tiny-checkbox class="block-select-all" :indeterminate="isIndeterminate" v-model="selectedAll">
{{ hiddenLabel ? '' : '全选' }}
</tiny-checkbox>
</template>
<script setup>
import { Checkbox as TinyCheckbox } from '@opentiny/vue'
import { computed, defineEmits, defineProps } from 'vue'
const props = defineProps({
allItems: {
type: Array,
default: () => []
},
selected: {
type: Array,
default: () => []
},
hiddenLabel: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['selectAll'])
const selectedAll = computed({
get() {
return props.allItems.length > 0 && props.allItems.length === props.selected.length
},
set(value) {
if (value) {
emit('selectAll', props.allItems)
} else {
emit('selectAll', null)
}
}
})
const isIndeterminate = computed(() => props.selected.length > 0 && props.selected.length !== props.allItems.length)
</script>

View File

@ -1,5 +1,5 @@
<template>
<span class="svg-button" @click="$emit('click', $event)">
<span class="svg-button" :class="{ 'svg-button-hover': hoverBgColor }" @click="$emit('click', $event)">
<tiny-tooltip effect="dark" :content="tips" :placement="placement">
<component :is="name" v-if="isTinyIcon" />
<svg-icon v-else :name="name"></svg-icon>
@ -27,6 +27,10 @@ export default {
name: {
type: String,
default: 'add'
},
hoverBgColor: {
type: Boolean,
default: true
}
},
emits: ['click'],
@ -45,17 +49,13 @@ export default {
width: 24px;
height: 24px;
font-size: 16px;
color: var(--ti-lowcode-component-svg-button-color);
color: var(--te-common-icon-secondary);
border: 1px solid transparent;
border-radius: 4px;
display: inline-flex;
justify-content: center;
align-items: center;
cursor: pointer;
&:hover {
color: var(--ti-lowcode-component-svg-button-hover-color);
background-color: var(--ti-lowcode-component-svg-button-hover-bg-color);
}
&.active {
color: var(--ti-lowcode-component-svg-button-active-color);
background-color: var(--ti-lowcode-component-svg-button-active-bg-color);
@ -68,4 +68,11 @@ export default {
outline: none;
}
}
.svg-button-hover {
color: var(--te-common-icon-primary);
&:hover {
color: var(--te-common-icon-primary);
background-color: var(--te-common-bg-prompt);
}
}
</style>

View File

@ -35,6 +35,7 @@ export { default as BlockLinkField } from './BlockLinkField.vue'
export { default as BlockLinkEvent } from './BlockLinkEvent.vue'
export { default as BlockDescription } from './BlockDescription.vue'
export { default as PluginBlockList } from './PluginBlockList.vue'
export { default as SelectAll } from './SelectAll.vue'
export { default as ButtonGroup } from './ButtonGroup.vue'
export { default as CloseIcon } from './CloseIcon.vue'
export { default as LifeCycles } from './LifeCycles.vue'
@ -44,7 +45,6 @@ export { default as VueMonaco, setGlobalMonacoEditorTheme } from './VueMonaco.vu
export { default as PublicIcon } from './PublicIcon.vue'
export { default as BindI18n } from './BindI18n.vue'
export { default as BlockDeployDialog } from './BlockDeployDialog.vue'
export { default as ProgressBar } from './ProgressBar.vue'
export { default as SearchEmpty } from './SearchEmpty.vue'
export { default as MetaDescription } from './MetaDescription.vue'
export { default as MetaList } from './MetaList.vue'

View File

@ -1,15 +1,14 @@
<template>
<tiny-button v-bind="extendAttrs" class="toolbar-button">
<span class="svg-wrap">
<div class="svg-wrap">
<span v-if="options?.showDots" class="dot"></span>
<tiny-button class="toolbar-button">
<svg-icon v-if="icon" :name="icon"></svg-icon>
<span v-if="options?.showDots" class="dots"></span>
</span>
<span class="save-title">{{ content }}</span>
<slot></slot>
</tiny-button>
<span class="save-title">{{ content }}</span>
<slot></slot>
</tiny-button>
</div>
</template>
<script>
import { inject } from 'vue'
import { Button } from '@opentiny/vue'
export default {
@ -29,12 +28,6 @@ export default {
type: Object,
default: () => ({})
}
},
setup() {
const extendAttrs = inject('extend-attributes') || {}
return {
extendAttrs
}
}
}
</script>
@ -52,20 +45,19 @@ export default {
.svg-wrap {
position: relative;
.dots {
.dot {
position: absolute;
width: 6px;
height: 6px;
background: var(--ti-lowcode-toolbar-dot-color);
border-radius: 50%;
display: inline-block;
position: absolute;
top: -2px;
right: -2px;
top: -3px;
right: 2px;
z-index: 100;
}
}
.save-title {
margin: 0 6px;
.svg-icon.svg-icon.svg-icon {
color: var(--te-common-icon-primary);
}
}
</style>

View File

@ -8,7 +8,7 @@
>
<template #reference>
<span class="icon">
<span class="icon-hides" v-bind="extendAttrs">
<span class="icon-hides" v-bind="$attrs">
<svg-icon :name="icon"></svg-icon>
<span v-if="options?.showDots" class="dots"></span>
</span>
@ -17,7 +17,6 @@
</tiny-popover>
</template>
<script>
import { inject } from 'vue'
import { Popover } from '@opentiny/vue'
export default {
@ -35,13 +34,7 @@ export default {
},
options: {
type: Object,
default: () => {}
}
},
setup() {
const extendAttrs = inject('extend-attributes') || {}
return {
extendAttrs
default: () => ({})
}
}
}

View File

@ -49,10 +49,35 @@ const getAllNestedBlocksSchema = async (pageSchema, fetchBlockSchemaApi, blockSe
const extraList = []
schemaList.forEach((item) => {
if (item.status === 'fulfilled' && item.value?.[0]?.content) {
res.push(item.value[0].content)
extraList.push(getAllNestedBlocksSchema(item.value[0].content, fetchBlockSchemaApi, blockSet))
const blockItem = item.value?.[0]
if (item.status !== 'fulfilled' || !blockItem) {
return
}
const historyId = blockItem?.current_history
const historySchema = blockItem?.histories?.find?.((historyItem) => historyItem?.id === historyId)
let schemaContent = null
if (historyId && historySchema?.content) {
schemaContent = historySchema.content
} else {
schemaContent = blockItem?.content
}
if (!schemaContent) {
return
}
// 区块 schema 中加上当前版本号,让后续数据支持缓存或更丰富的操作
schemaContent.version = historyId || ''
// 区块子依赖
schemaContent.subBlockDeps = blockNames
res.push(schemaContent)
extraList.push(getAllNestedBlocksSchema(schemaContent, fetchBlockSchemaApi, blockSet))
})
;(await Promise.allSettled(extraList)).forEach((item) => {
if (item.status === 'fulfilled' && item.value) {

View File

@ -16,7 +16,7 @@ import { useResource, getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-met
export const getCanvasStatus = (data) => {
const globalState = getMetaApi(META_SERVICE.GlobalService).getState()
// 写死ID 待删除
let isDemo = useResource().resState.isDemo
let isDemo = useResource().appSchemaState.isDemo
const { resetPasswordToken } = globalState.userInfo
if (isDemo && [PAGE_STATUS.Developer, PAGE_STATUS.SuperAdmin].includes(resetPasswordToken)) {

View File

@ -71,7 +71,7 @@ const getSnippetsSuggestions = (monaco, range, wordContent) =>
.filter(({ insertText }) => insertText.indexOf(wordContent) === 0)
const getUserWords = () => {
const { bridge = [], dataSource = [], utils = [], globalState = [] } = useResource().resState
const { bridge = [], dataSource = [], utils = [], globalState = [] } = useResource().appSchemaState
return {
state: {

View File

@ -42,7 +42,7 @@ const open = (params = {}) => {
openUrl = isDevelopEnv
? `./preview.html?tenant=${tenant}#${hashString}`
: `${href}/preview?tenant=${tenant}#${hashString}`
: `${href.endsWith('/') ? href : `${href}/`}preview?tenant=${tenant}#${hashString}`
const aTag = document.createElement('a')
aTag.href = openUrl

View File

@ -1,6 +1,6 @@
{
"name": "@opentiny/tiny-engine-common",
"version": "2.0.0-rc.4",
"version": "2.1.0",
"publishConfig": {
"access": "public"
},

View File

@ -1,6 +1,6 @@
{
"name": "@opentiny/tiny-engine-configurator",
"version": "2.0.0-rc.4",
"version": "2.1.0",
"description": "",
"main": "dist/index.js",
"module": "dist/index.js",

View File

@ -220,7 +220,7 @@ export default {
justify-content: space-between;
align-items: center;
padding: 10px 0;
color: var(--ti-lowcode-toolbar-breadcrumb-color);
color: var(--te-common-text-secondary);
.header-title {
font-size: 14px;

View File

@ -4,22 +4,22 @@
<tiny-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id"> </tiny-option>
</tiny-select>
<tiny-tooltip class="item" effect="dark" content="刷新数据源" placement="top">
<icon-conment-refresh @click="refreshData"></icon-conment-refresh>
<icon-common-refresh @click="refreshData"></icon-common-refresh>
</tiny-tooltip>
</div>
</template>
<script lang="jsx">
import { useModal, getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register'
import { Option, Select, Tooltip } from '@opentiny/vue'
import { IconConmentRefresh } from '@opentiny/vue-icon'
import { nextTick, ref } from 'vue'
import { Option, Select, Tooltip } from '@opentiny/vue'
import { iconConmentRefresh as iconCommonRefresh } from '@opentiny/vue-icon'
import { useModal, getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register'
export default {
components: {
TinySelect: Select,
TinyOption: Option,
IconConmentRefresh: IconConmentRefresh(),
IconCommonRefresh: iconCommonRefresh(),
TinyTooltip: Tooltip
},
props: {
@ -48,6 +48,8 @@ export default {
const fetchDataSourceList = (appId) => getMetaApi(META_SERVICE.Http).get(`/app-center/api/sources/list/${appId}`)
const appId = getMetaApi(META_SERVICE.GlobalService).getBaseInfo().id
//
fetchDataSourceList(appId).then((data) => {
options.value = data
})

View File

@ -4,7 +4,7 @@
<template #content="{ data }">
<div class="item-text">
<div class="tiny-input">
<input v-model="data.props.title" class="tiny-input__inner" />
<tiny-input v-model="data.props.title" @update:modelValue="onTitleUpdate(data)" />
</div>
</div>
</template>
@ -20,16 +20,18 @@
</div>
</template>
<script>
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import { Input } from '@opentiny/vue'
import { MetaListItems } from '@opentiny/tiny-engine-common'
import { useProperties, useMaterial, useHistory } from '@opentiny/tiny-engine-meta-register'
import { useProperties, useMaterial, useHistory, useCanvas } from '@opentiny/tiny-engine-meta-register'
import { utils } from '@opentiny/tiny-engine-utils'
import { iconDel } from '@opentiny/vue-icon'
export default {
components: {
MetaListItems,
IconDel: iconDel()
IconDel: iconDel(),
TinyInput: Input
},
setup() {
const { children: schemaChildren, componentName } = useProperties().getSchema()
@ -37,33 +39,62 @@ export default {
const childComponentName =
configureMap[componentName]?.nestingRule?.childWhitelist?.[0] || schemaChildren?.[0]?.componentName
schemaChildren.forEach((item) => {
if (!item.props) {
item.props = {
title: '选项卡',
name: ''
const updateChildrenToValid = () => {
const schema = useProperties().getSchema()
const schemaChildren = schema.children || []
let hasUpdate = false
const newChildren = schemaChildren.map((item) => {
if (!item.props) {
hasUpdate = true
item.props = {
title: '选项卡',
name: ''
}
}
return item
})
const { operateNode } = useCanvas()
if (hasUpdate) {
operateNode({ type: 'updateAttributes', id: schema.id, value: { children: newChildren } })
}
}
onMounted(() => {
updateChildrenToValid()
})
const children = ref(schemaChildren)
const addChildren = () => {
const name = utils.guid()
schemaChildren.push({
const schema = useProperties().getSchema()
const newNodeData = {
componentName: childComponentName,
props: {
title: '选项卡',
name
},
children: [{ componentName: 'div' }]
})
}
const { operateNode } = useCanvas()
operateNode({ type: 'insert', parentId: schema.id, newNodeData, position: 'after' })
children.value = [...schemaChildren]
}
const delChildren = (data) => {
schemaChildren.splice(children.value.indexOf(data), 1)
const { operateNode } = useCanvas()
operateNode({ type: 'delete', id: data.id })
children.value = [...schemaChildren]
useHistory().addHistory()
}
@ -74,14 +105,34 @@ export default {
return
}
const list = schemaChildren
const spliceItem = list.splice(oldIndex, 1)
const schema = useProperties().getSchema()
const schemaChildren = schema.children
list.splice(newIndex, 0, ...spliceItem)
children.value = [...list]
const { operateNode } = useCanvas()
const newNodeData = schemaChildren[oldIndex]
const referTargetNodeId = schemaChildren[newIndex].id
operateNode({ type: 'delete', id: schemaChildren[oldIndex].id })
operateNode({
type: 'insert',
parentId: schema.id,
newNodeData,
position: newIndex < oldIndex ? 'before' : 'after',
referTargetNodeId
})
children.value = [...schemaChildren]
}
return { children, addChildren, delChildren, dragEnd }
const onTitleUpdate = (value) => {
const { operateNode } = useCanvas()
const id = value.id
operateNode({ type: 'changeProps', id, value: { props: value.props } })
}
return { children, addChildren, delChildren, dragEnd, onTitleUpdate }
}
}
</script>

View File

@ -48,7 +48,7 @@
</template>
<script>
import { reactive, ref, watchEffect } from 'vue'
import { useProperties, useMaterial } from '@opentiny/tiny-engine-meta-register'
import { useProperties, useMaterial, useCanvas } from '@opentiny/tiny-engine-meta-register'
import { IconDel, IconEdit, IconClose, IconPlus } from '@opentiny/vue-icon'
import { Form, FormItem, Input, Button, Popover, Tooltip } from '@opentiny/vue'
import { utils } from '@opentiny/tiny-engine-utils'
@ -95,12 +95,26 @@ export default {
})
for (const [key, value] of Object.entries(useProperties().getSchema()?.props)) {
!properties.includes(key) &&
const isExcludedKey = properties.includes(key)
if (isExcludedKey) {
continue
}
const index = attrs.value.findIndex(({ data: { key: attrKey } }) => attrKey === key)
if (index === -1) {
attrs.value.push({
text: `${key} = '${value}'`,
data: { key, value },
id: utils.guid()
})
} else {
Object.assign(attrs.value[index], {
text: `${key} = '${value}'`,
data: { key, value }
})
}
}
})
@ -110,7 +124,10 @@ export default {
const updateSchema = () => {
const mergeProps = {}
const { props } = useProperties().getSchema()
const { operateNode } = useCanvas()
const { getSchema } = useProperties()
const { props } = getSchema()
for (const [key, value] of Object.entries(props)) {
if (properties.includes(key)) {
mergeProps[key] = value
@ -121,7 +138,9 @@ export default {
mergeProps[attr.data.key] = attr.data.value
})
useProperties().getSchema().props = mergeProps
const schema = useProperties().getSchema()
operateNode({ type: 'updateAttributes', id: schema.id, value: { props: mergeProps } })
}
const save = () => {
@ -195,8 +214,8 @@ export default {
width: 24px;
height: 24px;
font-size: 16px;
color: var(--ti-lowcode-toolbar-breadcrumb-color);
background: var(--ti-lowcode-canvas-wrap-bg);
color: var(--te-common-text-secondary);
background: var(--te-common-bg-container);
border: 1px solid var(--ti-lowcode-left-button-border-color);
border-radius: 2px;
display: inline-flex;
@ -223,7 +242,7 @@ export default {
border: 1px solid var(--ti-lowcode-optionitem-border-color);
background: var(--ti-lowcode-optionitem-background-color);
margin-bottom: -1px;
color: var(--ti-lowcode-toolbar-breadcrumb-color);
color: var(--te-common-text-secondary);
padding: 7px;
display: grid;
grid-template-columns: 3fr auto;

View File

@ -15,7 +15,7 @@
</template>
<script>
import { reactive, computed } from 'vue'
import { useProperties } from '@opentiny/tiny-engine-meta-register'
import { useProperties, useCanvas } from '@opentiny/tiny-engine-meta-register'
import InputConfigurator from '../input-configurator/InputConfigurator.vue'
export default {
@ -44,12 +44,20 @@ export default {
const triggerType = (item, index) => {
state.active = index
useProperties().getSchema().componentName = item.toLowerCase()
const { operateNode } = useCanvas()
const schema = useProperties().getSchema()
if (schema) {
operateNode({ type: 'updateAttributes', id: schema.id, value: { componentName: item.toLowerCase() } })
}
}
const change = (value) => {
if (useProperties().getSchema()) {
useProperties().getSchema().children = value
const { operateNode } = useCanvas()
const schema = useProperties().getSchema()
if (schema) {
operateNode({ type: 'updateAttributes', id: schema.id, value: { children: value } })
}
}

View File

@ -66,7 +66,7 @@ export default {
font-size: 16px;
&:hover {
cursor: pointer;
color: var(--ti-lowcode-dialog-font-color);
color: var(--te-common-text-primary);
}
}
</style>

View File

@ -5,7 +5,7 @@
<div v-for="(slot, index) in slotList" :key="slot.name" class="use-slot">
<div class="use-slot-item-name">
{{ slot.name }}
<tiny-tooltip effect="dark" :content="state.currentComponent?.content" placement="top">
<tiny-tooltip effect="dark" :content="state.currentComponent?.content" placement="bottom" width="260">
<span class="item-icon">
<component :is="state.currentComponent?.icon"></component>
</span>
@ -14,7 +14,7 @@
<div class="use-slot-item-content">
<div class="use-slot-switch-wrap">
<div :class="['e__switch', { 'e_is-checked': slot.bind }]">
<span class="e__switch-core" @click="toggleSlot(index, slot)"></span>
<span class="e__switch-core" @click="updateSlot(index, slot)"></span>
</div>
</div>
<tiny-form-item
@ -25,7 +25,7 @@
<tiny-input
v-model="slot.params"
class="use-slot-params"
@change="validParams(paramsPropPath(index), slot)"
@change="validParams(slot, paramsPropPath(index))"
></tiny-input>
</tiny-form-item>
</div>
@ -74,7 +74,7 @@ export default {
TinyGrid: {
content:
'暴露给插槽使用的变量为解构的参数可以使用多个用逗号分隔row(行数据)column(列数据)$table(内部表格实例)seq(序号)cell(单元格)columnIndex(列索引),rowIndex(行索引)',
icon: SvgICons['IconUnknow']()
icon: SvgICons['IconHelpCircle']()
}
}
@ -94,51 +94,7 @@ export default {
}
}
const updateSlotParams = (slotData) => {
emit('update:modelValue', slotData)
// jsslot
const [propsName] = path.split('.')
const schema = useProperties().getSchema()
schema.props[propsName] = JSON.parse(JSON.stringify(schema.props[propsName]))
}
const setSlotParams = ({ name, params = '' }) => {
if (!props.modelValue?.[name]) {
return
}
const slotData = { ...(props.modelValue || {}) }
if (params.length) {
slotData[name].params = params.split(',')
} else {
delete slotData[name].params
}
updateSlotParams(slotData)
}
const toggleSlot = (idx, { bind, name, params = '' }) => {
//
if (bind) {
useModal().confirm({
title: '提示',
message: '关闭后插槽内的内容将被清空,是否继续?',
status: 'info',
exec: () => {
slotList.value[idx].bind = false
const { [name]: _deleted, ...rest } = { ...(props.modelValue || {}) }
updateSlotParams(rest)
}
})
return
}
//
slotList.value[idx].bind = true
const updateSlot = (idx, { bind, name, params = '' }, isToggleSlot = true) => {
const slotInfo = {
[name]: {
type: 'JSSlot',
@ -150,17 +106,71 @@ export default {
}
}
const slotData = { ...slotInfo, ...(props.modelValue || {}) }
const { setProp } = useProperties()
const { setNode, operateNode } = useCanvas()
if (params.length) {
slotInfo[name].params = params.split(',')
slotData[name].params = params.split(',')
} else {
delete slotData[name].params
}
updateSlotParams({ ...(props.modelValue || {}), ...slotInfo })
if (bind && isToggleSlot) {
useModal().confirm({
title: '提示',
message: '关闭后插槽内的内容将被清空,是否继续?',
status: 'info',
exec: () => {
slotList.value[idx].bind = false
operateNode({ type: 'delete', id: slotData[name].value[0].id })
delete slotData[name]
emit('update:modelValue', slotData)
const [propsName] = path.split('.')
const schema = useProperties().getSchema()
setProp(propsName, schema.props[propsName])
}
})
} else {
slotList.value[idx].bind = true
}
emit('update:modelValue', slotData)
// jsslot
const [propsName] = path.split('.')
const schema = useProperties().getSchema()
for (const data of Object.values(slotData)) {
if (data.value?.[0] && !data.value[0]?.id) {
//
setNode(data.value[0], schema)
}
}
setProp(propsName, schema.props[propsName])
}
const validParams = (paramsPath, slot) => {
const setParams = (slot) => {
if (slot.bind) {
const slotData = props.modelValue
const { params, name } = slot
if (params.length) {
slotData[name].params = params.split(',')
} else {
delete slotData[name].params
}
}
}
const validParams = (slot, paramsPath) => {
slotRef.value.validateField([paramsPath], (error) => {
if (!error) {
slot.bind && setSlotParams(slot)
setParams(slot)
}
})
}
@ -171,12 +181,13 @@ export default {
})
return {
toggleSlot,
updateSlot,
slotList,
paramsPropPath,
slotRef,
paramsStringValidator,
validParams,
setParams,
state,
componentsMap
}
@ -202,6 +213,7 @@ export default {
.use-slot-item-content {
display: flex;
justify-content: space-between;
}
.use-slot-switch-wrap {
@ -227,6 +239,7 @@ export default {
.slot-name-form-item {
margin-bottom: 0;
margin-right: 6px;
}
.item-icon {
@ -240,6 +253,7 @@ export default {
position: relative;
font-size: 14px;
line-height: 20px;
width: 40px;
height: 20px;
vertical-align: middle;
cursor: pointer;
@ -262,7 +276,7 @@ export default {
.e__switch-core::after {
content: '';
position: absolute;
top: 1px;
top: 2px;
left: 1px;
border-radius: 100%;
transition: all 0.3s;

View File

@ -36,7 +36,7 @@
<script>
import { reactive, ref, computed } from 'vue'
import { SplitPanes, Pane } from '@opentiny/tiny-engine-common'
import { useProperties } from '@opentiny/tiny-engine-meta-register'
import { useProperties, useCanvas } from '@opentiny/tiny-engine-meta-register'
export default {
components: {
@ -195,6 +195,18 @@ export default {
const spans = item.split(':')
mode.value = item
const { operateNode } = useCanvas()
const schema = useProperties().getSchema()
if (!schema) {
// eslint-disable-next-line no-console
console.error('Error! There is no node selected. 错误!当前没有节点被选中。')
return
}
const cols = [...(schema?.children || [])]
spans.forEach((span, index) => {
if (cols[index]) {
cols[index].props.span = span
@ -217,7 +229,7 @@ export default {
}
})
cols.length = spans.length
operateNode({ type: 'updateAttributes', id: schema.id, value: { children: cols.slice(0, spans.length) } })
}
const modeClick = (index, item) => {
@ -268,7 +280,7 @@ export default {
box-sizing: border-box;
width: 48px;
border: 1px solid var(--ti-lowcode-tabs-border-color);
background: var(--ti-lowcode-canvas-wrap-bg);
background: var(--te-common-bg-container);
border-radius: 4px;
padding: 4px;
margin: 0 4px 4px 4px;
@ -313,13 +325,13 @@ export default {
justify-content: center;
align-items: center;
display: flex;
background: var(--ti-lowcode-canvas-wrap-bg);
color: var(--ti-lowcode-toolbar-breadcrumb-color);
background: var(--te-common-bg-container);
color: var(--te-common-text-secondary);
transition: none;
}
:deep(.splitpanes__splitter) {
background: var(--ti-lowcode-toolbar-view-hover-bg);
background: var(--te-common-bg-container);
border-left: var(--ti-lowcode-tabs-border-color);
}
@ -328,7 +340,7 @@ export default {
.customize {
margin-bottom: 8px;
color: var(--ti-lowcode-toolbar-breadcrumb-color);
color: var(--te-common-text-secondary);
}
}
}

View File

@ -90,7 +90,7 @@ export default {
let newValue = value
if (value?.type === CONSTANT.JSEXPRESSION) {
const pageSchema = useCanvas().canvasApi.value.getSchema()
const pageSchema = useCanvas().getSchema()
const stateName = value?.value?.replace(CONSTANT.STATE, '')
newValue = pageSchema?.state?.[stateName]
}

View File

@ -107,15 +107,15 @@ export default {
padding-right: 20px;
width: 136px;
cursor: pointer;
background: var(--ti-lowcode-tabs-bg);
color: var(--ti-lowcode-toolbar-breadcrumb-color);
background: var(--te-common-bg-prompt);
color: var(--te-common-text-secondary);
border: 1px solid var(--ti-lowcode-tabs-active-bg);
border-radius: 3px;
.icon-box {
border-right: 1px solid var(--ti-lowcode-tabs-active-bg);
padding: 4px 8px;
.tiny-svg {
color: var(--ti-lowcode-toolbar-breadcrumb-color);
color: var(--te-common-text-secondary);
font-size: 14px;
}
}

Some files were not shown because too many files have changed in this diff Show More