Skip to content

Commit f45f686

Browse files
feat(preview): support app preview (opentiny#1628)
1 parent 01722bc commit f45f686

15 files changed

Lines changed: 318 additions & 126 deletions

File tree

packages/common/component/ToolbarBase.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,19 @@ export default {
3232
options: {
3333
type: Object,
3434
default: () => ({})
35+
},
36+
trigger: {
37+
type: String,
38+
default: 'hover'
3539
}
3640
},
3741
emits: ['click-api'],
3842
setup(props, { emit }) {
3943
const state = reactive({
4044
icon: computed(() => props.icon),
4145
content: computed(() => props.content),
42-
options: computed(() => props.options)
46+
options: computed(() => props.options),
47+
trigger: computed(() => props.trigger)
4348
})
4449
4550
const click = () => {

packages/common/component/toolbar-built-in/ToolbarBaseIcon.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
<template>
22
<tiny-popover
3-
trigger="hover"
3+
:trigger="trigger"
44
:open-delay="1000"
55
popper-class="toolbar-right-popover"
66
append-to-body
77
:content="content"
88
>
9+
<slot></slot>
910
<template #reference>
1011
<span class="icon">
1112
<span class="icon-hides" v-bind="$attrs">
@@ -36,6 +37,10 @@ export default {
3637
options: {
3738
type: Object,
3839
default: () => ({})
40+
},
41+
trigger: {
42+
type: String,
43+
default: 'hover'
3944
}
4045
}
4146
}

packages/common/js/preview.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,8 @@ const getQueryParams = (params = {}, isHistory = false) => {
255255
query += `&history=${params.history}`
256256
}
257257

258+
query += `&previewType=${params.previewType}`
259+
258260
return query
259261
}
260262

packages/design-core/src/preview/src/preview/http.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,5 @@ export const getPageById = async (id) => getMetaApi(META_SERVICE.Http).get(`/app
4444
export const getBlockById = async (id) => getMetaApi(META_SERVICE.Http).get(`/material-center/api/block/detail/${id}`)
4545
export const fetchPageHistory = (pageId) =>
4646
getMetaApi(META_SERVICE.Http).get(`/app-center/api/pages/histories?page=${pageId}`)
47+
48+
export const fetchPageList = (appId) => getMetaApi(META_SERVICE.Http).get(`/app-center/api/pages/list/${appId}`)

packages/design-core/src/preview/src/preview/usePreviewData.ts

Lines changed: 262 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@ import { reactive } from 'vue'
22
import { constants } from '@opentiny/tiny-engine-utils'
33
import { getImportMap as getInitImportMap } from './importMap'
44
import { getMetaApi, getMergeMeta, META_SERVICE } from '@opentiny/tiny-engine-meta-register'
5-
import { fetchMetaData, fetchAppSchema, fetchBlockSchema, getPageById, getBlockById, fetchPageHistory } from './http'
5+
import {
6+
fetchMetaData,
7+
fetchAppSchema,
8+
fetchBlockSchema,
9+
getPageById,
10+
getBlockById,
11+
fetchPageHistory,
12+
fetchPageList
13+
} from './http'
614
import generateMetaFiles, { processAppJsCode } from './generate'
715
import srcFiles from './srcFiles'
816

@@ -350,59 +358,272 @@ export const usePreviewData = ({ setFiles, store, setImportMap }: IUsePreviewDat
350358
scripts: Record<string, string>
351359
styles: string[]
352360
}) => {
353-
const { appData, metaData, importMapData } = await getBasicData(basicFiles, params.scripts)
361+
const searchParams = new URLSearchParams(location.search)
362+
const previewType = searchParams.get('previewType')
354363

355-
previewState.currentPage = params.currentPage
356-
previewState.ancestors = params.ancestors
364+
if (previewType === 'page') {
365+
const { appData, metaData, importMapData } = await getBasicData(basicFiles, params.scripts)
357366

358-
// importMap 发生变化才更新 importMap
359-
if (JSON.stringify(previewState.importMap) !== JSON.stringify(importMapData)) {
360-
setImportMap(importMapData)
361-
previewState.importMap = importMapData
362-
}
367+
previewState.currentPage = params.currentPage
368+
previewState.ancestors = params.ancestors
369+
370+
// importMap 发生变化才更新 importMap
371+
if (JSON.stringify(previewState.importMap) !== JSON.stringify(importMapData)) {
372+
setImportMap(importMapData)
373+
previewState.importMap = importMapData
374+
}
363375

364-
const blockSet = new Set()
376+
const blockSet = new Set()
365377

366-
let blocks = []
367-
const { getAllNestedBlocksSchema, generatePageCode } = getMetaApi('engine.service.generateCode')
378+
let blocks = []
379+
const { getAllNestedBlocksSchema, generatePageCode } = getMetaApi('engine.service.generateCode')
368380

369-
if (params.ancestors?.length) {
370-
const promises = params.ancestors.map((item) =>
371-
getAllNestedBlocksSchema(item.page_content, fetchBlockSchema, blockSet)
381+
if (params.ancestors?.length) {
382+
const promises = params.ancestors.map((item) =>
383+
getAllNestedBlocksSchema(item.page_content, fetchBlockSchema, blockSet)
384+
)
385+
blocks = (await Promise.all(promises)).flat()
386+
}
387+
388+
const currentPageBlocks = await getAllNestedBlocksSchema(
389+
params.currentPage?.page_content || {},
390+
fetchBlockSchema,
391+
blockSet
372392
)
373-
blocks = (await Promise.all(promises)).flat()
374-
}
393+
blocks = blocks.concat(currentPageBlocks)
394+
const pageCode = [
395+
...getPageAncestryFiles(appData, params),
396+
...(blocks || []).map((blockSchema) => {
397+
return {
398+
panelName: `${blockSchema.fileName}.vue`,
399+
panelValue: generatePageCode(blockSchema, appData?.componentsMap || [], { blockRelativePath: './' }) || '',
400+
panelType: 'vue'
401+
}
402+
})
403+
]
404+
405+
const newFiles = store.getFiles()
406+
const enableTailwindCSS = getMergeMeta('engine.config')?.enableTailwindCSS
407+
const appJsCode = processAppJsCode(newFiles['app.js'] || '', params.styles, enableTailwindCSS)
408+
409+
newFiles['app.js'] = appJsCode
410+
pageCode.forEach((item) => assignFiles(item, newFiles))
411+
412+
const metaFiles = generateMetaFiles(metaData)
413+
Object.assign(newFiles, metaFiles)
414+
setFiles(newFiles, 'App.vue')
415+
} else if (previewType === 'app') {
416+
const appId = searchParams.get('id')
417+
const { getAllNestedBlocksSchema, generateAppCode } = getMetaApi('engine.service.generateCode')
418+
const importMap = await getImportMap(JSON.parse(searchParams.get('scripts') || '{}'))
419+
420+
let appSchema
421+
422+
const getPreGenerateInfo = async () => {
423+
const promises = [
424+
getMetaApi(META_SERVICE.Http).get(`/app-center/v1/api/apps/schema/${appId}`),
425+
fetchPageList(appId)
426+
]
427+
428+
const [appData, pageList] = await Promise.all(promises)
429+
const pageDetailList = pageList
430+
431+
// 这里需要手动传入 blockSet 的原因是多页面可能会存在重复的区块
432+
const blockSet = new Set()
433+
const list = pageDetailList.map((page) =>
434+
getAllNestedBlocksSchema(page.page_content, fetchBlockSchema, blockSet)
435+
)
436+
const blocks = await Promise.allSettled(list)
437+
438+
const blockSchema = []
439+
blocks.forEach((item) => {
440+
if (item.status === 'fulfilled' && Array.isArray(item.value)) {
441+
blockSchema.push(...item.value)
442+
}
443+
})
375444

376-
const currentPageBlocks = await getAllNestedBlocksSchema(
377-
params.currentPage?.page_content || {},
378-
fetchBlockSchema,
379-
blockSet
380-
)
381-
blocks = blocks.concat(currentPageBlocks)
382-
383-
const pageCode = [
384-
...getPageAncestryFiles(appData, params),
385-
...(blocks || []).map((blockSchema) => {
386-
return {
387-
panelName: `${blockSchema.fileName}.vue`,
388-
panelValue: generatePageCode(blockSchema, appData?.componentsMap || [], { blockRelativePath: './' }) || '',
389-
panelType: 'vue'
445+
appSchema = {
446+
dataSource: appData.dataSource,
447+
utils: appData.utils,
448+
i18n: appData.i18n,
449+
globalState: appData.globalState,
450+
// 页面 schema
451+
pageSchema: pageDetailList.map((item) => {
452+
const { page_content, ...meta } = item
453+
454+
return {
455+
...page_content,
456+
meta: {
457+
...meta,
458+
router: meta.route
459+
}
460+
}
461+
}),
462+
blockSchema,
463+
// 物料数据
464+
componentsMap: [...(appData.componentsMap || [])],
465+
// 物料依赖
466+
packages: [...(appData.packages || [])],
467+
meta: {
468+
...(appData.meta || {})
469+
}
390470
}
391-
})
392-
]
393471

394-
const newFiles = store.getFiles()
395-
const enableTailwindCSS = getMergeMeta('engine.config')?.enableTailwindCSS
396-
const appJsCode = processAppJsCode(newFiles['app.js'] || '', params.styles, enableTailwindCSS)
472+
const res = await generateAppCode(appSchema)
397473

398-
newFiles['app.js'] = appJsCode
474+
const { genResult = [] } = res || {}
475+
const fileRes = genResult.map(({ fileContent, fileName, path, fileType }) => {
476+
const slash = path.endsWith('/') || path === '.' ? '' : '/'
477+
let filePath = `${path}${slash}`
478+
if (filePath.startsWith('./')) {
479+
filePath = filePath.slice(2)
480+
}
481+
if (filePath.startsWith('.')) {
482+
filePath = filePath.slice(1)
483+
}
399484

400-
pageCode.forEach((item) => assignFiles(item, newFiles))
485+
if (filePath.startsWith('/')) {
486+
filePath = filePath.slice(1)
487+
}
401488

402-
const metaFiles = generateMetaFiles(metaData)
403-
Object.assign(newFiles, metaFiles)
489+
return {
490+
fileContent,
491+
filePath: `${filePath}${fileName}`,
492+
fileType
493+
}
494+
})
404495

405-
setFiles(newFiles, 'App.vue')
496+
return fileRes
497+
}
498+
499+
const buildTreeRoutes = (routes) => {
500+
const tree = []
501+
const routeMap = new Map()
502+
503+
// 首先将所有路由存入一个 Map 中,以便快速查找
504+
routes.forEach((route) => {
505+
routeMap.set(route.path, route)
506+
})
507+
508+
// 递归构建树状结构
509+
const buildTree = (route) => {
510+
const children = routes.filter((childRoute) => {
511+
return childRoute.path.startsWith(route.path) && childRoute.path !== route.path
512+
})
513+
514+
if (children.length > 0) {
515+
route.children = children.map((childRoute) => {
516+
const child = { ...childRoute }
517+
buildTree(child)
518+
return child
519+
})
520+
}
521+
}
522+
523+
// 找到所有根路由
524+
routes.forEach((route) => {
525+
if (!routes.some((otherRoute) => otherRoute.path !== route.path && route.path.startsWith(otherRoute.path))) {
526+
const root = { ...route }
527+
buildTree(root)
528+
tree.push(root)
529+
}
530+
})
531+
532+
return tree
533+
}
534+
535+
const getRoutesAndImportSet = (schema) => {
536+
const importSet = new Set()
537+
const pageSchema = (schema.pageSchema || []).sort((a, b) => a.meta?.router?.length - b.meta?.router?.length)
538+
const result = []
539+
const home = {
540+
path: '/'
541+
}
542+
let isGetHome = false
543+
pageSchema.forEach((item) => {
544+
if ((item.meta?.isHome || item.meta?.isDefault) && !isGetHome) {
545+
home.redirect = { name: `${item.meta.id}` }
546+
isGetHome = true
547+
}
548+
importSet.add(
549+
`import ${item.fileName} from './views${item.path ? `/${item.path}` : ''}/${item.fileName}.vue'`
550+
)
551+
const newNode = {
552+
path: `/${item.meta.router}`,
553+
component: item.fileName,
554+
name: `${item.meta.id}`
555+
}
556+
result.push(newNode)
557+
})
558+
if (!isGetHome) {
559+
isGetHome = true
560+
home.redirect = { name: result[0]?.name }
561+
}
562+
563+
return { routes: [home, ...buildTreeRoutes(result)], importSet }
564+
}
565+
566+
const getRouterFile = (schema) => {
567+
const { routes, importSet } = getRoutesAndImportSet(schema)
568+
const resultStr = JSON.stringify(routes, null, 2).replace(
569+
/("component":\s*)"(.*?)"/g,
570+
(match, p1, p2) => p1 + p2
571+
)
572+
573+
// TODO: 支持 hash 模式、history 模式
574+
const importSnippet = `import { createRouter, createMemoryHistory } from 'vue-router'\n${[...importSet].join(
575+
'\n'
576+
)}`
577+
const exportSnippet = `export default createRouter({history: createMemoryHistory(),routes })`
578+
579+
const routeSnippets = `const routes = ${resultStr}`
580+
581+
return `${importSnippet}\n ${routeSnippets} \n ${exportSnippet}`
582+
}
583+
584+
const formatCode = (fileContent, fileName) => {
585+
if (fileName === 'src/router/index.js') {
586+
fileContent = getRouterFile(appSchema)
587+
} else {
588+
fileContent = fileContent.replace(/(from\s*')(@)(\/.*')/g, '$1.$3')
589+
}
590+
if (fileName === 'src/App.vue') {
591+
fileContent = fileContent.replace(
592+
'<router-view></router-view>',
593+
`<tiny-select v-model="currentRoute" placeholder="请选择路由" render-type="tree" :tree-op="{ data: $router.options.routes.filter(item => item.path !== '/') }" text-field="path" value-field="path" @change="routeChange"></tiny-select>\n<router-view></router-view>`
594+
)
595+
fileContent = fileContent.replace(
596+
`import { provide } from 'vue'`,
597+
`import { Select as TinySelect } from '@opentiny/vue'\nimport { ref, provide, watchEffect } from 'vue'\nimport { useRoute, useRouter } from 'vue-router'`
598+
)
599+
fileContent = fileContent.replace(
600+
`provide(I18nInjectionKey, i18n)`,
601+
`const route = useRoute()\nconst router = useRouter()\nconst currentRoute = ref()\n\nwatchEffect(() => {\n\tcurrentRoute.value = route.path\n})\n\nconst routeChange = () => {\n\trouter.push(currentRoute.value)\n}\nprovide(I18nInjectionKey, i18n)`
602+
)
603+
}
604+
return fileContent
605+
}
606+
607+
const fileRes = await getPreGenerateInfo()
608+
const newFileRes = fileRes.filter((item) => item.filePath.includes('src/'))
609+
const srcFiles = newFileRes.reduce((prev, item) => {
610+
const fileName = item.filePath
611+
prev[fileName] = formatCode(item.fileContent, fileName)
612+
return prev
613+
}, {})
614+
srcFiles['import-map.json'] = JSON.stringify(importMap)
615+
const newFiles = store.getFiles()
616+
const enableTailwindCSS = getMergeMeta('engine.config')?.enableTailwindCSS
617+
const appJsCode = processAppJsCode(
618+
newFiles['app.js'],
619+
JSON.parse(searchParams.get('styles') || '[]'),
620+
enableTailwindCSS
621+
)
622+
srcFiles['app.js'] = appJsCode
623+
srcFiles['main.js'] = `import app from './app.js' \n ${srcFiles['src/main.js']}`
624+
srcFiles['main.js'] = srcFiles['main.js'].replace("import 'element-plus/dist/index.css'", '')
625+
setFiles(srcFiles, 'src/main.js')
626+
}
406627
}
407628

408629
const loadInitialData = async () => {

0 commit comments

Comments
 (0)