feat(vue-dsl): add app generate code (#178)

* feat(vue-dsl): add app generate code

* feat(vue-dsl): add app generate code

* feat(vue-dsl): add generate global store code

* feat(vue-dsl): delete parse config and parse schema hook

* feat(tempalte): refra generateTemplate logic

* feat(vue-generator): add generate script frame code

* feat(vue-generator): add hook flow for sfc file generate

* feat(vue-generator): support generate sfc script

* feat(vue-generator): support jsx generate

* fix(vue-generator): fix double quotes issue

* feat(vue-generator): handle app generate code

* feat(toolbar-generate-vue): toolbar-generate-vue use new codegen function

* feat(vue-generator): add requiredBlock parser

* feat(docs): add readme doc

* feat(docs): add more docs

* fix(vue-generator): adapt for more scenario

* feat(vue-generator): support tiny-grid editor use tiny component

* fix(vue-generator): complete unit test

* fix(vue-generator): add unit test

* feat(vue-generator): add sfc generator unit test

* feat(vue-generator[docs]): add contributing docs

* feat(vue-generator): add test coverage script and app generate test case

* fix(generate-vue): optimize desc and file list

* fix(vue-generate): [template] fix viteConfig process.env is processed

* fix(vue-generator): escape process.env string

* feat(vue-generator): support builtin components

* fix(vue-generator): add builtin componentsMap

* fix(vue-generator): fix bind utils props

* fix(vue-generator): support utils expression

* fix(vue-generator): support children expression add utils

* fix(vue-generator): support nested folder page

* fix(vue-generator): support get datasource with name

* fix(vue-generator): only write necessary dependencies into package.json

* feat(vue-generator): simplified genTemplate hooks

* fix(vue-generator): update vue-generator  docs

* feat(vue-generator): detect jsx on custom  method

* feat(vue-generator): add d.ts types declaration
This commit is contained in:
chilingling 2024-05-12 20:42:32 -07:00 committed by GitHub
parent aa8d361bd2
commit 3a66996fae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
128 changed files with 10770 additions and 138 deletions

View File

@ -1787,7 +1787,7 @@
},
{
"componentName": "TinyPlusFrozenPage",
"package": "@opentiny/vuee",
"package": "@opentiny/vue",
"exportName": "FrozenPage",
"destructuring": true,
"version": "3.4.1"

View File

@ -37,6 +37,7 @@
"@opentiny/tiny-engine-canvas": "workspace:*",
"@opentiny/tiny-engine-common": "workspace:*",
"@opentiny/tiny-engine-controller": "workspace:*",
"@opentiny/tiny-engine-dsl-vue": "workspace:*",
"@opentiny/tiny-engine-http": "workspace:*",
"@opentiny/tiny-engine-i18n-host": "workspace:*",
"@opentiny/tiny-engine-plugin-block": "workspace:*",

View File

@ -18,11 +18,11 @@ import { defineComponent, computed, defineAsyncComponent } from 'vue'
import { Repl, ReplStore } from '@vue/repl'
import vueJsx from '@vue/babel-plugin-jsx'
import { transformSync } from '@babel/core'
import { Notify } from '@opentiny/vue'
import { genSFCWithDefaultPlugin, parseRequiredBlocks } from '@opentiny/tiny-engine-dsl-vue'
import importMap from './importMap'
import srcFiles from './srcFiles'
import generateMetaFiles, { processAppJsCode } from './generate'
import { getSearchParams, fetchCode, fetchMetaData } from './http'
import { getSearchParams, fetchMetaData, fetchAppSchema, fetchBlockSchema } from './http'
import { PanelType, PreviewTips } from '../constant'
import { injectDebugSwitch } from './debugSwitch'
import '@vue/repl/style.css'
@ -71,31 +71,68 @@ export default {
const newImportMap = { imports: { ...importMap.imports, ...utilsImportMaps } }
store.setImportMap(newImportMap)
}
const getBlocksSchema = async (pageSchema, blockSet = new Set()) => {
let res = []
const blockNames = parseRequiredBlocks(pageSchema)
const promiseList = blockNames
.filter((name) => {
if (blockSet.has(name)) {
return false
}
blockSet.add(name)
return true
})
.map((name) => fetchBlockSchema(name))
const schemaList = await Promise.allSettled(promiseList)
schemaList.forEach((item) => {
if (item.status === 'fulfilled' && item.value?.[0]?.content) {
res.push(item.value[0].content)
res.push(...getBlocksSchema(item.value[0].content, blockSet))
}
})
return res
}
const queryParams = getSearchParams()
const promiseList = [fetchCode(queryParams), fetchMetaData(queryParams), setFiles(srcFiles, 'src/Main.vue')]
Promise.all(promiseList).then(([codeList, metaData]) => {
const promiseList = [
fetchAppSchema(queryParams?.app),
fetchMetaData(queryParams),
setFiles(srcFiles, 'src/Main.vue')
]
Promise.all(promiseList).then(async ([appData, metaData]) => {
addUtilsImportMap(metaData.utils || [])
const codeErrorMsgs = codeList
.filter(({ errors }) => errors?.length)
.map(({ errors }) => errors)
.flat()
.map(({ message }) => message)
if (codeErrorMsgs.length) {
const title = PreviewTips.ERROR_WHEN_COMPILE
Notify({
type: 'error',
title,
message: codeErrorMsgs.join('\n'),
//
duration: 0,
position: 'top-right'
const blocks = await getBlocksSchema(queryParams.pageInfo?.schema)
// TODO: block schema
// TODO: block
const pageCode = [
{
panelName: 'Main.vue',
panelValue:
genSFCWithDefaultPlugin(queryParams.pageInfo?.schema, appData?.componentsMap || [], {
blockRelativePath: './'
}) || '',
panelType: 'vue',
index: true
},
...(blocks || []).map((blockSchema) => {
return {
panelName: blockSchema.fileName,
panelValue:
genSFCWithDefaultPlugin(blockSchema, appData?.componentsMap || [], { blockRelativePath: './' }) || '',
panelType: 'vue',
index: true
}
})
return title
}
]
// [@vue/repl] `Only lang="ts" is supported for <script> blocks.`
const langReg = /lang="jsx"/
@ -143,7 +180,7 @@ export default {
newFiles['app.js'] = appJsCode
codeList.map(fixScriptLang).forEach(assignFiles)
pageCode.map(fixScriptLang).forEach(assignFiles)
const metaFiles = generateMetaFiles(metaData)
Object.assign(newFiles, metaFiles)

View File

@ -50,3 +50,6 @@ export const fetchMetaData = async ({ platform, app, type, id, history, tenant }
params: { platform, app, type, id, history }
})
: {}
export const fetchAppSchema = async (id) => http.get(`/app-center/v1/api/apps/schema/${id}`)
export const fetchBlockSchema = async (blockName) => http.get(`/material-center/api/block?label=${blockName}`)

View File

@ -40,4 +40,69 @@ srcFiles['locales.js'] = localesJS
srcFiles['stores.js'] = storesJS
srcFiles['storesHelper.js'] = storesHelperJS
export const genPreviewTemplate = () => {
return [
{
fileName: 'App.vue',
path: '',
fileContent: appVue
},
{
fileName: 'constant.js',
path: '',
fileContent: constantJS
},
{
fileName: 'app.js',
path: '',
fileContent: appJS.replace(/VITE_CDN_DOMAIN/g, import.meta.env.VITE_CDN_DOMAIN)
},
{
fileName: 'injectGlobal.js',
path: '',
fileContent: injectGlobalJS
},
{
fileName: 'lowcode.js',
path: '',
fileContent: lowcodeJS
},
{
fileName: 'dataSourceMap.js',
path: '',
fileContent: dataSourceMapJS
},
{
fileName: 'dataSource.js',
path: '',
fileContent: dataSourceJS
},
{
fileName: 'utils.js',
path: '',
fileContent: utilsJS
},
{
fileName: 'bridge.js',
path: '',
fileContent: bridgeJS
},
{
fileName: 'locales.js',
path: '',
fileContent: localesJS
},
{
fileName: 'stores.js',
path: '',
fileContent: storesJS
},
{
fileName: 'storesHelper.js',
path: '',
fileContent: storesHelperJS
}
]
}
export default srcFiles

View File

@ -26,6 +26,7 @@
"dependencies": {
"@opentiny/tiny-engine-canvas": "workspace:*",
"@opentiny/tiny-engine-controller": "workspace:*",
"@opentiny/tiny-engine-dsl-vue": "workspace:*",
"@opentiny/tiny-engine-http": "workspace:*",
"@opentiny/tiny-engine-utils": "workspace:*",
"prettier": "2.7.1"

View File

@ -54,15 +54,30 @@ export default {
emits: ['cancel', 'confirm'],
setup(props, { emit }) {
const getTableTreeData = (data) => {
const dataMap = {}
const res = []
data.forEach((item) => {
if (!dataMap[item.fileType]) {
dataMap[item.fileType] = { fileType: item.fileType, children: [] }
const folder = item.filePath.split('/').slice(0, -1)
if (!folder.length) {
res.push(item)
return
}
dataMap[item.fileType].children.push(item)
const parentFolder = folder.reduce((parent, curPath) => {
let curItem = parent.find((parItem) => parItem.path === curPath)
if (!curItem) {
curItem = { path: curPath, filePath: curPath, children: [] }
parent.push(curItem)
}
return curItem.children
}, res)
parentFolder.push(item)
})
return Object.values(dataMap)
return res
}
const tableData = computed(() => getTableTreeData(props.data))

View File

@ -4,7 +4,7 @@
:open-delay="1000"
popper-class="toolbar-right-popover"
append-to-body
content="生成当前页面/区块的Vue代码到本地文件"
content="生成当前应用代码到本地文件"
>
<template #reference>
<span class="icon" @click="generate">
@ -23,10 +23,18 @@
<script>
import { reactive } from 'vue'
import { Popover } from '@opentiny/vue'
import { getGlobalConfig, useBlock, useCanvas, useNotify, useLayout } from '@opentiny/tiny-engine-controller'
import {
getGlobalConfig,
useBlock,
useCanvas,
useNotify,
useLayout,
useEditorInfo
} from '@opentiny/tiny-engine-controller'
import { fs } from '@opentiny/tiny-engine-utils'
import { generateVuePage, generateVueBlock } from './generateCode'
import { fetchCode, fetchMetaData, fetchPageList } from './http'
import { useHttp } from '@opentiny/tiny-engine-http'
import { generateApp, parseRequiredBlocks } from '@opentiny/tiny-engine-dsl-vue'
import { fetchMetaData, fetchPageList, fetchBlockSchema } from './http'
import FileSelector from './FileSelector.vue'
export default {
@ -85,27 +93,130 @@ export default {
}
}
const getBlocksSchema = async (pageSchema, blockSet = new Set()) => {
let res = []
const blockNames = parseRequiredBlocks(pageSchema)
const promiseList = blockNames
.filter((name) => {
if (blockSet.has(name)) {
return false
}
blockSet.add(name)
return true
})
.map((name) => fetchBlockSchema(name))
const schemaList = await Promise.allSettled(promiseList)
const extraList = []
schemaList.forEach((item) => {
if (item.status === 'fulfilled' && item.value?.[0]?.content) {
res.push(item.value[0].content)
extraList.push(getBlocksSchema(item.value[0].content, blockSet))
}
})
;(await Promise.allSettled(extraList)).forEach((item) => {
if (item.status === 'fulfilled' && item.value) {
res.push(...item.value)
}
})
return res
}
const instance = generateApp()
const getAllPageDetails = async (pageList) => {
const detailPromise = pageList.map(({ id }) => useLayout().getPluginApi('AppManage').getPageById(id))
const detailList = await Promise.allSettled(detailPromise)
return detailList
.map((item) => {
if (item.status === 'fulfilled' && item.value) {
return item.value
}
})
.filter((item) => Boolean(item))
}
const getPreGenerateInfo = async () => {
const params = getParams()
const promises = [fetchCode(params), fetchMetaData(params), fetchPageList(params.app)]
const { id } = useEditorInfo().useInfo()
const promises = [
useHttp().get(`/app-center/v1/api/apps/schema/${id}`),
fetchMetaData(params),
fetchPageList(params.app)
]
if (!state.dirHandle) {
promises.push(fs.getUserBaseDirHandle())
}
const [codeList, metaData, pageList, dirHandle] = await Promise.all(promises)
const [appData, metaData, pageList, dirHandle] = await Promise.all(promises)
const pageDetailList = await getAllPageDetails(pageList)
return [params, codeList, metaData, pageList, dirHandle]
}
const blockSet = new Set()
const list = pageDetailList.map((page) => getBlocksSchema(page.page_content, blockSet))
const blocks = await Promise.allSettled(list)
const getToSaveFilesInfo = ({ params, codeList, metaData, pageList }) => {
const handlers = {
Block: generateVueBlock,
Page: generateVuePage
const blockSchema = []
blocks.forEach((item) => {
if (item.status === 'fulfilled' && Array.isArray(item.value)) {
blockSchema.push(...item.value)
}
})
const appSchema = {
// metaData dataSourceutilsi18nglobalState
...metaData,
// schema
pageSchema: pageDetailList.map((item) => {
const { page_content, ...meta } = item
return {
...page_content,
meta: {
...meta,
router: meta.route
}
}
}),
blockSchema,
//
componentsMap: [...(appData.componentsMap || [])],
meta: {
...(appData.meta || {})
}
}
const filesInfo = handlers[params.type]({ params, codeList, metaData, pageList })
return filesInfo
const res = await instance.generate(appSchema)
const { genResult = [] } = res || {}
const fileRes = genResult.map(({ fileContent, fileName, path, fileType }) => {
const slash = path.endsWith('/') || path === '.' ? '' : '/'
let filePath = `${path}${slash}`
if (filePath.startsWith('./')) {
filePath = filePath.slice(2)
}
if (filePath.startsWith('.')) {
filePath = filePath.slice(1)
}
if (filePath.startsWith('/')) {
filePath = filePath.slice(1)
}
return {
fileContent,
filePath: `${filePath}${fileName}`,
fileType
}
})
return [dirHandle, fileRes]
}
const saveCodeToLocal = async (filesInfo) => {
@ -133,10 +244,10 @@ export default {
try {
//
const [params, codeList, metaData, pageList, dirHandle] = await getPreGenerateInfo()
const [dirHandle, fileRes] = await getPreGenerateInfo()
//
state.saveFilesInfo = getToSaveFilesInfo({ params, codeList, metaData, pageList })
state.saveFilesInfo = fileRes
//
initDirHandle(dirHandle)

View File

@ -37,3 +37,5 @@ export const fetchMetaData = async ({ platform, app, type, id, history, tenant }
// 获取页面列表
export const fetchPageList = (appId) => http.get(`/app-center/api/pages/list/${appId}`)
export const fetchBlockSchema = async (blockName) => http.get(`/material-center/api/block?label=${blockName}`)

View File

@ -28,7 +28,7 @@
"homepage": "https://opentiny.design/tiny-engine",
"devDependencies": {
"vite": "^4.3.7",
"vitest": "^0.34.6"
"vitest": "^1.4.0"
},
"dependencies": {},
"peerDependencies": {

View File

@ -11,8 +11,11 @@ module.exports = {
node: true
},
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: {
jsx: true
}
}
},
// 忽略 expected 中的内容
ignorePatterns: ['**/**/expected/*', '**/**.ts']
}

View File

@ -1 +1,3 @@
test/testcases/full/**/result/*
test/**/result/*
coverage

View File

@ -0,0 +1,45 @@
# 如何参与 TinyEngine 出码能力共建
> 你好,很高兴你有兴趣参与 TinyEngine 出码能力的共建,增强出码能力。在参与贡献之前,请阅读以下的贡献指南。
## 提交 issue
请遵循 [issue 提交指引](https://github.com/opentiny/tiny-engine/blob/develop/CONTRIBUTING.zh-CN.md#%E6%8F%90%E4%BA%A4-issue)
## 提交 Pull Request
请遵循 [PR 提交指引](https://github.com/opentiny/tiny-engine/blob/develop/CONTRIBUTING.zh-CN.md#%E6%8F%90%E4%BA%A4-issue)
## 出码能力共建
1. 基于 develop 分支,创建新分支,如果是提交新 feature则分支名为 feat/xxx 格式,如果是 bugfix则分支名为 fix/xxx 格式。
2. 执行 pnpm install 安装依赖。
3. 在终端打开 `vue-generator` 目录,`cd packages/vue-generator`。
4. 在 `vue-generator/src` 目录下新增您的 feature 或者是修复 bug。
5. 在 `vue-generator/test` 目录下增加测试用例。
6. 在 `packages/vue-generator` 目录下, 终端执行 `pnpm run test:unit` 确保所有用例通过。
7. 在 Github 上发起 PR并通知 Opentiny 官方。
## 自测试指引
### 测试使用的 library
我们使用 [vitest](https://vitest.dev/),所以需要你同时遵守 vitest 相关的约定。
比如:测试文件以 `.test.js` 为后缀
### 执行单个用例文件
假如我们有测试文件 `testCaseName.test.js`,如果我们只想执行该测试文件,则可以:
```bash
pnpm test:unit testCaseName
```
### 使用 vscode debugger 调试能力调试测试用例。
1. 新建 vscode JavaScript Debug TerminalJavaScript 调试终端)
2. 终端打开 vue-generator 目录,`cd packages/vue-generator`
3. 对需要调试的位置打上断点VSCode 文件行数旁边)
4. 执行 `pnpm test:unit testCaseName`
### 更多测试指引,可参考 [vitest](https://vitest.dev/) 指引

View File

@ -0,0 +1,468 @@
# @opentiny/tiny-engine-dsl-vue
> 将 schema 转换成具体的,可读性高,可维护的代码
TODO:
- [ ] 架构支持配置出码
- [ ] 抽取通用底层能力,支持用户自定义插件,自定义出码结果
- [ ] 官方提供更多内置出码方案
## 安装
```bash
npm install @opentiny/tiny-engine-dsl-vue
```
## 使用
### 使用官方默认配置出码
```javascript
import { generateApp } from '@opentiny/tiny-engine-dsl-vue'
const instance = generateApp()
const res = await instance.generate(appSchema)
```
### 传入配置
```javascript
import { generateApp } from '@opentiny/tiny-engine-dsl-vue'
const instance = generateApp({
pluginConfig: {
// 对 formatCode 插件传入自定义配置
formatCode: {
singleQuote: false,
printWidth: 180,
semi: true
}
}
})
const res = await instance.generate(appSchema)
```
### 使用自定义插件替换官方插件
```javascript
import { generateApp } from '@opentiny/tiny-engine-dsl-vue'
const customDataSourcePlugin = () => {
return {
name: '',
description: '',
run: () {
// ... 自定义出码逻辑
}
}
}
const instance = generateApp({
customPlugins: {
// 使用自定义插件替换官方 dataSource 生成的插件
dataSource: customDataSourcePlugin()
}
})
const res = await instance.generate(appSchema)
```
## API
### generateApp
该函数返回出码实例
使用:
```typescript
function generateApp(config: IConfig): CodeGenInstance
```
config 传参配置:
```typescript
interface IConfig {
// 插件配置,会传入对应的官方插件配置里面
pluginConfig: {
template: ITemplatePluginConfig;
block: IBlockPluginConfig;
page: IPagePluginConfig;
dataSource: IDataSourcePluginConfig;
dependencies: IDependenciesPluginConfig;
globalState: IGlobalStatePluginConfig;
i18n: II18nPluginConfig;
router: IRouterPluginConfig;
utils: IUtilsPluginConfig;
formatCode: IFormatCodePluginConfig;
parseSchema: IParseSchemaPluginConfig;
};
// 自定义插件,可以替换官方插件,或者增加额外的插件
customPlugins: {
template: IPluginFunction;
block: IPluginFunction;
page: IPluginFunction;
dataSource: IPluginFunction;
dependencies: IPluginFunction;
i18n: IPluginFunction;
router: IPluginFunction;
utils: IPluginFunction;
formatCode: IPluginFunction;
parseSchema: IPluginFunction;
globalState: IPluginFunction;
// 解析类的插件
transformStart: Array<IPluginFunction>;
// 转换 schema 转换的插件
transform: Array<IPluginFunction>;
// 处理出码后的插件
transformEnd: Array<IPluginFunction>;
};
// 额外的上下文,可以在插件运行的时候获取到
customContext: Record<string, any>
}
```
### codeGenInstance 相关方法
#### generate
生成源码方法,传入 appSchema异步返回生成的文件列表
```javascript
async function generate(schema: IAppSchema): Promise<Array<IFileItem>>
```
传参&返回类型定义
```typescript
interface FolderItem {
componentName: string;
folderName: string
id: string;
parentId: string;
router: string;
}
interface SchemaChildrenItem {
children: Array<SchemaChildrenItem>;
componentName: string;
id: string;
props: Record<string, any>;
}
interface PageOrBlockSchema {
componentName: string;
css: string;
fileName: string;
lifeCycles: Record<string, Record<string, { type: "JSFunction"; value: string; }>>;
methods: Record<string, { type: "JSFunction"; value: string; }>;
props: Record<string, any>;
state: Array<Record<string, any>>;
meta: { id: Number, isHome: Boolean, parentId: String, rootElement: String, route: String };
children: Array.<SchemaChildrenItem>
schema?: { properties: Array<Object.<String, any>>, events: Object.<String> };
}
interface ComponentMapItem {
componentName: string;
destructuring: boolean;
exportName?: string;
package?: string;
main?: string;
version: string;
}
interface IAppSchema {
i18n: {
en_US: Record<string, any>;
zh_CN: Record<string, any>
};
utils: Array<{ name: string; type: 'npm' | 'function'; content: { type?: "JSExpression" | "JSFunction"; value?: string }; }>;
dataSource: {
dataHandler?: { type: "JSFunction"; value: string; };
errorHandler?:{ type: "JSFunction"; value: string; };
willFetch?: { type: "JSFunction"; value: string; };
list: Array<{ id: Number; name: String; data: Object }>;
};
globalState: Array<{
id: string; state: Record<string, any>;
actions: Record<string, { type: "JSFunction", value: String }>;
getters: Record<string, { type: "JSFunction", value: String }>;
}>;
// 页面 schema
pageSchema: Array<PageOrBlockSchema | FolderItem>;
// 区块 schema
blockSchema: Array<PageOrBlockSchema>;
// 组件对应 package map
componentsMap: Array<ComponentMapItem>;
// 设计器 meta 信息
meta: {
// 该应用 ID
name: string;
// 该应用描述
description: string;
};
}
```
### 官方内置 plugin API
#### genBlockPlugin 生成区块代码
```typescript
interface Config {
blockBasePath: String; // 区块生成文件所在的目录,默认值:'./src/component'
sfcConfig: ISFCConfig; // 生成 sfc 风格的 vue 文件的配置,详见下面 sfc 插件
}
```
#### genDataSourcePlugin 生成数据源代码
```typescript
interface IConfig {
path: string; // 生成数据源的路径,默认值:./src/lowcodeConfig
}
```
#### genGlobalState 生成全局 state
```typescript
interface IConfig {
path: string; // 生成全局 state 所在的目录,默认值 ./src/stores
}
```
#### genI18nPlugin 生成国际化相关文件
```typescript
interface IConfig {
localeFileName: string; // locale 文件名,默认值 locale.js
entryFileName: string; // 入口文件名,默认值 index.js
path: string; // 生成 i18n 所在的目录
}
```
#### genPagePlugin 生成页面 vue 文件
```typescript
interface IConfig {
pageBasePath: string; // 页面生成文件所在目录
}
```
#### genRouterPlugin 生成路由相关文件
```typescript
interface IConfig {
fileName: string; // 路由文件名,默认值: index.js
path: string; // 生成路由文件所在文件夹 默认值:./src/router
}
```
#### genTemplatePlugin
```typescript
interface IConfig {
template: string | () => Array<IFile> // 可指定出码模板,或自定义生成出码模板函数
}
```
#### genUtilsPlugin
```typescript
interface IConfig {
fileName: string; // 生成工具类的文件名默认值utils.js
path: string; // 生成工具类所在的目录 ./src
}
```
#### formatCodePlugin 格式化代码
```javascript
// prettier 配置
{
singleQuote: true,
printWidth: 120,
semi: false,
trailingComma: 'none'
}
```
#### genSFCWithDefaultPlugin & generateSFCFile
官方生成 sfc 风格的 .vue 文件,提供了 hook 插槽,可以对生成的.vue 文件做细微调整
- genSFCWithDefaultPlugin 带有官方 hooks 的生成 .vue 文件方法
- generateSFCFile 无官方 hooks 的生成 .vue 文件方法
##### 使用示例
**处理自定义 props**
```javascript
// 自定义插件处理 TinyGrid 中的 editor 配置
const customPropsHook = (schemaData, globalHooks) => {
const { componentName, props } = schemaData.schema
// 处理 TinyGrid 插槽
if (componentName !== 'TinyGrid' || !Array.isArray(props?.columns)) {
return
}
props.columns.forEach((item) => {
if (!item.editor?.component?.startsWith?.('Tiny')) {
return
}
const name = item.editor?.component
globalHooks.addImport('@opentiny/vue', {
destructuring: true,
exportName: name.slice(4),
componentName: name,
package: '@opentiny/vue'
})
item.editor.component = {
type: 'JSExpression',
value: name
}
})
}
// 使用
genSFCWithDefaultPlugin(schema, componentsMap, {
genTemplate: [customPropsHook]
})
```
## 如何编写自定义插件
如果官方配置不满足自定义出码的需求,我们还支持自定义出码插件。
### 替换官方出码插件
官方提供了以下几个官方的出码插件:
- template 生成静态出码模板
- block 生成区块代码
- page 生成页面代码
- dataSource 生成数据源相关代码
- dependencies 将组件依赖的 package 注入到 package.json 中
- i18n 生成 i18n 国际化数据
- router 生成路由文件
- utils 生成 utils 工具类文件
- formatCode 格式化已经生成的文件
- parseSchema 解析、预处理 schema
- globalState 生成全局状态文件
我们可以通过传入配置的方式替换掉官方的插件:
```javascript
generateApp({
customPlugins: {
template: customPluginItem // 传入自定义插件,替换官方插件
}
})
```
### 增加增量插件
如果是对官方 schema 做了增量的协议,需要增加对应的插件,我们也支持增加 `transformStart`、`transform`、`transformEnd` 几个生命周期钩子
```javascript
generateApp({
customPlugins: {
// 解析阶段的自定义插件
transformStart: [customPlugin1],
// 转换 schema出码的自定义插件
transform: [customPlugin2],
// 结束阶段的自定义插件
transformEnd: [customPlugin3]
}
})
```
### 插件相关约定
为了能够让 CodeGenInstance 实例能够调用用户传入的自定义插件,我们需要做相关的约定:
- 提供 run 函数,该不能是箭头函数,否则无法绑定相关上下文
- 函数名遵守 tinyEngine-generateCode-plugin-xxx 的规则
- 提供 options 进行配置并且有默认 options
比如:
```javascript
function customPlugin(options) {
const runtimeOptions = merge(defaultOptions, options)
return {
name: 'tinyEngine-generateCode-plugin-demo',
description: 'demo',
run(schema, context) {
console.log('here is a demo plugin')
}
}
}
```
run 函数传参说明:
- schema 即为 generate 函数中传入的 appSchema
- context codeInstance 提供的上下文,包括:
- config 当前 instance 的配置
- genResult 当前出码的文件数组
- genLogs 当前出码的日志
- ...customContext 用户在 generateApp 实例化函数中自定义传入的上下文
#### 插件提供的上下文
codeGenInstance 提供了一些相关的上下文,丰富了插件的拓展能力。
相关的上下文
- this.addLog(log): void 向 genLogs 中增加一条日志
- this.addFile(fileItem: FileItem, override: boolean): boolean 向 genResult 中增加一个文件
- this.getFile(path, fileName) 根据 path 和 fileName 在 genResult 中寻找目标文件
- this.replaceFile(fileItem) 替换文件
## 设计思想&原理
### 出码模块架构
TODO: 待补充
### 出码的本质&核心目标
出码的本质:是将在画布中可编排的协议,存储的 schema 信息,转换成我们在程序员可以看懂可维护的高质量代码。
目标:
- 一套 schema 协议(可增量拓展),支持多框架出码,比如 react、vue2.x、vue3.x、 Angular
- 支持用户自定义出码,具体为
- 支持自定义出码模板
- 支持自定义部分文件的出码(生成 jsx 风格、生成 setup 风格等等)
### 整体生成代码的流程
- `const instance = generateApp(config)` 传入配置得到出码实例
- `instance.generate(appSchema)` 调用generate 方法,传入 appSchema得到应用的代码文件
其中, generate 函数生成代码文件的过程:
- validate 校验传入 appSchema 的合法性
- transformStart 运行 transformStart 阶段的插件,该阶段的插件建议用户预处理 schema不实际生成代码文件
- transform 运行 transform 阶段的插件,该阶段主要将 schema 转换为目标代码文件(页面、区块、数据源、国际化、全局状态、静态模板文件、等等)
- transformEnd 运行 transformEnd 阶段的插件,该阶段建议用户处理已经生成的文件,比如代码格式化、校验生成的代码文件存在的冲突等等
### 生成页面代码的整体流程与设计
生成代码的过程中,主要的核心是处理可编排的页面 schema生成页面或者区块文件所以我们在这里展开讲讲官方的插件实现与思考。

View File

@ -4,7 +4,9 @@
"publishConfig": {
"access": "public"
},
"main": "dist/tiny-engine-dsl-vue.cjs.js",
"main": "dist/tiny-engine-dsl-vue.js",
"module": "dist/tiny-engine-dsl-vue.mjs",
"types": "dist/index.d.ts",
"files": [
"dist"
],
@ -12,6 +14,8 @@
"build": "vite build",
"test": "npx nyc@latest --reporter=lcov node test/test_generator.js",
"test:latest": "npm run build && node test/testcases/full/index.js",
"test:unit": "vitest",
"coverage": "vitest run --coverage",
"publish:npm": "npm publish --verbose"
},
"repository": {
@ -26,8 +30,8 @@
"license": "MIT",
"homepage": "https://opentiny.design/tiny-engine",
"dependencies": {
"@opentiny/tiny-engine-controller": "workspace:*",
"@opentiny/tiny-engine-builtin-component": "workspace:*",
"@opentiny/tiny-engine-controller": "workspace:*",
"@vue/compiler-sfc": "3.2.45",
"@vue/shared": "^3.3.4",
"vue": "^3.4.15",
@ -35,12 +39,20 @@
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.1.1",
"@vitest/coverage-v8": "^1.4.0",
"@vue/eslint-config-prettier": "^7.0.0",
"dir-compare": "^4.2.0",
"eslint": "^8.12.0",
"eslint-plugin-vue": "^8.6.0",
"fs-extra": "^10.0.1",
"prettier": "^2.6.1",
"vite": "^2.8.6",
"vite": "^4.3.7",
"vite-plugin-static-copy": "^1.0.4",
"vitest": "^1.4.0",
"winston": "^3.10.0"
},
"peerDependencies": {
"@babel/parser": "^7.18.13",
"@babel/traverse": "^7.18.13"
}
}

View File

@ -2174,26 +2174,26 @@ const DEFAULT_COMPONENTS_MAP = [
]
// 内置组件映射关系
const BUILTIN_COMPONENTS_MAP = [
export const BUILTIN_COMPONENTS_MAP = [
{
componentName: 'CanvasRow',
exportName: 'CanvasRow',
package: '@opentiny/tiny-engine-builtin-component',
version: '^0.1.0',
version: '^1.0.1',
destructuring: true
},
{
componentName: 'CanvasCol',
exportName: 'CanvasCol',
package: '@opentiny/tiny-engine-builtin-component',
version: '^0.1.0',
version: '^1.0.1',
destructuring: true
},
{
componentName: 'CanvasRowColContainer',
exportName: 'CanvasRowColContainer',
package: '@opentiny/tiny-engine-builtin-component',
version: '^0.1.0',
version: '^1.0.1',
destructuring: true
}
]
@ -2211,6 +2211,25 @@ const BUILTIN_COMPONENT_NAME = {
ICON: 'Icon'
}
export const BUILTIN_COMPONENT_NAME_MAP = {
Text: 'span',
Collection: 'div',
Block: 'div',
Img: 'img'
}
export const INSERT_POSITION = {
AFTER_IMPORT: 'AFTER_IMPORT',
BEFORE_PROPS: 'BEFORE_PROPS',
AFTER_PROPS: 'AFTER_PROPS',
BEFORE_EMIT: 'BEFORE_EMIT',
AFTER_EMIT: 'AFTER_EMIT',
BEFORE_STATE: 'BEFORE_STATE',
AFTER_STATE: 'AFTER_STATE',
BEFORE_METHODS: 'BEFORE_METHODS',
AFTER_METHODS: 'AFTER_METHODS'
}
/**
* 图标组件名统一前缀为 TinyIcon与从组件库引入的方法名 iconXxx 区分开
*/
@ -2224,6 +2243,8 @@ const UNWRAP_QUOTES = {
end: '#QUOTES_END#'
}
export const SPECIAL_UTILS_TYPE = ['utils', 'bridge']
/**
* 协议中的类型
*/
@ -2235,4 +2256,4 @@ export const [JS_EXPRESSION, JS_FUNCTION, JS_I18N, JS_RESOURCE, JS_SLOT] = [
'JSSlot'
]
export { DEFAULT_COMPONENTS_MAP, BUILTIN_COMPONENT_NAME, TINY_ICON, UNWRAP_QUOTES, BUILTIN_COMPONENTS_MAP }
export { DEFAULT_COMPONENTS_MAP, BUILTIN_COMPONENT_NAME, TINY_ICON, UNWRAP_QUOTES }

View File

@ -0,0 +1,207 @@
class CodeGenerator {
config = {}
genResult = []
plugins = []
genLogs = []
schema = {}
context = {}
// 是否允许插件报错
tolerateError = true
error = []
contextApi = {
addLog: this.addLog.bind(this),
addFile: this.addFile.bind(this),
getFile: this.getFile.bind(this),
replaceFile: this.replaceFile.bind(this)
}
constructor(config) {
this.config = config
this.plugins = config.plugins
this.context = {
...this.context,
...(this.config.context || {})
}
if (typeof config.tolerateError === 'boolean') {
this.tolerateError = config.tolerateError
}
}
getContext() {
return {
config: this.config,
genResult: this.genResult,
genLogs: this.genLogs,
error: this.error,
...this.context
}
}
async generate(schema) {
this.schema = this.parseSchema(schema)
this.error = []
this.genResult = []
this.genLogs = []
let curHookName = ''
try {
await this.transformStart()
await this.transform()
} catch (error) {
this.error.push(error)
if (!this.tolerateError) {
throw new Error(
`[codeGenerator][generate] get error when running hook: ${curHookName}. error message: ${JSON.stringify(
error
)}`
)
}
} finally {
await this.transformEnd()
}
return {
errors: this.error,
genResult: this.genResult,
genLogs: this.genLogs
}
}
/**
* 转换开始的钩子在正式开始转换前用户可以做一些预处理的动作
* @param {*} plugins
*/
async transformStart() {
for (const pluginItem of this.plugins.transformStart) {
if (typeof pluginItem.run !== 'function') {
continue
}
try {
await pluginItem.run.apply(this.contextApi, [this.schema, this.getContext()])
} catch (error) {
const err = { message: error.message, stack: error.stack, plugin: pluginItem.name }
this.error.push(err)
if (!this.tolerateError) {
throw new Error(`[${pluginItem.name}] throws error`, { cause: error })
}
}
}
}
async transform() {
for (const pluginItem of this.plugins.transform) {
if (typeof pluginItem.run !== 'function') {
continue
}
try {
const transformRes = await pluginItem.run.apply(this.contextApi, [this.schema, this.getContext()])
if (!transformRes) {
continue
}
if (Array.isArray(transformRes)) {
this.genResult.push(...transformRes)
} else {
this.genResult.push(transformRes)
}
} catch (error) {
const err = { message: error.message, stack: error.stack, plugin: pluginItem.name }
this.error.push(err)
if (!this.tolerateError) {
throw new Error(`[${pluginItem.name}] throws error`, { cause: error })
}
}
}
}
async transformEnd() {
for (const pluginItem of this.plugins.transformEnd) {
if (typeof pluginItem.run !== 'function') {
continue
}
try {
await pluginItem.run.apply(this.contextApi, [this.schema, this.getContext()])
} catch (error) {
const err = { message: error.message, stack: error.stack, plugin: pluginItem.name }
this.error.push(err)
if (!this.tolerateError) {
throw new Error(`[${pluginItem.name}] throws error`, { cause: error })
}
}
}
}
parseSchema(schema) {
if (!schema) {
throw new Error(
'[codeGenerator][generate] parseSchema error, schema is not valid, should be json object or json string.'
)
}
try {
return typeof schema === 'string' ? JSON.parse(schema) : schema
} catch (error) {
throw new Error(
'[codeGenerator][generate] parseSchema error, schema is not valid, please check the input params.'
)
}
}
/**
* 写入 log
* @param {*} log
*/
addLog(log) {
this.genLogs.push(log)
}
getFile(path, fileName) {
return this.genResult.find((item) => item.path === path && item.fileName === fileName)
}
addFile(file, override) {
const { path, fileName } = file
const isExist = this.getFile(path, fileName)
if (isExist && !override) {
return false
}
if (isExist) {
this.replaceFile(file)
return true
}
this.genResult.push(file)
return true
}
deleteFile(file) {
const { path, fileName } = file
const index = this.genResult.findIndex((item) => item.path === path && item.fileName === fileName)
if (index !== -1) {
this.genResult.splice(index, 1)
return true
}
return false
}
replaceFile(resultItem) {
const { path, fileName } = resultItem
const index = this.genResult.findIndex((item) => item.path === path && item.fileName === fileName)
if (index === -1) {
return false
}
this.genResult.splice(index, 1, resultItem)
return true
}
}
export default CodeGenerator

View File

@ -0,0 +1,76 @@
import {
genBlockPlugin,
genDataSourcePlugin,
genDependenciesPlugin,
genI18nPlugin,
genPagePlugin,
genRouterPlugin,
genTemplatePlugin,
genUtilsPlugin,
formatCodePlugin,
parseSchemaPlugin,
genGlobalState
} from '../plugins'
import CodeGenerator from './codeGenerator'
/**
* 整体应用出码
* @param {tinyEngineDslVue.IConfig} config
* @returns {tinyEngineDslVue.codeGenInstance}
*/
export function generateApp(config = {}) {
const defaultPlugins = {
template: genTemplatePlugin(config.pluginConfig?.template || {}),
block: genBlockPlugin(config.pluginConfig?.block || {}),
page: genPagePlugin(config.pluginConfig?.page || {}),
dataSource: genDataSourcePlugin(config.pluginConfig?.dataSource || {}),
dependencies: genDependenciesPlugin(config.pluginConfig?.dependencies || {}),
globalState: genGlobalState(config.pluginConfig?.globalState || {}),
i18n: genI18nPlugin(config.pluginConfig?.i18n || {}),
router: genRouterPlugin(config.pluginConfig?.router || {}),
utils: genUtilsPlugin(config.pluginConfig?.utils || {}),
formatCode: formatCodePlugin(config.pluginConfig?.formatCode || {}),
parseSchema: parseSchemaPlugin(config.pluginConfig?.parseSchema || {})
}
const { customPlugins = {} } = config
const {
template,
block,
page,
dataSource,
dependencies,
i18n,
router,
utils,
formatCode,
parseSchema,
globalState,
transformStart = [],
transform = [],
transformEnd = []
} = customPlugins
const mergeWithDefaultPlugin = {
template: template || defaultPlugins.template,
block: block || defaultPlugins.block,
page: page || defaultPlugins.page,
dataSource: dataSource || defaultPlugins.dataSource,
dependencies: dependencies || defaultPlugins.dependencies,
i18n: i18n || defaultPlugins.i18n,
router: router || defaultPlugins.router,
utils: utils || defaultPlugins.utils,
globalState: globalState || defaultPlugins.globalState
}
const codeGenInstance = new CodeGenerator({
plugins: {
transformStart: [parseSchema || defaultPlugins.parseSchema, ...transformStart],
transform: [...Object.values(mergeWithDefaultPlugin), ...transform],
transformEnd: [formatCode || defaultPlugins.formatCode, ...transformEnd]
},
context: config?.customContext || {}
})
return codeGenInstance
}

View File

@ -1,15 +1,16 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { generateCode, generateBlocksCode, generatePageCode } from './page'
export { generateCode, generateBlocksCode, generatePageCode }
export { generateCode, generateBlocksCode, generatePageCode } from './page'
export { genSFCWithDefaultPlugin, generateSFCFile } from './vue/sfc'
export { generateApp } from './generateApp'
export { default as CodeGenerator } from './codeGenerator'

View File

@ -94,6 +94,7 @@ const handleLiteralBinding = ({ key, item, attrsArr, description, state }) => {
// string 直接静态绑定
if (typeof item === 'string') return attrsArr.push(`${key}="${item.replace(/"/g, '&quot;')}"`)
// TODO: 拿到这里的场景 case
if (item?.componentName === BUILTIN_COMPONENT_NAME.ICON) {
const iconName = handleIconInProps(description, item)

View File

@ -0,0 +1,2 @@
# vue sfc code generator

View File

@ -0,0 +1,265 @@
import { getImportMap } from './parseImport'
import {
genTemplateByHook,
handleComponentNameHook,
handleTinyGrid,
handleTinyIcon,
handleExpressionChildren,
validEmptyTemplateHook
} from './generateTemplate'
import { generateStyleTag } from './generateStyle'
import {
handleConditionAttrHook,
handleLoopAttrHook,
handleSlotBindAttrHook,
handleAttrKeyHook,
handlePrimitiveAttributeHook,
handleExpressionAttrHook,
handleI18nAttrHook,
handleObjBindAttrHook,
handleEventAttrHook,
handleTinyIconPropsHook
} from './generateAttribute'
import {
GEN_SCRIPT_HOOKS,
genScriptByHook,
parsePropsHook,
parseReactiveStateHook,
addDefaultVueImport,
addDefaultVueI18nImport,
handleProvideStatesContextHook,
handleContextInjectHook,
defaultGenImportHook,
defaultGenPropsHook,
defaultGenEmitsHook,
defaultGenStateHook,
defaultGenMethodHook,
defaultGenLifecycleHook
} from './generateScript'
const parseConfig = (config = {}) => {
const {
blockRelativePath = '../components/',
blockSuffix = '.vue',
scriptConfig = {},
styleConfig = {}
} = config || {}
const res = {
...config,
blockRelativePath,
blockSuffix,
scriptConfig,
styleConfig
}
return res
}
const defaultScriptConfig = {
lang: '',
setup: true
}
const defaultStyleConfig = {
scoped: true,
lang: ''
}
const generateSFCFile = (schema, componentsMap, config = {}) => {
const parsedConfig = parseConfig(config)
const { blockRelativePath, blockSuffix, scriptConfig: initScriptConfig, styleConfig: initStyleConfig } = parsedConfig
// 前置动作,对 Schema 进行解析初始化相关配置与变量
if (!schema.state) {
schema.state = {}
}
// 解析 import
const { pkgMap, blockPkgMap } = getImportMap(schema, componentsMap, { blockRelativePath, blockSuffix })
// 解析 state
let stateRes = {}
// 解析 method
const methods = schema.methods || {}
// 其他表达式语句
const statements = {}
// config
let scriptConfig = {
...defaultScriptConfig,
...initScriptConfig
}
let styleConfig = {
...defaultStyleConfig,
...initStyleConfig
}
const globalHooks = {
addStatement: (newStatement) => {
if (!newStatement?.value) {
return false
}
const key = newStatement.key || newStatement.value
if (statements[key]) {
return false
}
statements[key] = newStatement
return true
},
getStatements: () => statements,
addMethods: (key, value) => {
if (methods[key]) {
return false
}
methods[key] = value
return true
},
getMethods: () => methods,
addState: (key, value) => {
if (schema.state[key] || stateRes[key]) {
return false
}
stateRes[key] = value
return true
},
getState: () => stateRes,
setState: () => {
// state = newState
},
addImport: (fromPath, config) => {
const dependenciesMap = pkgMap[fromPath] || blockPkgMap[fromPath]
if (dependenciesMap) {
// 默认导出
if (!config.destructuring && dependenciesMap.find(({ destructuring }) => !destructuring)) {
return false
}
const hasExists = dependenciesMap.find(({ destructuring, exportName, componentName }) => {
return (
destructuring === config.destructuring &&
exportName === config.exportName &&
componentName === config.componentName
)
})
if (hasExists) {
return false
}
dependenciesMap.push(config)
return true
}
pkgMap[fromPath] = [config]
return true
},
getImport: () => ({ ...pkgMap, ...blockPkgMap }),
setScriptConfig: (newConfig) => {
if (!newConfig || typeof newConfig !== 'object') {
return
}
scriptConfig = {
...scriptConfig,
...newConfig
}
},
getScriptConfig: () => scriptConfig,
setStyleConfig: (newConfig = {}) => {
if (!newConfig || typeof newConfig !== 'object') {
return
}
styleConfig = {
...styleConfig,
...newConfig
}
},
getStyleConfig: () => styleConfig,
addCss: (css) => {
schema.css = `${schema.css}\n${css}`
}
}
// 解析 template
const templateStr = genTemplateByHook(schema, globalHooks, { ...parsedConfig, componentsMap: componentsMap })
// 生成 script
const scriptStr = genScriptByHook(schema, globalHooks, { ...parsedConfig, componentsMap: componentsMap })
// 生成 style
const styleStr = generateStyleTag(schema, styleConfig)
return `${templateStr}\n${scriptStr}\n${styleStr}`
}
export const genSFCWithDefaultPlugin = (schema, componentsMap, config = {}) => {
const { templateItemValidate = [], genTemplate = [], parseScript = [], genScript = {} } = config.hooks || {}
const defaultComponentHooks = [handleComponentNameHook, handleTinyIcon]
const defaultAttributeHook = [
handleTinyGrid,
handleConditionAttrHook,
handleLoopAttrHook,
handleSlotBindAttrHook,
handleAttrKeyHook,
handlePrimitiveAttributeHook,
handleExpressionAttrHook,
handleI18nAttrHook,
handleTinyIconPropsHook,
handleObjBindAttrHook,
handleEventAttrHook
]
const defaultChildrenHook = [handleExpressionChildren]
const defaultTemplateItemValidateHook = [validEmptyTemplateHook]
const defaultParseScriptHook = [
addDefaultVueImport,
addDefaultVueI18nImport,
parsePropsHook,
parseReactiveStateHook,
handleProvideStatesContextHook,
handleContextInjectHook
]
const { GEN_IMPORT, GEN_PROPS, GEN_EMIT, GEN_STATE, GEN_METHOD, GEN_LIFECYCLE } = GEN_SCRIPT_HOOKS
const defaultGenScriptHooks = {
[GEN_IMPORT]: defaultGenImportHook,
[GEN_PROPS]: defaultGenPropsHook,
[GEN_EMIT]: defaultGenEmitsHook,
[GEN_STATE]: defaultGenStateHook,
[GEN_METHOD]: defaultGenMethodHook,
[GEN_LIFECYCLE]: defaultGenLifecycleHook
}
const newConfig = {
...config,
hooks: {
templateItemValidate: [...templateItemValidate, ...defaultTemplateItemValidateHook],
genTemplate: [...genTemplate, ...defaultComponentHooks, ...defaultAttributeHook, ...defaultChildrenHook],
parseScript: [...parseScript, ...defaultParseScriptHook],
genScript: {
...defaultGenScriptHooks,
...genScript
}
}
}
return generateSFCFile(schema, componentsMap, newConfig)
}
export default generateSFCFile

View File

@ -0,0 +1,517 @@
import {
JS_EXPRESSION,
JS_FUNCTION,
JS_I18N,
JS_RESOURCE,
JS_SLOT,
SPECIAL_UTILS_TYPE,
INSERT_POSITION,
TINY_ICON
} from '@/constant'
import {
isOn,
toEventKey,
thisPropsBindRe,
randomString,
getFunctionInfo,
hasAccessor,
thisRegexp,
isGetter,
isSetter
} from '@/utils'
import { recursiveGenTemplateByHook } from './generateTemplate'
import { getImportMap } from './parseImport'
const handleEventBinding = (key, item, isJSX) => {
const eventKey = toEventKey(key)
let eventBinding = ''
// vue 事件绑定,仅支持:内联事件处理器 or 方法事件处理器(绑定方法名或对某个方法的调用)
if (item?.type === JS_EXPRESSION) {
let eventHandler = item.value.replace(isJSX ? thisRegexp : thisPropsBindRe, '')
let renderKey = isJSX ? `${key}` : `@${eventKey}`
// Vue Template 中,为事件处理函数传递额外的参数时,需要使用内联箭头函数
if (item.params?.length) {
const extendParams = item.params.join(',')
eventHandler = `(...eventArgs) => ${eventHandler}(eventArgs, ${extendParams})`
}
if (isJSX) {
eventHandler = `{${eventHandler}}`
} else {
eventHandler = `"${eventHandler}"`
}
eventBinding = `${renderKey}=${eventHandler}`
}
return eventBinding
}
const specialTypes = [JS_FUNCTION, JS_RESOURCE, JS_SLOT]
export const checkHasSpecialType = (obj) => {
if (!obj || typeof obj !== 'object') {
return false
}
for (const item of Object.values(obj)) {
if (typeof item !== 'object') {
continue
}
if (specialTypes.includes(item?.type) || checkHasSpecialType(item)) {
return true
}
}
return false
}
const handleJSExpressionBinding = (key, value, isJSX) => {
const expressValue = value.value.replace(isJSX ? thisRegexp : thisPropsBindRe, '')
if (isJSX) {
return `${key}={${expressValue}}`
}
// 支持带参数的 v-model
if (value.model) {
const modelArgs = value.model?.prop ? `:${value.model.prop}` : ''
return `v-model${modelArgs}="${expressValue}"`
}
// expression 使用 v-bind 绑定
return `:${key}="${expressValue}"`
}
const handleBindI18n = (key, value, isJSX) => {
const tArguments = [`'${value.key}'`]
// TODO: 拿到场景用例
const i18nParams = JSON.stringify(value.params)
i18nParams && tArguments.push(i18nParams)
if (isJSX) {
return `${key}={t(${tArguments.join(',')})}`
}
return `:${key}="t(${tArguments.join(',')})"`
}
const handleJSXConditionBind = (schemaData, globalHooks, config) => {
const { prefix, suffix, schema: { condition } = {} } = schemaData
const isJSX = config.isJSX
if (!isJSX) {
return
}
if (typeof condition !== 'boolean' && !condition?.type) {
return
}
if (prefix[0] !== '{') {
prefix.unshift('{')
}
if (suffix.at(-1) !== '}') {
suffix.push('}')
}
if (typeof condition === 'boolean') {
prefix.push(`${condition} && `)
return
}
const conditionValue = condition?.value?.replace(thisRegexp, '')
prefix.push(`${conditionValue} &&`)
}
export const handleConditionAttrHook = (schemaData, globalHooks, config) => {
const { attributes, schema: { condition } = {} } = schemaData
const isJSX = config.isJSX
if (isJSX) {
handleJSXConditionBind(schemaData, globalHooks, config)
return
}
if (typeof condition === 'boolean') {
attributes.unshift(`v-if="${condition}"`)
return
}
if (!condition?.type) {
return
}
const conditionValue = condition?.value?.replace(isJSX ? thisRegexp : thisPropsBindRe, '')
if (condition?.kind === 'else') {
attributes.unshift('v-else')
}
attributes.unshift(`v-${condition?.kind || 'if'}="${conditionValue}"`)
}
export const handleLoopAttrHook = (schemaData = {}, globalHooks, config) => {
const { prefix, suffix, attributes, schema: { loop, loopArgs = [] } = {} } = schemaData
const isJSX = config.isJSX
if (!loop) {
return
}
let source = ''
if (loop?.value && loop?.type) {
source = loop.value.replace(isJSX ? thisRegexp : thisPropsBindRe, '')
} else {
source = JSON.stringify(loop).replaceAll("'", "\\'").replaceAll(/"/g, "'")
}
const iterVar = [...loopArgs]
if (!isJSX) {
attributes.push(`v-for="(${iterVar.join(',')}) in ${source}"`)
return
}
prefix.push(`${source}.map((${iterVar.join(',')}) => `)
suffix.unshift(`)`)
if (prefix[0] !== '{') {
prefix.unshift['{']
}
if (suffix.at(-1) !== '}') {
suffix.push('}')
}
}
export const handleEventAttrHook = (schemaData, globalHooks, config) => {
const { attributes, schema: { props = {} } = {} } = schemaData || {}
const isJSX = config.isJSX
const eventBindArr = Object.entries(props)
.filter(([key]) => isOn(key))
.map(([key, value]) => handleEventBinding(key, value, isJSX))
attributes.push(...eventBindArr)
}
export const handleSlotBindAttrHook = (schemaData) => {
const { attributes, schema: { props = {} } = {} } = schemaData || {}
const slot = props?.slot
if (!slot) {
return
}
if (typeof slot === 'string') {
attributes.push(`#${slot}`)
delete props.slot
return
}
const { name, params } = slot
let paramsValue = ''
if (Array.isArray(params)) {
paramsValue = `={ ${params.join(',')} }`
} else if (typeof params === 'string') {
paramsValue = `="${params}"`
}
attributes.push(`#${name}${paramsValue}`)
delete props.slot
}
export const handleAttrKeyHook = (schemaData) => {
const { schema: { props = {} } = {} } = schemaData
const specialKey = {
className: 'class'
}
Object.keys(props || {}).forEach((key) => {
if (specialKey[key]) {
props[specialKey[key]] = props[key]
delete props[key]
}
})
}
export const specialTypeHandler = {
[JS_EXPRESSION]: ({ value, computed }) => {
if (computed) {
return {
value: `vue.computed(${value.replace(/this\./g, '')})`
}
}
return {
value: value.replace(/this\./g, '')
}
},
[JS_FUNCTION]: ({ value }) => {
const { type, params, body } = getFunctionInfo(value)
const inlineFunc = `${type} (${params.join(',')}) => { ${body.replace(/this\./g, '')} }`
return {
value: inlineFunc
}
},
[JS_I18N]: ({ key }) => {
return {
value: `t("${key}")`
}
},
[JS_RESOURCE]: ({ value }, globalHooks) => {
const resourceType = value.split('.')[1]
if (SPECIAL_UTILS_TYPE.includes(resourceType)) {
globalHooks.addStatement({
position: INSERT_POSITION.BEFORE_STATE,
value: `const { ${resourceType} } = wrap(function() { return this })()`,
key: resourceType
})
}
return {
value: `${value.replace(/this\./g, '')}`
}
},
[JS_SLOT]: ({ value = [], params = ['row'] }, globalHooks, config) => {
globalHooks.setScriptConfig({ lang: 'jsx' })
const structData = {
children: [],
schema: { children: value }
}
const { pkgMap = {}, blockPkgMap = {} } = getImportMap(structData.schema, config.componentsMap, config)
Object.entries({ ...pkgMap, ...blockPkgMap }).forEach(([key, value]) => {
value.forEach((valueItem) => {
globalHooks.addImport(key, valueItem)
})
})
// TODO: 需要验证 template 的生成有无问题
recursiveGenTemplateByHook(structData, globalHooks, { ...config, isJSX: true })
// TODO: 这里不通用,需要设计通用的做法,或者独立成 grid 的 hook
return {
value: `({${params.join(',')}}, h) => ${structData.children.join('')}`
}
}
}
export const handleExpressionAttrHook = (schemaData, globalHooks, config) => {
const { attributes, schema: { props = {} } = {} } = schemaData || {}
const isJSX = config.isJSX
Object.entries(props).forEach(([key, value]) => {
if (value?.type === JS_EXPRESSION && !isOn(key)) {
specialTypeHandler[JS_RESOURCE](value, globalHooks, config)
attributes.push(handleJSExpressionBinding(key, value, isJSX))
delete props[key]
}
})
}
export const handleI18nAttrHook = (schemaData, globalHooks, config) => {
const { attributes, schema: { props = {} } = {} } = schemaData || {}
const isJSX = config.isJSX
Object.entries(props).forEach(([key, value]) => {
if (value?.type === JS_I18N) {
attributes.push(handleBindI18n(key, value, isJSX))
}
})
}
export const handleTinyIconPropsHook = (schemaData, globalHooks, config) => {
const { attributes, schema: { props = {} } = {} } = schemaData || {}
const isJSX = config.isJSX
Object.entries(props).forEach(([key, value]) => {
if (value?.componentName === 'Icon' && value?.props?.name) {
const name = value.props.name
const iconName = name.startsWith(TINY_ICON) ? name : `Tiny${name}`
const exportName = name.replace(TINY_ICON, 'icon')
const success = globalHooks.addImport('@opentiny/vue-icon', {
componentName: exportName,
exportName: exportName,
package: '@opentiny/vue-icon',
version: '^3.10.0',
destructuring: true
})
if (success) {
globalHooks.addStatement({
position: INSERT_POSITION.BEFORE_PROPS,
value: `const ${iconName} = ${exportName}()`,
key: iconName
})
}
attributes.push(isJSX ? `icon={${iconName}}` : `:icon="${iconName}"`)
delete props[key]
}
})
}
export const transformObjType = (obj, globalHooks, config) => {
if (!obj || typeof obj !== 'object') {
return obj
}
let resStr = []
let shouldBindToState = false
let shouldRenderKey = !Array.isArray(obj)
for (const [key, value] of Object.entries(obj)) {
let renderKey = shouldRenderKey ? `${key}: ` : ''
if (typeof value === 'string') {
resStr.push(`${renderKey}"${value.replaceAll("'", "\\'").replaceAll(/"/g, "'")}"`)
continue
}
if (typeof value !== 'object' || value === null) {
resStr.push(`${renderKey}${value}`)
continue
}
if (specialTypeHandler[value?.type]) {
const specialVal = specialTypeHandler[value.type](value, globalHooks, config)?.value || ''
resStr.push(`${renderKey}${specialVal}`)
if (specialTypes.includes(value.type)) {
shouldBindToState = true
}
continue
}
if (hasAccessor(value?.accessor)) {
resStr.push(`${renderKey}${value.defaultValue || "''"}`)
if (isSetter(value?.accessor)) {
globalHooks.addStatement({
position: INSERT_POSITION.AFTER_METHODS,
value: `vue.watchEffect(wrap(${value.accessor.setter?.value ?? ''}))`
})
}
if (isGetter(value?.accessor)) {
globalHooks.addStatement({
position: INSERT_POSITION.AFTER_METHODS,
value: `vue.watchEffect(wrap(${value.accessor.getter?.value ?? ''}))`
})
}
continue
}
const { res: tempRes, shouldBindToState: tempShouldBindToState } =
transformObjType(value, globalHooks, config) || {}
resStr.push(`${renderKey}${tempRes}`)
if (tempShouldBindToState) {
shouldBindToState = true
}
}
return {
shouldBindToState,
res: Array.isArray(obj) ? `[${resStr.join(',')}]` : `{ ${resStr.join(',')} }`
}
}
export const handleObjBindAttrHook = (schemaData, globalHooks, config) => {
const { attributes, schema: { props = {} } = {} } = schemaData || {}
const isJSX = config.isJSX
Object.entries(props).forEach(([key, value]) => {
if (!value || typeof value !== 'object') {
return
}
if ([JS_EXPRESSION, JS_I18N].includes(value?.type)) {
return
}
const { res, shouldBindToState } = transformObjType(value, globalHooks, config)
if (shouldBindToState && !isJSX) {
let stateKey = key
let addSuccess = globalHooks.addState(stateKey, `${stateKey}:${res}`)
while (!addSuccess) {
stateKey = `${key}${randomString()}`
addSuccess = globalHooks.addState(stateKey, `${stateKey}:${res}`)
}
attributes.push(`:${key}="state.${stateKey}"`)
} else {
attributes.push(isJSX ? `${key}={${res}}` : `:${key}="${res.replaceAll(/"/g, "'")}"`)
}
delete props[key]
})
}
// 处理基本类似的 attribute如 string、boolean
export const handlePrimitiveAttributeHook = (schemaData, globalHooks, config) => {
const { attributes } = schemaData
const props = schemaData.schema?.props || {}
const isJSX = config.isJSX
for (const [key, value] of Object.entries(props)) {
const valueType = typeof value
if (valueType === 'string') {
attributes.push(`${key}="${value.replaceAll(/"/g, "'")}"`)
delete props[key]
}
if (['boolean', 'number'].includes(valueType)) {
attributes.push(isJSX ? `${key}={${value}}` : `:${key}="${value}"`)
delete props[key]
}
}
}
// 检测表达式类型引用 utils 的场景,需要在 script 中声明 utils 表达式
export const handleBindUtilsHooks = (schemaData, globalHooks, config) => {
const { schema: { props = {} } = {} } = schemaData || {}
Object.entries(props).forEach(([key, value]) => {
if (value?.type === JS_EXPRESSION && !isOn(key)) {
specialTypeHandler[JS_RESOURCE](value, globalHooks, config)
}
})
}

View File

@ -0,0 +1,263 @@
import { capitalize } from '@vue/shared'
import { toEventKey, isGetter, isSetter } from '@/utils'
import { generateImportByPkgName } from '@/utils/generateImportStatement'
import { INSERT_POSITION } from '@/constant'
import { transformObjType } from './generateAttribute'
import { hasJsx } from '@/utils/hasJsx'
export const defaultGenImportHook = (schema, globalHooks) => {
const dependenciesMap = globalHooks.getImport() || {}
return Object.entries(dependenciesMap)
.map(([key, value]) => {
return generateImportByPkgName({ pkgName: key, imports: value }) || ''
})
.join('\n')
}
export const defaultGenPropsHook = (schema) => {
const propsArr = []
const properties = schema?.schema?.properties || []
properties.forEach(({ content = [] }) => {
content.forEach(({ property, type, defaultValue }) => {
let propType = capitalize(type)
let propValue = defaultValue
if (propType === 'String') {
propValue = JSON.stringify(defaultValue)
} else if (['Array', 'Object'].includes(propType)) {
propValue = `() => (${JSON.stringify(defaultValue)})`
} else if (propType === 'Function') {
propValue = defaultValue.value
}
propsArr.push(`${property}: { type: ${propType}, default: ${propValue} }`)
})
})
return `const props = defineProps({ ${propsArr.join(',')} })\n`
}
export const defaultGenEmitsHook = (schema) => {
const emitArr = schema?.schema?.events || {}
const renderArr = Object.keys(emitArr).map(toEventKey)
return `const emit = defineEmits(${JSON.stringify(renderArr)})`
}
export const defaultGenStateHook = (schema, globalHooks) => {
const reactiveStatement = `const state = vue.reactive({${Object.values(globalHooks.getState()).join(',')}})`
return reactiveStatement
}
export const defaultGenMethodHook = (schema, globalHooks) => {
const methods = globalHooks.getMethods() || {}
const methodsArr = Object.entries(methods).map(([key, item]) => `const ${key} = wrap(${item.value})`)
const methodsNames = Object.keys(methods)
const wrapMethods = methodsNames.length ? `wrap({ ${methodsNames.join(',')} })` : ''
return `${methodsArr.join('\n')}\n\n${wrapMethods}`
}
export const defaultGenLifecycleHook = (schema) => {
const { setup: setupFunc, ...restLifeCycle } = schema?.lifeCycles || {}
let setupRes = ''
if (setupFunc) {
const setupStatement = `const setup = wrap(${setupFunc.value})`
const setupExecution = 'setup({ props, context: { emit }, state, ...vue })'
setupRes = `${setupStatement}\n${setupExecution}`
}
const restLifeCycleRes = Object.entries(restLifeCycle).map(([key, item]) => `vue.${key}(wrap(${item.value}))`)
return `${setupRes}\n${restLifeCycleRes.join('\n')}`
}
export const parsePropsHook = (schema, globalHooks) => {
const properties = schema?.schema?.properties || []
properties.forEach(({ content = [] }) => {
content.forEach(({ accessor } = {}) => {
if (isGetter(accessor)) {
globalHooks.addStatement({
position: INSERT_POSITION.AFTER_METHODS,
value: `vue.watchEffect(wrap(${accessor.getter?.value ?? ''}))`
})
}
if (isSetter(accessor)) {
globalHooks.addStatement({
position: INSERT_POSITION.AFTER_METHODS,
value: `vue.watchEffect(wrap(${accessor.setter?.value ?? ''}))`
})
}
})
})
}
export const parseReactiveStateHook = (schema, globalHooks, config) => {
const { res } = transformObjType(schema.state, globalHooks, config)
globalHooks.addState('$$innerState', `${res.slice(1, -1)}`)
}
export const handleProvideStatesContextHook = (schema, globalHooks) => {
globalHooks.addStatement({
position: INSERT_POSITION.AFTER_STATE,
value: `wrap({ state })`
})
}
export const handleContextInjectHook = (schema, globalHooks) => {
const injectLowcode = 'const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()'
const injectLowcodeWrap = 'const wrap = lowcodeWrap(props, { emit })'
const wrapStoresStatement = `wrap({ stores })`
globalHooks.addStatement({
key: 'tiny-engine-inject-statement',
position: INSERT_POSITION.AFTER_EMIT,
value: `${injectLowcode}\n${injectLowcodeWrap}\n${wrapStoresStatement}`
})
}
export const addDefaultVueImport = (schema, globalHooks) => {
globalHooks.addImport('vue', {
destructuring: false,
exportName: '*',
componentName: 'vue'
})
globalHooks.addImport('vue', {
destructuring: true,
exportName: 'defineProps',
componentName: 'defineProps'
})
globalHooks.addImport('vue', {
destructuring: true,
exportName: 'defineEmits',
componentName: 'defineEmits'
})
}
export const addDefaultVueI18nImport = (schema, globalHooks) => {
globalHooks.addImport('vue-i18n', {
destructuring: true,
exportName: 'I18nInjectionKey',
componentName: 'I18nInjectionKey'
})
}
export const GEN_SCRIPT_HOOKS = {
GEN_IMPORT: 'GEN_IMPORT',
GEN_PROPS: 'GEN_PROPS',
GEN_EMIT: 'GEN_EMIT',
GEN_STATE: 'GEN_STATE',
GEN_METHOD: 'GEN_METHOD',
GEN_LIFECYCLE: 'GEN_LIFECYCLE'
}
export const genScriptByHook = (schema, globalHooks, config) => {
const hooks = config.hooks || {}
const { parseScript = [], genScript = {} } = hooks
for (const parseHook of parseScript) {
parseHook(schema, globalHooks, config)
}
const {
AFTER_IMPORT,
BEFORE_PROPS,
AFTER_PROPS,
BEFORE_STATE,
AFTER_STATE,
BEFORE_METHODS,
AFTER_METHODS,
BEFORE_EMIT,
AFTER_EMIT
} = INSERT_POSITION
const statementGroupByPosition = {
[AFTER_IMPORT]: [],
[BEFORE_PROPS]: [],
[AFTER_PROPS]: [],
[BEFORE_EMIT]: [],
[AFTER_EMIT]: [],
[BEFORE_STATE]: [],
[AFTER_STATE]: [],
[BEFORE_METHODS]: [],
[AFTER_METHODS]: []
}
const statements = globalHooks.getStatements() || {}
Object.values(statements).forEach((statement) => {
if (statementGroupByPosition[statement.position]) {
statementGroupByPosition[statement.position].push(statement?.value)
return
}
statementGroupByPosition[AFTER_METHODS].push(statement?.value)
})
const importStr = genScript[GEN_SCRIPT_HOOKS.GEN_IMPORT]?.(schema, globalHooks, config) || ''
const propsStr = genScript[GEN_SCRIPT_HOOKS.GEN_PROPS]?.(schema, globalHooks, config) || ''
const emitStr = genScript[GEN_SCRIPT_HOOKS.GEN_EMIT]?.(schema, globalHooks, config) || ''
const stateStr = genScript[GEN_SCRIPT_HOOKS.GEN_STATE]?.(schema, globalHooks, config) || ''
const methodStr = genScript[GEN_SCRIPT_HOOKS.GEN_METHOD]?.(schema, globalHooks, config) || ''
const lifeCycleStr = genScript[GEN_SCRIPT_HOOKS.GEN_LIFECYCLE]?.(schema, globalHooks, config) || ''
const scriptConfig = globalHooks.getScriptConfig()
let scriptTag = '<script'
if (scriptConfig.setup) {
scriptTag = `${scriptTag} setup`
}
if (scriptConfig.lang) {
scriptTag = `${scriptTag} lang="${scriptConfig.lang}"`
}
const content = `
${statementGroupByPosition[AFTER_IMPORT].join('\n')}
${statementGroupByPosition[BEFORE_PROPS].join('\n')}
${propsStr}
${statementGroupByPosition[AFTER_PROPS].join('\n')}
${statementGroupByPosition[BEFORE_EMIT].join('\n')}
${emitStr}
${statementGroupByPosition[AFTER_EMIT].join('\n')}
${statementGroupByPosition[BEFORE_STATE].join('\n')}
${stateStr}
${statementGroupByPosition[AFTER_STATE].join('\n')}
${statementGroupByPosition[BEFORE_METHODS].join('\n')}
${methodStr}
${statementGroupByPosition[AFTER_METHODS].join('\n')}
${lifeCycleStr}`
// 检测当前 script 内容是否有 jsx如果有且未配置lang则需要自动加上 jsx 的配置
const isHasJsx = hasJsx(content)
if (!scriptConfig.lang && isHasJsx) {
scriptTag = `${scriptTag} lang="jsx"`
}
scriptTag = `${scriptTag}>`
return `
${scriptTag}
${importStr}
${content}
</script>`
}

View File

@ -0,0 +1,17 @@
export const generateStyleTag = (schema, config = {}) => {
const { css } = schema
const { scoped = true, lang = '' } = config
let langDesc = ''
let scopedStr = ''
if (scoped) {
scopedStr = 'scoped'
}
if (lang) {
langDesc = `lang=${langDesc}`
}
return `<style ${langDesc} ${scopedStr}> ${css} </style>`
}

View File

@ -0,0 +1,49 @@
import { hyphenate } from '@vue/shared'
export const HTML_DEFAULT_VOID_ELEMENTS = [
'img',
'input',
'br',
'hr',
'link',
'area',
'base',
'col',
'embed',
'meta',
'source',
'track',
'wbr'
]
export const generateTag = (tagName, config = {}) => {
const { isVoidElement, isStartTag = true, attribute, isJSX = false, useHyphenate = !isJSX } = config
if (typeof tagName !== 'string' || !tagName) {
return ''
}
let renderTagName = tagName
const isVoidEle =
isVoidElement || (typeof isVoidElement !== 'boolean' && HTML_DEFAULT_VOID_ELEMENTS.includes(renderTagName))
// 自闭合标签生成闭合标签时,返回空字符串
if (!isStartTag && isVoidEle) {
return ''
}
if (useHyphenate) {
renderTagName = hyphenate(tagName)
}
if (isVoidEle) {
return `<${renderTagName} ${attribute || ''}/>`
}
if (isStartTag) {
return `<${renderTagName} ${attribute || ''}>`
}
return `</${renderTagName}>`
}

View File

@ -0,0 +1,242 @@
import {
BUILTIN_COMPONENT_NAME,
BUILTIN_COMPONENT_NAME_MAP,
TINY_ICON,
INSERT_POSITION,
JS_EXPRESSION,
JS_I18N,
JS_RESOURCE
} from '@/constant'
import { generateTag, HTML_DEFAULT_VOID_ELEMENTS } from './generateTag'
import { specialTypeHandler } from './generateAttribute'
import { thisPropsBindRe, thisRegexp } from '@/utils'
export const handleComponentNameHook = (optionData) => {
const { componentName, schema } = optionData
// 内置 component
if (!BUILTIN_COMPONENT_NAME_MAP[componentName]) {
return
}
if (componentName === BUILTIN_COMPONENT_NAME.TEXT && schema.props.text) {
schema.children = schema.props.text
delete schema.props.text
}
optionData.componentName = BUILTIN_COMPONENT_NAME_MAP[componentName]
if (HTML_DEFAULT_VOID_ELEMENTS.includes(optionData.componentName)) {
optionData.voidElement = true
}
}
export const handleTinyIcon = (nameObj, globalHooks) => {
if (BUILTIN_COMPONENT_NAME.ICON !== nameObj.componentName) {
return
}
const name = nameObj.schema.props.name
if (!name) {
return
}
const iconName = name.startsWith(TINY_ICON) ? name : `Tiny${name}`
const exportName = name.replace(TINY_ICON, 'icon')
const success = globalHooks.addImport('@opentiny/vue-icon', {
componentName: exportName,
exportName: exportName,
package: '@opentiny/vue-icon',
version: '^3.10.0',
destructuring: true
})
// tiny icon 需要调用
if (success) {
globalHooks.addStatement({
position: INSERT_POSITION.BEFORE_PROPS,
value: `const ${iconName} = ${exportName}()`,
key: iconName
})
}
nameObj.componentName = iconName
delete nameObj.schema.props.name
}
const handleTinyGridSlots = (value, globalHooks, config) => {
if (!Array.isArray(value)) {
return
}
value.forEach((slotItem) => {
const name = slotItem.componentName
if (!name) {
return
}
if (slotItem.componentType === 'Block') {
const importPath = `${config.blockRelativePath}${name}${config.blockSuffix}`
globalHooks.addImport(importPath, {
exportName: name,
componentName: name,
package: importPath
})
} else if (name?.startsWith?.('Tiny')) {
globalHooks.addImport('@opentiny/vue', {
destructuring: true,
exportName: name.slice(4),
componentName: name,
package: '@opentiny/vue'
})
}
handleTinyGridSlots(slotItem.children, globalHooks, config)
})
}
export const handleTinyGrid = (schemaData, globalHooks, config) => {
const { componentName, props } = schemaData.schema
// 同时存在 data 和 fetchData 的时候,删除 data
if (componentName === 'TinyGrid' && props?.data && props?.fetchData) {
delete props.data
}
// 处理 TinyGrid 插槽
if (componentName !== 'TinyGrid' || !Array.isArray(props?.columns)) {
return
}
// 处理 TinyGrid 组件 editor 插槽组件使用 opentiny/vue 组件的场景,需要在 import 中添加对应Tiny组件的引入
props.columns.forEach((item) => {
if (item.editor?.component?.startsWith?.('Tiny')) {
const name = item.editor?.component
globalHooks.addImport('@opentiny/vue', {
destructuring: true,
exportName: name.slice(4),
componentName: name,
package: '@opentiny/vue'
})
item.editor.component = {
type: 'JSExpression',
value: name
}
}
if (typeof item.slots === 'object') {
Object.values(item.slots).forEach((slotItem) => handleTinyGridSlots(slotItem?.value, globalHooks, config))
}
})
}
export const handleExpressionChildren = (schemaData = {}, globalHooks, config) => {
const { children, schema } = schemaData
const type = schema?.children?.type
const isJSX = config.isJSX
const prefix = isJSX ? '{' : '{{'
const suffix = isJSX ? '}' : '}}'
if (type === JS_EXPRESSION) {
specialTypeHandler[JS_RESOURCE](schema.children, globalHooks, config)
children.push(
`${prefix} ${schema.children?.value.replace(isJSX ? thisRegexp : thisPropsBindRe, '') || ''} ${suffix}`
)
delete schema.children
return
}
if (type === JS_I18N && schema.children?.key) {
children.push(`${prefix} t('${schema.children.key}') ${suffix}`)
delete schema.children
return
}
}
export const validEmptyTemplateHook = (schema = {}) => {
if (schema.componentName === BUILTIN_COMPONENT_NAME.TEMPLATE && !schema.children?.length) {
return false
}
return true
}
// TODO: 支持物料中自定义出码关联片段
export const recursiveGenTemplateByHook = (schemaWithRes, globalHooks, config = {}) => {
const schemaChildren = schemaWithRes?.schema?.children || []
const { hooks = {}, isJSX } = config
// 自定义 hooks
const { genTemplate: genTemplateHooks, templateItemValidate } = hooks
if (!Array.isArray(schemaChildren)) {
schemaWithRes.children.push(schemaChildren || '')
return
}
const resArr = schemaChildren.map((schemaItem) => {
for (const validateItem of templateItemValidate) {
if (!validateItem(schemaItem, globalHooks, config)) {
return ''
}
}
if (typeof schemaItem !== 'object' || !schemaItem) {
return schemaItem || ''
}
const { componentName, component } = schemaItem
const optionData = {
schema: schemaItem,
voidElement: false,
componentName: componentName ?? component ?? '',
prefix: [],
attributes: [],
children: [],
suffix: []
}
for (const hookItem of [...genTemplateHooks, recursiveGenTemplateByHook]) {
hookItem(optionData, globalHooks, config)
}
const startTag = generateTag(optionData.componentName, {
attribute: optionData.attributes.join(' '),
isVoidElement: optionData.voidElement,
isJSX
})
let endTag = ''
if (!optionData.voidElement) {
endTag = generateTag(optionData.componentName, { isStartTag: false, isJSX })
}
return `
${optionData.prefix.join('')}${startTag}${optionData.children.join('')}${endTag}${optionData.suffix.join('')}`
})
schemaWithRes.children = schemaWithRes.children.concat(resArr)
}
export const genTemplateByHook = (schema, globalHooks, config) => {
const parsedSchema = {
children: [],
schema: structuredClone({ children: [{ ...schema, componentName: 'div' }] })
}
recursiveGenTemplateByHook(parsedSchema, globalHooks, config)
return `<template>${parsedSchema.children.join('')}</template>`
}

View File

@ -0,0 +1 @@
export { default as generateSFCFile, genSFCWithDefaultPlugin } from './genSetupSFC'

View File

@ -0,0 +1,89 @@
import { BUILTIN_COMPONENT_NAME } from '@/constant'
import { generateImportByPkgName } from '@/utils/generateImportStatement'
export const parseImport = (children) => {
let components = []
let blocks = []
for (const item of children || []) {
if (item?.componentType === BUILTIN_COMPONENT_NAME.BLOCK) {
blocks.push(item?.componentName)
} else {
components.push(item?.componentName)
}
if (Array.isArray(item?.children) && item.children.length > 0) {
const { components: childComp, blocks: childBlocks } = parseImport(item.children)
components = components.concat(childComp)
blocks = blocks.concat(childBlocks)
}
}
return {
components: [...new Set(components)],
blocks: [...new Set(blocks)]
}
}
export const getImportMap = (schema, componentsMap, config) => {
const { components, blocks } = parseImport(schema.children)
const pkgMap = {}
const importComps = componentsMap.filter(({ componentName }) => components.includes(componentName))
importComps.forEach((item) => {
const key = item.package || item.main
if (!key) {
return
}
pkgMap[key] = pkgMap[key] || []
pkgMap[key].push(item)
})
const { blockRelativePath = '../components', blockSuffix = '.vue' } = config
const blockPkgMap = {}
const relativePath = blockRelativePath.endsWith('/') ? blockRelativePath.slice(0, -1) : blockRelativePath
blocks.map((name) => {
const source = `${relativePath}/${name}${blockSuffix}`
blockPkgMap[source] = blockPkgMap[source] || []
blockPkgMap[source].push({
componentName: name,
exportName: name,
destructuring: false,
package: source
})
})
return {
pkgMap,
blockPkgMap
}
}
export const genCompImport = (schema, componentsMap, config = {}) => {
const { components, blocks } = parseImport(schema.children)
const pkgMap = {}
const { blockRelativePath = '../components/', blockSuffix = '.vue' } = config
const importComps = componentsMap.filter(({ componentName }) => components.includes(componentName))
importComps.forEach((item) => {
pkgMap[item.package] = pkgMap[item.package] || []
pkgMap[item.package].push(item)
})
const batchImportStatements = Object.entries(pkgMap).map(([key, value]) => {
return generateImportByPkgName({ pkgName: key, imports: value })
})
const blockImportStatement = blocks.map((name) => {
return `import ${name} from ${blockRelativePath}/${name}${blockSuffix}`
})
return `${batchImportStatements.join('\n')}\n${blockImportStatement.join('\n')}`
}

151
packages/vue-generator/src/index.d.ts vendored Normal file
View File

@ -0,0 +1,151 @@
declare namespace tinyEngineDslVue {
type defaultPlugins =
| 'template'
| 'block'
| 'page'
| 'dataSource'
| 'dependencies'
| 'globalState'
| 'i18n'
| 'router'
| 'utils'
| 'formatCode'
| 'parseSchema'
type IPluginFun = (schema: IAppSchema, context: IContext) => void
interface IConfig {
customPlugins?: {
[key in defaultPlugins]?: IPluginFun
} & {
[key in 'transformStart' | 'transform' | 'transformEnd']?: Array<IPluginFun>
}
pluginConfig?: {
[k in defaultPlugins]: Record<string, any>
}
customContext?: Record<string, any>
}
interface IContext {
config: Record<string, any>
genResult: Array<IFile>
genLogs: Array<any>
error: Array<any>
}
export function generateApp(config?: IConfig): codeGenInstance
interface codeGenInstance {
generate(IAppSchema): ICodeGenResult
}
interface ICodeGenResult {
errors: Array<any>
genResult: Array<IFile>
genLogs: Array<any>
}
interface IFile {
fileType: string
fileName: string
path: string
fileContent: string
}
interface IAppSchema {
i18n: {
en_US: Record<string, any>
zh_CN: Record<string, any>
}
utils: Array<IUtilsItem>
dataSource: IDataSource
globalState: Array<IGlobalStateItem>
pageSchema: Array<IPageSchema | IFolderItem>
blockSchema: Array<IPageSchema>
componentsMap: Array<IComponentMapItem>
meta: IMetaInfo
}
interface IUtilsItem {
name: string
type: 'npm' | 'function'
content: object
}
interface IDataSource {
list: Array<{ id: number; name: string; data: object }>
dataHandler?: IFuncType
errorHandler?: IFuncType
willFetch?: IFuncType
}
interface IFuncType {
type: 'JSFunction'
value: string
}
interface IExpressionType {
type: 'JSExpression'
value: string
}
interface IGlobalStateItem {
id: string
state: Record<string, any>
actions: Record<string, IFuncType>
getters: Record<string, IFuncType>
}
interface IPageSchema {
componentName: 'Page' | 'Block'
css: string
fileName: string
lifeCycles: {
[key: string]: Record<string, IFuncType>
}
methods: Record<string, IFuncType>
props: Record<string, any>
state: Array<Record<string, any>>
meta: {
id: number
isHome: boolean
parentId: string
rootElement: string
route: string
}
children: Array<ISchemaChildrenItem>
schema?: {
properties: Array<Record<string, any>>
events: Record<string, any>
}
}
interface IFolderItem {
componentName: 'Folder'
depth: number
folderName: string
id: string
parentId: string
router: string
}
interface ISchemaChildrenItem {
children: Array<ISchemaChildrenItem>
componentName: string
id: string
props: Record<string, any>
}
interface IComponentMapItem {
componentName: string
destructuring: boolean
exportName?: string
package?: string
version: string
}
interface IMetaInfo {
name: string
description: string
}
}

View File

@ -1,15 +1,25 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { generateCode, generateBlocksCode, generatePageCode } from './generator'
/// <reference path="index.d.ts">
export { generateCode, generateBlocksCode, generatePageCode }
export {
generateCode,
generateBlocksCode,
generatePageCode,
generateApp,
CodeGenerator,
genSFCWithDefaultPlugin,
generateSFCFile
} from './generator'
export { parseRequiredBlocks } from './utils/parseRequiredBlocks'

View File

@ -1,14 +1,14 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { UNWRAP_QUOTES, JS_EXPRESSION, JS_FUNCTION, JS_I18N, JS_RESOURCE, JS_SLOT } from '../constant'
import { getFunctionInfo, hasAccessor, addAccessorRecord } from '../utils'
@ -17,7 +17,7 @@ import { generateJSXTemplate } from './jsx-slot'
const { start, end } = UNWRAP_QUOTES
const strategy = {
export const strategy = {
[JS_EXPRESSION]: ({ value, computed }) => {
if (computed) {
return `${start}vue.computed(${value.replace(/this\./g, '')})${end}`
@ -72,6 +72,7 @@ const transformType = (current, prop, description) => {
current[prop] = strategy[type](current[prop], description)
}
// TODO: 这个是什么场景?
if (hasAccessor(accessor)) {
current[prop] = defaultValue

View File

@ -0,0 +1,56 @@
import prettier from 'prettier'
import parserHtml from 'prettier/parser-html'
import parseCss from 'prettier/parser-postcss'
import parserBabel from 'prettier/parser-babel'
import { mergeOptions } from '../utils/mergeOptions'
function formatCode(options = {}) {
const defaultOption = {
singleQuote: true,
printWidth: 120,
semi: false,
trailingComma: 'none'
}
const parserMap = {
json: 'json-stringify',
js: 'babel',
jsx: 'babel',
css: 'css',
less: 'less',
html: 'html',
vue: 'vue'
}
const mergedOption = mergeOptions(defaultOption, options)
return {
name: 'tinyEngine-generateCode-plugin-format-code',
description: 'transform block schema to code',
/**
* 格式化出码
* @param {tinyEngineDslVue.IAppSchema} schema
* @returns
*/
run(schema, context) {
context.genResult.forEach((item) => {
const { fileContent, fileName } = item
const parser = parserMap[fileName.split('.').at(-1)]
if (!parser) {
return
}
const formattedCode = prettier.format(fileContent, {
parser,
plugins: [parserBabel, parseCss, parserHtml, ...(mergedOption.customPlugin || [])],
...mergedOption
})
this.replaceFile({ ...item, fileContent: formattedCode })
})
}
}
}
export default formatCode

View File

@ -0,0 +1,47 @@
import { mergeOptions } from '../utils/mergeOptions'
import { genSFCWithDefaultPlugin } from '../generator'
const defaultOption = {
blockBasePath: './src/components'
}
function genBlockPlugin(options = {}) {
const realOptions = mergeOptions(defaultOption, options)
const { blockBasePath, sfcConfig = {} } = realOptions
return {
name: 'tinyEngine-generateCode-plugin-block',
description: 'transform block schema to code',
/**
* 将区块 schema 转换成高代码
* @param {tinyEngineDslVue.IAppSchema} schema
* @returns
*/
run(schema) {
const blocks = schema?.blockSchema || []
const componentsMap = schema?.componentsMap
if (blocks && !Array.isArray(blocks)) {
throw new Error(`[codeGenerate][plugins] blockSchema should be array, but actually receive ${typeof blocks}`)
}
const resBlocks = []
for (const block of blocks) {
const res = genSFCWithDefaultPlugin(block, componentsMap, { blockRelativePath: './', ...sfcConfig })
resBlocks.push({
fileType: 'vue',
fileName: `${block.fileName}.vue`,
path: blockBasePath,
fileContent: res
})
}
return resBlocks
}
}
}
export default genBlockPlugin

View File

@ -0,0 +1,52 @@
import { mergeOptions } from '../utils/mergeOptions'
const defaultOption = {
fileName: 'dataSource.json',
path: './src/lowcodeConfig'
}
function genDataSourcePlugin(options = {}) {
const realOptions = mergeOptions(defaultOption, options)
const { path, fileName } = realOptions
return {
name: 'tinyEngine-generateCode-plugin-dataSource',
description: 'transform schema to dataSource plugin',
/**
* 转换 dataSource
* @param {tinyEngineDslVue.IAppSchema} schema
* @returns
*/
run(schema) {
const dataSource = schema?.dataSource || {}
const { dataHandler, errorHandler, willFetch, list } = dataSource || {}
const data = {
list: list.map(({ id, name, data }) => ({ id, name, ...data }))
}
if (dataHandler) {
data.dataHandler = dataHandler
}
if (errorHandler) {
data.errorHandler = errorHandler
}
if (willFetch) {
data.willFetch = willFetch
}
return {
fileType: 'json',
fileName,
path,
fileContent: JSON.stringify(data)
}
}
}
}
export default genDataSourcePlugin

View File

@ -0,0 +1,93 @@
import { mergeOptions } from '../utils/mergeOptions'
import { parseImport } from '@/generator/vue/sfc/parseImport'
const defaultOption = {
fileName: 'package.json',
path: '.'
}
const getComponentsSet = (schema) => {
const { pageSchema = [], blockSchema = [] } = schema
let allComponents = []
pageSchema.forEach((pageItem) => {
allComponents = allComponents.concat(parseImport(pageItem.children || [])?.components || [])
})
blockSchema.forEach((blockItem) => {
allComponents = allComponents.concat(parseImport(blockItem.children || [])?.components || [])
})
return new Set(allComponents)
}
const parseSchema = (schema) => {
const { utils = [], componentsMap = [] } = schema
const resDeps = {}
for (const {
type,
content: { package: packageName, version }
} of utils) {
if (type !== 'npm' || resDeps[packageName]) {
continue
}
resDeps[packageName] = version || 'latest'
}
const componentsSet = getComponentsSet(schema)
for (const { package: packageName, version, componentName } of componentsMap) {
if (packageName && !resDeps[packageName] && componentsSet.has(componentName)) {
resDeps[packageName] = version || 'latest'
}
}
// 处理内置 Icon如果使用了 tinyvue 组件,则默认添加 @opentiny/vue-icon 依赖,且依赖与 @opentiny/vue 依赖版本一致
if (resDeps['@opentiny/vue']) {
resDeps['@opentiny/vue-icon'] = resDeps['@opentiny/vue']
}
return resDeps
}
function genDependenciesPlugin(options = {}) {
const realOptions = mergeOptions(defaultOption, options)
const { path, fileName } = realOptions
return {
name: 'tinyEngine-generateCode-plugin-dependencies',
description: 'transform dependencies to package.json',
/**
* 分析依赖写入 package.json
* @param {tinyEngineDslVue.IAppSchema} schema
* @returns
*/
run(schema) {
const dependencies = parseSchema(schema)
const originPackageItem = this.getFile(path, fileName)
if (!originPackageItem) {
return {
fileName,
path,
fileContent: JSON.stringify({ dependencies })
}
}
let originPackageJSON = JSON.parse(originPackageItem.fileContent)
originPackageJSON.dependencies = {
...originPackageJSON.dependencies,
...dependencies
}
this.addFile({ fileType: 'json', fileName, path, fileContent: JSON.stringify(originPackageJSON) }, true)
}
}
}
export default genDependenciesPlugin

View File

@ -0,0 +1,98 @@
import { mergeOptions } from '../utils/mergeOptions'
const defaultOption = {
fileName: '',
path: './src/stores'
}
const parseSchema = (schema) => {
let globalState = schema?.globalState
if (!Array.isArray(globalState)) {
globalState = []
}
return globalState
}
function genDependenciesPlugin(options = {}) {
const realOptions = mergeOptions(defaultOption, options)
const { path } = realOptions
return {
name: 'tinyEngine-generateCode-plugin-globalState',
description: 'transform schema to globalState',
/**
* 转换 globalState
* @param {tinyEngineDslVue.IAppSchema} schema
* @returns
*/
run(schema) {
const globalState = parseSchema(schema)
const res = []
const ids = []
for (const stateItem of globalState) {
let importStatement = "import { defineStore } from 'pinia'"
const { id, state, getters, actions } = stateItem
ids.push(id)
const stateExpression = `() => ({ ${Object.entries(state)
.map((item) => {
let [key, value] = item
if (value === '') {
value = "''"
}
if (value && typeof value === 'object') {
value = JSON.stringify(value)
}
return [key, value].join(':')
})
.join(',')} })`
const getterExpression = Object.entries(getters)
.filter((item) => item.value?.type === 'JSFunction')
.map(([key, value]) => `${key}: ${value.value}`)
.join(',')
const actionExpressions = Object.entries(actions)
.filter((item) => item.value?.type === 'JSFunction')
.map(([key, value]) => `${key}: ${value.value}`)
.join(',')
const storeFiles = `
${importStatement}
export const ${id} = defineStore({
id: ${id},
state: ${stateExpression},
getters: { ${getterExpression} },
actions: { ${actionExpressions} }
})
`
res.push({
fileType: 'js',
fileName: `${id}.js`,
path,
fileContent: storeFiles
})
}
res.push({
fileType: 'js',
fileName: 'index.js',
path,
fileContent: ids.map((item) => `export { ${item} } from './${item}'`).join('\n')
})
return res
}
}
}
export default genDependenciesPlugin

View File

@ -0,0 +1,74 @@
import { mergeOptions } from '../utils/mergeOptions'
import { generateImportStatement } from '../utils/generateImportStatement'
const defaultOption = {
localeFileName: 'locale.js',
entryFileName: 'index.js',
path: './src/i18n'
}
function genI18nPlugin(options = {}) {
const realOptions = mergeOptions(defaultOption, options)
const { path, localeFileName, entryFileName } = realOptions
return {
name: 'tinyEngine-generateCode-plugin-i18n',
description: 'transform i18n schema to i18n code plugin',
/**
* 将国际化 schema 转换成 i18n 高代码
* @param {tinyEngineDslVue.IAppSchema} schema
* @returns
*/
run(schema) {
const i18n = schema?.i18n || []
const res = []
// 生成国际化词条文件
for (const [key, value] of Object.entries(i18n)) {
res.push({
fileType: 'json',
fileName: `${key}.json`,
path,
fileContent: JSON.stringify(value, null, 2)
})
}
const langs = Object.keys(i18n)
const importStatements = langs.map((lang) =>
generateImportStatement({ moduleName: `./${lang}.json`, exportName: lang })
)
// 生成 locale.js
res.push({
fileType: 'json',
fileName: localeFileName,
path,
fileContent: `
${importStatements.join('\n')}
export default { ${langs.join(',')} }`
})
// 生成 index.js 入口文件
res.push({
fileName: entryFileName,
path,
fileContent: `
import i18n from '@opentiny/tiny-engine-i18n-host'
import lowcode from '../lowcodeConfig/lowcode'
import locale from './${localeFileName}'
i18n.lowcode = lowcode
${langs.map((langItem) => `i18n.global.mergeLocaleMessage('${langItem}', locale.${langItem})`).join('\n')}
export default i18n`
})
return res
}
}
}
export default genI18nPlugin

View File

@ -0,0 +1,42 @@
import { mergeOptions } from '../utils/mergeOptions'
import { genSFCWithDefaultPlugin } from '../generator'
const defaultOption = {
pageBasePath: './src/views'
}
function genPagePlugin(options = {}) {
const realOptions = mergeOptions(defaultOption, options)
const { pageBasePath, sfcConfig = {} } = realOptions
return {
name: 'tinyEngine-generateCode-plugin-page',
description: 'transform page schema to code',
/**
* 将页面 schema 转换成高代码
* @param {tinyEngineDslVue.IAppSchema} schema
* @returns
*/
run(schema) {
const pages = schema.pageSchema
const resPage = []
for (const page of pages) {
const res = genSFCWithDefaultPlugin(page, schema.componentsMap, sfcConfig)
resPage.push({
fileType: 'vue',
fileName: `${page.fileName}.vue`,
path: `${pageBasePath}/${page.path || ''}`,
fileContent: res
})
}
return resPage
}
}
}
export default genPagePlugin

View File

@ -0,0 +1,84 @@
import { mergeOptions } from '../utils/mergeOptions'
const defaultOption = {
fileName: 'index.js',
path: './src/router'
}
const parseSchema = (schema) => {
const { pageSchema } = schema
const routes = pageSchema.map(({ meta: { isHome = false, router = '' } = {}, fileName, path }) => ({
filePath: `@/views${path ? `/${path}` : ''}/${fileName}.vue`,
fileName,
isHome,
path: router?.startsWith?.('/') ? router : `/${router}`
}))
const hasRoot = routes.some(({ path }) => path === '/')
if (!hasRoot && routes.length) {
const { path: homePath } = routes.find(({ isHome }) => isHome) || { path: routes[0].path }
routes.unshift({ path: '/', redirect: homePath })
}
return routes
}
function genRouterPlugin(options = {}) {
const realOptions = mergeOptions(defaultOption, options)
const { path, fileName } = realOptions
return {
name: 'tinyEngine-generateCode-plugin-router',
description: 'transform router schema to router code plugin',
/**
* 根据页面生成路由配置
* @param {tinyEngineDslVue.IAppSchema} schema
* @returns
*/
run(schema) {
const routesList = parseSchema(schema)
// TODO: 支持 hash 模式、history 模式
const importSnippet = "import { createRouter, createWebHashHistory } from 'vue-router'"
const exportSnippet = `
export default createRouter({
history: createWebHashHistory(),
routes
})`
const routes = routesList.map(({ fileName, path, redirect, filePath }) => {
let pathAttr = `path: '${path}'`
let redirectAttr = ''
let componentAttr = ''
if (redirect) {
redirectAttr = `redirect: '${redirect}'`
}
if (fileName) {
componentAttr = `component: () => import('${filePath}')`
}
const res = [pathAttr, redirectAttr, componentAttr].filter((item) => Boolean(item)).join(',')
return `{${res}}`
})
const routeSnippets = `const routes = [${routes.join(',')}]`
const res = {
fileType: 'js',
fileName,
path,
fileContent: `${importSnippet}\n ${routeSnippets} \n ${exportSnippet}`
}
return res
}
}
}
export default genRouterPlugin

View File

@ -0,0 +1,39 @@
import { templateMap } from '../templates'
function genTemplatePlugin(options = {}) {
return {
name: 'tinyEngine-generateCode-plugin-template',
description: 'generate template code',
run(schema, context) {
if (typeof options?.template === 'function') {
const res = options.template(schema, context)
if (Array.isArray(res)) {
return res
}
if (res?.fileContent && res?.fileName) {
return res
}
return
}
const template = context?.template || 'default'
if (!template) {
return
}
if (typeof template === 'function') {
context.genResult.push(...(template(schema) || []))
return
}
if (templateMap[template]) {
context.genResult.push(...templateMap[template](schema))
}
}
}
}
export default genTemplatePlugin

View File

@ -0,0 +1,92 @@
import { mergeOptions } from '../utils/mergeOptions'
import { generateImportStatement } from '../utils/generateImportStatement'
const defaultOption = {
fileName: 'utils.js',
path: './src'
}
function genUtilsPlugin(options = {}) {
const realOptions = mergeOptions(defaultOption, options)
const { path, fileName } = realOptions
const handleNpmUtils = (utilsConfig) => {
const { content } = utilsConfig
const { package: packageName, exportName, destructuring, subName } = content
const statement = generateImportStatement({ moduleName: packageName, exportName, alias: subName, destructuring })
let realExportName = exportName
if (subName) {
realExportName = subName
}
return {
res: statement,
exportName: realExportName
}
}
const handleFunctionUtils = (utilsConfig) => {
const { content, name } = utilsConfig
return {
res: `const ${name} = ${content.value}`,
exportName: name
}
}
return {
name: 'tinyEngine-generateCode-plugin-utils',
description: 'transform utils schema to utils code',
/**
* 生成 utils 源码
* @param {tinyEngineDslVue.IAppSchema} schema
* @returns
*/
run(schema) {
const { utils } = schema
if (!Array.isArray(utils)) {
return
}
const importStatements = []
const variableStatements = []
const exportVariables = []
const utilsHandlerMap = {
npm: handleNpmUtils,
function: handleFunctionUtils
}
for (const utilItem of utils) {
const { res, exportName } = utilsHandlerMap[utilItem.type](utilItem)
if (utilItem.type === 'function') {
variableStatements.push(res)
} else {
importStatements.push(res)
}
exportVariables.push(exportName)
}
const fileContent = `
${importStatements.join('\n')}
${variableStatements.join('\n')}
export { ${exportVariables.join(',')} }
`
return {
fileType: 'js',
fileName,
path,
fileContent
}
}
}
}
export default genUtilsPlugin

View File

@ -0,0 +1,11 @@
export { default as genDataSourcePlugin } from './genDataSourcePlugin'
export { default as genBlockPlugin } from './genBlockPlugin'
export { default as genDependenciesPlugin } from './genDependenciesPlugin'
export { default as genPagePlugin } from './genPagePlugin'
export { default as genRouterPlugin } from './genRouterPlugin'
export { default as genUtilsPlugin } from './genUtilsPlugin'
export { default as genI18nPlugin } from './genI18nPlugin'
export { default as genTemplatePlugin } from './genTemplatePlugin'
export { default as formatCodePlugin } from './formatCodePlugin'
export { default as genGlobalState } from './genGlobalState'
export { default as parseSchemaPlugin } from './parseSchemaPlugin'

View File

@ -0,0 +1,54 @@
import { BUILTIN_COMPONENTS_MAP } from '@/constant'
function parseSchema() {
return {
name: 'tinyEngine-generateCode-plugin-parse-schema',
description: 'parse schema, preprocess schema',
/**
* 解析schema预处理 schema
* @param {tinyEngineDslVue.IAppSchema} schema
* @returns
*/
run(schema) {
const { pageSchema } = schema
const pagesMap = {}
const resPageTree = []
schema.componentsMap = [...schema.componentsMap, ...BUILTIN_COMPONENTS_MAP]
for (const componentItem of pageSchema) {
pagesMap[componentItem.meta.id] = componentItem
}
for (const componentItem of pageSchema) {
if (!componentItem.meta.isPage) {
continue
}
const newComponentItem = {
...componentItem
}
let path = ''
let curParentId = componentItem.meta.parentId
let depth = 0
while (curParentId !== '0' && depth < 1000) {
const preFolder = pagesMap[curParentId]
path = `${preFolder.meta.name}${path ? '/' : ''}${path}`
newComponentItem.meta.router = `${preFolder.meta.router}/${newComponentItem.meta.router}`
curParentId = preFolder.meta.parentId
depth++
}
newComponentItem.path = path
resPageTree.push(newComponentItem)
}
schema.pageSchema = resPageTree
}
}
}
export default parseSchema

View File

@ -1,14 +1,14 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { BUILTIN_COMPONENT_NAME, TINY_ICON } from '../constant'

View File

@ -0,0 +1,5 @@
import { generateTemplate as genDefaultStaticTemplate } from './vue-template'
export const templateMap = {
default: genDefaultStaticTemplate
}

View File

@ -0,0 +1,125 @@
import readmeFile from './templateFiles/README.md?raw'
import genViteConfig from './templateFiles/genViteConfig'
import getPackageJson from './templateFiles/packageJson'
import gitIgnoreFile from './templateFiles/.gitignore?raw'
import entryHTMLFile from './templateFiles/index.html?raw'
import mainJSFile from './templateFiles/src/main.js?raw'
import appVueFile from './templateFiles/src/App.vue?raw'
import bridgeFile from './templateFiles/src/lowcodeConfig/bridge.js?raw'
import dataSourceFile from './templateFiles/src/lowcodeConfig/dataSource.js?raw'
import lowcodeJSFile from './templateFiles/src/lowcodeConfig/lowcode.js?raw'
import lowcodeStoreFile from './templateFiles/src/lowcodeConfig/store.js?raw'
import axiosFile from './templateFiles/src/http/axios.js?raw'
import axiosConfigFile from './templateFiles/src/http/config.js?raw'
import httpEntryFile from './templateFiles/src/http/index.js?raw'
/**
* 模板写入动态内容
* @param {*} context
* @param {*} str
* @returns
*/
const getTemplate = (schema, str) => {
return str.replace(/(\$\$TinyEngine{(.*)}END\$)/g, function (match, p1, p2) {
if (!p2) {
return ''
}
const keyArr = p2.split('.')
const value = keyArr.reduce((preVal, key) => preVal?.[key] ?? '', schema)
return value
})
}
/**
* get project template
* @returns
*/
export function generateTemplate(schema) {
return [
{
fileType: 'md',
fileName: 'README.md',
path: '.',
fileContent: getTemplate(schema, readmeFile)
},
{
fileType: 'js',
fileName: 'vite.config.js',
path: '.',
fileContent: genViteConfig(schema)
},
{
fileType: 'json',
fileName: 'package.json',
path: '.',
fileContent: getPackageJson(schema)
},
{
fileName: '.gitignore',
path: '.',
fileContent: getTemplate(schema, gitIgnoreFile)
},
{
fileType: 'html',
fileName: 'index.html',
path: '.',
fileContent: getTemplate(schema, entryHTMLFile)
},
{
fileType: 'js',
fileName: 'main.js',
path: './src',
fileContent: getTemplate(schema, mainJSFile)
},
{
fileType: 'vue',
fileName: 'App.vue',
path: './src',
fileContent: getTemplate(schema, appVueFile)
},
{
fileType: 'js',
fileName: 'bridge.js',
path: './src/lowcodeConfig',
fileContent: bridgeFile
},
{
fileType: 'js',
fileName: 'dataSource.js',
path: './src/lowcodeConfig',
fileContent: dataSourceFile
},
{
fileType: 'js',
fileName: 'lowcode.js',
path: './src/lowcodeConfig',
fileContent: lowcodeJSFile
},
{
fileType: 'js',
fileName: 'store.js',
path: './src/lowcodeConfig',
fileContent: lowcodeStoreFile
},
{
fileType: 'js',
fileName: 'axios.js',
path: './src/http',
fileContent: axiosFile
},
{
fileType: 'js',
fileName: 'config.js',
path: './src/http',
fileContent: axiosConfigFile
},
{
fileType: 'js',
fileName: 'index.js',
path: './src/http',
fileContent: httpEntryFile
}
]
}

View File

@ -0,0 +1,13 @@
node_modules
dist/
# local env files
.env.local
.env.*.local
# Editor directories and files
.vscode
.idea
yarn.lock
package-lock.json

View File

@ -0,0 +1,19 @@
## $$TinyEngine{meta.name}END$
本工程是使用 TinyEngine 低代码引擎搭建之后得到的出码工程。
## 使用
安装依赖:
```bash
npm install
```
本地启动项目:
```bash
npm run dev
```

View File

@ -0,0 +1,31 @@
export default () => {
// 避免在构建的时候,被 process. env 替换
const processStr = ['process', 'env']
const res = `
import { defineConfig } from 'vite'
import path from 'path'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
plugins: [vue(), vueJsx()],
define: {
'${processStr.join('.')}': { ...${processStr.join('.')} }
},
build: {
minify: true,
commonjsOptions: {
transformMixedEsModules: true
},
cssCodeSplit: false
}
})`
return res
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>$$TinyEngine{meta.name}END$</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,34 @@
// 这里 package.json 格式设置为 js避免被识别成一个 package
export default (schema) => {
const packageName = schema?.meta?.name || '@opentiny/tiny-engine-preview-vue'
const res = {
name: packageName,
version: '1.0.0',
scripts: {
dev: 'vite',
build: 'vite build',
preview: 'vite preview'
},
main: 'dist/index.js',
module: 'dist/index.js',
dependencies: {
'@opentiny/tiny-engine-i18n-host': '^1.0.0',
'@opentiny/vue': '^3.10.0',
'@opentiny/vue-icon': '^3.10.0',
axios: '^0.21.1',
'axios-mock-adapter': '^1.19.0',
vue: '^3.3.9',
'vue-i18n': '^9.2.0-beta.3',
'vue-router': '^4.2.5',
pinia: '^2.1.7'
},
devDependencies: {
'@vitejs/plugin-vue': '^4.5.1',
'@vitejs/plugin-vue-jsx': '^3.1.0',
vite: '^4.3.7'
}
}
return JSON.stringify(res)
}

View File

@ -0,0 +1,11 @@
<template>
<router-view></router-view>
</template>
<script setup>
import { I18nInjectionKey } from 'vue-i18n'
import { provide } from 'vue'
import i18n from './i18n'
provide(I18nInjectionKey, i18n)
</script>

View File

@ -0,0 +1,139 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'
export default (config) => {
const instance = axios.create(config)
const defaults = {}
let mock
if (typeof MockAdapter.prototype.proxy === 'undefined') {
MockAdapter.prototype.proxy = function ({ url, config = {}, proxy, response, handleData } = {}) {
let stream = this
const request = (proxy, any) => {
return (setting) => {
return new Promise((resolve) => {
config.responseType = 'json'
axios
.get(any ? proxy + setting.url + '.json' : proxy, config)
.then(({ data }) => {
/* eslint-disable no-useless-call */
typeof handleData === 'function' && (data = handleData.call(null, data, setting))
resolve([200, data])
})
.catch((error) => {
resolve([error.response.status, error.response.data])
})
})
}
}
if (url === '*' && proxy && typeof proxy === 'string') {
stream = proxy === '*' ? this.onAny().passThrough() : this.onAny().reply(request(proxy, true))
} else {
if (proxy && typeof proxy === 'string') {
stream = this.onAny(url).reply(request(proxy))
} else if (typeof response === 'function') {
stream = this.onAny(url).reply(response)
}
}
return stream
}
}
return {
request(config) {
return instance(config)
},
get(url, config) {
return instance.get(url, config)
},
delete(url, config) {
return instance.delete(url, config)
},
head(url, config) {
return instance.head(url, config)
},
post(url, data, config) {
return instance.post(url, data, config)
},
put(url, data, config) {
return instance.put(url, data, config)
},
patch(url, data, config) {
return instance.patch(url, data, config)
},
all(iterable) {
return axios.all(iterable)
},
spread(callback) {
return axios.spread(callback)
},
defaults(key, value) {
if (key && typeof key === 'string') {
if (typeof value === 'undefined') {
return instance.defaults[key]
}
instance.defaults[key] = value
defaults[key] = value
} else {
return instance.defaults
}
},
defaultSettings() {
return defaults
},
interceptors: {
request: {
use(fnHandle, fnError) {
return instance.interceptors.request.use(fnHandle, fnError)
},
eject(id) {
return instance.interceptors.request.eject(id)
}
},
response: {
use(fnHandle, fnError) {
return instance.interceptors.response.use(fnHandle, fnError)
},
eject(id) {
return instance.interceptors.response.eject(id)
}
}
},
mock(config) {
if (!mock) {
mock = new MockAdapter(instance)
}
if (Array.isArray(config)) {
config.forEach((item) => {
mock.proxy(item)
})
}
return mock
},
disableMock() {
mock && mock.restore()
mock = undefined
},
isMock() {
return typeof mock !== 'undefined'
},
CancelToken: axios.CancelToken,
isCancel: axios.isCancel
}
}

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
export default {
withCredentials: false
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import axios from './axios'
import config from './config'
export default (dataHandler) => {
const http = axios(config)
http.interceptors.response.use(dataHandler, (error) => {
const response = error.response
if (response.status === 403 && response.headers && response.headers['x-login-url']) {
// TODO 处理无权限时,重新登录再发送请求
}
})
return http
}

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
export default () => {}

View File

@ -0,0 +1,102 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import useHttp from '../http'
import dataSources from './dataSource.json'
const dataSourceMap = {}
// 暂时使用 eval 解析 JSON 数据里的函数
const createFn = (fnContent) => {
return (...args) => {
// eslint-disable-next-line no-eval
window.eval('var fn = ' + fnContent)
// eslint-disable-next-line no-undef
return fn.apply(this, args)
}
}
const globalDataHandle = dataSources.dataHandler ? createFn(dataSources.dataHandler.value) : (res) => res
const load = (http, options, dataSource, shouldFetch) => (params, customUrl) => {
// 如果没有配置远程请求,则直接返回静态数据,返回前可能会有全局数据处理
if (!options) {
return globalDataHandle(dataSource.config.data)
}
if (!shouldFetch()) {
return
}
dataSource.status = 'loading'
const { method, uri: url, params: defaultParams, timeout, headers } = options
const config = { method, url, headers, timeout }
const data = params || defaultParams
config.url = customUrl || config.url
if (method.toLowerCase() === 'get') {
config.params = data
} else {
config.data = data
}
return http.request(config)
}
dataSources.list.forEach((config) => {
const http = useHttp(globalDataHandle)
const dataSource = { config }
dataSourceMap[config.name] = dataSource
const shouldFetch = config.shouldFetch?.value ? createFn(config.shouldFetch.value) : () => true
const willFetch = config.willFetch?.value ? createFn(config.willFetch.value) : (options) => options
const dataHandler = (res) => {
const data = config.dataHandler?.value ? createFn(config.dataHandler.value)(res) : res
dataSource.status = 'loaded'
dataSource.data = data
return data
}
const errorHandler = (error) => {
config.errorHandler?.value && createFn(config.errorHandler.value)(error)
dataSource.status = 'error'
dataSource.error = error
}
http.interceptors.request.use(willFetch, errorHandler)
http.interceptors.response.use(dataHandler, errorHandler)
if (import.meta.env.VITE_APP_MOCK === 'mock') {
http.mock([
{
url: config.options?.uri,
response() {
return Promise.resolve([200, { data: config.data }])
}
},
{
url: '*',
proxy: '*'
}
])
}
dataSource.status = 'init'
dataSource.load = load(http, config.options, dataSource, shouldFetch)
})
export default dataSourceMap

View File

@ -0,0 +1,86 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { getCurrentInstance, nextTick, provide, inject } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { I18nInjectionKey } from 'vue-i18n'
import dataSourceMap from './dataSource'
import * as utils from '../utils'
import * as bridge from './bridge'
import { useStores } from './store'
export const lowcodeWrap = (props, context) => {
const global = {}
const instance = getCurrentInstance()
const router = useRouter()
const route = useRoute()
const { t, locale } = inject(I18nInjectionKey).global
const emit = context.emit
const ref = (ref) => instance.refs[ref]
const setState = (newState, callback) => {
Object.assign(global.state, newState)
nextTick(() => callback.apply(global))
}
const getLocale = () => locale.value
const setLocale = (val) => {
locale.value = val
}
const location = () => window.location
const history = () => window.history
Object.defineProperties(global, {
props: { get: () => props },
emit: { get: () => emit },
setState: { get: () => setState },
router: { get: () => router },
route: { get: () => route },
i18n: { get: () => t },
getLocale: { get: () => getLocale },
setLocale: { get: () => setLocale },
location: { get: location },
history: { get: history },
utils: { get: () => utils },
bridge: { get: () => bridge },
dataSourceMap: { get: () => dataSourceMap },
$: { get: () => ref }
})
const wrap = (fn) => {
if (typeof fn === 'function') {
return (...args) => fn.apply(global, args)
}
Object.entries(fn).forEach(([name, value]) => {
Object.defineProperty(global, name, {
get: () => value
})
})
fn.t = t
return fn
}
return wrap
}
export default () => {
const i18n = inject(I18nInjectionKey)
provide(I18nInjectionKey, i18n)
const stores = useStores()
return { t: i18n.global.t, lowcodeWrap, stores }
}

View File

@ -0,0 +1,13 @@
import * as useDefinedStores from '@/stores'
const useStores = () => {
const stores = {}
Object.values({ ...useDefinedStores }).forEach((store) => {
stores[store.$id] = store()
})
return stores
}
export { useStores }

View File

@ -10,16 +10,11 @@
*
*/
import { defineConfig } from 'vite'
import path from 'path'
import { createApp } from 'vue'
import router from './router'
import { createPinia } from 'pinia'
import App from './App.vue'
// https://vitejs.dev/config/
export default defineConfig({
build: {
lib: {
entry: path.resolve(__dirname, './src/index.js'),
formats: ['cjs']
},
sourcemap: true
}
})
const pinia = createPinia()
createApp(App).use(pinia).use(router).mount('#app')

View File

@ -0,0 +1,24 @@
import prettier from 'prettier'
import parserHtml from 'prettier/parser-html'
import parseCss from 'prettier/parser-postcss'
import parserBabel from 'prettier/parser-babel'
const defaultOption = {
singleQuote: true,
printWidth: 120,
semi: false,
trailingComma: 'none'
}
export const formatCode = (content, parser, options = {}) => {
if (!content || typeof content !== 'string') {
return content
}
return prettier.format(content, {
parser,
plugins: [parserBabel, parseCss, parserHtml],
...defaultOption,
...options
})
}

View File

@ -0,0 +1,52 @@
// TODO: 支持4种 import 的形式
export function generateImportStatement(config) {
const { moduleName, exportName, alias, destructuring } = config
let statementName = `${exportName}`
if (alias && alias !== exportName) {
statementName = `${exportName} as ${alias}`
}
if (destructuring) {
statementName = `{ ${statementName} }`
}
return `import ${statementName} from '${moduleName}'`
}
export function generateImportByPkgName(config) {
const { pkgName, imports } = config
const importStatements = imports
.filter(({ destructuring }) => destructuring)
.map(({ componentName, exportName }) => {
if (componentName === exportName) {
return componentName
}
return `${exportName} as ${componentName}`
})
// 默认导出如果存在,应该只有一个
let defaultImports = imports.find(({ destructuring }) => !destructuring)
let defaultImportStatement = ''
if (defaultImports) {
const { componentName, exportName } = defaultImports
if (exportName && exportName !== componentName) {
defaultImportStatement = `${exportName} as ${componentName}`
} else {
defaultImportStatement = `${exportName || componentName || ''}`
}
defaultImportStatement = `import ${defaultImportStatement} from "${pkgName}"\n`
}
if (!importStatements.length && defaultImportStatement) {
return defaultImportStatement
}
return `${defaultImportStatement}import { ${importStatements.join(',')} } from "${pkgName}"`
}

View File

@ -0,0 +1,25 @@
import { parse } from '@babel/parser'
import traverse from '@babel/traverse'
export function hasJsx(code) {
try {
const ast = parse(code, { plugins: ['jsx'] })
let res = false
traverse(ast, {
JSXElement(path) {
res = true
path.stop()
},
JSXFragment(path) {
res = true
path.stop()
}
})
return res
} catch (error) {
// 解析失败则认为不存在 jsx
return false
}
}

View File

@ -37,10 +37,10 @@ const getFunctionInfo = (fnStr) => {
const safeRandom = () => {
const mathConstructor = Math
return mathConstructor.random
return mathConstructor.random()
}
const randomString = (length = 4, chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') => {
export const randomString = (length = 4, chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') => {
let result = ''
for (let i = length; i > 0; --i) {
result += chars[Math.floor(safeRandom() * chars.length)]
@ -78,6 +78,9 @@ const prettierOpts = {
const onRE = /^on([A-Z]\w*)/
const onUpdateRE = /^on(Update:\w+)/
export const thisBindRe = /this\.(props\.)?/g
export const thisPropsBindRe = /this\.(props\.)?/g
export const thisRegexp = /this\./g
const isOn = (key) => onRE.test(key)
const isOnUpdate = (key) => onUpdateRE.test(key)
@ -92,9 +95,9 @@ const toEventKey = (str) => {
return hyphenate(strRemovedPrefix)
}
const isGetter = (accessor) => accessor?.getter?.type === JS_FUNCTION
const isSetter = (accessor) => accessor?.setter?.type === JS_FUNCTION
const hasAccessor = (accessor) => isGetter(accessor) || isSetter(accessor)
export const isGetter = (accessor) => accessor?.getter?.type === JS_FUNCTION
export const isSetter = (accessor) => accessor?.setter?.type === JS_FUNCTION
export const hasAccessor = (accessor) => isGetter(accessor) || isSetter(accessor)
const addAccessorRecord = (accessor, record) => {
if (isGetter(accessor)) {
@ -145,14 +148,12 @@ export {
getTypeOfSchema,
getFunctionInfo,
safeRandom,
randomString,
avoidDuplicateString,
lowerFirst,
toPascalCase,
prettierOpts,
isOn,
toEventKey,
hasAccessor,
addAccessorRecord,
addIconRecord,
handleIconInProps

View File

@ -0,0 +1,29 @@
function isObject(target) {
return Object.prototype.toString.call(target) === '[object Object]'
}
export const mergeOptions = (originOptions, newOptions) => {
if (!isObject(originOptions) || !isObject(newOptions)) {
return originOptions
}
const res = {}
for (const [key, value] of Object.entries(originOptions)) {
if (!Object.prototype.hasOwnProperty.call(newOptions, key)) {
res[key] = value
}
if (isObject(value) && isObject(newOptions[key])) {
res[key] = mergeOptions(value, newOptions[key])
}
}
for (const [key, value] of Object.entries(newOptions)) {
if (!Object.prototype.hasOwnProperty.call(res, key)) {
res[key] = value
}
}
return res
}

View File

@ -0,0 +1,18 @@
export const parseRequiredBlocks = (schema) => {
const res = []
if (!Array.isArray(schema?.children)) {
return res
}
for (const item of schema.children) {
if (item.componentType === 'Block') {
res.push(item.componentName)
}
if (Array.isArray(item.children)) {
res.push(...parseRequiredBlocks(item))
}
}
return res
}

View File

@ -14,7 +14,7 @@ const path = require('path')
const fs = require('fs-extra')
const prettier = require('prettier')
const { execSync } = require('child_process')
const { generateCode } = require('../../../dist/tiny-engine-dsl-vue.cjs')
const { generateCode } = require('../../../dist/tiny-engine-dsl-vue.js')
const { logger } = require('../../utils/logger')
const getPageData = (testCaseFile) => {

View File

@ -0,0 +1,13 @@
node_modules
dist/
# local env files
.env.local
.env.*.local
# Editor directories and files
.vscode
.idea
yarn.lock
package-lock.json

View File

@ -0,0 +1,19 @@
## portal-app
本工程是使用 TinyEngine 低代码引擎搭建之后得到的出码工程。
## 使用
安装依赖:
```bash
npm install
```
本地启动项目:
```bash
npm run dev
```

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>portal-app</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,27 @@
{
"name": "portal-app",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"main": "dist/index.js",
"module": "dist/index.js",
"dependencies": {
"@opentiny/tiny-engine-i18n-host": "^1.0.0",
"@opentiny/vue": "latest",
"@opentiny/vue-icon": "latest",
"axios": "latest",
"axios-mock-adapter": "^1.19.0",
"vue": "^3.3.9",
"vue-i18n": "^9.2.0-beta.3",
"vue-router": "^4.2.5",
"pinia": "^2.1.7"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.1",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"vite": "^4.3.7"
}
}

View File

@ -0,0 +1,11 @@
<template>
<router-view></router-view>
</template>
<script setup>
import { I18nInjectionKey } from 'vue-i18n'
import { provide } from 'vue'
import i18n from './i18n'
provide(I18nInjectionKey, i18n)
</script>

View File

@ -0,0 +1,139 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'
export default (config) => {
const instance = axios.create(config)
const defaults = {}
let mock
if (typeof MockAdapter.prototype.proxy === 'undefined') {
MockAdapter.prototype.proxy = function ({ url, config = {}, proxy, response, handleData } = {}) {
let stream = this
const request = (proxy, any) => {
return (setting) => {
return new Promise((resolve) => {
config.responseType = 'json'
axios
.get(any ? proxy + setting.url + '.json' : proxy, config)
.then(({ data }) => {
/* eslint-disable no-useless-call */
typeof handleData === 'function' && (data = handleData.call(null, data, setting))
resolve([200, data])
})
.catch((error) => {
resolve([error.response.status, error.response.data])
})
})
}
}
if (url === '*' && proxy && typeof proxy === 'string') {
stream = proxy === '*' ? this.onAny().passThrough() : this.onAny().reply(request(proxy, true))
} else {
if (proxy && typeof proxy === 'string') {
stream = this.onAny(url).reply(request(proxy))
} else if (typeof response === 'function') {
stream = this.onAny(url).reply(response)
}
}
return stream
}
}
return {
request(config) {
return instance(config)
},
get(url, config) {
return instance.get(url, config)
},
delete(url, config) {
return instance.delete(url, config)
},
head(url, config) {
return instance.head(url, config)
},
post(url, data, config) {
return instance.post(url, data, config)
},
put(url, data, config) {
return instance.put(url, data, config)
},
patch(url, data, config) {
return instance.patch(url, data, config)
},
all(iterable) {
return axios.all(iterable)
},
spread(callback) {
return axios.spread(callback)
},
defaults(key, value) {
if (key && typeof key === 'string') {
if (typeof value === 'undefined') {
return instance.defaults[key]
}
instance.defaults[key] = value
defaults[key] = value
} else {
return instance.defaults
}
},
defaultSettings() {
return defaults
},
interceptors: {
request: {
use(fnHandle, fnError) {
return instance.interceptors.request.use(fnHandle, fnError)
},
eject(id) {
return instance.interceptors.request.eject(id)
}
},
response: {
use(fnHandle, fnError) {
return instance.interceptors.response.use(fnHandle, fnError)
},
eject(id) {
return instance.interceptors.response.eject(id)
}
}
},
mock(config) {
if (!mock) {
mock = new MockAdapter(instance)
}
if (Array.isArray(config)) {
config.forEach((item) => {
mock.proxy(item)
})
}
return mock
},
disableMock() {
mock && mock.restore()
mock = undefined
},
isMock() {
return typeof mock !== 'undefined'
},
CancelToken: axios.CancelToken,
isCancel: axios.isCancel
}
}

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
export default {
withCredentials: false
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import axios from './axios'
import config from './config'
export default (dataHandler) => {
const http = axios(config)
http.interceptors.response.use(dataHandler, (error) => {
const response = error.response
if (response.status === 403 && response.headers && response.headers['x-login-url']) {
// TODO 处理无权限时,重新登录再发送请求
}
})
return http
}

View File

@ -0,0 +1,25 @@
{
"lowcode.c257d5e8": "search",
"lowcode.61c8ac8c": "dsdsa",
"lowcode.f53187a0": "test",
"lowcode.97ad00dd": "createMaterial",
"lowcode.61dcef52": "sadasda",
"lowcode.45f4c42a": "gfdgfd",
"lowcode.c6f5a652": "fsdafds",
"lowcode.34923432": "fdsafds",
"lowcode.6534943e": "fdsafdsa",
"lowcode.44252642": "aaaa",
"lowcode.2a743651": "fdsaf",
"lowcode.24315357": "fsdafds",
"lowcode.44621691": "sd",
"lowcode.65636226": "fdsfsd",
"lowcode.6426a4e2": "fdsafsd",
"lowcode.e41c6636": "aa",
"lowcode.51c23164": "aa",
"lowcode.17245b46": "aa",
"lowcode.4573143c": "a",
"lowcode.56432442": "aa",
"lowcode.33566643": "aa",
"lowcode.565128f3": "aa",
"lowcode.56643835": "aa"
}

View File

@ -0,0 +1,9 @@
import i18n from '@opentiny/tiny-engine-i18n-host'
import lowcode from '../lowcodeConfig/lowcode'
import locale from './locale.js'
i18n.lowcode = lowcode
i18n.global.mergeLocaleMessage('en_US', locale.en_US)
i18n.global.mergeLocaleMessage('zh_CN', locale.zh_CN)
export default i18n

View File

@ -0,0 +1,4 @@
import en_US from './en_US.json'
import zh_CN from './zh_CN.json'
export default { en_US, zh_CN }

View File

@ -0,0 +1,26 @@
{
"lowcode.c257d5e8": "查询",
"lowcode.61c8ac8c": "地方",
"lowcode.f53187a0": "测试",
"lowcode.97ad00dd": "创建物料资产包",
"lowcode.61dcef52": "terterere",
"lowcode.45f4c42a": "gdfgdf",
"lowcode.c6f5a652": "fsdaf",
"lowcode.34923432": "fdsafdsa",
"lowcode.48521e45": "fdsfds",
"lowcode.6534943e": "fdsafds",
"lowcode.44252642": "fdsafds",
"lowcode.2a743651": "sda",
"lowcode.24315357": "fdsafds",
"lowcode.44621691": "fdsafsd",
"lowcode.65636226": "fdsaf",
"lowcode.6426a4e2": "sd",
"lowcode.e41c6636": "aa",
"lowcode.51c23164": "aa",
"lowcode.17245b46": "aa",
"lowcode.4573143c": "aa",
"lowcode.56432442": "aa",
"lowcode.33566643": "aa",
"lowcode.565128f3": "aa",
"lowcode.56643835": "aa"
}

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
export default () => {}

View File

@ -0,0 +1,102 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import useHttp from '../http'
import dataSources from './dataSource.json'
const dataSourceMap = {}
// 暂时使用 eval 解析 JSON 数据里的函数
const createFn = (fnContent) => {
return (...args) => {
// eslint-disable-next-line no-eval
window.eval('var fn = ' + fnContent)
// eslint-disable-next-line no-undef
return fn.apply(this, args)
}
}
const globalDataHandle = dataSources.dataHandler ? createFn(dataSources.dataHandler.value) : (res) => res
const load = (http, options, dataSource, shouldFetch) => (params, customUrl) => {
// 如果没有配置远程请求,则直接返回静态数据,返回前可能会有全局数据处理
if (!options) {
return globalDataHandle(dataSource.config.data)
}
if (!shouldFetch()) {
return
}
dataSource.status = 'loading'
const { method, uri: url, params: defaultParams, timeout, headers } = options
const config = { method, url, headers, timeout }
const data = params || defaultParams
config.url = customUrl || config.url
if (method.toLowerCase() === 'get') {
config.params = data
} else {
config.data = data
}
return http.request(config)
}
dataSources.list.forEach((config) => {
const http = useHttp(globalDataHandle)
const dataSource = { config }
dataSourceMap[config.name] = dataSource
const shouldFetch = config.shouldFetch?.value ? createFn(config.shouldFetch.value) : () => true
const willFetch = config.willFetch?.value ? createFn(config.willFetch.value) : (options) => options
const dataHandler = (res) => {
const data = config.dataHandler?.value ? createFn(config.dataHandler.value)(res) : res
dataSource.status = 'loaded'
dataSource.data = data
return data
}
const errorHandler = (error) => {
config.errorHandler?.value && createFn(config.errorHandler.value)(error)
dataSource.status = 'error'
dataSource.error = error
}
http.interceptors.request.use(willFetch, errorHandler)
http.interceptors.response.use(dataHandler, errorHandler)
if (import.meta.env.VITE_APP_MOCK === 'mock') {
http.mock([
{
url: config.options?.uri,
response() {
return Promise.resolve([200, { data: config.data }])
}
},
{
url: '*',
proxy: '*'
}
])
}
dataSource.status = 'init'
dataSource.load = load(http, config.options, dataSource, shouldFetch)
})
export default dataSourceMap

View File

@ -0,0 +1,632 @@
{
"list": [
{
"id": 132,
"name": "getAllComponent",
"data": [],
"type": "array"
},
{
"id": 133,
"name": "getAllList",
"columns": [
{
"name": "test",
"title": "测试",
"field": "test",
"type": "string",
"format": {}
},
{
"name": "test1",
"title": "测试1",
"field": "test1",
"type": "string",
"format": {}
}
],
"type": "array",
"data": [
{
"test": "test1",
"test1": "test1",
"_id": "341efc48"
},
{
"test": "test2",
"test1": "test1",
"_id": "b86b516c"
},
{
"test": "test3",
"test1": "test1",
"_id": "f680cd78"
}
],
"options": {
"uri": "",
"method": "GET"
},
"dataHandler": {
"type": "JSFunction",
"value": "function dataHandler(data) { \n return data \n}"
},
"willFetch": {
"type": "JSFunction",
"value": "function willFetch(option) {\n return option \n}"
},
"shouldFetch": {
"type": "JSFunction",
"value": "function shouldFetch(option) {\n return true \n}"
},
"errorHandler": {
"type": "JSFunction",
"value": "function errorHandler(err) {}"
}
},
{
"id": 135,
"name": "getAllMaterialList",
"columns": [
{
"name": "id",
"title": "id",
"field": "id",
"type": "string",
"format": {}
},
{
"name": "name",
"title": "name",
"field": "name",
"type": "string",
"format": {}
},
{
"name": "framework",
"title": "framework",
"field": "framework",
"type": "string",
"format": {
"required": true
}
},
{
"name": "components",
"title": "components",
"field": "components",
"type": "string",
"format": {}
},
{
"name": "content",
"title": "content",
"field": "content",
"type": "string",
"format": {}
},
{
"name": "url",
"title": "url",
"field": "url",
"type": "string",
"format": {}
},
{
"name": "published_at",
"title": "published_at",
"field": "published_at",
"type": "string",
"format": {}
},
{
"name": "created_at",
"title": "created_at",
"field": "created_at",
"type": "string",
"format": {}
},
{
"name": "updated_at",
"title": "updated_at",
"field": "updated_at",
"type": "string",
"format": {}
},
{
"name": "published",
"title": "published",
"field": "published",
"type": "string",
"format": {}
},
{
"name": "last_build_info",
"title": "last_build_info",
"field": "last_build_info",
"type": "string",
"format": {}
},
{
"name": "tenant",
"title": "tenant",
"field": "tenant",
"type": "string",
"format": {}
},
{
"name": "version",
"title": "version",
"field": "version",
"type": "string",
"format": {}
},
{
"name": "description",
"title": "description",
"field": "description",
"type": "string",
"format": {}
}
],
"type": "array",
"data": [
{
"id": "f37123ec",
"url": "",
"name": "ng-material",
"tenant": "",
"content": "",
"version": "1.0.0",
"framework": "Angular",
"published": "",
"components": "",
"created_at": "2021-11-02T11:32:22.000Z",
"updated_at": "2021-11-02T11:32:22.000Z",
"description": "angular组件库物料",
"published_at": "2021-11-02T11:32:22.000Z",
"last_build_info": "",
"_id": "2a23e653"
},
{
"id": "f37123ec",
"url": "",
"name": "ng-material",
"tenant": "",
"content": "",
"version": "1.0.0",
"framework": "Angular",
"published": "",
"components": "",
"created_at": "2021-11-02T11:32:22.000Z",
"updated_at": "2021-11-02T11:32:22.000Z",
"description": "angular组件库物料",
"published_at": "2021-11-02T11:32:22.000Z",
"last_build_info": "",
"_id": "06b253be"
},
{
"id": "f37123ec",
"url": "",
"name": "ng-material",
"tenant": "",
"content": "",
"version": "1.0.0",
"framework": "Angular",
"published": "",
"components": "",
"created_at": "2021-11-02T11:32:22.000Z",
"updated_at": "2021-11-02T11:32:22.000Z",
"description": "angular组件库物料",
"published_at": "2021-11-02T11:32:22.000Z",
"last_build_info": "",
"_id": "c55a41ed"
},
{
"id": "f37123ec",
"url": "",
"name": "ng-material",
"tenant": "",
"content": "",
"version": "1.0.0",
"framework": "Angular",
"published": "",
"components": "",
"created_at": "2021-11-02T11:32:22.000Z",
"updated_at": "2021-11-02T11:32:22.000Z",
"description": "angular组件库物料",
"published_at": "2021-11-02T11:32:22.000Z",
"last_build_info": "",
"_id": "f37123ec"
},
{
"id": "7a63c1a2",
"url": "",
"name": "tiny-vue",
"tenant": "",
"content": "Tiny Vue物料",
"version": "1.0.0",
"framework": "Vue",
"published": "",
"components": "",
"created_at": "",
"updated_at": "",
"description": "Tiny Vue物料",
"published_at": "",
"last_build_info": "",
"_id": "7a63c1a2"
}
],
"options": {
"uri": "",
"method": "GET"
},
"willFetch": {
"type": "JSFunction",
"value": "function willFetch(option) {\n return option \n}"
},
"dataHandler": {
"type": "JSFunction",
"value": "function dataHandler(data) { \n return data \n}"
},
"shouldFetch": {
"type": "JSFunction",
"value": "function shouldFetch(option) {\n return true \n}"
},
"errorHandler": {
"type": "JSFunction",
"value": "function errorHandler(err) {}"
}
},
{
"id": 139,
"name": "treedata",
"data": [
{
"label": "level111",
"value": "111",
"id": "f6609643",
"pid": "",
"_RID": "row_4"
},
{
"label": "level1-son",
"value": "111-1",
"id": "af1f937f",
"pid": "f6609643",
"_RID": "row_5"
},
{
"label": "level222",
"value": "222",
"id": "28e3709c",
"pid": "",
"_RID": "row_6"
},
{
"label": "level2-son",
"value": "222-1",
"id": "6b571bef",
"pid": "28e3709c",
"_RID": "row_5"
},
{
"id": "6317c2cc",
"pid": "fdfa",
"label": "fsdfaa",
"value": "fsadf",
"_RID": "row_6"
},
{
"id": "9cce369f",
"pid": "test",
"label": "test1",
"value": "001"
}
],
"type": "tree"
},
{
"id": 150,
"name": "componentList",
"data": [
{
"_RID": "row_1",
"name": "表单",
"isSelected": "true",
"description": "由按钮、输入框、选择器、单选框、多选框等控件组成,用以收集、校验、提交数据"
},
{
"name": "按钮",
"isSelected": "false",
"description": "常用的操作按钮,提供包括默认按钮、图标按钮、图片按钮、下拉按钮等类型"
},
{
"id": "490f8a00",
"_RID": "row_3",
"name": "表单项",
"framework": "",
"materials": "",
"description": "Form 组件下的 FormItem 配置"
},
{
"id": "c259b8b3",
"_RID": "row_4",
"name": "开关",
"framework": "",
"materials": "",
"description": "关闭或打开"
},
{
"id": "083ed9c7",
"_RID": "row_5",
"name": "互斥按钮组",
"framework": "",
"materials": "",
"description": "以按钮组的方式出现,常用于多项类似操作"
},
{
"id": "09136cea",
"_RID": "row_6",
"name": "提示框",
"framework": "",
"materials": "",
"description": "Popover可通过对一个触发源操作触发弹出框,支持自定义弹出内容,延迟触发和渐变动画"
},
{
"id": "a63b57d5",
"_RID": "row_7",
"name": "文字提示框",
"framework": "",
"materials": "",
"description": "动态显示提示信息,一般通过鼠标事件进行响应;提供 warning、error、info、success 四种类型显示不同类别的信"
},
{
"id": "a0f6e8a3",
"_RID": "row_8",
"name": "树",
"framework": "",
"materials": "",
"description": "可进行展示有父子层级的数据,支持选择,异步加载等功能。但不推荐用它来展示菜单,展示菜单推荐使用树菜单"
},
{
"id": "d1aa18fc",
"_RID": "row_9",
"name": "分页",
"framework": "",
"materials": "",
"description": "当数据量过多时,使用分页分解数据,常用于 Grid 和 Repeater 组件"
},
{
"id": "ca49cc52",
"_RID": "row_10",
"name": "表格",
"framework": "",
"materials": "",
"description": "提供了非常强大数据表格功能,可以展示数据列表,可以对数据列表进行选择、编辑等"
},
{
"id": "4e20ecc9",
"name": "搜索框",
"framework": "",
"materials": "",
"description": "指定条件对象进行搜索数据"
},
{
"id": "6b093ee5",
"name": "折叠面板",
"framework": "",
"materials": "",
"description": "内容区可指定动态页面或自定义 html 等,支持展开收起操作"
},
{
"id": "0a09abc0",
"name": "对话框",
"framework": "",
"materials": "",
"description": "模态对话框,在浮层中显示,引导用户进行相关操作"
},
{
"id": "f814b901",
"name": "标签页签项",
"framework": "",
"materials": "",
"description": "tab页签"
},
{
"id": "c5ae797c",
"name": "单选",
"framework": "",
"materials": "",
"description": "用于配置不同场景的选项,在一组备选项中进行单选"
},
{
"id": "33d0c590",
"_RID": "row_13",
"name": "弹出编辑",
"framework": "",
"materials": "",
"description": "该组件只能在弹出的面板中选择数据,不能手动输入数据;弹出面板中显示为 Tree 组件或者 Grid 组件"
},
{
"id": "16711dfa",
"_RID": "row_14",
"name": "下拉框",
"framework": "",
"materials": "",
"description": "Select 选择器是一种通过点击弹出下拉列表展示数据并进行选择的 UI 组件"
},
{
"id": "a9fd190a",
"_RID": "row_15",
"name": "折叠面板项",
"framework": "",
"materials": "",
"description": "内容区可指定动态页面或自定义 html 等,支持展开收起操作"
},
{
"id": "a7dfa9ec",
"_RID": "row_16",
"name": "复选框",
"framework": "",
"materials": "",
"description": "用于配置不同场景的选项,提供用户可在一组选项中进行多选"
},
{
"id": "d4bb8330",
"name": "输入框",
"framework": "",
"materials": "",
"description": "通过鼠标或键盘输入字符"
},
{
"id": "ced3dc83",
"name": "时间线",
"framework": "",
"materials": "",
"description": "时间线"
}
],
"type": "array",
"columns": [
{
"name": "name",
"type": "string",
"field": "name",
"title": "name",
"format": {
"max": 0,
"min": 0,
"dateTime": false,
"required": false,
"stringType": ""
}
},
{
"name": "description",
"type": "string",
"field": "description",
"title": "description",
"format": {
"max": 0,
"min": 0,
"dateTime": false,
"required": false,
"stringType": ""
}
},
{
"name": "isSelected",
"type": "string",
"field": "isSelected",
"title": "isSelected",
"format": {
"max": 0,
"min": 0,
"dateTime": false,
"required": false,
"stringType": ""
}
}
],
"options": {
"uri": "http://localhost:9090/assets/json/bundle.json",
"method": "GET"
},
"willFetch": {
"type": "JSFunction",
"value": "function willFetch(option) {\n return option \n}"
},
"dataHandler": {
"type": "JSFunction",
"value": "function dataHandler(data) { \n return data \n}"
},
"shouldFetch": {
"type": "JSFunction",
"value": "function shouldFetch(option) {\n return true \n}"
},
"errorHandler": {
"type": "JSFunction",
"value": "function errorHandler(err) {}"
}
},
{
"id": 151,
"name": "selectedComponents",
"columns": [
{
"name": "name",
"title": "name",
"field": "name",
"type": "string",
"format": {
"required": false,
"stringType": "",
"min": 0,
"max": 0,
"dateTime": false
}
},
{
"name": "description",
"title": "description",
"field": "description",
"type": "string",
"format": {
"required": false,
"stringType": "",
"min": 0,
"max": 0,
"dateTime": false
}
},
{
"name": "isSelected",
"title": "isSelected",
"field": "isSelected",
"type": "string",
"format": {
"required": false,
"stringType": "",
"min": 0,
"max": 0,
"dateTime": false
}
}
],
"type": "array",
"data": [
{
"name": "标签页",
"description": "分隔内容上有关联但属于不同类别的数据集合",
"isSelected": "true",
"_RID": "row_2"
},
{
"name": "布局列",
"description": "列配置信息",
"isSelected": "true",
"id": "76a7080a",
"_RID": "row_4"
},
{
"name": "日期选择器",
"description": "用于设置/选择日期,包括年月/年月日/年月日时分/年月日时分秒日期格式",
"isSelected": "true",
"id": "76b20d73",
"_RID": "row_1"
},
{
"name": "走马灯",
"description": "常用于一组图片或卡片轮播,当内容空间不足时,可以用走马灯的形式进行收纳,进行轮播展现",
"isSelected": "true",
"id": "4c884c3d"
}
]
}
],
"dataHandler": {
"type": "JSFunction",
"value": "function dataHanlder(res){\n return res;\n}"
}
}

View File

@ -0,0 +1,86 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { getCurrentInstance, nextTick, provide, inject } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { I18nInjectionKey } from 'vue-i18n'
import dataSourceMap from './dataSource'
import * as utils from '../utils'
import * as bridge from './bridge'
import { useStores } from './store'
export const lowcodeWrap = (props, context) => {
const global = {}
const instance = getCurrentInstance()
const router = useRouter()
const route = useRoute()
const { t, locale } = inject(I18nInjectionKey).global
const emit = context.emit
const ref = (ref) => instance.refs[ref]
const setState = (newState, callback) => {
Object.assign(global.state, newState)
nextTick(() => callback.apply(global))
}
const getLocale = () => locale.value
const setLocale = (val) => {
locale.value = val
}
const location = () => window.location
const history = () => window.history
Object.defineProperties(global, {
props: { get: () => props },
emit: { get: () => emit },
setState: { get: () => setState },
router: { get: () => router },
route: { get: () => route },
i18n: { get: () => t },
getLocale: { get: () => getLocale },
setLocale: { get: () => setLocale },
location: { get: location },
history: { get: history },
utils: { get: () => utils },
bridge: { get: () => bridge },
dataSourceMap: { get: () => dataSourceMap },
$: { get: () => ref }
})
const wrap = (fn) => {
if (typeof fn === 'function') {
return (...args) => fn.apply(global, args)
}
Object.entries(fn).forEach(([name, value]) => {
Object.defineProperty(global, name, {
get: () => value
})
})
fn.t = t
return fn
}
return wrap
}
export default () => {
const i18n = inject(I18nInjectionKey)
provide(I18nInjectionKey, i18n)
const stores = useStores()
return { t: i18n.global.t, lowcodeWrap, stores }
}

View File

@ -0,0 +1,13 @@
import * as useDefinedStores from '@/stores'
const useStores = () => {
const stores = {}
Object.values({ ...useDefinedStores }).forEach((store) => {
stores[store.$id] = store()
})
return stores
}
export { useStores }

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { createApp } from 'vue'
import router from './router'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
createApp(App).use(pinia).use(router).mount('#app')

View File

@ -0,0 +1,11 @@
import { createRouter, createWebHashHistory } from 'vue-router'
const routes = [
{ path: '/', redirect: '/demopage' },
{ path: '/demopage', component: () => import('@/views/DemoPage.vue') },
{ path: '/createVm', component: () => import('@/views/createVm.vue') }
]
export default createRouter({
history: createWebHashHistory(),
routes
})

View File

@ -0,0 +1,13 @@
import axios from 'axios'
import { Button } from '@opentiny/vue'
import { NavMenu } from '@opentiny/vue'
import { Modal } from '@opentiny/vue'
import { Pager } from '@opentiny/vue'
const npm = ''
const test = function test() {
return 'test'
}
const util = function util() {
console.log(321)
}
export { axios, Button, NavMenu, Modal, npm, Pager, test, util }

View File

@ -0,0 +1,25 @@
<template>
<div>
<div>
<tiny-switch modelValue=""></tiny-switch>
</div>
</div>
</template>
<script setup>
import { Switch as TinySwitch } from '@opentiny/vue'
import * as vue from 'vue'
import { defineProps, defineEmits } from 'vue'
import { I18nInjectionKey } from 'vue-i18n'
const props = defineProps({})
const emit = defineEmits([])
const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()
const wrap = lowcodeWrap(props, { emit })
wrap({ stores })
const state = vue.reactive({})
wrap({ state })
</script>
<style scoped></style>

View File

@ -0,0 +1,408 @@
<template>
<div>
<div style="padding-bottom: 10px; padding-top: 10px">
<tiny-time-line
active="2"
:horizontal="true"
style="border-radius: 0px"
:data="[{ name: '基础配置' }, { name: '网络配置' }, { name: '高级配置' }, { name: '确认配置' }]"
></tiny-time-line>
</div>
<div
style="
border-width: 1px;
border-style: solid;
border-radius: 4px;
border-color: #fff;
padding-top: 10px;
padding-bottom: 10px;
padding-left: 10px;
padding-right: 10px;
box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px;
background-color: #fff;
margin-bottom: 10px;
"
>
<tiny-form
labelWidth="80px"
labelPosition="top"
:inline="false"
label-position="left "
label-width="150px"
style="border-radius: 0px"
>
<tiny-form-item label="计费模式">
<tiny-button-group
modelValue="1"
:data="[
{ text: '包年/包月', value: '1' },
{ text: '按需计费', value: '2' }
]"
></tiny-button-group
></tiny-form-item>
<tiny-form-item label="区域">
<tiny-button-group
modelValue="1"
style="border-radius: 0px; margin-right: 10px"
:data="[{ text: '乌兰察布二零一', value: '1' }]"
></tiny-button-group>
<span style="background-color: [object Event]; color: #8a8e99; font-size: 12px"
>温馨提示页面左上角切换区域</span
>
<span style="display: block; color: #8a8e99; border-radius: 0px; font-size: 12px"
>不同区域的云服务产品之间内网互不相通请就近选择靠近您业务的区域可减少网络时延提高访问速度</span
></tiny-form-item
>
<tiny-form-item label="可用区" style="border-radius: 0px">
<tiny-button-group
modelValue="1"
:data="[
{ text: '可用区1', value: '1' },
{ text: '可用区2', value: '2' },
{ text: '可用区3', value: '3' }
]"
></tiny-button-group></tiny-form-item
></tiny-form>
</div>
<div
style="
border-width: 1px;
border-style: solid;
border-radius: 4px;
border-color: #fff;
padding-top: 10px;
padding-bottom: 10px;
padding-left: 10px;
padding-right: 10px;
box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px;
background-color: #fff;
margin-bottom: 10px;
"
>
<tiny-form
labelWidth="80px"
labelPosition="top"
:inline="false"
label-position="left "
label-width="150px"
style="border-radius: 0px"
>
<tiny-form-item label="CPU架构">
<tiny-button-group
modelValue="1"
:data="[
{ text: 'x86计算', value: '1' },
{ text: '鲲鹏计算', value: '2' }
]"
></tiny-button-group
></tiny-form-item>
<tiny-form-item label="区域">
<div style="display: flex; justify-content: flex-start; align-items: center">
<div style="display: flex; align-items: center; margin-right: 10px">
<span style="width: 80px">vCPUs</span>
<tiny-select
modelValue=""
placeholder="请选择"
:options="[
{ value: '1', label: '黄金糕' },
{ value: '2', label: '双皮奶' }
]"
></tiny-select>
</div>
<div style="display: flex; align-items: center; margin-right: 10px">
<span style="width: 80px; border-radius: 0px">内存</span>
<tiny-select
modelValue=""
placeholder="请选择"
:options="[
{ value: '1', label: '黄金糕' },
{ value: '2', label: '双皮奶' }
]"
></tiny-select>
</div>
<div style="display: flex; align-items: center">
<span style="width: 80px">规格名称</span>
<tiny-search modelValue="" placeholder="输入关键词"></tiny-search>
</div>
</div>
<div style="border-radius: 0px">
<tiny-button-group
modelValue="1"
style="border-radius: 0px; margin-top: 12px"
:data="[
{ text: '通用计算型', value: '1' },
{ text: '通用计算增强型', value: '2' },
{ text: '内存优化型', value: '3' },
{ text: '内存优化型', value: '4' },
{ text: '磁盘增强型', value: '5' },
{ text: '超高I/O型', value: '6' },
{ text: 'GPU加速型', value: '7' }
]"
></tiny-button-group>
<tiny-grid
style="margin-top: 12px; border-radius: 0px"
:auto-resize="true"
:editConfig="{ trigger: 'click', mode: 'cell', showStatus: true }"
:columns="[
{ type: 'radio', width: 60 },
{ field: 'employees', title: '规格名称' },
{ field: 'created_date', title: 'vCPUs | 内存(GiB)', sortable: true },
{ field: 'city', title: 'CPU', sortable: true },
{ title: '基准 / 最大带宽 ', sortable: true },
{ title: '内网收发包', sortable: true }
]"
: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 style="margin-top: 12px; border-radius: 0px">
<span style="width: 150px; display: inline-block">当前规格</span>
<span style="font-weight: 700">通用计算型 | Si2.large.2 | 2vCPUs | 4 GiB</span>
</div>
</div></tiny-form-item
></tiny-form
>
</div>
<div
style="
border-width: 1px;
border-style: solid;
border-radius: 4px;
border-color: #fff;
padding-top: 10px;
padding-bottom: 10px;
padding-left: 10px;
padding-right: 10px;
box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px;
background-color: #fff;
margin-bottom: 10px;
"
>
<tiny-form
labelWidth="80px"
labelPosition="top"
:inline="false"
label-position="left "
label-width="150px"
style="border-radius: 0px"
>
<tiny-form-item label="镜像" style="border-radius: 0px">
<tiny-button-group
modelValue="1"
:data="[
{ text: '公共镜像', value: '1' },
{ text: '私有镜像', value: '2' },
{ text: '共享镜像', value: '3' }
]"
></tiny-button-group>
<div style="display: flex; margin-top: 12px; border-radius: 0px">
<tiny-select
modelValue=""
placeholder="请选择"
style="width: 170px; margin-right: 10px"
:options="[
{ value: '1', label: '黄金糕' },
{ value: '2', label: '双皮奶' }
]"
></tiny-select>
<tiny-select
modelValue=""
placeholder="请选择"
style="width: 340px"
:options="[
{ value: '1', label: '黄金糕' },
{ value: '2', label: '双皮奶' }
]"
></tiny-select>
</div>
<div style="margin-top: 12px">
<span style="color: #e37d29">请注意操作系统的语言类型</span>
</div></tiny-form-item
></tiny-form
>
</div>
<div
style="
border-width: 1px;
border-style: solid;
border-radius: 4px;
border-color: #fff;
padding-top: 10px;
padding-bottom: 10px;
padding-left: 10px;
padding-right: 10px;
box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px;
background-color: #fff;
margin-bottom: 10px;
"
>
<tiny-form
labelWidth="80px"
labelPosition="top"
:inline="false"
label-position="left "
label-width="150px"
style="border-radius: 0px"
>
<tiny-form-item label="系统盘" style="border-radius: 0px">
<div style="display: flex">
<tiny-select
modelValue=""
placeholder="请选择"
style="width: 200px; margin-right: 10px"
:options="[
{ value: '1', label: '黄金糕' },
{ value: '2', label: '双皮奶' }
]"
></tiny-select>
<tiny-input placeholder="请输入" modelValue="" style="width: 120px; margin-right: 10px"></tiny-input>
<span style="color: #575d6c; font-size: 12px">GiB IOPS上限240IOPS突发上限5,000</span>
</div></tiny-form-item
></tiny-form
>
<tiny-form
labelWidth="80px"
labelPosition="top"
:inline="false"
label-position="left "
label-width="150px"
style="border-radius: 0px"
>
<tiny-form-item label="数据盘" style="border-radius: 0px">
<div v-for="() in state.dataDisk" style="margin-top: 12px; display: flex">
<tiny-icon-panel-mini style="margin-right: 10px; width: 16px; height: 16px"></tiny-icon-panel-mini>
<tiny-select
modelValue=""
placeholder="请选择"
style="width: 200px; margin-right: 10px"
:options="[
{ value: '1', label: '黄金糕' },
{ value: '2', label: '双皮奶' }
]"
></tiny-select>
<tiny-input placeholder="请输入" modelValue="" style="width: 120px; margin-right: 10px"></tiny-input>
<span style="color: #575d6c; font-size: 12px; margin-right: 10px">GiB IOPS上限600IOPS突发上限5,000</span>
<tiny-input placeholder="请输入" modelValue="" style="width: 120px"></tiny-input>
</div>
<div style="display: flex; margin-top: 12px; border-radius: 0px">
<tiny-icon-plus style="width: 16px; height: 16px; margin-right: 10px"></tiny-icon-plus>
<span style="font-size: 12px; border-radius: 0px; margin-right: 10px">增加一块数据盘</span>
<span style="color: #8a8e99; font-size: 12px">您还可以挂载 21 块磁盘云硬盘</span>
</div></tiny-form-item
></tiny-form
>
</div>
<div
style="
border-width: 1px;
border-style: solid;
border-color: #ffffff;
padding-top: 10px;
padding-left: 10px;
padding-right: 10px;
box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px;
background-color: #fff;
position: fixed;
inset: auto 0% 0% 0%;
height: 80px;
line-height: 80px;
border-radius: 0px;
"
>
<tiny-form
labelWidth="80px"
labelPosition="top"
:inline="false"
label-position="left "
label-width="150px"
style="border-radius: 0px"
></tiny-form>
<tiny-row style="border-radius: 0px; height: 100%">
<tiny-col span="8">
<tiny-row style="border-radius: 0px">
<tiny-col span="5" style="display: flex">
<span style="margin-right: 10px">购买量</span>
<tiny-input placeholder="请输入" modelValue="" style="width: 120px; margin-right: 10px"></tiny-input>
<span></span></tiny-col
>
<tiny-col span="7">
<div>
<span style="font-size: 12px">配置费用</span>
<span style="padding-left: 10px; padding-right: 10px; color: #de504e">¥1.5776</span>
<span style="font-size: 12px">/小时</span>
</div>
<div>
<span style="font-size: 12px; border-radius: 0px">参考价格具体扣费请以账单为准</span>
<span style="font-size: 12px; color: #344899">了解计费详情</span>
</div></tiny-col
></tiny-row
></tiny-col
>
<tiny-col
span="4"
style="
display: flex;
flex-direction: row-reverse;
border-radius: 0px;
height: 100%;
justify-content: flex-start;
align-items: center;
"
>
<tiny-button text="下一步: 网络配置" type="danger" style="max-width: unset"></tiny-button></tiny-col
></tiny-row>
</div>
</div>
</template>
<script setup>
import {
Col as TinyCol,
Search as TinySearch,
Row as TinyRow,
FormItem as TinyFormItem,
Input as TinyInput,
TimeLine as TinyTimeLine,
Form as TinyForm,
Grid as TinyGrid,
Select as TinySelect,
ButtonGroup as TinyButtonGroup
} from '@opentiny/vue'
import { IconPanelMini, IconPlus } from '@opentiny/vue-icon'
import * as vue from 'vue'
import { defineProps, defineEmits } from 'vue'
import { I18nInjectionKey } from 'vue-i18n'
const TinyIconPanelMini = IconPanelMini()
const TinyIconPlus = IconPlus()
const props = defineProps({})
const emit = defineEmits([])
const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()
const wrap = lowcodeWrap(props, { emit })
wrap({ stores })
const state = vue.reactive({ dataDisk: [1, 2, 3] })
wrap({ state })
</script>
<style scoped>
body {
background-color: #eef0f5;
margin-bottom: 80px;
}
</style>

View File

@ -0,0 +1,23 @@
import { defineConfig } from 'vite'
import path from 'path'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
plugins: [vue(), vueJsx()],
define: {
'process.env': { ...process.env }
},
build: {
minify: true,
commonjsOptions: {
transformMixedEsModules: true
},
cssCodeSplit: false
}
})

View File

@ -0,0 +1,43 @@
import { expect, test, describe } from 'vitest'
import path from 'path'
import fs from 'fs'
import dirCompare from 'dir-compare'
import { generateApp } from '@/generator/generateApp'
import { appSchemaDemo01 } from './mockData'
import { logDiffResult } from '../../utils/logDiffResult'
describe('generate whole application', () => {
test('should not throw error', async () => {
const instance = generateApp()
const res = await instance.generate(appSchemaDemo01)
const { genResult } = res
// 写入文件
genResult.forEach(({ fileName, path: filePath, fileContent }) => {
fs.mkdirSync(path.resolve(__dirname, `./result/appdemo01/${filePath}`), { recursive: true })
fs.writeFileSync(
path.resolve(__dirname, `./result/appdemo01/${filePath}/${fileName}`),
// 这里需要将换行符替换成 CRLF 格式的
fileContent.replace(/\r?\n/g, '\r\n')
)
})
const compareOptions = {
compareContent: true,
ignoreLineEnding: true,
ignoreAllWhiteSpaces: true,
ignoreEmptyLines: true
}
const path1 = path.resolve(__dirname, './expected/appdemo01')
const path2 = path.resolve(__dirname, './result/appdemo01')
// 对比文件差异
const diffResult = dirCompare.compareSync(path1, path2, compareOptions)
logDiffResult(diffResult)
expect(diffResult.same).toBe(true)
})
})

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,207 @@
{
"componentName": "Block",
"fileName": "ImageTitle",
"css": ".image-title {\n margin-right: 15px;\ndisplay: flex;\n align-items: center; \n}\n.crm-title {\n margin-left: 8px;\n font-family: PingFangSC-Regular; \nfont-size: 22px; \ncolor: #333333; \nline-height: 30px; \n}\n.split {\r\n align-self: center;\r\n width: 1px;\r\n height: 20px;\r\n background-color: #dddee4;\r\n margin-left: 20px;\r\n}\r\n",
"props": {},
"lifeCycles": {},
"children": [
{
"componentName": "div",
"id": "ImageTitleizk3",
"props": {
"className": "image-title",
"onClick": {
"type": "JSExpression",
"value": "this.handleClick"
}
},
"children": [
{
"componentName": "img",
"id": "imageizk3",
"props": {
"src": {
"type": "JSExpression",
"value": "this.props.src"
}
}
},
{
"componentName": "span",
"id": "spanizk3",
"props": {
"className": "crm-title"
},
"children": {
"type": "JSExpression",
"value": "this.props.text"
}
},
{
"componentName": "span",
"id": "spanizk4",
"condition": {
"type": "JSExpression",
"value": "this.props.hasSplitLine"
},
"props": {
"className": "split"
}
}
]
}
],
"schema": {
"properties": [
{
"label": {
"zh_CN": "基础信息"
},
"description": {
"zh_CN": "基础信息"
},
"collapse": {
"number": 6,
"text": {
"zh_CN": "显示更多"
}
},
"content": [
{
"property": "handleClick",
"type": "Function",
"defaultValue": {
"type": "Function",
"value": "function handleClick(event) { return event }"
},
"label": {
"text": {
"zh_CN": "点击Image触发事件"
}
},
"cols": 12,
"rules": [],
"hidden": false,
"required": true,
"readOnly": false,
"disabled": false,
"widget": {
"component": "MetaCodeEditor",
"props": {}
}
},
{
"property": "options",
"type": "Array",
"defaultValue": [],
"label": {
"text": {
"zh_CN": "选项"
}
},
"cols": 12,
"rules": [],
"hidden": false,
"required": true,
"readOnly": false,
"disabled": false,
"widget": {
"component": "MetaCodeEditor",
"props": {
"modelValue": []
}
}
},
{
"property": "src",
"type": "string",
"defaultValue": "https://res-static.hc-cdn.cn/cloudbu-site/china/zh-cn/TinyLowCode/crm/img/bussiness/businessmanage.svg",
"label": {
"text": {
"zh_CN": "图片地址"
}
},
"cols": 12,
"rules": [],
"hidden": false,
"required": true,
"readOnly": false,
"disabled": false,
"widget": {
"component": "MetaInput",
"props": {
"modelValue": "https://res-static.hc-cdn.cn/cloudbu-site/china/zh-cn/TinyLowCode/crm/img/bussiness/businessmanage.svg"
}
}
},
{
"property": "text",
"type": "String",
"defaultValue": "商务管理",
"label": {
"text": {
"zh_CN": "标题文本"
}
},
"cols": 12,
"rules": [],
"hidden": false,
"required": true,
"readOnly": false,
"disabled": false,
"widget": {
"component": "MetaInput",
"props": {
"modelValue": "商务管理"
}
}
},
{
"property": "hasSplitLine",
"type": "Boolean",
"defaultValue": true,
"label": {
"text": {
"zh_CN": "是否添加分割线"
}
},
"cols": 12,
"rules": [],
"hidden": false,
"required": true,
"readOnly": false,
"disabled": false,
"widget": {
"component": "MetaSwitch",
"props": {
"modelValue": true
}
}
}
]
}
],
"events": {
"onClickLogo": {
"label": {
"zh_CN": "点击事件"
},
"description": {
"zh_CN": "通常用于配置处理点击跳转"
}
}
}
},
"state": {
"activeMethod": {
"type": "JSFunction",
"value": "function() {\n return this.props.isEdit;\r\n}"
}
},
"methods": {
"handleClick": {
"type": "JSFunction",
"value": "function() { this.emit('click-logo') }"
}
}
}

View File

@ -0,0 +1,41 @@
import { expect, test, beforeEach, afterEach, vi } from 'vitest'
import { genSFCWithDefaultPlugin } from '@/generator/vue/sfc'
import schema from './schema.json'
import blockSchema from './blocks.schema.json'
import componentsMap from './componentsMap.json'
import { formatCode } from '@/utils/formatCode'
let count = 0
const mockValue = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
beforeEach(() => {
// 伪随机数,保证每次快照都一致
vi.spyOn(global.Math, 'random').mockImplementation(() => {
const res = mockValue[count]
count++
if (count > 10) {
count = 0
}
return res
})
})
afterEach(() => {
vi.spyOn(global.Math, 'random').mockRestore()
})
test('should validate tagName', async () => {
const res = genSFCWithDefaultPlugin(schema, componentsMap)
const formattedCode = formatCode(res, 'vue')
await expect(formattedCode).toMatchFileSnapshot('./expected/FormTable.vue')
})
test('should generate block component correct', async () => {
const res = genSFCWithDefaultPlugin(blockSchema, componentsMap)
const formattedCode = formatCode(res, 'vue')
await expect(formattedCode).toMatchFileSnapshot('./expected/ImageTitle.vue')
})

View File

@ -0,0 +1,70 @@
[
{
"componentName": "TinyButton",
"exportName": "Button",
"package": "@opentiny/vue",
"version": "^3.10.0",
"destructuring": true
},
{
"componentName": "TinyForm",
"exportName": "Form",
"package": "@opentiny/vue",
"version": "^3.10.0",
"destructuring": true
},
{
"componentName": "TinyFormItem",
"exportName": "FormItem",
"package": "@opentiny/vue",
"version": "^3.10.0",
"destructuring": true
},
{
"componentName": "TinyGrid",
"exportName": "Grid",
"package": "@opentiny/vue",
"version": "^3.10.0",
"destructuring": true
},
{
"componentName": "TinyInput",
"exportName": "Input",
"package": "@opentiny/vue",
"version": "^3.10.0",
"destructuring": true
},
{
"componentName": "TinySelect",
"exportName": "Select",
"package": "@opentiny/vue",
"version": "^3.10.0",
"destructuring": true
},
{
"componentName": "TinySwitch",
"exportName": "Switch",
"package": "@opentiny/vue",
"version": "^3.10.0",
"destructuring": true
},
{
"componentName": "Img",
"exportName": "",
"package": "",
"version": "1.0.0",
"destructuring": true
},
{
"componentName": "FormTable",
"main": "./views"
},
{
"componentName": "ImageTitle",
"main": "./components"
},
{
"componentName": "CrmQuoteListGridStatus",
"main": "./views/crm/quote-list"
}
]

View File

@ -0,0 +1,332 @@
<template>
<div>
<span style="background: url('**/public/logo.png')" class="page-header">标题区</span>
<span style="background: url('**/public/background.png')">副标题区</span>
<image-title
text="配置报价"
:hasSplitLine="false"
:class="['basic-info', { 'form-fixed-layout': isFixed }, { 'form-auto-layout': isAuto }]"
@click-logo="(...eventArgs) => handleReset(eventArgs, state.flag)"
></image-title>
<tiny-form :inline="true" class="form" :style="{ margin: '12px' }">
<tiny-form-item :label="t('company.name')">
<tiny-input :disabled="false" :value="state.companyName"></tiny-input
></tiny-form-item>
<tiny-form-item v-if="state.cityOptions.length">
<template #label>城市</template>
<tiny-select
:value="state.companyCity"
:options="[
{ label: t('city.foochow'), value: 0 },
{ label: '深\'i\'圳', value: 1 },
{ label: '中山', value: 2 },
{ label: '龙岩', value: 3 },
{ label: '韶关', value: 4 },
{ label: '黄冈', value: 5 },
{ label: '赤壁', value: 6 },
{ label: '厦门', value: 7 }
]"
></tiny-select
></tiny-form-item>
<tiny-form-item>
<span class="form-footer">表单提交区</span>
<tiny-button type="primary" :icon="TinyIconSearch" @click="handleSearch">搜索</tiny-button>
<tiny-button @click="handleReset">{{ t('operation.reset') }}</tiny-button></tiny-form-item
></tiny-form
>
<div dataSource="a5f6ef4f">
<tiny-grid :columns="state.columns" :fetchData="{ api: getTableData }"></tiny-grid>
</div>
<div dataSource="a5f6ef4f">
<tiny-grid :fetchData="{ api: getTableData }" :columns="state.columns6cio"></tiny-grid>
</div>
<div :style="{ width: props.quotePopWidth }">循环渲染</div>
<tiny-icon-help-circle v-if="false"></tiny-icon-help-circle>
<tiny-button
v-for="(item, index) in state.buttons"
:key="item.text"
:type="item.type"
:text="index + item.text"
></tiny-button>
<br />
<tiny-button
v-for="item in [
{ type: 'primary', text: '字面量' },
{ type: 'success', text: '字面量' },
{ type: 'danger', text: '危险操作' }
]"
:key="item.text"
:type="item.type"
:text="item.text"
></tiny-button>
</div>
</template>
<script setup lang="jsx">
import {
Button as TinyButton,
Form as TinyForm,
FormItem as TinyFormItem,
Grid as TinyGrid,
Input as TinyInput,
Select as TinySelect,
Switch as TinySwitch
} from '@opentiny/vue'
import { IconSearch, IconDel, iconHelpCircle, IconEdit } from '@opentiny/vue-icon'
import * as vue from 'vue'
import { defineProps, defineEmits } from 'vue'
import { I18nInjectionKey } from 'vue-i18n'
import CrmQuoteListGridStatus from '../components/CrmQuoteListGridStatus.vue'
import ImageTitle from '../components/ImageTitle.vue'
const TinyIconSearch = IconSearch()
const TinyIconDel = IconDel()
const TinyIconHelpCircle = iconHelpCircle()
const TinyIconEdit = IconEdit()
const props = defineProps({})
const emit = defineEmits([])
const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()
const wrap = lowcodeWrap(props, { emit })
wrap({ stores })
const { utils } = wrap(function () {
return this
})()
const state = vue.reactive({
columns6cio: [
{ type: 'index', width: 60, title: '' },
{ type: 'selection', width: 60 },
{ field: 'employees', title: '员工数', slots: { default: ({ row, rowIndex }, h) => <TinyInput></TinyInput> } },
{ field: 'city', title: '城市' },
{
title: '产品',
slots: {
default: ({ row }, h) => (
<div>
<TinySwitch modelValue=""></TinySwitch>
</div>
)
}
},
{
title: '操作',
slots: {
default: ({ row }, h) => (
<TinyButton text="删除" icon={TinyIconDel} onClick={(...eventArgs) => emit(eventArgs, row)}></TinyButton>
)
}
}
],
IconPlusSquare: utils.IconPlusSquare(),
theme: "{ 'id': 22, 'name': '@cloud/tinybuilder-theme-dark', 'description': '黑暗主题' }",
companyName: '',
companyOptions: null,
companyCity: '',
cityOptions: [
{ label: '福州', value: 0 },
{ label: '深圳', value: 1 },
{ label: '中山', value: 2 },
{ label: '龙岩', value: 3 },
{ label: '韶关', value: 4 },
{ label: '黄冈', value: 5 },
{ label: '赤壁', value: 6 },
{ label: '厦门', value: 7 }
],
editConfig: {
trigger: 'click',
mode: 'cell',
showStatus: true,
activeMethod: () => {
return props.isEdit
}
},
columns: [
{ type: props.isEdit ? 'selection' : 'index', width: '60', title: props.isEdit ? '' : '序号' },
{
field: 'status',
title: '状态',
filter: {
layout: 'input,enum,default,extends,base',
inputFilter: {
component: utils.Numeric,
attrs: { format: 'yyyy/MM/dd hh:mm:ss' },
relation: 'A',
relations: [
{
label: '小于',
value: 'A',
method: ({ value, input }) => {
return value < input
}
},
{ label: '等于', value: 'equals' },
{ label: '大于', value: 'greaterThan' }
]
},
extends: [
{
label: '我要过滤大于800的数',
method: ({ value }) => {
return value > 800
}
},
{
label: '我要过滤全部的数',
method: () => {
return true
}
}
]
},
slots: {
default: ({ row }, h) => (
<div>
<TinyIconEdit></TinyIconEdit>
{props.isEdit && (
<CrmQuoteListGridStatus isEdit={props.isEdit} status={row.status}></CrmQuoteListGridStatus>
)}
</div>
)
}
},
{ type: 'index', width: 60 },
{ type: 'selection', width: 60 },
{ field: 'name', title: '公司名称' },
{ field: 'employees', title: '员工数' },
{ field: 'city', title: '城市' },
{
title: '操作',
slots: {
default: ({ row }, h) => (
<div
style="color: rgb(94,124, 224);cursor:pointer;"
visible={true}
text={t('operation.delete')}
prop1={{ a: 123 }}
onClick={emit}
>
<TinyInput value={row.giveamount}></TinyInput>
{state.cityOptions.length && <span>{t('operation.hello')}</span>}
<TinyIconHelpCircle style="margin-left: 6px; cursor: pointer;vertical-align: top;"></TinyIconHelpCircle>
</div>
)
}
}
],
tableData: [
{ id: '1', name: 'GFD科技有限公司', city: '福州', employees: 800, boole: false },
{ id: '2', name: 'WWW科技有限公司', city: '深圳', employees: 300, boole: true },
{ id: '3', name: 'RFV有限责任公司', city: '中山', employees: 1300, boole: false },
{ id: '4', name: 'TGB科技有限公司', city: '龙岩', employees: 360, boole: true },
{ id: '5', name: 'YHN科技有限公司', city: '韶关', employees: 810, boole: true },
{ id: '6', name: 'WSX科技有限公司', city: '黄冈', employees: 800, boole: true },
{ id: '7', name: 'KBG物业有限公司', city: '赤壁', employees: 400, boole: false },
{ id: '8', name: '深圳市福德宝网络技术有限公司', boole: true, city: '厦门', employees: 540 }
],
status: vue.computed(statusData),
buttons: [
{ type: 'primary', text: '主要操作' },
{ type: 'success', text: '成功操作' },
{ type: 'danger', text: t('operation.danger') }
]
})
wrap({ state })
const getTableData = wrap(function getData({ page, filterArgs }) {
const { curPage, pageSize } = page
const offset = (curPage - 1) * pageSize
return new Promise((resolve) => {
setTimeout(() => {
const { tableData } = this.state
let result = [...tableData]
if (filterArgs) {
result = result.filter((item) => item.city === filterArgs)
}
const total = result.length
result = result.slice(offset, offset + pageSize)
resolve({ result, page: { total } })
}, 500)
})
})
const handleSearch = wrap(function (e) {
return ['搜索:', this.i18n('operation.search'), e]
})
const handleReset = wrap(function handleReset(e) {
return ['重置:', e]
})
const statusData = wrap(function () {
return [
{ name: this.i18n('quotes.common.configure_basic_information'), status: 'ready' },
{ name: this.i18n('quotes.quote_list.quote'), status: 'wait' },
{ name: this.i18n('quotes.common.complete_configuration_quote'), status: 'wait' }
]
})
wrap({ getTableData, handleSearch, handleReset, statusData })
const setup = wrap(function ({ props, watch, onMounted }) {
onMounted(() => {
this.getTableDta()
})
watch(
() => props.load,
(load) => {
if (load.isLoad) {
this.getTableDta()
}
},
{
deep: true
}
)
})
setup({ props, context: { emit }, state, ...vue })
vue.onBeforeMount(
wrap(function () {
return '生命周期onBeforeMount'
})
)
vue.onMounted(
wrap(function onMounted() {
return '生命周期onMounted'
})
)
</script>
<style scoped>
.overflow-container .card {
padding-bottom: 8px;
}
.main-body {
display: flex;
flex: 1;
flex-direction: column;
margin: 20px 20px 9px 20px;
}
.card {
padding: 20px 20px;
background-color: #ffffff;
box-shadow: 0 2px 10px 0 rgb(0 0 0 / 6%);
border-radius: 2px;
}
.manage-list {
margin-bottom: 60px !important;
}
.crm-title-wrapper {
display: flex;
justify-content: start;
align-items: center;
margin-bottom: 20px;
gap: 20px;
}
.crm-import-button:not(:last-child) {
margin-right: 10px;
}
</style>

View File

@ -0,0 +1,70 @@
<template>
<div>
<div class="image-title" @click="handleClick">
<img :src="src" />
<span class="crm-title">{{ text }}</span>
<span v-if="hasSplitLine" class="split"></span>
</div>
</div>
</template>
<script setup>
import * as vue from 'vue'
import { defineProps, defineEmits } from 'vue'
import { I18nInjectionKey } from 'vue-i18n'
const props = defineProps({
handleClick: {
type: Function,
default: function handleClick(event) {
return event
}
},
options: { type: Array, default: () => [] },
src: {
type: String,
default: 'https://res-static.hc-cdn.cn/cloudbu-site/china/zh-cn/TinyLowCode/crm/img/bussiness/businessmanage.svg'
},
text: { type: String, default: '商务管理' },
hasSplitLine: { type: Boolean, default: true }
})
const emit = defineEmits(['click-logo'])
const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()
const wrap = lowcodeWrap(props, { emit })
wrap({ stores })
const state = vue.reactive({
activeMethod: () => {
return props.isEdit
}
})
wrap({ state })
const handleClick = wrap(function () {
this.emit('click-logo')
})
wrap({ handleClick })
</script>
<style scoped>
.image-title {
margin-right: 15px;
display: flex;
align-items: center;
}
.crm-title {
margin-left: 8px;
font-family: PingFangSC-Regular;
font-size: 22px;
color: #333333;
line-height: 30px;
}
.split {
align-self: center;
width: 1px;
height: 20px;
background-color: #dddee4;
margin-left: 20px;
}
</style>

View File

@ -0,0 +1,716 @@
{
"version": "1.1",
"componentName": "Page",
"fileName": "FormTable",
"css": ".overflow-container .card {\n padding-bottom: 8px;\n}\n.main-body {\n display: flex;\n flex: 1;\n flex-direction: column;\n margin: 20px 20px 9px 20px;\n}\n.card {\n padding: 20px 20px;\n background-color: #ffffff;\n box-shadow: 0 2px 10px 0 rgb(0 0 0 / 6%);\n border-radius: 2px;\n}\n.manage-list {\n margin-bottom: 60px !important;\n} .crm-title-wrapper{\n display: flex;\n justify-content: start;\n align-items: center;\n margin-bottom: 20px;\n gap: 20px;\n}\n .crm-import-button:not(:last-child) {\n margin-right: 10px;\n}",
"props": {},
"children": [
{
"componentName": "Text",
"props": {
"style": "background: url(\"**/public/logo.png\");",
"className": "page-header",
"text": "标题区"
}
},
{
"componentName": "Text",
"props": {
"style": "background: url('**/public/background.png');",
"text": "副标题区"
}
},
{
"componentName": "Template",
"props": {
"text": "空插槽,出码会跳过此节点"
},
"children": []
},
{
"componentName": "ImageTitle",
"fileName": "ImageTitle",
"componentType": "Block",
"props": {
"className": {
"type": "JSExpression",
"value": "['basic-info', {'form-fixed-layout': this.props.isFixed}, {'form-auto-layout': this.props.isAuto}]"
},
"text": "配置报价",
"hasSplitLine": false,
"onClickLogo": {
"type": "JSExpression",
"value": "this.handleReset",
"params": ["state.flag"]
}
}
},
{
"componentName": "TinyForm",
"props": {
"inline": true,
"style": {
"margin": "12px"
},
"className": "form"
},
"children": [
{
"componentName": "TinyFormItem",
"props": {
"label": {
"type": "i18n",
"key": "company.name"
}
},
"children": [
{
"componentName": "TinyInput",
"props": {
"disabled": false,
"value": {
"type": "JSExpression",
"value": "state.companyName"
}
}
}
]
},
{
"componentName": "TinyFormItem",
"condition": {
"type": "JSExpression",
"value": "state.cityOptions.length"
},
"children": [
{
"componentName": "Template",
"props": {
"slot": "label"
},
"children": "城市"
},
{
"componentName": "TinySelect",
"props": {
"value": {
"type": "JSExpression",
"value": "state.companyCity"
},
"options": [
{
"label": {
"type": "i18n",
"key": "city.foochow",
"zh_CN": "福州",
"en_US": "Foochow"
},
"value": 0
},
{
"label": "深'i'圳",
"value": 1
},
{
"label": "中山",
"value": 2
},
{
"label": "龙岩",
"value": 3
},
{
"label": "韶关",
"value": 4
},
{
"label": "黄冈",
"value": 5
},
{
"label": "赤壁",
"value": 6
},
{
"label": "厦门",
"value": 7
}
]
}
}
]
},
{
"componentName": "TinyFormItem",
"props": {},
"children": [
{
"componentName": "Text",
"props": {
"className": "form-footer",
"text": "表单提交区"
}
},
{
"componentName": "TinyButton",
"props": {
"type": "primary",
"icon": {
"componentName": "Icon",
"props": {
"name": "IconSearch"
}
},
"onClick": {
"type": "JSExpression",
"value": "this.handleSearch"
}
},
"children": "搜索"
},
{
"componentName": "TinyButton",
"props": {
"onClick": {
"type": "JSExpression",
"value": "this.handleReset"
}
},
"children": {
"type": "i18n",
"key": "operation.reset"
}
}
]
}
]
},
{
"componentName": "Collection",
"props": {
"dataSource": "a5f6ef4f"
},
"children": [
{
"componentName": "TinyGrid",
"props": {
"columns": {
"type": "JSExpression",
"value": "state.columns"
},
"data": {
"type": "JSExpression",
"value": "state.tableData"
},
"fetchData": {
"type": "JSExpression",
"value": "{ api: getTableData }"
}
}
}
]
},
{
"componentName": "Collection",
"props": {
"dataSource": "a5f6ef4f"
},
"children": [
{
"componentName": "TinyGrid",
"props": {
"columns": [
{ "type": "index", "width": 60, "title": "" },
{ "type": "selection", "width": 60 },
{
"field": "employees",
"title": "员工数",
"slots": {
"default": {
"type": "JSSlot",
"params": ["row", "rowIndex"],
"value": [{ "componentName": "TinyInput", "props": {}, "id": "49e232ce" }]
}
}
},
{ "field": "city", "title": "城市" },
{
"title": "产品",
"slots": {
"default": {
"type": "JSSlot",
"params": ["row"],
"value": [
{
"componentName": "div",
"id": "592fbc05",
"children": [{ "componentName": "TinySwitch", "props": { "modelValue": "" }, "id": "46a60c6f" }]
}
]
}
}
},
{
"title": "操作",
"slots": {
"default": {
"type": "JSSlot",
"value": [
{
"componentName": "TinyButton",
"props": {
"text": "删除",
"icon": {
"componentName": "Icon",
"props": {
"name": "IconDel"
}
},
"onClick": {
"type": "JSExpression",
"value": "this.emit",
"params": ["row"]
}
}
}
]
}
}
}
],
"data": {
"type": "JSExpression",
"value": "state.tableData"
},
"fetchData": {
"type": "JSExpression",
"value": "{ api: getTableData }"
}
}
}
]
},
{
"componentName": "div",
"props": {
"style": {
"width": {
"type": "JSExpression",
"value": "this.props.quotePopWidth"
}
}
},
"children": "循环渲染:"
},
{
"componentName": "Icon",
"condition": false,
"props": {
"name": "TinyIconHelpCircle"
}
},
{
"children": [
{
"componentName": "TinyButton",
"loop": {
"type": "JSExpression",
"value": "state.buttons"
},
"loopArgs": ["item", "index"],
"props": {
"key": {
"type": "JSExpression",
"value": "item.text"
},
"type": {
"type": "JSExpression",
"value": "item.type"
},
"text": {
"type": "JSExpression",
"value": "index + item.text"
}
}
}
]
},
{
"componentName": "br"
},
{
"children": [
{
"componentName": "TinyButton",
"loop": [
{
"type": "primary",
"text": "字面量"
},
{
"type": "success",
"text": "字面量"
},
{
"type": "danger",
"text": "危险操作"
}
],
"loopArgs": ["item"],
"props": {
"key": {
"type": "JSExpression",
"value": "item.text"
},
"type": {
"type": "JSExpression",
"value": "item.type"
},
"text": {
"type": "JSExpression",
"value": "item.text"
}
}
}
]
}
],
"state": {
"IconPlusSquare": {
"type": "JSResource",
"value": "this.utils.IconPlusSquare()"
},
"theme": "{ \"id\": 22, \"name\": \"@cloud/tinybuilder-theme-dark\", \"description\": \"黑暗主题\" }",
"companyName": "",
"companyOptions": null,
"companyCity": "",
"cityOptions": [
{
"label": "福州",
"value": 0
},
{
"label": "深圳",
"value": 1
},
{
"label": "中山",
"value": 2
},
{
"label": "龙岩",
"value": 3
},
{
"label": "韶关",
"value": 4
},
{
"label": "黄冈",
"value": 5
},
{
"label": "赤壁",
"value": 6
},
{
"label": "厦门",
"value": 7
}
],
"editConfig": {
"trigger": "click",
"mode": "cell",
"showStatus": true,
"activeMethod": {
"type": "JSFunction",
"value": "function() { return this.props.isEdit }"
}
},
"columns": [
{
"type": {
"type": "JSExpression",
"value": "this.props.isEdit ? 'selection' : 'index'"
},
"width": "60",
"title": {
"type": "JSExpression",
"value": "this.props.isEdit ? '' : '序号'"
}
},
{
"field": "status",
"title": "状态",
"filter": {
"layout": "input,enum,default,extends,base",
"inputFilter": {
"component": {
"type": "JSResource",
"value": "this.utils.Numeric"
},
"attrs": { "format": "yyyy/MM/dd hh:mm:ss" },
"relation": "A",
"relations": [
{
"label": "小于",
"value": "A",
"method": {
"type": "JSFunction",
"value": "function({ value, input }) { return value < input }"
}
},
{ "label": "等于", "value": "equals" },
{ "label": "大于", "value": "greaterThan" }
]
},
"extends": [
{
"label": "我要过滤大于800的数",
"method": {
"type": "JSFunction",
"value": "function({ value }) { return value > 800 }"
}
},
{
"label": "我要过滤全部的数",
"method": {
"type": "JSFunction",
"value": "function() { return true }"
}
}
]
},
"slots": {
"default": {
"type": "JSSlot",
"params": ["row"],
"value": [
{
"componentName": "div",
"children": [
{
"componentName": "Icon",
"props": {
"name": "IconEdit"
}
},
{
"componentName": "CrmQuoteListGridStatus",
"componentType": "Block",
"condition": {
"type": "JSExpression",
"value": "this.props.isEdit"
},
"props": {
"isEdit": {
"type": "JSExpression",
"value": "this.props.isEdit"
},
"status": {
"type": "JSExpression",
"value": "row.status"
}
}
}
]
}
]
}
}
},
{
"type": "index",
"width": 60
},
{
"type": "selection",
"width": 60
},
{
"field": "name",
"title": "公司名称"
},
{
"field": "employees",
"title": "员工数"
},
{
"field": "city",
"title": "城市"
},
{
"title": "操作",
"slots": {
"default": {
"type": "JSSlot",
"value": [
{
"component": "div",
"props": {
"style": "color: rgb(94,124, 224);cursor:pointer;",
"text": {
"type": "i18n",
"key": "operation.delete"
},
"prop1": {
"a": 123
},
"visible": true,
"onClick": {
"type": "JSExpression",
"value": "this.emit"
}
},
"children": [
{
"componentName": "TinyInput",
"props": {
"value": {
"type": "JSExpression",
"value": "row.giveamount",
"model": {
"prop": ""
}
}
}
},
{
"component": "span",
"condition": {
"type": "JSExpression",
"value": "state.cityOptions.length"
},
"children": {
"type": "i18n",
"key": "operation.hello"
}
},
{
"componentName": "Icon",
"props": {
"name": "TinyIconHelpCircle",
"style": "margin-left: 6px; cursor: pointer;vertical-align: top;"
}
}
]
}
]
}
}
}
],
"tableData": [
{
"id": "1",
"name": "GFD科技有限公司",
"city": "福州",
"employees": 800,
"boole": false
},
{
"id": "2",
"name": "WWW科技有限公司",
"city": "深圳",
"employees": 300,
"boole": true
},
{
"id": "3",
"name": "RFV有限责任公司",
"city": "中山",
"employees": 1300,
"boole": false
},
{
"id": "4",
"name": "TGB科技有限公司",
"city": "龙岩",
"employees": 360,
"boole": true
},
{
"id": "5",
"name": "YHN科技有限公司",
"city": "韶关",
"employees": 810,
"boole": true
},
{
"id": "6",
"name": "WSX科技有限公司",
"city": "黄冈",
"employees": 800,
"boole": true
},
{
"id": "7",
"name": "KBG物业有限公司",
"city": "赤壁",
"employees": 400,
"boole": false
},
{
"id": "8",
"name": "深圳市福德宝网络技术有限公司",
"boole": true,
"city": "厦门",
"employees": 540
}
],
"status": {
"type": "JSExpression",
"value": "this.statusData",
"computed": true
},
"buttons": [
{
"type": "primary",
"text": "主要操作"
},
{
"type": "success",
"text": "成功操作"
},
{
"type": "danger",
"text": {
"type": "i18n",
"key": "operation.danger"
}
}
]
},
"lifeCycles": {
"setup": {
"type": "JSFunction",
"value": "function({ props, watch, onMounted }) {\r\n onMounted(() => {\r\n this.getTableDta()\r\n })\r\n watch(\r\n () => props.load,\r\n (load) => {\r\n if (load.isLoad) {\r\n this.getTableDta()\r\n }\r\n },\r\n {\r\n deep: true\r\n }\r\n )\r\n}"
},
"onBeforeMount": {
"type": "JSFunction",
"value": "function() { return '生命周期onBeforeMount'; }"
},
"onMounted": {
"type": "JSFunction",
"value": "function onMounted() { return '生命周期onMounted'; }"
}
},
"methods": {
"getTableData": {
"type": "JSFunction",
"value": "function getData({ page, filterArgs }) {\n const { curPage, pageSize } = page;\n const offset = (curPage - 1) * pageSize;\n\n return new Promise((resolve) => {\n setTimeout(() => {\n const { tableData } = this.state;\n let result = [...tableData];\n\n if (filterArgs) {\n result = result.filter((item) => item.city === filterArgs);\n }\n\n const total = result.length;\n result = result.slice(offset, offset + pageSize);\n\n resolve({ result, page: { total } });\n }, 500);\n });\n}"
},
"handleSearch": {
"type": "JSFunction",
"value": "function(e) { return ['搜索:', this.i18n('operation.search'), e]; }"
},
"handleReset": {
"type": "JSFunction",
"value": "function handleReset(e) { return ['重置:', e]; }"
},
"statusData": {
"type": "JSFunction",
"value": "function () {\r\n return [\r\n { name: this.i18n('quotes.common.configure_basic_information'), status: 'ready' },\r\n { name: this.i18n('quotes.quote_list.quote'), status: 'wait' },\r\n { name: this.i18n('quotes.common.complete_configuration_quote'), status: 'wait' }\r\n ]\r\n}"
}
}
}

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