From 94c4bb6e4fa8bb713be0f9dc395c8edac8a7d6eb Mon Sep 17 00:00:00 2001
From: harrywan <445436867@qq.com>
Date: Mon, 11 Apr 2022 19:40:16 +0800
Subject: [PATCH] =?UTF-8?q?*=20feat(plugin-layout):=20layout=E6=94=AF?=
 =?UTF-8?q?=E6=8C=81=E8=BF=90=E8=A1=8C=E6=97=B6=E9=85=8D=E7=BD=AEmenus=20(?=
 =?UTF-8?q?#113)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* feat(plugin-layout): 支持运行时配置menus

* refactor(plugin-model): 去掉provide

* refactor(plugin-layout): 优化runtimeConfig,重新实现运行时menus配置方式,修复多页时setup执行两次的bug

* feat(plugin-layout): 菜单支持配置默认展开等

* refactor: 优化
---
 docs/reference/plugin/plugins/layout.md       | 53 +++++++++++++-
 packages/fes-plugin-layout/src/index.js       | 11 +--
 packages/fes-plugin-layout/src/node/helper.js | 71 +++----------------
 .../src/runtime/helpers/fillMenu.js           | 60 ++++++++++++++++
 .../src/runtime/helpers/getRuntimeConfig.js   | 22 ++++++
 .../fes-plugin-layout/src/runtime/index.tpl   | 45 ++++++++----
 .../fes-plugin-layout/src/runtime/runtime.js  | 14 +---
 .../src/runtime/views/BaseLayout.vue          | 27 ++++---
 .../src/runtime/views/MenuIcon.vue            | 36 ++++++----
 .../src/runtime/views/MultiTabProvider.vue    | 41 +++++------
 packages/fes-plugin-model/src/index.js        | 33 +++------
 .../fes-plugin-model/src/runtime/core.tpl     | 42 +++++------
 .../src/runtime/models/initialState.js        |  5 ++
 .../src/runtime/models/initialState.tpl       |  5 --
 .../fes-plugin-model/src/runtime/runtime.tpl  |  5 --
 .../fes-plugin-model/src/utils/getModels.js   |  6 +-
 .../src/plugins/generateFiles/fes/fes.tpl     |  3 +-
 .../plugins/misc/route/template/routes.tpl    |  2 +-
 packages/fes-template/.fes.js                 |  5 +-
 packages/fes-template/src/app.js              | 17 ++++-
 packages/fes-template/src/pages/editor.vue    |  1 +
 packages/fes-template/src/pages/index.vue     |  6 ++
 packages/fes-template/src/pages/store.vue     |  1 +
 23 files changed, 298 insertions(+), 213 deletions(-)
 create mode 100644 packages/fes-plugin-layout/src/runtime/helpers/fillMenu.js
 create mode 100644 packages/fes-plugin-layout/src/runtime/helpers/getRuntimeConfig.js
 create mode 100644 packages/fes-plugin-model/src/runtime/models/initialState.js
 delete mode 100644 packages/fes-plugin-model/src/runtime/models/initialState.tpl
 delete mode 100644 packages/fes-plugin-model/src/runtime/runtime.tpl

diff --git a/docs/reference/plugin/plugins/layout.md b/docs/reference/plugin/plugins/layout.md
index 238031c1..0aa320b6 100644
--- a/docs/reference/plugin/plugins/layout.md
+++ b/docs/reference/plugin/plugins/layout.md
@@ -20,7 +20,7 @@
 {
     "dependencies": {
         "@fesjs/fes": "^2.0.0",
-        "@fesjs/plugin-layout": "^2.0.0"
+        "@fesjs/plugin-layout": "^4.0.0"
     },
 }
 ```
@@ -115,7 +115,12 @@ export default {
             name: 'store'
         }, {
             name: 'simpleList'
-        }]
+        }],
+        menuConfig: {
+            defaultExpandAll: false,
+            expandedKeys: [],
+            accordion: false
+        }
     },
 ```
 
@@ -219,7 +224,19 @@ export default {
 ```
   
   - **children**:子菜单配置。
+
+#### menusConfig
+- **类型**:`Object`
   
+- **默认值**:`{}`
+
+- **详情**:菜单的配置:
+  
+  - **defaultExpandAll**:是否默认展开全部菜单。
+  
+  - **expandedKeys**:配置默认展开的菜单,需要传子项是菜单路径的数组。
+
+  - **accordion**:是否只保持一个子菜单的展开。
   
 ### 运行时配置
 在 `app.js` 中配置:
@@ -231,6 +248,38 @@ export const layout = {
 
 ```
 
+#### menus
+- **类型**:`(defaultMenus: [] )=> Ref | []`
+  
+- **详情**:运行时修改菜单,入参是默认菜单配置(.fes.js中的menu配置),需要返回一个`Ref`或者数组。
+
+```js
+import { ClusterOutlined } from '@fesjs/fes-design/icon'
+export const layout = layoutConfig => ({
+    ...layoutConfig,
+    customHeader: <UserCenter />,
+    menus: (defaultMenuData) => {
+        const menusRef = ref(defaultMenuData);
+        watch(() => layoutConfig.initialState.userName, () => {
+            menusRef.value = [{
+                name: 'store',
+                icon: <ClusterOutlined />
+            }];
+        });
+        return menusRef;
+    }
+});
+
+```
+`layoutConfig.initialState` 是 `beforeRender.action`执行后创建的应用初始状态数据。         
+
+如果菜单需要根据某些状态动态改变,则返回`Ref`,否则只需要返回数组。
+
+:::tip
+在运行时配置菜单中的icon,需要传组件本身,而不是组件的名称。
+:::tip
+
+
 #### header
 - **类型**:`String`
   
diff --git a/packages/fes-plugin-layout/src/index.js b/packages/fes-plugin-layout/src/index.js
index 14bafc45..a9417292 100644
--- a/packages/fes-plugin-layout/src/index.js
+++ b/packages/fes-plugin-layout/src/index.js
@@ -39,14 +39,9 @@ export default (api) => {
             ...(api.config.layout || {})
         };
 
-        // 路由信息
-        const routes = await api.getRoutes();
-        // 把路由的meta合并到menu配置中
-        userConfig.menus = helper.fillMenuByRoute(userConfig.menus, routes);
+        const iconNames = helper.getIconNamesFromMenu(userConfig.menus);
 
-        const icons = helper.getIconsFromMenu(userConfig.menus);
-
-        const iconsString = icons.map(
+        const iconsString = iconNames.map(
             iconName => `import { ${iconName} } from '@fesjs/fes-design/icon'`
         );
         api.writeTmpFile({
@@ -54,7 +49,7 @@ export default (api) => {
             content: `
         ${iconsString.join(';\n')}
         export default {
-            ${icons.join(',\n')}
+            ${iconNames.join(',\n')}
         }`
         });
 
diff --git a/packages/fes-plugin-layout/src/node/helper.js b/packages/fes-plugin-layout/src/node/helper.js
index d5b83208..effedb72 100644
--- a/packages/fes-plugin-layout/src/node/helper.js
+++ b/packages/fes-plugin-layout/src/node/helper.js
@@ -1,61 +1,5 @@
 
-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);
-            }
-        }
-    }
-    return res;
-};
-
-export const fillMenuByRoute = (menuConfig, routeConfig, dep = 0) => {
-    dep += 1;
-    if (dep > 3) {
-        console.warn('[plugin-layout]: 菜单层级最好不要超出三层!');
-    }
-    const arr = [];
-    if (Array.isArray(menuConfig) && Array.isArray(routeConfig)) {
-        menuConfig.forEach((menu) => {
-            const pageConfig = {};
-            if (menu.name) {
-                Object.assign(pageConfig, matchName(routeConfig, menu.name));
-            }
-            // menu的配置优先级高,当menu存在配置时,忽略页面的配置
-            Object.keys(pageConfig).forEach((prop) => {
-                if (menu[prop] === undefined || menu[prop] === null || menu[prop] === '') {
-                    menu[prop] = pageConfig[prop];
-                }
-            });
-            // 处理icon
-            if (menu.icon) {
-                const icon = menu.icon;
-                const urlReg = /^((https?|ftp|file):\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/;
-                if (typeof icon === 'string' && !((urlReg.test(icon) || icon.includes('.svg')))) {
-                    menu.icon = {
-                        type: 'icon',
-                        name: icon
-                    };
-                }
-            }
-            if (menu.children && menu.children.length > 0) {
-                menu.children = fillMenuByRoute(menu.children, routeConfig, dep);
-            }
-            arr.push(menu);
-        });
-    }
-    return arr;
-};
-
-export function getIconsFromMenu(data) {
+export function getIconNamesFromMenu(data) {
     if (!Array.isArray(data)) {
         return [];
     }
@@ -63,12 +7,19 @@ export function getIconsFromMenu(data) {
     data.forEach((item = { path: '/' }) => {
         if (item.icon) {
             const { icon } = item;
-            if (icon.type === 'icon') {
-                icons.push(icon.name);
+            // 处理icon
+            if (icon) {
+                const urlReg = /^((https?|ftp|file):\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/;
+                if (
+                    typeof icon === 'string'
+                    && !(urlReg.test(icon) || icon.includes('.svg'))
+                ) {
+                    icons.push(icon);
+                }
             }
         }
         if (item.children) {
-            icons = icons.concat(getIconsFromMenu(item.children));
+            icons = icons.concat(getIconNamesFromMenu(item.children));
         }
     });
 
diff --git a/packages/fes-plugin-layout/src/runtime/helpers/fillMenu.js b/packages/fes-plugin-layout/src/runtime/helpers/fillMenu.js
new file mode 100644
index 00000000..cbf7da55
--- /dev/null
+++ b/packages/fes-plugin-layout/src/runtime/helpers/fillMenu.js
@@ -0,0 +1,60 @@
+const getMetaByName = (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 = getMetaByName(item.children, name);
+                if (res.path) {
+                    break;
+                }
+            }
+        }
+    }
+    return res;
+};
+
+const fillMenuByRoute = (menuConfig, routeConfig, dep = 0) => {
+    dep += 1;
+    if (dep > 3) {
+        console.warn('[plugin-layout]: 菜单层级最好不要超出三层!');
+    }
+    const arr = [];
+    if (Array.isArray(menuConfig) && Array.isArray(routeConfig)) {
+        menuConfig.forEach((menu) => {
+            const pageConfig = {};
+            if (menu.name) {
+                Object.assign(
+                    pageConfig,
+                    getMetaByName(routeConfig, menu.name)
+                );
+            }
+            // menu的配置优先级高,当menu存在配置时,忽略页面的配置
+            Object.keys(pageConfig).forEach((prop) => {
+                if (
+                    menu[prop] === undefined
+                    || menu[prop] === null
+                    || menu[prop] === ''
+                ) {
+                    menu[prop] = pageConfig[prop];
+                }
+            });
+            if (menu.children && menu.children.length > 0) {
+                menu.children = fillMenuByRoute(
+                    menu.children,
+                    routeConfig,
+                    dep
+                );
+            }
+            arr.push(menu);
+        });
+    }
+    return arr;
+};
+
+export default fillMenuByRoute;
diff --git a/packages/fes-plugin-layout/src/runtime/helpers/getRuntimeConfig.js b/packages/fes-plugin-layout/src/runtime/helpers/getRuntimeConfig.js
new file mode 100644
index 00000000..c44b2797
--- /dev/null
+++ b/packages/fes-plugin-layout/src/runtime/helpers/getRuntimeConfig.js
@@ -0,0 +1,22 @@
+
+
+import { plugin, ApplyPluginsType } from '@@/core/coreExports';
+import { inject } from 'vue';
+
+let runtimeConfig;
+
+export default () => {
+    if (!runtimeConfig) {
+        runtimeConfig = plugin.applyPlugins({
+            key: 'layout',
+            type: ApplyPluginsType.modify,
+            initialValue: {
+                initialState: inject('initialState'),
+                sidebar: true,
+                header: true,
+                logo: true
+            }
+        });
+    }
+    return runtimeConfig;
+};
diff --git a/packages/fes-plugin-layout/src/runtime/index.tpl b/packages/fes-plugin-layout/src/runtime/index.tpl
index 3ffef152..683b9015 100644
--- a/packages/fes-plugin-layout/src/runtime/index.tpl
+++ b/packages/fes-plugin-layout/src/runtime/index.tpl
@@ -1,17 +1,25 @@
-import { reactive, defineComponent } from "vue";
-import { plugin, ApplyPluginsType } from "@@/core/coreExports";
-import BaseLayout from "./views/BaseLayout.vue";
+import { ref, defineComponent, computed } from 'vue';
+import { plugin, ApplyPluginsType, getRoutes } from '@@/core/coreExports';
+import BaseLayout from './views/BaseLayout.vue';
+import getRuntimeConfig from './helpers/getRuntimeConfig';
+import fillMenu from './helpers/fillMenu';
 
 const Layout = defineComponent({
     name: 'Layout',
-    setup(){
-        const userConfig = reactive({{{REPLACE_USER_CONFIG}}});
-        const runtimeConfig = plugin.applyPlugins({
-            key: "layout",
-            type: ApplyPluginsType.modify,
-            initialValue: {},
+    setup() {
+        const userConfig = {{{REPLACE_USER_CONFIG}}};
+        const runtimeConfig = getRuntimeConfig();
+        let menusRef = ref(userConfig.menus);
+        // 如果运行时配置了menus,则需要处理
+        if (runtimeConfig.menus && typeof runtimeConfig.menus === 'function') {
+            menusRef = ref(runtimeConfig.menus(userConfig.menus));
+        }
+        // 把路由的meta合并到menu配置中
+        const filledMenuRef = computed(() => {
+            return fillMenu(menusRef.value, getRoutes());
         });
-        const localeShared = plugin.getShared("locale");
+
+        const localeShared = plugin.getShared('locale');
         return () => {
             const slots = {
                 customHeader: () => {
@@ -24,14 +32,23 @@ const Layout = defineComponent({
                 },
                 locale: () => {
                     if (localeShared) {
-                        return <localeShared.SelectLang></localeShared.SelectLang>;
+                        return (
+                            <localeShared.SelectLang></localeShared.SelectLang>
+                        );
                     }
                     return null;
-                },
+                }
             };
-            return <BaseLayout locale={ localeShared ? true : false } {...userConfig} v-slots={slots}></BaseLayout>;
+            return (
+                <BaseLayout
+                    locale={localeShared ? true : false}
+                    {...userConfig}
+                    menus={filledMenuRef.value}
+                    v-slots={slots}
+                ></BaseLayout>
+            );
         };
     }
-})
+});
 
 export default Layout;
diff --git a/packages/fes-plugin-layout/src/runtime/runtime.js b/packages/fes-plugin-layout/src/runtime/runtime.js
index c676984f..4eb3273f 100644
--- a/packages/fes-plugin-layout/src/runtime/runtime.js
+++ b/packages/fes-plugin-layout/src/runtime/runtime.js
@@ -1,8 +1,8 @@
-import { plugin, ApplyPluginsType } from '@@/core/coreExports';
 // eslint-disable-next-line import/extensions
 import { access as accessApi } from '../plugin-access/core';
 import Exception404 from './views/404';
 import Exception403 from './views/403';
+import getRuntimeConfig from './helpers/getRuntimeConfig';
 
 if (!accessApi) {
     throw new Error(
@@ -30,11 +30,7 @@ export const access = memo => ({
     unAccessHandler({
         router, to, from, next
     }) {
-        const runtimeConfig = plugin.applyPlugins({
-            key: 'layout',
-            type: ApplyPluginsType.modify,
-            initialValue: {}
-        });
+        const runtimeConfig = getRuntimeConfig();
         if (runtimeConfig.unAccessHandler && typeof runtimeConfig.unAccessHandler === 'function') {
             return runtimeConfig.unAccessHandler({
                 router, to, from, next
@@ -50,11 +46,7 @@ export const access = memo => ({
     noFoundHandler({
         router, to, from, next
     }) {
-        const runtimeConfig = plugin.applyPlugins({
-            key: 'layout',
-            type: ApplyPluginsType.modify,
-            initialValue: {}
-        });
+        const runtimeConfig = getRuntimeConfig();
         if (runtimeConfig.noFoundHandler && typeof runtimeConfig.noFoundHandler === 'function') {
             return runtimeConfig.noFoundHandler({
                 router, to, from, next
diff --git a/packages/fes-plugin-layout/src/runtime/views/BaseLayout.vue b/packages/fes-plugin-layout/src/runtime/views/BaseLayout.vue
index 83065240..f10a353b 100644
--- a/packages/fes-plugin-layout/src/runtime/views/BaseLayout.vue
+++ b/packages/fes-plugin-layout/src/runtime/views/BaseLayout.vue
@@ -20,6 +20,9 @@
                     :collapsed="collapsedRef"
                     mode="vertical"
                     :inverted="theme === 'dark'"
+                    :expandedKeys="menuConfig?.expandedKeys"
+                    :defaultExpandAll="menuConfig?.defaultExpandAll"
+                    :accordion="menuConfig?.accordion"
                 />
             </f-aside>
             <f-layout
@@ -70,6 +73,9 @@
                     :menus="menus"
                     mode="horizontal"
                     :inverted="theme === 'dark'"
+                    :expandedKeys="menuConfig?.expandedKeys"
+                    :defaultExpandAll="menuConfig?.defaultExpandAll"
+                    :accordion="menuConfig?.accordion"
                 />
                 <div class="layout-header-custom">
                     <slot name="customHeader"></slot>
@@ -124,6 +130,9 @@
                         :menus="menus"
                         :collapsed="collapsedRef"
                         mode="vertical"
+                        :expandedKeys="menuConfig?.expandedKeys"
+                        :defaultExpandAll="menuConfig?.defaultExpandAll"
+                        :accordion="menuConfig?.accordion"
                     />
                 </f-aside>
                 <f-layout
@@ -146,13 +155,14 @@
 
 <script>
 import { ref, computed, onMounted } from 'vue';
-import { useRoute, plugin, ApplyPluginsType } from '@@/core/coreExports';
+import { useRoute } from '@@/core/coreExports';
 import {
     FLayout, FAside, FMain, FFooter, FHeader
 } from '@fesjs/fes-design';
 import Menu from './Menu';
 import MultiTabProvider from './MultiTabProvider';
 import defaultLogo from '../assets/logo.png';
+import getRuntimeConfig from '../helpers/getRuntimeConfig';
 
 export default {
     components: {
@@ -207,7 +217,10 @@ export default {
             type: Number,
             default: 200
         },
-        footer: String
+        footer: String,
+        menuConfig: {
+            type: Object
+        }
     },
     setup(props) {
         const headerRef = ref();
@@ -221,15 +234,7 @@ export default {
 
         const collapsedRef = ref(false);
         const route = useRoute();
-        const runtimeConfig = plugin.applyPlugins({
-            key: 'layout',
-            type: ApplyPluginsType.modify,
-            initialValue: {
-                sidebar: true,
-                header: true,
-                logo: true
-            }
-        });
+        const runtimeConfig = getRuntimeConfig();
         const routeLayout = computed(() => {
             let config;
             // meta 中 layout 默认为 true
diff --git a/packages/fes-plugin-layout/src/runtime/views/MenuIcon.vue b/packages/fes-plugin-layout/src/runtime/views/MenuIcon.vue
index ab24e975..8efd8dd8 100644
--- a/packages/fes-plugin-layout/src/runtime/views/MenuIcon.vue
+++ b/packages/fes-plugin-layout/src/runtime/views/MenuIcon.vue
@@ -1,34 +1,42 @@
 <script>
-import { ref, onBeforeMount } from 'vue';
+import { ref, onBeforeMount, isVNode } from 'vue';
 // eslint-disable-next-line import/extensions
 import Icons from '../icons';
 import { validateContent } from '../helpers/svg';
 
+const urlReg = /^((https?|ftp|file):\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/;
+const isUrlResource = name => urlReg.test(name) || name.includes('.svg');
+
 export default {
     props: {
         icon: [String, Object]
     },
     setup(props) {
-        const AIcon = ref(null);
+        const AIconComponent = ref(null);
         const AText = ref(null);
 
         onBeforeMount(() => {
-            if (props.icon && props.icon.type === 'icon') {
-                AIcon.value = Icons[props.icon.name];
-            } else {
-                fetch(props.icon).then((rsp) => {
-                    if (rsp.ok) {
-                        return rsp.text().then((svgContent) => {
-                            AText.value = validateContent(svgContent);
-                        });
-                    }
-                });
+            if (typeof props.icon === 'string') {
+                if (isUrlResource(props.icon)) {
+                    fetch(props.icon).then((rsp) => {
+                        if (rsp.ok) {
+                            return rsp.text().then((svgContent) => {
+                                AText.value = validateContent(svgContent);
+                            });
+                        }
+                    });
+                } else {
+                    AIconComponent.value = Icons[props.icon];
+                }
             }
         });
 
         return () => {
-            if (AIcon.value) {
-                return <AIcon.value />;
+            if (isVNode(props.icon)) {
+                return props.icon;
+            }
+            if (AIconComponent.value) {
+                return <AIconComponent.value />;
             }
             if (AText.value) {
                 return (
diff --git a/packages/fes-plugin-layout/src/runtime/views/MultiTabProvider.vue b/packages/fes-plugin-layout/src/runtime/views/MultiTabProvider.vue
index a7d35863..2dff56e8 100644
--- a/packages/fes-plugin-layout/src/runtime/views/MultiTabProvider.vue
+++ b/packages/fes-plugin-layout/src/runtime/views/MultiTabProvider.vue
@@ -38,17 +38,12 @@
     </template>
     <router-view v-else v-slot="{ Component, route }">
         <keep-alive :include="keepAlivePages">
-            <component
-                :is="getComponent(Component, route)"
-                :key="getPageKey(route)"
-            />
+            <component :is="getComponent(Component, route)" />
         </keep-alive>
     </router-view>
 </template>
 <script>
-import {
-    computed, onMounted, unref, ref
-} from 'vue';
+import { computed, unref, ref } from 'vue';
 import { FTabs, FTabPane, FDropdown } from '@fesjs/fes-design';
 import { ReloadOutlined, MoreOutlined } from '@fesjs/fes-design/icon';
 import { useRouter, useRoute } from '@@/core/coreExports';
@@ -68,20 +63,6 @@ export default {
         multiTabs: Boolean
     },
     setup() {
-        const route = useRoute();
-        const router = useRouter();
-        const pageList = ref([]);
-        const actions = [
-            {
-                value: 'closeOtherPage',
-                label: '关闭其他'
-            },
-            {
-                value: 'reloadPage',
-                label: '刷新当前页'
-            }
-        ];
-
         const createPage = (_route) => {
             const title = _route.meta.title;
             return {
@@ -93,11 +74,21 @@ export default {
             };
         };
 
-        const findPage = path => pageList.value.find(item => unref(item.path) === unref(path));
+        const route = useRoute();
+        const router = useRouter();
+        const pageList = ref([createPage(route)]);
+        const actions = [
+            {
+                value: 'closeOtherPage',
+                label: '关闭其他'
+            },
+            {
+                value: 'reloadPage',
+                label: '刷新当前页'
+            }
+        ];
 
-        onMounted(() => {
-            pageList.value = [createPage(route)];
-        });
+        const findPage = path => pageList.value.find(item => unref(item.path) === unref(path));
 
         router.beforeEach((to) => {
             if (!findPage(to.path)) {
diff --git a/packages/fes-plugin-model/src/index.js b/packages/fes-plugin-model/src/index.js
index 6c013f5d..d706158b 100644
--- a/packages/fes-plugin-model/src/index.js
+++ b/packages/fes-plugin-model/src/index.js
@@ -1,9 +1,6 @@
 
 import { readFileSync } from 'fs';
 import { join } from 'path';
-import { lodash, winPath } from '@fesjs/utils';
-import { getModels } from './utils/getModels';
-import { getTmpFile } from './utils/getTmpFile';
 
 const namespace = 'plugin-model';
 
@@ -13,6 +10,10 @@ export default (api) => {
         utils: { Mustache }
     } = api;
 
+    const { lodash, winPath } = require('@fesjs/utils');
+    const { getModels } = require('./utils/getModels');
+    const { getTmpFile } = require('./utils/getTmpFile');
+
     function getModelDir() {
         return api.config.singular ? 'model' : 'models';
     }
@@ -25,22 +26,16 @@ export default (api) => {
         const srcModelsPath = getModelsPath();
         return lodash.uniq([
             ...getModels(srcModelsPath)
-            // ...getModels(
-            //     paths.absPagesPath,
-            //     `**/${getModelDir()}/**/*.{js,jsx}`
-            // ),
-            // ...getModels(paths.absPagesPath, '**/*.model.{js,jsx}')
         ]);
     }
 
     const absCoreFilePath = join(namespace, 'core.js');
-    const absRuntimeFilePath = join(namespace, 'runtime.js');
-    const absInitlaStateFilePath = join(namespace, 'models/initialState.js');
+    const absInitialStateFilePath = join(namespace, 'models/initialState.js');
 
     api.register({
         key: 'addExtraModels',
         fn: () => [{
-            absPath: winPath(join(paths.absTmpPath, absInitlaStateFilePath)),
+            absPath: winPath(join(paths.absTmpPath, absInitialStateFilePath)),
             namespace: '@@initialState'
         }]
     });
@@ -63,16 +58,10 @@ export default (api) => {
             })
         });
 
-        api.writeTmpFile({
-            path: absRuntimeFilePath,
-            content: Mustache.render(readFileSync(join(__dirname, 'runtime/runtime.tpl'), 'utf-8'), {
-            })
-        });
-
-        api.writeTmpFile({
-            path: absInitlaStateFilePath,
-            content: Mustache.render(readFileSync(join(__dirname, 'runtime/models/initialState.tpl'), 'utf-8'), {
-            })
+        api.copyTmpFiles({
+            namespace,
+            path: join(__dirname, 'runtime'),
+            ignore: ['.tpl']
         });
     });
 
@@ -82,6 +71,4 @@ export default (api) => {
             source: absCoreFilePath
         }
     ]);
-
-    api.addRuntimePlugin(() => `@@/${absRuntimeFilePath}`);
 };
diff --git a/packages/fes-plugin-model/src/runtime/core.tpl b/packages/fes-plugin-model/src/runtime/core.tpl
index 5dc0ed61..679d1e03 100644
--- a/packages/fes-plugin-model/src/runtime/core.tpl
+++ b/packages/fes-plugin-model/src/runtime/core.tpl
@@ -1,39 +1,29 @@
-import { inject } from "vue";
-
-const modelKey = Symbol("plugin-model");
 
 {{{userImports}}}
 {{{extraImports}}}
 
-export const models = { 
+export const models = {
 {{#extraModels}}
     {{{extraModels}}},
 {{/extraModels}}
 {{#userModels}}
     {{{userModels}}},
 {{/userModels}}
-}
-const cache = new Map();
-
-export const install = (app)=>{
-    const useModel = (name) => {
-        const model = models[name];
-        if(model === undefined){
-            throw new Error("[plugin-model]: useModel, name is undefined.");
-        }
-        if (typeof model !== "function") {
-            throw new Error("[plugin-model]: useModel is not a function.");
-        }
-        if(!cache.has(name)){
-            cache.set(name, model())
-        }
-        return cache.get(name)
-    };
-    app.provide(modelKey, useModel);
-}
-
-export const useModel = (name) => {
-    return inject(modelKey)(name);
 };
 
 
+const cache = new Map();
+
+export const useModel = (name) => {
+    const modelFunc = models[name];
+    if (modelFunc === undefined) {
+        throw new Error('[plugin-model]: useModel, name is undefined.');
+    }
+    if (typeof modelFunc !== 'function') {
+        throw new Error('[plugin-model]: useModel is not a function.');
+    }
+    if (!cache.has(name)) {
+        cache.set(name, modelFunc());
+    }
+    return cache.get(name);
+};
diff --git a/packages/fes-plugin-model/src/runtime/models/initialState.js b/packages/fes-plugin-model/src/runtime/models/initialState.js
new file mode 100644
index 00000000..da7c6f64
--- /dev/null
+++ b/packages/fes-plugin-model/src/runtime/models/initialState.js
@@ -0,0 +1,5 @@
+import { inject } from 'vue';
+
+export default function initialStateModel() {
+    return inject('initialState');
+}
diff --git a/packages/fes-plugin-model/src/runtime/models/initialState.tpl b/packages/fes-plugin-model/src/runtime/models/initialState.tpl
deleted file mode 100644
index 0af1920b..00000000
--- a/packages/fes-plugin-model/src/runtime/models/initialState.tpl
+++ /dev/null
@@ -1,5 +0,0 @@
-import { inject, reactive } from "vue";
-
-export default function initalModel() {
-    return reactive(inject("initialState"));
-}
diff --git a/packages/fes-plugin-model/src/runtime/runtime.tpl b/packages/fes-plugin-model/src/runtime/runtime.tpl
deleted file mode 100644
index 41c090a7..00000000
--- a/packages/fes-plugin-model/src/runtime/runtime.tpl
+++ /dev/null
@@ -1,5 +0,0 @@
-import { install } from "./core";
-
-export function onAppCreated({ app }) {
-    install(app)
-}
diff --git a/packages/fes-plugin-model/src/utils/getModels.js b/packages/fes-plugin-model/src/utils/getModels.js
index cd33456f..d8175b93 100644
--- a/packages/fes-plugin-model/src/utils/getModels.js
+++ b/packages/fes-plugin-model/src/utils/getModels.js
@@ -3,13 +3,13 @@ import { getValidFiles } from '.';
 
 export function getModels(cwd, pattern) {
     const files = glob
-        .sync(pattern || '**/*.{js,jsx}', {
+        .sync(pattern || '**/*.{js,jsx,ts,tsx}', {
             cwd
         })
         .filter(
             file => !file.endsWith('.d.ts')
-        && !file.endsWith('.test.js')
-        && !file.endsWith('.test.jsx')
+                && !file.endsWith('.test.js')
+                && !file.endsWith('.test.jsx')
         );
 
     return getValidFiles(files, cwd);
diff --git a/packages/fes-preset-built-in/src/plugins/generateFiles/fes/fes.tpl b/packages/fes-preset-built-in/src/plugins/generateFiles/fes/fes.tpl
index a2ecd1d9..0f7f5f5c 100644
--- a/packages/fes-preset-built-in/src/plugins/generateFiles/fes/fes.tpl
+++ b/packages/fes-preset-built-in/src/plugins/generateFiles/fes/fes.tpl
@@ -26,7 +26,8 @@ const renderClient = (opts = {}) => {
     });
 
     const app = createApp(rootContainer);
-    app.provide("initialState", initialState);
+    // initialState是响应式的,后期可以更改
+    app.provide("initialState", reactive(initialState));
 
     plugin.applyPlugins({
         key: 'onAppCreated',
diff --git a/packages/fes-preset-built-in/src/plugins/misc/route/template/routes.tpl b/packages/fes-preset-built-in/src/plugins/misc/route/template/routes.tpl
index 6d2083d4..ff615a7c 100644
--- a/packages/fes-preset-built-in/src/plugins/misc/route/template/routes.tpl
+++ b/packages/fes-preset-built-in/src/plugins/misc/route/template/routes.tpl
@@ -1,5 +1,5 @@
 import { createRouter as createVueRouter, {{{ CREATE_HISTORY }}}, ApplyPluginsType } from '{{{ runtimePath }}}';
-import { plugin } from '@@/core/coreExports';
+import { plugin } from '@@/core/plugin';
 
 export function getRoutes() {
   const routes = {{{ routes }}};
diff --git a/packages/fes-template/.fes.js b/packages/fes-template/.fes.js
index 82c2d81d..43dfcb4b 100644
--- a/packages/fes-template/.fes.js
+++ b/packages/fes-template/.fes.js
@@ -72,7 +72,10 @@ export default {
             {
                 name: 'pinia'
             }
-        ]
+        ],
+        menuConfig: {
+            defaultExpandAll: false
+        }
     },
     locale: {
         legacy: true
diff --git a/packages/fes-template/src/app.js b/packages/fes-template/src/app.js
index 167601b7..c93adcad 100644
--- a/packages/fes-template/src/app.js
+++ b/packages/fes-template/src/app.js
@@ -4,6 +4,7 @@ import { access as accessApi, pinia } from '@fesjs/fes';
 import PageLoading from '@/components/PageLoading';
 import UserCenter from '@/components/UserCenter';
 import { useStore } from '@/store/main';
+import { ref } from 'vue';
 
 export const beforeRender = {
     loading: <PageLoading />,
@@ -24,6 +25,16 @@ export const beforeRender = {
     }
 };
 
-export const layout = {
-    customHeader: <UserCenter />
-};
+export const layout = initialValue => ({
+    ...initialValue,
+    customHeader: <UserCenter />,
+    menus: (defaultMenuData) => {
+        const menusRef = ref(defaultMenuData);
+        // watch(() => initialValue.initialState.userName, () => {
+        //     menusRef.value = [{
+        //         name: 'store'
+        //     }];
+        // });
+        return menusRef;
+    }
+});
diff --git a/packages/fes-template/src/pages/editor.vue b/packages/fes-template/src/pages/editor.vue
index 6aed5d6b..891e9311 100644
--- a/packages/fes-template/src/pages/editor.vue
+++ b/packages/fes-template/src/pages/editor.vue
@@ -24,6 +24,7 @@ export default {
         MonacoEditor
     },
     setup() {
+        console.log('editor.vue');
         const editorRef = ref();
         const json = ref('');
         const language = ref('json');
diff --git a/packages/fes-template/src/pages/index.vue b/packages/fes-template/src/pages/index.vue
index c8fb7f9b..1c392885 100644
--- a/packages/fes-template/src/pages/index.vue
+++ b/packages/fes-template/src/pages/index.vue
@@ -7,12 +7,18 @@
 
 <script>
 import { FButton } from '@fesjs/fes-design';
+import { useModel } from '@fesjs/fes';
 
 export default {
     components: {
         FButton
     },
     setup() {
+        const initialState = useModel('@@initialState');
+        setTimeout(() => {
+            initialState.userName = '1';
+        }, 1000);
+        console.log('index.vue');
         return {
         };
     }
diff --git a/packages/fes-template/src/pages/store.vue b/packages/fes-template/src/pages/store.vue
index 63c9df2b..bcb1a939 100644
--- a/packages/fes-template/src/pages/store.vue
+++ b/packages/fes-template/src/pages/store.vue
@@ -28,6 +28,7 @@ import { MUTATION_TYPES, GETTER_TYPES, ACTION_TYPES } from '@fesjs/fes';
 
 export default {
     setup() {
+        console.log('store.vue');
         const store = useStore();
         console.log('store==>', store);
         const disabled = ref(false);