forked from opentiny/tiny-engine
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:
parent
aa8d361bd2
commit
3a66996fae
|
@ -1787,7 +1787,7 @@
|
|||
},
|
||||
{
|
||||
"componentName": "TinyPlusFrozenPage",
|
||||
"package": "@opentiny/vuee",
|
||||
"package": "@opentiny/vue",
|
||||
"exportName": "FrozenPage",
|
||||
"destructuring": true,
|
||||
"version": "3.4.1"
|
||||
|
|
|
@ -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:*",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}`)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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 包含dataSource、utils、i18n、globalState
|
||||
...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)
|
||||
|
|
|
@ -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}`)
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -11,8 +11,11 @@ module.exports = {
|
|||
node: true
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
}
|
||||
},
|
||||
// 忽略 expected 中的内容
|
||||
ignorePatterns: ['**/**/expected/*', '**/**.ts']
|
||||
}
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
test/testcases/full/**/result/*
|
||||
test/**/result/*
|
||||
|
||||
coverage
|
|
@ -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 Terminal(JavaScript 调试终端)
|
||||
2. 终端打开 vue-generator 目录,`cd packages/vue-generator`
|
||||
3. 对需要调试的位置打上断点(VSCode 文件行数旁边)
|
||||
4. 执行 `pnpm test:unit testCaseName`
|
||||
|
||||
### 更多测试指引,可参考 [vitest](https://vitest.dev/) 指引
|
|
@ -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,生成页面或者区块文件,所以我们在这里展开讲讲官方的插件实现与思考。
|
||||
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -94,6 +94,7 @@ const handleLiteralBinding = ({ key, item, attrsArr, description, state }) => {
|
|||
// string 直接静态绑定
|
||||
if (typeof item === 'string') return attrsArr.push(`${key}="${item.replace(/"/g, '"')}"`)
|
||||
|
||||
// TODO: 拿到这里的场景 case?
|
||||
if (item?.componentName === BUILTIN_COMPONENT_NAME.ICON) {
|
||||
const iconName = handleIconInProps(description, item)
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
# vue sfc code generator
|
||||
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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>`
|
||||
}
|
|
@ -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>`
|
||||
}
|
|
@ -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}>`
|
||||
}
|
|
@ -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>`
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { default as generateSFCFile, genSFCWithDefaultPlugin } from './genSetupSFC'
|
|
@ -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')}`
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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'
|
|
@ -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
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { generateTemplate as genDefaultStaticTemplate } from './vue-template'
|
||||
|
||||
export const templateMap = {
|
||||
default: genDefaultStaticTemplate
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,19 @@
|
|||
## $$TinyEngine{meta.name}END$
|
||||
|
||||
本工程是使用 TinyEngine 低代码引擎搭建之后得到的出码工程。
|
||||
|
||||
## 使用
|
||||
|
||||
安装依赖:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
本地启动项目:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 () => {}
|
|
@ -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
|
|
@ -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 }
|
||||
}
|
|
@ -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 }
|
|
@ -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')
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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}"`
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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) => {
|
||||
|
|
13
packages/vue-generator/test/testcases/generator/expected/appdemo01/.gitignore
vendored
Normal file
13
packages/vue-generator/test/testcases/generator/expected/appdemo01/.gitignore
vendored
Normal 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
|
|
@ -0,0 +1,19 @@
|
|||
## portal-app
|
||||
|
||||
本工程是使用 TinyEngine 低代码引擎搭建之后得到的出码工程。
|
||||
|
||||
## 使用
|
||||
|
||||
安装依赖:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
本地启动项目:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
|
|
@ -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>
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
import en_US from './en_US.json'
|
||||
import zh_CN from './zh_CN.json'
|
||||
|
||||
export default { en_US, zh_CN }
|
|
@ -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"
|
||||
}
|
|
@ -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 () => {}
|
|
@ -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
|
|
@ -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}"
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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 }
|
|
@ -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')
|
|
@ -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
|
||||
})
|
|
@ -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 }
|
|
@ -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>
|
|
@ -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上限240,IOPS突发上限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上限600,IOPS突发上限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>
|
|
@ -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
|
||||
}
|
||||
})
|
|
@ -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
|
@ -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') }"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
})
|
|
@ -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"
|
||||
}
|
||||
]
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
Loading…
Reference in New Issue