diff --git a/package-lock.json b/package-lock.json index f571eb63..f714bd41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "fes.js", - "version": "0.1.0", + "version": "2.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/fes-plugin-layout/README.md b/packages/fes-plugin-layout/README.md new file mode 100644 index 00000000..46ae75dd --- /dev/null +++ b/packages/fes-plugin-layout/README.md @@ -0,0 +1,5 @@ +主题: light/dark +布局: 左右(上/下)、上/下、上/下(左/右) +固定Header: 是/否 +固定sidebar: 是/否 +multi tabs: 是/否 diff --git a/packages/fes-plugin-layout/package.json b/packages/fes-plugin-layout/package.json index 88da06ce..bdb34f5a 100644 --- a/packages/fes-plugin-layout/package.json +++ b/packages/fes-plugin-layout/package.json @@ -15,6 +15,7 @@ "license": "MIT", "peerDependencies": { "vue": "^3.0.0", - "@webank/fes": "^2.0.0" + "@webank/fes": "^2.0.0", + "ant-design-vue": "2.0.0-rc.3" } } diff --git a/packages/fes-plugin-layout/src/helpers.js b/packages/fes-plugin-layout/src/helpers.js deleted file mode 100644 index b9743ef1..00000000 --- a/packages/fes-plugin-layout/src/helpers.js +++ /dev/null @@ -1 +0,0 @@ -export const noop = () => { }; diff --git a/packages/fes-plugin-layout/src/helpers/addAccessTag.js b/packages/fes-plugin-layout/src/helpers/addAccessTag.js new file mode 100644 index 00000000..feedb371 --- /dev/null +++ b/packages/fes-plugin-layout/src/helpers/addAccessTag.js @@ -0,0 +1,35 @@ +import { unref, computed } from 'vue'; +import { useAccess } from '@webank/fes'; + +if (!useAccess) { + throw new Error( + '[plugin-layout]: pLugin-layout依赖plugin-access,请先安装plugin-access!' + ); +} + +const hasAccess = (item) => { + let res; + if (item.path && (!item.children || item.children.length === 0)) { + res = useAccess(item.path); + } else if (item.children && item.children.length > 0) { + res = computed(() => item.children.some(child => hasAccess(child))); + } + return res; +}; + +const addAcessTag = (arr) => { + if (Array.isArray(arr)) { + arr.forEach((item) => { + item.access = hasAccess(item); + if (item.children && item.children.length > 0) { + addAcessTag(item.children); + } + }); + } +}; + +export default function (menus) { + const originData = unref(menus); + addAcessTag(originData); + return originData; +} diff --git a/packages/fes-plugin-layout/src/helpers/index.js b/packages/fes-plugin-layout/src/helpers/index.js new file mode 100644 index 00000000..484d4343 --- /dev/null +++ b/packages/fes-plugin-layout/src/helpers/index.js @@ -0,0 +1,65 @@ +export const noop = () => {}; + +const matchName = (config, name) => { + let res; + if (Array.isArray(config)) { + for (let i = 0; i < config.length; i++) { + const item = config[i]; + if (item.meta && item.meta.name === name) { + res = item.meta; + res.path = item.path; + break; + } + if (item.children && item.children.length > 0) { + res = matchName(item.children, name); + if (res) { + break; + } + } + } + } + return res; +}; + +const matchPath = (config, path) => { + let res = {}; + if (Array.isArray(config)) { + for (let i = 0; i < config.length; i++) { + const item = config[i]; + if (item.path && item.path === path) { + res = item.meta; + res.path = item.path; + break; + } + if (item.children && item.children.length > 0) { + res = matchPath(item.children, path); + if (res) { + break; + } + } + } + } + return res; +}; + +export const fillMenuData = (menuConfig, routeConfig, dep = 0) => { + dep += 1; + if (dep > 3) { + throw new Error('[plugin-layout]: menu层级不能超出三层!'); + } + const arr = []; + if (Array.isArray(menuConfig) && Array.isArray(routeConfig)) { + menuConfig.forEach((item) => { + if (item.path !== undefined && item.path !== null) { + Object.assign(item, matchPath(routeConfig, item.path)); + } else { + Object.assign(item, matchName(routeConfig, item.name)); + } + if (item.children && item.children.length > 0) { + item.children = fillMenuData(item.children, routeConfig, dep); + } + arr.push(item); + }); + } + return arr; +}; diff --git a/packages/fes-plugin-layout/src/index.js b/packages/fes-plugin-layout/src/index.js index 71a8174b..1e7eda2b 100644 --- a/packages/fes-plugin-layout/src/index.js +++ b/packages/fes-plugin-layout/src/index.js @@ -1,4 +1,4 @@ -import { readFileSync } from 'fs'; +import { readFileSync, copyFileSync, statSync } from 'fs'; import { join } from 'path'; const namespace = 'plugin-layout'; @@ -9,53 +9,55 @@ export default (api) => { } = api; api.describe({ + key: 'layout', config: { schema(joi) { - return joi.object({ - menus: joi.array() - }); + return joi.object(); }, - default: {} + onChange: api.ConfigChangeType.regenerateTmpFiles } }); - const absoluteFilePath = join(namespace, 'core.js'); - const absRuntimeFilePath = join(namespace, 'runtime.js'); api.onGenerateFiles(() => { // 文件写出 - const { menus = [] } = api.config.layout || {}; - - console.log(menus); - - // api.writeTmpFile({ - // path: absoluteFilePath, - // content: Mustache.render( - // readFileSync(join(__dirname, 'template/core.tpl'), 'utf-8'), - // { - // REPLACE_ROLES: JSON.stringify(roles) - // } - // ) - // }); + const userConfig = api.config.layout || {}; api.writeTmpFile({ path: absRuntimeFilePath, - content: readFileSync( - join(__dirname, 'template/runtime.tpl'), - 'utf-8' + content: Mustache.render( + readFileSync(join(__dirname, 'template/runtime.tpl'), 'utf-8'), + { + REPLACE_USER_CONFIG: JSON.stringify(userConfig) + } ) }); }); - // api.addPluginExports(() => [ - // { - // specifiers: ['access', 'useAccess'], - // source: absoluteFilePath - // } - // ]); + let generatedOnce = false; + api.onGenerateFiles(() => { + if (generatedOnce) return; + generatedOnce = true; + const cwd = join(__dirname, '../src'); + const files = api.utils.glob.sync('**/*', { + cwd + }); + const base = join(api.paths.absTmpPath, namespace); + files.forEach((file) => { + if (file.indexOf('template') !== -1) return; + if (file === 'index.js') return; + const source = join(cwd, file); + const target = join(base, file); + if (statSync(source).isDirectory()) { + api.utils.mkdirp.sync(target); + } else { + copyFileSync(source, target); + } + }); + }); - // api.addRuntimePluginKey(() => 'noAccessHandler'); + api.addRuntimePluginKey(() => 'layout'); - // api.addRuntimePlugin(() => `@@/${absRuntimeFilePath}`); + api.addRuntimePlugin(() => `@@/${absRuntimeFilePath}`); }; diff --git a/packages/fes-plugin-layout/src/template/runtime.tpl b/packages/fes-plugin-layout/src/template/runtime.tpl index 7fc09b78..34817e1c 100644 --- a/packages/fes-plugin-layout/src/template/runtime.tpl +++ b/packages/fes-plugin-layout/src/template/runtime.tpl @@ -1,23 +1,30 @@ +import { reactive, toRefs } from "vue"; +import { getRoutes, plugin, ApplyPluginsType } from "@@/core/coreExports"; +import BaseLayout from "./views/BaseLayout.vue"; +import { fillMenuData } from "./helpers"; + +const userConfig = reactive({{{REPLACE_USER_CONFIG}}}); export function rootContainer(childComponent, args) { - const useRuntimeConfig = - plugin.applyPlugins({ - key: "initialStateConfig", - type: ApplyPluginsType.modify, - initialValue: {}, - }) || {}; - return { - setup() { - const { loading } = useModel("@@initialState") || {}; - return () => { - if (loading.value) { - return useRuntimeConfig.loading ? ( - <useRuntimeConfig.loading /> - ) : ( - <></> - ); + const runtimeConfig = plugin.applyPlugins({ + key: "layout", + type: ApplyPluginsType.modify, + initialValue: {}, + }); + const routeConfig = getRoutes(); + userConfig.menus = fillMenuData(userConfig.menus, routeConfig); + return () => { + const slots = { + default: () => <childComponent></childComponent>, + userCenter: () => { + if(runtimeConfig.userCenter){ + return <runtimeConfig.userCenter></runtimeConfig.userCenter> } - return <childComponent />; - }; - }, + return <></> + } + }; + return ( + <BaseLayout {...userConfig} v-slots={slots}> + </BaseLayout> + ); }; } \ No newline at end of file diff --git a/packages/fes-plugin-layout/src/views/BaseLayout.vue b/packages/fes-plugin-layout/src/views/BaseLayout.vue index e69de29b..1bf08293 100644 --- a/packages/fes-plugin-layout/src/views/BaseLayout.vue +++ b/packages/fes-plugin-layout/src/views/BaseLayout.vue @@ -0,0 +1,151 @@ +<template> + <a-layout class="main-layout"> + <a-layout-sider + v-if="routeLayout" + v-model:collapsed="collapsed" + :width="sideWidth" + :class="{ collapsed: collapsed }" + collapsible + theme="dark" + class="layout-sider" + > + <div class="logo"> + <img :src="logo" class="logo-img" /> + <h1 class="logo-name">{{title}}</h1> + </div> + <Menu :menus="menus" :theme="theme" /> + </a-layout-sider> + <a-layout> + <a-layout-header v-if="routeLayout" class="layout-header"> + <slot name="userCenter"></slot> + </a-layout-header> + <a-layout-content class="layout-content"> + <slot></slot> + </a-layout-content> + <a-layout-footer v-if="routeLayout" class="layout-footer"> + Ant Design ©2020 Created by MumbleFe + </a-layout-footer> + </a-layout> + </a-layout> +</template> + +<script> +import { ref, computed } from 'vue'; +import { useRoute } from '@webank/fes'; +import Layout from 'ant-design-vue/lib/layout'; +import 'ant-design-vue/lib/layout/style'; +import Menu from './Menu.vue'; + +export default { + components: { + [Layout.name]: Layout, + [Layout.Sider.name]: Layout.Sider, + [Layout.Content.name]: Layout.Content, + [Layout.Header.name]: Layout.Header, + [Layout.Footer.name]: Layout.Footer, + Menu + }, + props: { + menus: { + type: Array, + default() { + return []; + } + }, + title: { + type: String, + default: '' + }, + locale: { + type: Boolean, + default: false + }, + logo: { + type: String, + default: '' + }, + theme: { + type: String, + default: 'dark' + }, + navigation: { + type: String, + default: 'side' // side 左右(上/下)、 top 上/下、 mixin 上/下(左/右) + }, + fixedHeader: { + type: Boolean, + default: false + }, + fixedSideBar: { + type: Boolean, + default: true + }, + multiTabs: { + type: Boolean, + default: false + }, + sideWidth: { + type: Number, + default: 200 + } + }, + setup(props, content) { + const route = useRoute(); + const routeLayout = computed(() => { + const _routeLayout = route.meta.layout; + return _routeLayout === undefined ? true : _routeLayout; + }); + return { + routeLayout, + collapsed: ref(false) + }; + } +}; +</script> + +<style lang="less"> +.main-layout { + min-height: 100vh; + .layout-sider{ + &.collapsed{ + .logo{ + justify-content: center; + .logo-name{ + display: none; + } + } + } + .logo { + height: 32px; + margin: 16px; + display: flex; + justify-content: flex-start; + align-items: center; + .logo-img{ + height: 32px; + width: auto; + } + .logo-name{ + overflow: hidden; + margin: 0 0 0 12px; + color: #fff; + font-weight: 600; + font-size: 18px; + line-height: 32px; + } + } + } + .layout-header { + height: 48px; + line-height: 48px; + background: #fff; + padding: 0; + } + .layout-content { + position: relative; + } + .layout-footer { + text-align: center; + } +} +</style> diff --git a/packages/fes-plugin-layout/src/views/Menu.vue b/packages/fes-plugin-layout/src/views/Menu.vue new file mode 100644 index 00000000..00053c21 --- /dev/null +++ b/packages/fes-plugin-layout/src/views/Menu.vue @@ -0,0 +1,104 @@ +<template> + <a-menu + :selectedKeys="selectedKeys" + @click="onMenuClick" + theme="dark" + mode="inline" + > + <template v-for="(item, index) in menus" :key="index"> + <template v-if="item.access"> + <a-sub-menu v-if="item.children" :title="item.title"> + <template + v-for="(item1, index) in item.children" + :key="index" + > + <template v-if="item1.access"> + <a-sub-menu + v-if="item1.children" + :title="item1.title" + > + <template + v-for="(item2, index) in item1.children" + :key="index" + > + <a-menu-item + v-if="item2.access" + :key="item2.path" + > + {{item2.title}} + </a-menu-item> + </template> + </a-sub-menu> + <a-menu-item v-else :key="item1.path"> + {{item1.title}} + </a-menu-item> + </template> + </template> + </a-sub-menu> + <a-menu-item v-else :key="item.path"> + <UserOutlined /> + <span>{{item.title}}</span> + </a-menu-item> + </template> + </template> + </a-menu> +</template> + +<script> +import { ref, toRefs, computed } from 'vue'; +import { useRoute, useRouter } from '@webank/fes'; +import Menu from 'ant-design-vue/lib/menu'; +import 'ant-design-vue/lib/menu/style'; +import { + UserOutlined +} from '@ant-design/icons-vue'; +import addAccessTag from '../helpers/addAccessTag'; + +export default { + components: { + [Menu.name]: Menu, + [Menu.SubMenu.name]: Menu.SubMenu, + [Menu.Item.name]: Menu.Item, + UserOutlined + }, + props: { + menus: { + type: Array, + default() { + return []; + } + }, + theme: { + type: String, + default: 'dark' + } + }, + setup(props) { + const { menus } = toRefs(props); + const route = useRoute(); + const router = useRouter(); + const fixedMenus = addAccessTag(menus); + const onMenuClick = (e) => { + const path = e.key; + if (/^https?:\/\//.test(path)) { + window.open(path, '_blank'); + } else if (/^\//.test(path)) { + router.push(path); + } else { + console.warn( + '[plugin-layout]: 菜单的path只能使以http(s)开头的网址或者路由地址' + ); + } + }; + const selectedKeys = computed(() => [route.path]); + return { + selectedKeys, + menus: fixedMenus, + onMenuClick + }; + } +}; +</script> + +<style lang="less"> +</style> diff --git a/packages/fes-plugin-layout/src/views/layout.vue b/packages/fes-plugin-layout/src/views/layout.vue deleted file mode 100644 index d31e3806..00000000 --- a/packages/fes-plugin-layout/src/views/layout.vue +++ /dev/null @@ -1,99 +0,0 @@ -<template> - <a-layout id="components-layout-demo-custom-trigger"> - <a-layout-sider - v-model:collapsed="collapsed" - :trigger="null" - collapsible - > - <div class="logo" /> - <a-menu - v-model:selectedKeys="selectedKeys" - theme="dark" - mode="inline" - > - <a-menu-item key="1"> - <user-outlined /> - <span>nav 1</span> - </a-menu-item> - <a-menu-item key="2"> - <video-camera-outlined /> - <span>nav 2</span> - </a-menu-item> - <a-menu-item key="3"> - <upload-outlined /> - <span>nav 3</span> - </a-menu-item> - </a-menu> - </a-layout-sider> - <a-layout> - <a-layout-header style="background: #fff; padding: 0"> - <menu-unfold-outlined - v-if="collapsed" - @click="() => (collapsed = !collapsed)" - class="trigger" - /> - <menu-fold-outlined - v-else - @click="() => (collapsed = !collapsed)" - class="trigger" - /> - </a-layout-header> - <a-layout-content - :style="{ - margin: '24px 16px', - padding: '24px', - background: '#fff', - minHeight: '280px' - }" - > - Content - </a-layout-content> - </a-layout> - </a-layout> -</template> - -<script> -import { - UserOutlined, - VideoCameraOutlined, - UploadOutlined, - MenuUnfoldOutlined, - MenuFoldOutlined -} from '@ant-design/icons-vue'; - -export default { - components: { - UserOutlined, - VideoCameraOutlined, - UploadOutlined, - MenuUnfoldOutlined, - MenuFoldOutlined - }, - data() { - return { - selectedKeys: ['1'], - collapsed: false - }; - } -}; -</script> - -<style> -#components-layout-demo-custom-trigger .trigger { - font-size: 18px; - line-height: 64px; - padding: 0 24px; - cursor: pointer; - transition: color 0.3s; -} - -#components-layout-demo-custom-trigger .trigger:hover { - color: #1890ff; -} - -#components-layout-demo-custom-trigger .logo { - height: 32px; - background: rgba(255, 255, 255, 0.2); - margin: 16px; -} -</style> diff --git a/packages/fes-preset-built-in/src/plugins/commands/dev/index.js b/packages/fes-preset-built-in/src/plugins/commands/dev/index.js index ad6151dd..3c03fbcf 100644 --- a/packages/fes-preset-built-in/src/plugins/commands/dev/index.js +++ b/packages/fes-preset-built-in/src/plugins/commands/dev/index.js @@ -33,7 +33,6 @@ export default (api) => { description: 'start a dev server for development', async fn({ args = {} }) { const defaultPort = process.env.PORT || args.port || api.config.devServer?.port; - console.log(api.config.devServer); port = await portfinder.getPortPromise({ port: defaultPort ? parseInt(String(defaultPort), 10) : 8000 }); diff --git a/packages/fes-preset-built-in/src/plugins/misc/route/index.js b/packages/fes-preset-built-in/src/plugins/misc/route/index.js index 2afb5d61..2855b2c1 100644 --- a/packages/fes-preset-built-in/src/plugins/misc/route/index.js +++ b/packages/fes-preset-built-in/src/plugins/misc/route/index.js @@ -267,5 +267,12 @@ export default function (api) { }); }); + api.addCoreExports(() => [ + { + specifiers: ['getRoutes'], + source: absCoreFilePath + } + ]); + api.addRuntimePlugin(() => `@@/${absRuntimeFilePath}`); } diff --git a/packages/fes-template/.fes.js b/packages/fes-template/.fes.js index 3afe65a6..8d38133d 100644 --- a/packages/fes-template/.fes.js +++ b/packages/fes-template/.fes.js @@ -1,15 +1,19 @@ -// fes.config.js 只负责管理 cli 相关的配置 +// .fes.js 只负责管理编译时配置,只能使用plain Object export default { access: { roles: { - admin: ["/"] + admin: ["/", "/onepiece"] } }, layout: { + title: "Fes.js", + logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg', menus: [{ - path: '/' + name: 'index' + }, { + name: 'onepiece' }] }, devServer: { diff --git a/packages/fes-template/src/app.js b/packages/fes-template/src/app.js index 70e65954..e837a94a 100644 --- a/packages/fes-template/src/app.js +++ b/packages/fes-template/src/app.js @@ -1,6 +1,6 @@ import { access } from '@webank/fes'; import PageLoading from '@/components/PageLoading.vue'; - +import UserCenter from '@/components/UserCenter.vue'; export const beforeRender = { loading: <PageLoading />, @@ -10,10 +10,13 @@ export const beforeRender = { setTimeout(() => { setRole('admin'); resolve({ - a: 1, - b: 2 + userName: 'harrywan' }); - }, 3000); + }, 1000); }); } }; + +export const layout = { + userCenter: <UserCenter /> +}; diff --git a/packages/fes-template/src/components/PageLoading.vue b/packages/fes-template/src/components/PageLoading.vue index bc678fd3..88c79423 100644 --- a/packages/fes-template/src/components/PageLoading.vue +++ b/packages/fes-template/src/components/PageLoading.vue @@ -4,7 +4,7 @@ </div> </template> <script> -import { Spin } from 'ant-design-vue'; +import Spin from 'ant-design-vue/lib/spin'; import 'ant-design-vue/lib/spin/style'; export default { diff --git a/packages/fes-template/src/components/UserCenter.vue b/packages/fes-template/src/components/UserCenter.vue new file mode 100644 index 00000000..e0066039 --- /dev/null +++ b/packages/fes-template/src/components/UserCenter.vue @@ -0,0 +1,15 @@ +<template> + <div>{{initialState.userName}}</div> +</template> +<script> +import { useModel } from '@webank/fes'; + +export default { + setup() { + const { initialState } = useModel('@@initialState'); + return { + initialState + }; + } +}; +</script> diff --git a/packages/fes-template/src/pages/index.vue b/packages/fes-template/src/pages/index.vue index 418cdbd3..d96cf094 100644 --- a/packages/fes-template/src/pages/index.vue +++ b/packages/fes-template/src/pages/index.vue @@ -6,8 +6,9 @@ </template> <config> { + "name": "index", "title": "首页", - "layout": "false" + "layout": false } </config> <script> diff --git a/packages/fes-template/src/pages/onepiece.vue b/packages/fes-template/src/pages/onepiece.vue index 3df0594e..063dea96 100644 --- a/packages/fes-template/src/pages/onepiece.vue +++ b/packages/fes-template/src/pages/onepiece.vue @@ -3,8 +3,8 @@ </template> <config> { - "title": "onepiece", - "layout": "true" + "name": "onepiece", + "title": "onepiece" } </config> <script>