diff --git a/mockServer/src/mock/get/app-center/v1/apps/schema/918.json b/mockServer/src/mock/get/app-center/v1/apps/schema/918.json index 28a861d55..534fbfcd1 100644 --- a/mockServer/src/mock/get/app-center/v1/apps/schema/918.json +++ b/mockServer/src/mock/get/app-center/v1/apps/schema/918.json @@ -1787,7 +1787,7 @@ }, { "componentName": "TinyPlusFrozenPage", - "package": "@opentiny/vuee", + "package": "@opentiny/vue", "exportName": "FrozenPage", "destructuring": true, "version": "3.4.1" diff --git a/packages/design-core/package.json b/packages/design-core/package.json index 6b45fe77f..ba63987a2 100644 --- a/packages/design-core/package.json +++ b/packages/design-core/package.json @@ -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:*", diff --git a/packages/design-core/src/preview/src/preview/Preview.vue b/packages/design-core/src/preview/src/preview/Preview.vue index a08723642..26ca50ccd 100644 --- a/packages/design-core/src/preview/src/preview/Preview.vue +++ b/packages/design-core/src/preview/src/preview/Preview.vue @@ -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' - }) - return title - } + 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 + } + }) + ] // [@vue/repl] `Only lang="ts" is supported for ` +} diff --git a/packages/vue-generator/src/generator/vue/sfc/generateStyle.js b/packages/vue-generator/src/generator/vue/sfc/generateStyle.js new file mode 100644 index 000000000..53fe284f0 --- /dev/null +++ b/packages/vue-generator/src/generator/vue/sfc/generateStyle.js @@ -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 `` +} diff --git a/packages/vue-generator/src/generator/vue/sfc/generateTag.js b/packages/vue-generator/src/generator/vue/sfc/generateTag.js new file mode 100644 index 000000000..b0c409717 --- /dev/null +++ b/packages/vue-generator/src/generator/vue/sfc/generateTag.js @@ -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 `` +} diff --git a/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js b/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js new file mode 100644 index 000000000..6a2820eea --- /dev/null +++ b/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js @@ -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 `` +} diff --git a/packages/vue-generator/src/generator/vue/sfc/index.js b/packages/vue-generator/src/generator/vue/sfc/index.js new file mode 100644 index 000000000..27a612c08 --- /dev/null +++ b/packages/vue-generator/src/generator/vue/sfc/index.js @@ -0,0 +1 @@ +export { default as generateSFCFile, genSFCWithDefaultPlugin } from './genSetupSFC' diff --git a/packages/vue-generator/src/generator/vue/sfc/parseImport.js b/packages/vue-generator/src/generator/vue/sfc/parseImport.js new file mode 100644 index 000000000..3c337f46b --- /dev/null +++ b/packages/vue-generator/src/generator/vue/sfc/parseImport.js @@ -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')}` +} diff --git a/packages/vue-generator/src/index.d.ts b/packages/vue-generator/src/index.d.ts new file mode 100644 index 000000000..618b83bca --- /dev/null +++ b/packages/vue-generator/src/index.d.ts @@ -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 + } + pluginConfig?: { + [k in defaultPlugins]: Record + } + customContext?: Record + } + + interface IContext { + config: Record + genResult: Array + genLogs: Array + error: Array + } + + export function generateApp(config?: IConfig): codeGenInstance + + interface codeGenInstance { + generate(IAppSchema): ICodeGenResult + } + + interface ICodeGenResult { + errors: Array + genResult: Array + genLogs: Array + } + + interface IFile { + fileType: string + fileName: string + path: string + fileContent: string + } + + interface IAppSchema { + i18n: { + en_US: Record + zh_CN: Record + } + utils: Array + dataSource: IDataSource + globalState: Array + pageSchema: Array + blockSchema: Array + componentsMap: Array + 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 + actions: Record + getters: Record + } + + interface IPageSchema { + componentName: 'Page' | 'Block' + css: string + fileName: string + lifeCycles: { + [key: string]: Record + } + methods: Record + props: Record + state: Array> + meta: { + id: number + isHome: boolean + parentId: string + rootElement: string + route: string + } + children: Array + schema?: { + properties: Array> + events: Record + } + } + + interface IFolderItem { + componentName: 'Folder' + depth: number + folderName: string + id: string + parentId: string + router: string + } + + interface ISchemaChildrenItem { + children: Array + componentName: string + id: string + props: Record + } + + interface IComponentMapItem { + componentName: string + destructuring: boolean + exportName?: string + package?: string + version: string + } + + interface IMetaInfo { + name: string + description: string + } +} diff --git a/packages/vue-generator/src/index.js b/packages/vue-generator/src/index.js index 4da0c2d49..c46e34d11 100644 --- a/packages/vue-generator/src/index.js +++ b/packages/vue-generator/src/index.js @@ -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' +/// -export { generateCode, generateBlocksCode, generatePageCode } +export { + generateCode, + generateBlocksCode, + generatePageCode, + generateApp, + CodeGenerator, + genSFCWithDefaultPlugin, + generateSFCFile +} from './generator' + +export { parseRequiredBlocks } from './utils/parseRequiredBlocks' diff --git a/packages/vue-generator/src/parser/state-type.js b/packages/vue-generator/src/parser/state-type.js index c78645d69..744ee723b 100644 --- a/packages/vue-generator/src/parser/state-type.js +++ b/packages/vue-generator/src/parser/state-type.js @@ -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 diff --git a/packages/vue-generator/src/plugins/formatCodePlugin.js b/packages/vue-generator/src/plugins/formatCodePlugin.js new file mode 100644 index 000000000..5662c41bf --- /dev/null +++ b/packages/vue-generator/src/plugins/formatCodePlugin.js @@ -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 diff --git a/packages/vue-generator/src/plugins/genBlockPlugin.js b/packages/vue-generator/src/plugins/genBlockPlugin.js new file mode 100644 index 000000000..f2b834fba --- /dev/null +++ b/packages/vue-generator/src/plugins/genBlockPlugin.js @@ -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 diff --git a/packages/vue-generator/src/plugins/genDataSourcePlugin.js b/packages/vue-generator/src/plugins/genDataSourcePlugin.js new file mode 100644 index 000000000..0b8b0c74d --- /dev/null +++ b/packages/vue-generator/src/plugins/genDataSourcePlugin.js @@ -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 diff --git a/packages/vue-generator/src/plugins/genDependenciesPlugin.js b/packages/vue-generator/src/plugins/genDependenciesPlugin.js new file mode 100644 index 000000000..da84f3b7b --- /dev/null +++ b/packages/vue-generator/src/plugins/genDependenciesPlugin.js @@ -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 diff --git a/packages/vue-generator/src/plugins/genGlobalState.js b/packages/vue-generator/src/plugins/genGlobalState.js new file mode 100644 index 000000000..4c640d6a4 --- /dev/null +++ b/packages/vue-generator/src/plugins/genGlobalState.js @@ -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 diff --git a/packages/vue-generator/src/plugins/genI18nPlugin.js b/packages/vue-generator/src/plugins/genI18nPlugin.js new file mode 100644 index 000000000..be5fd00fd --- /dev/null +++ b/packages/vue-generator/src/plugins/genI18nPlugin.js @@ -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 diff --git a/packages/vue-generator/src/plugins/genPagePlugin.js b/packages/vue-generator/src/plugins/genPagePlugin.js new file mode 100644 index 000000000..9f6fb4306 --- /dev/null +++ b/packages/vue-generator/src/plugins/genPagePlugin.js @@ -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 diff --git a/packages/vue-generator/src/plugins/genRouterPlugin.js b/packages/vue-generator/src/plugins/genRouterPlugin.js new file mode 100644 index 000000000..1a92510f4 --- /dev/null +++ b/packages/vue-generator/src/plugins/genRouterPlugin.js @@ -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 diff --git a/packages/vue-generator/src/plugins/genTemplatePlugin.js b/packages/vue-generator/src/plugins/genTemplatePlugin.js new file mode 100644 index 000000000..a48089a7a --- /dev/null +++ b/packages/vue-generator/src/plugins/genTemplatePlugin.js @@ -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 diff --git a/packages/vue-generator/src/plugins/genUtilsPlugin.js b/packages/vue-generator/src/plugins/genUtilsPlugin.js new file mode 100644 index 000000000..ba89c5265 --- /dev/null +++ b/packages/vue-generator/src/plugins/genUtilsPlugin.js @@ -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 diff --git a/packages/vue-generator/src/plugins/index.js b/packages/vue-generator/src/plugins/index.js new file mode 100644 index 000000000..1980ba602 --- /dev/null +++ b/packages/vue-generator/src/plugins/index.js @@ -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' diff --git a/packages/vue-generator/src/plugins/parseSchemaPlugin.js b/packages/vue-generator/src/plugins/parseSchemaPlugin.js new file mode 100644 index 000000000..5e62f520b --- /dev/null +++ b/packages/vue-generator/src/plugins/parseSchemaPlugin.js @@ -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 diff --git a/packages/vue-generator/src/pre-processor/index.js b/packages/vue-generator/src/pre-processor/index.js index 331910ba8..b19e70564 100644 --- a/packages/vue-generator/src/pre-processor/index.js +++ b/packages/vue-generator/src/pre-processor/index.js @@ -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' diff --git a/packages/vue-generator/src/templates/index.js b/packages/vue-generator/src/templates/index.js new file mode 100644 index 000000000..1a7cfd552 --- /dev/null +++ b/packages/vue-generator/src/templates/index.js @@ -0,0 +1,5 @@ +import { generateTemplate as genDefaultStaticTemplate } from './vue-template' + +export const templateMap = { + default: genDefaultStaticTemplate +} diff --git a/packages/vue-generator/src/templates/vue-template/index.js b/packages/vue-generator/src/templates/vue-template/index.js new file mode 100644 index 000000000..b2a5ab9b4 --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/index.js @@ -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 + } + ] +} diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/.gitignore b/packages/vue-generator/src/templates/vue-template/templateFiles/.gitignore new file mode 100644 index 000000000..9961aac9d --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/.gitignore @@ -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 \ No newline at end of file diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/README.md b/packages/vue-generator/src/templates/vue-template/templateFiles/README.md new file mode 100644 index 000000000..c653b40df --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/README.md @@ -0,0 +1,19 @@ +## $$TinyEngine{meta.name}END$ + +本工程是使用 TinyEngine 低代码引擎搭建之后得到的出码工程。 + +## 使用 + +安装依赖: + +```bash +npm install +``` + +本地启动项目: + +```bash +npm run dev +``` + + diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/genViteConfig.js b/packages/vue-generator/src/templates/vue-template/templateFiles/genViteConfig.js new file mode 100644 index 000000000..da9563182 --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/genViteConfig.js @@ -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 +} diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/index.html b/packages/vue-generator/src/templates/vue-template/templateFiles/index.html new file mode 100644 index 000000000..26aaf6d1d --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/index.html @@ -0,0 +1,13 @@ + + + + + + + $$TinyEngine{meta.name}END$ + + +
+ + + diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/packageJson.js b/packages/vue-generator/src/templates/vue-template/templateFiles/packageJson.js new file mode 100644 index 000000000..09c3944a8 --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/packageJson.js @@ -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) +} diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/src/App.vue b/packages/vue-generator/src/templates/vue-template/templateFiles/src/App.vue new file mode 100644 index 000000000..72b6032de --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/src/App.vue @@ -0,0 +1,11 @@ + + + diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/src/http/axios.js b/packages/vue-generator/src/templates/vue-template/templateFiles/src/http/axios.js new file mode 100644 index 000000000..4b2d6e420 --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/src/http/axios.js @@ -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 + } +} diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/src/http/config.js b/packages/vue-generator/src/templates/vue-template/templateFiles/src/http/config.js new file mode 100644 index 000000000..cfa3714e1 --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/src/http/config.js @@ -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 +} diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/src/http/index.js b/packages/vue-generator/src/templates/vue-template/templateFiles/src/http/index.js new file mode 100644 index 000000000..b0a08546a --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/src/http/index.js @@ -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 +} diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/bridge.js b/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/bridge.js new file mode 100644 index 000000000..7a19e4a11 --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/bridge.js @@ -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 () => {} diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/dataSource.js b/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/dataSource.js new file mode 100644 index 000000000..80e1e88e7 --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/dataSource.js @@ -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 diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/lowcode.js b/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/lowcode.js new file mode 100644 index 000000000..29da8186b --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/lowcode.js @@ -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 } +} diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/store.js b/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/store.js new file mode 100644 index 000000000..f7f39c7a8 --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/store.js @@ -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 } diff --git a/packages/vue-generator/vite.config.js b/packages/vue-generator/src/templates/vue-template/templateFiles/src/main.js similarity index 65% rename from packages/vue-generator/vite.config.js rename to packages/vue-generator/src/templates/vue-template/templateFiles/src/main.js index 96ac13f6a..c4574461b 100644 --- a/packages/vue-generator/vite.config.js +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/src/main.js @@ -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') diff --git a/packages/vue-generator/src/utils/formatCode.js b/packages/vue-generator/src/utils/formatCode.js new file mode 100644 index 000000000..c0f254ecc --- /dev/null +++ b/packages/vue-generator/src/utils/formatCode.js @@ -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 + }) +} diff --git a/packages/vue-generator/src/utils/generateImportStatement.js b/packages/vue-generator/src/utils/generateImportStatement.js new file mode 100644 index 000000000..2357ed2f9 --- /dev/null +++ b/packages/vue-generator/src/utils/generateImportStatement.js @@ -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}"` +} diff --git a/packages/vue-generator/src/utils/hasJsx.js b/packages/vue-generator/src/utils/hasJsx.js new file mode 100644 index 000000000..e270f3363 --- /dev/null +++ b/packages/vue-generator/src/utils/hasJsx.js @@ -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 + } +} diff --git a/packages/vue-generator/src/utils/index.js b/packages/vue-generator/src/utils/index.js index d9e7b1bd0..0266d85f1 100644 --- a/packages/vue-generator/src/utils/index.js +++ b/packages/vue-generator/src/utils/index.js @@ -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 diff --git a/packages/vue-generator/src/utils/mergeOptions.js b/packages/vue-generator/src/utils/mergeOptions.js new file mode 100644 index 000000000..2f83e42da --- /dev/null +++ b/packages/vue-generator/src/utils/mergeOptions.js @@ -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 +} diff --git a/packages/vue-generator/src/utils/parseRequiredBlocks.js b/packages/vue-generator/src/utils/parseRequiredBlocks.js new file mode 100644 index 000000000..5819a3536 --- /dev/null +++ b/packages/vue-generator/src/utils/parseRequiredBlocks.js @@ -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 +} diff --git a/packages/vue-generator/test/testcases/full/index.js b/packages/vue-generator/test/testcases/full/index.js index 7f9d2b8ea..47d225f6a 100644 --- a/packages/vue-generator/test/testcases/full/index.js +++ b/packages/vue-generator/test/testcases/full/index.js @@ -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) => { diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/.gitignore b/packages/vue-generator/test/testcases/generator/expected/appdemo01/.gitignore new file mode 100644 index 000000000..9961aac9d --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/.gitignore @@ -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 \ No newline at end of file diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/README.md b/packages/vue-generator/test/testcases/generator/expected/appdemo01/README.md new file mode 100644 index 000000000..275a5e79c --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/README.md @@ -0,0 +1,19 @@ +## portal-app + +本工程是使用 TinyEngine 低代码引擎搭建之后得到的出码工程。 + +## 使用 + +安装依赖: + +```bash +npm install +``` + +本地启动项目: + +```bash +npm run dev +``` + + diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/index.html b/packages/vue-generator/test/testcases/generator/expected/appdemo01/index.html new file mode 100644 index 000000000..2f10f362d --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/index.html @@ -0,0 +1,13 @@ + + + + + + + portal-app + + +
+ + + diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/package.json b/packages/vue-generator/test/testcases/generator/expected/appdemo01/package.json new file mode 100644 index 000000000..c41ca77f7 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/package.json @@ -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" + } +} diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/App.vue b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/App.vue new file mode 100644 index 000000000..72b6032de --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/App.vue @@ -0,0 +1,11 @@ + + + diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/http/axios.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/http/axios.js new file mode 100644 index 000000000..4b2d6e420 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/http/axios.js @@ -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 + } +} diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/http/config.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/http/config.js new file mode 100644 index 000000000..cfa3714e1 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/http/config.js @@ -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 +} diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/http/index.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/http/index.js new file mode 100644 index 000000000..b0a08546a --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/http/index.js @@ -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 +} diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/en_US.json b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/en_US.json new file mode 100644 index 000000000..be5c684e5 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/en_US.json @@ -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" +} diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/index.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/index.js new file mode 100644 index 000000000..f6c510b27 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/index.js @@ -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 diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/locale.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/locale.js new file mode 100644 index 000000000..75308fc75 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/locale.js @@ -0,0 +1,4 @@ +import en_US from './en_US.json' +import zh_CN from './zh_CN.json' + +export default { en_US, zh_CN } diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/zh_CN.json b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/zh_CN.json new file mode 100644 index 000000000..59357fdfc --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/zh_CN.json @@ -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" +} diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/bridge.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/bridge.js new file mode 100644 index 000000000..7a19e4a11 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/bridge.js @@ -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 () => {} diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/dataSource.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/dataSource.js new file mode 100644 index 000000000..80e1e88e7 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/dataSource.js @@ -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 diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/dataSource.json b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/dataSource.json new file mode 100644 index 000000000..73ff9cb05 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/dataSource.json @@ -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}" + } +} diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/lowcode.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/lowcode.js new file mode 100644 index 000000000..29da8186b --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/lowcode.js @@ -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 } +} diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/store.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/store.js new file mode 100644 index 000000000..f7f39c7a8 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/store.js @@ -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 } diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/main.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/main.js new file mode 100644 index 000000000..c4574461b --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/main.js @@ -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') diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/router/index.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/router/index.js new file mode 100644 index 000000000..7cd5ff8e8 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/router/index.js @@ -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 +}) diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/stores/index.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/stores/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/utils.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/utils.js new file mode 100644 index 000000000..42009b621 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/utils.js @@ -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 } diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/DemoPage.vue b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/DemoPage.vue new file mode 100644 index 000000000..a5dc80472 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/DemoPage.vue @@ -0,0 +1,25 @@ + + + + diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/createVm.vue b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/createVm.vue new file mode 100644 index 000000000..c3dfa7f81 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/createVm.vue @@ -0,0 +1,408 @@ + + + + diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/vite.config.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/vite.config.js new file mode 100644 index 000000000..e1e57978b --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/vite.config.js @@ -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 + } +}) diff --git a/packages/vue-generator/test/testcases/generator/generateApp.test.js b/packages/vue-generator/test/testcases/generator/generateApp.test.js new file mode 100644 index 000000000..8ee64baee --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/generateApp.test.js @@ -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) + }) +}) diff --git a/packages/vue-generator/test/testcases/generator/mockData.js b/packages/vue-generator/test/testcases/generator/mockData.js new file mode 100644 index 000000000..6067bbb55 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/mockData.js @@ -0,0 +1,2135 @@ +export const appSchemaDemo01 = { + dataSource: { + list: [ + { + id: 132, + name: 'getAllComponent', + data: { + data: [], + type: 'array' + }, + tpl: null, + app: '918', + desc: null, + created_at: '2022-06-28T06:26:26.000Z', + updated_at: '2022-06-28T07:02:30.000Z' + }, + { + id: 133, + name: 'getAllList', + data: { + 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) {}' + } + }, + tpl: null, + app: '918', + desc: null, + created_at: '2022-06-28T07:32:16.000Z', + updated_at: '2023-01-19T03:29:11.000Z' + }, + { + id: 135, + name: 'getAllMaterialList', + data: { + 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) {}' + } + }, + tpl: null, + app: '918', + desc: null, + created_at: '2022-06-29T00:57:50.000Z', + updated_at: '2023-05-15T02:37:12.000Z' + }, + { + id: 139, + name: 'treedata', + data: { + 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' + }, + tpl: null, + app: '918', + desc: null, + created_at: '2022-06-30T06:13:57.000Z', + updated_at: '2022-07-29T03:14:55.000Z' + }, + { + id: 150, + name: 'componentList', + data: { + 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) {}' + } + }, + tpl: null, + app: '918', + desc: null, + created_at: '2022-07-04T02:20:07.000Z', + updated_at: '2022-07-04T06:25:29.000Z' + }, + { + id: 151, + name: 'selectedComponents', + data: { + 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' + } + ] + }, + tpl: null, + app: '918', + desc: null, + created_at: '2022-07-04T03:04:05.000Z', + updated_at: '2022-07-04T03:43:40.000Z' + } + ], + dataHandler: { + type: 'JSFunction', + value: 'function dataHanlder(res){\n return res;\n}' + } + }, + globalState: [], + utils: [ + { + name: 'axios', + type: 'npm', + content: { + type: 'JSFunction', + value: '', + package: 'axios', + destructuring: false, + exportName: 'axios' + } + }, + { + name: 'Button', + type: 'npm', + content: { + package: '@opentiny/vue', + version: '', + exportName: 'Button', + subName: '', + destructuring: true, + main: '' + } + }, + { + name: 'Menu', + type: 'npm', + content: { + type: 'JSFunction', + value: '', + package: '@opentiny/vue', + exportName: 'NavMenu', + destructuring: true + } + }, + { + name: 'Modal ', + type: 'npm', + content: { + package: '@opentiny/vue', + version: '', + exportName: 'Modal ', + subName: '', + destructuring: true, + main: '' + } + }, + { + name: 'npm', + type: 'function', + content: { + type: 'JSFunction', + value: "''" + } + }, + { + name: 'Pager', + type: 'npm', + content: { + package: '@opentiny/vue', + version: '', + exportName: 'Pager', + subName: '', + destructuring: true, + main: '' + } + }, + { + name: 'test', + type: 'function', + content: { + type: 'JSFunction', + value: "function test() {\r\n return 'test'\r\n}" + } + }, + { + name: 'util', + type: 'function', + content: { + type: 'JSFunction', + value: 'function util () {\r\n console.log(321)\r\n}' + } + } + ], + i18n: { + en_US: { + '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' + }, + zh_CN: { + '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' + } + }, + pageSchema: [ + { + state: {}, + methods: {}, + componentName: 'Page', + css: '', + props: {}, + lifeCycles: {}, + children: [ + { + componentName: 'div', + props: {}, + id: '85375559', + children: [ + { + componentName: 'TinySwitch', + props: { + modelValue: '' + }, + id: '33433546' + } + ] + } + ], + dataSource: { + list: [] + }, + utils: [], + bridge: [], + inputs: [], + outputs: [], + fileName: 'DemoPage', + meta: { + name: 'DemoPage', + id: '5bhD7p5FUsUOTFRN', + app: '918', + router: 'demopage', + tenant: 1, + isBody: false, + parentId: '0', + group: 'staticPages', + depth: 0, + isPage: true, + isDefault: false, + occupier: { + id: 86, + username: '开发者', + email: 'developer@lowcode.com', + resetPasswordToken: 'developer', + confirmationToken: 'dfb2c162-351f-4f44-ad5f-8998', + is_admin: true + }, + isHome: false, + message: 'Page auto save', + _id: '5bhD7p5FUsUOTFRN' + } + }, + { + state: { + dataDisk: [1, 2, 3] + }, + methods: {}, + componentName: 'Page', + css: 'body {\r\n background-color:#eef0f5 ;\r\n margin-bottom: 80px;\r\n}', + props: {}, + children: [ + { + componentName: 'div', + props: { + style: 'padding-bottom: 10px; padding-top: 10px;' + }, + id: '2b2cabf0', + children: [ + { + componentName: 'TinyTimeLine', + props: { + active: '2', + data: [ + { + name: '基础配置' + }, + { + name: '网络配置' + }, + { + name: '高级配置' + }, + { + name: '确认配置' + } + ], + horizontal: true, + style: 'border-radius: 0px;' + }, + id: 'dd764b17' + } + ] + }, + { + componentName: 'div', + props: { + 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;' + }, + id: '30c94cc8', + children: [ + { + componentName: 'TinyForm', + props: { + labelWidth: '80px', + labelPosition: 'top', + inline: false, + 'label-position': 'left ', + 'label-width': '150px', + style: 'border-radius: 0px;' + }, + children: [ + { + componentName: 'TinyFormItem', + props: { + label: '计费模式' + }, + children: [ + { + componentName: 'TinyButtonGroup', + props: { + data: [ + { + text: '包年/包月', + value: '1' + }, + { + text: '按需计费', + value: '2' + } + ], + modelValue: '1' + }, + id: 'a8d84361' + } + ], + id: '9f39f3e7' + }, + { + componentName: 'TinyFormItem', + props: { + label: '区域' + }, + children: [ + { + componentName: 'TinyButtonGroup', + props: { + data: [ + { + text: '乌兰察布二零一', + value: '1' + } + ], + modelValue: '1', + style: 'border-radius: 0px; margin-right: 10px;' + }, + id: 'c97ccd99' + }, + { + componentName: 'Text', + props: { + text: '温馨提示:页面左上角切换区域', + style: 'background-color: [object Event]; color: #8a8e99; font-size: 12px;' + }, + id: '20923497' + }, + { + componentName: 'Text', + props: { + text: '不同区域的云服务产品之间内网互不相通;请就近选择靠近您业务的区域,可减少网络时延,提高访问速度', + style: 'display: block; color: #8a8e99; border-radius: 0px; font-size: 12px;' + }, + id: '54780a26' + } + ], + id: '4966384d' + }, + { + componentName: 'TinyFormItem', + props: { + label: '可用区', + style: 'border-radius: 0px;' + }, + children: [ + { + componentName: 'TinyButtonGroup', + props: { + data: [ + { + text: '可用区1', + value: '1' + }, + { + text: '可用区2', + value: '2' + }, + { + text: '可用区3', + value: '3' + } + ], + modelValue: '1' + }, + id: '6184481b' + } + ], + id: '690837bf' + } + ], + id: 'b6a425d4' + } + ] + }, + { + componentName: 'div', + props: { + 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;' + }, + children: [ + { + componentName: 'TinyForm', + props: { + labelWidth: '80px', + labelPosition: 'top', + inline: false, + 'label-position': 'left ', + 'label-width': '150px', + style: 'border-radius: 0px;' + }, + children: [ + { + componentName: 'TinyFormItem', + props: { + label: 'CPU架构' + }, + children: [ + { + componentName: 'TinyButtonGroup', + props: { + data: [ + { + text: 'x86计算', + value: '1' + }, + { + text: '鲲鹏计算', + value: '2' + } + ], + modelValue: '1' + }, + id: '7d33ced7' + } + ], + id: '05ed5a79' + }, + { + componentName: 'TinyFormItem', + props: { + label: '区域' + }, + children: [ + { + componentName: 'div', + props: { + style: 'display: flex; justify-content: flex-start; align-items: center;' + }, + id: '606edf78', + children: [ + { + componentName: 'div', + props: { + style: 'display: flex; align-items: center; margin-right: 10px;' + }, + id: 'f3f98246', + children: [ + { + componentName: 'Text', + props: { + text: 'vCPUs', + style: 'width: 80px;' + }, + id: 'c287437e' + }, + { + componentName: 'TinySelect', + props: { + modelValue: '', + placeholder: '请选择', + options: [ + { + value: '1', + label: '黄金糕' + }, + { + value: '2', + label: '双皮奶' + } + ] + }, + id: '4c43286b' + } + ] + }, + { + componentName: 'div', + props: { + style: 'display: flex; align-items: center; margin-right: 10px;' + }, + children: [ + { + componentName: 'Text', + props: { + text: '内存', + style: 'width: 80px; border-radius: 0px;' + }, + id: '38b8fa1f' + }, + { + componentName: 'TinySelect', + props: { + modelValue: '', + placeholder: '请选择', + options: [ + { + value: '1', + label: '黄金糕' + }, + { + value: '2', + label: '双皮奶' + } + ] + }, + id: 'cd33328e' + } + ], + id: '2b2c678f' + }, + { + componentName: 'div', + props: { + style: 'display: flex; align-items: center;' + }, + children: [ + { + componentName: 'Text', + props: { + text: '规格名称', + style: 'width: 80px;' + }, + id: 'd3eb6352' + }, + { + componentName: 'TinySearch', + props: { + modelValue: '', + placeholder: '输入关键词' + }, + id: '21cb9282' + } + ], + id: 'b8e0f35c' + } + ] + }, + { + componentName: 'div', + props: { + style: 'border-radius: 0px;' + }, + id: '5000c83e', + children: [ + { + componentName: 'TinyButtonGroup', + props: { + 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' + } + ], + modelValue: '1', + style: 'border-radius: 0px; margin-top: 12px;' + }, + id: 'b8724703' + }, + { + componentName: 'TinyGrid', + props: { + 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: '基准 / 最大带宽\t', + 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 + } + ], + style: 'margin-top: 12px; border-radius: 0px;', + 'auto-resize': true + }, + id: '77701c25' + }, + { + componentName: 'div', + props: { + style: 'margin-top: 12px; border-radius: 0px;' + }, + id: '3339838b', + children: [ + { + componentName: 'Text', + props: { + text: '当前规格', + style: 'width: 150px; display: inline-block;' + }, + id: '203b012b' + }, + { + componentName: 'Text', + props: { + text: '通用计算型 | Si2.large.2 | 2vCPUs | 4 GiB', + style: 'font-weight: 700;' + }, + id: '87723f52' + } + ] + } + ] + } + ], + id: '657fb2fc' + } + ], + id: 'd19b15cf' + } + ], + id: '9991228b' + }, + { + componentName: 'div', + props: { + 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;' + }, + children: [ + { + componentName: 'TinyForm', + props: { + labelWidth: '80px', + labelPosition: 'top', + inline: false, + 'label-position': 'left ', + 'label-width': '150px', + style: 'border-radius: 0px;' + }, + children: [ + { + componentName: 'TinyFormItem', + props: { + label: '镜像', + style: 'border-radius: 0px;' + }, + children: [ + { + componentName: 'TinyButtonGroup', + props: { + data: [ + { + text: '公共镜像', + value: '1' + }, + { + text: '私有镜像', + value: '2' + }, + { + text: '共享镜像', + value: '3' + } + ], + modelValue: '1' + }, + id: '922b14cb' + }, + { + componentName: 'div', + props: { + style: 'display: flex; margin-top: 12px; border-radius: 0px;' + }, + id: '6b679524', + children: [ + { + componentName: 'TinySelect', + props: { + modelValue: '', + placeholder: '请选择', + options: [ + { + value: '1', + label: '黄金糕' + }, + { + value: '2', + label: '双皮奶' + } + ], + style: 'width: 170px; margin-right: 10px;' + }, + id: '4851fff7' + }, + { + componentName: 'TinySelect', + props: { + modelValue: '', + placeholder: '请选择', + options: [ + { + value: '1', + label: '黄金糕' + }, + { + value: '2', + label: '双皮奶' + } + ], + style: 'width: 340px;' + }, + id: 'a7183eb7' + } + ] + }, + { + componentName: 'div', + props: { + style: 'margin-top: 12px;' + }, + id: '57aee314', + children: [ + { + componentName: 'Text', + props: { + text: '请注意操作系统的语言类型。', + style: 'color: #e37d29;' + }, + id: '56d36c27' + } + ] + } + ], + id: 'e3b02436' + } + ], + id: '59aebf2b' + } + ], + id: '87ff7b99' + }, + { + componentName: 'div', + props: { + 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;' + }, + children: [ + { + componentName: 'TinyForm', + props: { + labelWidth: '80px', + labelPosition: 'top', + inline: false, + 'label-position': 'left ', + 'label-width': '150px', + style: 'border-radius: 0px;' + }, + children: [ + { + componentName: 'TinyFormItem', + props: { + label: '系统盘', + style: 'border-radius: 0px;' + }, + children: [ + { + componentName: 'div', + props: { + style: 'display: flex;' + }, + id: 'cddba5b8', + children: [ + { + componentName: 'TinySelect', + props: { + modelValue: '', + placeholder: '请选择', + options: [ + { + value: '1', + label: '黄金糕' + }, + { + value: '2', + label: '双皮奶' + } + ], + style: 'width: 200px; margin-right: 10px;' + }, + id: 'a97fbe15' + }, + { + componentName: 'TinyInput', + props: { + placeholder: '请输入', + modelValue: '', + style: 'width: 120px; margin-right: 10px;' + }, + id: '1cde4c0f' + }, + { + componentName: 'Text', + props: { + text: 'GiB \nIOPS上限240,IOPS突发上限5,000', + style: 'color: #575d6c; font-size: 12px;' + }, + id: '2815d82d' + } + ] + } + ], + id: '50239a3a' + } + ], + id: 'e8582986' + }, + { + componentName: 'TinyForm', + props: { + labelWidth: '80px', + labelPosition: 'top', + inline: false, + 'label-position': 'left ', + 'label-width': '150px', + style: 'border-radius: 0px;' + }, + children: [ + { + componentName: 'TinyFormItem', + props: { + label: '数据盘', + style: 'border-radius: 0px;' + }, + children: [ + { + componentName: 'div', + props: { + style: 'margin-top: 12px; display: flex;' + }, + id: '728c9825', + children: [ + { + componentName: 'Icon', + props: { + style: 'margin-right: 10px; width: 16px; height: 16px;', + name: 'IconPanelMini' + }, + id: 'fded6930' + }, + { + componentName: 'TinySelect', + props: { + modelValue: '', + placeholder: '请选择', + options: [ + { + value: '1', + label: '黄金糕' + }, + { + value: '2', + label: '双皮奶' + } + ], + style: 'width: 200px; margin-right: 10px;' + }, + id: '62734e3f' + }, + { + componentName: 'TinyInput', + props: { + placeholder: '请输入', + modelValue: '', + style: 'width: 120px; margin-right: 10px;' + }, + id: '667c7926' + }, + { + componentName: 'Text', + props: { + text: 'GiB \nIOPS上限600,IOPS突发上限5,000', + style: 'color: #575d6c; font-size: 12px; margin-right: 10px;' + }, + id: 'e7bc36d6' + }, + { + componentName: 'TinyInput', + props: { + placeholder: '请输入', + modelValue: '', + style: 'width: 120px;' + }, + id: '1bd56dc0' + } + ], + loop: { + type: 'JSExpression', + value: 'this.state.dataDisk' + } + }, + { + componentName: 'div', + props: { + style: 'display: flex; margin-top: 12px; border-radius: 0px;' + }, + children: [ + { + componentName: 'Icon', + props: { + name: 'IconPlus', + style: 'width: 16px; height: 16px; margin-right: 10px;' + }, + id: '65c89f2b' + }, + { + componentName: 'Text', + props: { + text: '增加一块数据盘', + style: 'font-size: 12px; border-radius: 0px; margin-right: 10px;' + }, + id: 'cb344071' + }, + { + componentName: 'Text', + props: { + text: '您还可以挂载 21 块磁盘(云硬盘)', + style: 'color: #8a8e99; font-size: 12px;' + }, + id: '80eea996' + } + ], + id: 'e9e530ab' + } + ], + id: '078e03ef' + } + ], + id: 'ccef886e' + } + ], + id: '0fb7bd74' + }, + { + componentName: 'div', + props: { + 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;' + }, + children: [ + { + componentName: 'TinyForm', + props: { + labelWidth: '80px', + labelPosition: 'top', + inline: false, + 'label-position': 'left ', + 'label-width': '150px', + style: 'border-radius: 0px;' + }, + children: [], + id: '21ed4475' + }, + { + componentName: 'TinyRow', + props: { + style: 'border-radius: 0px; height: 100%;' + }, + children: [ + { + componentName: 'TinyCol', + props: { + span: '8' + }, + id: 'b9d051a5', + children: [ + { + componentName: 'TinyRow', + props: { + style: 'border-radius: 0px;' + }, + children: [ + { + componentName: 'TinyCol', + props: { + span: '5', + style: 'display: flex;' + }, + id: '02352776', + children: [ + { + componentName: 'Text', + props: { + text: '购买量', + style: 'margin-right: 10px;' + }, + id: '0cd9ed5c' + }, + { + componentName: 'TinyInput', + props: { + placeholder: '请输入', + modelValue: '', + style: 'width: 120px; margin-right: 10px;' + }, + id: '2f9cf442' + }, + { + componentName: 'Text', + props: { + text: '台' + }, + id: 'facd4481' + } + ] + }, + { + componentName: 'TinyCol', + props: { + span: '7' + }, + id: '82b6c659', + children: [ + { + componentName: 'div', + props: {}, + id: '9cd65874', + children: [ + { + componentName: 'Text', + props: { + text: '配置费用', + style: 'font-size: 12px;' + }, + id: 'b5a0a0da' + }, + { + componentName: 'Text', + props: { + text: '¥1.5776', + style: 'padding-left: 10px; padding-right: 10px; color: #de504e;' + }, + id: 'd9464214' + }, + { + componentName: 'Text', + props: { + text: '/小时', + style: 'font-size: 12px;' + }, + id: 'af7cc5e6' + } + ] + }, + { + componentName: 'div', + props: {}, + id: '89063830', + children: [ + { + componentName: 'Text', + props: { + text: '参考价格,具体扣费请以账单为准。', + style: 'font-size: 12px; border-radius: 0px;' + }, + id: 'd8995fbc' + }, + { + componentName: 'Text', + props: { + text: '了解计费详情', + style: 'font-size: 12px; color: #344899;' + }, + id: 'b383c3e2' + } + ] + } + ] + } + ], + id: '94fc0e43' + } + ] + }, + { + componentName: 'TinyCol', + props: { + span: '4', + style: + 'display: flex; flex-direction: row-reverse; border-radius: 0px; height: 100%; justify-content: flex-start; align-items: center;' + }, + id: '10b73009', + children: [ + { + componentName: 'TinyButton', + props: { + text: '下一步: 网络配置', + type: 'danger', + style: 'max-width: unset;' + }, + id: '0b584011' + } + ] + } + ], + id: 'd414a473' + } + ], + id: 'e8ec029b' + } + ], + fileName: 'createVm', + meta: { + name: 'createVm', + id: 'NTJ4MjvqoVj8OVsc', + app: '918', + router: 'createVm', + tenant: 1, + isBody: false, + parentId: '0', + group: 'staticPages', + depth: 0, + isPage: true, + isDefault: false, + occupier: { + id: 86, + username: '开发者', + email: 'developer@lowcode.com', + resetPasswordToken: 'developer', + confirmationToken: 'dfb2c162-351f-4f44-ad5f-8998', + is_admin: true + }, + isHome: false, + _id: 'NTJ4MjvqoVj8OVsc' + } + } + ], + componentsMap: [ + { + componentName: 'TinyCarouselItem', + package: '@opentiny/vue', + exportName: 'CarouselItem', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyCheckboxButton', + package: '@opentiny/vue', + exportName: 'CheckboxButton', + destructuring: true, + version: '0.1.17' + }, + { + componentName: 'TinyTree', + package: '@opentiny/vue', + exportName: 'Tree', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyPopover', + package: '@opentiny/vue', + exportName: 'Popover', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyTooltip', + package: '@opentiny/vue', + exportName: 'Tooltip', + destructuring: true, + version: '3.2.0' + }, + { + componentName: 'TinyCol', + package: '@opentiny/vue', + exportName: 'Col', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyDropdownItem', + package: '@opentiny/vue', + exportName: 'DropdownItem', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyPager', + package: '@opentiny/vue', + exportName: 'Pager', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyPlusAccessdeclined', + package: '@opentiny/vue', + exportName: 'AccessDeclined', + destructuring: true, + version: '3.4.1' + }, + { + componentName: 'TinyPlusFrozenPage', + package: '@opentiny/vue', + exportName: 'FrozenPage', + destructuring: true, + version: '3.4.1' + }, + { + componentName: 'TinyPlusNonSupportRegion', + package: '@opentiny/vue', + exportName: 'NonSupportRegion', + destructuring: true, + version: '3.4.1' + }, + { + componentName: 'TinyPlusBeta', + package: '@opentiny/vue', + exportName: 'Beta', + destructuring: true, + version: '3.4.1' + }, + { + componentName: 'TinySearch', + package: '@opentiny/vue', + exportName: 'Search', + destructuring: true, + version: '0.1.13' + }, + { + componentName: 'TinyRow', + package: '@opentiny/vue', + exportName: 'Row', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyFormItem', + package: '@opentiny/vue', + exportName: 'FormItem', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyAlert', + package: '@opentiny/vue', + exportName: 'Alert', + destructuring: true, + version: '3.2.0' + }, + { + componentName: 'TinyInput', + package: '@opentiny/vue', + exportName: 'Input', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyTabs', + package: '@opentiny/vue', + exportName: 'Tabs', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyDropdownMenu', + package: '@opentiny/vue', + exportName: 'DropdownMenu', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyDialogBox', + package: '@opentiny/vue', + exportName: 'DialogBox', + destructuring: true, + version: '3.2.0' + }, + { + componentName: 'TinySwitch', + package: '@opentiny/vue', + exportName: 'Switch', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyTimeLine', + package: '@opentiny/vue', + exportName: 'TimeLine', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyTabItem', + package: '@opentiny/vue', + exportName: 'TabItem', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyRadio', + package: '@opentiny/vue', + exportName: 'Radio', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyForm', + package: '@opentiny/vue', + exportName: 'Form', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyGrid', + package: '@opentiny/vue', + exportName: 'Grid', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyNumeric', + package: '@opentiny/vue', + exportName: 'Numeric', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyCheckboxGroup', + package: '@opentiny/vue', + exportName: 'CheckboxGroup', + destructuring: true, + version: '0.1.17' + }, + { + componentName: 'TinySelect', + package: '@opentiny/vue', + exportName: 'Select', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyButtonGroup', + package: '@opentiny/vue', + exportName: 'ButtonGroup', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyCarousel', + package: '@opentiny/vue', + exportName: 'Carousel', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyPopeditor', + package: '@opentiny/vue', + exportName: 'Popeditor', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyDatePicker', + package: '@opentiny/vue', + exportName: 'DatePicker', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyDropdown', + package: '@opentiny/vue', + exportName: 'Dropdown', + destructuring: true, + version: '0.1.20' + }, + { + componentName: 'TinyChartHistogram', + package: '@opentiny/vue', + exportName: 'ChartHistogram', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'PortalHome', + main: 'common/components/home', + destructuring: false, + version: '1.0.0' + }, + { + componentName: 'PreviewBlock1', + main: 'preview', + destructuring: false, + version: '1.0.0' + }, + { + componentName: 'PortalHeader', + main: 'common', + destructuring: false, + version: '1.0.0' + }, + { + componentName: 'PortalBlock', + main: 'portal', + destructuring: false, + version: '1.0.0' + }, + { + componentName: 'PortalPermissionBlock', + main: '', + destructuring: false, + version: '1.0.0' + } + ], + meta: { + name: 'portal-app', + tenant: 1, + git_group: '', + project_name: '', + description: 'demo应用', + branch: 'develop', + is_demo: null, + global_state: [], + appId: '918', + creator: '', + gmt_create: '2022-06-08 03:19:01', + gmt_modified: '2023-08-23 10:22:28' + } +} diff --git a/packages/vue-generator/test/testcases/sfc/case01/blocks.schema.json b/packages/vue-generator/test/testcases/sfc/case01/blocks.schema.json new file mode 100644 index 000000000..f6824ce7a --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case01/blocks.schema.json @@ -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') }" + } + } +} diff --git a/packages/vue-generator/test/testcases/sfc/case01/case01.test.js b/packages/vue-generator/test/testcases/sfc/case01/case01.test.js new file mode 100644 index 000000000..22d3c6509 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case01/case01.test.js @@ -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') +}) diff --git a/packages/vue-generator/test/testcases/sfc/case01/componentsMap.json b/packages/vue-generator/test/testcases/sfc/case01/componentsMap.json new file mode 100644 index 000000000..2f62c77fb --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case01/componentsMap.json @@ -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" + } +] diff --git a/packages/vue-generator/test/testcases/sfc/case01/expected/FormTable.vue b/packages/vue-generator/test/testcases/sfc/case01/expected/FormTable.vue new file mode 100644 index 000000000..e262db8ce --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case01/expected/FormTable.vue @@ -0,0 +1,332 @@ + + + + diff --git a/packages/vue-generator/test/testcases/sfc/case01/expected/ImageTitle.vue b/packages/vue-generator/test/testcases/sfc/case01/expected/ImageTitle.vue new file mode 100644 index 000000000..48c6f24fd --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case01/expected/ImageTitle.vue @@ -0,0 +1,70 @@ + + + + diff --git a/packages/vue-generator/test/testcases/sfc/case01/schema.json b/packages/vue-generator/test/testcases/sfc/case01/schema.json new file mode 100644 index 000000000..a0d384e9d --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case01/schema.json @@ -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}" + } + } +} diff --git a/packages/vue-generator/test/testcases/sfc/case02/blocks.schema.json b/packages/vue-generator/test/testcases/sfc/case02/blocks.schema.json new file mode 100644 index 000000000..517e48a73 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case02/blocks.schema.json @@ -0,0 +1,150 @@ +{ + "componentName": "Block", + "fileName": "PropAccessor", + "css": "", + "props": {}, + "state": { + "firstName": "", + "lastName": "" + }, + "methods": {}, + "lifeCycles": {}, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "姓" + }, + "children": [ + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入姓氏", + "modelValue": { + "type": "JSExpression", + "value": "this.state.lastName", + "model": true + } + }, + "id": "acdd0030" + } + ], + "id": "6140da6b" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "名" + }, + "children": [ + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入名字", + "modelValue": { + "type": "JSExpression", + "value": "this.state.firstName", + "model": true + } + }, + "id": "f029ce23" + } + ], + "id": "3751a68b" + }, + { + "componentName": "TinyFormItem", + "props": {}, + "children": [ + { + "componentName": "TinyButton", + "props": { + "text": "提交", + "type": "primary" + }, + "id": "d2b35138" + }, + { + "componentName": "TinyButton", + "props": { + "text": "重置", + "style": "margin-left: 10px" + }, + "id": "89f92a92" + } + ], + "id": "2e93998e" + } + ], + "id": "88ecfcff" + } + ], + "schema": { + "properties": [ + { + "label": { + "zh_CN": "基础信息" + }, + "description": { + "zh_CN": "基础信息" + }, + "collapse": { + "number": 6, + "text": { + "zh_CN": "显示更多" + } + }, + "content": [ + { + "property": "name", + "type": "String", + "defaultValue": "", + "label": { + "text": { + "zh_CN": "全名" + } + }, + "cols": 12, + "rules": [], + "accessor": { + "getter": { + "type": "JSFunction", + "value": "function getter() {\r\n this.emit('update:name', `${this.state.firstName} ${this.state.lastName}`)\r\n}" + }, + "setter": { + "type": "JSFunction", + "value": "function setter() {\r\n const [firstName, lastName] = this.props.name.split(' ')\r\n this.state.firstName = firstName\r\n this.state.lastName = lastName\r\n}" + } + }, + "hidden": false, + "required": true, + "readOnly": false, + "disabled": false, + "widget": { + "component": "MetaInput", + "props": {} + } + } + ] + } + ], + "events": { + "onUpdate:name": { + "label": { + "zh_CN": "双向绑定的name变化时触发" + }, + "description": { + "zh_CN": "" + } + } + }, + "slots": {} + } +} diff --git a/packages/vue-generator/test/testcases/sfc/case02/case02.test.js b/packages/vue-generator/test/testcases/sfc/case02/case02.test.js new file mode 100644 index 000000000..c150d463b --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case02/case02.test.js @@ -0,0 +1,22 @@ +import { expect, test } from 'vitest' +import { genSFCWithDefaultPlugin } from '@/generator/vue/sfc' +import schema from './page.schema.json' +import componentsMap from './components-map.json' +import blockSchema from './blocks.schema.json' +import { formatCode } from '@/utils/formatCode' + +test('should generate use prop accessor correctly', async () => { + const res = genSFCWithDefaultPlugin(schema, componentsMap) + + const formattedCode = formatCode(res, 'vue') + + await expect(formattedCode).toMatchFileSnapshot('./expected/UsePropAccessor.vue') +}) + +test('should generate prop accessor correctly', async () => { + const res = genSFCWithDefaultPlugin(blockSchema, componentsMap) + + const formattedCode = formatCode(res, 'vue') + + await expect(formattedCode).toMatchFileSnapshot('./expected/PropAccessor.vue') +}) diff --git a/packages/vue-generator/test/testcases/sfc/case02/components-map.json b/packages/vue-generator/test/testcases/sfc/case02/components-map.json new file mode 100644 index 000000000..631735610 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case02/components-map.json @@ -0,0 +1,38 @@ +[ + { + "componentName": "UsePropAccessor", + "main": "./views" + }, + { + "componentName": "PropAccessor", + "main": "./components" + }, + { + "componentName": "TinyButton", + "exportName": "Button", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + }, + { + "componentName": "TinyInput", + "exportName": "Input", + "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 + } +] diff --git a/packages/vue-generator/test/testcases/sfc/case02/expected/PropAccessor.vue b/packages/vue-generator/test/testcases/sfc/case02/expected/PropAccessor.vue new file mode 100644 index 000000000..6a938b7d9 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case02/expected/PropAccessor.vue @@ -0,0 +1,46 @@ + + + + diff --git a/packages/vue-generator/test/testcases/sfc/case02/expected/UsePropAccessor.vue b/packages/vue-generator/test/testcases/sfc/case02/expected/UsePropAccessor.vue new file mode 100644 index 000000000..69bfc22bc --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case02/expected/UsePropAccessor.vue @@ -0,0 +1,26 @@ + + + + diff --git a/packages/vue-generator/test/testcases/sfc/case02/page.schema.json b/packages/vue-generator/test/testcases/sfc/case02/page.schema.json new file mode 100644 index 000000000..4f60b89ae --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case02/page.schema.json @@ -0,0 +1,46 @@ +{ + "componentName": "Page", + "fileName": "UsePropAccessor", + "css": "", + "props": {}, + "state": { + "fullName": "" + }, + "methods": {}, + "lifeCycles": {}, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "全名" + }, + "children": [ + { + "componentType": "Block", + "componentName": "PropAccessor", + "props": { + "name": { + "type": "JSExpression", + "value": "this.state.fullName", + "model": { + "prop": "name" + } + } + }, + "id": "acdd0030" + } + ], + "id": "6140da6b" + } + ], + "id": "88ecfcff" + } + ] +} diff --git a/packages/vue-generator/test/testcases/sfc/case03/blocks.schema.json b/packages/vue-generator/test/testcases/sfc/case03/blocks.schema.json new file mode 100644 index 000000000..8b0c98f28 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case03/blocks.schema.json @@ -0,0 +1,158 @@ +{ + "componentName": "Block", + "fileName": "StateAccessor", + "css": "", + "props": {}, + "state": { + "fullName": { + "defaultValue": "", + "accessor": { + "getter": { + "type": "JSFunction", + "value": "function getter() {\r\n this.state.fullName = `${this.props.firstName} ${this.props.lastName}`\r\n}" + }, + "setter": { + "type": "JSFunction", + "value": "function setter() {\r\n const [firstName, lastName] = this.state.fullName.split(' ')\r\n this.emit('update:firstName', firstName)\r\n this.emit('update:lastName', lastName)\r\n}" + } + } + } + }, + "methods": {}, + "lifeCycles": {}, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "全名" + }, + "children": [ + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入全名", + "modelValue": { + "type": "JSExpression", + "value": "this.state.fullName", + "model": true + } + }, + "id": "acdd0030" + } + ], + "id": "6140da6b" + }, + { + "componentName": "TinyFormItem", + "props": {}, + "children": [ + { + "componentName": "TinyButton", + "props": { + "text": "提交", + "type": "primary" + }, + "id": "d2b35138" + }, + { + "componentName": "TinyButton", + "props": { + "text": "重置", + "style": "margin-left: 10px" + }, + "id": "89f92a92" + } + ], + "id": "2e93998e" + } + ], + "id": "88ecfcff" + } + ], + "schema": { + "properties": [ + { + "label": { + "zh_CN": "基础信息" + }, + "description": { + "zh_CN": "基础信息" + }, + "collapse": { + "number": 6, + "text": { + "zh_CN": "显示更多" + } + }, + "content": [ + { + "property": "firstName", + "type": "String", + "defaultValue": "", + "label": { + "text": { + "zh_CN": "姓氏" + } + }, + "cols": 12, + "rules": [], + "hidden": false, + "required": true, + "readOnly": false, + "disabled": false, + "widget": { + "component": "MetaInput", + "props": {} + } + }, + { + "property": "lastName", + "type": "String", + "defaultValue": "", + "label": { + "text": { + "zh_CN": "名字" + } + }, + "cols": 12, + "rules": [], + "hidden": false, + "required": true, + "readOnly": false, + "disabled": false, + "widget": { + "component": "MetaInput", + "props": {} + } + } + ] + } + ], + "events": { + "onUpdate:firstName": { + "label": { + "zh_CN": "firstName变化时触发" + }, + "description": { + "zh_CN": "" + } + }, + "onUpdate:lastName": { + "label": { + "zh_CN": "lastName变化时触发" + }, + "description": { + "zh_CN": "" + } + } + }, + "slots": {} + } +} diff --git a/packages/vue-generator/test/testcases/sfc/case03/case03.test.js b/packages/vue-generator/test/testcases/sfc/case03/case03.test.js new file mode 100644 index 000000000..3d4cc61a0 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case03/case03.test.js @@ -0,0 +1,20 @@ +import { expect, test } from 'vitest' +import { genSFCWithDefaultPlugin } from '@/generator/vue/sfc' +import schema from './page.schema.json' +import blockSchema from './blocks.schema.json' +import componentsMap from './components-map.json' +import { formatCode } from '@/utils/formatCode' + +test('should generate useStateAccessor Correct', async () => { + const res = genSFCWithDefaultPlugin(schema, componentsMap) + const formattedCode = formatCode(res, 'vue') + + await expect(formattedCode).toMatchFileSnapshot('./expected/UseStateAccessor.vue') +}) + +test('should generate block state accessor correct', async () => { + const res = genSFCWithDefaultPlugin(blockSchema, componentsMap) + const formattedCode = formatCode(res, 'vue') + + await expect(formattedCode).toMatchFileSnapshot('./expected/StateAccessor.vue') +}) diff --git a/packages/vue-generator/test/testcases/sfc/case03/components-map.json b/packages/vue-generator/test/testcases/sfc/case03/components-map.json new file mode 100644 index 000000000..0512382b5 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case03/components-map.json @@ -0,0 +1,38 @@ +[ + { + "componentName": "UseStateAccessor", + "main": "./views" + }, + { + "componentName": "StateAccessor", + "main": "./components" + }, + { + "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": "TinyButton", + "exportName": "Button", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + }, + { + "componentName": "TinyInput", + "exportName": "Input", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + } +] diff --git a/packages/vue-generator/test/testcases/sfc/case03/expected/StateAccessor.vue b/packages/vue-generator/test/testcases/sfc/case03/expected/StateAccessor.vue new file mode 100644 index 000000000..1ecafa5fa --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case03/expected/StateAccessor.vue @@ -0,0 +1,43 @@ + + + + diff --git a/packages/vue-generator/test/testcases/sfc/case03/expected/UseStateAccessor.vue b/packages/vue-generator/test/testcases/sfc/case03/expected/UseStateAccessor.vue new file mode 100644 index 000000000..82302754a --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case03/expected/UseStateAccessor.vue @@ -0,0 +1,27 @@ + + + + diff --git a/packages/vue-generator/test/testcases/sfc/case03/page.schema.json b/packages/vue-generator/test/testcases/sfc/case03/page.schema.json new file mode 100644 index 000000000..6b1ff8dfc --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case03/page.schema.json @@ -0,0 +1,48 @@ +{ + "componentName": "Page", + "fileName": "UseStateAccessor", + "css": "", + "props": {}, + "state": { + "firstName": "", + "lastName": "" + }, + "methods": {}, + "lifeCycles": {}, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "全名" + }, + "children": [ + { + "componentType": "Block", + "componentName": "StateAccessor", + "props": { + "firstName": { + "type": "JSExpression", + "value": "this.state.firstName" + }, + "lastName": { + "type": "JSExpression", + "value": "this.state.lastName" + } + }, + "id": "acdd0030" + } + ], + "id": "6140da6b" + } + ], + "id": "88ecfcff" + } + ] +} diff --git a/packages/vue-generator/test/testcases/sfc/case04/case04.test.js b/packages/vue-generator/test/testcases/sfc/case04/case04.test.js new file mode 100644 index 000000000..677b3dd22 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case04/case04.test.js @@ -0,0 +1,12 @@ +import { expect, test } from 'vitest' +import { genSFCWithDefaultPlugin } from '@/generator/vue/sfc' +import schema from './page.schema.json' +import componentsMap from './components-map.json' +import { formatCode } from '@/utils/formatCode' + +test('should generate use utils expression Correct', async () => { + const res = genSFCWithDefaultPlugin(schema, componentsMap) + const formattedCode = formatCode(res, 'vue') + + await expect(formattedCode).toMatchFileSnapshot('./expected/UseUtils.vue') +}) diff --git a/packages/vue-generator/test/testcases/sfc/case04/components-map.json b/packages/vue-generator/test/testcases/sfc/case04/components-map.json new file mode 100644 index 000000000..0512382b5 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case04/components-map.json @@ -0,0 +1,38 @@ +[ + { + "componentName": "UseStateAccessor", + "main": "./views" + }, + { + "componentName": "StateAccessor", + "main": "./components" + }, + { + "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": "TinyButton", + "exportName": "Button", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + }, + { + "componentName": "TinyInput", + "exportName": "Input", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + } +] diff --git a/packages/vue-generator/test/testcases/sfc/case04/expected/UseUtils.vue b/packages/vue-generator/test/testcases/sfc/case04/expected/UseUtils.vue new file mode 100644 index 000000000..7870c86ef --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case04/expected/UseUtils.vue @@ -0,0 +1,33 @@ + + + + diff --git a/packages/vue-generator/test/testcases/sfc/case04/page.schema.json b/packages/vue-generator/test/testcases/sfc/case04/page.schema.json new file mode 100644 index 000000000..053c7cff8 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case04/page.schema.json @@ -0,0 +1,65 @@ +{ + "componentName": "Page", + "fileName": "UseUtils", + "css": "", + "props": {}, + "state": { + "firstName": "", + "lastName": "" + }, + "methods": {}, + "lifeCycles": {}, + "children": [ + { + "componentName": "div", + "props": {}, + "id": "45237635", + "children": [ + { + "componentName": "Text", + "props": { + "text": { + "type": "JSExpression", + "value": "this.utils.test()" + } + }, + "id": "3223a366" + } + ] + }, + { + "componentName": "div", + "props": {}, + "children": [ + { + "componentName": "Text", + "props": { + "text": { + "type": "JSExpression", + "value": "`aaaa${this.utils.test()}`" + } + }, + "id": "22665633" + } + ], + "id": "24114e53" + }, + { + "componentName": "div", + "props": {}, + "children": [ + { + "componentName": "Text", + "props": { + "text": { + "type": "JSExpression", + "value": "this.utils.test()" + } + }, + "id": "62336681" + } + ], + "id": "4492e386" + } + ] +} diff --git a/packages/vue-generator/test/testcases/sfc/case05/case05.test.js b/packages/vue-generator/test/testcases/sfc/case05/case05.test.js new file mode 100644 index 000000000..a9353f908 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case05/case05.test.js @@ -0,0 +1,12 @@ +import { expect, test } from 'vitest' +import { genSFCWithDefaultPlugin } from '@/generator/vue/sfc' +import schema from './page.schema.json' +import componentsMap from './components-map.json' +import { formatCode } from '@/utils/formatCode' + +test('should auto detect jsx declare on custom method', async () => { + const res = genSFCWithDefaultPlugin(schema, componentsMap) + const formattedCode = formatCode(res, 'vue') + + await expect(formattedCode).toMatchFileSnapshot('./expected/UseUtils.vue') +}) diff --git a/packages/vue-generator/test/testcases/sfc/case05/components-map.json b/packages/vue-generator/test/testcases/sfc/case05/components-map.json new file mode 100644 index 000000000..8cbf6ff6c --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case05/components-map.json @@ -0,0 +1,30 @@ +[ + { + "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": "TinyButton", + "exportName": "Button", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + }, + { + "componentName": "TinyInput", + "exportName": "Input", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + } +] diff --git a/packages/vue-generator/test/testcases/sfc/case05/expected/UseUtils.vue b/packages/vue-generator/test/testcases/sfc/case05/expected/UseUtils.vue new file mode 100644 index 000000000..e3e81baf4 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case05/expected/UseUtils.vue @@ -0,0 +1,34 @@ + + + + diff --git a/packages/vue-generator/test/testcases/sfc/case05/page.schema.json b/packages/vue-generator/test/testcases/sfc/case05/page.schema.json new file mode 100644 index 000000000..ae628e768 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case05/page.schema.json @@ -0,0 +1,37 @@ +{ + "componentName": "Page", + "fileName": "testJsx", + "css": "", + "props": {}, + "state": { + "firstName": "", + "lastName": "" + }, + "methods": { + "onClickNew": { + "type": "JSFunction", + "value": "function onClickNew(event) {\n this.utils.Moddal.alert({\n message: () =>
testtest
\n })\n}" + } + }, + "lifeCycles": {}, + "children": [ + { + "componentName": "div", + "props": {}, + "id": "3d443c65", + "children": [ + { + "componentName": "Text", + "props": { + "text": "TinyEngine 前端可视化设计器,为设计器开发者提供定制服务,在线构建出自己专属的设计器。", + "onClick": { + "type": "JSExpression", + "value": "this.onClickNew" + } + }, + "id": "34253314" + } + ] + } + ] +} diff --git a/packages/vue-generator/test/unit/hasJSX.test.js b/packages/vue-generator/test/unit/hasJSX.test.js new file mode 100644 index 000000000..c66b87f11 --- /dev/null +++ b/packages/vue-generator/test/unit/hasJSX.test.js @@ -0,0 +1,48 @@ +import { describe, expect, test } from 'vitest' +import { hasJsx } from '@/utils/hasJsx' + +describe('normal case', () => { + test('raw jsx should be true', () => { + expect(hasJsx('
')).toBe(true) + }) + + test('fragment jsx should be true', () => { + expect(hasJsx('<>
')).toBe(true) + }) + + test('only fragment jsx should be true', () => { + expect(hasJsx('<>helloworld')).toBe(true) + }) + + test('function with jsx should be true', () => { + expect(hasJsx('function foo() { return
hello world
}')).toBe(true) + }) + + test('params with anonymous function should be true', () => { + expect( + hasJsx( + 'function message() { this.utils.Modal.alert({ message: () =>
helloworld
}) }' + ) + ).toBe(true) + }) + + test('jsx inside script tag should be true', () => { + expect( + hasJsx( + '' + ) + ).toBe(true) + }) + + test('jsx with custom element should be true', () => { + expect(hasJsx('helloworld')).toBe(true) + }) + + test('combo example should be true', () => { + expect( + hasJsx( + '' + ) + ).toBe(true) + }) +}) diff --git a/packages/vue-generator/test/unit/parseRequiredBlocks.test.js b/packages/vue-generator/test/unit/parseRequiredBlocks.test.js new file mode 100644 index 000000000..ea5e3c258 --- /dev/null +++ b/packages/vue-generator/test/unit/parseRequiredBlocks.test.js @@ -0,0 +1,46 @@ +import { expect, test } from 'vitest' +import { parseRequiredBlocks } from '@/utils/parseRequiredBlocks' + +test('should return empty array when children is no array', () => { + expect(parseRequiredBlocks()).toStrictEqual([]) + expect(parseRequiredBlocks({})).toStrictEqual([]) + expect(parseRequiredBlocks({ children: null })).toStrictEqual([]) +}) + +test('should recursive parse children', () => { + const mockData = { + children: [ + { + componentType: 'Block', + componentName: 'Header', + children: [ + { + componentType: 'Block', + componentName: 'MenuList' + } + ] + }, + { + componentName: 'div', + children: [ + { + componentName: 'div', + children: [ + { + componentName: 'div', + children: [ + { + componentType: 'Block', + componentName: 'Container' + } + ] + } + ] + } + ] + } + ] + } + + expect(parseRequiredBlocks(mockData)).toStrictEqual(['Header', 'MenuList', 'Container']) +}) diff --git a/packages/vue-generator/test/unit/template/test/generate.test.js b/packages/vue-generator/test/unit/template/test/generate.test.js new file mode 100644 index 000000000..652647b2e --- /dev/null +++ b/packages/vue-generator/test/unit/template/test/generate.test.js @@ -0,0 +1,41 @@ +import { expect, test } from 'vitest' +import { generateTag } from '@/generator/vue/sfc/generateTag' + +test('should validate tagName', () => { + expect(generateTag('')).toBe('') +}) + +test('should generate start tag correctly', () => { + expect(generateTag('div', { isStartTag: true })).toBe('
') +}) + +test('should generate close tag correctly', () => { + expect(generateTag('div', { isStartTag: false })).toBe('
') +}) + +test('void element should generate self close tag', () => { + expect(generateTag('img')).toBe('') + expect(generateTag('input')).toBe('') + expect(generateTag('br')).toBe('
') + expect(generateTag('hr')).toBe('
') + expect(generateTag('link')).toBe('') + expect(generateTag('area')).toBe('') + expect(generateTag('base')).toBe('') + expect(generateTag('col')).toBe('') + expect(generateTag('embed')).toBe('') + expect(generateTag('meta')).toBe('') + expect(generateTag('source')).toBe('') + expect(generateTag('track')).toBe('') + expect(generateTag('wbr')).toBe('') + // should respect config + expect(generateTag('div', { isVoidElement: true })).toBe('
') +}) + +test('should default transform to hyphenate style', () => { + expect(generateTag('TinyFormItem', { isStartTag: true })).toBe('') +}) + +test('should generate attribute', () => { + const attribute = ':class=["test"] v-model="state.formItem" @click="handleClick"' + expect(generateTag('TinyFormItem', { isStartTag: true, attribute })).toBe(``) +}) diff --git a/packages/vue-generator/test/utils/logDiffResult.js b/packages/vue-generator/test/utils/logDiffResult.js new file mode 100644 index 000000000..a2fe8ef85 --- /dev/null +++ b/packages/vue-generator/test/utils/logDiffResult.js @@ -0,0 +1,32 @@ +export const logDiffResult = (result) => { + console.log( + 'Statistics - equal entries: %s, distinct entries: %s, left only entries: %s, right only entries: %s, differences: %s', + result.equal, + result.distinct, + result.left, + result.right, + result.differences + ) + + result.diffSet.forEach((dif) => { + // diff 结果相等 + if (dif.state === 'equal') { + return + } + + // diff 结果不相等 + if (dif.state === 'distinct') { + console.log(`Difference on ${dif.relativePath} ${dif.name1} please compare two files detail.`) + return + } + + // diff 结果多余文件 + if (dif.state === 'right') { + console.log(`unexpected extra file ${dif.path} ${dif.name2}.`) + + return + } + // diff 结果缺失文件 + console.log(`expect file: ${dif.path} ${dif.name1}, but result is missing.`) + }) +} diff --git a/packages/vue-generator/test/utils/logger/index.js b/packages/vue-generator/test/utils/logger/index.js index e73441da7..ebbfbb0f9 100644 --- a/packages/vue-generator/test/utils/logger/index.js +++ b/packages/vue-generator/test/utils/logger/index.js @@ -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. + * + */ const { createLogger, format, transports } = require('winston') diff --git a/packages/vue-generator/vite.config.mjs b/packages/vue-generator/vite.config.mjs new file mode 100644 index 000000000..60005cb53 --- /dev/null +++ b/packages/vue-generator/vite.config.mjs @@ -0,0 +1,48 @@ +/** + * 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 { defineConfig } from 'vite' +import path from 'path' +import { viteStaticCopy } from 'vite-plugin-static-copy' + +// https://vitejs.dev/config/ +export default defineConfig({ + test: { + exclude: ['**/result/**'], + watchExclude: ['**/result/**'] + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src') + } + }, + plugins: [ + viteStaticCopy({ + targets: [ + { + src: './src/index.d.ts', + dest: '.' + } + ] + }) + ], + build: { + lib: { + entry: path.resolve(__dirname, './src/index.js'), + formats: ['cjs', 'es'] + }, + sourcemap: true, + rollupOptions: { + external: ['@babel/parser', '@babel/traverse'] + } + } +}) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1f9173986..78b16e7e2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,5 @@ packages: - 'packages/**' - 'mockServer' + # 忽略测试文件夹中的 project + - '!packages/**/test/**' diff --git a/scripts/setup.js b/scripts/setup.js index 64a7d1f23..d32752fb7 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -1,9 +1,3 @@ const { exec } = require('child_process') -const fs = require('fs') -const path = require('path') -fs.access(path.resolve(__dirname, 'packages/vue-generator/dist'), (err) => { - if (err) { - exec('pnpm -F @opentiny/tiny-engine-dsl-vue build') - } -}) +exec('pnpm -F @opentiny/tiny-engine-dsl-vue build')